ar-saas 0.3.1 → 0.3.2

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 (114) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +338 -314
  3. package/dist/cli.js +19 -0
  4. package/dist/generator.js +166 -55
  5. package/package.json +52 -50
  6. package/templates/backend/.env.example +67 -67
  7. package/templates/backend/.prettierrc +4 -4
  8. package/templates/backend/README.md +249 -168
  9. package/templates/backend/eslint.config.mjs +35 -35
  10. package/templates/backend/nest-cli.json +8 -8
  11. package/templates/backend/package-lock.json +10979 -10979
  12. package/templates/backend/package.json +88 -88
  13. package/templates/backend/src/app.controller.spec.ts +24 -24
  14. package/templates/backend/src/app.controller.ts +15 -15
  15. package/templates/backend/src/app.module.ts +40 -40
  16. package/templates/backend/src/app.service.ts +11 -11
  17. package/templates/backend/src/common/base/base.repository.ts +221 -221
  18. package/templates/backend/src/common/base/base.schema.ts +24 -24
  19. package/templates/backend/src/common/decorators/cookie.decorator.ts +9 -9
  20. package/templates/backend/src/common/decorators/current-user.decorator.ts +20 -20
  21. package/templates/backend/src/common/decorators/workspace-id.decorator.ts +14 -14
  22. package/templates/backend/src/common/filters/global-exception.filter.ts +61 -61
  23. package/templates/backend/src/common/guards/jwt-auth.guard.ts +5 -5
  24. package/templates/backend/src/common/interceptors/workspace-tenant.interceptor.ts +45 -45
  25. package/templates/backend/src/main.ts +51 -51
  26. package/templates/backend/src/modules/auth/auth.controller.ts +158 -158
  27. package/templates/backend/src/modules/auth/auth.module.ts +20 -20
  28. package/templates/backend/src/modules/auth/auth.service.ts +257 -257
  29. package/templates/backend/src/modules/auth/dto/forgot-password.dto.ts +9 -9
  30. package/templates/backend/src/modules/auth/dto/login.dto.ts +14 -14
  31. package/templates/backend/src/modules/auth/dto/refresh-token.dto.ts +12 -12
  32. package/templates/backend/src/modules/auth/dto/register.dto.ts +26 -26
  33. package/templates/backend/src/modules/auth/dto/reset-password.dto.ts +16 -16
  34. package/templates/backend/src/modules/auth/dto/verify-email.dto.ts +9 -9
  35. package/templates/backend/src/modules/auth/strategies/jwt.strategy.ts +43 -43
  36. package/templates/backend/src/modules/mail/mail.module.ts +9 -9
  37. package/templates/backend/src/modules/mail/mail.service.ts +141 -141
  38. package/templates/backend/src/modules/users/schemas/user.schema.ts +54 -54
  39. package/templates/backend/src/modules/users/users.module.ts +14 -14
  40. package/templates/backend/src/modules/users/users.repository.ts +51 -51
  41. package/templates/backend/src/modules/users/users.service.ts +104 -104
  42. package/templates/backend/src/modules/workspaces/schemas/workspace.schema.ts +26 -26
  43. package/templates/backend/src/modules/workspaces/workspaces.module.ts +16 -16
  44. package/templates/backend/src/modules/workspaces/workspaces.repository.ts +34 -34
  45. package/templates/backend/src/modules/workspaces/workspaces.service.ts +42 -42
  46. package/templates/backend/test/app.e2e-spec.ts +25 -25
  47. package/templates/backend/test/jest-e2e.json +9 -9
  48. package/templates/backend/tsconfig.build.json +4 -4
  49. package/templates/backend/tsconfig.json +26 -26
  50. package/templates/frontend/.env.local.example +1 -1
  51. package/templates/frontend/README.md +152 -0
  52. package/templates/frontend/components.json +20 -20
  53. package/templates/frontend/eslint.config.mjs +14 -14
  54. package/templates/frontend/next.config.ts +5 -5
  55. package/templates/frontend/package-lock.json +6722 -6722
  56. package/templates/frontend/package.json +48 -48
  57. package/templates/frontend/pnpm-lock.yaml +5012 -5012
  58. package/templates/frontend/pnpm-workspace.yaml +3 -3
  59. package/templates/frontend/postcss.config.mjs +7 -7
  60. package/templates/frontend/src/app/(auth)/forgot-password/page.tsx +84 -84
  61. package/templates/frontend/src/app/(auth)/layout.tsx +28 -28
  62. package/templates/frontend/src/app/(auth)/login/page.tsx +111 -111
  63. package/templates/frontend/src/app/(auth)/register/page.tsx +161 -161
  64. package/templates/frontend/src/app/(auth)/reset-password/page.tsx +120 -120
  65. package/templates/frontend/src/app/(auth)/verify-email/page.tsx +78 -78
  66. package/templates/frontend/src/app/(dashboard)/billing/page.tsx +111 -111
  67. package/templates/frontend/src/app/(dashboard)/dashboard/page.tsx +105 -105
  68. package/templates/frontend/src/app/(dashboard)/layout.tsx +38 -38
  69. package/templates/frontend/src/app/(dashboard)/profile/page.tsx +226 -226
  70. package/templates/frontend/src/app/(dashboard)/settings/page.tsx +156 -156
  71. package/templates/frontend/src/app/(dashboard)/team/page.tsx +178 -178
  72. package/templates/frontend/src/app/(legal)/privacy/page.tsx +127 -127
  73. package/templates/frontend/src/app/(legal)/terms/page.tsx +118 -118
  74. package/templates/frontend/src/app/globals.css +81 -81
  75. package/templates/frontend/src/app/layout.tsx +26 -26
  76. package/templates/frontend/src/app/page.tsx +5 -45
  77. package/templates/frontend/src/app/setup/page.tsx +371 -275
  78. package/templates/frontend/src/components/dashboard/header.tsx +89 -89
  79. package/templates/frontend/src/components/dashboard/sidebar.tsx +71 -71
  80. package/templates/frontend/src/components/dashboard/stat-card.tsx +34 -34
  81. package/templates/frontend/src/components/landing/faq.tsx +39 -39
  82. package/templates/frontend/src/components/landing/features.tsx +54 -54
  83. package/templates/frontend/src/components/landing/footer.tsx +76 -76
  84. package/templates/frontend/src/components/landing/hero.tsx +72 -72
  85. package/templates/frontend/src/components/landing/navbar.tsx +78 -78
  86. package/templates/frontend/src/components/landing/pricing.tsx +90 -90
  87. package/templates/frontend/src/components/ui/accordion.tsx +52 -52
  88. package/templates/frontend/src/components/ui/avatar.tsx +46 -46
  89. package/templates/frontend/src/components/ui/badge.tsx +30 -30
  90. package/templates/frontend/src/components/ui/button.tsx +52 -52
  91. package/templates/frontend/src/components/ui/card.tsx +50 -50
  92. package/templates/frontend/src/components/ui/checkbox.tsx +27 -27
  93. package/templates/frontend/src/components/ui/dialog.tsx +100 -100
  94. package/templates/frontend/src/components/ui/dropdown-menu.tsx +173 -173
  95. package/templates/frontend/src/components/ui/form.tsx +158 -158
  96. package/templates/frontend/src/components/ui/input.tsx +21 -21
  97. package/templates/frontend/src/components/ui/label.tsx +22 -22
  98. package/templates/frontend/src/components/ui/separator.tsx +25 -25
  99. package/templates/frontend/src/components/ui/skeleton.tsx +7 -7
  100. package/templates/frontend/src/components/ui/switch.tsx +28 -28
  101. package/templates/frontend/src/components/ui/tabs.tsx +54 -54
  102. package/templates/frontend/src/components/ui/textarea.tsx +20 -20
  103. package/templates/frontend/src/components/ui/toast.tsx +109 -109
  104. package/templates/frontend/src/components/ui/toaster.tsx +30 -30
  105. package/templates/frontend/src/config/site.ts +197 -197
  106. package/templates/frontend/src/hooks/use-toast.ts +116 -116
  107. package/templates/frontend/src/lib/api/auth.ts +39 -39
  108. package/templates/frontend/src/lib/api/client.ts +66 -66
  109. package/templates/frontend/src/lib/hooks/use-auth.ts +1 -1
  110. package/templates/frontend/src/lib/utils.ts +6 -6
  111. package/templates/frontend/src/providers/auth-provider.tsx +60 -60
  112. package/templates/frontend/src/types/api.ts +12 -12
  113. package/templates/frontend/src/types/auth.ts +27 -27
  114. package/templates/frontend/tsconfig.json +23 -23
