create-blitzpack 0.1.0 → 0.1.2

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 +92 -94
  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,377 +0,0 @@
1
- import cookie from '@fastify/cookie';
2
- import cors from '@fastify/cors';
3
- import formbody from '@fastify/formbody';
4
- import helmet from '@fastify/helmet';
5
- import rateLimit from '@fastify/rate-limit';
6
- import { AppError } from '@repo/packages-utils/errors';
7
- import type { FastifyError, FastifyReply, FastifyRequest } from 'fastify';
8
- import Fastify from 'fastify';
9
- import {
10
- serializerCompiler,
11
- validatorCompiler,
12
- type ZodTypeProvider,
13
- } from 'fastify-type-provider-zod';
14
-
15
- import { loadEnv } from '@/config/env';
16
- import type { RateLimitRole } from '@/config/rate-limit';
17
- import { RATE_LIMIT_CONFIG } from '@/config/rate-limit';
18
- import { metricsService } from '@/services/metrics.service';
19
-
20
- const env = loadEnv();
21
-
22
- export const app = Fastify({
23
- logger: {
24
- level: 'trace',
25
- formatters: {
26
- level: (label) => ({ level: label }),
27
- },
28
- transport:
29
- env.NODE_ENV === 'development'
30
- ? {
31
- target: 'pino-pretty',
32
- options: {
33
- colorize: true,
34
- ignore: 'pid,hostname',
35
- singleLine: false,
36
- translateTime: 'HH:MM:ss',
37
- },
38
- }
39
- : undefined,
40
- },
41
- disableRequestLogging: true,
42
- requestIdHeader: 'x-request-id',
43
- genReqId: () => `req-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
44
- bodyLimit: 1048576,
45
- routerOptions: {
46
- ignoreTrailingSlash: true,
47
- },
48
- onProtoPoisoning: 'error',
49
- onConstructorPoisoning: 'error',
50
- }).withTypeProvider<ZodTypeProvider>();
51
-
52
- app.setValidatorCompiler(validatorCompiler);
53
- app.setSerializerCompiler(serializerCompiler);
54
-
55
- await app.register(helmet, {
56
- contentSecurityPolicy: {
57
- directives: {
58
- defaultSrc: ["'self'"],
59
- styleSrc: ["'self'", "'unsafe-inline'"],
60
- scriptSrc: ["'self'", "'unsafe-inline'"],
61
- imgSrc: ["'self'", 'data:', 'https:'],
62
- },
63
- },
64
- });
65
-
66
- await app.register(cors, {
67
- origin: env.FRONTEND_URL,
68
- credentials: true,
69
- methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
70
- allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
71
- exposedHeaders: ['X-Request-ID'],
72
- });
73
-
74
- // @ts-expect-error - Known issue with @fastify/rate-limit type definitions
75
- await app.register(rateLimit, {
76
- global: true,
77
- max: async (request: FastifyRequest) => {
78
- const session = await request.server.auth.api
79
- .getSession({
80
- headers: request.headers as unknown as Headers,
81
- })
82
- .catch(() => null);
83
-
84
- if (!session?.user) {
85
- return RATE_LIMIT_CONFIG.anonymous.max;
86
- }
87
-
88
- const userWithRole = session.user as typeof session.user & {
89
- role?: string;
90
- };
91
- const role = (userWithRole.role || 'user') as RateLimitRole;
92
-
93
- return RATE_LIMIT_CONFIG[role]?.max || RATE_LIMIT_CONFIG.user.max;
94
- },
95
- timeWindow: 60 * 1000,
96
- keyGenerator: async (request: FastifyRequest) => {
97
- const session = await request.server.auth.api
98
- .getSession({
99
- headers: request.headers as unknown as Headers,
100
- })
101
- .catch(() => null);
102
-
103
- if (session?.user?.id) {
104
- return `user:${session.user.id}`;
105
- }
106
-
107
- return `ip:${request.ip}`;
108
- },
109
- addHeadersOnExceeding: {
110
- 'X-RateLimit-Limit': true,
111
- 'X-RateLimit-Remaining': true,
112
- 'X-RateLimit-Reset': true,
113
- },
114
- addHeaders: {
115
- 'X-RateLimit-Limit': true,
116
- 'X-RateLimit-Remaining': true,
117
- 'X-RateLimit-Reset': true,
118
- },
119
- errorResponseBuilder: (request: FastifyRequest) => ({
120
- statusCode: 429,
121
- error: 'Too Many Requests',
122
- message: 'Rate limit exceeded. Please try again later.',
123
- }),
124
- });
125
-
126
- await app.register(cookie, {
127
- secret: env.COOKIE_SECRET,
128
- parseOptions: {},
129
- });
130
-
131
- await app.register(formbody);
132
-
133
- const { default: multipartPlugin } = await import('@/plugins/multipart.js');
134
- await app.register(multipartPlugin);
135
-
136
- const { default: loggerPlugin } = await import('@/plugins/logger.js');
137
- await app.register(loggerPlugin);
138
-
139
- const { default: databasePlugin } = await import('@/plugins/database.js');
140
- await app.register(databasePlugin);
141
-
142
- const { default: servicesPlugin } = await import('@/plugins/services.js');
143
- await app.register(servicesPlugin);
144
-
145
- const { default: authPlugin } = await import('@/plugins/auth.js');
146
- await app.register(authPlugin);
147
-
148
- const { default: swaggerPlugin } = await import('@/plugins/swagger.js');
149
- await app.register(swaggerPlugin);
150
-
151
- const { default: scalarPlugin } = await import('@/plugins/scalar.js');
152
- await app.register(scalarPlugin);
153
-
154
- const { default: schedulePlugin } = await import('@/plugins/schedule.js');
155
- await app.register(schedulePlugin);
156
-
157
- const errorHandler = (
158
- error: FastifyError,
159
- request: FastifyRequest,
160
- reply: FastifyReply
161
- ): void => {
162
- request.log.error(
163
- {
164
- err: error,
165
- reqId: request.id,
166
- url: request.url,
167
- method: request.method,
168
- },
169
- 'Request error'
170
- );
171
-
172
- if (error instanceof AppError) {
173
- void reply.status(error.statusCode).send({
174
- error: {
175
- message: error.message,
176
- code: error.code,
177
- ...(error.details && { details: error.details }),
178
- },
179
- });
180
- return;
181
- }
182
-
183
- if (error.validation) {
184
- void reply.status(400).send({
185
- error: {
186
- message: 'Validation failed',
187
- code: 'VALIDATION_ERROR',
188
- details: error.validation,
189
- },
190
- });
191
- return;
192
- }
193
-
194
- const isProduction = env.NODE_ENV === 'production';
195
- const statusCode = error.statusCode || 500;
196
-
197
- void reply.status(statusCode).send({
198
- error: {
199
- message:
200
- isProduction && statusCode === 500
201
- ? 'Internal server error'
202
- : error.message || 'An error occurred',
203
- code: 'INTERNAL_ERROR',
204
- },
205
- });
206
- };
207
-
208
- app.setErrorHandler(errorHandler);
209
-
210
- app.addHook('onRequest', async (request) => {
211
- if (env.LOG_LEVEL === 'detailed' || env.LOG_LEVEL === 'verbose') {
212
- request.log = request.log.child({ reqId: request.id });
213
- }
214
- });
215
-
216
- app.addHook('onResponse', async (request, reply) => {
217
- try {
218
- const responseTime = reply.elapsedTime;
219
- metricsService.recordRequest(responseTime, reply.statusCode);
220
-
221
- const statusCode = reply.statusCode;
222
- const isError = statusCode >= 400;
223
- const logMessage = `${request.method} ${request.url} → ${statusCode} (${responseTime.toFixed(2)}ms)`;
224
-
225
- switch (env.LOG_LEVEL) {
226
- case 'minimal':
227
- if (isError) {
228
- request.log.error(
229
- {
230
- method: request.method,
231
- url: request.url,
232
- statusCode,
233
- responseTime: `${responseTime.toFixed(2)}ms`,
234
- },
235
- logMessage
236
- );
237
- }
238
- break;
239
-
240
- case 'normal':
241
- if (isError) {
242
- request.log.error(logMessage);
243
- } else {
244
- request.log.info(logMessage);
245
- }
246
- break;
247
-
248
- case 'detailed':
249
- if (isError) {
250
- request.log.error(
251
- {
252
- method: request.method,
253
- url: request.url,
254
- statusCode,
255
- responseTime: `${responseTime.toFixed(2)}ms`,
256
- ip: request.ip,
257
- userAgent: request.headers['user-agent'],
258
- },
259
- logMessage
260
- );
261
- } else {
262
- request.log.info(
263
- {
264
- method: request.method,
265
- url: request.url,
266
- statusCode,
267
- responseTime: `${responseTime.toFixed(2)}ms`,
268
- ip: request.ip,
269
- userAgent: request.headers['user-agent'],
270
- },
271
- logMessage
272
- );
273
- }
274
- break;
275
-
276
- case 'verbose':
277
- if (isError) {
278
- request.log.error(
279
- {
280
- method: request.method,
281
- url: request.url,
282
- statusCode,
283
- responseTime: `${responseTime.toFixed(2)}ms`,
284
- ip: request.ip,
285
- userAgent: request.headers['user-agent'],
286
- req: {
287
- params: request.params,
288
- query: request.query,
289
- headers: request.headers,
290
- },
291
- res: {
292
- headers: reply.getHeaders(),
293
- },
294
- },
295
- logMessage
296
- );
297
- } else {
298
- request.log.info(
299
- {
300
- method: request.method,
301
- url: request.url,
302
- statusCode,
303
- responseTime: `${responseTime.toFixed(2)}ms`,
304
- ip: request.ip,
305
- userAgent: request.headers['user-agent'],
306
- req: {
307
- params: request.params,
308
- query: request.query,
309
- headers: request.headers,
310
- },
311
- res: {
312
- headers: reply.getHeaders(),
313
- },
314
- },
315
- logMessage
316
- );
317
- }
318
- break;
319
- }
320
- } catch (error) {
321
- console.error('[onResponse hook error]:', error);
322
- }
323
- });
324
-
325
- app.get('/health', async (request, reply) => {
326
- try {
327
- await app.prisma.$queryRaw`SELECT 1`;
328
- return {
329
- status: 'ok',
330
- timestamp: new Date().toISOString(),
331
- database: 'connected',
332
- };
333
- } catch (error) {
334
- request.log.error(error, 'Database health check failed');
335
- return reply.status(503).send({
336
- status: 'error',
337
- timestamp: new Date().toISOString(),
338
- database: 'disconnected',
339
- });
340
- }
341
- });
342
-
343
- const { default: usersRoutes } = await import('@/routes/users.js');
344
- const { default: sessionsRoutes } = await import('@/routes/sessions.js');
345
- const { default: passwordRoutes } = await import('@/routes/password.js');
346
- const { default: verificationRoutes } = await import(
347
- '@/routes/verification.js'
348
- );
349
- const { default: uploadsRoutes } = await import('@/routes/uploads.js');
350
- const { default: uploadsServeRoutes } = await import(
351
- '@/routes/uploads-serve.js'
352
- );
353
- const { default: accountsRoutes } = await import('@/routes/accounts.js');
354
- const { default: statsRoutes } = await import('@/routes/stats.js');
355
- const { default: metricsRoutes } = await import('@/routes/metrics.js');
356
- const { default: adminSessionsRoutes } = await import(
357
- '@/routes/admin-sessions.js'
358
- );
359
-
360
- metricsService.start();
361
-
362
- await app.register(uploadsServeRoutes);
363
-
364
- await app.register(
365
- async (app) => {
366
- await app.register(usersRoutes);
367
- await app.register(sessionsRoutes);
368
- await app.register(passwordRoutes);
369
- await app.register(verificationRoutes);
370
- await app.register(uploadsRoutes);
371
- await app.register(accountsRoutes);
372
- await app.register(statsRoutes);
373
- await app.register(metricsRoutes);
374
- await app.register(adminSessionsRoutes);
375
- },
376
- { prefix: '/api' }
377
- );
@@ -1,227 +0,0 @@
1
- import type { FastifyBaseLogger } from 'fastify';
2
- import pino, { type Logger } from 'pino';
3
-
4
- import { loadEnv } from '@/config/env';
5
-
6
- type VerbosityLevel = 'minimal' | 'normal' | 'detailed' | 'verbose';
7
- type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'trace';
8
-
9
- interface LogContext {
10
- [key: string]: unknown;
11
- }
12
-
13
- interface PerformanceMetrics {
14
- operation: string;
15
- duration: number;
16
- [key: string]: unknown;
17
- }
18
-
19
- export class LoggerService {
20
- private readonly logger: Logger | FastifyBaseLogger;
21
- private readonly globalVerbosity: VerbosityLevel;
22
- private readonly isDevelopment: boolean;
23
- private verbosityOverride?: VerbosityLevel;
24
- private context?: string;
25
-
26
- constructor(existingLogger?: Logger | FastifyBaseLogger) {
27
- const env = loadEnv();
28
- this.isDevelopment = env.NODE_ENV === 'development';
29
- this.globalVerbosity = env.LOG_LEVEL;
30
-
31
- if (existingLogger) {
32
- this.logger = existingLogger;
33
- } else {
34
- this.logger = pino({
35
- level: 'trace',
36
- formatters: {
37
- level: (label) => ({ level: label }),
38
- },
39
- serializers: {
40
- err: pino.stdSerializers.err,
41
- error: pino.stdSerializers.err,
42
- req: pino.stdSerializers.req,
43
- res: pino.stdSerializers.res,
44
- },
45
- transport: this.isDevelopment
46
- ? {
47
- target: 'pino-pretty',
48
- options: {
49
- colorize: true,
50
- ignore: 'pid,hostname',
51
- singleLine: false,
52
- messageFormat: '{if context}[{context}] {end}{msg}',
53
- translateTime: 'HH:MM:ss',
54
- },
55
- }
56
- : undefined,
57
- });
58
- }
59
- }
60
-
61
- minimal(): this {
62
- const instance = this.clone();
63
- instance.verbosityOverride = 'minimal';
64
- return instance;
65
- }
66
-
67
- normal(): this {
68
- const instance = this.clone();
69
- instance.verbosityOverride = 'normal';
70
- return instance;
71
- }
72
-
73
- detailed(): this {
74
- const instance = this.clone();
75
- instance.verbosityOverride = 'detailed';
76
- return instance;
77
- }
78
-
79
- verbose(): this {
80
- const instance = this.clone();
81
- instance.verbosityOverride = 'verbose';
82
- return instance;
83
- }
84
-
85
- setContext(context: string): void {
86
- this.context = context;
87
- }
88
-
89
- child(context: string): LoggerService {
90
- const instance = this.clone();
91
- instance.context = context;
92
- return instance;
93
- }
94
-
95
- log(message: string, context?: LogContext): void {
96
- this.info(message, context);
97
- }
98
-
99
- info(message: string, context?: LogContext): void {
100
- if (this.shouldLog('info')) {
101
- this.writeLog('info', message, context);
102
- }
103
- }
104
-
105
- error(message: string, error?: Error | string, context?: LogContext): void {
106
- if (this.shouldLog('error')) {
107
- const errorContext =
108
- error instanceof Error
109
- ? { err: error, ...context }
110
- : error
111
- ? { trace: error, ...context }
112
- : context;
113
-
114
- this.writeLog('error', message, errorContext);
115
- }
116
- }
117
-
118
- warn(message: string, context?: LogContext): void {
119
- if (this.shouldLog('warn')) {
120
- this.writeLog('warn', message, context);
121
- }
122
- }
123
-
124
- debug(message: string, context?: LogContext): void {
125
- if (this.shouldLog('debug')) {
126
- this.writeLog('debug', message, context);
127
- }
128
- }
129
-
130
- trace(message: string, context?: LogContext): void {
131
- if (this.shouldLog('trace')) {
132
- this.writeLog('trace', message, context);
133
- }
134
- }
135
-
136
- perf(message: string, metrics: PerformanceMetrics): void {
137
- if (this.shouldLog('debug')) {
138
- this.writeLog('debug', message, {
139
- performance: true,
140
- ...metrics,
141
- });
142
- }
143
- }
144
-
145
- getVerbosity(): VerbosityLevel {
146
- return this.verbosityOverride ?? this.globalVerbosity;
147
- }
148
-
149
- getRawLogger(): Logger | FastifyBaseLogger {
150
- return this.logger;
151
- }
152
-
153
- private shouldLog(level: LogLevel): boolean {
154
- const effectiveVerbosity = this.verbosityOverride ?? this.globalVerbosity;
155
-
156
- const verbosityOrder: VerbosityLevel[] = [
157
- 'minimal',
158
- 'normal',
159
- 'detailed',
160
- 'verbose',
161
- ];
162
- const currentLevel =
163
- verbosityOrder.indexOf(effectiveVerbosity) >= 0
164
- ? verbosityOrder.indexOf(effectiveVerbosity)
165
- : 1;
166
-
167
- switch (level) {
168
- case 'error':
169
- case 'warn':
170
- return currentLevel >= 0;
171
- case 'info':
172
- return currentLevel >= 1;
173
- case 'debug':
174
- return currentLevel >= 2;
175
- case 'trace':
176
- return currentLevel >= 3;
177
- default:
178
- return false;
179
- }
180
- }
181
-
182
- private writeLog(
183
- level: 'info' | 'error' | 'warn' | 'debug' | 'trace',
184
- message: string,
185
- context?: LogContext
186
- ): void {
187
- const effectiveVerbosity = this.verbosityOverride ?? this.globalVerbosity;
188
- const enrichedContext: LogContext = {};
189
-
190
- if (effectiveVerbosity === 'minimal') {
191
- // Minimal: message only + critical error fields
192
- if (context?.err) enrichedContext.err = context.err;
193
- if (context?.error) enrichedContext.error = context.error;
194
- } else if (effectiveVerbosity === 'normal') {
195
- // Normal: [context] + message + error fields (no additional fields)
196
- if (this.context) {
197
- enrichedContext.context = this.context;
198
- }
199
- if (context?.err) enrichedContext.err = context.err;
200
- if (context?.error) enrichedContext.error = context.error;
201
- if (context?.trace) enrichedContext.trace = context.trace;
202
- } else {
203
- // Detailed & Verbose: [context] + message + all fields
204
- if (this.context) {
205
- enrichedContext.context = this.context;
206
- }
207
- if (context) {
208
- Object.assign(enrichedContext, context);
209
- }
210
- }
211
-
212
- (this.logger[level] as (obj: object, msg?: string) => void)(
213
- enrichedContext,
214
- message
215
- );
216
- }
217
-
218
- private clone(): this {
219
- const instance = Object.create(Object.getPrototypeOf(this));
220
- instance.logger = this.logger;
221
- instance.globalVerbosity = this.globalVerbosity;
222
- instance.isDevelopment = this.isDevelopment;
223
- instance.context = this.context;
224
- instance.verbosityOverride = this.verbosityOverride;
225
- return instance;
226
- }
227
- }
@@ -1,60 +0,0 @@
1
- import dotenvFlow from 'dotenv-flow';
2
- import { z } from 'zod';
3
-
4
- const EnvSchema = z.object({
5
- NODE_ENV: z
6
- .enum(['development', 'production', 'test'])
7
- .default('development'),
8
- API_URL: z.string().url(),
9
- FRONTEND_URL: z.string().url(),
10
- DATABASE_URL: z.string().min(1),
11
- PORT: z.string().transform(Number).pipe(z.number().int().positive()),
12
- COOKIE_SECRET: z
13
- .string()
14
- .min(16, 'COOKIE_SECRET must be at least 16 characters'),
15
- LOG_LEVEL: z
16
- .enum(['minimal', 'normal', 'detailed', 'verbose'])
17
- .default('normal'),
18
- BETTER_AUTH_SECRET: z
19
- .string()
20
- .min(32, 'BETTER_AUTH_SECRET must be at least 32 characters'),
21
- BETTER_AUTH_URL: z.string().url(),
22
-
23
- // OAuth Provider Credentials (Optional)
24
- // Only required if you enable social login providers in Better Auth configuration
25
- // Leave empty to use email/password authentication only
26
- GITHUB_CLIENT_ID: z.string().optional(),
27
- GITHUB_CLIENT_SECRET: z.string().optional(),
28
- GOOGLE_CLIENT_ID: z.string().optional(),
29
- GOOGLE_CLIENT_SECRET: z.string().optional(),
30
-
31
- // Email Service Configuration (Optional)
32
- // If RESEND_API_KEY is not provided, emails will be logged to console (dev mode)
33
- // EMAIL_FROM: The sender email address (e.g., 'noreply@yourdomain.com')
34
- // Note: You MUST verify your domain in Resend dashboard before sending emails
35
- RESEND_API_KEY: z.string().optional(),
36
- EMAIL_FROM: z.string().email().optional(),
37
-
38
- // File Storage Configuration (Optional)
39
- STORAGE_TYPE: z.enum(['local', 's3', 'r2']).optional().default('local'),
40
- S3_BUCKET: z.string().optional(),
41
- S3_REGION: z.string().optional(),
42
- S3_ACCESS_KEY_ID: z.string().optional(),
43
- S3_SECRET_ACCESS_KEY: z.string().optional(),
44
- S3_ENDPOINT: z.string().url().optional(), // For R2/MinIO compatibility
45
- });
46
-
47
- export type Env = z.infer<typeof EnvSchema>;
48
-
49
- export function loadEnv(path?: string): Env {
50
- dotenvFlow.config({ path: path || process.cwd() });
51
-
52
- const result = EnvSchema.safeParse(process.env);
53
-
54
- if (!result.success) {
55
- console.error('❌ Invalid environment variables:', result.error.format());
56
- throw new Error('Invalid environment variables');
57
- }
58
-
59
- return result.data;
60
- }
@@ -1,29 +0,0 @@
1
- export const RATE_LIMIT_CONFIG = {
2
- // Role-based rate limits (requests per minute)
3
- admin: {
4
- max: 200,
5
- timeWindow: 60 * 1000,
6
- },
7
- user: {
8
- max: 60,
9
- timeWindow: 60 * 1000,
10
- },
11
- anonymous: {
12
- max: 30,
13
- timeWindow: 60 * 1000,
14
- },
15
-
16
- // Route-specific overrides
17
- routes: {
18
- auth: {
19
- max: 10,
20
- timeWindow: 60 * 1000,
21
- },
22
- uploads: {
23
- max: 20,
24
- timeWindow: 60 * 1000,
25
- },
26
- },
27
- } as const;
28
-
29
- export type RateLimitRole = 'admin' | 'user' | 'anonymous';