Merge remote-tracking branch 'origin/dev' into fix/testdata-niet-meer-correct-opgezet
# Conflicts: # backend/tests/controllers/teachers.test.ts
This commit is contained in:
		
						commit
						67cf87dc9b
					
				
					 67 changed files with 884 additions and 335 deletions
				
			
		|  | @ -10,6 +10,7 @@ | ||||||
| ### Dwengo ### | ### Dwengo ### | ||||||
| 
 | 
 | ||||||
| DWENGO_PORT=3000 | DWENGO_PORT=3000 | ||||||
|  | DWENGO_RUN_MODE=test | ||||||
| 
 | 
 | ||||||
| DWENGO_DB_NAME=":memory:" | DWENGO_DB_NAME=":memory:" | ||||||
| DWENGO_DB_UPDATE=true | DWENGO_DB_UPDATE=true | ||||||
|  |  | ||||||
|  | @ -1,10 +1,11 @@ | ||||||
| import { UnauthorizedException } from '../exceptions/unauthorized-exception.js'; | import { UnauthorizedException } from '../exceptions/unauthorized-exception.js'; | ||||||
| import { getLogger } from '../logging/initalize.js'; | import { getLogger } from '../logging/initalize.js'; | ||||||
| import { AuthenticatedRequest } from '../middleware/auth/authenticated-request.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'; | import { createOrUpdateStudent } from '../services/students.js'; | ||||||
|  | import { Request, Response } from 'express'; | ||||||
|  | import { createOrUpdateTeacher } from '../services/teachers.js'; | ||||||
|  | import { AccountType } from '@dwengo-1/common/util/account-types'; | ||||||
| 
 | 
 | ||||||