@@ -1,161 +1,161 @@
1
- 'use client'
2
-
3
- import { useState } from 'react'
4
- import Link from 'next/link'
5
- import { useForm } from 'react-hook-form'
6
- import { useAuth } from '@/lib/hooks/use-auth'
7
- import { Button } from '@/components/ui/button'
8
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
9
- import { Checkbox } from '@/components/ui/checkbox'
10
- import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
11
- import { Input } from '@/components/ui/input'
12
-
13
- interface RegisterForm {
14
- name: string
15
- email: string
16
- password: string
17
- terms: boolean
18
- }
19
-
20
- export default function RegisterPage() {
21
- const { register } = useAuth()
22
- const [success, setSuccess] = useState(false)
23
- const [apiError, setApiError] = useState('')
24
-
25
- const form = useForm<RegisterForm>({
26
- defaultValues: { name: '', email: '', password: '', terms: false },
27
- })
28
-
29
- async function onSubmit(data: RegisterForm) {
30
- setApiError('')
31
- try {
32
- await register(data.name, data.email, data.password)
33
- setSuccess(true)
34
- } catch (err: unknown) {
35
- const msg =
36
- err instanceof Error
37
- ? err.message
38
- : (err as { message?: string })?.message ?? 'Error al crear la cuenta'
39
- setApiError(msg)
40
- }
41
- }
42
-
43
- if (success) {
44
- return (
45
- <Card>
46
- <CardHeader>
47
- <CardTitle>¡Revisá tu email!</CardTitle>
48
- <CardDescription>
49
- Te enviamos un link de verificación. Hacé click en el link para activar tu cuenta.
50
- </CardDescription>
51
- </CardHeader>
52
- </Card>
53
- )
54
- }
55
-
56
- return (
57
- <Card>
58
- <CardHeader>
59
- <CardTitle>Crear cuenta</CardTitle>
60
- <CardDescription>Completá tus datos para empezar</CardDescription>
61
- </CardHeader>
62
- <CardContent>
63
- <Form {...form}>
64
- <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
65
- <FormField
66
- control={form.control}
67
- name="name"
68
- rules={{
69
- required: 'El nombre es requerido',
70
- minLength: { value: 2, message: 'Mínimo 2 caracteres' },
71
- }}
72
- render={({ field }) => (
73
- <FormItem>
74
- <FormLabel>Nombre completo</FormLabel>
75
- <FormControl>
76
- <Input placeholder="Juan Pérez" {...field} />
77
- </FormControl>
78
- <FormMessage />
79
- </FormItem>
80
- )}
81
- />
82
- <FormField
83
- control={form.control}
84
- name="email"
85
- rules={{
86
- required: 'El email es requerido',
87
- pattern: { value: /\S+@\S+\.\S+/, message: 'Email inválido' },
88
- }}
89
- render={({ field }) => (
90
- <FormItem>
91
- <FormLabel>Email</FormLabel>
92
- <FormControl>
93
- <Input type="email" placeholder="juan@empresa.com" {...field} />
94
- </FormControl>
95
- <FormMessage />
96
- </FormItem>
97
- )}
98
- />
99
- <FormField
100
- control={form.control}
101
- name="password"
102
- rules={{
103
- required: 'La contraseña es requerida',
104
- minLength: { value: 8, message: 'Mínimo 8 caracteres' },
105
- }}
106
- render={({ field }) => (
107
- <FormItem>
108
- <FormLabel>Contraseña</FormLabel>
109
- <FormControl>
110
- <Input type="password" placeholder="Mínimo 8 caracteres" {...field} />
111
- </FormControl>
112
- <FormMessage />
113
- </FormItem>
114
- )}
115
- />
116
-
117
- <FormField
118
- control={form.control}
119
- name="terms"
120
- rules={{ validate: (v) => v || 'Debés aceptar los términos para continuar' }}
121
- render={({ field }) => (
122
- <FormItem className="flex flex-row items-start space-x-3 space-y-0">
123
- <FormControl>
124
- <Checkbox
125
- checked={field.value}
126
- onCheckedChange={field.onChange}
127
- />
128
- </FormControl>
129
- <div className="space-y-1 leading-none">
130
- <FormLabel className="text-sm font-normal">
131
- Acepto los{' '}
132
- <Link href="/terms" target="_blank" className="underline underline-offset-4 hover:text-foreground">
133
- Términos y Condiciones
134
- </Link>{' '}
135
- y la{' '}
136
- <Link href="/privacy" target="_blank" className="underline underline-offset-4 hover:text-foreground">
137
- Política de Privacidad
138
- </Link>
139
- </FormLabel>
140
- <FormMessage />
141
- </div>
142
- </FormItem>
143
- )}
144
- />
145
-
146
- {apiError && <p className="text-sm text-destructive">{apiError}</p>}
147
- <Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
148
- {form.formState.isSubmitting ? 'Creando cuenta...' : 'Crear cuenta'}
149
- </Button>
150
- </form>
151
- </Form>
152
- <p className="mt-4 text-center text-sm text-muted-foreground">
153
- ¿Ya tenés cuenta?{' '}
154
- <Link href="/login" className="underline underline-offset-4 hover:text-foreground">
155
- Iniciá sesión
156
- </Link>
157
- </p>
158
- </CardContent>
159
- </Card>
160
- )
161
- }
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import Link from 'next/link'
5
+ import { useForm } from 'react-hook-form'
6
+ import { useAuth } from '@/lib/hooks/use-auth'
7
+ import { Button } from '@/components/ui/button'
8
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
9
+ import { Checkbox } from '@/components/ui/checkbox'
10
+ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
11
+ import { Input } from '@/components/ui/input'
12
+
13
+ interface RegisterForm {
14
+ name: string
15
+ email: string
16
+ password: string
17
+ terms: boolean
18
+ }
19
+
20
+ export default function RegisterPage() {
21
+ const { register } = useAuth()
22
+ const [success, setSuccess] = useState(false)
23
+ const [apiError, setApiError] = useState('')
24
+
25
+ const form = useForm<RegisterForm>({
26
+ defaultValues: { name: '', email: '', password: '', terms: false },
27
+ })
28
+
29
+ async function onSubmit(data: RegisterForm) {
30
+ setApiError('')
31
+ try {
32
+ await register(data.name, data.email, data.password)
33
+ setSuccess(true)
34
+ } catch (err: unknown) {
35
+ const msg =
36
+ err instanceof Error
37
+ ? err.message
38
+ : (err as { message?: string })?.message ?? 'Error al crear la cuenta'
39
+ setApiError(msg)
40
+ }
41
+ }
42
+
43
+ if (success) {
44
+ return (
45
+ <Card>
46
+ <CardHeader>
47
+ <CardTitle>¡Revisá tu email!</CardTitle>
48
+ <CardDescription>
49
+ Te enviamos un link de verificación. Hacé click en el link para activar tu cuenta.
50
+ </CardDescription>
51
+ </CardHeader>
52
+ </Card>
53
+ )
54
+ }
55
+
56
+ return (
57
+ <Card>
58
+ <CardHeader>
59
+ <CardTitle>Crear cuenta</CardTitle>
60
+ <CardDescription>Completá tus datos para empezar</CardDescription>
61
+ </CardHeader>
62
+ <CardContent>
63
+ <Form {...form}>
64
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
65
+ <FormField
66
+ control={form.control}
67
+ name="name"
68
+ rules={{
69
+ required: 'El nombre es requerido',
70
+ minLength: { value: 2, message: 'Mínimo 2 caracteres' },
71
+ }}
72
+ render={({ field }) => (
73
+ <FormItem>
74
+ <FormLabel>Nombre completo</FormLabel>
75
+ <FormControl>
76
+ <Input placeholder="Juan Pérez" {...field} />
77
+ </FormControl>
78
+ <FormMessage />
79
+ </FormItem>
80
+ )}
81
+ />
82
+ <FormField
83
+ control={form.control}
84
+ name="email"
85
+ rules={{
86
+ required: 'El email es requerido',
87
+ pattern: { value: /\S+@\S+\.\S+/, message: 'Email inválido' },
88
+ }}
89
+ render={({ field }) => (
90
+ <FormItem>
91
+ <FormLabel>Email</FormLabel>
92
+ <FormControl>
93
+ <Input type="email" placeholder="juan@empresa.com" {...field} />
94
+ </FormControl>
95
+ <FormMessage />
96
+ </FormItem>
97
+ )}
98
+ />
99
+ <FormField
100
+ control={form.control}
101
+ name="password"
102
+ rules={{
103
+ required: 'La contraseña es requerida',
104
+ minLength: { value: 8, message: 'Mínimo 8 caracteres' },
105
+ }}
106
+ render={({ field }) => (
107
+ <FormItem>
108
+ <FormLabel>Contraseña</FormLabel>
109
+ <FormControl>
110
+ <Input type="password" placeholder="Mínimo 8 caracteres" {...field} />
111
+ </FormControl>
112
+ <FormMessage />
113
+ </FormItem>
114
+ )}
115
+ />
116
+
117
+ <FormField
118
+ control={form.control}
119
+ name="terms"
120
+ rules={{ validate: (v) => v || 'Debés aceptar los términos para continuar' }}
121
+ render={({ field }) => (
122
+ <FormItem className="flex flex-row items-start space-x-3 space-y-0">
123
+ <FormControl>
124
+ <Checkbox
125
+ checked={field.value}
126
+ onCheckedChange={field.onChange}
127
+ />
128
+ </FormControl>
129
+ <div className="space-y-1 leading-none">
130
+ <FormLabel className="text-sm font-normal">
131
+ Acepto los{' '}
132
+ <Link href="/terms" target="_blank" className="underline underline-offset-4 hover:text-foreground">
133
+ Términos y Condiciones
134
+ </Link>{' '}
135
+ y la{' '}
136
+ <Link href="/privacy" target="_blank" className="underline underline-offset-4 hover:text-foreground">
137
+ Política de Privacidad
138
+ </Link>
139
+ </FormLabel>
140
+ <FormMessage />
141
+ </div>
142
+ </FormItem>
143
+ )}
144
+ />
145
+
146
+ {apiError && <p className="text-sm text-destructive">{apiError}</p>}
147
+ <Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
148
+ {form.formState.isSubmitting ? 'Creando cuenta...' : 'Crear cuenta'}
149
+ </Button>
150
+ </form>
151
+ </Form>
152
+ <p className="mt-4 text-center text-sm text-muted-foreground">
153
+ ¿Ya tenés cuenta?{' '}
154
+ <Link href="/login" className="underline underline-offset-4 hover:text-foreground">
155
+ Iniciá sesión
156
+ </Link>
157
+ </p>
158
+ </CardContent>
159
+ </Card>
160
+ )
161
+ }
@@ -1,120 +1,120 @@
1
- 'use client'
2
-
3
- import { Suspense, useState } from 'react'
4
- import Link from 'next/link'
5
- import { useRouter, useSearchParams } from 'next/navigation'
6
- import { useForm } from 'react-hook-form'
7
- import { authApi } from '@/lib/api/auth'
8
- import { Button } from '@/components/ui/button'
9
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
10
- import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
11
- import { Input } from '@/components/ui/input'
12
-
13
- interface ResetPasswordForm {
14
- newPassword: string
15
- confirmPassword: string
16
- }
17
-
18
- function ResetPasswordContent() {
19
- const router = useRouter()
20
- const searchParams = useSearchParams()
21
- const token = searchParams.get('token')
22
- const [apiError, setApiError] = useState('')
23
-
24
- const form = useForm<ResetPasswordForm>({
25
- defaultValues: { newPassword: '', confirmPassword: '' },
26
- })
27
-
28
- async function onSubmit(data: ResetPasswordForm) {
29
- if (!token) {
30
- setApiError('Token inválido o faltante.')
31
- return
32
- }
33
- setApiError('')
34
- try {
35
- await authApi.resetPassword(token, data.newPassword)
36
- router.push('/login')
37
- } catch (err: unknown) {
38
- const msg = (err as { message?: string })?.message ?? 'Error al restablecer la contraseña'
39
- setApiError(msg)
40
- }
41
- }
42
-
43
- if (!token) {
44
- return (
45
- <Card>
46
- <CardHeader>
47
- <CardTitle>Link inválido</CardTitle>
48
- <CardDescription>El link de restablecimiento es inválido o expiró.</CardDescription>
49
- </CardHeader>
50
- <CardContent>
51
- <Button asChild variant="outline" className="w-full">
52
- <Link href="/forgot-password">Solicitar nuevo link</Link>
53
- </Button>
54
- </CardContent>
55
- </Card>
56
- )
57
- }
58
-
59
- return (
60
- <Card>
61
- <CardHeader>
62
- <CardTitle>Nueva contraseña</CardTitle>
63
- <CardDescription>Elegí una nueva contraseña para tu cuenta.</CardDescription>
64
- </CardHeader>
65
- <CardContent>
66
- <Form {...form}>
67
- <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
68
- <FormField
69
- control={form.control}
70
- name="newPassword"
71
- rules={{
72
- required: 'La contraseña es requerida',
73
- minLength: { value: 8, message: 'Mínimo 8 caracteres' },
74
- }}
75
- render={({ field }) => (
76
- <FormItem>
77
- <FormLabel>Nueva contraseña</FormLabel>
78
- <FormControl>
79
- <Input type="password" placeholder="Mínimo 8 caracteres" {...field} />
80
- </FormControl>
81
- <FormMessage />
82
- </FormItem>
83
- )}
84
- />
85
- <FormField
86
- control={form.control}
87
- name="confirmPassword"
88
- rules={{
89
- required: 'Confirmá la contraseña',
90
- validate: (val) =>
91
- val === form.getValues('newPassword') || 'Las contraseñas no coinciden',
92
- }}
93
- render={({ field }) => (
94
- <FormItem>
95
- <FormLabel>Confirmar contraseña</FormLabel>
96
- <FormControl>
97
- <Input type="password" placeholder="Repetí la contraseña" {...field} />
98
- </FormControl>
99
- <FormMessage />
100
- </FormItem>
101
- )}
102
- />
103
- {apiError && <p className="text-sm text-destructive">{apiError}</p>}
104
- <Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
105
- {form.formState.isSubmitting ? 'Guardando...' : 'Guardar nueva contraseña'}
106
- </Button>
107
- </form>
108
- </Form>
109
- </CardContent>
110
- </Card>
111
- )
112
- }
113
-
114
- export default function ResetPasswordPage() {
115
- return (
116
- <Suspense fallback={<Card><CardHeader><CardTitle>Cargando...</CardTitle></CardHeader></Card>}>
117
- <ResetPasswordContent />
118
- </Suspense>
119
- )
120
- }
1
+ 'use client'
2
+
3
+ import { Suspense, useState } from 'react'
4
+ import Link from 'next/link'
5
+ import { useRouter, useSearchParams } from 'next/navigation'
6
+ import { useForm } from 'react-hook-form'
7
+ import { authApi } from '@/lib/api/auth'
8
+ import { Button } from '@/components/ui/button'
9
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
10
+ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
11
+ import { Input } from '@/components/ui/input'
12
+
13
+ interface ResetPasswordForm {
14
+ newPassword: string
15
+ confirmPassword: string
16
+ }
17
+
18
+ function ResetPasswordContent() {
19
+ const router = useRouter()
20
+ const searchParams = useSearchParams()
21
+ const token = searchParams.get('token')
22
+ const [apiError, setApiError] = useState('')
23
+
24
+ const form = useForm<ResetPasswordForm>({
25
+ defaultValues: { newPassword: '', confirmPassword: '' },
26
+ })
27
+
28
+ async function onSubmit(data: ResetPasswordForm) {
29
+ if (!token) {
30
+ setApiError('Token inválido o faltante.')
31
+ return
32
+ }
33
+ setApiError('')
34
+ try {
35
+ await authApi.resetPassword(token, data.newPassword)
36
+ router.push('/login')
37
+ } catch (err: unknown) {
38
+ const msg = (err as { message?: string })?.message ?? 'Error al restablecer la contraseña'
39
+ setApiError(msg)
40
+ }
41
+ }
42
+
43
+ if (!token) {
44
+ return (
45
+ <Card>
46
+ <CardHeader>
47
+ <CardTitle>Link inválido</CardTitle>
48
+ <CardDescription>El link de restablecimiento es inválido o expiró.</CardDescription>
49
+ </CardHeader>
50
+ <CardContent>
51
+ <Button asChild variant="outline" className="w-full">
52
+ <Link href="/forgot-password">Solicitar nuevo link</Link>
53
+ </Button>
54
+ </CardContent>
55
+ </Card>
56
+ )
57
+ }
58
+
59
+ return (
60
+ <Card>
61
+ <CardHeader>
62
+ <CardTitle>Nueva contraseña</CardTitle>
63
+ <CardDescription>Elegí una nueva contraseña para tu cuenta.</CardDescription>
64
+ </CardHeader>
65
+ <CardContent>
66
+ <Form {...form}>
67
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
68
+ <FormField
69
+ control={form.control}
70
+ name="newPassword"
71
+ rules={{
72
+ required: 'La contraseña es requerida',
73
+ minLength: { value: 8, message: 'Mínimo 8 caracteres' },
74
+ }}
75
+ render={({ field }) => (
76
+ <FormItem>
77
+ <FormLabel>Nueva contraseña</FormLabel>
78
+ <FormControl>
79
+ <Input type="password" placeholder="Mínimo 8 caracteres" {...field} />
80
+ </FormControl>
81
+ <FormMessage />
82
+ </FormItem>
83
+ )}
84
+ />
85
+ <FormField
86
+ control={form.control}
87
+ name="confirmPassword"
88
+ rules={{
89
+ required: 'Confirmá la contraseña',
90
+ validate: (val) =>
91
+ val === form.getValues('newPassword') || 'Las contraseñas no coinciden',
92
+ }}
93
+ render={({ field }) => (
94
+ <FormItem>
95
+ <FormLabel>Confirmar contraseña</FormLabel>
96
+ <FormControl>
97
+ <Input type="password" placeholder="Repetí la contraseña" {...field} />
98
+ </FormControl>
99
+ <FormMessage />
100
+ </FormItem>
101
+ )}
102
+ />
103
+ {apiError && <p className="text-sm text-destructive">{apiError}</p>}
104
+ <Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
105
+ {form.formState.isSubmitting ? 'Guardando...' : 'Guardar nueva contraseña'}
106
+ </Button>
107
+ </form>
108
+ </Form>
109
+ </CardContent>
110
+ </Card>
111
+ )
112
+ }
113
+
114
+ export default function ResetPasswordPage() {
115
+ return (
116
+ <Suspense fallback={<Card><CardHeader><CardTitle>Cargando...</CardTitle></CardHeader></Card>}>
117
+ <ResetPasswordContent />
118
+ </Suspense>
119
+ )
120
+ }