feat: Mechanisme voor automatische aanmaak en update van accounts aangemaakt.
This commit is contained in:
		
							parent
							
								
									6cb8a1b98f
								
							
						
					
					
						commit
						9339eca9cf
					
				
					 9 changed files with 84 additions and 23 deletions
				
			
		|  | @ -1,4 +1,9 @@ | ||||||
| import { envVars, getEnvVar } from '../util/envVars.js'; | 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 { | interface FrontendIdpConfig { | ||||||
|     authority: string; |     authority: string; | ||||||
|  | @ -15,7 +20,7 @@ interface FrontendAuthConfig { | ||||||
| const SCOPE = 'openid profile email'; | const SCOPE = 'openid profile email'; | ||||||
| const RESPONSE_TYPE = 'code'; | const RESPONSE_TYPE = 'code'; | ||||||
| 
 | 
 | ||||||
| export function getFrontendAuthConfig(): FrontendAuthConfig { | function getFrontendAuthConfig(): FrontendAuthConfig { | ||||||
|     return { |     return { | ||||||
|         student: { |         student: { | ||||||
|             authority: getEnvVar(envVars.IdpStudentUrl), |             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); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -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. |  * Middleware which rejects requests from unauthenticated users or users that aren't teachers. | ||||||
|  */ |  */ | ||||||
| export const teachersOnly = authorize((auth) => auth.accountType === 'teacher'); | 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); | ||||||
|  |  | ||||||
|  | @ -1,7 +1,12 @@ | ||||||
| import {authorize} from "./auth-checks"; | import {authorize} from "./auth-checks"; | ||||||
| import {AuthenticationInfo} from "../authentication-info"; | import {AuthenticationInfo} from "../authentication-info"; | ||||||
| import {AuthenticatedRequest} from "../authenticated-request"; | 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. |  * 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) { |         if (req.params.username === auth.username) { | ||||||
|             return true; |             return true; | ||||||
|         } else if (auth.accountType === "teacher") { |         } else if (auth.accountType === "teacher") { | ||||||
|             const classes: string[] = (await getClassesByTeacher(auth.username, false)) as string[]; |             return teaches(auth.username, req.params.classId); | ||||||
|             return req.params.classId in classes; |  | ||||||
|         } else { |         } else { | ||||||
|             return false; |             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), | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | @ -1,13 +1,15 @@ | ||||||
| import express from 'express'; | 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"; | import {authenticatedOnly, studentsOnly, teachersOnly} from "../middleware/auth/checks/auth-checks"; | ||||||
| 
 | 
 | ||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
| 
 | 
 | ||||||
| // Returns auth configuration for frontend
 | // Returns auth configuration for frontend
 | ||||||
| router.get('/config', (_req, res) => { | router.get('/config', handleGetFrontendAuthConfig); | ||||||
|     res.json(getFrontendAuthConfig()); | 
 | ||||||
| }); | // 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) => { | router.get('/testAuthenticatedOnly', authenticatedOnly, (_req, res) => { | ||||||
|     /* #swagger.security = [{ "student": [ ] }, { "teacher": [ ] }] */ |     /* #swagger.security = [{ "student": [ ] }, { "teacher": [ ] }] */ | ||||||
|  |  | ||||||
|  | @ -12,13 +12,16 @@ import { | ||||||
| } from '../controllers/students.js'; | } from '../controllers/students.js'; | ||||||
| import joinRequestRouter from './student-join-requests.js'; | import joinRequestRouter from './student-join-requests.js'; | ||||||
| import {onlyAllowUserHimself} from "../middleware/auth/checks/user-auth-checks"; | import {onlyAllowUserHimself} from "../middleware/auth/checks/user-auth-checks"; | ||||||
|  | import {adminOnly} from "../middleware/auth/checks/auth-checks"; | ||||||
| 
 | 
 | ||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
| 
 | 
 | ||||||
| // Root endpoint used to search objects
 | // 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); | router.delete('/:username', onlyAllowUserHimself, deleteStudentHandler); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -10,26 +10,29 @@ import { | ||||||
|     getTeacherStudentHandler, |     getTeacherStudentHandler, | ||||||
|     updateStudentJoinRequestHandler, |     updateStudentJoinRequestHandler, | ||||||
| } from '../controllers/teachers.js'; | } 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(); | const router = express.Router(); | ||||||
| 
 | 
 | ||||||
| // Root endpoint used to search objects
 | // 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
 | // Invitations to other classes a teacher received
 | ||||||
| router.get('/:id/invitations', (_req, res) => { | router.get('/:id/invitations', (_req, res) => { | ||||||
|  |  | ||||||
|  | @ -52,11 +52,11 @@ export async function getStudent(username: string): Promise<StudentDTO> { | ||||||
|     return mapToStudentDTO(user); |     return mapToStudentDTO(user); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createStudent(userData: StudentDTO): Promise<StudentDTO> { | export async function createStudent(userData: StudentDTO, allowUpdate: boolean = false): Promise<StudentDTO> { | ||||||
|     const studentRepository = getStudentRepository(); |     const studentRepository = getStudentRepository(); | ||||||
| 
 | 
 | ||||||
|     const newStudent = mapToStudent(userData); |     const newStudent = mapToStudent(userData); | ||||||
|     await studentRepository.save(newStudent, { preventOverwrite: true }); |     await studentRepository.save(newStudent, { preventOverwrite: !allowUpdate }); | ||||||
|     return userData; |     return userData; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -56,11 +56,11 @@ export async function getTeacher(username: string): Promise<TeacherDTO> { | ||||||
|     return mapToTeacherDTO(user); |     return mapToTeacherDTO(user); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createTeacher(userData: TeacherDTO): Promise<TeacherDTO> { | export async function createTeacher(userData: TeacherDTO, update?: boolean): Promise<TeacherDTO> { | ||||||
|     const teacherRepository: TeacherRepository = getTeacherRepository(); |     const teacherRepository: TeacherRepository = getTeacherRepository(); | ||||||
| 
 | 
 | ||||||
|     const newTeacher = mapToTeacher(userData); |     const newTeacher = mapToTeacher(userData); | ||||||
|     await teacherRepository.save(newTeacher, { preventOverwrite: true }); |     await teacherRepository.save(newTeacher, { preventOverwrite: !update }); | ||||||
|     return mapToTeacherDTO(newTeacher); |     return mapToTeacherDTO(newTeacher); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -29,6 +29,10 @@ const authState = reactive<AuthState>({ | ||||||
|     activeRole: authStorage.getActiveRole() || null, |     activeRole: authStorage.getActiveRole() || null, | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | async function sendHello(): Promise<void> { | ||||||
|  |     return apiClient.post("/auth/hello"); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * Load the information about who is currently logged in from the IDP. |  * Load the information about who is currently logged in from the IDP. | ||||||
|  */ |  */ | ||||||
|  | @ -41,6 +45,8 @@ async function loadUser(): Promise<User | null> { | ||||||
|     authState.user = user; |     authState.user = user; | ||||||
|     authState.accessToken = user?.access_token || null; |     authState.accessToken = user?.access_token || null; | ||||||
|     authState.activeRole = activeRole || null; |     authState.activeRole = activeRole || null; | ||||||
|  | 
 | ||||||
|  |     await sendHello(); | ||||||
|     return user; |     return user; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -72,6 +78,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 sendHello(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger