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,177 @@
|
|
|
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 { Info, MailIcon, TriangleAlert } from 'lucide-react';
|
|
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 { RedirectIfAuthenticated } from '@/components/auth/redirect-if-authenticated';
|
|
16
|
+
import { authClient } from '@/lib/auth';
|
|
17
|
+
|
|
18
|
+
const forgotPasswordSchema = z.object({
|
|
19
|
+
email: z.string().email('Invalid email address'),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
type ForgotPasswordFormData = z.infer<typeof forgotPasswordSchema>;
|
|
23
|
+
|
|
24
|
+
export default function ForgotPasswordPage() {
|
|
25
|
+
const router = useRouter();
|
|
26
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
27
|
+
const [emailSent, setEmailSent] = useState(false);
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
register,
|
|
31
|
+
handleSubmit,
|
|
32
|
+
formState: { errors },
|
|
33
|
+
getValues,
|
|
34
|
+
} = useForm<ForgotPasswordFormData>({
|
|
35
|
+
resolver: zodResolver(forgotPasswordSchema),
|
|
36
|
+
defaultValues: {
|
|
37
|
+
email: '',
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const onSubmit = async (data: ForgotPasswordFormData) => {
|
|
42
|
+
setIsLoading(true);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const result = await authClient.forgetPassword({
|
|
46
|
+
email: data.email,
|
|
47
|
+
redirectTo: '/reset-password',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (result.error) {
|
|
51
|
+
toast.error(result.error.message || 'Failed to send reset email');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
setEmailSent(true);
|
|
56
|
+
toast.success('Password reset email sent! Check your inbox.');
|
|
57
|
+
} catch (error) {
|
|
58
|
+
toast.error('An unexpected error occurred');
|
|
59
|
+
console.error('Forgot password error:', error);
|
|
60
|
+
} finally {
|
|
61
|
+
setIsLoading(false);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (emailSent) {
|
|
66
|
+
return (
|
|
67
|
+
<RedirectIfAuthenticated>
|
|
68
|
+
<div className="flex min-h-screen items-center justify-center p-4">
|
|
69
|
+
<div className="bg-card w-full max-w-md rounded-lg border p-8">
|
|
70
|
+
<div className="mb-6 text-center">
|
|
71
|
+
<div className="bg-primary text-primary-foreground mx-auto mb-4 flex size-16 items-center justify-center rounded-full">
|
|
72
|
+
<MailIcon className="size-8" />
|
|
73
|
+
</div>
|
|
74
|
+
<h1 className="text-2xl font-semibold">Check your email</h1>
|
|
75
|
+
<p className="text-muted-foreground mt-2 text-sm">
|
|
76
|
+
We sent a password reset link to
|
|
77
|
+
</p>
|
|
78
|
+
<p className="text-foreground mt-1 text-sm font-medium">
|
|
79
|
+
{getValues('email')}
|
|
80
|
+
</p>
|
|
81
|
+
</div>
|
|
82
|
+
<div className="mb-6 space-y-4 rounded-lg border p-4">
|
|
83
|
+
<div className="flex gap-3">
|
|
84
|
+
<div className="mt-0.5 flex-shrink-0">
|
|
85
|
+
<Info className="size-4" />
|
|
86
|
+
</div>
|
|
87
|
+
<div className="space-y-1">
|
|
88
|
+
<p className="text-foreground text-sm font-medium">
|
|
89
|
+
Next steps
|
|
90
|
+
</p>
|
|
91
|
+
<p className="text-muted-foreground text-sm leading-relaxed">
|
|
92
|
+
Click the link in the email to reset your password. The link
|
|
93
|
+
will expire in 1 hour.
|
|
94
|
+
</p>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<div className="flex gap-3">
|
|
98
|
+
<div className="mt-0.5 flex-shrink-0">
|
|
99
|
+
<TriangleAlert className="size-4" />
|
|
100
|
+
</div>
|
|
101
|
+
<div className="space-y-1">
|
|
102
|
+
<p className="text-foreground text-sm font-medium">
|
|
103
|
+
Didn't receive it?
|
|
104
|
+
</p>
|
|
105
|
+
<p className="text-muted-foreground text-sm leading-relaxed">
|
|
106
|
+
Check your spam folder or use the button below to send
|
|
107
|
+
another email.
|
|
108
|
+
</p>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
<div className="flex flex-col gap-3">
|
|
113
|
+
<Button
|
|
114
|
+
variant="outline"
|
|
115
|
+
className="w-full"
|
|
116
|
+
onClick={() => setEmailSent(false)}
|
|
117
|
+
>
|
|
118
|
+
Send another email
|
|
119
|
+
</Button>
|
|
120
|
+
<Button
|
|
121
|
+
variant="ghost"
|
|
122
|
+
className="w-full"
|
|
123
|
+
onClick={() => router.push('/login')}
|
|
124
|
+
>
|
|
125
|
+
Back to sign in
|
|
126
|
+
</Button>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</RedirectIfAuthenticated>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<RedirectIfAuthenticated>
|
|
136
|
+
<div className="flex min-h-screen items-center justify-center p-4">
|
|
137
|
+
<div className="bg-card w-full max-w-md rounded-lg border p-8">
|
|
138
|
+
<div className="mb-6">
|
|
139
|
+
<h1 className="text-2xl font-semibold">Reset your password</h1>
|
|
140
|
+
<p className="text-muted-foreground mt-2 text-sm">
|
|
141
|
+
Enter your email address and we'll send you a link to reset your
|
|
142
|
+
password
|
|
143
|
+
</p>
|
|
144
|
+
</div>
|
|
145
|
+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
146
|
+
<div className="space-y-2">
|
|
147
|
+
<Label htmlFor="email">Email</Label>
|
|
148
|
+
<Input
|
|
149
|
+
id="email"
|
|
150
|
+
type="email"
|
|
151
|
+
placeholder="name@example.com"
|
|
152
|
+
{...register('email')}
|
|
153
|
+
disabled={isLoading}
|
|
154
|
+
/>
|
|
155
|
+
{errors.email && (
|
|
156
|
+
<p className="text-destructive text-xs">
|
|
157
|
+
{errors.email.message}
|
|
158
|
+
</p>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
<div className="flex flex-col gap-4 pt-2">
|
|
162
|
+
<Button type="submit" className="w-full" isLoading={isLoading}>
|
|
163
|
+
Send reset link
|
|
164
|
+
</Button>
|
|
165
|
+
<p className="text-muted-foreground text-center text-sm">
|
|
166
|
+
Remember your password?{' '}
|
|
167
|
+
<Link href="/login" className="text-primary hover:underline">
|
|
168
|
+
Sign in
|
|
169
|
+
</Link>
|
|
170
|
+
</p>
|
|
171
|
+
</div>
|
|
172
|
+
</form>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</RedirectIfAuthenticated>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
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 { Input } from '@repo/packages-ui/input';
|
|
7
|
+
import { Label } from '@repo/packages-ui/label';
|
|
8
|
+
import { PasswordInput } from '@repo/packages-ui/password-input';
|
|
9
|
+
import Link from 'next/link';
|
|
10
|
+
import { useRouter } from 'next/navigation';
|
|
11
|
+
import React, { useState } from 'react';
|
|
12
|
+
import { useForm } from 'react-hook-form';
|
|
13
|
+
import { toast } from 'sonner';
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
|
|
16
|
+
import { GithubButton } from '@/components/auth/github-button';
|
|
17
|
+
import { GoogleButton } from '@/components/auth/google-button';
|
|
18
|
+
import { RedirectIfAuthenticated } from '@/components/auth/redirect-if-authenticated';
|
|
19
|
+
import { authClient } from '@/lib/auth';
|
|
20
|
+
|
|
21
|
+
const loginSchema = z.object({
|
|
22
|
+
email: z.string().email('Invalid email address'),
|
|
23
|
+
password: z.string().min(8, 'Password must be at least 8 characters'),
|
|
24
|
+
rememberMe: z.boolean().default(false),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export default function LoginPage() {
|
|
28
|
+
const router = useRouter();
|
|
29
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
register,
|
|
33
|
+
handleSubmit,
|
|
34
|
+
formState: { errors },
|
|
35
|
+
} = useForm({
|
|
36
|
+
resolver: zodResolver(loginSchema),
|
|
37
|
+
defaultValues: {
|
|
38
|
+
email: '',
|
|
39
|
+
password: '',
|
|
40
|
+
rememberMe: false,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const onSubmit = async (data: z.infer<typeof loginSchema>) => {
|
|
45
|
+
setIsLoading(true);
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const result = await authClient.signIn.email({
|
|
49
|
+
email: data.email,
|
|
50
|
+
password: data.password,
|
|
51
|
+
rememberMe: data.rememberMe,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (result.error) {
|
|
55
|
+
toast.error(result.error.message || 'Failed to sign in');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
toast.success('Welcome back!');
|
|
60
|
+
router.push('/dashboard');
|
|
61
|
+
} catch (error) {
|
|
62
|
+
toast.error('An unexpected error occurred');
|
|
63
|
+
console.error('Login error:', error);
|
|
64
|
+
} finally {
|
|
65
|
+
setIsLoading(false);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<RedirectIfAuthenticated>
|
|
71
|
+
<div className="flex min-h-screen items-center justify-center p-4">
|
|
72
|
+
<div className="bg-card w-full max-w-md rounded-lg border p-8">
|
|
73
|
+
<div className="mb-6">
|
|
74
|
+
<h1 className="text-2xl font-semibold">Sign In</h1>
|
|
75
|
+
<p className="text-muted-foreground mt-2 text-sm">
|
|
76
|
+
Enter your email and password to access your account
|
|
77
|
+
</p>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div className="mb-6 space-y-3">
|
|
81
|
+
<GoogleButton />
|
|
82
|
+
<GithubButton />
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div className="relative mb-6">
|
|
86
|
+
<div className="absolute inset-0 flex items-center">
|
|
87
|
+
<span className="w-full border-t" />
|
|
88
|
+
</div>
|
|
89
|
+
<div className="relative flex justify-center text-xs uppercase">
|
|
90
|
+
<span className="bg-card text-muted-foreground px-2">
|
|
91
|
+
Or continue with email
|
|
92
|
+
</span>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
97
|
+
<div className="space-y-2">
|
|
98
|
+
<Label htmlFor="email">Email</Label>
|
|
99
|
+
<Input
|
|
100
|
+
id="email"
|
|
101
|
+
type="email"
|
|
102
|
+
placeholder="name@example.com"
|
|
103
|
+
{...register('email')}
|
|
104
|
+
disabled={isLoading}
|
|
105
|
+
/>
|
|
106
|
+
{errors.email && (
|
|
107
|
+
<p className="text-destructive text-xs">
|
|
108
|
+
{errors.email.message}
|
|
109
|
+
</p>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
<div className="space-y-2">
|
|
113
|
+
<Label htmlFor="password">Password</Label>
|
|
114
|
+
<PasswordInput
|
|
115
|
+
id="password"
|
|
116
|
+
placeholder="Enter your password"
|
|
117
|
+
{...register('password')}
|
|
118
|
+
disabled={isLoading}
|
|
119
|
+
/>
|
|
120
|
+
{errors.password && (
|
|
121
|
+
<p className="text-destructive text-xs">
|
|
122
|
+
{errors.password.message}
|
|
123
|
+
</p>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
<div className="flex items-center justify-between pt-2">
|
|
127
|
+
<div className="flex items-center space-x-2">
|
|
128
|
+
<Checkbox id="rememberMe" {...register('rememberMe')} />
|
|
129
|
+
<Label
|
|
130
|
+
htmlFor="rememberMe"
|
|
131
|
+
className="text-sm font-normal leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
132
|
+
>
|
|
133
|
+
Remember me for 30 days
|
|
134
|
+
</Label>
|
|
135
|
+
</div>
|
|
136
|
+
<Link
|
|
137
|
+
href="/forgot-password"
|
|
138
|
+
className="text-primary text-sm hover:underline"
|
|
139
|
+
>
|
|
140
|
+
Forgot password?
|
|
141
|
+
</Link>
|
|
142
|
+
</div>
|
|
143
|
+
<div className="flex flex-col gap-4 pt-2">
|
|
144
|
+
<Button type="submit" className="w-full" isLoading={isLoading}>
|
|
145
|
+
Sign In
|
|
146
|
+
</Button>
|
|
147
|
+
<p className="text-muted-foreground text-center text-sm">
|
|
148
|
+
Don't have an account?{' '}
|
|
149
|
+
<Link href="/signup" className="text-primary hover:underline">
|
|
150
|
+
Sign up
|
|
151
|
+
</Link>
|
|
152
|
+
</p>
|
|
153
|
+
</div>
|
|
154
|
+
</form>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</RedirectIfAuthenticated>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
4
|
+
import { Button } from '@repo/packages-ui/button';
|
|
5
|
+
import { Label } from '@repo/packages-ui/label';
|
|
6
|
+
import { PasswordInput } from '@repo/packages-ui/password-input';
|
|
7
|
+
import { ShieldAlert } from 'lucide-react';
|
|
8
|
+
import Link from 'next/link';
|
|
9
|
+
import { useRouter, useSearchParams } from 'next/navigation';
|
|
10
|
+
import React, { Suspense, useState } from 'react';
|
|
11
|
+
import { useForm } from 'react-hook-form';
|
|
12
|
+
import { toast } from 'sonner';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
|
|
15
|
+
import { authClient } from '@/lib/auth';
|
|
16
|
+
|
|
17
|
+
const resetPasswordSchema = z.object({
|
|
18
|
+
password: z.string().min(8, 'Password must be at least 8 characters'),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
type ResetPasswordFormData = z.infer<typeof resetPasswordSchema>;
|
|
22
|
+
|
|
23
|
+
function ResetPasswordContent() {
|
|
24
|
+
const router = useRouter();
|
|
25
|
+
const searchParams = useSearchParams();
|
|
26
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
27
|
+
const [resetSuccess, setResetSuccess] = useState(false);
|
|
28
|
+
|
|
29
|
+
const token = searchParams.get('token');
|
|
30
|
+
const error = searchParams.get('error');
|
|
31
|
+
|
|
32
|
+
const {
|
|
33
|
+
register,
|
|
34
|
+
handleSubmit,
|
|
35
|
+
formState: { errors },
|
|
36
|
+
} = useForm<ResetPasswordFormData>({
|
|
37
|
+
resolver: zodResolver(resetPasswordSchema),
|
|
38
|
+
defaultValues: {
|
|
39
|
+
password: '',
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const hasErrors = Object.keys(errors).length > 0;
|
|
44
|
+
|
|
45
|
+
if (!token || error) {
|
|
46
|
+
return (
|
|
47
|
+
<div className="flex min-h-screen items-center justify-center p-4">
|
|
48
|
+
<div className="bg-card w-full max-w-md rounded-lg border p-8">
|
|
49
|
+
<div className="mb-6 text-center">
|
|
50
|
+
<div className="bg-destructive/10 text-destructive mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full">
|
|
51
|
+
<svg
|
|
52
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
53
|
+
fill="none"
|
|
54
|
+
viewBox="0 0 24 24"
|
|
55
|
+
strokeWidth={1.5}
|
|
56
|
+
stroke="currentColor"
|
|
57
|
+
className="h-7 w-7"
|
|
58
|
+
>
|
|
59
|
+
<path
|
|
60
|
+
strokeLinecap="round"
|
|
61
|
+
strokeLinejoin="round"
|
|
62
|
+
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"
|
|
63
|
+
/>
|
|
64
|
+
</svg>
|
|
65
|
+
</div>
|
|
66
|
+
<h1 className="text-2xl font-semibold">Invalid reset link</h1>
|
|
67
|
+
<p className="text-muted-foreground mt-2 text-sm">
|
|
68
|
+
This password reset link is invalid or has expired.
|
|
69
|
+
</p>
|
|
70
|
+
</div>
|
|
71
|
+
<div className="bg-muted/50 mb-6 space-y-2 rounded-lg p-4">
|
|
72
|
+
<p className="text-muted-foreground text-sm">
|
|
73
|
+
Password reset links expire after 1 hour for security reasons.
|
|
74
|
+
</p>
|
|
75
|
+
<p className="text-muted-foreground text-sm">
|
|
76
|
+
Please request a new password reset link to continue.
|
|
77
|
+
</p>
|
|
78
|
+
</div>
|
|
79
|
+
<div className="flex flex-col gap-3">
|
|
80
|
+
<Button
|
|
81
|
+
className="w-full"
|
|
82
|
+
onClick={() => router.push('/forgot-password')}
|
|
83
|
+
>
|
|
84
|
+
Request new reset link
|
|
85
|
+
</Button>
|
|
86
|
+
<Button
|
|
87
|
+
variant="ghost"
|
|
88
|
+
className="w-full"
|
|
89
|
+
onClick={() => router.push('/login')}
|
|
90
|
+
>
|
|
91
|
+
Back to sign in
|
|
92
|
+
</Button>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const onSubmit = async (data: ResetPasswordFormData) => {
|
|
100
|
+
if (!token) {
|
|
101
|
+
toast.error('Reset token is missing');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setIsLoading(true);
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const result = await authClient.resetPassword({
|
|
109
|
+
newPassword: data.password,
|
|
110
|
+
token,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (result.error) {
|
|
114
|
+
toast.error(result.error.message || 'Failed to reset password');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
setResetSuccess(true);
|
|
119
|
+
toast.success('Password reset successful!');
|
|
120
|
+
} catch (error) {
|
|
121
|
+
toast.error('An unexpected error occurred');
|
|
122
|
+
console.error('Reset password error:', error);
|
|
123
|
+
} finally {
|
|
124
|
+
setIsLoading(false);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
if (resetSuccess) {
|
|
129
|
+
return (
|
|
130
|
+
<div className="flex min-h-screen items-center justify-center p-4">
|
|
131
|
+
<div className="bg-card w-full max-w-md rounded-lg border p-8">
|
|
132
|
+
<div className="mb-6 text-center">
|
|
133
|
+
<div className="bg-primary/10 text-primary mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full">
|
|
134
|
+
<svg
|
|
135
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
136
|
+
fill="none"
|
|
137
|
+
viewBox="0 0 24 24"
|
|
138
|
+
strokeWidth={1.5}
|
|
139
|
+
stroke="currentColor"
|
|
140
|
+
className="h-7 w-7"
|
|
141
|
+
>
|
|
142
|
+
<path
|
|
143
|
+
strokeLinecap="round"
|
|
144
|
+
strokeLinejoin="round"
|
|
145
|
+
d="m4.5 12.75 6 6 9-13.5"
|
|
146
|
+
/>
|
|
147
|
+
</svg>
|
|
148
|
+
</div>
|
|
149
|
+
<h1 className="text-2xl font-semibold">
|
|
150
|
+
Password reset successful
|
|
151
|
+
</h1>
|
|
152
|
+
<p className="text-muted-foreground mt-2 text-sm">
|
|
153
|
+
Your password has been successfully reset. You can now sign in
|
|
154
|
+
with your new password.
|
|
155
|
+
</p>
|
|
156
|
+
</div>
|
|
157
|
+
<Button className="w-full" onClick={() => router.push('/login')}>
|
|
158
|
+
Continue to sign in
|
|
159
|
+
</Button>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<div className="flex min-h-screen items-center justify-center p-4">
|
|
167
|
+
<div className="bg-card w-full max-w-md rounded-lg border p-8">
|
|
168
|
+
<div className="mb-6">
|
|
169
|
+
<h1 className="text-2xl font-semibold">Create new password</h1>
|
|
170
|
+
<p className="text-muted-foreground mt-2 text-sm">
|
|
171
|
+
Enter a new password for your account
|
|
172
|
+
</p>
|
|
173
|
+
</div>
|
|
174
|
+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
175
|
+
<div className="space-y-2">
|
|
176
|
+
<Label htmlFor="password">New password</Label>
|
|
177
|
+
<PasswordInput
|
|
178
|
+
id="password"
|
|
179
|
+
placeholder="Enter your new password"
|
|
180
|
+
{...register('password')}
|
|
181
|
+
disabled={isLoading}
|
|
182
|
+
/>
|
|
183
|
+
{errors.password && (
|
|
184
|
+
<p className="text-destructive text-xs">
|
|
185
|
+
{errors.password.message}
|
|
186
|
+
</p>
|
|
187
|
+
)}
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
{hasErrors && (
|
|
191
|
+
<div className="border-destructive/50 bg-destructive/5 rounded-lg border p-4">
|
|
192
|
+
<div className="flex items-start gap-3">
|
|
193
|
+
<ShieldAlert className="text-destructive mt-0.5 h-5 w-5 shrink-0" />
|
|
194
|
+
<div>
|
|
195
|
+
<p className="text-destructive text-sm font-medium">
|
|
196
|
+
Password requirements:
|
|
197
|
+
</p>
|
|
198
|
+
<ul className="text-destructive/80 mt-1.5 space-y-1 text-sm">
|
|
199
|
+
<li>• At least 8 characters long</li>
|
|
200
|
+
</ul>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
{!hasErrors && (
|
|
207
|
+
<div className="bg-muted/50 rounded-lg p-4">
|
|
208
|
+
<p className="text-muted-foreground text-sm font-medium">
|
|
209
|
+
Password requirements:
|
|
210
|
+
</p>
|
|
211
|
+
<ul className="text-muted-foreground mt-2 space-y-1 text-sm">
|
|
212
|
+
<li>• At least 8 characters long</li>
|
|
213
|
+
</ul>
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
<div className="flex flex-col gap-4 pt-2">
|
|
217
|
+
<Button type="submit" className="w-full" isLoading={isLoading}>
|
|
218
|
+
Reset password
|
|
219
|
+
</Button>
|
|
220
|
+
<p className="text-muted-foreground text-center text-sm">
|
|
221
|
+
Remember your password?{' '}
|
|
222
|
+
<Link href="/login" className="text-primary hover:underline">
|
|
223
|
+
Sign in
|
|
224
|
+
</Link>
|
|
225
|
+
</p>
|
|
226
|
+
</div>
|
|
227
|
+
</form>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export default function ResetPasswordPage() {
|
|
234
|
+
return (
|
|
235
|
+
<Suspense
|
|
236
|
+
fallback={
|
|
237
|
+
<div className="flex min-h-screen items-center justify-center">
|
|
238
|
+
<div className="text-muted-foreground">Loading...</div>
|
|
239
|
+
</div>
|
|
240
|
+
}
|
|
241
|
+
>
|
|
242
|
+
<ResetPasswordContent />
|
|
243
|
+
</Suspense>
|
|
244
|
+
);
|
|
245
|
+
}
|