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
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm'
|
|
2
|
+
import type { Plugin, CreatePluginInput } from '@shared/schemas'
|
|
3
|
+
import { getDb } from '@server/db'
|
|
4
|
+
import { plugins, type PluginTable } from '@server/db/schema'
|
|
5
|
+
import { generateUUID } from '@server/utils/uuid'
|
|
6
|
+
import { NotFoundError, AuthorizationError, ConflictError } from '@server/utils/app-error'
|
|
7
|
+
import { parseJsonField, serializeJsonField } from '@server/utils/json'
|
|
8
|
+
|
|
9
|
+
function mapRow(row: PluginTable): Plugin {
|
|
10
|
+
return {
|
|
11
|
+
id: row.id,
|
|
12
|
+
name: row.name,
|
|
13
|
+
slug: row.slug,
|
|
14
|
+
description: row.description ?? '',
|
|
15
|
+
readme: row.readme ?? undefined,
|
|
16
|
+
authorId: row.authorId,
|
|
17
|
+
authorName: row.authorName,
|
|
18
|
+
repositoryUrl: row.repositoryUrl ?? undefined,
|
|
19
|
+
homepageUrl: row.homepageUrl ?? undefined,
|
|
20
|
+
npmPackage: row.npmPackage ?? undefined,
|
|
21
|
+
license: row.license ?? undefined,
|
|
22
|
+
version: row.version,
|
|
23
|
+
status: row.status as Plugin['status'],
|
|
24
|
+
downloadCount: row.downloadCount,
|
|
25
|
+
viewCount: row.viewCount,
|
|
26
|
+
featured: row.featured,
|
|
27
|
+
screenshotUrl: row.screenshotUrl ?? undefined,
|
|
28
|
+
siteUrls: parseJsonField<string[]>(row.siteUrls),
|
|
29
|
+
tags: parseJsonField<string[]>(row.tags),
|
|
30
|
+
commands: parseJsonField<Array<{ name: string; description?: string }>>(row.commands),
|
|
31
|
+
rejectReason: row.rejectReason ?? undefined,
|
|
32
|
+
createdAt: row.createdAt.getTime(),
|
|
33
|
+
updatedAt: row.updatedAt.getTime(),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CreatePluginData extends CreatePluginInput {
|
|
38
|
+
authorId: string
|
|
39
|
+
authorName: string
|
|
40
|
+
screenshotUrl?: string
|
|
41
|
+
readme?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function createPlugin(data: CreatePluginData): Promise<Plugin> {
|
|
45
|
+
const db = await getDb()
|
|
46
|
+
|
|
47
|
+
const existing = await db.select().from(plugins).where(eq(plugins.slug, data.slug))
|
|
48
|
+
if (existing.length > 0) {
|
|
49
|
+
throw new ConflictError(`Plugin with slug '${data.slug}' already exists`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const id = generateUUID()
|
|
53
|
+
const now = new Date()
|
|
54
|
+
const result = await db
|
|
55
|
+
.insert(plugins)
|
|
56
|
+
.values({
|
|
57
|
+
id,
|
|
58
|
+
name: data.name,
|
|
59
|
+
slug: data.slug,
|
|
60
|
+
description: data.description,
|
|
61
|
+
readme: data.readme ?? null,
|
|
62
|
+
authorId: data.authorId,
|
|
63
|
+
authorName: data.authorName,
|
|
64
|
+
repositoryUrl: data.repositoryUrl ?? null,
|
|
65
|
+
homepageUrl: data.homepageUrl ?? null,
|
|
66
|
+
npmPackage: data.npmPackage ?? null,
|
|
67
|
+
license: data.license ?? null,
|
|
68
|
+
version: '0.0.1',
|
|
69
|
+
status: 'pending',
|
|
70
|
+
screenshotUrl: data.screenshotUrl ?? null,
|
|
71
|
+
siteUrls: serializeJsonField(data.siteUrls),
|
|
72
|
+
tags: serializeJsonField(data.tags),
|
|
73
|
+
commands: serializeJsonField(data.commands),
|
|
74
|
+
createdAt: now,
|
|
75
|
+
updatedAt: now,
|
|
76
|
+
})
|
|
77
|
+
.returning()
|
|
78
|
+
|
|
79
|
+
return mapRow(result[0])
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface UpdatePluginData {
|
|
83
|
+
name?: string
|
|
84
|
+
description?: string
|
|
85
|
+
readme?: string | null
|
|
86
|
+
repositoryUrl?: string | null
|
|
87
|
+
homepageUrl?: string | null
|
|
88
|
+
npmPackage?: string | null
|
|
89
|
+
license?: string | null
|
|
90
|
+
screenshotUrl?: string | null
|
|
91
|
+
siteUrls?: string[] | null
|
|
92
|
+
tags?: string[] | null
|
|
93
|
+
commands?: Array<{ name: string; description?: string | null }> | null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function updatePlugin(
|
|
97
|
+
slug: string,
|
|
98
|
+
data: UpdatePluginData,
|
|
99
|
+
userId: string
|
|
100
|
+
): Promise<Plugin> {
|
|
101
|
+
const db = await getDb()
|
|
102
|
+
|
|
103
|
+
const rows = await db.select().from(plugins).where(eq(plugins.slug, slug))
|
|
104
|
+
if (rows.length === 0) {
|
|
105
|
+
throw new NotFoundError('Plugin', slug)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const existing = rows[0]
|
|
109
|
+
if (existing.authorId !== userId) {
|
|
110
|
+
throw new AuthorizationError('Only the plugin author can update this plugin')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const updateData: Partial<PluginTable> = {
|
|
114
|
+
updatedAt: new Date(),
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (data.name !== undefined) updateData.name = data.name
|
|
118
|
+
if (data.description !== undefined) updateData.description = data.description
|
|
119
|
+
if (data.readme !== undefined) updateData.readme = data.readme
|
|
120
|
+
if (data.repositoryUrl !== undefined) updateData.repositoryUrl = data.repositoryUrl
|
|
121
|
+
if (data.homepageUrl !== undefined) updateData.homepageUrl = data.homepageUrl
|
|
122
|
+
if (data.npmPackage !== undefined) updateData.npmPackage = data.npmPackage
|
|
123
|
+
if (data.license !== undefined) updateData.license = data.license
|
|
124
|
+
if (data.screenshotUrl !== undefined) updateData.screenshotUrl = data.screenshotUrl
|
|
125
|
+
if (data.siteUrls !== undefined) updateData.siteUrls = serializeJsonField(data.siteUrls)
|
|
126
|
+
if (data.tags !== undefined) updateData.tags = serializeJsonField(data.tags)
|
|
127
|
+
if (data.commands !== undefined) updateData.commands = serializeJsonField(data.commands)
|
|
128
|
+
|
|
129
|
+
const result = await db.update(plugins).set(updateData).where(eq(plugins.slug, slug)).returning()
|
|
130
|
+
return mapRow(result[0])
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function deletePlugin(slug: string, userId: string): Promise<void> {
|
|
134
|
+
const db = await getDb()
|
|
135
|
+
|
|
136
|
+
const rows = await db.select().from(plugins).where(eq(plugins.slug, slug))
|
|
137
|
+
if (rows.length === 0) {
|
|
138
|
+
throw new NotFoundError('Plugin', slug)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const existing = rows[0]
|
|
142
|
+
if (existing.authorId !== userId) {
|
|
143
|
+
throw new AuthorizationError('Only the plugin author can delete this plugin')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await db.delete(plugins).where(eq(plugins.slug, slug))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function trackInstall(slug: string): Promise<void> {
|
|
150
|
+
const db = await getDb()
|
|
151
|
+
|
|
152
|
+
const rows = await db.select().from(plugins).where(eq(plugins.slug, slug))
|
|
153
|
+
if (rows.length === 0) {
|
|
154
|
+
throw new NotFoundError('Plugin', slug)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
await db
|
|
158
|
+
.update(plugins)
|
|
159
|
+
.set({ downloadCount: rows[0].downloadCount + 1, updatedAt: new Date() })
|
|
160
|
+
.where(eq(plugins.slug, slug))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export { mapRow, parseJsonField, serializeJsonField }
|
|
@@ -68,7 +68,7 @@ function ticketWithoutReplies(row: TicketTable): Ticket {
|
|
|
68
68
|
|
|
69
69
|
export async function seedTicketsIfEmpty(): Promise<void> {
|
|
70
70
|
const db = await getDb()
|
|
71
|
-
const existing = await db.select().from(tickets)
|
|
71
|
+
const existing = await db.select().from(tickets)
|
|
72
72
|
if (existing.length === 0) {
|
|
73
73
|
const PRIORITIES: TicketPriority[] = ['low', 'medium', 'high', 'urgent']
|
|
74
74
|
const STATUSES: TicketStatus[] = [
|
|
@@ -23,8 +23,6 @@ const listRoute = createRoute({
|
|
|
23
23
|
method: 'get',
|
|
24
24
|
path: '/todos',
|
|
25
25
|
tags: ['todos'],
|
|
26
|
-
security: [{ Bearer: [] }],
|
|
27
|
-
middleware: [authMiddleware()],
|
|
28
26
|
responses: {
|
|
29
27
|
200: successResponse(TodoListSchema, 'List all todos'),
|
|
30
28
|
500: errorResponse('Internal server error'),
|
|
@@ -35,8 +33,6 @@ const getRoute = createRoute({
|
|
|
35
33
|
method: 'get',
|
|
36
34
|
path: '/todos/{id}',
|
|
37
35
|
tags: ['todos'],
|
|
38
|
-
security: [{ Bearer: [] }],
|
|
39
|
-
middleware: [authMiddleware()],
|
|
40
36
|
request: {
|
|
41
37
|
params: z.object({ id: z.coerce.number().int().positive() }),
|
|
42
38
|
},
|
|
@@ -7,6 +7,7 @@ import { auditLogRoutes } from './module-permission/routes/audit-log-routes'
|
|
|
7
7
|
import { notificationRoutes } from './module-notifications/routes/notification-routes'
|
|
8
8
|
import { chatRoutes } from './module-chat/routes/chat-routes'
|
|
9
9
|
import { adminRoutes } from './module-admin/routes/admin-routes'
|
|
10
|
+
import { clientAuthRoutes } from './module-admin/routes/client-auth-routes'
|
|
10
11
|
import { captchaRoutes } from './module-captcha/routes/captcha-routes'
|
|
11
12
|
import { orderRoutes } from './module-order/routes/order-routes'
|
|
12
13
|
import { ticketRoutes } from './module-ticket/routes/ticket-routes'
|
|
@@ -14,6 +15,13 @@ import { disputeRoutes } from './module-dispute/routes/dispute-routes'
|
|
|
14
15
|
import { contentRoutes } from './module-content/routes/content-routes'
|
|
15
16
|
import { publicContentRoutes } from './module-content/routes/public-content-routes'
|
|
16
17
|
import { fileRoutes } from './module-file/routes/file-routes'
|
|
18
|
+
import { authRoutes } from './module-auth/routes/auth-routes'
|
|
19
|
+
import { pluginRoutes } from './module-plugin/routes/plugin-routes'
|
|
20
|
+
import { pluginAdminRoutes } from './module-plugin/routes/plugin-admin-routes'
|
|
21
|
+
import { dashboardRoutes } from './module-admin/routes/dashboard-routes'
|
|
22
|
+
import { cartRoutes } from './module-order/routes/cart-routes'
|
|
23
|
+
import { ordersMockRoutes } from './module-order/routes/orders-mock-routes'
|
|
24
|
+
import { topicsRoutes } from './module-content/routes/topics-routes'
|
|
17
25
|
|
|
18
26
|
const apiRateLimit = rateLimitMiddleware({
|
|
19
27
|
windowMs: 60_000,
|
|
@@ -21,13 +29,18 @@ const apiRateLimit = rateLimitMiddleware({
|
|
|
21
29
|
message: 'Too many requests',
|
|
22
30
|
})
|
|
23
31
|
|
|
24
|
-
// 客户端路由 - 普通用户使用的 API
|
|
25
32
|
export const clientApiRoutes = new OpenAPIHono()
|
|
26
33
|
.use('*', apiRateLimit)
|
|
34
|
+
.route('/api', clientAuthRoutes)
|
|
35
|
+
.route('/api', authRoutes)
|
|
27
36
|
.route('/api', chatRoutes)
|
|
28
37
|
.route('/api', notificationRoutes)
|
|
29
38
|
.route('/api', apiRoutes)
|
|
39
|
+
.route('/api', pluginRoutes)
|
|
30
40
|
.route('/api', publicContentRoutes)
|
|
41
|
+
.route('/api', cartRoutes)
|
|
42
|
+
.route('/api', ordersMockRoutes)
|
|
43
|
+
.route('/api', topicsRoutes)
|
|
31
44
|
|
|
32
45
|
// 管理后台路由 - 普通用户使用的 API + 管理功能
|
|
33
46
|
export const adminApiRoutes = new OpenAPIHono()
|
|
@@ -41,6 +54,8 @@ export const adminApiRoutes = new OpenAPIHono()
|
|
|
41
54
|
.route('/api', roleRoutes)
|
|
42
55
|
.route('/api', auditLogRoutes)
|
|
43
56
|
.route('/api', adminRoutes)
|
|
57
|
+
.route('/api', pluginAdminRoutes)
|
|
58
|
+
.route('/api', dashboardRoutes)
|
|
44
59
|
|
|
45
60
|
// 导出类型
|
|
46
61
|
export type ClientApiRoutes = typeof clientApiRoutes
|
|
@@ -51,8 +51,7 @@ export function createTestClient(baseUrl?: string, options?: TestClientOptions)
|
|
|
51
51
|
})
|
|
52
52
|
}
|
|
53
53
|
return hc<AppType>('http://localhost', {
|
|
54
|
-
|
|
55
|
-
fetch: (input: any, init?: any) => {
|
|
54
|
+
fetch: (input: RequestInfo | URL, init?: RequestInit) => {
|
|
56
55
|
const request = new Request(input, init)
|
|
57
56
|
Object.entries(defaultHeaders).forEach(([key, value]) => {
|
|
58
57
|
if (!request.headers.has(key)) {
|
|
@@ -42,6 +42,8 @@ const mockTokens: Map<string, string> = new Map([
|
|
|
42
42
|
['user-token', '3'],
|
|
43
43
|
])
|
|
44
44
|
|
|
45
|
+
const userPasswordHashes: Map<string, string> = new Map()
|
|
46
|
+
|
|
45
47
|
export function getAuthUser(c: Context): AuthUser {
|
|
46
48
|
return c.get('authUser')
|
|
47
49
|
}
|
|
@@ -63,3 +65,7 @@ export function getMockUsers(): User[] {
|
|
|
63
65
|
export function getMockTokens(): Map<string, string> {
|
|
64
66
|
return mockTokens
|
|
65
67
|
}
|
|
68
|
+
|
|
69
|
+
export function getUserPasswordHashes(): Map<string, string> {
|
|
70
|
+
return userPasswordHashes
|
|
71
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function parseJsonField<T>(field: string | null | undefined): T | undefined {
|
|
2
|
+
if (!field) return undefined
|
|
3
|
+
try {
|
|
4
|
+
return JSON.parse(field) as T
|
|
5
|
+
} catch {
|
|
6
|
+
return undefined
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function serializeJsonField<T>(value: T | undefined | null): string | null {
|
|
11
|
+
if (value === undefined || value === null) return null
|
|
12
|
+
return JSON.stringify(value)
|
|
13
|
+
}
|
|
@@ -28,12 +28,9 @@ export interface ModuleManifest {
|
|
|
28
28
|
/** Route registration info */
|
|
29
29
|
routes: {
|
|
30
30
|
/** Routes mounted under client API (/api) */
|
|
31
|
-
client?:
|
|
32
|
-
|
|
33
|
-
importPath: string
|
|
34
|
-
/** Exported variable name from the route file */
|
|
35
|
-
exportName: string
|
|
36
|
-
}
|
|
31
|
+
client?:
|
|
32
|
+
| { importPath: string; exportName: string }
|
|
33
|
+
| { importPath: string; exportName: string }[]
|
|
37
34
|
/** Routes mounted under admin API (/api/admin) */
|
|
38
35
|
admin?: {
|
|
39
36
|
importPath: string
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from '@hono/zod-openapi'
|
|
2
|
+
|
|
3
|
+
export const DeveloperProfileSchema = z.object({
|
|
4
|
+
id: z.string(),
|
|
5
|
+
username: z.string(),
|
|
6
|
+
email: z.string().email(),
|
|
7
|
+
role: z.string(),
|
|
8
|
+
createdAt: z.string().datetime(),
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export const LoginSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
account: z.string().nullish(),
|
|
14
|
+
email: z.string().email().nullish(),
|
|
15
|
+
password: z.string().min(6),
|
|
16
|
+
})
|
|
17
|
+
.refine(data => data.account || data.email, {
|
|
18
|
+
message: 'Either account or email is required',
|
|
19
|
+
path: ['account'],
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
export const RegisterSchema = z.object({
|
|
23
|
+
username: z.string().min(3).max(50),
|
|
24
|
+
email: z.string().email(),
|
|
25
|
+
password: z.string().min(6).max(100),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export const TokenResponseSchema = z.object({
|
|
29
|
+
token: z.string(),
|
|
30
|
+
profile: DeveloperProfileSchema,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export const ProfileSchema = z.object({
|
|
34
|
+
id: z.string(),
|
|
35
|
+
username: z.string(),
|
|
36
|
+
email: z.string().email(),
|
|
37
|
+
bio: z.string().optional().nullable(),
|
|
38
|
+
joinDate: z.string(),
|
|
39
|
+
stats: z.object({
|
|
40
|
+
posts: z.number(),
|
|
41
|
+
followers: z.number(),
|
|
42
|
+
following: z.number(),
|
|
43
|
+
}),
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
export type DeveloperProfile = z.infer<typeof DeveloperProfileSchema>
|
|
47
|
+
export type LoginInput = z.infer<typeof LoginSchema>
|
|
48
|
+
export type RegisterInput = z.infer<typeof RegisterSchema>
|
|
49
|
+
export type TokenResponse = z.infer<typeof TokenResponseSchema>
|
|
50
|
+
export type Profile = z.infer<typeof ProfileSchema>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './schemas'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from '@hono/zod-openapi'
|
|
2
|
+
|
|
3
|
+
export const CartItemSchema = z.object({
|
|
4
|
+
id: z.number(),
|
|
5
|
+
name: z.string(),
|
|
6
|
+
variant: z.string(),
|
|
7
|
+
price: z.number(),
|
|
8
|
+
quantity: z.number(),
|
|
9
|
+
color: z.string(),
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
export const CartSummarySchema = z.object({
|
|
13
|
+
subtotal: z.number(),
|
|
14
|
+
shipping: z.number(),
|
|
15
|
+
tax: z.number(),
|
|
16
|
+
total: z.number(),
|
|
17
|
+
totalItems: z.number(),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export const CartResponseSchema = z.object({
|
|
21
|
+
items: z.array(CartItemSchema),
|
|
22
|
+
summary: CartSummarySchema,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
export const AddCartItemSchema = z.object({
|
|
26
|
+
id: z.number(),
|
|
27
|
+
name: z.string(),
|
|
28
|
+
variant: z.string(),
|
|
29
|
+
price: z.number(),
|
|
30
|
+
quantity: z.number().min(1).default(1),
|
|
31
|
+
color: z.string(),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export const CartItemIdSchema = z.object({
|
|
35
|
+
id: z.string(),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
export type CartItem = z.infer<typeof CartItemSchema>
|
|
39
|
+
export type CartSummary = z.infer<typeof CartSummarySchema>
|
|
40
|
+
export type CartResponse = z.infer<typeof CartResponseSchema>
|
|
41
|
+
export type AddCartItemInput = z.infer<typeof AddCartItemSchema>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './schemas'
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { z } from '@hono/zod-openapi'
|
|
2
|
+
|
|
3
|
+
export const TopicStatusSchema = z.enum(['hot', 'unanswered', 'solved'])
|
|
4
|
+
|
|
5
|
+
export const TopicTagSchema = z.object({
|
|
6
|
+
label: z.string(),
|
|
7
|
+
color: z.string(),
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export const TopicAuthorSchema = z.object({
|
|
11
|
+
name: z.string(),
|
|
12
|
+
initials: z.string(),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export const TopicSchema = z.object({
|
|
16
|
+
id: z.string(),
|
|
17
|
+
title: z.string(),
|
|
18
|
+
excerpt: z.string(),
|
|
19
|
+
votes: z.number(),
|
|
20
|
+
replyCount: z.number(),
|
|
21
|
+
viewCount: z.number(),
|
|
22
|
+
status: TopicStatusSchema,
|
|
23
|
+
tags: z.array(TopicTagSchema),
|
|
24
|
+
author: TopicAuthorSchema,
|
|
25
|
+
createdAt: z.string(),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export const TopicsResponseSchema = z.array(TopicSchema)
|
|
29
|
+
|
|
30
|
+
export const ProfileStatsSchema = z.object({
|
|
31
|
+
topics: z.number(),
|
|
32
|
+
replies: z.number(),
|
|
33
|
+
likes: z.number(),
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
export const ActivityTypeSchema = z.enum(['topic', 'reply', 'like'])
|
|
37
|
+
|
|
38
|
+
export const ProfileActivitySchema = z.object({
|
|
39
|
+
id: z.string(),
|
|
40
|
+
type: ActivityTypeSchema,
|
|
41
|
+
text: z.string(),
|
|
42
|
+
target: z.string(),
|
|
43
|
+
time: z.string(),
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
export const ProfileResponseSchema = z.object({
|
|
47
|
+
stats: ProfileStatsSchema,
|
|
48
|
+
activity: z.array(ProfileActivitySchema),
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
export type TopicStatus = z.infer<typeof TopicStatusSchema>
|
|
52
|
+
export type TopicTag = z.infer<typeof TopicTagSchema>
|
|
53
|
+
export type TopicAuthor = z.infer<typeof TopicAuthorSchema>
|
|
54
|
+
export type Topic = z.infer<typeof TopicSchema>
|
|
55
|
+
export type ProfileStats = z.infer<typeof ProfileStatsSchema>
|
|
56
|
+
export type ActivityType = z.infer<typeof ActivityTypeSchema>
|
|
57
|
+
export type ProfileActivity = z.infer<typeof ProfileActivitySchema>
|
|
58
|
+
export type ProfileResponse = z.infer<typeof ProfileResponseSchema>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './schemas'
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from '@hono/zod-openapi'
|
|
2
|
+
|
|
3
|
+
export const DashboardStatSchema = z.object({
|
|
4
|
+
label: z.string(),
|
|
5
|
+
value: z.string(),
|
|
6
|
+
trend: z.number(),
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
export const RevenueDataSchema = z.object({
|
|
10
|
+
month: z.string(),
|
|
11
|
+
value: z.number(),
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export const ActivityStatusSchema = z.enum(['Active', 'Pending', 'Inactive'])
|
|
15
|
+
|
|
16
|
+
export const ActivitySchema = z.object({
|
|
17
|
+
id: z.number(),
|
|
18
|
+
user: z.string(),
|
|
19
|
+
action: z.string(),
|
|
20
|
+
date: z.string(),
|
|
21
|
+
status: ActivityStatusSchema,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export const DashboardResponseSchema = z.object({
|
|
25
|
+
stats: z.array(DashboardStatSchema),
|
|
26
|
+
revenue: z.array(RevenueDataSchema),
|
|
27
|
+
userGrowth: z.array(RevenueDataSchema),
|
|
28
|
+
activity: z.array(ActivitySchema),
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
export type DashboardStat = z.infer<typeof DashboardStatSchema>
|
|
32
|
+
export type RevenueData = z.infer<typeof RevenueDataSchema>
|
|
33
|
+
export type ActivityStatus = z.infer<typeof ActivityStatusSchema>
|
|
34
|
+
export type Activity = z.infer<typeof ActivitySchema>
|
|
35
|
+
export type DashboardResponse = z.infer<typeof DashboardResponseSchema>
|
|
@@ -68,3 +68,44 @@ export {
|
|
|
68
68
|
type PermissionInfo,
|
|
69
69
|
type UserPermissions,
|
|
70
70
|
} from './permission'
|
|
71
|
+
export {
|
|
72
|
+
DeveloperProfileSchema,
|
|
73
|
+
LoginSchema,
|
|
74
|
+
RegisterSchema,
|
|
75
|
+
TokenResponseSchema,
|
|
76
|
+
type DeveloperProfile,
|
|
77
|
+
type LoginInput,
|
|
78
|
+
type RegisterInput,
|
|
79
|
+
type TokenResponse,
|
|
80
|
+
} from './auth'
|
|
81
|
+
export {
|
|
82
|
+
PluginSchema,
|
|
83
|
+
PluginStatusSchema,
|
|
84
|
+
CreatePluginSchema,
|
|
85
|
+
UpdatePluginSchema,
|
|
86
|
+
PluginVersionStatusSchema,
|
|
87
|
+
VersionSchema,
|
|
88
|
+
CategorySchema,
|
|
89
|
+
ReviewSchema,
|
|
90
|
+
CreateReviewSchema,
|
|
91
|
+
MarketplaceStatsSchema,
|
|
92
|
+
PluginListResponseSchema,
|
|
93
|
+
AdminPluginSchema,
|
|
94
|
+
AdminDashboardStatsSchema,
|
|
95
|
+
PluginListQuerySchema,
|
|
96
|
+
PluginSlugSchema,
|
|
97
|
+
type Plugin,
|
|
98
|
+
type PluginStatus,
|
|
99
|
+
type CreatePluginInput,
|
|
100
|
+
type UpdatePluginInput,
|
|
101
|
+
type PluginVersionStatus,
|
|
102
|
+
type Version,
|
|
103
|
+
type Category,
|
|
104
|
+
type Review,
|
|
105
|
+
type CreateReviewInput,
|
|
106
|
+
type MarketplaceStats,
|
|
107
|
+
type PluginListResponse,
|
|
108
|
+
type AdminPlugin,
|
|
109
|
+
type AdminDashboardStats,
|
|
110
|
+
type PluginListQuery,
|
|
111
|
+
} from './plugins'
|
|
@@ -61,3 +61,32 @@ export type DeleteResult = z.infer<typeof DeleteResultSchema>
|
|
|
61
61
|
export type ProcessOrderInput = z.infer<typeof ProcessOrderSchema>
|
|
62
62
|
export type CancelOrderInput = z.infer<typeof CancelOrderSchema>
|
|
63
63
|
export type OrderQueryInput = z.infer<typeof OrderQuerySchema>
|
|
64
|
+
|
|
65
|
+
export const RemoveCartItemResponseSchema = z.object({ removedId: z.string() })
|
|
66
|
+
|
|
67
|
+
export const ECommerceProductSchema = z.object({
|
|
68
|
+
name: z.string(),
|
|
69
|
+
color: z.string(),
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
export const ECommerceOrderStatusSchema = z.enum([
|
|
73
|
+
'processing',
|
|
74
|
+
'shipped',
|
|
75
|
+
'delivered',
|
|
76
|
+
'cancelled',
|
|
77
|
+
])
|
|
78
|
+
|
|
79
|
+
export const ECommerceOrderSchema = z.object({
|
|
80
|
+
id: z.string(),
|
|
81
|
+
date: z.string(),
|
|
82
|
+
status: ECommerceOrderStatusSchema,
|
|
83
|
+
products: z.array(ECommerceProductSchema),
|
|
84
|
+
total: z.number(),
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
export const ECommerceOrderListSchema = z.array(ECommerceOrderSchema)
|
|
88
|
+
|
|
89
|
+
export type RemoveCartItemResponse = z.infer<typeof RemoveCartItemResponseSchema>
|
|
90
|
+
export type ECommerceProduct = z.infer<typeof ECommerceProductSchema>
|
|
91
|
+
export type ECommerceOrderStatus = z.infer<typeof ECommerceOrderStatusSchema>
|
|
92
|
+
export type ECommerceOrder = z.infer<typeof ECommerceOrderSchema>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export {
|
|
2
|
+
PluginSchema,
|
|
3
|
+
PluginStatusSchema,
|
|
4
|
+
CreatePluginSchema,
|
|
5
|
+
UpdatePluginSchema,
|
|
6
|
+
PluginVersionStatusSchema,
|
|
7
|
+
VersionSchema,
|
|
8
|
+
CategorySchema,
|
|
9
|
+
ReviewSchema,
|
|
10
|
+
CreateReviewSchema,
|
|
11
|
+
MarketplaceStatsSchema,
|
|
12
|
+
PluginListResponseSchema,
|
|
13
|
+
AdminPluginSchema,
|
|
14
|
+
AdminDashboardStatsSchema,
|
|
15
|
+
PluginListQuerySchema,
|
|
16
|
+
PluginSlugSchema,
|
|
17
|
+
PluginSearchQuerySchema,
|
|
18
|
+
PluginDeleteResponseSchema,
|
|
19
|
+
ReviewIdParamsSchema,
|
|
20
|
+
ReviewDeleteResponseSchema,
|
|
21
|
+
CategorySlugParamsSchema,
|
|
22
|
+
CategoryPluginsQuerySchema,
|
|
23
|
+
PluginListAdminSchema,
|
|
24
|
+
AdminListQuerySchema,
|
|
25
|
+
AdminListAllQuerySchema,
|
|
26
|
+
RejectPluginBodySchema,
|
|
27
|
+
BulkApproveBodySchema,
|
|
28
|
+
BulkRejectBodySchema,
|
|
29
|
+
BulkResponseSchema,
|
|
30
|
+
CreateCategoryBodySchema,
|
|
31
|
+
UpdateCategoryBodySchema,
|
|
32
|
+
CategoryIdParamsSchema,
|
|
33
|
+
CategoryIdResponseSchema,
|
|
34
|
+
type Plugin,
|
|
35
|
+
type PluginStatus,
|
|
36
|
+
type CreatePluginInput,
|
|
37
|
+
type UpdatePluginInput,
|
|
38
|
+
type PluginVersionStatus,
|
|
39
|
+
type Version,
|
|
40
|
+
type Category,
|
|
41
|
+
type Review,
|
|
42
|
+
type CreateReviewInput,
|
|
43
|
+
type MarketplaceStats,
|
|
44
|
+
type PluginListResponse,
|
|
45
|
+
type AdminPlugin,
|
|
46
|
+
type AdminDashboardStats,
|
|
47
|
+
type PluginListQuery,
|
|
48
|
+
} from './schemas'
|