From aac96e9bc6553e77629c5d2d86f37d24061984e5 Mon Sep 17 00:00:00 2001 From: Bram Reyniers Date: Fri, 3 May 2024 00:31:12 +0200 Subject: [PATCH 01/23] generic permission middleware --- frontend/src/composables/useIsTeacher.ts | 7 ++--- frontend/src/main.ts | 7 +++-- frontend/src/queries/User.ts | 19 +++++++++----- frontend/src/router/index.ts | 8 ++++++ frontend/src/router/middleware/canVisit.ts | 30 ++++++++++++++++++++++ 5 files changed, 60 insertions(+), 11 deletions(-) create mode 100644 frontend/src/router/middleware/canVisit.ts diff --git a/frontend/src/composables/useIsTeacher.ts b/frontend/src/composables/useIsTeacher.ts index 8f052a76..6a505e6b 100644 --- a/frontend/src/composables/useIsTeacher.ts +++ b/frontend/src/composables/useIsTeacher.ts @@ -1,8 +1,9 @@ import { computed } from "vue"; import { useUserQuery } from "@/queries/User"; +import type { QueryClient } from "@tanstack/vue-query"; -export default function useIsTeacher() { - const { data: user } = useUserQuery(); +export default function useIsTeacher(queryClient?: QueryClient) { + const { data: user, isLoading } = useUserQuery(null, queryClient); const isTeacher = computed(() => user.value?.is_teacher || false); - return { isTeacher }; + return { isTeacher, isLoading }; } diff --git a/frontend/src/main.ts b/frontend/src/main.ts index f31b4b77..d4758d18 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -3,7 +3,7 @@ import "@mdi/font/css/materialdesignicons.css"; import { createApp } from "vue"; import { createPinia } from "pinia"; -import { VueQueryPlugin } from "@tanstack/vue-query"; +import { VueQueryPlugin, QueryClient } from "@tanstack/vue-query"; import App from "./App.vue"; import router from "./router"; @@ -16,10 +16,13 @@ const app = createApp(App); const pinia = createPinia(); +const queryClient = new QueryClient(); +app.provide("queryClient", queryClient); + app.use(router); app.use(pinia); app.use(vuetify); app.use(i18n); -app.use(VueQueryPlugin); +app.use(VueQueryPlugin, { queryClient }); app.mount("#app"); diff --git a/frontend/src/queries/User.ts b/frontend/src/queries/User.ts index 79d5eebd..327fb499 100644 --- a/frontend/src/queries/User.ts +++ b/frontend/src/queries/User.ts @@ -4,6 +4,7 @@ import { useQueryClient, type UseQueryReturnType, type UseMutationReturnType, + type QueryClient, } from "@tanstack/vue-query"; import type User from "@/models/User"; import { getMySubjects, getUser, getUsers, toggleAdmin, toggleTeacher } from "@/services/user"; @@ -18,12 +19,18 @@ function USERS_QUERY_KEY(): string[] { return ["users"]; } -export function useUserQuery(uid: Ref | null): UseQueryReturnType { - return useQuery({ - queryKey: computed(() => USER_QUERY_KEY(uid?.value!)), - queryFn: () => getUser(uid?.value!), - enabled: uid === null || uid?.value !== undefined, - }); +export function useUserQuery( + uid: Ref | null, + queryClient?: QueryClient +): UseQueryReturnType { + return useQuery( + { + queryKey: computed(() => USER_QUERY_KEY(uid?.value!)), + queryFn: () => getUser(uid?.value!), + enabled: uid === null || uid?.value !== undefined, + }, + queryClient + ); } export function useUsersQuery(): UseQueryReturnType { diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 5d3bbbc6..e4e7303a 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -2,6 +2,8 @@ import { createRouter, createWebHistory } from "vue-router"; import { type Middleware, type MiddlewareContext, nextFactory } from "./middleware/index"; import isAuthenticated from "./middleware/isAuthenticated"; import loginMiddleware from "./middleware/login"; +import useCanVisit from "./middleware/canVisit"; +import useIsTeacher from "@/composables/useIsTeacher"; declare module "vue-router" { interface RouteMeta { @@ -75,6 +77,12 @@ const router = createRouter({ name: "create-project", component: () => import("../views/CreateProjectView.vue"), props: (route) => ({ subjectId: Number(route.params.subjectId) }), + meta: { + middleware: useCanVisit((queryClient) => { + const { isTeacher, isLoading } = useIsTeacher(queryClient); + return { condition: isTeacher, isLoading }; + }), + }, }, { path: "/subjects/register/:uuid", diff --git a/frontend/src/router/middleware/canVisit.ts b/frontend/src/router/middleware/canVisit.ts new file mode 100644 index 00000000..650cb7ed --- /dev/null +++ b/frontend/src/router/middleware/canVisit.ts @@ -0,0 +1,30 @@ +import { inject, type Ref } from "vue"; +import { type Middleware } from "./index"; +import { QueryClient } from "@tanstack/vue-query"; + +interface CanVisitCondition { + (queryClient: QueryClient): { condition: Ref; isLoading: Ref }; +} + +function useCanVisit(useCondition: CanVisitCondition): Middleware { + return async ({ to, next, router }) => { + const queryClient = inject("queryClient", new QueryClient()); + const { condition, isLoading } = useCondition(queryClient); + const awaitLoading = () => + new Promise((resolve) => { + const interval = setInterval(() => { + if (!isLoading.value) { + clearInterval(interval); + resolve(); + } + }, 10); + }); + await awaitLoading(); + if (!condition.value) { + router.replace({ path: "/404" }); + } + return next(); + }; +} + +export default useCanVisit; From 8e5650891e2cb8ab7b419804e56f42ff2c612290 Mon Sep 17 00:00:00 2001 From: Bram Reyniers Date: Fri, 3 May 2024 10:12:28 +0200 Subject: [PATCH 02/23] placeholder middleware implementations --- frontend/src/router/index.ts | 26 ++++++++++++++++++++++ frontend/src/router/middleware/canVisit.ts | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index e4e7303a..16ed7e3e 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -4,6 +4,7 @@ import isAuthenticated from "./middleware/isAuthenticated"; import loginMiddleware from "./middleware/login"; import useCanVisit from "./middleware/canVisit"; import useIsTeacher from "@/composables/useIsTeacher"; +import { ref } from "vue"; declare module "vue-router" { interface RouteMeta { @@ -53,12 +54,24 @@ const router = createRouter({ name: "project", component: () => import("../views/ProjectView.vue"), props: (route) => ({ projectId: Number(route.params.projectId) }), + meta : { + middlweware: useCanVisit((queryClient) => { + // TODO: implement -> check if user is enlroled in subject + return { condition: ref(true), isLoading: ref(false) }; + }), + } }, { path: "/project/:projectId(\\d+)/submit", name: "onSubmit", component: () => import("../views/SubmitView.vue"), props: (route) => ({ projectId: Number(route.params.projectId) }), + meta : { + middlweware: useCanVisit((queryClient) => { + // TODO: implement -> check if user is enlroled in subject and in a group for project + return { condition: ref(true), isLoading: ref(false) }; + }), + } }, { path: "/subjects", @@ -71,6 +84,12 @@ const router = createRouter({ name: "subject", component: () => import("../views/subject/SubjectView.vue"), props: (route) => ({ subjectId: Number(route.params.subjectId) }), + meta : { + middlweware: useCanVisit((queryClient) => { + // TODO: implement -> check if user is enlroled in subject + return { condition: ref(true), isLoading: ref(false) }; + }), + } }, { path: "/subjects/:subjectId(\\d+)/create-project", @@ -79,6 +98,7 @@ const router = createRouter({ props: (route) => ({ subjectId: Number(route.params.subjectId) }), meta: { middleware: useCanVisit((queryClient) => { + // TODO: check if user is teacher or instructor of subject const { isTeacher, isLoading } = useIsTeacher(queryClient); return { condition: isTeacher, isLoading }; }), @@ -99,6 +119,12 @@ const router = createRouter({ path: "/admin", name: "admin", component: () => import("../views/AdminView.vue"), + meta : { + middlweware: useCanVisit((queryClient) => { + // TODO: implement check if user is admin + return { condition: ref(true), isLoading: ref(false) }; + }), + } }, { path: "/:pathMatch(.*)", diff --git a/frontend/src/router/middleware/canVisit.ts b/frontend/src/router/middleware/canVisit.ts index 650cb7ed..a5b872ef 100644 --- a/frontend/src/router/middleware/canVisit.ts +++ b/frontend/src/router/middleware/canVisit.ts @@ -2,7 +2,7 @@ import { inject, type Ref } from "vue"; import { type Middleware } from "./index"; import { QueryClient } from "@tanstack/vue-query"; -interface CanVisitCondition { +export interface CanVisitCondition { (queryClient: QueryClient): { condition: Ref; isLoading: Ref }; } From d822806dddf7588c27d279714d36c3273aa7ebb9 Mon Sep 17 00:00:00 2001 From: Bram Reyniers Date: Fri, 3 May 2024 10:26:56 +0200 Subject: [PATCH 03/23] implement isAdmin check --- frontend/src/composables/useIsAdmin.ts | 7 ++++--- frontend/src/router/middleware/canVisit.ts | 8 +++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/frontend/src/composables/useIsAdmin.ts b/frontend/src/composables/useIsAdmin.ts index f5930f60..b596e29f 100644 --- a/frontend/src/composables/useIsAdmin.ts +++ b/frontend/src/composables/useIsAdmin.ts @@ -1,8 +1,9 @@ import { computed } from "vue"; import { useUserQuery } from "@/queries/User"; +import type { QueryClient } from "@tanstack/vue-query"; -export default function useIsAdmin() { - const { data: user } = useUserQuery(null); +export default function useIsAdmin(queryClient?: QueryClient) { + const { data: user, isLoading } = useUserQuery(null, queryClient); const isAdmin = computed(() => user.value?.is_admin || false); - return { isAdmin }; + return { isAdmin, isLoading }; } diff --git a/frontend/src/router/middleware/canVisit.ts b/frontend/src/router/middleware/canVisit.ts index a5b872ef..c97c7f6f 100644 --- a/frontend/src/router/middleware/canVisit.ts +++ b/frontend/src/router/middleware/canVisit.ts @@ -1,6 +1,7 @@ import { inject, type Ref } from "vue"; import { type Middleware } from "./index"; import { QueryClient } from "@tanstack/vue-query"; +import useIsAdmin from "@/composables/useIsAdmin"; export interface CanVisitCondition { (queryClient: QueryClient): { condition: Ref; isLoading: Ref }; @@ -21,10 +22,15 @@ function useCanVisit(useCondition: CanVisitCondition): Middleware { }); await awaitLoading(); if (!condition.value) { - router.replace({ path: "/404" }); + router.replace({ path: "forbidden" }); } return next(); }; } export default useCanVisit; + +export const useIsAdminCondition: CanVisitCondition = (queryClient) => { + const { isAdmin, isLoading } = useIsAdmin(queryClient); + return { condition: isAdmin, isLoading }; +} From f6d7d47801024f24dd7396a4e45a78f972970512 Mon Sep 17 00:00:00 2001 From: Bram Reyniers Date: Fri, 3 May 2024 10:27:29 +0200 Subject: [PATCH 04/23] permissions on admin page + fix typo --- frontend/src/router/index.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 16ed7e3e..4ae1a3eb 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -2,7 +2,7 @@ import { createRouter, createWebHistory } from "vue-router"; import { type Middleware, type MiddlewareContext, nextFactory } from "./middleware/index"; import isAuthenticated from "./middleware/isAuthenticated"; import loginMiddleware from "./middleware/login"; -import useCanVisit from "./middleware/canVisit"; +import useCanVisit, { useIsAdminCondition } from "./middleware/canVisit"; import useIsTeacher from "@/composables/useIsTeacher"; import { ref } from "vue"; @@ -55,7 +55,7 @@ const router = createRouter({ component: () => import("../views/ProjectView.vue"), props: (route) => ({ projectId: Number(route.params.projectId) }), meta : { - middlweware: useCanVisit((queryClient) => { + middleware: useCanVisit((queryClient) => { // TODO: implement -> check if user is enlroled in subject return { condition: ref(true), isLoading: ref(false) }; }), @@ -67,7 +67,7 @@ const router = createRouter({ component: () => import("../views/SubmitView.vue"), props: (route) => ({ projectId: Number(route.params.projectId) }), meta : { - middlweware: useCanVisit((queryClient) => { + middleware: useCanVisit((queryClient) => { // TODO: implement -> check if user is enlroled in subject and in a group for project return { condition: ref(true), isLoading: ref(false) }; }), @@ -85,7 +85,7 @@ const router = createRouter({ component: () => import("../views/subject/SubjectView.vue"), props: (route) => ({ subjectId: Number(route.params.subjectId) }), meta : { - middlweware: useCanVisit((queryClient) => { + middleware: useCanVisit((queryClient) => { // TODO: implement -> check if user is enlroled in subject return { condition: ref(true), isLoading: ref(false) }; }), @@ -120,10 +120,7 @@ const router = createRouter({ name: "admin", component: () => import("../views/AdminView.vue"), meta : { - middlweware: useCanVisit((queryClient) => { - // TODO: implement check if user is admin - return { condition: ref(true), isLoading: ref(false) }; - }), + middleware: useCanVisit(useIsAdminCondition), } }, { From 19ff1402aecd19c8b5627f120215943971e2b8ad Mon Sep 17 00:00:00 2001 From: Bram Reyniers Date: Fri, 3 May 2024 10:42:48 +0200 Subject: [PATCH 05/23] logical functions for conditions --- frontend/src/router/index.ts | 16 ++++---- frontend/src/router/middleware/canVisit.ts | 46 ++++++++++++++++++++-- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 4ae1a3eb..8e9c92c2 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -54,24 +54,24 @@ const router = createRouter({ name: "project", component: () => import("../views/ProjectView.vue"), props: (route) => ({ projectId: Number(route.params.projectId) }), - meta : { + meta: { middleware: useCanVisit((queryClient) => { // TODO: implement -> check if user is enlroled in subject return { condition: ref(true), isLoading: ref(false) }; }), - } + }, }, { path: "/project/:projectId(\\d+)/submit", name: "onSubmit", component: () => import("../views/SubmitView.vue"), props: (route) => ({ projectId: Number(route.params.projectId) }), - meta : { + meta: { middleware: useCanVisit((queryClient) => { // TODO: implement -> check if user is enlroled in subject and in a group for project return { condition: ref(true), isLoading: ref(false) }; }), - } + }, }, { path: "/subjects", @@ -84,12 +84,12 @@ const router = createRouter({ name: "subject", component: () => import("../views/subject/SubjectView.vue"), props: (route) => ({ subjectId: Number(route.params.subjectId) }), - meta : { + meta: { middleware: useCanVisit((queryClient) => { // TODO: implement -> check if user is enlroled in subject return { condition: ref(true), isLoading: ref(false) }; }), - } + }, }, { path: "/subjects/:subjectId(\\d+)/create-project", @@ -119,9 +119,9 @@ const router = createRouter({ path: "/admin", name: "admin", component: () => import("../views/AdminView.vue"), - meta : { + meta: { middleware: useCanVisit(useIsAdminCondition), - } + }, }, { path: "/:pathMatch(.*)", diff --git a/frontend/src/router/middleware/canVisit.ts b/frontend/src/router/middleware/canVisit.ts index c97c7f6f..cb704cc4 100644 --- a/frontend/src/router/middleware/canVisit.ts +++ b/frontend/src/router/middleware/canVisit.ts @@ -1,7 +1,8 @@ -import { inject, type Ref } from "vue"; +import { computed, inject, type Ref } from "vue"; import { type Middleware } from "./index"; import { QueryClient } from "@tanstack/vue-query"; import useIsAdmin from "@/composables/useIsAdmin"; +import useIsTeacher from "@/composables/useIsTeacher"; export interface CanVisitCondition { (queryClient: QueryClient): { condition: Ref; isLoading: Ref }; @@ -30,7 +31,44 @@ function useCanVisit(useCondition: CanVisitCondition): Middleware { export default useCanVisit; -export const useIsAdminCondition: CanVisitCondition = (queryClient) => { - const { isAdmin, isLoading } = useIsAdmin(queryClient); - return { condition: isAdmin, isLoading }; +export function useOrCondition( + condition1: CanVisitCondition, + condition2: CanVisitCondition +): CanVisitCondition { + return (qc) => { + const { condition: condition1Value, isLoading: isLoading1 } = condition1(qc); + const { condition: condition2Value, isLoading: isLoading2 } = condition2(qc); + return { + condition: computed(() => condition1Value.value || condition2Value.value), + isLoading: computed(() => isLoading1.value || isLoading2.value), + }; + }; +} + +export function useAndCondition( + condition1: CanVisitCondition, + condition2: CanVisitCondition +): CanVisitCondition { + return (qc) => { + const { condition: condition1Value, isLoading: isLoading1 } = condition1(qc); + const { condition: condition2Value, isLoading: isLoading2 } = condition2(qc); + return { + condition: condition1Value && condition2Value, + isLoading: isLoading1 || isLoading2, + }; + }; } + +export const useIsAdminCondition: CanVisitCondition = (qc) => { + const { isAdmin, isLoading } = useIsAdmin(qc); + return { condition: isAdmin, isLoading }; +}; + +export const useIsTeacherCondition: CanVisitCondition = (qc) => { + const { isTeacher, isLoading } = useIsTeacher(qc); + return { condition: isTeacher, isLoading }; +}; + +export const useIsPartOfSubjectCondition: CanVisitCondition = (qc) => { + return { condition: false, isLoading: false }; +}; From 1476e651f9f82db103a822e366f24bf89b7301b7 Mon Sep 17 00:00:00 2001 From: Bram Reyniers Date: Fri, 3 May 2024 10:53:58 +0200 Subject: [PATCH 06/23] pass MiddlewareContext to conditions --- frontend/src/queries/Subject.ts | 4 ++-- frontend/src/router/middleware/canVisit.ts | 22 ++++++++++++---------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/frontend/src/queries/Subject.ts b/frontend/src/queries/Subject.ts index 6cd286d5..e6615717 100644 --- a/frontend/src/queries/Subject.ts +++ b/frontend/src/queries/Subject.ts @@ -1,4 +1,4 @@ -import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; +import { QueryClient, useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; import { getSubject, getSubjectInstructors, @@ -63,7 +63,7 @@ export function useSubjectProjectsQuery( }); } -export function useSubjectsQuery(): UseQueryReturnType { +export function useSubjectsQuery(queryClient?: QueryClient): UseQueryReturnType { return useQuery({ queryKey: createSubjectQueryKey("all"), queryFn: getSubjects, diff --git a/frontend/src/router/middleware/canVisit.ts b/frontend/src/router/middleware/canVisit.ts index cb704cc4..6ccda5c6 100644 --- a/frontend/src/router/middleware/canVisit.ts +++ b/frontend/src/router/middleware/canVisit.ts @@ -1,17 +1,19 @@ import { computed, inject, type Ref } from "vue"; -import { type Middleware } from "./index"; +import type { Middleware, MiddlewareContext } from "./index"; import { QueryClient } from "@tanstack/vue-query"; import useIsAdmin from "@/composables/useIsAdmin"; import useIsTeacher from "@/composables/useIsTeacher"; +import { useSubjectsQuery } from "@/queries/Subject"; export interface CanVisitCondition { - (queryClient: QueryClient): { condition: Ref; isLoading: Ref }; + (queryClient: QueryClient, context: MiddlewareContext): { condition: Ref; isLoading: Ref }; } function useCanVisit(useCondition: CanVisitCondition): Middleware { - return async ({ to, next, router }) => { + return async (context) => { + const { next, router } = context; const queryClient = inject("queryClient", new QueryClient()); - const { condition, isLoading } = useCondition(queryClient); + const { condition, isLoading } = useCondition(queryClient, context); const awaitLoading = () => new Promise((resolve) => { const interval = setInterval(() => { @@ -35,9 +37,9 @@ export function useOrCondition( condition1: CanVisitCondition, condition2: CanVisitCondition ): CanVisitCondition { - return (qc) => { - const { condition: condition1Value, isLoading: isLoading1 } = condition1(qc); - const { condition: condition2Value, isLoading: isLoading2 } = condition2(qc); + return (qc, ctx) => { + const { condition: condition1Value, isLoading: isLoading1 } = condition1(qc, ctx); + const { condition: condition2Value, isLoading: isLoading2 } = condition2(qc, ctx); return { condition: computed(() => condition1Value.value || condition2Value.value), isLoading: computed(() => isLoading1.value || isLoading2.value), @@ -49,9 +51,9 @@ export function useAndCondition( condition1: CanVisitCondition, condition2: CanVisitCondition ): CanVisitCondition { - return (qc) => { - const { condition: condition1Value, isLoading: isLoading1 } = condition1(qc); - const { condition: condition2Value, isLoading: isLoading2 } = condition2(qc); + return (qc, ctx) => { + const { condition: condition1Value, isLoading: isLoading1 } = condition1(qc, ctx); + const { condition: condition2Value, isLoading: isLoading2 } = condition2(qc, ctx); return { condition: condition1Value && condition2Value, isLoading: isLoading1 || isLoading2, From f59fc16d5f514691fb09f3d50f8b4ac3f910c1db Mon Sep 17 00:00:00 2001 From: Bram Reyniers Date: Fri, 3 May 2024 11:17:28 +0200 Subject: [PATCH 07/23] permission to check if user is part of subject --- frontend/src/router/index.ts | 7 ++----- frontend/src/router/middleware/canVisit.ts | 9 +++++++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 8e9c92c2..7d8141eb 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -2,7 +2,7 @@ import { createRouter, createWebHistory } from "vue-router"; import { type Middleware, type MiddlewareContext, nextFactory } from "./middleware/index"; import isAuthenticated from "./middleware/isAuthenticated"; import loginMiddleware from "./middleware/login"; -import useCanVisit, { useIsAdminCondition } from "./middleware/canVisit"; +import useCanVisit, { useIsAdminCondition, useIsPartOfSubjectCondition } from "./middleware/canVisit"; import useIsTeacher from "@/composables/useIsTeacher"; import { ref } from "vue"; @@ -85,10 +85,7 @@ const router = createRouter({ component: () => import("../views/subject/SubjectView.vue"), props: (route) => ({ subjectId: Number(route.params.subjectId) }), meta: { - middleware: useCanVisit((queryClient) => { - // TODO: implement -> check if user is enlroled in subject - return { condition: ref(true), isLoading: ref(false) }; - }), + middleware: useCanVisit(useIsPartOfSubjectCondition), }, }, { diff --git a/frontend/src/router/middleware/canVisit.ts b/frontend/src/router/middleware/canVisit.ts index 6ccda5c6..51cf3223 100644 --- a/frontend/src/router/middleware/canVisit.ts +++ b/frontend/src/router/middleware/canVisit.ts @@ -71,6 +71,11 @@ export const useIsTeacherCondition: CanVisitCondition = (qc) => { return { condition: isTeacher, isLoading }; }; -export const useIsPartOfSubjectCondition: CanVisitCondition = (qc) => { - return { condition: false, isLoading: false }; +export const useIsPartOfSubjectCondition: CanVisitCondition = (qc, ctx) => { + const subjectId = Number(ctx.to.params.subjectId); + const {data: subjects, isLoading} = useSubjectsQuery(qc); + const condition = computed(() => { + return subjects.value?.findIndex((subject) => subject.id === subjectId) !== -1; + }) + return { condition, isLoading }; }; From d717c72b74ec474ce177ad2b6cd864bb4c8e6063 Mon Sep 17 00:00:00 2001 From: Bram Reyniers Date: Fri, 3 May 2024 11:31:27 +0200 Subject: [PATCH 08/23] permission to check if user can create a project for a subject --- frontend/src/router/index.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 7d8141eb..4f783c6d 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -2,8 +2,7 @@ import { createRouter, createWebHistory } from "vue-router"; import { type Middleware, type MiddlewareContext, nextFactory } from "./middleware/index"; import isAuthenticated from "./middleware/isAuthenticated"; import loginMiddleware from "./middleware/login"; -import useCanVisit, { useIsAdminCondition, useIsPartOfSubjectCondition } from "./middleware/canVisit"; -import useIsTeacher from "@/composables/useIsTeacher"; +import useCanVisit, { useIsAdminCondition, useIsTeacherCondition, useIsPartOfSubjectCondition, useAndCondition } from "./middleware/canVisit"; import { ref } from "vue"; declare module "vue-router" { @@ -94,11 +93,7 @@ const router = createRouter({ component: () => import("../views/CreateProjectView.vue"), props: (route) => ({ subjectId: Number(route.params.subjectId) }), meta: { - middleware: useCanVisit((queryClient) => { - // TODO: check if user is teacher or instructor of subject - const { isTeacher, isLoading } = useIsTeacher(queryClient); - return { condition: isTeacher, isLoading }; - }), + middleware: useCanVisit(useAndCondition(useIsPartOfSubjectCondition, useIsTeacherCondition)), }, }, { From f876ce1807c12d0758e0f9bf0ca6641de399f625 Mon Sep 17 00:00:00 2001 From: Bram Reyniers Date: Fri, 3 May 2024 11:35:17 +0200 Subject: [PATCH 09/23] run formatter --- frontend/src/router/index.ts | 11 +++++++++-- frontend/src/router/middleware/canVisit.ts | 17 ++++++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 4f783c6d..1ec00919 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -2,7 +2,12 @@ import { createRouter, createWebHistory } from "vue-router"; import { type Middleware, type MiddlewareContext, nextFactory } from "./middleware/index"; import isAuthenticated from "./middleware/isAuthenticated"; import loginMiddleware from "./middleware/login"; -import useCanVisit, { useIsAdminCondition, useIsTeacherCondition, useIsPartOfSubjectCondition, useAndCondition } from "./middleware/canVisit"; +import useCanVisit, { + useIsAdminCondition, + useIsTeacherCondition, + useIsPartOfSubjectCondition, + useAndCondition, +} from "./middleware/canVisit"; import { ref } from "vue"; declare module "vue-router" { @@ -93,7 +98,9 @@ const router = createRouter({ component: () => import("../views/CreateProjectView.vue"), props: (route) => ({ subjectId: Number(route.params.subjectId) }), meta: { - middleware: useCanVisit(useAndCondition(useIsPartOfSubjectCondition, useIsTeacherCondition)), + middleware: useCanVisit( + useAndCondition(useIsPartOfSubjectCondition, useIsTeacherCondition) + ), }, }, { diff --git a/frontend/src/router/middleware/canVisit.ts b/frontend/src/router/middleware/canVisit.ts index 51cf3223..0496a9d5 100644 --- a/frontend/src/router/middleware/canVisit.ts +++ b/frontend/src/router/middleware/canVisit.ts @@ -6,7 +6,10 @@ import useIsTeacher from "@/composables/useIsTeacher"; import { useSubjectsQuery } from "@/queries/Subject"; export interface CanVisitCondition { - (queryClient: QueryClient, context: MiddlewareContext): { condition: Ref; isLoading: Ref }; + ( + queryClient: QueryClient, + context: MiddlewareContext + ): { condition: Ref; isLoading: Ref }; } function useCanVisit(useCondition: CanVisitCondition): Middleware { @@ -72,10 +75,10 @@ export const useIsTeacherCondition: CanVisitCondition = (qc) => { }; export const useIsPartOfSubjectCondition: CanVisitCondition = (qc, ctx) => { - const subjectId = Number(ctx.to.params.subjectId); - const {data: subjects, isLoading} = useSubjectsQuery(qc); - const condition = computed(() => { - return subjects.value?.findIndex((subject) => subject.id === subjectId) !== -1; - }) - return { condition, isLoading }; + const subjectId = Number(ctx.to.params.subjectId); + const { data: subjects, isLoading } = useSubjectsQuery(qc); + const condition = computed(() => { + return subjects.value?.findIndex((subject) => subject.id === subjectId) !== -1; + }); + return { condition, isLoading }; }; From 2650564bc02c36e952d948d86ce804bbf381d06c Mon Sep 17 00:00:00 2001 From: Bram Reyniers Date: Sat, 18 May 2024 00:45:24 +0200 Subject: [PATCH 10/23] restructure navigation guard middlewares --- frontend/src/queries/Subject.ts | 17 +++++++++------ frontend/src/router/index.ts | 21 ++++++++++++------- frontend/src/router/middleware/canVisit.ts | 21 ++++++++++++++----- frontend/src/router/middleware/index.ts | 18 +++++----------- .../src/router/middleware/isAuthenticated.ts | 12 ++++++++--- frontend/src/router/middleware/logger.ts | 11 ++++++++++ frontend/src/router/middleware/login.ts | 15 ++++++++----- 7 files changed, 76 insertions(+), 39 deletions(-) create mode 100644 frontend/src/router/middleware/logger.ts diff --git a/frontend/src/queries/Subject.ts b/frontend/src/queries/Subject.ts index e598543f..e48fe76b 100644 --- a/frontend/src/queries/Subject.ts +++ b/frontend/src/queries/Subject.ts @@ -1,7 +1,7 @@ import { computed, toValue } from "vue"; import type { MaybeRefOrGetter } from "vue"; import { useMutation, useQuery, useQueryClient } from "@tanstack/vue-query"; -import type { UseQueryReturnType, UseMutationReturnType } from "@tanstack/vue-query"; +import type { UseQueryReturnType, UseMutationReturnType, QueryClient } from "@tanstack/vue-query"; import { getSubject, getSubjectInstructors, @@ -66,11 +66,16 @@ export function useSubjectUuidQuery( /** * Query composable for fetching all subjects of the current user */ -export function useSubjectsQuery(): UseQueryReturnType { - return useQuery({ - queryKey: SUBJECTS_QUERY_KEY(), - queryFn: getSubjects, - }); +export function useSubjectsQuery( + queryClient?: QueryClient +): UseQueryReturnType { + return useQuery( + { + queryKey: SUBJECTS_QUERY_KEY(), + queryFn: getSubjects, + }, + queryClient + ); } /** diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index fd7d9ceb..1049d2da 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -1,5 +1,5 @@ import { createRouter, createWebHistory } from "vue-router"; -import { type Middleware, type MiddlewareContext, nextFactory } from "./middleware/index"; +import { type Middleware, type MiddlewareContext } from "./middleware/index"; import isAuthenticated from "./middleware/isAuthenticated"; import loginMiddleware from "./middleware/login"; import useCanVisit, { @@ -158,22 +158,29 @@ const router = createRouter({ }); router.beforeEach(async (to, from, next) => { - const middleware: Middleware[] = []; + const middlewares: Middleware[] = []; // Always check for authentication - middleware.push(isAuthenticated); + middlewares.push(isAuthenticated); // Add additional middleware if specified if (to.meta.middleware) { const meta_middleware = Array.isArray(to.meta.middleware) ? to.meta.middleware : [to.meta.middleware]; - middleware.push(...meta_middleware.filter((m) => m !== isAuthenticated)); + middlewares.push(...meta_middleware.filter((m) => m !== isAuthenticated)); } - const context: MiddlewareContext = { to, from, next, router }; - const nextMiddleware = nextFactory(context, middleware, 0); - return nextMiddleware(); + let new_next = next; + for (let middleware of middlewares) { + const context: MiddlewareContext = { to, from, next: new_next, router }; + const { next: returned_next, final } = await middleware(context); + if (final) { + return returned_next(); + } + new_next = returned_next; + } + return new_next(); }); export default router; diff --git a/frontend/src/router/middleware/canVisit.ts b/frontend/src/router/middleware/canVisit.ts index 0496a9d5..afd92fdb 100644 --- a/frontend/src/router/middleware/canVisit.ts +++ b/frontend/src/router/middleware/canVisit.ts @@ -14,8 +14,10 @@ export interface CanVisitCondition { function useCanVisit(useCondition: CanVisitCondition): Middleware { return async (context) => { - const { next, router } = context; - const queryClient = inject("queryClient", new QueryClient()); + const { next } = context; + // TODO: Figure out why this doesn't work anymore + // const queryClient = inject("queryClient", new QueryClient()); + const queryClient = new QueryClient(); const { condition, isLoading } = useCondition(queryClient, context); const awaitLoading = () => new Promise((resolve) => { @@ -28,9 +30,12 @@ function useCanVisit(useCondition: CanVisitCondition): Middleware { }); await awaitLoading(); if (!condition.value) { - router.replace({ path: "forbidden" }); + return { + next: () => next({ path: "forbidden" }), + final: true, + }; } - return next(); + return { next, final: false }; }; } @@ -78,7 +83,13 @@ export const useIsPartOfSubjectCondition: CanVisitCondition = (qc, ctx) => { const subjectId = Number(ctx.to.params.subjectId); const { data: subjects, isLoading } = useSubjectsQuery(qc); const condition = computed(() => { - return subjects.value?.findIndex((subject) => subject.id === subjectId) !== -1; + const student_subjects = subjects.value?.as_student || []; + const instructor_subjects = subjects.value?.as_instructor || []; + return ( + [...student_subjects, ...instructor_subjects].findIndex( + (subject) => subject.id === subjectId + ) !== -1 + ); }); return { condition, isLoading }; }; diff --git a/frontend/src/router/middleware/index.ts b/frontend/src/router/middleware/index.ts index ae4e230c..d6eeffda 100644 --- a/frontend/src/router/middleware/index.ts +++ b/frontend/src/router/middleware/index.ts @@ -7,17 +7,9 @@ export interface MiddlewareContext { router: Router; } -export type Middleware = (_: MiddlewareContext) => void; - -export function nextFactory( - context: MiddlewareContext, - middleware: Array, - index: number -): () => void { - const currentMiddleware = middleware[index]; - if (!currentMiddleware) { - return context.next; - } - const nextMiddleware = nextFactory(context, middleware, index + 1); - return () => currentMiddleware({ ...context, next: nextMiddleware }); +export interface MiddlewareResponse { + next: NavigationGuardNext; + final: boolean; } + +export type Middleware = (_: MiddlewareContext) => Promise; diff --git a/frontend/src/router/middleware/isAuthenticated.ts b/frontend/src/router/middleware/isAuthenticated.ts index 0b5449ba..edacd521 100644 --- a/frontend/src/router/middleware/isAuthenticated.ts +++ b/frontend/src/router/middleware/isAuthenticated.ts @@ -1,13 +1,19 @@ import { type Middleware } from "./index"; import { useAuthStore } from "@/stores/auth-store"; -const isAuthenticated: Middleware = ({ to, next, router }) => { +const isAuthenticated: Middleware = async ({ to, next }) => { const requiresAuth = to.meta.requiresAuth !== undefined ? to.meta.requiresAuth : true; const { isLoggedIn } = useAuthStore(); if (requiresAuth && !isLoggedIn) { - router.replace({ name: "login", query: { redirect: to.fullPath } }); + return { + next: () => next({ name: "login", query: { redirect: to.fullPath } }), + final: true, + }; } - return next(); + return { + next, + final: false, + }; }; export default isAuthenticated; diff --git a/frontend/src/router/middleware/logger.ts b/frontend/src/router/middleware/logger.ts new file mode 100644 index 00000000..3ccf16bc --- /dev/null +++ b/frontend/src/router/middleware/logger.ts @@ -0,0 +1,11 @@ +import type { Middleware, MiddlewareContext } from "@/router/middleware"; + +/** + * This middleware logs a message to the console. + * It's main purpose is debugging. + */ +const logger: Middleware = async ({ next }: MiddlewareContext) => { + console.log("Middleware logger"); + return { next, final: false }; +}; +export default logger; diff --git a/frontend/src/router/middleware/login.ts b/frontend/src/router/middleware/login.ts index 0a153f04..5c8f0913 100644 --- a/frontend/src/router/middleware/login.ts +++ b/frontend/src/router/middleware/login.ts @@ -1,20 +1,25 @@ import { type Middleware } from "./index"; import { useAuthStore } from "@/stores/auth-store"; -const login: Middleware = async ({ to, next, router }) => { +const login: Middleware = async ({ to, next }) => { const { login, setRedirect, isLoggedIn } = useAuthStore(); const nextPage = to.query.redirect?.toString() || "/home"; if (isLoggedIn) { - router.replace(nextPage); - return next(); + return { + next: () => next(nextPage), + final: true, + }; } const ticket = to.query.ticket?.toString(); setRedirect(`${nextPage}`); const redirect = await login(nextPage, ticket); if (redirect) { - router.replace(redirect); + return { + next: () => next(redirect), + final: true, + }; } - return next(); + return { next, final: false }; }; export default login; From bcc0dc8e0cc5f001e14833868b64b93881874f8c Mon Sep 17 00:00:00 2001 From: Bram Reyniers Date: Sat, 18 May 2024 11:09:10 +0200 Subject: [PATCH 11/23] subject details permissions --- frontend/src/router/index.ts | 14 +++++++++----- frontend/src/router/middleware/canVisit.ts | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 1049d2da..1e577224 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -5,8 +5,11 @@ import loginMiddleware from "./middleware/login"; import useCanVisit, { useIsAdminCondition, useIsTeacherCondition, + useIsStudentOfSubjectCondition, + useIsInstructorOfSubjectCondition, useIsPartOfSubjectCondition, useAndCondition, + useOrCondition, } from "./middleware/canVisit"; import { ref } from "vue"; @@ -59,10 +62,6 @@ const router = createRouter({ component: () => import("../views/ProjectView.vue"), props: (route) => ({ projectId: Number(route.params.projectId) }), meta: { - middleware: useCanVisit((queryClient) => { - // TODO: implement -> check if user is enlroled in subject - return { condition: ref(true), isLoading: ref(false) }; - }), }, }, { @@ -101,7 +100,12 @@ const router = createRouter({ component: () => import("../views/subject/SubjectView.vue"), props: (route) => ({ subjectId: Number(route.params.subjectId) }), meta: { - middleware: useCanVisit(useIsPartOfSubjectCondition), + middleware: useCanVisit( + useOrCondition( + useIsStudentOfSubjectCondition, + useIsInstructorOfSubjectCondition + ) + ), }, }, { diff --git a/frontend/src/router/middleware/canVisit.ts b/frontend/src/router/middleware/canVisit.ts index afd92fdb..01095087 100644 --- a/frontend/src/router/middleware/canVisit.ts +++ b/frontend/src/router/middleware/canVisit.ts @@ -79,6 +79,26 @@ export const useIsTeacherCondition: CanVisitCondition = (qc) => { return { condition: isTeacher, isLoading }; }; +export const useIsStudentOfSubjectCondition: CanVisitCondition = (qc, ctx) => { + const subjectId = Number(ctx.to.params.subjectId); + const { data: subjects, isLoading } = useSubjectsQuery(qc); + const condition = computed(() => { + return subjects.value?.as_student.findIndex((subject) => subject.id === subjectId) !== -1; + }); + return { condition, isLoading }; +}; + +export const useIsInstructorOfSubjectCondition: CanVisitCondition = (qc, ctx) => { + const subjectId = Number(ctx.to.params.subjectId); + const { data: subjects, isLoading } = useSubjectsQuery(qc); + const condition = computed(() => { + return ( + subjects.value?.as_instructor.findIndex((subject) => subject.id === subjectId) !== -1 + ); + }); + return { condition, isLoading }; +}; + export const useIsPartOfSubjectCondition: CanVisitCondition = (qc, ctx) => { const subjectId = Number(ctx.to.params.subjectId); const { data: subjects, isLoading } = useSubjectsQuery(qc); From 3338bf0c44256c53bb205ca22f84a5503233acfe Mon Sep 17 00:00:00 2001 From: Bram Reyniers Date: Sat, 18 May 2024 11:39:47 +0200 Subject: [PATCH 12/23] project details permissions --- frontend/src/queries/Project.ts | 15 +++++++++------ frontend/src/router/index.ts | 11 ++++++++--- frontend/src/router/middleware/canVisit.ts | 19 +++++++++++++++++++ 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/frontend/src/queries/Project.ts b/frontend/src/queries/Project.ts index 49202bec..5e3952f5 100644 --- a/frontend/src/queries/Project.ts +++ b/frontend/src/queries/Project.ts @@ -1,7 +1,7 @@ import { computed, toValue } from "vue"; import type { MaybeRefOrGetter } from "vue"; import { useMutation, useQuery, useQueryClient } from "@tanstack/vue-query"; -import type { UseMutationReturnType, UseQueryReturnType } from "@tanstack/vue-query"; +import type { QueryClient, UseMutationReturnType, UseQueryReturnType } from "@tanstack/vue-query"; import type Project from "@/models/Project"; import type { ProjectForm } from "@/models/Project"; import { getProject, createProject, getProjects } from "@/services/project"; @@ -30,11 +30,14 @@ export function useProjectQuery( /** * Query composable for fetching all projects of the current user */ -export function useProjectsQuery(): UseQueryReturnType { - return useQuery({ - queryKey: PROJECTS_QUERY_KEY(), - queryFn: getProjects, - }); +export function useProjectsQuery(queryClient?: QueryClient): UseQueryReturnType { + return useQuery( + { + queryKey: PROJECTS_QUERY_KEY(), + queryFn: getProjects, + }, + queryClient + ); } /** diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 1e577224..5a0da125 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -7,6 +7,8 @@ import useCanVisit, { useIsTeacherCondition, useIsStudentOfSubjectCondition, useIsInstructorOfSubjectCondition, + useIsStudentOfProjectCondition, + useIsInstructorOfProjectCondition, useIsPartOfSubjectCondition, useAndCondition, useOrCondition, @@ -62,6 +64,12 @@ const router = createRouter({ component: () => import("../views/ProjectView.vue"), props: (route) => ({ projectId: Number(route.params.projectId) }), meta: { + middleware: useCanVisit( + useOrCondition( + useIsStudentOfProjectCondition, + useIsInstructorOfProjectCondition + ) + ), }, }, { @@ -114,9 +122,6 @@ const router = createRouter({ component: () => import("../views/CreateProjectView.vue"), props: (route) => ({ subjectId: Number(route.params.subjectId) }), meta: { - middleware: useCanVisit( - useAndCondition(useIsPartOfSubjectCondition, useIsTeacherCondition) - ), }, }, { diff --git a/frontend/src/router/middleware/canVisit.ts b/frontend/src/router/middleware/canVisit.ts index 01095087..de8d8d17 100644 --- a/frontend/src/router/middleware/canVisit.ts +++ b/frontend/src/router/middleware/canVisit.ts @@ -4,6 +4,7 @@ import { QueryClient } from "@tanstack/vue-query"; import useIsAdmin from "@/composables/useIsAdmin"; import useIsTeacher from "@/composables/useIsTeacher"; import { useSubjectsQuery } from "@/queries/Subject"; +import { useProjectsQuery } from "@/queries/Project"; export interface CanVisitCondition { ( @@ -99,6 +100,24 @@ export const useIsInstructorOfSubjectCondition: CanVisitCondition = (qc, ctx) => return { condition, isLoading }; }; +export const useIsStudentOfProjectCondition: CanVisitCondition = (qc, ctx) => { + const projectId = Number(ctx.to.params.projectId); + const { data: projects, isLoading } = useProjectsQuery(qc); + const condition = computed(() => { + return projects.value?.findIndex((project) => project.id === projectId) !== -1; + }); + return { condition, isLoading }; +}; + +export const useIsInstructorOfProjectCondition: CanVisitCondition = (qc, ctx) => { + const projectId = Number(ctx.to.params.projectId); + const { data: projects, isLoading } = useProjectsQuery(qc); + const condition = computed(() => { + return projects.value?.findIndex((project) => project.id === projectId) !== -1; + }); + return { condition, isLoading }; +}; + export const useIsPartOfSubjectCondition: CanVisitCondition = (qc, ctx) => { const subjectId = Number(ctx.to.params.subjectId); const { data: subjects, isLoading } = useSubjectsQuery(qc); From 8df7655ba89ced03f48b35c3c3c085f73a5e046b Mon Sep 17 00:00:00 2001 From: Bram Reyniers Date: Sat, 18 May 2024 11:43:57 +0200 Subject: [PATCH 13/23] go to /not-found instead of /forbidden --- frontend/src/router/middleware/canVisit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/router/middleware/canVisit.ts b/frontend/src/router/middleware/canVisit.ts index de8d8d17..a7167b21 100644 --- a/frontend/src/router/middleware/canVisit.ts +++ b/frontend/src/router/middleware/canVisit.ts @@ -32,7 +32,7 @@ function useCanVisit(useCondition: CanVisitCondition): Middleware { await awaitLoading(); if (!condition.value) { return { - next: () => next({ path: "forbidden" }), + next: () => next({ path: "not-found" }), final: true, }; } From 50bf7e631b4847b2847b0d71da540bee99b7b851 Mon Sep 17 00:00:00 2001 From: Bram Reyniers Date: Sat, 18 May 2024 11:51:15 +0200 Subject: [PATCH 14/23] create project permissions --- frontend/src/router/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 5a0da125..a6876935 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -122,6 +122,7 @@ const router = createRouter({ component: () => import("../views/CreateProjectView.vue"), props: (route) => ({ subjectId: Number(route.params.subjectId) }), meta: { + middleware: useCanVisit(useIsInstructorOfSubjectCondition), }, }, { From de7589bc084a7876456dca75b91e2863aa0ecdf1 Mon Sep 17 00:00:00 2001 From: Bram Reyniers Date: Sun, 19 May 2024 15:21:45 +0200 Subject: [PATCH 15/23] fix project permission checks --- frontend/src/queries/Project.ts | 4 +++- frontend/src/router/middleware/canVisit.ts | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/src/queries/Project.ts b/frontend/src/queries/Project.ts index dd6d4de3..419702fc 100644 --- a/frontend/src/queries/Project.ts +++ b/frontend/src/queries/Project.ts @@ -30,7 +30,9 @@ export function useProjectQuery( /** * Query composable for fetching all projects of the current user */ -export function useProjectsQuery(queryClient?: QueryClient): UseQueryReturnType { +export function useProjectsQuery( + queryClient?: QueryClient +): UseQueryReturnType { return useQuery( { queryKey: PROJECTS_QUERY_KEY(), diff --git a/frontend/src/router/middleware/canVisit.ts b/frontend/src/router/middleware/canVisit.ts index a7167b21..14144247 100644 --- a/frontend/src/router/middleware/canVisit.ts +++ b/frontend/src/router/middleware/canVisit.ts @@ -104,7 +104,7 @@ export const useIsStudentOfProjectCondition: CanVisitCondition = (qc, ctx) => { const projectId = Number(ctx.to.params.projectId); const { data: projects, isLoading } = useProjectsQuery(qc); const condition = computed(() => { - return projects.value?.findIndex((project) => project.id === projectId) !== -1; + return projects.value?.as_student.findIndex((project) => project.id === projectId) !== -1; }); return { condition, isLoading }; }; @@ -113,7 +113,9 @@ export const useIsInstructorOfProjectCondition: CanVisitCondition = (qc, ctx) => const projectId = Number(ctx.to.params.projectId); const { data: projects, isLoading } = useProjectsQuery(qc); const condition = computed(() => { - return projects.value?.findIndex((project) => project.id === projectId) !== -1; + return ( + projects.value?.as_instructor.findIndex((project) => project.id === projectId) !== -1 + ); }); return { condition, isLoading }; }; From 9f997b7f590a4dc09105c1111f8ad72912e18e65 Mon Sep 17 00:00:00 2001 From: Bram Reyniers Date: Sun, 19 May 2024 16:03:23 +0200 Subject: [PATCH 16/23] permissions for submit page --- frontend/src/queries/Group.ts | 42 +++++++++++++--------- frontend/src/router/index.ts | 6 ++-- frontend/src/router/middleware/canVisit.ts | 10 ++++++ 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/frontend/src/queries/Group.ts b/frontend/src/queries/Group.ts index 26b13a90..fdd97943 100644 --- a/frontend/src/queries/Group.ts +++ b/frontend/src/queries/Group.ts @@ -1,7 +1,7 @@ import { computed, toValue } from "vue"; import type { MaybeRefOrGetter } from "vue"; import { useMutation, useQuery, useQueryClient } from "@tanstack/vue-query"; -import type { UseMutationReturnType, UseQueryReturnType } from "@tanstack/vue-query"; +import type { QueryClient, UseMutationReturnType, UseQueryReturnType } from "@tanstack/vue-query"; import type Group from "@/models/Group"; import type { GroupForm } from "@/models/Group"; import { @@ -51,13 +51,17 @@ export function useGroupQuery( * Query composable for fetching all groups of a project */ export function useProjectGroupsQuery( - projectId: MaybeRefOrGetter + projectId: MaybeRefOrGetter, + queryClient?: QueryClient ): UseQueryReturnType { - return useQuery({ - queryKey: computed(() => PROJECT_GROUPS_QUERY_KEY(toValue(projectId)!)), - queryFn: () => getProjectGroups(toValue(projectId)!), - enabled: !!toValue(projectId), - }); + return useQuery( + { + queryKey: computed(() => PROJECT_GROUPS_QUERY_KEY(toValue(projectId)!)), + queryFn: () => getProjectGroups(toValue(projectId)!), + enabled: !!toValue(projectId), + }, + queryClient + ); } export function useUserGroupsQuery(): UseQueryReturnType { @@ -73,20 +77,26 @@ export function useUserGroupsQuery(): UseQueryReturnType { * @returns The group the user is in for the project, undefined if the user is not in a group */ export function useProjectGroupQuery( - projectId: MaybeRefOrGetter + projectId: MaybeRefOrGetter, + queryClient?: QueryClient ): UseQueryReturnType { - const { data: projectGroups } = useProjectGroupsQuery(projectId); - const { data: user } = useCurrentUserQuery(); - const userGroup = computed( - () => + const { data: projectGroups } = useProjectGroupsQuery(projectId, queryClient); + const { data: user } = useCurrentUserQuery(queryClient); + const userGroup = computed(() => { + if (projectGroups.value === undefined || user.value === undefined) return undefined; + return ( projectGroups.value?.find((group) => group.members.some((member) => member.uid === user.value?.uid) ) || null - ); - return useQuery({ - queryKey: computed(() => PROJECT_USER_GROUP_QUERY_KEY(toValue(projectId)!)), - queryFn: () => userGroup, + ); }); + return useQuery( + { + queryKey: computed(() => PROJECT_USER_GROUP_QUERY_KEY(toValue(projectId)!)), + queryFn: () => userGroup, + }, + queryClient + ); } /** diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 5673bc30..284a15d8 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -12,6 +12,7 @@ import useCanVisit, { useIsPartOfSubjectCondition, useAndCondition, useOrCondition, + useIsInGroupOfProjectCondition, } from "./middleware/canVisit"; import { ref } from "vue"; @@ -78,10 +79,7 @@ const router = createRouter({ component: () => import("../views/SubmitView.vue"), props: (route) => ({ projectId: Number(route.params.projectId) }), meta: { - middleware: useCanVisit((queryClient) => { - // TODO: implement -> check if user is enlroled in subject and in a group for project - return { condition: ref(true), isLoading: ref(false) }; - }), + middleware: useCanVisit(useIsInGroupOfProjectCondition), }, }, { diff --git a/frontend/src/router/middleware/canVisit.ts b/frontend/src/router/middleware/canVisit.ts index 14144247..7e997cc7 100644 --- a/frontend/src/router/middleware/canVisit.ts +++ b/frontend/src/router/middleware/canVisit.ts @@ -5,6 +5,7 @@ import useIsAdmin from "@/composables/useIsAdmin"; import useIsTeacher from "@/composables/useIsTeacher"; import { useSubjectsQuery } from "@/queries/Subject"; import { useProjectsQuery } from "@/queries/Project"; +import { useProjectGroupQuery } from "@/queries/Group"; export interface CanVisitCondition { ( @@ -120,6 +121,15 @@ export const useIsInstructorOfProjectCondition: CanVisitCondition = (qc, ctx) => return { condition, isLoading }; }; +export const useIsInGroupOfProjectCondition: CanVisitCondition = (qc, ctx) => { + const projectId = Number(ctx.to.params.projectId); + const { data: group, isLoading } = useProjectGroupQuery(projectId, qc); + const condition = computed(() => { + return group.value !== null; + }); + return { condition, isLoading }; +}; + export const useIsPartOfSubjectCondition: CanVisitCondition = (qc, ctx) => { const subjectId = Number(ctx.to.params.subjectId); const { data: subjects, isLoading } = useSubjectsQuery(qc); From ceb7f4cc105e297c0358037be416d9235b96c66a Mon Sep 17 00:00:00 2001 From: Bram Reyniers Date: Mon, 20 May 2024 16:32:13 +0200 Subject: [PATCH 17/23] permissions for groups overview --- frontend/src/router/index.ts | 10 ++++++++-- frontend/src/router/middleware/canVisit.ts | 15 --------------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 284a15d8..32587b76 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -9,12 +9,10 @@ import useCanVisit, { useIsInstructorOfSubjectCondition, useIsStudentOfProjectCondition, useIsInstructorOfProjectCondition, - useIsPartOfSubjectCondition, useAndCondition, useOrCondition, useIsInGroupOfProjectCondition, } from "./middleware/canVisit"; -import { ref } from "vue"; declare module "vue-router" { interface RouteMeta { @@ -87,6 +85,14 @@ const router = createRouter({ name: "groups", component: () => import("../views/GroupsView.vue"), props: (route) => ({ projectId: Number(route.params.projectId) }), + meta: { + middleware: useCanVisit( + useOrCondition( + useIsStudentOfProjectCondition, + useIsInstructorOfProjectCondition + ) + ), + }, }, { path: "/groups/:groupId(\\d+)", diff --git a/frontend/src/router/middleware/canVisit.ts b/frontend/src/router/middleware/canVisit.ts index 7e997cc7..4a7b66b0 100644 --- a/frontend/src/router/middleware/canVisit.ts +++ b/frontend/src/router/middleware/canVisit.ts @@ -129,18 +129,3 @@ export const useIsInGroupOfProjectCondition: CanVisitCondition = (qc, ctx) => { }); return { condition, isLoading }; }; - -export const useIsPartOfSubjectCondition: CanVisitCondition = (qc, ctx) => { - const subjectId = Number(ctx.to.params.subjectId); - const { data: subjects, isLoading } = useSubjectsQuery(qc); - const condition = computed(() => { - const student_subjects = subjects.value?.as_student || []; - const instructor_subjects = subjects.value?.as_instructor || []; - return ( - [...student_subjects, ...instructor_subjects].findIndex( - (subject) => subject.id === subjectId - ) !== -1 - ); - }); - return { condition, isLoading }; -}; From 47a0ee1f0c90ed13e7355f3f4a25e1c528131155 Mon Sep 17 00:00:00 2001 From: Bram Reyniers Date: Mon, 20 May 2024 16:58:35 +0200 Subject: [PATCH 18/23] permissions for group details --- frontend/src/queries/Group.ts | 16 +++++++---- frontend/src/queries/Project.ts | 16 +++++++---- frontend/src/router/index.ts | 7 +++++ frontend/src/router/middleware/canVisit.ts | 32 ++++++++++++++++++++-- 4 files changed, 57 insertions(+), 14 deletions(-) diff --git a/frontend/src/queries/Group.ts b/frontend/src/queries/Group.ts index fdd97943..0cdbf151 100644 --- a/frontend/src/queries/Group.ts +++ b/frontend/src/queries/Group.ts @@ -38,13 +38,17 @@ function PROJECT_USER_GROUP_QUERY_KEY(projectId: number): (string | number)[] { * Query composable for fetching a group by id */ export function useGroupQuery( - groupId: MaybeRefOrGetter + groupId: MaybeRefOrGetter, + queryClient?: QueryClient ): UseQueryReturnType { - return useQuery({ - queryKey: GROUP_QUERY_KEY(toValue(groupId)!), - queryFn: () => getGroup(toValue(groupId)!), - enabled: () => !!toValue(groupId), - }); + return useQuery( + { + queryKey: GROUP_QUERY_KEY(toValue(groupId)!), + queryFn: () => getGroup(toValue(groupId)!), + enabled: () => !!toValue(groupId), + }, + queryClient + ); } /** diff --git a/frontend/src/queries/Project.ts b/frontend/src/queries/Project.ts index 419702fc..b20e30a0 100644 --- a/frontend/src/queries/Project.ts +++ b/frontend/src/queries/Project.ts @@ -18,13 +18,17 @@ function PROJECTS_QUERY_KEY(): string[] { * Query composable for fetching a project by id */ export function useProjectQuery( - projectId: MaybeRefOrGetter + projectId: MaybeRefOrGetter, + queryClient?: QueryClient ): UseQueryReturnType { - return useQuery({ - queryKey: computed(() => PROJECT_QUERY_KEY(toValue(projectId)!)), - queryFn: () => getProject(toValue(projectId)!), - enabled: () => !!toValue(projectId), - }); + return useQuery( + { + queryKey: computed(() => PROJECT_QUERY_KEY(toValue(projectId)!)), + queryFn: () => getProject(toValue(projectId)!), + enabled: () => !!toValue(projectId), + }, + queryClient + ); } /** diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 32587b76..1fad6cf7 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -9,6 +9,8 @@ import useCanVisit, { useIsInstructorOfSubjectCondition, useIsStudentOfProjectCondition, useIsInstructorOfProjectCondition, + useIsInGroupCondition, + useIsInstructorOfGroupCondition, useAndCondition, useOrCondition, useIsInGroupOfProjectCondition, @@ -99,6 +101,11 @@ const router = createRouter({ name: "group", component: () => import("../views/GroupView.vue"), props: (route) => ({ groupId: Number(route.params.groupId) }), + meta: { + middleware: useCanVisit( + useOrCondition(useIsInGroupCondition, useIsInstructorOfGroupCondition) + ), + }, }, { path: "/project/:projectId(\\d+)/submissions", diff --git a/frontend/src/router/middleware/canVisit.ts b/frontend/src/router/middleware/canVisit.ts index 4a7b66b0..da13be87 100644 --- a/frontend/src/router/middleware/canVisit.ts +++ b/frontend/src/router/middleware/canVisit.ts @@ -4,8 +4,9 @@ import { QueryClient } from "@tanstack/vue-query"; import useIsAdmin from "@/composables/useIsAdmin"; import useIsTeacher from "@/composables/useIsTeacher"; import { useSubjectsQuery } from "@/queries/Subject"; -import { useProjectsQuery } from "@/queries/Project"; -import { useProjectGroupQuery } from "@/queries/Group"; +import { useProjectQuery, useProjectsQuery } from "@/queries/Project"; +import { useGroupQuery, useProjectGroupQuery } from "@/queries/Group"; +import { useCurrentUserQuery } from "@/queries/User"; export interface CanVisitCondition { ( @@ -129,3 +130,30 @@ export const useIsInGroupOfProjectCondition: CanVisitCondition = (qc, ctx) => { }); return { condition, isLoading }; }; + +export const useIsInGroupCondition: CanVisitCondition = (qc, ctx) => { + const groupId = Number(ctx.to.params.groupId); + const { data: group, isLoading: isGroupLoading } = useGroupQuery(groupId, qc); + const { data: user, isLoading: isUserLoading } = useCurrentUserQuery(qc); + const condition = computed(() => { + return group.value?.members.findIndex((member) => member.uid === user.value?.uid) !== -1; + }); + return { condition, isLoading: computed(() => isGroupLoading.value || isUserLoading.value) }; +}; + +export const useIsInstructorOfGroupCondition: CanVisitCondition = (qc, ctx) => { + const groupId = Number(ctx.to.params.groupId); + const { data: group, isLoading: isGroupLoading } = useGroupQuery(groupId, qc); + const { data: projects, isLoading: isProjectsLoading } = useProjectsQuery(qc); + const condition = computed(() => { + return ( + projects.value?.as_instructor.findIndex( + (project) => project.id === group.value?.project_id + ) !== -1 + ); + }); + return { + condition, + isLoading: computed(() => isGroupLoading.value || isProjectsLoading.value), + }; +}; From 5e7f10c5518a03d4dff5aebba6d76e9e45bb9653 Mon Sep 17 00:00:00 2001 From: Bram Reyniers Date: Mon, 20 May 2024 17:03:58 +0200 Subject: [PATCH 19/23] permissions for submission list of project --- frontend/src/router/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 1fad6cf7..47b3fedb 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -112,12 +112,14 @@ const router = createRouter({ name: "projectSubmissions", component: () => import("../views/SubmissionsTeacherView.vue"), props: (route) => ({ projectId: Number(route.params.projectId) }), + meta: { + middleware: useCanVisit(useIsInstructorOfProjectCondition), + }, }, { path: "/subjects", name: "subjects", component: () => import("../views/subject/SubjectsView.vue"), - children: [], }, { path: "/subjects/:subjectId(\\d+)", From 1c92def2cbe5212690329e758cbf7d8cb55c6e2a Mon Sep 17 00:00:00 2001 From: Bram Reyniers Date: Mon, 20 May 2024 17:10:00 +0200 Subject: [PATCH 20/23] remove unused imports --- frontend/src/router/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 47b3fedb..e87161c0 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -4,14 +4,12 @@ import isAuthenticated from "./middleware/isAuthenticated"; import loginMiddleware from "./middleware/login"; import useCanVisit, { useIsAdminCondition, - useIsTeacherCondition, useIsStudentOfSubjectCondition, useIsInstructorOfSubjectCondition, useIsStudentOfProjectCondition, useIsInstructorOfProjectCondition, useIsInGroupCondition, useIsInstructorOfGroupCondition, - useAndCondition, useOrCondition, useIsInGroupOfProjectCondition, } from "./middleware/canVisit"; From 7a34f88843453154d0bcd480ad866223b7f5c18b Mon Sep 17 00:00:00 2001 From: Bram Reyniers <55666730+reyniersbram@users.noreply.github.com> Date: Mon, 20 May 2024 23:48:33 +0200 Subject: [PATCH 21/23] Apply suggestions from code review Co-authored-by: Xander Bil <47951455+xerbalind@users.noreply.github.com> --- frontend/src/router/middleware/canVisit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/router/middleware/canVisit.ts b/frontend/src/router/middleware/canVisit.ts index da13be87..8ab3bcc6 100644 --- a/frontend/src/router/middleware/canVisit.ts +++ b/frontend/src/router/middleware/canVisit.ts @@ -4,7 +4,7 @@ import { QueryClient } from "@tanstack/vue-query"; import useIsAdmin from "@/composables/useIsAdmin"; import useIsTeacher from "@/composables/useIsTeacher"; import { useSubjectsQuery } from "@/queries/Subject"; -import { useProjectQuery, useProjectsQuery } from "@/queries/Project"; +import { useProjectsQuery } from "@/queries/Project"; import { useGroupQuery, useProjectGroupQuery } from "@/queries/Group"; import { useCurrentUserQuery } from "@/queries/User"; From 1aa668e6dcf60c67e315186fde90eebab4b8da19 Mon Sep 17 00:00:00 2001 From: Bram Reyniers Date: Wed, 22 May 2024 16:42:39 +0200 Subject: [PATCH 22/23] permissions for new pages --- frontend/src/router/index.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 5cfedfde..5c842af7 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -12,6 +12,7 @@ import useCanVisit, { useIsInstructorOfGroupCondition, useOrCondition, useIsInGroupOfProjectCondition, + useIsTeacherCondition, } from "./middleware/canVisit"; declare module "vue-router" { @@ -123,6 +124,9 @@ const router = createRouter({ path: "/subjects/create", name: "create-subject", component: () => import("../views/subject/CreateSubjectView.vue"), + meta: { + middleware: useCanVisit(useOrCondition(useIsAdminCondition, useIsTeacherCondition)), + }, }, { path: "/subjects/:subjectId(\\d+)", @@ -152,6 +156,9 @@ const router = createRouter({ name: "edit-project", component: () => import("../views/CreateProjectView.vue"), // Ensure this is correct props: (route) => ({ projectId: Number(route.params.projectId), isEditMode: true }), + meta: { + middleware: useCanVisit(useIsInstructorOfProjectCondition), + }, }, { path: "/subjects/register/:uuid", From ade32df03607ac73c8e8cabfa62cc95d5b2107ce Mon Sep 17 00:00:00 2001 From: Bram Reyniers Date: Wed, 22 May 2024 16:50:46 +0200 Subject: [PATCH 23/23] allow admins to visit all pages --- frontend/src/router/middleware/canVisit.ts | 42 ++++++++++++---------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/frontend/src/router/middleware/canVisit.ts b/frontend/src/router/middleware/canVisit.ts index 8ab3bcc6..0081e1c8 100644 --- a/frontend/src/router/middleware/canVisit.ts +++ b/frontend/src/router/middleware/canVisit.ts @@ -1,4 +1,4 @@ -import { computed, inject, type Ref } from "vue"; +import { computed, type Ref } from "vue"; import type { Middleware, MiddlewareContext } from "./index"; import { QueryClient } from "@tanstack/vue-query"; import useIsAdmin from "@/composables/useIsAdmin"; @@ -15,30 +15,34 @@ export interface CanVisitCondition { ): { condition: Ref; isLoading: Ref }; } +function useAwaitLoading(isLoading: Ref): Promise { + return new Promise((resolve) => { + const interval = setInterval(() => { + if (!isLoading.value) { + clearInterval(interval); + resolve(); + } + }, 10); + }); +} + function useCanVisit(useCondition: CanVisitCondition): Middleware { return async (context) => { const { next } = context; - // TODO: Figure out why this doesn't work anymore - // const queryClient = inject("queryClient", new QueryClient()); const queryClient = new QueryClient(); + const { condition: isAdmin, isLoading: isAdminLoading } = useIsAdminCondition( + queryClient, + context + ); const { condition, isLoading } = useCondition(queryClient, context); - const awaitLoading = () => - new Promise((resolve) => { - const interval = setInterval(() => { - if (!isLoading.value) { - clearInterval(interval); - resolve(); - } - }, 10); - }); - await awaitLoading(); - if (!condition.value) { - return { - next: () => next({ path: "not-found" }), - final: true, - }; + await useAwaitLoading(computed(() => isAdminLoading.value || isLoading.value)); + if (isAdmin.value || condition.value) { + return { next, final: false }; } - return { next, final: false }; + return { + next: () => next({ path: "not-found" }), + final: true, + }; }; }