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,134 +0,0 @@
1
- import { createMockPrisma } from '@test/helpers/mock-prisma';
2
- import { beforeEach, describe, expect, it, vi } from 'vitest';
3
-
4
- import type { PrismaClient } from '@/generated/client/client.js';
5
-
6
- import { SessionsService } from './sessions.service';
7
-
8
- describe('SessionsService', () => {
9
- let service: SessionsService;
10
- let prisma: PrismaClient;
11
-
12
- beforeEach(() => {
13
- prisma = createMockPrisma();
14
- service = new SessionsService(prisma);
15
- });
16
-
17
- describe('getUserSessions', () => {
18
- const mockSessions = [
19
- {
20
- id: 'session-1',
21
- userId: 'user-1',
22
- token: 'token-1',
23
- ipAddress: '192.168.1.1',
24
- userAgent: 'Mozilla/5.0',
25
- createdAt: new Date(),
26
- updatedAt: new Date(),
27
- expiresAt: new Date(Date.now() + 86400000),
28
- },
29
- {
30
- id: 'session-2',
31
- userId: 'user-1',
32
- token: 'token-2',
33
- ipAddress: '192.168.1.2',
34
- userAgent: 'Chrome/120.0',
35
- createdAt: new Date(),
36
- updatedAt: new Date(),
37
- expiresAt: new Date(Date.now() + 86400000),
38
- },
39
- ];
40
-
41
- it('should return all user sessions', async () => {
42
- vi.mocked(prisma.session.findMany).mockResolvedValue(mockSessions);
43
-
44
- const result = await service.getUserSessions('user-1');
45
-
46
- expect(result).toHaveLength(2);
47
- expect(result[0].id).toBe('session-1');
48
- expect(result[1].id).toBe('session-2');
49
- });
50
-
51
- it('should return empty array when user has no sessions', async () => {
52
- vi.mocked(prisma.session.findMany).mockResolvedValue([]);
53
-
54
- const result = await service.getUserSessions('user-1');
55
-
56
- expect(result).toEqual([]);
57
- });
58
- });
59
-
60
- describe('revokeSession', () => {
61
- const mockSession = {
62
- id: 'session-1',
63
- userId: 'user-1',
64
- expiresAt: new Date(Date.now() + 86400000),
65
- token: 'token-1',
66
- ipAddress: '192.168.1.1',
67
- userAgent: 'Mozilla/5.0',
68
- createdAt: new Date(),
69
- updatedAt: new Date(),
70
- };
71
-
72
- it('should revoke session successfully', async () => {
73
- vi.mocked(prisma.session.findFirst).mockResolvedValue(mockSession);
74
- vi.mocked(prisma.session.delete).mockResolvedValue(mockSession);
75
-
76
- await service.revokeSession('user-1', 'session-1');
77
-
78
- expect(prisma.session.delete).toHaveBeenCalledWith({
79
- where: { id: 'session-1' },
80
- });
81
- });
82
-
83
- it('should throw error when session not found', async () => {
84
- vi.mocked(prisma.session.findFirst).mockResolvedValue(null);
85
-
86
- await expect(
87
- service.revokeSession('user-1', 'non-existent')
88
- ).rejects.toThrow('Session not found');
89
- });
90
-
91
- it('should throw error when session belongs to different user', async () => {
92
- vi.mocked(prisma.session.findFirst).mockResolvedValue(null);
93
-
94
- await expect(
95
- service.revokeSession('user-1', 'session-1')
96
- ).rejects.toThrow('Session not found');
97
- });
98
- });
99
-
100
- describe('revokeAllSessions', () => {
101
- it('should revoke all sessions except current one', async () => {
102
- vi.mocked(prisma.session.deleteMany).mockResolvedValue({ count: 3 });
103
-
104
- await service.revokeAllSessions('user-1', 'current-session-id');
105
-
106
- expect(prisma.session.deleteMany).toHaveBeenCalledWith({
107
- where: {
108
- userId: 'user-1',
109
- id: { not: 'current-session-id' },
110
- },
111
- });
112
- });
113
-
114
- it('should handle case when no other sessions exist', async () => {
115
- vi.mocked(prisma.session.deleteMany).mockResolvedValue({ count: 0 });
116
-
117
- await service.revokeAllSessions('user-1', 'current-session-id');
118
-
119
- expect(prisma.session.deleteMany).toHaveBeenCalled();
120
- });
121
-
122
- it('should revoke all sessions when no current session provided', async () => {
123
- vi.mocked(prisma.session.deleteMany).mockResolvedValue({ count: 5 });
124
-
125
- await service.revokeAllSessions('user-1');
126
-
127
- expect(prisma.session.deleteMany).toHaveBeenCalledWith({
128
- where: {
129
- userId: 'user-1',
130
- },
131
- });
132
- });
133
- });
134
- });
@@ -1,276 +0,0 @@
1
- import type { PaginatedResponse } from '@repo/packages-types/pagination';
2
- import type { Role } from '@repo/packages-types/role';
3
- import type {
4
- AdminSession,
5
- QuerySessions,
6
- SessionStats,
7
- } from '@repo/packages-types/session';
8
- import { ForbiddenError, NotFoundError } from '@repo/packages-utils/errors';
9
-
10
- import type { Prisma, PrismaClient } from '@/generated/client/client.js';
11
- import type { AuthorizationService } from '@/services/authorization.service';
12
-
13
- export interface SessionInfo {
14
- id: string;
15
- ipAddress: string | null;
16
- userAgent: string | null;
17
- createdAt: Date;
18
- updatedAt: Date;
19
- expiresAt: Date;
20
- isCurrent?: boolean;
21
- }
22
-
23
- export class SessionsService {
24
- constructor(
25
- private readonly prisma: PrismaClient,
26
- private readonly authorizationService?: AuthorizationService
27
- ) {}
28
-
29
- async getUserSessions(userId: string): Promise<SessionInfo[]> {
30
- const sessions = await this.prisma.session.findMany({
31
- where: {
32
- userId,
33
- expiresAt: {
34
- gt: new Date(),
35
- },
36
- },
37
- select: {
38
- id: true,
39
- ipAddress: true,
40
- userAgent: true,
41
- createdAt: true,
42
- updatedAt: true,
43
- expiresAt: true,
44
- },
45
- orderBy: {
46
- updatedAt: 'desc',
47
- },
48
- });
49
-
50
- return sessions;
51
- }
52
-
53
- async revokeSession(userId: string, sessionId: string): Promise<void> {
54
- const session = await this.prisma.session.findFirst({
55
- where: {
56
- id: sessionId,
57
- userId,
58
- },
59
- });
60
-
61
- if (!session) {
62
- throw new NotFoundError('Session not found', {
63
- sessionId,
64
- userId,
65
- });
66
- }
67
-
68
- await this.prisma.session.delete({
69
- where: {
70
- id: sessionId,
71
- },
72
- });
73
- }
74
-
75
- async revokeAllSessions(
76
- userId: string,
77
- currentSessionId?: string
78
- ): Promise<void> {
79
- await this.prisma.session.deleteMany({
80
- where: {
81
- userId,
82
- ...(currentSessionId && { id: { not: currentSessionId } }),
83
- },
84
- });
85
- }
86
-
87
- async revokeAllUserSessions(userId: string): Promise<void> {
88
- await this.prisma.session.deleteMany({
89
- where: {
90
- userId,
91
- },
92
- });
93
- }
94
-
95
- async getAdminSessions(
96
- query: QuerySessions
97
- ): Promise<PaginatedResponse<AdminSession>> {
98
- const { page, limit, search, status, userId, sortBy, sortOrder } = query;
99
- const skip = (page - 1) * limit;
100
- const now = new Date();
101
-
102
- const where: Prisma.SessionWhereInput = {};
103
-
104
- if (status === 'active') {
105
- where.expiresAt = { gt: now };
106
- } else if (status === 'expired') {
107
- where.expiresAt = { lte: now };
108
- }
109
-
110
- if (userId) {
111
- where.userId = userId;
112
- }
113
-
114
- if (search) {
115
- where.user = {
116
- OR: [
117
- { email: { contains: search, mode: 'insensitive' } },
118
- { name: { contains: search, mode: 'insensitive' } },
119
- ],
120
- };
121
- }
122
-
123
- const [sessions, total] = await Promise.all([
124
- this.prisma.session.findMany({
125
- where,
126
- include: {
127
- user: {
128
- select: {
129
- id: true,
130
- name: true,
131
- email: true,
132
- image: true,
133
- },
134
- },
135
- },
136
- orderBy: { [sortBy]: sortOrder },
137
- skip,
138
- take: limit,
139
- }),
140
- this.prisma.session.count({ where }),
141
- ]);
142
-
143
- return {
144
- data: sessions,
145
- pagination: {
146
- page,
147
- limit,
148
- total,
149
- totalPages: Math.ceil(total / limit),
150
- },
151
- };
152
- }
153
-
154
- async getSessionStats(): Promise<SessionStats> {
155
- const now = new Date();
156
- const todayStart = new Date(now);
157
- todayStart.setHours(0, 0, 0, 0);
158
-
159
- const oneDayFromNow = new Date(now.getTime() + 24 * 60 * 60 * 1000);
160
-
161
- const [activeSessions, uniqueUsers, sessionsToday, expiringSoon] =
162
- await Promise.all([
163
- this.prisma.session.count({
164
- where: { expiresAt: { gt: now } },
165
- }),
166
- this.prisma.session
167
- .groupBy({
168
- by: ['userId'],
169
- where: { expiresAt: { gt: now } },
170
- })
171
- .then((r) => r.length),
172
- this.prisma.session.count({
173
- where: { createdAt: { gte: todayStart } },
174
- }),
175
- this.prisma.session.count({
176
- where: {
177
- expiresAt: { gt: now, lte: oneDayFromNow },
178
- },
179
- }),
180
- ]);
181
-
182
- return { activeSessions, uniqueUsers, sessionsToday, expiringSoon };
183
- }
184
-
185
- async adminRevokeSession(
186
- actorId: string,
187
- actorRole: Role,
188
- sessionId: string
189
- ): Promise<void> {
190
- const session = await this.prisma.session.findUnique({
191
- where: { id: sessionId },
192
- include: {
193
- user: {
194
- select: {
195
- id: true,
196
- role: true,
197
- },
198
- },
199
- },
200
- });
201
-
202
- if (!session) {
203
- throw new NotFoundError('Session not found', { sessionId });
204
- }
205
-
206
- // Check if actor can revoke this user's session (role hierarchy check)
207
- if (this.authorizationService) {
208
- if (actorId === session.userId) {
209
- // Allow revoking own sessions
210
- } else if (
211
- !this.authorizationService.canModifyUser(
212
- actorRole,
213
- session.user.role as Role
214
- )
215
- ) {
216
- throw new ForbiddenError(
217
- `Insufficient permissions to revoke session for user with role: ${session.user.role}`,
218
- {
219
- requiredLevel: 'higher than target',
220
- actorRole,
221
- targetRole: session.user.role,
222
- }
223
- );
224
- }
225
- }
226
-
227
- await this.prisma.session.delete({
228
- where: { id: sessionId },
229
- });
230
- }
231
-
232
- async adminRevokeUserSessions(
233
- actorId: string,
234
- actorRole: Role,
235
- targetUserId: string
236
- ): Promise<number> {
237
- // Fetch target user to check their role
238
- const targetUser = await this.prisma.user.findUnique({
239
- where: { id: targetUserId },
240
- select: {
241
- id: true,
242
- role: true,
243
- },
244
- });
245
-
246
- if (!targetUser) {
247
- throw new NotFoundError('User not found', { userId: targetUserId });
248
- }
249
-
250
- // Check if actor can revoke this user's sessions (role hierarchy check)
251
- if (this.authorizationService) {
252
- if (actorId === targetUserId) {
253
- // Allow revoking own sessions
254
- } else if (
255
- !this.authorizationService.canModifyUser(
256
- actorRole,
257
- targetUser.role as Role
258
- )
259
- ) {
260
- throw new ForbiddenError(
261
- `Insufficient permissions to revoke sessions for user with role: ${targetUser.role}`,
262
- {
263
- requiredLevel: 'higher than target',
264
- actorRole,
265
- targetRole: targetUser.role,
266
- }
267
- );
268
- }
269
- }
270
-
271
- const result = await this.prisma.session.deleteMany({
272
- where: { userId: targetUserId },
273
- });
274
- return result.count;
275
- }
276
- }
@@ -1,273 +0,0 @@
1
- import type {
2
- AuthBreakdown,
3
- HealthCheckResponse,
4
- RecentSignup,
5
- RoleDistributionItem,
6
- SessionActivityPoint,
7
- StatsOverview,
8
- SystemHealth,
9
- SystemStats,
10
- UserGrowthPoint,
11
- } from '@repo/packages-types/stats';
12
-
13
- import type { LoggerService } from '@/common/logger.service';
14
- import type { PrismaClient } from '@/generated/client/client.js';
15
-
16
- const SERVER_START_TIME = Date.now();
17
-
18
- export class StatsService {
19
- constructor(
20
- private readonly prisma: PrismaClient,
21
- private readonly logger: LoggerService
22
- ) {
23
- this.logger.setContext('StatsService');
24
- }
25
-
26
- async getSystemStats(): Promise<SystemStats> {
27
- const [
28
- overview,
29
- userGrowth,
30
- sessionActivity,
31
- authBreakdown,
32
- roleDistribution,
33
- recentSignups,
34
- systemHealth,
35
- ] = await Promise.all([
36
- this.getOverview(),
37
- this.getUserGrowth(),
38
- this.getSessionActivity(),
39
- this.getAuthBreakdown(),
40
- this.getRoleDistribution(),
41
- this.getRecentSignups(),
42
- this.getSystemHealth(),
43
- ]);
44
-
45
- return {
46
- overview,
47
- userGrowth,
48
- sessionActivity,
49
- authBreakdown,
50
- roleDistribution,
51
- recentSignups,
52
- systemHealth,
53
- };
54
- }
55
-
56
- private async getOverview(): Promise<StatsOverview> {
57
- const now = new Date();
58
- const todayStart = new Date(
59
- now.getFullYear(),
60
- now.getMonth(),
61
- now.getDate()
62
- );
63
- const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
64
- const dayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
65
-
66
- const [
67
- totalUsers,
68
- activeSessionsLast24h,
69
- totalUploads,
70
- storageResult,
71
- newUsersToday,
72
- newUsersThisWeek,
73
- ] = await Promise.all([
74
- this.prisma.user.count(),
75
- this.prisma.session.count({
76
- where: {
77
- createdAt: { gte: dayAgo },
78
- expiresAt: { gt: now },
79
- },
80
- }),
81
- this.prisma.upload.count(),
82
- this.prisma.upload.aggregate({
83
- _sum: { size: true },
84
- }),
85
- this.prisma.user.count({
86
- where: { createdAt: { gte: todayStart } },
87
- }),
88
- this.prisma.user.count({
89
- where: { createdAt: { gte: weekAgo } },
90
- }),
91
- ]);
92
-
93
- return {
94
- totalUsers,
95
- activeSessionsLast24h,
96
- totalUploads,
97
- storageUsedBytes: storageResult._sum.size ?? 0,
98
- newUsersToday,
99
- newUsersThisWeek,
100
- };
101
- }
102
-
103
- private async getUserGrowth(): Promise<UserGrowthPoint[]> {
104
- const thirtyDaysAgo = new Date();
105
- thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
106
-
107
- const users = await this.prisma.user.findMany({
108
- where: { createdAt: { gte: thirtyDaysAgo } },
109
- select: { createdAt: true },
110
- orderBy: { createdAt: 'asc' },
111
- });
112
-
113
- const totalBeforeRange = await this.prisma.user.count({
114
- where: { createdAt: { lt: thirtyDaysAgo } },
115
- });
116
-
117
- const dailyCounts: Record<string, number> = {};
118
- const dates: string[] = [];
119
-
120
- for (let i = 29; i >= 0; i--) {
121
- const date = new Date();
122
- date.setDate(date.getDate() - i);
123
- const dateStr = date.toISOString().split('T')[0];
124
- dates.push(dateStr);
125
- dailyCounts[dateStr] = 0;
126
- }
127
-
128
- users.forEach((user) => {
129
- const dateStr = user.createdAt.toISOString().split('T')[0];
130
- if (dailyCounts[dateStr] !== undefined) {
131
- dailyCounts[dateStr]++;
132
- }
133
- });
134
-
135
- let cumulative = totalBeforeRange;
136
- return dates.map((date) => {
137
- cumulative += dailyCounts[date];
138
- return {
139
- date,
140
- count: dailyCounts[date],
141
- cumulative,
142
- };
143
- });
144
- }
145
-
146
- private async getSessionActivity(): Promise<SessionActivityPoint[]> {
147
- const thirtyDaysAgo = new Date();
148
- thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
149
-
150
- const sessions = await this.prisma.session.findMany({
151
- where: { createdAt: { gte: thirtyDaysAgo } },
152
- select: { createdAt: true },
153
- });
154
-
155
- const dailyCounts: Record<string, number> = {};
156
- const dates: string[] = [];
157
-
158
- for (let i = 29; i >= 0; i--) {
159
- const date = new Date();
160
- date.setDate(date.getDate() - i);
161
- const dateStr = date.toISOString().split('T')[0];
162
- dates.push(dateStr);
163
- dailyCounts[dateStr] = 0;
164
- }
165
-
166
- sessions.forEach((session) => {
167
- const dateStr = session.createdAt.toISOString().split('T')[0];
168
- if (dailyCounts[dateStr] !== undefined) {
169
- dailyCounts[dateStr]++;
170
- }
171
- });
172
-
173
- return dates.map((date) => ({
174
- date,
175
- count: dailyCounts[date],
176
- }));
177
- }
178
-
179
- private async getAuthBreakdown(): Promise<AuthBreakdown> {
180
- const [verified, unverified, banned] = await Promise.all([
181
- this.prisma.user.count({ where: { emailVerified: true, banned: false } }),
182
- this.prisma.user.count({
183
- where: { emailVerified: false, banned: false },
184
- }),
185
- this.prisma.user.count({ where: { banned: true } }),
186
- ]);
187
-
188
- return { verified, unverified, banned };
189
- }
190
-
191
- private async getRoleDistribution(): Promise<RoleDistributionItem[]> {
192
- const roles = await this.prisma.user.groupBy({
193
- by: ['role'],
194
- _count: { role: true },
195
- orderBy: { _count: { role: 'desc' } },
196
- });
197
-
198
- return roles.map((r) => ({
199
- role: r.role,
200
- count: r._count.role,
201
- }));
202
- }
203
-
204
- private async getRecentSignups(): Promise<RecentSignup[]> {
205
- const users = await this.prisma.user.findMany({
206
- orderBy: { createdAt: 'desc' },
207
- take: 10,
208
- select: {
209
- id: true,
210
- email: true,
211
- name: true,
212
- image: true,
213
- createdAt: true,
214
- },
215
- });
216
-
217
- return users.map((u) => ({
218
- id: u.id,
219
- email: u.email,
220
- name: u.name,
221
- image: u.image,
222
- createdAt: u.createdAt.toISOString(),
223
- }));
224
- }
225
-
226
- private async getSystemHealth(): Promise<SystemHealth> {
227
- const start = Date.now();
228
- let database: 'connected' | 'degraded' | 'disconnected' = 'connected';
229
-
230
- try {
231
- await this.prisma.$queryRaw`SELECT 1`;
232
- const latency = Date.now() - start;
233
- if (latency > 500) {
234
- database = 'degraded';
235
- }
236
- } catch {
237
- database = 'disconnected';
238
- }
239
-
240
- return {
241
- database,
242
- uptime: Math.floor((Date.now() - SERVER_START_TIME) / 1000),
243
- lastChecked: new Date().toISOString(),
244
- dbLatencyMs: Date.now() - start,
245
- };
246
- }
247
-
248
- async getHealthCheck(): Promise<HealthCheckResponse> {
249
- const start = Date.now();
250
- let database: 'connected' | 'degraded' | 'disconnected' = 'connected';
251
- let status: 'ok' | 'degraded' | 'error' = 'ok';
252
-
253
- try {
254
- await this.prisma.$queryRaw`SELECT 1`;
255
- const latency = Date.now() - start;
256
- if (latency > 500) {
257
- database = 'degraded';
258
- status = 'degraded';
259
- }
260
- } catch {
261
- database = 'disconnected';
262
- status = 'error';
263
- }
264
-
265
- return {
266
- status,
267
- database,
268
- dbLatencyMs: Date.now() - start,
269
- uptime: Math.floor((Date.now() - SERVER_START_TIME) / 1000),
270
- timestamp: new Date().toISOString(),
271
- };
272
- }
273
- }