create-blitzpack 0.1.0
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 +452 -0
- package/package.json +57 -0
- package/template/.dockerignore +59 -0
- package/template/.github/workflows/ci.yml +157 -0
- package/template/.husky/pre-commit +1 -0
- package/template/.husky/pre-push +1 -0
- package/template/.lintstagedrc.cjs +4 -0
- package/template/.nvmrc +1 -0
- package/template/.prettierrc +9 -0
- package/template/.vscode/settings.json +13 -0
- package/template/CLAUDE.md +175 -0
- package/template/CONTRIBUTING.md +32 -0
- package/template/Dockerfile +90 -0
- package/template/GETTING_STARTED.md +35 -0
- package/template/LICENSE +21 -0
- package/template/README.md +116 -0
- package/template/apps/api/.dockerignore +51 -0
- package/template/apps/api/.env.local.example +62 -0
- package/template/apps/api/emails/account-deleted-email.tsx +69 -0
- package/template/apps/api/emails/components/email-layout.tsx +154 -0
- package/template/apps/api/emails/config.ts +22 -0
- package/template/apps/api/emails/password-changed-email.tsx +88 -0
- package/template/apps/api/emails/password-reset-email.tsx +86 -0
- package/template/apps/api/emails/verification-email.tsx +85 -0
- package/template/apps/api/emails/welcome-email.tsx +70 -0
- package/template/apps/api/package.json +84 -0
- package/template/apps/api/prisma/migrations/20251012111439_init/migration.sql +13 -0
- package/template/apps/api/prisma/migrations/20251018162629_add_better_auth_fields/migration.sql +67 -0
- package/template/apps/api/prisma/migrations/20251019142208_add_user_role_enum/migration.sql +5 -0
- package/template/apps/api/prisma/migrations/20251019182151_user_auth/migration.sql +7 -0
- package/template/apps/api/prisma/migrations/20251019211416_faster_session_lookup/migration.sql +2 -0
- package/template/apps/api/prisma/migrations/20251119124337_add_upload_model/migration.sql +26 -0
- package/template/apps/api/prisma/migrations/20251120071241_add_scope_to_account/migration.sql +2 -0
- package/template/apps/api/prisma/migrations/20251120072608_add_oauth_token_expiration_fields/migration.sql +10 -0
- package/template/apps/api/prisma/migrations/20251120144705_add_audit_logs/migration.sql +29 -0
- package/template/apps/api/prisma/migrations/20251127123614_remove_impersonated_by/migration.sql +8 -0
- package/template/apps/api/prisma/migrations/20251127125630_remove_audit_logs/migration.sql +11 -0
- package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
- package/template/apps/api/prisma/schema.prisma +116 -0
- package/template/apps/api/prisma/seed.ts +159 -0
- package/template/apps/api/prisma.config.ts +14 -0
- package/template/apps/api/src/app.ts +377 -0
- package/template/apps/api/src/common/logger.service.ts +227 -0
- package/template/apps/api/src/config/env.ts +60 -0
- package/template/apps/api/src/config/rate-limit.ts +29 -0
- package/template/apps/api/src/hooks/auth.ts +122 -0
- package/template/apps/api/src/plugins/auth.ts +198 -0
- package/template/apps/api/src/plugins/database.ts +45 -0
- package/template/apps/api/src/plugins/logger.ts +33 -0
- package/template/apps/api/src/plugins/multipart.ts +16 -0
- package/template/apps/api/src/plugins/scalar.ts +20 -0
- package/template/apps/api/src/plugins/schedule.ts +52 -0
- package/template/apps/api/src/plugins/services.ts +66 -0
- package/template/apps/api/src/plugins/swagger.ts +56 -0
- package/template/apps/api/src/routes/accounts.ts +91 -0
- package/template/apps/api/src/routes/admin-sessions.ts +92 -0
- package/template/apps/api/src/routes/metrics.ts +71 -0
- package/template/apps/api/src/routes/password.ts +46 -0
- package/template/apps/api/src/routes/sessions.ts +53 -0
- package/template/apps/api/src/routes/stats.ts +38 -0
- package/template/apps/api/src/routes/uploads-serve.ts +27 -0
- package/template/apps/api/src/routes/uploads.ts +154 -0
- package/template/apps/api/src/routes/users.ts +114 -0
- package/template/apps/api/src/routes/verification.ts +90 -0
- package/template/apps/api/src/server.ts +34 -0
- package/template/apps/api/src/services/accounts.service.ts +125 -0
- package/template/apps/api/src/services/authorization.service.ts +162 -0
- package/template/apps/api/src/services/email.service.ts +170 -0
- package/template/apps/api/src/services/file-storage.service.ts +267 -0
- package/template/apps/api/src/services/metrics.service.ts +175 -0
- package/template/apps/api/src/services/password.service.ts +56 -0
- package/template/apps/api/src/services/sessions.service.spec.ts +134 -0
- package/template/apps/api/src/services/sessions.service.ts +276 -0
- package/template/apps/api/src/services/stats.service.ts +273 -0
- package/template/apps/api/src/services/uploads.service.ts +163 -0
- package/template/apps/api/src/services/users.service.spec.ts +249 -0
- package/template/apps/api/src/services/users.service.ts +198 -0
- package/template/apps/api/src/utils/file-validation.ts +108 -0
- package/template/apps/api/start.sh +33 -0
- package/template/apps/api/test/helpers/fastify-app.ts +24 -0
- package/template/apps/api/test/helpers/mock-authorization.ts +16 -0
- package/template/apps/api/test/helpers/mock-logger.ts +28 -0
- package/template/apps/api/test/helpers/mock-prisma.ts +30 -0
- package/template/apps/api/test/helpers/test-db.ts +125 -0
- package/template/apps/api/test/integration/auth-flow.integration.spec.ts +449 -0
- package/template/apps/api/test/integration/password.integration.spec.ts +427 -0
- package/template/apps/api/test/integration/rate-limit.integration.spec.ts +51 -0
- package/template/apps/api/test/integration/sessions.integration.spec.ts +445 -0
- package/template/apps/api/test/integration/users.integration.spec.ts +211 -0
- package/template/apps/api/test/setup.ts +31 -0
- package/template/apps/api/tsconfig.json +26 -0
- package/template/apps/api/vitest.config.ts +35 -0
- package/template/apps/web/.env.local.example +11 -0
- package/template/apps/web/components.json +24 -0
- package/template/apps/web/next.config.ts +22 -0
- package/template/apps/web/package.json +56 -0
- package/template/apps/web/postcss.config.js +5 -0
- 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 +3 -0
- package/template/apps/web/src/app/(admin)/admin/layout.tsx +222 -0
- package/template/apps/web/src/app/(admin)/admin/page.tsx +157 -0
- package/template/apps/web/src/app/(admin)/admin/sessions/page.tsx +18 -0
- package/template/apps/web/src/app/(admin)/admin/users/page.tsx +20 -0
- package/template/apps/web/src/app/(auth)/forgot-password/page.tsx +177 -0
- package/template/apps/web/src/app/(auth)/login/page.tsx +159 -0
- package/template/apps/web/src/app/(auth)/reset-password/page.tsx +245 -0
- package/template/apps/web/src/app/(auth)/signup/page.tsx +153 -0
- package/template/apps/web/src/app/dashboard/change-password/page.tsx +255 -0
- package/template/apps/web/src/app/dashboard/page.tsx +296 -0
- package/template/apps/web/src/app/error.tsx +32 -0
- package/template/apps/web/src/app/examples/file-upload/page.tsx +200 -0
- package/template/apps/web/src/app/favicon.ico +0 -0
- package/template/apps/web/src/app/global-error.tsx +96 -0
- package/template/apps/web/src/app/globals.css +22 -0
- package/template/apps/web/src/app/icon.png +0 -0
- package/template/apps/web/src/app/layout.tsx +34 -0
- package/template/apps/web/src/app/not-found.tsx +28 -0
- package/template/apps/web/src/app/page.tsx +192 -0
- package/template/apps/web/src/components/admin/activity-feed.tsx +101 -0
- package/template/apps/web/src/components/admin/charts/auth-breakdown-chart.tsx +114 -0
- package/template/apps/web/src/components/admin/charts/chart-tooltip.tsx +124 -0
- package/template/apps/web/src/components/admin/charts/realtime-metrics-chart.tsx +511 -0
- package/template/apps/web/src/components/admin/charts/role-distribution-chart.tsx +102 -0
- package/template/apps/web/src/components/admin/charts/session-activity-chart.tsx +90 -0
- package/template/apps/web/src/components/admin/charts/user-growth-chart.tsx +108 -0
- package/template/apps/web/src/components/admin/health-indicator.tsx +175 -0
- package/template/apps/web/src/components/admin/refresh-control.tsx +90 -0
- package/template/apps/web/src/components/admin/session-revoke-all-dialog.tsx +79 -0
- package/template/apps/web/src/components/admin/session-revoke-dialog.tsx +74 -0
- package/template/apps/web/src/components/admin/sessions-management-table.tsx +372 -0
- package/template/apps/web/src/components/admin/stat-card.tsx +137 -0
- package/template/apps/web/src/components/admin/user-create-dialog.tsx +152 -0
- package/template/apps/web/src/components/admin/user-delete-dialog.tsx +73 -0
- package/template/apps/web/src/components/admin/user-edit-dialog.tsx +170 -0
- package/template/apps/web/src/components/admin/users-management-table.tsx +285 -0
- package/template/apps/web/src/components/auth/email-verification-banner.tsx +85 -0
- package/template/apps/web/src/components/auth/github-button.tsx +40 -0
- package/template/apps/web/src/components/auth/google-button.tsx +54 -0
- package/template/apps/web/src/components/auth/protected-route.tsx +66 -0
- package/template/apps/web/src/components/auth/redirect-if-authenticated.tsx +31 -0
- package/template/apps/web/src/components/auth/with-auth.tsx +30 -0
- package/template/apps/web/src/components/error/error-card.tsx +47 -0
- package/template/apps/web/src/components/error/forbidden.tsx +25 -0
- package/template/apps/web/src/components/landing/command-block.tsx +64 -0
- package/template/apps/web/src/components/landing/feature-card.tsx +60 -0
- package/template/apps/web/src/components/landing/included-feature-card.tsx +63 -0
- package/template/apps/web/src/components/landing/logo.tsx +41 -0
- package/template/apps/web/src/components/landing/tech-badge.tsx +11 -0
- package/template/apps/web/src/components/layout/auth-nav.tsx +58 -0
- package/template/apps/web/src/components/layout/footer.tsx +3 -0
- package/template/apps/web/src/config/landing-data.ts +152 -0
- package/template/apps/web/src/config/site.ts +5 -0
- package/template/apps/web/src/hooks/api/__tests__/use-users.test.tsx +181 -0
- package/template/apps/web/src/hooks/api/use-admin-sessions.ts +75 -0
- package/template/apps/web/src/hooks/api/use-admin-stats.ts +33 -0
- package/template/apps/web/src/hooks/api/use-sessions.ts +52 -0
- package/template/apps/web/src/hooks/api/use-uploads.ts +156 -0
- package/template/apps/web/src/hooks/api/use-users.ts +149 -0
- package/template/apps/web/src/hooks/use-mobile.ts +21 -0
- package/template/apps/web/src/hooks/use-realtime-metrics.ts +120 -0
- package/template/apps/web/src/lib/__tests__/utils.test.ts +29 -0
- package/template/apps/web/src/lib/api.ts +151 -0
- package/template/apps/web/src/lib/auth.ts +13 -0
- package/template/apps/web/src/lib/env.ts +52 -0
- package/template/apps/web/src/lib/form-utils.ts +11 -0
- package/template/apps/web/src/lib/utils.ts +1 -0
- package/template/apps/web/src/providers.tsx +34 -0
- package/template/apps/web/src/store/atoms.ts +15 -0
- package/template/apps/web/src/test/helpers/test-utils.tsx +44 -0
- package/template/apps/web/src/test/setup.ts +8 -0
- package/template/apps/web/tailwind.config.ts +5 -0
- package/template/apps/web/tsconfig.json +26 -0
- package/template/apps/web/vitest.config.ts +32 -0
- package/template/assets/logo-512.png +0 -0
- package/template/assets/logo.svg +4 -0
- package/template/docker-compose.prod.yml +66 -0
- package/template/docker-compose.yml +36 -0
- package/template/eslint.config.ts +119 -0
- package/template/package.json +77 -0
- package/template/packages/tailwind-config/package.json +9 -0
- package/template/packages/tailwind-config/theme.css +179 -0
- package/template/packages/types/package.json +29 -0
- package/template/packages/types/src/__tests__/schemas.test.ts +255 -0
- package/template/packages/types/src/api-response.ts +53 -0
- package/template/packages/types/src/health-check.ts +11 -0
- package/template/packages/types/src/pagination.ts +41 -0
- package/template/packages/types/src/role.ts +5 -0
- package/template/packages/types/src/session.ts +48 -0
- package/template/packages/types/src/stats.ts +113 -0
- package/template/packages/types/src/upload.ts +51 -0
- package/template/packages/types/src/user.ts +36 -0
- package/template/packages/types/tsconfig.json +5 -0
- package/template/packages/types/vitest.config.ts +21 -0
- package/template/packages/ui/components.json +21 -0
- package/template/packages/ui/package.json +108 -0
- package/template/packages/ui/src/__tests__/button.test.tsx +70 -0
- package/template/packages/ui/src/alert-dialog.tsx +141 -0
- package/template/packages/ui/src/alert.tsx +66 -0
- package/template/packages/ui/src/animated-theme-toggler.tsx +167 -0
- package/template/packages/ui/src/avatar.tsx +53 -0
- package/template/packages/ui/src/badge.tsx +36 -0
- package/template/packages/ui/src/button.tsx +84 -0
- package/template/packages/ui/src/card.tsx +92 -0
- package/template/packages/ui/src/checkbox.tsx +32 -0
- package/template/packages/ui/src/data-table/data-table-column-header.tsx +68 -0
- package/template/packages/ui/src/data-table/data-table-pagination.tsx +99 -0
- package/template/packages/ui/src/data-table/data-table-toolbar.tsx +55 -0
- package/template/packages/ui/src/data-table/data-table-view-options.tsx +63 -0
- package/template/packages/ui/src/data-table/data-table.tsx +167 -0
- package/template/packages/ui/src/dialog.tsx +143 -0
- package/template/packages/ui/src/dropdown-menu.tsx +257 -0
- package/template/packages/ui/src/empty-state.tsx +52 -0
- package/template/packages/ui/src/file-upload-input.tsx +202 -0
- package/template/packages/ui/src/form.tsx +168 -0
- package/template/packages/ui/src/hooks/use-mobile.ts +19 -0
- package/template/packages/ui/src/icons/brand-icons.tsx +16 -0
- package/template/packages/ui/src/input.tsx +21 -0
- package/template/packages/ui/src/label.tsx +24 -0
- package/template/packages/ui/src/lib/utils.ts +6 -0
- package/template/packages/ui/src/password-input.tsx +102 -0
- package/template/packages/ui/src/popover.tsx +48 -0
- package/template/packages/ui/src/radio-group.tsx +45 -0
- package/template/packages/ui/src/scroll-area.tsx +58 -0
- package/template/packages/ui/src/select.tsx +187 -0
- package/template/packages/ui/src/separator.tsx +28 -0
- package/template/packages/ui/src/sheet.tsx +139 -0
- package/template/packages/ui/src/sidebar.tsx +726 -0
- package/template/packages/ui/src/skeleton-variants.tsx +87 -0
- package/template/packages/ui/src/skeleton.tsx +13 -0
- package/template/packages/ui/src/slider.tsx +63 -0
- package/template/packages/ui/src/sonner.tsx +25 -0
- package/template/packages/ui/src/spinner.tsx +16 -0
- package/template/packages/ui/src/switch.tsx +31 -0
- package/template/packages/ui/src/table.tsx +116 -0
- package/template/packages/ui/src/tabs.tsx +66 -0
- package/template/packages/ui/src/textarea.tsx +18 -0
- package/template/packages/ui/src/tooltip.tsx +61 -0
- package/template/packages/ui/src/user-avatar.tsx +97 -0
- package/template/packages/ui/test-config.js +3 -0
- package/template/packages/ui/tsconfig.json +12 -0
- package/template/packages/ui/turbo.json +18 -0
- package/template/packages/ui/vitest.config.ts +17 -0
- package/template/packages/ui/vitest.setup.ts +1 -0
- package/template/packages/utils/package.json +23 -0
- package/template/packages/utils/src/__tests__/utils.test.ts +223 -0
- package/template/packages/utils/src/array.ts +18 -0
- package/template/packages/utils/src/async.ts +3 -0
- package/template/packages/utils/src/date.ts +77 -0
- package/template/packages/utils/src/errors.ts +73 -0
- package/template/packages/utils/src/number.ts +11 -0
- package/template/packages/utils/src/string.ts +13 -0
- package/template/packages/utils/tsconfig.json +5 -0
- package/template/packages/utils/vitest.config.ts +21 -0
- package/template/pnpm-workspace.yaml +4 -0
- package/template/tsconfig.base.json +32 -0
- package/template/turbo.json +133 -0
- package/template/vitest.shared.ts +26 -0
- package/template/vitest.workspace.ts +9 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { ForbiddenError, UnauthorizedError } from '@repo/packages-utils/errors';
|
|
2
|
+
import type { FastifyReply, FastifyRequest } from 'fastify';
|
|
3
|
+
|
|
4
|
+
export interface AuthUser {
|
|
5
|
+
id: string;
|
|
6
|
+
email: string;
|
|
7
|
+
name: string;
|
|
8
|
+
role: string;
|
|
9
|
+
session?: {
|
|
10
|
+
id: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
declare module 'fastify' {
|
|
15
|
+
interface FastifyRequest {
|
|
16
|
+
user?: AuthUser;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function requireAuth(
|
|
21
|
+
request: FastifyRequest,
|
|
22
|
+
reply: FastifyReply
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
try {
|
|
25
|
+
const session = await request.server.auth.api.getSession({
|
|
26
|
+
headers: request.headers as unknown as Headers,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (!session?.user) {
|
|
30
|
+
throw new UnauthorizedError('Authentication required');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Fetch user from database to check ban status and get latest role
|
|
34
|
+
const user = await request.server.prisma.user.findUnique({
|
|
35
|
+
where: { id: session.user.id },
|
|
36
|
+
select: {
|
|
37
|
+
id: true,
|
|
38
|
+
email: true,
|
|
39
|
+
name: true,
|
|
40
|
+
role: true,
|
|
41
|
+
banned: true,
|
|
42
|
+
banReason: true,
|
|
43
|
+
banExpires: true,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (!user) {
|
|
48
|
+
request.log.warn(
|
|
49
|
+
{
|
|
50
|
+
userId: session.user.id,
|
|
51
|
+
},
|
|
52
|
+
'User not found in database'
|
|
53
|
+
);
|
|
54
|
+
throw new UnauthorizedError('User not found');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check if user is banned
|
|
58
|
+
if (user.banned) {
|
|
59
|
+
// Check if ban has expired
|
|
60
|
+
if (user.banExpires && new Date() > user.banExpires) {
|
|
61
|
+
// Automatically unban user
|
|
62
|
+
await request.server.prisma.user.update({
|
|
63
|
+
where: { id: user.id },
|
|
64
|
+
data: {
|
|
65
|
+
banned: false,
|
|
66
|
+
banReason: null,
|
|
67
|
+
banExpires: null,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
request.log.info({ userId: user.id }, 'User ban expired and removed');
|
|
71
|
+
} else {
|
|
72
|
+
request.log.warn(
|
|
73
|
+
{
|
|
74
|
+
userId: user.id,
|
|
75
|
+
banReason: user.banReason,
|
|
76
|
+
},
|
|
77
|
+
'Banned user attempted to access system'
|
|
78
|
+
);
|
|
79
|
+
throw new ForbiddenError('Your account has been banned', {
|
|
80
|
+
reason: user.banReason,
|
|
81
|
+
expiresAt: user.banExpires?.toISOString(),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
request.user = {
|
|
87
|
+
id: user.id,
|
|
88
|
+
email: user.email,
|
|
89
|
+
name: user.name ?? '',
|
|
90
|
+
role: user.role,
|
|
91
|
+
session: session.session
|
|
92
|
+
? {
|
|
93
|
+
id: session.session.id,
|
|
94
|
+
}
|
|
95
|
+
: undefined,
|
|
96
|
+
};
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (error instanceof UnauthorizedError || error instanceof ForbiddenError) {
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
request.log.error(error, 'Auth hook error');
|
|
102
|
+
throw new UnauthorizedError('Invalid or expired session');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function requireRole(roles: string[]) {
|
|
107
|
+
return async (
|
|
108
|
+
request: FastifyRequest,
|
|
109
|
+
reply: FastifyReply
|
|
110
|
+
): Promise<void> => {
|
|
111
|
+
if (!request.user) {
|
|
112
|
+
throw new UnauthorizedError('Authentication required');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!roles.includes(request.user.role)) {
|
|
116
|
+
throw new ForbiddenError('Insufficient permissions', {
|
|
117
|
+
required: roles,
|
|
118
|
+
current: request.user.role,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { betterAuth } from 'better-auth';
|
|
2
|
+
import { prismaAdapter } from 'better-auth/adapters/prisma';
|
|
3
|
+
import { admin } from 'better-auth/plugins';
|
|
4
|
+
import type { FastifyPluginAsync, FastifyRequest } from 'fastify';
|
|
5
|
+
import fp from 'fastify-plugin';
|
|
6
|
+
|
|
7
|
+
import { loadEnv } from '@/config/env.js';
|
|
8
|
+
import { RATE_LIMIT_CONFIG } from '@/config/rate-limit.js';
|
|
9
|
+
import { type PrismaClient } from '@/generated/client/client.js';
|
|
10
|
+
|
|
11
|
+
declare module 'fastify' {
|
|
12
|
+
interface FastifyInstance {
|
|
13
|
+
auth: ReturnType<typeof betterAuth>;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const authPlugin: FastifyPluginAsync = async (app) => {
|
|
18
|
+
const env = loadEnv();
|
|
19
|
+
|
|
20
|
+
const socialProviders: Record<string, unknown> = {};
|
|
21
|
+
|
|
22
|
+
if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {
|
|
23
|
+
socialProviders.google = {
|
|
24
|
+
clientId: env.GOOGLE_CLIENT_ID,
|
|
25
|
+
clientSecret: env.GOOGLE_CLIENT_SECRET,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (env.GITHUB_CLIENT_ID && env.GITHUB_CLIENT_SECRET) {
|
|
30
|
+
socialProviders.github = {
|
|
31
|
+
clientId: env.GITHUB_CLIENT_ID,
|
|
32
|
+
clientSecret: env.GITHUB_CLIENT_SECRET,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
app.log.info(
|
|
37
|
+
`[+] OAuth configured with providers: ${Object.keys(socialProviders).join(', ')}`
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const auth = betterAuth({
|
|
41
|
+
database: prismaAdapter(app.prisma as unknown as PrismaClient, {
|
|
42
|
+
provider: 'postgresql',
|
|
43
|
+
}),
|
|
44
|
+
baseURL: env.BETTER_AUTH_URL,
|
|
45
|
+
secret: env.BETTER_AUTH_SECRET,
|
|
46
|
+
emailAndPassword: {
|
|
47
|
+
enabled: true,
|
|
48
|
+
requireEmailVerification: false,
|
|
49
|
+
sendEmailVerificationOnSignUp: true,
|
|
50
|
+
autoSignInAfterVerification: true,
|
|
51
|
+
resetPasswordTokenExpiresIn: 3600,
|
|
52
|
+
sendResetPassword: async ({ user, token }) => {
|
|
53
|
+
const resetUrl = `${env.FRONTEND_URL}/reset-password?token=${token}`;
|
|
54
|
+
await app.emailService.sendPasswordResetEmail(user.email, resetUrl);
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
emailVerification: {
|
|
58
|
+
sendOnSignUp: true,
|
|
59
|
+
autoSignInAfterVerification: true,
|
|
60
|
+
sendVerificationEmail: async ({ user, url }) => {
|
|
61
|
+
const urlObj = new URL(url);
|
|
62
|
+
urlObj.searchParams.set('callbackURL', env.FRONTEND_URL);
|
|
63
|
+
await app.emailService.sendVerificationEmail(
|
|
64
|
+
user.email,
|
|
65
|
+
urlObj.toString()
|
|
66
|
+
);
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
session: {
|
|
70
|
+
expiresIn: 60 * 60 * 24 * 7, // 7 days
|
|
71
|
+
updateAge: 60 * 60 * 24, // Update every 24 hours
|
|
72
|
+
cookieCache: {
|
|
73
|
+
enabled: true,
|
|
74
|
+
maxAge: 60 * 60 * 24 * 30, // 30 days
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
advanced: {
|
|
78
|
+
// IMPORTANT: For cross-domain deployments
|
|
79
|
+
// useSecureCookies forces secure cookies even in development
|
|
80
|
+
useSecureCookies: env.NODE_ENV === 'production',
|
|
81
|
+
database: {
|
|
82
|
+
generateId: () =>
|
|
83
|
+
`auth-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
|
84
|
+
},
|
|
85
|
+
redirectURLs: {
|
|
86
|
+
onError: env.FRONTEND_URL,
|
|
87
|
+
afterSignIn: env.FRONTEND_URL,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
trustedOrigins: [env.FRONTEND_URL],
|
|
91
|
+
socialProviders,
|
|
92
|
+
plugins: [
|
|
93
|
+
admin({
|
|
94
|
+
defaultRole: 'user',
|
|
95
|
+
adminRoles: ['admin', 'super_admin'],
|
|
96
|
+
}),
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
app.decorate('auth', auth);
|
|
101
|
+
|
|
102
|
+
app.all(
|
|
103
|
+
'/api/auth/*',
|
|
104
|
+
{
|
|
105
|
+
config: {
|
|
106
|
+
rateLimit: {
|
|
107
|
+
max: RATE_LIMIT_CONFIG.routes.auth.max,
|
|
108
|
+
timeWindow: RATE_LIMIT_CONFIG.routes.auth.timeWindow,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
async (request, reply) => {
|
|
113
|
+
try {
|
|
114
|
+
// Convert Fastify request to Web Request for Better Auth
|
|
115
|
+
const webRequest = await toWebRequest(request);
|
|
116
|
+
|
|
117
|
+
// Handle the request with Better Auth
|
|
118
|
+
const response = await auth.handler(webRequest);
|
|
119
|
+
|
|
120
|
+
// Set status
|
|
121
|
+
reply.status(response.status);
|
|
122
|
+
|
|
123
|
+
// Process headers and modify Set-Cookie for cross-domain
|
|
124
|
+
response.headers.forEach((value, key) => {
|
|
125
|
+
if (key.toLowerCase() === 'set-cookie') {
|
|
126
|
+
// For cross-domain cookie support, we need to modify cookie attributes
|
|
127
|
+
// Better Auth sets cookies, but we need to ensure SameSite=None for cross-domain
|
|
128
|
+
const cookieValue = value;
|
|
129
|
+
|
|
130
|
+
if (env.NODE_ENV === 'production') {
|
|
131
|
+
let modifiedCookie = cookieValue;
|
|
132
|
+
|
|
133
|
+
if (!modifiedCookie.includes('Secure')) {
|
|
134
|
+
modifiedCookie += '; Secure';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (modifiedCookie.includes('SameSite=Lax')) {
|
|
138
|
+
modifiedCookie = modifiedCookie.replace(
|
|
139
|
+
'SameSite=Lax',
|
|
140
|
+
'SameSite=None'
|
|
141
|
+
);
|
|
142
|
+
} else if (!modifiedCookie.includes('SameSite=')) {
|
|
143
|
+
modifiedCookie += '; SameSite=None';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
reply.header(key, modifiedCookie);
|
|
147
|
+
} else {
|
|
148
|
+
reply.header(key, cookieValue);
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
reply.header(key, value);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const body = await response.text();
|
|
156
|
+
return reply.send(body);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
app.log.error(error, 'Better Auth handler error');
|
|
159
|
+
return reply.status(500).send({
|
|
160
|
+
statusCode: 500,
|
|
161
|
+
error: 'Internal Server Error',
|
|
162
|
+
message: 'Authentication error occurred',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
app.log.info('[+] Better Auth configured');
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
async function toWebRequest(request: FastifyRequest): Promise<Request> {
|
|
172
|
+
const url = new URL(request.url, `${request.protocol}://${request.hostname}`);
|
|
173
|
+
|
|
174
|
+
const headers = new Headers();
|
|
175
|
+
Object.entries(request.headers).forEach(([key, value]) => {
|
|
176
|
+
if (value) {
|
|
177
|
+
const headerValue = Array.isArray(value)
|
|
178
|
+
? value.join(', ')
|
|
179
|
+
: String(value);
|
|
180
|
+
headers.set(key, headerValue);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
let body: string | null = null;
|
|
185
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
186
|
+
if (request.body) {
|
|
187
|
+
body = JSON.stringify(request.body);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return new Request(url.toString(), {
|
|
192
|
+
method: request.method,
|
|
193
|
+
headers,
|
|
194
|
+
body,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export default fp(authPlugin);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import 'dotenv-flow/config';
|
|
2
|
+
|
|
3
|
+
import { PrismaPg } from '@prisma/adapter-pg';
|
|
4
|
+
import { type FastifyPluginAsync } from 'fastify';
|
|
5
|
+
import fp from 'fastify-plugin';
|
|
6
|
+
import { Pool } from 'pg';
|
|
7
|
+
|
|
8
|
+
import { loadEnv } from '@/config/env';
|
|
9
|
+
import { PrismaClient } from '@/generated/client/client.js';
|
|
10
|
+
|
|
11
|
+
declare module 'fastify' {
|
|
12
|
+
interface FastifyInstance {
|
|
13
|
+
prisma: PrismaClient;
|
|
14
|
+
pgPool: Pool;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const databasePlugin: FastifyPluginAsync = async (app) => {
|
|
19
|
+
const env = loadEnv();
|
|
20
|
+
|
|
21
|
+
const pool = new Pool({ connectionString: env.DATABASE_URL });
|
|
22
|
+
const adapter = new PrismaPg(pool);
|
|
23
|
+
|
|
24
|
+
const prisma = new PrismaClient({
|
|
25
|
+
adapter,
|
|
26
|
+
log:
|
|
27
|
+
env.LOG_LEVEL === 'verbose'
|
|
28
|
+
? ['query', 'info', 'warn', 'error']
|
|
29
|
+
: ['warn', 'error'],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
await prisma.$connect();
|
|
33
|
+
app.log.info('[+] Database connected successfully');
|
|
34
|
+
|
|
35
|
+
app.decorate('prisma', prisma);
|
|
36
|
+
app.decorate('pgPool', pool);
|
|
37
|
+
|
|
38
|
+
app.addHook('onClose', async (instance) => {
|
|
39
|
+
instance.log.info('[-] Disconnecting from database...');
|
|
40
|
+
await instance.prisma.$disconnect();
|
|
41
|
+
await instance.pgPool.end();
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default fp(databasePlugin);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { FastifyPluginAsync } from 'fastify';
|
|
2
|
+
import fp from 'fastify-plugin';
|
|
3
|
+
|
|
4
|
+
import { LoggerService } from '@/common/logger.service';
|
|
5
|
+
import { loadEnv } from '@/config/env';
|
|
6
|
+
|
|
7
|
+
const env = loadEnv();
|
|
8
|
+
|
|
9
|
+
declare module 'fastify' {
|
|
10
|
+
interface FastifyInstance {
|
|
11
|
+
logger: LoggerService;
|
|
12
|
+
}
|
|
13
|
+
interface FastifyRequest {
|
|
14
|
+
logger: LoggerService;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const loggerPlugin: FastifyPluginAsync = async (app) => {
|
|
19
|
+
const logger = new LoggerService(app.log);
|
|
20
|
+
logger.setContext('FastifyApp');
|
|
21
|
+
|
|
22
|
+
app.decorate('logger', logger);
|
|
23
|
+
|
|
24
|
+
app.addHook('onRequest', async (request) => {
|
|
25
|
+
request.logger = new LoggerService(request.log);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
app.log.info(
|
|
29
|
+
`[+] Logger service configured with verbosity: ${env.LOG_LEVEL}`
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default fp(loggerPlugin);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import multipart from '@fastify/multipart';
|
|
2
|
+
import type { FastifyPluginAsync } from 'fastify';
|
|
3
|
+
import fp from 'fastify-plugin';
|
|
4
|
+
|
|
5
|
+
import { MAX_FILE_SIZE } from '@/utils/file-validation';
|
|
6
|
+
|
|
7
|
+
const multipartPlugin: FastifyPluginAsync = async (app) => {
|
|
8
|
+
await app.register(multipart, {
|
|
9
|
+
limits: {
|
|
10
|
+
fileSize: MAX_FILE_SIZE,
|
|
11
|
+
files: 1, // Allow 1 file per request (can be increased)
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default fp(multipartPlugin);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import apiReference from '@scalar/fastify-api-reference';
|
|
2
|
+
import type { FastifyPluginAsync } from 'fastify';
|
|
3
|
+
import fp from 'fastify-plugin';
|
|
4
|
+
|
|
5
|
+
const scalarPlugin: FastifyPluginAsync = async (app) => {
|
|
6
|
+
await app.register(apiReference, {
|
|
7
|
+
routePrefix: '/docs',
|
|
8
|
+
configuration: {
|
|
9
|
+
theme: 'purple',
|
|
10
|
+
darkMode: true,
|
|
11
|
+
layout: 'modern',
|
|
12
|
+
showSidebar: true,
|
|
13
|
+
searchHotKey: 'k',
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
app.log.info('[+] Scalar docs available at /docs');
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default fp(scalarPlugin);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fastifySchedule from '@fastify/schedule';
|
|
2
|
+
import type { FastifyPluginAsync } from 'fastify';
|
|
3
|
+
import fp from 'fastify-plugin';
|
|
4
|
+
import { SimpleIntervalJob, Task } from 'toad-scheduler';
|
|
5
|
+
|
|
6
|
+
const schedulePlugin: FastifyPluginAsync = async (app) => {
|
|
7
|
+
// Register the schedule plugin
|
|
8
|
+
await app.register(fastifySchedule);
|
|
9
|
+
|
|
10
|
+
// Session cleanup task - runs every day at 3 AM (86400 seconds = 24 hours)
|
|
11
|
+
const sessionCleanupTask = new Task('session-cleanup', async () => {
|
|
12
|
+
const logger = app.logger.child('SessionCleanupTask');
|
|
13
|
+
logger.info('Starting expired session cleanup');
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const result = await app.prisma.session.deleteMany({
|
|
17
|
+
where: {
|
|
18
|
+
expiresAt: {
|
|
19
|
+
lt: new Date(),
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
logger.info('Expired sessions cleaned up successfully', {
|
|
25
|
+
deletedCount: result.count,
|
|
26
|
+
});
|
|
27
|
+
} catch (error) {
|
|
28
|
+
logger.error(
|
|
29
|
+
'Failed to cleanup expired sessions',
|
|
30
|
+
error instanceof Error ? error : new Error(String(error))
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Create a job that runs every 24 hours
|
|
36
|
+
const job = new SimpleIntervalJob(
|
|
37
|
+
{ days: 1, runImmediately: false },
|
|
38
|
+
sessionCleanupTask
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Add the job to the scheduler
|
|
42
|
+
app.scheduler.addSimpleIntervalJob(job);
|
|
43
|
+
|
|
44
|
+
app.log.info('[+] Scheduled tasks configured');
|
|
45
|
+
|
|
46
|
+
app.addHook('onClose', async (instance) => {
|
|
47
|
+
instance.log.info('[-] Stopping scheduled tasks...');
|
|
48
|
+
instance.scheduler.stop();
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default fp(schedulePlugin);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { FastifyPluginAsync } from 'fastify';
|
|
2
|
+
import fp from 'fastify-plugin';
|
|
3
|
+
|
|
4
|
+
import type { Env } from '@/config/env';
|
|
5
|
+
import { loadEnv } from '@/config/env';
|
|
6
|
+
import { AccountsService } from '@/services/accounts.service';
|
|
7
|
+
import { AuthorizationService } from '@/services/authorization.service';
|
|
8
|
+
import { EmailService } from '@/services/email.service';
|
|
9
|
+
import { FileStorageService } from '@/services/file-storage.service';
|
|
10
|
+
import { PasswordService } from '@/services/password.service';
|
|
11
|
+
import { SessionsService } from '@/services/sessions.service';
|
|
12
|
+
import { StatsService } from '@/services/stats.service';
|
|
13
|
+
import { UploadsService } from '@/services/uploads.service';
|
|
14
|
+
import { UsersService } from '@/services/users.service';
|
|
15
|
+
|
|
16
|
+
declare module 'fastify' {
|
|
17
|
+
interface FastifyInstance {
|
|
18
|
+
env: Env;
|
|
19
|
+
authorizationService: AuthorizationService;
|
|
20
|
+
usersService: UsersService;
|
|
21
|
+
sessionsService: SessionsService;
|
|
22
|
+
passwordService: PasswordService;
|
|
23
|
+
emailService: EmailService;
|
|
24
|
+
fileStorageService: FileStorageService;
|
|
25
|
+
uploadsService: UploadsService;
|
|
26
|
+
accountsService: AccountsService;
|
|
27
|
+
statsService: StatsService;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const servicesPlugin: FastifyPluginAsync = async (app) => {
|
|
32
|
+
const env = loadEnv();
|
|
33
|
+
|
|
34
|
+
const authorizationService = new AuthorizationService(app.logger);
|
|
35
|
+
const emailService = new EmailService(env, app.logger, app.prisma);
|
|
36
|
+
const fileStorageService = new FileStorageService(env, app.logger);
|
|
37
|
+
const usersService = new UsersService(
|
|
38
|
+
app.prisma,
|
|
39
|
+
app.logger,
|
|
40
|
+
authorizationService
|
|
41
|
+
);
|
|
42
|
+
const sessionsService = new SessionsService(app.prisma, authorizationService);
|
|
43
|
+
const passwordService = new PasswordService(app.prisma, sessionsService);
|
|
44
|
+
const uploadsService = new UploadsService(
|
|
45
|
+
app.prisma,
|
|
46
|
+
fileStorageService,
|
|
47
|
+
app.logger
|
|
48
|
+
);
|
|
49
|
+
const accountsService = new AccountsService(app.prisma, app.logger);
|
|
50
|
+
const statsService = new StatsService(app.prisma, app.logger);
|
|
51
|
+
|
|
52
|
+
app.decorate('env', env);
|
|
53
|
+
app.decorate('authorizationService', authorizationService);
|
|
54
|
+
app.decorate('emailService', emailService);
|
|
55
|
+
app.decorate('fileStorageService', fileStorageService);
|
|
56
|
+
app.decorate('usersService', usersService);
|
|
57
|
+
app.decorate('sessionsService', sessionsService);
|
|
58
|
+
app.decorate('passwordService', passwordService);
|
|
59
|
+
app.decorate('uploadsService', uploadsService);
|
|
60
|
+
app.decorate('accountsService', accountsService);
|
|
61
|
+
app.decorate('statsService', statsService);
|
|
62
|
+
|
|
63
|
+
app.log.info('[+] Services configured');
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export default fp(servicesPlugin);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import swagger from '@fastify/swagger';
|
|
2
|
+
import type { FastifyPluginAsync } from 'fastify';
|
|
3
|
+
import fp from 'fastify-plugin';
|
|
4
|
+
import {
|
|
5
|
+
jsonSchemaTransform,
|
|
6
|
+
type ZodTypeProvider,
|
|
7
|
+
} from 'fastify-type-provider-zod';
|
|
8
|
+
|
|
9
|
+
import { loadEnv } from '@/config/env';
|
|
10
|
+
|
|
11
|
+
const swaggerPlugin: FastifyPluginAsync = async (app) => {
|
|
12
|
+
const env = loadEnv();
|
|
13
|
+
|
|
14
|
+
await app.withTypeProvider<ZodTypeProvider>().register(swagger, {
|
|
15
|
+
openapi: {
|
|
16
|
+
info: {
|
|
17
|
+
title: 'API',
|
|
18
|
+
description: 'Production-ready TypeScript API built with Fastify',
|
|
19
|
+
version: '1.0.0',
|
|
20
|
+
},
|
|
21
|
+
servers: [
|
|
22
|
+
{
|
|
23
|
+
url: env.API_URL,
|
|
24
|
+
description: `${env.NODE_ENV.charAt(0).toUpperCase() + env.NODE_ENV.slice(1)} server`,
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
components: {
|
|
28
|
+
securitySchemes: {
|
|
29
|
+
cookieAuth: {
|
|
30
|
+
type: 'apiKey',
|
|
31
|
+
in: 'cookie',
|
|
32
|
+
name: 'better-auth.session_token',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
security: [
|
|
37
|
+
{
|
|
38
|
+
cookieAuth: [],
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
tags: [
|
|
42
|
+
{ name: 'Health', description: 'Health check endpoints' },
|
|
43
|
+
{ name: 'Users', description: 'User management endpoints' },
|
|
44
|
+
{
|
|
45
|
+
name: 'Auth',
|
|
46
|
+
description: 'Authentication endpoints (handled by Better Auth)',
|
|
47
|
+
},
|
|
48
|
+
{ name: 'Sessions', description: 'Session management endpoints' },
|
|
49
|
+
{ name: 'Password', description: 'Password management endpoints' },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
transform: jsonSchemaTransform,
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export default fp(swaggerPlugin);
|
|
@@ -0,0 +1,91 @@
|
|
|
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 accountsRoutes: FastifyPluginAsync = async (app) => {
|
|
8
|
+
const server = app.withTypeProvider<ZodTypeProvider>();
|
|
9
|
+
|
|
10
|
+
server.get(
|
|
11
|
+
'/accounts',
|
|
12
|
+
{
|
|
13
|
+
schema: {
|
|
14
|
+
description: 'Get all connected accounts for the authenticated user',
|
|
15
|
+
tags: ['Accounts'],
|
|
16
|
+
response: {
|
|
17
|
+
200: z.object({
|
|
18
|
+
userId: z.string(),
|
|
19
|
+
hasPassword: z.boolean(),
|
|
20
|
+
connectedAccounts: z.array(
|
|
21
|
+
z.object({
|
|
22
|
+
providerId: z.string(),
|
|
23
|
+
accountId: z.string(),
|
|
24
|
+
connectedAt: z.date(),
|
|
25
|
+
scope: z.string().optional(),
|
|
26
|
+
})
|
|
27
|
+
),
|
|
28
|
+
}),
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
preHandler: requireAuth,
|
|
32
|
+
},
|
|
33
|
+
async (request) => {
|
|
34
|
+
const userId = request.user!.id;
|
|
35
|
+
return app.accountsService.getUserAccounts(userId);
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
server.delete(
|
|
40
|
+
'/accounts/:providerId',
|
|
41
|
+
{
|
|
42
|
+
schema: {
|
|
43
|
+
description: 'Unlink an OAuth provider from the user account',
|
|
44
|
+
tags: ['Accounts'],
|
|
45
|
+
params: z.object({
|
|
46
|
+
providerId: z.enum(['google', 'github']),
|
|
47
|
+
}),
|
|
48
|
+
response: {
|
|
49
|
+
200: z.object({
|
|
50
|
+
success: z.boolean(),
|
|
51
|
+
}),
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
preHandler: requireAuth,
|
|
55
|
+
},
|
|
56
|
+
async (request) => {
|
|
57
|
+
const userId = request.user!.id;
|
|
58
|
+
const { providerId } = request.params;
|
|
59
|
+
const result = await app.accountsService.unlinkAccount(
|
|
60
|
+
userId,
|
|
61
|
+
providerId
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
server.get(
|
|
69
|
+
'/accounts/can-change-password',
|
|
70
|
+
{
|
|
71
|
+
schema: {
|
|
72
|
+
description:
|
|
73
|
+
'Check if user can change password (has password set via credential provider)',
|
|
74
|
+
tags: ['Accounts'],
|
|
75
|
+
response: {
|
|
76
|
+
200: z.object({
|
|
77
|
+
canChangePassword: z.boolean(),
|
|
78
|
+
}),
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
preHandler: requireAuth,
|
|
82
|
+
},
|
|
83
|
+
async (request) => {
|
|
84
|
+
const userId = request.user!.id;
|
|
85
|
+
const canChange = await app.accountsService.canChangePassword(userId);
|
|
86
|
+
return { canChangePassword: canChange };
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export default accountsRoutes;
|