feat: Auth implementation

- Add Login & Logout page
- Add middleware to protect pages
- Add Header Server Side Component
This commit is contained in:
M1000fr 2024-12-12 15:12:57 +01:00
parent 08a41e5e2b
commit 2ff781feea
13 changed files with 145 additions and 102 deletions

View File

@ -3,6 +3,7 @@ NEXT_PUBLIC_API_URL=
NEXTAUTH_URL=
NEXTAUTH_SECRET=
OAUTH_PROVIDER_NAME=
OAUTH_CLIENT_ID=
OAUTH_CLIENT_SECRET=
OAUTH_ISSUER=

View File

@ -0,0 +1,45 @@
"use client";
import { authOptions } from "@/authOptions";
import { signIn } from "next-auth/react";
const LoginPage = () => {
const providers = authOptions.providers;
return (
<div className="flex flex-col h-screen items-center justify-center bg-gradient-to-br from-green-700 to-green-950 bg-[length:200%_200%] animate-gradient-x">
<div className="flex flex-col justify-center items-center w-1/2 gap-6 bg-black bg-opacity-40 p-4 rounded-md">
<div className="flex items-center w-full gap-5">
<div className="border-t border-white flex-grow"></div>
<h1 className="text-3xl font-bold">Toogether</h1>
<div className="border-t border-white flex-grow"></div>
</div>
<div className="flex flex-col items-center gap-2">
<h2 className="font-bold text-2xl">Connexion</h2>
<p>
Pour accéder à la plateforme, merci de vous
authentifier.
</p>
</div>
<div className="w-full border-t border-white"></div>
<h3 className="font-bold text-xl">Via</h3>
<ul>
{providers.map((provider) => (
<li key={provider.id}>
<button
className="bg-white text-black p-2 rounded-md"
onClick={() => signIn(provider.id)}
>
{provider.name}
</button>
</li>
))}
</ul>
</div>
</div>
);
};
export default LoginPage;

View File

@ -0,0 +1,18 @@
"use client";
import { useEffect } from "react";
import { signOut, useSession } from "next-auth/react";
const LogoutPage = () => {
const session = useSession();
useEffect(() => {
if (session) {
signOut();
}
}, [session]);
return null;
};
export default LogoutPage;

View File

@ -1,56 +0,0 @@
"use client"
import { signIn, signOut, useSession } from "next-auth/react";
const Auth = () => {
const { data: session } = useSession();
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
marginTop: "50px",
}}
>
<h1>NextAuth Login</h1>
{!session ? (
<div>
<p>You are not signed in.</p>
<button
style={{
padding: "10px 20px",
backgroundColor: "#7289DA",
color: "white",
border: "none",
borderRadius: "5px",
cursor: "pointer",
}}
onClick={() => signIn("oauth2")}
>
Sign in with OAuth2
</button>
</div>
) : (
<div>
<p>Welcome, {session.user?.name || "User"}!</p>
<button
style={{
padding: "10px 20px",
backgroundColor: "red",
color: "white",
border: "none",
borderRadius: "5px",
cursor: "pointer",
}}
onClick={() => signOut()}
>
Sign out
</button>
</div>
)}
</div>
);
};
export default Auth;

View File

@ -1,35 +0,0 @@
import { axiosInstance } from "@/lib/axios";
import axios, { AxiosError } from "axios";
import { useEffect, useState } from "react";
const FetchWithSession = () => {
const [data, setData] = useState<{ message: string } | null>(null);
const [error, setError] = useState<Error | AxiosError | null>(null);
useEffect(() => {
const fetchData = async () => {
await axiosInstance
.get<{
message: string;
}>(`/ping`)
.then((response) => setData(response.data))
.catch((err: Error | AxiosError) => {
if (axios.isAxiosError(err)) setError(err.response?.data);
else setError(err);
});
};
setInterval(() => {
fetchData();
}, 1000);
}, []);
return (
<div>
{error && <p>Error: {error.message}</p>}
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
</div>
);
};
export default FetchWithSession;

View File

