feat: Auth implementation
- Add Login & Logout page - Add middleware to protect pages - Add Header Server Side Component
This commit is contained in:
parent
08a41e5e2b
commit
2ff781feea
@ -3,6 +3,7 @@ NEXT_PUBLIC_API_URL=
|
||||
NEXTAUTH_URL=
|
||||
NEXTAUTH_SECRET=
|
||||
|
||||
OAUTH_PROVIDER_NAME=
|
||||
OAUTH_CLIENT_ID=
|
||||
OAUTH_CLIENT_SECRET=
|
||||
OAUTH_ISSUER=
|
||||
|
45
src/app/auth/login/page.tsx
Normal file
45
src/app/auth/login/page.tsx
Normal 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;
|
18
src/app/auth/logout/page.tsx
Normal file
18
src/app/auth/logout/page.tsx
Normal 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;
|
@ -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;
|
@ -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;
|
28
src/app/components/Header.tsx
Normal file
28
src/app/components/Header.tsx
Normal 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;
|
@ -8,7 +8,7 @@ export default function RootLayout({
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<body className="text-white">
|
||||
<SessionProviderWrapper>{children}</SessionProviderWrapper>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -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;
|
||||
|
@ -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
3
src/lib/stringUtils.ts
Normal 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
25
src/middleware.ts
Normal 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
1
src/types/env.d.ts
vendored
@ -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;
|
||||
|
@ -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: [],
|
||||
|
Loading…
Reference in New Issue
Block a user