Added user data context
This commit is contained in:
parent
047e1f6fa9
commit
02a2356da1
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,34 @@
|
||||||
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">
|
||||||
<head>
|
<head>
|
||||||
<ColorSchemeScript/>
|
<ColorSchemeScript />
|
||||||
</head>
|
</head>
|
||||||
<body className={inter.className}>
|
<body className={inter.className}>
|
||||||
<MantineProvider>
|
<MantineProvider>
|
||||||
{children}
|
<AuthContextProvider>{children}</AuthContextProvider>
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) }}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 user;
|
return {
|
||||||
|
name: user.name ?? "",
|
||||||
|
email: user.email ?? "",
|
||||||
|
photoUrl: user.photoProfile?.path ?? null
|
||||||
|
}
|
||||||
} 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;
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -8,51 +8,75 @@ import UserClaims from "./types/UserClaims";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hashes a plain text password using bcrypt.
|
* Hashes a plain text password using bcrypt.
|
||||||
*
|
*
|
||||||
* @param password - The plain text password to hash.
|
* @param password - The plain text password to hash.
|
||||||
* @returns The hashed password.
|
* @returns The hashed password.
|
||||||
*/
|
*/
|
||||||
export async function hashPassword(password: string): Promise<string> {
|
export async function hashPassword(password: string): Promise<string> {
|
||||||
return bcrypt.hash(password, authConfig.saltRounds);
|
return bcrypt.hash(password, authConfig.saltRounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compares a plain text password with a hashed password.
|
* Compares a plain text password with a hashed password.
|
||||||
*
|
*
|
||||||
* @param password - The plain text password to compare.
|
* @param password - The plain text password to compare.
|
||||||
* @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(
|
||||||
return bcrypt.compare(password, hash);
|
password: string,
|
||||||
|
hash: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
return bcrypt.compare(password, hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a JWT token based on user claims.
|
* Creates a JWT token based on user claims.
|
||||||
*
|
*
|
||||||
* @param userClaims - The user claims to encode in the JWT.
|
* @param userClaims - The user claims to encode in the JWT.
|
||||||
* @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(
|
||||||
const secret = process.env.JWT_SECRET;
|
userClaims: UserClaims,
|
||||||
if (!secret) throw new AuthError(AuthErrorCode.JWT_SECRET_EMPTY);
|
options?: SignOptions
|
||||||
return jwt.sign(userClaims, secret, options);
|
): 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.
|
* Decodes a JWT token and retrieves the payload.
|
||||||
*
|
*
|
||||||
* @param token - The JWT token to decode.
|
* @param token - The JWT token to decode.
|
||||||
* @returns The decoded payload.
|
* @returns The decoded payload.
|
||||||
*/
|
*/
|
||||||
export function decodeJwtToken(token: string): JwtPayload | string {
|
export function decodeJwtToken(token: string): JwtPayload | 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);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return jwt.verify(token, secret) as JwtPayload;
|
return jwt.verify(token, secret) as JwtPayload;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
66
src/features/auth/contexts/AuthContext.tsx
Normal file
66
src/features/auth/contexts/AuthContext.tsx
Normal 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;
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user