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.
- package/dist/cli/index.js +1673 -696
- package/dist/cli/index.js.map +1 -1
- package/package.json +16 -10
- package/template/.husky/pre-commit +1 -1
- package/template/modules.config.ts +29 -2
- package/template/package.json +12 -12
- package/template/patches/{typescript+5.8.3.patch → typescript+5.9.3.patch} +2 -4
- package/template/playwright.config.ts +7 -1
- package/template/src/admin/App.tsx +2 -2
- package/template/src/admin/components/CaptchaModal.tsx +7 -9
- package/template/src/admin/layouts/Header.tsx +56 -22
- package/template/src/admin/layouts/Layout.tsx +14 -9
- package/template/src/admin/layouts/Sidebar.tsx +122 -105
- package/template/src/admin/pages/CategoryManagementPage.tsx +241 -0
- package/template/src/admin/pages/ContentPage.tsx +2 -0
- package/template/src/admin/pages/DashboardPage.tsx +2 -0
- package/template/src/admin/pages/DisputesPage.tsx +2 -0
- package/template/src/admin/pages/MediaTestPage.tsx +1 -1
- package/template/src/admin/pages/OrdersPage.tsx +2 -0
- package/template/src/admin/pages/PluginDashboardPage.tsx +297 -0
- package/template/src/admin/pages/PluginManagementPage.tsx +340 -0
- package/template/src/admin/pages/PluginReviewPage.tsx +255 -0
- package/template/src/admin/pages/SystemLogsPage.tsx +11 -2
- package/template/src/admin/pages/TicketsPage.tsx +2 -0
- package/template/src/admin/pages/UsersPage.tsx +3 -2
- package/template/src/admin/stores/adminStore.ts +5 -0
- package/template/src/cli/index.ts +17 -19
- package/template/src/cli/modules/auth/index.ts +65 -0
- package/template/src/cli/modules/config/index.ts +74 -77
- package/template/src/cli/modules/index.ts +28 -6
- package/template/src/cli/modules/notification/index.ts +95 -79
- package/template/src/cli/modules/plugin/index.ts +111 -0
- package/template/src/cli/modules/todo/index.ts +99 -51
- package/template/src/cli/utils/auto-command.ts +7 -25
- package/template/src/cli/utils/index.ts +3 -1
- package/template/src/client/App.tsx +36 -16
- package/template/src/client/Layout.tsx +67 -10
- package/template/src/client/components/AuthButton.tsx +25 -18
- package/template/src/client/components/BottomTabBar.tsx +107 -0
- package/template/src/client/components/Navigation.tsx +162 -52
- package/template/src/client/components/__tests__/App.test.tsx +48 -32
- package/template/src/client/components/__tests__/AuthButton.test.tsx +78 -77
- package/template/src/client/components/__tests__/Navigation.test.tsx +31 -20
- package/template/src/client/components/index.ts +1 -0
- package/template/src/client/contexts/PresetContext.tsx +10 -0
- package/template/src/client/main.tsx +63 -8
- package/template/src/client/pages/CartPage.tsx +244 -0
- package/template/src/client/pages/CategoriesPage.tsx +100 -0
- package/template/src/client/pages/ContentDetailPage.tsx +9 -14
- package/template/src/client/pages/ContentListPage.tsx +4 -11
- package/template/src/client/pages/DashboardPage.tsx +261 -0
- package/template/src/client/pages/DeveloperDashboardPage.tsx +211 -0
- package/template/src/client/pages/LoginPage.tsx +127 -0
- package/template/src/client/pages/OrdersPage.tsx +196 -0
- package/template/src/client/pages/PluginDetailPage.tsx +345 -0
- package/template/src/client/pages/PluginsPage.tsx +223 -0
- package/template/src/client/pages/ProfilePage.tsx +206 -0
- package/template/src/client/pages/PublishPage.tsx +336 -0
- package/template/src/client/pages/RegisterPage.tsx +136 -0
- package/template/src/client/pages/SearchPage.tsx +204 -0
- package/template/src/client/pages/SettingsPage.tsx +220 -0
- package/template/src/client/pages/TopicsPage.tsx +180 -0
- package/template/src/client/pages/__tests__/LoginPage.test.tsx +170 -0
- package/template/src/client/pages/__tests__/RegisterPage.test.tsx +168 -0
- package/template/src/client/preset-ui-config.ts +492 -0
- package/template/src/client/services/apiClient.ts +14 -4
- package/template/src/client/stores/__tests__/authStore.test.ts +293 -38
- package/template/src/client/stores/__tests__/todoStore.test.ts +7 -17
- package/template/src/client/stores/authStore.ts +58 -5
- package/template/src/client/stores/chatWSStore.ts +9 -0
- package/template/src/client/stores/notificationStore.ts +4 -0
- package/template/src/client/stores/pluginStore.ts +279 -0
- package/template/src/client/stores/todoStore.ts +2 -6
- package/template/src/server/core/__tests__/isr-cache.test.ts +117 -0
- package/template/src/server/core/__tests__/isr-invalidation.test.ts +72 -0
- package/template/src/server/core/__tests__/ssr-renderer.test.ts +89 -0
- package/template/src/server/core/isr-cache.ts +239 -0
- package/template/src/server/core/isr-invalidation.ts +45 -0
- package/template/src/server/core/module-loader.ts +14 -7
- package/template/src/server/core/ssr-renderer.ts +240 -0
- package/template/src/server/db/init.ts +257 -10
- package/template/src/server/db/schema/developers.ts +20 -0
- package/template/src/server/db/schema/index.ts +2 -0
- package/template/src/server/db/schema/plugins.ts +114 -0
- package/template/src/server/db/test-setup.ts +91 -0
- package/template/src/server/entries/cloudflare.ts +79 -7
- package/template/src/server/entries/node.ts +48 -5
- package/template/src/server/middleware/__tests__/captcha.test.ts +23 -13
- package/template/src/server/middleware/auth.ts +8 -1
- package/template/src/server/middleware/captcha.ts +14 -4
- package/template/src/server/middleware/rate-limit.ts +7 -2
- package/template/src/server/module-admin/module.ts +9 -5
- package/template/src/server/module-admin/routes/admin-notification-routes.ts +43 -14
- package/template/src/server/module-admin/routes/admin-routes.ts +0 -2
- package/template/src/server/module-admin/routes/client-auth-routes.ts +90 -0
- package/template/src/server/module-admin/routes/dashboard-routes.ts +79 -0
- package/template/src/server/module-admin/services/admin-service.ts +68 -11
- package/template/src/server/module-auth/__tests__/auth-service.test.ts +239 -0
- package/template/src/server/module-auth/index.ts +7 -0
- package/template/src/server/module-auth/module.ts +40 -0
- package/template/src/server/module-auth/routes/auth-routes.ts +94 -0
- package/template/src/server/module-auth/routes/profile-routes.ts +31 -0
- package/template/src/server/module-auth/services/auth-service.ts +100 -0
- package/template/src/server/module-content/module.ts +10 -4
- package/template/src/server/module-content/routes/public-content-routes.ts +2 -2
- package/template/src/server/module-content/routes/topics-routes.ts +205 -0
- package/template/src/server/module-content/services/content-service.ts +18 -2
- package/template/src/server/module-dispute/routes/dispute-routes.ts +2 -1
- package/template/src/server/module-dispute/services/dispute-service.ts +1 -1
- package/template/src/server/module-notifications/__tests__/sse-rpc.test.ts +14 -14
- package/template/src/server/module-notifications/routes/notification-routes.ts +34 -8
- package/template/src/server/module-order/__tests__/order-route.test.ts +22 -2
- package/template/src/server/module-order/module.ts +15 -0
- package/template/src/server/module-order/routes/cart-routes.ts +103 -0
- package/template/src/server/module-order/routes/orders-mock-routes.ts +67 -0
- package/template/src/server/module-order/services/order-service.ts +1 -1
- package/template/src/server/module-plugin/__tests__/plugin-query-service.test.ts +203 -0
- package/template/src/server/module-plugin/__tests__/plugin-service.test.ts +234 -0
- package/template/src/server/module-plugin/index.ts +2 -0
- package/template/src/server/module-plugin/module.ts +52 -0
- package/template/src/server/module-plugin/routes/plugin-admin-routes.ts +261 -0
- package/template/src/server/module-plugin/routes/plugin-routes.ts +354 -0
- package/template/src/server/module-plugin/services/admin-category-service.ts +99 -0
- package/template/src/server/module-plugin/services/admin-plugin-service.ts +170 -0
- package/template/src/server/module-plugin/services/admin-stats-service.ts +41 -0
- package/template/src/server/module-plugin/services/plugin-query-service.ts +360 -0
- package/template/src/server/module-plugin/services/plugin-review-service.ts +95 -0
- package/template/src/server/module-plugin/services/plugin-service.ts +163 -0
- package/template/src/server/module-ticket/services/ticket-service.ts +1 -1
- package/template/src/server/module-todos/routes/todos-routes.ts +0 -4
- package/template/src/server/route-registry.ts +16 -1
- package/template/src/server/test-utils/test-client.ts +1 -2
- package/template/src/server/utils/auth.ts +6 -0
- package/template/src/server/utils/json.ts +13 -0
- package/template/src/shared/core/module-manifest.ts +3 -6
- package/template/src/shared/modules/auth/index.ts +12 -0
- package/template/src/shared/modules/auth/schemas.ts +50 -0
- package/template/src/shared/modules/cart/index.ts +1 -0
- package/template/src/shared/modules/cart/schemas.ts +41 -0
- package/template/src/shared/modules/community/index.ts +1 -0
- package/template/src/shared/modules/community/schemas.ts +58 -0
- package/template/src/shared/modules/dashboard/index.ts +1 -0
- package/template/src/shared/modules/dashboard/schemas.ts +35 -0
- package/template/src/shared/modules/index.ts +41 -0
- package/template/src/shared/modules/order/schemas.ts +29 -0
- package/template/src/shared/modules/plugins/index.ts +48 -0
- package/template/src/shared/modules/plugins/schemas.ts +227 -0
- package/template/src/shared/schemas/index.ts +130 -0
- package/template/tests/e2e/todo.spec.ts +23 -18
- package/template/tests/e2e/visual-screenshots.spec.ts +1824 -0
- package/template/uploads/.gitkeep +0 -0
- package/template/vite.config.ts +2 -1
- package/template/vitest.setup.ts +11 -0
- package/template/wrangler.toml +4 -3
- 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
|
-
|
|
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
|
|
150
|
+
let localStore: ReturnType<typeof createTestStore>
|
|
31
151
|
|
|
32
152
|
beforeEach(() => {
|
|
33
153
|
vi.clearAllMocks()
|
|
34
|
-
|
|
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(
|
|
40
|
-
expect(
|
|
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(
|
|
45
|
-
expect(
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
63
|
-
|
|
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 =
|
|
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
|
-
|
|
72
|
-
expect(
|
|
73
|
-
expect(
|
|
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
|
-
|
|
198
|
+
localStore.getState().setToken('new-token')
|
|
80
199
|
|
|
81
|
-
const state =
|
|
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
|
-
|
|
88
|
-
|
|
206
|
+
localStore.getState().setAuth('old', { id: '1', username: 'user', role: 'user' })
|
|
207
|
+
localStore.getState().setToken('refreshed-token')
|
|
89
208
|
|
|
90
|
-
const state =
|
|
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
|
-
|
|
99
|
-
|
|
217
|
+
localStore.getState().setAuth('token', { id: '1', username: 'user', role: 'user' })
|
|
218
|
+
localStore.getState().logout()
|
|
100
219
|
|
|
101
|
-
const state =
|
|
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
|
-
|
|
227
|
+
localStore.getState().logout()
|
|
109
228
|
|
|
110
|
-
const state =
|
|
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(
|
|
238
|
+
expect(localStore.getState().isAuthenticated).toBe(false)
|
|
120
239
|
|
|
121
|
-
|
|
122
|
-
expect(
|
|
123
|
-
expect(
|
|
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
|
-
|
|
126
|
-
expect(
|
|
127
|
-
expect(
|
|
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
|
|
131
|
-
|
|
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().
|
|
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
|
|
438
|
-
const
|
|
439
|
-
mockJson.mockResolvedValue({ success: true, data:
|
|
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(
|
|
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
|
-
|
|
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
|
|