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,261 @@
|
|
|
1
|
+
import { createRoute, z } from '@hono/zod-openapi'
|
|
2
|
+
import { OpenAPIHono } from '@hono/zod-openapi'
|
|
3
|
+
import type { AuthUser } from '@server/middleware/auth'
|
|
4
|
+
import * as adminPluginService from '../services/admin-plugin-service'
|
|
5
|
+
import * as adminCategoryService from '../services/admin-category-service'
|
|
6
|
+
import * as adminStatsService from '../services/admin-stats-service'
|
|
7
|
+
import {
|
|
8
|
+
PluginSchema,
|
|
9
|
+
CategorySchema,
|
|
10
|
+
AdminDashboardStatsSchema,
|
|
11
|
+
PluginListAdminSchema,
|
|
12
|
+
AdminListQuerySchema,
|
|
13
|
+
AdminListAllQuerySchema,
|
|
14
|
+
PluginSlugSchema,
|
|
15
|
+
RejectPluginBodySchema,
|
|
16
|
+
PluginDeleteResponseSchema,
|
|
17
|
+
BulkApproveBodySchema,
|
|
18
|
+
BulkRejectBodySchema,
|
|
19
|
+
BulkResponseSchema,
|
|
20
|
+
CreateCategoryBodySchema,
|
|
21
|
+
UpdateCategoryBodySchema,
|
|
22
|
+
CategoryIdParamsSchema,
|
|
23
|
+
CategoryIdResponseSchema,
|
|
24
|
+
} from '@shared/schemas'
|
|
25
|
+
import { successResponse, errorResponse, success, created } from '@server/utils/route-helpers'
|
|
26
|
+
|
|
27
|
+
const getDashboardStatsRoute = createRoute({
|
|
28
|
+
method: 'get',
|
|
29
|
+
path: '/stats/dashboard',
|
|
30
|
+
tags: ['admin-plugins'],
|
|
31
|
+
responses: {
|
|
32
|
+
200: successResponse(AdminDashboardStatsSchema, 'Dashboard stats'),
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const listPendingRoute = createRoute({
|
|
37
|
+
method: 'get',
|
|
38
|
+
path: '/plugins/pending',
|
|
39
|
+
tags: ['admin-plugins'],
|
|
40
|
+
request: {
|
|
41
|
+
query: AdminListQuerySchema,
|
|
42
|
+
},
|
|
43
|
+
responses: {
|
|
44
|
+
200: successResponse(PluginListAdminSchema, 'Pending plugins'),
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const listAllPluginsRoute = createRoute({
|
|
49
|
+
method: 'get',
|
|
50
|
+
path: '/plugins',
|
|
51
|
+
tags: ['admin-plugins'],
|
|
52
|
+
request: {
|
|
53
|
+
query: AdminListAllQuerySchema,
|
|
54
|
+
},
|
|
55
|
+
responses: {
|
|
56
|
+
200: successResponse(PluginListAdminSchema, 'All plugins'),
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const approvePluginRoute = createRoute({
|
|
61
|
+
method: 'put',
|
|
62
|
+
path: '/plugins/{slug}/approve',
|
|
63
|
+
tags: ['admin-plugins'],
|
|
64
|
+
request: { params: PluginSlugSchema },
|
|
65
|
+
responses: {
|
|
66
|
+
200: successResponse(PluginSchema, 'Plugin approved'),
|
|
67
|
+
404: errorResponse('Plugin not found'),
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const rejectPluginRoute = createRoute({
|
|
72
|
+
method: 'put',
|
|
73
|
+
path: '/plugins/{slug}/reject',
|
|
74
|
+
tags: ['admin-plugins'],
|
|
75
|
+
request: {
|
|
76
|
+
params: PluginSlugSchema,
|
|
77
|
+
body: { content: { 'application/json': { schema: RejectPluginBodySchema } } },
|
|
78
|
+
},
|
|
79
|
+
responses: {
|
|
80
|
+
200: successResponse(PluginSchema, 'Plugin rejected'),
|
|
81
|
+
404: errorResponse('Plugin not found'),
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const toggleFeaturedRoute = createRoute({
|
|
86
|
+
method: 'put',
|
|
87
|
+
path: '/plugins/{slug}/feature',
|
|
88
|
+
tags: ['admin-plugins'],
|
|
89
|
+
request: { params: PluginSlugSchema },
|
|
90
|
+
responses: {
|
|
91
|
+
200: successResponse(PluginSchema, 'Featured toggled'),
|
|
92
|
+
404: errorResponse('Plugin not found'),
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const removePluginRoute = createRoute({
|
|
97
|
+
method: 'delete',
|
|
98
|
+
path: '/plugins/{slug}',
|
|
99
|
+
tags: ['admin-plugins'],
|
|
100
|
+
request: { params: PluginSlugSchema },
|
|
101
|
+
responses: {
|
|
102
|
+
200: successResponse(PluginDeleteResponseSchema, 'Plugin removed'),
|
|
103
|
+
404: errorResponse('Plugin not found'),
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const bulkApproveRoute = createRoute({
|
|
108
|
+
method: 'post',
|
|
109
|
+
path: '/plugins/bulk-approve',
|
|
110
|
+
tags: ['admin-plugins'],
|
|
111
|
+
request: {
|
|
112
|
+
body: { content: { 'application/json': { schema: BulkApproveBodySchema } } },
|
|
113
|
+
},
|
|
114
|
+
responses: {
|
|
115
|
+
200: successResponse(BulkResponseSchema, 'Bulk approved'),
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const bulkRejectRoute = createRoute({
|
|
120
|
+
method: 'post',
|
|
121
|
+
path: '/plugins/bulk-reject',
|
|
122
|
+
tags: ['admin-plugins'],
|
|
123
|
+
request: {
|
|
124
|
+
body: {
|
|
125
|
+
content: {
|
|
126
|
+
'application/json': {
|
|
127
|
+
schema: BulkRejectBodySchema,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
responses: {
|
|
133
|
+
200: successResponse(BulkResponseSchema, 'Bulk rejected'),
|
|
134
|
+
},
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const listCategoriesRoute = createRoute({
|
|
138
|
+
method: 'get',
|
|
139
|
+
path: '/categories',
|
|
140
|
+
tags: ['admin-plugins'],
|
|
141
|
+
responses: {
|
|
142
|
+
200: successResponse(z.array(CategorySchema), 'List categories'),
|
|
143
|
+
},
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const createCategoryRoute = createRoute({
|
|
147
|
+
method: 'post',
|
|
148
|
+
path: '/categories',
|
|
149
|
+
tags: ['admin-plugins'],
|
|
150
|
+
request: {
|
|
151
|
+
body: {
|
|
152
|
+
content: {
|
|
153
|
+
'application/json': {
|
|
154
|
+
schema: CreateCategoryBodySchema,
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
responses: {
|
|
160
|
+
201: successResponse(CategorySchema, 'Category created'),
|
|
161
|
+
409: errorResponse('Category slug already exists'),
|
|
162
|
+
},
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
const updateCategoryRoute = createRoute({
|
|
166
|
+
method: 'put',
|
|
167
|
+
path: '/categories/{id}',
|
|
168
|
+
tags: ['admin-plugins'],
|
|
169
|
+
request: {
|
|
170
|
+
params: CategoryIdParamsSchema,
|
|
171
|
+
body: {
|
|
172
|
+
content: {
|
|
173
|
+
'application/json': {
|
|
174
|
+
schema: UpdateCategoryBodySchema,
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
responses: {
|
|
180
|
+
200: successResponse(CategorySchema, 'Category updated'),
|
|
181
|
+
404: errorResponse('Category not found'),
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
const deleteCategoryRoute = createRoute({
|
|
186
|
+
method: 'delete',
|
|
187
|
+
path: '/categories/{id}',
|
|
188
|
+
tags: ['admin-plugins'],
|
|
189
|
+
request: { params: CategoryIdParamsSchema },
|
|
190
|
+
responses: {
|
|
191
|
+
200: successResponse(CategoryIdResponseSchema, 'Category deleted'),
|
|
192
|
+
404: errorResponse('Category not found'),
|
|
193
|
+
},
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
export const pluginAdminRoutes = new OpenAPIHono<{ Variables: { authUser: AuthUser } }>()
|
|
197
|
+
.openapi(getDashboardStatsRoute, async c => {
|
|
198
|
+
const stats = await adminStatsService.getDashboardStats()
|
|
199
|
+
return c.json(success(stats), 200)
|
|
200
|
+
})
|
|
201
|
+
.openapi(listPendingRoute, async c => {
|
|
202
|
+
const { page, limit } = c.req.valid('query')
|
|
203
|
+
const result = await adminPluginService.listPending({ page, limit })
|
|
204
|
+
return c.json(success(result), 200)
|
|
205
|
+
})
|
|
206
|
+
.openapi(listAllPluginsRoute, async c => {
|
|
207
|
+
const { page, limit, status } = c.req.valid('query')
|
|
208
|
+
const result = await adminPluginService.listAllPlugins({ page, limit, status })
|
|
209
|
+
return c.json(success(result), 200)
|
|
210
|
+
})
|
|
211
|
+
.openapi(approvePluginRoute, async c => {
|
|
212
|
+
const { slug } = c.req.valid('param')
|
|
213
|
+
const plugin = await adminPluginService.approvePlugin(slug)
|
|
214
|
+
return c.json(success(plugin), 200)
|
|
215
|
+
})
|
|
216
|
+
.openapi(rejectPluginRoute, async c => {
|
|
217
|
+
const { slug } = c.req.valid('param')
|
|
218
|
+
const { reason } = c.req.valid('json')
|
|
219
|
+
const plugin = await adminPluginService.rejectPlugin(slug, reason)
|
|
220
|
+
return c.json(success(plugin), 200)
|
|
221
|
+
})
|
|
222
|
+
.openapi(toggleFeaturedRoute, async c => {
|
|
223
|
+
const { slug } = c.req.valid('param')
|
|
224
|
+
const plugin = await adminPluginService.toggleFeatured(slug)
|
|
225
|
+
return c.json(success(plugin), 200)
|
|
226
|
+
})
|
|
227
|
+
.openapi(removePluginRoute, async c => {
|
|
228
|
+
const { slug } = c.req.valid('param')
|
|
229
|
+
await adminPluginService.adminRemovePlugin(slug)
|
|
230
|
+
return c.json(success({ slug }), 200)
|
|
231
|
+
})
|
|
232
|
+
.openapi(bulkApproveRoute, async c => {
|
|
233
|
+
const { slugs } = c.req.valid('json')
|
|
234
|
+
const count = await adminPluginService.bulkApprove(slugs)
|
|
235
|
+
return c.json(success({ count }), 200)
|
|
236
|
+
})
|
|
237
|
+
.openapi(bulkRejectRoute, async c => {
|
|
238
|
+
const { slugs, reason } = c.req.valid('json')
|
|
239
|
+
const count = await adminPluginService.bulkReject(slugs, reason)
|
|
240
|
+
return c.json(success({ count }), 200)
|
|
241
|
+
})
|
|
242
|
+
.openapi(listCategoriesRoute, async c => {
|
|
243
|
+
const categories = await adminCategoryService.listAllCategories()
|
|
244
|
+
return c.json(success(categories), 200)
|
|
245
|
+
})
|
|
246
|
+
.openapi(createCategoryRoute, async c => {
|
|
247
|
+
const data = c.req.valid('json')
|
|
248
|
+
const category = await adminCategoryService.createCategory(data)
|
|
249
|
+
return c.json(created(category), 201)
|
|
250
|
+
})
|
|
251
|
+
.openapi(updateCategoryRoute, async c => {
|
|
252
|
+
const { id } = c.req.valid('param')
|
|
253
|
+
const data = c.req.valid('json')
|
|
254
|
+
const category = await adminCategoryService.updateCategory(id, data)
|
|
255
|
+
return c.json(success(category), 200)
|
|
256
|
+
})
|
|
257
|
+
.openapi(deleteCategoryRoute, async c => {
|
|
258
|
+
const { id } = c.req.valid('param')
|
|
259
|
+
await adminCategoryService.deleteCategory(id)
|
|
260
|
+
return c.json(success({ id }), 200)
|
|
261
|
+
})
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { createRoute, z } from '@hono/zod-openapi'
|
|
2
|
+
import { OpenAPIHono } from '@hono/zod-openapi'
|
|
3
|
+
import * as pluginService from '../services/plugin-service'
|
|
4
|
+
import * as queryService from '../services/plugin-query-service'
|
|
5
|
+
import * as reviewService from '../services/plugin-review-service'
|
|
6
|
+
import {
|
|
7
|
+
PluginSchema,
|
|
8
|
+
CreatePluginSchema,
|
|
9
|
+
UpdatePluginSchema,
|
|
10
|
+
PluginSlugSchema,
|
|
11
|
+
ReviewSchema,
|
|
12
|
+
CreateReviewSchema,
|
|
13
|
+
MarketplaceStatsSchema,
|
|
14
|
+
CategorySchema,
|
|
15
|
+
VersionSchema,
|
|
16
|
+
PluginListResponseSchema,
|
|
17
|
+
PluginListQuerySchema,
|
|
18
|
+
PluginSearchQuerySchema,
|
|
19
|
+
PluginDeleteResponseSchema,
|
|
20
|
+
ReviewIdParamsSchema,
|
|
21
|
+
ReviewDeleteResponseSchema,
|
|
22
|
+
CategorySlugParamsSchema,
|
|
23
|
+
CategoryPluginsQuerySchema,
|
|
24
|
+
} from '@shared/schemas'
|
|
25
|
+
import { successResponse, errorResponse, success, created } from '@server/utils/route-helpers'
|
|
26
|
+
import { getAuthUser } from '@server/utils/auth'
|
|
27
|
+
|
|
28
|
+
const listRoute = createRoute({
|
|
29
|
+
method: 'get',
|
|
30
|
+
path: '/plugins',
|
|
31
|
+
tags: ['plugins'],
|
|
32
|
+
request: { query: PluginListQuerySchema },
|
|
33
|
+
responses: {
|
|
34
|
+
200: successResponse(PluginListResponseSchema, 'List plugins'),
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const searchRoute = createRoute({
|
|
39
|
+
method: 'get',
|
|
40
|
+
path: '/plugins/search',
|
|
41
|
+
tags: ['plugins'],
|
|
42
|
+
request: {
|
|
43
|
+
query: PluginSearchQuerySchema,
|
|
44
|
+
},
|
|
45
|
+
responses: {
|
|
46
|
+
200: successResponse(PluginListResponseSchema, 'Search plugins'),
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const getBySlugRoute = createRoute({
|
|
51
|
+
method: 'get',
|
|
52
|
+
path: '/plugins/{slug}',
|
|
53
|
+
tags: ['plugins'],
|
|
54
|
+
request: { params: PluginSlugSchema },
|
|
55
|
+
responses: {
|
|
56
|
+
200: successResponse(PluginSchema, 'Get plugin by slug'),
|
|
57
|
+
404: errorResponse('Plugin not found'),
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const getVersionsRoute = createRoute({
|
|
62
|
+
method: 'get',
|
|
63
|
+
path: '/plugins/{slug}/versions',
|
|
64
|
+
tags: ['plugins'],
|
|
65
|
+
request: { params: PluginSlugSchema },
|
|
66
|
+
responses: {
|
|
67
|
+
200: successResponse(z.array(VersionSchema), 'List plugin versions'),
|
|
68
|
+
404: errorResponse('Plugin not found'),
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const createRouteDef = createRoute({
|
|
73
|
+
method: 'post',
|
|
74
|
+
path: '/plugins',
|
|
75
|
+
tags: ['plugins'],
|
|
76
|
+
security: [{ Bearer: [] }],
|
|
77
|
+
request: {
|
|
78
|
+
body: { content: { 'application/json': { schema: CreatePluginSchema } } },
|
|
79
|
+
},
|
|
80
|
+
responses: {
|
|
81
|
+
201: successResponse(PluginSchema, 'Create plugin'),
|
|
82
|
+
400: errorResponse('Invalid input'),
|
|
83
|
+
409: errorResponse('Plugin slug already exists'),
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const updateRoute = createRoute({
|
|
88
|
+
method: 'put',
|
|
89
|
+
path: '/plugins/{slug}',
|
|
90
|
+
tags: ['plugins'],
|
|
91
|
+
security: [{ Bearer: [] }],
|
|
92
|
+
request: {
|
|
93
|
+
params: PluginSlugSchema,
|
|
94
|
+
body: { content: { 'application/json': { schema: UpdatePluginSchema } } },
|
|
95
|
+
},
|
|
96
|
+
responses: {
|
|
97
|
+
200: successResponse(PluginSchema, 'Update plugin'),
|
|
98
|
+
404: errorResponse('Plugin not found'),
|
|
99
|
+
403: errorResponse('Forbidden'),
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const deleteRoute = createRoute({
|
|
104
|
+
method: 'delete',
|
|
105
|
+
path: '/plugins/{slug}',
|
|
106
|
+
tags: ['plugins'],
|
|
107
|
+
security: [{ Bearer: [] }],
|
|
108
|
+
request: { params: PluginSlugSchema },
|
|
109
|
+
responses: {
|
|
110
|
+
200: successResponse(PluginDeleteResponseSchema, 'Delete plugin'),
|
|
111
|
+
404: errorResponse('Plugin not found'),
|
|
112
|
+
403: errorResponse('Forbidden'),
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const submitReviewRoute = createRoute({
|
|
117
|
+
method: 'post',
|
|
118
|
+
path: '/plugins/{slug}/reviews',
|
|
119
|
+
tags: ['plugins'],
|
|
120
|
+
security: [{ Bearer: [] }],
|
|
121
|
+
request: {
|
|
122
|
+
params: PluginSlugSchema,
|
|
123
|
+
body: { content: { 'application/json': { schema: CreateReviewSchema } } },
|
|
124
|
+
},
|
|
125
|
+
responses: {
|
|
126
|
+
201: successResponse(ReviewSchema, 'Submit review'),
|
|
127
|
+
404: errorResponse('Plugin not found'),
|
|
128
|
+
409: errorResponse('Already reviewed'),
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const getReviewsRoute = createRoute({
|
|
133
|
+
method: 'get',
|
|
134
|
+
path: '/plugins/{slug}/reviews',
|
|
135
|
+
tags: ['plugins'],
|
|
136
|
+
request: { params: PluginSlugSchema },
|
|
137
|
+
responses: {
|
|
138
|
+
200: successResponse(z.array(ReviewSchema), 'List reviews'),
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
const deleteReviewRoute = createRoute({
|
|
143
|
+
method: 'delete',
|
|
144
|
+
path: '/plugins/{slug}/reviews/{reviewId}',
|
|
145
|
+
tags: ['plugins'],
|
|
146
|
+
security: [{ Bearer: [] }],
|
|
147
|
+
request: {
|
|
148
|
+
params: ReviewIdParamsSchema,
|
|
149
|
+
},
|
|
150
|
+
responses: {
|
|
151
|
+
200: successResponse(ReviewDeleteResponseSchema, 'Review deleted'),
|
|
152
|
+
404: errorResponse('Review not found'),
|
|
153
|
+
403: errorResponse('Forbidden'),
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const trackInstallRoute = createRoute({
|
|
158
|
+
method: 'post',
|
|
159
|
+
path: '/plugins/{slug}/install',
|
|
160
|
+
tags: ['plugins'],
|
|
161
|
+
request: { params: PluginSlugSchema },
|
|
162
|
+
responses: {
|
|
163
|
+
200: successResponse(PluginDeleteResponseSchema, 'Install tracked'),
|
|
164
|
+
404: errorResponse('Plugin not found'),
|
|
165
|
+
},
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const listCategoriesRoute = createRoute({
|
|
169
|
+
method: 'get',
|
|
170
|
+
path: '/categories',
|
|
171
|
+
tags: ['plugins'],
|
|
172
|
+
responses: {
|
|
173
|
+
200: successResponse(z.array(CategorySchema), 'List categories'),
|
|
174
|
+
},
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const getPluginsByCategoryRoute = createRoute({
|
|
178
|
+
method: 'get',
|
|
179
|
+
path: '/categories/{slug}/plugins',
|
|
180
|
+
tags: ['plugins'],
|
|
181
|
+
request: {
|
|
182
|
+
params: CategorySlugParamsSchema,
|
|
183
|
+
query: CategoryPluginsQuerySchema,
|
|
184
|
+
},
|
|
185
|
+
responses: {
|
|
186
|
+
200: successResponse(PluginListResponseSchema, 'Plugins by category'),
|
|
187
|
+
404: errorResponse('Category not found'),
|
|
188
|
+
},
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
const getStatsRoute = createRoute({
|
|
192
|
+
method: 'get',
|
|
193
|
+
path: '/stats',
|
|
194
|
+
tags: ['plugins'],
|
|
195
|
+
responses: {
|
|
196
|
+
200: successResponse(MarketplaceStatsSchema, 'Marketplace stats'),
|
|
197
|
+
},
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
const listMyPluginsRoute = createRoute({
|
|
201
|
+
method: 'get',
|
|
202
|
+
path: '/plugins/mine',
|
|
203
|
+
tags: ['plugins'],
|
|
204
|
+
security: [{ Bearer: [] }],
|
|
205
|
+
responses: {
|
|
206
|
+
200: successResponse(z.array(PluginSchema), 'My plugins'),
|
|
207
|
+
},
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
export const pluginRoutes = new OpenAPIHono()
|
|
211
|
+
.openapi(listMyPluginsRoute, async c => {
|
|
212
|
+
const user = getAuthUser(c)
|
|
213
|
+
if (!user) {
|
|
214
|
+
return c.json({ success: false as const, error: 'Unauthorized' }, 401)
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
const plugins = await queryService.listMyPlugins(user.id)
|
|
218
|
+
return c.json(success(plugins), 200)
|
|
219
|
+
} catch {
|
|
220
|
+
return c.json(success([]), 200)
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
.openapi(listRoute, async c => {
|
|
224
|
+
const query = c.req.valid('query')
|
|
225
|
+
try {
|
|
226
|
+
const result = await queryService.listPlugins({
|
|
227
|
+
page: query.page,
|
|
228
|
+
limit: query.limit,
|
|
229
|
+
status: query.status,
|
|
230
|
+
sort: query.sort,
|
|
231
|
+
featured: query.featured,
|
|
232
|
+
})
|
|
233
|
+
return c.json(success(result), 200)
|
|
234
|
+
} catch {
|
|
235
|
+
return c.json(success({ plugins: [], total: 0, page: 1, limit: 20 }), 200)
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
.openapi(searchRoute, async c => {
|
|
239
|
+
const { q, page, limit, category } = c.req.valid('query')
|
|
240
|
+
try {
|
|
241
|
+
const result = await queryService.searchPlugins(q, { page, limit, category })
|
|
242
|
+
return c.json(success(result), 200)
|
|
243
|
+
} catch {
|
|
244
|
+
return c.json(success({ plugins: [], total: 0, page: 1, limit: 20 }), 200)
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
.openapi(getBySlugRoute, async c => {
|
|
248
|
+
const { slug } = c.req.valid('param')
|
|
249
|
+
try {
|
|
250
|
+
const plugin = await queryService.getPluginBySlug(slug)
|
|
251
|
+
return c.json(success(plugin), 200)
|
|
252
|
+
} catch {
|
|
253
|
+
return c.json({ success: false as const, error: 'Plugin not found' }, 404)
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
.openapi(getVersionsRoute, async c => {
|
|
257
|
+
const { slug } = c.req.valid('param')
|
|
258
|
+
try {
|
|
259
|
+
const plugin = await queryService.getPluginBySlug(slug)
|
|
260
|
+
const versions = await queryService.getVersions(plugin.id)
|
|
261
|
+
return c.json(success(versions), 200)
|
|
262
|
+
} catch {
|
|
263
|
+
return c.json(success([]), 200)
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
.openapi(createRouteDef, async c => {
|
|
267
|
+
const data = c.req.valid('json')
|
|
268
|
+
const user = getAuthUser(c)
|
|
269
|
+
const plugin = await pluginService.createPlugin({
|
|
270
|
+
...data,
|
|
271
|
+
authorId: user.id,
|
|
272
|
+
authorName: user.username,
|
|
273
|
+
})
|
|
274
|
+
return c.json(created(plugin), 201)
|
|
275
|
+
})
|
|
276
|
+
.openapi(updateRoute, async c => {
|
|
277
|
+
const { slug } = c.req.valid('param')
|
|
278
|
+
const data = c.req.valid('json')
|
|
279
|
+
const user = getAuthUser(c)
|
|
280
|
+
const plugin = await pluginService.updatePlugin(slug, data, user.id)
|
|
281
|
+
return c.json(success(plugin), 200)
|
|
282
|
+
})
|
|
283
|
+
.openapi(deleteRoute, async c => {
|
|
284
|
+
const { slug } = c.req.valid('param')
|
|
285
|
+
const user = getAuthUser(c)
|
|
286
|
+
await pluginService.deletePlugin(slug, user.id)
|
|
287
|
+
return c.json(success({ slug }), 200)
|
|
288
|
+
})
|
|
289
|
+
.openapi(submitReviewRoute, async c => {
|
|
290
|
+
const { slug } = c.req.valid('param')
|
|
291
|
+
const data = c.req.valid('json')
|
|
292
|
+
const user = getAuthUser(c)
|
|
293
|
+
try {
|
|
294
|
+
const plugin = await queryService.getPluginBySlug(slug)
|
|
295
|
+
const review = await reviewService.submitReview({
|
|
296
|
+
pluginId: plugin.id,
|
|
297
|
+
userId: user.id,
|
|
298
|
+
userName: user.username,
|
|
299
|
+
rating: data.rating,
|
|
300
|
+
title: data.title,
|
|
301
|
+
content: data.content,
|
|
302
|
+
})
|
|
303
|
+
return c.json(created(review), 201)
|
|
304
|
+
} catch {
|
|
305
|
+
return c.json({ success: false as const, error: 'Plugin not found' }, 404)
|
|
306
|
+
}
|
|
307
|
+
})
|
|
308
|
+
.openapi(getReviewsRoute, async c => {
|
|
309
|
+
const { slug } = c.req.valid('param')
|
|
310
|
+
try {
|
|
311
|
+
const plugin = await queryService.getPluginBySlug(slug)
|
|
312
|
+
const reviews = await reviewService.getReviews(plugin.id)
|
|
313
|
+
return c.json(success(reviews), 200)
|
|
314
|
+
} catch {
|
|
315
|
+
return c.json(success([]), 200)
|
|
316
|
+
}
|
|
317
|
+
})
|
|
318
|
+
.openapi(deleteReviewRoute, async c => {
|
|
319
|
+
const { reviewId } = c.req.valid('param')
|
|
320
|
+
const user = getAuthUser(c)
|
|
321
|
+
await reviewService.deleteReview(reviewId, user.id)
|
|
322
|
+
return c.json(success({ id: reviewId }), 200)
|
|
323
|
+
})
|
|
324
|
+
.openapi(trackInstallRoute, async c => {
|
|
325
|
+
const { slug } = c.req.valid('param')
|
|
326
|
+
try {
|
|
327
|
+
await pluginService.trackInstall(slug)
|
|
328
|
+
return c.json(success({ slug }), 200)
|
|
329
|
+
} catch {
|
|
330
|
+
return c.json(success({ slug }), 200)
|
|
331
|
+
}
|
|
332
|
+
})
|
|
333
|
+
.openapi(listCategoriesRoute, async c => {
|
|
334
|
+
try {
|
|
335
|
+
const categories = await queryService.listCategories()
|
|
336
|
+
return c.json(success(categories), 200)
|
|
337
|
+
} catch {
|
|
338
|
+
return c.json(success([]), 200)
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
.openapi(getPluginsByCategoryRoute, async c => {
|
|
342
|
+
const { slug } = c.req.valid('param')
|
|
343
|
+
const { page, limit } = c.req.valid('query')
|
|
344
|
+
try {
|
|
345
|
+
const result = await queryService.getPluginsByCategory(slug, { page, limit })
|
|
346
|
+
return c.json(success(result), 200)
|
|
347
|
+
} catch {
|
|
348
|
+
return c.json(success({ plugins: [], total: 0, page: 1, limit: 20 }), 200)
|
|
349
|
+
}
|
|
350
|
+
})
|
|
351
|
+
.openapi(getStatsRoute, async c => {
|
|
352
|
+
const stats = await queryService.getStats()
|
|
353
|
+
return c.json(success(stats), 200)
|
|
354
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { eq, asc } from 'drizzle-orm'
|
|
2
|
+
import type { Category } from '@shared/schemas'
|
|
3
|
+
import { getDb } from '@server/db'
|
|
4
|
+
import { pluginCategories } from '@server/db/schema'
|
|
5
|
+
import { generateUUID } from '@server/utils/uuid'
|
|
6
|
+
import { NotFoundError, ConflictError } from '@server/utils/app-error'
|
|
7
|
+
|
|
8
|
+
export interface CreateCategoryData {
|
|
9
|
+
name: string
|
|
10
|
+
slug: string
|
|
11
|
+
description?: string | null
|
|
12
|
+
icon?: string | null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UpdateCategoryData {
|
|
16
|
+
name?: string | null
|
|
17
|
+
slug?: string | null
|
|
18
|
+
description?: string | null
|
|
19
|
+
icon?: string | null
|
|
20
|
+
sortOrder?: number | null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function mapCategoryRow(row: typeof pluginCategories.$inferSelect): Category {
|
|
24
|
+
return {
|
|
25
|
+
id: row.id,
|
|
26
|
+
name: row.name,
|
|
27
|
+
slug: row.slug,
|
|
28
|
+
description: row.description ?? undefined,
|
|
29
|
+
icon: row.icon ?? undefined,
|
|
30
|
+
sortOrder: row.sortOrder,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function createCategory(data: CreateCategoryData): Promise<Category> {
|
|
35
|
+
const db = await getDb()
|
|
36
|
+
|
|
37
|
+
const existing = await db
|
|
38
|
+
.select()
|
|
39
|
+
.from(pluginCategories)
|
|
40
|
+
.where(eq(pluginCategories.slug, data.slug))
|
|
41
|
+
if (existing.length > 0) {
|
|
42
|
+
throw new ConflictError(`Category with slug '${data.slug}' already exists`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const id = generateUUID()
|
|
46
|
+
const result = await db
|
|
47
|
+
.insert(pluginCategories)
|
|
48
|
+
.values({
|
|
49
|
+
id,
|
|
50
|
+
name: data.name,
|
|
51
|
+
slug: data.slug,
|
|
52
|
+
description: data.description ?? null,
|
|
53
|
+
icon: data.icon ?? null,
|
|
54
|
+
sortOrder: 0,
|
|
55
|
+
})
|
|
56
|
+
.returning()
|
|
57
|
+
|
|
58
|
+
return mapCategoryRow(result[0])
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function updateCategory(id: string, data: UpdateCategoryData): Promise<Category> {
|
|
62
|
+
const db = await getDb()
|
|
63
|
+
|
|
64
|
+
const rows = await db.select().from(pluginCategories).where(eq(pluginCategories.id, id))
|
|
65
|
+
if (rows.length === 0) {
|
|
66
|
+
throw new NotFoundError('Category', id)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const updateData: Partial<typeof pluginCategories.$inferInsert> = {}
|
|
70
|
+
if (data.name !== undefined && data.name !== null) updateData.name = data.name
|
|
71
|
+
if (data.slug !== undefined && data.slug !== null) updateData.slug = data.slug
|
|
72
|
+
if (data.description !== undefined) updateData.description = data.description
|
|
73
|
+
if (data.icon !== undefined) updateData.icon = data.icon
|
|
74
|
+
if (data.sortOrder !== undefined && data.sortOrder !== null) updateData.sortOrder = data.sortOrder
|
|
75
|
+
|
|
76
|
+
const result = await db
|
|
77
|
+
.update(pluginCategories)
|
|
78
|
+
.set(updateData)
|
|
79
|
+
.where(eq(pluginCategories.id, id))
|
|
80
|
+
.returning()
|
|
81
|
+
return mapCategoryRow(result[0])
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function deleteCategory(id: string): Promise<void> {
|
|
85
|
+
const db = await getDb()
|
|
86
|
+
|
|
87
|
+
const rows = await db.select().from(pluginCategories).where(eq(pluginCategories.id, id))
|
|
88
|
+
if (rows.length === 0) {
|
|
89
|
+
throw new NotFoundError('Category', id)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await db.delete(pluginCategories).where(eq(pluginCategories.id, id))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function listAllCategories(): Promise<Category[]> {
|
|
96
|
+
const db = await getDb()
|
|
97
|
+
const rows = await db.select().from(pluginCategories).orderBy(asc(pluginCategories.sortOrder))
|
|
98
|
+
return rows.map(mapCategoryRow)
|
|
99
|
+
}
|