Skip to content

Commit

Permalink
Merge pull request #2 from AmrMustafa282/feat/add-o-auth
Browse files Browse the repository at this point in the history
add Authentication useing auth hook
  • Loading branch information
AmrMustafa282 authored Feb 6, 2025
2 parents 5fef93c + 0455e38 commit 2267edd
Show file tree
Hide file tree
Showing 19 changed files with 7,347 additions and 8,025 deletions.
5 changes: 5 additions & 0 deletions .temp.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
BACKEND_URL="process.env.BACKEND_URL"

GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
NEXTAUTH_SECRET=""
37 changes: 14 additions & 23 deletions app/(protected)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,20 @@
"use client";
import { useEffect, useState } from "react";
import useUserStore from "@/zustand/userStore";
import { useEffect } from "react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import useCheckUserExpiration from "@/hooks/useCheckUserExpiration";

const ProtectedLayout = ({ children }: { children: React.ReactNode }) => {
const router = useRouter();
const { user } = useUserStore();
const [isMounted, setIsMounted] = useState(false);
console.log(user);
useCheckUserExpiration();
// Ensure we only access persisted state after mount
useEffect(() => {
setIsMounted(true);
}, []);
export default function MePage() {
const { data: session, status } = useSession();
const router = useRouter();

useEffect(() => {
if (isMounted && !user) {
router.push("/login");
}
}, [isMounted, user, router]);
useEffect(() => {
if (status === "unauthenticated") {
router.push("/login");
}
}, [status, router]);

if (!isMounted) return null; // Prevent hydration mismatch
if (status === "loading") return <p>Loading...</p>;
if (!session) return null;

return <>{children}</>;
};

export default ProtectedLayout;
return <h1>Welcome, {session.user?.name}!</h1>;
}
125 changes: 125 additions & 0 deletions app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import NextAuth from "next-auth";
import axios from "axios";
import type { NextAuthOptions } from "next-auth";
import Credentials from "next-auth/providers/credentials";
import Google from "next-auth/providers/google";
import GitHub from "next-auth/providers/github"; // Add GitHub provider

export const authConfig = {
providers: [
Credentials({
name: "Credentials",
credentials: {
username: { label: "Email", type: "text" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
const res = await axios.post(`${process.env.BACKEND_URL}/login`, {
username: credentials?.username,
password: credentials?.password,
});

const user = res.data.data;

if (res.status && user) {
return user; // Return user object if authentication is successful
} else {
return null; // Return null if authentication fails
}
},
}),

// Google Authentication
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),

// GitHub Authentication
GitHub({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}),
],

// Customize session and JWT behavior
session: {
strategy: "jwt",
},

callbacks: {
// Handle JWT token creation
async jwt({ token, user }) {
if (user) {
token.id = user.id;
token.email = user.email;
}
return token;
},

// Handle session creation
async session({ session, token }: { session: any; token: any }) {
session.user.id = token.id;
session.user.email = token.email;
return session;
},

// Handle sign-in logic
async signIn({ user, account, profile }) {
if (account?.provider === "google") {
// Call your backend API to find or register the user
try {
const res = await axios.post(`${process.env.BACKEND_URL}/api/v1/oauth`, {
email: profile?.email,
name: profile?.name,
googleId: profile?.sub,
});

if (res.data.user) {
user.id = res.data.user.id;
user.email = res.data.user.email;
return true;
} else {
return false;
}
} catch (error) {
console.error("Error during Google sign-in:", error);
return false;
}
}

if (account?.provider === "github") {
// Call your backend API to find or register the user
try {
const res = await axios.post(`${process.env.BACKEND_URL}/api/v1/oauth`, {
email: profile?.email,
name: profile?.name,
githubId: profile?.sub,
});

if (res.data.user) {
user.id = res.data.user.id;
user.email = res.data.user.email;
return true;
} else {
return false;
}
} catch (error) {
console.error("Error during GitHub sign-in:", error);
return false;
}
}

// For credentials provider, proceed as usual
return true;
},
},

// Custom pages (optional)
pages: {
signIn: "/auth/signin", // Custom sign-in page
},
} satisfies NextAuthOptions;

