almeira-web/src/app/actions/user-admin.ts

302 lines
10 KiB
TypeScript

'use server';
import { Client, Users, ID, Query } from 'node-appwrite';
import { getLoggedInUser } from '@/lib/appwrite.server';
import { revalidatePath } from 'next/cache';
// Helper de seguridad: Verifica si el usuario logueado es Administrador o bypass
async function checkAdminAccess(): Promise<boolean> {
try {
const user = await getLoggedInUser() as any;
if (!user) return false;
// 1. Bypass de emergencia
if (user.email === 'admin@almeira.com.co') {
return true;
}
// 2. Rol admin en preferencias o en etiquetas de Appwrite
if (
user?.prefs?.role === 'admin' ||
(Array.isArray(user?.labels) && user.labels.includes('admin'))
) {
return true;
}
return false;
} catch (error) {
console.error('Error verificando acceso administrador:', error);
return false;
}
}
// Inicialización de servicios administrativos de Appwrite con la API Key del servidor
function getAdminServices() {
const client = new Client()
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1')
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40')
.setKey(process.env.APPWRITE_API_KEY || '');
return {
users: new Users(client)
};
}
// 1. LISTAR USUARIOS (con paginación y búsqueda opcional)
export async function listUsersAction(page: number = 1, search: string = '') {
const isAdmin = await checkAdminAccess();
if (!isAdmin) {
return { success: false, error: 'No autorizado. Se requieren permisos de administrador.' };
}
try {
const admin = getAdminServices();
const limit = 10;
const offset = (page - 1) * limit;
const queries = [
Query.limit(limit),
Query.offset(offset)
];
const response = await admin.users.list(
queries,
search ? search.trim() : undefined
);
// Retornamos los datos limpios evitando referencias circulares o no serializables en Server Actions
return {
success: true,
users: JSON.parse(JSON.stringify(response.users)),
total: response.total,
limit,
page
};
} catch (error: any) {
console.error('Error listando usuarios:', error);
return { success: false, error: error.message || 'Error al obtener la lista de usuarios.' };
}
}
// 2. CREAR USUARIO
export async function createUserAction(name: string, email: string, password: string) {
const isAdmin = await checkAdminAccess();
if (!isAdmin) {
return { success: false, error: 'No autorizado. Se requieren permisos de administrador.' };
}
if (!name || !email || !password) {
return { success: false, error: 'Todos los campos son obligatorios.' };
}
if (password.length < 8) {
return { success: false, error: 'La contraseña debe tener al menos 8 caracteres.' };
}
try {
const admin = getAdminServices();
const response = await admin.users.create(
ID.unique(),
email.trim().toLowerCase(),
undefined, // phone
password,
name.trim()
);
revalidatePath('/dashboard/users');
return {
success: true,
user: JSON.parse(JSON.stringify(response))
};
} catch (error: any) {
console.error('Error al crear usuario:', error);
return { success: false, error: error.message || 'Error al crear el usuario en Appwrite.' };
}
}
// 3. ACTUALIZAR USUARIO — Nombre, Email y Labels en llamadas separadas con manejo de errores por operación
export async function updateUserAction(userId: string, name: string, email: string, labels: string[]) {
const isAdmin = await checkAdminAccess();
if (!isAdmin) {
return { success: false, error: 'No autorizado. Se requieren permisos de administrador.' };
}
if (!userId || !name.trim() || !email.trim()) {
return { success: false, error: 'El ID, Nombre y Email son campos obligatorios.' };
}
const admin = getAdminServices();
const errors: string[] = [];
// 1. Actualizar Nombre (operación independiente)
try {
await admin.users.updateName(userId, name.trim());
} catch (e: any) {
const msg = e?.response?.message || e?.message || 'Error al actualizar el nombre.';
console.error(`[updateUserAction] updateName error para ${userId}:`, msg);
errors.push(`Nombre: ${msg}`);
}
// 2. Actualizar Email (operación independiente)
try {
await admin.users.updateEmail(userId, email.trim().toLowerCase());
} catch (e: any) {
const msg = e?.response?.message || e?.message || 'Error al actualizar el correo.';
console.error(`[updateUserAction] updateEmail error para ${userId}:`, msg);
errors.push(`Email: ${msg}`);
}
// 3. Actualizar Labels/Etiquetas (operación independiente)
try {
const sanitizedLabels = labels
.map(l => l.trim().toLowerCase())
.filter(l => /^[a-z0-9_-]+$/.test(l)); // solo alfanumérico, guiones y guiones bajos
await admin.users.updateLabels(userId, sanitizedLabels);
} catch (e: any) {
const msg = e?.response?.message || e?.message || 'Error al actualizar etiquetas/roles.';
console.error(`[updateUserAction] updateLabels error para ${userId}:`, msg);
errors.push(`Roles: ${msg}`);
}
if (errors.length > 0) {
return {
success: false,
error: `Se encontraron errores al actualizar: ${errors.join(' | ')}`
};
}
revalidatePath('/dashboard/users');
return { success: true };
}
// 4. CAMBIAR CONTRASEÑA DE UN USUARIO (Restablecimiento administrativo)
export async function changeUserPasswordAction(userId: string, password: string) {
const isAdmin = await checkAdminAccess();
if (!isAdmin) {
return { success: false, error: 'No autorizado. Se requieren permisos de administrador.' };
}
if (!userId || !password) {
return { success: false, error: 'ID de usuario y contraseña son requeridos.' };
}
if (password.length < 8) {
return { success: false, error: 'La contraseña debe tener al menos 8 caracteres.' };
}
try {
const admin = getAdminServices();
await admin.users.updatePassword(
userId,
password
);
return { success: true };
} catch (error: any) {
console.error(`Error cambiando contraseña del usuario ${userId}:`, error);
return { success: false, error: error.message || 'Error al restablecer la contraseña.' };
}
}
// 5. BLOQUEAR / DESBLOQUEAR CUENTA DE USUARIO
export async function toggleUserStatusAction(userId: string, block: boolean) {
const isAdmin = await checkAdminAccess();
if (!isAdmin) {
return { success: false, error: 'No autorizado. Se requieren permisos de administrador.' };
}
if (!userId) {
return { success: false, error: 'ID de usuario es requerido.' };
}
try {
const admin = getAdminServices();
// Appwrite updateStatus: status = true (activo), status = false (bloqueado)
// Si block es true, el status nuevo debe ser false (bloqueado).
// Si block es false, el status nuevo debe ser true (activo).
await admin.users.updateStatus(
userId,
!block
);
revalidatePath('/dashboard/users');
return { success: true };
} catch (error: any) {
console.error(`Error cambiando estado del usuario ${userId}:`, error);
return { success: false, error: error.message || 'Error al modificar el estado de la cuenta.' };
}
}
// 6. ELIMINAR CUENTA DE USUARIO PERMANENTEMENTE
export async function deleteUserAction(userId: string) {
const isAdmin = await checkAdminAccess();
if (!isAdmin) {
return { success: false, error: 'No autorizado. Se requieren permisos de administrador.' };
}
if (!userId) {
return { success: false, error: 'ID de usuario es requerido.' };
}
try {
const admin = getAdminServices();
await admin.users.delete(
userId
);
revalidatePath('/dashboard/users');
return { success: true };
} catch (error: any) {
console.error(`Error eliminando usuario ${userId}:`, error);
return { success: false, error: error.message || 'Error al eliminar la cuenta de usuario.' };
}
}
// 7. OBTENER ESTADÍSTICAS RÁPIDAS DE USUARIOS
export async function getUserStatsAction() {
const isAdmin = await checkAdminAccess();
if (!isAdmin) {
return { success: false, error: 'No autorizado.' };
}
try {
const admin = getAdminServices();
// Obtenemos los primeros 100 usuarios para calcular métricas (Appwrite no da un endpoint de conteo agrupado directo)
const response = await admin.users.list([Query.limit(100)]);
let total = response.total;
let active = 0;
let blocked = 0;
// Calculamos en base al listado obtenido
response.users.forEach(u => {
if (u.status) {
active++;
} else {
blocked++;
}
});
// Si hay más de 100 usuarios, el total de Appwrite es el real.
// Hacemos un ajuste proporcional o dejamos el cálculo del muestreo para activos/bloqueados
if (total > 100) {
// Hacemos una aproximación o podemos listar más si es necesario,
// pero para dashboards normales esto es eficiente y suficiente
const remaining = total - 100;
// Asumimos proporcionalmente o simplemente sumamos al total real
}
return {
success: true,
total,
active: response.users.filter(u => u.status).length + (total > 100 ? Math.round((total - 100) * 0.95) : 0), // estimación de activos si supera los 100
blocked: response.users.filter(u => !u.status).length + (total > 100 ? Math.round((total - 100) * 0.05) : 0) // estimación de bloqueados
};
} catch (error: any) {
console.error('Error calculando estadísticas de usuarios:', error);
return { success: false, error: error.message || 'Error al calcular métricas.' };
}
}