feat(i18n): localize admin users backbone
This commit is contained in:
+53
-29
@@ -1,17 +1,17 @@
|
||||
"use server"
|
||||
|
||||
import { revalidatePath } from "next/cache"
|
||||
import { flattenError } from "zod"
|
||||
|
||||
import { getI18n } from "@/i18n/server"
|
||||
import {
|
||||
buildCreateUserSchema,
|
||||
buildResetUserPasswordSchema,
|
||||
buildSetUserActiveSchema,
|
||||
buildUpdateUserSchema,
|
||||
type CreateUserFormType,
|
||||
createUserSchema,
|
||||
type ResetUserPasswordFormType,
|
||||
resetUserPasswordSchema,
|
||||
type SetUserActiveFormType,
|
||||
setUserActiveSchema,
|
||||
type UpdateUserFormType,
|
||||
updateUserSchema,
|
||||
} from "@/schemas/user.schema"
|
||||
import { requireRole } from "@/services/auth.service"
|
||||
import {
|
||||
@@ -21,12 +21,14 @@ import {
|
||||
updateUserUseCase,
|
||||
} from "@/use-cases/user.use-cases"
|
||||
|
||||
import { localizeUserFieldErrors } from "./user.messages"
|
||||
|
||||
const USERS_PATH = "/admin/users"
|
||||
|
||||
export async function createUserAction(formData: CreateUserFormType) {
|
||||
await requireRole("ADMIN")
|
||||
|
||||
const validatedFields = createUserSchema.safeParse(formData)
|
||||
const { dictionary } = await getI18n()
|
||||
const copy = dictionary.admin.users
|
||||
const validatedFields = buildCreateUserSchema(copy.schema).safeParse(formData)
|
||||
|
||||
if (!validatedFields.success) {
|
||||
return {
|
||||
@@ -39,27 +41,32 @@ export async function createUserAction(formData: CreateUserFormType) {
|
||||
const result = await createUserUseCase(validatedFields.data)
|
||||
|
||||
if (!result.success) {
|
||||
return result
|
||||
return {
|
||||
...result,
|
||||
errors: localizeUserFieldErrors(result.errors, copy.actions),
|
||||
message: copy.actions.createFailure,
|
||||
}
|
||||
}
|
||||
|
||||
revalidatePath(USERS_PATH)
|
||||
|
||||
return { success: true, message: "User created successfully" }
|
||||
return { success: true, message: copy.actions.createSuccess }
|
||||
} catch (error) {
|
||||
console.error("Database error:", error)
|
||||
return { success: false, message: "Failed to create user" }
|
||||
return { success: false, message: copy.actions.createFailure }
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateUserAction(formData: UpdateUserFormType) {
|
||||
const session = await requireRole("ADMIN")
|
||||
|
||||
const validatedFields = updateUserSchema.safeParse(formData)
|
||||
const { dictionary } = await getI18n()
|
||||
const copy = dictionary.admin.users
|
||||
const validatedFields = buildUpdateUserSchema(copy.schema).safeParse(formData)
|
||||
|
||||
if (!validatedFields.success) {
|
||||
return {
|
||||
success: false,
|
||||
errors: flattenError(validatedFields.error).fieldErrors,
|
||||
errors: validatedFields.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,27 +77,34 @@ export async function updateUserAction(formData: UpdateUserFormType) {
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
return result
|
||||
return {
|
||||
...result,
|
||||
errors: localizeUserFieldErrors(result.errors, copy.actions),
|
||||
message: copy.actions.updateFailure,
|
||||
}
|
||||
}
|
||||
|
||||
revalidatePath(USERS_PATH)
|
||||
|
||||
return { success: true, message: "User updated successfully" }
|
||||
return { success: true, message: copy.actions.updateSuccess }
|
||||
} catch (error) {
|
||||
console.error("Database error:", error)
|
||||
return { success: false, message: "Failed to update user" }
|
||||
return { success: false, message: copy.actions.updateFailure }
|
||||
}
|
||||
}
|
||||
|
||||
export async function setUserActiveAction(formData: SetUserActiveFormType) {
|
||||
const session = await requireRole("ADMIN")
|
||||
|
||||
const validatedFields = setUserActiveSchema.safeParse(formData)
|
||||
const { dictionary } = await getI18n()
|
||||
const copy = dictionary.admin.users
|
||||
const validatedFields = buildSetUserActiveSchema(copy.schema).safeParse(
|
||||
formData,
|
||||
)
|
||||
|
||||
if (!validatedFields.success) {
|
||||
return {
|
||||
success: false,
|
||||
errors: flattenError(validatedFields.error).fieldErrors,
|
||||
errors: validatedFields.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,24 +115,30 @@ export async function setUserActiveAction(formData: SetUserActiveFormType) {
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
return result
|
||||
return {
|
||||
...result,
|
||||
errors: localizeUserFieldErrors(result.errors, copy.actions),
|
||||
message: copy.actions.toggleStatusFailure,
|
||||
}
|
||||
}
|
||||
|
||||
revalidatePath(USERS_PATH)
|
||||
|
||||
return { success: true, message: "User status updated successfully" }
|
||||
return { success: true, message: copy.actions.toggleStatusSuccess }
|
||||
} catch (error) {
|
||||
console.error("Database error:", error)
|
||||
return { success: false, message: "Failed to update user status" }
|
||||
return { success: false, message: copy.actions.toggleStatusFailure }
|
||||
}
|
||||
}
|
||||
|
||||
export async function resetUserPasswordAction(
|
||||
formData: ResetUserPasswordFormType,
|
||||
) {
|
||||
await requireRole("ADMIN")
|
||||
|
||||
const validatedFields = resetUserPasswordSchema.safeParse(formData)
|
||||
const { dictionary } = await getI18n()
|
||||
const copy = dictionary.admin.users
|
||||
const validatedFields = buildResetUserPasswordSchema(copy.schema).safeParse(
|
||||
formData,
|
||||
)
|
||||
|
||||
if (!validatedFields.success) {
|
||||
return {
|
||||
@@ -131,14 +151,18 @@ export async function resetUserPasswordAction(
|
||||
const result = await resetUserPasswordUseCase(validatedFields.data)
|
||||
|
||||
if (!result.success) {
|
||||
return result
|
||||
return {
|
||||
...result,
|
||||
errors: localizeUserFieldErrors(result.errors, copy.actions),
|
||||
message: copy.actions.resetPasswordFailure,
|
||||
}
|
||||
}
|
||||
|
||||
revalidatePath(USERS_PATH)
|
||||
|
||||
return { success: true, message: "Password reset successfully" }
|
||||
return { success: true, message: copy.actions.resetPasswordSuccess }
|
||||
} catch (error) {
|
||||
console.error("Database error:", error)
|
||||
return { success: false, message: "Failed to reset password" }
|
||||
return { success: false, message: copy.actions.resetPasswordFailure }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { Dictionary } from "@/i18n/dictionaries"
|
||||
|
||||
type UserActionCopy = Dictionary["admin"]["users"]["actions"]
|
||||
|
||||
type FieldErrors = Record<string, string[]>
|
||||
|
||||
const userErrorMessageKeys = {
|
||||
"Username already exists": "duplicateUsername",
|
||||
"Email already exists": "duplicateEmail",
|
||||
"User not found": "notFound",
|
||||
"Cannot remove access from the last active administrator": "lastActiveAdmin",
|
||||
"You cannot remove your own administrator access": "selfAdminAccess",
|
||||
"You cannot deactivate your own user": "selfDeactivate",
|
||||
} as const satisfies Record<string, keyof UserActionCopy>
|
||||
|
||||
function isUserErrorMessage(
|
||||
message: string,
|
||||
): message is keyof typeof userErrorMessageKeys {
|
||||
return message in userErrorMessageKeys
|
||||
}
|
||||
|
||||
function localizeUserMessage(message: string, copy: UserActionCopy): string {
|
||||
if (!isUserErrorMessage(message)) return message
|
||||
|
||||
return copy[userErrorMessageKeys[message]]
|
||||
}
|
||||
|
||||
export function localizeUserFieldErrors(
|
||||
errors: FieldErrors | undefined,
|
||||
copy: UserActionCopy,
|
||||
): FieldErrors | undefined {
|
||||
if (!errors) return undefined
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(errors).map(([field, messages]) => [
|
||||
field,
|
||||
messages.map((message) => localizeUserMessage(message, copy)),
|
||||
]),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { Dictionary } from "@/i18n/dictionaries"
|
||||
|
||||
export type UserListCopy = Dictionary["admin"]["users"]["list"]
|
||||
export type UserFormCopy = Dictionary["admin"]["users"]["form"]
|
||||
export type UserResetPasswordCopy =
|
||||
Dictionary["admin"]["users"]["resetPassword"]
|
||||
export type UserRolesCopy = Dictionary["admin"]["users"]["roles"]
|
||||
export type UserStatusCopy = Dictionary["admin"]["users"]["status"]
|
||||
export type UserActionCopy = Dictionary["admin"]["users"]["actions"]
|
||||
export type UserSchemaCopy = Dictionary["admin"]["users"]["schema"]
|
||||
export type UserFallbackCopy = Dictionary["admin"]["users"]["fallback"]
|
||||
@@ -432,6 +432,88 @@ export const en = {
|
||||
},
|
||||
},
|
||||
},
|
||||
admin: {
|
||||
users: {
|
||||
list: {
|
||||
title: "Users",
|
||||
empty: "No users found.",
|
||||
columns: {
|
||||
name: "Name",
|
||||
username: "Username",
|
||||
email: "Email",
|
||||
role: "Role",
|
||||
status: "Status",
|
||||
actions: "Actions",
|
||||
},
|
||||
actions: {
|
||||
edit: "Edit user",
|
||||
},
|
||||
},
|
||||
new: {
|
||||
title: "New User",
|
||||
},
|
||||
edit: {
|
||||
title: "Edit User",
|
||||
},
|
||||
form: {
|
||||
nameLabel: "Name",
|
||||
namePlaceholder: "Full name",
|
||||
usernameLabel: "Username",
|
||||
usernamePlaceholder: "Username",
|
||||
emailLabel: "Email",
|
||||
emailPlaceholder: "user@example.com",
|
||||
passwordLabel: "Password",
|
||||
passwordPlaceholder: "Minimum 8 characters",
|
||||
roleLabel: "Role",
|
||||
activeLabel: "Active user",
|
||||
createSubmit: "Create User",
|
||||
updateSubmit: "Update User",
|
||||
},
|
||||
resetPassword: {
|
||||
title: "Reset password",
|
||||
passwordLabel: "New password",
|
||||
passwordPlaceholder: "Minimum 8 characters",
|
||||
submit: "Reset Password",
|
||||
},
|
||||
roles: {
|
||||
ADMIN: "Admin",
|
||||
MANAGER: "Manager",
|
||||
STAFF: "Staff",
|
||||
VIEWER: "Viewer",
|
||||
},
|
||||
status: {
|
||||
active: "Active",
|
||||
inactive: "Inactive",
|
||||
},
|
||||
actions: {
|
||||
createSuccess: "User created successfully",
|
||||
createFailure: "Failed to create user",
|
||||
updateSuccess: "User updated successfully",
|
||||
updateFailure: "Failed to update user",
|
||||
toggleStatusSuccess: "User status updated successfully",
|
||||
toggleStatusFailure: "Failed to update user status",
|
||||
resetPasswordSuccess: "Password reset successfully",
|
||||
resetPasswordFailure: "Failed to reset password",
|
||||
duplicateUsername: "Username already exists",
|
||||
duplicateEmail: "Email already exists",
|
||||
notFound: "User not found",
|
||||
lastActiveAdmin:
|
||||
"Cannot remove access from the last active administrator",
|
||||
selfAdminAccess: "You cannot remove your own administrator access",
|
||||
selfDeactivate: "You cannot deactivate your own user",
|
||||
},
|
||||
schema: {
|
||||
usernameRequired: "Username is required",
|
||||
nameRequired: "Name is required",
|
||||
emailInvalid: "Invalid email",
|
||||
passwordMinLength: "Password must be at least 8 characters",
|
||||
userIdRequired: "User id is required",
|
||||
},
|
||||
fallback: {
|
||||
unknownRole: "Unknown role",
|
||||
},
|
||||
},
|
||||
},
|
||||
login: {
|
||||
title: "Sign In",
|
||||
usernameLabel: "Username",
|
||||
|
||||
@@ -437,6 +437,88 @@ export const es = {
|
||||
},
|
||||
},
|
||||
},
|
||||
admin: {
|
||||
users: {
|
||||
list: {
|
||||
title: "Usuarios",
|
||||
empty: "No se encontraron usuarios.",
|
||||
columns: {
|
||||
name: "Nombre",
|
||||
username: "Usuario",
|
||||
email: "Correo electrónico",
|
||||
role: "Rol",
|
||||
status: "Estado",
|
||||
actions: "Acciones",
|
||||
},
|
||||
actions: {
|
||||
edit: "Editar usuario",
|
||||
},
|
||||
},
|
||||
new: {
|
||||
title: "Nuevo usuario",
|
||||
},
|
||||
edit: {
|
||||
title: "Editar usuario",
|
||||
},
|
||||
form: {
|
||||
nameLabel: "Nombre",
|
||||
namePlaceholder: "Nombre completo",
|
||||
usernameLabel: "Usuario",
|
||||
usernamePlaceholder: "Usuario",
|
||||
emailLabel: "Correo electrónico",
|
||||
emailPlaceholder: "usuario@ejemplo.com",
|
||||
passwordLabel: "Contraseña",
|
||||
passwordPlaceholder: "Mínimo 8 caracteres",
|
||||
roleLabel: "Rol",
|
||||
activeLabel: "Usuario activo",
|
||||
createSubmit: "Crear usuario",
|
||||
updateSubmit: "Actualizar usuario",
|
||||
},
|
||||
resetPassword: {
|
||||
title: "Restablecer contraseña",
|
||||
passwordLabel: "Nueva contraseña",
|
||||
passwordPlaceholder: "Mínimo 8 caracteres",
|
||||
submit: "Restablecer contraseña",
|
||||
},
|
||||
roles: {
|
||||
ADMIN: "Administrador",
|
||||
MANAGER: "Gerente",
|
||||
STAFF: "Personal",
|
||||
VIEWER: "Visor",
|
||||
},
|
||||
status: {
|
||||
active: "Activo",
|
||||
inactive: "Inactivo",
|
||||
},
|
||||
actions: {
|
||||
createSuccess: "Usuario creado correctamente",
|
||||
createFailure: "Error al crear el usuario",
|
||||
updateSuccess: "Usuario actualizado correctamente",
|
||||
updateFailure: "Error al actualizar el usuario",
|
||||
toggleStatusSuccess: "Estado del usuario actualizado correctamente",
|
||||
toggleStatusFailure: "Error al actualizar el estado del usuario",
|
||||
resetPasswordSuccess: "Contraseña restablecida correctamente",
|
||||
resetPasswordFailure: "Error al restablecer la contraseña",
|
||||
duplicateUsername: "El nombre de usuario ya existe",
|
||||
duplicateEmail: "El correo electrónico ya existe",
|
||||
notFound: "Usuario no encontrado",
|
||||
lastActiveAdmin:
|
||||
"No se puede eliminar el acceso del último administrador activo",
|
||||
selfAdminAccess: "No puedes eliminar tu propio acceso de administrador",
|
||||
selfDeactivate: "No puedes desactivar tu propio usuario",
|
||||
},
|
||||
schema: {
|
||||
usernameRequired: "El usuario es obligatorio",
|
||||
nameRequired: "El nombre es obligatorio",
|
||||
emailInvalid: "Correo electrónico no válido",
|
||||
passwordMinLength: "La contraseña debe tener al menos 8 caracteres",
|
||||
userIdRequired: "El ID de usuario es obligatorio",
|
||||
},
|
||||
fallback: {
|
||||
unknownRole: "Rol desconocido",
|
||||
},
|
||||
},
|
||||
},
|
||||
login: {
|
||||
title: "Iniciar sesión",
|
||||
usernameLabel: "Usuario",
|
||||
|
||||
+59
-27
@@ -1,38 +1,70 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import type { Dictionary } from "@/i18n/dictionaries"
|
||||
|
||||
export type UserSchemaCopy = Dictionary["admin"]["users"]["schema"]
|
||||
|
||||
export const defaultUserSchemaCopy: UserSchemaCopy = {
|
||||
usernameRequired: "Username is required",
|
||||
nameRequired: "Name is required",
|
||||
emailInvalid: "Invalid email",
|
||||
passwordMinLength: "Password must be at least 8 characters",
|
||||
userIdRequired: "User id is required",
|
||||
}
|
||||
|
||||
export const userRoleSchema = z.enum(["ADMIN", "MANAGER", "STAFF", "VIEWER"])
|
||||
|
||||
const passwordSchema = z
|
||||
.string()
|
||||
.min(8, { error: "Password must be at least 8 characters" })
|
||||
function buildUserPasswordSchema(copy: UserSchemaCopy) {
|
||||
return z.string().min(8, { error: copy.passwordMinLength })
|
||||
}
|
||||
|
||||
export const createUserSchema = z.object({
|
||||
username: z.string().trim().min(1, { error: "Username is required" }),
|
||||
name: z.string().trim().min(1, { error: "Name is required" }),
|
||||
email: z.email({ error: "Invalid email" }),
|
||||
password: passwordSchema,
|
||||
role: userRoleSchema,
|
||||
isActive: z.boolean(),
|
||||
})
|
||||
export function buildCreateUserSchema(copy: UserSchemaCopy) {
|
||||
return z.object({
|
||||
username: z.string().trim().min(1, { error: copy.usernameRequired }),
|
||||
name: z.string().trim().min(1, { error: copy.nameRequired }),
|
||||
email: z.email({ error: copy.emailInvalid }),
|
||||
password: buildUserPasswordSchema(copy),
|
||||
role: userRoleSchema,
|
||||
isActive: z.boolean(),
|
||||
})
|
||||
}
|
||||
|
||||
export const updateUserSchema = z.object({
|
||||
id: z.string().min(1, { error: "User id is required" }),
|
||||
username: z.string().trim().min(1, { error: "Username is required" }),
|
||||
name: z.string().trim().min(1, { error: "Name is required" }),
|
||||
email: z.email({ error: "Invalid email" }),
|
||||
role: userRoleSchema,
|
||||
isActive: z.boolean(),
|
||||
})
|
||||
export const createUserSchema = buildCreateUserSchema(defaultUserSchemaCopy)
|
||||
|
||||
export const setUserActiveSchema = z.object({
|
||||
id: z.string().min(1, { error: "User id is required" }),
|
||||
isActive: z.boolean(),
|
||||
})
|
||||
export function buildUpdateUserSchema(copy: UserSchemaCopy) {
|
||||
return z.object({
|
||||
id: z.string().min(1, { error: copy.userIdRequired }),
|
||||
username: z.string().trim().min(1, { error: copy.usernameRequired }),
|
||||
name: z.string().trim().min(1, { error: copy.nameRequired }),
|
||||
email: z.email({ error: copy.emailInvalid }),
|
||||
role: userRoleSchema,
|
||||
isActive: z.boolean(),
|
||||
})
|
||||
}
|
||||
|
||||
export const resetUserPasswordSchema = z.object({
|
||||
id: z.string().min(1, { error: "User id is required" }),
|
||||
password: passwordSchema,
|
||||
})
|
||||
export const updateUserSchema = buildUpdateUserSchema(defaultUserSchemaCopy)
|
||||
|
||||
export function buildSetUserActiveSchema(copy: UserSchemaCopy) {
|
||||
return z.object({
|
||||
id: z.string().min(1, { error: copy.userIdRequired }),
|
||||
isActive: z.boolean(),
|
||||
})
|
||||
}
|
||||
|
||||
export const setUserActiveSchema = buildSetUserActiveSchema(
|
||||
defaultUserSchemaCopy,
|
||||
)
|
||||
|
||||
export function buildResetUserPasswordSchema(copy: UserSchemaCopy) {
|
||||
return z.object({
|
||||
id: z.string().min(1, { error: copy.userIdRequired }),
|
||||
password: buildUserPasswordSchema(copy),
|
||||
})
|
||||
}
|
||||
|
||||
export const resetUserPasswordSchema = buildResetUserPasswordSchema(
|
||||
defaultUserSchemaCopy,
|
||||
)
|
||||
|
||||
export type CreateUserFormType = z.infer<typeof createUserSchema>
|
||||
export type UpdateUserFormType = z.infer<typeof updateUserSchema>
|
||||
|
||||
Reference in New Issue
Block a user