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?
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 {

View File

@ -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: React.ReactNode
children: React.ReactNode;
}) {
return (
<html lang="en">
<head>
<ColorSchemeScript/>
<ColorSchemeScript />
</head>
<body className={inter.className}>
<MantineProvider>
{children}
<AuthContextProvider>{children}</AuthContextProvider>
</MantineProvider>
</body>
</html>
)
);
}

View File

@ -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) => (
<UserMenuItem item={item} key={i} />
));
@ -62,13 +65,13 @@ export default function AppHeader(props: Props) {
>
<Group gap={7}>
<Avatar
src={mockUserData.image}
alt={mockUserData.name}
src={user?.photoUrl}
alt={user?.name}
radius="xl"
size={20}
/>
<Text fw={500} size="sm" lh={1} mr={3}>
{mockUserData.name}
{user?.name}
</Text>
<TbChevronDown
style={{ width: rem(12), height: rem(12) }}

View File

@ -1,10 +1,11 @@
"use client";
import React from "react";
import React, { useEffect } from "react";
import { AppShell } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import AppHeader from "../AppHeader";
import AppNavbar from "../AppNavbar";
import { useAuth } from "@/features/auth/contexts/AuthContext";
interface Props {
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
const [openNavbar, { toggle }] = useDisclosure(false);
const {fetchUserData} = useAuth();
useEffect(() => {
fetchUserData()
}, [fetchUserData])
return (
<AppShell
padding="md"

View File

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

View File

@ -6,6 +6,7 @@ import { comparePassword, createJwtToken } from "../authUtils";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import BaseError from "@/BaseError";
import { revalidatePath } from "next/cache";
/**
* 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.
* @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);
}
@ -34,7 +37,10 @@ export async function comparePassword(password: string, hash: string): Promise<b
* @param options - Optional signing options.
* @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;
if (!secret) throw new AuthError(AuthErrorCode.JWT_SECRET_EMPTY);
return jwt.sign(userClaims, secret, options);
@ -56,3 +62,21 @@ export function decodeJwtToken(token: string): JwtPayload | string {
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;
};