@zhang_libo/resource-hub 1.0.2 → 1.0.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.
Files changed (49) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +13 -3
  3. package/README.ja.md +12 -2
  4. package/README.md +12 -2
  5. package/README.zh-TW.md +12 -2
  6. package/dist/app.js +4 -1
  7. package/dist/app.js.map +1 -1
  8. package/dist/db/migrate.js +136 -111
  9. package/dist/db/migrate.js.map +1 -1
  10. package/package.json +70 -73
  11. package/public/app.jsx +174 -174
  12. package/public/features/ChangePasswordModal.jsx +198 -187
  13. package/public/features/ResourceFormModal.jsx +915 -915
  14. package/public/layout/AppLayout.jsx +119 -119
  15. package/public/layout/Sidebar.jsx +1 -1
  16. package/public/pages/LoginPage.jsx +10 -0
  17. package/public/pages/SetupPage.jsx +1 -1
  18. package/public/utils/security.jsx +65 -28
  19. package/public/vendor/forge.min.js +2 -0
  20. package/dist/app.d.ts +0 -2
  21. package/dist/app.d.ts.map +0 -1
  22. package/dist/db/migrate.d.ts +0 -3
  23. package/dist/db/migrate.d.ts.map +0 -1
  24. package/dist/db/schema.d.ts +0 -743
  25. package/dist/db/schema.d.ts.map +0 -1
  26. package/dist/plugins/admin.d.ts +0 -4
  27. package/dist/plugins/admin.d.ts.map +0 -1
  28. package/dist/plugins/auth.d.ts +0 -4
  29. package/dist/plugins/auth.d.ts.map +0 -1
  30. package/dist/routes/auth.d.ts +0 -4
  31. package/dist/routes/auth.d.ts.map +0 -1
  32. package/dist/routes/categories.d.ts +0 -4
  33. package/dist/routes/categories.d.ts.map +0 -1
  34. package/dist/routes/config.d.ts +0 -4
  35. package/dist/routes/config.d.ts.map +0 -1
  36. package/dist/routes/resources.d.ts +0 -4
  37. package/dist/routes/resources.d.ts.map +0 -1
  38. package/dist/routes/tags.d.ts +0 -4
  39. package/dist/routes/tags.d.ts.map +0 -1
  40. package/dist/routes/users.d.ts +0 -4
  41. package/dist/routes/users.d.ts.map +0 -1
  42. package/dist/services/crypto.js +0 -49
  43. package/dist/services/crypto.js.map +0 -1
  44. package/dist/services/mail.d.ts +0 -16
  45. package/dist/services/mail.d.ts.map +0 -1
  46. package/dist/services/token.d.ts +0 -9
  47. package/dist/services/token.d.ts.map +0 -1
  48. package/dist/types.d.ts +0 -80
  49. package/dist/types.d.ts.map +0 -1
