Merge pull request #200 from SELab-2/feat/endpoints-beschermen-met-authenticatie-#105

feat: Endpoints beschermen met authenticatie
This commit is contained in:
Gabriellvl 2025-05-16 10:14:37 +02:00 committed by GitHub
commit f69a6c155a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
58 changed files with 727 additions and 253 deletions

View file

@ -17,6 +17,7 @@
"test:e2e": "playwright test"
},
"dependencies": {
"@dwengo-1/common": "^0.2.0",
"@tanstack/react-query": "^5.69.0",
"@tanstack/vue-query": "^5.69.0",
"@vueuse/core": "^13.1.0",

View file

@ -6,6 +6,7 @@
import type { AnswersResponse } from "@/controllers/answers";
import type { AnswerData, AnswerDTO } from "@dwengo-1/common/interfaces/answer";
import authService from "@/services/auth/auth-service";
import { AccountType } from "@dwengo-1/common/util/account-types";
const props = defineProps<{
question: QuestionDTO;
@ -80,7 +81,7 @@
{{ question.content }}
</div>
<div
v-if="authService.authState.activeRole === 'teacher'"
v-if="authService.authState.activeRole === AccountType.Teacher"
class="answer-input-container"
>
<input

View file

@ -15,6 +15,7 @@ import { invalidateAllGroupKeys } from "./groups";
import { invalidateAllSubmissionKeys } from "./submissions";
import type { TeachersResponse } from "@/controllers/teachers";
import type { TeacherInvitationsResponse } from "@/controllers/teacher-invitations";
import { studentClassesQueryKey } from "@/queries/students.ts";
const classController = new ClassController();
@ -171,6 +172,8 @@ export function useClassDeleteStudentMutation(): UseMutationReturnType<
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, false) });
await queryClient.invalidateQueries({ queryKey: studentClassesQueryKey(data.class.id, false) });
await queryClient.invalidateQueries({ queryKey: studentClassesQueryKey(data.class.id, true) });
},
});
}

View file

@ -33,7 +33,7 @@ function studentsQueryKey(full: boolean): [string, boolean] {
function studentQueryKey(username: string): [string, string] {
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];
}
function studentAssignmentsQueryKey(username: string, full: boolean): [string, string, boolean] {

View file

@ -3,6 +3,7 @@
import dwengoLogo from "../../../assets/img/dwengo-groen-zwart.svg";
import auth from "@/services/auth/auth-service.ts";
import { watch } from "vue";
import { AccountType } from "@dwengo-1/common/util/account-types";
const router = useRouter();
@ -17,11 +18,11 @@
);
async function loginAsStudent(): Promise<void> {
await auth.loginAs("student");
await auth.loginAs(AccountType.Student);
}
async function loginAsTeacher(): Promise<void> {
await auth.loginAs("teacher");
await auth.loginAs(AccountType.Teacher);
}
</script>

View file

@ -14,6 +14,7 @@
import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment";
import { useCreateAssignmentMutation } from "@/queries/assignments.ts";
import { useRoute } from "vue-router";
import { AccountType } from "@dwengo-1/common/util/account-types";
const route = useRoute();
const router = useRouter();
@ -23,7 +24,7 @@
onMounted(async () => {
// Redirect student
if (role.value === "student") {
if (role.value === AccountType.Student) {
await router.push("/user");
}

View file

@ -8,9 +8,10 @@
import { useGetLearningPathQuery } from "@/queries/learning-paths.ts";
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
import type { GroupDTO } from "@dwengo-1/common/interfaces/group";
import { AccountType } from "@dwengo-1/common/util/account-types";
const role = auth.authState.activeRole;
const isTeacher = computed(() => role === "teacher");
const isTeacher = computed(() => role === AccountType.Teacher);
const route = useRoute();
const classId = ref<string>(route.params.classId as string);

View file

@ -9,6 +9,7 @@
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
import { asyncComputed } from "@vueuse/core";
import { useDeleteAssignmentMutation } from "@/queries/assignments.ts";
import { AccountType } from "@dwengo-1/common/util/account-types";
import "../../assets/common.css";
const { t, locale } = useI18n();
@ -17,7 +18,7 @@
const role = ref(auth.authState.activeRole);
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
let classesQueryResults = undefined;

View 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>

View file

@ -77,7 +77,7 @@
},
onError: (e) => {
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) => {
showSnackbar(t("failed") + ": " + e.message, "error");
showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error");
},
},
);
@ -126,7 +126,7 @@
usernameTeacher.value = "";
},
onError: (e) => {
showSnackbar(t("failed") + ": " + e.message, "error");
showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error");
},
});
}

