diff --git a/src/app/api/middleware.ts b/src/app/api/middleware.ts deleted file mode 100644 index 4606dfa..0000000 --- a/src/app/api/middleware.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; -import { jwtVerify, createRemoteJWKSet } from "jose"; - -const hankoApiUrl = process.env.NEXT_PUBLIC_HANKO_API_URL!; - -// This function can be marked `async` if using `await` inside -export async function middleware(req: NextRequest) { - const hanko = req.cookies.get("hanko")?.value; - - const JWKS = createRemoteJWKSet( - new URL(`${hankoApiUrl}/.well-known/jwks.json`) - ); - try { - const verifiedJWT = await jwtVerify(hanko ?? "", JWKS); - } catch { - return NextResponse.json( - { - message: "Unauthenticated", - detail: "Please log in to access this resource.", - }, - { status: 401 } - ); - } -} -export const config = { - matcher: ["/api/secure/:path*"], -}; diff --git a/src/app/api/secure/batches/all/route.ts b/src/app/api/secure/batches/all/route.ts index eca5d7f..367d368 100644 --- a/src/app/api/secure/batches/all/route.ts +++ b/src/app/api/secure/batches/all/route.ts @@ -4,7 +4,7 @@ import { Branch, Student } from "@prisma/client"; import { getUserId } from "@/components/auth/server"; export async function GET(request: NextRequest) { - const userId = await getUserId(); + const userId = await getUserId(request); const results = await prisma.batch.findMany({ where: { diff --git a/src/app/api/secure/batches/new/route.ts b/src/app/api/secure/batches/new/route.ts index d1f1e06..113e9be 100644 --- a/src/app/api/secure/batches/new/route.ts +++ b/src/app/api/secure/batches/new/route.ts @@ -16,7 +16,7 @@ const schema = z.object({ }); export async function GET(request: NextRequest) { - const userId = await getUserId(); + const userId = await getUserId(request); const body = schema.safeParse(await request.json()); if (!body.success) { const { errors } = body.error; diff --git a/src/app/api/secure/branches/all/route.ts b/src/app/api/secure/branches/all/route.ts index e1654e7..0fb8f9e 100644 --- a/src/app/api/secure/branches/all/route.ts +++ b/src/app/api/secure/branches/all/route.ts @@ -3,7 +3,7 @@ import { prisma } from "@/server/db/prisma"; import { getUserId } from "@/components/auth/server"; export async function GET(request: NextRequest) { - const userId = await getUserId(); + const userId = await getUserId(request); const results = await prisma.branch.findMany({ select: { diff --git a/src/app/api/secure/profile/route.ts b/src/app/api/secure/profile/route.ts index d4964ba..ad8dcee 100644 --- a/src/app/api/secure/profile/route.ts +++ b/src/app/api/secure/profile/route.ts @@ -28,7 +28,7 @@ export async function PUT(request: NextRequest) { { status: 400 } ); } - const userId = await getUserId(); + const userId = await getUserId(request); const res = await prisma.user.upsert({ where: { @@ -57,7 +57,7 @@ export async function PUT(request: NextRequest) { } export async function GET(request: NextRequest) { - const userId = await getUserId(); + const userId = await getUserId(request); const res = await prisma.user.findUnique({ where: { diff --git a/src/app/api/secure/sbte-result/history/route.ts b/src/app/api/secure/sbte-result/history/route.ts index 38f8c78..adc3607 100644 --- a/src/app/api/secure/sbte-result/history/route.ts +++ b/src/app/api/secure/sbte-result/history/route.ts @@ -4,7 +4,7 @@ import { z } from "zod"; import { getUserId } from "@/components/auth/server"; export async function GET(request: NextRequest) { - const userId = await getUserId(); + const userId = await getUserId(request); const results = await prisma.examResultFormatHistory.findMany({ where: { @@ -50,7 +50,7 @@ export async function DELETE(request: NextRequest) { { status: 400 } ); } - const userId = await getUserId(); + const userId = await getUserId(request); await prisma.examResultFormatHistory.delete({ where: { diff --git a/src/app/api/secure/sbte-result/route.ts b/src/app/api/secure/sbte-result/route.ts index 3ca2323..299dcdd 100644 --- a/src/app/api/secure/sbte-result/route.ts +++ b/src/app/api/secure/sbte-result/route.ts @@ -67,7 +67,7 @@ export async function POST(request: NextRequest) { { status: 400 } ); } - const userId = await getUserId(); + const userId = await getUserId(request); const myProfile = await prisma.user.findUnique({ where: { diff --git a/src/app/api/secure/sbte-result/single/[resultId]/route.ts b/src/app/api/secure/sbte-result/single/[resultId]/route.ts index b11f0db..1742b4c 100644 --- a/src/app/api/secure/sbte-result/single/[resultId]/route.ts +++ b/src/app/api/secure/sbte-result/single/[resultId]/route.ts @@ -7,7 +7,7 @@ export async function GET( request: NextRequest, { params }: { params: { resultId: string } } ) { - const userId = await getUserId(); + const userId = await getUserId(request); const results = await prisma.examResultFormatHistory.findUnique({ where: { diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 79bf93e..e5ca25d 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -1,11 +1,35 @@ "use client"; import Navigation from "@/components/Navigation"; +import { useProfile } from "@/lib/swr"; +import { useEffect } from "react"; +import { usePathname, useRouter } from "next/navigation"; +import { useToast } from "@/components/ui/use-toast"; export default function RootLayout({ children, }: { children: React.ReactNode; }) { + const { data } = useProfile(); + const pathName = usePathname(); + const router = useRouter(); + const { toast } = useToast(); + + useEffect(() => { + // check data.college, data.department, data.email, data.phonenumber exists + // if not, redirect to /profile + + // if current page is /profile, do nothing + if (pathName === "/dashboard/profile") return; + + if (data) { + if (!data.collegeId || !data.designation || !data.phone || !data.name) { + toast({ title: "Please complete your profile first" }); + router.replace("/dashboard/profile"); + } + } + }, [data, pathName, router, toast]); + return ( <> diff --git a/src/app/middleware.ts b/src/app/middleware.ts deleted file mode 100644 index 23656e2..0000000 --- a/src/app/middleware.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { NextResponse, NextRequest } from "next/server"; - -import { jwtVerify, createRemoteJWKSet } from "jose"; -import { hankoApiUrl } from "@/components/auth/vars"; - -export async function middleware(req: NextRequest) { - const hanko = req.cookies.get("hanko")?.value; - - const JWKS = createRemoteJWKSet( - new URL(`${hankoApiUrl}/.well-known/jwks.json`) - ); - - try { - const verifiedJWT = await jwtVerify(hanko ?? "", JWKS); - } catch { - return NextResponse.redirect(new URL("/login", req.url)); - } -} - -export const config = { - matcher: ["/dashboard/:path*"], -}; diff --git a/src/components/auth/LogOut.tsx b/src/components/auth/LogOut.tsx index 50ac769..d5432f3 100644 --- a/src/components/auth/LogOut.tsx +++ b/src/components/auth/LogOut.tsx @@ -42,7 +42,7 @@ export function LogoutBtn() { const logout = async () => { try { await hanko?.user.logout(); - router.push("/login"); + router.push("/auth"); router.refresh(); return; } catch (error) { diff --git a/src/components/auth/Profile.tsx b/src/components/auth/Profile.tsx index 05f6003..690c004 100644 --- a/src/components/auth/Profile.tsx +++ b/src/components/auth/Profile.tsx @@ -2,6 +2,7 @@ import { useEffect } from "react"; import { register } from "@teamhanko/hanko-elements"; +import { toast } from "../ui/use-toast"; const hankoApi = process.env.NEXT_PUBLIC_HANKO_API_URL!; @@ -9,6 +10,7 @@ export default function HankoProfile() { useEffect(() => { register(hankoApi).catch((error) => { // handle error + toast(error.toString()); }); }, []); diff --git a/src/components/auth/server.ts b/src/components/auth/server.ts index 710b8f2..3d4cc1b 100644 --- a/src/components/auth/server.ts +++ b/src/components/auth/server.ts @@ -1,12 +1,30 @@ -"use server"; import axios from "axios"; import { hankoApiUrl } from "./vars"; +import { createRemoteJWKSet, jwtVerify } from "jose"; +import { NextRequest } from "next/server"; +import { z } from "zod"; -export const getUserId = async () => { - // get user details from Hanko - const { - data: { id }, - } = await axios.get<{ id: string }>(`${hankoApiUrl}/me`); +export const fetchUserId = async (hanko: string) => { + const JWKS = createRemoteJWKSet( + new URL(`${hankoApiUrl}/.well-known/jwks.json`) + ); - return id; + const verifiedJWT = await jwtVerify(hanko ?? "", JWKS); + return verifiedJWT.payload.sub; +}; + +export const getUserId = async (req: NextRequest) => { + // const userId = req.headers.get("x-user-id"); + const hanko = req.cookies.get("hanko")?.value; + if (!hanko) { + throw new Error("No Hanko cookie found"); + } + const userId = await fetchUserId(hanko); + // validate userId is uuid using zod + const schema = z.string().uuid(); + const validatedUserId = schema.safeParse(userId); + if (!validatedUserId.success) { + throw new Error("Invalid user id"); + } + return validatedUserId.data; }; diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..1b0be03 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,31 @@ +import { NextResponse, NextRequest } from "next/server"; + +import { fetchUserId, getUserId } from "@/components/auth/server"; + +export async function middleware(req: NextRequest) { + try { + const hanko = req.cookies.get("hanko")?.value; + if (!hanko) { + throw new Error("No Hanko cookie found"); + } + const userId = await fetchUserId(hanko); + + userId && req.headers.set("x-user-id", userId); + return NextResponse.next(); + } catch { + if (req.url.startsWith("/api/secure")) { + return NextResponse.json( + { + message: "Unauthenticated", + detail: "Please log in to access this resource.", + }, + { status: 401 } + ); + } + return NextResponse.redirect(new URL("/login", req.url)); + } +} + +export const config = { + matcher: ["/dashboard/:path*", "/api/secure/:path*"], +}; diff --git a/src/server/auth/server.ts b/src/server/auth/server.ts deleted file mode 100644 index dc92253..0000000 --- a/src/server/auth/server.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { PrismaAdapter } from "@next-auth/prisma-adapter"; -import { - getServerSession, - type DefaultSession, - type NextAuthOptions, -} from "next-auth"; -import GitHubProvider from "next-auth/providers/github"; -import GoogleProvider from "next-auth/providers/google"; - -import type { Role, RoleTypes } from "@prisma/client"; -import { prisma } from "../db/prisma"; - -export type { Session, DefaultSession as DefaultAuthSession } from "next-auth"; - -/** - * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session` - * object and keep type safety. - * - * @see https://next-auth.js.org/getting-started/typescript#module-augmentation - */ -declare module "next-auth" { - interface Session extends DefaultSession { - user: DefaultSession["user"] & { - id: string; - role: RoleTypes[]; - }; - } - - interface User { - createdAt: Date; - roles: Role[]; - } -} - -if (!process.env.GITHUB_ID) { - throw new Error("No GITHUB_ID has been provided."); -} - -if (!process.env.GITHUB_SECRET) { - throw new Error("No GITHUB_SECRET has been provided."); -} - -const useSecureCookies = Boolean(process.env.VERCEL_URL); - -export const authOptions: NextAuthOptions = { - secret: process.env.SECRET!, - cookies: { - sessionToken: { - name: `${useSecureCookies ? "__Secure-" : ""}next-auth.session-token`, - options: { - httpOnly: true, - sameSite: "lax", - path: "/", - domain: useSecureCookies - ? "sbte-tools.vercel.app" - : process.env.VERCEL_URL, - secure: useSecureCookies, - }, - }, - }, - callbacks: { - redirect: ({ url, baseUrl }) => { - if (url.startsWith("/")) return `${baseUrl}${url}`; - else if (new URL(url).origin === baseUrl) return url; - return baseUrl; - }, - session: async ({ session, user }) => { - // 1. State - let userRoles: RoleTypes[] = []; - - // 2. If user already has roles, reduce them to a RoleTypes array. - if (user.roles) { - userRoles = user.roles.reduce((acc: RoleTypes[], role) => { - acc.push(role.role); - return acc; - }, []); - } - - // 3. If the current user doesn't have a USER role. Assign one. - if (!userRoles.includes("USER")) { - const updatedUser = await prisma.user.update({ - where: { - id: user.id, - }, - data: { - roles: { - connectOrCreate: { - where: { - role: "USER", - }, - create: { - role: "USER", - }, - }, - }, - }, - include: { - roles: true, - }, - }); - - userRoles = updatedUser.roles.reduce((acc: RoleTypes[], role) => { - acc.push(role.role); - return acc; - }, []); - } - - return { - ...session, - user: { - ...session.user, - id: user.id, - role: userRoles, - createAt: user.createdAt, - }, - }; - }, - }, - adapter: PrismaAdapter(prisma), - providers: [ - GitHubProvider({ - clientId: process.env.GITHUB_ID!, - clientSecret: process.env.GITHUB_SECRET!, - }), - GoogleProvider({ - clientId: process.env.GOOGLE_CLIENT_ID!, - clientSecret: process.env.GOOGLE_CLIENT_SECRET!, - }), - ], -}; - -/** - * Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file. - * - * @see https://next-auth.js.org/configuration/nextjs - */ -export const getServerAuthSession = () => { - return getServerSession(authOptions); -};