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
@@ -1,11 +1,10 @@
1
1
  import { describe, it, expect, beforeEach, vi } from 'vitest'
2
2
  import { create } from 'zustand'
3
+ import { persist } from 'zustand/middleware'
4
+ import { apiClient } from '@client/services/apiClient'
5
+ import type { DeveloperProfile } from '@shared/schemas'
3
6
 
4
- interface UserProfile {
5
- id: string
6
- username: string
7
- role: string
8
- }
7
+ type UserProfile = Pick<DeveloperProfile, 'id' | 'username' | 'role'>
9
8
 
10
9
  interface AuthState {
11
10
  token: string | null
@@ -26,68 +25,188 @@ const createTestStore = () =>
26
25
  logout: () => set({ token: null, isAuthenticated: false, user: null }),
27
26
  }))
28
27
 
28
+ const { mockJson, mockLoginPost, mockRegisterPost } = vi.hoisted(() => {
29
+ const mockJson = vi.fn()
30
+ return {
31
+ mockJson,
32
+ mockLoginPost: vi.fn().mockResolvedValue({ json: () => mockJson() }),
33
+ mockRegisterPost: vi.fn().mockResolvedValue({ json: () => mockJson() }),
34
+ }
35
+ })
36
+
37
+ vi.mock('@client/services/apiClient', () => ({
38
+ apiClient: {
39
+ api: {
40
+ auth: {
41
+ login: { $post: mockLoginPost },
42
+ register: { $post: mockRegisterPost },
43
+ },
44
+ },
45
+ },
46
+ }))
47
+
48
+ const noopStorage = {
49
+ getItem: () => null,
50
+ setItem: () => {},
51
+ removeItem: () => {},
52
+ }
53
+
54
+ interface FullAuthState extends AuthState {
55
+ loading: boolean
56
+ error: string | null
57
+ login: (username: string, password: string) => Promise<void>
58
+ register: (username: string, email: string, password: string) => Promise<void>
59
+ clearError: () => void
60
+ }
61
+
62
+ const createFullAuthStore = () =>
63
+ create<FullAuthState>()(
64
+ persist(
65
+ set => ({
66
+ token: null,
67
+ isAuthenticated: false,
68
+ user: null,
69
+ loading: false,
70
+ error: null,
71
+
72
+ login: async (username: string, password: string) => {
73
+ set({ loading: true, error: null })
74
+ try {
75
+ const response = await apiClient.api.auth.login.$post({
76
+ json: { account: username, password },
77
+ })
78
+ const result = await response.json()
79
+ if (result.success) {
80
+ const { token, profile } = result.data
81
+ set({
82
+ token,
83
+ isAuthenticated: true,
84
+ user: {
85
+ id: profile.id,
86
+ username: profile.username,
87
+ role: profile.role as 'user' | 'admin',
88
+ },
89
+ loading: false,
90
+ error: null,
91
+ })
92
+ } else {
93
+ set({ loading: false, error: result.error })
94
+ }
95
+ } catch {
96
+ set({ loading: false, error: 'Login failed. Please try again.' })
97
+ }
98
+ },
99
+
100
+ register: async (username: string, email: string, password: string) => {
101
+ set({ loading: true, error: null })
102
+ try {
103
+ const response = await apiClient.api.auth.register.$post({
104
+ json: { username, email, password },
105
+ })
106
+ const result = await response.json()
107
+ if (result.success) {
108
+ set({ loading: false, error: null })
109
+ } else {
110
+ set({ loading: false, error: result.error })
111
+ }
112
+ } catch {
113
+ set({ loading: false, error: 'Registration failed. Please try again.' })
114
+ }
115
+ },
116
+
117
+ setAuth: (token: string, user: UserProfile) =>
118
+ set({
119
+ token,
120
+ isAuthenticated: true,
121
+ user,
122
+ }),
123
+
124
+ setToken: (token: string) =>
125
+ set({
126
+ token,
127
+ isAuthenticated: true,
128
+ }),
129
+
130
+ logout: () =>
131
+ set({
132
+ token: null,
133
+ isAuthenticated: false,
134
+ user: null,
135
+ error: null,
136
+ }),
137
+
138
+ clearError: () => set({ error: null }),
139
+ }),
140
+ {
141
+ name: 'auth-token-test',
142
+ storage: noopStorage as unknown as ReturnType<
143
+ typeof import('zustand/middleware').createJSONStorage<FullAuthState>
144
+ >,
145
+ }
146
+ )
147
+ )
148
+
29
149
  describe('authStore', () => {
30
- let useAuthStore: ReturnType<typeof createTestStore>
150
+ let localStore: ReturnType<typeof createTestStore>
31
151
 
32
152
  beforeEach(() => {
33
153
  vi.clearAllMocks()
34
- useAuthStore = createTestStore()
154
+ mockJson.mockReset()
155
+ localStore = createTestStore()
35
156
  })
36
157
 
37
158
  describe('Initial State', () => {
38
159
  it('should have null token and not authenticated', () => {
39
- expect(useAuthStore.getState().token).toBeNull()
40
- expect(useAuthStore.getState().isAuthenticated).toBe(false)
160
+ expect(localStore.getState().token).toBeNull()
161
+ expect(localStore.getState().isAuthenticated).toBe(false)
41
162
  })
42
163
 
43
164
  it('should have null user and not authenticated', () => {
44
- expect(useAuthStore.getState().user).toBeNull()
45
- expect(useAuthStore.getState().isAuthenticated).toBe(false)
165
+ expect(localStore.getState().user).toBeNull()
166
+ expect(localStore.getState().isAuthenticated).toBe(false)
46
167
  })
47
168
  })
48
169
 
49
170
  describe('setAuth', () => {
50
171
  it('should set token and user', () => {
51
- useAuthStore
52
- .getState()
53
- .setAuth('jwt-token-123', { id: '1', username: 'admin', role: 'admin' })
172
+ localStore.getState().setAuth('jwt-token-123', { id: '1', username: 'admin', role: 'admin' })
54
173
 
55
- const state = useAuthStore.getState()
174
+ const state = localStore.getState()
56
175
  expect(state.token).toBe('jwt-token-123')
57
176
  expect(state.isAuthenticated).toBe(true)
58
177
  expect(state.user).toEqual({ id: '1', username: 'admin', role: 'admin' })
59
178
  })
60
179
 
61
180
  it('should update existing auth state', () => {
62
- useAuthStore.getState().setAuth('token-1', { id: '1', username: 'user1', role: 'user' })
63
- useAuthStore.getState().setAuth('token-2', { id: '2', username: 'user2', role: 'admin' })
181
+ localStore.getState().setAuth('token-1', { id: '1', username: 'user1', role: 'user' })
182
+ localStore.getState().setAuth('token-2', { id: '2', username: 'user2', role: 'admin' })
64
183
 
65
- const state = useAuthStore.getState()
184
+ const state = localStore.getState()
66
185
  expect(state.token).toBe('token-2')
67
186
  expect(state.user?.username).toBe('user2')
68
187
  })
69
188
 
70
189
  it('should set isAuthenticated to true and preserve token', () => {
71
- useAuthStore.getState().setAuth('t', { id: '1', username: 'u', role: 'r' })
72
- expect(useAuthStore.getState().isAuthenticated).toBe(true)
73
- expect(useAuthStore.getState().token).toBe('t')
190
+ localStore.getState().setAuth('t', { id: '1', username: 'u', role: 'r' })
191
+ expect(localStore.getState().isAuthenticated).toBe(true)
192
+ expect(localStore.getState().token).toBe('t')
74
193
  })
75
194
  })
76
195
 
77
196
  describe('setToken', () => {
78
197
  it('should set token and mark as authenticated', () => {
79
- useAuthStore.getState().setToken('new-token')
198
+ localStore.getState().setToken('new-token')
80
199
 
81
- const state = useAuthStore.getState()
200
+ const state = localStore.getState()
82
201
  expect(state.token).toBe('new-token')
83
202
  expect(state.isAuthenticated).toBe(true)
84
203
  })
85
204
 
86
205
  it('should not modify user when setting token only', () => {
87
- useAuthStore.getState().setAuth('old', { id: '1', username: 'user', role: 'user' })
88
- useAuthStore.getState().setToken('refreshed-token')
206
+ localStore.getState().setAuth('old', { id: '1', username: 'user', role: 'user' })
207
+ localStore.getState().setToken('refreshed-token')
89
208
 
90
- const state = useAuthStore.getState()
209
+ const state = localStore.getState()
91
210
  expect(state.user).toEqual({ id: '1', username: 'user', role: 'user' })
92
211
  expect(state.token).toBe('refreshed-token')
93
212
  })
@@ -95,19 +214,19 @@ describe('authStore', () => {
95
214
 
96
215
  describe('logout', () => {
97
216
  it('should clear all auth state', () => {
98
- useAuthStore.getState().setAuth('token', { id: '1', username: 'user', role: 'user' })
99
- useAuthStore.getState().logout()
217
+ localStore.getState().setAuth('token', { id: '1', username: 'user', role: 'user' })
218
+ localStore.getState().logout()
100
219
 
101
- const state = useAuthStore.getState()
220
+ const state = localStore.getState()
102
221
  expect(state.token).toBeNull()
103
222
  expect(state.isAuthenticated).toBe(false)
104
223
  expect(state.user).toBeNull()
105
224
  })
106
225
 
107
226
  it('should be safe to call logout when already logged out', () => {
108
- useAuthStore.getState().logout()
227
+ localStore.getState().logout()
109
228
 
110
- const state = useAuthStore.getState()
229
+ const state = localStore.getState()
111
230
  expect(state.token).toBeNull()
112
231
  expect(state.isAuthenticated).toBe(false)
113
232
  expect(state.user).toBeNull()
@@ -116,15 +235,15 @@ describe('authStore', () => {
116
235
 
117
236
  describe('Full auth lifecycle', () => {
118
237
  it('should handle login -> use -> logout flow', () => {
119
- expect(useAuthStore.getState().isAuthenticated).toBe(false)
238
+ expect(localStore.getState().isAuthenticated).toBe(false)
120
239
 
121
- useAuthStore.getState().setAuth('jwt', { id: '1', username: 'admin', role: 'admin' })
122
- expect(useAuthStore.getState().isAuthenticated).toBe(true)
123
- expect(useAuthStore.getState().user?.role).toBe('admin')
240
+ localStore.getState().setAuth('jwt', { id: '1', username: 'admin', role: 'admin' })
241
+ expect(localStore.getState().isAuthenticated).toBe(true)
242
+ expect(localStore.getState().user?.role).toBe('admin')
124
243
 
125
- useAuthStore.getState().logout()
126
- expect(useAuthStore.getState().isAuthenticated).toBe(false)
127
- expect(useAuthStore.getState().user).toBeNull()
244
+ localStore.getState().logout()
245
+ expect(localStore.getState().isAuthenticated).toBe(false)
246
+ expect(localStore.getState().user).toBeNull()
128
247
  })
129
248
  })
130
249
 
@@ -137,3 +256,139 @@ describe('authStore', () => {
137
256
  })
138
257
  })
139
258
  })
259
+
260
+ describe('authStore - login action', () => {
261
+ let store: ReturnType<typeof createFullAuthStore>
262
+
263
+ beforeEach(() => {
264
+ vi.clearAllMocks()
265
+ mockJson.mockReset()
266
+ store = createFullAuthStore()
267
+ })
268
+
269
+ it('should login successfully', async () => {
270
+ mockJson.mockResolvedValue({
271
+ success: true,
272
+ data: {
273
+ token: 'jwt-abc',
274
+ profile: { id: '1', username: 'testuser', role: 'user' },
275
+ },
276
+ })
277
+
278
+ await store.getState().login('testuser', 'pass123')
279
+
280
+ const state = store.getState()
281
+ expect(state.token).toBe('jwt-abc')
282
+ expect(state.isAuthenticated).toBe(true)
283
+ expect(state.user).toEqual({ id: '1', username: 'testuser', role: 'user' })
284
+ expect(state.loading).toBe(false)
285
+ expect(state.error).toBeNull()
286
+ })
287
+
288
+ it('should handle login failure response', async () => {
289
+ mockJson.mockResolvedValue({
290
+ success: false,
291
+ error: 'Invalid credentials',
292
+ })
293
+
294
+ await store.getState().login('wrong', 'wrong')
295
+
296
+ const state = store.getState()
297
+ expect(state.isAuthenticated).toBe(false)
298
+ expect(state.error).toBe('Invalid credentials')
299
+ expect(state.loading).toBe(false)
300
+ })
301
+
302
+ it('should handle login network error', async () => {
303
+ mockLoginPost.mockRejectedValueOnce(new Error('Network error'))
304
+
305
+ await store.getState().login('user', 'pass')
306
+
307
+ const state = store.getState()
308
+ expect(state.error).toBe('Login failed. Please try again.')
309
+ expect(state.loading).toBe(false)
310
+ })
311
+
312
+ it('should set loading to true during login', async () => {
313
+ let resolvePromise!: (value: unknown) => void
314
+ mockLoginPost.mockReturnValueOnce(
315
+ new Promise(r => {
316
+ resolvePromise = r
317
+ })
318
+ )
319
+
320
+ const loginPromise = store.getState().login('user', 'pass')
321
+ expect(store.getState().loading).toBe(true)
322
+
323
+ resolvePromise({ json: () => Promise.resolve({ success: false, error: 'fail' }) })
324
+ await loginPromise
325
+
326
+ expect(store.getState().loading).toBe(false)
327
+ })
328
+ })
329
+
330
+ describe('authStore - register action', () => {
331
+ let store: ReturnType<typeof createFullAuthStore>
332
+
333
+ beforeEach(() => {
334
+ vi.clearAllMocks()
335
+ mockJson.mockReset()
336
+ store = createFullAuthStore()
337
+ })
338
+
339
+ it('should register successfully', async () => {
340
+ mockJson.mockResolvedValue({
341
+ success: true,
342
+ data: { id: '2', username: 'newuser' },
343
+ })
344
+
345
+ await store.getState().register('newuser', 'new@test.com', 'pass123')
346
+
347
+ const state = store.getState()
348
+ expect(state.loading).toBe(false)
349
+ expect(state.error).toBeNull()
350
+ })
351
+
352
+ it('should handle register failure response', async () => {
353
+ mockJson.mockResolvedValue({
354
+ success: false,
355
+ error: 'Username already taken',
356
+ })
357
+
358
+ await store.getState().register('taken', 'taken@test.com', 'pass123')
359
+
360
+ const state = store.getState()
361
+ expect(state.error).toBe('Username already taken')
362
+ expect(state.loading).toBe(false)
363
+ })
364
+
365
+ it('should handle register network error', async () => {
366
+ mockRegisterPost.mockRejectedValueOnce(new Error('Network error'))
367
+
368
+ await store.getState().register('user', 'e@e.com', 'pass')
369
+
370
+ const state = store.getState()
371
+ expect(state.error).toBe('Registration failed. Please try again.')
372
+ expect(state.loading).toBe(false)
373
+ })
374
+ })
375
+
376
+ describe('authStore - clearError action', () => {
377
+ let store: ReturnType<typeof createFullAuthStore>
378
+
379
+ beforeEach(() => {
380
+ vi.clearAllMocks()
381
+ mockJson.mockReset()
382
+ store = createFullAuthStore()
383
+ })
384
+
385
+ it('should clear error', async () => {
386
+ mockJson.mockResolvedValue({ success: false, error: 'test error' })
387
+ await store.getState().login('x', 'y')
388
+
389
+ expect(store.getState().error).toBe('test error')
390
+
391
+ store.getState().clearError()
392
+ expect(store.getState().error).toBeNull()
393
+ })
394
+ })
@@ -127,23 +127,13 @@ describe('Todo Store', () => {
127
127
  expect(state.error).toBeNull()
128
128
  })
129
129
 
130
- it('should fetch todos with { items } response format', async () => {
131
- const todos = [createMockTodo({ id: 1 })]
132
- mockJson.mockResolvedValue({ success: true, data: { items: todos } })
133
-
134
- await useTodoStore.getState().fetchTodos()
135
-
136
- expect(useTodoStore.getState().todos).toEqual(todos)
137
- expect(useTodoStore.getState().loading).toBe(false)
138
- })
139
-
140
- it('should handle empty items in object response', async () => {
141
- mockJson.mockResolvedValue({ success: true, data: {} })
130
+ it('should handle empty array response', async () => {
131
+ mockJson.mockResolvedValue({ success: true, data: [] })
142
132
 
143
133
  await useTodoStore.getState().fetchTodos()
144
134
 
145
135
  expect(useTodoStore.getState().todos).toEqual([])
146
- expect(useTodoStore.getState().error).toBeNull()
136
+ expect(useTodoStore.getState().loading).toBe(false)
147
137
  })
148
138
 
149
139
  it('should handle failed response', async () => {
@@ -434,13 +424,13 @@ describe('Todo Store', () => {
434
424
  expect(useTodoStore.getState().attachments.get(1)).toEqual(attachments)
435
425
  })
436
426
 
437
- it('should fetch attachments with items response format', async () => {
438
- const attachments = [createMockAttachment()]
439
- mockJson.mockResolvedValue({ success: true, data: { items: attachments } })
427
+ it('should store data directly even if not an array', async () => {
428
+ const obj = { items: [createMockAttachment()] }
429
+ mockJson.mockResolvedValue({ success: true, data: obj })
440
430
 
441
431
  await useTodoStore.getState().fetchAttachments(1)
442
432
 
443
- expect(useTodoStore.getState().attachments.get(1)).toEqual(attachments)
433
+ expect(useTodoStore.getState().attachments.get(1)).toEqual(obj)
444
434
  })
445
435
 
446
436
  it('should handle fetch error gracefully', async () => {
@@ -1,20 +1,23 @@
1
1
  import { create } from 'zustand'
2
2
  import { persist } from 'zustand/middleware'
3
+ import { apiClient } from '@client/services/apiClient'
4
+ import type { DeveloperProfile } from '@shared/schemas'
3
5
 
4
- interface UserProfile {
5
- id: string
6
- username: string
7
- role: string
8
- }
6
+ type UserProfile = Pick<DeveloperProfile, 'id' | 'username' | 'role'>
9
7
 
10
8
  interface AuthState {
11
9
  token: string | null
12
10
  isAuthenticated: boolean
13
11
  user: UserProfile | null
12
+ loading: boolean
13
+ error: string | null
14
14
 
15
+ login: (username: string, password: string) => Promise<void>
16
+ register: (username: string, email: string, password: string) => Promise<void>
15
17
  setAuth: (token: string, user: UserProfile) => void
16
18
  setToken: (token: string) => void
17
19
  logout: () => void
20
+ clearError: () => void
18
21
  }
19
22
 
20
23
  export const useAuthStore = create<AuthState>()(
@@ -23,6 +26,53 @@ export const useAuthStore = create<AuthState>()(
23
26
  token: null,
24
27
  isAuthenticated: false,
25
28
  user: null,
29
+ loading: false,
30
+ error: null,
31
+
32
+ login: async (username: string, password: string) => {
33
+ set({ loading: true, error: null })
34
+ try {
35
+ const response = await apiClient.api.auth.login.$post({
36
+ json: { account: username, password },
37
+ })
38
+ const result = await response.json()
39
+ if (result.success) {
40
+ const { token, profile } = result.data
41
+ set({
42
+ token,
43
+ isAuthenticated: true,
44
+ user: {
45
+ id: profile.id,
46
+ username: profile.username,
47
+ role: profile.role as 'user' | 'admin',
48
+ },
49
+ loading: false,
50
+ error: null,
51
+ })
52
+ } else {
53
+ set({ loading: false, error: result.error })
54
+ }
55
+ } catch {
56
+ set({ loading: false, error: 'Login failed. Please try again.' })
57
+ }
58
+ },
59
+
60
+ register: async (username: string, email: string, password: string) => {
61
+ set({ loading: true, error: null })
62
+ try {
63
+ const response = await apiClient.api.auth.register.$post({
64
+ json: { username, email, password },
65
+ })
66
+ const result = await response.json()
67
+ if (result.success) {
68
+ set({ loading: false, error: null })
69
+ } else {
70
+ set({ loading: false, error: result.error })
71
+ }
72
+ } catch {
73
+ set({ loading: false, error: 'Registration failed. Please try again.' })
74
+ }
75
+ },
26
76
 
27
77
  setAuth: (token: string, user: UserProfile) =>
28
78
  set({
@@ -42,7 +92,10 @@ export const useAuthStore = create<AuthState>()(
42
92
  token: null,
43
93
  isAuthenticated: false,
44
94
  user: null,
95
+ error: null,
45
96
  }),
97
+
98
+ clearError: () => set({ error: null }),
46
99
  }),
47
100
  {
48
101
  name: 'auth-token',
@@ -11,6 +11,7 @@ interface WSMessage {
11
11
  interface WSState {
12
12
  status: WSStatus
13
13
  messages: WSMessage[]
14
+ error: string | null
14
15
 
15
16
  connect: () => void
16
17
  disconnect: () => void
@@ -19,6 +20,7 @@ interface WSState {
19
20
  broadcast: (params: { message: string; timestamp: number }) => void
20
21
  notification: (params: { title: string; body: string; timestamp: number }) => void
21
22
  clearMessages: () => void
23
+ clearError: () => void
22
24
  }
23
25
 
24
26
  let wsClient: WSClient<ChatProtocol> | null = null
@@ -26,6 +28,7 @@ let wsClient: WSClient<ChatProtocol> | null = null
26
28
  export const useChatWsStore = create<WSState>((set, get) => ({
27
29
  status: 'closed',
28
30
  messages: [],
31
+ error: null,
29
32
 
30
33
  connect: () => {
31
34
  if (wsClient) return
@@ -87,6 +90,7 @@ export const useChatWsStore = create<WSState>((set, get) => ({
87
90
  }))
88
91
  } catch (error) {
89
92
  console.error('Echo failed:', error)
93
+ set({ error: error instanceof Error ? error.message : 'Echo failed' })
90
94
  }
91
95
  },
92
96
 
@@ -103,6 +107,7 @@ export const useChatWsStore = create<WSState>((set, get) => ({
103
107
  }))
104
108
  } catch (error) {
105
109
  console.error('Ping failed:', error)
110
+ set({ error: error instanceof Error ? error.message : 'Ping failed' })
106
111
  }
107
112
  },
108
113
 
@@ -119,4 +124,8 @@ export const useChatWsStore = create<WSState>((set, get) => ({
119
124
  clearMessages: () => {
120
125
  set({ messages: [] })
121
126
  },
127
+
128
+ clearError: () => {
129
+ set({ error: null })
130
+ },
122
131
  }))
@@ -91,6 +91,7 @@ export const useNotificationStore = create<NotificationState>(set => ({
91
91
  }
92
92
  } catch (error) {
93
93
  console.error('Failed to mark as read:', error)
94
+ set({ error: error instanceof Error ? error.message : 'Unknown error' })
94
95
  }
95
96
  },
96
97
 
@@ -106,6 +107,7 @@ export const useNotificationStore = create<NotificationState>(set => ({
106
107
  }
107
108
  } catch (error) {
108
109
  console.error('Failed to mark all as read:', error)
110
+ set({ error: error instanceof Error ? error.message : 'Unknown error' })
109
111
  }
110
112
  },
111
113
 
@@ -127,6 +129,7 @@ export const useNotificationStore = create<NotificationState>(set => ({
127
129
  }
128
130
  } catch (error) {
129
131
  console.error('Failed to delete:', error)
132
+ set({ error: error instanceof Error ? error.message : 'Unknown error' })
130
133
  }
131
134
  },
132
135
 
@@ -139,6 +142,7 @@ export const useNotificationStore = create<NotificationState>(set => ({
139
142
  }
140
143
  } catch (error) {
141
144
  console.error('Failed to fetch unread count:', error)
145
+ set({ error: error instanceof Error ? error.message : 'Unknown error' })
142
146
  }
143
147
  },
144
148