View file

@ -100,7 +100,7 @@
showSnackbar(t("sent"), "success");
},
onError: (e) => {
showSnackbar(t("failed") + ": " + e.message, "error");
showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error");
},
},
);

View file

@ -8,7 +8,7 @@
import { useTeacherClassesQuery } from "@/queries/teachers";
import type { ClassesResponse } from "@/controllers/classes";
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 {
useRespondTeacherInvitationMutation,
@ -16,6 +16,7 @@
} from "@/queries/teacher-invitations";
import { useDisplay } from "vuetify";
import "../../assets/common.css";
import ClassDisplay from "@/views/classes/ClassDisplay.vue";
const { t } = useI18n();
@ -41,7 +42,6 @@
// Fetch all classes of the logged in teacher
const classesQuery = useTeacherClassesQuery(username, true);
const allClassesQuery = useClassesQuery();
const { mutate } = useCreateClassMutation();
const getInvitationsQuery = useTeacherInvitationsReceivedQuery(username);
const { mutate: respondToInvitation } = useRespondTeacherInvitationMutation();
@ -70,7 +70,7 @@
await getInvitationsQuery.refetch();
},
onError: (e) => {
showSnackbar(t("failed") + ": " + e.message, "error");
showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error");
},
});
}
@ -170,6 +170,7 @@
// Code display dialog logic
const viewCodeDialog = ref(false);
const selectedCode = ref("");
function openCodeDialog(codeToView: string): void {
selectedCode.value = codeToView;
viewCodeDialog.value = true;
@ -231,7 +232,7 @@
variant="text"
>
{{ c.displayName }}
<v-icon end> mdi-menu-right </v-icon>
<v-icon end> mdi-menu-right</v-icon>
</v-btn>
</td>
<td>
@ -300,8 +301,8 @@
type="submit"
@click="createClass"
block
>{{ t("create") }}</v-btn
>
>{{ t("create") }}
</v-btn>
</v-form>
</v-sheet>
<v-container>
@ -368,85 +369,75 @@
:query-result="getInvitationsQuery"
v-slot="invitationsResponse: { data: TeacherInvitationsResponse }"
>
<using-query-result
:query-result="allClassesQuery"
v-slot="classesResponse: { data: ClassesResponse }"
>
<template v-if="invitationsResponse.data.invitations.length">
<tr
v-for="i in invitationsResponse.data.invitations as TeacherInvitationDTO[]"
:key="i.classId"
<template v-if="invitationsResponse.data.invitations.length">
<tr
v-for="i in invitationsResponse.data.invitations as TeacherInvitationDTO[]"
:key="i.classId"
>
<td>
<ClassDisplay :classId="i.classId" />
</td>
<td>
{{
(i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName
}}
</td>
<td class="text-right">
<span v-if="!isSmAndDown">
<div>
<v-btn
color="green"
@click="handleInvitation(i, true)"
class="mr-2"
>
{{ t("accept") }}
</v-btn>
<v-btn
color="red"
@click="handleInvitation(i, false)"
>
{{ t("deny") }}
</v-btn>
</div>
</span>
<span v-else>
<div>
<v-btn
@click="handleInvitation(i, true)"
class="mr-2"
icon="mdi-check-circle"
color="green"
variant="text"
>
</v-btn>
<v-btn
@click="handleInvitation(i, false)"
class="mr-2"
icon="mdi-close-circle"
color="red"
variant="text"
>
</v-btn>
</div>
</span>
</td>
</tr>
</template>
<template v-else>
<tr>
<td
colspan="3"
class="empty-message"
>
<td>
{{
(classesResponse.data.classes as ClassDTO[]).filter(
(c) => c.id == i.classId,
)[0].displayName
}}
</td>
<td>
{{
(i.sender as TeacherDTO).firstName +
" " +
(i.sender as TeacherDTO).lastName
}}
</td>
<td class="text-right">
<span v-if="!isSmAndDown">
<div>
<v-btn
color="green"
@click="handleInvitation(i, true)"
class="mr-2"
>
{{ t("accept") }}
</v-btn>
<v-btn
color="red"
@click="handleInvitation(i, false)"
>
{{ t("deny") }}
</v-btn>
</div>
</span>
<span v-else>
<div>
<v-btn
@click="handleInvitation(i, true)"
class="mr-2"
icon="mdi-check-circle"
color="green"
variant="text"
>
</v-btn>
<v-btn
@click="handleInvitation(i, false)"
class="mr-2"
icon="mdi-close-circle"
color="red"
variant="text"
>
</v-btn></div
></span>
</td>
</tr>
</template>
<template v-else>
<tr>
<td
colspan="3"
class="empty-message"
<v-icon
icon="mdi-information-outline"
size="small"
>
<v-icon
icon="mdi-information-outline"
size="small"
>
</v-icon>
{{ t("no-invitations-found") }}
</td>
</tr>
</template>
</using-query-result>
</v-icon>
{{ t("no-invitations-found") }}
</td>
</tr>
</template>
</using-query-result>
</tbody>
</v-table>

View file

@ -2,6 +2,7 @@
import authState from "@/services/auth/auth-service.ts";
import TeacherClasses from "./TeacherClasses.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
const role: string = authState.authState.activeRole!;
@ -9,7 +10,7 @@
<template>
<main>
<TeacherClasses v-if="role === 'teacher'"></TeacherClasses>
<TeacherClasses v-if="role === AccountType.Teacher"></TeacherClasses>
<StudentClasses v-else></StudentClasses>
</main>
</template>

View file

@ -22,6 +22,7 @@
import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment";
import type { GroupDTO } from "@dwengo-1/common/interfaces/group";
import QuestionNotification from "@/components/QuestionNotification.vue";
import { AccountType } from "@dwengo-1/common/util/account-types";
const router = useRouter();
const route = useRoute();
@ -235,8 +236,10 @@
</p>
</template>
</v-list-item>
<v-list-item
v-if="query.classId && query.assignmentNo && authService.authState.activeRole === 'teacher'"
<v-list-itemF
v-if="
query.classId && query.assignmentNo && authService.authState.activeRole === AccountType.Teacher
"
>
<template v-slot:default>
<learning-path-group-selector
@ -245,7 +248,7 @@
v-model="forGroupQueryParam"
/>
</template>
</v-list-item>
</v-list-itemF>
<v-divider></v-divider>
<div v-if="props.learningObjectHruid">
<using-query-result
@ -259,7 +262,9 @@
:title="node.title"
:active="node.key === props.learningObjectHruid"
:key="node.key"
v-if="!node.teacherExclusive || authService.authState.activeRole === 'teacher'"
v-if="
!node.teacherExclusive || authService.authState.activeRole === AccountType.Teacher
"
>
<template v-slot:prepend>
<v-icon
@ -283,7 +288,7 @@
</using-query-result>
</div>
<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>
<v-btn
class="button-in-nav"
@ -296,7 +301,7 @@
</v-list-item>
<v-list-item>
<div
v-if="authService.authState.activeRole === 'student' && pathIsAssignment"
v-if="authService.authState.activeRole === AccountType.Student && pathIsAssignment"
class="assignment-indicator"
>
{{ t("assignmentIndicator") }}
@ -325,7 +330,7 @@
></learning-object-view>
</div>
<div
v-if="authService.authState.activeRole === 'student' && pathIsAssignment"
v-if="authService.authState.activeRole === AccountType.Student && pathIsAssignment"
class="question-box"
>
<div class="input-wrapper">

View file

@ -8,8 +8,9 @@
import type { SubmissionData } from "@/views/learning-paths/learning-object/submission-data";
import LearningObjectContentView from "@/views/learning-paths/learning-object/content/LearningObjectContentView.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<{
hruid: string;

View file

@ -11,6 +11,7 @@
import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
import type { GroupDTO } from "@dwengo-1/common/interfaces/group";
import { useI18n } from "vue-i18n";
import { AccountType } from "@dwengo-1/common/util/account-types";
const { t } = useI18n();
@ -31,7 +32,7 @@
mutate: submitSolution,
} = useCreateSubmissionMutation();
const isStudent = computed(() => authService.authState.activeRole === "student");
const isStudent = computed(() => authService.authState.activeRole === AccountType.Student);
const isSubmitDisabled = computed(() => {
if (!props.submissionData || props.submissions === undefined) {