docnest 0.1.9 → 0.1.10
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 +3 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +2 -0
- package/dist/server.js +13 -3
- package/docnest.config.example.mjs +2 -0
- package/package.json +1 -1
- package/server/static/css/styles.css +16 -0
- package/server/static/js/core/auth.js +218 -53
- package/server/static/js/core/doc-client-nav.js +19 -1
- package/server/views/login.ejs +8 -3
- package/server/views/partials/appearance-head.ejs +5 -0
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` 可选,用于为不同宿主项目隔离浏览器存储键。右上角的“修改口令”可以动态更新口令,修改后会清除本地缓存并使所有现有会话立即失效,必须使用新口令重新进入。
|
|
79
81
|
建议把 `.docnest/` 加入宿主项目的 `.gitignore`,并为口令状态文件设置仅当前用户可读写的权限。
|
|
80
82
|
|
|
81
83
|
授权开启但既没有状态文件,也没有初始 `passphrase` 时,DocNest 会拒绝启动,避免意外以空口令开放文档。
|
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,14 +297,22 @@ 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
|
-
}
|
|
301
300
|
const message = req.query.changed
|
|
302
301
|
? '口令已更新,请使用新口令重新进入。'
|
|
303
302
|
: '';
|
|
304
303
|
return renderLoginPage(res, req.query.next, message);
|
|
305
304
|
});
|
|
305
|
+
app.post('/auth/verify', (req, res) => {
|
|
306
|
+
if (!BUILT_IN_AUTH_ENABLED)
|
|
307
|
+
return res.json({ ok: true });
|
|
308
|
+
const token = AUTH_MANAGER.createSession(String(req.body?.passphrase || ''));
|
|
309
|
+
if (!token) {
|
|
310
|
+
clearAuthCookie(res);
|
|
311
|
+
return res.status(401).json({ ok: false, error: '口令不正确,请重试。' });
|
|
312
|
+
}
|
|
313
|
+
setAuthCookie(res, token);
|
|
314
|
+
return res.json({ ok: true });
|
|
315
|
+
});
|
|
306
316
|
app.post('/auth/login', (req, res) => {
|
|
307
317
|
if (!BUILT_IN_AUTH_ENABLED)
|
|
308
318
|
return res.redirect('/');
|
|
@@ -37,6 +37,8 @@ export default defineConfig({
|
|
|
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
|
@@ -5762,6 +5762,22 @@ body[data-theme="dark"] .dir-listing-count {
|
|
|
5762
5762
|
}
|
|
5763
5763
|
|
|
5764
5764
|
/* 极简单口令授权 */
|
|
5765
|
+
html.docnest-auth-pending body > * {
|
|
5766
|
+
visibility: hidden;
|
|
5767
|
+
}
|
|
5768
|
+
|
|
5769
|
+
html.docnest-auth-pending::before {
|
|
5770
|
+
content: '正在校验授权…';
|
|
5771
|
+
position: fixed;
|
|
5772
|
+
inset: 0;
|
|
5773
|
+
z-index: 2000;
|
|
5774
|
+
display: grid;
|
|
5775
|
+
place-items: center;
|
|
5776
|
+
color: var(--linear-text-muted, #63727f);
|
|
5777
|
+
background: var(--linear-bg, #ffffff);
|
|
5778
|
+
font-size: 0.9rem;
|
|
5779
|
+
}
|
|
5780
|
+
|
|
5765
5781
|
.auth-actions {
|
|
5766
5782
|
display: inline-flex;
|
|
5767
5783
|
align-items: center;
|
|
@@ -1,61 +1,226 @@
|
|
|
1
1
|
(function () {
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
var
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 };
|
|
65
|
+
});
|
|
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;
|
|
80
|
+
}
|
|
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 initChangePassword() {
|
|
96
|
+
var modal = document.getElementById('auth-change-password-modal');
|
|
97
|
+
var openButton = document.getElementById('auth-change-password-btn');
|
|
98
|
+
var form = document.getElementById('auth-change-password-form');
|
|
99
|
+
var error = document.getElementById('auth-change-password-error');
|
|
100
|
+
if (!modal || !openButton || !form) return;
|
|
101
|
+
|
|
102
|
+
var closeButtons = modal.querySelectorAll('[data-auth-close]');
|
|
103
|
+
|
|
104
|
+
function setError(message) {
|
|
105
|
+
if (!error) return;
|
|
106
|
+
error.textContent = message || '';
|
|
107
|
+
error.hidden = !message;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function closeModal() {
|
|
111
|
+
modal.hidden = true;
|
|
112
|
+
setError('');
|
|
113
|
+
form.reset();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
openButton.addEventListener('click', function () {
|
|
117
|
+
modal.hidden = false;
|
|
118
|
+
var firstInput = form.querySelector('input');
|
|
119
|
+
if (firstInput) firstInput.focus();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
closeButtons.forEach(function (button) {
|
|
123
|
+
button.addEventListener('click', closeModal);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
form.addEventListener('submit', async function (event) {
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
setError('');
|
|
129
|
+
var formData = new FormData(form);
|
|
130
|
+
var nextPassphrase = String(formData.get('nextPassphrase') || '');
|
|
131
|
+
var confirmPassphrase = String(formData.get('confirmPassphrase') || '');
|
|
132
|
+
if (nextPassphrase !== confirmPassphrase) {
|
|
133
|
+
setError('两次输入的新口令不一致。');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
var response = await fetch('/auth/change-password', {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
credentials: 'same-origin',
|
|
141
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
142
|
+
body: JSON.stringify({
|
|
143
|
+
currentPassphrase: String(formData.get('currentPassphrase') || ''),
|
|
144
|
+
nextPassphrase: nextPassphrase,
|
|
145
|
+
}),
|
|
146
|
+
});
|
|
147
|
+
var result = await response.json().catch(function () { return {}; });
|
|
148
|
+
if (!response.ok || !result.ok) {
|
|
149
|
+
throw new Error(result.error || '修改口令失败,请重试。');
|
|
150
|
+
}
|
|
151
|
+
clearPassphrase();
|
|
152
|
+
window.location.assign('/login?changed=1');
|
|
153
|
+
} catch (requestError) {
|
|
154
|
+
setError(requestError instanceof Error ? requestError.message : '修改口令失败,请重试。');
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function initLogout() {
|
|
160
|
+
document.querySelectorAll('.auth-logout-form').forEach(function (form) {
|
|
161
|
+
form.addEventListener('submit', function () {
|
|
162
|
+
clearPassphrase();
|
|
51
163
|
});
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function initLoginPage() {
|
|
168
|
+
var form = document.querySelector('.auth-login-form');
|
|
169
|
+
var error = document.querySelector('.auth-login-form .auth-form-error');
|
|
170
|
+
var passphraseInput = form ? form.querySelector('input[name="passphrase"]') : null;
|
|
171
|
+
var nextInput = form ? form.querySelector('input[name="next"]') : null;
|
|
172
|
+
if (!form || !passphraseInput) return;
|
|
173
|
+
|
|
174
|
+
function setError(message) {
|
|
175
|
+
if (!error) return;
|
|
176
|
+
error.textContent = message || '';
|
|
177
|
+
error.hidden = !message;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function submitPassphrase(passphrase, auto) {
|
|
181
|
+
if (!passphrase) return false;
|
|
182
|
+
if (!auto) setError('');
|
|
183
|
+
var result = await verifyWithBackend(passphrase);
|
|
184
|
+
if (!result.ok) {
|
|
185
|
+
if (auto && !result.unavailable) clearPassphrase();
|
|
186
|
+
setError(auto && !result.unavailable ? '本地保存的口令已失效,请输入新口令。' : (result.error || '口令不正确,请重试。'));
|
|
187
|
+
if (!auto) passphraseInput.focus();
|
|
188
|
+
return false;
|
|
55
189
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
190
|
+
if (!writePassphrase(passphrase)) {
|
|
191
|
+
setError('浏览器拒绝保存口令,请检查本地存储权限。');
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
window.location.replace(safeNextPath(nextInput ? nextInput.value : '/'));
|
|
195
|
+
return true;
|
|
59
196
|
}
|
|
60
|
-
|
|
197
|
+
|
|
198
|
+
form.addEventListener('submit', function (event) {
|
|
199
|
+
event.preventDefault();
|
|
200
|
+
submitPassphrase(String(passphraseInput.value || ''), false);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
var cachedPassphrase = readPassphrase();
|
|
204
|
+
if (cachedPassphrase) submitPassphrase(cachedPassphrase, true);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function initProtectedPage() {
|
|
208
|
+
ensureAuthorized().then(function (authorized) {
|
|
209
|
+
if (!authorized) {
|
|
210
|
+
redirectToLogin();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
removePendingState();
|
|
214
|
+
initChangePassword();
|
|
215
|
+
initLogout();
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (document.body && document.body.classList.contains('auth-page')) {
|
|
220
|
+
initLoginPage();
|
|
221
|
+
} else if (document.readyState === 'loading') {
|
|
222
|
+
document.addEventListener('DOMContentLoaded', initProtectedPage);
|
|
223
|
+
} else {
|
|
224
|
+
initProtectedPage();
|
|
225
|
+
}
|
|
61
226
|
})();
|
|
@@ -155,7 +155,24 @@
|
|
|
155
155
|
article.style.opacity = '0.6';
|
|
156
156
|
article.style.pointerEvents = 'none';
|
|
157
157
|
|
|
158
|
-
|
|
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
|
})
|
package/server/views/login.ejs
CHANGED
|
@@ -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
|
-
|
|
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>
|