create-fullstack-scaffold 0.1.1 → 0.2.0

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 (155) hide show
  1. package/dist/cli/index.js +1673 -696
  2. package/dist/cli/index.js.map +1 -1
  3. package/package.json +16 -10
  4. package/template/.husky/pre-commit +1 -1
  5. package/template/modules.config.ts +29 -2
  6. package/template/package.json +12 -12
  7. package/template/patches/{typescript+5.8.3.patch → typescript+5.9.3.patch} +2 -4
  8. package/template/playwright.config.ts +7 -1
  9. package/template/src/admin/App.tsx +2 -2
  10. package/template/src/admin/components/CaptchaModal.tsx +7 -9
  11. package/template/src/admin/layouts/Header.tsx +56 -22
  12. package/template/src/admin/layouts/Layout.tsx +14 -9
  13. package/template/src/admin/layouts/Sidebar.tsx +122 -105
  14. package/template/src/admin/pages/CategoryManagementPage.tsx +241 -0
  15. package/template/src/admin/pages/ContentPage.tsx +2 -0
  16. package/template/src/admin/pages/DashboardPage.tsx +2 -0
  17. package/template/src/admin/pages/DisputesPage.tsx +2 -0
  18. package/template/src/admin/pages/MediaTestPage.tsx +1 -1
  19. package/template/src/admin/pages/OrdersPage.tsx +2 -0
  20. package/template/src/admin/pages/PluginDashboardPage.tsx +297 -0
  21. package/template/src/admin/pages/PluginManagementPage.tsx +340 -0
  22. package/template/src/admin/pages/PluginReviewPage.tsx +255 -0
  23. package/template/src/admin/pages/SystemLogsPage.tsx +11 -2
  24. package/template/src/admin/pages/TicketsPage.tsx +2 -0
  25. package/template/src/admin/pages/UsersPage.tsx +3 -2
  26. package/template/src/admin/stores/adminStore.ts +5 -0
  27. package/template/src/cli/index.ts +17 -19
  28. package/template/src/cli/modules/auth/index.ts +65 -0
  29. package/template/src/cli/modules/config/index.ts +74 -77
  30. package/template/src/cli/modules/index.ts +28 -6
  31. package/template/src/cli/modules/notification/index.ts +95 -79
  32. package/template/src/cli/modules/plugin/index.ts +111 -0
  33. package/template/src/cli/modules/todo/index.ts +99 -51
  34. package/template/src/cli/utils/auto-command.ts +7 -25
  35. package/template/src/cli/utils/index.ts +3 -1
  36. package/template/src/client/App.tsx +36 -16
  37. package/template/src/client/Layout.tsx +67 -10
  38. package/template/src/client/components/AuthButton.tsx +25 -18
  39. package/template/src/client/components/BottomTabBar.tsx +107 -0
  40. package/template/src/client/components/Navigation.tsx +162 -52
  41. package/template/src/client/components/__tests__/App.test.tsx +48 -32
  42. package/template/src/client/components/__tests__/AuthButton.test.tsx +78 -77
  43. package/template/src/client/components/__tests__/Navigation.test.tsx +31 -20
  44. package/template/src/client/components/index.ts +1 -0
  45. package/template/src/client/contexts/PresetContext.tsx +10 -0
  46. package/template/src/client/main.tsx +63 -8
  47. package/template/src/client/pages/CartPage.tsx +244 -0
  48. package/template/src/client/pages/CategoriesPage.tsx +100 -0
  49. package/template/src/client/pages/ContentDetailPage.tsx +9 -14
  50. package/template/src/client/pages/ContentListPage.tsx +4 -11
  51. package/template/src/client/pages/DashboardPage.tsx +261 -0
  52. package/template/src/client/pages/DeveloperDashboardPage.tsx +211 -0
  53. package/template/src/client/pages/LoginPage.tsx +127 -0
  54. package/template/src/client/pages/OrdersPage.tsx +196 -0
  55. package/template/src/client/pages/PluginDetailPage.tsx +345 -0
  56. package/template/src/client/pages/PluginsPage.tsx +223 -0
  57. package/template/src/client/pages/ProfilePage.tsx +206 -0
  58. package/template/src/client/pages/PublishPage.tsx +336 -0
  59. package/template/src/client/pages/RegisterPage.tsx +136 -0
  60. package/template/src/client/pages/SearchPage.tsx +204 -0
  61. package/template/src/client/pages/SettingsPage.tsx +220 -0
  62. package/template/src/client/pages/TopicsPage.tsx +180 -0
  63. package/template/src/client/pages/__tests__/LoginPage.test.tsx +170 -0
  64. package/template/src/client/pages/__tests__/RegisterPage.test.tsx +168 -0
  65. package/template/src/client/preset-ui-config.ts +492 -0
  66. package/template/src/client/services/apiClient.ts +14 -4
  67. package/template/src/client/stores/__tests__/authStore.test.ts +293 -38
  68. package/template/src/client/stores/__tests__/todoStore.test.ts +7 -17
  69. package/template/src/client/stores/authStore.ts +58 -5
  70. package/template/src/client/stores/chatWSStore.ts +9 -0
  71. package/template/src/client/stores/notificationStore.ts +4 -0
  72. package/template/src/client/stores/pluginStore.ts +279 -0
  73. package/template/src/client/stores/todoStore.ts +2 -6
  74. package/template/src/server/core/__tests__/isr-cache.test.ts +117 -0
  75. package/template/src/server/core/__tests__/isr-invalidation.test.ts +72 -0
  76. package/template/src/server/core/__tests__/ssr-renderer.test.ts +89 -0
  77. package/template/src/server/core/isr-cache.ts +239 -0
  78. package/template/src/server/core/isr-invalidation.ts +45 -0
  79. package/template/src/server/core/module-loader.ts +14 -7
  80. package/template/src/server/core/ssr-renderer.ts +240 -0
  81. package/template/src/server/db/init.ts +257 -10
  82. package/template/src/server/db/schema/developers.ts +20 -0
  83. package/template/src/server/db/schema/index.ts +2 -0
  84. package/template/src/server/db/schema/plugins.ts +114 -0
  85. package/template/src/server/db/test-setup.ts +91 -0
  86. package/template/src/server/entries/cloudflare.ts +79 -7
  87. package/template/src/server/entries/node.ts +48 -5
  88. package/template/src/server/middleware/__tests__/captcha.test.ts +23 -13
  89. package/template/src/server/middleware/auth.ts +8 -1
  90. package/template/src/server/middleware/captcha.ts +14 -4
  91. package/template/src/server/middleware/rate-limit.ts +7 -2
  92. package/template/src/server/module-admin/module.ts +9 -5
  93. package/template/src/server/module-admin/routes/admin-notification-routes.ts +43 -14
  94. package/template/src/server/module-admin/routes/admin-routes.ts +0 -2
  95. package/template/src/server/module-admin/routes/client-auth-routes.ts +90 -0
  96. package/template/src/server/module-admin/routes/dashboard-routes.ts +79 -0
  97. package/template/src/server/module-admin/services/admin-service.ts +68 -11
  98. package/template/src/server/module-auth/__tests__/auth-service.test.ts +239 -0
  99. package/template/src/server/module-auth/index.ts +7 -0
  100. package/template/src/server/module-auth/module.ts +40 -0
  101. package/template/src/server/module-auth/routes/auth-routes.ts +94 -0
  102. package/template/src/server/module-auth/routes/profile-routes.ts +31 -0
  103. package/template/src/server/module-auth/services/auth-service.ts +100 -0
  104. package/template/src/server/module-content/module.ts +10 -4
  105. package/template/src/server/module-content/routes/public-content-routes.ts +2 -2
  106. package/template/src/server/module-content/routes/topics-routes.ts +205 -0
  107. package/template/src/server/module-content/services/content-service.ts +18 -2
  108. package/template/src/server/module-dispute/routes/dispute-routes.ts +2 -1
  109. package/template/src/server/module-dispute/services/dispute-service.ts +1 -1
  110. package/template/src/server/module-notifications/__tests__/sse-rpc.test.ts +14 -14
  111. package/template/src/server/module-notifications/routes/notification-routes.ts +34 -8
  112. package/template/src/server/module-order/__tests__/order-route.test.ts +22 -2
  113. package/template/src/server/module-order/module.ts +15 -0
  114. package/template/src/server/module-order/routes/cart-routes.ts +103 -0
  115. package/template/src/server/module-order/routes/orders-mock-routes.ts +67 -0
  116. package/template/src/server/module-order/services/order-service.ts +1 -1
  117. package/template/src/server/module-plugin/__tests__/plugin-query-service.test.ts +203 -0
  118. package/template/src/server/module-plugin/__tests__/plugin-service.test.ts +234 -0
  119. package/template/src/server/module-plugin/index.ts +2 -0
  120. package/template/src/server/module-plugin/module.ts +52 -0
  121. package/template/src/server/module-plugin/routes/plugin-admin-routes.ts +261 -0
  122. package/template/src/server/module-plugin/routes/plugin-routes.ts +354 -0
  123. package/template/src/server/module-plugin/services/admin-category-service.ts +99 -0
  124. package/template/src/server/module-plugin/services/admin-plugin-service.ts +170 -0
  125. package/template/src/server/module-plugin/services/admin-stats-service.ts +41 -0
  126. package/template/src/server/module-plugin/services/plugin-query-service.ts +360 -0
  127. package/template/src/server/module-plugin/services/plugin-review-service.ts +95 -0
  128. package/template/src/server/module-plugin/services/plugin-service.ts +163 -0
  129. package/template/src/server/module-ticket/services/ticket-service.ts +1 -1
  130. package/template/src/server/module-todos/routes/todos-routes.ts +0 -4
  131. package/template/src/server/route-registry.ts +16 -1
  132. package/template/src/server/test-utils/test-client.ts +1 -2
  133. package/template/src/server/utils/auth.ts +6 -0
  134. package/template/src/server/utils/json.ts +13 -0
  135. package/template/src/shared/core/module-manifest.ts +3 -6
  136. package/template/src/shared/modules/auth/index.ts +12 -0
  137. package/template/src/shared/modules/auth/schemas.ts +50 -0
  138. package/template/src/shared/modules/cart/index.ts +1 -0
  139. package/template/src/shared/modules/cart/schemas.ts +41 -0
  140. package/template/src/shared/modules/community/index.ts +1 -0
  141. package/template/src/shared/modules/community/schemas.ts +58 -0
  142. package/template/src/shared/modules/dashboard/index.ts +1 -0
  143. package/template/src/shared/modules/dashboard/schemas.ts +35 -0
  144. package/template/src/shared/modules/index.ts +41 -0
  145. package/template/src/shared/modules/order/schemas.ts +29 -0
  146. package/template/src/shared/modules/plugins/index.ts +48 -0
  147. package/template/src/shared/modules/plugins/schemas.ts +227 -0
  148. package/template/src/shared/schemas/index.ts +130 -0
  149. package/template/tests/e2e/todo.spec.ts +23 -18
  150. package/template/tests/e2e/visual-screenshots.spec.ts +1824 -0
  151. package/template/uploads/.gitkeep +0 -0
  152. package/template/vite.config.ts +2 -1
  153. package/template/vitest.setup.ts +11 -0
  154. package/template/wrangler.toml +4 -3
  155. package/template/package-lock.json +0 -14554
@@ -16,6 +16,9 @@ import { getAppConfig } from '../config'
16
16
  import { logger } from '../utils/logger'
17
17
  import { createApp } from '../app'
18
18
  import { getDb, runMigrations } from '../db'
19
+ import { createISRCache, isISRRoute } from '@server/core/isr-cache'
20
+ import { renderPage } from '@server/core/ssr-renderer'
21
+ import { setISRCache } from '@server/core/isr-invalidation'
19
22
  import { setRuntimeAdapter } from '@server/core/runtime'
20
23
  import { getNodeRuntimeAdapter } from '@server/core/runtime-node'
21
24
 
@@ -41,9 +44,16 @@ const adminHtml = existsSync(adminHtmlPath)
41
44
 
42
45
  const log = logger.api()
43
46
 
47
+ const isrCache = createISRCache()
48
+ setISRCache(isrCache)
49
+
44
50
  const runtimeAdapter = getNodeRuntimeAdapter()
45
51
  setRuntimeAdapter(runtimeAdapter)
46
52
 
53
+ runtimeAdapter.handleWS('/api/chat/ws')
54
+ runtimeAdapter.handleSSE('/api/notifications/stream')
55
+ runtimeAdapter.handleSSE('/api/admin/notifications/stream')
56
+
47
57
  // 先创建基础应用
