Merge pull request #205 from SELab-2/fix/verschillende-authenticatieproblemen
fix: Verschillende authenticatie-gerelateerde problemen
This commit is contained in:
		
						commit
						1b65eee38f
					
				
					 20 changed files with 149 additions and 55 deletions
				
			
		
							
								
								
									
										2
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -738,3 +738,5 @@ flycheck_*.el | ||||||
| /network-security.data | /network-security.data | ||||||
| 
 | 
 | ||||||
| docs/.venv | docs/.venv | ||||||
|  | idp_data/h2/keycloakdb.mv.db | ||||||
|  | idp_data/h2/keycloakdb.trace.db | ||||||
|  |  | ||||||
|  | @ -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 |  | ||||||
|  | @ -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 { envVars, getEnvVar } from '../util/envVars.js'; | ||||||
|  | import { Response } from 'express'; | ||||||
| 
 | 
 | ||||||
| interface FrontendIdpConfig { | interface FrontendIdpConfig { | ||||||
|     authority: string; |     authority: string; | ||||||
|  | @ -15,6 +21,8 @@ interface FrontendAuthConfig { | ||||||
| const SCOPE = 'openid profile email'; | const SCOPE = 'openid profile email'; | ||||||
| const RESPONSE_TYPE = 'code'; | const RESPONSE_TYPE = 'code'; | ||||||
| 
 | 
 | ||||||
|  | const logger = getLogger(); | ||||||
|  | 
 | ||||||
| export function getFrontendAuthConfig(): FrontendAuthConfig { | export function getFrontendAuthConfig(): FrontendAuthConfig { | ||||||
|     return { |     return { | ||||||
|         student: { |         student: { | ||||||
|  | @ -31,3 +39,24 @@ export function getFrontendAuthConfig(): FrontendAuthConfig { | ||||||
|         }, |         }, | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export async function postHelloHandler(req: AuthenticatedRequest, res: Response): Promise<void> { | ||||||
|  |     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!' }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -1,7 +1,9 @@ | ||||||
|  | import { HasStatusCode } from './has-status-code'; | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * Exceptions which are associated with a HTTP error 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( |     constructor( | ||||||
|         public status: number, |         public status: number, | ||||||
|         public error: string |         public error: string | ||||||
|  |  | ||||||
							
								
								
									
										6
									
								
								backend/src/exceptions/has-status-code.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								backend/src/exceptions/has-status-code.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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'; | ||||||
|  | } | ||||||
|  | @ -48,14 +48,14 @@ const idpConfigs = { | ||||||
| const verifyJwtToken = expressjwt({ | const verifyJwtToken = expressjwt({ | ||||||
|     secret: async (_: express.Request, token: jwt.Jwt | undefined) => { |     secret: async (_: express.Request, token: jwt.Jwt | undefined) => { | ||||||
|         if (!token?.payload || !(token.payload as JwtPayload).iss) { |         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 issuer = (token.payload as JwtPayload).iss; | ||||||
| 
 | 
 | ||||||
|         const idpConfig = Object.values(idpConfigs).find((config) => config.issuer === issuer); |         const idpConfig = Object.values(idpConfigs).find((config) => config.issuer === issuer); | ||||||
|         if (!idpConfig) { |         if (!idpConfig) { | ||||||
|             throw new Error('Issuer not accepted.'); |             throw new UnauthorizedException('Issuer not accepted.'); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const signingKey = await idpConfig.jwksClient.getSigningKey(token.header.kid); |         const signingKey = await idpConfig.jwksClient.getSigningKey(token.header.kid); | ||||||
|  |  | ||||||
|  | @ -1,11 +1,11 @@ | ||||||
| import { NextFunction, Request, Response } from 'express'; | import { NextFunction, Request, Response } from 'express'; | ||||||
| import { getLogger, Logger } from '../../logging/initalize.js'; | 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(); | const logger: Logger = getLogger(); | ||||||
| 
 | 
 | ||||||
| export function errorHandler(err: unknown, _req: Request, res: Response, _: NextFunction): void { | 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})`); |         logger.warn(`An error occurred while handling a request: ${err} (-> HTTP ${err.status})`); | ||||||
|         res.status(err.status).json(err); |         res.status(err.status).json(err); | ||||||
|     } else { |     } else { | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import express from 'express'; | 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'; | import { authenticatedOnly, studentsOnly, teachersOnly } from '../middleware/auth/auth.js'; | ||||||
| const router = express.Router(); | 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!' }); |     res.json({ message: 'If you see this, you should be a teacher!' }); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | router.post('/hello', authenticatedOnly, postHelloHandler); | ||||||
|  | 
 | ||||||
| export default router; | export default router; | ||||||
|  |  | ||||||
|  | @ -63,6 +63,16 @@ export async function createStudent(userData: StudentDTO): Promise<StudentDTO> { | ||||||
| 
 | 
 | ||||||
|     const newStudent = mapToStudent(userData); |     const newStudent = mapToStudent(userData); | ||||||
|     await studentRepository.save(newStudent, { preventOverwrite: true }); |     await studentRepository.save(newStudent, { preventOverwrite: true }); | ||||||
|  | 
 | ||||||
|  |     return userData; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function createOrUpdateStudent(userData: StudentDTO): Promise<StudentDTO> { | ||||||
|  |     await getStudentRepository().upsert({ | ||||||
|  |         username: userData.username, | ||||||
|  |         firstName: userData.firstName, | ||||||
|  |         lastName: userData.lastName, | ||||||
|  |     }); | ||||||
|     return userData; |     return userData; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -62,9 +62,19 @@ export async function createTeacher(userData: TeacherDTO): Promise<TeacherDTO> { | ||||||
| 
 | 
 | ||||||
|     const newTeacher = mapToTeacher(userData); |     const newTeacher = mapToTeacher(userData); | ||||||
|     await teacherRepository.save(newTeacher, { preventOverwrite: true }); |     await teacherRepository.save(newTeacher, { preventOverwrite: true }); | ||||||
|  | 
 | ||||||
|     return mapToTeacherDTO(newTeacher); |     return mapToTeacherDTO(newTeacher); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export async function createOrUpdateTeacher(userData: TeacherDTO): Promise<TeacherDTO> { | ||||||
|  |     await getTeacherRepository().upsert({ | ||||||
|  |         username: userData.username, | ||||||
|  |         firstName: userData.firstName, | ||||||
|  |         lastName: userData.lastName, | ||||||
|  |     }); | ||||||
|  |     return userData; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export async function deleteTeacher(username: string): Promise<TeacherDTO> { | export async function deleteTeacher(username: string): Promise<TeacherDTO> { | ||||||
|     const teacherRepository: TeacherRepository = getTeacherRepository(); |     const teacherRepository: TeacherRepository = getTeacherRepository(); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -10,10 +10,6 @@ | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const showMenuBar = computed(() => (route.meta as RouteMeta).requiresAuth && auth.authState.user); |     const showMenuBar = computed(() => (route.meta as RouteMeta).requiresAuth && auth.authState.user); | ||||||
| 
 |  | ||||||
|     auth.loadUser().catch((_error) => { |  | ||||||
|         // TODO Could not load user! |  | ||||||
|     }); |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|     import { ref } from "vue"; |     import { ref } from "vue"; | ||||||
|     import { useI18n } from "vue-i18n"; |     import { useI18n } from "vue-i18n"; | ||||||
|  |     import { useRouter } from "vue-router"; | ||||||
| 
 | 
 | ||||||
|     import auth from "@/services/auth/auth-service.ts"; |     import auth from "@/services/auth/auth-service.ts"; | ||||||
| 
 | 
 | ||||||
|  | @ -10,6 +11,7 @@ | ||||||
|     const { t, locale } = useI18n(); |     const { t, locale } = useI18n(); | ||||||
| 
 | 
 | ||||||
|     const role = auth.authState.activeRole; |     const role = auth.authState.activeRole; | ||||||
|  |     const router = useRouter(); | ||||||
| 
 | 
 | ||||||
|     const name: string = auth.authState.user!.profile.name!; |     const name: string = auth.authState.user!.profile.name!; | ||||||
|     const initials: string = name |     const initials: string = name | ||||||
|  |  | ||||||
|  | @ -75,6 +75,9 @@ | ||||||
|     "sent": "sent", |     "sent": "sent", | ||||||
|     "failed": "fehlgeschlagen", |     "failed": "fehlgeschlagen", | ||||||
|     "wrong": "etwas ist schief gelaufen", |     "wrong": "etwas ist schief gelaufen", | ||||||
|  |     "created": "erstellt", | ||||||
|  |     "callbackLoading": "Sie werden angemeldet...", | ||||||
|  |     "loginUnexpectedError": "Anmeldung fehlgeschlagen", | ||||||
|     "submitSolution": "Lösung einreichen", |     "submitSolution": "Lösung einreichen", | ||||||
|     "submitNewSolution": "Neue Lösung einreichen", |     "submitNewSolution": "Neue Lösung einreichen", | ||||||
|     "markAsDone": "Als fertig markieren", |     "markAsDone": "Als fertig markieren", | ||||||
|  | @ -86,11 +89,10 @@ | ||||||
|     "noSubmissionsYet": "Noch keine Lösungen eingereicht.", |     "noSubmissionsYet": "Noch keine Lösungen eingereicht.", | ||||||
|     "viewAsGroup": "Fortschritt ansehen von Gruppe...", |     "viewAsGroup": "Fortschritt ansehen von Gruppe...", | ||||||
|     "assignLearningPath": "Als Aufgabe geben", |     "assignLearningPath": "Als Aufgabe geben", | ||||||
|     "created": "erstellt", |  | ||||||
|     "remove": "entfernen", |     "remove": "entfernen", | ||||||
|     "students": "Studenten", |     "students": "Studenten", | ||||||
|     "classJoinRequests": "Anfragen verbinden", |     "classJoinRequests": "Beitrittsanfragen", | ||||||
|     "reject": "zurückweisen", |     "reject": "ablehnen", | ||||||
|     "areusure": "Sind Sie sicher?", |     "areusure": "Sind Sie sicher?", | ||||||
|     "yes": "ja", |     "yes": "ja", | ||||||
|     "teachers": "Lehrer", |     "teachers": "Lehrer", | ||||||
|  |  | ||||||
|  | @ -75,6 +75,9 @@ | ||||||
|     "sent": "sent", |     "sent": "sent", | ||||||
|     "failed": "failed", |     "failed": "failed", | ||||||
|     "wrong": "something went wrong", |     "wrong": "something went wrong", | ||||||
|  |     "created": "created", | ||||||
|  |     "callbackLoading": "You are being logged in...", | ||||||
|  |     "loginUnexpectedError": "Login failed", | ||||||
|     "submitSolution": "Submit solution", |     "submitSolution": "Submit solution", | ||||||
|     "submitNewSolution": "Submit new solution", |     "submitNewSolution": "Submit new solution", | ||||||
|     "markAsDone": "Mark as completed", |     "markAsDone": "Mark as completed", | ||||||
|  | @ -86,7 +89,6 @@ | ||||||
|     "noSubmissionsYet": "No submissions yet.", |     "noSubmissionsYet": "No submissions yet.", | ||||||
|     "viewAsGroup": "View progress of group...", |     "viewAsGroup": "View progress of group...", | ||||||
|     "assignLearningPath": "assign", |     "assignLearningPath": "assign", | ||||||
|     "created": "created", |  | ||||||
|     "remove": "remove", |     "remove": "remove", | ||||||
|     "students": "students", |     "students": "students", | ||||||
|     "classJoinRequests": "join requests", |     "classJoinRequests": "join requests", | ||||||
|  |  | ||||||
|  | @ -76,6 +76,8 @@ | ||||||
|     "failed": "échoué", |     "failed": "échoué", | ||||||
|     "wrong": "quelque chose n'a pas fonctionné", |     "wrong": "quelque chose n'a pas fonctionné", | ||||||
|     "created": "créé", |     "created": "créé", | ||||||
|  |     "callbackLoading": "Vous serez connecté...", | ||||||
|  |     "loginUnexpectedError": "La connexion a échoué", | ||||||
|     "submitSolution": "Soumettre la solution", |     "submitSolution": "Soumettre la solution", | ||||||
|     "submitNewSolution": "Soumettre une nouvelle solution", |     "submitNewSolution": "Soumettre une nouvelle solution", | ||||||
|     "markAsDone": "Marquer comme terminé", |     "markAsDone": "Marquer comme terminé", | ||||||
|  |  | ||||||
|  | @ -76,6 +76,8 @@ | ||||||
|     "failed": "mislukt", |     "failed": "mislukt", | ||||||
|     "wrong": "er ging iets verkeerd", |     "wrong": "er ging iets verkeerd", | ||||||
|     "created": "gecreëerd", |     "created": "gecreëerd", | ||||||
|  |     "callbackLoading": "Je wordt ingelogd...", | ||||||
|  |     "loginUnexpectedError": "Inloggen mislukt", | ||||||
|     "submitSolution": "Oplossing indienen", |     "submitSolution": "Oplossing indienen", | ||||||
|     "submitNewSolution": "Nieuwe oplossing indienen", |     "submitNewSolution": "Nieuwe oplossing indienen", | ||||||
|     "markAsDone": "Markeren als afgewerkt", |     "markAsDone": "Markeren als afgewerkt", | ||||||
|  |  | ||||||
|  | @ -12,9 +12,11 @@ import App from "./App.vue"; | ||||||
| import router from "./router"; | import router from "./router"; | ||||||
| import { aliases, mdi } from "vuetify/iconsets/mdi"; | import { aliases, mdi } from "vuetify/iconsets/mdi"; | ||||||
| import { VueQueryPlugin, QueryClient } from "@tanstack/vue-query"; | import { VueQueryPlugin, QueryClient } from "@tanstack/vue-query"; | ||||||
|  | import authService from "./services/auth/auth-service.ts"; | ||||||
| 
 | 
 | ||||||
| const app = createApp(App); | const app = createApp(App); | ||||||
| 
 | 
 | ||||||
|  | await authService.loadUser(); | ||||||
| app.use(router); | app.use(router); | ||||||
| 
 | 
 | ||||||
| const link = document.createElement("link"); | const link = document.createElement("link"); | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ import CreateDiscussion from "@/views/discussions/CreateDiscussion.vue"; | ||||||
| import CallbackPage from "@/views/CallbackPage.vue"; | import CallbackPage from "@/views/CallbackPage.vue"; | ||||||
| import UserClasses from "@/views/classes/UserClasses.vue"; | import UserClasses from "@/views/classes/UserClasses.vue"; | ||||||
| import UserAssignments from "@/views/classes/UserAssignments.vue"; | import UserAssignments from "@/views/classes/UserAssignments.vue"; | ||||||
| import authState from "@/services/auth/auth-service.ts"; | import authService from "@/services/auth/auth-service.ts"; | ||||||
| import LearningPathPage from "@/views/learning-paths/LearningPathPage.vue"; | import LearningPathPage from "@/views/learning-paths/LearningPathPage.vue"; | ||||||
| import LearningPathSearchPage from "@/views/learning-paths/LearningPathSearchPage.vue"; | import LearningPathSearchPage from "@/views/learning-paths/LearningPathSearchPage.vue"; | ||||||
| import UserHomePage from "@/views/homepage/UserHomePage.vue"; | import UserHomePage from "@/views/homepage/UserHomePage.vue"; | ||||||
|  | @ -138,9 +138,8 @@ const router = createRouter({ | ||||||
| router.beforeEach(async (to, _from, next) => { | router.beforeEach(async (to, _from, next) => { | ||||||
|     // Verify if user is logged in before accessing certain routes
 |     // Verify if user is logged in before accessing certain routes
 | ||||||
|     if (to.meta.requiresAuth) { |     if (to.meta.requiresAuth) { | ||||||
|         if (!authState.isLoggedIn.value) { |         if (!authService.isLoggedIn.value) { | ||||||
|             //Next("/login");
 |             next("/login"); | ||||||
|             next(); |  | ||||||
|         } else { |         } else { | ||||||
|             next(); |             next(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -26,7 +26,7 @@ async function getUserManagers(): Promise<UserManagersForRoles> { | ||||||
| const authState = reactive<AuthState>({ | const authState = reactive<AuthState>({ | ||||||
|     user: null, |     user: null, | ||||||
|     accessToken: null, |     accessToken: null, | ||||||
|     activeRole: authStorage.getActiveRole() || null, |     activeRole: authStorage.getActiveRole() ?? null, | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -38,18 +38,38 @@ async function loadUser(): Promise<User | null> { | ||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
|     const user = await (await getUserManagers())[activeRole].getUser(); |     const user = await (await getUserManagers())[activeRole].getUser(); | ||||||
|     authState.user = user; |     setUserAuthInfo(user); | ||||||
|     authState.accessToken = user?.access_token || null; |     authState.activeRole = activeRole ?? null; | ||||||
|     authState.activeRole = activeRole || null; |  | ||||||
|     return user; |     return user; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const isLoggedIn = computed(() => authState.user !== null); | const isLoggedIn = computed(() => authState.user !== null); | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * Clears all the cached information about the current authentication. | ||||||
|  |  */ | ||||||
|  | function clearAuthState(): void { | ||||||
|  |     authStorage.deleteActiveRole(); | ||||||
|  |     authState.accessToken = null; | ||||||
|  |     authState.user = null; | ||||||
|  |     authState.activeRole = null; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Sets the information about the currently logged-in user in the cache. | ||||||
|  |  */ | ||||||
|  | function setUserAuthInfo(newUser: User | null): void { | ||||||
|  |     authState.user = newUser; | ||||||
|  |     authState.accessToken = newUser?.access_token ?? null; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * Redirect the user to the login page where he/she can choose whether to log in as a student or teacher. |  * Redirect the user to the login page where he/she can choose whether to log in as a student or teacher. | ||||||
|  */ |  */ | ||||||
| async function initiateLogin(): Promise<void> { | async function initiateLogin(): Promise<void> { | ||||||
|  |     if (isLoggedIn.value) { | ||||||
|  |         clearAuthState(); | ||||||
|  |     } | ||||||
|     await router.push(loginRoute); |     await router.push(loginRoute); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -72,6 +92,7 @@ async function handleLoginCallback(): Promise<void> { | ||||||
|         throw new Error("Login callback received, but the user is not logging in!"); |         throw new Error("Login callback received, but the user is not logging in!"); | ||||||
|     } |     } | ||||||
|     authState.user = (await (await getUserManagers())[activeRole].signinCallback()) || null; |     authState.user = (await (await getUserManagers())[activeRole].signinCallback()) || null; | ||||||
|  |     await apiClient.post("/auth/hello"); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -80,14 +101,14 @@ async function handleLoginCallback(): Promise<void> { | ||||||
| async function renewToken(): Promise<User | null> { | async function renewToken(): Promise<User | null> { | ||||||
|     const activeRole = authStorage.getActiveRole(); |     const activeRole = authStorage.getActiveRole(); | ||||||
|     if (!activeRole) { |     if (!activeRole) { | ||||||
|         // FIXME console.log("Can't renew the token: Not logged in!");
 |  | ||||||
|         await initiateLogin(); |         await initiateLogin(); | ||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
|     try { |     try { | ||||||
|         return await (await getUserManagers())[activeRole].signinSilent(); |         const userManagerForRole = (await getUserManagers())[activeRole]; | ||||||
|     } catch (_error) { |         const user = await userManagerForRole.signinSilent(); | ||||||
|         // FIXME console.log("Can't renew the token: " + error);
 |         setUserAuthInfo(user); | ||||||
|  |     } catch (_error: unknown) { | ||||||
|         await initiateLogin(); |         await initiateLogin(); | ||||||
|     } |     } | ||||||
|     return null; |     return null; | ||||||
|  | @ -101,6 +122,7 @@ async function logout(): Promise<void> { | ||||||
|     if (activeRole) { |     if (activeRole) { | ||||||
|         await (await getUserManagers())[activeRole].signoutRedirect(); |         await (await getUserManagers())[activeRole].signoutRedirect(); | ||||||
|         authStorage.deleteActiveRole(); |         authStorage.deleteActiveRole(); | ||||||
|  |         clearAuthState(); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -119,13 +141,15 @@ apiClient.interceptors.request.use( | ||||||
| // Registering interceptor to refresh the token when a request failed because it was expired.
 | // Registering interceptor to refresh the token when a request failed because it was expired.
 | ||||||
| apiClient.interceptors.response.use( | apiClient.interceptors.response.use( | ||||||
|     (response) => response, |     (response) => response, | ||||||
|     async (error: AxiosError<{ message?: string }>) => { |     async (error: AxiosError<{ message?: string; inner?: { message?: string } }>) => { | ||||||
|         if (error.response?.status === 401) { |         if (error.response?.status === 401) { | ||||||
|             if (error.response.data.message === "token_expired") { |             // If the user should already be logged in, his token is probably just expired.
 | ||||||
|                 // FIXME console.log("Access token expired, trying to refresh...");
 |             if (isLoggedIn.value) { | ||||||
|                 await renewToken(); |                 await renewToken(); | ||||||
|                 return apiClient(error.config!); // Retry the request
 |                 return apiClient(error.config!); // Retry the request
 | ||||||
|             } // Apparently, the user got a 401 because he was not logged in yet at all. Redirect him to login.
 |             } | ||||||
|  | 
 | ||||||
|  |             // Apparently, the user got a 401 because he was not logged in yet at all. Redirect him to login.
 | ||||||
|             await initiateLogin(); |             await initiateLogin(); | ||||||
|         } |         } | ||||||
|         return Promise.reject(error); |         return Promise.reject(error); | ||||||
|  |  | ||||||
|  | @ -1,8 +1,11 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|     import { useRouter } from "vue-router"; |     import { useRouter } from "vue-router"; | ||||||
|  |     import { useI18n } from "vue-i18n"; | ||||||
|     import { onMounted, ref, type Ref } from "vue"; |     import { onMounted, ref, type Ref } from "vue"; | ||||||
|     import auth from "../services/auth/auth-service.ts"; |     import auth from "../services/auth/auth-service.ts"; | ||||||
| 
 | 
 | ||||||
|  |     const { t } = useI18n(); | ||||||
|  | 
 | ||||||
|     const router = useRouter(); |     const router = useRouter(); | ||||||
| 
 | 
 | ||||||
|     const errorMessage: Ref<string | null> = ref(null); |     const errorMessage: Ref<string | null> = ref(null); | ||||||
|  | @ -12,14 +15,34 @@ | ||||||
|             await auth.handleLoginCallback(); |             await auth.handleLoginCallback(); | ||||||
|             await router.replace("/user"); // Redirect to theme page |             await router.replace("/user"); // Redirect to theme page | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             errorMessage.value = `OIDC callback error: ${error}`; |             errorMessage.value = `${t("loginUnexpectedError")}: ${error}`; | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|     <p v-if="!errorMessage">Logging you in...</p> |     <div class="callback"> | ||||||
|     <p v-else>{{ errorMessage }}</p> |         <div | ||||||
|  |             class="callback-loading" | ||||||
|  |             v-if="!errorMessage" | ||||||
|  |         > | ||||||
|  |             <v-progress-circular indeterminate></v-progress-circular> | ||||||
|  |             <p>{{ t("callbackLoading") }}</p> | ||||||
|  |         </div> | ||||||
|  |         <v-alert | ||||||
|  |             icon="mdi-alert-circle" | ||||||
|  |             type="error" | ||||||
|  |             variant="elevated" | ||||||
|  |             v-if="errorMessage" | ||||||
|  |         > | ||||||
|  |             {{ errorMessage }} | ||||||
|  |         </v-alert> | ||||||
|  |     </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped></style> | <style scoped> | ||||||
|  |     .callback { | ||||||
|  |         text-align: center; | ||||||
|  |         margin: 20px; | ||||||
|  |     } | ||||||
|  | </style> | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger