docnest 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -68,6 +68,7 @@ export default defineConfig({
68
68
  enabled: true,
69
69
  passphrase: '请替换为项目自己的长口令',
70
70
  stateFile: '.docnest/auth.json',
71
+ localStorageKey: 'my-project:auth-passphrase',
71
72
  sessionTtlMinutes: 24 * 60,
72
73
  },
73
74
  })
@@ -75,7 +76,8 @@ export default defineConfig({
75
76
 
76
77
  授权只有一道关口,没有账号、密码组合或角色体系。首次启动时,`passphrase` 会初始化到由
77
78
  `stateFile` 指定的本地授权状态文件;状态文件只保存带随机盐的口令哈希,不保存明文口令。
78
- 进入文档中心后,右上角的“修改口令”可以动态更新口令。修改后所有现有会话立即失效,必须使用新口令重新进入。
79
+ 按极简模式约定,前端会把可回填的口令永久保存到浏览器 `localStorage`(不写入 URL);每次打开或刷新文档页面,前端都会把当前缓存的口令交给后台重新校验。校验失败会回到口令页,校验成功才会继续打开当前页面;输入页会保留跳转前的目标地址,错误口令会停留在输入页。由于 `localStorage` 是浏览器本地明文存储,请只在受信任的浏览器环境使用此模式。
80
+ `localStorageKey` 可选,用于为不同宿主项目隔离浏览器存储键。内置授权不提供“修改口令”或“退出”按钮;需要更换口令时,停止服务、删除 `stateFile`,修改配置中的 `passphrase` 后重新启动。
79
81
  建议把 `.docnest/` 加入宿主项目的 `.gitignore`,并为口令状态文件设置仅当前用户可读写的权限。
80
82
 
81
83
  授权开启但既没有状态文件,也没有初始 `passphrase` 时,DocNest 会拒绝启动,避免意外以空口令开放文档。
package/dist/auth.d.ts CHANGED
@@ -12,7 +12,5 @@ export type DocNestAuthManager = {
12
12
  verifyPassphrase: (passphrase: string) => boolean;
13
13
  createSession: (passphrase: string) => string | null;
14
14
  authenticateSession: (token: string | undefined) => boolean;
15
- revokeSession: (token: string | undefined) => void;
16
- changePassphrase: (currentPassphrase: string, nextPassphrase: string) => boolean;
17
15
  };
18
16
  export declare function createAuthManager(config: DocNestAuthConfig): DocNestAuthManager;
package/dist/auth.js CHANGED
@@ -130,11 +130,9 @@ function createAuthManager(config) {
130
130
  verifyPassphrase: () => true,
131
131
  createSession: () => null,
132
132
  authenticateSession: () => true,
133
- revokeSession: () => undefined,
134
- changePassphrase: () => false,
135
133
  };
136
134
  }
137
- let state = readOrCreateState(config);
135
+ const state = readOrCreateState(config);
138
136
  function verifyPassphrase(passphrase) {
139
137
  return matchesHash(passphrase, state);
140
138
  }
@@ -163,19 +161,6 @@ function createAuthManager(config) {
163
161
  session.expiresAt = Date.now() + sessionMaxAgeSeconds * 1000;
164
162
  return true;
165
163
  }
166
- function revokeSession(token) {
167
- if (token)
168
- sessions.delete(token);
169
- }
170
- function changePassphrase(currentPassphrase, nextPassphrase) {
171
- if (!verifyPassphrase(currentPassphrase))
172
- return false;
173
- assertPassphrase(nextPassphrase, '新授权口令');
174
- state = createState(nextPassphrase);
175
- writeState(config.stateFile, state);
176
- sessions.clear();
177
- return true;
178
- }
179
164
  return {
180
165
  enabled,
181
166
  stateFile: config.stateFile,
@@ -184,7 +169,5 @@ function createAuthManager(config) {
184
169
  verifyPassphrase,
185
170
  createSession,
186
171
  authenticateSession,
187
- revokeSession,
188
- changePassphrase,
189
172
  };
190
173
  }
package/dist/config.d.ts CHANGED
@@ -29,6 +29,7 @@ export type DocNestConfig = {
29
29
  enabled?: boolean;
30
30
  passphrase?: string;
31
31
  stateFile?: string;
32
+ localStorageKey?: string;
32
33
  sessionTtlMinutes?: number;
33
34
  };
34
35
  export?: {
@@ -50,6 +51,7 @@ export type ResolvedDocNestConfig = Required<DocNestConfig> & {
50
51
  enabled: boolean;
51
52
  passphrase: string;
52
53
  stateFile: string;
54
+ localStorageKey: string;
53
55
  sessionTtlMinutes: number;
54
56
  };
55
57
  export: {
package/dist/config.js CHANGED
@@ -89,6 +89,7 @@ async function loadConfig(projectRoot = process.cwd()) {
89
89
  const auth = userConfig.auth ?? {};
90
90
  const watermark = userConfig.export?.watermark ?? {};
91
91
  const storageKeyPrefix = site.storageKeyPrefix?.trim() || `docnest:${projectName}`;
92
+ const authLocalStorageKey = auth.localStorageKey?.trim() || `${storageKeyPrefix}:auth-passphrase`;
92
93
  return {
93
94
  projectRoot,
94
95
  docsDir: node_path_1.default.resolve(projectRoot, userConfig.docsDir?.trim() || 'docs'),
@@ -114,6 +115,7 @@ async function loadConfig(projectRoot = process.cwd()) {
114
115
  enabled: auth.enabled ?? false,
115
116
  passphrase: typeof auth.passphrase === 'string' ? auth.passphrase : '',
116
117
  stateFile: node_path_1.default.resolve(projectRoot, auth.stateFile?.trim() || '.docnest/auth.json'),
118
+ localStorageKey: authLocalStorageKey,
117
119
  sessionTtlMinutes: normalizePositiveNumber(auth.sessionTtlMinutes, 24 * 60),
118
120
  },
119
121
  export: {
package/dist/server.js CHANGED
@@ -92,6 +92,7 @@ const AUTH_CONFIG = globalThis.__DOCNEST_AUTH_CONFIG__ || {
92
92
  enabled: false,
93
93
  passphrase: '',
94
94
  stateFile: path.join(process.cwd(), '.docnest', 'auth.json'),
95
+ localStorageKey: `${DOCS_STORAGE_KEY_PREFIX}:auth-passphrase`,
95
96
  sessionTtlMinutes: 24 * 60,
96
97
  };
97
98
  const AUTH_MANAGER = createAuthManager(AUTH_CONFIG);
@@ -110,6 +111,7 @@ app.locals.docsThemeEnabled = DOCS_THEME_ENABLED;
110
111
  app.locals.docsWatermarkEnabled = DOCS_WATERMARK_ENABLED;
111
112
  app.locals.docsWatermarkText = DOCS_WATERMARK_TEXT;
112
113
  app.locals.docsAuthEnabled = BUILT_IN_AUTH_ENABLED;
114
+ app.locals.docsAuthStorageKey = AUTH_CONFIG.localStorageKey || `${DOCS_STORAGE_KEY_PREFIX}:auth-passphrase`;
113
115
  function normalizeDocRelativePath(value) {
114
116
  const raw = String(value || '').replace(/\\/g, '/').replace(/^\/+/, '');
115
117
  const normalized = path.posix.normalize(raw);
@@ -295,13 +297,18 @@ function renderLoginPage(res, nextPath, error, status = 200) {
295
297
  app.get('/login', (req, res) => {
296
298
  if (!BUILT_IN_AUTH_ENABLED)
297
299
  return res.redirect('/');
298
- if (AUTH_MANAGER.authenticateSession(readAuthToken(req))) {
299
- return res.redirect(safeNextPath(req.query.next));
300
+ return renderLoginPage(res, req.query.next, '');
301
+ });
302
+ app.post('/auth/verify', (req, res) => {
303
+ if (!BUILT_IN_AUTH_ENABLED)
304
+ return res.json({ ok: true });
305
+ const token = AUTH_MANAGER.createSession(String(req.body?.passphrase || ''));
306
+ if (!token) {
307
+ clearAuthCookie(res);
308
+ return res.status(401).json({ ok: false, error: '口令不正确,请重试。' });
300
309
  }
301
- const message = req.query.changed
302
- ? '口令已更新,请使用新口令重新进入。'
303
- : '';
304
- return renderLoginPage(res, req.query.next, message);
310
+ setAuthCookie(res, token);
311
+ return res.json({ ok: true });
305
312
  });
306
313
  app.post('/auth/login', (req, res) => {
307
314
  if (!BUILT_IN_AUTH_ENABLED)
@@ -313,29 +320,6 @@ app.post('/auth/login', (req, res) => {
313
320
  setAuthCookie(res, token);
314
321
  return res.redirect(nextPath);
315
322
  });
316
- app.post('/auth/logout', (req, res) => {
317
- AUTH_MANAGER.revokeSession(readAuthToken(req));
318
- clearAuthCookie(res);
319
- return res.redirect('/login');
320
- });
321
- app.post('/auth/change-password', (req, res) => {
322
- if (!BUILT_IN_AUTH_ENABLED || !AUTH_MANAGER.authenticateSession(readAuthToken(req))) {
323
- return res.status(401).json({ ok: false, error: '当前会话未授权。' });
324
- }
325
- try {
326
- const changed = AUTH_MANAGER.changePassphrase(String(req.body?.currentPassphrase || ''), String(req.body?.nextPassphrase || ''));
327
- if (!changed)
328
- return res.status(401).json({ ok: false, error: '当前口令不正确。' });
329
- clearAuthCookie(res);
330
- return res.json({ ok: true });
331
- }
332
- catch (error) {
333
- return res.status(400).json({
334
- ok: false,
335
- error: error instanceof Error ? error.message : '新口令无效。',
336
- });
337
- }
338
- });
339
323
  function documentAuthMiddleware(req, res, next) {
340
324
  if (!BUILT_IN_AUTH_ENABLED)
341
325
  return next();
@@ -34,9 +34,11 @@ export default defineConfig({
34
34
  auth: {
35
35
  // 极简单口令授权;关闭时文档中心保持本地开放。
36
36
  enabled: false,
37
- // 仅首次启动且授权状态文件不存在时使用,之后可在页面内修改。
37
+ // 仅首次启动且授权状态文件不存在时使用;更换口令需手动删除状态文件后重启。
38
38
  passphrase: '',
39
39
  stateFile: '.docnest/auth.json',
40
+ // 前端 localStorage 的键;口令会永久保存在浏览器本地,直到手动清除或口令失效。
41
+ localStorageKey: 'my-project:auth-passphrase',
40
42
  sessionTtlMinutes: 24 * 60,
41
43
  },
42
44
  export: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docnest",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "可复用的本地 Markdown 文档中心",
5
5
  "repository": {
6
6
  "type": "git",
@@ -5762,43 +5762,22 @@ body[data-theme="dark"] .dir-listing-count {
5762
5762
  }
5763
5763
 
5764
5764
  /* 极简单口令授权 */
5765
- .auth-actions {
5766
- display: inline-flex;
5767
- align-items: center;
5768
- gap: 0.4rem;
5769
- }
5770
-
5771
- .auth-logout-form {
5772
- display: inline-flex;
5773
- margin: 0;
5774
- }
5775
-
5776
- .auth-change-password-btn,
5777
- .auth-logout-btn {
5778
- white-space: nowrap;
5779
- }
5780
-
5781
- .auth-modal[hidden] {
5782
- display: none;
5765
+ html.docnest-auth-pending body > * {
5766
+ visibility: hidden;
5783
5767
  }
5784
5768
 
5785
- .auth-modal {
5769
+ html.docnest-auth-pending::before {
5770
+ content: '正在校验授权…';
5786
5771
  position: fixed;
5787
5772
  inset: 0;
5788
- z-index: 1000;
5773
+ z-index: 2000;
5789
5774
  display: grid;
5790
5775
  place-items: center;
5791
- padding: 1.25rem;
5792
- }
5793
-
5794
- .auth-modal-backdrop {
5795
- position: absolute;
5796
- inset: 0;
5797
- background: rgba(10, 16, 24, 0.42);
5798
- backdrop-filter: blur(4px);
5776
+ color: var(--linear-text-muted, #63727f);
5777
+ background: var(--linear-bg, #ffffff);
5778
+ font-size: 0.9rem;
5799
5779
  }
5800
5780
 
5801
- .auth-modal-card,
5802
5781
  .auth-login-card {
5803
5782
  position: relative;
5804
5783
  z-index: 1;
@@ -5809,25 +5788,11 @@ body[data-theme="dark"] .dir-listing-count {
5809
5788
  box-shadow: 0 24px 70px rgba(17, 29, 40, 0.2);
5810
5789
  }
5811
5790
 
5812
- .auth-modal-card {
5813
- width: min(100%, 440px);
5814
- padding: 1.5rem;
5815
- }
5816
-
5817
- .auth-modal-header {
5818
- display: flex;
5819
- align-items: flex-start;
5820
- justify-content: space-between;
5821
- gap: 1rem;
5822
- }
5823
-
5824
- .auth-modal-header h2,
5825
5791
  .auth-login-card h1 {
5826
5792
  margin: 0;
5827
5793
  color: var(--linear-text, #18212a);
5828
5794
  }
5829
5795
 
5830
- .auth-modal-eyebrow,
5831
5796
  .auth-login-eyebrow {
5832
5797
  margin: 0 0 0.35rem;
5833
5798
  color: var(--linear-accent, #267c83);
@@ -5836,35 +5801,12 @@ body[data-theme="dark"] .dir-listing-count {
5836
5801
  letter-spacing: 0.12em;
5837
5802
  }
5838
5803
 
5839
- .auth-modal-close {
5840
- width: 2rem;
5841
- height: 2rem;
5842
- padding: 0;
5843
- border: 0;
5844
- border-radius: 999px;
5845
- color: var(--linear-text-muted, #63727f);
5846
- background: transparent;
5847
- font-size: 1.5rem;
5848
- line-height: 1;
5849
- cursor: pointer;
5850
- }
5851
-
5852
- .auth-modal-close:hover {
5853
- background: var(--linear-bg-subtle, #eef2f5);
5854
- }
5855
-
5856
- .auth-modal-help,
5857
5804
  .auth-login-description,
5858
5805
  .auth-login-note {
5859
5806
  color: var(--linear-text-muted, #63727f);
5860
5807
  line-height: 1.6;
5861
5808
  }
5862
5809
 
5863
- .auth-modal-help {
5864
- margin: 1rem 0 1.25rem;
5865
- font-size: 0.875rem;
5866
- }
5867
-
5868
5810
  .auth-form {
5869
5811
  display: grid;
5870
5812
  gap: 0.9rem;
@@ -5904,15 +5846,7 @@ body[data-theme="dark"] .dir-listing-count {
5904
5846
  font-size: 0.875rem;
5905
5847
  }
5906
5848
 
5907
- .auth-modal-actions {
5908
- display: flex;
5909
- justify-content: flex-end;
5910
- gap: 0.6rem;
5911
- margin-top: 0.25rem;
5912
- }
5913
-
5914
- .auth-primary-btn,
5915
- .auth-secondary-btn {
5849
+ .auth-primary-btn {
5916
5850
  min-height: 2.5rem;
5917
5851
  padding: 0.55rem 0.95rem;
5918
5852
  border-radius: 8px;
@@ -5927,12 +5861,6 @@ body[data-theme="dark"] .dir-listing-count {
5927
5861
  background: var(--linear-accent, #267c83);
5928
5862
  }
5929
5863
 
5930
- .auth-secondary-btn {
5931
- border: 1px solid var(--linear-border, #d7dee5);
5932
- color: var(--linear-text, #18212a);
5933
- background: var(--linear-bg-subtle, #eef2f5);
5934
- }
5935
-
5936
5864
  .auth-page {
5937
5865
  min-height: 100vh;
5938
5866
  display: grid;
@@ -5994,16 +5922,3 @@ body[data-theme="dark"] .dir-listing-count {
5994
5922
  font-size: 0.75rem;
5995
5923
  text-align: center;
5996
5924
  }
5997
-
5998
- @media (max-width: 768px) {
5999
- .auth-actions {
6000
- gap: 0.25rem;
6001
- }
6002
-
6003
- .auth-change-password-btn,
6004
- .auth-logout-btn {
6005
- min-width: 44px;
6006
- padding-inline: 0.5rem;
6007
- font-size: 0.75rem;
6008
- }
6009
- }
@@ -1,61 +1,152 @@
1
1
  (function () {
2
- var modal = document.getElementById('auth-change-password-modal');
3
- var openButton = document.getElementById('auth-change-password-btn');
4
- var form = document.getElementById('auth-change-password-form');
5
- var error = document.getElementById('auth-change-password-error');
6
- if (!modal || !openButton || !form) return;
7
-
8
- var closeButtons = modal.querySelectorAll('[data-auth-close]');
9
-
10
- function setError(message) {
11
- if (!error) return;
12
- error.textContent = message || '';
13
- error.hidden = !message;
14
- }
15
-
16
- function closeModal() {
17
- modal.hidden = true;
18
- setError('');
19
- form.reset();
20
- }
21
-
22
- openButton.addEventListener('click', function () {
23
- modal.hidden = false;
24
- var firstInput = form.querySelector('input');
25
- if (firstInput) firstInput.focus();
26
- });
27
-
28
- closeButtons.forEach(function (button) {
29
- button.addEventListener('click', closeModal);
30
- });
31
-
32
- form.addEventListener('submit', async function (event) {
33
- event.preventDefault();
34
- setError('');
35
- var formData = new FormData(form);
36
- var nextPassphrase = String(formData.get('nextPassphrase') || '');
37
- var confirmPassphrase = String(formData.get('confirmPassphrase') || '');
38
- if (nextPassphrase !== confirmPassphrase) {
39
- setError('两次输入的新口令不一致。');
40
- return;
2
+ var config = window.DOCNEST_CONFIG || {};
3
+ if (config.authEnabled !== true) return;
4
+
5
+ var storageKey = config.authStorageKey || 'docnest:auth-passphrase';
6
+ var verificationPromise = null;
7
+
8
+ function readPassphrase() {
9
+ try {
10
+ return window.localStorage.getItem(storageKey) || '';
11
+ } catch (error) {
12
+ return '';
13
+ }
14
+ }
15
+
16
+ function writePassphrase(passphrase) {
17
+ try {
18
+ window.localStorage.setItem(storageKey, passphrase);
19
+ return true;
20
+ } catch (error) {
21
+ return false;
41
22
  }
23
+ }
42
24
 
25
+ function clearPassphrase() {
43
26
  try {
44
- var response = await fetch('/auth/change-password', {
45
- method: 'POST',
46
- headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
47
- body: JSON.stringify({
48
- currentPassphrase: String(formData.get('currentPassphrase') || ''),
49
- nextPassphrase: nextPassphrase,
50
- }),
27
+ window.localStorage.removeItem(storageKey);
28
+ } catch (error) {
29
+ // The browser may block storage; the next request will simply ask again.
30
+ }
31
+ }
32
+
33
+ function currentPageUrl() {
34
+ return window.location.pathname + window.location.search + window.location.hash;
35
+ }
36
+
37
+ function safeNextPath(value) {
38
+ if (
39
+ typeof value !== 'string' ||
40
+ value.charAt(0) !== '/' ||
41
+ value.indexOf('//') === 0 ||
42
+ value.indexOf('\\') !== -1 ||
43
+ /[\r\n]/.test(value)
44
+ ) return '/';
45
+ return value;
46
+ }
47
+
48
+ function redirectToLogin() {
49
+ var next = encodeURIComponent(currentPageUrl() || '/');
50
+ window.location.replace('/login?next=' + next);
51
+ }
52
+
53
+ function verifyWithBackend(passphrase) {
54
+ return fetch('/auth/verify', {
55
+ method: 'POST',
56
+ credentials: 'same-origin',
57
+ headers: {
58
+ 'Content-Type': 'application/json',
59
+ Accept: 'application/json',
60
+ },
61
+ body: JSON.stringify({ passphrase: passphrase }),
62
+ }).then(function (response) {
63
+ return response.json().catch(function () { return {}; }).then(function (result) {
64
+ return { ok: response.ok && result.ok === true, error: result.error || '', unavailable: false };
51
65
  });
52
- var result = await response.json().catch(function () { return {}; });
53
- if (!response.ok || !result.ok) {
54
- throw new Error(result.error || '修改口令失败,请重试。');
66
+ }).catch(function () {
67
+ return { ok: false, error: '无法连接授权服务,请稍后重试。', unavailable: true };
68
+ });
69
+ }
70
+
71
+ function ensureAuthorized() {
72
+ if (verificationPromise) return verificationPromise;
73
+ var passphrase = readPassphrase();
74
+ if (!passphrase) return Promise.resolve(false);
75
+
76
+ verificationPromise = verifyWithBackend(passphrase).then(function (result) {
77
+ if (!result.ok) {
78
+ clearPassphrase();
79
+ return false;
55
80
  }
56
- window.location.assign('/login?changed=1');
57
- } catch (requestError) {
58
- setError(requestError instanceof Error ? requestError.message : '修改口令失败,请重试。');
81
+ return true;
82
+ }).finally(function () {
83
+ verificationPromise = null;
84
+ });
85
+ return verificationPromise;
86
+ }
87
+
88
+ window.docNestEnsureAuthorized = ensureAuthorized;
89
+ window.docNestRedirectToLogin = redirectToLogin;
90
+
91
+ function removePendingState() {
92
+ document.documentElement.classList.remove('docnest-auth-pending');
93
+ }
94
+
95
+ function initLoginPage() {
96
+ var form = document.querySelector('.auth-login-form');
97
+ var error = document.querySelector('.auth-login-form .auth-form-error');
98
+ var passphraseInput = form ? form.querySelector('input[name="passphrase"]') : null;
99
+ var nextInput = form ? form.querySelector('input[name="next"]') : null;
100
+ if (!form || !passphraseInput) return;
101
+
102
+ function setError(message) {
103
+ if (!error) return;
104
+ error.textContent = message || '';
105
+ error.hidden = !message;
59
106
  }
60
- });
107
+
108
+ async function submitPassphrase(passphrase, auto) {
109
+ if (!passphrase) return false;
110
+ if (!auto) setError('');
111
+ var result = await verifyWithBackend(passphrase);
112
+ if (!result.ok) {
113
+ if (auto && !result.unavailable) clearPassphrase();
114
+ setError(auto && !result.unavailable ? '本地保存的口令已失效,请输入新口令。' : (result.error || '口令不正确,请重试。'));
115
+ if (!auto) passphraseInput.focus();
116
+ return false;
117
+ }
118
+ if (!writePassphrase(passphrase)) {
119
+ setError('浏览器拒绝保存口令,请检查本地存储权限。');
120
+ return false;
121
+ }
122
+ window.location.replace(safeNextPath(nextInput ? nextInput.value : '/'));
123
+ return true;
124
+ }
125
+
126
+ form.addEventListener('submit', function (event) {
127
+ event.preventDefault();
128
+ submitPassphrase(String(passphraseInput.value || ''), false);
129
+ });
130
+
131
+ var cachedPassphrase = readPassphrase();
132
+ if (cachedPassphrase) submitPassphrase(cachedPassphrase, true);
133
+ }
134
+
135
+ function initProtectedPage() {
136
+ ensureAuthorized().then(function (authorized) {
137
+ if (!authorized) {
138
+ redirectToLogin();
139
+ return;
140
+ }
141
+ removePendingState();
142
+ });
143
+ }
144
+
145
+ if (document.body && document.body.classList.contains('auth-page')) {
146
+ initLoginPage();
147
+ } else if (document.readyState === 'loading') {
148
+ document.addEventListener('DOMContentLoaded', initProtectedPage);
149
+ } else {
150
+ initProtectedPage();
151
+ }
61
152
  })();
@@ -155,7 +155,24 @@
155
155
  article.style.opacity = '0.6';
156
156
  article.style.pointerEvents = 'none';
157
157
 
158
- fetch(absoluteUrl, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
158
+ var authCheck = typeof window.docNestEnsureAuthorized === 'function'
159
+ ? window.docNestEnsureAuthorized()
160
+ : Promise.resolve(true);
161
+
162
+ authCheck.then(function(authorized) {
163
+ if (!authorized) {
164
+ if (typeof window.docNestRedirectToLogin === 'function') {
165
+ window.docNestRedirectToLogin();
166
+ }
167
+ var redirectError = new Error('AUTH_REDIRECT');
168
+ redirectError.redirected = true;
169
+ throw redirectError;
170
+ }
171
+ return fetch(absoluteUrl, {
172
+ credentials: 'same-origin',
173
+ headers: { 'X-Requested-With': 'XMLHttpRequest' },
174
+ });
175
+ })
159
176
  .then(function(res) {
160
177
  if (!res.ok) {
161
178
  throw new Error('HTTP ' + res.status);
@@ -198,6 +215,7 @@
198
215
  }
199
216
  })
200
217
  .catch(function(err) {
218
+ if (err && err.redirected) return;
201
219
  showNavError('文档加载失败,请稍后重试');
202
220
  console.warn('SPA 加载失败,保持当前页面:', err);
203
221
  })
@@ -7,6 +7,12 @@
7
7
  <link rel="icon" href="/favicon.ico?v=<%= assetVersion %>" sizes="any">
8
8
  <link rel="stylesheet" href="/static/css/styles.css?v=<%= assetVersion %>">
9
9
  <link rel="stylesheet" href="/static/css/themes.css?v=<%= assetVersion %>">
10
+ <script>
11
+ window.DOCNEST_CONFIG = {
12
+ authEnabled: true,
13
+ authStorageKey: <%- JSON.stringify(docsAuthStorageKey) %>
14
+ };
15
+ </script>
10
16
  </head>
11
17
  <body class="auth-page">
12
18
  <main class="auth-login-card" aria-labelledby="auth-login-title">
@@ -30,12 +36,11 @@
30
36
  授权口令
31
37
  <input type="password" name="passphrase" autocomplete="current-password" autofocus required>
32
38
  </label>
33
- <% if (error) { %>
34
- <p class="auth-form-error" role="alert"><%= error %></p>
35
- <% } %>
39
+ <p class="auth-form-error" role="alert" id="auth-login-error" <%= error ? '' : 'hidden' %>><%= error %></p>
36
40
  <button type="submit" class="auth-primary-btn">进入文档中心</button>
37
41
  </form>
38
42
  <p class="auth-login-note">没有账号体系,也没有角色区分;口令是唯一的入口。</p>
39
43
  </main>
44
+ <script src="/static/js/core/auth.js?v=<%= assetVersion %>"></script>
40
45
  </body>
41
46
  </html>
@@ -1,5 +1,7 @@
1
1
  <script>
2
2
  window.DOCNEST_CONFIG = {
3
+ authEnabled: <%= docsAuthEnabled ? 'true' : 'false' %>,
4
+ authStorageKey: <%- JSON.stringify(docsAuthStorageKey) %>,
3
5
  watermarkEnabled: <%= docsWatermarkEnabled ? 'true' : 'false' %>,
4
6
  watermarkText: <%- JSON.stringify(docsWatermarkText) %>,
5
7
  storageKeyPrefix: <%- JSON.stringify(docsStorageKeyPrefix) %>,
@@ -9,4 +11,7 @@
9
11
  enabledThemes: <%- JSON.stringify(docsThemeEnabled) %>
10
12
  }
11
13
  };
14
+ if (window.DOCNEST_CONFIG.authEnabled) {
15
+ document.documentElement.classList.add('docnest-auth-pending');
16
+ }
12
17
  </script>
@@ -54,52 +54,10 @@
54
54
  <span class="theme-icon" aria-hidden="true">A</span>
55
55
  <span class="theme-text">跟随系统</span>
56
56
  </button>
57
- <% if (docsAuthEnabled) { %>
58
- <div class="auth-actions" aria-label="文档授权">
59
- <button id="auth-change-password-btn" class="header-action-btn auth-change-password-btn" type="button">
60
- 修改口令
61
- </button>
62
- <form action="/auth/logout" method="post" class="auth-logout-form">
63
- <button class="header-action-btn auth-logout-btn" type="submit">退出</button>
64
- </form>
65
- </div>
66
- <% } %>
67
57
  </div>
68
58
  </div>
69
59
  </div>
70
60
  </header>
71
61
  <% if (docsAuthEnabled) { %>
72
- <div class="auth-modal" id="auth-change-password-modal" hidden>
73
- <div class="auth-modal-backdrop" data-auth-close></div>
74
- <section class="auth-modal-card" role="dialog" aria-modal="true" aria-labelledby="auth-change-password-title">
75
- <div class="auth-modal-header">
76
- <div>
77
- <p class="auth-modal-eyebrow">DOCUMENT ACCESS</p>
78
- <h2 id="auth-change-password-title">修改授权口令</h2>
79
- </div>
80
- <button type="button" class="auth-modal-close" data-auth-close aria-label="关闭">×</button>
81
- </div>
82
- <p class="auth-modal-help">修改后所有已打开的文档会退出授权,需要使用新口令重新进入。</p>
83
- <form id="auth-change-password-form" class="auth-form">
84
- <label>
85
- 当前口令
86
- <input type="password" name="currentPassphrase" autocomplete="current-password" required>
87
- </label>
88
- <label>
89
- 新口令
90
- <input type="password" name="nextPassphrase" autocomplete="new-password" required>
91
- </label>
92
- <label>
93
- 确认新口令
94
- <input type="password" name="confirmPassphrase" autocomplete="new-password" required>
95
- </label>
96
- <p class="auth-form-error" id="auth-change-password-error" role="alert" hidden></p>
97
- <div class="auth-modal-actions">
98
- <button type="button" class="auth-secondary-btn" data-auth-close>取消</button>
99
- <button type="submit" class="auth-primary-btn">保存新口令</button>
100
- </div>
101
- </form>
102
- </section>
103
- </div>
104
62
  <script src="/static/js/core/auth.js?v=<%= typeof assetVersion !== 'undefined' ? assetVersion : 'dev' %>"></script>
105
63
  <% } %>