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,153 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
4
|
+
import { Button } from '@repo/packages-ui/button';
|
|
5
|
+
import { Input } from '@repo/packages-ui/input';
|
|
6
|
+
import { Label } from '@repo/packages-ui/label';
|
|
7
|
+
import { PasswordInput } from '@repo/packages-ui/password-input';
|
|
8
|
+
import Link from 'next/link';
|
|
9
|
+
import { useRouter } from 'next/navigation';
|
|
10
|
+
import React, { useState } from 'react';
|
|
11
|
+
import { useForm } from 'react-hook-form';
|
|
12
|
+
import { toast } from 'sonner';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
|
|
15
|
+
import { GithubButton } from '@/components/auth/github-button';
|
|
16
|
+
import { GoogleButton } from '@/components/auth/google-button';
|
|
17
|
+
import { RedirectIfAuthenticated } from '@/components/auth/redirect-if-authenticated';
|
|
18
|
+
import { authClient } from '@/lib/auth';
|
|
19
|
+
|
|
20
|
+
const signupSchema = z.object({
|
|
21
|
+
name: z.string().min(2, 'Name must be at least 2 characters'),
|
|
22
|
+
email: z.string().email('Invalid email address'),
|
|
23
|
+
password: z.string().min(8, 'Password must be at least 8 characters'),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
type SignupFormData = z.infer<typeof signupSchema>;
|
|
27
|
+
|
|
28
|
+
export default function SignupPage() {
|
|
29
|
+
const router = useRouter();
|
|
30
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
31
|
+
|
|
32
|
+
const {
|
|
33
|
+
register,
|
|
34
|
+
handleSubmit,
|
|
35
|
+
formState: { errors },
|
|
36
|
+
} = useForm<SignupFormData>({
|
|
37
|
+
resolver: zodResolver(signupSchema),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const onSubmit = async (data: SignupFormData) => {
|
|
41
|
+
setIsLoading(true);
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const result = await authClient.signUp.email({
|
|
45
|
+
email: data.email,
|
|
46
|
+
password: data.password,
|
|
47
|
+
name: data.name,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (result.error) {
|
|
51
|
+
toast.error(result.error.message || 'Failed to create account');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
toast.success('Account created successfully!');
|
|
56
|
+
router.push('/dashboard');
|
|
57
|
+
} catch (error) {
|
|
58
|
+
toast.error('An unexpected error occurred');
|
|
59
|
+
console.error('Signup error:', error);
|
|
60
|
+
} finally {
|
|
61
|
+
setIsLoading(false);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<RedirectIfAuthenticated>
|
|
67
|
+
<div className="flex min-h-screen items-center justify-center p-4">
|
|
68
|
+
<div className="bg-card w-full max-w-md rounded-lg border p-8">
|
|
69
|
+
<div className="mb-6">
|
|
70
|
+
<h1 className="text-2xl font-semibold">Create Account</h1>
|
|
71
|
+
<p className="text-muted-foreground mt-2 text-sm">
|
|
72
|
+
Enter your information to create a new account
|
|
73
|
+
</p>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div className="mb-6 space-y-3">
|
|
77
|
+
<GoogleButton />
|
|
78
|
+
<GithubButton />
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div className="relative mb-6">
|
|
82
|
+
<div className="absolute inset-0 flex items-center">
|
|
83
|
+
<span className="w-full border-t" />
|
|
84
|
+
</div>
|
|
85
|
+
<div className="relative flex justify-center text-xs uppercase">
|
|
86
|
+
<span className="bg-card text-muted-foreground px-2">
|
|
87
|
+
Or continue with email
|
|
88
|
+
</span>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
93
|
+
<div className="space-y-2">
|
|
94
|
+
<Label htmlFor="name">Name</Label>
|
|
95
|
+
<Input
|
|
96
|
+
id="name"
|
|
97
|
+
type="text"
|
|
98
|
+
placeholder="John Doe"
|
|
99
|
+
{...register('name')}
|
|
100
|
+
disabled={isLoading}
|
|
101
|
+
/>
|
|
102
|
+
{errors.name && (
|
|
103
|
+
<p className="text-destructive text-xs">
|
|
104
|
+
{errors.name.message}
|
|
105
|
+
</p>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
<div className="space-y-2">
|
|
109
|
+
<Label htmlFor="email">Email</Label>
|
|
110
|
+
<Input
|
|
111
|
+
id="email"
|
|
112
|
+
type="email"
|
|
113
|
+
placeholder="name@example.com"
|
|
114
|
+
{...register('email')}
|
|
115
|
+
disabled={isLoading}
|
|
116
|
+
/>
|
|
117
|
+
{errors.email && (
|
|
118
|
+
<p className="text-destructive text-xs">
|
|
119
|
+
{errors.email.message}
|
|
120
|
+
</p>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
<div className="space-y-2">
|
|
124
|
+
<Label htmlFor="password">Password</Label>
|
|
125
|
+
<PasswordInput
|
|
126
|
+
id="password"
|
|
127
|
+
placeholder="Create a password (min. 8 characters)"
|
|
128
|
+
{...register('password')}
|
|
129
|
+
disabled={isLoading}
|
|
130
|
+
/>
|
|
131
|
+
{errors.password && (
|
|
132
|
+
<p className="text-destructive text-xs">
|
|
133
|
+
{errors.password.message}
|
|
134
|
+
</p>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
<div className="flex flex-col gap-4 pt-2">
|
|
138
|
+
<Button type="submit" className="w-full" isLoading={isLoading}>
|
|
139
|
+
Create Account
|
|
140
|
+
</Button>
|
|
141
|
+
<p className="text-muted-foreground text-center text-sm">
|
|
142
|
+
Already have an account?{' '}
|
|
143
|
+
<Link href="/login" className="text-primary hover:underline">
|
|
144
|
+
Sign in
|
|
145
|
+
</Link>
|
|
146
|
+
</p>
|
|
147
|
+
</div>
|
|
148
|
+
</form>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</RedirectIfAuthenticated>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
4
|
+
import { Button } from '@repo/packages-ui/button';
|
|
5
|
+
import { Checkbox } from '@repo/packages-ui/checkbox';
|
|
6
|
+
import {
|
|
7
|
+
Form,
|
|
8
|
+
FormControl,
|
|
9
|
+
FormDescription,
|
|
10
|
+
FormField,
|
|
11
|
+
FormItem,
|
|
12
|
+
FormLabel,
|
|
13
|
+
FormMessage,
|
|
14
|
+
} from '@repo/packages-ui/form';
|
|
15
|
+
import { PasswordInput } from '@repo/packages-ui/password-input';
|
|
16
|
+
import { ArrowLeft, CheckCircle2, KeyRound, ShieldAlert } from 'lucide-react';
|
|
17
|
+
import Link from 'next/link';
|
|
18
|
+
import { useRouter } from 'next/navigation';
|
|
19
|
+
import React, { useState } from 'react';
|
|
20
|
+
import { useForm } from 'react-hook-form';
|
|
21
|
+
import { toast } from 'sonner';
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
|
|
24
|
+
import { ProtectedRoute } from '@/components/auth/protected-route';
|
|
25
|
+
import { authClient } from '@/lib/auth';
|
|
26
|
+
|
|
27
|
+
const changePasswordSchema = z
|
|
28
|
+
.object({
|
|
29
|
+
currentPassword: z.string().min(1, 'Current password is required'),
|
|
30
|
+
newPassword: z.string().min(8, 'Password must be at least 8 characters'),
|
|
31
|
+
confirmPassword: z.string(),
|
|
32
|
+
revokeOtherSessions: z.boolean(),
|
|
33
|
+
})
|
|
34
|
+
.refine((data) => data.newPassword === data.confirmPassword, {
|
|
35
|
+
message: 'Passwords do not match',
|
|
36
|
+
path: ['confirmPassword'],
|
|
37
|
+
})
|
|
38
|
+
.refine((data) => data.currentPassword !== data.newPassword, {
|
|
39
|
+
message: 'New password must be different from current password',
|
|
40
|
+
path: ['newPassword'],
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
type ChangePasswordFormData = z.infer<typeof changePasswordSchema>;
|
|
44
|
+
|
|
45
|
+
export default function ChangePasswordPage() {
|
|
46
|
+
const router = useRouter();
|
|
47
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
48
|
+
const [changeSuccess, setChangeSuccess] = useState(false);
|
|
49
|
+
|
|
50
|
+
const form = useForm<ChangePasswordFormData>({
|
|
51
|
+
resolver: zodResolver(changePasswordSchema),
|
|
52
|
+
defaultValues: {
|
|
53
|
+
currentPassword: '',
|
|
54
|
+
newPassword: '',
|
|
55
|
+
confirmPassword: '',
|
|
56
|
+
revokeOtherSessions: true,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const onSubmit = async (data: ChangePasswordFormData) => {
|
|
61
|
+
setIsLoading(true);
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const result = await authClient.changePassword({
|
|
65
|
+
currentPassword: data.currentPassword,
|
|
66
|
+
newPassword: data.newPassword,
|
|
67
|
+
revokeOtherSessions: data.revokeOtherSessions,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (result.error) {
|
|
71
|
+
toast.error(result.error.message || 'Failed to change password');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
setChangeSuccess(true);
|
|
76
|
+
form.reset();
|
|
77
|
+
toast.success(
|
|
78
|
+
data.revokeOtherSessions
|
|
79
|
+
? 'Password changed successfully! All other sessions have been signed out.'
|
|
80
|
+
: 'Password changed successfully!'
|
|
81
|
+
);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
toast.error('An unexpected error occurred');
|
|
84
|
+
console.error('Change password error:', error);
|
|
85
|
+
} finally {
|
|
86
|
+
setIsLoading(false);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const hasErrors = Object.keys(form.formState.errors).length > 0;
|
|
91
|
+
|
|
92
|
+
if (changeSuccess) {
|
|
93
|
+
return (
|
|
94
|
+
<ProtectedRoute redirectTo="/login">
|
|
95
|
+
<div className="container mx-auto max-w-3xl space-y-6 p-8">
|
|
96
|
+
<div className="border-border bg-card rounded-lg border p-8">
|
|
97
|
+
<div className="mb-6 text-center">
|
|
98
|
+
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-green-500/10 text-green-600 dark:text-green-500">
|
|
99
|
+
<CheckCircle2 className="h-8 w-8" />
|
|
100
|
+
</div>
|
|
101
|
+
<h1 className="text-2xl font-bold">
|
|
102
|
+
Password Changed Successfully
|
|
103
|
+
</h1>
|
|
104
|
+
<p className="text-muted-foreground mt-2">
|
|
105
|
+
Your password has been updated. You can continue using your
|
|
106
|
+
account with the new password.
|
|
107
|
+
</p>
|
|
108
|
+
</div>
|
|
109
|
+
<div className="flex gap-3">
|
|
110
|
+
<Button
|
|
111
|
+
className="flex-1"
|
|
112
|
+
onClick={() => router.push('/dashboard')}
|
|
113
|
+
>
|
|
114
|
+
Back to Dashboard
|
|
115
|
+
</Button>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</ProtectedRoute>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<ProtectedRoute redirectTo="/login">
|
|
125
|
+
<div className="container mx-auto max-w-3xl space-y-6 p-8">
|
|
126
|
+
<div className="flex items-center gap-2">
|
|
127
|
+
<Link
|
|
128
|
+
href="/dashboard"
|
|
129
|
+
className="text-muted-foreground hover:text-foreground flex items-center gap-1 text-sm transition-colors"
|
|
130
|
+
>
|
|
131
|
+
<ArrowLeft className="h-4 w-4" />
|
|
132
|
+
Back to Dashboard
|
|
133
|
+
</Link>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div className="border-border bg-card rounded-lg border p-8">
|
|
137
|
+
<div className="mb-6 flex items-center gap-3">
|
|
138
|
+
<div className="bg-primary/10 text-primary rounded-full p-3">
|
|
139
|
+
<KeyRound className="h-6 w-6" />
|
|
140
|
+
</div>
|
|
141
|
+
<div>
|
|
142
|
+
<h1 className="text-2xl font-bold">Change Password</h1>
|
|
143
|
+
<p className="text-muted-foreground">
|
|
144
|
+
Update your password to keep your account secure
|
|
145
|
+
</p>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<Form {...form}>
|
|
150
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
151
|
+
<FormField
|
|
152
|
+
control={form.control}
|
|
153
|
+
name="currentPassword"
|
|
154
|
+
render={({ field }) => (
|
|
155
|
+
<FormItem>
|
|
156
|
+
<FormLabel>Current Password</FormLabel>
|
|
157
|
+
<FormControl>
|
|
158
|
+
<PasswordInput
|
|
159
|
+
placeholder="Enter current password"
|
|
160
|
+
disabled={isLoading}
|
|
161
|
+
{...field}
|
|
162
|
+
/>
|
|
163
|
+
</FormControl>
|
|
164
|
+
<FormMessage />
|
|
165
|
+
</FormItem>
|
|
166
|
+
)}
|
|
167
|
+
/>
|
|
168
|
+
|
|
169
|
+
<FormField
|
|
170
|
+
control={form.control}
|
|
171
|
+
name="newPassword"
|
|
172
|
+
render={({ field }) => (
|
|
173
|
+
<FormItem>
|
|
174
|
+
<FormLabel>New Password</FormLabel>
|
|
175
|
+
<FormControl>
|
|
176
|
+
<PasswordInput
|
|
177
|
+
placeholder="Enter new password"
|
|
178
|
+
disabled={isLoading}
|
|
179
|
+
{...field}
|
|
180
|
+
/>
|
|
181
|
+
</FormControl>
|
|
182
|
+
<FormMessage />
|
|
183
|
+
</FormItem>
|
|
184
|
+
)}
|
|
185
|
+
/>
|
|
186
|
+
|
|
187
|
+
<FormField
|
|
188
|
+
control={form.control}
|
|
189
|
+
name="confirmPassword"
|
|
190
|
+
render={({ field }) => (
|
|
191
|
+
<FormItem>
|
|
192
|
+
<FormLabel>Confirm New Password</FormLabel>
|
|
193
|
+
<FormControl>
|
|
194
|
+
<PasswordInput
|
|
195
|
+
placeholder="Confirm new password"
|
|
196
|
+
disabled={isLoading}
|
|
197
|
+
{...field}
|
|
198
|
+
/>
|
|
199
|
+
</FormControl>
|
|
200
|
+
<FormMessage />
|
|
201
|
+
</FormItem>
|
|
202
|
+
)}
|
|
203
|
+
/>
|
|
204
|
+
|
|
205
|
+
{hasErrors && (
|
|
206
|
+
<div className="border-destructive/50 bg-destructive/5 rounded-lg border p-4">
|
|
207
|
+
<div className="flex items-start gap-3">
|
|
208
|
+
<ShieldAlert className="text-destructive mt-0.5 h-5 w-5 shrink-0" />
|
|
209
|
+
<div>
|
|
210
|
+
<p className="text-destructive text-sm font-medium">
|
|
211
|
+
Password requirements:
|
|
212
|
+
</p>
|
|
213
|
+
<ul className="text-destructive/80 mt-1.5 space-y-1 text-sm">
|
|
214
|
+
<li>• At least 8 characters long</li>
|
|
215
|
+
<li>• Different from current password</li>
|
|
216
|
+
<li>• Passwords must match</li>
|
|
217
|
+
</ul>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
|
|
223
|
+
<FormField
|
|
224
|
+
control={form.control}
|
|
225
|
+
name="revokeOtherSessions"
|
|
226
|
+
render={({ field }) => (
|
|
227
|
+
<FormItem className="border-border bg-muted/30 flex flex-row items-start space-x-3 space-y-0 rounded-lg border p-4">
|
|
228
|
+
<FormControl>
|
|
229
|
+
<Checkbox
|
|
230
|
+
checked={field.value}
|
|
231
|
+
onCheckedChange={field.onChange}
|
|
232
|
+
disabled={isLoading}
|
|
233
|
+
/>
|
|
234
|
+
</FormControl>
|
|
235
|
+
<div className="space-y-1 leading-none">
|
|
236
|
+
<FormLabel>Sign out all other devices</FormLabel>
|
|
237
|
+
<FormDescription>
|
|
238
|
+
Recommended for security. This will end all active
|
|
239
|
+
sessions on other devices.
|
|
240
|
+
</FormDescription>
|
|
241
|
+
</div>
|
|
242
|
+
</FormItem>
|
|
243
|
+
)}
|
|
244
|
+
/>
|
|
245
|
+
|
|
246
|
+
<Button type="submit" className="w-full" isLoading={isLoading}>
|
|
247
|
+
Change Password
|
|
248
|
+
</Button>
|
|
249
|
+
</form>
|
|
250
|
+
</Form>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</ProtectedRoute>
|
|
254
|
+
);
|
|
255
|
+
}
|