const handler = NextAuth(authConfig);
export { handler as GET, handler as POST };
13 changes: 13 additions & 0 deletions app/api/auth/googleAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export async function authenticateGoogle(token: string) {
const res = await fetch(`${process.env.BACKEND_URL}/api/auth/google`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token }),
});

if (!res.ok) {
throw new Error("Google authentication failed");
}

return res.json();
}
41 changes: 23 additions & 18 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,40 @@ import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import Header from "@/components/Header";
import AuthProvider from "@/components/auth/AuthProvider";

const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
variable: "--font-geist-sans",
subsets: ["latin"],
});

const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
variable: "--font-geist-mono",
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,
}: Readonly<{
children: React.ReactNode;
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<main className="flex flex-col h-screen">
<Header />
<div className="flex-grow container mx-auto">{children}</div>
</main>
</body>
</html>
);
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<main className="flex flex-col h-screen">
<AuthProvider>
<Header />
<div className="flex-grow container mx-auto">{children}</div>
</AuthProvider>
</main>
</body>
</html>
);
}
2 changes: 2 additions & 0 deletions app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ export default function LoginPage() {
</div>
);
}


2 changes: 1 addition & 1 deletion app/signup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { GalleryVerticalEnd } from "lucide-react";

import { SignupForm } from "@/components/auth/signup-form";

export default function LoginPage() {
export default function SignPage() {
return (
<div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10">
<div className="flex w-full max-w-sm flex-col gap-6">
Expand Down
Empty file added auth.config.ts
Empty file.
65 changes: 38 additions & 27 deletions components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,47 @@
"use client";
import { useEffect, useState } from "react";
import { useEffect } from "react";
import useUserStore from "../zustand/userStore";
import { Button } from "./ui/button";
import { useRouter } from "next/navigation";
import useAuthenticate from "../hooks/useAuthenticate";

export default function Header() {
const [isMounted, setIsMounted] = useState(false);
const { user, clearUser } = useUserStore();
const router = useRouter();
useEffect(() => {
setIsMounted(true);
}, [user]);
const { session, status, isSessionExpired, signOut } = useAuthenticate();
const { user, clearUser } = useUserStore();
const router = useRouter();

if (!isMounted) {
return null;
}
return (
<header className="bg-white py-4 ">
<nav className="flex justify-between items-center container mx-auto ">
<div>Welcome, {user ? user.name : "Guest"}</div>
useEffect(() => {
if (isSessionExpired) {
clearUser();
}
}, [isSessionExpired, clearUser]);

{user ? (
<Button onClick={clearUser}>Logout</Button>
) : (
<div className="flex gap-1">
<Button variant={"ghost"} size={"lg"} onClick={() => router.push("/signup")}>
SignUp
</Button>
<Button size={"lg"} onClick={() => router.push("/login")}>Login</Button>
</div>
)}
</nav>
</header>
);
if (status === "loading") {
return null;
}

return (
<header className="bg-white py-4">
<nav className="flex justify-between items-center container mx-auto">
<div>Welcome, {session?.user?.name || "Guest"}</div>

{session?.user ? (
<Button onClick={() => signOut()}>Logout</Button>
) : (
<div className="flex gap-1">
<Button
variant="ghost"
size="lg"
onClick={() => router.push("/signup")}
>
SignUp
</Button>
<Button size="lg" onClick={() => router.push("/login")}>
Login
</Button>
</div>
)}
</nav>
</header>
);
}
11 changes: 11 additions & 0 deletions components/auth/AuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"use client";

import { SessionProvider } from "next-auth/react";

export default function AuthProvider({
children,
}: {
children: React.ReactNode;
}) {
return <SessionProvider>{children}</SessionProvider>;
}
Loading

0 comments on commit 2267edd

Please sign in to comment.