| interface FrontendIdpConfig { | interface FrontendIdpConfig { | ||||||
|     authority: string; |     authority: string; | ||||||
|  | @ -40,6 +41,10 @@ export function getFrontendAuthConfig(): FrontendAuthConfig { | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export function handleGetFrontendAuthConfig(_req: Request, res: Response): void { | ||||||
|  |     res.json(getFrontendAuthConfig()); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export async function postHelloHandler(req: AuthenticatedRequest, res: Response): Promise<void> { | export async function postHelloHandler(req: AuthenticatedRequest, res: Response): Promise<void> { | ||||||
|     const auth = req.auth; |     const auth = req.auth; | ||||||
|     if (!auth) { |     if (!auth) { | ||||||
|  | @ -51,7 +56,7 @@ export async function postHelloHandler(req: AuthenticatedRequest, res: Response) | ||||||
|         firstName: auth.firstName ?? '', |         firstName: auth.firstName ?? '', | ||||||
|         lastName: auth.lastName ?? '', |         lastName: auth.lastName ?? '', | ||||||
|     }; |     }; | ||||||
|     if (auth.accountType === 'student') { |     if (auth.accountType === AccountType.Student) { | ||||||
|         await createOrUpdateStudent(userData); |         await createOrUpdateStudent(userData); | ||||||
|         logger.debug(`Synchronized student ${userData.username} with IDP`); |         logger.debug(`Synchronized student ${userData.username} with IDP`); | ||||||
|     } else { |     } else { | ||||||
|  |  | ||||||
|  | @ -113,7 +113,7 @@ export async function createStudentRequestHandler(req: Request, res: Response): | ||||||
|     const classId = req.body.classId; |     const classId = req.body.classId; | ||||||
|     requireFields({ username, classId }); |     requireFields({ username, classId }); | ||||||
| 
 | 
 | ||||||
|     const request = await createClassJoinRequest(username, classId); |     const request = await createClassJoinRequest(username, classId.toUpperCase()); | ||||||
|     res.json({ request }); |     res.json({ request }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ import { Request, Response } from 'express'; | ||||||
| import { requireFields } from './error-helper.js'; | import { requireFields } from './error-helper.js'; | ||||||
| import { createInvitation, deleteInvitation, getAllInvitations, getInvitation, updateInvitation } from '../services/teacher-invitations.js'; | import { createInvitation, deleteInvitation, getAllInvitations, getInvitation, updateInvitation } from '../services/teacher-invitations.js'; | ||||||
| import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation'; | import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation'; | ||||||
|  | import { ConflictException } from '../exceptions/conflict-exception.js'; | ||||||
| 
 | 
 | ||||||
| export async function getAllInvitationsHandler(req: Request, res: Response): Promise<void> { | export async function getAllInvitationsHandler(req: Request, res: Response): Promise<void> { | ||||||
|     const username = req.params.username; |     const username = req.params.username; | ||||||
|  | @ -30,6 +31,10 @@ export async function createInvitationHandler(req: Request, res: Response): Prom | ||||||
|     const classId = req.body.class; |     const classId = req.body.class; | ||||||
|     requireFields({ sender, receiver, classId }); |     requireFields({ sender, receiver, classId }); | ||||||
| 
 | 
 | ||||||
|  |     if (sender === receiver) { | ||||||
|  |         throw new ConflictException('Cannot send an invitation to yourself'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     const data = req.body as TeacherInvitationData; |     const data = req.body as TeacherInvitationData; | ||||||
|     const invitation = await createInvitation(data); |     const invitation = await createInvitation(data); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -3,9 +3,9 @@ import { Question } from '../../entities/questions/question.entity.js'; | ||||||
| import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; | import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; | ||||||
| import { Student } from '../../entities/users/student.entity.js'; | import { Student } from '../../entities/users/student.entity.js'; | ||||||
| import { LearningObject } from '../../entities/content/learning-object.entity.js'; | import { LearningObject } from '../../entities/content/learning-object.entity.js'; | ||||||
|  | import { Group } from '../../entities/assignments/group.entity.js'; | ||||||
| import { Assignment } from '../../entities/assignments/assignment.entity.js'; | import { Assignment } from '../../entities/assignments/assignment.entity.js'; | ||||||
| import { Loaded } from '@mikro-orm/core'; | import { Loaded } from '@mikro-orm/core'; | ||||||
| import { Group } from '../../entities/assignments/group.entity'; |  | ||||||
| 
 | 
 | ||||||
| export class QuestionRepository extends DwengoEntityRepository<Question> { | export class QuestionRepository extends DwengoEntityRepository<Question> { | ||||||
|     public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; inGroup: Group; content: string }): Promise<Question> { |     public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; inGroup: Group; content: string }): Promise<Question> { | ||||||
|  |  | ||||||
|  | @ -10,6 +10,10 @@ export function mapToUserDTO(user: User): UserDTO { | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export function mapToUsername(user: { username: string }): string { | ||||||
|  |     return user.username; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export function mapToUser<T extends User>(userData: UserDTO, userInstance: T): T { | export function mapToUser<T extends User>(userData: UserDTO, userInstance: T): T { | ||||||
|     userInstance.username = userData.username; |     userInstance.username = userData.username; | ||||||
|     userInstance.firstName = userData.firstName; |     userInstance.firstName = userData.firstName; | ||||||
|  |  | ||||||
|  | @ -7,7 +7,6 @@ import * as express from 'express'; | ||||||
| import { AuthenticatedRequest } from './authenticated-request.js'; | import { AuthenticatedRequest } from './authenticated-request.js'; | ||||||
| import { AuthenticationInfo } from './authentication-info.js'; | import { AuthenticationInfo } from './authentication-info.js'; | ||||||
| import { UnauthorizedException } from '../../exceptions/unauthorized-exception.js'; | import { UnauthorizedException } from '../../exceptions/unauthorized-exception.js'; | ||||||
| import { ForbiddenException } from '../../exceptions/forbidden-exception.js'; |  | ||||||
| 
 | 
 | ||||||
| const JWKS_CACHE = true; | const JWKS_CACHE = true; | ||||||
| const JWKS_RATE_LIMIT = true; | const JWKS_RATE_LIMIT = true; | ||||||
|  | @ -108,36 +107,3 @@ function addAuthenticationInfo(req: AuthenticatedRequest, _res: express.Response | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const authenticateUser = [verifyJwtToken, addAuthenticationInfo]; | export const authenticateUser = [verifyJwtToken, addAuthenticationInfo]; | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Middleware which rejects unauthenticated users (with HTTP 401) and authenticated users which do not fulfill |  | ||||||
|  * the given access condition. |  | ||||||
|  * @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates |  | ||||||
|  *                        to true. |  | ||||||
|  */ |  | ||||||
| export function authorize(accessCondition: (auth: AuthenticationInfo) => boolean) { |  | ||||||
|     return (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void => { |  | ||||||
|         if (!req.auth) { |  | ||||||
|             throw new UnauthorizedException(); |  | ||||||
|         } else if (!accessCondition(req.auth)) { |  | ||||||
|             throw new ForbiddenException(); |  | ||||||
|         } else { |  | ||||||
|             next(); |  | ||||||
|         } |  | ||||||
|     }; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Middleware which rejects all unauthenticated users, but accepts all authenticated users. |  | ||||||
|  */ |  | ||||||
| export const authenticatedOnly = authorize((_) => true); |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Middleware which rejects requests from unauthenticated users or users that aren't students. |  | ||||||
|  */ |  | ||||||
| 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'); |  | ||||||
|  |  | ||||||
|  | @ -1,8 +1,15 @@ | ||||||
| import { Request } from 'express'; | import { Request } from 'express'; | ||||||
| import { JwtPayload } from 'jsonwebtoken'; | import { JwtPayload } from 'jsonwebtoken'; | ||||||
| import { AuthenticationInfo } from './authentication-info.js'; | import { AuthenticationInfo } from './authentication-info.js'; | ||||||
|  | import * as core from 'express-serve-static-core'; | ||||||
| 
 | 
 | ||||||
| export interface AuthenticatedRequest extends Request { | export interface AuthenticatedRequest< | ||||||
|  |     P = core.ParamsDictionary, | ||||||
|  |     ResBody = unknown, | ||||||
|  |     ReqBody = unknown, | ||||||
|  |     ReqQuery = core.Query, | ||||||
|  |     Locals extends Record<string, unknown> = Record<string, unknown>, | ||||||
|  | > extends Request<P, ResBody, ReqBody, ReqQuery, Locals> { | ||||||
|     // Properties are optional since the user is not necessarily authenticated.
 |     // Properties are optional since the user is not necessarily authenticated.
 | ||||||
|     jwtPayload?: JwtPayload; |     jwtPayload?: JwtPayload; | ||||||
|     auth?: AuthenticationInfo; |     auth?: AuthenticationInfo; | ||||||
|  |  | ||||||
							
								
								
									
										21
									
								
								backend/src/middleware/auth/checks/assignment-auth-checks.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								backend/src/middleware/auth/checks/assignment-auth-checks.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | ||||||
|  | import { authorize } from './auth-checks.js'; | ||||||
|  | import { fetchClass } from '../../../services/classes.js'; | ||||||
|  | import { fetchAllGroups } from '../../../services/groups.js'; | ||||||
|  | import { mapToUsername } from '../../../interfaces/user.js'; | ||||||
|  | import { AccountType } from '@dwengo-1/common/util/account-types'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Expects the path to contain the path parameters 'classId' and 'id' (meaning the ID of the assignment). | ||||||
|  |  * Only allows requests from users who are | ||||||
|  |  * - either teachers of the class the assignment was posted in, | ||||||
|  |  * - or students in a group of the assignment. | ||||||
|  |  */ | ||||||
|  | export const onlyAllowIfHasAccessToAssignment = authorize(async (auth, req) => { | ||||||
|  |     const { classid: classId, id: assignmentId } = req.params as { classid: string; id: number }; | ||||||
|  |     if (auth.accountType === AccountType.Teacher) { | ||||||
|  |         const clazz = await fetchClass(classId); | ||||||
|  |         return clazz.teachers.map(mapToUsername).includes(auth.username); | ||||||
|  |     } | ||||||
|  |     const groups = await fetchAllGroups(classId, assignmentId); | ||||||
|  |     return groups.some((group) => group.members.map((member) => member.username).includes(auth.username)); | ||||||
|  | }); | ||||||
							
								
								
									
										61
									
								
								backend/src/middleware/auth/checks/auth-checks.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								backend/src/middleware/auth/checks/auth-checks.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,61 @@ | ||||||
|  | import { AuthenticationInfo } from '../authentication-info.js'; | ||||||
|  | import { AuthenticatedRequest } from '../authenticated-request.js'; | ||||||
|  | import * as express from 'express'; | ||||||
|  | import { RequestHandler } from 'express'; | ||||||
|  | import { UnauthorizedException } from '../../../exceptions/unauthorized-exception.js'; | ||||||
|  | import { ForbiddenException } from '../../../exceptions/forbidden-exception.js'; | ||||||
|  | import { envVars, getEnvVar } from '../../../util/envVars.js'; | ||||||
|  | import { AccountType } from '@dwengo-1/common/util/account-types'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Middleware which rejects unauthenticated users (with HTTP 401) and authenticated users which do not fulfill | ||||||
|  |  * the given access condition. | ||||||
|  |  * @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates | ||||||
|  |  *                        to true. | ||||||
|  |  */ | ||||||
|  | export function authorize<P, ResBody, ReqBody, ReqQuery, Locals extends Record<string, unknown>>( | ||||||
|  |     accessCondition: (auth: AuthenticationInfo, req: AuthenticatedRequest<P, ResBody, ReqBody, ReqQuery, Locals>) => boolean | Promise<boolean> | ||||||
|  | ): RequestHandler<P, ResBody, ReqBody, ReqQuery, Locals> { | ||||||
|  |     // Bypass authentication during testing
 | ||||||
|  |     if (getEnvVar(envVars.RunMode) === 'test') { | ||||||
|  |         return async ( | ||||||
|  |             _req: AuthenticatedRequest<P, ResBody, ReqBody, ReqQuery, Locals>, | ||||||
|  |             _res: express.Response, | ||||||
|  |             next: express.NextFunction | ||||||
|  |         ): Promise<void> => { | ||||||
|  |             next(); | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return async ( | ||||||
|  |         req: AuthenticatedRequest<P, ResBody, ReqBody, ReqQuery, Locals>, | ||||||
|  |         _res: express.Response, | ||||||
|  |         next: express.NextFunction | ||||||
|  |     ): Promise<void> => { | ||||||
|  |         if (!req.auth) { | ||||||
|  |             throw new UnauthorizedException(); | ||||||
|  |         } else if (!(await accessCondition(req.auth, req))) { | ||||||
|  |             throw new ForbiddenException(); | ||||||
|  |         } else { | ||||||
|  |             next(); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Middleware which rejects all unauthenticated users, but accepts all authenticated users. | ||||||
|  |  */ | ||||||
|  | export const authenticatedOnly = authorize((_) => true); | ||||||
|  | /** | ||||||
|  |  * Middleware which rejects requests from unauthenticated users or users that aren't students. | ||||||
|  |  */ | ||||||
|  | export const studentsOnly = authorize((auth) => auth.accountType === AccountType.Student); | ||||||
|  | /** | ||||||
|  |  * Middleware which rejects requests from unauthenticated users or users that aren't teachers. | ||||||
|  |  */ | ||||||
|  | export const teachersOnly = authorize((auth) => auth.accountType === 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); | ||||||
							
								
								
									
										70
									
								
								backend/src/middleware/auth/checks/class-auth-checks.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								backend/src/middleware/auth/checks/class-auth-checks.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,70 @@ | ||||||
|  | import { authorize } from './auth-checks.js'; | ||||||
|  | import { AuthenticationInfo } from '../authentication-info.js'; | ||||||
|  | import { AuthenticatedRequest } from '../authenticated-request.js'; | ||||||
|  | import { fetchClass } from '../../../services/classes.js'; | ||||||
|  | import { mapToUsername } from '../../../interfaces/user.js'; | ||||||
|  | import { getAllInvitations } from '../../../services/teacher-invitations.js'; | ||||||
|  | import { AccountType } from '@dwengo-1/common/util/account-types'; | ||||||
|  | 
 | ||||||
|  | async function teaches(teacherUsername: string, classId: string): Promise<boolean> { | ||||||
|  |     const clazz = await fetchClass(classId); | ||||||
|  |     return clazz.teachers.map(mapToUsername).includes(teacherUsername); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * To be used on a request with path parameters username and classId. | ||||||
|  |  * Only allows requests whose username parameter is equal to the username of the user who is logged in and requests | ||||||
|  |  * whose classId parameter references a class the logged-in user is a teacher of. | ||||||
|  |  */ | ||||||
|  | export const onlyAllowStudentHimselfAndTeachersOfClass = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { | ||||||
|  |     if (req.params.username === auth.username) { | ||||||
|  |         return true; | ||||||
|  |     } else if (auth.accountType === AccountType.Teacher) { | ||||||
|  |         return teaches(auth.username, req.params.classId); | ||||||
|  |     } | ||||||
|  |     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) | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Only let the request pass through if the class id in it refers to a class the current user is in (as a student | ||||||
|  |  * or teacher) | ||||||
|  |  */ | ||||||
|  | export const onlyAllowIfInClass = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { | ||||||
|  |     const classId = req.params.classId ?? req.params.classid ?? req.params.id; | ||||||
|  |     const clazz = await fetchClass(classId); | ||||||
|  |     if (auth.accountType === AccountType.Teacher) { | ||||||
|  |         return clazz.teachers.map(mapToUsername).includes(auth.username); | ||||||
|  |     } | ||||||
|  |     return clazz.students.map(mapToUsername).includes(auth.username); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | export const onlyAllowIfInClassOrInvited = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { | ||||||
|  |     const classId = req.params.classId ?? req.params.classid ?? req.params.id; | ||||||
|  |     const clazz = await fetchClass(classId); | ||||||
|  |     if (auth.accountType === AccountType.Teacher) { | ||||||
|  |         const invitations = await getAllInvitations(auth.username, false); | ||||||
|  |         return clazz.teachers.map(mapToUsername).includes(auth.username) || invitations.some((invitation) => invitation.classId === classId); | ||||||
|  |     } | ||||||
|  |     return clazz.students.map(mapToUsername).includes(auth.username); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Only allows the request to pass if the 'class' property in its body is a class the current user is a member of. | ||||||
|  |  */ | ||||||
|  | export const onlyAllowOwnClassInBody = authorize(async (auth, req) => { | ||||||
|  |     const classId = (req.body as { class: string })?.class; | ||||||
|  |     const clazz = await fetchClass(classId); | ||||||
|  | 
 | ||||||
|  |     if (auth.accountType === AccountType.Teacher) { | ||||||
|  |         return clazz.teachers.map(mapToUsername).includes(auth.username); | ||||||
|  |     } | ||||||
|  |     return clazz.students.map(mapToUsername).includes(auth.username); | ||||||
|  | }); | ||||||
							
								
								
									
										26
									
								
								backend/src/middleware/auth/checks/group-auth-checker.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								backend/src/middleware/auth/checks/group-auth-checker.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,26 @@ | ||||||
|  | import { authorize } from './auth-checks.js'; | ||||||
|  | import { fetchClass } from '../../../services/classes.js'; | ||||||
|  | import { fetchGroup } from '../../../services/groups.js'; | ||||||
|  | import { mapToUsername } from '../../../interfaces/user.js'; | ||||||
|  | import { AccountType } from '@dwengo-1/common/util/account-types'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Expects the path to contain the path parameters 'classid', 'assignmentid' and 'groupid'. | ||||||
|  |  * Only allows requests from users who are | ||||||
|  |  * - either teachers of the class the assignment for the group was posted in, | ||||||
|  |  * - or students in the group | ||||||
|  |  */ | ||||||
|  | export const onlyAllowIfHasAccessToGroup = authorize(async (auth, req) => { | ||||||
|  |     const { | ||||||
|  |         classid: classId, | ||||||
|  |         assignmentid: assignmentId, | ||||||
|  |         groupid: groupId, | ||||||
|  |     } = req.params as { classid: string; assignmentid: number; groupid: number }; | ||||||
|  | 
 | ||||||
|  |     if (auth.accountType === AccountType.Teacher) { | ||||||
|  |         const clazz = await fetchClass(classId); | ||||||
|  |         return clazz.teachers.map(mapToUsername).includes(auth.username); | ||||||
|  |     } // User is student
 | ||||||
|  |     const group = await fetchGroup(classId, assignmentId, groupId); | ||||||
|  |     return group.members.map(mapToUsername).includes(auth.username); | ||||||
|  | }); | ||||||
|  | @ -0,0 +1,21 @@ | ||||||
|  | import { authorize } from './auth-checks'; | ||||||
|  | import { AuthenticationInfo } from '../authentication-info'; | ||||||
|  | import { AuthenticatedRequest } from '../authenticated-request'; | ||||||
|  | import { AccountType } from '@dwengo-1/common/util/account-types'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Only allows requests whose learning path personalization query parameters ('forGroup' / 'assignmentNo' / 'classId') | ||||||
|  |  * are | ||||||
|  |  * - either not set | ||||||
|  |  * - or set to a group the user is in, | ||||||
|  |  * - or set to anything if the user is a teacher. | ||||||
|  |  */ | ||||||
|  | export const onlyAllowPersonalizationForOwnGroup = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { | ||||||
|  |     const { forGroup, assignmentNo, classId } = req.params; | ||||||
|  |     if (auth.accountType === AccountType.Student && forGroup && assignmentNo && classId) { | ||||||
|  |         // TODO: groupNumber?
 | ||||||
|  |         // Const group = await fetchGroup(Number(classId), Number(assignmentNo), )
 | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |     return true; | ||||||
|  | }); | ||||||
							
								
								
									
										66
									
								
								backend/src/middleware/auth/checks/question-checks.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								backend/src/middleware/auth/checks/question-checks.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,66 @@ | ||||||
|  | import { authorize } from './auth-checks.js'; | ||||||
|  | import { AuthenticationInfo } from '../authentication-info.js'; | ||||||
|  | import { AuthenticatedRequest } from '../authenticated-request.js'; | ||||||
|  | import { requireFields } from '../../../controllers/error-helper.js'; | ||||||
|  | import { getLearningObjectId, getQuestionId } from '../../../controllers/questions.js'; | ||||||
|  | import { fetchQuestion } from '../../../services/questions.js'; | ||||||
|  | import { FALLBACK_SEQ_NUM } from '../../../config.js'; | ||||||
|  | import { fetchAnswer } from '../../../services/answers.js'; | ||||||
|  | import { mapToUsername } from '../../../interfaces/user.js'; | ||||||
|  | import { AccountType } from '@dwengo-1/common/util/account-types'; | ||||||
|  | 
 | ||||||
|  | export const onlyAllowAuthor = authorize( | ||||||
|  |     (auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { author: string }).author === auth.username | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | export const onlyAllowAuthorRequest = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { | ||||||
|  |     const hruid = req.params.hruid; | ||||||
|  |     const version = req.params.version; | ||||||
|  |     const language = req.query.lang as string; | ||||||
|  |     const seq = req.params.seq; | ||||||
|  |     requireFields({ hruid }); | ||||||
|  | 
 | ||||||
|  |     const learningObjectId = getLearningObjectId(hruid, version, language); | ||||||
|  |     const questionId = getQuestionId(learningObjectId, seq); | ||||||
|  | 
 | ||||||
|  |     const question = await fetchQuestion(questionId); | ||||||
|  | 
 | ||||||
|  |     return question.author.username === auth.username; | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | export const onlyAllowAuthorRequestAnswer = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { | ||||||
|  |     const hruid = req.params.hruid; | ||||||
|  |     const version = req.params.version; | ||||||
|  |     const language = req.query.lang as string; | ||||||
|  |     const seq = req.params.seq; | ||||||
|  |     const seqAnswer = req.params.seqAnswer; | ||||||
|  |     requireFields({ hruid }); | ||||||
|  | 
 | ||||||
|  |     const learningObjectId = getLearningObjectId(hruid, version, language); | ||||||
|  |     const questionId = getQuestionId(learningObjectId, seq); | ||||||
|  | 
 | ||||||
|  |     const sequenceNumber = Number(seqAnswer) || FALLBACK_SEQ_NUM; | ||||||
|  |     const answer = await fetchAnswer(questionId, sequenceNumber); | ||||||
|  | 
 | ||||||
|  |     return answer.author.username === auth.username; | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | export const onlyAllowIfHasAccessToQuestion = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { | ||||||
|  |     const hruid = req.params.hruid; | ||||||
|  |     const version = req.params.version; | ||||||
|  |     const language = req.query.lang as string; | ||||||
|  |     const seq = req.params.seq; | ||||||
|  |     requireFields({ hruid }); | ||||||
|  | 
 | ||||||
|  |     const learningObjectId = getLearningObjectId(hruid, version, language); | ||||||
|  |     const questionId = getQuestionId(learningObjectId, seq); | ||||||
|  | 
 | ||||||
|  |     const question = await fetchQuestion(questionId); | ||||||
|  |     const group = question.inGroup; | ||||||
|  | 
 | ||||||
|  |     if (auth.accountType === AccountType.Teacher) { | ||||||
|  |         const cls = group.assignment.within; // TODO check if contains full objects
 | ||||||
|  |         return cls.teachers.map(mapToUsername).includes(auth.username); | ||||||
|  |     } // User is student
 | ||||||
|  |     return group.members.map(mapToUsername).includes(auth.username); | ||||||
|  | }); | ||||||
							
								
								
									
										28
									
								
								backend/src/middleware/auth/checks/submission-checks.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								backend/src/middleware/auth/checks/submission-checks.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | ||||||
|  | import { languageMap } from '@dwengo-1/common/util/language'; | ||||||
|  | import { LearningObjectIdentifier } from '../../../entities/content/learning-object-identifier.js'; | ||||||
|  | import { fetchSubmission } from '../../../services/submissions.js'; | ||||||
|  | import { AuthenticatedRequest } from '../authenticated-request.js'; | ||||||
|  | import { AuthenticationInfo } from '../authentication-info.js'; | ||||||
|  | import { authorize } from './auth-checks.js'; | ||||||
|  | import { FALLBACK_LANG } from '../../../config.js'; | ||||||
|  | import { mapToUsername } from '../../../interfaces/user.js'; | ||||||
|  | import { AccountType } from '@dwengo-1/common/util/account-types'; | ||||||
|  | 
 | ||||||
|  | export const onlyAllowSubmitter = authorize( | ||||||
|  |     (auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { submitter: string }).submitter === auth.username | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | export const onlyAllowIfHasAccessToSubmission = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { | ||||||
|  |     const { hruid: lohruid, id: submissionNumber } = req.params; | ||||||
|  |     const { language: lang, version: version } = req.query; | ||||||
|  | 
 | ||||||
|  |     const loId = new LearningObjectIdentifier(lohruid, languageMap[lang as string] ?? FALLBACK_LANG, Number(version)); | ||||||
|  |     const submission = await fetchSubmission(loId, Number(submissionNumber)); | ||||||
|  | 
 | ||||||
|  |     if (auth.accountType === AccountType.Teacher) { | ||||||
|  |         // Dit kan niet werken om dat al deze objecten niet gepopulate zijn.
 | ||||||
|  |         return submission.onBehalfOf.assignment.within.teachers.map(mapToUsername).includes(auth.username); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return submission.onBehalfOf.members.map(mapToUsername).includes(auth.username); | ||||||
|  | }); | ||||||
|  | @ -0,0 +1,17 @@ | ||||||
|  | import { authorize } from './auth-checks.js'; | ||||||
|  | import { AuthenticationInfo } from '../authentication-info.js'; | ||||||
|  | import { AuthenticatedRequest } from '../authenticated-request.js'; | ||||||
|  | 
 | ||||||
|  | export const onlyAllowSenderOrReceiver = authorize( | ||||||
|  |     (auth: AuthenticationInfo, req: AuthenticatedRequest) => req.params.sender === auth.username || req.params.receiver === auth.username | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | export const onlyAllowSender = authorize((auth: AuthenticationInfo, req: AuthenticatedRequest) => req.params.sender === auth.username); | ||||||
|  | 
 | ||||||
|  | export const onlyAllowSenderBody = authorize( | ||||||
|  |     (auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { sender: string }).sender === auth.username | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | export const onlyAllowReceiverBody = authorize( | ||||||
|  |     (auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { receiver: string }).receiver === auth.username | ||||||
|  | ); | ||||||
							
								
								
									
										8
									
								
								backend/src/middleware/auth/checks/user-auth-checks.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								backend/src/middleware/auth/checks/user-auth-checks.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | ||||||
|  | import { authorize } from './auth-checks.js'; | ||||||
|  | import { AuthenticationInfo } from '../authentication-info.js'; | ||||||
|  | import { AuthenticatedRequest } from '../authenticated-request.js'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Only allow the user whose username is in the path parameter "username" to access the endpoint. | ||||||
|  |  */ | ||||||
|  | export const preventImpersonation = authorize((auth: AuthenticationInfo, req: AuthenticatedRequest) => req.params.username === auth.username); | ||||||
|  | @ -1,16 +1,18 @@ | ||||||
| import express from 'express'; | import express from 'express'; | ||||||
| import { createAnswerHandler, deleteAnswerHandler, getAnswerHandler, getAllAnswersHandler, updateAnswerHandler } from '../controllers/answers.js'; | import { createAnswerHandler, deleteAnswerHandler, getAnswerHandler, getAllAnswersHandler, updateAnswerHandler } from '../controllers/answers.js'; | ||||||
|  | import { authenticatedOnly, teachersOnly } from '../middleware/auth/checks/auth-checks.js'; | ||||||
|  | import { onlyAllowAuthor, onlyAllowAuthorRequestAnswer, onlyAllowIfHasAccessToQuestion } from '../middleware/auth/checks/question-checks.js'; | ||||||
| 
 | 
 | ||||||
| const router = express.Router({ mergeParams: true }); | const router = express.Router({ mergeParams: true }); | ||||||
| 
 | 
 | ||||||
| router.get('/', getAllAnswersHandler); | router.get('/', authenticatedOnly, getAllAnswersHandler); | ||||||
| 
 | 
 | ||||||
| router.post('/', createAnswerHandler); | router.post('/', teachersOnly, onlyAllowAuthor, createAnswerHandler); | ||||||
| 
 | 
 | ||||||
| router.get('/:seqAnswer', getAnswerHandler); | router.get('/:seqAnswer', onlyAllowIfHasAccessToQuestion, getAnswerHandler); | ||||||
| 
 | 
 | ||||||
| router.delete('/:seqAnswer', deleteAnswerHandler); | router.delete('/:seqAnswer', teachersOnly, onlyAllowAuthorRequestAnswer, deleteAnswerHandler); | ||||||
| 
 | 
 | ||||||
| router.put('/:seqAnswer', updateAnswerHandler); | router.put('/:seqAnswer', teachersOnly, onlyAllowAuthorRequestAnswer, updateAnswerHandler); | ||||||
| 
 | 
 | ||||||
| export default router; | export default router; | ||||||
|  |  | ||||||
|  | @ -9,22 +9,25 @@ import { | ||||||
|     putAssignmentHandler, |     putAssignmentHandler, | ||||||
| } from '../controllers/assignments.js'; | } from '../controllers/assignments.js'; | ||||||
| import groupRouter from './groups.js'; | import groupRouter from './groups.js'; | ||||||
|  | import { teachersOnly } from '../middleware/auth/checks/auth-checks.js'; | ||||||
|  | import { onlyAllowIfInClass } from '../middleware/auth/checks/class-auth-checks.js'; | ||||||
|  | import { onlyAllowIfHasAccessToAssignment } from '../middleware/auth/checks/assignment-auth-checks.js'; | ||||||
| 
 | 
 | ||||||
| const router = express.Router({ mergeParams: true }); | const router = express.Router({ mergeParams: true }); | ||||||
| 
 | 
 | ||||||
| router.get('/', getAllAssignmentsHandler); | router.get('/', teachersOnly, onlyAllowIfInClass, getAllAssignmentsHandler); | ||||||
| 
 | 
 | ||||||
| router.post('/', createAssignmentHandler); | router.post('/', teachersOnly, onlyAllowIfInClass, createAssignmentHandler); | ||||||
| 
 | 
 | ||||||
| router.get('/:id', getAssignmentHandler); | router.get('/:id', onlyAllowIfHasAccessToAssignment, getAssignmentHandler); | ||||||
| 
 | 
 | ||||||
| router.put('/:id', putAssignmentHandler); | router.put('/:id', teachersOnly, onlyAllowIfHasAccessToAssignment, putAssignmentHandler); | ||||||
| 
 | 
 | ||||||
| router.delete('/:id', deleteAssignmentHandler); | router.delete('/:id', teachersOnly, onlyAllowIfHasAccessToAssignment, deleteAssignmentHandler); | ||||||
| 
 | 
 | ||||||
| router.get('/:id/submissions', getAssignmentsSubmissionsHandler); | router.get('/:id/submissions', teachersOnly, onlyAllowIfHasAccessToAssignment, getAssignmentsSubmissionsHandler); | ||||||
| 
 | 
 | ||||||
| router.get('/:id/questions', getAssignmentQuestionsHandler); | router.get('/:id/questions', teachersOnly, onlyAllowIfHasAccessToAssignment, getAssignmentQuestionsHandler); | ||||||
| 
 | 
 | ||||||
| router.use('/:assignmentid/groups', groupRouter); | router.use('/:assignmentid/groups', groupRouter); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,28 +1,35 @@ | ||||||
| import express from 'express'; | import express from 'express'; | ||||||
| import { getFrontendAuthConfig, postHelloHandler } from '../controllers/auth.js'; | import { handleGetFrontendAuthConfig, postHelloHandler } from '../controllers/auth.js'; | ||||||
| import { authenticatedOnly, studentsOnly, teachersOnly } from '../middleware/auth/auth.js'; | import { authenticatedOnly, studentsOnly, teachersOnly } from '../middleware/auth/checks/auth-checks.js'; | ||||||
|  | 
 | ||||||
| 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()); |  | ||||||
| }); |  | ||||||
| 
 | 
 | ||||||
| router.get('/testAuthenticatedOnly', authenticatedOnly, (_req, res) => { | router.get('/testAuthenticatedOnly', authenticatedOnly, (_req, res) => { | ||||||
|     /* #swagger.security = [{ "student": [ ] }, { "teacher": [ ] }] */ |     /* #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */ | ||||||
|     res.json({ message: 'If you see this, you should be authenticated!' }); |     res.json({ message: 'If you see this, you should be authenticated!' }); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| router.get('/testStudentsOnly', studentsOnly, (_req, res) => { | router.get('/testStudentsOnly', studentsOnly, (_req, res) => { | ||||||
|     /* #swagger.security = [{ "student": [ ] }] */ |     /* #swagger.security = [{ "studentProduction": [ ] }, { "studentStaging": [ ] }, { "studentDev": [ ] }] */ | ||||||
|     res.json({ message: 'If you see this, you should be a student!' }); |     res.json({ message: 'If you see this, you should be a student!' }); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| router.get('/testTeachersOnly', teachersOnly, (_req, res) => { | router.get('/testTeachersOnly', teachersOnly, (_req, res) => { | ||||||
|     /* #swagger.security = [{ "teacher": [ ] }] */ |     /* #swagger.security = [{ "teacherProduction": [ ] }, { "teacherStaging": [ ] }, { "teacherDev": [ ] }] */ | ||||||
|     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); | // 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, | ||||||
|  |     /* | ||||||
|  |     #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] | ||||||
|  | */ postHelloHandler | ||||||
|  | ); | ||||||
| 
 | 
 | ||||||
| export default router; | export default router; | ||||||
|  |  | ||||||
|  | @ -14,33 +14,35 @@ import { | ||||||
|     putClassHandler, |     putClassHandler, | ||||||
| } from '../controllers/classes.js'; | } from '../controllers/classes.js'; | ||||||
| import assignmentRouter from './assignments.js'; | import assignmentRouter from './assignments.js'; | ||||||
|  | import { adminOnly, teachersOnly } from '../middleware/auth/checks/auth-checks.js'; | ||||||
|  | import { onlyAllowIfInClass, onlyAllowIfInClassOrInvited } from '../middleware/auth/checks/class-auth-checks.js'; | ||||||
| 
 | 
 | ||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
| 
 | 
 | ||||||
| // Root endpoint used to search objects
 | router.get('/', adminOnly, getAllClassesHandler); | ||||||
| router.get('/', getAllClassesHandler); |  | ||||||
| 
 | 
 | ||||||
| router.post('/', createClassHandler); | router.post('/', teachersOnly, createClassHandler); | ||||||
| 
 | 
 | ||||||
| router.get('/:id', getClassHandler); | router.get('/:id', onlyAllowIfInClassOrInvited, getClassHandler); | ||||||
| 
 | 
 | ||||||
| router.put('/:id', putClassHandler); | router.put('/:id', teachersOnly, onlyAllowIfInClass, putClassHandler); | ||||||
| 
 | 
 | ||||||
| router.delete('/:id', deleteClassHandler); | router.delete('/:id', teachersOnly, onlyAllowIfInClass, deleteClassHandler); | ||||||
| 
 | 
 | ||||||
| router.get('/:id/teacher-invitations', getTeacherInvitationsHandler); | router.get('/:id/teacher-invitations', teachersOnly, onlyAllowIfInClass, getTeacherInvitationsHandler); | ||||||
| 
 | 
 | ||||||
| router.get('/:id/students', getClassStudentsHandler); | router.get('/:id/students', onlyAllowIfInClass, getClassStudentsHandler); | ||||||
| 
 | 
 | ||||||
| router.post('/:id/students', addClassStudentHandler); | router.post('/:id/students', teachersOnly, onlyAllowIfInClass, addClassStudentHandler); | ||||||
| 
 | 
 | ||||||
| router.delete('/:id/students/:username', deleteClassStudentHandler); | router.delete('/:id/students/:username', teachersOnly, onlyAllowIfInClass, deleteClassStudentHandler); | ||||||
| 
 | 
 | ||||||
| router.get('/:id/teachers', getClassTeachersHandler); | router.get('/:id/teachers', onlyAllowIfInClass, getClassTeachersHandler); | ||||||
| 
 | 
 | ||||||
| router.post('/:id/teachers', addClassTeacherHandler); | // De combinatie van deze POST en DELETE endpoints kan lethal zijn
 | ||||||
|  | router.post('/:id/teachers', teachersOnly, onlyAllowIfInClass, addClassTeacherHandler); | ||||||
| 
 | 
 | ||||||
| router.delete('/:id/teachers/:username', deleteClassTeacherHandler); | router.delete('/:id/teachers/:username', teachersOnly, onlyAllowIfInClass, deleteClassTeacherHandler); | ||||||
| 
 | 
 | ||||||
| router.use('/:classid/assignments', assignmentRouter); | router.use('/:classid/assignments', assignmentRouter); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -8,22 +8,24 @@ import { | ||||||
|     getGroupSubmissionsHandler, |     getGroupSubmissionsHandler, | ||||||
|     putGroupHandler, |     putGroupHandler, | ||||||
| } from '../controllers/groups.js'; | } from '../controllers/groups.js'; | ||||||
|  | import { onlyAllowIfHasAccessToGroup } from '../middleware/auth/checks/group-auth-checker.js'; | ||||||
|  | import { teachersOnly } from '../middleware/auth/checks/auth-checks.js'; | ||||||
|  | import { onlyAllowIfHasAccessToAssignment } from '../middleware/auth/checks/assignment-auth-checks.js'; | ||||||
| 
 | 
 | ||||||
| const router = express.Router({ mergeParams: true }); | const router = express.Router({ mergeParams: true }); | ||||||
| 
 | 
 | ||||||
| // Root endpoint used to search objects
 | router.get('/', onlyAllowIfHasAccessToAssignment, getAllGroupsHandler); | ||||||
| router.get('/', getAllGroupsHandler); |  | ||||||
| 
 | 
 | ||||||
| router.post('/', createGroupHandler); | router.post('/', teachersOnly, onlyAllowIfHasAccessToAssignment, createGroupHandler); | ||||||
| 
 | 
 | ||||||
| router.get('/:groupid', getGroupHandler); | router.get('/:groupid', onlyAllowIfHasAccessToAssignment, getGroupHandler); | ||||||
| 
 | 
 | ||||||
| router.put('/:groupid', putGroupHandler); | router.put('/:groupid', teachersOnly, onlyAllowIfHasAccessToAssignment, putGroupHandler); | ||||||
| 
 | 
 | ||||||
| router.delete('/:groupid', deleteGroupHandler); | router.delete('/:groupid', teachersOnly, onlyAllowIfHasAccessToAssignment, deleteGroupHandler); | ||||||
| 
 | 
 | ||||||
| router.get('/:groupid/submissions', getGroupSubmissionsHandler); | router.get('/:groupid/submissions', onlyAllowIfHasAccessToGroup, getGroupSubmissionsHandler); | ||||||
| 
 | 
 | ||||||
| router.get('/:groupid/questions', getGroupQuestionsHandler); | router.get('/:groupid/questions', onlyAllowIfHasAccessToGroup, getGroupQuestionsHandler); | ||||||
| 
 | 
 | ||||||
| export default router; | export default router; | ||||||
|  |  | ||||||
|  | @ -1,8 +1,8 @@ | ||||||
| import express from 'express'; | import express from 'express'; | ||||||
| import { getAllLearningObjects, getAttachment, getLearningObject, getLearningObjectHTML } from '../controllers/learning-objects.js'; | import { getAllLearningObjects, getAttachment, getLearningObject, getLearningObjectHTML } from '../controllers/learning-objects.js'; | ||||||
| 
 |  | ||||||
| import submissionRoutes from './submissions.js'; | import submissionRoutes from './submissions.js'; | ||||||
| import questionRoutes from './questions.js'; | import questionRoutes from './questions.js'; | ||||||
|  | import { authenticatedOnly } from '../middleware/auth/checks/auth-checks.js'; | ||||||
| 
 | 
 | ||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
| 
 | 
 | ||||||
|  | @ -16,13 +16,13 @@ const router = express.Router(); | ||||||
| 
 | 
 | ||||||
| // Route 2: list of object data
 | // Route 2: list of object data
 | ||||||
| // Example 2: http://localhost:3000/learningObject?full=true&hruid=un_artificiele_intelligentie
 | // Example 2: http://localhost:3000/learningObject?full=true&hruid=un_artificiele_intelligentie
 | ||||||
| router.get('/', getAllLearningObjects); | router.get('/', authenticatedOnly, getAllLearningObjects); | ||||||
| 
 | 
 | ||||||
| // Parameter: hruid of learning object
 | // Parameter: hruid of learning object
 | ||||||
| // Query: language
 | // Query: language
 | ||||||
| // Route to fetch data of one learning object based on its hruid
 | // Route to fetch data of one learning object based on its hruid
 | ||||||
| // Example: http://localhost:3000/learningObject/un_ai7
 | // Example: http://localhost:3000/learningObject/un_ai7
 | ||||||
| router.get('/:hruid', getLearningObject); | router.get('/:hruid', authenticatedOnly, getLearningObject); | ||||||
| 
 | 
 | ||||||
| router.use('/:hruid/submissions', submissionRoutes); | router.use('/:hruid/submissions', submissionRoutes); | ||||||
| 
 | 
 | ||||||
|  | @ -32,12 +32,12 @@ router.use('/:hruid/:version/questions', questionRoutes); | ||||||
| // Query: language, version (optional)
 | // Query: language, version (optional)
 | ||||||
| // Route to fetch the HTML rendering of one learning object based on its hruid.
 | // Route to fetch the HTML rendering of one learning object based on its hruid.
 | ||||||
| // Example: http://localhost:3000/learningObject/un_ai7/html
 | // Example: http://localhost:3000/learningObject/un_ai7/html
 | ||||||
| router.get('/:hruid/html', getLearningObjectHTML); | router.get('/:hruid/html', authenticatedOnly, getLearningObjectHTML); | ||||||
| 
 | 
 | ||||||
| // Parameter: hruid of learning object, name of attachment.
 | // Parameter: hruid of learning object, name of attachment.
 | ||||||
| // Query: language, version (optional).
 | // Query: language, version (optional).
 | ||||||
| // Route to get the raw data of the attachment for one learning object based on its hruid.
 | // Route to get the raw data of the attachment for one learning object based on its hruid.
 | ||||||
| // Example: http://localhost:3000/learningObject/u_test/attachment/testimage.png
 | // Example: http://localhost:3000/learningObject/u_test/attachment/testimage.png
 | ||||||
| router.get('/:hruid/html/:attachmentName', getAttachment); | router.get('/:hruid/html/:attachmentName', authenticatedOnly, getAttachment); | ||||||
| 
 | 
 | ||||||
| export default router; | export default router; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| import express from 'express'; | import express from 'express'; | ||||||
| import { getLearningPaths } from '../controllers/learning-paths.js'; | import { getLearningPaths } from '../controllers/learning-paths.js'; | ||||||
|  | import { authenticatedOnly } from '../middleware/auth/checks/auth-checks.js'; | ||||||
| 
 | 
 | ||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
| 
 | 
 | ||||||
|  | @ -22,6 +23,6 @@ const router = express.Router(); | ||||||
| // Route to fetch learning paths based on a theme
 | // Route to fetch learning paths based on a theme
 | ||||||
| // Example: http://localhost:3000/learningPath?theme=kiks
 | // Example: http://localhost:3000/learningPath?theme=kiks
 | ||||||
| 
 | 
 | ||||||
| router.get('/', getLearningPaths); | router.get('/', authenticatedOnly, getLearningPaths); | ||||||
| 
 | 
 | ||||||
| export default router; | export default router; | ||||||
|  |  | ||||||
|  | @ -1,20 +1,25 @@ | ||||||
| import express from 'express'; | import express from 'express'; | ||||||
| import { createQuestionHandler, deleteQuestionHandler, getAllQuestionsHandler, getQuestionHandler } from '../controllers/questions.js'; | import { createQuestionHandler, deleteQuestionHandler, getAllQuestionsHandler, getQuestionHandler } from '../controllers/questions.js'; | ||||||
| import answerRoutes from './answers.js'; | import answerRoutes from './answers.js'; | ||||||
|  | import { authenticatedOnly, studentsOnly } from '../middleware/auth/checks/auth-checks.js'; | ||||||
|  | import { updateAnswerHandler } from '../controllers/answers.js'; | ||||||
|  | import { onlyAllowAuthor, onlyAllowAuthorRequest, onlyAllowIfHasAccessToQuestion } from '../middleware/auth/checks/question-checks.js'; | ||||||
| 
 | 
 | ||||||
| const router = express.Router({ mergeParams: true }); | const router = express.Router({ mergeParams: true }); | ||||||
| 
 | 
 | ||||||
| // Query language
 | // Query language
 | ||||||
| 
 | 
 | ||||||
| // Root endpoint used to search objects
 | // Root endpoint used to search objects
 | ||||||
| router.get('/', getAllQuestionsHandler); | router.get('/', authenticatedOnly, getAllQuestionsHandler); | ||||||
| 
 | 
 | ||||||
| router.post('/', createQuestionHandler); | router.post('/', studentsOnly, onlyAllowAuthor, createQuestionHandler); | ||||||
| 
 |  | ||||||
| router.delete('/:seq', deleteQuestionHandler); |  | ||||||
| 
 | 
 | ||||||
| // Information about a question with id
 | // Information about a question with id
 | ||||||
| router.get('/:seq', getQuestionHandler); | router.get('/:seq', onlyAllowIfHasAccessToQuestion, getQuestionHandler); | ||||||
|  | 
 | ||||||
|  | router.delete('/:seq', studentsOnly, onlyAllowAuthorRequest, deleteQuestionHandler); | ||||||
|  | 
 | ||||||
|  | router.put('/:seq', studentsOnly, onlyAllowAuthorRequest, updateAnswerHandler); | ||||||
| 
 | 
 | ||||||
| router.use('/:seq/answers', answerRoutes); | router.use('/:seq/answers', answerRoutes); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -18,12 +18,30 @@ router.get('/', (_, res: Response) => { | ||||||
|     }); |     }); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| router.use('/student', studentRouter /* #swagger.tags = ['Student'] */); |  | ||||||
| router.use('/teacher', teacherRouter /* #swagger.tags = ['Teacher'] */); |  | ||||||
| router.use('/class', classRouter /* #swagger.tags = ['Class'] */); |  | ||||||
| router.use('/auth', authRouter /* #swagger.tags = ['Auth'] */); | router.use('/auth', authRouter /* #swagger.tags = ['Auth'] */); | ||||||
| router.use('/theme', themeRoutes /* #swagger.tags = ['Theme'] */); | router.use( | ||||||
| router.use('/learningPath', learningPathRoutes /* #swagger.tags = ['Learning Path'] */); |     '/class', | ||||||
| router.use('/learningObject', learningObjectRoutes /* #swagger.tags = ['Learning Object'] */); |     classRouter /* #swagger.tags = ['Class'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */ | ||||||
|  | ); | ||||||
|  | router.use( | ||||||
|  |     '/learningObject', | ||||||
|  |     learningObjectRoutes /* #swagger.tags = ['Learning Object'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */ | ||||||
|  | ); | ||||||
|  | router.use( | ||||||
|  |     '/learningPath', | ||||||
|  |     learningPathRoutes /* #swagger.tags = ['Learning Path'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */ | ||||||
|  | ); | ||||||
|  | router.use( | ||||||
|  |     '/student', | ||||||
|  |     studentRouter /* #swagger.tags = ['Student'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */ | ||||||
|  | ); | ||||||
|  | router.use( | ||||||
|  |     '/teacher', | ||||||
|  |     teacherRouter /* #swagger.tags = ['Teacher'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */ | ||||||
|  | ); | ||||||
|  | router.use( | ||||||
|  |     '/theme', | ||||||
|  |     themeRoutes /* #swagger.tags = ['Theme'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */ | ||||||
|  | ); | ||||||
| 
 | 
 | ||||||
| export default router; | export default router; | ||||||
|  |  | ||||||
|  | @ -5,15 +5,19 @@ import { | ||||||
|     getStudentRequestHandler, |     getStudentRequestHandler, | ||||||
|     getStudentRequestsHandler, |     getStudentRequestsHandler, | ||||||
| } from '../controllers/students.js'; | } from '../controllers/students.js'; | ||||||
|  | import { preventImpersonation } from '../middleware/auth/checks/user-auth-checks.js'; | ||||||
|  | import { onlyAllowStudentHimselfAndTeachersOfClass } from '../middleware/auth/checks/class-auth-checks.js'; | ||||||
|  | 
 | ||||||
|  | // Under /:username/joinRequests/
 | ||||||
| 
 | 
 | ||||||
| const router = express.Router({ mergeParams: true }); | const router = express.Router({ mergeParams: true }); | ||||||
| 
 | 
 | ||||||
| router.get('/', getStudentRequestsHandler); | router.get('/', preventImpersonation, getStudentRequestsHandler); | ||||||
| 
 | 
 | ||||||
| router.post('/', createStudentRequestHandler); | router.post('/', preventImpersonation, createStudentRequestHandler); | ||||||
| 
 | 
 | ||||||
| router.get('/:classId', getStudentRequestHandler); | router.get('/:classId', onlyAllowStudentHimselfAndTeachersOfClass, getStudentRequestHandler); | ||||||
| 
 | 
 | ||||||
| router.delete('/:classId', deleteClassJoinRequestHandler); | router.delete('/:classId', onlyAllowStudentHimselfAndTeachersOfClass, deleteClassJoinRequestHandler); | ||||||
| 
 | 
 | ||||||
| export default router; | export default router; | ||||||
|  |  | ||||||
|  | @ -11,33 +11,37 @@ import { | ||||||
|     getStudentSubmissionsHandler, |     getStudentSubmissionsHandler, | ||||||
| } from '../controllers/students.js'; | } from '../controllers/students.js'; | ||||||
| import joinRequestRouter from './student-join-requests.js'; | import joinRequestRouter from './student-join-requests.js'; | ||||||
|  | import { preventImpersonation } from '../middleware/auth/checks/user-auth-checks.js'; | ||||||
|  | import { adminOnly } from '../middleware/auth/checks/auth-checks.js'; | ||||||
| 
 | 
 | ||||||
| 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', deleteStudentHandler); | router.delete('/:username', preventImpersonation, deleteStudentHandler); | ||||||
| 
 | 
 | ||||||
| // Information about a student's profile
 | // Information about a student's profile
 | ||||||
| router.get('/:username', getStudentHandler); | router.get('/:username', preventImpersonation, getStudentHandler); | ||||||
| 
 | 
 | ||||||
| // The list of classes a student is in
 | // The list of classes a student is in
 | ||||||
| router.get('/:username/classes', getStudentClassesHandler); | router.get('/:username/classes', preventImpersonation, getStudentClassesHandler); | ||||||
| 
 | 
 | ||||||
| // The list of submissions a student has made
 | // The list of submissions a student has made
 | ||||||
| router.get('/:username/submissions', getStudentSubmissionsHandler); | router.get('/:username/submissions', preventImpersonation, getStudentSubmissionsHandler); | ||||||
| 
 | 
 | ||||||
| // The list of assignments a student has
 | // The list of assignments a student has
 | ||||||
| router.get('/:username/assignments', getStudentAssignmentsHandler); | router.get('/:username/assignments', preventImpersonation, getStudentAssignmentsHandler); | ||||||
| 
 | 
 | ||||||
| // The list of groups a student is in
 | // The list of groups a student is in
 | ||||||
| router.get('/:username/groups', getStudentGroupsHandler); | router.get('/:username/groups', preventImpersonation, getStudentGroupsHandler); | ||||||
| 
 | 
 | ||||||
| // A list of questions a user has created
 | // A list of questions a user has created
 | ||||||
| router.get('/:username/questions', getStudentQuestionsHandler); | router.get('/:username/questions', preventImpersonation, getStudentQuestionsHandler); | ||||||
| 
 | 
 | ||||||
| router.use('/:username/joinRequests', joinRequestRouter); | router.use('/:username/joinRequests', joinRequestRouter); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,15 +1,15 @@ | ||||||
| import express from 'express'; | import express from 'express'; | ||||||
| import { createSubmissionHandler, deleteSubmissionHandler, getSubmissionHandler, getSubmissionsHandler } from '../controllers/submissions.js'; | import { createSubmissionHandler, deleteSubmissionHandler, getSubmissionHandler, getSubmissionsHandler } from '../controllers/submissions.js'; | ||||||
|  | import { onlyAllowIfHasAccessToSubmission, onlyAllowSubmitter } from '../middleware/auth/checks/submission-checks.js'; | ||||||
|  | import { adminOnly, studentsOnly } from '../middleware/auth/checks/auth-checks.js'; | ||||||
| const router = express.Router({ mergeParams: true }); | const router = express.Router({ mergeParams: true }); | ||||||
| 
 | 
 | ||||||
| // Root endpoint used to search objects
 | router.get('/', adminOnly, getSubmissionsHandler); | ||||||
| router.get('/', getSubmissionsHandler); |  | ||||||
| 
 | 
 | ||||||
| router.post('/', createSubmissionHandler); | router.post('/', studentsOnly, onlyAllowSubmitter, createSubmissionHandler); | ||||||
| 
 | 
 | ||||||
| // Information about an submission with id 'id'
 | router.get('/:id', onlyAllowIfHasAccessToSubmission, getSubmissionHandler); | ||||||
| router.get('/:id', getSubmissionHandler); |  | ||||||
| 
 | 
 | ||||||
| router.delete('/:id', deleteSubmissionHandler); | router.delete('/:id', onlyAllowIfHasAccessToSubmission, deleteSubmissionHandler); | ||||||
| 
 | 
 | ||||||
| export default router; | export default router; | ||||||
|  |  | ||||||
|  | @ -6,17 +6,24 @@ import { | ||||||
|     getInvitationHandler, |     getInvitationHandler, | ||||||
|     updateInvitationHandler, |     updateInvitationHandler, | ||||||
| } from '../controllers/teacher-invitations.js'; | } from '../controllers/teacher-invitations.js'; | ||||||
|  | import { preventImpersonation } from '../middleware/auth/checks/user-auth-checks.js'; | ||||||
|  | import { | ||||||
|  |     onlyAllowReceiverBody, | ||||||
|  |     onlyAllowSender, | ||||||
|  |     onlyAllowSenderBody, | ||||||
|  |     onlyAllowSenderOrReceiver, | ||||||
|  | } from '../middleware/auth/checks/teacher-invitation-checks.js'; | ||||||
| 
 | 
 | ||||||
| const router = express.Router({ mergeParams: true }); | const router = express.Router({ mergeParams: true }); | ||||||
| 
 | 
 | ||||||
| router.get('/:username', getAllInvitationsHandler); | router.get('/:username', preventImpersonation, getAllInvitationsHandler); | ||||||
| 
 | 
 | ||||||
| router.get('/:sender/:receiver/:classId', getInvitationHandler); | router.get('/:sender/:receiver/:classId', onlyAllowSenderOrReceiver, getInvitationHandler); | ||||||
| 
 | 
 | ||||||
| router.post('/', createInvitationHandler); | router.post('/', onlyAllowSenderBody, createInvitationHandler); | ||||||
| 
 | 
 | ||||||
| router.put('/', updateInvitationHandler); | router.put('/', onlyAllowReceiverBody, updateInvitationHandler); | ||||||
| 
 | 
 | ||||||
| router.delete('/:sender/:receiver/:classId', deleteInvitationHandler); | router.delete('/:sender/:receiver/:classId', onlyAllowSender, deleteInvitationHandler); | ||||||
| 
 | 
 | ||||||
| export default router; | export default router; | ||||||
|  |  | ||||||
|  | @ -10,25 +10,27 @@ import { | ||||||
|     updateStudentJoinRequestHandler, |     updateStudentJoinRequestHandler, | ||||||
| } from '../controllers/teachers.js'; | } from '../controllers/teachers.js'; | ||||||
| import invitationRouter from './teacher-invitations.js'; | import invitationRouter from './teacher-invitations.js'; | ||||||
| 
 | import { adminOnly } from '../middleware/auth/checks/auth-checks.js'; | ||||||
|  | import { preventImpersonation } from '../middleware/auth/checks/user-auth-checks.js'; | ||||||
|  | import { onlyAllowTeacherOfClass } from '../middleware/auth/checks/class-auth-checks.js'; | ||||||
| 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', preventImpersonation, getTeacherHandler); | ||||||
| 
 | 
 | ||||||
| router.delete('/:username', deleteTeacherHandler); | router.delete('/:username', preventImpersonation, deleteTeacherHandler); | ||||||
| 
 | 
 | ||||||
| router.get('/:username/classes', getTeacherClassHandler); | router.get('/:username/classes', preventImpersonation, getTeacherClassHandler); | ||||||
| 
 | 
 | ||||||
| router.get('/:username/students', getTeacherStudentHandler); | router.get('/:username/students', preventImpersonation, getTeacherStudentHandler); | ||||||
| 
 | 
 | ||||||
| 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.use('/invitations', invitationRouter); | router.use('/invitations', invitationRouter); | ||||||
|  |  | ||||||
|  | @ -1,14 +1,15 @@ | ||||||
| import express from 'express'; | import express from 'express'; | ||||||
| import { getThemesHandler, getHruidsByThemeHandler } from '../controllers/themes.js'; | import { getThemesHandler, getHruidsByThemeHandler } from '../controllers/themes.js'; | ||||||
|  | import { authenticatedOnly } from '../middleware/auth/checks/auth-checks.js'; | ||||||
| 
 | 
 | ||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
| 
 | 
 | ||||||
| // Query: language
 | // Query: language
 | ||||||
| //  Route to fetch list of {key, title, description, image} themes in their respective language
 | //  Route to fetch list of {key, title, description, image} themes in their respective language
 | ||||||
| router.get('/', getThemesHandler); | router.get('/', authenticatedOnly, getThemesHandler); | ||||||
| 
 | 
 | ||||||
| // Arg: theme (key)
 | // Arg: theme (key)
 | ||||||
| //  Route to fetch list of hruids based on theme
 | //  Route to fetch list of hruids based on theme
 | ||||||
| router.get('/:theme', getHruidsByThemeHandler); | router.get('/:theme', authenticatedOnly, getHruidsByThemeHandler); | ||||||
| 
 | 
 | ||||||
| export default router; | export default router; | ||||||
|  |  | ||||||
|  | @ -34,7 +34,7 @@ export async function createAnswer(questionId: QuestionId, answerData: AnswerDat | ||||||
|     return mapToAnswerDTO(answer); |     return mapToAnswerDTO(answer); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function fetchAnswer(questionId: QuestionId, sequenceNumber: number): Promise<Answer> { | export async function fetchAnswer(questionId: QuestionId, sequenceNumber: number): Promise<Answer> { | ||||||
|     const answerRepository = getAnswerRepository(); |     const answerRepository = getAnswerRepository(); | ||||||
|     const question = await fetchQuestion(questionId); |     const question = await fetchQuestion(questionId); | ||||||
|     const answer = await answerRepository.findAnswer(question, sequenceNumber); |     const answer = await answerRepository.findAnswer(question, sequenceNumber); | ||||||
|  |  | ||||||
|  | @ -34,6 +34,15 @@ export async function fetchGroup(classId: string, assignmentNumber: number, grou | ||||||
|     return group; |     return group; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export async function fetchAllGroups(classId: string, assignmentNumber: number): Promise<Group[]> { | ||||||
|  |     const assignment = await fetchAssignment(classId, assignmentNumber); | ||||||
|  | 
 | ||||||
|  |     const groupRepository = getGroupRepository(); | ||||||
|  |     const groups = await groupRepository.findAllGroupsForAssignment(assignment); | ||||||
|  | 
 | ||||||
|  |     return groups; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise<GroupDTO> { | export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise<GroupDTO> { | ||||||
|     const group = await fetchGroup(classId, assignmentNumber, groupNumber); |     const group = await fetchGroup(classId, assignmentNumber, groupNumber); | ||||||
|     return mapToGroupDTO(group, group.assignment.within); |     return mapToGroupDTO(group, group.assignment.within); | ||||||
|  |  | ||||||
|  | @ -13,6 +13,7 @@ import { fetchStudent } from './students.js'; | ||||||
| import { NotFoundException } from '../exceptions/not-found-exception.js'; | import { NotFoundException } from '../exceptions/not-found-exception.js'; | ||||||
| import { FALLBACK_VERSION_NUM } from '../config.js'; | import { FALLBACK_VERSION_NUM } from '../config.js'; | ||||||
| import { fetchAssignment } from './assignments.js'; | import { fetchAssignment } from './assignments.js'; | ||||||
|  | import { ConflictException } from '../exceptions/conflict-exception.js'; | ||||||
| 
 | 
 | ||||||
| export async function getQuestionsAboutLearningObjectInAssignment( | export async function getQuestionsAboutLearningObjectInAssignment( | ||||||
|     loId: LearningObjectIdentifier, |     loId: LearningObjectIdentifier, | ||||||
|  | @ -99,10 +100,18 @@ export async function createQuestion(loId: LearningObjectIdentifier, questionDat | ||||||
| 
 | 
 | ||||||
|     const inGroup = await getGroupRepository().findByAssignmentAndGroupNumber(assignment, questionData.inGroup.groupNumber); |     const inGroup = await getGroupRepository().findByAssignmentAndGroupNumber(assignment, questionData.inGroup.groupNumber); | ||||||
| 
 | 
 | ||||||
|  |     if (!inGroup) { | ||||||
|  |         throw new NotFoundException('Group with id and assignment not found'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!inGroup.members.contains(author)) { | ||||||
|  |         throw new ConflictException('Author is not part of this group'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     const question = await questionRepository.createQuestion({ |     const question = await questionRepository.createQuestion({ | ||||||
|         loId, |         loId, | ||||||
|         author, |         author, | ||||||
|         inGroup: inGroup!, |         inGroup: inGroup, | ||||||
|         content, |         content, | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -24,7 +24,8 @@ import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/subm | ||||||
| import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; | import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; | ||||||
| import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; | import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; | ||||||
| import { ConflictException } from '../exceptions/conflict-exception.js'; | import { ConflictException } from '../exceptions/conflict-exception.js'; | ||||||
| import { Submission } from '../entities/assignments/submission.entity'; | import { Submission } from '../entities/assignments/submission.entity.js'; | ||||||
|  | import { mapToUsername } from '../interfaces/user.js'; | ||||||
| 
 | 
 | ||||||
| export async function getAllStudents(full: boolean): Promise<StudentDTO[] | string[]> { | export async function getAllStudents(full: boolean): Promise<StudentDTO[] | string[]> { | ||||||
|     const studentRepository = getStudentRepository(); |     const studentRepository = getStudentRepository(); | ||||||
|  | @ -34,7 +35,7 @@ export async function getAllStudents(full: boolean): Promise<StudentDTO[] | stri | ||||||
|         return users.map(mapToStudentDTO); |         return users.map(mapToStudentDTO); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return users.map((user) => user.username); |     return users.map(mapToUsername); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function fetchStudent(username: string): Promise<Student> { | export async function fetchStudent(username: string): Promise<Student> { | ||||||
|  | @ -64,7 +65,7 @@ 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; |     return mapToStudentDTO(newStudent); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createOrUpdateStudent(userData: StudentDTO): Promise<StudentDTO> { | export async function createOrUpdateStudent(userData: StudentDTO): Promise<StudentDTO> { | ||||||
|  |  | ||||||
|  | @ -32,6 +32,10 @@ export async function createInvitation(data: TeacherInvitationData): Promise<Tea | ||||||
|         throw new ConflictException('The teacher sending the invite is not part of the class'); |         throw new ConflictException('The teacher sending the invite is not part of the class'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     if (cls.teachers.contains(receiver)) { | ||||||
|  |         throw new ConflictException('The teacher receiving the invite is already part of the class'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     const newInvitation = mapToInvitation(sender, receiver, cls); |     const newInvitation = mapToInvitation(sender, receiver, cls); | ||||||
|     await teacherInvitationRepository.save(newInvitation, { preventOverwrite: true }); |     await teacherInvitationRepository.save(newInvitation, { preventOverwrite: true }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -18,6 +18,7 @@ import { StudentDTO } from '@dwengo-1/common/interfaces/student'; | ||||||
| import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; | import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; | ||||||
| import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; | import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; | ||||||
| import { ConflictException } from '../exceptions/conflict-exception.js'; | import { ConflictException } from '../exceptions/conflict-exception.js'; | ||||||
|  | import { mapToUsername } from '../interfaces/user.js'; | ||||||
| 
 | 
 | ||||||
| export async function getAllTeachers(full: boolean): Promise<TeacherDTO[] | string[]> { | export async function getAllTeachers(full: boolean): Promise<TeacherDTO[] | string[]> { | ||||||
|     const teacherRepository: TeacherRepository = getTeacherRepository(); |     const teacherRepository: TeacherRepository = getTeacherRepository(); | ||||||
|  | @ -26,7 +27,7 @@ export async function getAllTeachers(full: boolean): Promise<TeacherDTO[] | stri | ||||||
|     if (full) { |     if (full) { | ||||||
|         return users.map(mapToTeacherDTO); |         return users.map(mapToTeacherDTO); | ||||||
|     } |     } | ||||||
|     return users.map((user) => user.username); |     return users.map(mapToUsername); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function fetchTeacher(username: string): Promise<Teacher> { | export async function fetchTeacher(username: string): Promise<Teacher> { | ||||||
|  | @ -45,7 +46,8 @@ export async function getTeacher(username: string): Promise<TeacherDTO> { | ||||||
|     return mapToTeacherDTO(user); |     return mapToTeacherDTO(user); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createTeacher(userData: TeacherDTO): Promise<TeacherDTO> { | // TODO update parameter
 | ||||||
|  | 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); | ||||||
|  | @ -98,7 +100,9 @@ export async function getStudentsByTeacher(username: string, full: boolean): Pro | ||||||
| 
 | 
 | ||||||
|     const classIds: string[] = classes.map((cls) => cls.id); |     const classIds: string[] = classes.map((cls) => cls.id); | ||||||
| 
 | 
 | ||||||
|     const students: StudentDTO[] = (await Promise.all(classIds.map(async (username) => await getClassStudentsDTO(username)))).flat(); |     const students: StudentDTO[] = (await Promise.all(classIds.map(async (classId) => await getClassStudentsDTO(classId)))) | ||||||
|  |         .flat() | ||||||
|  |         .filter((student, index, self) => self.findIndex((s) => s.username === student.username) === index); | ||||||
| 
 | 
 | ||||||
|     if (full) { |     if (full) { | ||||||
|         return students; |         return students; | ||||||
|  |  | ||||||
|  | @ -15,7 +15,6 @@ import { | ||||||
| import { BadRequestException } from '../../src/exceptions/bad-request-exception.js'; | import { BadRequestException } from '../../src/exceptions/bad-request-exception.js'; | ||||||
| import { EntityAlreadyExistsException } from '../../src/exceptions/entity-already-exists-exception.js'; | import { EntityAlreadyExistsException } from '../../src/exceptions/entity-already-exists-exception.js'; | ||||||
| import { getStudentRequestsHandler } from '../../src/controllers/students.js'; | import { getStudentRequestsHandler } from '../../src/controllers/students.js'; | ||||||
| import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; |  | ||||||
| import { getClassHandler } from '../../src/controllers/classes'; | import { getClassHandler } from '../../src/controllers/classes'; | ||||||
| import { getFooFighters, getTestleerkracht1 } from '../test_assets/users/teachers.testdata.js'; | import { getFooFighters, getTestleerkracht1 } from '../test_assets/users/teachers.testdata.js'; | ||||||
| import { getClass02 } from '../test_assets/classes/classes.testdata.js'; | import { getClass02 } from '../test_assets/classes/classes.testdata.js'; | ||||||
|  | @ -101,7 +100,7 @@ describe('Teacher controllers', () => { | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('Teacher list', async () => { |     it('Teacher list', async () => { | ||||||
|         req = { query: { full: 'true' } }; |         req = { query: { full: 'false' } }; | ||||||
| 
 | 
 | ||||||
|         await getAllTeachersHandler(req as Request, res as Response); |         await getAllTeachersHandler(req as Request, res as Response); | ||||||
| 
 | 
 | ||||||
|  | @ -109,10 +108,8 @@ describe('Teacher controllers', () => { | ||||||
| 
 | 
 | ||||||
|         const result = jsonMock.mock.lastCall?.[0]; |         const result = jsonMock.mock.lastCall?.[0]; | ||||||
| 
 | 
 | ||||||
|         const teacherUsernames = result.teachers.map((s: TeacherDTO) => s.username); |  | ||||||
| 
 |  | ||||||
|         const teacher = getTestleerkracht1(); |         const teacher = getTestleerkracht1(); | ||||||
|         expect(teacherUsernames).toContain(teacher.username); |         expect(result.teachers).toContain(teacher.username); | ||||||
| 
 | 
 | ||||||
|         expect(result.teachers).toHaveLength(5); |         expect(result.teachers).toHaveLength(5); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  | @ -13,8 +13,8 @@ export interface QuestionDTO { | ||||||
| 
 | 
 | ||||||
| export interface QuestionData { | export interface QuestionData { | ||||||
|     author?: string; |     author?: string; | ||||||
|     content: string; |  | ||||||
|     inGroup: GroupDTO; |     inGroup: GroupDTO; | ||||||
|  |     content: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface QuestionId { | export interface QuestionId { | ||||||
|  |  | ||||||
							
								
								
									
										4
									
								
								common/src/util/account-types.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								common/src/util/account-types.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | ||||||
|  | export enum AccountType { | ||||||
|  |     Student = 'student', | ||||||
|  |     Teacher = 'teacher', | ||||||
|  | } | ||||||
|  | @ -26,7 +26,59 @@ const doc = { | ||||||
|     ], |     ], | ||||||
|     components: { |     components: { | ||||||
|         securitySchemes: { |         securitySchemes: { | ||||||
|             student: { |             studentDev: { | ||||||
|  |                 type: 'oauth2', | ||||||
|  |                 flows: { | ||||||
|  |                     implicit: { | ||||||
|  |                         authorizationUrl: 'http://localhost:7080/realms/student/protocol/openid-connect/auth', | ||||||
|  |                         scopes: { | ||||||
|  |                             openid: 'openid', | ||||||
|  |                             profile: 'profile', | ||||||
|  |                             email: 'email', | ||||||
|  |                         }, | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |             teacherDev: { | ||||||
|  |                 type: 'oauth2', | ||||||
|  |                 flows: { | ||||||
|  |                     implicit: { | ||||||
|  |                         authorizationUrl: 'http://localhost:7080/realms/teacher/protocol/openid-connect/auth', | ||||||
|  |                         scopes: { | ||||||
|  |                             openid: 'openid', | ||||||
|  |                             profile: 'profile', | ||||||
|  |                             email: 'email', | ||||||
|  |                         }, | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |             studentStaging: { | ||||||
|  |                 type: 'oauth2', | ||||||
|  |                 flows: { | ||||||
|  |                     implicit: { | ||||||
|  |                         authorizationUrl: 'http://localhost/idp/realms/student/protocol/openid-connect/auth', | ||||||
|  |                         scopes: { | ||||||
|  |                             openid: 'openid', | ||||||
|  |                             profile: 'profile', | ||||||
|  |                             email: 'email', | ||||||
|  |                         }, | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |             teacherStaging: { | ||||||
|  |                 type: 'oauth2', | ||||||
|  |                 flows: { | ||||||
|  |                     implicit: { | ||||||
|  |                         authorizationUrl: 'http://localhost/idp/realms/teacher/protocol/openid-connect/auth', | ||||||
|  |                         scopes: { | ||||||
|  |                             openid: 'openid', | ||||||
|  |                             profile: 'profile', | ||||||
|  |                             email: 'email', | ||||||
|  |                         }, | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |             studentProduction: { | ||||||
|                 type: 'oauth2', |                 type: 'oauth2', | ||||||
|                 flows: { |                 flows: { | ||||||
|                     implicit: { |                     implicit: { | ||||||
|  | @ -39,7 +91,7 @@ const doc = { | ||||||
|                     }, |                     }, | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|             teacher: { |             teacherProduction: { | ||||||
|                 type: 'oauth2', |                 type: 'oauth2', | ||||||
|                 flows: { |                 flows: { | ||||||
|                     implicit: { |                     implicit: { | ||||||
|  |  | ||||||
|  | @ -17,6 +17,7 @@ | ||||||
|         "test:e2e": "playwright test" |         "test:e2e": "playwright test" | ||||||
|     }, |     }, | ||||||
|     "dependencies": { |     "dependencies": { | ||||||
|  |         "@dwengo-1/common": "^0.2.0", | ||||||
|         "@tanstack/react-query": "^5.69.0", |         "@tanstack/react-query": "^5.69.0", | ||||||
|         "@tanstack/vue-query": "^5.69.0", |         "@tanstack/vue-query": "^5.69.0", | ||||||
|         "@vueuse/core": "^13.1.0", |         "@vueuse/core": "^13.1.0", | ||||||
|  |  | ||||||
|  | @ -31,4 +31,9 @@ | ||||||
|     ></v-text-field> |     ></v-text-field> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped></style> | <style scoped> | ||||||
|  |     .search-field { | ||||||
|  |         width: 25%; | ||||||
|  |         min-width: 300px; | ||||||
|  |     } | ||||||
|  | </style> | ||||||
|  |  | ||||||
|  | @ -53,9 +53,9 @@ | ||||||
|         white-space: normal; |         white-space: normal; | ||||||
|     } |     } | ||||||
|     .results-grid { |     .results-grid { | ||||||
|         margin: 20px; |         margin: 20px auto; | ||||||
|         display: flex; |         display: flex; | ||||||
|         align-items: stretch; |         justify-content: center; | ||||||
|         gap: 20px; |         gap: 20px; | ||||||
|         flex-wrap: wrap; |         flex-wrap: wrap; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -14,6 +14,7 @@ | ||||||
|     const _router = useRouter(); // Zonder '_' gaf dit een linter error voor unused variable |     const _router = useRouter(); // Zonder '_' gaf dit een linter error voor unused variable | ||||||
| 
 | 
 | ||||||
|     const name: string = auth.authState.user!.profile.name!; |     const name: string = auth.authState.user!.profile.name!; | ||||||
|  |     const username = auth.authState.user!.profile.preferred_username!; | ||||||
|     const email = auth.authState.user!.profile.email; |     const email = auth.authState.user!.profile.email; | ||||||
|     const initials: string = name |     const initials: string = name | ||||||
|         .split(" ") |         .split(" ") | ||||||
|  | @ -180,10 +181,15 @@ | ||||||
|             <v-card> |             <v-card> | ||||||
|                 <v-card-text> |                 <v-card-text> | ||||||
|                     <div class="mx-auto text-center"> |                     <div class="mx-auto text-center"> | ||||||
|                         <v-avatar color="#0e6942"> |                         <v-avatar | ||||||
|                             <span class="text-h5">{{ initials }}</span> |                             color="#0e6942" | ||||||
|  |                             size="large" | ||||||
|  |                             class="user-button mb-3" | ||||||
|  |                         > | ||||||
|  |                             <span>{{ initials }}</span> | ||||||
|                         </v-avatar> |                         </v-avatar> | ||||||
|                         <h3>{{ name }}</h3> |                         <h3>{{ name }}</h3> | ||||||
|  |                         <p class="text-caption mt-1">{{ username }}</p> | ||||||
|                         <p class="text-caption mt-1">{{ email }}</p> |                         <p class="text-caption mt-1">{{ email }}</p> | ||||||
|                         <v-divider class="my-3"></v-divider> |                         <v-divider class="my-3"></v-divider> | ||||||
|                         <v-btn |                         <v-btn | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ | ||||||
|     import type { AnswersResponse } from "@/controllers/answers"; |     import type { AnswersResponse } from "@/controllers/answers"; | ||||||
|     import type { AnswerData, AnswerDTO } from "@dwengo-1/common/interfaces/answer"; |     import type { AnswerData, AnswerDTO } from "@dwengo-1/common/interfaces/answer"; | ||||||
|     import authService from "@/services/auth/auth-service"; |     import authService from "@/services/auth/auth-service"; | ||||||
|  |     import { AccountType } from "@dwengo-1/common/util/account-types"; | ||||||
| 
 | 
 | ||||||
|     const props = defineProps<{ |     const props = defineProps<{ | ||||||
|         question: QuestionDTO; |         question: QuestionDTO; | ||||||
|  | @ -80,7 +81,7 @@ | ||||||
|             {{ question.content }} |             {{ question.content }} | ||||||
|         </div> |         </div> | ||||||
|         <div |         <div | ||||||
|             v-if="authService.authState.activeRole === 'teacher'" |             v-if="authService.authState.activeRole === AccountType.Teacher" | ||||||
|             class="answer-input-container" |             class="answer-input-container" | ||||||
|         > |         > | ||||||
|             <input |             <input | ||||||
|  |  | ||||||
|  | @ -15,6 +15,7 @@ import { invalidateAllGroupKeys } from "./groups"; | ||||||
| import { invalidateAllSubmissionKeys } from "./submissions"; | import { invalidateAllSubmissionKeys } from "./submissions"; | ||||||
| import type { TeachersResponse } from "@/controllers/teachers"; | import type { TeachersResponse } from "@/controllers/teachers"; | ||||||
| import type { TeacherInvitationsResponse } from "@/controllers/teacher-invitations"; | import type { TeacherInvitationsResponse } from "@/controllers/teacher-invitations"; | ||||||
|  | import { studentClassesQueryKey } from "@/queries/students.ts"; | ||||||
| 
 | 
 | ||||||
| const classController = new ClassController(); | const classController = new ClassController(); | ||||||
| 
 | 
 | ||||||
|  | @ -171,6 +172,8 @@ export function useClassDeleteStudentMutation(): UseMutationReturnType< | ||||||
|             await queryClient.invalidateQueries({ queryKey: classQueryKey(data.class.id) }); |             await queryClient.invalidateQueries({ queryKey: classQueryKey(data.class.id) }); | ||||||
|             await queryClient.invalidateQueries({ queryKey: classStudentsKey(data.class.id, true) }); |             await queryClient.invalidateQueries({ queryKey: classStudentsKey(data.class.id, true) }); | ||||||
|             await queryClient.invalidateQueries({ queryKey: classStudentsKey(data.class.id, false) }); |             await queryClient.invalidateQueries({ queryKey: classStudentsKey(data.class.id, false) }); | ||||||
|  |             await queryClient.invalidateQueries({ queryKey: studentClassesQueryKey(data.class.id, false) }); | ||||||
|  |             await queryClient.invalidateQueries({ queryKey: studentClassesQueryKey(data.class.id, true) }); | ||||||
|         }, |         }, | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -33,7 +33,7 @@ function studentsQueryKey(full: boolean): [string, boolean] { | ||||||
| function studentQueryKey(username: string): [string, string] { | function studentQueryKey(username: string): [string, string] { | ||||||
|     return ["student", username]; |     return ["student", username]; | ||||||
| } | } | ||||||
| function studentClassesQueryKey(username: string, full: boolean): [string, string, boolean] { | export function studentClassesQueryKey(username: string, full: boolean): [string, string, boolean] { | ||||||
|     return ["student-classes", username, full]; |     return ["student-classes", username, full]; | ||||||
| } | } | ||||||
| function studentAssignmentsQueryKey(username: string, full: boolean): [string, string, boolean] { | function studentAssignmentsQueryKey(username: string, full: boolean): [string, string, boolean] { | ||||||
|  |  | ||||||
|  | @ -14,6 +14,7 @@ import UserHomePage from "@/views/homepage/UserHomePage.vue"; | ||||||
| import SingleTheme from "@/views/SingleTheme.vue"; | import SingleTheme from "@/views/SingleTheme.vue"; | ||||||
| import LearningObjectView from "@/views/learning-paths/learning-object/LearningObjectView.vue"; | import LearningObjectView from "@/views/learning-paths/learning-object/LearningObjectView.vue"; | ||||||
| import authService from "@/services/auth/auth-service"; | import authService from "@/services/auth/auth-service"; | ||||||
|  | import { allowRedirect, Redirect } from "@/utils/redirect.ts"; | ||||||
| 
 | 
 | ||||||
| const router = createRouter({ | const router = createRouter({ | ||||||
|     history: createWebHistory(import.meta.env.BASE_URL), |     history: createWebHistory(import.meta.env.BASE_URL), | ||||||
|  | @ -143,7 +144,11 @@ 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 (!authService.isLoggedIn.value && !(await authService.loadUser())) { |         if (!authService.isLoggedIn.value && !(await authService.loadUser())) { | ||||||
|             next("/login"); |             const path = to.fullPath; | ||||||
|  |             if (allowRedirect(path)) { | ||||||
|  |                 localStorage.setItem(Redirect.AFTER_LOGIN_KEY, path); | ||||||
|  |             } | ||||||
|  |             next(Redirect.LOGIN); | ||||||
|         } else { |         } else { | ||||||
|             next(); |             next(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
							
								
								
									
										12
									
								
								frontend/src/utils/redirect.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								frontend/src/utils/redirect.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | ||||||
|  | export enum Redirect { | ||||||
|  |     AFTER_LOGIN_KEY = "redirectAfterLogin", | ||||||
|  |     HOME = "/user", | ||||||
|  |     LOGIN = "/login", | ||||||
|  |     ROOT = "/", | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const NOT_ALLOWED_REDIRECTS = new Set<Redirect>([Redirect.HOME, Redirect.ROOT, Redirect.LOGIN]); | ||||||
|  | 
 | ||||||
|  | export function allowRedirect(path: string): boolean { | ||||||
|  |     return !NOT_ALLOWED_REDIRECTS.has(path as Redirect); | ||||||
|  | } | ||||||
|  | @ -3,6 +3,7 @@ | ||||||
|     import { useI18n } from "vue-i18n"; |     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"; | ||||||
|  |     import { Redirect } from "@/utils/redirect.ts"; | ||||||
| 
 | 
 | ||||||
|     const { t } = useI18n(); |     const { t } = useI18n(); | ||||||
| 
 | 
 | ||||||
|  | @ -10,10 +11,20 @@ | ||||||
| 
 | 
 | ||||||
|     const errorMessage: Ref<string | null> = ref(null); |     const errorMessage: Ref<string | null> = ref(null); | ||||||
| 
 | 
 | ||||||
|  |     async function redirectPage(): Promise<void> { | ||||||
|  |         const redirectUrl = localStorage.getItem(Redirect.AFTER_LOGIN_KEY); | ||||||
|  |         if (redirectUrl) { | ||||||
|  |             localStorage.removeItem(Redirect.AFTER_LOGIN_KEY); | ||||||
|  |             await router.replace(redirectUrl); | ||||||
|  |         } else { | ||||||
|  |             await router.replace(Redirect.HOME); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     onMounted(async () => { |     onMounted(async () => { | ||||||
|         try { |         try { | ||||||
|             await auth.handleLoginCallback(); |             await auth.handleLoginCallback(); | ||||||
|             await router.replace("/user"); // Redirect to theme page |             await redirectPage(); | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             errorMessage.value = `${t("loginUnexpectedError")}: ${error}`; |             errorMessage.value = `${t("loginUnexpectedError")}: ${error}`; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ | ||||||
|     import dwengoLogo from "../../../assets/img/dwengo-groen-zwart.svg"; |     import dwengoLogo from "../../../assets/img/dwengo-groen-zwart.svg"; | ||||||
|     import auth from "@/services/auth/auth-service.ts"; |     import auth from "@/services/auth/auth-service.ts"; | ||||||
|     import { watch } from "vue"; |     import { watch } from "vue"; | ||||||
|  |     import { AccountType } from "@dwengo-1/common/util/account-types"; | ||||||
| 
 | 
 | ||||||
|     const router = useRouter(); |     const router = useRouter(); | ||||||
| 
 | 
 | ||||||
|  | @ -17,11 +18,11 @@ | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     async function loginAsStudent(): Promise<void> { |     async function loginAsStudent(): Promise<void> { | ||||||
|         await auth.loginAs("student"); |         await auth.loginAs(AccountType.Student); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async function loginAsTeacher(): Promise<void> { |     async function loginAsTeacher(): Promise<void> { | ||||||
|         await auth.loginAs("teacher"); |         await auth.loginAs(AccountType.Teacher); | ||||||
|     } |     } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -35,13 +35,14 @@ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|     <div class="container"> |     <div class="container d-flex flex-column align-items-center justify-center"> | ||||||
|         <using-query-result :query-result="themeQueryResult"> |         <using-query-result :query-result="themeQueryResult"> | ||||||
|             <h1>{{ currentThemeInfo!!.title }}</h1> |             <h1>{{ currentThemeInfo!!.title }}</h1> | ||||||
|             <p>{{ currentThemeInfo!!.description }}</p> |             <p>{{ currentThemeInfo!!.description }}</p> | ||||||
|             <div class="search-field-container"> |             <br /> | ||||||
|  |             <div class="search-field-container mt-sm-6"> | ||||||
|                 <v-text-field |                 <v-text-field | ||||||
|                     class="search-field" |                     class="search-field mx-auto" | ||||||
|                     :label="t('search')" |                     :label="t('search')" | ||||||
|                     append-inner-icon="mdi-magnify" |                     append-inner-icon="mdi-magnify" | ||||||
|                     v-model="searchFilter" |                     v-model="searchFilter" | ||||||
|  | @ -60,13 +61,15 @@ | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
|     .search-field-container { |     .search-field-container { | ||||||
|         display: block; |         justify-content: center !important; | ||||||
|         margin: 20px; |  | ||||||
|     } |     } | ||||||
|     .search-field { |     .search-field { | ||||||
|         max-width: 300px; |         width: 25%; | ||||||
|  |         min-width: 300px; | ||||||
|     } |     } | ||||||
|     .container { |     .container { | ||||||
|         padding: 20px; |         padding: 20px; | ||||||
|  |         justify-content: center; | ||||||
|  |         justify-items: center; | ||||||
|     } |     } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -14,6 +14,7 @@ | ||||||
|     import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment"; |     import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment"; | ||||||
|     import { useCreateAssignmentMutation } from "@/queries/assignments.ts"; |     import { useCreateAssignmentMutation } from "@/queries/assignments.ts"; | ||||||
|     import { useRoute } from "vue-router"; |     import { useRoute } from "vue-router"; | ||||||
|  |     import { AccountType } from "@dwengo-1/common/util/account-types"; | ||||||
| 
 | 
 | ||||||
|     const route = useRoute(); |     const route = useRoute(); | ||||||
|     const router = useRouter(); |     const router = useRouter(); | ||||||
|  | @ -23,7 +24,7 @@ | ||||||
| 
 | 
 | ||||||
|     onMounted(async () => { |     onMounted(async () => { | ||||||
|         // Redirect student |         // Redirect student | ||||||
|         if (role.value === "student") { |         if (role.value === AccountType.Student) { | ||||||
|             await router.push("/user"); |             await router.push("/user"); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -8,9 +8,10 @@ | ||||||
|     import { useGetLearningPathQuery } from "@/queries/learning-paths.ts"; |     import { useGetLearningPathQuery } from "@/queries/learning-paths.ts"; | ||||||
|     import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; |     import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; | ||||||
|     import type { GroupDTO } from "@dwengo-1/common/interfaces/group"; |     import type { GroupDTO } from "@dwengo-1/common/interfaces/group"; | ||||||
|  |     import { AccountType } from "@dwengo-1/common/util/account-types"; | ||||||
| 
 | 
 | ||||||
|     const role = auth.authState.activeRole; |     const role = auth.authState.activeRole; | ||||||
|     const isTeacher = computed(() => role === "teacher"); |     const isTeacher = computed(() => role === AccountType.Teacher); | ||||||
| 
 | 
 | ||||||
|     const route = useRoute(); |     const route = useRoute(); | ||||||
|     const classId = ref<string>(route.params.classId as string); |     const classId = ref<string>(route.params.classId as string); | ||||||
|  |  | ||||||
|  | @ -9,6 +9,7 @@ | ||||||
|     import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; |     import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; | ||||||
|     import { asyncComputed } from "@vueuse/core"; |     import { asyncComputed } from "@vueuse/core"; | ||||||
|     import { useDeleteAssignmentMutation } from "@/queries/assignments.ts"; |     import { useDeleteAssignmentMutation } from "@/queries/assignments.ts"; | ||||||
|  |     import { AccountType } from "@dwengo-1/common/util/account-types"; | ||||||
|     import "../../assets/common.css"; |     import "../../assets/common.css"; | ||||||
| 
 | 
 | ||||||
|     const { t, locale } = useI18n(); |     const { t, locale } = useI18n(); | ||||||
|  | @ -17,7 +18,7 @@ | ||||||
|     const role = ref(auth.authState.activeRole); |     const role = ref(auth.authState.activeRole); | ||||||
|     const username = ref<string>(""); |     const username = ref<string>(""); | ||||||
| 
 | 
 | ||||||
|     const isTeacher = computed(() => role.value === "teacher"); |     const isTeacher = computed(() => role.value === AccountType.Teacher); | ||||||
| 
 | 
 | ||||||
|     // Fetch and store all the teacher's classes |     // Fetch and store all the teacher's classes | ||||||
|     let classesQueryResults = undefined; |     let classesQueryResults = undefined; | ||||||
|  |  | ||||||
							
								
								
									
										20
									
								
								frontend/src/views/classes/ClassDisplay.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								frontend/src/views/classes/ClassDisplay.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | ||||||
|  | <script setup lang="ts"> | ||||||
|  |     import { useClassQuery } from "@/queries/classes"; | ||||||
|  |     import { defineProps } from "vue"; | ||||||
|  |     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||||
|  | 
 | ||||||
|  |     const props = defineProps({ | ||||||
|  |         classId: String, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const classQuery = useClassQuery(props.classId); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |     <using-query-result | ||||||
|  |         :query-result="classQuery" | ||||||
|  |         v-slot="{ data: classResponse }" | ||||||
|  |     > | ||||||
|  |         <span>{{ classResponse?.class.displayName }}</span> | ||||||
|  |     </using-query-result> | ||||||
|  | </template> | ||||||
|  | @ -77,7 +77,7 @@ | ||||||
|                 }, |                 }, | ||||||
|                 onError: (e) => { |                 onError: (e) => { | ||||||
|                     dialog.value = false; |                     dialog.value = false; | ||||||
|                     showSnackbar(t("failed") + ": " + e.message, "error"); |                     showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error"); | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|         ); |         ); | ||||||
|  | @ -105,7 +105,7 @@ | ||||||
|                     } |                     } | ||||||
|                 }, |                 }, | ||||||
|                 onError: (e) => { |                 onError: (e) => { | ||||||
|                     showSnackbar(t("failed") + ": " + e.message, "error"); |                     showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error"); | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|         ); |         ); | ||||||
|  | @ -126,7 +126,7 @@ | ||||||
|                 usernameTeacher.value = ""; |                 usernameTeacher.value = ""; | ||||||
|             }, |             }, | ||||||
|             onError: (e) => { |             onError: (e) => { | ||||||
|                 showSnackbar(t("failed") + ": " + e.message, "error"); |                 showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error"); | ||||||
|             }, |             }, | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
|     import { useI18n } from "vue-i18n"; |     import { useI18n } from "vue-i18n"; | ||||||
|     import authState from "@/services/auth/auth-service.ts"; |     import authState from "@/services/auth/auth-service.ts"; | ||||||
|     import { computed, onMounted, ref } from "vue"; |     import { computed, onMounted, ref } from "vue"; | ||||||
|     import { validate, version } from "uuid"; |     import { useRoute } from "vue-router"; | ||||||
|     import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; |     import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; | ||||||
|     import { useCreateJoinRequestMutation, useStudentClassesQuery } from "@/queries/students"; |     import { useCreateJoinRequestMutation, useStudentClassesQuery } from "@/queries/students"; | ||||||
|     import type { StudentDTO } from "@dwengo-1/common/interfaces/student"; |     import type { StudentDTO } from "@dwengo-1/common/interfaces/student"; | ||||||
|  | @ -15,6 +15,7 @@ | ||||||
|     import "../../assets/common.css"; |     import "../../assets/common.css"; | ||||||
| 
 | 
 | ||||||
|     const { t } = useI18n(); |     const { t } = useI18n(); | ||||||
|  |     const route = useRoute(); | ||||||
| 
 | 
 | ||||||
|     // Username of logged in student |     // Username of logged in student | ||||||
|     const username = ref<string | undefined>(undefined); |     const username = ref<string | undefined>(undefined); | ||||||
|  | @ -38,6 +39,11 @@ | ||||||
|         } finally { |         } finally { | ||||||
|             isLoading.value = false; |             isLoading.value = false; | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         const queryCode = route.query.code as string | undefined; | ||||||
|  |         if (queryCode) { | ||||||
|  |             code.value = queryCode; | ||||||
|  |         } | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     // Fetch all classes of the logged in student |     // Fetch all classes of the logged in student | ||||||
|  | @ -75,11 +81,15 @@ | ||||||
| 
 | 
 | ||||||
|     // The code a student sends in to join a class needs to be formatted as v4 to be valid |     // The code a student sends in to join a class needs to be formatted as v4 to be valid | ||||||
|     // These rules are used to display a message to the user if they use a code that has an invalid format |     // These rules are used to display a message to the user if they use a code that has an invalid format | ||||||
|  |     function codeRegex(value: string): boolean { | ||||||
|  |         return /^[a-zA-Z0-9]{6}$/.test(value); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     const codeRules = [ |     const codeRules = [ | ||||||
|         (value: string | undefined): string | boolean => { |         (value: string | undefined): string | boolean => { | ||||||
|             if (value === undefined || value === "") { |             if (value === undefined || value === "") { | ||||||
|                 return true; |                 return true; | ||||||
|             } else if (value !== undefined && validate(value) && version(value) === 4) { |             } else if (codeRegex(value)) { | ||||||
|                 return true; |                 return true; | ||||||
|             } |             } | ||||||
|             return t("invalidFormat"); |             return t("invalidFormat"); | ||||||
|  | @ -92,7 +102,7 @@ | ||||||
|     // Function called when a student submits a code to join a class |     // Function called when a student submits a code to join a class | ||||||
|     function submitCode(): void { |     function submitCode(): void { | ||||||
|         // Check if the code is valid |         // Check if the code is valid | ||||||
|         if (code.value !== undefined && validate(code.value) && version(code.value) === 4) { |         if (code.value !== undefined && codeRegex(code.value)) { | ||||||
|             mutate( |             mutate( | ||||||
|                 { username: username.value!, classId: code.value }, |                 { username: username.value!, classId: code.value }, | ||||||
|                 { |                 { | ||||||
|  | @ -100,7 +110,7 @@ | ||||||
|                         showSnackbar(t("sent"), "success"); |                         showSnackbar(t("sent"), "success"); | ||||||
|                     }, |                     }, | ||||||
|                     onError: (e) => { |                     onError: (e) => { | ||||||
|                         showSnackbar(t("failed") + ": " + e.message, "error"); |                         showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error"); | ||||||
|                     }, |                     }, | ||||||
|                 }, |                 }, | ||||||
|             ); |             ); | ||||||
|  | @ -260,7 +270,7 @@ | ||||||
|                             <v-text-field |                             <v-text-field | ||||||
|                                 label="CODE" |                                 label="CODE" | ||||||
|                                 v-model="code" |                                 v-model="code" | ||||||
|                                 placeholder="XXXXXXXX-XXXX-4XXX-XXXX-XXXXXXXXXXXX" |                                 placeholder="XXXXXX" | ||||||
|                                 :rules="codeRules" |                                 :rules="codeRules" | ||||||
|                                 variant="outlined" |                                 variant="outlined" | ||||||
|                             ></v-text-field> |                             ></v-text-field> | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ | ||||||
|     import { useTeacherClassesQuery } from "@/queries/teachers"; |     import { useTeacherClassesQuery } from "@/queries/teachers"; | ||||||
|     import type { ClassesResponse } from "@/controllers/classes"; |     import type { ClassesResponse } from "@/controllers/classes"; | ||||||
|     import UsingQueryResult from "@/components/UsingQueryResult.vue"; |     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||||
|     import { useClassesQuery, useCreateClassMutation } from "@/queries/classes"; |     import { useCreateClassMutation } from "@/queries/classes"; | ||||||
|     import type { TeacherInvitationsResponse } from "@/controllers/teacher-invitations"; |     import type { TeacherInvitationsResponse } from "@/controllers/teacher-invitations"; | ||||||
|     import { |     import { | ||||||
|         useRespondTeacherInvitationMutation, |         useRespondTeacherInvitationMutation, | ||||||
|  | @ -16,6 +16,7 @@ | ||||||
|     } from "@/queries/teacher-invitations"; |     } from "@/queries/teacher-invitations"; | ||||||
|     import { useDisplay } from "vuetify"; |     import { useDisplay } from "vuetify"; | ||||||
|     import "../../assets/common.css"; |     import "../../assets/common.css"; | ||||||
|  |     import ClassDisplay from "@/views/classes/ClassDisplay.vue"; | ||||||
| 
 | 
 | ||||||
|     const { t } = useI18n(); |     const { t } = useI18n(); | ||||||
| 
 | 
 | ||||||
|  | @ -41,7 +42,6 @@ | ||||||
| 
 | 
 | ||||||
|     // Fetch all classes of the logged in teacher |     // Fetch all classes of the logged in teacher | ||||||
|     const classesQuery = useTeacherClassesQuery(username, true); |     const classesQuery = useTeacherClassesQuery(username, true); | ||||||
|     const allClassesQuery = useClassesQuery(); |  | ||||||
|     const { mutate } = useCreateClassMutation(); |     const { mutate } = useCreateClassMutation(); | ||||||
|     const getInvitationsQuery = useTeacherInvitationsReceivedQuery(username); |     const getInvitationsQuery = useTeacherInvitationsReceivedQuery(username); | ||||||
|     const { mutate: respondToInvitation } = useRespondTeacherInvitationMutation(); |     const { mutate: respondToInvitation } = useRespondTeacherInvitationMutation(); | ||||||
|  | @ -70,7 +70,7 @@ | ||||||
|                 await getInvitationsQuery.refetch(); |                 await getInvitationsQuery.refetch(); | ||||||
|             }, |             }, | ||||||
|             onError: (e) => { |             onError: (e) => { | ||||||
|                 showSnackbar(t("failed") + ": " + e.message, "error"); |                 showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error"); | ||||||
|             }, |             }, | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  | @ -132,17 +132,12 @@ | ||||||
|     // Show the teacher, copying of the code was a successs |     // Show the teacher, copying of the code was a successs | ||||||
|     const copied = ref(false); |     const copied = ref(false); | ||||||
| 
 | 
 | ||||||
|     // Copy the generated code to the clipboard |     async function copyToClipboard(code: string, isDialog = false, isLink = false): Promise<void> { | ||||||
|     async function copyToClipboard(): Promise<void> { |         const content = isLink ? `${window.location.origin}/user/class?code=${code}` : code; | ||||||
|         await navigator.clipboard.writeText(code.value); |         await navigator.clipboard.writeText(content); | ||||||
|         copied.value = true; |         copied.value = isDialog; | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     async function copyCode(selectedCode: string): Promise<void> { |         if (!isDialog) showSnackbar(t("copied"), "white"); | ||||||
|         code.value = selectedCode; |  | ||||||
|         await copyToClipboard(); |  | ||||||
|         showSnackbar(t("copied"), "white"); |  | ||||||
|         copied.value = false; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Custom breakpoints |     // Custom breakpoints | ||||||
|  | @ -170,6 +165,7 @@ | ||||||
|     // Code display dialog logic |     // Code display dialog logic | ||||||
|     const viewCodeDialog = ref(false); |     const viewCodeDialog = ref(false); | ||||||
|     const selectedCode = ref(""); |     const selectedCode = ref(""); | ||||||
|  | 
 | ||||||
|     function openCodeDialog(codeToView: string): void { |     function openCodeDialog(codeToView: string): void { | ||||||
|         selectedCode.value = codeToView; |         selectedCode.value = codeToView; | ||||||
|         viewCodeDialog.value = true; |         viewCodeDialog.value = true; | ||||||
|  | @ -235,20 +231,34 @@ | ||||||
|                                             </v-btn> |                                             </v-btn> | ||||||
|                                         </td> |                                         </td> | ||||||
|                                         <td> |                                         <td> | ||||||
|                                             <v-btn |                                             <v-row | ||||||
|                                                 v-if="!isMdAndDown" |                                                 v-if="!isMdAndDown" | ||||||
|  |                                                 dense | ||||||
|  |                                                 align="center" | ||||||
|  |                                                 no-gutters | ||||||
|  |                                             > | ||||||
|  |                                                 <v-btn | ||||||
|                                                     variant="text" |                                                     variant="text" | ||||||
|                                                     append-icon="mdi-content-copy" |                                                     append-icon="mdi-content-copy" | ||||||
|                                                 @click="copyCode(c.id)" |                                                     @click="copyToClipboard(c.id)" | ||||||
|                                                 > |                                                 > | ||||||
|                                                     {{ c.id }} |                                                     {{ c.id }} | ||||||
|                                                 </v-btn> |                                                 </v-btn> | ||||||
|  |                                                 <v-btn | ||||||
|  |                                                     icon | ||||||
|  |                                                     variant="text" | ||||||
|  |                                                     @click="copyToClipboard(c.id, false, true)" | ||||||
|  |                                                 > | ||||||
|  |                                                     <v-icon>mdi-link-variant</v-icon> | ||||||
|  |                                                 </v-btn> | ||||||
|  |                                             </v-row> | ||||||
|                                             <span |                                             <span | ||||||
|                                                 v-else |                                                 v-else | ||||||
|                                                 style="cursor: pointer" |                                                 style="cursor: pointer" | ||||||
|                                                 @click="openCodeDialog(c.id)" |                                                 @click="openCodeDialog(c.id)" | ||||||
|                                                 ><v-icon icon="mdi-eye"></v-icon |                                             > | ||||||
|                                             ></span> |                                                 <v-icon icon="mdi-eye"></v-icon> | ||||||
|  |                                             </span> | ||||||
|                                         </td> |                                         </td> | ||||||
| 
 | 
 | ||||||
|                                         <td>{{ c.students.length }}</td> |                                         <td>{{ c.students.length }}</td> | ||||||
|  | @ -300,8 +310,8 @@ | ||||||
|                                             type="submit" |                                             type="submit" | ||||||
|                                             @click="createClass" |                                             @click="createClass" | ||||||
|                                             block |                                             block | ||||||
|                                             >{{ t("create") }}</v-btn |                                             >{{ t("create") }} | ||||||
|                                         > |                                         </v-btn> | ||||||
|                                     </v-form> |                                     </v-form> | ||||||
|                                 </v-sheet> |                                 </v-sheet> | ||||||
|                                 <v-container> |                                 <v-container> | ||||||
|  | @ -310,14 +320,29 @@ | ||||||
|                                         max-width="400px" |                                         max-width="400px" | ||||||
|                                     > |                                     > | ||||||
|                                         <v-card> |                                         <v-card> | ||||||
|                                             <v-card-title class="headline">code</v-card-title> |                                             <v-card-title class="headline">{{ t("code") }}</v-card-title> | ||||||
|                                             <v-card-text> |                                             <v-card-text> | ||||||
|                                                 <v-text-field |                                                 <v-text-field | ||||||
|                                                     v-model="code" |                                                     v-model="code" | ||||||
|                                                     readonly |                                                     readonly | ||||||
|                                                     append-inner-icon="mdi-content-copy" |                                                 > | ||||||
|                                                     @click:append-inner="copyToClipboard" |                                                     <template #append> | ||||||
|                                                 ></v-text-field> |                                                         <v-btn | ||||||
|  |                                                             icon | ||||||
|  |                                                             variant="text" | ||||||
|  |                                                             @click="copyToClipboard(code, true)" | ||||||
|  |                                                         > | ||||||
|  |                                                             <v-icon>mdi-content-copy</v-icon> | ||||||
|  |                                                         </v-btn> | ||||||
|  |                                                         <v-btn | ||||||
|  |                                                             icon | ||||||
|  |                                                             variant="text" | ||||||
|  |                                                             @click="copyToClipboard(code, true, true)" | ||||||
|  |                                                         > | ||||||
|  |                                                             <v-icon>mdi-link-variant</v-icon> | ||||||
|  |                                                         </v-btn> | ||||||
|  |                                                     </template> | ||||||
|  |                                                 </v-text-field> | ||||||
|                                                 <v-slide-y-transition> |                                                 <v-slide-y-transition> | ||||||
|                                                     <div |                                                     <div | ||||||
|                                                         v-if="copied" |                                                         v-if="copied" | ||||||
|  | @ -367,10 +392,6 @@ | ||||||
|                         <using-query-result |                         <using-query-result | ||||||
|                             :query-result="getInvitationsQuery" |                             :query-result="getInvitationsQuery" | ||||||
|                             v-slot="invitationsResponse: { data: TeacherInvitationsResponse }" |                             v-slot="invitationsResponse: { data: TeacherInvitationsResponse }" | ||||||
|                         > |  | ||||||
|                             <using-query-result |  | ||||||
|                                 :query-result="allClassesQuery" |  | ||||||
|                                 v-slot="classesResponse: { data: ClassesResponse }" |  | ||||||
|                         > |                         > | ||||||
|                             <template v-if="invitationsResponse.data.invitations.length"> |                             <template v-if="invitationsResponse.data.invitations.length"> | ||||||
|                                 <tr |                                 <tr | ||||||
|  | @ -378,17 +399,11 @@ | ||||||
|                                     :key="i.classId" |                                     :key="i.classId" | ||||||
|                                 > |                                 > | ||||||
|                                     <td> |                                     <td> | ||||||
|                                             {{ |                                         <ClassDisplay :classId="i.classId" /> | ||||||
|                                                 (classesResponse.data.classes as ClassDTO[]).filter( |  | ||||||
|                                                     (c) => c.id == i.classId, |  | ||||||
|                                                 )[0].displayName |  | ||||||
|                                             }} |  | ||||||
|                                     </td> |                                     </td> | ||||||
|                                     <td> |                                     <td> | ||||||
|                                         {{ |                                         {{ | ||||||
|                                                 (i.sender as TeacherDTO).firstName + |                                             (i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName | ||||||
|                                                 " " + |  | ||||||
|                                                 (i.sender as TeacherDTO).lastName |  | ||||||
|                                         }} |                                         }} | ||||||
|                                     </td> |                                     </td> | ||||||
|                                     <td class="text-right"> |                                     <td class="text-right"> | ||||||
|  | @ -426,8 +441,9 @@ | ||||||
|                                                     color="red" |                                                     color="red" | ||||||
|                                                     variant="text" |                                                     variant="text" | ||||||
|                                                 > |                                                 > | ||||||
|                                                     </v-btn></div |                                                 </v-btn> | ||||||
|                                             ></span> |                                             </div> | ||||||
|  |                                         </span> | ||||||
|                                     </td> |                                     </td> | ||||||
|                                 </tr> |                                 </tr> | ||||||
|                             </template> |                             </template> | ||||||
|  | @ -447,7 +463,6 @@ | ||||||
|                                 </tr> |                                 </tr> | ||||||
|                             </template> |                             </template> | ||||||
|                         </using-query-result> |                         </using-query-result> | ||||||
|                         </using-query-result> |  | ||||||
|                     </tbody> |                     </tbody> | ||||||
|                 </v-table> |                 </v-table> | ||||||
|             </v-container> |             </v-container> | ||||||
|  | @ -469,9 +484,24 @@ | ||||||
|                     <v-text-field |                     <v-text-field | ||||||
|                         v-model="selectedCode" |                         v-model="selectedCode" | ||||||
|                         readonly |                         readonly | ||||||
|                         append-inner-icon="mdi-content-copy" |                     > | ||||||
|                         @click:append-inner="copyToClipboard" |                         <template #append> | ||||||
|                     ></v-text-field> |                             <v-btn | ||||||
|  |                                 icon | ||||||
|  |                                 variant="text" | ||||||
|  |                                 @click="copyToClipboard(selectedCode, true)" | ||||||
|  |                             > | ||||||
|  |                                 <v-icon>mdi-content-copy</v-icon> | ||||||
|  |                             </v-btn> | ||||||
|  |                             <v-btn | ||||||
|  |                                 icon | ||||||
|  |                                 variant="text" | ||||||
|  |                                 @click="copyToClipboard(selectedCode, true, true)" | ||||||
|  |                             > | ||||||
|  |                                 <v-icon>mdi-link-variant</v-icon> | ||||||
|  |                             </v-btn> | ||||||
|  |                         </template> | ||||||
|  |                     </v-text-field> | ||||||
|                     <v-slide-y-transition> |                     <v-slide-y-transition> | ||||||
|                         <div |                         <div | ||||||
|                             v-if="copied" |                             v-if="copied" | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ | ||||||
|     import authState from "@/services/auth/auth-service.ts"; |     import authState from "@/services/auth/auth-service.ts"; | ||||||
|     import TeacherClasses from "./TeacherClasses.vue"; |     import TeacherClasses from "./TeacherClasses.vue"; | ||||||
|     import StudentClasses from "./StudentClasses.vue"; |     import StudentClasses from "./StudentClasses.vue"; | ||||||
|  |     import { AccountType } from "@dwengo-1/common/util/account-types"; | ||||||
| 
 | 
 | ||||||
|     // Determine if role is student or teacher to render correct view |     // Determine if role is student or teacher to render correct view | ||||||
|     const role: string = authState.authState.activeRole!; |     const role: string = authState.authState.activeRole!; | ||||||
|  | @ -9,7 +10,7 @@ | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|     <main> |     <main> | ||||||
|         <TeacherClasses v-if="role === 'teacher'"></TeacherClasses> |         <TeacherClasses v-if="role === AccountType.Teacher"></TeacherClasses> | ||||||
|         <StudentClasses v-else></StudentClasses> |         <StudentClasses v-else></StudentClasses> | ||||||
|     </main> |     </main> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|  | @ -22,6 +22,7 @@ | ||||||
|     import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment"; |     import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment"; | ||||||
|     import type { GroupDTO } from "@dwengo-1/common/interfaces/group"; |     import type { GroupDTO } from "@dwengo-1/common/interfaces/group"; | ||||||
|     import QuestionNotification from "@/components/QuestionNotification.vue"; |     import QuestionNotification from "@/components/QuestionNotification.vue"; | ||||||
|  |     import { AccountType } from "@dwengo-1/common/util/account-types"; | ||||||
| 
 | 
 | ||||||
|     const router = useRouter(); |     const router = useRouter(); | ||||||
|     const route = useRoute(); |     const route = useRoute(); | ||||||
|  | @ -235,8 +236,10 @@ | ||||||
|                         </p> |                         </p> | ||||||
|                     </template> |                     </template> | ||||||
|                 </v-list-item> |                 </v-list-item> | ||||||
|                 <v-list-item |                 <v-list-itemF | ||||||
|                     v-if="query.classId && query.assignmentNo && authService.authState.activeRole === 'teacher'" |                     v-if=" | ||||||
|  |                         query.classId && query.assignmentNo && authService.authState.activeRole === AccountType.Teacher | ||||||
|  |                     " | ||||||
|                 > |                 > | ||||||
|                     <template v-slot:default> |                     <template v-slot:default> | ||||||
|                         <learning-path-group-selector |                         <learning-path-group-selector | ||||||
|  | @ -245,7 +248,7 @@ | ||||||
|                             v-model="forGroupQueryParam" |                             v-model="forGroupQueryParam" | ||||||
|                         /> |                         /> | ||||||
|                     </template> |                     </template> | ||||||
|                 </v-list-item> |                 </v-list-itemF> | ||||||
|                 <v-divider></v-divider> |                 <v-divider></v-divider> | ||||||
|                 <div v-if="props.learningObjectHruid"> |                 <div v-if="props.learningObjectHruid"> | ||||||
|                     <using-query-result |                     <using-query-result | ||||||
|  | @ -259,7 +262,9 @@ | ||||||
|                                 :title="node.title" |                                 :title="node.title" | ||||||
|                                 :active="node.key === props.learningObjectHruid" |                                 :active="node.key === props.learningObjectHruid" | ||||||
|                                 :key="node.key" |                                 :key="node.key" | ||||||
|                                 v-if="!node.teacherExclusive || authService.authState.activeRole === 'teacher'" |                                 v-if=" | ||||||
|  |                                     !node.teacherExclusive || authService.authState.activeRole === AccountType.Teacher | ||||||
|  |                                 " | ||||||
|                             > |                             > | ||||||
|                                 <template v-slot:prepend> |                                 <template v-slot:prepend> | ||||||
|                                     <v-icon |                                     <v-icon | ||||||
|  | @ -283,7 +288,7 @@ | ||||||
|                     </using-query-result> |                     </using-query-result> | ||||||
|                 </div> |                 </div> | ||||||
|                 <v-spacer></v-spacer> |                 <v-spacer></v-spacer> | ||||||
|                 <v-list-item v-if="authService.authState.activeRole === 'teacher'"> |                 <v-list-item v-if="authService.authState.activeRole === AccountType.Teacher"> | ||||||
|                     <template v-slot:default> |                     <template v-slot:default> | ||||||
|                         <v-btn |                         <v-btn | ||||||
|                             class="button-in-nav" |                             class="button-in-nav" | ||||||
|  | @ -296,7 +301,7 @@ | ||||||
|                 </v-list-item> |                 </v-list-item> | ||||||
|                 <v-list-item> |                 <v-list-item> | ||||||
|                     <div |                     <div | ||||||
|                         v-if="authService.authState.activeRole === 'student' && pathIsAssignment" |                         v-if="authService.authState.activeRole === AccountType.Student && pathIsAssignment" | ||||||
|                         class="assignment-indicator" |                         class="assignment-indicator" | ||||||
|                     > |                     > | ||||||
|                         {{ t("assignmentIndicator") }} |                         {{ t("assignmentIndicator") }} | ||||||
|  | @ -325,7 +330,7 @@ | ||||||
|             ></learning-object-view> |             ></learning-object-view> | ||||||
|         </div> |         </div> | ||||||
|         <div |         <div | ||||||
|             v-if="authService.authState.activeRole === 'student' && pathIsAssignment" |             v-if="authService.authState.activeRole === AccountType.Student && pathIsAssignment" | ||||||
|             class="question-box" |             class="question-box" | ||||||
|         > |         > | ||||||
|             <div class="input-wrapper"> |             <div class="input-wrapper"> | ||||||
|  |  | ||||||
|  | @ -17,23 +17,11 @@ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|     <v-container class="search-page-container"> |     <div class="search-page-container d-flex flex-column align-items-center justify-center"> | ||||||
|         <v-row |         <div class="search-field-container"> | ||||||
|             justify="center" |             <learning-path-search-field class="mx-auto" /> | ||||||
|             class="mb-6" |         </div> | ||||||
|         > |  | ||||||
|             <v-col |  | ||||||
|                 cols="12" |  | ||||||
|                 sm="8" |  | ||||||
|                 md="6" |  | ||||||
|                 lg="4" |  | ||||||
|             > |  | ||||||
|                 <learning-path-search-field class="search-field" /> |  | ||||||
|             </v-col> |  | ||||||
|         </v-row> |  | ||||||
| 
 | 
 | ||||||
|         <v-row justify="center"> |  | ||||||
|             <v-col cols="12"> |  | ||||||
|         <using-query-result |         <using-query-result | ||||||
|             :query-result="searchQueryResults" |             :query-result="searchQueryResults" | ||||||
|             v-slot="{ data }: { data: LearningPath[] }" |             v-slot="{ data }: { data: LearningPath[] }" | ||||||
|  | @ -51,9 +39,7 @@ | ||||||
|                 :text="t('enterSearchTermDescription')" |                 :text="t('enterSearchTermDescription')" | ||||||
|             /> |             /> | ||||||
|         </div> |         </div> | ||||||
|             </v-col> |     </div> | ||||||
|         </v-row> |  | ||||||
|     </v-container> |  | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
|  | @ -61,8 +47,7 @@ | ||||||
|         padding-top: 40px; |         padding-top: 40px; | ||||||
|         padding-bottom: 40px; |         padding-bottom: 40px; | ||||||
|     } |     } | ||||||
| 
 |     .search-field-container { | ||||||
|     .search-field { |         justify-content: center !important; | ||||||
|         max-width: 100%; |  | ||||||
|     } |     } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -8,8 +8,9 @@ | ||||||
|     import type { SubmissionData } from "@/views/learning-paths/learning-object/submission-data"; |     import type { SubmissionData } from "@/views/learning-paths/learning-object/submission-data"; | ||||||
|     import LearningObjectContentView from "@/views/learning-paths/learning-object/content/LearningObjectContentView.vue"; |     import LearningObjectContentView from "@/views/learning-paths/learning-object/content/LearningObjectContentView.vue"; | ||||||
|     import LearningObjectSubmissionsView from "@/views/learning-paths/learning-object/submissions/LearningObjectSubmissionsView.vue"; |     import LearningObjectSubmissionsView from "@/views/learning-paths/learning-object/submissions/LearningObjectSubmissionsView.vue"; | ||||||
|  |     import { AccountType } from "@dwengo-1/common/util/account-types"; | ||||||
| 
 | 
 | ||||||
|     const _isStudent = computed(() => authService.authState.activeRole === "student"); |     const _isStudent = computed(() => authService.authState.activeRole === AccountType.Student); | ||||||
| 
 | 
 | ||||||
|     const props = defineProps<{ |     const props = defineProps<{ | ||||||
|         hruid: string; |         hruid: string; | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ | ||||||
|     import type { StudentDTO } from "@dwengo-1/common/interfaces/student"; |     import type { StudentDTO } from "@dwengo-1/common/interfaces/student"; | ||||||
|     import type { GroupDTO } from "@dwengo-1/common/interfaces/group"; |     import type { GroupDTO } from "@dwengo-1/common/interfaces/group"; | ||||||
|     import { useI18n } from "vue-i18n"; |     import { useI18n } from "vue-i18n"; | ||||||
|  |     import { AccountType } from "@dwengo-1/common/util/account-types"; | ||||||
| 
 | 
 | ||||||
|     const { t } = useI18n(); |     const { t } = useI18n(); | ||||||
| 
 | 
 | ||||||
|  | @ -31,7 +32,7 @@ | ||||||
|         mutate: submitSolution, |         mutate: submitSolution, | ||||||
|     } = useCreateSubmissionMutation(); |     } = useCreateSubmissionMutation(); | ||||||
| 
 | 
 | ||||||
|     const isStudent = computed(() => authService.authState.activeRole === "student"); |     const isStudent = computed(() => authService.authState.activeRole === AccountType.Student); | ||||||
| 
 | 
 | ||||||
|     const isSubmitDisabled = computed(() => { |     const isSubmitDisabled = computed(() => { | ||||||
|         if (!props.submissionData || props.submissions === undefined) { |         if (!props.submissionData || props.submissions === undefined) { | ||||||
|  |  | ||||||
							
								
								
									
										1
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -100,6 +100,7 @@ | ||||||
|             "name": "dwengo-1-frontend", |             "name": "dwengo-1-frontend", | ||||||
|             "version": "0.2.0", |             "version": "0.2.0", | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|  |                 "@dwengo-1/common": "^0.2.0", | ||||||
|                 "@tanstack/react-query": "^5.69.0", |                 "@tanstack/react-query": "^5.69.0", | ||||||
|                 "@tanstack/vue-query": "^5.69.0", |                 "@tanstack/vue-query": "^5.69.0", | ||||||
|                 "@vueuse/core": "^13.1.0", |                 "@vueuse/core": "^13.1.0", | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Gabriellvl
						Gabriellvl