Skip to content

Commit

Permalink
Merge pull request #1 from IslemMedjahdi/auth
Browse files Browse the repository at this point in the history
protecting routes
  • Loading branch information
IslemMedjahdi authored Dec 25, 2022
2 parents 225a102 + 2f2f9f0 commit ce46511
Show file tree
Hide file tree
Showing 29 changed files with 836 additions and 18 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
NEXT_PUBLIC_BACKEND_URL=
NEXT_PUBLIC_GOOGLE_CLIENT_ID=
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ yarn-error.log*

# local env files
.env*.local
.env

# vercel
.vercel
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@
"@types/react": "18.0.26",
"@types/react-dom": "18.0.9",
"autoprefixer": "^10.4.13",
"axios": "^1.2.1",
"eslint": "8.30.0",
"eslint-config-next": "13.1.0",
"next": "13.1.0",
"next-seo": "^5.15.0",
"postcss": "^8.4.20",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-icons": "^4.7.1",
"react-loader-spinner": "^5.3.4",
"tailwindcss": "^3.2.4",
"typescript": "4.9.4"
},
Expand Down
Binary file added public/auth-background.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/favicon.ico
Binary file not shown.
3 changes: 3 additions & 0 deletions public/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion public/next.svg

This file was deleted.

1 change: 0 additions & 1 deletion public/thirteen.svg

This file was deleted.

1 change: 0 additions & 1 deletion public/vercel.svg

This file was deleted.

58 changes: 58 additions & 0 deletions src/components/auth/AuthIndex.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import Image from "next/image";
import { useEffect } from "react";
import { IMAGES } from "../../constants/images";
import { INFO } from "../../constants/info";
import useGoogleAuth from "../../hooks/useGoogleAuth";

const AuthIndex = () => {
const { handleGoogle, loading } = useGoogleAuth();

useEffect(() => {
const google = (window as any).google;
if (google) {
google.accounts.id.initialize({
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
callback: handleGoogle,
});
google.accounts.id.renderButton(document.getElementById("authDiv"), {
type: "standard",
size: "large",
text: "continue_with",
shape: "pill",
});
google.accounts.id.prompt();
}
}, [handleGoogle]);

return (
<div className="grid h-screen grid-cols-1 text-gray-900 md:grid-cols-2">
<div
style={{ backgroundImage: `url(${IMAGES.Auth_background})` }}
className="flex h-full w-full flex-col bg-white bg-cover bg-no-repeat p-8 md:!bg-none"
>
<div className="flex cursor-pointer flex-wrap items-baseline gap-2">
<Image src={IMAGES.Logo} alt="logo" width={50} height={50} />
<h1 className="whitespace-nowrap font-serif text-xl font-bold text-white md:text-gray-900">
{INFO.Title}
</h1>
</div>
<div className="flex flex-1 flex-col items-center justify-center gap-2">
<p className="text-center text-lg font-medium text-white md:text-gray-900">
Connectez vous pour continuer:
</p>
{loading ? (
<div>Loading....</div>
) : (
<div id="authDiv" data-text="continue_with"></div>
)}
</div>
</div>
<div
className="hidden h-full w-full bg-cover bg-no-repeat md:block"
style={{ backgroundImage: `url(${IMAGES.Auth_background})` }}
/>
</div>
);
};

export default AuthIndex;
18 changes: 18 additions & 0 deletions src/components/shared/Loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { RotatingSquare } from "react-loader-spinner";

const Loading = () => {
return (
<div className="flex h-screen w-screen items-center justify-center bg-white">
<RotatingSquare
height="100"
width="100"
color="#1C3988"
ariaLabel="rotating-square-loading"
strokeWidth="4"
visible={true}
/>
</div>
);
};

export default Loading;
26 changes: 26 additions & 0 deletions src/constants/axios.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Axios from "axios";
import TokenService from "../services/token.service";

const tokenService = TokenService.getInstance();

const BASE_URL = process.env.NEXT_PUBLIC_BACKEND_URL;

const axios = Axios.create({
baseURL: BASE_URL,
});

