diff --git a/backend/src/controllers/auth.ts b/backend/src/controllers/auth.ts index b87eaf7b..e30a412e 100644 --- a/backend/src/controllers/auth.ts +++ b/backend/src/controllers/auth.ts @@ -1,4 +1,9 @@ import { envVars, getEnvVar } from '../util/envVars.js'; +import {AuthenticatedRequest} from "../middleware/auth/authenticated-request"; +import {createStudent} from "../services/students"; +import {AuthenticationInfo} from "../middleware/auth/authentication-info"; +import {Request, Response} from "express"; +import {createTeacher} from "../services/teachers"; interface FrontendIdpConfig { authority: string; @@ -15,7 +20,7 @@ interface FrontendAuthConfig { const SCOPE = 'openid profile email'; const RESPONSE_TYPE = 'code'; -export function getFrontendAuthConfig(): FrontendAuthConfig { +function getFrontendAuthConfig(): FrontendAuthConfig { return { student: { authority: getEnvVar(envVars.IdpStudentUrl), @@ -31,3 +36,26 @@ export function getFrontendAuthConfig(): FrontendAuthConfig { }, }; } + +export function handleGetFrontendAuthConfig(_req: Request, res: Response): void { + res.json(getFrontendAuthConfig()); +} + +export async function handleHello(req: AuthenticatedRequest) { + const auth: AuthenticationInfo = req.auth!; + if (auth.accountType === "teacher") { + await createTeacher({ + id: auth.username, + username: auth.username, + firstName: auth.firstName ?? "", + lastName: auth.lastName ?? "", + }, true); + } else { + await createStudent({ + id: auth.username, + username: auth.username, + firstName: auth.firstName ?? "", + lastName: auth.lastName ?? "", + }, true); + } +} diff --git a/backend/src/middleware/auth/checks/auth-checks.ts b/backend/src/middleware/auth/checks/auth-checks.ts index bf15e551..ee70c109 100644 --- a/backend/src/middleware/auth/checks/auth-checks.ts +++ b/backend/src/middleware/auth/checks/auth-checks.ts @@ -36,3 +36,8 @@ export const studentsOnly = authorize((auth) => auth.accountType === 'student'); * Middleware which rejects requests from unauthenticated users or users that aren't teachers. */ export const teachersOnly = authorize((auth) => auth.accountType === 'teacher'); +/** + * Middleware which is to be used on requests no normal user should be able to execute. + * Since there is no concept of administrator accounts yet, currently, those requests will always be blocked. + */ +export const adminOnly = authorize(() => false); diff --git a/backend/src/middleware/auth/checks/class-auth-checks.ts b/backend/src/middleware/auth/checks/class-auth-checks.ts index 6c92e190..13adf4a8 100644 --- a/backend/src/middleware/auth/checks/class-auth-checks.ts +++ b/backend/src/middleware/auth/checks/class-auth-checks.ts @@ -1,7 +1,12 @@ import {authorize} from "./auth-checks"; import {AuthenticationInfo} from "../authentication-info"; import {AuthenticatedRequest} from "../authenticated-request"; -import {getClassesByTeacher} from "../../../services/teachers"; +import {getClass} from "../../../services/classes"; + +async function teaches(teacherUsername: string, classId: string) { + const clazz = await getClass(classId); + return clazz != null && teacherUsername in clazz.teachers; +} /** * To be used on a request with path parameters username and classId. @@ -13,10 +18,18 @@ export const onlyAllowStudentHimselfAndTeachersOfClass = authorize( if (req.params.username === auth.username) { return true; } else if (auth.accountType === "teacher") { - const classes: string[] = (await getClassesByTeacher(auth.username, false)) as string[]; - return req.params.classId in classes; + return teaches(auth.username, req.params.classId); } else { return false; } } ); + +/** + * Only let the request pass through if its path parameter "username" is the username of the currently logged-in + * teacher and the path parameter "classId" refers to a class the teacher teaches. + */ +export const onlyAllowTeacherOfClass = authorize( + async (auth: AuthenticationInfo, req: AuthenticatedRequest) => + req.params.username === auth.username && teaches(auth.username, req.params.classId), +); diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 8c4ab450..7f204076 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -1,13 +1,15 @@ import express from 'express'; -import { getFrontendAuthConfig } from '../controllers/auth.js'; +import {handleGetFrontendAuthConfig, handleHello} from '../controllers/auth.js'; import {authenticatedOnly, studentsOnly, teachersOnly} from "../middleware/auth/checks/auth-checks"; const router = express.Router(); // Returns auth configuration for frontend -router.get('/config', (_req, res) => { - res.json(getFrontendAuthConfig()); -}); +router.get('/config', handleGetFrontendAuthConfig); + +// This endpoint is called by the client when the user has just logged in. +// It creates or updates the user entity based on the authentication data the endpoint was called with. +router.post('/hello', authenticatedOnly, handleHello); router.get('/testAuthenticatedOnly', authenticatedOnly, (_req, res) => { /* #swagger.security = [{ "student": [ ] }, { "teacher": [ ] }] */ diff --git a/backend/src/routes/students.ts b/backend/src/routes/students.ts index 54b0d894..e378e6ea 100644 --- a/backend/src/routes/students.ts +++ b/backend/src/routes/students.ts @@ -12,13 +12,16 @@ import { } from '../controllers/students.js'; import joinRequestRouter from './student-join-requests.js'; import {onlyAllowUserHimself} from "../middleware/auth/checks/user-auth-checks"; +import {adminOnly} from "../middleware/auth/checks/auth-checks"; const router = express.Router(); // Root endpoint used to search objects -router.get('/', getAllStudentsHandler); +router.get('/', adminOnly, getAllStudentsHandler); -router.post('/', createStudentHandler); +// Users will be created automatically when some resource is created for them. Therefore, this endpoint +// can only be used by an administrator. +router.post('/', adminOnly, createStudentHandler); router.delete('/:username', onlyAllowUserHimself, deleteStudentHandler); diff --git a/backend/src/routes/teachers.ts b/backend/src/routes/teachers.ts index a6106a80..3e463500 100644 --- a/backend/src/routes/teachers.ts +++ b/backend/src/routes/teachers.ts @@ -10,26 +10,29 @@ import { getTeacherStudentHandler, updateStudentJoinRequestHandler, } from '../controllers/teachers.js'; +import {adminOnly} from "../middleware/auth/checks/auth-checks"; +import {onlyAllowUserHimself} from "../middleware/auth/checks/user-auth-checks"; +import {onlyAllowTeacherOfClass} from "../middleware/auth/checks/class-auth-checks"; const router = express.Router(); // Root endpoint used to search objects -router.get('/', getAllTeachersHandler); +router.get('/', adminOnly, getAllTeachersHandler); -router.post('/', createTeacherHandler); +router.post('/', adminOnly, createTeacherHandler); -router.get('/:username', getTeacherHandler); +router.get('/:username', onlyAllowUserHimself, getTeacherHandler); -router.delete('/:username', deleteTeacherHandler); +router.delete('/:username', onlyAllowUserHimself, deleteTeacherHandler); -router.get('/:username/classes', getTeacherClassHandler); +router.get('/:username/classes', onlyAllowUserHimself, getTeacherClassHandler); -router.get('/:username/students', getTeacherStudentHandler); +router.get('/:username/students', onlyAllowUserHimself, getTeacherStudentHandler); -router.get('/:username/questions', getTeacherQuestionHandler); +router.get('/:username/questions', onlyAllowUserHimself, getTeacherQuestionHandler); -router.get('/:username/joinRequests/:classId', getStudentJoinRequestHandler); +router.get('/:username/joinRequests/:classId', onlyAllowTeacherOfClass, getStudentJoinRequestHandler); -router.put('/:username/joinRequests/:classId/:studentUsername', updateStudentJoinRequestHandler); +router.put('/:username/joinRequests/:classId/:studentUsername', onlyAllowTeacherOfClass, updateStudentJoinRequestHandler); // Invitations to other classes a teacher received router.get('/:id/invitations', (_req, res) => { diff --git a/backend/src/services/students.ts b/backend/src/services/students.ts index 960edb93..6158a0f8 100644 --- a/backend/src/services/students.ts +++ b/backend/src/services/students.ts @@ -52,11 +52,11 @@ export async function getStudent(username: string): Promise { return mapToStudentDTO(user); } -export async function createStudent(userData: StudentDTO): Promise { +export async function createStudent(userData: StudentDTO, allowUpdate: boolean = false): Promise { const studentRepository = getStudentRepository(); const newStudent = mapToStudent(userData); - await studentRepository.save(newStudent, { preventOverwrite: true }); + await studentRepository.save(newStudent, { preventOverwrite: !allowUpdate }); return userData; } diff --git a/backend/src/services/teachers.ts b/backend/src/services/teachers.ts index 1b7643fb..501f03f0 100644 --- a/backend/src/services/teachers.ts +++ b/backend/src/services/teachers.ts @@ -56,11 +56,11 @@ export async function getTeacher(username: string): Promise { return mapToTeacherDTO(user); } -export async function createTeacher(userData: TeacherDTO): Promise { +export async function createTeacher(userData: TeacherDTO, update?: boolean): Promise { const teacherRepository: TeacherRepository = getTeacherRepository(); const newTeacher = mapToTeacher(userData); - await teacherRepository.save(newTeacher, { preventOverwrite: true }); + await teacherRepository.save(newTeacher, { preventOverwrite: !update }); return mapToTeacherDTO(newTeacher); } diff --git a/frontend/src/services/auth/auth-service.ts b/frontend/src/services/auth/auth-service.ts index 977e1dbf..0cb62803 100644 --- a/frontend/src/services/auth/auth-service.ts +++ b/frontend/src/services/auth/auth-service.ts @@ -29,6 +29,10 @@ const authState = reactive({ activeRole: authStorage.getActiveRole() || null, }); +async function sendHello(): Promise { + return apiClient.post("/auth/hello"); +} + /** * Load the information about who is currently logged in from the IDP. */ @@ -41,6 +45,8 @@ async function loadUser(): Promise { authState.user = user; authState.accessToken = user?.access_token || null; authState.activeRole = activeRole || null; + + await sendHello(); return user; } @@ -72,6 +78,7 @@ async function handleLoginCallback(): Promise { throw new Error("Login callback received, but the user is not logging in!"); } authState.user = (await (await getUserManagers())[activeRole].signinCallback()) || null; + await sendHello(); } /**