docnest 0.1.10 → 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 +1 -1
- package/dist/auth.d.ts +0 -2
- package/dist/auth.js +1 -18
- package/dist/server.js +1 -27
- package/docnest.config.example.mjs +2 -2
- package/package.json +1 -1
- package/server/static/css/styles.css +1 -102
- package/server/static/js/core/auth.js +0 -74
- package/server/views/partials/header.ejs +0 -42
package/README.md
CHANGED
|
@@ -77,7 +77,7 @@ export default defineConfig({
|
|
|
77
77
|
授权只有一道关口,没有账号、密码组合或角色体系。首次启动时,`passphrase` 会初始化到由
|
|
78
78
|
`stateFile` 指定的本地授权状态文件;状态文件只保存带随机盐的口令哈希,不保存明文口令。
|
|
79
79
|
按极简模式约定,前端会把可回填的口令永久保存到浏览器 `localStorage`(不写入 URL);每次打开或刷新文档页面,前端都会把当前缓存的口令交给后台重新校验。校验失败会回到口令页,校验成功才会继续打开当前页面;输入页会保留跳转前的目标地址,错误口令会停留在输入页。由于 `localStorage` 是浏览器本地明文存储,请只在受信任的浏览器环境使用此模式。
|
|
80
|
-
`localStorageKey`
|
|
80
|
+
`localStorageKey` 可选,用于为不同宿主项目隔离浏览器存储键。内置授权不提供“修改口令”或“退出”按钮;需要更换口令时,停止服务、删除 `stateFile`,修改配置中的 `passphrase` 后重新启动。
|
|
81
81
|
建议把 `.docnest/` 加入宿主项目的 `.gitignore`,并为口令状态文件设置仅当前用户可读写的权限。
|
|
82
82
|
|
|
83
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
|
-
|
|
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/server.js
CHANGED
|
@@ -297,10 +297,7 @@ function renderLoginPage(res, nextPath, error, status = 200) {
|
|
|
297
297
|
app.get('/login', (req, res) => {
|
|
298
298
|
if (!BUILT_IN_AUTH_ENABLED)
|
|
299
299
|
return res.redirect('/');
|
|
300
|
-
|
|
301
|
-
? '口令已更新,请使用新口令重新进入。'
|
|
302
|
-
: '';
|
|
303
|
-
return renderLoginPage(res, req.query.next, message);
|
|
300
|
+
return renderLoginPage(res, req.query.next, '');
|
|
304
301
|
});
|
|
305
302
|
app.post('/auth/verify', (req, res) => {
|
|
306
303
|
if (!BUILT_IN_AUTH_ENABLED)
|
|
@@ -323,29 +320,6 @@ app.post('/auth/login', (req, res) => {
|
|
|
323
320
|
setAuthCookie(res, token);
|
|
324
321
|
return res.redirect(nextPath);
|
|
325
322
|
});
|
|
326
|
-
app.post('/auth/logout', (req, res) => {
|
|
327
|
-
AUTH_MANAGER.revokeSession(readAuthToken(req));
|
|
328
|
-
clearAuthCookie(res);
|
|
329
|
-
return res.redirect('/login');
|
|
330
|
-
});
|
|
331
|
-
app.post('/auth/change-password', (req, res) => {
|
|
332
|
-
if (!BUILT_IN_AUTH_ENABLED || !AUTH_MANAGER.authenticateSession(readAuthToken(req))) {
|
|
333
|
-
return res.status(401).json({ ok: false, error: '当前会话未授权。' });
|
|
334
|
-
}
|
|
335
|
-
try {
|
|
336
|
-
const changed = AUTH_MANAGER.changePassphrase(String(req.body?.currentPassphrase || ''), String(req.body?.nextPassphrase || ''));
|
|
337
|
-
if (!changed)
|
|
338
|
-
return res.status(401).json({ ok: false, error: '当前口令不正确。' });
|
|
339
|
-
clearAuthCookie(res);
|
|
340
|
-
return res.json({ ok: true });
|
|
341
|
-
}
|
|
342
|
-
catch (error) {
|
|
343
|
-
return res.status(400).json({
|
|
344
|
-
ok: false,
|
|
345
|
-
error: error instanceof Error ? error.message : '新口令无效。',
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
});
|
|
349
323
|
function documentAuthMiddleware(req, res, next) {
|
|
350
324
|
if (!BUILT_IN_AUTH_ENABLED)
|
|
351
325
|
return next();
|
|
@@ -34,10 +34,10 @@ 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
|
|
40
|
+
// 前端 localStorage 的键;口令会永久保存在浏览器本地,直到手动清除或口令失效。
|
|
41
41
|
localStorageKey: 'my-project:auth-passphrase',
|
|
42
42
|
sessionTtlMinutes: 24 * 60,
|
|
43
43
|
},
|
package/package.json
CHANGED
|
@@ -5778,43 +5778,6 @@ html.docnest-auth-pending::before {
|
|
|
5778
5778
|
font-size: 0.9rem;
|
|
5779
5779
|
}
|
|
5780
5780
|
|
|
5781
|
-
.auth-actions {
|
|
5782
|
-
display: inline-flex;
|
|
5783
|
-
align-items: center;
|
|
5784
|
-
gap: 0.4rem;
|
|
5785
|
-
}
|
|
5786
|
-
|
|
5787
|
-
.auth-logout-form {
|
|
5788
|
-
display: inline-flex;
|
|
5789
|
-
margin: 0;
|
|
5790
|
-
}
|
|
5791
|
-
|
|
5792
|
-
.auth-change-password-btn,
|
|
5793
|
-
.auth-logout-btn {
|
|
5794
|
-
white-space: nowrap;
|
|
5795
|
-
}
|
|
5796
|
-
|
|
5797
|
-
.auth-modal[hidden] {
|
|
5798
|
-
display: none;
|
|
5799
|
-
}
|
|
5800
|
-
|
|
5801
|
-
.auth-modal {
|
|
5802
|
-
position: fixed;
|
|
5803
|
-
inset: 0;
|
|
5804
|
-
z-index: 1000;
|
|
5805
|
-
display: grid;
|
|
5806
|
-
place-items: center;
|
|
5807
|
-
padding: 1.25rem;
|
|
5808
|
-
}
|
|
5809
|
-
|
|
5810
|
-
.auth-modal-backdrop {
|
|
5811
|
-
position: absolute;
|
|
5812
|
-
inset: 0;
|
|
5813
|
-
background: rgba(10, 16, 24, 0.42);
|
|
5814
|
-
backdrop-filter: blur(4px);
|
|
5815
|
-
}
|
|
5816
|
-
|
|
5817
|
-
.auth-modal-card,
|
|
5818
5781
|
.auth-login-card {
|
|
5819
5782
|
position: relative;
|
|
5820
5783
|
z-index: 1;
|
|
@@ -5825,25 +5788,11 @@ html.docnest-auth-pending::before {
|
|
|
5825
5788
|
box-shadow: 0 24px 70px rgba(17, 29, 40, 0.2);
|
|
5826
5789
|
}
|
|
5827
5790
|
|
|
5828
|
-
.auth-modal-card {
|
|
5829
|
-
width: min(100%, 440px);
|
|
5830
|
-
padding: 1.5rem;
|
|
5831
|
-
}
|
|
5832
|
-
|
|
5833
|
-
.auth-modal-header {
|
|
5834
|
-
display: flex;
|
|
5835
|
-
align-items: flex-start;
|
|
5836
|
-
justify-content: space-between;
|
|
5837
|
-
gap: 1rem;
|
|
5838
|
-
}
|
|
5839
|
-
|
|
5840
|
-
.auth-modal-header h2,
|
|
5841
5791
|
.auth-login-card h1 {
|
|
5842
5792
|
margin: 0;
|
|
5843
5793
|
color: var(--linear-text, #18212a);
|
|
5844
5794
|
}
|
|
5845
5795
|
|
|
5846
|
-
.auth-modal-eyebrow,
|
|
5847
5796
|
.auth-login-eyebrow {
|
|
5848
5797
|
margin: 0 0 0.35rem;
|
|
5849
5798
|
color: var(--linear-accent, #267c83);
|
|
@@ -5852,35 +5801,12 @@ html.docnest-auth-pending::before {
|
|
|
5852
5801
|
letter-spacing: 0.12em;
|
|
5853
5802
|
}
|
|
5854
5803
|
|
|
5855
|
-
.auth-modal-close {
|
|
5856
|
-
width: 2rem;
|
|
5857
|
-
height: 2rem;
|
|
5858
|
-
padding: 0;
|
|
5859
|
-
border: 0;
|
|
5860
|
-
border-radius: 999px;
|
|
5861
|
-
color: var(--linear-text-muted, #63727f);
|
|
5862
|
-
background: transparent;
|
|
5863
|
-
font-size: 1.5rem;
|
|
5864
|
-
line-height: 1;
|
|
5865
|
-
cursor: pointer;
|
|
5866
|
-
}
|
|
5867
|
-
|
|
5868
|
-
.auth-modal-close:hover {
|
|
5869
|
-
background: var(--linear-bg-subtle, #eef2f5);
|
|
5870
|
-
}
|
|
5871
|
-
|
|
5872
|
-
.auth-modal-help,
|
|
5873
5804
|
.auth-login-description,
|
|
5874
5805
|
.auth-login-note {
|
|
5875
5806
|
color: var(--linear-text-muted, #63727f);
|
|
5876
5807
|
line-height: 1.6;
|
|
5877
5808
|
}
|
|
5878
5809
|
|
|
5879
|
-
.auth-modal-help {
|
|
5880
|
-
margin: 1rem 0 1.25rem;
|
|
5881
|
-
font-size: 0.875rem;
|
|
5882
|
-
}
|
|
5883
|
-
|
|
5884
5810
|
.auth-form {
|
|
5885
5811
|
display: grid;
|
|
5886
5812
|
gap: 0.9rem;
|
|
@@ -5920,15 +5846,7 @@ html.docnest-auth-pending::before {
|
|
|
5920
5846
|
font-size: 0.875rem;
|
|
5921
5847
|
}
|
|
5922
5848
|
|
|
5923
|
-
.auth-
|
|
5924
|
-
display: flex;
|
|
5925
|
-
justify-content: flex-end;
|
|
5926
|
-
gap: 0.6rem;
|
|
5927
|
-
margin-top: 0.25rem;
|
|
5928
|
-
}
|
|
5929
|
-
|
|
5930
|
-
.auth-primary-btn,
|
|
5931
|
-
.auth-secondary-btn {
|
|
5849
|
+
.auth-primary-btn {
|
|
5932
5850
|
min-height: 2.5rem;
|
|
5933
5851
|
padding: 0.55rem 0.95rem;
|
|
5934
5852
|
border-radius: 8px;
|
|
@@ -5943,12 +5861,6 @@ html.docnest-auth-pending::before {
|
|
|
5943
5861
|
background: var(--linear-accent, #267c83);
|
|
5944
5862
|
}
|
|
5945
5863
|
|
|
5946
|
-
.auth-secondary-btn {
|
|
5947
|
-
border: 1px solid var(--linear-border, #d7dee5);
|
|
5948
|
-
color: var(--linear-text, #18212a);
|
|
5949
|
-
background: var(--linear-bg-subtle, #eef2f5);
|
|
5950
|
-
}
|
|
5951
|
-
|
|
5952
5864
|
.auth-page {
|
|
5953
5865
|
min-height: 100vh;
|
|
5954
5866
|
display: grid;
|
|
@@ -6010,16 +5922,3 @@ html.docnest-auth-pending::before {
|
|
|
6010
5922
|
font-size: 0.75rem;
|
|
6011
5923
|
text-align: center;
|
|
6012
5924
|
}
|
|
6013
|
-
|
|
6014
|
-
@media (max-width: 768px) {
|
|
6015
|
-
.auth-actions {
|
|
6016
|
-
gap: 0.25rem;
|
|
6017
|
-
}
|
|
6018
|
-
|
|
6019
|
-
.auth-change-password-btn,
|
|
6020
|
-
.auth-logout-btn {
|
|
6021
|
-
min-width: 44px;
|
|
6022
|
-
padding-inline: 0.5rem;
|
|
6023
|
-
font-size: 0.75rem;
|
|
6024
|
-
}
|
|
6025
|
-
}
|
|
@@ -92,78 +92,6 @@
|
|
|
92
92
|
document.documentElement.classList.remove('docnest-auth-pending');
|
|
93
93
|
}
|
|
94
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();
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
95
|
function initLoginPage() {
|
|
168
96
|
var form = document.querySelector('.auth-login-form');
|
|
169
97
|
var error = document.querySelector('.auth-login-form .auth-form-error');
|
|
@@ -211,8 +139,6 @@
|
|
|
211
139
|
return;
|
|
212
140
|
}
|
|
213
141
|
removePendingState();
|
|
214
|
-
initChangePassword();
|
|
215
|
-
initLogout();
|
|
216
142
|
});
|
|
217
143
|
}
|
|
218
144
|
|
|
@@ -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
|
<% } %>
|