diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 0000000000..2edb305a48 --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,24 @@ +{ + "features": { + "ghcr.io/devcontainers-extra/features/pnpm:2": { + "version": "2.0.5", + "resolved": "ghcr.io/devcontainers-extra/features/pnpm@sha256:694c2b6182435c9e9c06f6071728b087c1181b38c18cecf0defe2ab8c11cddd6", + "integrity": "sha256:694c2b6182435c9e9c06f6071728b087c1181b38c18cecf0defe2ab8c11cddd6" + }, + "ghcr.io/devcontainers/features/go:1": { + "version": "1.3.4", + "resolved": "ghcr.io/devcontainers/features/go@sha256:d85e921f91b41340055bb12b325d9d551170ed04b3b832e33530bf42f167c032", + "integrity": "sha256:d85e921f91b41340055bb12b325d9d551170ed04b3b832e33530bf42f167c032" + }, + "ghcr.io/devcontainers/features/java:1": { + "version": "1.8.0", + "resolved": "ghcr.io/devcontainers/features/java@sha256:9663ce0219ff85786e87901ce5f0a59f488edd5f99b46015192cda48468b233a", + "integrity": "sha256:9663ce0219ff85786e87901ce5f0a59f488edd5f99b46015192cda48468b233a" + }, + "ghcr.io/devcontainers/features/terraform:1": { + "version": "1.4.3", + "resolved": "ghcr.io/devcontainers/features/terraform@sha256:503d0888b3a43a32335e08cd4477f8c4629404ad83f6a36f0e0fee7f567c06c7", + "integrity": "sha256:503d0888b3a43a32335e08cd4477f8c4629404ad83f6a36f0e0fee7f567c06c7" + } + } +} diff --git a/apps/backend/libs/auth/src/roles/group-leader.guard.ts b/apps/backend/libs/auth/src/roles/group-leader.guard.ts index 722cdefe84..7bc25deece 100644 --- a/apps/backend/libs/auth/src/roles/group-leader.guard.ts +++ b/apps/backend/libs/auth/src/roles/group-leader.guard.ts @@ -1,4 +1,5 @@ import { + ForbiddenException, Injectable, type CanActivate, type ExecutionContext @@ -54,6 +55,8 @@ export class GroupLeaderGuard implements CanActivate { if (isGroupLeader) { return true } - return false + throw new ForbiddenException( + 'You do not have permission to manage this course' + ) } } diff --git a/apps/frontend/app/admin/course/[courseId]/_components/CourseDetailTabs.tsx b/apps/frontend/app/admin/course/[courseId]/_components/CourseDetailTabs.tsx new file mode 100644 index 0000000000..6555923a65 --- /dev/null +++ b/apps/frontend/app/admin/course/[courseId]/_components/CourseDetailTabs.tsx @@ -0,0 +1,85 @@ +'use client' + +import { cn } from '@/libs/utils' +import Link from 'next/link' +import { usePathname } from 'next/navigation' + +interface CourseDetailTabsProps { + courseId: string + courseCode: string + courseTitle: string +} + +export function CourseDetailTabs({ + courseId, + courseCode, + courseTitle +}: CourseDetailTabsProps) { + const pathname = usePathname() + + const tabs = [ + { name: 'Home', title: 'HOME', href: `/admin/course/${courseId}` }, + { + name: 'Member', + title: 'MEMBER', + description: + "Here's a list of the instructors and students of the course", + href: `/admin/course/${courseId}/user` + }, + { + name: 'Assignment', + title: 'ASSIGNMENT', + description: "Here's a assignment list you made", + href: `/admin/course/${courseId}/assignment` + }, + { + name: 'Exercise', + title: 'EXERCISE', + description: "Here's a exercise list you made", + href: `/admin/course/${courseId}/exercise` + }, + { + name: 'Q&A', + title: 'Question & Answer', + description: + 'Assignment와 Exercise 문제와 관련된 질문과 답변을 제공합니다.', + href: `/admin/course/${courseId}/qna` + } + ] + + const activeTabName = + tabs.find((tab) => pathname === tab.href)?.title || 'HOME' + + return ( + <> +

{activeTabName}

+

+ {activeTabName === 'HOME' + ? `[${courseCode}] ${courseTitle}` + : tabs.find((tab) => tab.title === activeTabName)?.description} +

+ +
+ +
+ + ) +} diff --git a/apps/frontend/app/admin/course/[courseId]/layout.tsx b/apps/frontend/app/admin/course/[courseId]/layout.tsx index 0b58560cb7..6dd85e569a 100644 --- a/apps/frontend/app/admin/course/[courseId]/layout.tsx +++ b/apps/frontend/app/admin/course/[courseId]/layout.tsx @@ -1,102 +1,73 @@ -'use client' +import { auth } from '@/libs/auth' +import { adminBaseUrl } from '@/libs/constants' +import { redirect } from 'next/navigation' +import { CourseDetailTabs } from './_components/CourseDetailTabs' -import { GET_COURSE } from '@/graphql/course/queries' -import { cn } from '@/libs/utils' -import { useQuery } from '@apollo/client' -import Link from 'next/link' -import { useParams, usePathname } from 'next/navigation' -import { title } from 'process' -import { description } from 'valibot' +const GET_COURSE_QUERY = ` + query GetCourse($groupId: Int!) { + getCourse(groupId: $groupId) { + id + groupName + courseInfo { + courseNum + classNum + } + } + } +` -export default function CourseDetailLayout({ - children +export default async function CourseDetailLayout({ + children, + params }: { children: React.ReactNode + params: Promise<{ courseId: string }> }) { - const pathname = usePathname() - const params = useParams() - const courseId = params?.courseId as string + const { courseId } = await params + const session = await auth() - const { data, loading } = useQuery(GET_COURSE, { - variables: { groupId: Number(courseId) }, - skip: !courseId - }) + if (!session) { + redirect('/') + } - const currentCourse = data?.getCourse + let json + try { + const res = await fetch(adminBaseUrl as string, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: session.token.accessToken + }, + body: JSON.stringify({ + query: GET_COURSE_QUERY, + variables: { groupId: Number(courseId) } + }) + }) + json = await res.json() + } catch { + redirect('/') + } - const courseNum = currentCourse?.courseInfo?.courseNum - const classNum = currentCourse?.courseInfo?.classNum + // If the user doesn't have permission, the backend's GroupLeaderGuard + // will throw a ForbiddenException and return errors here. + if (json.errors || !json.data?.getCourse) { + redirect('/') + } + const course = json.data.getCourse + const courseNum = course.courseInfo?.courseNum + const classNum = course.courseInfo?.classNum const courseCode = courseNum ? `${courseNum}-${classNum}` : courseId - const courseTitle = - currentCourse?.groupName || (loading ? '로딩 중...' : '과목 정보 없음') - - const tabs = [ - { name: 'Home', title: 'HOME', href: `/admin/course/${courseId}` }, - { - name: 'Member', - title: 'MEMBER', - description: - "Here's a list of the instructors and students of the course", - href: `/admin/course/${courseId}/user` - }, - { - name: 'Assignment', - title: 'ASSIGNMENT', - description: "Here's a assignment list you made", - href: `/admin/course/${courseId}/assignment` - }, - { - name: 'Exercise', - title: 'EXERCISE', - description: "Here's a exercise list you made", - href: `/admin/course/${courseId}/exercise` - }, - { - name: 'Q&A', - title: 'Question & Answer', - description: - 'Assignment와 Exercise 문제와 관련된 질문과 답변을 제공합니다.', - href: `/admin/course/${courseId}/qna` - } - ] - - const activeTabName = - tabs.find((tab) => pathname === tab.href)?.title || 'HOME' + const courseTitle = course.groupName || '' return (
-

{activeTabName}

-

- {activeTabName === 'HOME' - ? `[${courseCode}] ${courseTitle}` - : tabs.find((tab) => tab.title === activeTabName)?.description} -

- -
-
- -
-
+
{children}