48
58
  const baseApp = createApp()
49
59
 
@@ -104,12 +114,41 @@ app.get('/admin/*', c => {
104
114
  return c.html(adminHtml)
105
115
  })
106
116
 
107
- // 其他非 API 路由返回 index.html
108
- app.get('*', c => {
109
- // 如果路径以 /api/ 或 /files/ 开头,让 Hono 继续处理(可能会返回 404)
117
+ // 其他非 API 路由返回 index.html(ISR 路由尝试缓存)
118
+ app.get('*', async c => {
110
119
  if (c.req.path.startsWith('/api/') || c.req.path.startsWith('/files/')) {
111
120
  return c.notFound()
112
121
  }
122
+
123
+ const pathname = c.req.path
124
+
125
+ if (hasDist && isISRRoute(pathname)) {
126
+ const result = await isrCache.lookup(pathname)
127
+
128
+ if (result.status === 'fresh' && result.html) {
129
+ return c.html(result.html)
130
+ }
131
+
132
+ if (result.status === 'stale' && result.html) {
133
+ renderPage(pathname)
134
+ .then(rendered => {
135
+ isrCache.store(pathname, rendered.html).catch(e => {
136
+ console.warn('ISR cache store (background revalidation) failed:', e)
137
+ })
138
+ })
139
+ .catch(e => {
140
+ console.warn('ISR background render failed:', e)
141
+ })
142
+ return c.html(result.html)
143
+ }
144
+
145
+ const rendered = await renderPage(pathname)
146
+ isrCache.store(pathname, rendered.html).catch(e => {
147
+ console.warn('ISR cache store failed:', e)
148
+ })
149
+ return c.html(rendered.html)
150
+ }
151
+
113
152
  return c.html(indexHtml)
114
153
  })
115
154
 
@@ -118,10 +157,14 @@ app.get('*', c => {
118
157
  app.onError((err, c) => {
119
158
  log.error({ err, path: c.req.path }, 'server error')
120
159
  c.res.headers.set('Content-Type', 'application/json')
121
- const statusCode = err instanceof Error && 'status' in err ? (err as { status: number }).status : 500
160
+ const statusCode =
161
+ err instanceof Error && 'status' in err ? (err as { status: number }).status : 500
122
162
  const message = err.message || 'Internal server error'
123
163
  const responseStatus = statusCode || 500
124
- return c.json({ success: false as const, error: message, status: responseStatus }, responseStatus as 500)
164
+ return c.json(
165
+ { success: false as const, error: message, status: responseStatus },
166
+ responseStatus as 500
167
+ )
125
168
  })
126
169
 
127
170
  export default app
@@ -8,9 +8,7 @@ import {
8
8
  } from '../captcha'
9
9
 
10
10
  describe('captchaMiddleware', () => {
11
- beforeEach(() => {
12
- vi.stubEnv('NODE_ENV', 'development')
13
- })
11
+ beforeEach(() => {})
14
12
 
15
13
  afterEach(() => {
16
14
  vi.unstubAllEnvs()
@@ -28,10 +26,22 @@ describe('captchaMiddleware', () => {
28
26
  expect(handler).toHaveBeenCalled()
29
27
  })
30
28
 
31
- it('should skip configured skip paths', async () => {
29
+ it('should skip in development environment', async () => {
30
+ vi.stubEnv('NODE_ENV', 'development')
32
31
  const app = new Hono()
33
32
  const handler = vi.fn().mockResolvedValue(new Response('ok'))
34
33
  app.use('*', captchaMiddleware())
34
+ app.get('/api/dev', handler)
35
+
36
+ const res = await app.request('/api/dev')
37
+ expect(res.status).toBe(200)
38
+ expect(handler).toHaveBeenCalled()
39
+ })
40
+
41
+ it('should skip configured skip paths', async () => {
42
+ const app = new Hono()
43
+ const handler = vi.fn().mockResolvedValue(new Response('ok'))
44
+ app.use('*', captchaMiddleware({ forceEnabled: true }))
35
45
  app.get('/api/captcha', handler)
36
46
  app.post('/api/verify-captcha', handler)
37
47
  app.post('/api/admin/login', handler)
@@ -54,7 +64,7 @@ describe('captchaMiddleware', () => {
54
64
  it('should skip custom skip paths', async () => {
55
65
  const app = new Hono()
56
66
  const handler = vi.fn().mockResolvedValue(new Response('ok'))
57
- app.use('*', captchaMiddleware({ skipPaths: ['/api/custom'] }))
67
+ app.use('*', captchaMiddleware({ forceEnabled: true, skipPaths: ['/api/custom'] }))
58
68
  app.get('/api/custom', handler)
59
69
 
60
70
  const res = await app.request('/api/custom')
@@ -65,7 +75,7 @@ describe('captchaMiddleware', () => {
65
75
  it('should allow requests within rate limit', async () => {
66
76
  const app = new Hono()
67
77
  let callCount = 0
68
- app.use('*', captchaMiddleware({ maxRequests: 10, windowMs: 60000 }))
78
+ app.use('*', captchaMiddleware({ forceEnabled: true, maxRequests: 10, windowMs: 60000 }))
69
79
  app.get('/api/data', c => {
70
80
  callCount++
71
81
  return c.json({ ok: true })
@@ -82,7 +92,7 @@ describe('captchaMiddleware', () => {
82
92
 
83
93
  it('should block requests exceeding rate limit', async () => {
84
94
  const app = new Hono()
85
- app.use('*', captchaMiddleware({ maxRequests: 3, windowMs: 60000 }))
95
+ app.use('*', captchaMiddleware({ forceEnabled: true, maxRequests: 3, windowMs: 60000 }))
86
96
  app.get('/api/data', c => c.json({ ok: true }))
87
97
 
88
98
  const ua = 'Mozilla/5.0 Test Browser For Rate Limit Testing'
@@ -109,7 +119,7 @@ describe('captchaMiddleware', () => {
109
119
 
110
120
  it('should block suspicious requests with short User-Agent', async () => {
111
121
  const app = new Hono()
112
- app.use('*', captchaMiddleware())
122
+ app.use('*', captchaMiddleware({ forceEnabled: true }))
113
123
  app.get('/api/data', c => c.json({ ok: true }))
114
124
 
115
125
  const res = await app.request('/api/data', {
@@ -122,7 +132,7 @@ describe('captchaMiddleware', () => {
122
132
 
123
133
  it('should block requests with bot in User-Agent', async () => {
124
134
  const app = new Hono()
125
- app.use('*', captchaMiddleware())
135
+ app.use('*', captchaMiddleware({ forceEnabled: true }))
126
136
  app.get('/api/data', c => c.json({ ok: true }))
127
137
 
128
138
  const res = await app.request('/api/data', {
@@ -133,7 +143,7 @@ describe('captchaMiddleware', () => {
133
143
 
134
144
  it('should block requests with crawler in User-Agent', async () => {
135
145
  const app = new Hono()
136
- app.use('*', captchaMiddleware())
146
+ app.use('*', captchaMiddleware({ forceEnabled: true }))
137
147
  app.get('/api/data', c => c.json({ ok: true }))
138
148
 
139
149
  const res = await app.request('/api/data', {
@@ -144,7 +154,7 @@ describe('captchaMiddleware', () => {
144
154
 
145
155
  it('should block requests with empty User-Agent', async () => {
146
156
  const app = new Hono()
147
- app.use('*', captchaMiddleware())
157
+ app.use('*', captchaMiddleware({ forceEnabled: true }))
148
158
  app.get('/api/data', c => c.json({ ok: true }))
149
159
 
150
160
  const res = await app.request('/api/data')
@@ -154,7 +164,7 @@ describe('captchaMiddleware', () => {
154
164
  it('should reset rate limit after window expires', async () => {
155
165
  vi.useFakeTimers()
156
166
  const app = new Hono()
157
- app.use('*', captchaMiddleware({ maxRequests: 2, windowMs: 1000 }))
167
+ app.use('*', captchaMiddleware({ forceEnabled: true, maxRequests: 2, windowMs: 1000 }))
158
168
  app.get('/api/data', c => c.json({ ok: true }))
159
169
 
160
170
  const ua = 'Mozilla/5.0 Window Reset Test Browser'
@@ -191,7 +201,7 @@ describe('captchaMiddleware', () => {
191
201
  markCaptchaVerifiedMiddleware(sessionId)
192
202
 
193
203
  const app = new Hono()
194
- app.use('*', captchaMiddleware({ maxRequests: 1, windowMs: 60000 }))
204
+ app.use('*', captchaMiddleware({ forceEnabled: true, maxRequests: 1, windowMs: 60000 }))
195
205
  app.get('/api/data', c => c.json({ ok: true }))
196
206
 
197
207
  const res = await app.request('/api/data', {
@@ -40,7 +40,14 @@ if (secretKey === defaultSecretKey && process.env.NODE_ENV === 'production') {
40
40
  }
41
41
 
42
42
  const isDevTokensEnabled = (): boolean => {
43
- return process.env.ENABLE_DEV_TOKENS === 'true' && process.env.NODE_ENV !== 'production'
43
+ // 显式启用 dev tokens(优先级最高,即使 production 也能用)
44
+ if (process.env.ENABLE_DEV_TOKENS === 'true') return true
45
+ // 显式禁用
46
+ if (process.env.ENABLE_DEV_TOKENS === 'false') return false
47
+ // production 默认禁用
48
+ if (process.env.NODE_ENV === 'production') return false
49
+ // 开发环境默认启用
50
+ return true
44
51
  }
45
52
 
46
53
  if (isDevTokensEnabled()) {
@@ -6,6 +6,7 @@ export interface CaptchaConfig {
6
6
  skipPaths?: string[]
7
7
  maxRequests?: number
8
8
  windowMs?: number
9
+ forceEnabled?: boolean
9
10
  }
10
11
 
11
12
  interface CaptchaSession {
@@ -40,10 +41,12 @@ export function captchaMiddleware(config: CaptchaConfig = {}) {
40
41
  skipPaths = ['/api/captcha', '/api/verify-captcha', '/api/admin/login', '/api/admin/register'],
41
42
  maxRequests = 10,
42
43
  windowMs = 60000,
44
+ forceEnabled = false,
43
45
  } = config
44
46
 
45
47
  return async (c: Context, next: Next) => {
46
- if (process.env.NODE_ENV === 'test') {
48
+ const skipEnvCheck = !forceEnabled && process.env.NODE_ENV === 'test'
49
+ if (skipEnvCheck) {
47
50
  return next()
48
51
  }
49
52
 
@@ -97,7 +100,7 @@ export function captchaMiddleware(config: CaptchaConfig = {}) {
97
100
  )
98
101
  }
99
102
 
100
- if (isSuspiciousRequest(c)) {
103
+ if (isSuspiciousRequest(c, forceEnabled)) {
101
104
  return c.json(
102
105
  {
103
106
  success: false as const,
@@ -136,14 +139,21 @@ function generateSessionId(): string {
136
139
  return randomUUID()
137
140
  }
138
141
 
139
- function isSuspiciousRequest(c: Context): boolean {
142
+ function isSuspiciousRequest(c: Context, forceEnabled = false): boolean {
143
+ if (
144
+ !forceEnabled &&
145
+ (process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'development')
146
+ ) {
147
+ return false
148
+ }
149
+
140
150
  const userAgent = c.req.header('User-Agent') || ''
141
151
 
142
152
  if (!userAgent || userAgent.length < 10) {
143
153
  return true
144
154
  }
145
155
 
146
- if (userAgent.includes('bot') || userAgent.includes('crawler')) {
156
+ if (userAgent.toLowerCase().includes('bot') || userAgent.toLowerCase().includes('crawler')) {
147
157
  return true
148
158
  }
149
159
 
@@ -4,14 +4,18 @@ type RateLimitEntry = { count: number; resetAt: number }
4
4
 
5
5
  const rateLimitStore = new Map<string, RateLimitEntry>()
6
6
 
7
- setInterval(() => {
7
+ let lastCleanup = Date.now()
8
+
9
+ function cleanupExpired() {
8
10
  const now = Date.now()
11
+ if (now - lastCleanup < 60_000) return
12
+ lastCleanup = now
9
13
  for (const [key, entry] of rateLimitStore) {
10
14
  if (entry.resetAt <= now) {
11
15
  rateLimitStore.delete(key)
12
16
  }
13
17
  }
14
- }, 60_000)
18
+ }
15
19
 
16
20
  export type RateLimitOptions = {
17
21
  windowMs?: number
@@ -29,6 +33,7 @@ export function rateLimitMiddleware(options: RateLimitOptions = {}) {
29
33
  return next()
30
34
  }
31
35
 
36
+ cleanupExpired()
32
37
  const ip =
33
38
  c.req.header('x-forwarded-for')?.split(',')[0]?.trim() ||
34
39
  c.req.header('x-real-ip') ||
@@ -8,21 +8,27 @@ const adminManifest: ModuleManifest = {
8
8
  dependsOn: ['permission', 'notifications'],
9
9
 
10
10
  routes: {
11
+ client: {
12
+ importPath: './routes/client-auth-routes',
13
+ exportName: 'clientAuthRoutes',
14
+ },
11
15
  admin: [
12
16
  {
13
17
  importPath: './routes/admin-routes',
14
18
  exportName: 'adminRoutes',
15
19
  },
16
20
  ],
17
- // Note: admin-routes.ts is an aggregator that composes sub-routes:
18
- // auth-routes (login/register/me), user-management-routes, admin-notification-routes,
19
- // media-routes (avatar/svg), export-routes (CSV), system-routes (stats/health/activity)
20
21
  },
21
22
 
22
23
  sharedSchemas: {
23
24
  path: 'admin',
24
25
  },
25
26
 
27
+ clientPages: [
28
+ { name: 'DashboardPage', route: '/dashboard' },
29
+ { name: 'SettingsPage', route: '/settings' },
30
+ ],
31
+
26
32
  adminPages: [
27
33
  { name: 'LoginPage', route: '/login', isPublic: true },
28
34
  { name: 'RegisterPage', route: '/register', isPublic: true },
@@ -32,8 +38,6 @@ const adminManifest: ModuleManifest = {
32
38
  { name: 'MediaTestPage', route: '/test/media' },
33
39
  ],
34
40
 
35
- clientStores: ['authStore'],
36
-
37
41
  hasSSE: true,
38
42
  }
39
43
 
@@ -17,6 +17,30 @@ import {
17
17
  } from '@shared/modules/notifications'
18
18
  import { SuccessSchema } from '@shared/modules/admin'
19
19
 
20
+ function createFallbackSSEResponse(): Response {
21
+ const stream = new ReadableStream({
22
+ start(controller) {
23
+ const encoder = new TextEncoder()
24
+ const sendPing = () => {
25
+ controller.enqueue(encoder.encode(`event: ping\ndata: {"timestamp":${Date.now()}}\n\n`))
26
+ }
27
+ sendPing()
28
+ const interval = setInterval(sendPing, 30000)
29
+ setTimeout(() => {
30
+ clearInterval(interval)
31
+ controller.close()
32
+ }, 300000)
33
+ },
34
+ })
35
+ return new Response(stream, {
36
+ headers: {
37
+ 'Content-Type': 'text/event-stream',
38
+ 'Cache-Control': 'no-cache',
39
+ Connection: 'keep-alive',
40
+ },
41
+ })
42
+ }
43
+
20
44
  const getNotificationsRoute = createRoute({
21
45
  method: 'get',
22
46
  path: '/admin/notifications',
@@ -160,18 +184,18 @@ export const adminNotificationRoutes = new OpenAPIHono<{ Variables: { authUser:
160
184
  type === 'warning'
161
185
  ? '警告通知'
162
186
  : type === 'error'
163
- ? '错误通知'
164
- : type === 'success'
165
- ? '成功通知'
166
- : '系统通知',
187
+ ? '错误通知'
188
+ : type === 'success'
189
+ ? '成功通知'
190
+ : '系统通知',
167
191
  message:
168
192
  type === 'warning'
169
193
  ? '这是一条警告通知,请注意!'
170
194
  : type === 'error'
171
- ? '这是一条错误通知,请立即处理!'
172
- : type === 'success'
173
- ? '操作成功完成!'
174
- : '这是一条普通信息通知',
195
+ ? '这是一条错误通知,请立即处理!'
196
+ : type === 'success'
197
+ ? '操作成功完成!'
198
+ : '这是一条普通信息通知',
175
199
  })
176
200
  return c.json(success(notification), 200)
177
201
  })
@@ -187,11 +211,16 @@ export const adminNotificationRoutes = new OpenAPIHono<{ Variables: { authUser:
187
211
  return stub.fetch(doRequest)
188
212
  }
189
213
 
190
- const { getRuntimeAdapter } = await import('@server/core/runtime')
191
- const adapter = getRuntimeAdapter()
192
- if (adapter.handleSSERequest) {
193
- const response = await adapter.handleSSERequest()
194
- return response
214
+ try {
215
+ const { getRuntimeAdapter } = await import('@server/core/runtime')
216
+ const adapter = getRuntimeAdapter()
217
+ if (adapter.handleSSERequest) {
218
+ const response = await adapter.handleSSERequest()
219
+ return response
220
+ }
221
+ } catch {
222
+ return createFallbackSSEResponse()
195
223
  }
196
- return c.json({ success: false as const, error: 'SSE not supported' }, 500)
224
+
225
+ return createFallbackSSEResponse()
197
226
  })
@@ -16,5 +16,3 @@ const adminBase2 = adminBase1.route('/', adminNotificationRoutes).route('/', med
16
16
  const adminBase3 = adminBase2.route('/', exportRoutes).route('/', systemRoutes)
17
17
 
18
18
  export const adminRoutes = adminBase3
19
-
20
- export default adminRoutes
@@ -0,0 +1,90 @@
1
+ import { createRoute } from '@hono/zod-openapi'
2
+ import { OpenAPIHono } from '@hono/zod-openapi'
3
+ import { authMiddleware, type AuthUser } from '@server/middleware/auth'
4
+ import { strictRateLimitMiddleware } from '@server/middleware/rate-limit'
5
+ import { getAuthUser } from '@server/utils/auth'
6
+ import * as adminService from '../services/admin-service'
7
+ import { successResponse, errorResponse, success } from '@server/utils/route-helpers'
8
+ import {
9
+ AuthUserSchema,
10
+ LoginRequestSchema,
11
+ LoginResponseSchema,
12
+ RegisterRequestSchema,
13
+ UserSchema,
14
+ } from '@shared/modules/admin'
15
+
16
+ const loginRoute = createRoute({
17
+ method: 'post',
18
+ path: '/auth/login',
19
+ tags: ['auth'],
20
+ middleware: [strictRateLimitMiddleware] as const,
21
+ request: {
22
+ body: {
23
+ content: {
24
+ 'application/json': {
25
+ schema: LoginRequestSchema,
26
+ },
27
+ },
28
+ },
29
+ },
30
+ responses: {
31
+ 200: successResponse(LoginResponseSchema, 'Login successful'),
32
+ 401: errorResponse('Invalid credentials'),
33
+ },
34
+ })
35
+
36
+ const registerRoute = createRoute({
37
+ method: 'post',
38
+ path: '/auth/register',
39
+ tags: ['auth'],
40
+ middleware: [strictRateLimitMiddleware] as const,
41
+ request: {
42
+ body: {
43
+ content: {
44
+ 'application/json': {
45
+ schema: RegisterRequestSchema,
46
+ },
47
+ },
48
+ },
49
+ },
50
+ responses: {
51
+ 201: successResponse(UserSchema, 'User registered'),
52
+ 400: errorResponse('User already exists'),
53
+ },
54
+ })
55
+
56
+ const meRoute = createRoute({
57
+ method: 'get',
58
+ path: '/auth/me',
59
+ tags: ['auth'],
60
+ security: [{ Bearer: [] }],
61
+ middleware: [authMiddleware()],
62
+ responses: {
63
+ 200: successResponse(AuthUserSchema, 'Get current authenticated user'),
64
+ 401: errorResponse('Unauthorized'),
65
+ },
66
+ })
67
+
68
+ export const clientAuthRoutes = new OpenAPIHono<{ Variables: { authUser: AuthUser } }>()
69
+ .openapi(loginRoute, async c => {
70
+ try {
71
+ const data = c.req.valid('json')
72
+ const result = await adminService.login(data)
73
+ return c.json(success(result), 200)
74
+ } catch {
75
+ return c.json({ success: false as const, error: 'Invalid credentials' }, 401)
76
+ }
77
+ })
78
+ .openapi(registerRoute, async c => {
79
+ try {
80
+ const data = c.req.valid('json')
81
+ const user = await adminService.register(data)
82
+ return c.json(success(user), 201)
83
+ } catch {
84
+ return c.json({ success: false as const, error: 'User already exists' }, 400)
85
+ }
86
+ })
87
+ .openapi(meRoute, async c => {
88
+ const user = getAuthUser(c)
89
+ return c.json(success(user), 200)
90
+ })
@@ -0,0 +1,79 @@
1
+ import { createRoute } from '@hono/zod-openapi'
2
+ import { OpenAPIHono } from '@hono/zod-openapi'
3
+ import { successResponse } from '@server/utils/route-helpers'
4
+ import { DashboardResponseSchema } from '@shared/schemas'
5
+
6
+ const getDashboardStatsRoute = createRoute({
7
+ method: 'get',
8
+ path: '/admin/dashboard/stats',
9
+ responses: {
10
+ 200: successResponse(DashboardResponseSchema, 'Dashboard stats'),
11
+ },
12
+ })
13
+
14
+ export const dashboardRoutes = new OpenAPIHono().openapi(getDashboardStatsRoute, async c => {
15
+ return c.json({
16
+ success: true as const,
17
+ data: {
18
+ stats: [
19
+ { label: 'Total Users', value: '12,847', trend: 12.5 },
20
+ { label: 'Active Users', value: '8,234', trend: 8.2 },
21
+ { label: 'Revenue', value: '$48,293', trend: -2.4 },
22
+ { label: 'Conversion', value: '3.2%', trend: 4.1 },
23
+ ],
24
+ revenue: [
25
+ { month: 'Jan', value: 65 },
26
+ { month: 'Feb', value: 45 },
27
+ { month: 'Mar', value: 78 },
28
+ { month: 'Apr', value: 52 },
29
+ { month: 'May', value: 90 },
30
+ { month: 'Jun', value: 70 },
31
+ ],
32
+ userGrowth: [
33
+ { month: 'Jan', value: 30 },
34
+ { month: 'Feb', value: 45 },
35
+ { month: 'Mar', value: 55 },
36
+ { month: 'Apr', value: 60 },
37
+ { month: 'May', value: 72 },
38
+ { month: 'Jun', value: 85 },
39
+ ],
40
+ activity: [
41
+ {
42
+ id: 1,
43
+ user: 'Sarah Chen',
44
+ action: 'Upgraded to Pro plan',
45
+ date: '2024-01-15',
46
+ status: 'Active' as const,
47
+ },
48
+ {
49
+ id: 2,
50
+ user: 'Mike Johnson',
51
+ action: 'Submitted support ticket',
52
+ date: '2024-01-14',
53
+ status: 'Pending' as const,
54
+ },
55
+ {
56
+ id: 3,
57
+ user: 'Emily Davis',
58
+ action: 'Cancelled subscription',
59
+ date: '2024-01-13',
60
+ status: 'Inactive' as const,
61
+ },
62
+ {
63
+ id: 4,
64
+ user: 'Alex Turner',
65
+ action: 'Registered new account',
66
+ date: '2024-01-12',
67
+ status: 'Active' as const,
68
+ },
69
+ {
70
+ id: 5,
71
+ user: 'Lisa Park',
72
+ action: 'Updated billing info',
73
+ date: '2024-01-11',
74
+ status: 'Active' as const,
75
+ },
76
+ ],
77
+ },
78
+ })
79
+ })