Ir al contenido

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.

TalentBricksAI implementa autorización en tres niveles:

Usa authRequired: true en main.wasp para requerir que el usuario esté autenticado:

// En main.wasp
page 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"
}

Verifica el usuario en tus queries y actions:

app/src/courses/operations.ts
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.user existe solo si el usuario está autenticado
  • Si no hay usuario → context.user es undefined
  • Contiene: id, email, username, isAdmin, etc. (campos del modelo User)

Tipos de verificación:

// 1. Verificar autenticación
if (!context.user) {
throw new HttpError(401, "No autorizado");
}
// 2. Verificar admin
if (!context.user.isAdmin) {
throw new HttpError(403, "Solo administradores");
}
// 3. Verificar propietario del recurso
const 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");
}

Implementa roles verificando campos del modelo User:

app/src/admin/operations.ts
// Helper para verificar admin
function 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 },
});
};
export const myOperation: MyOperation = async (args, context) => {
if (!context.user) {
throw new HttpError(401, "Debes iniciar sesión");
}
// Continuar con la lógica...
};
export const adminOperation: AdminOperation = async (args, context) => {
if (!context.user?.isAdmin) {
throw new HttpError(403, "Acceso denegado - solo administradores");
}
// Lógica de admin...
};
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...
};

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,
};
};

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 autenticado
throw new HttpError(401, "Debes iniciar sesión");
// Sin permisos
throw new HttpError(403, "No tienes acceso a este recurso");
// No encontrado
throw new HttpError(404, "Curso no encontrado");
app/src/courses/MyCoursesPage.tsx
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>
);
}
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>
);
}
  1. Siempre verificar en el servidor - La autorización en el cliente es solo para UX

    // ✅ Verificar en el servidor
    export const deleteUser: DeleteUser = async (args, context) => {
    if (!context.user?.isAdmin) {
    throw new HttpError(403, "Solo admins");
    }
    // ...
    };
  2. Usar helpers reutilizables

    // ✅ Helper centralizado
    function requireAdmin(user: any) {
    if (!user?.isAdmin) throw new HttpError(403, "Admin only");
    }
  3. Filtrar datos sensibles

    // ✅ No exponer emails de otros usuarios
    const users = await context.entities.User.findMany({
    select: { id: true, username: true }, // Sin email
    });
  4. Verificar permisos ANTES de modificar datos

    // ✅ Verificar primero
    if (!context.user?.isAdmin) {
    throw new HttpError(403, "No autorizado");
    }
    await context.entities.Course.delete({ where: { id } });
  1. No confiar solo en autorización del cliente

    // ❌ NUNCA hacer esto
    function deleteUser(userId) {
    if (user.isAdmin) {
    // Solo check en cliente
    await deleteUserAction({ userId }); // Sin check en servidor
    }
    }
  2. No exponer datos sensibles

    // ❌ Expone datos de todos los usuarios
    export const getAllUsers: GetAllUsers = async (args, context) => {
    return context.entities.User.findMany({
    include: { enrollments: true, certificates: true },
    });
    };
    // ✅ Solo datos propios
    export 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 },
    });
    };
  3. No usar any sin verificar

    // ❌ Peligroso
    export const updateCourse = async (args, context) => {
    await context.entities.Course.update({
    where: { id: args.id },
    data: args.data, // Cualquier campo!
    });
    };
    // ✅ Validar y restringir
    export const updateCourse = async (args, context) => {
    if (!context.user?.isAdmin) throw new HttpError(403);
    const { title, description } = args; // Solo campos permitidos
    await context.entities.Course.update({
    where: { id: args.id },
    data: { title, description },
    });
    };
app/src/courses/operations.ts
import { HttpError } from "wasp/server";
import type {
GetCourses,
GetCourseById,
CreateCourse,
UpdateCourse,
DeleteCourse,
} from "wasp/server/operations";
// Helper
function requireAdmin(user: any) {
if (!user?.isAdmin) {
throw new HttpError(403, "Solo administradores");
}
}
// Público - cualquiera puede ver cursos publicados
export const getCourses: GetCourses = async (_args, context) => {
return context.entities.Course.findMany({
where: { isPublished: true },
include: { lessons: true },
});
};
// Público - ver detalle de curso
export 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 curso
export 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 curso
export 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 curso
export 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 },
});
};
  1. Page-level: authRequired: true en main.wasp
  2. Operation-level: Verificar context.user en queries/actions
  3. Role-based: Verificar context.user.isAdmin u otros campos
  4. Siempre validar en el servidor - nunca confiar solo en el cliente
  5. Usar códigos HTTP correctos - 401 (no autenticado), 403 (sin permisos), 404 (no encontrado)
  6. Filtrar datos sensibles - no exponer información de otros usuarios