Skip to content

Commit

Permalink
JWT와 web storage로 로그인 기능을 구현합니다. (#144)
Browse files Browse the repository at this point in the history
* feat: `WebStorage` 클래스 구현

local, session storage 관리를 위한 클래스 구현

* feat: authInterceptor 구현

storage에 토큰이 있다면 자동으로 토큰을 넣는 interceptor 구현

* feat: `useGetUser` 구현

로그인 유/무, 현재 로그인 유저의 정보를 가져오는 커스텀 훅 구현

* feat: login/logout 구현
  • Loading branch information
ludacirs authored Sep 23, 2023
1 parent dd9f7b4 commit c50fcc0
Show file tree
Hide file tree
Showing 8 changed files with 209 additions and 12 deletions.
35 changes: 35 additions & 0 deletions apps/realworld/src/entities/user/api/useGetUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
getGetCurrentUserQueryKey,
useGetCurrentUser,
} from '@/shared/api/realworld/endpoints/user-and-authentication/user-and-authentication';

const useGetUser = () => {
const { data: user, isError: isLoginError } = useGetCurrentUser({
query: {
staleTime: Infinity,
queryKey: getGetUserQueryKey(),
select: data => {
return data.user;
},
},
});
const isLogin = Boolean(user && !isLoginError);

if (isLogin) {
return {
isLogin,
user: user!,
};
}

return {
isLogin,
user: null,
};
};

export default useGetUser;

export const getGetUserQueryKey = () => {
return [...getGetCurrentUserQueryKey(), 'useGetUser'];
};
3 changes: 2 additions & 1 deletion apps/realworld/src/features/user/ui/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import LoginForm from './login-form/login-form';
import LogoutButton from './logout-button/logout-button';
import RegisterForm from './register-form/register-form';
import UserFollowToggleButton from './user-follow-toggle-button/user-follow-toggle-button';

export { UserFollowToggleButton, LoginForm, RegisterForm };
export { UserFollowToggleButton, LoginForm, RegisterForm, LogoutButton };
22 changes: 21 additions & 1 deletion apps/realworld/src/features/user/ui/login-form/login-form.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
'use client';

import { getGetUserQueryKey } from '@/entities/user/api/useGetUser';
import { useLogin } from '@/shared/api/realworld/endpoints/user-and-authentication/user-and-authentication';
import { PathBuilder } from '@/shared/utils/routes';
import webStorage, { StorageKey } from '@/shared/utils/webStorage';
import { Button, Input } from '@packages/ui';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/navigation';
import React, { useState } from 'react';

const LoginForm = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { mutateAsync: login, error: loginError, isLoading: loginLoading } = useLogin();
const { push } = useRouter();
const queryClient = useQueryClient();

const {
mutateAsync: login,
error: loginError,
isLoading: loginLoading,
} = useLogin({
mutation: {
onSuccess: async data => {
webStorage.setItem(StorageKey.userToken, data.user.token);
await queryClient.invalidateQueries(getGetUserQueryKey());
push(PathBuilder.buildHome().getPath());
},
},
});

