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,170 +0,0 @@
1
- import { render } from '@react-email/components';
2
- import PasswordResetEmail from 'emails/password-reset-email';
3
- import VerificationEmail from 'emails/verification-email';
4
- import { Resend } from 'resend';
5
-
6
- import type { LoggerService } from '@/common/logger.service';
7
- import type { Env } from '@/config/env';
8
- import type { PrismaClient } from '@/generated/client/client.js';
9
-
10
- export class EmailService {
11
- private resend: Resend | null = null;
12
- private isConfigured: boolean;
13
- private emailFrom: string;
14
-
15
- constructor(
16
- private readonly env: Env,
17
- private readonly logger: LoggerService,
18
- private readonly prisma: PrismaClient
19
- ) {
20
- this.logger.setContext('EmailService');
21
-
22
- this.isConfigured = !!(this.env.RESEND_API_KEY && this.env.EMAIL_FROM);
23
- this.emailFrom = this.env.EMAIL_FROM || 'noreply@example.com';
24
-
25
- if (this.isConfigured) {
26
- this.resend = new Resend(this.env.RESEND_API_KEY!);
27
- this.logger.info(
28
- `[+] Email service initialized with Resend. Email: ${this.emailFrom}`
29
- );
30
- } else {
31
- this.logger.warn(
32
- 'Email service not configured (missing RESEND_API_KEY or EMAIL_FROM) - emails will be logged to console'
33
- );
34
- }
35
- }
36
-
37
- async sendVerificationEmail(
38
- email: string,
39
- verificationUrl: string
40
- ): Promise<{ success: boolean; error?: string }> {
41
- const subject = 'Verify your email address';
42
-
43
- try {
44
- const html = await render(
45
- VerificationEmail({
46
- verificationUrl,
47
- userEmail: email,
48
- })
49
- );
50
-
51
- await this.sendEmail({
52
- to: email,
53
- subject,
54
- html,
55
- });
56
-
57
- this.logger.info('Verification email sent', { to: email });
58
- return { success: true };
59
- } catch (error) {
60
- this.logger.error(
61
- 'Failed to send verification email - user can resend later',
62
- error instanceof Error ? error : new Error(String(error)),
63
- { to: email }
64
- );
65
- return {
66
- success: false,
67
- error: error instanceof Error ? error.message : String(error),
68
- };
69
- }
70
- }
71
-
72
- async sendPasswordResetEmail(
73
- email: string,
74
- resetUrl: string
75
- ): Promise<{ success: boolean; error?: string }> {
76
- const subject = 'Reset your password';
77
-
78
- try {
79
- const html = await render(
80
- PasswordResetEmail({
81
- resetUrl,
82
- userEmail: email,
83
- })
84
- );
85
-
86
- await this.sendEmail({
87
- to: email,
88
- subject,
89
- html,
90
- });
91
-
92
- this.logger.info('Password reset email sent', { to: email });
93
- return { success: true };
94
- } catch (error) {
95
- this.logger.error(
96
- 'Failed to send password reset email',
97
- error instanceof Error ? error : new Error(String(error)),
98
- { to: email }
99
- );
100
- return {
101
- success: false,
102
- error: error instanceof Error ? error.message : String(error),
103
- };
104
- }
105
- }
106
-
107
- private async sendEmail({
108
- to,
109
- subject,
110
- html,
111
- }: {
112
- to: string;
113
- subject: string;
114
- html: string;
115
- }) {
116
- if (!this.isConfigured) {
117
- this.logger
118
- .detailed()
119
- .info('Email not sent (dev mode - no API key configured)', {
120
- to,
121
- subject,
122
- });
123
-
124
- console.log(
125
- '\n┌─────────────────────────────────────────────────────────┐'
126
- );
127
- console.log(
128
- '│ 📧 EMAIL PREVIEW │'
129
- );
130
- console.log(
131
- '└─────────────────────────────────────────────────────────┘'
132
- );
133
- console.log(`To: ${to}`);
134
- console.log(`From: ${this.emailFrom}`);
135
- console.log(`Subject: ${subject}`);
136
- console.log('─────────────────────────────────────────────────────────');
137
- console.log(`Preview: ${html.slice(0, 300)}...`);
138
- console.log(
139
- '─────────────────────────────────────────────────────────\n'
140
- );
141
-
142
- return;
143
- }
144
-
145
- try {
146
- const result = await this.resend!.emails.send({
147
- from: this.emailFrom,
148
- to,
149
- subject,
150
- html,
151
- });
152
-
153
- this.logger.detailed().debug('Email sent successfully', {
154
- to,
155
- subject,
156
- emailId: result.data?.id,
157
- });
158
- } catch (error) {
159
- this.logger.error(
160
- 'Failed to send email via Resend',
161
- error instanceof Error ? error : new Error(String(error)),
162
- {
163
- to,
164
- subject,
165
- }
166
- );
167
- throw error;
168
- }
169
- }
170
- }
@@ -1,267 +0,0 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
-
4
- import {
5
- DeleteObjectCommand,
6
- PutObjectCommand,
7
- S3Client,
8
- } from '@aws-sdk/client-s3';
9
- import sharp from 'sharp';
10
-
11
- import type { LoggerService } from '@/common/logger.service';
12
- import type { Env } from '@/config/env';
13
- import { isImageFile } from '@/utils/file-validation';
14
-
15
- export interface FileUploadResult {
16
- filename: string;
17
- url: string;
18
- size: number;
19
- mimeType: string;
20
- }
21
-
22
- export interface FileUploadOptions {
23
- buffer: Buffer;
24
- originalFilename: string;
25
- mimeType: string;
26
- optimizeImage?: boolean;
27
- }
28
-
29
- export class FileStorageService {
30
- private s3Client?: S3Client;
31
- private storageType: 'local' | 's3' | 'r2';
32
- private localUploadDir: string;
33
-
34
- constructor(
35
- private readonly env: Env,
36
- private readonly logger: LoggerService
37
- ) {
38
- this.logger.setContext('FileStorageService');
39
- this.storageType = env.STORAGE_TYPE || 'local';
40
- this.localUploadDir = path.join(process.cwd(), 'public', 'uploads');
41
-
42
- this.initializeStorage();
43
- }
44
-
45
- private initializeStorage(): void {
46
- if (this.storageType === 'local') {
47
- this.logger.info(
48
- `Using local file storage. Upload directory: ${this.localUploadDir}`
49
- );
50
- return;
51
- }
52
-
53
- // Initialize S3/R2 client
54
- const {
55
- S3_BUCKET,
56
- S3_REGION,
57
- S3_ACCESS_KEY_ID,
58
- S3_SECRET_ACCESS_KEY,
59
- S3_ENDPOINT,
60
- } = this.env;
61
-
62
- if (
63
- !S3_BUCKET ||
64
- !S3_REGION ||
65
- !S3_ACCESS_KEY_ID ||
66
- !S3_SECRET_ACCESS_KEY
67
- ) {
68
- this.logger.warn(
69
- 'S3 credentials incomplete, falling back to local storage'
70
- );
71
- this.storageType = 'local';
72
- return;
73
- }
74
-
75
- const clientConfig: ConstructorParameters<typeof S3Client>[0] = {
76
- region: S3_REGION,
77
- credentials: {
78
- accessKeyId: S3_ACCESS_KEY_ID,
79
- secretAccessKey: S3_SECRET_ACCESS_KEY,
80
- },
81
- };
82
-
83
- // Add custom endpoint for MinIO/R2
84
- if (S3_ENDPOINT) {
85
- clientConfig.endpoint = S3_ENDPOINT;
86
- clientConfig.forcePathStyle = true; // Required for MinIO
87
- this.logger.info('Using S3-compatible storage', {
88
- endpoint: S3_ENDPOINT,
89
- type: this.storageType,
90
- });
91
- } else {
92
- this.logger.info('Using AWS S3 storage', { region: S3_REGION });
93
- }
94
-
95
- this.s3Client = new S3Client(clientConfig);
96
- }
97
-
98
- async uploadFile(options: FileUploadOptions): Promise<FileUploadResult> {
99
- let { buffer } = options;
100
- const { originalFilename, mimeType, optimizeImage = true } = options;
101
-
102
- // Optimize images if enabled
103
- if (optimizeImage && isImageFile(mimeType)) {
104
- buffer = await this.optimizeImage(buffer, mimeType);
105
- }
106
-
107
- const size = buffer.length;
108
-
109
- if (this.storageType === 'local') {
110
- return this.uploadToLocal(buffer, originalFilename, mimeType, size);
111
- }
112
-
113
- return this.uploadToS3(buffer, originalFilename, mimeType, size);
114
- }
115
-
116
- private async uploadToLocal(
117
- buffer: Buffer,
118
- filename: string,
119
- mimeType: string,
120
- size: number
121
- ): Promise<FileUploadResult> {
122
- // Ensure upload directory exists
123
- await fs.mkdir(this.localUploadDir, { recursive: true });
124
-
125
- const filePath = path.join(this.localUploadDir, filename);
126
- await fs.writeFile(filePath, buffer);
127
-
128
- this.logger.info('File uploaded to local storage', { filename, size });
129
-
130
- // Return URL relative to API server
131
- const url = `${this.env.API_URL}/uploads/files/${filename}`;
132
-
133
- return { filename, url, size, mimeType };
134
- }
135
-
136
- private async uploadToS3(
137
- buffer: Buffer,
138
- filename: string,
139
- mimeType: string,
140
- size: number
141
- ): Promise<FileUploadResult> {
142
- if (!this.s3Client) {
143
- throw new Error('S3 client not initialized');
144
- }
145
-
146
- const bucket = this.env.S3_BUCKET!;
147
-
148
- const command = new PutObjectCommand({
149
- Bucket: bucket,
150
- Key: filename,
151
- Body: buffer,
152
- ContentType: mimeType,
153
- // Make files publicly readable (adjust based on your security needs)
154
- ACL: 'public-read',
155
- });
156
-
157
- await this.s3Client.send(command);
158
-
159
- this.logger.info('File uploaded to S3', { filename, size, bucket });
160
-
161
- // Construct public URL
162
- const url = this.getS3Url(filename);
163
-
164
- return { filename, url, size, mimeType };
165
- }
166
-
167
- async deleteFile(filename: string): Promise<void> {
168
- if (this.storageType === 'local') {
169
- return this.deleteFromLocal(filename);
170
- }
171
-
172
- return this.deleteFromS3(filename);
173
- }
174
-
175
- private async deleteFromLocal(filename: string): Promise<void> {
176
- const filePath = path.join(this.localUploadDir, filename);
177
-
178
- try {
179
- await fs.unlink(filePath);
180
- this.logger.info('File deleted from local storage', { filename });
181
- } catch (error) {
182
- const err = error as { code?: string };
183
- if (err.code !== 'ENOENT') {
184
- throw error;
185
- }
186
- this.logger.warn('File not found for deletion', { filename });
187
- }
188
- }
189
-
190
- private async deleteFromS3(filename: string): Promise<void> {
191
- if (!this.s3Client) {
192
- throw new Error('S3 client not initialized');
193
- }
194
-
195
- const bucket = this.env.S3_BUCKET!;
196
-
197
- const command = new DeleteObjectCommand({
198
- Bucket: bucket,
199
- Key: filename,
200
- });
201
-
202
- await this.s3Client.send(command);
203
-
204
- this.logger.info('File deleted from S3', { filename, bucket });
205
- }
206
-
207
- private async optimizeImage(
208
- buffer: Buffer,
209
- mimeType: string
210
- ): Promise<Buffer> {
211
- try {
212
- let transformer = sharp(buffer).rotate(); // Auto-rotate based on EXIF
213
-
214
- // Resize if too large (max 2048px on longest side)
215
- const metadata = await sharp(buffer).metadata();
216
- const maxDimension = Math.max(metadata.width || 0, metadata.height || 0);
217
-
218
- if (maxDimension > 2048) {
219
- transformer = transformer.resize(2048, 2048, {
220
- fit: 'inside',
221
- withoutEnlargement: true,
222
- });
223
- }
224
-
225
- // Convert and compress based on type
226
- if (mimeType === 'image/png') {
227
- transformer = transformer.png({ quality: 90, compressionLevel: 9 });
228
- } else if (mimeType === 'image/webp') {
229
- transformer = transformer.webp({ quality: 90 });
230
- } else {
231
- // Default to JPEG for other formats
232
- transformer = transformer.jpeg({ quality: 85 });
233
- }
234
-
235
- const optimized = await transformer.toBuffer();
236
-
237
- this.logger.detailed().debug('Image optimized', {
238
- originalSize: buffer.length,
239
- optimizedSize: optimized.length,
240
- reduction: Math.round(
241
- ((buffer.length - optimized.length) / buffer.length) * 100
242
- ),
243
- });
244
-
245
- return optimized;
246
- } catch (error) {
247
- this.logger.warn('Image optimization failed, using original', { error });
248
- return buffer;
249
- }
250
- }
251
-
252
- private getS3Url(filename: string): string {
253
- const { S3_BUCKET, S3_REGION, S3_ENDPOINT } = this.env;
254
-
255
- // For custom endpoints (MinIO, R2, etc.)
256
- if (S3_ENDPOINT) {
257
- return `${S3_ENDPOINT}/${S3_BUCKET}/${filename}`;
258
- }
259
-
260
- // Standard AWS S3 URL
261
- return `https://${S3_BUCKET}.s3.${S3_REGION}.amazonaws.com/${filename}`;
262
- }
263
-
264
- getStorageType(): string {
265
- return this.storageType;
266
- }
267
- }
@@ -1,175 +0,0 @@
1
- import type { RealtimeMetricsPoint } from '@repo/packages-types/stats';
2
- import { cpus } from 'os';
3
-
4
- const HISTORY_SIZE = 60;
5
- const COLLECTION_INTERVAL_MS = 1000;
6
-
7
- interface RequestMetric {
8
- timestamp: number;
9
- responseTimeMs: number;
10
- isError: boolean;
11
- }
12
-
13
- export class MetricsService {
14
- private history: RealtimeMetricsPoint[] = [];
15
- private requestMetrics: RequestMetric[] = [];
16
- private lastCpuUsage: NodeJS.CpuUsage | null = null;
17
- private lastCpuTime = 0;
18
- private intervalId: NodeJS.Timeout | null = null;
19
- private subscribers = new Set<(metrics: RealtimeMetricsPoint) => void>();
20
-
21
- start() {
22
- if (this.intervalId) return;
23
-
24
- this.lastCpuUsage = process.cpuUsage();
25
- this.lastCpuTime = Date.now();
26
-
27
- setTimeout(() => {
28
- this.collectMetrics();
29
- this.intervalId = setInterval(
30
- () => this.collectMetrics(),
31
- COLLECTION_INTERVAL_MS
32
- );
33
- }, COLLECTION_INTERVAL_MS);
34
- }
35
-
36
- stop() {
37
- if (this.intervalId) {
38
- clearInterval(this.intervalId);
39
- this.intervalId = null;
40
- }
41
- }
42
-
43
- recordRequest(responseTimeMs: number, statusCode: number) {
44
- const now = Date.now();
45
- this.requestMetrics.push({
46
- timestamp: now,
47
- responseTimeMs,
48
- isError: statusCode >= 400,
49
- });
50
-
51
- const cutoff = now - 5000;
52
- this.requestMetrics = this.requestMetrics.filter(
53
- (m) => m.timestamp > cutoff
54
- );
55
- }
56
-
57
- subscribe(callback: (metrics: RealtimeMetricsPoint) => void) {
58
- this.subscribers.add(callback);
59
- return () => this.subscribers.delete(callback);
60
- }
61
-
62
- getHistory(): RealtimeMetricsPoint[] {
63
- return [...this.history];
64
- }
65
-
66
- getLatest(): RealtimeMetricsPoint | null {
67
- return this.history[this.history.length - 1] ?? null;
68
- }
69
-
70
- private collectMetrics() {
71
- const now = Date.now();
72
- const memUsage = process.memoryUsage();
73
-
74
- const cpuPercentage = this.calculateCpuPercentage();
75
-
76
- const { rps, avgResponseTime, errorRate } =
77
- this.calculateRequestMetrics(now);
78
-
79
- const heapUsedMB =
80
- Math.round((memUsage.heapUsed / 1024 / 1024) * 100) / 100;
81
- const heapTotalMB =
82
- Math.round((memUsage.heapTotal / 1024 / 1024) * 100) / 100;
83
- const usedPercent =
84
- heapTotalMB > 0
85
- ? Math.round((heapUsedMB / heapTotalMB) * 100 * 100) / 100
86
- : 0;
87
-
88
- const metrics: RealtimeMetricsPoint = {
89
- timestamp: now,
90
- memory: {
91
- heapUsedMB,
92
- heapTotalMB,
93
- rssMB: Math.round((memUsage.rss / 1024 / 1024) * 100) / 100,
94
- usedPercent,
95
- },
96
- cpu: {
97
- percentage: cpuPercentage,
98
- },
99
- errors: {
100
- rate: errorRate,
101
- },
102
- requests: {
103
- perSecond: rps,
104
- avgResponseTimeMs: avgResponseTime,
105
- },
106
- };
107
-
108
- this.history.push(metrics);
109
- if (this.history.length > HISTORY_SIZE) {
110
- this.history.shift();
111
- }
112
-
113
- this.subscribers.forEach((cb) => cb(metrics));
114
- }
115
-
116
- private calculateCpuPercentage(): number {
117
- if (!this.lastCpuUsage) {
118
- this.lastCpuUsage = process.cpuUsage();
119
- this.lastCpuTime = Date.now();
120
- return 0.01;
121
- }
122
-
123
- const currentCpuUsage = process.cpuUsage(this.lastCpuUsage);
124
- const currentTime = Date.now();
125
- const elapsedMs = currentTime - this.lastCpuTime;
126
-
127
- if (elapsedMs <= 0) {
128
- return 0.01;
129
- }
130
-
131
- const cpuCount = cpus().length;
132
- const totalMicroseconds = currentCpuUsage.user + currentCpuUsage.system;
133
- const percentage =
134
- ((totalMicroseconds / 1000 / elapsedMs) * 100) / cpuCount;
135
-
136
- this.lastCpuUsage = process.cpuUsage();
137
- this.lastCpuTime = currentTime;
138
-
139
- const rounded = Math.round(Math.min(100, percentage) * 100) / 100;
140
- return Math.max(0.01, rounded);
141
- }
142
-
143
- private calculateRequestMetrics(now: number): {
144
- rps: number;
145
- avgResponseTime: number;
146
- errorRate: number;
147
- } {
148
- const windowMs = 5000;
149
- const cutoff = now - windowMs;
150
- const recentRequests = this.requestMetrics.filter(
151
- (m) => m.timestamp > cutoff
152
- );
153
-
154
- if (recentRequests.length === 0) {
155
- return { rps: 0, avgResponseTime: 0, errorRate: 0 };
156
- }
157
-
158
- const rps =
159
- Math.round((recentRequests.length / (windowMs / 1000)) * 100) / 100;
160
- const avgResponseTime =
161
- Math.round(
162
- (recentRequests.reduce((sum, m) => sum + m.responseTimeMs, 0) /
163
- recentRequests.length) *
164
- 100
165
- ) / 100;
166
-
167
- const errorCount = recentRequests.filter((m) => m.isError).length;
168
- const errorRate =
169
- Math.round((errorCount / recentRequests.length) * 100 * 100) / 100;
170
-
171
- return { rps, avgResponseTime, errorRate };
172
- }
173
- }
174
-
175
- export const metricsService = new MetricsService();
@@ -1,56 +0,0 @@
1
- import {
2
- UnauthorizedError,
3
- ValidationError,
4
- } from '@repo/packages-utils/errors';
5
- import * as bcrypt from 'bcryptjs';
6
-
7
- import type { PrismaClient } from '@/generated/client/client.js';
8
- import { type SessionsService } from '@/services/sessions.service';
9
-
10
- export class PasswordService {
11
- constructor(
12
- private readonly prisma: PrismaClient,
13
- private readonly sessionsService: SessionsService
14
- ) {}
15
-
16
- async changePassword(
17
- userId: string,
18
- currentPassword: string,
19
- newPassword: string
20
- ): Promise<void> {
21
- const account = await this.prisma.account.findFirst({
22
- where: {
23
- userId,
24
- providerId: 'credential',
25
- },
26
- });
27
-
28
- if (!account || !account.password) {
29
- throw new ValidationError('Password authentication not available', {
30
- userId,
31
- });
32
- }
33
-
34
- const isValidPassword = await bcrypt.compare(
35
- currentPassword,
36
- account.password
37
- );
38
-
39
- if (!isValidPassword) {
40
- throw new UnauthorizedError('Current password is incorrect');
41
- }
42
-
43
- const hashedPassword = await bcrypt.hash(newPassword, 10);
44
-
45
- await this.prisma.account.update({
46
- where: {
47
- id: account.id,
48
- },
49
- data: {
50
- password: hashedPassword,
51
- },
52
- });
53
-
54
- await this.sessionsService.revokeAllUserSessions(userId);
55
- }
56
- }