diff --git a/.gitignore b/.gitignore index d3905d1f..e10668cd 100644 --- a/.gitignore +++ b/.gitignore @@ -738,3 +738,5 @@ flycheck_*.el /network-security.data docs/.venv +idp_data/h2/keycloakdb.mv.db +idp_data/h2/keycloakdb.trace.db diff --git a/backend/.env-old b/backend/.env-old deleted file mode 100644 index bedfb0b7..00000000 --- a/backend/.env-old +++ /dev/null @@ -1,21 +0,0 @@ -PORT=3000 -DWENGO_DB_HOST=db -DWENGO_DB_PORT=5432 -DWENGO_DB_USERNAME=postgres -DWENGO_DB_PASSWORD=postgres -DWENGO_DB_UPDATE=false - -DWENGO_AUTH_STUDENT_URL=http://localhost/idp/realms/student -DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo -DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://idp:7080/idp/realms/student/protocol/openid-connect/certs -DWENGO_AUTH_TEACHER_URL=http://localhost/idp/realms/teacher -DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo -DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://idp:7080/idp/realms/teacher/protocol/openid-connect/certs - -# Allow Vite dev-server to access the backend (for testing purposes). Don't forget to remove this in production! -#DWENGO_CORS_ALLOWED_ORIGINS=http://localhost/,127.0.0.1:80,http://127.0.0.1,http://localhost:80,http://127.0.0.1:80,localhost -DWENGO_CORS_ALLOWED_ORIGINS=http://localhost/*,http://idp:7080,https://idp:7080 - -# Logging and monitoring - -LOKI_HOST=http://logging:3102 diff --git a/backend/src/controllers/auth.ts b/backend/src/controllers/auth.ts index b87eaf7b..49e2159b 100644 --- a/backend/src/controllers/auth.ts +++ b/backend/src/controllers/auth.ts @@ -1,4 +1,10 @@ +import { UnauthorizedException } from '../exceptions/unauthorized-exception.js'; +import { getLogger } from '../logging/initalize.js'; +import { AuthenticatedRequest } from '../middleware/auth/authenticated-request.js'; +import { createOrUpdateStudent } from '../services/students.js'; +import { createOrUpdateTeacher } from '../services/teachers.js'; import { envVars, getEnvVar } from '../util/envVars.js'; +import { Response } from 'express'; interface FrontendIdpConfig { authority: string; @@ -15,6 +21,8 @@ interface FrontendAuthConfig { const SCOPE = 'openid profile email'; const RESPONSE_TYPE = 'code'; +const logger = getLogger(); + export function getFrontendAuthConfig(): FrontendAuthConfig { return { student: { @@ -31,3 +39,24 @@ export function getFrontendAuthConfig(): FrontendAuthConfig { }, }; } + +export async function postHelloHandler(req: AuthenticatedRequest, res: Response): Promise { + const auth = req.auth; + if (!auth) { + throw new UnauthorizedException('Cannot say hello when not authenticated.'); + } + const userData = { + id: auth.username, + username: auth.username, + firstName: auth.firstName ?? '', + lastName: auth.lastName ?? '', + }; + if (auth.accountType === 'student') { + await createOrUpdateStudent(userData); + logger.debug(`Synchronized student ${userData.username} with IDP`); + } else { + await createOrUpdateTeacher(userData); + logger.debug(`Synchronized teacher ${userData.username} with IDP`); + } + res.status(200).send({ message: 'Welcome!' }); +} diff --git a/backend/src/exceptions/exception-with-http-state.ts b/backend/src/exceptions/exception-with-http-state.ts index e5b9b9bd..5f12e25d 100644 --- a/backend/src/exceptions/exception-with-http-state.ts +++ b/backend/src/exceptions/exception-with-http-state.ts @@ -1,7 +1,9 @@ +import { HasStatusCode } from './has-status-code'; + /** * Exceptions which are associated with a HTTP error code. */ -export abstract class ExceptionWithHttpState extends Error { +export abstract class ExceptionWithHttpState extends Error implements HasStatusCode { constructor( public status: number, public error: string diff --git a/backend/src/exceptions/has-status-code.ts b/backend/src/exceptions/has-status-code.ts new file mode 100644 index 00000000..46b8e491 --- /dev/null +++ b/backend/src/exceptions/has-status-code.ts @@ -0,0 +1,6 @@ +export interface HasStatusCode { + status: number; +} +export function hasStatusCode(err: unknown): err is HasStatusCode { + return typeof err === 'object' && err !== null && 'status' in err && typeof (err as HasStatusCode)?.status === 'number'; +} diff --git a/backend/src/middleware/auth/auth.ts b/backend/src/middleware/auth/auth.ts index a91932ea..73a65b9a 100644 --- a/backend/src/middleware/auth/auth.ts +++ b/backend/src/middleware/auth/auth.ts @@ -48,14 +48,14 @@ const idpConfigs = { const verifyJwtToken = expressjwt({ secret: async (_: express.Request, token: jwt.Jwt | undefined) => { if (!token?.payload || !(token.payload as JwtPayload).iss) { - throw new Error('Invalid token'); + throw new UnauthorizedException('Invalid token.'); } const issuer = (token.payload as JwtPayload).iss; const idpConfig = Object.values(idpConfigs).find((config) => config.issuer === issuer); if (!idpConfig) { - throw new Error('Issuer not accepted.'); + throw new UnauthorizedException('Issuer not accepted.'); } const signingKey = await idpConfig.jwksClient.getSigningKey(token.header.kid); diff --git a/backend/src/middleware/error-handling/error-handler.ts b/backend/src/middleware/error-handling/error-handler.ts index 8ec93e37..f2cddf43 100644 --- a/backend/src/middleware/error-handling/error-handler.ts +++ b/backend/src/middleware/error-handling/error-handler.ts @@ -1,11 +1,11 @@ import { NextFunction, Request, Response } from 'express'; import { getLogger, Logger } from '../../logging/initalize.js'; -import { ExceptionWithHttpState } from '../../exceptions/exception-with-http-state.js'; +import { hasStatusCode } from '../../exceptions/has-status-code.js'; const logger: Logger = getLogger(); export function errorHandler(err: unknown, _req: Request, res: Response, _: NextFunction): void { - if (err instanceof ExceptionWithHttpState) { + if (hasStatusCode(err)) { logger.warn(`An error occurred while handling a request: ${err} (-> HTTP ${err.status})`); res.status(err.status).json(err); } else { diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 4a1f27d2..6f153836 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -1,5 +1,5 @@ import express from 'express'; -import { getFrontendAuthConfig } from '../controllers/auth.js'; +import { getFrontendAuthConfig, postHelloHandler } from '../controllers/auth.js'; import { authenticatedOnly, studentsOnly, teachersOnly } from '../middleware/auth/auth.js'; const router = express.Router(); @@ -23,4 +23,6 @@ router.get('/testTeachersOnly', teachersOnly, (_req, res) => { res.json({ message: 'If you see this, you should be a teacher!' }); }); +router.post('/hello', authenticatedOnly, postHelloHandler); + export default router; diff --git a/backend/src/services/students.ts b/backend/src/services/students.ts index 03a6d8fa..77ec6648 100644 --- a/backend/src/services/students.ts +++ b/backend/src/services/students.ts @@ -63,6 +63,16 @@ export async function createStudent(userData: StudentDTO): Promise { const newStudent = mapToStudent(userData); await studentRepository.save(newStudent, { preventOverwrite: true }); + + return userData; +} + +export async function createOrUpdateStudent(userData: StudentDTO): Promise { + await getStudentRepository().upsert({ + username: userData.username, + firstName: userData.firstName, + lastName: userData.lastName, + }); return userData; } diff --git a/backend/src/services/teachers.ts b/backend/src/services/teachers.ts index 982b657b..4fdb15be 100644 --- a/backend/src/services/teachers.ts +++ b/backend/src/services/teachers.ts @@ -62,9 +62,19 @@ export async function createTeacher(userData: TeacherDTO): Promise { const newTeacher = mapToTeacher(userData); await teacherRepository.save(newTeacher, { preventOverwrite: true }); + return mapToTeacherDTO(newTeacher); } +export async function createOrUpdateTeacher(userData: TeacherDTO): Promise { + await getTeacherRepository().upsert({ + username: userData.username, + firstName: userData.firstName, + lastName: userData.lastName, + }); + return userData; +} + export async function deleteTeacher(username: string): Promise { const teacherRepository: TeacherRepository = getTeacherRepository(); diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 4b885403..b1207448 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -10,10 +10,6 @@ } const showMenuBar = computed(() => (route.meta as RouteMeta).requiresAuth && auth.authState.user); - - auth.loadUser().catch((_error) => { - // TODO Could not load user! - });