ar-saas 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 (85) hide show
  1. package/README.md +62 -0
  2. package/dist/cli.js +67 -0
  3. package/dist/generator.js +242 -0
  4. package/dist/index.js +13 -0
  5. package/dist/license.js +71 -0
  6. package/package.json +46 -0
  7. package/templates/backend/.env.example +67 -0
  8. package/templates/backend/.prettierrc +4 -0
  9. package/templates/backend/README.md +168 -0
  10. package/templates/backend/eslint.config.mjs +35 -0
  11. package/templates/backend/nest-cli.json +8 -0
  12. package/templates/backend/package-lock.json +10979 -0
  13. package/templates/backend/package.json +88 -0
  14. package/templates/backend/src/app.controller.spec.ts +24 -0
  15. package/templates/backend/src/app.controller.ts +15 -0
  16. package/templates/backend/src/app.module.ts +40 -0
  17. package/templates/backend/src/app.service.ts +11 -0
  18. package/templates/backend/src/common/base/base.repository.ts +221 -0
  19. package/templates/backend/src/common/base/base.schema.ts +24 -0
  20. package/templates/backend/src/common/decorators/cookie.decorator.ts +9 -0
  21. package/templates/backend/src/common/decorators/current-user.decorator.ts +20 -0
  22. package/templates/backend/src/common/decorators/workspace-id.decorator.ts +14 -0
  23. package/templates/backend/src/common/filters/global-exception.filter.ts +61 -0
  24. package/templates/backend/src/common/guards/jwt-auth.guard.ts +5 -0
  25. package/templates/backend/src/common/interceptors/workspace-tenant.interceptor.ts +45 -0
  26. package/templates/backend/src/main.ts +51 -0
  27. package/templates/backend/src/modules/auth/auth.controller.ts +158 -0
  28. package/templates/backend/src/modules/auth/auth.module.ts +20 -0
  29. package/templates/backend/src/modules/auth/auth.service.ts +257 -0
  30. package/templates/backend/src/modules/auth/dto/forgot-password.dto.ts +9 -0
  31. package/templates/backend/src/modules/auth/dto/login.dto.ts +14 -0
  32. package/templates/backend/src/modules/auth/dto/refresh-token.dto.ts +12 -0
  33. package/templates/backend/src/modules/auth/dto/register.dto.ts +26 -0
  34. package/templates/backend/src/modules/auth/dto/reset-password.dto.ts +16 -0
  35. package/templates/backend/src/modules/auth/dto/verify-email.dto.ts +9 -0
  36. package/templates/backend/src/modules/auth/strategies/jwt.strategy.ts +43 -0
  37. package/templates/backend/src/modules/mail/mail.module.ts +9 -0
  38. package/templates/backend/src/modules/mail/mail.service.ts +141 -0
  39. package/templates/backend/src/modules/users/schemas/user.schema.ts +54 -0
  40. package/templates/backend/src/modules/users/users.module.ts +14 -0
  41. package/templates/backend/src/modules/users/users.repository.ts +51 -0
  42. package/templates/backend/src/modules/users/users.service.ts +104 -0
  43. package/templates/backend/src/modules/workspaces/schemas/workspace.schema.ts +26 -0
  44. package/templates/backend/src/modules/workspaces/workspaces.module.ts +16 -0
  45. package/templates/backend/src/modules/workspaces/workspaces.repository.ts +34 -0
  46. package/templates/backend/src/modules/workspaces/workspaces.service.ts +42 -0
  47. package/templates/backend/test/app.e2e-spec.ts +25 -0
  48. package/templates/backend/test/jest-e2e.json +9 -0
  49. package/templates/backend/tsconfig.build.json +4 -0
  50. package/templates/backend/tsconfig.json +26 -0
  51. package/templates/frontend/.env.local.example +1 -0
  52. package/templates/frontend/components.json +20 -0
  53. package/templates/frontend/eslint.config.mjs +14 -0
  54. package/templates/frontend/next.config.ts +5 -0
  55. package/templates/frontend/package-lock.json +6722 -0
  56. package/templates/frontend/package.json +40 -0
  57. package/templates/frontend/postcss.config.mjs +7 -0
  58. package/templates/frontend/src/app/(auth)/forgot-password/page.tsx +84 -0
  59. package/templates/frontend/src/app/(auth)/layout.tsx +28 -0
  60. package/templates/frontend/src/app/(auth)/login/page.tsx +111 -0
  61. package/templates/frontend/src/app/(auth)/register/page.tsx +119 -0
  62. package/templates/frontend/src/app/(auth)/reset-password/page.tsx +120 -0
  63. package/templates/frontend/src/app/(auth)/verify-email/page.tsx +78 -0
  64. package/templates/frontend/src/app/(dashboard)/dashboard/page.tsx +36 -0
  65. package/templates/frontend/src/app/(dashboard)/layout.tsx +59 -0
  66. package/templates/frontend/src/app/globals.css +81 -0
  67. package/templates/frontend/src/app/layout.tsx +26 -0
  68. package/templates/frontend/src/app/page.tsx +5 -0
  69. package/templates/frontend/src/app/setup/page.tsx +278 -0
  70. package/templates/frontend/src/components/ui/button.tsx +52 -0
  71. package/templates/frontend/src/components/ui/card.tsx +50 -0
  72. package/templates/frontend/src/components/ui/form.tsx +158 -0
  73. package/templates/frontend/src/components/ui/input.tsx +21 -0
  74. package/templates/frontend/src/components/ui/label.tsx +22 -0
  75. package/templates/frontend/src/components/ui/toast.tsx +109 -0
  76. package/templates/frontend/src/components/ui/toaster.tsx +30 -0
  77. package/templates/frontend/src/hooks/use-toast.ts +116 -0
  78. package/templates/frontend/src/lib/api/auth.ts +39 -0
  79. package/templates/frontend/src/lib/api/client.ts +66 -0
  80. package/templates/frontend/src/lib/hooks/use-auth.ts +1 -0
  81. package/templates/frontend/src/lib/utils.ts +6 -0
  82. package/templates/frontend/src/providers/auth-provider.tsx +60 -0
  83. package/templates/frontend/src/types/api.ts +12 -0
  84. package/templates/frontend/src/types/auth.ts +27 -0
  85. package/templates/frontend/tsconfig.json +23 -0
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "create-saas-ar-frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev --turbopack",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "next": "^15.3.3",
13
+ "react": "^19.1.0",
14
+ "react-dom": "^19.1.0",
15
+ "axios": "^1.7.9",
16
+ "js-cookie": "^3.0.5",
17
+ "@radix-ui/react-slot": "^1.1.2",
18
+ "@radix-ui/react-label": "^2.1.2",
19
+ "@radix-ui/react-toast": "^1.2.6",
20
+ "class-variance-authority": "^0.7.1",
21
+ "clsx": "^2.1.1",
22
+ "tailwind-merge": "^2.6.0",
23
+ "lucide-react": "^0.477.0",
24
+ "react-hook-form": "^7.54.2",
25
+ "@hookform/resolvers": "^3.9.1",
26
+ "zod": "^3.24.1"
27
+ },
28
+ "devDependencies": {
29
+ "typescript": "^5.7.3",
30
+ "@types/node": "^22.10.7",
31
+ "@types/react": "^19.1.0",
32
+ "@types/react-dom": "^19.1.0",
33
+ "@types/js-cookie": "^3.0.6",
34
+ "tailwindcss": "^4.1.0",
35
+ "@tailwindcss/postcss": "^4.1.0",
36
+ "eslint": "^9.18.0",
37
+ "eslint-config-next": "^15.3.3",
38
+ "@eslint/eslintrc": "^3.2.0"
39
+ }
40
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ '@tailwindcss/postcss': {},
4
+ },
5
+ }
6
+
7
+ export default config
@@ -0,0 +1,84 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import Link from 'next/link'
5
+ import { useForm } from 'react-hook-form'
6
+ import { authApi } from '@/lib/api/auth'
7
+ import { Button } from '@/components/ui/button'
8
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
9
+ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
10
+ import { Input } from '@/components/ui/input'
11
+
12
+ interface ForgotPasswordForm {
13
+ email: string
14
+ }
15
+
16
+ export default function ForgotPasswordPage() {
17
+ const [sent, setSent] = useState(false)
18
+
19
+ const form = useForm<ForgotPasswordForm>({
20
+ defaultValues: { email: '' },
21
+ })
22
+
23
+ async function onSubmit(data: ForgotPasswordForm) {
24
+ await authApi.forgotPassword(data.email).catch(() => null)
25
+ setSent(true)
26
+ }
27
+
28
+ if (sent) {
29
+ return (
30
+ <Card>
31
+ <CardHeader>
32
+ <CardTitle>Revisá tu email</CardTitle>
33
+ <CardDescription>
34
+ Si el email está registrado, te enviamos un link para restablecer tu contraseña.
35
+ </CardDescription>
36
+ </CardHeader>
37
+ <CardContent>
38
+ <Button asChild variant="outline" className="w-full">
39
+ <Link href="/login">Volver al inicio de sesión</Link>
40
+ </Button>
41
+ </CardContent>
42
+ </Card>
43
+ )
44
+ }
45
+
46
+ return (
47
+ <Card>
48
+ <CardHeader>
49
+ <CardTitle>Olvidé mi contraseña</CardTitle>
50
+ <CardDescription>
51
+ Ingresá tu email y te enviamos el link de restablecimiento.
52
+ </CardDescription>
53
+ </CardHeader>
54
+ <CardContent>
55
+ <Form {...form}>
56
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
57
+ <FormField
58
+ control={form.control}
59
+ name="email"
60
+ rules={{ required: 'El email es requerido', pattern: { value: /\S+@\S+\.\S+/, message: 'Email inválido' } }}
61
+ render={({ field }) => (
62
+ <FormItem>
63
+ <FormLabel>Email</FormLabel>
64
+ <FormControl>
65
+ <Input type="email" placeholder="juan@empresa.com" {...field} />
66
+ </FormControl>
67
+ <FormMessage />
68
+ </FormItem>
69
+ )}
70
+ />
71
+ <Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
72
+ {form.formState.isSubmitting ? 'Enviando...' : 'Enviar link de restablecimiento'}
73
+ </Button>
74
+ </form>
75
+ </Form>
76
+ <p className="mt-4 text-center text-sm text-muted-foreground">
77
+ <Link href="/login" className="underline underline-offset-4 hover:text-foreground">
78
+ Volver al inicio de sesión
79
+ </Link>
80
+ </p>
81
+ </CardContent>
82
+ </Card>
83
+ )
84
+ }
@@ -0,0 +1,28 @@
1
+ 'use client'
2
+
3
+ import { useEffect } from 'react'
4
+ import { useRouter } from 'next/navigation'
5
+ import { useAuth } from '@/lib/hooks/use-auth'
6
+
7
+ export default function AuthLayout({ children }: { children: React.ReactNode }) {
8
+ const { isAuthenticated, isLoading } = useAuth()
9
+ const router = useRouter()
10
+
11
+ useEffect(() => {
12
+ if (!isLoading && isAuthenticated) {
13
+ router.replace('/dashboard')
14
+ }
15
+ }, [isAuthenticated, isLoading, router])
16
+
17
+ if (isLoading) return null
18
+
19
+ return (
20
+ <div className="min-h-screen flex flex-col items-center justify-center bg-muted/40 px-4">
21
+ <div className="mb-8 text-center">
22
+ <h1 className="text-2xl font-bold tracking-tight">create-saas-ar</h1>
23
+ <p className="text-sm text-muted-foreground mt-1">Tu SaaS listo para Argentina</p>
24
+ </div>
25
+ <div className="w-full max-w-md">{children}</div>
26
+ </div>
27
+ )
28
+ }
@@ -0,0 +1,111 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import Link from 'next/link'
5
+ import { Eye, EyeOff } from 'lucide-react'
6
+ import { useForm } from 'react-hook-form'
7
+ import { useAuth } from '@/lib/hooks/use-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 LoginForm {
14
+ email: string
15
+ password: string
16
+ }
17
+
18
+ export default function LoginPage() {
19
+ const { login } = useAuth()
20
+ const [showPassword, setShowPassword] = useState(false)
21
+ const [apiError, setApiError] = useState('')
22
+
23
+ const form = useForm<LoginForm>({
24
+ defaultValues: { email: '', password: '' },
25
+ })
26
+
27
+ async function onSubmit(data: LoginForm) {
28
+ setApiError('')
29
+ try {
30
+ await login(data.email, data.password)
31
+ } catch (err: unknown) {
32
+ const msg = err instanceof Error
33
+ ? err.message
34
+ : (err as { message?: string })?.message ?? 'Error al iniciar sesión'
35
+ setApiError(msg)
36
+ }
37
+ }
38
+
39
+ return (
40
+ <Card>
41
+ <CardHeader>
42
+ <CardTitle>Iniciar sesión</CardTitle>
43
+ <CardDescription>Ingresá tu email y contraseña</CardDescription>
44
+ </CardHeader>
45
+ <CardContent>
46
+ <Form {...form}>
47
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
48
+ <FormField
49
+ control={form.control}
50
+ name="email"
51
+ rules={{ required: 'El email es requerido', pattern: { value: /\S+@\S+\.\S+/, message: 'Email inválido' } }}
52
+ render={({ field }) => (
53
+ <FormItem>
54
+ <FormLabel>Email</FormLabel>
55
+ <FormControl>
56
+ <Input type="email" placeholder="juan@empresa.com" {...field} />
57
+ </FormControl>
58
+ <FormMessage />
59
+ </FormItem>
60
+ )}
61
+ />
62
+ <FormField
63
+ control={form.control}
64
+ name="password"
65
+ rules={{ required: 'La contraseña es requerida' }}
66
+ render={({ field }) => (
67
+ <FormItem>
68
+ <div className="flex items-center justify-between">
69
+ <FormLabel>Contraseña</FormLabel>
70
+ <Link href="/forgot-password" className="text-xs text-muted-foreground underline underline-offset-4 hover:text-foreground">
71
+ ¿Olvidaste tu contraseña?
72
+ </Link>
73
+ </div>
74
+ <FormControl>
75
+ <div className="relative">
76
+ <Input
77
+ type={showPassword ? 'text' : 'password'}
78
+ placeholder="Tu contraseña"
79
+ className="pr-10"
80
+ {...field}
81
+ />
82
+ <button
83
+ type="button"
84
+ onClick={() => setShowPassword((v) => !v)}
85
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
86
+ tabIndex={-1}
87
+ >
88
+ {showPassword ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
89
+ </button>
90
+ </div>
91
+ </FormControl>
92
+ <FormMessage />
93
+ </FormItem>
94
+ )}
95
+ />
96
+ {apiError && <p className="text-sm text-destructive">{apiError}</p>}
97
+ <Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
98
+ {form.formState.isSubmitting ? 'Ingresando...' : 'Iniciar sesión'}
99
+ </Button>
100
+ </form>
101
+ </Form>
102
+ <p className="mt-4 text-center text-sm text-muted-foreground">
103
+ ¿No tenés cuenta?{' '}
104
+ <Link href="/register" className="underline underline-offset-4 hover:text-foreground">
105
+ Registrate
106
+ </Link>
107
+ </p>
108
+ </CardContent>
109
+ </Card>
110
+ )
111
+ }
@@ -0,0 +1,119 @@
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 { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
10
+ import { Input } from '@/components/ui/input'
11
+
12
+ interface RegisterForm {
13
+ name: string
14
+ email: string
15
+ password: string
16
+ }
17
+
18
+ export default function RegisterPage() {
19
+ const { register } = useAuth()
20
+ const [success, setSuccess] = useState(false)
21
+ const [apiError, setApiError] = useState('')
22
+
23
+ const form = useForm<RegisterForm>({
24
+ defaultValues: { name: '', email: '', password: '' },
25
+ })
26
+
27
+ async function onSubmit(data: RegisterForm) {
28
+ setApiError('')
29
+ try {
30
+ await register(data.name, data.email, data.password)
31
+ setSuccess(true)
32
+ } catch (err: unknown) {
33
+ const msg = err instanceof Error
34
+ ? err.message
35
+ : (err as { message?: string })?.message ?? 'Error al crear la cuenta'
36
+ setApiError(msg)
37
+ }
38
+ }
39
+
40
+ if (success) {
41
+ return (
42
+ <Card>
43
+ <CardHeader>
44
+ <CardTitle>¡Revisá tu email!</CardTitle>
45
+ <CardDescription>
46
+ Te enviamos un link de verificación. Hacé click en el link para activar tu cuenta.
47
+ </CardDescription>
48
+ </CardHeader>
49
+ </Card>
50
+ )
51
+ }
52
+
53
+ return (
54
+ <Card>
55
+ <CardHeader>
56
+ <CardTitle>Crear cuenta</CardTitle>
57
+ <CardDescription>Completá tus datos para empezar</CardDescription>
58
+ </CardHeader>
59
+ <CardContent>
60
+ <Form {...form}>
61
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
62
+ <FormField
63
+ control={form.control}
64
+ name="name"
65
+ rules={{ required: 'El nombre es requerido', minLength: { value: 2, message: 'Mínimo 2 caracteres' } }}
66
+ render={({ field }) => (
67
+ <FormItem>
68
+ <FormLabel>Nombre completo</FormLabel>
69
+ <FormControl>
70
+ <Input placeholder="Juan Pérez" {...field} />
71
+ </FormControl>
72
+ <FormMessage />
73
+ </FormItem>
74
+ )}
75
+ />
76
+ <FormField
77
+ control={form.control}
78
+ name="email"
79
+ rules={{ required: 'El email es requerido', pattern: { value: /\S+@\S+\.\S+/, message: 'Email inválido' } }}
80
+ render={({ field }) => (
81
+ <FormItem>
82
+ <FormLabel>Email</FormLabel>
83
+ <FormControl>
84
+ <Input type="email" placeholder="juan@empresa.com" {...field} />
85
+ </FormControl>
86
+ <FormMessage />
87
+ </FormItem>
88
+ )}
89
+ />
90
+ <FormField
91
+ control={form.control}
92
+ name="password"
93
+ rules={{ required: 'La contraseña es requerida', minLength: { value: 8, message: 'Mínimo 8 caracteres' } }}
94
+ render={({ field }) => (
95
+ <FormItem>
96
+ <FormLabel>Contraseña</FormLabel>
97
+ <FormControl>
98
+ <Input type="password" placeholder="Mínimo 8 caracteres" {...field} />
99
+ </FormControl>
100
+ <FormMessage />
101
+ </FormItem>
102
+ )}
103
+ />
104
+ {apiError && <p className="text-sm text-destructive">{apiError}</p>}
105
+ <Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
106
+ {form.formState.isSubmitting ? 'Creando cuenta...' : 'Crear cuenta'}
107
+ </Button>
108
+ </form>
109
+ </Form>
110
+ <p className="mt-4 text-center text-sm text-muted-foreground">
111
+ ¿Ya tenés cuenta?{' '}
112
+ <Link href="/login" className="underline underline-offset-4 hover:text-foreground">
113
+ Iniciá sesión
114
+ </Link>
115
+ </p>
116
+ </CardContent>
117
+ </Card>
118
+ )
119
+ }
@@ -0,0 +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
+ }
@@ -0,0 +1,78 @@
1
+ 'use client'
2
+
3
+ import { Suspense, useEffect, useState } from 'react'
4
+ import Link from 'next/link'
5
+ import { useSearchParams } from 'next/navigation'
6
+ import { authApi } from '@/lib/api/auth'
7
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
8
+ import { Button } from '@/components/ui/button'
9
+
10
+ type Status = 'loading' | 'success' | 'error' | 'no-token'
11
+
12
+ function VerifyEmailContent() {
13
+ const searchParams = useSearchParams()
14
+ const token = searchParams.get('token')
15
+ const [status, setStatus] = useState<Status>(token ? 'loading' : 'no-token')
16
+ const [error, setError] = useState('')
17
+
18
+ useEffect(() => {
19
+ if (!token) return
20
+ authApi
21
+ .verifyEmail(token)
22
+ .then(() => setStatus('success'))
23
+ .catch((err: unknown) => {
24
+ const msg = (err as { message?: string })?.message ?? 'Error al verificar el email'
25
+ setError(msg)
26
+ setStatus('error')
27
+ })
28
+ }, [token])
29
+
30
+ return (
31
+ <Card>
32
+ <CardHeader>
33
+ <CardTitle>Verificación de email</CardTitle>
34
+ </CardHeader>
35
+ <CardContent className="space-y-4">
36
+ {status === 'loading' && (
37
+ <CardDescription>Verificando tu email...</CardDescription>
38
+ )}
39
+ {status === 'success' && (
40
+ <>
41
+ <CardDescription>
42
+ ✓ Email verificado correctamente. Ya podés iniciar sesión.
43
+ </CardDescription>
44
+ <Button asChild className="w-full">
45
+ <Link href="/login">Ir al inicio de sesión</Link>
46
+ </Button>
47
+ </>
48
+ )}
49
+ {status === 'error' && (
50
+ <>
51
+ <CardDescription className="text-destructive">{error}</CardDescription>
52
+ <Button asChild variant="outline" className="w-full">
53
+ <Link href="/login">Volver al inicio de sesión</Link>
54
+ </Button>
55
+ </>
56
+ )}
57
+ {status === 'no-token' && (
58
+ <>
59
+ <CardDescription className="text-destructive">
60
+ Link de verificación inválido o faltante.
61
+ </CardDescription>
62
+ <Button asChild variant="outline" className="w-full">
63
+ <Link href="/login">Volver al inicio de sesión</Link>
64
+ </Button>
65
+ </>
66
+ )}
67
+ </CardContent>
68
+ </Card>
69
+ )
70
+ }
71
+
72
+ export default function VerifyEmailPage() {
73
+ return (
74
+ <Suspense fallback={<Card><CardHeader><CardTitle>Verificando...</CardTitle></CardHeader></Card>}>
75
+ <VerifyEmailContent />
76
+ </Suspense>
77
+ )
78
+ }
@@ -0,0 +1,36 @@
1
+ 'use client'
2
+
3
+ import { useAuth } from '@/lib/hooks/use-auth'
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
5
+ import { Rocket } from 'lucide-react'
6
+
7
+ export default function DashboardPage() {
8
+ const { user } = useAuth()
9
+
10
+ return (
11
+ <div className="space-y-6">
12
+ <div>
13
+ <h1 className="text-2xl font-bold">Bienvenido, {user?.name}</h1>
14
+ <p className="text-muted-foreground">{user?.email}</p>
15
+ </div>
16
+ <Card className="max-w-md">
17
+ <CardHeader>
18
+ <div className="flex items-center gap-2">
19
+ <Rocket className="size-5" />
20
+ <CardTitle>Tu SaaS está listo para construir</CardTitle>
21
+ </div>
22
+ <CardDescription>
23
+ El backend y el frontend están configurados y funcionando. Agregá tus módulos de negocio.
24
+ </CardDescription>
25
+ </CardHeader>
26
+ <CardContent>
27
+ <p className="text-sm text-muted-foreground">
28
+ Consultá la documentación en{' '}
29
+ <span className="font-mono text-foreground">.ai-docs/</span> para ver los patrones
30
+ disponibles.
31
+ </p>
32
+ </CardContent>
33
+ </Card>
34
+ </div>
35
+ )
36
+ }