create-blitzpack 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/dist/index.js +452 -0
  2. package/package.json +57 -0
  3. package/template/.dockerignore +59 -0
  4. package/template/.github/workflows/ci.yml +157 -0
  5. package/template/.husky/pre-commit +1 -0
  6. package/template/.husky/pre-push +1 -0
  7. package/template/.lintstagedrc.cjs +4 -0
  8. package/template/.nvmrc +1 -0
  9. package/template/.prettierrc +9 -0
  10. package/template/.vscode/settings.json +13 -0
  11. package/template/CLAUDE.md +175 -0
  12. package/template/CONTRIBUTING.md +32 -0
  13. package/template/Dockerfile +90 -0
  14. package/template/GETTING_STARTED.md +35 -0
  15. package/template/LICENSE +21 -0
  16. package/template/README.md +116 -0
  17. package/template/apps/api/.dockerignore +51 -0
  18. package/template/apps/api/.env.local.example +62 -0
  19. package/template/apps/api/emails/account-deleted-email.tsx +69 -0
  20. package/template/apps/api/emails/components/email-layout.tsx +154 -0
  21. package/template/apps/api/emails/config.ts +22 -0
  22. package/template/apps/api/emails/password-changed-email.tsx +88 -0
  23. package/template/apps/api/emails/password-reset-email.tsx +86 -0
  24. package/template/apps/api/emails/verification-email.tsx +85 -0
  25. package/template/apps/api/emails/welcome-email.tsx +70 -0
  26. package/template/apps/api/package.json +84 -0
  27. package/template/apps/api/prisma/migrations/20251012111439_init/migration.sql +13 -0
  28. package/template/apps/api/prisma/migrations/20251018162629_add_better_auth_fields/migration.sql +67 -0
  29. package/template/apps/api/prisma/migrations/20251019142208_add_user_role_enum/migration.sql +5 -0
  30. package/template/apps/api/prisma/migrations/20251019182151_user_auth/migration.sql +7 -0
  31. package/template/apps/api/prisma/migrations/20251019211416_faster_session_lookup/migration.sql +2 -0
  32. package/template/apps/api/prisma/migrations/20251119124337_add_upload_model/migration.sql +26 -0
  33. package/template/apps/api/prisma/migrations/20251120071241_add_scope_to_account/migration.sql +2 -0
  34. package/template/apps/api/prisma/migrations/20251120072608_add_oauth_token_expiration_fields/migration.sql +10 -0
  35. package/template/apps/api/prisma/migrations/20251120144705_add_audit_logs/migration.sql +29 -0
  36. package/template/apps/api/prisma/migrations/20251127123614_remove_impersonated_by/migration.sql +8 -0
  37. package/template/apps/api/prisma/migrations/20251127125630_remove_audit_logs/migration.sql +11 -0
  38. package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
  39. package/template/apps/api/prisma/schema.prisma +116 -0
  40. package/template/apps/api/prisma/seed.ts +159 -0
  41. package/template/apps/api/prisma.config.ts +14 -0
  42. package/template/apps/api/src/app.ts +377 -0
  43. package/template/apps/api/src/common/logger.service.ts +227 -0
  44. package/template/apps/api/src/config/env.ts +60 -0
  45. package/template/apps/api/src/config/rate-limit.ts +29 -0
  46. package/template/apps/api/src/hooks/auth.ts +122 -0
  47. package/template/apps/api/src/plugins/auth.ts +198 -0
  48. package/template/apps/api/src/plugins/database.ts +45 -0
  49. package/template/apps/api/src/plugins/logger.ts +33 -0
  50. package/template/apps/api/src/plugins/multipart.ts +16 -0
  51. package/template/apps/api/src/plugins/scalar.ts +20 -0
  52. package/template/apps/api/src/plugins/schedule.ts +52 -0
  53. package/template/apps/api/src/plugins/services.ts +66 -0
  54. package/template/apps/api/src/plugins/swagger.ts +56 -0
  55. package/template/apps/api/src/routes/accounts.ts +91 -0
  56. package/template/apps/api/src/routes/admin-sessions.ts +92 -0
  57. package/template/apps/api/src/routes/metrics.ts +71 -0
  58. package/template/apps/api/src/routes/password.ts +46 -0
  59. package/template/apps/api/src/routes/sessions.ts +53 -0
  60. package/template/apps/api/src/routes/stats.ts +38 -0
  61. package/template/apps/api/src/routes/uploads-serve.ts +27 -0
  62. package/template/apps/api/src/routes/uploads.ts +154 -0
  63. package/template/apps/api/src/routes/users.ts +114 -0
  64. package/template/apps/api/src/routes/verification.ts +90 -0
  65. package/template/apps/api/src/server.ts +34 -0
  66. package/template/apps/api/src/services/accounts.service.ts +125 -0
  67. package/template/apps/api/src/services/authorization.service.ts +162 -0
  68. package/template/apps/api/src/services/email.service.ts +170 -0
  69. package/template/apps/api/src/services/file-storage.service.ts +267 -0
  70. package/template/apps/api/src/services/metrics.service.ts +175 -0
  71. package/template/apps/api/src/services/password.service.ts +56 -0
  72. package/template/apps/api/src/services/sessions.service.spec.ts +134 -0
  73. package/template/apps/api/src/services/sessions.service.ts +276 -0
  74. package/template/apps/api/src/services/stats.service.ts +273 -0
  75. package/template/apps/api/src/services/uploads.service.ts +163 -0
  76. package/template/apps/api/src/services/users.service.spec.ts +249 -0
  77. package/template/apps/api/src/services/users.service.ts +198 -0
  78. package/template/apps/api/src/utils/file-validation.ts +108 -0
  79. package/template/apps/api/start.sh +33 -0
  80. package/template/apps/api/test/helpers/fastify-app.ts +24 -0
  81. package/template/apps/api/test/helpers/mock-authorization.ts +16 -0
  82. package/template/apps/api/test/helpers/mock-logger.ts +28 -0
  83. package/template/apps/api/test/helpers/mock-prisma.ts +30 -0
  84. package/template/apps/api/test/helpers/test-db.ts +125 -0
  85. package/template/apps/api/test/integration/auth-flow.integration.spec.ts +449 -0
  86. package/template/apps/api/test/integration/password.integration.spec.ts +427 -0
  87. package/template/apps/api/test/integration/rate-limit.integration.spec.ts +51 -0
  88. package/template/apps/api/test/integration/sessions.integration.spec.ts +445 -0
  89. package/template/apps/api/test/integration/users.integration.spec.ts +211 -0
  90. package/template/apps/api/test/setup.ts +31 -0
  91. package/template/apps/api/tsconfig.json +26 -0
  92. package/template/apps/api/vitest.config.ts +35 -0
  93. package/template/apps/web/.env.local.example +11 -0
  94. package/template/apps/web/components.json +24 -0
  95. package/template/apps/web/next.config.ts +22 -0
  96. package/template/apps/web/package.json +56 -0
  97. package/template/apps/web/postcss.config.js +5 -0
  98. package/template/apps/web/public/apple-icon.png +0 -0
  99. package/template/apps/web/public/icon.png +0 -0
  100. package/template/apps/web/public/robots.txt +3 -0
  101. package/template/apps/web/src/app/(admin)/admin/layout.tsx +222 -0
  102. package/template/apps/web/src/app/(admin)/admin/page.tsx +157 -0
  103. package/template/apps/web/src/app/(admin)/admin/sessions/page.tsx +18 -0
  104. package/template/apps/web/src/app/(admin)/admin/users/page.tsx +20 -0
  105. package/template/apps/web/src/app/(auth)/forgot-password/page.tsx +177 -0
  106. package/template/apps/web/src/app/(auth)/login/page.tsx +159 -0
  107. package/template/apps/web/src/app/(auth)/reset-password/page.tsx +245 -0
  108. package/template/apps/web/src/app/(auth)/signup/page.tsx +153 -0
  109. package/template/apps/web/src/app/dashboard/change-password/page.tsx +255 -0
  110. package/template/apps/web/src/app/dashboard/page.tsx +296 -0
  111. package/template/apps/web/src/app/error.tsx +32 -0
  112. package/template/apps/web/src/app/examples/file-upload/page.tsx +200 -0
  113. package/template/apps/web/src/app/favicon.ico +0 -0
  114. package/template/apps/web/src/app/global-error.tsx +96 -0
  115. package/template/apps/web/src/app/globals.css +22 -0
  116. package/template/apps/web/src/app/icon.png +0 -0
  117. package/template/apps/web/src/app/layout.tsx +34 -0
  118. package/template/apps/web/src/app/not-found.tsx +28 -0
  119. package/template/apps/web/src/app/page.tsx +192 -0
  120. package/template/apps/web/src/components/admin/activity-feed.tsx +101 -0
  121. package/template/apps/web/src/components/admin/charts/auth-breakdown-chart.tsx +114 -0
  122. package/template/apps/web/src/components/admin/charts/chart-tooltip.tsx +124 -0
  123. package/template/apps/web/src/components/admin/charts/realtime-metrics-chart.tsx +511 -0
  124. package/template/apps/web/src/components/admin/charts/role-distribution-chart.tsx +102 -0
  125. package/template/apps/web/src/components/admin/charts/session-activity-chart.tsx +90 -0
  126. package/template/apps/web/src/components/admin/charts/user-growth-chart.tsx +108 -0
  127. package/template/apps/web/src/components/admin/health-indicator.tsx +175 -0
  128. package/template/apps/web/src/components/admin/refresh-control.tsx +90 -0
  129. package/template/apps/web/src/components/admin/session-revoke-all-dialog.tsx +79 -0
  130. package/template/apps/web/src/components/admin/session-revoke-dialog.tsx +74 -0
  131. package/template/apps/web/src/components/admin/sessions-management-table.tsx +372 -0
  132. package/template/apps/web/src/components/admin/stat-card.tsx +137 -0
  133. package/template/apps/web/src/components/admin/user-create-dialog.tsx +152 -0
  134. package/template/apps/web/src/components/admin/user-delete-dialog.tsx +73 -0
  135. package/template/apps/web/src/components/admin/user-edit-dialog.tsx +170 -0
  136. package/template/apps/web/src/components/admin/users-management-table.tsx +285 -0
  137. package/template/apps/web/src/components/auth/email-verification-banner.tsx +85 -0
  138. package/template/apps/web/src/components/auth/github-button.tsx +40 -0
  139. package/template/apps/web/src/components/auth/google-button.tsx +54 -0
  140. package/template/apps/web/src/components/auth/protected-route.tsx +66 -0
  141. package/template/apps/web/src/components/auth/redirect-if-authenticated.tsx +31 -0
  142. package/template/apps/web/src/components/auth/with-auth.tsx +30 -0
  143. package/template/apps/web/src/components/error/error-card.tsx +47 -0
  144. package/template/apps/web/src/components/error/forbidden.tsx +25 -0
  145. package/template/apps/web/src/components/landing/command-block.tsx +64 -0
  146. package/template/apps/web/src/components/landing/feature-card.tsx +60 -0
  147. package/template/apps/web/src/components/landing/included-feature-card.tsx +63 -0
  148. package/template/apps/web/src/components/landing/logo.tsx +41 -0
  149. package/template/apps/web/src/components/landing/tech-badge.tsx +11 -0
  150. package/template/apps/web/src/components/layout/auth-nav.tsx +58 -0
  151. package/template/apps/web/src/components/layout/footer.tsx +3 -0
  152. package/template/apps/web/src/config/landing-data.ts +152 -0
  153. package/template/apps/web/src/config/site.ts +5 -0
  154. package/template/apps/web/src/hooks/api/__tests__/use-users.test.tsx +181 -0
  155. package/template/apps/web/src/hooks/api/use-admin-sessions.ts +75 -0
  156. package/template/apps/web/src/hooks/api/use-admin-stats.ts +33 -0
  157. package/template/apps/web/src/hooks/api/use-sessions.ts +52 -0
  158. package/template/apps/web/src/hooks/api/use-uploads.ts +156 -0
  159. package/template/apps/web/src/hooks/api/use-users.ts +149 -0
  160. package/template/apps/web/src/hooks/use-mobile.ts +21 -0
  161. package/template/apps/web/src/hooks/use-realtime-metrics.ts +120 -0
  162. package/template/apps/web/src/lib/__tests__/utils.test.ts +29 -0
  163. package/template/apps/web/src/lib/api.ts +151 -0
  164. package/template/apps/web/src/lib/auth.ts +13 -0
  165. package/template/apps/web/src/lib/env.ts +52 -0
  166. package/template/apps/web/src/lib/form-utils.ts +11 -0
  167. package/template/apps/web/src/lib/utils.ts +1 -0
  168. package/template/apps/web/src/providers.tsx +34 -0
  169. package/template/apps/web/src/store/atoms.ts +15 -0
  170. package/template/apps/web/src/test/helpers/test-utils.tsx +44 -0
  171. package/template/apps/web/src/test/setup.ts +8 -0
  172. package/template/apps/web/tailwind.config.ts +5 -0
  173. package/template/apps/web/tsconfig.json +26 -0
  174. package/template/apps/web/vitest.config.ts +32 -0
  175. package/template/assets/logo-512.png +0 -0
  176. package/template/assets/logo.svg +4 -0
  177. package/template/docker-compose.prod.yml +66 -0
  178. package/template/docker-compose.yml +36 -0
  179. package/template/eslint.config.ts +119 -0
  180. package/template/package.json +77 -0
  181. package/template/packages/tailwind-config/package.json +9 -0
  182. package/template/packages/tailwind-config/theme.css +179 -0
  183. package/template/packages/types/package.json +29 -0
  184. package/template/packages/types/src/__tests__/schemas.test.ts +255 -0
  185. package/template/packages/types/src/api-response.ts +53 -0
  186. package/template/packages/types/src/health-check.ts +11 -0
  187. package/template/packages/types/src/pagination.ts +41 -0
  188. package/template/packages/types/src/role.ts +5 -0
  189. package/template/packages/types/src/session.ts +48 -0
  190. package/template/packages/types/src/stats.ts +113 -0
  191. package/template/packages/types/src/upload.ts +51 -0
  192. package/template/packages/types/src/user.ts +36 -0
  193. package/template/packages/types/tsconfig.json +5 -0
  194. package/template/packages/types/vitest.config.ts +21 -0
  195. package/template/packages/ui/components.json +21 -0
  196. package/template/packages/ui/package.json +108 -0
  197. package/template/packages/ui/src/__tests__/button.test.tsx +70 -0
  198. package/template/packages/ui/src/alert-dialog.tsx +141 -0
  199. package/template/packages/ui/src/alert.tsx +66 -0
  200. package/template/packages/ui/src/animated-theme-toggler.tsx +167 -0
  201. package/template/packages/ui/src/avatar.tsx +53 -0
  202. package/template/packages/ui/src/badge.tsx +36 -0
  203. package/template/packages/ui/src/button.tsx +84 -0
  204. package/template/packages/ui/src/card.tsx +92 -0
  205. package/template/packages/ui/src/checkbox.tsx +32 -0
  206. package/template/packages/ui/src/data-table/data-table-column-header.tsx +68 -0
  207. package/template/packages/ui/src/data-table/data-table-pagination.tsx +99 -0
  208. package/template/packages/ui/src/data-table/data-table-toolbar.tsx +55 -0
  209. package/template/packages/ui/src/data-table/data-table-view-options.tsx +63 -0
  210. package/template/packages/ui/src/data-table/data-table.tsx +167 -0
  211. package/template/packages/ui/src/dialog.tsx +143 -0
  212. package/template/packages/ui/src/dropdown-menu.tsx +257 -0
  213. package/template/packages/ui/src/empty-state.tsx +52 -0
  214. package/template/packages/ui/src/file-upload-input.tsx +202 -0
  215. package/template/packages/ui/src/form.tsx +168 -0
  216. package/template/packages/ui/src/hooks/use-mobile.ts +19 -0
  217. package/template/packages/ui/src/icons/brand-icons.tsx +16 -0
  218. package/template/packages/ui/src/input.tsx +21 -0
  219. package/template/packages/ui/src/label.tsx +24 -0
  220. package/template/packages/ui/src/lib/utils.ts +6 -0
  221. package/template/packages/ui/src/password-input.tsx +102 -0
  222. package/template/packages/ui/src/popover.tsx +48 -0
  223. package/template/packages/ui/src/radio-group.tsx +45 -0
  224. package/template/packages/ui/src/scroll-area.tsx +58 -0
  225. package/template/packages/ui/src/select.tsx +187 -0
  226. package/template/packages/ui/src/separator.tsx +28 -0
  227. package/template/packages/ui/src/sheet.tsx +139 -0
  228. package/template/packages/ui/src/sidebar.tsx +726 -0
  229. package/template/packages/ui/src/skeleton-variants.tsx +87 -0
  230. package/template/packages/ui/src/skeleton.tsx +13 -0
  231. package/template/packages/ui/src/slider.tsx +63 -0
  232. package/template/packages/ui/src/sonner.tsx +25 -0
  233. package/template/packages/ui/src/spinner.tsx +16 -0
  234. package/template/packages/ui/src/switch.tsx +31 -0
  235. package/template/packages/ui/src/table.tsx +116 -0
  236. package/template/packages/ui/src/tabs.tsx +66 -0
  237. package/template/packages/ui/src/textarea.tsx +18 -0
  238. package/template/packages/ui/src/tooltip.tsx +61 -0
  239. package/template/packages/ui/src/user-avatar.tsx +97 -0
  240. package/template/packages/ui/test-config.js +3 -0
  241. package/template/packages/ui/tsconfig.json +12 -0
  242. package/template/packages/ui/turbo.json +18 -0
  243. package/template/packages/ui/vitest.config.ts +17 -0
  244. package/template/packages/ui/vitest.setup.ts +1 -0
  245. package/template/packages/utils/package.json +23 -0
  246. package/template/packages/utils/src/__tests__/utils.test.ts +223 -0
  247. package/template/packages/utils/src/array.ts +18 -0
  248. package/template/packages/utils/src/async.ts +3 -0
  249. package/template/packages/utils/src/date.ts +77 -0
  250. package/template/packages/utils/src/errors.ts +73 -0
  251. package/template/packages/utils/src/number.ts +11 -0
  252. package/template/packages/utils/src/string.ts +13 -0
  253. package/template/packages/utils/tsconfig.json +5 -0
  254. package/template/packages/utils/vitest.config.ts +21 -0
  255. package/template/pnpm-workspace.yaml +4 -0
  256. package/template/tsconfig.base.json +32 -0
  257. package/template/turbo.json +133 -0
  258. package/template/vitest.shared.ts +26 -0
  259. package/template/vitest.workspace.ts +9 -0
