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.
Files changed (259) hide show
  1. package/dist/index.js +452 -0
  2. package/package.json +57 -0
  3. package/template/.dockerignore +59 -0
  4. package/template/.github/workflows/ci.yml +157 -0
  5. package/template/.husky/pre-commit +1 -0
  6. package/template/.husky/pre-push +1 -0
  7. package/template/.lintstagedrc.cjs +4 -0
  8. package/template/.nvmrc +1 -0
  9. package/template/.prettierrc +9 -0
  10. package/template/.vscode/settings.json +13 -0
  11. package/template/CLAUDE.md +175 -0
  12. package/template/CONTRIBUTING.md +32 -0
  13. package/template/Dockerfile +90 -0
  14. package/template/GETTING_STARTED.md +35 -0
  15. package/template/LICENSE +21 -0
  16. package/template/README.md +116 -0
  17. package/template/apps/api/.dockerignore +51 -0
  18. package/template/apps/api/.env.local.example +62 -0
  19. package/template/apps/api/emails/account-deleted-email.tsx +69 -0
  20. package/template/apps/api/emails/components/email-layout.tsx +154 -0
  21. package/template/apps/api/emails/config.ts +22 -0
  22. package/template/apps/api/emails/password-changed-email.tsx +88 -0
  23. package/template/apps/api/emails/password-reset-email.tsx +86 -0
  24. package/template/apps/api/emails/verification-email.tsx +85 -0
  25. package/template/apps/api/emails/welcome-email.tsx +70 -0
  26. package/template/apps/api/package.json +84 -0
  27. package/template/apps/api/prisma/migrations/20251012111439_init/migration.sql +13 -0
  28. package/template/apps/api/prisma/migrations/20251018162629_add_better_auth_fields/migration.sql +67 -0
  29. package/template/apps/api/prisma/migrations/20251019142208_add_user_role_enum/migration.sql +5 -0
  30. package/template/apps/api/prisma/migrations/20251019182151_user_auth/migration.sql +7 -0
  31. package/template/apps/api/prisma/migrations/20251019211416_faster_session_lookup/migration.sql +2 -0
  32. package/template/apps/api/prisma/migrations/20251119124337_add_upload_model/migration.sql +26 -0
  33. package/template/apps/api/prisma/migrations/20251120071241_add_scope_to_account/migration.sql +2 -0
  34. package/template/apps/api/prisma/migrations/20251120072608_add_oauth_token_expiration_fields/migration.sql +10 -0
  35. package/template/apps/api/prisma/migrations/20251120144705_add_audit_logs/migration.sql +29 -0
  36. package/template/apps/api/prisma/migrations/20251127123614_remove_impersonated_by/migration.sql +8 -0
  37. package/template/apps/api/prisma/migrations/20251127125630_remove_audit_logs/migration.sql +11 -0
  38. package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
  39. package/template/apps/api/prisma/schema.prisma +116 -0
  40. package/template/apps/api/prisma/seed.ts +159 -0
  41. package/template/apps/api/prisma.config.ts +14 -0
  42. package/template/apps/api/src/app.ts +377 -0
  43. package/template/apps/api/src/common/logger.service.ts +227 -0
  44. package/template/apps/api/src/config/env.ts +60 -0
  45. package/template/apps/api/src/config/rate-limit.ts +29 -0
  46. package/template/apps/api/src/hooks/auth.ts +122 -0
  47. package/template/apps/api/src/plugins/auth.ts +198 -0
  48. package/template/apps/api/src/plugins/database.ts +45 -0
  49. package/template/apps/api/src/plugins/logger.ts +33 -0
  50. package/template/apps/api/src/plugins/multipart.ts +16 -0
  51. package/template/apps/api/src/plugins/scalar.ts +20 -0
  52. package/template/apps/api/src/plugins/schedule.ts +52 -0
  53. package/template/apps/api/src/plugins/services.ts +66 -0
  54. package/template/apps/api/src/plugins/swagger.ts +56 -0
  55. package/template/apps/api/src/routes/accounts.ts +91 -0
  56. package/template/apps/api/src/routes/admin-sessions.ts +92 -0
  57. package/template/apps/api/src/routes/metrics.ts +71 -0
  58. package/template/apps/api/src/routes/password.ts +46 -0
  59. package/template/apps/api/src/routes/sessions.ts +53 -0
  60. package/template/apps/api/src/routes/stats.ts +38 -0
  61. package/template/apps/api/src/routes/uploads-serve.ts +27 -0
  62. package/template/apps/api/src/routes/uploads.ts +154 -0
  63. package/template/apps/api/src/routes/users.ts +114 -0
  64. package/template/apps/api/src/routes/verification.ts +90 -0
  65. package/template/apps/api/src/server.ts +34 -0
  66. package/template/apps/api/src/services/accounts.service.ts +125 -0
  67. package/template/apps/api/src/services/authorization.service.ts +162 -0
  68. package/template/apps/api/src/services/email.service.ts +170 -0
  69. package/template/apps/api/src/services/file-storage.service.ts +267 -0
  70. package/template/apps/api/src/services/metrics.service.ts +175 -0
  71. package/template/apps/api/src/services/password.service.ts +56 -0
  72. package/template/apps/api/src/services/sessions.service.spec.ts +134 -0
  73. package/template/apps/api/src/services/sessions.service.ts +276 -0
  74. package/template/apps/api/src/services/stats.service.ts +273 -0
  75. package/template/apps/api/src/services/uploads.service.ts +163 -0
  76. package/template/apps/api/src/services/users.service.spec.ts +249 -0
  77. package/template/apps/api/src/services/users.service.ts +198 -0
  78. package/template/apps/api/src/utils/file-validation.ts +108 -0
  79. package/template/apps/api/start.sh +33 -0
  80. package/template/apps/api/test/helpers/fastify-app.ts +24 -0
  81. package/template/apps/api/test/helpers/mock-authorization.ts +16 -0
  82. package/template/apps/api/test/helpers/mock-logger.ts +28 -0
  83. package/template/apps/api/test/helpers/mock-prisma.ts +30 -0
  84. package/template/apps/api/test/helpers/test-db.ts +125 -0
  85. package/template/apps/api/test/integration/auth-flow.integration.spec.ts +449 -0
  86. package/template/apps/api/test/integration/password.integration.spec.ts +427 -0
  87. package/template/apps/api/test/integration/rate-limit.integration.spec.ts +51 -0
  88. package/template/apps/api/test/integration/sessions.integration.spec.ts +445 -0
  89. package/template/apps/api/test/integration/users.integration.spec.ts +211 -0
  90. package/template/apps/api/test/setup.ts +31 -0
  91. package/template/apps/api/tsconfig.json +26 -0
  92. package/template/apps/api/vitest.config.ts +35 -0
  93. package/template/apps/web/.env.local.example +11 -0
  94. package/template/apps/web/components.json +24 -0
  95. package/template/apps/web/next.config.ts +22 -0
  96. package/template/apps/web/package.json +56 -0
  97. package/template/apps/web/postcss.config.js +5 -0
  98. package/template/apps/web/public/apple-icon.png +0 -0
  99. package/template/apps/web/public/icon.png +0 -0
  100. package/template/apps/web/public/robots.txt +3 -0
  101. package/template/apps/web/src/app/(admin)/admin/layout.tsx +222 -0
  102. package/template/apps/web/src/app/(admin)/admin/page.tsx +157 -0
  103. package/template/apps/web/src/app/(admin)/admin/sessions/page.tsx +18 -0
  104. package/template/apps/web/src/app/(admin)/admin/users/page.tsx +20 -0
  105. package/template/apps/web/src/app/(auth)/forgot-password/page.tsx +177 -0
  106. package/template/apps/web/src/app/(auth)/login/page.tsx +159 -0
  107. package/template/apps/web/src/app/(auth)/reset-password/page.tsx +245 -0
  108. package/template/apps/web/src/app/(auth)/signup/page.tsx +153 -0
  109. package/template/apps/web/src/app/dashboard/change-password/page.tsx +255 -0
  110. package/template/apps/web/src/app/dashboard/page.tsx +296 -0
  111. package/template/apps/web/src/app/error.tsx +32 -0
  112. package/template/apps/web/src/app/examples/file-upload/page.tsx +200 -0
  113. package/template/apps/web/src/app/favicon.ico +0 -0
  114. package/template/apps/web/src/app/global-error.tsx +96 -0
  115. package/template/apps/web/src/app/globals.css +22 -0
  116. package/template/apps/web/src/app/icon.png +0 -0
  117. package/template/apps/web/src/app/layout.tsx +34 -0
  118. package/template/apps/web/src/app/not-found.tsx +28 -0
  119. package/template/apps/web/src/app/page.tsx +192 -0
  120. package/template/apps/web/src/components/admin/activity-feed.tsx +101 -0
  121. package/template/apps/web/src/components/admin/charts/auth-breakdown-chart.tsx +114 -0
  122. package/template/apps/web/src/components/admin/charts/chart-tooltip.tsx +124 -0
  123. package/template/apps/web/src/components/admin/charts/realtime-metrics-chart.tsx +511 -0
  124. package/template/apps/web/src/components/admin/charts/role-distribution-chart.tsx +102 -0
  125. package/template/apps/web/src/components/admin/charts/session-activity-chart.tsx +90 -0
  126. package/template/apps/web/src/components/admin/charts/user-growth-chart.tsx +108 -0
  127. package/template/apps/web/src/components/admin/health-indicator.tsx +175 -0
  128. package/template/apps/web/src/components/admin/refresh-control.tsx +90 -0
  129. package/template/apps/web/src/components/admin/session-revoke-all-dialog.tsx +79 -0
  130. package/template/apps/web/src/components/admin/session-revoke-dialog.tsx +74 -0
  131. package/template/apps/web/src/components/admin/sessions-management-table.tsx +372 -0
  132. package/template/apps/web/src/components/admin/stat-card.tsx +137 -0
  133. package/template/apps/web/src/components/admin/user-create-dialog.tsx +152 -0
  134. package/template/apps/web/src/components/admin/user-delete-dialog.tsx +73 -0
  135. package/template/apps/web/src/components/admin/user-edit-dialog.tsx +170 -0
  136. package/template/apps/web/src/components/admin/users-management-table.tsx +285 -0
  137. package/template/apps/web/src/components/auth/email-verification-banner.tsx +85 -0
  138. package/template/apps/web/src/components/auth/github-button.tsx +40 -0
  139. package/template/apps/web/src/components/auth/google-button.tsx +54 -0
  140. package/template/apps/web/src/components/auth/protected-route.tsx +66 -0
  141. package/template/apps/web/src/components/auth/redirect-if-authenticated.tsx +31 -0
  142. package/template/apps/web/src/components/auth/with-auth.tsx +30 -0
  143. package/template/apps/web/src/components/error/error-card.tsx +47 -0
  144. package/template/apps/web/src/components/error/forbidden.tsx +25 -0
  145. package/template/apps/web/src/components/landing/command-block.tsx +64 -0
  146. package/template/apps/web/src/components/landing/feature-card.tsx +60 -0
  147. package/template/apps/web/src/components/landing/included-feature-card.tsx +63 -0
  148. package/template/apps/web/src/components/landing/logo.tsx +41 -0
  149. package/template/apps/web/src/components/landing/tech-badge.tsx +11 -0
  150. package/template/apps/web/src/components/layout/auth-nav.tsx +58 -0
  151. package/template/apps/web/src/components/layout/footer.tsx +3 -0
  152. package/template/apps/web/src/config/landing-data.ts +152 -0
  153. package/template/apps/web/src/config/site.ts +5 -0
  154. package/template/apps/web/src/hooks/api/__tests__/use-users.test.tsx +181 -0
  155. package/template/apps/web/src/hooks/api/use-admin-sessions.ts +75 -0
  156. package/template/apps/web/src/hooks/api/use-admin-stats.ts +33 -0
  157. package/template/apps/web/src/hooks/api/use-sessions.ts +52 -0
  158. package/template/apps/web/src/hooks/api/use-uploads.ts +156 -0
  159. package/template/apps/web/src/hooks/api/use-users.ts +149 -0
  160. package/template/apps/web/src/hooks/use-mobile.ts +21 -0
  161. package/template/apps/web/src/hooks/use-realtime-metrics.ts +120 -0
  162. package/template/apps/web/src/lib/__tests__/utils.test.ts +29 -0
  163. package/template/apps/web/src/lib/api.ts +151 -0
  164. package/template/apps/web/src/lib/auth.ts +13 -0
  165. package/template/apps/web/src/lib/env.ts +52 -0
  166. package/template/apps/web/src/lib/form-utils.ts +11 -0
  167. package/template/apps/web/src/lib/utils.ts +1 -0
  168. package/template/apps/web/src/providers.tsx +34 -0
  169. package/template/apps/web/src/store/atoms.ts +15 -0
  170. package/template/apps/web/src/test/helpers/test-utils.tsx +44 -0
  171. package/template/apps/web/src/test/setup.ts +8 -0
  172. package/template/apps/web/tailwind.config.ts +5 -0
  173. package/template/apps/web/tsconfig.json +26 -0
  174. package/template/apps/web/vitest.config.ts +32 -0
  175. package/template/assets/logo-512.png +0 -0
  176. package/template/assets/logo.svg +4 -0
  177. package/template/docker-compose.prod.yml +66 -0
  178. package/template/docker-compose.yml +36 -0
  179. package/template/eslint.config.ts +119 -0
  180. package/template/package.json +77 -0
  181. package/template/packages/tailwind-config/package.json +9 -0
  182. package/template/packages/tailwind-config/theme.css +179 -0
  183. package/template/packages/types/package.json +29 -0
  184. package/template/packages/types/src/__tests__/schemas.test.ts +255 -0
  185. package/template/packages/types/src/api-response.ts +53 -0
  186. package/template/packages/types/src/health-check.ts +11 -0
  187. package/template/packages/types/src/pagination.ts +41 -0
  188. package/template/packages/types/src/role.ts +5 -0
  189. package/template/packages/types/src/session.ts +48 -0
  190. package/template/packages/types/src/stats.ts +113 -0
  191. package/template/packages/types/src/upload.ts +51 -0
  192. package/template/packages/types/src/user.ts +36 -0
  193. package/template/packages/types/tsconfig.json +5 -0
  194. package/template/packages/types/vitest.config.ts +21 -0
  195. package/template/packages/ui/components.json +21 -0
  196. package/template/packages/ui/package.json +108 -0
  197. package/template/packages/ui/src/__tests__/button.test.tsx +70 -0
  198. package/template/packages/ui/src/alert-dialog.tsx +141 -0
  199. package/template/packages/ui/src/alert.tsx +66 -0
  200. package/template/packages/ui/src/animated-theme-toggler.tsx +167 -0
  201. package/template/packages/ui/src/avatar.tsx +53 -0
  202. package/template/packages/ui/src/badge.tsx +36 -0
  203. package/template/packages/ui/src/button.tsx +84 -0
  204. package/template/packages/ui/src/card.tsx +92 -0
  205. package/template/packages/ui/src/checkbox.tsx +32 -0
  206. package/template/packages/ui/src/data-table/data-table-column-header.tsx +68 -0
  207. package/template/packages/ui/src/data-table/data-table-pagination.tsx +99 -0
  208. package/template/packages/ui/src/data-table/data-table-toolbar.tsx +55 -0
  209. package/template/packages/ui/src/data-table/data-table-view-options.tsx +63 -0
  210. package/template/packages/ui/src/data-table/data-table.tsx +167 -0
  211. package/template/packages/ui/src/dialog.tsx +143 -0
  212. package/template/packages/ui/src/dropdown-menu.tsx +257 -0
  213. package/template/packages/ui/src/empty-state.tsx +52 -0
  214. package/template/packages/ui/src/file-upload-input.tsx +202 -0
  215. package/template/packages/ui/src/form.tsx +168 -0
  216. package/template/packages/ui/src/hooks/use-mobile.ts +19 -0
  217. package/template/packages/ui/src/icons/brand-icons.tsx +16 -0
  218. package/template/packages/ui/src/input.tsx +21 -0
  219. package/template/packages/ui/src/label.tsx +24 -0
  220. package/template/packages/ui/src/lib/utils.ts +6 -0
  221. package/template/packages/ui/src/password-input.tsx +102 -0
  222. package/template/packages/ui/src/popover.tsx +48 -0
  223. package/template/packages/ui/src/radio-group.tsx +45 -0
  224. package/template/packages/ui/src/scroll-area.tsx +58 -0
  225. package/template/packages/ui/src/select.tsx +187 -0
  226. package/template/packages/ui/src/separator.tsx +28 -0
  227. package/template/packages/ui/src/sheet.tsx +139 -0
  228. package/template/packages/ui/src/sidebar.tsx +726 -0
  229. package/template/packages/ui/src/skeleton-variants.tsx +87 -0
  230. package/template/packages/ui/src/skeleton.tsx +13 -0
  231. package/template/packages/ui/src/slider.tsx +63 -0
  232. package/template/packages/ui/src/sonner.tsx +25 -0
  233. package/template/packages/ui/src/spinner.tsx +16 -0
  234. package/template/packages/ui/src/switch.tsx +31 -0
  235. package/template/packages/ui/src/table.tsx +116 -0
  236. package/template/packages/ui/src/tabs.tsx +66 -0
  237. package/template/packages/ui/src/textarea.tsx +18 -0
  238. package/template/packages/ui/src/tooltip.tsx +61 -0
  239. package/template/packages/ui/src/user-avatar.tsx +97 -0
  240. package/template/packages/ui/test-config.js +3 -0
  241. package/template/packages/ui/tsconfig.json +12 -0
  242. package/template/packages/ui/turbo.json +18 -0
  243. package/template/packages/ui/vitest.config.ts +17 -0
  244. package/template/packages/ui/vitest.setup.ts +1 -0
  245. package/template/packages/utils/package.json +23 -0
  246. package/template/packages/utils/src/__tests__/utils.test.ts +223 -0
  247. package/template/packages/utils/src/array.ts +18 -0
  248. package/template/packages/utils/src/async.ts +3 -0
  249. package/template/packages/utils/src/date.ts +77 -0
  250. package/template/packages/utils/src/errors.ts +73 -0
  251. package/template/packages/utils/src/number.ts +11 -0
  252. package/template/packages/utils/src/string.ts +13 -0
  253. package/template/packages/utils/tsconfig.json +5 -0
  254. package/template/packages/utils/vitest.config.ts +21 -0
  255. package/template/pnpm-workspace.yaml +4 -0
  256. package/template/tsconfig.base.json +32 -0
  257. package/template/turbo.json +133 -0
  258. package/template/vitest.shared.ts +26 -0
  259. 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&apos;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
+ }