@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.
Files changed (133) hide show
  1. package/README.md +549 -549
  2. package/package.json +48 -48
  3. package/template/.claude/skills/anti-patterns.md +150 -0
  4. package/template/.claude/skills/drizzle-schema.md +178 -0
  5. package/template/.claude/skills/formatting.md +56 -0
  6. package/template/.claude/skills/module-architecture.md +143 -0
  7. package/template/.claude/skills/supabase-server-actions.md +199 -0
  8. package/template/.claude/skills/ui-patterns.md +161 -0
  9. package/template/CLAUDE.md +114 -1239
  10. package/template/drizzle.config.ts +12 -12
  11. package/template/eslint.config.mjs +16 -16
  12. package/template/gitignore +36 -36
  13. package/template/next.config.ts +7 -7
  14. package/template/package.json +86 -86
  15. package/template/postcss.config.mjs +7 -7
  16. package/template/proxy.ts +12 -12
  17. package/template/public/logolft.svg +11 -11
  18. package/template/src/app/(auth)/dashboard/dashboard-content.tsx +124 -124
  19. package/template/src/app/(auth)/dashboard/page.tsx +9 -9
  20. package/template/src/app/(auth)/layout.tsx +7 -7
  21. package/template/src/app/(auth)/users/page.tsx +9 -9
  22. package/template/src/app/(auth)/users/users-content.tsx +26 -26
  23. package/template/src/app/(public)/layout.tsx +7 -7
  24. package/template/src/app/(public)/login/page.tsx +17 -17
  25. package/template/src/app/api/webhooks/route.ts +20 -20
  26. package/template/src/app/globals.css +249 -249
  27. package/template/src/app/layout.tsx +37 -37
  28. package/template/src/app/page.tsx +5 -5
  29. package/template/src/app/providers.tsx +27 -27
  30. package/template/src/components/layout/main-content.tsx +28 -28
  31. package/template/src/components/layout/sidebar-context.tsx +33 -33
  32. package/template/src/components/layout/sidebar.tsx +141 -141
  33. package/template/src/components/tables/data-table-column-header.tsx +68 -68
  34. package/template/src/components/tables/data-table-date-filter.tsx +203 -203
  35. package/template/src/components/tables/data-table-faceted-filter.tsx +185 -185
  36. package/template/src/components/tables/data-table-filters-dropdown.tsx +130 -130
  37. package/template/src/components/tables/data-table-number-filter.tsx +295 -295
  38. package/template/src/components/tables/data-table-pagination.tsx +99 -99
  39. package/template/src/components/tables/data-table-toolbar.tsx +140 -140
  40. package/template/src/components/tables/data-table-view-options.tsx +63 -63
  41. package/template/src/components/tables/data-table.tsx +148 -148
  42. package/template/src/components/tables/index.ts +9 -9
  43. package/template/src/components/ui/accordion.tsx +58 -58
  44. package/template/src/components/ui/alert-dialog.tsx +165 -165
  45. package/template/src/components/ui/alert.tsx +66 -66
  46. package/template/src/components/ui/animations/index.ts +44 -44
  47. package/template/src/components/ui/avatar.tsx +55 -55
  48. package/template/src/components/ui/badge.tsx +50 -50
  49. package/template/src/components/ui/button.tsx +118 -118
  50. package/template/src/components/ui/calendar.tsx +220 -220
  51. package/template/src/components/ui/card.tsx +113 -113
  52. package/template/src/components/ui/checkbox.tsx +38 -38
  53. package/template/src/components/ui/collapsible.tsx +33 -33
  54. package/template/src/components/ui/command.tsx +196 -196
  55. package/template/src/components/ui/dialog.tsx +156 -156
  56. package/template/src/components/ui/dropdown-menu.tsx +280 -280
  57. package/template/src/components/ui/form.tsx +171 -171
  58. package/template/src/components/ui/icons.tsx +167 -167
  59. package/template/src/components/ui/input.tsx +28 -28
  60. package/template/src/components/ui/label.tsx +25 -25
  61. package/template/src/components/ui/motion.tsx +197 -197
  62. package/template/src/components/ui/page-transition.tsx +166 -166
  63. package/template/src/components/ui/popover.tsx +59 -59
  64. package/template/src/components/ui/progress.tsx +32 -32
  65. package/template/src/components/ui/radio-group.tsx +45 -45
  66. package/template/src/components/ui/scroll-area.tsx +63 -63
  67. package/template/src/components/ui/select.tsx +208 -208
  68. package/template/src/components/ui/separator.tsx +28 -28
  69. package/template/src/components/ui/sheet.tsx +170 -170
  70. package/template/src/components/ui/sidebar.tsx +726 -726
  71. package/template/src/components/ui/skeleton.tsx +15 -15
  72. package/template/src/components/ui/slider.tsx +58 -58
  73. package/template/src/components/ui/sonner.tsx +47 -47
  74. package/template/src/components/ui/spinner.tsx +27 -27
  75. package/template/src/components/ui/submit-button.tsx +47 -47
  76. package/template/src/components/ui/switch.tsx +31 -31
  77. package/template/src/components/ui/table.tsx +120 -120
  78. package/template/src/components/ui/tabs.tsx +75 -75
  79. package/template/src/components/ui/textarea.tsx +26 -26
  80. package/template/src/components/ui/tooltip.tsx +70 -70
  81. package/template/src/config/navigation.ts +59 -59
  82. package/template/src/config/roles.ts +27 -27
  83. package/template/src/config/site.ts +12 -12
  84. package/template/src/db/index.ts +12 -12
  85. package/template/src/db/schema/index.ts +1 -1
  86. package/template/src/db/schema/users.ts +16 -16
  87. package/template/src/db/seed.ts +39 -39
  88. package/template/src/hooks/index.ts +3 -3
  89. package/template/src/hooks/use-mobile.ts +21 -21
  90. package/template/src/hooks/useDataTable.ts +82 -82
  91. package/template/src/hooks/useDebounce.ts +49 -49
  92. package/template/src/hooks/useMediaQuery.ts +36 -36
  93. package/template/src/lib/date/config.ts +36 -36
  94. package/template/src/lib/date/formatters.ts +127 -127
  95. package/template/src/lib/date/index.ts +26 -26
  96. package/template/src/lib/excel/exporter.ts +89 -89
  97. package/template/src/lib/excel/index.ts +14 -14
  98. package/template/src/lib/excel/parser.ts +96 -96
  99. package/template/src/lib/query-client.ts +35 -35
  100. package/template/src/lib/supabase/admin.ts +23 -23
  101. package/template/src/lib/supabase/client.ts +11 -11
  102. package/template/src/lib/supabase/proxy.ts +67 -67
  103. package/template/src/lib/supabase/server.ts +38 -38
  104. package/template/src/lib/supabase/types.ts +53 -53
  105. package/template/src/lib/utils.ts +6 -6
  106. package/template/src/lib/validations/common.ts +75 -75
  107. package/template/src/lib/validations/index.ts +20 -20
  108. package/template/src/modules/auth/actions/auth-actions.ts +59 -59
  109. package/template/src/modules/auth/components/login-form.tsx +68 -68
  110. package/template/src/modules/auth/hooks/useAuth.ts +38 -38
  111. package/template/src/modules/auth/hooks/useAuthMutations.ts +43 -43
  112. package/template/src/modules/auth/hooks/useAuthQueries.ts +43 -43
  113. package/template/src/modules/auth/index.ts +12 -12
  114. package/template/src/modules/auth/schemas/auth.schema.ts +32 -32
  115. package/template/src/modules/auth/stores/useAuthStore.ts +37 -37
  116. package/template/src/modules/users/actions/users-actions.ts +166 -166
  117. package/template/src/modules/users/columns.tsx +106 -106
  118. package/template/src/modules/users/components/users-list.tsx +48 -48
  119. package/template/src/modules/users/hooks/useUsers.ts +39 -39
  120. package/template/src/modules/users/hooks/useUsersMutations.ts +55 -55
  121. package/template/src/modules/users/hooks/useUsersQueries.ts +35 -35
  122. package/template/src/modules/users/index.ts +30 -30
  123. package/template/src/modules/users/schemas/users.schema.ts +51 -51
  124. package/template/src/modules/users/stores/useUsersStore.ts +60 -60
  125. package/template/src/modules/users/types/auth-user.types.ts +42 -42
  126. package/template/src/modules/users/utils/user-mapper.ts +32 -32
  127. package/template/src/stores/index.ts +1 -1
  128. package/template/src/stores/useUiStore.ts +55 -55
  129. package/template/src/types/api.ts +28 -28
  130. package/template/src/types/index.ts +2 -2
  131. package/template/src/types/table.ts +34 -34
  132. package/template/supabase/config.toml +94 -94
  133. 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
+ }