axios.interceptors.request.use(
(config) => {
const token = tokenService.getAccessToken();
if (token) {
if (config && config.headers)
config.headers["Authorization"] = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);

export default axios;
5 changes: 4 additions & 1 deletion src/constants/images.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export const IMAGES = {};
export const IMAGES = {
Logo: "/logo.svg",
Auth_background: "/auth-background.png",
};
8 changes: 7 additions & 1 deletion src/constants/info.ts
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
export const INFO = {};
import { IMAGES } from "./images";

export const INFO = {
Title: "Future Immobilier",
Icon: IMAGES.Logo,
Description: "",
};
4 changes: 4 additions & 0 deletions src/constants/roles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum ROLES {
ADMIN = "A",
USER = "U",
}
14 changes: 14 additions & 0 deletions src/constants/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ROLES } from "./roles";

export const ROUTES = {
AUTH: {
path: "/auth",
pathname: "/auth",
allowedRoles: [] as ROLES[],
},
HOME: {
path: "/",
pathname: "/",
allowedRoles: [ROLES.ADMIN, ROLES.USER] as ROLES[],
},
};
107 changes: 107 additions & 0 deletions src/context/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { useRouter } from "next/router";
import React, { createContext, useEffect, useMemo, useState } from "react";
import Loading from "../components/shared/Loading";
import { ROUTES } from "../constants/routes";
import TokenService from "../services/token.service";
import UserService from "../services/user.service";
import { Auth } from "../typings/user";

const tokenService = TokenService.getInstance();
const userService = UserService.getInstance();

export const AuthContext = createContext<{ updateUser?: () => Promise<void> }>(
{}
);

const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const router = useRouter();
const [pageLoading, setPageLoading] = useState<boolean>(true);
const [currentUser, setCurrentUser] = useState<Auth.User | null | undefined>(
undefined
);

const updateUser = async () => {
try {
const response = await userService.updateUser();
setCurrentUser(response.data);
} catch (e) {
throw e;
}
};

const handleAuth = async () => {
setPageLoading(true);
if (tokenService.getAccessToken()) {
if (router.pathname === ROUTES.AUTH.path) {
await router.replace(ROUTES.HOME.path);
}
const user = userService.getUserInfo(); // get user if exist
if (user) {
setCurrentUser(user); // get the user
setPageLoading(false);
}
try {
await updateUser();
setPageLoading(false);
} catch (e) {
console.log(e);
}
} else {
await router.replace(ROUTES.AUTH.path);
setPageLoading(false);
setCurrentUser(null);
}
};

useEffect(() => {
handleAuth();
}, []);

useEffect(() => {
const handleRouteChange = async () => {
if (currentUser !== undefined) {
if (currentUser === null) {
await router.replace(ROUTES.AUTH.path);
} else {
if (router.pathname === ROUTES.AUTH.path) {
await router.replace(ROUTES.HOME.path);
}
}
}
};
handleRouteChange();
}, [currentUser]);

useEffect(() => {
if (currentUser) {
if (
!Object.entries(ROUTES)
.find(([_, item]) => item.pathname === router.pathname)?.[1]
.allowedRoles.includes(currentUser.role)
) {
router.replace(ROUTES.HOME.path);
}
}
}, [router.pathname, currentUser]);

const contextValue = useMemo(() => ({ updateUser }), [updateUser]);

if (
pageLoading ||
currentUser === undefined ||
(currentUser &&
!Object.entries(ROUTES)
.find(([_, item]) => item.pathname === router.pathname)?.[1]
.allowedRoles.includes(currentUser.role))
) {
return <Loading />;
}

return (
<AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>
);
};

export default AuthProvider;
12 changes: 12 additions & 0 deletions src/hooks/useAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useContext } from "react";
import { AuthContext } from "../context/AuthContext";

const useAuth = () => {
if (AuthContext) {
return useContext(AuthContext);
} else {
throw new Error("AuthProvider is required");
}
};

export default useAuth;
28 changes: 28 additions & 0 deletions src/hooks/useGoogleAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useState } from "react";
import AuthService from "../services/auth.service";
import TokenService from "../services/token.service";
import useAuth from "./useAuth";

const authService = AuthService.getInstance();
const tokenService = TokenService.getInstance();

const useGoogleAuth = () => {
const [loading, setLoading] = useState<boolean>(false);
const { updateUser } = useAuth();

const handleGoogle = async (response: any) => {
setLoading(true);
try {
const res = await authService.postGoogleCredential(response.credential);
tokenService.updateAccessToken(res.data.token);
updateUser && (await updateUser());
} catch (e) {
console.log(e);
} finally {
setLoading(false);
}
};
return { loading, handleGoogle };
};

export default useGoogleAuth;
17 changes: 14 additions & 3 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import '../styles/globals.css'
import type { AppProps } from 'next/app'
import { NextSeo } from "next-seo";
import type { AppProps } from "next/app";
import { INFO } from "../constants/info";
import AuthProvider from "../context/AuthContext";
import "../styles/globals.css";

export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
return (
<AuthProvider>
<NextSeo
titleTemplate={`${INFO.Title} | %s`}
description={INFO.Description}
/>
<Component {...pageProps} />
</AuthProvider>
);
}
23 changes: 21 additions & 2 deletions src/pages/_document.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
import { Html, Head, Main, NextScript } from "next/document";
import { Head, Html, Main, NextScript } from "next/document";
import { INFO } from "../constants/info";

export default function Document() {
return (
<Html lang="en">
<Head></Head>
<Head>
<link rel="icon" type="image/x-icon" href={INFO.Icon} />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossOrigin=""
/>
<link
href="https://fonts.googleapis.com/css2?family=Merriweather:wght@400;700&family=Open+Sans:wght@300;400;500;600;700&display=swap"
rel="stylesheet"
></link>

<script
src="https://accounts.google.com/gsi/client"
async
defer
></script>
</Head>
<body>
<Main />
<NextScript />
Expand Down
14 changes: 14 additions & 0 deletions src/pages/auth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { NextSeo } from "next-seo";

import AuthIndex from "../components/auth/AuthIndex";

const Auth = () => {
return (
<>
<NextSeo title="Auth" />
<AuthIndex />
</>
);
};

export default Auth;
Loading

0 comments on commit ce46511

Please sign in to comment.