create-blitzpack 0.1.0 → 0.1.1

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 +35 -77
  2. package/package.json +5 -6
  3. package/template/.dockerignore +0 -59
  4. package/template/.github/workflows/ci.yml +0 -157
  5. package/template/.husky/pre-commit +0 -1
  6. package/template/.husky/pre-push +0 -1
  7. package/template/.lintstagedrc.cjs +0 -4
  8. package/template/.nvmrc +0 -1
  9. package/template/.prettierrc +0 -9
  10. package/template/.vscode/settings.json +0 -13
  11. package/template/CLAUDE.md +0 -175
  12. package/template/CONTRIBUTING.md +0 -32
  13. package/template/Dockerfile +0 -90
  14. package/template/GETTING_STARTED.md +0 -35
  15. package/template/LICENSE +0 -21
  16. package/template/README.md +0 -116
  17. package/template/apps/api/.dockerignore +0 -51
  18. package/template/apps/api/.env.local.example +0 -62
  19. package/template/apps/api/emails/account-deleted-email.tsx +0 -69
  20. package/template/apps/api/emails/components/email-layout.tsx +0 -154
  21. package/template/apps/api/emails/config.ts +0 -22
  22. package/template/apps/api/emails/password-changed-email.tsx +0 -88
  23. package/template/apps/api/emails/password-reset-email.tsx +0 -86
  24. package/template/apps/api/emails/verification-email.tsx +0 -85
  25. package/template/apps/api/emails/welcome-email.tsx +0 -70
  26. package/template/apps/api/package.json +0 -84
  27. package/template/apps/api/prisma/migrations/20251012111439_init/migration.sql +0 -13
  28. package/template/apps/api/prisma/migrations/20251018162629_add_better_auth_fields/migration.sql +0 -67
  29. package/template/apps/api/prisma/migrations/20251019142208_add_user_role_enum/migration.sql +0 -5
  30. package/template/apps/api/prisma/migrations/20251019182151_user_auth/migration.sql +0 -7
  31. package/template/apps/api/prisma/migrations/20251019211416_faster_session_lookup/migration.sql +0 -2
  32. package/template/apps/api/prisma/migrations/20251119124337_add_upload_model/migration.sql +0 -26
  33. package/template/apps/api/prisma/migrations/20251120071241_add_scope_to_account/migration.sql +0 -2
  34. package/template/apps/api/prisma/migrations/20251120072608_add_oauth_token_expiration_fields/migration.sql +0 -10
  35. package/template/apps/api/prisma/migrations/20251120144705_add_audit_logs/migration.sql +0 -29
  36. package/template/apps/api/prisma/migrations/20251127123614_remove_impersonated_by/migration.sql +0 -8
  37. package/template/apps/api/prisma/migrations/20251127125630_remove_audit_logs/migration.sql +0 -11
  38. package/template/apps/api/prisma/migrations/migration_lock.toml +0 -3
  39. package/template/apps/api/prisma/schema.prisma +0 -116
  40. package/template/apps/api/prisma/seed.ts +0 -159
  41. package/template/apps/api/prisma.config.ts +0 -14
  42. package/template/apps/api/src/app.ts +0 -377
  43. package/template/apps/api/src/common/logger.service.ts +0 -227
  44. package/template/apps/api/src/config/env.ts +0 -60
  45. package/template/apps/api/src/config/rate-limit.ts +0 -29
  46. package/template/apps/api/src/hooks/auth.ts +0 -122
  47. package/template/apps/api/src/plugins/auth.ts +0 -198
  48. package/template/apps/api/src/plugins/database.ts +0 -45
  49. package/template/apps/api/src/plugins/logger.ts +0 -33
  50. package/template/apps/api/src/plugins/multipart.ts +0 -16
  51. package/template/apps/api/src/plugins/scalar.ts +0 -20
  52. package/template/apps/api/src/plugins/schedule.ts +0 -52
  53. package/template/apps/api/src/plugins/services.ts +0 -66
  54. package/template/apps/api/src/plugins/swagger.ts +0 -56
  55. package/template/apps/api/src/routes/accounts.ts +0 -91
  56. package/template/apps/api/src/routes/admin-sessions.ts +0 -92
  57. package/template/apps/api/src/routes/metrics.ts +0 -71
  58. package/template/apps/api/src/routes/password.ts +0 -46
  59. package/template/apps/api/src/routes/sessions.ts +0 -53
  60. package/template/apps/api/src/routes/stats.ts +0 -38
  61. package/template/apps/api/src/routes/uploads-serve.ts +0 -27
  62. package/template/apps/api/src/routes/uploads.ts +0 -154
  63. package/template/apps/api/src/routes/users.ts +0 -114
  64. package/template/apps/api/src/routes/verification.ts +0 -90
  65. package/template/apps/api/src/server.ts +0 -34
  66. package/template/apps/api/src/services/accounts.service.ts +0 -125
  67. package/template/apps/api/src/services/authorization.service.ts +0 -162
  68. package/template/apps/api/src/services/email.service.ts +0 -170
  69. package/template/apps/api/src/services/file-storage.service.ts +0 -267
  70. package/template/apps/api/src/services/metrics.service.ts +0 -175
  71. package/template/apps/api/src/services/password.service.ts +0 -56
  72. package/template/apps/api/src/services/sessions.service.spec.ts +0 -134
  73. package/template/apps/api/src/services/sessions.service.ts +0 -276
  74. package/template/apps/api/src/services/stats.service.ts +0 -273
  75. package/template/apps/api/src/services/uploads.service.ts +0 -163
  76. package/template/apps/api/src/services/users.service.spec.ts +0 -249
  77. package/template/apps/api/src/services/users.service.ts +0 -198
  78. package/template/apps/api/src/utils/file-validation.ts +0 -108
  79. package/template/apps/api/start.sh +0 -33
  80. package/template/apps/api/test/helpers/fastify-app.ts +0 -24
  81. package/template/apps/api/test/helpers/mock-authorization.ts +0 -16
  82. package/template/apps/api/test/helpers/mock-logger.ts +0 -28
  83. package/template/apps/api/test/helpers/mock-prisma.ts +0 -30
  84. package/template/apps/api/test/helpers/test-db.ts +0 -125
  85. package/template/apps/api/test/integration/auth-flow.integration.spec.ts +0 -449
  86. package/template/apps/api/test/integration/password.integration.spec.ts +0 -427
  87. package/template/apps/api/test/integration/rate-limit.integration.spec.ts +0 -51
  88. package/template/apps/api/test/integration/sessions.integration.spec.ts +0 -445
  89. package/template/apps/api/test/integration/users.integration.spec.ts +0 -211
  90. package/template/apps/api/test/setup.ts +0 -31
  91. package/template/apps/api/tsconfig.json +0 -26
  92. package/template/apps/api/vitest.config.ts +0 -35
  93. package/template/apps/web/.env.local.example +0 -11
  94. package/template/apps/web/components.json +0 -24
  95. package/template/apps/web/next.config.ts +0 -22
  96. package/template/apps/web/package.json +0 -56
  97. package/template/apps/web/postcss.config.js +0 -5
  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 +0 -3
  101. package/template/apps/web/src/app/(admin)/admin/layout.tsx +0 -222
  102. package/template/apps/web/src/app/(admin)/admin/page.tsx +0 -157
  103. package/template/apps/web/src/app/(admin)/admin/sessions/page.tsx +0 -18
  104. package/template/apps/web/src/app/(admin)/admin/users/page.tsx +0 -20
  105. package/template/apps/web/src/app/(auth)/forgot-password/page.tsx +0 -177
  106. package/template/apps/web/src/app/(auth)/login/page.tsx +0 -159
  107. package/template/apps/web/src/app/(auth)/reset-password/page.tsx +0 -245
  108. package/template/apps/web/src/app/(auth)/signup/page.tsx +0 -153
  109. package/template/apps/web/src/app/dashboard/change-password/page.tsx +0 -255
  110. package/template/apps/web/src/app/dashboard/page.tsx +0 -296
  111. package/template/apps/web/src/app/error.tsx +0 -32
  112. package/template/apps/web/src/app/examples/file-upload/page.tsx +0 -200
  113. package/template/apps/web/src/app/favicon.ico +0 -0
  114. package/template/apps/web/src/app/global-error.tsx +0 -96
  115. package/template/apps/web/src/app/globals.css +0 -22
  116. package/template/apps/web/src/app/icon.png +0 -0
  117. package/template/apps/web/src/app/layout.tsx +0 -34
  118. package/template/apps/web/src/app/not-found.tsx +0 -28
  119. package/template/apps/web/src/app/page.tsx +0 -192
  120. package/template/apps/web/src/components/admin/activity-feed.tsx +0 -101
  121. package/template/apps/web/src/components/admin/charts/auth-breakdown-chart.tsx +0 -114
  122. package/template/apps/web/src/components/admin/charts/chart-tooltip.tsx +0 -124
  123. package/template/apps/web/src/components/admin/charts/realtime-metrics-chart.tsx +0 -511
  124. package/template/apps/web/src/components/admin/charts/role-distribution-chart.tsx +0 -102
  125. package/template/apps/web/src/components/admin/charts/session-activity-chart.tsx +0 -90
  126. package/template/apps/web/src/components/admin/charts/user-growth-chart.tsx +0 -108
  127. package/template/apps/web/src/components/admin/health-indicator.tsx +0 -175
  128. package/template/apps/web/src/components/admin/refresh-control.tsx +0 -90
  129. package/template/apps/web/src/components/admin/session-revoke-all-dialog.tsx +0 -79
  130. package/template/apps/web/src/components/admin/session-revoke-dialog.tsx +0 -74
  131. package/template/apps/web/src/components/admin/sessions-management-table.tsx +0 -372
  132. package/template/apps/web/src/components/admin/stat-card.tsx +0 -137
  133. package/template/apps/web/src/components/admin/user-create-dialog.tsx +0 -152
  134. package/template/apps/web/src/components/admin/user-delete-dialog.tsx +0 -73
  135. package/template/apps/web/src/components/admin/user-edit-dialog.tsx +0 -170
  136. package/template/apps/web/src/components/admin/users-management-table.tsx +0 -285
  137. package/template/apps/web/src/components/auth/email-verification-banner.tsx +0 -85
  138. package/template/apps/web/src/components/auth/github-button.tsx +0 -40
  139. package/template/apps/web/src/components/auth/google-button.tsx +0 -54
  140. package/template/apps/web/src/components/auth/protected-route.tsx +0 -66
  141. package/template/apps/web/src/components/auth/redirect-if-authenticated.tsx +0 -31
  142. package/template/apps/web/src/components/auth/with-auth.tsx +0 -30
  143. package/template/apps/web/src/components/error/error-card.tsx +0 -47
  144. package/template/apps/web/src/components/error/forbidden.tsx +0 -25
  145. package/template/apps/web/src/components/landing/command-block.tsx +0 -64
  146. package/template/apps/web/src/components/landing/feature-card.tsx +0 -60
  147. package/template/apps/web/src/components/landing/included-feature-card.tsx +0 -63
  148. package/template/apps/web/src/components/landing/logo.tsx +0 -41
  149. package/template/apps/web/src/components/landing/tech-badge.tsx +0 -11
  150. package/template/apps/web/src/components/layout/auth-nav.tsx +0 -58
  151. package/template/apps/web/src/components/layout/footer.tsx +0 -3
  152. package/template/apps/web/src/config/landing-data.ts +0 -152
  153. package/template/apps/web/src/config/site.ts +0 -5
  154. package/template/apps/web/src/hooks/api/__tests__/use-users.test.tsx +0 -181
  155. package/template/apps/web/src/hooks/api/use-admin-sessions.ts +0 -75
  156. package/template/apps/web/src/hooks/api/use-admin-stats.ts +0 -33
  157. package/template/apps/web/src/hooks/api/use-sessions.ts +0 -52
  158. package/template/apps/web/src/hooks/api/use-uploads.ts +0 -156
  159. package/template/apps/web/src/hooks/api/use-users.ts +0 -149
  160. package/template/apps/web/src/hooks/use-mobile.ts +0 -21
  161. package/template/apps/web/src/hooks/use-realtime-metrics.ts +0 -120
  162. package/template/apps/web/src/lib/__tests__/utils.test.ts +0 -29
  163. package/template/apps/web/src/lib/api.ts +0 -151
  164. package/template/apps/web/src/lib/auth.ts +0 -13
  165. package/template/apps/web/src/lib/env.ts +0 -52
  166. package/template/apps/web/src/lib/form-utils.ts +0 -11
  167. package/template/apps/web/src/lib/utils.ts +0 -1
  168. package/template/apps/web/src/providers.tsx +0 -34
  169. package/template/apps/web/src/store/atoms.ts +0 -15
  170. package/template/apps/web/src/test/helpers/test-utils.tsx +0 -44
  171. package/template/apps/web/src/test/setup.ts +0 -8
  172. package/template/apps/web/tailwind.config.ts +0 -5
  173. package/template/apps/web/tsconfig.json +0 -26
  174. package/template/apps/web/vitest.config.ts +0 -32
  175. package/template/assets/logo-512.png +0 -0
  176. package/template/assets/logo.svg +0 -4
  177. package/template/docker-compose.prod.yml +0 -66
  178. package/template/docker-compose.yml +0 -36
  179. package/template/eslint.config.ts +0 -119
  180. package/template/package.json +0 -77
  181. package/template/packages/tailwind-config/package.json +0 -9
  182. package/template/packages/tailwind-config/theme.css +0 -179
  183. package/template/packages/types/package.json +0 -29
  184. package/template/packages/types/src/__tests__/schemas.test.ts +0 -255
  185. package/template/packages/types/src/api-response.ts +0 -53
  186. package/template/packages/types/src/health-check.ts +0 -11
  187. package/template/packages/types/src/pagination.ts +0 -41
  188. package/template/packages/types/src/role.ts +0 -5
  189. package/template/packages/types/src/session.ts +0 -48
  190. package/template/packages/types/src/stats.ts +0 -113
  191. package/template/packages/types/src/upload.ts +0 -51
  192. package/template/packages/types/src/user.ts +0 -36
  193. package/template/packages/types/tsconfig.json +0 -5
  194. package/template/packages/types/vitest.config.ts +0 -21
  195. package/template/packages/ui/components.json +0 -21
  196. package/template/packages/ui/package.json +0 -108
  197. package/template/packages/ui/src/__tests__/button.test.tsx +0 -70
  198. package/template/packages/ui/src/alert-dialog.tsx +0 -141
  199. package/template/packages/ui/src/alert.tsx +0 -66
  200. package/template/packages/ui/src/animated-theme-toggler.tsx +0 -167
  201. package/template/packages/ui/src/avatar.tsx +0 -53
  202. package/template/packages/ui/src/badge.tsx +0 -36
  203. package/template/packages/ui/src/button.tsx +0 -84
  204. package/template/packages/ui/src/card.tsx +0 -92
  205. package/template/packages/ui/src/checkbox.tsx +0 -32
  206. package/template/packages/ui/src/data-table/data-table-column-header.tsx +0 -68
  207. package/template/packages/ui/src/data-table/data-table-pagination.tsx +0 -99
  208. package/template/packages/ui/src/data-table/data-table-toolbar.tsx +0 -55
  209. package/template/packages/ui/src/data-table/data-table-view-options.tsx +0 -63
  210. package/template/packages/ui/src/data-table/data-table.tsx +0 -167
  211. package/template/packages/ui/src/dialog.tsx +0 -143
  212. package/template/packages/ui/src/dropdown-menu.tsx +0 -257
  213. package/template/packages/ui/src/empty-state.tsx +0 -52
  214. package/template/packages/ui/src/file-upload-input.tsx +0 -202
  215. package/template/packages/ui/src/form.tsx +0 -168
  216. package/template/packages/ui/src/hooks/use-mobile.ts +0 -19
  217. package/template/packages/ui/src/icons/brand-icons.tsx +0 -16
  218. package/template/packages/ui/src/input.tsx +0 -21
  219. package/template/packages/ui/src/label.tsx +0 -24
  220. package/template/packages/ui/src/lib/utils.ts +0 -6
  221. package/template/packages/ui/src/password-input.tsx +0 -102
  222. package/template/packages/ui/src/popover.tsx +0 -48
  223. package/template/packages/ui/src/radio-group.tsx +0 -45
  224. package/template/packages/ui/src/scroll-area.tsx +0 -58
  225. package/template/packages/ui/src/select.tsx +0 -187
  226. package/template/packages/ui/src/separator.tsx +0 -28
  227. package/template/packages/ui/src/sheet.tsx +0 -139
  228. package/template/packages/ui/src/sidebar.tsx +0 -726
  229. package/template/packages/ui/src/skeleton-variants.tsx +0 -87
  230. package/template/packages/ui/src/skeleton.tsx +0 -13
  231. package/template/packages/ui/src/slider.tsx +0 -63
  232. package/template/packages/ui/src/sonner.tsx +0 -25
  233. package/template/packages/ui/src/spinner.tsx +0 -16
  234. package/template/packages/ui/src/switch.tsx +0 -31
  235. package/template/packages/ui/src/table.tsx +0 -116
  236. package/template/packages/ui/src/tabs.tsx +0 -66
  237. package/template/packages/ui/src/textarea.tsx +0 -18
  238. package/template/packages/ui/src/tooltip.tsx +0 -61
  239. package/template/packages/ui/src/user-avatar.tsx +0 -97
  240. package/template/packages/ui/test-config.js +0 -3
  241. package/template/packages/ui/tsconfig.json +0 -12
  242. package/template/packages/ui/turbo.json +0 -18
  243. package/template/packages/ui/vitest.config.ts +0 -17
  244. package/template/packages/ui/vitest.setup.ts +0 -1
  245. package/template/packages/utils/package.json +0 -23
  246. package/template/packages/utils/src/__tests__/utils.test.ts +0 -223
  247. package/template/packages/utils/src/array.ts +0 -18
  248. package/template/packages/utils/src/async.ts +0 -3
  249. package/template/packages/utils/src/date.ts +0 -77
  250. package/template/packages/utils/src/errors.ts +0 -73
  251. package/template/packages/utils/src/number.ts +0 -11
  252. package/template/packages/utils/src/string.ts +0 -13
  253. package/template/packages/utils/tsconfig.json +0 -5
  254. package/template/packages/utils/vitest.config.ts +0 -21
  255. package/template/pnpm-workspace.yaml +0 -4
  256. package/template/tsconfig.base.json +0 -32
  257. package/template/turbo.json +0 -133
  258. package/template/vitest.shared.ts +0 -26
  259. package/template/vitest.workspace.ts +0 -9