@@ -0,0 +1,92 @@
1
+ import {
2
+ QuerySessionsSchema,
3
+ RevokeSessionParamsSchema,
4
+ RevokeUserSessionsParamsSchema,
5
+ } from '@repo/packages-types/session';
6
+ import type { FastifyPluginAsync } from 'fastify';
7
+ import type { ZodTypeProvider } from 'fastify-type-provider-zod';
8
+ import type { z } from 'zod';
9
+
10
+ import { requireAuth, requireRole } from '@/hooks/auth';
11
+
12
+ type RevokeSessionParams = z.infer<typeof RevokeSessionParamsSchema>;
13
+ type RevokeUserSessionsParams = z.infer<typeof RevokeUserSessionsParamsSchema>;
14
+
15
+ const adminSessionsRoutes: FastifyPluginAsync = async (app) => {
16
+ const server = app.withTypeProvider<ZodTypeProvider>();
17
+
18
+ server.get(
19
+ '/admin/sessions',
20
+ {
21
+ preHandler: [requireAuth, requireRole(['admin', 'super_admin'])],
22
+ schema: {
23
+ querystring: QuerySessionsSchema,
24
+ description: 'Get paginated sessions with filtering (Admin only)',
25
+ tags: ['Admin'],
26
+ },
27
+ },
28
+ async (request) => {
29
+ return app.sessionsService.getAdminSessions(request.query);
30
+ }
31
+ );
32
+
33
+ server.get(
34
+ '/admin/sessions/stats',
35
+ {
36
+ preHandler: [requireAuth, requireRole(['admin', 'super_admin'])],
37
+ schema: {
38
+ description: 'Get session statistics (Admin only)',
39
+ tags: ['Admin'],
40
+ },
41
+ },
42
+ async () => {
43
+ return app.sessionsService.getSessionStats();
44
+ }
45
+ );
46
+
47
+ server.delete<{ Params: RevokeSessionParams }>(
48
+ '/admin/sessions/:sessionId',
49
+ {
50
+ preHandler: [requireAuth, requireRole(['admin', 'super_admin'])],
51
+ schema: {
52
+ params: RevokeSessionParamsSchema,
53
+ description: 'Force revoke a session (Admin only)',
54
+ tags: ['Admin'],
55
+ },
56
+ },
57
+ async (request) => {
58
+ const { sessionId } = request.params;
59
+ await app.sessionsService.adminRevokeSession(
60
+ request.user!.id,
61
+ request.user!.role as 'user' | 'admin' | 'super_admin',
62
+ sessionId
63
+ );
64
+
65
+ return { message: 'Session revoked successfully' };
66
+ }
67
+ );
68
+
69
+ server.delete<{ Params: RevokeUserSessionsParams }>(
70
+ '/admin/sessions/user/:userId',
71
+ {
72
+ preHandler: [requireAuth, requireRole(['admin', 'super_admin'])],
73
+ schema: {
74
+ params: RevokeUserSessionsParamsSchema,
75
+ description: 'Revoke all sessions for a user (Admin only)',
76
+ tags: ['Admin'],
77
+ },
78
+ },
79
+ async (request) => {
80
+ const { userId } = request.params;
81
+ const count = await app.sessionsService.adminRevokeUserSessions(
82
+ request.user!.id,
83
+ request.user!.role as 'user' | 'admin' | 'super_admin',
84
+ userId
85
+ );
86
+
87
+ return { message: `Revoked ${count} session(s) for user` };
88
+ }
89
+ );
90
+ };
91
+
92
+ export default adminSessionsRoutes;
@@ -0,0 +1,71 @@
1
+ import type { FastifyPluginAsync } from 'fastify';
2
+
3
+ import { requireAuth, requireRole } from '@/hooks/auth';
4
+ import { metricsService } from '@/services/metrics.service';
5
+
6
+ const metricsRoutes: FastifyPluginAsync = async (app) => {
7
+ app.get(
8
+ '/admin/metrics/stream',
9
+ {
10
+ preHandler: [requireAuth, requireRole(['admin', 'super_admin'])],
11
+ schema: {
12
+ description: 'Stream real-time system metrics via SSE (Admin only)',
13
+ tags: ['Admin'],
14
+ },
15
+ },
16
+ async (request, reply) => {
17
+ reply.raw.writeHead(200, {
18
+ 'Content-Type': 'text/event-stream',
19
+ 'Cache-Control': 'no-cache',
20
+ Connection: 'keep-alive',
21
+ 'Access-Control-Allow-Origin': app.env.FRONTEND_URL,
22
+ 'Access-Control-Allow-Credentials': 'true',
23
+ });
24
+
25
+ const history = metricsService.getHistory();
26
+ if (history.length > 0) {
27
+ reply.raw.write(`event: history\n`);
28
+ reply.raw.write(`data: ${JSON.stringify(history)}\n\n`);
29
+ }
30
+
31
+ const unsubscribe = metricsService.subscribe((metrics) => {
32
+ if (!reply.raw.destroyed) {
33
+ reply.raw.write(`event: metrics\n`);
34
+ reply.raw.write(`data: ${JSON.stringify(metrics)}\n\n`);
35
+ }
36
+ });
37
+
38
+ const heartbeatInterval = setInterval(() => {
39
+ if (!reply.raw.destroyed) {
40
+ reply.raw.write(`:heartbeat\n\n`);
41
+ }
42
+ }, 15000);
43
+
44
+ request.raw.on('close', () => {
45
+ unsubscribe();
46
+ clearInterval(heartbeatInterval);
47
+ });
48
+
49
+ return reply;
50
+ }
51
+ );
52
+
53
+ app.get(
54
+ '/admin/metrics/snapshot',
55
+ {
56
+ preHandler: [requireAuth, requireRole(['admin', 'super_admin'])],
57
+ schema: {
58
+ description: 'Get current metrics snapshot (Admin only)',
59
+ tags: ['Admin'],
60
+ },
61
+ },
62
+ async () => {
63
+ return {
64
+ current: metricsService.getLatest(),
65
+ history: metricsService.getHistory(),
66
+ };
67
+ }
68
+ );
69
+ };
70
+
71
+ export default metricsRoutes;
@@ -0,0 +1,46 @@
1
+ import { ValidationError } from '@repo/packages-utils/errors';
2
+ import type { FastifyPluginAsync } from 'fastify';
3
+ import { z } from 'zod';
4
+
5
+ import { requireAuth } from '@/hooks/auth';
6
+
7
+ const ChangePasswordSchema = z.object({
8
+ currentPassword: z.string().min(8, 'Password must be at least 8 characters'),
9
+ newPassword: z.string().min(8, 'Password must be at least 8 characters'),
10
+ });
11
+
12
+ const passwordRoutes: FastifyPluginAsync = async (app) => {
13
+ app.post<{
14
+ Body: z.infer<typeof ChangePasswordSchema>;
15
+ }>(
16
+ '/password/change',
17
+ {
18
+ preHandler: requireAuth,
19
+ schema: {
20
+ body: ChangePasswordSchema,
21
+ },
22
+ },
23
+ async (request) => {
24
+ const { currentPassword, newPassword } = request.body;
25
+
26
+ if (currentPassword === newPassword) {
27
+ throw new ValidationError(
28
+ 'New password must be different from current password'
29
+ );
30
+ }
31
+
32
+ await app.passwordService.changePassword(
33
+ request.user!.id,
34
+ currentPassword,
35
+ newPassword
36
+ );
37
+
38
+ return {
39
+ message:
40
+ 'Password changed successfully. All other sessions have been revoked.',
41
+ };
42
+ }
43
+ );
44
+ };
45
+
46
+ export default passwordRoutes;
@@ -0,0 +1,53 @@
1
+ import type { FastifyPluginAsync } from 'fastify';
2
+
3
+ import { requireAuth } from '@/hooks/auth';
4
+
5
+ const sessionsRoutes: FastifyPluginAsync = async (app) => {
6
+ app.get(
7
+ '/sessions',
8
+ {
9
+ preHandler: requireAuth,
10
+ },
11
+ async (request) => {
12
+ const sessions = await app.sessionsService.getUserSessions(
13
+ request.user!.id
14
+ );
15
+ return { data: sessions };
16
+ }
17
+ );
18
+
19
+ app.delete<{
20
+ Params: { sessionId: string };
21
+ }>(
22
+ '/sessions/:sessionId',
23
+ {
24
+ preHandler: requireAuth,
25
+ },
26
+ async (request) => {
27
+ await app.sessionsService.revokeSession(
28
+ request.user!.id,
29
+ request.params.sessionId
30
+ );
31
+
32
+ return { message: 'Session revoked successfully' };
33
+ }
34
+ );
35
+
36
+ app.delete(
37
+ '/sessions',
38
+ {
39
+ preHandler: requireAuth,
40
+ },
41
+ async (request) => {
42
+ const currentSessionId = request.user?.session?.id;
43
+ await app.sessionsService.revokeAllSessions(
44
+ request.user!.id,
45
+ currentSessionId
46
+ );
47
+
48
+ return { message: 'All other sessions revoked successfully' };
49
+ }
50
+ );
51
+ };
52
+
53
+ export default sessionsRoutes;
@@ -0,0 +1,38 @@
1
+ import type { FastifyPluginAsync } from 'fastify';
2
+ import type { ZodTypeProvider } from 'fastify-type-provider-zod';
3
+
4
+ import { requireAuth, requireRole } from '@/hooks/auth';
5
+
6
+ const statsRoutes: FastifyPluginAsync = async (app) => {
7
+ const server = app.withTypeProvider<ZodTypeProvider>();
8
+
9
+ server.get(
10
+ '/admin/stats',
11
+ {
12
+ preHandler: [requireAuth, requireRole(['admin', 'super_admin'])],
13
+ schema: {
14
+ description: 'Get comprehensive system statistics (Admin only)',
15
+ tags: ['Admin'],
16
+ },
17
+ },
18
+ async () => {
19
+ return app.statsService.getSystemStats();
20
+ }
21
+ );
22
+
23
+ server.get(
24
+ '/admin/stats/health',
25
+ {
26
+ preHandler: [requireAuth, requireRole(['admin', 'super_admin'])],
27
+ schema: {
28
+ description: 'Get system health check with DB latency (Admin only)',
29
+ tags: ['Admin'],
30
+ },
31
+ },
32
+ async () => {
33
+ return app.statsService.getHealthCheck();
34
+ }
35
+ );
36
+ };
37
+
38
+ export default statsRoutes;
@@ -0,0 +1,27 @@
1
+ import path from 'node:path';
2
+
3
+ import fastifyStatic from '@fastify/static';
4
+ import type { FastifyPluginAsync } from 'fastify';
5
+
6
+ const uploadsServeRoutes: FastifyPluginAsync = async (app) => {
7
+ const storageType = app.fileStorageService.getStorageType();
8
+
9
+ // Only serve local files if using local storage
10
+ if (storageType === 'local') {
11
+ const uploadsDir = path.join(process.cwd(), 'public', 'uploads');
12
+
13
+ await app.register(fastifyStatic, {
14
+ root: uploadsDir,
15
+ prefix: '/uploads/files/',
16
+ decorateReply: false,
17
+ });
18
+
19
+ app.log.info('[+] Local file serving configured at /uploads/files/');
20
+ } else {
21
+ app.log.info(
22
+ `[+] Using ${storageType} storage - local file serving disabled`
23
+ );
24
+ }
25
+ };
26
+
27
+ export default uploadsServeRoutes;
@@ -0,0 +1,154 @@
1
+ import type { MultipartFile } from '@fastify/multipart';
2
+ import {
3
+ DeleteUploadParamsSchema,
4
+ GetUploadsQuerySchema,
5
+ UploadResponseSchema,
6
+ UploadStatsSchema,
7
+ } from '@repo/packages-types/upload';
8
+ import {
9
+ UnauthorizedError,
10
+ ValidationError,
11
+ } from '@repo/packages-utils/errors';
12
+ import type { FastifyPluginAsync } from 'fastify';
13
+ import type { ZodTypeProvider } from 'fastify-type-provider-zod';
14
+ import { z } from 'zod';
15
+
16
+ import { RATE_LIMIT_CONFIG } from '@/config/rate-limit.js';
17
+ import { requireAuth } from '@/hooks/auth';
18
+
19
+ const uploadsRoutes: FastifyPluginAsync = async (app) => {
20
+ const server = app.withTypeProvider<ZodTypeProvider>();
21
+
22
+ // POST /api/uploads - Upload a file
23
+ server.post(
24
+ '/uploads',
25
+ {
26
+ onRequest: requireAuth,
27
+ config: {
28
+ rateLimit: {
29
+ max: RATE_LIMIT_CONFIG.routes.uploads.max,
30
+ timeWindow: RATE_LIMIT_CONFIG.routes.uploads.timeWindow,
31
+ },
32
+ },
33
+ schema: {
34
+ description: 'Upload a file',
35
+ tags: ['Uploads'],
36
+ response: {
37
+ 201: UploadResponseSchema,
38
+ },
39
+ },
40
+ },
41
+ async (request, reply) => {
42
+ const userId = request.user?.id;
43
+
44
+ if (!userId) {
45
+ throw new UnauthorizedError();
46
+ }
47
+
48
+ const data = await request.file();
49
+
50
+ if (!data) {
51
+ throw new ValidationError('No file provided');
52
+ }
53
+
54
+ const file = data as MultipartFile;
55
+
56
+ const upload = await app.uploadsService.uploadFile(file, userId);
57
+
58
+ return reply.status(201).send(upload);
59
+ }
60
+ );
61
+
62
+ // GET /api/uploads - Get user's uploads
63
+ server.get(
64
+ '/uploads',
65
+ {
66
+ onRequest: requireAuth,
67
+ schema: {
68
+ querystring: GetUploadsQuerySchema,
69
+ description: 'Get paginated list of user uploads',
70
+ tags: ['Uploads'],
71
+ response: {
72
+ 200: z.array(
73
+ UploadResponseSchema.omit({ user: true }).extend({
74
+ userId: z.string(),
75
+ })
76
+ ),
77
+ },
78
+ },
79
+ },
80
+ async (request) => {
81
+ const userId = request.user?.id;
82
+
83
+ if (!userId) {
84
+ throw new UnauthorizedError();
85
+ }
86
+
87
+ const { limit, offset } = request.query;
88
+
89
+ const uploads = await app.uploadsService.getUserUploads(userId, {
90
+ limit,
91
+ offset,
92
+ });
93
+
94
+ return uploads;
95
+ }
96
+ );
97
+
98
+ // GET /api/uploads/stats - Get upload statistics
99
+ server.get(
100
+ '/uploads/stats',
101
+ {
102
+ onRequest: requireAuth,
103
+ schema: {
104
+ description: 'Get upload statistics for current user',
105
+ tags: ['Uploads'],
106
+ response: {
107
+ 200: UploadStatsSchema,
108
+ },
109
+ },
110
+ },
111
+ async (request) => {
112
+ const userId = request.user?.id;
113
+
114
+ if (!userId) {
115
+ throw new UnauthorizedError();
116
+ }
117
+
118
+ const stats = await app.uploadsService.getUploadStats(userId);
119
+
120
+ return stats;
121
+ }
122
+ );
123
+
124
+ // DELETE /api/uploads/:id - Delete an upload
125
+ server.delete(
126
+ '/uploads/:id',
127
+ {
128
+ onRequest: requireAuth,
129
+ schema: {
130
+ params: DeleteUploadParamsSchema,
131
+ description: 'Delete an upload',
132
+ tags: ['Uploads'],
133
+ response: {
134
+ 204: z.void(),
135
+ },
136
+ },
137
+ },
138
+ async (request, reply) => {
139
+ const userId = request.user?.id;
140
+
141
+ if (!userId) {
142
+ throw new UnauthorizedError();
143
+ }
144
+
145
+ const { id } = request.params;
146
+
147
+ await app.uploadsService.deleteUpload(id, userId);
148
+
149
+ return reply.status(204).send();
150
+ }
151
+ );
152
+ };
153
+
154
+ export default uploadsRoutes;
@@ -0,0 +1,114 @@
1
+ import { QueryUsersSchema } from '@repo/packages-types/pagination';
2
+ import {
3
+ CreateUserSchema,
4
+ GetUserByIdSchema,
5
+ UpdateUserSchema,
6
+ } from '@repo/packages-types/user';
7
+ import type { FastifyPluginAsync } from 'fastify';
8
+ import type { ZodTypeProvider } from 'fastify-type-provider-zod';
9
+
10
+ import { requireAuth, requireRole } from '@/hooks/auth';
11
+
12
+ const usersRoutes: FastifyPluginAsync = async (app) => {
13
+ const server = app.withTypeProvider<ZodTypeProvider>();
14
+
15
+ // GET /users - Get paginated users with filtering and sorting (Admin only)
16
+ server.get(
17
+ '/users',
18
+ {
19
+ preHandler: [requireAuth, requireRole(['admin', 'super_admin'])],
20
+ schema: {
21
+ querystring: QueryUsersSchema,
22
+ description:
23
+ 'Get paginated users with filtering and sorting (Admin only)',
24
+ tags: ['Users'],
25
+ },
26
+ },
27
+ async (request) => {
28
+ return app.usersService.getUsers(request.query);
29
+ }
30
+ );
31
+
32
+ // GET /users/:id - Get user by ID (Admin only)
33
+ server.get(
34
+ '/users/:id',
35
+ {
36
+ preHandler: [requireAuth, requireRole(['admin', 'super_admin'])],
37
+ schema: {
38
+ params: GetUserByIdSchema,
39
+ description: 'Get user by ID (Admin only)',
40
+ tags: ['Users'],
41
+ },
42
+ },
43
+ async (request) => {
44
+ const user = await app.usersService.getUserById(request.params.id);
45
+ return { data: user };
46
+ }
47
+ );
48
+
49
+ // POST /users - Create a new user (Admin only)
50
+ server.post(
51
+ '/users',
52
+ {
53
+ preHandler: [requireAuth, requireRole(['admin', 'super_admin'])],
54
+ schema: {
55
+ body: CreateUserSchema,
56
+ description: 'Create a new user (Admin only)',
57
+ tags: ['Users'],
58
+ },
59
+ },
60
+ async (request, reply) => {
61
+ const user = await app.usersService.createUser(request.body);
62
+
63
+ return reply.status(201).send({ data: user });
64
+ }
65
+ );
66
+
67
+ // PATCH /users/:id - Update user by ID (Admin only)
68
+ server.patch(
69
+ '/users/:id',
70
+ {
71
+ preHandler: [requireAuth, requireRole(['admin', 'super_admin'])],
72
+ schema: {
73
+ params: GetUserByIdSchema,
74
+ body: UpdateUserSchema,
75
+ description: 'Update user by ID (Admin only)',
76
+ tags: ['Users'],
77
+ },
78
+ },
79
+ async (request) => {
80
+ const user = await app.usersService.updateUser(
81
+ request.user!.id,
82
+ request.user!.role as 'user' | 'admin' | 'super_admin',
83
+ request.params.id,
84
+ request.body
85
+ );
86
+
87
+ return { data: user };
88
+ }
89
+ );
90
+
91
+ // DELETE /users/:id - Delete user by ID (Admin only)
92
+ server.delete(
93
+ '/users/:id',
94
+ {
95
+ preHandler: [requireAuth, requireRole(['admin', 'super_admin'])],
96
+ schema: {
97
+ params: GetUserByIdSchema,
98
+ description: 'Delete user by ID (Admin only)',
99
+ tags: ['Users'],
100
+ },
101
+ },
102
+ async (request) => {
103
+ await app.usersService.deleteUser(
104
+ request.user!.id,
105
+ request.user!.role as 'user' | 'admin' | 'super_admin',
106
+ request.params.id
107
+ );
108
+
109
+ return { message: 'User deleted successfully' };
110
+ }
111
+ );
112
+ };
113
+
114
+ export default usersRoutes;
@@ -0,0 +1,90 @@
1
+ import type { FastifyPluginAsync } from 'fastify';
2
+ import type { ZodTypeProvider } from 'fastify-type-provider-zod';
3
+ import { z } from 'zod';
4
+
5
+ import { requireAuth } from '@/hooks/auth';
6
+
7
+ const ResendVerificationSchema = z.object({
8
+ email: z.string().email().optional(),
9
+ });
10
+
11
+ const verificationRoutes: FastifyPluginAsync = async (app) => {
12
+ const server = app.withTypeProvider<ZodTypeProvider>();
13
+
14
+ // POST /verification/resend - Resend verification email
15
+ server.post(
16
+ '/verification/resend',
17
+ {
18
+ preHandler: [requireAuth],
19
+ schema: {
20
+ body: ResendVerificationSchema,
21
+ description:
22
+ 'Resend verification email to authenticated user or specified email',
23
+ tags: ['Verification'],
24
+ },
25
+ },
26
+ async (request, reply) => {
27
+ const currentUser = request.user;
28
+ if (!currentUser) {
29
+ return reply.status(401).send({
30
+ error: 'Unauthorized',
31
+ message: 'Authentication required',
32
+ });
33
+ }
34
+
35
+ const targetEmail = request.body.email || currentUser.email;
36
+
37
+ if (request.body.email && request.body.email !== currentUser.email) {
38
+ return reply.status(403).send({
39
+ error: 'Forbidden',
40
+ message: 'You can only resend verification for your own email',
41
+ });
42
+ }
43
+
44
+ const user = await app.prisma.user.findUnique({
45
+ where: { email: targetEmail },
46
+ });
47
+
48
+ if (!user) {
49
+ return reply.status(404).send({
50
+ error: 'Not Found',
51
+ message: 'User not found',
52
+ });
53
+ }
54
+
55
+ if (user.emailVerified) {
56
+ return reply.status(400).send({
57
+ error: 'Bad Request',
58
+ message: 'Email is already verified',
59
+ });
60
+ }
61
+
62
+ try {
63
+ await app.auth.api.sendVerificationEmail({
64
+ body: {
65
+ email: user.email,
66
+ callbackURL: app.env.FRONTEND_URL,
67
+ },
68
+ });
69
+
70
+ return { message: 'Verification email sent successfully' };
71
+ } catch (error) {
72
+ request.log.error(
73
+ {
74
+ err: error instanceof Error ? error : new Error(String(error)),
75
+ email: user.email,
76
+ },
77
+ 'Failed to send verification email via Better Auth'
78
+ );
79
+
80
+ return reply.status(500).send({
81
+ error: 'Email Send Failed',
82
+ message:
83
+ 'Failed to send verification email. Please try again later or contact support.',
84
+ });
85
+ }
86
+ }
87
+ );
88
+ };
89
+
90
+ export default verificationRoutes;