@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
package/public/app.jsx CHANGED
@@ -1,186 +1,186 @@
1
- // app.jsx — Entry point: initialization + routing
2
-
3
- function App() {
4
- const state = window.useAppState();
5
- const dispatch = window.useAppDispatch();
6
- const { request } = window.useApi();
7
- const { route, navigate } = window.useRouter();
8
- const [initializing, setInitializing] = React.useState(true);
9
-
10
- // Extract route info at top level (needed for useEffect below)
11
- const { path } = route;
12
- const isAdmin = !!(state && state.currentUser && state.currentUser.role === 'admin');
1
+ // app.jsx — Entry point: initialization + routing
2
+
3
+ function App() {
4
+ const state = window.useAppState();
5
+ const dispatch = window.useAppDispatch();
6
+ const { request } = window.useApi();
7
+ const { route, navigate } = window.useRouter();
8
+ const [initializing, setInitializing] = React.useState(true);
9
+
10
+ // Extract route info at top level (needed for useEffect below)
11
+ const { path } = route;
12
+ const isAdmin = !!(state && state.currentUser && state.currentUser.role === 'admin');
13
13
  const currentToken = state ? state.token : null;
14
14
  const currentUserId = state?.currentUser?.id ?? null;
15
15
  const currentLocale = state?.locale || window.i18n?.getCurrentLocale?.() || 'zh-Hans';
16
-
17
- const hydrateAuthenticatedPreferences = React.useCallback((user) => {
18
- if (!user || !window.uiPreferences?.getUserPreferences) return;
19
- dispatch({
20
- type: 'HYDRATE_UI_PREFERENCES',
21
- user,
22
- preferences: window.uiPreferences.getUserPreferences(user.id),
23
- });
24
- }, [dispatch]);
25
-
26
- // 1. App initialization (runs once on mount)
16
+
17
+ const hydrateAuthenticatedPreferences = React.useCallback((user) => {
18
+ if (!user || !window.uiPreferences?.getUserPreferences) return;
19
+ dispatch({
20
+ type: 'HYDRATE_UI_PREFERENCES',
21
+ user,
22
+ preferences: window.uiPreferences.getUserPreferences(user.id),
23
+ });
24
+ }, [dispatch]);
25
+
26
+ // 1. App initialization (runs once on mount)
27
27
  React.useEffect(() => {
28
28
  if (window.i18n?.applyLocale) window.i18n.applyLocale(currentLocale);
29
29
  const theme = state?.theme || window.uiPreferences?.getGuestPreferences?.().theme || 'system';
30
30
  if (window.applyTheme) window.applyTheme(theme);
31
-
32
- async function init() {
33
- try {
34
- // Check init status
35
- const initRes = await request('/api/auth/init-status');
36
- if (!initRes.ok || !initRes.data.data.initialized) {
37
- setInitializing(false);
38
- if (window.location.hash !== '#/setup') navigate('#/setup');
39
- return;
40
- }
41
-
42
- // Load public config
43
- const cfgRes = await request('/api/config/system');
44
- if (cfgRes.ok) dispatch({ type: 'SET_CONFIG', config: cfgRes.data.data });
45
-
46
- // Load categories + tags in parallel
47
- const [catsRes, tagsRes] = await Promise.allSettled([
48
- request('/api/categories'),
49
- request('/api/tags'),
50
- ]);
51
- if (catsRes.status === 'fulfilled' && catsRes.value.ok)
52
- dispatch({ type: 'SET_CATEGORIES', categories: catsRes.value.data.data || [] });
53
- if (tagsRes.status === 'fulfilled' && tagsRes.value.ok)
54
- dispatch({ type: 'SET_TAGS', tags: (tagsRes.value.data.data || []).map(t => t.tag || t) });
55
-
56
- // Restore session
57
- const token = sessionStorage.getItem('rh_token');
58
- let restoredAuthenticatedSession = false;
59
- if (token) {
60
- const meRes = await request('/api/auth/me');
61
- if (meRes.ok) {
62
- dispatch({ type: 'LOGIN', user: meRes.data.data, token });
63
- restoredAuthenticatedSession = true;
64
- } else {
65
- dispatch({ type: 'LOGOUT' });
66
- }
67
- }
68
-
69
- if (!restoredAuthenticatedSession) {
70
- const resourcesRes = await request('/api/resources');
71
- if (resourcesRes.ok) {
72
- dispatch({ type: 'SET_RESOURCES', resources: resourcesRes.data.data || [] });
73
- }
74
- }
75
-
76
- } catch (e) {
77
- console.error('[ResourceHub] Init error:', e);
78
- }
79
- setInitializing(false);
80
- }
81
-
82
- init();
31
+
32
+ async function init() {
33
+ try {
34
+ // Check init status
35
+ const initRes = await request('/api/auth/init-status');
36
+ if (!initRes.ok || !initRes.data.data.initialized) {
37
+ setInitializing(false);
38
+ if (window.location.hash !== '#/setup') navigate('#/setup');
39
+ return;
40
+ }
41
+
42
+ // Load public config
43
+ const cfgRes = await request('/api/config/system');
44
+ if (cfgRes.ok) dispatch({ type: 'SET_CONFIG', config: cfgRes.data.data });
45
+
46
+ // Load categories + tags in parallel
47
+ const [catsRes, tagsRes] = await Promise.allSettled([
48
+ request('/api/categories'),
49
+ request('/api/tags'),
50
+ ]);
51
+ if (catsRes.status === 'fulfilled' && catsRes.value.ok)
52
+ dispatch({ type: 'SET_CATEGORIES', categories: catsRes.value.data.data || [] });
53
+ if (tagsRes.status === 'fulfilled' && tagsRes.value.ok)
54
+ dispatch({ type: 'SET_TAGS', tags: (tagsRes.value.data.data || []).map(t => t.tag || t) });
55
+
56
+ // Restore session
57
+ const token = sessionStorage.getItem('rh_token');
58
+ let restoredAuthenticatedSession = false;
59
+ if (token) {
60
+ const meRes = await request('/api/auth/me');
61
+ if (meRes.ok) {
62
+ dispatch({ type: 'LOGIN', user: meRes.data.data, token });
63
+ restoredAuthenticatedSession = true;
64
+ } else {
65
+ dispatch({ type: 'LOGOUT' });
66
+ }
67
+ }
68
+
69
+ if (!restoredAuthenticatedSession) {
70
+ const resourcesRes = await request('/api/resources');
71
+ if (resourcesRes.ok) {
72
+ dispatch({ type: 'SET_RESOURCES', resources: resourcesRes.data.data || [] });
73
+ }
74
+ }
75
+
76
+ } catch (e) {
77
+ console.error('[ResourceHub] Init error:', e);
78
+ }
79
+ setInitializing(false);
80
+ }
81
+
82
+ init();
83
83
  }, []); // eslint-disable-line react-hooks/exhaustive-deps
84
84
 
85
85
  React.useEffect(() => {
86
86
  document.title = state?.config?.siteTitle || window.i18n?.translateText?.('资源导航系统') || '资源导航系统';
87
87
  }, [currentLocale, state?.config?.siteTitle]);
88
-
89
- // 2. Reload user data when token changes (after login/logout)
90
- React.useEffect(() => {
91
- if (initializing) return;
92
- if (currentToken && currentUserId) {
93
- // Logged in: reload resources + user-specific lists in parallel
94
- Promise.allSettled([
95
- request('/api/resources'),
96
- request('/api/resources/favorites'),
97
- request('/api/resources/history'),
98
- request('/api/resources/mine'),
99
- ]).then(([resRes, favRes, histRes, mineRes]) => {
100
- if (resRes.status === 'fulfilled' && resRes.value.ok)
101
- dispatch({ type: 'SET_RESOURCES', resources: resRes.value.data.data || [] });
102
- if (favRes.status === 'fulfilled' && favRes.value.ok)
103
- dispatch({ type: 'SET_FAVORITES', favorites: favRes.value.data.data || [] });
104
- if (histRes.status === 'fulfilled' && histRes.value.ok)
105
- dispatch({ type: 'SET_HISTORY', history: histRes.value.data.data || [] });
106
- if (mineRes.status === 'fulfilled' && mineRes.value.ok)
107
- dispatch({ type: 'SET_MINE', mine: mineRes.value.data.data || [] });
108
- hydrateAuthenticatedPreferences({ id: currentUserId });
109
- }).catch(() => {});
110
- } else if (currentToken && !currentUserId) {
111
- return;
112
- } else {
113
- // Logged out: reload public resources only; LOGOUT reducer clears favorites/history/mine
114
- request('/api/resources').then(res => {
115
- if (res.ok) dispatch({ type: 'SET_RESOURCES', resources: res.data.data || [] });
116
- }).catch(() => {});
117
- }
118
- }, [currentToken, currentUserId, hydrateAuthenticatedPreferences, initializing]); // eslint-disable-line react-hooks/exhaustive-deps
119
-
120
- // 3. Admin route guard(所有 /admin、/admin/* 均需管理员)
121
- React.useEffect(() => {
122
- if (!initializing && path.startsWith('/admin') && !isAdmin) {
123
- navigate('#/');
124
- }
125
- }, [initializing, path, isAdmin, navigate]);
126
-
127
- // 4. /admin 无子路径时重定向到类别管理
128
- React.useEffect(() => {
129
- if (!initializing && isAdmin && (path === '/admin' || path === '/admin/')) {
130
- navigate('#/admin/categories');
131
- }
132
- }, [initializing, isAdmin, path, navigate]);
133
-
134
- React.useEffect(() => {
135
- if (!initializing && path === '/login' && currentToken && currentUserId) {
136
- navigate('#/');
137
- }
138
- }, [initializing, path, currentToken, currentUserId, navigate]);
139
-
140
- // Loading screen
141
- if (initializing) {
142
- return (
143
- <div style={{
144
- display: 'flex', alignItems: 'center', justifyContent: 'center',
145
- height: '100vh', background: 'var(--bg-primary)',
146
- }}>
147
- <div style={{ textAlign: 'center' }}>
148
- <div style={{
149
- width: '52px', height: '52px', borderRadius: '14px',
150
- background: 'var(--brand)', color: '#fff',
151
- display: 'flex', alignItems: 'center', justifyContent: 'center',
152
- fontSize: '22px', fontWeight: 700, margin: '0 auto 16px',
153
- }}>R</div>
154
- <p style={{ color: 'var(--text-secondary)', fontSize: '14px', margin: 0 }}>加载中…</p>
155
- </div>
156
- </div>
157
- );
158
- }
159
-
160
- // Router
161
- if (path === '/setup') return <window.SetupPage />;
162
- if (path === '/login') {
163
- if (currentToken && currentUserId) return null;
164
- return <window.LoginPage />;
165
- }
166
- if (path === '/register') return <window.RegisterPage />;
167
- if (path === '/forgot-password') return <window.ForgotPasswordPage />;
168
- if (path === '/reset-password') return <window.ResetPasswordPage />;
169
- if (path.startsWith('/admin')) {
170
- if (!isAdmin) return null;
171
- if (path === '/admin' || path === '/admin/') return null;
172
- return <window.AdminPage />;
173
- }
174
- if (path === '/resources') return <window.HomePage pageType="results" />;
175
-
176
- return <window.HomePage pageType="overview" />;
177
- }
178
-
179
- // Mount React root
180
- const rootEl = document.getElementById('root');
181
- const root = ReactDOM.createRoot(rootEl);
182
- root.render(
183
- <window.AppProvider>
184
- <App />
185
- </window.AppProvider>
186
- );
88
+
89
+ // 2. Reload user data when token changes (after login/logout)
90
+ React.useEffect(() => {
91
+ if (initializing) return;
92
+ if (currentToken && currentUserId) {
93
+ // Logged in: reload resources + user-specific lists in parallel
94
+ Promise.allSettled([
95
+ request('/api/resources'),
96
+ request('/api/resources/favorites'),
97
+ request('/api/resources/history'),
98
+ request('/api/resources/mine'),
99
+ ]).then(([resRes, favRes, histRes, mineRes]) => {
100
+ if (resRes.status === 'fulfilled' && resRes.value.ok)
101
+ dispatch({ type: 'SET_RESOURCES', resources: resRes.value.data.data || [] });
102
+ if (favRes.status === 'fulfilled' && favRes.value.ok)
103
+ dispatch({ type: 'SET_FAVORITES', favorites: favRes.value.data.data || [] });
104
+ if (histRes.status === 'fulfilled' && histRes.value.ok)
105
+ dispatch({ type: 'SET_HISTORY', history: histRes.value.data.data || [] });
106
+ if (mineRes.status === 'fulfilled' && mineRes.value.ok)
107
+ dispatch({ type: 'SET_MINE', mine: mineRes.value.data.data || [] });
108
+ hydrateAuthenticatedPreferences({ id: currentUserId });
109
+ }).catch(() => {});
110
+ } else if (currentToken && !currentUserId) {
111
+ return;
112
+ } else {
113
+ // Logged out: reload public resources only; LOGOUT reducer clears favorites/history/mine
114
+ request('/api/resources').then(res => {
115
+ if (res.ok) dispatch({ type: 'SET_RESOURCES', resources: res.data.data || [] });
116
+ }).catch(() => {});
117
+ }
118
+ }, [currentToken, currentUserId, hydrateAuthenticatedPreferences, initializing]); // eslint-disable-line react-hooks/exhaustive-deps
119
+
120
+ // 3. Admin route guard(所有 /admin、/admin/* 均需管理员)
121
+ React.useEffect(() => {
122
+ if (!initializing && path.startsWith('/admin') && !isAdmin) {
123
+ navigate('#/');
124
+ }
125
+ }, [initializing, path, isAdmin, navigate]);
126
+
127
+ // 4. /admin 无子路径时重定向到类别管理
128
+ React.useEffect(() => {
129
+ if (!initializing && isAdmin && (path === '/admin' || path === '/admin/')) {
130
+ navigate('#/admin/categories');
131
+ }
132
+ }, [initializing, isAdmin, path, navigate]);
133
+
134
+ React.useEffect(() => {
135
+ if (!initializing && path === '/login' && currentToken && currentUserId) {
136
+ navigate('#/');
137
+ }
138
+ }, [initializing, path, currentToken, currentUserId, navigate]);
139
+
140
+ // Loading screen
141
+ if (initializing) {
142
+ return (
143
+ <div style={{
144
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
145
+ height: '100vh', background: 'var(--bg-primary)',
146
+ }}>
147
+ <div style={{ textAlign: 'center' }}>
148
+ <div style={{
149
+ width: '52px', height: '52px', borderRadius: '14px',
150
+ background: 'var(--brand)', color: '#fff',
151
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
152
+ fontSize: '22px', fontWeight: 700, margin: '0 auto 16px',
153
+ }}>R</div>
154
+ <p style={{ color: 'var(--text-secondary)', fontSize: '14px', margin: 0 }}>加载中…</p>
155
+ </div>
156
+ </div>
157
+ );
158
+ }
159
+
160
+ // Router
161
+ if (path === '/setup') return <window.SetupPage />;
162
+ if (path === '/login') {
163
+ if (currentToken && currentUserId) return null;
164
+ return <window.LoginPage />;
165
+ }
166
+ if (path === '/register') return <window.RegisterPage />;
167
+ if (path === '/forgot-password') return <window.ForgotPasswordPage />;
168
+ if (path === '/reset-password') return <window.ResetPasswordPage />;
169
+ if (path.startsWith('/admin')) {
170
+ if (!isAdmin) return null;
171
+ if (path === '/admin' || path === '/admin/') return null;
172
+ return <window.AdminPage />;
173
+ }
174
+ if (path === '/resources') return <window.HomePage pageType="results" />;
175
+
176
+ return <window.HomePage pageType="overview" />;
177
+ }
178
+
179
+ // Mount React root
180
+ const rootEl = document.getElementById('root');
181
+ const root = ReactDOM.createRoot(rootEl);
182
+ root.render(
183
+ <window.AppProvider>
184
+ <App />
185
+ </window.AppProvider>
186
+ );