@@ -1,154 +0,0 @@
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;
@@ -1,114 +0,0 @@
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;
@@ -1,90 +0,0 @@
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;
@@ -1,34 +0,0 @@
1
- import closeWithGrace from 'close-with-grace';
2
-
3
- import { app } from '@/app';
4
- import { loadEnv } from '@/config/env';
5
-
6
- const env = loadEnv();
7
-
8
- const start = async () => {
9
- try {
10
- await app.listen({ port: env.PORT, host: '0.0.0.0' });
11
- app.log.info(`API server ready at ${env.API_URL}`);
12
- app.log.info(`Environment: ${env.NODE_ENV}`);
13
- app.log.info(`CORS enabled for: ${env.FRONTEND_URL}`);
14
- } catch (err) {
15
- app.log.error(err);
16
- process.exit(1);
17
- }
18
- };
19
-
20
- const closeListeners = closeWithGrace(
21
- { delay: Number(process.env.FASTIFY_CLOSE_GRACE_DELAY) || 500 },
22
- async ({ err }) => {
23
- if (err) {
24
- app.log.error(err);
25
- }
26
- await app.close();
27
- }
28
- );
29
-
30
- app.addHook('onClose', async () => {
31
- closeListeners.uninstall();
32
- });
33
-
34
- start();
@@ -1,125 +0,0 @@
1
- import { NotFoundError, ValidationError } from '@repo/packages-utils/errors';
2
-
3
- import { type LoggerService } from '@/common/logger.service';
4
- import type { PrismaClient } from '@/generated/client/client.js';
5
-
6
- export interface ConnectedAccount {
7
- providerId: string;
8
- accountId: string;
9
- connectedAt: Date;
10
- scope?: string;
11
- }
12
-
13
- export interface UserAccounts {
14
- userId: string;
15
- hasPassword: boolean;
16
- connectedAccounts: ConnectedAccount[];
17
- }
18
-
19
- export class AccountsService {
20
- constructor(
21
- private readonly prisma: PrismaClient,
22
- private readonly logger: LoggerService
23
- ) {
24
- this.logger.setContext('AccountsService');
25
- }
26
-
27
- async getUserAccounts(userId: string): Promise<UserAccounts> {
28
- this.logger.info('Fetching user accounts', { userId });
29
-
30
- const accounts = await this.prisma.account.findMany({
31
- where: { userId },
32
- select: {
33
- providerId: true,
34
- accountId: true,
35
- createdAt: true,
36
- scope: true,
37
- password: true,
38
- },
39
- });
40
-
41
- if (accounts.length === 0) {
42
- throw new NotFoundError('User has no connected accounts');
43
- }
44
-
45
- const credentialAccount = accounts.find(
46
- (a) => a.providerId === 'credential'
47
- );
48
- const hasPassword = !!(credentialAccount && credentialAccount.password);
49
-
50
- const connectedAccounts: ConnectedAccount[] = accounts
51
- .filter((a) => a.providerId !== 'credential')
52
- .map((a) => ({
53
- providerId: a.providerId,
54
- accountId: a.accountId,
55
- connectedAt: a.createdAt,
56
- scope: a.scope || undefined,
57
- }));
58
-
59
- if (hasPassword) {
60
- connectedAccounts.unshift({
61
- providerId: 'credential',
62
- accountId: credentialAccount!.accountId,
63
- connectedAt: credentialAccount!.createdAt,
64
- });
65
- }
66
-
67
- return {
68
- userId,
69
- hasPassword,
70
- connectedAccounts,
71
- };
72
- }
73
-
74
- async unlinkAccount(
75
- userId: string,
76
- providerId: string
77
- ): Promise<{ success: boolean }> {
78
- this.logger.info('Unlinking account', { userId, providerId });
79
-
80
- const accounts = await this.prisma.account.findMany({
81
- where: { userId },
82
- });
83
-
84
- if (accounts.length <= 1) {
85
- throw new ValidationError(
86
- 'Cannot unlink the only account. User must have at least one login method.'
87
- );
88
- }
89
-
90
- if (providerId === 'credential') {
91
- throw new ValidationError(
92
- 'Cannot unlink password login. Please change your password or contact support.'
93
- );
94
- }
95
-
96
- const account = accounts.find((a) => a.providerId === providerId);
97
- if (!account) {
98
- throw new NotFoundError(
99
- `Account with provider ${providerId} not found for this user`
100
- );
101
- }
102
-
103
- await this.prisma.account.delete({
104
- where: { id: account.id },
105
- });
106
-
107
- this.logger.info('Account unlinked successfully', { userId, providerId });
108
-
109
- return { success: true };
110
- }
111
-
112
- async canChangePassword(userId: string): Promise<boolean> {
113
- const credentialAccount = await this.prisma.account.findFirst({
114
- where: {
115
- userId,
116
- providerId: 'credential',
117
- },
118
- select: {
119
- password: true,
120
- },
121
- });
122
-
123
- return !!(credentialAccount && credentialAccount.password);
124
- }
125
- }
@@ -1,162 +0,0 @@
1
- import type { Role } from '@repo/packages-types/role';
2
- import { ForbiddenError } from '@repo/packages-utils/errors';
3
-
4
- import type { LoggerService } from '@/common/logger.service';
5
-
6
- export interface AuthorizationContext {
7
- actorId: string;
8
- actorRole: Role;
9
- targetUserId?: string;
10
- targetUserRole?: Role;
11
- }
12
-
13
- export class AuthorizationService {
14
- private readonly roleHierarchy: Record<Role, number> = {
15
- super_admin: 3,
16
- admin: 2,
17
- user: 1,
18
- };
19
-
20
- constructor(private readonly logger: LoggerService) {
21
- this.logger.setContext('AuthorizationService');
22
- }
23
-
24
- private getRoleLevel(role: Role): number {
25
- return this.roleHierarchy[role];
26
- }
27
-
28
- canModifyUser(actorRole: Role, targetRole: Role): boolean {
29
- const actorLevel = this.getRoleLevel(actorRole);
30
- const targetLevel = this.getRoleLevel(targetRole);
31
-
32
- return actorLevel > targetLevel;
33
- }
34
-
35
- canDeleteUser(actorRole: Role, targetRole: Role): boolean {
36
- return this.canModifyUser(actorRole, targetRole);
37
- }
38
-
39
- canChangeRole(
40
- actorRole: Role,
41
- targetCurrentRole: Role,
42
- newRole: Role
43
- ): boolean {
44
- const actorLevel = this.getRoleLevel(actorRole);
45
- const targetLevel = this.getRoleLevel(targetCurrentRole);
46
- const newRoleLevel = this.getRoleLevel(newRole);
47
-
48
- return actorLevel > targetLevel && actorLevel > newRoleLevel;
49
- }
50
-
51
- canChangeEmail(actorRole: Role): boolean {
52
- return actorRole === 'super_admin';
53
- }
54
-
55
- assertCanModifyUser(
56
- actorId: string,
57
- actorRole: Role,
58
- targetUserId: string,
59
- targetRole: Role
60
- ): void {
61
- if (actorId === targetUserId) {
62
- return;
63
- }
64
-
65
- if (!this.canModifyUser(actorRole, targetRole)) {
66
- this.logger.warn('Authorization failed: Cannot modify user', {
67
- actorId,
68
- actorRole,
69
- targetUserId,
70
- targetRole,
71
- });
72
- throw new ForbiddenError(
73
- `Insufficient permissions to modify user with role: ${targetRole}`,
74
- {
75
- requiredLevel: 'higher than target',
76
- actorRole,
77
- targetRole,
78
- }
79
- );
80
- }
81
- }
82
-
83
- assertCanDeleteUser(
84
- actorId: string,
85
- actorRole: Role,
86
- targetUserId: string,
87
- targetRole: Role
88
- ): void {
89
- if (actorId === targetUserId) {
90
- this.logger.warn('Authorization failed: Cannot delete own account', {
91
- actorId,
92
- });
93
- throw new ForbiddenError('Cannot delete your own account');
94
- }
95
-
96
- if (!this.canDeleteUser(actorRole, targetRole)) {
97
- this.logger.warn('Authorization failed: Cannot delete user', {
98
- actorId,
99
- actorRole,
100
- targetUserId,
101
- targetRole,
102
- });
103
- throw new ForbiddenError(
104
- `Insufficient permissions to delete user with role: ${targetRole}`,
105
- {
106
- requiredLevel: 'higher than target',
107
- actorRole,
108
- targetRole,
109
- }
110
- );
111
- }
112
- }
113
-
114
- assertCanChangeRole(
115
- actorId: string,
116
- actorRole: Role,
117
- targetUserId: string,
118
- targetCurrentRole: Role,
119
- newRole: Role
120
- ): void {
121
- if (actorId === targetUserId) {
122
- this.logger.warn('Authorization failed: Cannot modify own role', {
123
- actorId,
124
- });
125
- throw new ForbiddenError('Cannot modify your own role');
126
- }
127
-
128
- if (!this.canChangeRole(actorRole, targetCurrentRole, newRole)) {
129
- this.logger.warn('Authorization failed: Cannot change role', {
130
- actorId,
131
- actorRole,
132
- targetUserId,
133
- targetCurrentRole,
134
- newRole,
135
- });
136
- throw new ForbiddenError(
137
- `Insufficient permissions to change role from ${targetCurrentRole} to ${newRole}`,
138
- {
139
- requiredLevel: 'higher than both current and target roles',
140
- actorRole,
141
- targetCurrentRole,
142
- newRole,
143
- }
144
- );
145
- }
146
- }
147
-
148
- assertCanChangeEmail(actorRole: Role): void {
149
- if (!this.canChangeEmail(actorRole)) {
150
- this.logger.warn('Authorization failed: Cannot change email', {
151
- actorRole,
152
- });
153
- throw new ForbiddenError(
154
- 'Only super admins can change user email addresses',
155
- {
156
- requiredRole: 'super_admin',
157
- currentRole: actorRole,
158
- }
159
- );
160
- }
161
- }
162
- }