Autorización
Sección titulada «Autorización»esta guía explica cómo implementar la autorización (control de acceso) en TalentBricksAI. La autorización determina qué usuarios pueden acceder a qué recursos y qué acciones pueden realizar.
Diferencia entre Autenticación y Autorización
Sección titulada «Diferencia entre Autenticación y Autorización»- Autenticación = ¿Quién eres? (verificar identidad mediante login)
- Autorización = ¿qué puedes hacer? (controlar acceso a recursos y acciones)
Wasp maneja la autenticación automáticamente. La autorización la implementas tú verificando permisos en tu código.
Niveles de Autorización
Sección titulada «Niveles de Autorización»TalentBricksAI implementa autorización en tres niveles:
1. Nivel de Página (Page-level)
Sección titulada «1. Nivel de Página (Page-level)»Usa authRequired: true en main.wasp para requerir que el usuario esté autenticado:
// En main.wasppage MyCoursesPage { authRequired: true, component: import { MyCoursesPage } from "@src/courses/MyCoursesPage"}Comportamiento:
- Si el usuario NO está autenticado → redirige a
/login - Si el usuario está autenticado → permite acceso a la página
Cuándo usar:
- Páginas que requieren login (cuenta de usuario, cursos comprados, configuración)
- Cualquier página con datos personales del usuario
Ejemplo en el código:
route MyCoursesRoute { path: "/my-courses", to: MyCoursesPage }page MyCoursesPage { authRequired: true, // Solo usuarios autenticados component: import { MyCoursesPage } from "@src/courses/MyCoursesPage"}
route LearnRoute { path: "/courses/:slug/learn", to: LearnPage }page LearnPage { authRequired: true, // Solo usuarios con enrollments component: import { LearnPage } from "@src/courses/LearnPage"}2. Nivel de Operación (Operation-level)
Sección titulada «2. Nivel de Operación (Operation-level)»Verifica el usuario en tus queries y actions:
import { HttpError } from "wasp/server";import type { GetMyEnrollments } from "wasp/server/operations";
export const getMyEnrollments: GetMyEnrollments<void, Enrollment[]> = async (_args, context) => { // Verificar que el usuario esté autenticado if (!context.user) { throw new HttpError(401, "Debes iniciar sesión para ver tus cursos"); }
// Retornar solo los datos del usuario autenticado return context.entities.Enrollment.findMany({ where: { userId: context.user.id }, include: { course: true, progress: true }, });};Objeto context.user:
context.userexiste solo si el usuario está autenticado- Si no hay usuario →
context.useresundefined - Contiene:
id,email,username,isAdmin, etc. (campos del modeloUser)
Tipos de verificación:
// 1. Verificar autenticaciónif (!context.user) { throw new HttpError(401, "No autorizado");}
// 2. Verificar adminif (!context.user.isAdmin) { throw new HttpError(403, "Solo administradores");}
// 3. Verificar propietario del recursoconst course = await context.entities.Course.findUnique({ where: { id: args.courseId },});
if (course.instructorId !== context.user.id) { throw new HttpError(403, "No tienes permiso para editar este curso");}3. Nivel de Rol (Role-based)
Sección titulada «3. Nivel de Rol (Role-based)»Implementa roles verificando campos del modelo User:
// Helper para verificar adminfunction requireAdmin(user: any) { if (!user?.isAdmin) { throw new HttpError(403, "Solo administradores pueden realizar está acción"); }}
export const getAdminCourses: GetAdminCourses<void, Course[]> = async (_args, context) => { requireAdmin(context.user);
// Retornar TODOS los cursos (incluyendo no publicados) return context.entities.Course.findMany({ include: { lessons: true }, });};Patrones Comunes
Sección titulada «Patrones Comunes»Verificar Autenticación
Sección titulada «Verificar Autenticación»export const myOperation: MyOperation = async (args, context) => { if (!context.user) { throw new HttpError(401, "Debes iniciar sesión"); }
// Continuar con la lógica...};Verificar Rol Admin
Sección titulada «Verificar Rol Admin»export const adminOperation: AdminOperation = async (args, context) => { if (!context.user?.isAdmin) { throw new HttpError(403, "Acceso denegado - solo administradores"); }
// Lógica de admin...};Verificar Propietario de Recurso
Sección titulada «Verificar Propietario de Recurso»export const updateCourse: UpdateCourse = async (args, context) => { if (!context.user) { throw new HttpError(401, "No autorizado"); }
const course = await context.entities.Course.findUnique({ where: { id: args.courseId }, });
if (!course) { throw new HttpError(404, "Curso no encontrado"); }
// Verificar que el usuario sea el instructor O admin if (course.instructorId !== context.user.id && !context.user.isAdmin) { throw new HttpError(403, "No puedes editar este curso"); }
// Continuar con la actualización...};Autorización Condicional
Sección titulada «Autorización Condicional»Retornar datos diferentes según el usuario:
export const getCourseBySlug: GetCourseBySlug = async (args, context) => { const course = await context.entities.Course.findUnique({ where: { slug: args.slug }, include: { lessons: true }, });
if (!course) { return null; }
// Verificar si el usuario está enrollado let isEnrolled = false; if (context.user) { const enrollment = await context.entities.Enrollment.findUnique({ where: { userId_courseId: { userId: context.user.id, courseId: course.id, }, }, }); isEnrolled = !!enrollment; }
// Filtrar lecciones según enrollment const visibleLessons = isEnrolled ? course.lessons // Todas las lecciones : course.lessons.filter(l => l.isPreview); // Solo previews
return { ...course, lessons: visibleLessons, isEnrolled, };};Códigos de Error HTTP
Sección titulada «Códigos de Error HTTP»Usa los códigos HTTP correctos:
- 401 Unauthorized - Usuario no autenticado
- 403 Forbidden - Usuario autenticado pero sin permisos
- 404 Not Found - Recurso no existe (o no debe revelarse que existe)
import { HttpError } from "wasp/server";
// No autenticadothrow new HttpError(401, "Debes iniciar sesión");
// Sin permisosthrow new HttpError(403, "No tienes acceso a este recurso");
// No encontradothrow new HttpError(404, "Curso no encontrado");Autorización en el Cliente
Sección titulada «Autorización en el Cliente»Usando el Hook useAuth
Sección titulada «Usando el Hook useAuth»import { useAuth } from "wasp/client/auth";
export function MyCoursesPage() { const { data: user } = useAuth();
// Mostrar UI diferente según el usuario return ( <div> {user?.isAdmin && <Link to="/admin">Panel de Administración</Link>}
<h1>Mis Cursos</h1> {/* ... */} </div> );}Renderizado Condicional
Sección titulada «Renderizado Condicional»export function CourseCard({ course }) { const { data: user } = useAuth();
return ( <div> <h2>{course.title}</h2>
{/* Mostrar botón solo si está enrollado */} {course.isEnrolled && ( <Button to={`/courses/${course.slug}/learn`}>Continuar Aprendiendo</Button> )}
{/* Admin puede editar */} {user?.isAdmin && <Button to={`/admin/courses/${course.id}/edit`}>Editar Curso</Button>} </div> );}Mejores Prácticas
Sección titulada «Mejores Prácticas»✅ Hacer (Do)
Sección titulada «✅ Hacer (Do)»-
Siempre verificar en el servidor - La autorización en el cliente es solo para UX
// ✅ Verificar en el servidorexport const deleteUser: DeleteUser = async (args, context) => {if (!context.user?.isAdmin) {throw new HttpError(403, "Solo admins");}// ...}; -
Usar helpers reutilizables
// ✅ Helper centralizadofunction requireAdmin(user: any) {if (!user?.isAdmin) throw new HttpError(403, "Admin only");} -
Filtrar datos sensibles
// ✅ No exponer emails de otros usuariosconst users = await context.entities.User.findMany({select: { id: true, username: true }, // Sin email}); -
Verificar permisos ANTES de modificar datos
// ✅ Verificar primeroif (!context.user?.isAdmin) {throw new HttpError(403, "No autorizado");}await context.entities.Course.delete({ where: { id } });
❌ No Hacer (Don’t)
Sección titulada «❌ No Hacer (Don’t)»-
No confiar solo en autorización del cliente
// ❌ NUNCA hacer estofunction deleteUser(userId) {if (user.isAdmin) {// Solo check en clienteawait deleteUserAction({ userId }); // Sin check en servidor}} -
No exponer datos sensibles
// ❌ Expone datos de todos los usuariosexport const getAllUsers: GetAllUsers = async (args, context) => {return context.entities.User.findMany({include: { enrollments: true, certificates: true },});};// ✅ Solo datos propiosexport const getMyData: GetMyData = async (args, context) => {if (!context.user) throw new HttpError(401);return context.entities.User.findUnique({where: { id: context.user.id },include: { enrollments: true, certificates: true },});}; -
No usar
anysin verificar// ❌ Peligrosoexport const updateCourse = async (args, context) => {await context.entities.Course.update({where: { id: args.id },data: args.data, // Cualquier campo!});};// ✅ Validar y restringirexport const updateCourse = async (args, context) => {if (!context.user?.isAdmin) throw new HttpError(403);const { title, description } = args; // Solo campos permitidosawait context.entities.Course.update({where: { id: args.id },data: { title, description },});};
Ejemplo Completo: CRUD de Cursos
Sección titulada «Ejemplo Completo: CRUD de Cursos»import { HttpError } from "wasp/server";import type { GetCourses, GetCourseById, CreateCourse, UpdateCourse, DeleteCourse,} from "wasp/server/operations";
// Helperfunction requireAdmin(user: any) { if (!user?.isAdmin) { throw new HttpError(403, "Solo administradores"); }}
// Público - cualquiera puede ver cursos publicadosexport const getCourses: GetCourses = async (_args, context) => { return context.entities.Course.findMany({ where: { isPublished: true }, include: { lessons: true }, });};
// Público - ver detalle de cursoexport const getCourseById: GetCourseById = async (args, context) => { const course = await context.entities.Course.findUnique({ where: { id: args.id }, include: { lessons: true }, });
if (!course) { throw new HttpError(404, "Curso no encontrado"); }
// Solo mostrar si está publicado O el usuario es admin if (!course.isPublished && !context.user?.isAdmin) { throw new HttpError(404, "Curso no encontrado"); }
return course;};
// Admin only - crear cursoexport const createCourse: CreateCourse = async (args, context) => { requireAdmin(context.user);
return context.entities.Course.create({ data: { title: args.title, slug: args.slug, description: args.description, instructorName: args.instructorName, // ... }, });};
// Admin only - actualizar cursoexport const updateCourse: UpdateCourse = async (args, context) => { requireAdmin(context.user);
const course = await context.entities.Course.findUnique({ where: { id: args.id }, });
if (!course) { throw new HttpError(404, "Curso no encontrado"); }
return context.entities.Course.update({ where: { id: args.id }, data: args.data, });};
// Admin only - eliminar cursoexport const deleteCourse: DeleteCourse = async (args, context) => { requireAdmin(context.user);
const course = await context.entities.Course.findUnique({ where: { id: args.id }, });
if (!course) { throw new HttpError(404, "Curso no encontrado"); }
return context.entities.Course.delete({ where: { id: args.id }, });};Recursos Adicionales
Sección titulada «Recursos Adicionales»Resumen
Sección titulada «Resumen»- Page-level:
authRequired: trueenmain.wasp - Operation-level: Verificar
context.useren queries/actions - Role-based: Verificar
context.user.isAdminu otros campos - Siempre validar en el servidor - nunca confiar solo en el cliente
- Usar códigos HTTP correctos - 401 (no autenticado), 403 (sin permisos), 404 (no encontrado)
- Filtrar datos sensibles - no exponer información de otros usuarios