@ -0,0 +1,28 @@
import { getServerSession, Session } from "next-auth";
import Link from "next/link";
const Header = async () => {
const session = (await getServerSession()) as Session;
return (
<header className="bg-gray-900 text-white p-4">
<div className="container mx-auto flex justify-between items-center">
<h1 className="text-xl font-bold">Toogether</h1>
<div className="flex items-center gap-2">
<p>
Logged in as <strong>{session.user.name}</strong>
</p>
<nav className="space-x-4">
<Link href="/auth/logout">
<button className="bg-gray-800 px-4 py-2 rounded hover:scale-105 transition duration-200">
Logout
</button>
</Link>
</nav>
</div>
</div>
</header>
);
};
export default Header;

View File

@ -8,7 +8,7 @@ export default function RootLayout({
}) {
return (
<html lang="en">
<body>
<body className="text-white">
<SessionProviderWrapper>{children}</SessionProviderWrapper>
</body>
</html>

View File

@ -1,12 +1,11 @@
"use client";
import Auth from "./components/Auth";
import FetchWithSession from "./components/FetchWithSession";
import Header from "./components/Header";
export default function Home() {
const HomePage = () => {
return (
<>
<Auth />
<FetchWithSession />
<Header />
</>
);
}
};
export default HomePage;

View File

@ -6,8 +6,8 @@ import { JWT } from "next-auth/jwt";
export const authOptions: AuthOptions = {
providers: [
{
id: "oauth2",
name: "oauth2",
id: "oauth",
name: process.env.OAUTH_PROVIDER_NAME,
type: "oauth",
clientId: process.env.OAUTH_CLIENT_ID,
clientSecret: process.env.OAUTH_CLIENT_SECRET,
@ -39,7 +39,7 @@ export const authOptions: AuthOptions = {
async jwt({ token, account, user }) {
if (account && user) {
token.accessToken = account.access_token;
token.accessTokenExpires = moment(account.expires_at).subtract(5, "s");
token.accessTokenExpires = moment(account.expires_at * 1000).subtract(5, "s");
token.refreshToken = account.refresh_token;
token.user = user;
return token;
@ -62,6 +62,10 @@ export const authOptions: AuthOptions = {
return session;
},
},
pages: {
signIn: "/auth/login",
signOut: "/auth/logout",
}
};
const refreshAccessToken = async (token: JWT): Promise<JWT> => {

3
src/lib/stringUtils.ts Normal file
View File

@ -0,0 +1,3 @@
export const UppercaseFirstLetter = (str: string) => {
return str.slice(0, 1).toLocaleUpperCase() + str.slice(1);
}

25
src/middleware.ts Normal file
View File

@ -0,0 +1,25 @@
import { NextRequest, NextResponse } from "next/server";
import { getToken } from "next-auth/jwt";
export async function middleware(req: NextRequest) {
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
const isAuth = !!token;
const url = req.nextUrl.clone();
if (isAuth && url.pathname === "/auth/login") {
url.pathname = "/";
return NextResponse.redirect(url);
}
if (!isAuth && url.pathname !== "/auth/login") {
url.pathname = "/auth/login";
return NextResponse.redirect(url);
}
return NextResponse.next();
}
export const config = {
matcher: ["/((?!api|_next|static|favicon.ico).*)"],
};

1
src/types/env.d.ts vendored
View File

@ -1,5 +1,6 @@
declare namespace NodeJS {
interface ProcessEnv {
OAUTH_PROVIDER_NAME: string;
OAUTH_CLIENT_ID: string;
OAUTH_CLIENT_SECRET: string;
OAUTH_AUTHORIZATION_URL: string;

View File

@ -12,6 +12,16 @@ export default {
background: "var(--background)",
foreground: "var(--foreground)",
},
animation: {
"gradient-x": "gradient-x 5s ease infinite",
},
keyframes: {
"gradient-x": {
"0%": { "background-position": "0% 50%" },
"50%": { "background-position": "100% 50%" },
"100%": { "background-position": "0% 50%" },
},
},
},
},
plugins: [],