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.
- package/LICENSE +21 -21
- package/README.md +338 -314
- package/dist/cli.js +19 -0
- package/dist/generator.js +166 -55
- package/package.json +52 -50
- package/templates/backend/.env.example +67 -67
- package/templates/backend/.prettierrc +4 -4
- package/templates/backend/README.md +249 -168
- package/templates/backend/eslint.config.mjs +35 -35
- package/templates/backend/nest-cli.json +8 -8
- package/templates/backend/package-lock.json +10979 -10979
- package/templates/backend/package.json +88 -88
- package/templates/backend/src/app.controller.spec.ts +24 -24
- package/templates/backend/src/app.controller.ts +15 -15
- package/templates/backend/src/app.module.ts +40 -40
- package/templates/backend/src/app.service.ts +11 -11
- package/templates/backend/src/common/base/base.repository.ts +221 -221
- package/templates/backend/src/common/base/base.schema.ts +24 -24
- package/templates/backend/src/common/decorators/cookie.decorator.ts +9 -9
- package/templates/backend/src/common/decorators/current-user.decorator.ts +20 -20
- package/templates/backend/src/common/decorators/workspace-id.decorator.ts +14 -14
- package/templates/backend/src/common/filters/global-exception.filter.ts +61 -61
- package/templates/backend/src/common/guards/jwt-auth.guard.ts +5 -5
- package/templates/backend/src/common/interceptors/workspace-tenant.interceptor.ts +45 -45
- package/templates/backend/src/main.ts +51 -51
- package/templates/backend/src/modules/auth/auth.controller.ts +158 -158
- package/templates/backend/src/modules/auth/auth.module.ts +20 -20
- package/templates/backend/src/modules/auth/auth.service.ts +257 -257
- package/templates/backend/src/modules/auth/dto/forgot-password.dto.ts +9 -9
- package/templates/backend/src/modules/auth/dto/login.dto.ts +14 -14
- package/templates/backend/src/modules/auth/dto/refresh-token.dto.ts +12 -12
- package/templates/backend/src/modules/auth/dto/register.dto.ts +26 -26
- package/templates/backend/src/modules/auth/dto/reset-password.dto.ts +16 -16
- package/templates/backend/src/modules/auth/dto/verify-email.dto.ts +9 -9
- package/templates/backend/src/modules/auth/strategies/jwt.strategy.ts +43 -43
- package/templates/backend/src/modules/mail/mail.module.ts +9 -9
- package/templates/backend/src/modules/mail/mail.service.ts +141 -141
- package/templates/backend/src/modules/users/schemas/user.schema.ts +54 -54
- package/templates/backend/src/modules/users/users.module.ts +14 -14
- package/templates/backend/src/modules/users/users.repository.ts +51 -51
- package/templates/backend/src/modules/users/users.service.ts +104 -104
- package/templates/backend/src/modules/workspaces/schemas/workspace.schema.ts +26 -26
- package/templates/backend/src/modules/workspaces/workspaces.module.ts +16 -16
- package/templates/backend/src/modules/workspaces/workspaces.repository.ts +34 -34
- package/templates/backend/src/modules/workspaces/workspaces.service.ts +42 -42
- package/templates/backend/test/app.e2e-spec.ts +25 -25
- package/templates/backend/test/jest-e2e.json +9 -9
- package/templates/backend/tsconfig.build.json +4 -4
- package/templates/backend/tsconfig.json +26 -26
- package/templates/frontend/.env.local.example +1 -1
- package/templates/frontend/README.md +152 -0
- package/templates/frontend/components.json +20 -20
- package/templates/frontend/eslint.config.mjs +14 -14
- package/templates/frontend/next.config.ts +5 -5
- package/templates/frontend/package-lock.json +6722 -6722
- package/templates/frontend/package.json +48 -48
- package/templates/frontend/pnpm-lock.yaml +5012 -5012
- package/templates/frontend/pnpm-workspace.yaml +3 -3
- package/templates/frontend/postcss.config.mjs +7 -7
- package/templates/frontend/src/app/(auth)/forgot-password/page.tsx +84 -84
- package/templates/frontend/src/app/(auth)/layout.tsx +28 -28
- package/templates/frontend/src/app/(auth)/login/page.tsx +111 -111
- package/templates/frontend/src/app/(auth)/register/page.tsx +161 -161
- package/templates/frontend/src/app/(auth)/reset-password/page.tsx +120 -120
- package/templates/frontend/src/app/(auth)/verify-email/page.tsx +78 -78
- package/templates/frontend/src/app/(dashboard)/billing/page.tsx +111 -111
- package/templates/frontend/src/app/(dashboard)/dashboard/page.tsx +105 -105
- package/templates/frontend/src/app/(dashboard)/layout.tsx +38 -38
- package/templates/frontend/src/app/(dashboard)/profile/page.tsx +226 -226
- package/templates/frontend/src/app/(dashboard)/settings/page.tsx +156 -156
- package/templates/frontend/src/app/(dashboard)/team/page.tsx +178 -178
- package/templates/frontend/src/app/(legal)/privacy/page.tsx +127 -127
- package/templates/frontend/src/app/(legal)/terms/page.tsx +118 -118
- package/templates/frontend/src/app/globals.css +81 -81
- package/templates/frontend/src/app/layout.tsx +26 -26
- package/templates/frontend/src/app/page.tsx +5 -45
- package/templates/frontend/src/app/setup/page.tsx +371 -275
- package/templates/frontend/src/components/dashboard/header.tsx +89 -89
- package/templates/frontend/src/components/dashboard/sidebar.tsx +71 -71
- package/templates/frontend/src/components/dashboard/stat-card.tsx +34 -34
- package/templates/frontend/src/components/landing/faq.tsx +39 -39
- package/templates/frontend/src/components/landing/features.tsx +54 -54
- package/templates/frontend/src/components/landing/footer.tsx +76 -76
- package/templates/frontend/src/components/landing/hero.tsx +72 -72
- package/templates/frontend/src/components/landing/navbar.tsx +78 -78
- package/templates/frontend/src/components/landing/pricing.tsx +90 -90
- package/templates/frontend/src/components/ui/accordion.tsx +52 -52
- package/templates/frontend/src/components/ui/avatar.tsx +46 -46
- package/templates/frontend/src/components/ui/badge.tsx +30 -30
- package/templates/frontend/src/components/ui/button.tsx +52 -52
- package/templates/frontend/src/components/ui/card.tsx +50 -50
- package/templates/frontend/src/components/ui/checkbox.tsx +27 -27
- package/templates/frontend/src/components/ui/dialog.tsx +100 -100
- package/templates/frontend/src/components/ui/dropdown-menu.tsx +173 -173
- package/templates/frontend/src/components/ui/form.tsx +158 -158
- package/templates/frontend/src/components/ui/input.tsx +21 -21
- package/templates/frontend/src/components/ui/label.tsx +22 -22
- package/templates/frontend/src/components/ui/separator.tsx +25 -25
- package/templates/frontend/src/components/ui/skeleton.tsx +7 -7
- package/templates/frontend/src/components/ui/switch.tsx +28 -28
- package/templates/frontend/src/components/ui/tabs.tsx +54 -54
- package/templates/frontend/src/components/ui/textarea.tsx +20 -20
- package/templates/frontend/src/components/ui/toast.tsx +109 -109
- package/templates/frontend/src/components/ui/toaster.tsx +30 -30
- package/templates/frontend/src/config/site.ts +197 -197
- package/templates/frontend/src/hooks/use-toast.ts +116 -116
- package/templates/frontend/src/lib/api/auth.ts +39 -39
- package/templates/frontend/src/lib/api/client.ts +66 -66
- package/templates/frontend/src/lib/hooks/use-auth.ts +1 -1
- package/templates/frontend/src/lib/utils.ts +6 -6
- package/templates/frontend/src/providers/auth-provider.tsx +60 -60
- package/templates/frontend/src/types/api.ts +12 -12
- package/templates/frontend/src/types/auth.ts +27 -27
- 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
|
+
}
|