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.
- package/dist/index.js +35 -77
- package/package.json +5 -6
- package/template/.dockerignore +0 -59
- package/template/.github/workflows/ci.yml +0 -157
- package/template/.husky/pre-commit +0 -1
- package/template/.husky/pre-push +0 -1
- package/template/.lintstagedrc.cjs +0 -4
- package/template/.nvmrc +0 -1
- package/template/.prettierrc +0 -9
- package/template/.vscode/settings.json +0 -13
- package/template/CLAUDE.md +0 -175
- package/template/CONTRIBUTING.md +0 -32
- package/template/Dockerfile +0 -90
- package/template/GETTING_STARTED.md +0 -35
- package/template/LICENSE +0 -21
- package/template/README.md +0 -116
- package/template/apps/api/.dockerignore +0 -51
- package/template/apps/api/.env.local.example +0 -62
- package/template/apps/api/emails/account-deleted-email.tsx +0 -69
- package/template/apps/api/emails/components/email-layout.tsx +0 -154
- package/template/apps/api/emails/config.ts +0 -22
- package/template/apps/api/emails/password-changed-email.tsx +0 -88
- package/template/apps/api/emails/password-reset-email.tsx +0 -86
- package/template/apps/api/emails/verification-email.tsx +0 -85
- package/template/apps/api/emails/welcome-email.tsx +0 -70
- package/template/apps/api/package.json +0 -84
- package/template/apps/api/prisma/migrations/20251012111439_init/migration.sql +0 -13
- package/template/apps/api/prisma/migrations/20251018162629_add_better_auth_fields/migration.sql +0 -67
- package/template/apps/api/prisma/migrations/20251019142208_add_user_role_enum/migration.sql +0 -5
- package/template/apps/api/prisma/migrations/20251019182151_user_auth/migration.sql +0 -7
- package/template/apps/api/prisma/migrations/20251019211416_faster_session_lookup/migration.sql +0 -2
- package/template/apps/api/prisma/migrations/20251119124337_add_upload_model/migration.sql +0 -26
- package/template/apps/api/prisma/migrations/20251120071241_add_scope_to_account/migration.sql +0 -2
- package/template/apps/api/prisma/migrations/20251120072608_add_oauth_token_expiration_fields/migration.sql +0 -10
- package/template/apps/api/prisma/migrations/20251120144705_add_audit_logs/migration.sql +0 -29
- package/template/apps/api/prisma/migrations/20251127123614_remove_impersonated_by/migration.sql +0 -8
- package/template/apps/api/prisma/migrations/20251127125630_remove_audit_logs/migration.sql +0 -11
- package/template/apps/api/prisma/migrations/migration_lock.toml +0 -3
- package/template/apps/api/prisma/schema.prisma +0 -116
- package/template/apps/api/prisma/seed.ts +0 -159
- package/template/apps/api/prisma.config.ts +0 -14
- package/template/apps/api/src/app.ts +0 -377
- package/template/apps/api/src/common/logger.service.ts +0 -227
- package/template/apps/api/src/config/env.ts +0 -60
- package/template/apps/api/src/config/rate-limit.ts +0 -29
- package/template/apps/api/src/hooks/auth.ts +0 -122
- package/template/apps/api/src/plugins/auth.ts +0 -198
- package/template/apps/api/src/plugins/database.ts +0 -45
- package/template/apps/api/src/plugins/logger.ts +0 -33
- package/template/apps/api/src/plugins/multipart.ts +0 -16
- package/template/apps/api/src/plugins/scalar.ts +0 -20
- package/template/apps/api/src/plugins/schedule.ts +0 -52
- package/template/apps/api/src/plugins/services.ts +0 -66
- package/template/apps/api/src/plugins/swagger.ts +0 -56
- package/template/apps/api/src/routes/accounts.ts +0 -91
- package/template/apps/api/src/routes/admin-sessions.ts +0 -92
- package/template/apps/api/src/routes/metrics.ts +0 -71
- package/template/apps/api/src/routes/password.ts +0 -46
- package/template/apps/api/src/routes/sessions.ts +0 -53
- package/template/apps/api/src/routes/stats.ts +0 -38
- package/template/apps/api/src/routes/uploads-serve.ts +0 -27
- package/template/apps/api/src/routes/uploads.ts +0 -154
- package/template/apps/api/src/routes/users.ts +0 -114
- package/template/apps/api/src/routes/verification.ts +0 -90
- package/template/apps/api/src/server.ts +0 -34
- package/template/apps/api/src/services/accounts.service.ts +0 -125
- package/template/apps/api/src/services/authorization.service.ts +0 -162
- package/template/apps/api/src/services/email.service.ts +0 -170
- package/template/apps/api/src/services/file-storage.service.ts +0 -267
- package/template/apps/api/src/services/metrics.service.ts +0 -175
- package/template/apps/api/src/services/password.service.ts +0 -56
- package/template/apps/api/src/services/sessions.service.spec.ts +0 -134
- package/template/apps/api/src/services/sessions.service.ts +0 -276
- package/template/apps/api/src/services/stats.service.ts +0 -273
- package/template/apps/api/src/services/uploads.service.ts +0 -163
- package/template/apps/api/src/services/users.service.spec.ts +0 -249
- package/template/apps/api/src/services/users.service.ts +0 -198
- package/template/apps/api/src/utils/file-validation.ts +0 -108
- package/template/apps/api/start.sh +0 -33
- package/template/apps/api/test/helpers/fastify-app.ts +0 -24
- package/template/apps/api/test/helpers/mock-authorization.ts +0 -16
- package/template/apps/api/test/helpers/mock-logger.ts +0 -28
- package/template/apps/api/test/helpers/mock-prisma.ts +0 -30
- package/template/apps/api/test/helpers/test-db.ts +0 -125
- package/template/apps/api/test/integration/auth-flow.integration.spec.ts +0 -449
- package/template/apps/api/test/integration/password.integration.spec.ts +0 -427
- package/template/apps/api/test/integration/rate-limit.integration.spec.ts +0 -51
- package/template/apps/api/test/integration/sessions.integration.spec.ts +0 -445
- package/template/apps/api/test/integration/users.integration.spec.ts +0 -211
- package/template/apps/api/test/setup.ts +0 -31
- package/template/apps/api/tsconfig.json +0 -26
- package/template/apps/api/vitest.config.ts +0 -35
- package/template/apps/web/.env.local.example +0 -11
- package/template/apps/web/components.json +0 -24
- package/template/apps/web/next.config.ts +0 -22
- package/template/apps/web/package.json +0 -56
- package/template/apps/web/postcss.config.js +0 -5
- package/template/apps/web/public/apple-icon.png +0 -0
- package/template/apps/web/public/icon.png +0 -0
- package/template/apps/web/public/robots.txt +0 -3
- package/template/apps/web/src/app/(admin)/admin/layout.tsx +0 -222
- package/template/apps/web/src/app/(admin)/admin/page.tsx +0 -157
- package/template/apps/web/src/app/(admin)/admin/sessions/page.tsx +0 -18
- package/template/apps/web/src/app/(admin)/admin/users/page.tsx +0 -20
- package/template/apps/web/src/app/(auth)/forgot-password/page.tsx +0 -177
- package/template/apps/web/src/app/(auth)/login/page.tsx +0 -159
- package/template/apps/web/src/app/(auth)/reset-password/page.tsx +0 -245
- package/template/apps/web/src/app/(auth)/signup/page.tsx +0 -153
- package/template/apps/web/src/app/dashboard/change-password/page.tsx +0 -255
- package/template/apps/web/src/app/dashboard/page.tsx +0 -296
- package/template/apps/web/src/app/error.tsx +0 -32
- package/template/apps/web/src/app/examples/file-upload/page.tsx +0 -200
- package/template/apps/web/src/app/favicon.ico +0 -0
- package/template/apps/web/src/app/global-error.tsx +0 -96
- package/template/apps/web/src/app/globals.css +0 -22
- package/template/apps/web/src/app/icon.png +0 -0
- package/template/apps/web/src/app/layout.tsx +0 -34
- package/template/apps/web/src/app/not-found.tsx +0 -28
- package/template/apps/web/src/app/page.tsx +0 -192
- package/template/apps/web/src/components/admin/activity-feed.tsx +0 -101
- package/template/apps/web/src/components/admin/charts/auth-breakdown-chart.tsx +0 -114
- package/template/apps/web/src/components/admin/charts/chart-tooltip.tsx +0 -124
- package/template/apps/web/src/components/admin/charts/realtime-metrics-chart.tsx +0 -511
- package/template/apps/web/src/components/admin/charts/role-distribution-chart.tsx +0 -102
- package/template/apps/web/src/components/admin/charts/session-activity-chart.tsx +0 -90
- package/template/apps/web/src/components/admin/charts/user-growth-chart.tsx +0 -108
- package/template/apps/web/src/components/admin/health-indicator.tsx +0 -175
- package/template/apps/web/src/components/admin/refresh-control.tsx +0 -90
- package/template/apps/web/src/components/admin/session-revoke-all-dialog.tsx +0 -79
- package/template/apps/web/src/components/admin/session-revoke-dialog.tsx +0 -74
- package/template/apps/web/src/components/admin/sessions-management-table.tsx +0 -372
- package/template/apps/web/src/components/admin/stat-card.tsx +0 -137
- package/template/apps/web/src/components/admin/user-create-dialog.tsx +0 -152
- package/template/apps/web/src/components/admin/user-delete-dialog.tsx +0 -73
- package/template/apps/web/src/components/admin/user-edit-dialog.tsx +0 -170
- package/template/apps/web/src/components/admin/users-management-table.tsx +0 -285
- package/template/apps/web/src/components/auth/email-verification-banner.tsx +0 -85
- package/template/apps/web/src/components/auth/github-button.tsx +0 -40
- package/template/apps/web/src/components/auth/google-button.tsx +0 -54
- package/template/apps/web/src/components/auth/protected-route.tsx +0 -66
- package/template/apps/web/src/components/auth/redirect-if-authenticated.tsx +0 -31
- package/template/apps/web/src/components/auth/with-auth.tsx +0 -30
- package/template/apps/web/src/components/error/error-card.tsx +0 -47
- package/template/apps/web/src/components/error/forbidden.tsx +0 -25
- package/template/apps/web/src/components/landing/command-block.tsx +0 -64
- package/template/apps/web/src/components/landing/feature-card.tsx +0 -60
- package/template/apps/web/src/components/landing/included-feature-card.tsx +0 -63
- package/template/apps/web/src/components/landing/logo.tsx +0 -41
- package/template/apps/web/src/components/landing/tech-badge.tsx +0 -11
- package/template/apps/web/src/components/layout/auth-nav.tsx +0 -58
- package/template/apps/web/src/components/layout/footer.tsx +0 -3
- package/template/apps/web/src/config/landing-data.ts +0 -152
- package/template/apps/web/src/config/site.ts +0 -5
- package/template/apps/web/src/hooks/api/__tests__/use-users.test.tsx +0 -181
- package/template/apps/web/src/hooks/api/use-admin-sessions.ts +0 -75
- package/template/apps/web/src/hooks/api/use-admin-stats.ts +0 -33
- package/template/apps/web/src/hooks/api/use-sessions.ts +0 -52
- package/template/apps/web/src/hooks/api/use-uploads.ts +0 -156
- package/template/apps/web/src/hooks/api/use-users.ts +0 -149
- package/template/apps/web/src/hooks/use-mobile.ts +0 -21
- package/template/apps/web/src/hooks/use-realtime-metrics.ts +0 -120
- package/template/apps/web/src/lib/__tests__/utils.test.ts +0 -29
- package/template/apps/web/src/lib/api.ts +0 -151
- package/template/apps/web/src/lib/auth.ts +0 -13
- package/template/apps/web/src/lib/env.ts +0 -52
- package/template/apps/web/src/lib/form-utils.ts +0 -11
- package/template/apps/web/src/lib/utils.ts +0 -1
- package/template/apps/web/src/providers.tsx +0 -34
- package/template/apps/web/src/store/atoms.ts +0 -15
- package/template/apps/web/src/test/helpers/test-utils.tsx +0 -44
- package/template/apps/web/src/test/setup.ts +0 -8
- package/template/apps/web/tailwind.config.ts +0 -5
- package/template/apps/web/tsconfig.json +0 -26
- package/template/apps/web/vitest.config.ts +0 -32
- package/template/assets/logo-512.png +0 -0
- package/template/assets/logo.svg +0 -4
- package/template/docker-compose.prod.yml +0 -66
- package/template/docker-compose.yml +0 -36
- package/template/eslint.config.ts +0 -119
- package/template/package.json +0 -77
- package/template/packages/tailwind-config/package.json +0 -9
- package/template/packages/tailwind-config/theme.css +0 -179
- package/template/packages/types/package.json +0 -29
- package/template/packages/types/src/__tests__/schemas.test.ts +0 -255
- package/template/packages/types/src/api-response.ts +0 -53
- package/template/packages/types/src/health-check.ts +0 -11
- package/template/packages/types/src/pagination.ts +0 -41
- package/template/packages/types/src/role.ts +0 -5
- package/template/packages/types/src/session.ts +0 -48
- package/template/packages/types/src/stats.ts +0 -113
- package/template/packages/types/src/upload.ts +0 -51
- package/template/packages/types/src/user.ts +0 -36
- package/template/packages/types/tsconfig.json +0 -5
- package/template/packages/types/vitest.config.ts +0 -21
- package/template/packages/ui/components.json +0 -21
- package/template/packages/ui/package.json +0 -108
- package/template/packages/ui/src/__tests__/button.test.tsx +0 -70
- package/template/packages/ui/src/alert-dialog.tsx +0 -141
- package/template/packages/ui/src/alert.tsx +0 -66
- package/template/packages/ui/src/animated-theme-toggler.tsx +0 -167
- package/template/packages/ui/src/avatar.tsx +0 -53
- package/template/packages/ui/src/badge.tsx +0 -36
- package/template/packages/ui/src/button.tsx +0 -84
- package/template/packages/ui/src/card.tsx +0 -92
- package/template/packages/ui/src/checkbox.tsx +0 -32
- package/template/packages/ui/src/data-table/data-table-column-header.tsx +0 -68
- package/template/packages/ui/src/data-table/data-table-pagination.tsx +0 -99
- package/template/packages/ui/src/data-table/data-table-toolbar.tsx +0 -55
- package/template/packages/ui/src/data-table/data-table-view-options.tsx +0 -63
- package/template/packages/ui/src/data-table/data-table.tsx +0 -167
- package/template/packages/ui/src/dialog.tsx +0 -143
- package/template/packages/ui/src/dropdown-menu.tsx +0 -257
- package/template/packages/ui/src/empty-state.tsx +0 -52
- package/template/packages/ui/src/file-upload-input.tsx +0 -202
- package/template/packages/ui/src/form.tsx +0 -168
- package/template/packages/ui/src/hooks/use-mobile.ts +0 -19
- package/template/packages/ui/src/icons/brand-icons.tsx +0 -16
- package/template/packages/ui/src/input.tsx +0 -21
- package/template/packages/ui/src/label.tsx +0 -24
- package/template/packages/ui/src/lib/utils.ts +0 -6
- package/template/packages/ui/src/password-input.tsx +0 -102
- package/template/packages/ui/src/popover.tsx +0 -48
- package/template/packages/ui/src/radio-group.tsx +0 -45
- package/template/packages/ui/src/scroll-area.tsx +0 -58
- package/template/packages/ui/src/select.tsx +0 -187
- package/template/packages/ui/src/separator.tsx +0 -28
- package/template/packages/ui/src/sheet.tsx +0 -139
- package/template/packages/ui/src/sidebar.tsx +0 -726
- package/template/packages/ui/src/skeleton-variants.tsx +0 -87
- package/template/packages/ui/src/skeleton.tsx +0 -13
- package/template/packages/ui/src/slider.tsx +0 -63
- package/template/packages/ui/src/sonner.tsx +0 -25
- package/template/packages/ui/src/spinner.tsx +0 -16
- package/template/packages/ui/src/switch.tsx +0 -31
- package/template/packages/ui/src/table.tsx +0 -116
- package/template/packages/ui/src/tabs.tsx +0 -66
- package/template/packages/ui/src/textarea.tsx +0 -18
- package/template/packages/ui/src/tooltip.tsx +0 -61
- package/template/packages/ui/src/user-avatar.tsx +0 -97
- package/template/packages/ui/test-config.js +0 -3
- package/template/packages/ui/tsconfig.json +0 -12
- package/template/packages/ui/turbo.json +0 -18
- package/template/packages/ui/vitest.config.ts +0 -17
- package/template/packages/ui/vitest.setup.ts +0 -1
- package/template/packages/utils/package.json +0 -23
- package/template/packages/utils/src/__tests__/utils.test.ts +0 -223
- package/template/packages/utils/src/array.ts +0 -18
- package/template/packages/utils/src/async.ts +0 -3
- package/template/packages/utils/src/date.ts +0 -77
- package/template/packages/utils/src/errors.ts +0 -73
- package/template/packages/utils/src/number.ts +0 -11
- package/template/packages/utils/src/string.ts +0 -13
- package/template/packages/utils/tsconfig.json +0 -5
- package/template/packages/utils/vitest.config.ts +0 -21
- package/template/pnpm-workspace.yaml +0 -4
- package/template/tsconfig.base.json +0 -32
- package/template/turbo.json +0 -133
- package/template/vitest.shared.ts +0 -26
- package/template/vitest.workspace.ts +0 -9
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import type { MultipartFile } from '@fastify/multipart';
|
|
2
|
-
import {
|
|
3
|
-
DeleteUploadParamsSchema,
|
|
4
|
-
GetUploadsQuerySchema,
|
|
5
|
-
UploadResponseSchema,
|
|
6
|
-
UploadStatsSchema,
|
|
7
|
-
} from '@repo/packages-types/upload';
|
|
8
|
-
import {
|
|
9
|
-
UnauthorizedError,
|
|
10
|
-
ValidationError,
|
|
11
|
-
} from '@repo/packages-utils/errors';
|
|
12
|
-
import type { FastifyPluginAsync } from 'fastify';
|
|
13
|
-
import type { ZodTypeProvider } from 'fastify-type-provider-zod';
|
|
14
|
-
import { z } from 'zod';
|
|
15
|
-
|
|
16
|
-
import { RATE_LIMIT_CONFIG } from '@/config/rate-limit.js';
|
|
17
|
-
import { requireAuth } from '@/hooks/auth';
|
|
18
|
-
|
|
19
|
-
const uploadsRoutes: FastifyPluginAsync = async (app) => {
|
|
20
|
-
const server = app.withTypeProvider<ZodTypeProvider>();
|
|
21
|
-
|
|
22
|
-
// POST /api/uploads - Upload a file
|
|
23
|
-
server.post(
|
|
24
|
-
'/uploads',
|
|
25
|
-
{
|
|
26
|
-
onRequest: requireAuth,
|
|
27
|
-
config: {
|
|
28
|
-
rateLimit: {
|
|
29
|
-
max: RATE_LIMIT_CONFIG.routes.uploads.max,
|
|
30
|
-
timeWindow: RATE_LIMIT_CONFIG.routes.uploads.timeWindow,
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
schema: {
|
|
34
|
-
description: 'Upload a file',
|
|
35
|
-
tags: ['Uploads'],
|
|
36
|
-
response: {
|
|
37
|
-
201: UploadResponseSchema,
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
async (request, reply) => {
|
|
42
|
-
const userId = request.user?.id;
|
|
43
|
-
|
|
44
|
-
if (!userId) {
|
|
45
|
-
throw new UnauthorizedError();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const data = await request.file();
|
|
49
|
-
|
|
50
|
-
if (!data) {
|
|
51
|
-
throw new ValidationError('No file provided');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const file = data as MultipartFile;
|
|
55
|
-
|
|
56
|
-
const upload = await app.uploadsService.uploadFile(file, userId);
|
|
57
|
-
|
|
58
|
-
return reply.status(201).send(upload);
|
|
59
|
-
}
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
// GET /api/uploads - Get user's uploads
|
|
63
|
-
server.get(
|
|
64
|
-
'/uploads',
|
|
65
|
-
{
|
|
66
|
-
onRequest: requireAuth,
|
|
67
|
-
schema: {
|
|
68
|
-
querystring: GetUploadsQuerySchema,
|
|
69
|
-
description: 'Get paginated list of user uploads',
|
|
70
|
-
tags: ['Uploads'],
|
|
71
|
-
response: {
|
|
72
|
-
200: z.array(
|
|
73
|
-
UploadResponseSchema.omit({ user: true }).extend({
|
|
74
|
-
userId: z.string(),
|
|
75
|
-
})
|
|
76
|
-
),
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
|
-
async (request) => {
|
|
81
|
-
const userId = request.user?.id;
|
|
82
|
-
|
|
83
|
-
if (!userId) {
|
|
84
|
-
throw new UnauthorizedError();
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const { limit, offset } = request.query;
|
|
88
|
-
|
|
89
|
-
const uploads = await app.uploadsService.getUserUploads(userId, {
|
|
90
|
-
limit,
|
|
91
|
-
offset,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
return uploads;
|
|
95
|
-
}
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
// GET /api/uploads/stats - Get upload statistics
|
|
99
|
-
server.get(
|
|
100
|
-
'/uploads/stats',
|
|
101
|
-
{
|
|
102
|
-
onRequest: requireAuth,
|
|
103
|
-
schema: {
|
|
104
|
-
description: 'Get upload statistics for current user',
|
|
105
|
-
tags: ['Uploads'],
|
|
106
|
-
response: {
|
|
107
|
-
200: UploadStatsSchema,
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
async (request) => {
|
|
112
|
-
const userId = request.user?.id;
|
|
113
|
-
|
|
114
|
-
if (!userId) {
|
|
115
|
-
throw new UnauthorizedError();
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const stats = await app.uploadsService.getUploadStats(userId);
|
|
119
|
-
|
|
120
|
-
return stats;
|
|
121
|
-
}
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
// DELETE /api/uploads/:id - Delete an upload
|
|
125
|
-
server.delete(
|
|
126
|
-
'/uploads/:id',
|
|
127
|
-
{
|
|
128
|
-
onRequest: requireAuth,
|
|
129
|
-
schema: {
|
|
130
|
-
params: DeleteUploadParamsSchema,
|
|
131
|
-
description: 'Delete an upload',
|
|
132
|
-
tags: ['Uploads'],
|
|
133
|
-
response: {
|
|
134
|
-
204: z.void(),
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
async (request, reply) => {
|
|
139
|
-
const userId = request.user?.id;
|
|
140
|
-
|
|
141
|
-
if (!userId) {
|
|
142
|
-
throw new UnauthorizedError();
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const { id } = request.params;
|
|
146
|
-
|
|
147
|
-
await app.uploadsService.deleteUpload(id, userId);
|
|
148
|
-
|
|
149
|
-
return reply.status(204).send();
|
|
150
|
-
}
|
|
151
|
-
);
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
export default uploadsRoutes;
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { QueryUsersSchema } from '@repo/packages-types/pagination';
|
|
2
|
-
import {
|
|
3
|
-
CreateUserSchema,
|
|
4
|
-
GetUserByIdSchema,
|
|
5
|
-
UpdateUserSchema,
|
|
6
|
-
} from '@repo/packages-types/user';
|
|
7
|
-
import type { FastifyPluginAsync } from 'fastify';
|
|
8
|
-
import type { ZodTypeProvider } from 'fastify-type-provider-zod';
|
|
9
|
-
|
|
10
|
-
import { requireAuth, requireRole } from '@/hooks/auth';
|
|
11
|
-
|
|
12
|
-
const usersRoutes: FastifyPluginAsync = async (app) => {
|
|
13
|
-
const server = app.withTypeProvider<ZodTypeProvider>();
|
|
14
|
-
|
|
15
|
-
// GET /users - Get paginated users with filtering and sorting (Admin only)
|
|
16
|
-
server.get(
|
|
17
|
-
'/users',
|
|
18
|
-
{
|
|
19
|
-
preHandler: [requireAuth, requireRole(['admin', 'super_admin'])],
|
|
20
|
-
schema: {
|
|
21
|
-
querystring: QueryUsersSchema,
|
|
22
|
-
description:
|
|
23
|
-
'Get paginated users with filtering and sorting (Admin only)',
|
|
24
|
-
tags: ['Users'],
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
async (request) => {
|
|
28
|
-
return app.usersService.getUsers(request.query);
|
|
29
|
-
}
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
// GET /users/:id - Get user by ID (Admin only)
|
|
33
|
-
server.get(
|
|
34
|
-
'/users/:id',
|
|
35
|
-
{
|
|
36
|
-
preHandler: [requireAuth, requireRole(['admin', 'super_admin'])],
|
|
37
|
-
schema: {
|
|
38
|
-
params: GetUserByIdSchema,
|
|
39
|
-
description: 'Get user by ID (Admin only)',
|
|
40
|
-
tags: ['Users'],
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
async (request) => {
|
|
44
|
-
const user = await app.usersService.getUserById(request.params.id);
|
|
45
|
-
return { data: user };
|
|
46
|
-
}
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
// POST /users - Create a new user (Admin only)
|
|
50
|
-
server.post(
|
|
51
|
-
'/users',
|
|
52
|
-
{
|
|
53
|
-
preHandler: [requireAuth, requireRole(['admin', 'super_admin'])],
|
|
54
|
-
schema: {
|
|
55
|
-
body: CreateUserSchema,
|
|
56
|
-
description: 'Create a new user (Admin only)',
|
|
57
|
-
tags: ['Users'],
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
async (request, reply) => {
|
|
61
|
-
const user = await app.usersService.createUser(request.body);
|
|
62
|
-
|
|
63
|
-
return reply.status(201).send({ data: user });
|
|
64
|
-
}
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
// PATCH /users/:id - Update user by ID (Admin only)
|
|
68
|
-
server.patch(
|
|
69
|
-
'/users/:id',
|
|
70
|
-
{
|
|
71
|
-
preHandler: [requireAuth, requireRole(['admin', 'super_admin'])],
|
|
72
|
-
schema: {
|
|
73
|
-
params: GetUserByIdSchema,
|
|
74
|
-
body: UpdateUserSchema,
|
|
75
|
-
description: 'Update user by ID (Admin only)',
|
|
76
|
-
tags: ['Users'],
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
async (request) => {
|
|
80
|
-
const user = await app.usersService.updateUser(
|
|
81
|
-
request.user!.id,
|
|
82
|
-
request.user!.role as 'user' | 'admin' | 'super_admin',
|
|
83
|
-
request.params.id,
|
|
84
|
-
request.body
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
return { data: user };
|
|
88
|
-
}
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
// DELETE /users/:id - Delete user by ID (Admin only)
|
|
92
|
-
server.delete(
|
|
93
|
-
'/users/:id',
|
|
94
|
-
{
|
|
95
|
-
preHandler: [requireAuth, requireRole(['admin', 'super_admin'])],
|
|
96
|
-
schema: {
|
|
97
|
-
params: GetUserByIdSchema,
|
|
98
|
-
description: 'Delete user by ID (Admin only)',
|
|
99
|
-
tags: ['Users'],
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
async (request) => {
|
|
103
|
-
await app.usersService.deleteUser(
|
|
104
|
-
request.user!.id,
|
|
105
|
-
request.user!.role as 'user' | 'admin' | 'super_admin',
|
|
106
|
-
request.params.id
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
return { message: 'User deleted successfully' };
|
|
110
|
-
}
|
|
111
|
-
);
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
export default usersRoutes;
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import type { FastifyPluginAsync } from 'fastify';
|
|
2
|
-
import type { ZodTypeProvider } from 'fastify-type-provider-zod';
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
|
|
5
|
-
import { requireAuth } from '@/hooks/auth';
|
|
6
|
-
|
|
7
|
-
const ResendVerificationSchema = z.object({
|
|
8
|
-
email: z.string().email().optional(),
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
const verificationRoutes: FastifyPluginAsync = async (app) => {
|
|
12
|
-
const server = app.withTypeProvider<ZodTypeProvider>();
|
|
13
|
-
|
|
14
|
-
// POST /verification/resend - Resend verification email
|
|
15
|
-
server.post(
|
|
16
|
-
'/verification/resend',
|
|
17
|
-
{
|
|
18
|
-
preHandler: [requireAuth],
|
|
19
|
-
schema: {
|
|
20
|
-
body: ResendVerificationSchema,
|
|
21
|
-
description:
|
|
22
|
-
'Resend verification email to authenticated user or specified email',
|
|
23
|
-
tags: ['Verification'],
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
async (request, reply) => {
|
|
27
|
-
const currentUser = request.user;
|
|
28
|
-
if (!currentUser) {
|
|
29
|
-
return reply.status(401).send({
|
|
30
|
-
error: 'Unauthorized',
|
|
31
|
-
message: 'Authentication required',
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const targetEmail = request.body.email || currentUser.email;
|
|
36
|
-
|
|
37
|
-
if (request.body.email && request.body.email !== currentUser.email) {
|
|
38
|
-
return reply.status(403).send({
|
|
39
|
-
error: 'Forbidden',
|
|
40
|
-
message: 'You can only resend verification for your own email',
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const user = await app.prisma.user.findUnique({
|
|
45
|
-
where: { email: targetEmail },
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
if (!user) {
|
|
49
|
-
return reply.status(404).send({
|
|
50
|
-
error: 'Not Found',
|
|
51
|
-
message: 'User not found',
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (user.emailVerified) {
|
|
56
|
-
return reply.status(400).send({
|
|
57
|
-
error: 'Bad Request',
|
|
58
|
-
message: 'Email is already verified',
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
await app.auth.api.sendVerificationEmail({
|
|
64
|
-
body: {
|
|
65
|
-
email: user.email,
|
|
66
|
-
callbackURL: app.env.FRONTEND_URL,
|
|
67
|
-
},
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
return { message: 'Verification email sent successfully' };
|
|
71
|
-
} catch (error) {
|
|
72
|
-
request.log.error(
|
|
73
|
-
{
|
|
74
|
-
err: error instanceof Error ? error : new Error(String(error)),
|
|
75
|
-
email: user.email,
|
|
76
|
-
},
|
|
77
|
-
'Failed to send verification email via Better Auth'
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
return reply.status(500).send({
|
|
81
|
-
error: 'Email Send Failed',
|
|
82
|
-
message:
|
|
83
|
-
'Failed to send verification email. Please try again later or contact support.',
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
);
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
export default verificationRoutes;
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import closeWithGrace from 'close-with-grace';
|
|
2
|
-
|
|
3
|
-
import { app } from '@/app';
|
|
4
|
-
import { loadEnv } from '@/config/env';
|
|
5
|
-
|
|
6
|
-
const env = loadEnv();
|
|
7
|
-
|
|
8
|
-
const start = async () => {
|
|
9
|
-
try {
|
|
10
|
-
await app.listen({ port: env.PORT, host: '0.0.0.0' });
|
|
11
|
-
app.log.info(`API server ready at ${env.API_URL}`);
|
|
12
|
-
app.log.info(`Environment: ${env.NODE_ENV}`);
|
|
13
|
-
app.log.info(`CORS enabled for: ${env.FRONTEND_URL}`);
|
|
14
|
-
} catch (err) {
|
|
15
|
-
app.log.error(err);
|
|
16
|
-
process.exit(1);
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const closeListeners = closeWithGrace(
|
|
21
|
-
{ delay: Number(process.env.FASTIFY_CLOSE_GRACE_DELAY) || 500 },
|
|
22
|
-
async ({ err }) => {
|
|
23
|
-
if (err) {
|
|
24
|
-
app.log.error(err);
|
|
25
|
-
}
|
|
26
|
-
await app.close();
|
|
27
|
-
}
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
app.addHook('onClose', async () => {
|
|
31
|
-
closeListeners.uninstall();
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
start();
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { NotFoundError, ValidationError } from '@repo/packages-utils/errors';
|
|
2
|
-
|
|
3
|
-
import { type LoggerService } from '@/common/logger.service';
|
|
4
|
-
import type { PrismaClient } from '@/generated/client/client.js';
|
|
5
|
-
|
|
6
|
-
export interface ConnectedAccount {
|
|
7
|
-
providerId: string;
|
|
8
|
-
accountId: string;
|
|
9
|
-
connectedAt: Date;
|
|
10
|
-
scope?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface UserAccounts {
|
|
14
|
-
userId: string;
|
|
15
|
-
hasPassword: boolean;
|
|
16
|
-
connectedAccounts: ConnectedAccount[];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export class AccountsService {
|
|
20
|
-
constructor(
|
|
21
|
-
private readonly prisma: PrismaClient,
|
|
22
|
-
private readonly logger: LoggerService
|
|
23
|
-
) {
|
|
24
|
-
this.logger.setContext('AccountsService');
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async getUserAccounts(userId: string): Promise<UserAccounts> {
|
|
28
|
-
this.logger.info('Fetching user accounts', { userId });
|
|
29
|
-
|
|
30
|
-
const accounts = await this.prisma.account.findMany({
|
|
31
|
-
where: { userId },
|
|
32
|
-
select: {
|
|
33
|
-
providerId: true,
|
|
34
|
-
accountId: true,
|
|
35
|
-
createdAt: true,
|
|
36
|
-
scope: true,
|
|
37
|
-
password: true,
|
|
38
|
-
},
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
if (accounts.length === 0) {
|
|
42
|
-
throw new NotFoundError('User has no connected accounts');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const credentialAccount = accounts.find(
|
|
46
|
-
(a) => a.providerId === 'credential'
|
|
47
|
-
);
|
|
48
|
-
const hasPassword = !!(credentialAccount && credentialAccount.password);
|
|
49
|
-
|
|
50
|
-
const connectedAccounts: ConnectedAccount[] = accounts
|
|
51
|
-
.filter((a) => a.providerId !== 'credential')
|
|
52
|
-
.map((a) => ({
|
|
53
|
-
providerId: a.providerId,
|
|
54
|
-
accountId: a.accountId,
|
|
55
|
-
connectedAt: a.createdAt,
|
|
56
|
-
scope: a.scope || undefined,
|
|
57
|
-
}));
|
|
58
|
-
|
|
59
|
-
if (hasPassword) {
|
|
60
|
-
connectedAccounts.unshift({
|
|
61
|
-
providerId: 'credential',
|
|
62
|
-
accountId: credentialAccount!.accountId,
|
|
63
|
-
connectedAt: credentialAccount!.createdAt,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
userId,
|
|
69
|
-
hasPassword,
|
|
70
|
-
connectedAccounts,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async unlinkAccount(
|
|
75
|
-
userId: string,
|
|
76
|
-
providerId: string
|
|
77
|
-
): Promise<{ success: boolean }> {
|
|
78
|
-
this.logger.info('Unlinking account', { userId, providerId });
|
|
79
|
-
|
|
80
|
-
const accounts = await this.prisma.account.findMany({
|
|
81
|
-
where: { userId },
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
if (accounts.length <= 1) {
|
|
85
|
-
throw new ValidationError(
|
|
86
|
-
'Cannot unlink the only account. User must have at least one login method.'
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (providerId === 'credential') {
|
|
91
|
-
throw new ValidationError(
|
|
92
|
-
'Cannot unlink password login. Please change your password or contact support.'
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const account = accounts.find((a) => a.providerId === providerId);
|
|
97
|
-
if (!account) {
|
|
98
|
-
throw new NotFoundError(
|
|
99
|
-
`Account with provider ${providerId} not found for this user`
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
await this.prisma.account.delete({
|
|
104
|
-
where: { id: account.id },
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
this.logger.info('Account unlinked successfully', { userId, providerId });
|
|
108
|
-
|
|
109
|
-
return { success: true };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async canChangePassword(userId: string): Promise<boolean> {
|
|
113
|
-
const credentialAccount = await this.prisma.account.findFirst({
|
|
114
|
-
where: {
|
|
115
|
-
userId,
|
|
116
|
-
providerId: 'credential',
|
|
117
|
-
},
|
|
118
|
-
select: {
|
|
119
|
-
password: true,
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
return !!(credentialAccount && credentialAccount.password);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import type { Role } from '@repo/packages-types/role';
|
|
2
|
-
import { ForbiddenError } from '@repo/packages-utils/errors';
|
|
3
|
-
|
|
4
|
-
import type { LoggerService } from '@/common/logger.service';
|
|
5
|
-
|
|
6
|
-
export interface AuthorizationContext {
|
|
7
|
-
actorId: string;
|
|
8
|
-
actorRole: Role;
|
|
9
|
-
targetUserId?: string;
|
|
10
|
-
targetUserRole?: Role;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export class AuthorizationService {
|
|
14
|
-
private readonly roleHierarchy: Record<Role, number> = {
|
|
15
|
-
super_admin: 3,
|
|
16
|
-
admin: 2,
|
|
17
|
-
user: 1,
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
constructor(private readonly logger: LoggerService) {
|
|
21
|
-
this.logger.setContext('AuthorizationService');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
private getRoleLevel(role: Role): number {
|
|
25
|
-
return this.roleHierarchy[role];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
canModifyUser(actorRole: Role, targetRole: Role): boolean {
|
|
29
|
-
const actorLevel = this.getRoleLevel(actorRole);
|
|
30
|
-
const targetLevel = this.getRoleLevel(targetRole);
|
|
31
|
-
|
|
32
|
-
return actorLevel > targetLevel;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
canDeleteUser(actorRole: Role, targetRole: Role): boolean {
|
|
36
|
-
return this.canModifyUser(actorRole, targetRole);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
canChangeRole(
|
|
40
|
-
actorRole: Role,
|
|
41
|
-
targetCurrentRole: Role,
|
|
42
|
-
newRole: Role
|
|
43
|
-
): boolean {
|
|
44
|
-
const actorLevel = this.getRoleLevel(actorRole);
|
|
45
|
-
const targetLevel = this.getRoleLevel(targetCurrentRole);
|
|
46
|
-
const newRoleLevel = this.getRoleLevel(newRole);
|
|
47
|
-
|
|
48
|
-
return actorLevel > targetLevel && actorLevel > newRoleLevel;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
canChangeEmail(actorRole: Role): boolean {
|
|
52
|
-
return actorRole === 'super_admin';
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
assertCanModifyUser(
|
|
56
|
-
actorId: string,
|
|
57
|
-
actorRole: Role,
|
|
58
|
-
targetUserId: string,
|
|
59
|
-
targetRole: Role
|
|
60
|
-
): void {
|
|
61
|
-
if (actorId === targetUserId) {
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (!this.canModifyUser(actorRole, targetRole)) {
|
|
66
|
-
this.logger.warn('Authorization failed: Cannot modify user', {
|
|
67
|
-
actorId,
|
|
68
|
-
actorRole,
|
|
69
|
-
targetUserId,
|
|
70
|
-
targetRole,
|
|
71
|
-
});
|
|
72
|
-
throw new ForbiddenError(
|
|
73
|
-
`Insufficient permissions to modify user with role: ${targetRole}`,
|
|
74
|
-
{
|
|
75
|
-
requiredLevel: 'higher than target',
|
|
76
|
-
actorRole,
|
|
77
|
-
targetRole,
|
|
78
|
-
}
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
assertCanDeleteUser(
|
|
84
|
-
actorId: string,
|
|
85
|
-
actorRole: Role,
|
|
86
|
-
targetUserId: string,
|
|
87
|
-
targetRole: Role
|
|
88
|
-
): void {
|
|
89
|
-
if (actorId === targetUserId) {
|
|
90
|
-
this.logger.warn('Authorization failed: Cannot delete own account', {
|
|
91
|
-
actorId,
|
|
92
|
-
});
|
|
93
|
-
throw new ForbiddenError('Cannot delete your own account');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (!this.canDeleteUser(actorRole, targetRole)) {
|
|
97
|
-
this.logger.warn('Authorization failed: Cannot delete user', {
|
|
98
|
-
actorId,
|
|
99
|
-
actorRole,
|
|
100
|
-
targetUserId,
|
|
101
|
-
targetRole,
|
|
102
|
-
});
|
|
103
|
-
throw new ForbiddenError(
|
|
104
|
-
`Insufficient permissions to delete user with role: ${targetRole}`,
|
|
105
|
-
{
|
|
106
|
-
requiredLevel: 'higher than target',
|
|
107
|
-
actorRole,
|
|
108
|
-
targetRole,
|
|
109
|
-
}
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
assertCanChangeRole(
|
|
115
|
-
actorId: string,
|
|
116
|
-
actorRole: Role,
|
|
117
|
-
targetUserId: string,
|
|
118
|
-
targetCurrentRole: Role,
|
|
119
|
-
newRole: Role
|
|
120
|
-
): void {
|
|
121
|
-
if (actorId === targetUserId) {
|
|
122
|
-
this.logger.warn('Authorization failed: Cannot modify own role', {
|
|
123
|
-
actorId,
|
|
124
|
-
});
|
|
125
|
-
throw new ForbiddenError('Cannot modify your own role');
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (!this.canChangeRole(actorRole, targetCurrentRole, newRole)) {
|
|
129
|
-
this.logger.warn('Authorization failed: Cannot change role', {
|
|
130
|
-
actorId,
|
|
131
|
-
actorRole,
|
|
132
|
-
targetUserId,
|
|
133
|
-
targetCurrentRole,
|
|
134
|
-
newRole,
|
|
135
|
-
});
|
|
136
|
-
throw new ForbiddenError(
|
|
137
|
-
`Insufficient permissions to change role from ${targetCurrentRole} to ${newRole}`,
|
|
138
|
-
{
|
|
139
|
-
requiredLevel: 'higher than both current and target roles',
|
|
140
|
-
actorRole,
|
|
141
|
-
targetCurrentRole,
|
|
142
|
-
newRole,
|
|
143
|
-
}
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
assertCanChangeEmail(actorRole: Role): void {
|
|
149
|
-
if (!this.canChangeEmail(actorRole)) {
|
|
150
|
-
this.logger.warn('Authorization failed: Cannot change email', {
|
|
151
|
-
actorRole,
|
|
152
|
-
});
|
|
153
|
-
throw new ForbiddenError(
|
|
154
|
-
'Only super admins can change user email addresses',
|
|
155
|
-
{
|
|
156
|
-
requiredRole: 'super_admin',
|
|
157
|
-
currentRole: actorRole,
|
|
158
|
-
}
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|