@create-lft-app/nextjs 3.2.0 → 3.3.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/README.md +549 -549
- package/package.json +48 -48
- package/template/.claude/skills/anti-patterns.md +150 -0
- package/template/.claude/skills/drizzle-schema.md +178 -0
- package/template/.claude/skills/formatting.md +56 -0
- package/template/.claude/skills/module-architecture.md +143 -0
- package/template/.claude/skills/supabase-server-actions.md +199 -0
- package/template/.claude/skills/ui-patterns.md +161 -0
- package/template/CLAUDE.md +114 -1239
- package/template/drizzle.config.ts +12 -12
- package/template/eslint.config.mjs +16 -16
- package/template/gitignore +36 -36
- package/template/next.config.ts +7 -7
- package/template/package.json +86 -86
- package/template/postcss.config.mjs +7 -7
- package/template/proxy.ts +12 -12
- package/template/public/logolft.svg +11 -11
- package/template/src/app/(auth)/dashboard/dashboard-content.tsx +124 -124
- package/template/src/app/(auth)/dashboard/page.tsx +9 -9
- package/template/src/app/(auth)/layout.tsx +7 -7
- package/template/src/app/(auth)/users/page.tsx +9 -9
- package/template/src/app/(auth)/users/users-content.tsx +26 -26
- package/template/src/app/(public)/layout.tsx +7 -7
- package/template/src/app/(public)/login/page.tsx +17 -17
- package/template/src/app/api/webhooks/route.ts +20 -20
- package/template/src/app/globals.css +249 -249
- package/template/src/app/layout.tsx +37 -37
- package/template/src/app/page.tsx +5 -5
- package/template/src/app/providers.tsx +27 -27
- package/template/src/components/layout/main-content.tsx +28 -28
- package/template/src/components/layout/sidebar-context.tsx +33 -33
- package/template/src/components/layout/sidebar.tsx +141 -141
- package/template/src/components/tables/data-table-column-header.tsx +68 -68
- package/template/src/components/tables/data-table-date-filter.tsx +203 -203
- package/template/src/components/tables/data-table-faceted-filter.tsx +185 -185
- package/template/src/components/tables/data-table-filters-dropdown.tsx +130 -130
- package/template/src/components/tables/data-table-number-filter.tsx +295 -295
- package/template/src/components/tables/data-table-pagination.tsx +99 -99
- package/template/src/components/tables/data-table-toolbar.tsx +140 -140
- package/template/src/components/tables/data-table-view-options.tsx +63 -63
- package/template/src/components/tables/data-table.tsx +148 -148
- package/template/src/components/tables/index.ts +9 -9
- package/template/src/components/ui/accordion.tsx +58 -58
- package/template/src/components/ui/alert-dialog.tsx +165 -165
- package/template/src/components/ui/alert.tsx +66 -66
- package/template/src/components/ui/animations/index.ts +44 -44
- package/template/src/components/ui/avatar.tsx +55 -55
- package/template/src/components/ui/badge.tsx +50 -50
- package/template/src/components/ui/button.tsx +118 -118
- package/template/src/components/ui/calendar.tsx +220 -220
- package/template/src/components/ui/card.tsx +113 -113
- package/template/src/components/ui/checkbox.tsx +38 -38
- package/template/src/components/ui/collapsible.tsx +33 -33
- package/template/src/components/ui/command.tsx +196 -196
- package/template/src/components/ui/dialog.tsx +156 -156
- package/template/src/components/ui/dropdown-menu.tsx +280 -280
- package/template/src/components/ui/form.tsx +171 -171
- package/template/src/components/ui/icons.tsx +167 -167
- package/template/src/components/ui/input.tsx +28 -28
- package/template/src/components/ui/label.tsx +25 -25
- package/template/src/components/ui/motion.tsx +197 -197
- package/template/src/components/ui/page-transition.tsx +166 -166
- package/template/src/components/ui/popover.tsx +59 -59
- package/template/src/components/ui/progress.tsx +32 -32
- package/template/src/components/ui/radio-group.tsx +45 -45
- package/template/src/components/ui/scroll-area.tsx +63 -63
- package/template/src/components/ui/select.tsx +208 -208
- package/template/src/components/ui/separator.tsx +28 -28
- package/template/src/components/ui/sheet.tsx +170 -170
- package/template/src/components/ui/sidebar.tsx +726 -726
- package/template/src/components/ui/skeleton.tsx +15 -15
- package/template/src/components/ui/slider.tsx +58 -58
- package/template/src/components/ui/sonner.tsx +47 -47
- package/template/src/components/ui/spinner.tsx +27 -27
- package/template/src/components/ui/submit-button.tsx +47 -47
- package/template/src/components/ui/switch.tsx +31 -31
- package/template/src/components/ui/table.tsx +120 -120
- package/template/src/components/ui/tabs.tsx +75 -75
- package/template/src/components/ui/textarea.tsx +26 -26
- package/template/src/components/ui/tooltip.tsx +70 -70
- package/template/src/config/navigation.ts +59 -59
- package/template/src/config/roles.ts +27 -27
- package/template/src/config/site.ts +12 -12
- package/template/src/db/index.ts +12 -12
- package/template/src/db/schema/index.ts +1 -1
- package/template/src/db/schema/users.ts +16 -16
- package/template/src/db/seed.ts +39 -39
- package/template/src/hooks/index.ts +3 -3
- package/template/src/hooks/use-mobile.ts +21 -21
- package/template/src/hooks/useDataTable.ts +82 -82
- package/template/src/hooks/useDebounce.ts +49 -49
- package/template/src/hooks/useMediaQuery.ts +36 -36
- package/template/src/lib/date/config.ts +36 -36
- package/template/src/lib/date/formatters.ts +127 -127
- package/template/src/lib/date/index.ts +26 -26
- package/template/src/lib/excel/exporter.ts +89 -89
- package/template/src/lib/excel/index.ts +14 -14
- package/template/src/lib/excel/parser.ts +96 -96
- package/template/src/lib/query-client.ts +35 -35
- package/template/src/lib/supabase/admin.ts +23 -23
- package/template/src/lib/supabase/client.ts +11 -11
- package/template/src/lib/supabase/proxy.ts +67 -67
- package/template/src/lib/supabase/server.ts +38 -38
- package/template/src/lib/supabase/types.ts +53 -53
- package/template/src/lib/utils.ts +6 -6
- package/template/src/lib/validations/common.ts +75 -75
- package/template/src/lib/validations/index.ts +20 -20
- package/template/src/modules/auth/actions/auth-actions.ts +59 -59
- package/template/src/modules/auth/components/login-form.tsx +68 -68
- package/template/src/modules/auth/hooks/useAuth.ts +38 -38
- package/template/src/modules/auth/hooks/useAuthMutations.ts +43 -43
- package/template/src/modules/auth/hooks/useAuthQueries.ts +43 -43
- package/template/src/modules/auth/index.ts +12 -12
- package/template/src/modules/auth/schemas/auth.schema.ts +32 -32
- package/template/src/modules/auth/stores/useAuthStore.ts +37 -37
- package/template/src/modules/users/actions/users-actions.ts +166 -166
- package/template/src/modules/users/columns.tsx +106 -106
- package/template/src/modules/users/components/users-list.tsx +48 -48
- package/template/src/modules/users/hooks/useUsers.ts +39 -39
- package/template/src/modules/users/hooks/useUsersMutations.ts +55 -55
- package/template/src/modules/users/hooks/useUsersQueries.ts +35 -35
- package/template/src/modules/users/index.ts +30 -30
- package/template/src/modules/users/schemas/users.schema.ts +51 -51
- package/template/src/modules/users/stores/useUsersStore.ts +60 -60
- package/template/src/modules/users/types/auth-user.types.ts +42 -42
- package/template/src/modules/users/utils/user-mapper.ts +32 -32
- package/template/src/stores/index.ts +1 -1
- package/template/src/stores/useUiStore.ts +55 -55
- package/template/src/types/api.ts +28 -28
- package/template/src/types/index.ts +2 -2
- package/template/src/types/table.ts +34 -34
- package/template/supabase/config.toml +94 -94
- package/template/tsconfig.json +42 -42
|
@@ -1,43 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,12 +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'
|
|
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'
|
|
@@ -1,32 +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>
|
|
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>
|
|
@@ -1,37 +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
|
-
}))
|
|
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
|
+
}))
|
|
@@ -1,166 +1,166 @@
|
|
|
1
|
-
'use server'
|
|
2
|
-
|
|
3
|
-
import { createClient } from '@/lib/supabase/server'
|
|
4
|
-
import { createAdminClient } from '@/lib/supabase/admin'
|
|
5
|
-
import {
|
|
6
|
-
createAuthUserSchema,
|
|
7
|
-
updateAuthUserSchema,
|
|
8
|
-
type CreateAuthUserInput,
|
|
9
|
-
type UpdateAuthUserInput,
|
|
10
|
-
} from '../schemas/users.schema'
|
|
11
|
-
import { mapAuthUserToUser, mapAuthUsersToUsers } from '../utils/user-mapper'
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Obtiene todos los usuarios desde auth.users
|
|
15
|
-
*/
|
|
16
|
-
export async function getUsers() {
|
|
17
|
-
const supabase = await createClient()
|
|
18
|
-
const { data: { user } } = await supabase.auth.getUser()
|
|
19
|
-
|
|
20
|
-
if (!user) throw new Error('Unauthorized')
|
|
21
|
-
|
|
22
|
-
const adminClient = createAdminClient()
|
|
23
|
-
const { data, error } = await adminClient.auth.admin.listUsers()
|
|
24
|
-
|
|
25
|
-
if (error) throw error
|
|
26
|
-
|
|
27
|
-
return mapAuthUsersToUsers(data.users)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Obtiene un usuario por ID desde auth.users
|
|
32
|
-
*/
|
|
33
|
-
export async function getUserById(id: string) {
|
|
34
|
-
const supabase = await createClient()
|
|
35
|
-
const { data: { user } } = await supabase.auth.getUser()
|
|
36
|
-
|
|
37
|
-
if (!user) throw new Error('Unauthorized')
|
|
38
|
-
|
|
39
|
-
const adminClient = createAdminClient()
|
|
40
|
-
const { data, error } = await adminClient.auth.admin.getUserById(id)
|
|
41
|
-
|
|
42
|
-
if (error) throw error
|
|
43
|
-
|
|
44
|
-
return mapAuthUserToUser(data.user)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Crea un nuevo usuario en auth.users
|
|
49
|
-
*/
|
|
50
|
-
export async function createUser(input: CreateAuthUserInput) {
|
|
51
|
-
const parsed = createAuthUserSchema.safeParse(input)
|
|
52
|
-
|
|
53
|
-
if (!parsed.success) {
|
|
54
|
-
throw new Error(parsed.error.errors[0].message)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const supabase = await createClient()
|
|
58
|
-
const { data: { user } } = await supabase.auth.getUser()
|
|
59
|
-
|
|
60
|
-
if (!user) throw new Error('Unauthorized')
|
|
61
|
-
|
|
62
|
-
const adminClient = createAdminClient()
|
|
63
|
-
|
|
64
|
-
const { data, error } = await adminClient.auth.admin.createUser({
|
|
65
|
-
email: parsed.data.email,
|
|
66
|
-
password: parsed.data.password,
|
|
67
|
-
email_confirm: !parsed.data.send_invite,
|
|
68
|
-
app_metadata: {
|
|
69
|
-
role: parsed.data.role,
|
|
70
|
-
},
|
|
71
|
-
user_metadata: {
|
|
72
|
-
name: parsed.data.name,
|
|
73
|
-
avatar_url: parsed.data.avatar_url ?? null,
|
|
74
|
-
},
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
if (error) throw error
|
|
78
|
-
|
|
79
|
-
// Enviar email de invitación si se solicita y no se proporcionó password
|
|
80
|
-
if (parsed.data.send_invite && !parsed.data.password) {
|
|
81
|
-
await adminClient.auth.admin.inviteUserByEmail(parsed.data.email)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return mapAuthUserToUser(data.user)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Actualiza un usuario en auth.users
|
|
89
|
-
*/
|
|
90
|
-
export async function updateUser(id: string, input: UpdateAuthUserInput) {
|
|
91
|
-
const parsed = updateAuthUserSchema.safeParse(input)
|
|
92
|
-
|
|
93
|
-
if (!parsed.success) {
|
|
94
|
-
throw new Error(parsed.error.errors[0].message)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const supabase = await createClient()
|
|
98
|
-
const { data: { user } } = await supabase.auth.getUser()
|
|
99
|
-
|
|
100
|
-
if (!user) throw new Error('Unauthorized')
|
|
101
|
-
|
|
102
|
-
const adminClient = createAdminClient()
|
|
103
|
-
|
|
104
|
-
// Obtener usuario actual para merge de metadata
|
|
105
|
-
const { data: currentUser, error: fetchError } = await adminClient.auth.admin.getUserById(id)
|
|
106
|
-
|
|
107
|
-
if (fetchError) throw fetchError
|
|
108
|
-
if (!currentUser.user) throw new Error('User not found')
|
|
109
|
-
|
|
110
|
-
const updatePayload: Parameters<typeof adminClient.auth.admin.updateUserById>[1] = {}
|
|
111
|
-
|
|
112
|
-
if (parsed.data.email) {
|
|
113
|
-
updatePayload.email = parsed.data.email
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (parsed.data.password) {
|
|
117
|
-
updatePayload.password = parsed.data.password
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Merge app_metadata (solo role)
|
|
121
|
-
if (parsed.data.role) {
|
|
122
|
-
updatePayload.app_metadata = {
|
|
123
|
-
...currentUser.user.app_metadata,
|
|
124
|
-
role: parsed.data.role,
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Merge user_metadata
|
|
129
|
-
const hasUserMetadataChanges =
|
|
130
|
-
parsed.data.name !== undefined ||
|
|
131
|
-
parsed.data.avatar_url !== undefined
|
|
132
|
-
|
|
133
|
-
if (hasUserMetadataChanges) {
|
|
134
|
-
updatePayload.user_metadata = {
|
|
135
|
-
...currentUser.user.user_metadata,
|
|
136
|
-
...(parsed.data.name !== undefined && { name: parsed.data.name }),
|
|
137
|
-
...(parsed.data.avatar_url !== undefined && { avatar_url: parsed.data.avatar_url }),
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const { data, error } = await adminClient.auth.admin.updateUserById(id, updatePayload)
|
|
142
|
-
|
|
143
|
-
if (error) throw error
|
|
144
|
-
|
|
145
|
-
return mapAuthUserToUser(data.user)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Elimina un usuario de auth.users
|
|
150
|
-
*/
|
|
151
|
-
export async function deleteUser(id: string) {
|
|
152
|
-
const supabase = await createClient()
|
|
153
|
-
const { data: { user } } = await supabase.auth.getUser()
|
|
154
|
-
|
|
155
|
-
if (!user) throw new Error('Unauthorized')
|
|
156
|
-
|
|
157
|
-
// Prevenir auto-eliminación
|
|
158
|
-
if (user.id === id) {
|
|
159
|
-
throw new Error('No puedes eliminar tu propio usuario')
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const adminClient = createAdminClient()
|
|
163
|
-
const { error } = await adminClient.auth.admin.deleteUser(id)
|
|
164
|
-
|
|
165
|
-
if (error) throw error
|
|
166
|
-
}
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import { createClient } from '@/lib/supabase/server'
|
|
4
|
+
import { createAdminClient } from '@/lib/supabase/admin'
|
|
5
|
+
import {
|
|
6
|
+
createAuthUserSchema,
|
|
7
|
+
updateAuthUserSchema,
|
|
8
|
+
type CreateAuthUserInput,
|
|
9
|
+
type UpdateAuthUserInput,
|
|
10
|
+
} from '../schemas/users.schema'
|
|
11
|
+
import { mapAuthUserToUser, mapAuthUsersToUsers } from '../utils/user-mapper'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Obtiene todos los usuarios desde auth.users
|
|
15
|
+
*/
|
|
16
|
+
export async function getUsers() {
|
|
17
|
+
const supabase = await createClient()
|
|
18
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
19
|
+
|
|
20
|
+
if (!user) throw new Error('Unauthorized')
|
|
21
|
+
|
|
22
|
+
const adminClient = createAdminClient()
|
|
23
|
+
const { data, error } = await adminClient.auth.admin.listUsers()
|
|
24
|
+
|
|
25
|
+
if (error) throw error
|
|
26
|
+
|
|
27
|
+
return mapAuthUsersToUsers(data.users)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Obtiene un usuario por ID desde auth.users
|
|
32
|
+
*/
|
|
33
|
+
export async function getUserById(id: string) {
|
|
34
|
+
const supabase = await createClient()
|
|
35
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
36
|
+
|
|
37
|
+
if (!user) throw new Error('Unauthorized')
|
|
38
|
+
|
|
39
|
+
const adminClient = createAdminClient()
|
|
40
|
+
const { data, error } = await adminClient.auth.admin.getUserById(id)
|
|
41
|
+
|
|
42
|
+
if (error) throw error
|
|
43
|
+
|
|
44
|
+
return mapAuthUserToUser(data.user)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Crea un nuevo usuario en auth.users
|
|
49
|
+
*/
|
|
50
|
+
export async function createUser(input: CreateAuthUserInput) {
|
|
51
|
+
const parsed = createAuthUserSchema.safeParse(input)
|
|
52
|
+
|
|
53
|
+
if (!parsed.success) {
|
|
54
|
+
throw new Error(parsed.error.errors[0].message)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const supabase = await createClient()
|
|
58
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
59
|
+
|
|
60
|
+
if (!user) throw new Error('Unauthorized')
|
|
61
|
+
|
|
62
|
+
const adminClient = createAdminClient()
|
|
63
|
+
|
|
64
|
+
const { data, error } = await adminClient.auth.admin.createUser({
|
|
65
|
+
email: parsed.data.email,
|
|
66
|
+
password: parsed.data.password,
|
|
67
|
+
email_confirm: !parsed.data.send_invite,
|
|
68
|
+
app_metadata: {
|
|
69
|
+
role: parsed.data.role,
|
|
70
|
+
},
|
|
71
|
+
user_metadata: {
|
|
72
|
+
name: parsed.data.name,
|
|
73
|
+
avatar_url: parsed.data.avatar_url ?? null,
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
if (error) throw error
|
|
78
|
+
|
|
79
|
+
// Enviar email de invitación si se solicita y no se proporcionó password
|
|
80
|
+
if (parsed.data.send_invite && !parsed.data.password) {
|
|
81
|
+
await adminClient.auth.admin.inviteUserByEmail(parsed.data.email)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return mapAuthUserToUser(data.user)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Actualiza un usuario en auth.users
|
|
89
|
+
*/
|
|
90
|
+
export async function updateUser(id: string, input: UpdateAuthUserInput) {
|
|
91
|
+
const parsed = updateAuthUserSchema.safeParse(input)
|
|
92
|
+
|
|
93
|
+
if (!parsed.success) {
|
|
94
|
+
throw new Error(parsed.error.errors[0].message)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const supabase = await createClient()
|
|
98
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
99
|
+
|
|
100
|
+
if (!user) throw new Error('Unauthorized')
|
|
101
|
+
|
|
102
|
+
const adminClient = createAdminClient()
|
|
103
|
+
|
|
104
|
+
// Obtener usuario actual para merge de metadata
|
|
105
|
+
const { data: currentUser, error: fetchError } = await adminClient.auth.admin.getUserById(id)
|
|
106
|
+
|
|
107
|
+
if (fetchError) throw fetchError
|
|
108
|
+
if (!currentUser.user) throw new Error('User not found')
|
|
109
|
+
|
|
110
|
+
const updatePayload: Parameters<typeof adminClient.auth.admin.updateUserById>[1] = {}
|
|
111
|
+
|
|
112
|
+
if (parsed.data.email) {
|
|
113
|
+
updatePayload.email = parsed.data.email
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (parsed.data.password) {
|
|
117
|
+
updatePayload.password = parsed.data.password
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Merge app_metadata (solo role)
|
|
121
|
+
if (parsed.data.role) {
|
|
122
|
+
updatePayload.app_metadata = {
|
|
123
|
+
...currentUser.user.app_metadata,
|
|
124
|
+
role: parsed.data.role,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Merge user_metadata
|
|
129
|
+
const hasUserMetadataChanges =
|
|
130
|
+
parsed.data.name !== undefined ||
|
|
131
|
+
parsed.data.avatar_url !== undefined
|
|
132
|
+
|
|
133
|
+
if (hasUserMetadataChanges) {
|
|
134
|
+
updatePayload.user_metadata = {
|
|
135
|
+
...currentUser.user.user_metadata,
|
|
136
|
+
...(parsed.data.name !== undefined && { name: parsed.data.name }),
|
|
137
|
+
...(parsed.data.avatar_url !== undefined && { avatar_url: parsed.data.avatar_url }),
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const { data, error } = await adminClient.auth.admin.updateUserById(id, updatePayload)
|
|
142
|
+
|
|
143
|
+
if (error) throw error
|
|
144
|
+
|
|
145
|
+
return mapAuthUserToUser(data.user)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Elimina un usuario de auth.users
|
|
150
|
+
*/
|
|
151
|
+
export async function deleteUser(id: string) {
|
|
152
|
+
const supabase = await createClient()
|
|
153
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
154
|
+
|
|
155
|
+
if (!user) throw new Error('Unauthorized')
|
|
156
|
+
|
|
157
|
+
// Prevenir auto-eliminación
|
|
158
|
+
if (user.id === id) {
|
|
159
|
+
throw new Error('No puedes eliminar tu propio usuario')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const adminClient = createAdminClient()
|
|
163
|
+
const { error } = await adminClient.auth.admin.deleteUser(id)
|
|
164
|
+
|
|
165
|
+
if (error) throw error
|
|
166
|
+
}
|