Skip to content

Commit

Permalink
feat: Protect route from admin #17
Browse files Browse the repository at this point in the history
  • Loading branch information
dgd03146 committed Jul 16, 2023
1 parent dd50fee commit 6d07412
Show file tree
Hide file tree
Showing 22 changed files with 216 additions and 170 deletions.
17 changes: 17 additions & 0 deletions components/common/button/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import {} from 'twin.macro';

type TProps = {
text: string;
onClick: React.MouseEventHandler<HTMLButtonElement> | undefined;
};

const Button = ({ text, onClick }: TProps) => {
return (
<button tw="bg-primary2 py-2 px-4 text-primary7 rounded-md hover:brightness-110" onClick={onClick}>
{text}
</button>
);
};

export default Button;
53 changes: 28 additions & 25 deletions components/hoc/withAuth.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,35 @@
// import { useUser } from 'queries/hooks/auth/useUser';
import { ComponentType, useEffect } from 'react';
import { ComponentType, PropsWithChildren } from 'react';
import { useRouter } from 'next/router';
import { useAuthContext } from 'context/authContext';

const WithAuth = (WrappedComponent: ComponentType) => {
// FIXME: useRouter 못쓰는건가?
// const router = useRouter();

const HOC = () => {
// 유저가 없으면 로그인 페이지로 이동 로그인이 필요한 페이지만 적용 ex) 상품 구매? 페이지?

// FIXME: 서버 페이지 연결하고 주석 풀기
// useEffect(() => {
// const { user } = useUser();
// if (!user) {
// // FIXME: Alert 대신 Toast 띄어주기!
// alert('로그인이 필요합니다!');
// router.push('/login');
// } else {
// // FIXME: Alert 대신 Toast 띄어주기!
// alert('로그인이 되어있습니다!');
// router.push('/');
// }
// }, []);

return <WrappedComponent />;
import {} from 'twin.macro';

const WithAuth = (WrappedComponent: ComponentType<PropsWithChildren>, requireAdmin?: boolean) => {
const AuthenticatedComponent = (props: PropsWithChildren) => {
const router = useRouter();
const { loading, user } = useAuthContext();

console.log(user, 'withAuth 안 user');

if (loading) {
return (
<div tw="flex justify-center items-center mt-32 ">
<p tw="te">Loading...</p>
</div>
);
}

if ((requireAdmin && !user?.isAdmin) || !user) {
if (typeof window !== 'undefined') {
router.push('/');
}
return null;
}

return <WrappedComponent {...props} />;
};

return HOC;
return AuthenticatedComponent;
};

export default WithAuth;
57 changes: 21 additions & 36 deletions components/layouts/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,19 @@ import Link from 'next/link';
import React, { useEffect, useState } from 'react';
import { AiOutlineMenu } from 'react-icons/ai';
import { AiOutlineClose } from 'react-icons/ai';
import { IoIosArrowForward } from 'react-icons/io';
import { Pages } from '@lib/constants/constant';
import { BsFillCartPlusFill } from 'react-icons/bs';
import tw from 'twin.macro';
import { useRouter } from 'next/router';
import { login, logout, onUserStateChange } from '@services/api/firebase';
import UserInfo from '@components/user';

// TODO: 다이나믹 라우팅, 배열로 돌리기
import Button from '@components/common/button';
import { useAuthContext } from 'context/authContext';

const Header = () => {
const [showMenu, setShowMenu] = useState(false);
const [isScroll, setIsScroll] = useState(false);
const { pathname } = useRouter();
const isHomePage = pathname === '/';
const [user, setUser] = useState();

useEffect(() => {
onUserStateChange(setUser);
}, []);
const { user, login, logout } = useAuthContext();

useEffect(() => {
const handleScroll = () => {
Expand All @@ -45,7 +39,7 @@ const Header = () => {
return (
<header
css={[
tw`z-[1] fixed py-[30px] px-[60px] flex items-center w-full text-primary3`,
tw`z-[1] fixed py-[30px] px-[60px] flex items-center w-full justify-between text-primary3`,
!isHomePage && isScroll && tw`bg-primary1 py-2`,
]}
>
Expand All @@ -54,34 +48,25 @@ const Header = () => {
OWNUS
</Link>
</div>
<ul
css={[
tw`mobile:flex gap-20 absolute mobile:static top-[56px] bg-white1 mobile:bg-opacity-0 w-full mobile:w-auto left-0 px-8 mobile:px-0 py-4 mobile:py-0 justify-center basis-[50%] `,
!showMenu && tw`hidden`,
]}
>
{Pages.map((page) => (
<li tw="flex justify-between py-2 mobile:py-0 hover:text-primary4" key={page.href}>
<Link className="group" tw="relative font-bold text-[12px]" href={page.href}>
{page.title}
{/* <div tw="absolute w-full h-0.5 bg-primary4 scale-x-0 group-hover:scale-x-100 transition-transform duration-500" /> */}
</Link>
<div>
<IoIosArrowForward tw="text-2xl block mobile:hidden" />
</div>
</li>
))}
</ul>
<div tw="hidden mobile:flex items-center gap-x-12 font-semibold text-[12px] basis-[25%] justify-end ">
<Link href={'/'} tw="hover:text-primary4">
<p>CART</p>
{user && (
<Link className="group" tw="relative" href={'/cart'}>
<p>CART</p>
<div tw="absolute w-full h-0.5 bg-primary4 scale-x-0 group-hover:scale-x-100 transition-transform duration-500" />
</Link>
)}
<Link className="group" tw="relative" href={'/products'}>
<p>PRODUCTS</p>
<div tw="absolute w-full h-0.5 bg-primary4 scale-x-0 group-hover:scale-x-100 transition-transform duration-500" />
</Link>
{/* <Link href={'/'} tw="hover:text-primary4">
<p>MY PAGE</p>
</Link> */}
{user && user.isAdmin && (
<Link href={'/products'} tw="hover:text-primary4 text-2xl">
<BsFillCartPlusFill />
</Link>
)}
{user && <UserInfo user={user} />}
{!user && <button onClick={login}>LOGIN</button>}
{user && <button onClick={logout}>LOGOUT</button>}
{!user && <Button text="LOGIN" onClick={login} />}
{user && <Button text="LOGOUT" onClick={logout} />}
</div>
<button tw="block text-2xl mobile:hidden" onClick={handleTogle}>
{showMenu ? <AiOutlineClose /> : <AiOutlineMenu />}
Expand Down
8 changes: 4 additions & 4 deletions components/layouts/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ const Layout = ({ children, title }: LayoutProps) => {
<Head>
<title>{title}</title>
</Head>
<div tw="h-screen flex relative">
<div tw="z-10 w-full h-full absolute flex">
<div tw="w-full">
<div tw="h-screen flex relative ">
<div tw="z-10 w-full h-full absolute flex ">
<div tw="w-full flex flex-col">
{!isAuthPage && (
<>
<header tw="mb-[92px]">
Expand All @@ -43,7 +43,7 @@ const Layout = ({ children, title }: LayoutProps) => {
/>
</>
)}
<main tw="max-w-[1280px] mx-auto my-0">{children}</main>
<main tw="w-[90%] max-w-[1280px] mx-auto my-0 flex-[1]">{children}</main>
{!isAuthPage && !isHomePage && <Footer />}
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions components/layouts/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

2 changes: 1 addition & 1 deletion components/user/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type TProps = {

const UserInfo = ({ user: { photoURL, displayName } }: TProps) => {
return (
<div tw="flex items-center">
<div tw="flex items-center shrink-0">
<Image src={photoURL!} alt={displayName!} width={30} height={30} tw="rounded-full mr-2" />
<span tw="hidden desktop:block">{displayName}</span>
</div>
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion lib/constants/constant.ts → constants/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export const Pages = [
// { href: '/about', title: 'ABOUT US' },
{ href: '/products', title: 'SHOP' },
// { href: '/contacts', title: 'COMMUNITY' },
{ href: '/contacts', title: 'CONTACTS' },
// { href: '/contacts', title: 'CONTACTS' },
];

export const ProductsFilter = [
Expand Down
File renamed without changes.
56 changes: 56 additions & 0 deletions context/authContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Dispatch, ReactNode, SetStateAction, createContext, useContext, useEffect, useState } from 'react';
import { User, getAuth, onAuthStateChanged } from 'firebase/auth';
import { adminUser, login, logout, onUserStateChange } from '@services/firebase';

interface IUser extends User {
isAdmin?: boolean;
}

type AuthContextType = {
user: IUser | null;
loading: boolean;
// setUserInfo: Dispatch<SetStateAction<IUser | undefined>>;
login: typeof login;
logout: typeof logout;
};

const initialAuthContext: Partial<AuthContextType> = {
login: login,
logout: logout,
};

type AuthContextProviderProps = {
children: ReactNode;
};

const AuthContext = createContext<typeof initialAuthContext>(initialAuthContext);

export function AuthContextProvider({ children }: AuthContextProviderProps) {
// const [userInfo, setUserInfo] = useState<IUser>();
const [authState, setAuthState] = useState<{
user: IUser | null;
loading: boolean;
}>({ user: null, loading: true });

const { user, loading } = authState;

useEffect(() => {
const stopListen = onAuthStateChanged(getAuth(), (user) => {
if (user) {
adminUser(user).then((user) => setAuthState({ user, loading: false }));
} else {
setAuthState({ user: null, loading: false });
}
});

return () => stopListen();
}, [adminUser, getAuth]);

return <AuthContext.Provider value={{ user, login, logout, loading }}>{children}</AuthContext.Provider>;
}

export function useAuthContext() {
return useContext(AuthContext);
}

export default AuthContext;
43 changes: 11 additions & 32 deletions lib/api/httpClient.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import { EndPoint } from '@lib/constants/endpoint';
import axios, {
AxiosDefaults,
AxiosError,
AxiosInstance,
AxiosRequestConfig,
AxiosResponse
} from 'axios';
import { EndPoint } from 'constants/endpoint';
import axios, { AxiosDefaults, AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { TokenService } from '../../services/tokenService';

interface IFailedRequestQueue {
Expand All @@ -25,33 +19,27 @@ export class HttpClientService {
this.tokenRepository = tokenRepository;
this.baseURL = baseURL;
this.instance = axios.create({
baseURL: this.baseURL
baseURL: this.baseURL,
// headers: { Authorization: this.tokenRepository.getToken() }
});

this.handleInterceptor();
}

private setAuthorizationHeader(
request: AxiosDefaults | AxiosRequestConfig | any,
token: string
) {
private setAuthorizationHeader(request: AxiosDefaults | AxiosRequestConfig | any, token: string) {
request.headers.Authorization = `Bearer ${token}`;
}

private async handleRefreshToken(refreshToken: string) {
this.isTokenRefreshing = true;
try {
const { data } = await this.instance.post(EndPoint.refresh, {
refreshToken
refreshToken,
});
const { accessToken: newAccessToken, refreshToken: newRefreshToken } =
data;
const { accessToken: newAccessToken, refreshToken: newRefreshToken } = data;
this.tokenRepository.saveToken(newAccessToken, newRefreshToken);
this.setAuthorizationHeader(this.instance.defaults, newAccessToken);
this.failedRequestQueue.forEach((request) =>
request.onSuccess(newAccessToken)
);
this.failedRequestQueue.forEach((request) => request.onSuccess(newAccessToken));
this.failedRequestQueue = [];
} catch (error) {
this.failedRequestQueue.forEach((request) => request.onFailure(error));
Expand Down Expand Up @@ -80,10 +68,7 @@ export class HttpClientService {
private async handleResponseError(error: AxiosError) {
const { status, message, config } = error;
if (status === 401) {
if (
message === 'token expired' ||
(message === 'no authorization' && !this.isTokenRefreshing)
) {
if (message === 'token expired' || (message === 'no authorization' && !this.isTokenRefreshing)) {
// 토큰이 만료되었거나 페이지를 새로고침해서 accessToken이 없어지는 경우 isTokenRefreshing이 false인 경우에만 token refresh 요청
const originalConfig = config!; // 원래의 요청
// token expired 메세지가 나타날 경우
Expand All @@ -99,7 +84,7 @@ export class HttpClientService {
},
onFailure: (error) => {
reject(error);
}
},
});
});
} else {
Expand All @@ -110,14 +95,8 @@ export class HttpClientService {
}

private handleInterceptor() {
this.instance.interceptors.request.use(
this.handleRequest,
this.handleRequestError
);
this.instance.interceptors.request.use(this.handleRequest, this.handleRequestError);

this.instance.interceptors.response.use(
this.handleResponse,
this.handleResponseError
);
this.instance.interceptors.response.use(this.handleResponse, this.handleResponseError);
}
}
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@emotion/react": "^11.10.5",
"@emotion/server": "^11.11.0",
"@emotion/styled": "^11.10.5",
"@firebase/app": "^0.9.14",
"@next/font": "13.1.1",
"@tanstack/react-query": "^4.20.4",
"@tanstack/react-query-devtools": "^4.20.4",
Expand Down
Loading

0 comments on commit 6d07412

Please sign in to comment.