@create-lft-app/nextjs 3.1.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/package.json +1 -1
- 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 +74 -239
- package/template/src/components/layout/sidebar.tsx +4 -9
- package/template/src/components/tables/data-table-date-filter.tsx +203 -0
- package/template/src/components/tables/data-table-faceted-filter.tsx +185 -0
- package/template/src/components/tables/data-table-filters-dropdown.tsx +130 -0
- package/template/src/components/tables/data-table-number-filter.tsx +295 -0
- package/template/src/components/tables/data-table-toolbar.tsx +115 -25
- package/template/src/components/tables/data-table-view-options.tsx +10 -6
- package/template/src/components/tables/data-table.tsx +41 -21
- package/template/src/components/tables/index.ts +5 -1
- package/template/src/components/ui/alert-dialog.tsx +1 -1
- package/template/src/components/ui/avatar.tsx +2 -2
- package/template/src/components/ui/badge.tsx +2 -2
- package/template/src/components/ui/button.tsx +1 -1
- package/template/src/components/ui/card.tsx +1 -1
- package/template/src/components/ui/command.tsx +4 -4
- package/template/src/components/ui/dropdown-menu.tsx +4 -4
- package/template/src/components/ui/form.tsx +1 -1
- package/template/src/components/ui/icons.tsx +1 -1
- package/template/src/components/ui/popover.tsx +1 -1
- package/template/src/components/ui/progress.tsx +1 -1
- package/template/src/components/ui/select.tsx +3 -3
- package/template/src/components/ui/sonner.tsx +1 -1
- package/template/src/components/ui/spinner.tsx +1 -1
- package/template/src/components/ui/table.tsx +3 -3
- package/template/src/components/ui/tooltip.tsx +2 -2
- package/template/src/config/navigation.ts +1 -11
- package/template/src/config/roles.ts +27 -0
- package/template/src/lib/date/config.ts +4 -2
- package/template/src/lib/date/formatters.ts +7 -0
- package/template/src/lib/date/index.ts +8 -1
- package/template/src/lib/supabase/admin.ts +23 -0
- package/template/src/lib/supabase/proxy.ts +1 -1
- package/template/src/modules/users/actions/users-actions.ts +106 -34
- package/template/src/modules/users/columns.tsx +29 -9
- package/template/src/modules/users/components/users-list.tsx +27 -1
- package/template/src/modules/users/hooks/useUsersMutations.ts +3 -3
- package/template/src/modules/users/index.ts +20 -2
- package/template/src/modules/users/schemas/users.schema.ts +29 -1
- package/template/src/modules/users/types/auth-user.types.ts +42 -0
- package/template/src/modules/users/utils/user-mapper.ts +32 -0
- package/template/tsconfig.tsbuildinfo +1 -1
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
dayjs,
|
|
3
|
+
setDefaultTimezone,
|
|
4
|
+
DEFAULT_LOCALE,
|
|
5
|
+
DEFAULT_TIMEZONE,
|
|
6
|
+
DEFAULT_CURRENCY,
|
|
7
|
+
} from './config'
|
|
2
8
|
export {
|
|
3
9
|
formatDate,
|
|
4
10
|
formatDateLong,
|
|
11
|
+
formatDateShort,
|
|
5
12
|
formatTime,
|
|
6
13
|
formatTimeWithSeconds,
|
|
7
14
|
formatDateTime,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js'
|
|
2
|
+
|
|
3
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
|
|
4
|
+
const secretKey = process.env.SUPABASE_SECRET_DEFAULT_KEY!
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Cliente admin para operaciones que requieren privilegios elevados.
|
|
8
|
+
* Usa la nueva Secret Key de Supabase (sb_secret_...).
|
|
9
|
+
* SOLO usar en server-side (actions, API routes).
|
|
10
|
+
* NUNCA exponer en el cliente.
|
|
11
|
+
*/
|
|
12
|
+
export function createAdminClient() {
|
|
13
|
+
if (!secretKey) {
|
|
14
|
+
throw new Error('SUPABASE_SECRET_DEFAULT_KEY is not defined')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return createClient(supabaseUrl, secretKey, {
|
|
18
|
+
auth: {
|
|
19
|
+
autoRefreshToken: false,
|
|
20
|
+
persistSession: false,
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
}
|
|
@@ -46,7 +46,7 @@ export async function updateSession(request: NextRequest) {
|
|
|
46
46
|
// Define route protection
|
|
47
47
|
const pathname = request.nextUrl.pathname
|
|
48
48
|
|
|
49
|
-
const protectedPaths = ['/dashboard', '/users', '/
|
|
49
|
+
const protectedPaths = ['/dashboard', '/users', '/reports']
|
|
50
50
|
const publicOnlyPaths = ['/login', '/register']
|
|
51
51
|
|
|
52
52
|
const isProtectedPath = protectedPaths.some((path) => pathname.startsWith(path))
|
|
@@ -1,41 +1,54 @@
|
|
|
1
1
|
'use server'
|
|
2
2
|
|
|
3
3
|
import { createClient } from '@/lib/supabase/server'
|
|
4
|
-
import {
|
|
5
|
-
|
|
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
|
+
*/
|
|
6
16
|
export async function getUsers() {
|
|
7
17
|
const supabase = await createClient()
|
|
8
18
|
const { data: { user } } = await supabase.auth.getUser()
|
|
9
19
|
|
|
10
20
|
if (!user) throw new Error('Unauthorized')
|
|
11
21
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
.select('*')
|
|
15
|
-
.order('created_at', { ascending: false })
|
|
22
|
+
const adminClient = createAdminClient()
|
|
23
|
+
const { data, error } = await adminClient.auth.admin.listUsers()
|
|
16
24
|
|
|
17
25
|
if (error) throw error
|
|
18
|
-
|
|
26
|
+
|
|
27
|
+
return mapAuthUsersToUsers(data.users)
|
|
19
28
|
}
|
|
20
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Obtiene un usuario por ID desde auth.users
|
|
32
|
+
*/
|
|
21
33
|
export async function getUserById(id: string) {
|
|
22
34
|
const supabase = await createClient()
|
|
23
35
|
const { data: { user } } = await supabase.auth.getUser()
|
|
24
36
|
|
|
25
37
|
if (!user) throw new Error('Unauthorized')
|
|
26
38
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
.select('*')
|
|
30
|
-
.eq('id', id)
|
|
31
|
-
.single()
|
|
39
|
+
const adminClient = createAdminClient()
|
|
40
|
+
const { data, error } = await adminClient.auth.admin.getUserById(id)
|
|
32
41
|
|
|
33
42
|
if (error) throw error
|
|
34
|
-
|
|
43
|
+
|
|
44
|
+
return mapAuthUserToUser(data.user)
|
|
35
45
|
}
|
|
36
46
|
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Crea un nuevo usuario en auth.users
|
|
49
|
+
*/
|
|
50
|
+
export async function createUser(input: CreateAuthUserInput) {
|
|
51
|
+
const parsed = createAuthUserSchema.safeParse(input)
|
|
39
52
|
|
|
40
53
|
if (!parsed.success) {
|
|
41
54
|
throw new Error(parsed.error.errors[0].message)
|
|
@@ -46,18 +59,36 @@ export async function createUser(input: CreateUserInput) {
|
|
|
46
59
|
|
|
47
60
|
if (!user) throw new Error('Unauthorized')
|
|
48
61
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
.
|
|
53
|
-
.
|
|
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
|
+
})
|
|
54
76
|
|
|
55
77
|
if (error) throw error
|
|
56
|
-
|
|
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)
|
|
57
85
|
}
|
|
58
86
|
|
|
59
|
-
|
|
60
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Actualiza un usuario en auth.users
|
|
89
|
+
*/
|
|
90
|
+
export async function updateUser(id: string, input: UpdateAuthUserInput) {
|
|
91
|
+
const parsed = updateAuthUserSchema.safeParse(input)
|
|
61
92
|
|
|
62
93
|
if (!parsed.success) {
|
|
63
94
|
throw new Error(parsed.error.errors[0].message)
|
|
@@ -68,27 +99,68 @@ export async function updateUser(id: string, input: UpdateUserInput) {
|
|
|
68
99
|
|
|
69
100
|
if (!user) throw new Error('Unauthorized')
|
|
70
101
|
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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)
|
|
77
142
|
|
|
78
143
|
if (error) throw error
|
|
79
|
-
|
|
144
|
+
|
|
145
|
+
return mapAuthUserToUser(data.user)
|
|
80
146
|
}
|
|
81
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Elimina un usuario de auth.users
|
|
150
|
+
*/
|
|
82
151
|
export async function deleteUser(id: string) {
|
|
83
152
|
const supabase = await createClient()
|
|
84
153
|
const { data: { user } } = await supabase.auth.getUser()
|
|
85
154
|
|
|
86
155
|
if (!user) throw new Error('Unauthorized')
|
|
87
156
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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)
|
|
92
164
|
|
|
93
165
|
if (error) throw error
|
|
94
166
|
}
|
|
@@ -12,7 +12,12 @@ import {
|
|
|
12
12
|
DropdownMenuTrigger,
|
|
13
13
|
} from '@/components/ui/dropdown-menu'
|
|
14
14
|
import { DataTableColumnHeader } from '@/components/tables/data-table-column-header'
|
|
15
|
+
import { dateRangeFilterFn } from '@/components/tables/data-table-date-filter'
|
|
16
|
+
// numberRangeFilterFn disponible para campos numéricos: import { numberRangeFilterFn } from '@/components/tables/data-table-number-filter'
|
|
17
|
+
import { formatDate } from '@/lib/date'
|
|
18
|
+
import { ROLE_LABELS } from '@/config/roles'
|
|
15
19
|
import type { User } from './schemas/users.schema'
|
|
20
|
+
import type { UserRole } from '@/config/roles'
|
|
16
21
|
|
|
17
22
|
export const columns: ColumnDef<User>[] = [
|
|
18
23
|
{
|
|
@@ -33,13 +38,11 @@ export const columns: ColumnDef<User>[] = [
|
|
|
33
38
|
<DataTableColumnHeader column={column} title="Rol" />
|
|
34
39
|
),
|
|
35
40
|
cell: ({ row }) => {
|
|
36
|
-
const role = row.getValue('role') as
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
return labels[role] ?? role
|
|
41
|
+
const role = row.getValue('role') as UserRole
|
|
42
|
+
return ROLE_LABELS[role] ?? role
|
|
43
|
+
},
|
|
44
|
+
filterFn: (row, id, value) => {
|
|
45
|
+
return value.includes(row.getValue(id))
|
|
43
46
|
},
|
|
44
47
|
},
|
|
45
48
|
{
|
|
@@ -48,10 +51,27 @@ export const columns: ColumnDef<User>[] = [
|
|
|
48
51
|
<DataTableColumnHeader column={column} title="Creado" />
|
|
49
52
|
),
|
|
50
53
|
cell: ({ row }) => {
|
|
51
|
-
|
|
52
|
-
return date.toLocaleDateString('es-ES')
|
|
54
|
+
return formatDate(row.getValue('created_at'))
|
|
53
55
|
},
|
|
56
|
+
filterFn: dateRangeFilterFn,
|
|
54
57
|
},
|
|
58
|
+
// Ejemplo de columna numérica con filtro de rango:
|
|
59
|
+
// import { DEFAULT_LOCALE, DEFAULT_CURRENCY } from '@/lib/date'
|
|
60
|
+
// {
|
|
61
|
+
// accessorKey: 'balance',
|
|
62
|
+
// header: ({ column }) => (
|
|
63
|
+
// <DataTableColumnHeader column={column} title="Balance" />
|
|
64
|
+
// ),
|
|
65
|
+
// cell: ({ row }) => {
|
|
66
|
+
// const balance = row.getValue('balance') as number | undefined
|
|
67
|
+
// if (balance === undefined || balance === null) return '-'
|
|
68
|
+
// return new Intl.NumberFormat(DEFAULT_LOCALE, {
|
|
69
|
+
// style: 'currency',
|
|
70
|
+
// currency: DEFAULT_CURRENCY,
|
|
71
|
+
// }).format(balance)
|
|
72
|
+
// },
|
|
73
|
+
// filterFn: numberRangeFilterFn,
|
|
74
|
+
// },
|
|
55
75
|
{
|
|
56
76
|
id: 'actions',
|
|
57
77
|
cell: ({ row }) => {
|
|
@@ -1,9 +1,33 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { DataTable } from '@/components/tables/data-table'
|
|
3
|
+
import { DataTable, type FilterConfig } from '@/components/tables/data-table'
|
|
4
|
+
import { ROLE_OPTIONS } from '@/config/roles'
|
|
4
5
|
import { useUsers } from '../hooks/useUsers'
|
|
5
6
|
import { columns } from '../columns'
|
|
6
7
|
|
|
8
|
+
// Configuración de filtros para la tabla de usuarios
|
|
9
|
+
const filters: FilterConfig[] = [
|
|
10
|
+
{
|
|
11
|
+
columnId: 'role',
|
|
12
|
+
type: 'faceted',
|
|
13
|
+
title: 'Rol',
|
|
14
|
+
options: ROLE_OPTIONS,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
columnId: 'created_at',
|
|
18
|
+
type: 'date-range',
|
|
19
|
+
title: 'Fecha',
|
|
20
|
+
},
|
|
21
|
+
// Ejemplo de filtro numérico (rango):
|
|
22
|
+
// {
|
|
23
|
+
// columnId: 'balance',
|
|
24
|
+
// type: 'number-range',
|
|
25
|
+
// title: 'Balance',
|
|
26
|
+
// format: 'currency',
|
|
27
|
+
// currencySymbol: '$',
|
|
28
|
+
// },
|
|
29
|
+
]
|
|
30
|
+
|
|
7
31
|
export function UsersList() {
|
|
8
32
|
const { users, isLoading } = useUsers()
|
|
9
33
|
|
|
@@ -17,6 +41,8 @@ export function UsersList() {
|
|
|
17
41
|
data={users}
|
|
18
42
|
searchKey="name"
|
|
19
43
|
searchPlaceholder="Buscar por nombre..."
|
|
44
|
+
filters={filters}
|
|
45
|
+
filterMode="inline"
|
|
20
46
|
/>
|
|
21
47
|
)
|
|
22
48
|
}
|
|
@@ -4,13 +4,13 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
|
4
4
|
import { toast } from 'sonner'
|
|
5
5
|
import { createUser, updateUser, deleteUser } from '../actions/users-actions'
|
|
6
6
|
import { usersKeys } from './useUsersQueries'
|
|
7
|
-
import type {
|
|
7
|
+
import type { CreateAuthUserInput, UpdateAuthUserInput } from '../schemas/users.schema'
|
|
8
8
|
|
|
9
9
|
export function useUsersMutations() {
|
|
10
10
|
const queryClient = useQueryClient()
|
|
11
11
|
|
|
12
12
|
const createMutation = useMutation({
|
|
13
|
-
mutationFn: (input:
|
|
13
|
+
mutationFn: (input: CreateAuthUserInput) => createUser(input),
|
|
14
14
|
onSuccess: () => {
|
|
15
15
|
queryClient.invalidateQueries({ queryKey: usersKeys.lists() })
|
|
16
16
|
toast.success('Usuario creado correctamente')
|
|
@@ -21,7 +21,7 @@ export function useUsersMutations() {
|
|
|
21
21
|
})
|
|
22
22
|
|
|
23
23
|
const updateMutation = useMutation({
|
|
24
|
-
mutationFn: ({ id, input }: { id: string; input:
|
|
24
|
+
mutationFn: ({ id, input }: { id: string; input: UpdateAuthUserInput }) =>
|
|
25
25
|
updateUser(id, input),
|
|
26
26
|
onSuccess: (_, { id }) => {
|
|
27
27
|
queryClient.invalidateQueries({ queryKey: usersKeys.lists() })
|
|
@@ -5,8 +5,26 @@ export { UsersList } from './components/users-list'
|
|
|
5
5
|
export { useUsers } from './hooks/useUsers'
|
|
6
6
|
|
|
7
7
|
// Schemas
|
|
8
|
-
export {
|
|
9
|
-
|
|
8
|
+
export {
|
|
9
|
+
userSchema,
|
|
10
|
+
createUserSchema,
|
|
11
|
+
updateUserSchema,
|
|
12
|
+
createAuthUserSchema,
|
|
13
|
+
updateAuthUserSchema,
|
|
14
|
+
} from './schemas/users.schema'
|
|
15
|
+
export type {
|
|
16
|
+
User,
|
|
17
|
+
CreateUserInput,
|
|
18
|
+
UpdateUserInput,
|
|
19
|
+
CreateAuthUserInput,
|
|
20
|
+
UpdateAuthUserInput,
|
|
21
|
+
} from './schemas/users.schema'
|
|
22
|
+
|
|
23
|
+
// Types - Auth User types
|
|
24
|
+
export type { UserRole, UserAppMetadata, UserMetadata } from './types/auth-user.types'
|
|
25
|
+
|
|
26
|
+
// Utils - Mappers
|
|
27
|
+
export { mapAuthUserToUser, mapAuthUsersToUsers } from './utils/user-mapper'
|
|
10
28
|
|
|
11
29
|
// Columns
|
|
12
30
|
export { columns as usersColumns } from './columns'
|
|
@@ -1,15 +1,40 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
+
import { USER_ROLES, DEFAULT_ROLE } from '@/config/roles'
|
|
2
3
|
|
|
4
|
+
// Schema de rol reutilizable
|
|
5
|
+
const roleSchema = z.enum(USER_ROLES)
|
|
6
|
+
|
|
7
|
+
// Schema base del usuario (estructura que devuelve el mapper)
|
|
3
8
|
export const userSchema = z.object({
|
|
4
9
|
id: z.string().uuid(),
|
|
5
10
|
email: z.string().email(),
|
|
6
11
|
name: z.string().min(1, 'El nombre es requerido'),
|
|
7
|
-
role:
|
|
12
|
+
role: roleSchema.default(DEFAULT_ROLE),
|
|
8
13
|
avatar_url: z.string().url().nullable().optional(),
|
|
9
14
|
created_at: z.string().datetime(),
|
|
10
15
|
updated_at: z.string().datetime(),
|
|
11
16
|
})
|
|
12
17
|
|
|
18
|
+
// Schema para crear usuario via Supabase Auth Admin
|
|
19
|
+
export const createAuthUserSchema = z.object({
|
|
20
|
+
email: z.string().email('Email inválido'),
|
|
21
|
+
password: z.string().min(8, 'Mínimo 8 caracteres').optional(),
|
|
22
|
+
name: z.string().min(1, 'El nombre es requerido'),
|
|
23
|
+
role: roleSchema.default(DEFAULT_ROLE),
|
|
24
|
+
avatar_url: z.string().url().nullable().optional(),
|
|
25
|
+
send_invite: z.boolean().default(true),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// Schema para actualizar usuario via Supabase Auth Admin
|
|
29
|
+
export const updateAuthUserSchema = z.object({
|
|
30
|
+
email: z.string().email('Email inválido').optional(),
|
|
31
|
+
password: z.string().min(8, 'Mínimo 8 caracteres').optional(),
|
|
32
|
+
name: z.string().min(1, 'El nombre es requerido').optional(),
|
|
33
|
+
role: roleSchema.optional(),
|
|
34
|
+
avatar_url: z.string().url().nullable().optional(),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// Schemas legacy (mantener compatibilidad)
|
|
13
38
|
export const createUserSchema = userSchema.omit({
|
|
14
39
|
id: true,
|
|
15
40
|
created_at: true,
|
|
@@ -18,6 +43,9 @@ export const createUserSchema = userSchema.omit({
|
|
|
18
43
|
|
|
19
44
|
export const updateUserSchema = createUserSchema.partial()
|
|
20
45
|
|
|
46
|
+
// Types
|
|
21
47
|
export type User = z.infer<typeof userSchema>
|
|
22
48
|
export type CreateUserInput = z.infer<typeof createUserSchema>
|
|
23
49
|
export type UpdateUserInput = z.infer<typeof updateUserSchema>
|
|
50
|
+
export type CreateAuthUserInput = z.infer<typeof createAuthUserSchema>
|
|
51
|
+
export type UpdateAuthUserInput = z.infer<typeof updateAuthUserSchema>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { User as SupabaseAuthUser } from '@supabase/supabase-js'
|
|
2
|
+
import type { UserRole } from '@/config/roles'
|
|
3
|
+
|
|
4
|
+
// Re-exportar desde config para mantener compatibilidad
|
|
5
|
+
export type { UserRole } from '@/config/roles'
|
|
6
|
+
|
|
7
|
+
export interface UserAppMetadata {
|
|
8
|
+
role: UserRole
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface UserMetadata {
|
|
12
|
+
name: string
|
|
13
|
+
avatar_url?: string | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AuthUserWithMetadata extends SupabaseAuthUser {
|
|
17
|
+
app_metadata: UserAppMetadata
|
|
18
|
+
user_metadata: UserMetadata
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Input para crear usuario via auth.admin.createUser
|
|
23
|
+
*/
|
|
24
|
+
export interface CreateAuthUserInput {
|
|
25
|
+
email: string
|
|
26
|
+
password?: string
|
|
27
|
+
name: string
|
|
28
|
+
role?: UserRole
|
|
29
|
+
avatar_url?: string | null
|
|
30
|
+
send_invite?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Input para actualizar usuario via auth.admin.updateUserById
|
|
35
|
+
*/
|
|
36
|
+
export interface UpdateAuthUserInput {
|
|
37
|
+
email?: string
|
|
38
|
+
password?: string
|
|
39
|
+
name?: string
|
|
40
|
+
role?: UserRole
|
|
41
|
+
avatar_url?: string | null
|
|
42
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { User as SupabaseAuthUser } from '@supabase/supabase-js'
|
|
2
|
+
import type { User } from '../schemas/users.schema'
|
|
3
|
+
import { DEFAULT_ROLE, type UserRole } from '@/config/roles'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Mapea un usuario de auth.users al tipo User de la aplicación.
|
|
7
|
+
* Extrae datos de app_metadata (role) y user_metadata (name, avatar_url).
|
|
8
|
+
*/
|
|
9
|
+
export function mapAuthUserToUser(authUser: SupabaseAuthUser): User {
|
|
10
|
+
const appMetadata = authUser.app_metadata as { role?: UserRole } | undefined
|
|
11
|
+
const userMetadata = authUser.user_metadata as {
|
|
12
|
+
name?: string
|
|
13
|
+
avatar_url?: string | null
|
|
14
|
+
} | undefined
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
id: authUser.id,
|
|
18
|
+
email: authUser.email ?? '',
|
|
19
|
+
name: userMetadata?.name ?? '',
|
|
20
|
+
role: appMetadata?.role ?? DEFAULT_ROLE,
|
|
21
|
+
avatar_url: userMetadata?.avatar_url ?? null,
|
|
22
|
+
created_at: authUser.created_at ?? new Date().toISOString(),
|
|
23
|
+
updated_at: authUser.updated_at ?? new Date().toISOString(),
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Mapea array de usuarios de auth.users al tipo User[]
|
|
29
|
+
*/
|
|
30
|
+
export function mapAuthUsersToUsers(authUsers: SupabaseAuthUser[]): User[] {
|
|
31
|
+
return authUsers.map(mapAuthUserToUser)
|
|
32
|
+
}
|