@create-lft-app/nextjs 1.0.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -3
- package/template/CLAUDE.md +279 -0
- package/template/drizzle.config.ts +12 -0
- package/template/package.json +31 -6
- package/template/proxy.ts +12 -0
- package/template/src/app/(auth)/dashboard/dashboard-content.tsx +124 -0
- package/template/src/app/(auth)/dashboard/page.tsx +9 -0
- package/template/src/app/(auth)/layout.tsx +7 -0
- package/template/src/app/(auth)/users/page.tsx +9 -0
- package/template/src/app/(auth)/users/users-content.tsx +26 -0
- package/template/src/app/(public)/layout.tsx +7 -0
- package/template/src/app/(public)/login/page.tsx +17 -0
- package/template/src/app/api/webhooks/route.ts +20 -0
- package/template/src/app/layout.tsx +13 -12
- package/template/src/app/providers.tsx +27 -0
- package/template/src/components/layout/{midday-sidebar.tsx → sidebar.tsx} +2 -7
- package/template/src/components/tables/data-table-column-header.tsx +68 -0
- package/template/src/components/tables/data-table-pagination.tsx +99 -0
- package/template/src/components/tables/data-table-toolbar.tsx +50 -0
- package/template/src/components/tables/data-table-view-options.tsx +59 -0
- package/template/src/components/tables/data-table.tsx +128 -0
- package/template/src/components/tables/index.ts +5 -0
- package/template/src/components/ui/animations/index.ts +44 -0
- package/template/src/components/ui/button.tsx +50 -21
- package/template/src/components/ui/card.tsx +27 -3
- package/template/src/components/ui/dialog.tsx +38 -35
- package/template/src/components/ui/motion.tsx +197 -0
- package/template/src/components/ui/page-transition.tsx +166 -0
- package/template/src/components/ui/sheet.tsx +65 -41
- package/template/src/config/navigation.ts +69 -0
- package/template/src/config/site.ts +12 -0
- package/template/src/db/index.ts +12 -0
- package/template/src/db/schema/index.ts +1 -0
- package/template/src/db/schema/users.ts +16 -0
- package/template/src/db/seed.ts +39 -0
- package/template/src/hooks/index.ts +3 -0
- package/template/src/hooks/useDataTable.ts +82 -0
- package/template/src/hooks/useDebounce.ts +49 -0
- package/template/src/hooks/useMediaQuery.ts +36 -0
- package/template/src/lib/date/config.ts +34 -0
- package/template/src/lib/date/formatters.ts +120 -0
- package/template/src/lib/date/index.ts +19 -0
- package/template/src/lib/excel/exporter.ts +89 -0
- package/template/src/lib/excel/index.ts +14 -0
- package/template/src/lib/excel/parser.ts +96 -0
- package/template/src/lib/query-client.ts +35 -0
- package/template/src/lib/supabase/client.ts +5 -2
- package/template/src/lib/supabase/proxy.ts +67 -0
- package/template/src/lib/supabase/server.ts +6 -4
- package/template/src/lib/supabase/types.ts +53 -0
- package/template/src/lib/validations/common.ts +75 -0
- package/template/src/lib/validations/index.ts +20 -0
- package/template/src/modules/auth/actions/auth-actions.ts +51 -4
- package/template/src/modules/auth/components/login-form.tsx +68 -0
- package/template/src/modules/auth/hooks/useAuth.ts +38 -0
- package/template/src/modules/auth/hooks/useAuthMutations.ts +43 -0
- package/template/src/modules/auth/hooks/useAuthQueries.ts +43 -0
- package/template/src/modules/auth/index.ts +12 -0
- package/template/src/modules/auth/schemas/auth.schema.ts +32 -0
- package/template/src/modules/auth/stores/useAuthStore.ts +37 -0
- package/template/src/modules/users/actions/users-actions.ts +94 -0
- package/template/src/modules/users/columns.tsx +86 -0
- package/template/src/modules/users/components/users-list.tsx +22 -0
- package/template/src/modules/users/hooks/useUsers.ts +39 -0
- package/template/src/modules/users/hooks/useUsersMutations.ts +55 -0
- package/template/src/modules/users/hooks/useUsersQueries.ts +35 -0
- package/template/src/modules/users/index.ts +12 -0
- package/template/src/modules/users/schemas/users.schema.ts +23 -0
- package/template/src/modules/users/stores/useUsersStore.ts +60 -0
- package/template/src/stores/index.ts +1 -0
- package/template/src/stores/useUiStore.ts +55 -0
- package/template/src/types/api.ts +28 -0
- package/template/src/types/index.ts +2 -0
- package/template/src/types/table.ts +34 -0
- package/template/supabase/config.toml +94 -0
- package/template/tsconfig.json +2 -1
- package/template/tsconfig.tsbuildinfo +1 -0
- package/template/next-env.d.ts +0 -6
- package/template/package-lock.json +0 -8454
- package/template/src/app/dashboard/page.tsx +0 -111
- package/template/src/components/dashboard/widget.tsx +0 -113
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createServerClient, type CookieOptions } from '@supabase/ssr'
|
|
2
|
+
import { NextResponse, type NextRequest } from 'next/server'
|
|
3
|
+
|
|
4
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
|
|
5
|
+
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY
|
|
6
|
+
|
|
7
|
+
type CookieToSet = {
|
|
8
|
+
name: string
|
|
9
|
+
value: string
|
|
10
|
+
options: CookieOptions
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function updateSession(request: NextRequest) {
|
|
14
|
+
let supabaseResponse = NextResponse.next({
|
|
15
|
+
request: {
|
|
16
|
+
headers: request.headers,
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const supabase = createServerClient(
|
|
21
|
+
supabaseUrl!,
|
|
22
|
+
supabaseKey!,
|
|
23
|
+
{
|
|
24
|
+
cookies: {
|
|
25
|
+
getAll() {
|
|
26
|
+
return request.cookies.getAll()
|
|
27
|
+
},
|
|
28
|
+
setAll(cookiesToSet: CookieToSet[]) {
|
|
29
|
+
cookiesToSet.forEach(({ name, value }) =>
|
|
30
|
+
request.cookies.set(name, value)
|
|
31
|
+
)
|
|
32
|
+
supabaseResponse = NextResponse.next({
|
|
33
|
+
request,
|
|
34
|
+
})
|
|
35
|
+
cookiesToSet.forEach(({ name, value, options }) =>
|
|
36
|
+
supabaseResponse.cookies.set(name, value, options)
|
|
37
|
+
)
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
// Refresh session if expired
|
|
44
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
45
|
+
|
|
46
|
+
// Define route protection
|
|
47
|
+
const pathname = request.nextUrl.pathname
|
|
48
|
+
|
|
49
|
+
const protectedPaths = ['/dashboard', '/users', '/settings', '/reports']
|
|
50
|
+
const publicOnlyPaths = ['/login', '/register']
|
|
51
|
+
|
|
52
|
+
const isProtectedPath = protectedPaths.some((path) => pathname.startsWith(path))
|
|
53
|
+
const isPublicOnlyPath = publicOnlyPaths.some((path) => pathname.startsWith(path))
|
|
54
|
+
|
|
55
|
+
// Redirect logic
|
|
56
|
+
if (!user && isProtectedPath) {
|
|
57
|
+
const redirectUrl = new URL('/login', request.url)
|
|
58
|
+
redirectUrl.searchParams.set('redirectTo', pathname)
|
|
59
|
+
return NextResponse.redirect(redirectUrl)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (user && isPublicOnlyPath) {
|
|
63
|
+
return NextResponse.redirect(new URL('/dashboard', request.url))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return supabaseResponse
|
|
67
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { createServerClient } from '@supabase/ssr'
|
|
1
|
+
import { createServerClient, type CookieOptions } from '@supabase/ssr'
|
|
2
2
|
import { cookies } from 'next/headers'
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
|
|
5
|
+
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY
|
|
4
6
|
|
|
5
7
|
type CookieToSet = {
|
|
6
8
|
name: string
|
|
@@ -12,8 +14,8 @@ export async function createClient() {
|
|
|
12
14
|
const cookieStore = await cookies()
|
|
13
15
|
|
|
14
16
|
return createServerClient(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
supabaseUrl!,
|
|
18
|
+
supabaseKey!,
|
|
17
19
|
{
|
|
18
20
|
cookies: {
|
|
19
21
|
getAll() {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type Json =
|
|
2
|
+
| string
|
|
3
|
+
| number
|
|
4
|
+
| boolean
|
|
5
|
+
| null
|
|
6
|
+
| { [key: string]: Json | undefined }
|
|
7
|
+
| Json[]
|
|
8
|
+
|
|
9
|
+
export interface Database {
|
|
10
|
+
public: {
|
|
11
|
+
Tables: {
|
|
12
|
+
users: {
|
|
13
|
+
Row: {
|
|
14
|
+
id: string
|
|
15
|
+
email: string
|
|
16
|
+
name: string
|
|
17
|
+
role: 'admin' | 'user' | 'viewer'
|
|
18
|
+
avatar_url: string | null
|
|
19
|
+
created_at: string
|
|
20
|
+
updated_at: string
|
|
21
|
+
}
|
|
22
|
+
Insert: {
|
|
23
|
+
id?: string
|
|
24
|
+
email: string
|
|
25
|
+
name: string
|
|
26
|
+
role?: 'admin' | 'user' | 'viewer'
|
|
27
|
+
avatar_url?: string | null
|
|
28
|
+
created_at?: string
|
|
29
|
+
updated_at?: string
|
|
30
|
+
}
|
|
31
|
+
Update: {
|
|
32
|
+
id?: string
|
|
33
|
+
email?: string
|
|
34
|
+
name?: string
|
|
35
|
+
role?: 'admin' | 'user' | 'viewer'
|
|
36
|
+
avatar_url?: string | null
|
|
37
|
+
created_at?: string
|
|
38
|
+
updated_at?: string
|
|
39
|
+
}
|
|
40
|
+
Relationships: []
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
Views: {
|
|
44
|
+
[_ in never]: never
|
|
45
|
+
}
|
|
46
|
+
Functions: {
|
|
47
|
+
[_ in never]: never
|
|
48
|
+
}
|
|
49
|
+
Enums: {
|
|
50
|
+
user_role: 'admin' | 'user' | 'viewer'
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
// Common reusable schemas
|
|
4
|
+
|
|
5
|
+
export const emailSchema = z
|
|
6
|
+
.string()
|
|
7
|
+
.min(1, 'El email es requerido')
|
|
8
|
+
.email('Email inválido')
|
|
9
|
+
|
|
10
|
+
export const passwordSchema = z
|
|
11
|
+
.string()
|
|
12
|
+
.min(1, 'La contraseña es requerida')
|
|
13
|
+
.min(6, 'La contraseña debe tener al menos 6 caracteres')
|
|
14
|
+
.max(100, 'La contraseña no puede tener más de 100 caracteres')
|
|
15
|
+
|
|
16
|
+
export const phoneSchema = z
|
|
17
|
+
.string()
|
|
18
|
+
.min(1, 'El teléfono es requerido')
|
|
19
|
+
.regex(/^[+]?[\d\s-()]+$/, 'Teléfono inválido')
|
|
20
|
+
.min(10, 'El teléfono debe tener al menos 10 dígitos')
|
|
21
|
+
|
|
22
|
+
export const urlSchema = z
|
|
23
|
+
.string()
|
|
24
|
+
.url('URL inválida')
|
|
25
|
+
.or(z.literal(''))
|
|
26
|
+
|
|
27
|
+
export const uuidSchema = z
|
|
28
|
+
.string()
|
|
29
|
+
.uuid('ID inválido')
|
|
30
|
+
|
|
31
|
+
export const dateSchema = z
|
|
32
|
+
.string()
|
|
33
|
+
.datetime({ message: 'Fecha inválida' })
|
|
34
|
+
|
|
35
|
+
export const positiveNumberSchema = z
|
|
36
|
+
.number()
|
|
37
|
+
.positive('El número debe ser positivo')
|
|
38
|
+
|
|
39
|
+
export const nonNegativeNumberSchema = z
|
|
40
|
+
.number()
|
|
41
|
+
.nonnegative('El número no puede ser negativo')
|
|
42
|
+
|
|
43
|
+
export const percentageSchema = z
|
|
44
|
+
.number()
|
|
45
|
+
.min(0, 'El porcentaje no puede ser menor a 0')
|
|
46
|
+
.max(100, 'El porcentaje no puede ser mayor a 100')
|
|
47
|
+
|
|
48
|
+
export const currencySchema = z
|
|
49
|
+
.number()
|
|
50
|
+
.nonnegative('El monto no puede ser negativo')
|
|
51
|
+
.multipleOf(0.01, 'El monto debe tener máximo 2 decimales')
|
|
52
|
+
|
|
53
|
+
// Pagination schemas
|
|
54
|
+
export const paginationSchema = z.object({
|
|
55
|
+
page: z.number().int().positive().default(1),
|
|
56
|
+
pageSize: z.number().int().positive().max(100).default(10),
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
export const sortSchema = z.object({
|
|
60
|
+
sortBy: z.string().optional(),
|
|
61
|
+
sortOrder: z.enum(['asc', 'desc']).default('asc'),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// Search schema
|
|
65
|
+
export const searchSchema = z.object({
|
|
66
|
+
query: z.string().optional(),
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// Combined query params schema
|
|
70
|
+
export const queryParamsSchema = paginationSchema.merge(sortSchema).merge(searchSchema)
|
|
71
|
+
|
|
72
|
+
export type PaginationInput = z.infer<typeof paginationSchema>
|
|
73
|
+
export type SortInput = z.infer<typeof sortSchema>
|
|
74
|
+
export type SearchInput = z.infer<typeof searchSchema>
|
|
75
|
+
export type QueryParamsInput = z.infer<typeof queryParamsSchema>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export {
|
|
2
|
+
emailSchema,
|
|
3
|
+
passwordSchema,
|
|
4
|
+
phoneSchema,
|
|
5
|
+
urlSchema,
|
|
6
|
+
uuidSchema,
|
|
7
|
+
dateSchema,
|
|
8
|
+
positiveNumberSchema,
|
|
9
|
+
nonNegativeNumberSchema,
|
|
10
|
+
percentageSchema,
|
|
11
|
+
currencySchema,
|
|
12
|
+
paginationSchema,
|
|
13
|
+
sortSchema,
|
|
14
|
+
searchSchema,
|
|
15
|
+
queryParamsSchema,
|
|
16
|
+
type PaginationInput,
|
|
17
|
+
type SortInput,
|
|
18
|
+
type SearchInput,
|
|
19
|
+
type QueryParamsInput,
|
|
20
|
+
} from './common'
|
|
@@ -1,12 +1,59 @@
|
|
|
1
1
|
'use server'
|
|
2
2
|
|
|
3
|
+
import { createClient } from '@/lib/supabase/server'
|
|
3
4
|
import { redirect } from 'next/navigation'
|
|
5
|
+
import { loginSchema, registerSchema, type LoginInput, type RegisterInput } from '../schemas/auth.schema'
|
|
6
|
+
|
|
7
|
+
export async function login(input: LoginInput) {
|
|
8
|
+
const parsed = loginSchema.safeParse(input)
|
|
9
|
+
|
|
10
|
+
if (!parsed.success) {
|
|
11
|
+
return { error: parsed.error.errors[0].message }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const supabase = await createClient()
|
|
15
|
+
|
|
16
|
+
const { error } = await supabase.auth.signInWithPassword({
|
|
17
|
+
email: parsed.data.email,
|
|
18
|
+
password: parsed.data.password,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
if (error) {
|
|
22
|
+
return { error: error.message }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
redirect('/dashboard')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function register(input: RegisterInput) {
|
|
29
|
+
const parsed = registerSchema.safeParse(input)
|
|
30
|
+
|
|
31
|
+
if (!parsed.success) {
|
|
32
|
+
return { error: parsed.error.errors[0].message }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const supabase = await createClient()
|
|
36
|
+
|
|
37
|
+
const { error } = await supabase.auth.signUp({
|
|
38
|
+
email: parsed.data.email,
|
|
39
|
+
password: parsed.data.password,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
if (error) {
|
|
43
|
+
return { error: error.message }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
redirect('/dashboard')
|
|
47
|
+
}
|
|
4
48
|
|
|
5
49
|
export async function logout() {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
// await supabase.auth.signOut()
|
|
50
|
+
const supabase = await createClient()
|
|
51
|
+
await supabase.auth.signOut()
|
|
9
52
|
redirect('/login')
|
|
10
53
|
}
|
|
11
54
|
|
|
12
|
-
export
|
|
55
|
+
export async function getUser() {
|
|
56
|
+
const supabase = await createClient()
|
|
57
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
58
|
+
return user
|
|
59
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useForm } from 'react-hook-form'
|
|
4
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
5
|
+
import { toast } from 'sonner'
|
|
6
|
+
import { Button } from '@/components/ui/button'
|
|
7
|
+
import { Input } from '@/components/ui/input'
|
|
8
|
+
import { Label } from '@/components/ui/label'
|
|
9
|
+
import { useAuth } from '../hooks/useAuth'
|
|
10
|
+
import { loginSchema, type LoginInput } from '../schemas/auth.schema'
|
|
11
|
+
|
|
12
|
+
export function LoginForm() {
|
|
13
|
+
const { login, isLoggingIn } = useAuth()
|
|
14
|
+
|
|
15
|
+
const form = useForm<LoginInput>({
|
|
16
|
+
resolver: zodResolver(loginSchema),
|
|
17
|
+
defaultValues: {
|
|
18
|
+
email: '',
|
|
19
|
+
password: '',
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
async function onSubmit(data: LoginInput) {
|
|
24
|
+
const result = await login(data)
|
|
25
|
+
|
|
26
|
+
if (result?.error) {
|
|
27
|
+
toast.error(result.error)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
33
|
+
<div className="space-y-2">
|
|
34
|
+
<Label htmlFor="email">Email</Label>
|
|
35
|
+
<Input
|
|
36
|
+
id="email"
|
|
37
|
+
type="email"
|
|
38
|
+
placeholder="tu@email.com"
|
|
39
|
+
{...form.register('email')}
|
|
40
|
+
/>
|
|
41
|
+
{form.formState.errors.email && (
|
|
42
|
+
<p className="text-sm text-destructive">
|
|
43
|
+
{form.formState.errors.email.message}
|
|
44
|
+
</p>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div className="space-y-2">
|
|
49
|
+
<Label htmlFor="password">Contraseña</Label>
|
|
50
|
+
<Input
|
|
51
|
+
id="password"
|
|
52
|
+
type="password"
|
|
53
|
+
placeholder="••••••••"
|
|
54
|
+
{...form.register('password')}
|
|
55
|
+
/>
|
|
56
|
+
{form.formState.errors.password && (
|
|
57
|
+
<p className="text-sm text-destructive">
|
|
58
|
+
{form.formState.errors.password.message}
|
|
59
|
+
</p>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<Button type="submit" className="w-full" disabled={isLoggingIn}>
|
|
64
|
+
{isLoggingIn ? 'Iniciando sesión...' : 'Iniciar sesión'}
|
|
65
|
+
</Button>
|
|
66
|
+
</form>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
4
|
+
import { useAuthStore } from '../stores/useAuthStore'
|
|
5
|
+
import { useAuthQueries } from './useAuthQueries'
|
|
6
|
+
import { useAuthMutations } from './useAuthMutations'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Hook unificado para autenticación.
|
|
10
|
+
* Los componentes SOLO deben importar este hook, nunca useAuthQueries o useAuthMutations directamente.
|
|
11
|
+
*/
|
|
12
|
+
export function useAuth() {
|
|
13
|
+
const state = useAuthStore(useShallow((s) => s.state))
|
|
14
|
+
const actions = useAuthStore(useShallow((s) => s.actions))
|
|
15
|
+
|
|
16
|
+
const queries = useAuthQueries()
|
|
17
|
+
const mutations = useAuthMutations()
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
// State from store (UI state)
|
|
21
|
+
...state,
|
|
22
|
+
...actions,
|
|
23
|
+
|
|
24
|
+
// Queries
|
|
25
|
+
user: queries.user,
|
|
26
|
+
session: queries.session,
|
|
27
|
+
isAuthenticated: queries.isAuthenticated,
|
|
28
|
+
isLoadingUser: queries.isLoading,
|
|
29
|
+
|
|
30
|
+
// Mutations
|
|
31
|
+
login: mutations.login,
|
|
32
|
+
register: mutations.register,
|
|
33
|
+
logout: mutations.logout,
|
|
34
|
+
isLoggingIn: mutations.isLoggingIn,
|
|
35
|
+
isRegistering: mutations.isRegistering,
|
|
36
|
+
isLoggingOut: mutations.isLoggingOut,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
4
|
+
import { login, register, logout } from '../actions/auth-actions'
|
|
5
|
+
import { authKeys } from './useAuthQueries'
|
|
6
|
+
import type { LoginInput, RegisterInput } from '../schemas/auth.schema'
|
|
7
|
+
|
|
8
|
+
export function useAuthMutations() {
|
|
9
|
+
const queryClient = useQueryClient()
|
|
10
|
+
|
|
11
|
+
const loginMutation = useMutation({
|
|
12
|
+
mutationFn: (input: LoginInput) => login(input),
|
|
13
|
+
onSuccess: () => {
|
|
14
|
+
queryClient.invalidateQueries({ queryKey: authKeys.all })
|
|
15
|
+
},
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const registerMutation = useMutation({
|
|
19
|
+
mutationFn: (input: RegisterInput) => register(input),
|
|
20
|
+
onSuccess: () => {
|
|
21
|
+
queryClient.invalidateQueries({ queryKey: authKeys.all })
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const logoutMutation = useMutation({
|
|
26
|
+
mutationFn: () => logout(),
|
|
27
|
+
onSuccess: () => {
|
|
28
|
+
queryClient.invalidateQueries({ queryKey: authKeys.all })
|
|
29
|
+
queryClient.clear()
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
login: loginMutation.mutateAsync,
|
|
35
|
+
register: registerMutation.mutateAsync,
|
|
36
|
+
logout: logoutMutation.mutateAsync,
|
|
37
|
+
isLoggingIn: loginMutation.isPending,
|
|
38
|
+
isRegistering: registerMutation.isPending,
|
|
39
|
+
isLoggingOut: logoutMutation.isPending,
|
|
40
|
+
loginError: loginMutation.error,
|
|
41
|
+
registerError: registerMutation.error,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useQuery } from '@tanstack/react-query'
|
|
4
|
+
import { createClient } from '@/lib/supabase/client'
|
|
5
|
+
|
|
6
|
+
export const authKeys = {
|
|
7
|
+
all: ['auth'] as const,
|
|
8
|
+
user: () => [...authKeys.all, 'user'] as const,
|
|
9
|
+
session: () => [...authKeys.all, 'session'] as const,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useAuthQueries() {
|
|
13
|
+
const supabase = createClient()
|
|
14
|
+
|
|
15
|
+
const userQuery = useQuery({
|
|
16
|
+
queryKey: authKeys.user(),
|
|
17
|
+
queryFn: async () => {
|
|
18
|
+
const { data: { user }, error } = await supabase.auth.getUser()
|
|
19
|
+
if (error) throw error
|
|
20
|
+
return user
|
|
21
|
+
},
|
|
22
|
+
staleTime: 1000 * 60 * 5, // 5 minutos
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const sessionQuery = useQuery({
|
|
26
|
+
queryKey: authKeys.session(),
|
|
27
|
+
queryFn: async () => {
|
|
28
|
+
const { data: { session }, error } = await supabase.auth.getSession()
|
|
29
|
+
if (error) throw error
|
|
30
|
+
return session
|
|
31
|
+
},
|
|
32
|
+
staleTime: 1000 * 60 * 5,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
user: userQuery.data,
|
|
37
|
+
session: sessionQuery.data,
|
|
38
|
+
isLoading: userQuery.isLoading || sessionQuery.isLoading,
|
|
39
|
+
isAuthenticated: !!userQuery.data,
|
|
40
|
+
userQuery,
|
|
41
|
+
sessionQuery,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Components
|
|
2
|
+
export { LoginForm } from './components/login-form'
|
|
3
|
+
|
|
4
|
+
// Hooks - Solo exportar el hook unificado
|
|
5
|
+
export { useAuth } from './hooks/useAuth'
|
|
6
|
+
|
|
7
|
+
// Schemas
|
|
8
|
+
export { loginSchema, registerSchema } from './schemas/auth.schema'
|
|
9
|
+
export type { LoginInput, RegisterInput } from './schemas/auth.schema'
|
|
10
|
+
|
|
11
|
+
// Actions
|
|
12
|
+
export { login, register, logout, getUser } from './actions/auth-actions'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export const loginSchema = z.object({
|
|
4
|
+
email: z
|
|
5
|
+
.string()
|
|
6
|
+
.min(1, 'El email es requerido')
|
|
7
|
+
.email('Email inválido'),
|
|
8
|
+
password: z
|
|
9
|
+
.string()
|
|
10
|
+
.min(1, 'La contraseña es requerida')
|
|
11
|
+
.min(6, 'La contraseña debe tener al menos 6 caracteres'),
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export const registerSchema = z.object({
|
|
15
|
+
email: z
|
|
16
|
+
.string()
|
|
17
|
+
.min(1, 'El email es requerido')
|
|
18
|
+
.email('Email inválido'),
|
|
19
|
+
password: z
|
|
20
|
+
.string()
|
|
21
|
+
.min(1, 'La contraseña es requerida')
|
|
22
|
+
.min(6, 'La contraseña debe tener al menos 6 caracteres'),
|
|
23
|
+
confirmPassword: z
|
|
24
|
+
.string()
|
|
25
|
+
.min(1, 'Confirma tu contraseña'),
|
|
26
|
+
}).refine((data) => data.password === data.confirmPassword, {
|
|
27
|
+
message: 'Las contraseñas no coinciden',
|
|
28
|
+
path: ['confirmPassword'],
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
export type LoginInput = z.infer<typeof loginSchema>
|
|
32
|
+
export type RegisterInput = z.infer<typeof registerSchema>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { create } from 'zustand'
|
|
2
|
+
import type { User } from '@supabase/supabase-js'
|
|
3
|
+
|
|
4
|
+
interface AuthState {
|
|
5
|
+
state: {
|
|
6
|
+
user: User | null
|
|
7
|
+
isLoading: boolean
|
|
8
|
+
}
|
|
9
|
+
actions: {
|
|
10
|
+
setUser: (user: User | null) => void
|
|
11
|
+
setLoading: (isLoading: boolean) => void
|
|
12
|
+
reset: () => void
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const initialState = {
|
|
17
|
+
user: null,
|
|
18
|
+
isLoading: true,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const useAuthStore = create<AuthState>()((set) => ({
|
|
22
|
+
state: initialState,
|
|
23
|
+
actions: {
|
|
24
|
+
setUser: (user) =>
|
|
25
|
+
set((state) => ({
|
|
26
|
+
state: { ...state.state, user },
|
|
27
|
+
})),
|
|
28
|
+
setLoading: (isLoading) =>
|
|
29
|
+
set((state) => ({
|
|
30
|
+
state: { ...state.state, isLoading },
|
|
31
|
+
})),
|
|
32
|
+
reset: () =>
|
|
33
|
+
set(() => ({
|
|
34
|
+
state: initialState,
|
|
35
|
+
})),
|
|
36
|
+
},
|
|
37
|
+
}))
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import { createClient } from '@/lib/supabase/server'
|
|
4
|
+
import { createUserSchema, updateUserSchema, type CreateUserInput, type UpdateUserInput } from '../schemas/users.schema'
|
|
5
|
+
|
|
6
|
+
export async function getUsers() {
|
|
7
|
+
const supabase = await createClient()
|
|
8
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
9
|
+
|
|
10
|
+
if (!user) throw new Error('Unauthorized')
|
|
11
|
+
|
|
12
|
+
const { data, error } = await supabase
|
|
13
|
+
.from('users')
|
|
14
|
+
.select('*')
|
|
15
|
+
.order('created_at', { ascending: false })
|
|
16
|
+
|
|
17
|
+
if (error) throw error
|
|
18
|
+
return data
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function getUserById(id: string) {
|
|
22
|
+
const supabase = await createClient()
|
|
23
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
24
|
+
|
|
25
|
+
if (!user) throw new Error('Unauthorized')
|
|
26
|
+
|
|
27
|
+
const { data, error } = await supabase
|
|
28
|
+
.from('users')
|
|
29
|
+
.select('*')
|
|
30
|
+
.eq('id', id)
|
|
31
|
+
.single()
|
|
32
|
+
|
|
33
|
+
if (error) throw error
|
|
34
|
+
return data
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function createUser(input: CreateUserInput) {
|
|
38
|
+
const parsed = createUserSchema.safeParse(input)
|
|
39
|
+
|
|
40
|
+
if (!parsed.success) {
|
|
41
|
+
throw new Error(parsed.error.errors[0].message)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const supabase = await createClient()
|
|
45
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
46
|
+
|
|
47
|
+
if (!user) throw new Error('Unauthorized')
|
|
48
|
+
|
|
49
|
+
const { data, error } = await supabase
|
|
50
|
+
.from('users')
|
|
51
|
+
.insert(parsed.data)
|
|
52
|
+
.select()
|
|
53
|
+
.single()
|
|
54
|
+
|
|
55
|
+
if (error) throw error
|
|
56
|
+
return data
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function updateUser(id: string, input: UpdateUserInput) {
|
|
60
|
+
const parsed = updateUserSchema.safeParse(input)
|
|
61
|
+
|
|
62
|
+
if (!parsed.success) {
|
|
63
|
+
throw new Error(parsed.error.errors[0].message)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const supabase = await createClient()
|
|
67
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
68
|
+
|
|
69
|
+
if (!user) throw new Error('Unauthorized')
|
|
70
|
+
|
|
71
|
+
const { data, error } = await supabase
|
|
72
|
+
.from('users')
|
|
73
|
+
.update(parsed.data)
|
|
74
|
+
.eq('id', id)
|
|
75
|
+
.select()
|
|
76
|
+
.single()
|
|
77
|
+
|
|
78
|
+
if (error) throw error
|
|
79
|
+
return data
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function deleteUser(id: string) {
|
|
83
|
+
const supabase = await createClient()
|
|
84
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
85
|
+
|
|
86
|
+
if (!user) throw new Error('Unauthorized')
|
|
87
|
+
|
|
88
|
+
const { error } = await supabase
|
|
89
|
+
.from('users')
|
|
90
|
+
.delete()
|
|
91
|
+
.eq('id', id)
|
|
92
|
+
|
|
93
|
+
if (error) throw error
|
|
94
|
+
}
|