const handleChangeEmail = (_: React.ChangeEvent<HTMLInputElement>, email: string) => {
setEmail(email);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use client';
import { getGetUserQueryKey } from '@/entities/user/api/useGetUser';
import { PathBuilder } from '@/shared/utils/routes';
import webStorage, { StorageKey } from '@/shared/utils/webStorage';
import { Button, ButtonProps } from '@packages/ui';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/navigation';
import React, { ReactNode } from 'react';

interface LogoutButtonProps extends Partial<ButtonProps> {
children: ReactNode;
}

const LogoutButton = ({
children,
onClick,
color = 'error',
size = 'm',
variant = 'outlined',
...rest
}: LogoutButtonProps) => {
const queryClient = useQueryClient();
const { push } = useRouter();

const logout = async () => {
webStorage.removeItem(StorageKey.userToken);
await queryClient.invalidateQueries(getGetUserQueryKey());
};
const movePath = () => {
const homePath = PathBuilder.buildHome().getPath();
push(homePath);
};

return (
<Button
{...rest}
variant={variant}
color={color}
size={size}
onClick={async e => {
await logout();
movePath();
onClick?.(e);
}}
>
{children}
</Button>
);
};

export default LogoutButton;
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import defaultAxios, { AxiosRequestConfig } from 'axios';
import { authInterceptor } from './requestInterceptors/authInterceptor';

const axios = defaultAxios.create({
baseURL: 'https://api.realworld.io/api/',
Expand All @@ -8,6 +9,8 @@ const axios = defaultAxios.create({
},
});

axios.interceptors.request.use(authInterceptor);

export const axiosInstance = async <T>(config: AxiosRequestConfig): Promise<T> => {
config.data;
const data = await axios(config);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import webStorage, { StorageKey } from '@/shared/utils/webStorage';
import { InternalAxiosRequestConfig } from 'axios';

export const authInterceptor: (value: InternalAxiosRequestConfig) => InternalAxiosRequestConfig = config => {
const token = webStorage.getItem(StorageKey.userToken);

if (token) {
config.headers.Authorization = `Token ${token}`;
}

return config;
};
46 changes: 46 additions & 0 deletions apps/realworld/src/shared/utils/webStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { match } from 'ts-pattern';

export enum StorageKey {
userToken = 'u',
}

StorageKey.userToken === 'u';

class WebStorage {
private localStorage;
constructor() {
if (typeof window !== 'undefined') {
this.localStorage = window.localStorage;
}
}

setItem(key: StorageKey, value: any) {
if (!this.localStorage) {
return;
}
this.localStorage.setItem(key, JSON.stringify(value));
}

getItem(key: StorageKey.userToken): string | null;
getItem(key: StorageKey) {
if (!this.localStorage) {
return null;
}
const stringifyItem = this.localStorage.getItem(key);

return match({ stringifyItem, key })
.with({ stringifyItem: null }, () => null)
.with({ key: StorageKey.userToken }, () => JSON.parse(stringifyItem!))
.exhaustive();
}
removeItem(key: StorageKey) {
if (!this.localStorage) {
return;
}
this.localStorage.removeItem(key);
}
}

const webStorage = new WebStorage();

export default webStorage;
49 changes: 39 additions & 10 deletions apps/realworld/src/widgets/gnb/ui/gnb/gnb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ import Link from 'next/link';
import { usePathname } from 'next/navigation';
import React from 'react';
import { isActiveLink } from '../../api/isActiveLink';
import useGetUser from '@/entities/user/api/useGetUser';
import { Avatar } from '@packages/ui';

const GNB = () => {
const pathname = usePathname();
const { isLogin, user } = useGetUser();

const activeLinkStyle = (path: RoutePath) => {
return isActiveLink(pathname, path) ? 'text-black' : 'text-black/30';
};
Expand All @@ -25,16 +29,41 @@ const GNB = () => {
<p className={`hover:text-black ${activeLinkStyle('/')}`}>Home</p>
</Link>
</li>
<li>
<Link data-testid="gnbSignIn" href={PathBuilder.buildLogin().getPath()} className="no-underline">
<p className={`hover:text-black ${activeLinkStyle('/login')}`}>Sign in</p>
</Link>
</li>
<li>
<Link data-testid="gnbSignUp" href={PathBuilder.buildRegister().getPath()} className="no-underline">
<p className={`hover:text-black ${activeLinkStyle('/register')}`}>Sign up</p>
</Link>
</li>
{isLogin ? (
<>
<li>
<Link data-testid="gnbEditor" href={PathBuilder.buildNewArticle().getPath()} className="no-underline">
<p className={`hover:text-black ${activeLinkStyle('/editor')}`}>New Article</p>
</Link>
</li>
<li>
<Link data-testid="gnbSettings" href={PathBuilder.buildSettings().getPath()} className="no-underline">
<p className={`hover:text-black ${activeLinkStyle('/settings')}`}>Settings</p>
</Link>
</li>
<li>
<Link data-testid="gnbUser" href={PathBuilder.buildUser().getPath()} className="no-underline">
<p className={`flex items-center gap-4 hover:text-black ${activeLinkStyle('/user')}`}>
<Avatar size="xxs" src={user.image} />
{user.username}
</p>
</Link>
</li>
</>
) : (
<>
<li>
<Link data-testid="gnbSignIn" href={PathBuilder.buildLogin().getPath()} className="no-underline">
<p className={`hover:text-black ${activeLinkStyle('/login')}`}>Sign in</p>
</Link>
</li>
<li>
<Link data-testid="gnbSignUp" href={PathBuilder.buildRegister().getPath()} className="no-underline">
<p className={`hover:text-black ${activeLinkStyle('/register')}`}>Sign up</p>
</Link>
</li>
</>
)}
</ul>
</div>
</nav>
Expand Down

0 comments on commit c50fcc0

Please sign in to comment.