diff --git a/prisma/migrations/20240122063550_add_user_photo_profile/migration.sql b/prisma/migrations/20240122063550_add_user_photo_profile/migration.sql new file mode 100644 index 0000000..7c605ea --- /dev/null +++ b/prisma/migrations/20240122063550_add_user_photo_profile/migration.sql @@ -0,0 +1,11 @@ +-- CreateTable +CREATE TABLE `UserPhotoProfiles` ( + `id` VARCHAR(191) NOT NULL, + `userId` VARCHAR(191) NOT NULL, + + UNIQUE INDEX `UserPhotoProfiles_userId_key`(`userId`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `UserPhotoProfiles` ADD CONSTRAINT `UserPhotoProfiles_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20240122063854_move_user_profile_image_from_user_to_user_photo_table/migration.sql b/prisma/migrations/20240122063854_move_user_profile_image_from_user_to_user_photo_table/migration.sql new file mode 100644 index 0000000..6524c4f --- /dev/null +++ b/prisma/migrations/20240122063854_move_user_profile_image_from_user_to_user_photo_table/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - You are about to drop the column `image` on the `User` table. All the data in the column will be lost. + - Added the required column `path` to the `UserPhotoProfiles` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE `User` DROP COLUMN `image`; + +-- AlterTable +ALTER TABLE `UserPhotoProfiles` ADD COLUMN `path` VARCHAR(191) NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9b4ba05..ffc26e3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -42,10 +42,17 @@ model User { name String? email String? @unique emailVerified DateTime? - image String? passwordHash String? accounts Account[] sessions Session[] + photoProfile UserPhotoProfiles? +} + +model UserPhotoProfiles { + id String @id @default(cuid()) + userId String @unique + path String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) } model VerificationToken { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b16587e..e84ee87 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,33 +1,34 @@ -import type { Metadata } from 'next' -import { Inter } from 'next/font/google' +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; -import "./globals.css" -import '@mantine/core/styles.css'; +import "./globals.css"; +import "@mantine/core/styles.css"; -import { ColorSchemeScript, MantineProvider } from '@mantine/core'; +import { ColorSchemeScript, MantineProvider } from "@mantine/core"; +import { AuthContextProvider } from "@/features/auth/contexts/AuthContext"; -const inter = Inter({ subsets: ['latin'] }) +const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { - title: 'Create Next App', - description: 'Generated by create next app', -} + title: "Create Next App", + description: "Generated by create next app", +}; export default function RootLayout({ - children, + children, }: { - children: React.ReactNode + children: React.ReactNode; }) { - return ( - - - - - - - {children} - - - - ) + return ( + + + + + + + {children} + + + + ); } diff --git a/src/components/AppHeader/Header.tsx b/src/components/AppHeader/Header.tsx index 8027a62..41b8d35 100644 --- a/src/components/AppHeader/Header.tsx +++ b/src/components/AppHeader/Header.tsx @@ -17,21 +17,24 @@ import classNames from "./styles.module.css"; import { TbChevronDown, TbLogout, TbSettings } from "react-icons/tb"; import userMenuItems from "./_data/userMenuItems"; import UserMenuItem from "./_components/UserMenuItem/UserMenuItem"; +import { useAuth } from "@/features/auth/contexts/AuthContext"; interface Props { openNavbar: boolean; toggle: () => void; } -const mockUserData = { - name: "Fulan bin Fulanah", - email: "janspoon@fighter.dev", - image: "https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-5.png", -}; +// const mockUserData = { +// name: "Fulan bin Fulanah", +// email: "janspoon@fighter.dev", +// image: "https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-5.png", +// }; export default function AppHeader(props: Props) { const [userMenuOpened, setUserMenuOpened] = useState(false); + const { user } = useAuth() + const userMenus = userMenuItems.map((item, i) => ( )); @@ -62,13 +65,13 @@ export default function AppHeader(props: Props) { > - {mockUserData.name} + {user?.name} { + fetchUserData() + }, [fetchUserData]) + return ( { - return bcrypt.hash(password, authConfig.saltRounds); + return bcrypt.hash(password, authConfig.saltRounds); } /** * Compares a plain text password with a hashed password. - * + * * @param password - The plain text password to compare. * @param hash - The hashed password to compare against. * @returns True if the passwords match, false otherwise. */ -export async function comparePassword(password: string, hash: string): Promise { - return bcrypt.compare(password, hash); +export async function comparePassword( + password: string, + hash: string +): Promise { + return bcrypt.compare(password, hash); } /** * Creates a JWT token based on user claims. - * + * * @param userClaims - The user claims to encode in the JWT. * @param options - Optional signing options. * @returns The generated JWT token. */ -export function createJwtToken(userClaims: UserClaims, options?: SignOptions): string { - const secret = process.env.JWT_SECRET; - if (!secret) throw new AuthError(AuthErrorCode.JWT_SECRET_EMPTY); - return jwt.sign(userClaims, secret, options); +export function createJwtToken( + userClaims: UserClaims, + options?: SignOptions +): string { + const secret = process.env.JWT_SECRET; + if (!secret) throw new AuthError(AuthErrorCode.JWT_SECRET_EMPTY); + return jwt.sign(userClaims, secret, options); } /** * Decodes a JWT token and retrieves the payload. - * + * * @param token - The JWT token to decode. * @returns The decoded payload. */ export function decodeJwtToken(token: string): JwtPayload | string { - const secret = process.env.JWT_SECRET; - if (!secret) throw new AuthError(AuthErrorCode.JWT_SECRET_EMPTY); + const secret = process.env.JWT_SECRET; + if (!secret) throw new AuthError(AuthErrorCode.JWT_SECRET_EMPTY); - try { - return jwt.verify(token, secret) as JwtPayload; - } catch (error) { - throw new AuthError(AuthErrorCode.INVALID_JWT_TOKEN); - } + try { + return jwt.verify(token, secret) as JwtPayload; + } catch (error) { + throw new AuthError(AuthErrorCode.INVALID_JWT_TOKEN); + } +} + +export async function getUserFromToken(token: string) { + const decodedToken = decodeJwtToken(token) as { + id: string; + iat: number; + }; + + const user = await prisma.user.findFirst({ + include:{ + photoProfile: true, + }, + where: { + id: decodedToken.id, + }, + }); + + return user; } diff --git a/src/features/auth/contexts/AuthContext.tsx b/src/features/auth/contexts/AuthContext.tsx new file mode 100644 index 0000000..11a3415 --- /dev/null +++ b/src/features/auth/contexts/AuthContext.tsx @@ -0,0 +1,66 @@ +"use client"; +import React, { + ReactElement, + ReactNode, + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from "react"; +import getUser from "../actions/getUser"; + +interface UserData { + name: string; + email: string + photoUrl: string | null; + // Add additional user fields as needed +} + +interface AuthContextState { + user: UserData | null; + fetchUserData: () => void; + logout: () => void; +} + +interface Props { + children: ReactNode; +} + +const AuthContext = createContext(undefined); + +export const AuthContextProvider = ({ children }: Props) => { + const [user, setUser] = useState(null); + + const fetchUserData = useCallback(() => { + const getUserData = async () => { + const user = await getUser(); + setUser(user) + } + + getUserData().then() + }, []) + + useEffect(() => { + fetchUserData() + }, [fetchUserData]); + + const logout = () => { + setUser(null); + }; + + return ( + + {children} + + ); +}; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth must be used within an AuthContextProvider"); + } + return context; +};