@@ -1,187 +1,198 @@
1
- function ChangePasswordModal({ isOpen, onClose }) {
2
- const state = window.useAppState();
3
- const dispatch = window.useAppDispatch();
4
- const { request } = window.useApi();
5
- const currentUser = state?.currentUser;
6
-
7
- const [form, setForm] = React.useState({ currentPassword: '', newPassword: '', confirmPassword: '' });
8
- const [errors, setErrors] = React.useState({});
9
- const [loading, setLoading] = React.useState(false);
10
- const [showFields, setShowFields] = React.useState({ current: false, new: false, confirm: false });
11
-
12
- React.useEffect(() => {
13
- if (isOpen) {
14
- setForm({ currentPassword: '', newPassword: '', confirmPassword: '' });
15
- setErrors({});
16
- setShowFields({ current: false, new: false, confirm: false });
17
- }
18
- }, [isOpen]);
19
-
20
- const validate = () => {
21
- const errs = {};
22
- if (!form.currentPassword)
23
- errs.currentPassword = '请输入当前密码';
24
- if (form.newPassword.length < 8 || !/[a-zA-Z]/.test(form.newPassword) || !/[0-9]/.test(form.newPassword))
25
- errs.newPassword = '新密码至少8位,需包含字母和数字';
26
- else if (form.newPassword === form.currentPassword)
27
- errs.newPassword = '新密码不能与当前密码相同';
28
- if (form.newPassword !== form.confirmPassword)
29
- errs.confirmPassword = '两次密码不一致';
30
- return errs;
31
- };
32
-
33
- const handleSubmit = async () => {
34
- const errs = validate();
35
- if (Object.keys(errs).length > 0) { setErrors(errs); return; }
36
- setLoading(true);
37
- try {
38
- const { passwordEnc: currentPasswordEnc, ts } = await window.security.encryptPasswordWithTs(form.currentPassword);
39
- const { passwordEnc: newPasswordEnc } = await window.security.encryptPasswordWithTs(form.newPassword);
40
- const { ok, data } = await request('/api/auth/me/password', {
41
- method: 'PUT',
42
- body: JSON.stringify({
43
- currentPasswordEnc,
44
- newPasswordEnc,
45
- ts,
46
- }),
47
- });
48
- if (ok) {
49
- dispatch({ type: 'ADD_TOAST', toastType: 'success', message: '密码已修改' });
50
- onClose();
51
- } else {
52
- if (data.code === 'WRONG_PASSWORD') {
53
- setErrors({ currentPassword: '当前密码错误' });
54
- } else if (data.code === 'VALIDATION_ERROR' && data.fields) {
55
- setErrors(data.fields);
56
- }
57
- dispatch({ type: 'ADD_TOAST', toastType: 'error', message: data.error || '修改失败' });
58
- }
59
- } finally {
60
- setLoading(false);
61
- }
62
- };
63
-
64
- const PasswordField = ({ field, label, showKey, placeholder, autoComplete }) => (
65
- <div style={{ marginBottom: '14px' }}>
66
- <label style={{
67
- fontSize: '13px', fontWeight: 500, color: 'var(--text-primary)',
68
- display: 'block', marginBottom: '6px',
69
- }}>
70
- {label}
71
- </label>
72
- <div style={{ position: 'relative' }}>
73
- <input
74
- type={showFields[showKey] ? 'text' : 'password'}
75
- name={field}
76
- autoComplete={autoComplete}
77
- value={form[field]}
78
- placeholder={placeholder}
79
- onChange={e => setForm(f => ({ ...f, [field]: e.target.value }))}
80
- onKeyDown={e => { if (e.key === 'Enter') handleSubmit(); }}
81
- disabled={loading}
82
- style={{
83
- width: '100%', padding: '8px 40px 8px 12px',
84
- border: `1px solid ${errors[field] ? 'var(--danger)' : 'var(--border)'}`,
85
- borderRadius: '8px', background: 'var(--bg-secondary)',
86
- color: 'var(--text-primary)', fontSize: '14px',
87
- outline: 'none', boxSizing: 'border-box',
88
- }}
89
- />
90
- <button
91
- type="button"
92
- onClick={() => setShowFields(s => ({ ...s, [showKey]: !s[showKey] }))}
93
- style={{
94
- position: 'absolute', right: '10px', top: '50%', transform: 'translateY(-50%)',
95
- background: 'none', border: 'none', cursor: 'pointer',
96
- color: 'var(--text-secondary)', display: 'flex', alignItems: 'center',
97
- }}
98
- >
99
- {showFields[showKey] ? <lucide.EyeOff size={16} /> : <lucide.Eye size={16} />}
100
- </button>
101
- </div>
102
- {field === 'newPassword' && (
103
- <window.PasswordStrength password={form.newPassword} />
104
- )}
105
- {errors[field] && (
106
- <div style={{ fontSize: '12px', color: 'var(--danger)', marginTop: '4px' }}>{errors[field]}</div>
107
- )}
108
- </div>
109
- );
110
-
111
- return (
112
- <window.Modal isOpen={isOpen} onClose={onClose} title="修改密码" width="440px" closeOnBackdrop={false} closeOnEscape>
113
- <form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
114
- <input
115
- type="text"
116
- name="username"
117
- autoComplete="username"
118
- value={currentUser?.username || currentUser?.email || ''}
119
- readOnly
120
- tabIndex={-1}
121
- style={{
122
- position: 'absolute',
123
- opacity: 0,
124
- pointerEvents: 'none',
125
- width: '1px',
126
- height: '1px',
127
- }}
128
- />
129
- <PasswordField
130
- field="currentPassword"
131
- label="当前密码"
132
- showKey="current"
133
- placeholder="请输入当前密码"
134
- autoComplete="current-password"
135
- />
136
- <PasswordField
137
- field="newPassword"
138
- label="新密码"
139
- showKey="new"
140
- placeholder="至少8位,含字母和数字"
141
- autoComplete="new-password"
142
- />
143
- <PasswordField
144
- field="confirmPassword"
145
- label="确认新密码"
146
- showKey="confirm"
147
- placeholder="再次输入新密码"
148
- autoComplete="new-password"
149
- />
150
-
151
- <div style={{
152
- display: 'flex', gap: '8px', justifyContent: 'flex-end',
153
- marginTop: '8px', paddingTop: '16px', borderTop: '1px solid var(--border)',
154
- }}>
155
- <button
156
- type="button"
157
- onClick={onClose}
158
- disabled={loading}
159
- style={{
160
- padding: '8px 20px', border: '1px solid var(--border)',
161
- background: 'var(--bg-secondary)', color: 'var(--text-primary)',
162
- borderRadius: '8px', cursor: 'pointer', fontSize: '14px',
163
- }}
164
- >
165
- 取消
166
- </button>
167
- <button
168
- type="submit"
169
- disabled={loading}
170
- style={{
171
- padding: '8px 20px', background: 'var(--brand)', color: '#fff',
172
- border: 'none', borderRadius: '8px',
173
- cursor: loading ? 'not-allowed' : 'pointer',
174
- fontSize: '14px', fontWeight: 500, opacity: loading ? 0.8 : 1,
175
- display: 'flex', alignItems: 'center', gap: '6px',
176
- }}
177
- >
178
- {loading && <lucide.Loader size={14} />}
179
- 确认修改
180
- </button>
181
- </div>
182
- </form>
183
- </window.Modal>
184
- );
185
- }
186
-
187
- window.ChangePasswordModal = ChangePasswordModal;
1
+ function ChangePasswordModal({ isOpen, onClose }) {
2
+ const state = window.useAppState();
3
+ const dispatch = window.useAppDispatch();
4
+ const { request } = window.useApi();
5
+ const currentUser = state?.currentUser;
6
+
7
+ const [form, setForm] = React.useState({ currentPassword: '', newPassword: '', confirmPassword: '' });
8
+ const [errors, setErrors] = React.useState({});
9
+ const [loading, setLoading] = React.useState(false);
10
+ const [showFields, setShowFields] = React.useState({ current: false, new: false, confirm: false });
11
+
12
+ React.useEffect(() => {
13
+ if (isOpen) {
14
+ setForm({ currentPassword: '', newPassword: '', confirmPassword: '' });
15
+ setErrors({});
16
+ setShowFields({ current: false, new: false, confirm: false });
17
+ }
18
+ }, [isOpen]);
19
+
20
+ const validate = () => {
21
+ const errs = {};
22
+ if (!form.currentPassword)
23
+ errs.currentPassword = '请输入当前密码';
24
+ if (form.newPassword.length < 8 || !/[a-zA-Z]/.test(form.newPassword) || !/[0-9]/.test(form.newPassword))
25
+ errs.newPassword = '新密码至少8位,需包含字母和数字';
26
+ else if (form.newPassword === form.currentPassword)
27
+ errs.newPassword = '新密码不能与当前密码相同';
28
+ if (form.newPassword !== form.confirmPassword)
29
+ errs.confirmPassword = '两次密码不一致';
30
+ return errs;
31
+ };
32
+
33
+ const handleSubmit = async () => {
34
+ const errs = validate();
35
+ if (Object.keys(errs).length > 0) { setErrors(errs); return; }
36
+ setLoading(true);
37
+ try {
38
+ const { passwordEnc: currentPasswordEnc, ts } = await window.security.encryptPasswordWithTs(form.currentPassword);
39
+ const { passwordEnc: newPasswordEnc } = await window.security.encryptPasswordWithTs(form.newPassword);
40
+ const { ok, data } = await request('/api/auth/me/password', {
41
+ method: 'PUT',
42
+ body: JSON.stringify({
43
+ currentPasswordEnc,
44
+ newPasswordEnc,
45
+ ts,
46
+ }),
47
+ });
48
+ if (ok) {
49
+ dispatch({ type: 'ADD_TOAST', toastType: 'success', message: '密码已修改' });
50
+ onClose();
51
+ } else {
52
+ if (data.code === 'WRONG_PASSWORD') {
53
+ setErrors({ currentPassword: '当前密码错误' });
54
+ } else if (data.code === 'VALIDATION_ERROR' && data.fields) {
55
+ setErrors(data.fields);
56
+ }
57
+ dispatch({ type: 'ADD_TOAST', toastType: 'error', message: data.error || '修改失败' });
58
+ }
59
+ } catch (err) {
60
+ console.error('[ChangePassword] Error:', err);
61
+ const msg = (err?.message || '').toLowerCase();
62
+ const isCryptoError = msg.includes('crypto') || msg.includes('forge') || msg.includes('importkey');
63
+ dispatch({
64
+ type: 'ADD_TOAST',
65
+ toastType: 'error',
66
+ message: isCryptoError
67
+ ? '当前浏览器环境不支持安全加密,请使用 HTTPS localhost 访问'
68
+ : '操作过程中发生错误,请稍后重试',
69
+ });
70
+ } finally {
71
+ setLoading(false);
72
+ }
73
+ };
74
+
75
+ const PasswordField = ({ field, label, showKey, placeholder, autoComplete }) => (
76
+ <div style={{ marginBottom: '14px' }}>
77
+ <label style={{
78
+ fontSize: '13px', fontWeight: 500, color: 'var(--text-primary)',
79
+ display: 'block', marginBottom: '6px',
80
+ }}>
81
+ {label}
82
+ </label>
83
+ <div style={{ position: 'relative' }}>
84
+ <input
85
+ type={showFields[showKey] ? 'text' : 'password'}
86
+ name={field}
87
+ autoComplete={autoComplete}
88
+ value={form[field]}
89
+ placeholder={placeholder}
90
+ onChange={e => setForm(f => ({ ...f, [field]: e.target.value }))}
91
+ onKeyDown={e => { if (e.key === 'Enter') handleSubmit(); }}
92
+ disabled={loading}
93
+ style={{
94
+ width: '100%', padding: '8px 40px 8px 12px',
95
+ border: `1px solid ${errors[field] ? 'var(--danger)' : 'var(--border)'}`,
96
+ borderRadius: '8px', background: 'var(--bg-secondary)',
97
+ color: 'var(--text-primary)', fontSize: '14px',
98
+ outline: 'none', boxSizing: 'border-box',
99
+ }}
100
+ />
101
+ <button
102
+ type="button"
103
+ onClick={() => setShowFields(s => ({ ...s, [showKey]: !s[showKey] }))}
104
+ style={{
105
+ position: 'absolute', right: '10px', top: '50%', transform: 'translateY(-50%)',
106
+ background: 'none', border: 'none', cursor: 'pointer',
107
+ color: 'var(--text-secondary)', display: 'flex', alignItems: 'center',
108
+ }}
109
+ >
110
+ {showFields[showKey] ? <lucide.EyeOff size={16} /> : <lucide.Eye size={16} />}
111
+ </button>
112
+ </div>
113
+ {field === 'newPassword' && (
114
+ <window.PasswordStrength password={form.newPassword} />
115
+ )}
116
+ {errors[field] && (
117
+ <div style={{ fontSize: '12px', color: 'var(--danger)', marginTop: '4px' }}>{errors[field]}</div>
118
+ )}
119
+ </div>
120
+ );
121
+
122
+ return (
123
+ <window.Modal isOpen={isOpen} onClose={onClose} title="修改密码" width="440px" closeOnBackdrop={false} closeOnEscape>
124
+ <form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
125
+ <input
126
+ type="text"
127
+ name="username"
128
+ autoComplete="username"
129
+ value={currentUser?.username || currentUser?.email || ''}
130
+ readOnly
131
+ tabIndex={-1}
132
+ style={{
133
+ position: 'absolute',
134
+ opacity: 0,
135
+ pointerEvents: 'none',
136
+ width: '1px',
137
+ height: '1px',
138
+ }}
139
+ />
140
+ <PasswordField
141
+ field="currentPassword"
142
+ label="当前密码"
143
+ showKey="current"
144
+ placeholder="请输入当前密码"
145
+ autoComplete="current-password"
146
+ />
147
+ <PasswordField
148
+ field="newPassword"
149
+ label="新密码"
150
+ showKey="new"
151
+ placeholder="至少8位,含字母和数字"
152
+ autoComplete="new-password"
153
+ />
154
+ <PasswordField
155
+ field="confirmPassword"
156
+ label="确认新密码"
157
+ showKey="confirm"
158
+ placeholder="再次输入新密码"
159
+ autoComplete="new-password"
160
+ />
161
+
162
+ <div style={{
163
+ display: 'flex', gap: '8px', justifyContent: 'flex-end',
164
+ marginTop: '8px', paddingTop: '16px', borderTop: '1px solid var(--border)',
165
+ }}>
166
+ <button
167
+ type="button"
168
+ onClick={onClose}
169
+ disabled={loading}
170
+ style={{
171
+ padding: '8px 20px', border: '1px solid var(--border)',
172
+ background: 'var(--bg-secondary)', color: 'var(--text-primary)',
173
+ borderRadius: '8px', cursor: 'pointer', fontSize: '14px',
174
+ }}
175
+ >
176
+ 取消
177
+ </button>
178
+ <button
179
+ type="submit"
180
+ disabled={loading}
181
+ style={{
182
+ padding: '8px 20px', background: 'var(--brand)', color: '#fff',
183
+ border: 'none', borderRadius: '8px',
184
+ cursor: loading ? 'not-allowed' : 'pointer',
185
+ fontSize: '14px', fontWeight: 500, opacity: loading ? 0.8 : 1,
186
+ display: 'flex', alignItems: 'center', gap: '6px',
187
+ }}
188
+ >
189
+ {loading && <lucide.Loader size={14} />}
190
+ 确认修改
191
+ </button>
192
+ </div>
193
+ </form>
194
+ </window.Modal>
195
+ );
196
+ }
197
+
198
+ window.ChangePasswordModal = ChangePasswordModal;