Added user data context

This commit is contained in:
Sianida26 2024-01-22 13:51:18 +07:00
parent 047e1f6fa9
commit 02a2356da1
10 changed files with 191 additions and 60 deletions

View File

@ -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;

View File

@ -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;

View File

@ -42,10 +42,17 @@ model User {
name String? name String?
email String? @unique email String? @unique
emailVerified DateTime? emailVerified DateTime?
image String?
passwordHash String? passwordHash String?
accounts Account[] accounts Account[]
sessions Session[] 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 { model VerificationToken {

View File

@ -1,22 +1,23 @@
import type { Metadata } from 'next' import type { Metadata } from "next";
import { Inter } from 'next/font/google' import { Inter } from "next/font/google";
import "./globals.css" import "./globals.css";
import '@mantine/core/styles.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 = { export const metadata: Metadata = {
title: 'Create Next App', title: "Create Next App",
description: 'Generated by create next app', description: "Generated by create next app",
} };
export default function RootLayout({ export default function RootLayout({
children, children,
}: { }: {
children: React.ReactNode children: React.ReactNode;
}) { }) {
return ( return (
<html lang="en"> <html lang="en">
@ -25,9 +26,9 @@ export default function RootLayout({
</head> </head>
<body className={inter.className}> <body className={inter.className}>
<MantineProvider> <MantineProvider>
{children} <AuthContextProvider>{children}</AuthContextProvider>
</MantineProvider> </MantineProvider>
</body> </body>
</html> </html>
) );
} }

View File

@ -17,21 +17,24 @@ import classNames from "./styles.module.css";
import { TbChevronDown, TbLogout, TbSettings } from "react-icons/tb"; import { TbChevronDown, TbLogout, TbSettings } from "react-icons/tb";
import userMenuItems from "./_data/userMenuItems"; import userMenuItems from "./_data/userMenuItems";
import UserMenuItem from "./_components/UserMenuItem/UserMenuItem"; import UserMenuItem from "./_components/UserMenuItem/UserMenuItem";
import { useAuth } from "@/features/auth/contexts/AuthContext";
interface Props { interface Props {
openNavbar: boolean; openNavbar: boolean;
toggle: () => void; toggle: () => void;
} }
const mockUserData = { // const mockUserData = {
name: "Fulan bin Fulanah", // name: "Fulan bin Fulanah",
email: "janspoon@fighter.dev", // email: "janspoon@fighter.dev",
image: "https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-5.png", // image: "https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-5.png",
}; // };
export default function AppHeader(props: Props) { export default function AppHeader(props: Props) {
const [userMenuOpened, setUserMenuOpened] = useState(false); const [userMenuOpened, setUserMenuOpened] = useState(false);
const { user } = useAuth()
const userMenus = userMenuItems.map((item, i) => ( const userMenus = userMenuItems.map((item, i) => (
<UserMenuItem item={item} key={i} /> <UserMenuItem item={item} key={i} />
)); ));
@ -62,13 +65,13 @@ export default function AppHeader(props: Props) {
> >
<Group gap={7}> <Group gap={7}>
<Avatar <Avatar
src={mockUserData.image} src={user?.photoUrl}
alt={mockUserData.name} alt={user?.name}
radius="xl" radius="xl"
size={20} size={20}
/> />
<Text fw={500} size="sm" lh={1} mr={3}> <Text fw={500} size="sm" lh={1} mr={3}>
{mockUserData.name} {user?.name}
</Text> </Text>
<TbChevronDown <TbChevronDown
style={{ width: rem(12), height: rem(12) }} style={{ width: rem(12), height: rem(12) }}

View File

@ -1,10 +1,11 @@
"use client"; "use client";
import React from "react"; import React, { useEffect } from "react";
import { AppShell } from "@mantine/core"; import { AppShell } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks"; import { useDisclosure } from "@mantine/hooks";
import AppHeader from "../AppHeader"; import AppHeader from "../AppHeader";
import AppNavbar from "../AppNavbar"; import AppNavbar from "../AppNavbar";
import { useAuth } from "@/features/auth/contexts/AuthContext";
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
@ -22,6 +23,12 @@ export default function DashboardLayout(props: Props) {
// State and toggle function for handling the disclosure of the navigation bar // State and toggle function for handling the disclosure of the navigation bar
const [openNavbar, { toggle }] = useDisclosure(false); const [openNavbar, { toggle }] = useDisclosure(false);
const {fetchUserData} = useAuth();
useEffect(() => {
fetchUserData()
}, [fetchUserData])
return ( return (
<AppShell <AppShell
padding="md" padding="md"

View File

@ -2,7 +2,7 @@
import { cookies } from "next/headers" import { cookies } from "next/headers"
import "server-only" import "server-only"
import { decodeJwtToken } from "../authUtils"; import { decodeJwtToken, getUserFromToken } from "../authUtils";
import prisma from "@/db"; import prisma from "@/db";
import AuthError, { AuthErrorCode } from "../AuthError"; import AuthError, { AuthErrorCode } from "../AuthError";
import logout from "./logout"; import logout from "./logout";
@ -13,16 +13,15 @@ export default async function getUser(){
if (!token) return null; if (!token) return null;
const decodedToken = decodeJwtToken(token.value) as {id: string, iat: number}; const user = await getUserFromToken(token.value);
console.log('token', decodedToken)
const user = await prisma.user.findFirst({ if (!user) return null;
where: {
id: decodedToken.id return {
name: user.name ?? "",
email: user.email ?? "",
photoUrl: user.photoProfile?.path ?? null
} }
});
return user;
} catch (e: unknown){ } catch (e: unknown){
if (e instanceof AuthError && e.errorCode === AuthErrorCode.INVALID_JWT_TOKEN){ if (e instanceof AuthError && e.errorCode === AuthErrorCode.INVALID_JWT_TOKEN){
return null; return null;

View File

@ -6,6 +6,7 @@ import { comparePassword, createJwtToken } from "../authUtils";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import BaseError from "@/BaseError"; import BaseError from "@/BaseError";
import { revalidatePath } from "next/cache";
/** /**
* Handles the sign-in process for a user. * Handles the sign-in process for a user.

View File

@ -23,7 +23,10 @@ export async function hashPassword(password: string): Promise<string> {
* @param hash - The hashed password to compare against. * @param hash - The hashed password to compare against.
* @returns True if the passwords match, false otherwise. * @returns True if the passwords match, false otherwise.
*/ */
export async function comparePassword(password: string, hash: string): Promise<boolean> { export async function comparePassword(
password: string,
hash: string
): Promise<boolean> {
return bcrypt.compare(password, hash); return bcrypt.compare(password, hash);
} }
@ -34,7 +37,10 @@ export async function comparePassword(password: string, hash: string): Promise<b
* @param options - Optional signing options. * @param options - Optional signing options.
* @returns The generated JWT token. * @returns The generated JWT token.
*/ */
export function createJwtToken(userClaims: UserClaims, options?: SignOptions): string { export function createJwtToken(
userClaims: UserClaims,
options?: SignOptions
): string {
const secret = process.env.JWT_SECRET; const secret = process.env.JWT_SECRET;
if (!secret) throw new AuthError(AuthErrorCode.JWT_SECRET_EMPTY); if (!secret) throw new AuthError(AuthErrorCode.JWT_SECRET_EMPTY);
return jwt.sign(userClaims, secret, options); return jwt.sign(userClaims, secret, options);
@ -56,3 +62,21 @@ export function decodeJwtToken(token: string): JwtPayload | string {
throw new AuthError(AuthErrorCode.INVALID_JWT_TOKEN); 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;
}

View File

@ -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<AuthContextState | undefined>(undefined);
export const AuthContextProvider = ({ children }: Props) => {
const [user, setUser] = useState<UserData | null>(null);
const fetchUserData = useCallback(() => {
const getUserData = async () => {
const user = await getUser();
setUser(user)
}
getUserData().then()
}, [])
useEffect(() => {
fetchUserData()
}, [fetchUserData]);
const logout = () => {
setUser(null);
};
return (
<AuthContext.Provider value={{ user, fetchUserData, logout }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthContextProvider");
}
return context;
};