diff --git a/package.json b/package.json index edce5e7..686b369 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@types/bcrypt": "^5.0.2", "@types/jsonwebtoken": "^9.0.5", "bcrypt": "^5.1.1", + "client-only": "^0.0.1", "clsx": "^2.1.0", "jsonwebtoken": "^9.0.2", "next": "14.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 287c6b8..36e7ada 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ dependencies: bcrypt: specifier: ^5.1.1 version: 5.1.1 + client-only: + specifier: ^0.0.1 + version: 0.0.1 clsx: specifier: ^2.1.0 version: 2.1.0 diff --git a/src/app/(auth)/login/layout.tsx b/src/app/(auth)/login/layout.tsx new file mode 100644 index 0000000..1c704d6 --- /dev/null +++ b/src/app/(auth)/login/layout.tsx @@ -0,0 +1,14 @@ +import React from "react"; + +import guestOnly from "@/features/auth/actions/guestOnly"; + +interface Props { + children: React.ReactNode; +} + +export default async function LoginLayout({ children }: Props) { + + await guestOnly() + + return <>{children}; +} diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index de8cd32..197026f 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -1,5 +1,6 @@ "use client"; +import getUser from "@/features/auth/actions/getUser"; import signIn from "@/features/auth/actions/signIn"; import { Paper, @@ -12,7 +13,9 @@ import { Button, Alert, } from "@mantine/core"; -import React from "react"; +import { redirect } from "next/navigation"; +import { useRouter } from "next/navigation"; +import React, { useEffect, useState } from "react"; import { useFormState } from "react-dom"; const initialState = { diff --git a/src/app/(auth)/logout/page.tsx b/src/app/(auth)/logout/page.tsx new file mode 100644 index 0000000..a918218 --- /dev/null +++ b/src/app/(auth)/logout/page.tsx @@ -0,0 +1,21 @@ +"use client" +import getUser from "@//features/auth/actions/getUser"; +import logout from "@/features/auth/actions/logout"; +import { redirect } from "next/navigation"; +import React, { useEffect } from "react"; + +/** + * LogoutPage component handles the logout process. + * It checks if a user is logged in, logs them out, and redirects to the login page. + */ +export default function LogoutPage() { + + useEffect(() => { + + const logoutAction = async () => await logout() + + logoutAction() + }, []) + + return
; +} diff --git a/src/app/(auth)/register/layout.tsx b/src/app/(auth)/register/layout.tsx new file mode 100644 index 0000000..5a0925c --- /dev/null +++ b/src/app/(auth)/register/layout.tsx @@ -0,0 +1,14 @@ +import React from "react"; + +import guestOnly from "@/features/auth/actions/guestOnly"; + +interface Props { + children: React.ReactNode; +} + +export default async function RegisterLayout({ children }: Props) { + + await guestOnly() + + return <>{children}; +} diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 265230f..09738ec 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -13,9 +13,9 @@ interface Props { children: React.ReactNode } -export default function Layout(props: Props) { +export default async function Layout(props: Props) { - const user = getUser() + const user = await getUser() if (!user){ redirect("/login") diff --git a/src/components/AppHeader/Header.tsx b/src/components/AppHeader/Header.tsx index be67749..8027a62 100644 --- a/src/components/AppHeader/Header.tsx +++ b/src/components/AppHeader/Header.tsx @@ -15,7 +15,7 @@ import logo from "@/assets/logos/logo-dsg.png"; import cx from "clsx"; import classNames from "./styles.module.css"; import { TbChevronDown, TbLogout, TbSettings } from "react-icons/tb"; -import userMenuItems from "./_data/UserMenuItems"; +import userMenuItems from "./_data/userMenuItems"; import UserMenuItem from "./_components/UserMenuItem/UserMenuItem"; interface Props { diff --git a/src/components/AppHeader/_components/UserMenuItem/UserMenuItem.tsx b/src/components/AppHeader/_components/UserMenuItem/UserMenuItem.tsx index b945ba6..c97ac9a 100644 --- a/src/components/AppHeader/_components/UserMenuItem/UserMenuItem.tsx +++ b/src/components/AppHeader/_components/UserMenuItem/UserMenuItem.tsx @@ -1,6 +1,6 @@ import { Menu, rem } from '@mantine/core' import React from 'react' -import { UserMenuItem } from '../../_data/UserMenuItems' +import { UserMenuItem } from '../../_data/userMenuItems' interface Props { item: UserMenuItem @@ -10,12 +10,14 @@ export default function UserMenuItem({item}: Props) { return ( } + href={item.href} > {item.label} diff --git a/src/components/AppHeader/_data/UserMenuItems.ts b/src/components/AppHeader/_data/userMenuItems.ts similarity index 80% rename from src/components/AppHeader/_data/UserMenuItems.ts rename to src/components/AppHeader/_data/userMenuItems.ts index 442d686..f8bae79 100644 --- a/src/components/AppHeader/_data/UserMenuItems.ts +++ b/src/components/AppHeader/_data/userMenuItems.ts @@ -5,7 +5,8 @@ import { TbLogout, TbSettings } from "react-icons/tb" export interface UserMenuItem { label: string, icon: React.FC, - color?: ThemeIconProps['color'] + color?: ThemeIconProps['color'], + href?: string, } const userMenuItems: UserMenuItem[] = [ @@ -16,7 +17,8 @@ const userMenuItems: UserMenuItem[] = [ { label: "Logout", icon: TbLogout, - color: "red" + color: "red", + href: "/logout" } ]; diff --git a/src/features/auth/actions/getUser.ts b/src/features/auth/actions/getUser.ts index 6662d20..25e44c6 100644 --- a/src/features/auth/actions/getUser.ts +++ b/src/features/auth/actions/getUser.ts @@ -4,19 +4,29 @@ import { cookies } from "next/headers" import "server-only" import { decodeJwtToken } from "../authUtils"; import prisma from "@/db"; +import AuthError, { AuthErrorCode } from "../AuthError"; +import logout from "./logout"; export default async function getUser(){ - const token = cookies().get('token'); + try { + const token = cookies().get('token'); - if (!token) return null; + if (!token) return null; - const decodedToken = decodeJwtToken(token.value) as {id: string, iat: number}; + const decodedToken = decodeJwtToken(token.value) as {id: string, iat: number}; + console.log('token', decodedToken) - const user = await prisma.user.findFirst({ - where: { - id: decodedToken.id + const user = await prisma.user.findFirst({ + where: { + id: decodedToken.id + } + }); + + return user; + } catch (e: unknown){ + if (e instanceof AuthError && e.errorCode === AuthErrorCode.INVALID_JWT_TOKEN){ + return null; } - }); - - return user; + throw e; + } } diff --git a/src/features/auth/actions/guestOnly.ts b/src/features/auth/actions/guestOnly.ts new file mode 100644 index 0000000..89ab24f --- /dev/null +++ b/src/features/auth/actions/guestOnly.ts @@ -0,0 +1,13 @@ +"use server" + +import { redirect } from "next/navigation"; +import getUser from "./getUser" + +export default async function guestOnly(){ + + const user = await getUser(); + + if (user){ + redirect("dashboard") + } +} diff --git a/src/features/auth/actions/logout.ts b/src/features/auth/actions/logout.ts new file mode 100644 index 0000000..f43a041 --- /dev/null +++ b/src/features/auth/actions/logout.ts @@ -0,0 +1,16 @@ +"use server" + +import { cookies } from "next/headers" +import { redirect } from "next/navigation"; +import "server-only" + +/** + * Handles user logout by deleting the authentication token and redirecting to the login page. + * This function is intended to be used on the server side. + * + * @returns A promise that resolves when the logout process is complete. + */ +export default async function logout(){ + cookies().delete("token"); + redirect("/login") +} \ No newline at end of file