From 0a60faa76c4219698b674b1bce45309f665c80c0 Mon Sep 17 00:00:00 2001 From: JeongwooHam Date: Wed, 27 Mar 2024 16:36:57 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20react-cookie=EB=A5=BC=20=20?= =?UTF-8?q?=ED=99=9C=EC=9A=A9=ED=95=9C=20=EC=BF=A0=ED=82=A4=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 34 +++++++++++++- src/features/hooks/useAuthCookies.tsx | 49 +++++++++++++++++++++ src/features/utils/.gitkeep | 0 src/features/utils/cookies/cookies.tsx | 18 ++++++++ src/features/utils/cookies/cookies.types.ts | 10 +++++ src/main.tsx | 5 ++- src/shared/apis/@core.tsx | 31 +++++++++---- src/shared/constants/tokenKey.ts | 6 +++ 9 files changed, 143 insertions(+), 11 deletions(-) create mode 100644 src/features/hooks/useAuthCookies.tsx delete mode 100644 src/features/utils/.gitkeep create mode 100644 src/features/utils/cookies/cookies.tsx create mode 100644 src/features/utils/cookies/cookies.types.ts create mode 100644 src/shared/constants/tokenKey.ts diff --git a/package.json b/package.json index dc3dbc2..bd5f097 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "framer-motion": "^11.0.14", "react": "^18.2.0", "react-chartjs-2": "^5.2.0", + "react-cookie": "^7.1.0", "react-dom": "^18.2.0", "react-hook-form": "^7.51.1", "react-icons": "^5.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 33a2b2e..5ee8e8b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ dependencies: react-chartjs-2: specifier: ^5.2.0 version: 5.2.0(chart.js@4.4.2)(react@18.2.0) + react-cookie: + specifier: ^7.1.0 + version: 7.1.0(react@18.2.0) react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) @@ -4308,7 +4311,6 @@ packages: /@types/cookie@0.6.0: resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} - dev: true /@types/cross-spawn@6.0.6: resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==} @@ -4570,6 +4572,13 @@ packages: resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} dev: false + /@types/hoist-non-react-statics@3.3.5: + resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==} + dependencies: + '@types/react': 18.2.66 + hoist-non-react-statics: 3.3.2 + dev: false + /@types/http-errors@2.0.4: resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} dev: true @@ -5864,6 +5873,11 @@ packages: engines: {node: '>= 0.6'} dev: true + /cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + dev: false + /copy-to-clipboard@3.3.3: resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} dependencies: @@ -9474,6 +9488,17 @@ packages: tween-functions: 1.2.0 dev: true + /react-cookie@7.1.0(react@18.2.0): + resolution: {integrity: sha512-n2+Gt07/xxuShXary+SImk1sw5l7a1UguQOQEN55YewEW5LoA0opbR4nbeo8sY6OYwR37iCFJtqJ0AGEywqAtg==} + peerDependencies: + react: '>= 16.3.0' + dependencies: + '@types/hoist-non-react-statics': 3.3.5 + hoist-non-react-statics: 3.3.2 + react: 18.2.0 + universal-cookie: 7.1.0 + dev: false + /react-docgen-typescript@2.2.2(typescript@5.4.3): resolution: {integrity: sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==} peerDependencies: @@ -10723,6 +10748,13 @@ packages: unist-util-visit-parents: 6.0.1 dev: true + /universal-cookie@7.1.0: + resolution: {integrity: sha512-LCLHwP0whxTqkBYMptW1dzNS0xxIVJmU6c51N5CfPNheVxuJW7fVxPa6MUGX7boUSyOlpMveBO96hMs5Gee6Fg==} + dependencies: + '@types/cookie': 0.6.0 + cookie: 0.6.0 + dev: false + /universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} diff --git a/src/features/hooks/useAuthCookies.tsx b/src/features/hooks/useAuthCookies.tsx new file mode 100644 index 0000000..4271f5d --- /dev/null +++ b/src/features/hooks/useAuthCookies.tsx @@ -0,0 +1,49 @@ +import tokenKey from '@/shared/constants/tokenKey' + +import { getCookie, removeCookie, setCookie } from '../utils/cookies/cookies' + +const useAuthCookies = () => { + const getAccessToken = getCookie(tokenKey.ACCESS_TOKEN) + + const getAuthCookies = async () => { + try { + const [accessToken, refreshToken] = await Promise.all([ + getCookie(tokenKey.ACCESS_TOKEN), + getCookie(tokenKey.REFRESH_TOKEN), + ]) + + return { + accessToken: accessToken?.value, + refreshToken: refreshToken?.value, + } + } catch (err) { + return { accessToken: undefined, refreshToken: undefined } + } + } + + const setAuthCookies = async ( + accessToken: string, + refreshToken: string, + expires: Date, + ) => + await Promise.all([ + setCookie(tokenKey.ACCESS_TOKEN, accessToken, { + path: '/', + expires, + httpOnly: true, + }), + setCookie(tokenKey.REFRESH_TOKEN, refreshToken, { + maxAge: 60 * 60 * 24 * 20, + httpOnly: true, + }), + ]) + + const removeAuthCookies = () => { + removeCookie(tokenKey.ACCESS_TOKEN) + removeCookie(tokenKey.REFRESH_TOKEN, { maxAge: 60 * 60 * 24 * 20 }) + } + + return { getAccessToken, getAuthCookies, setAuthCookies, removeAuthCookies } +} + +export default useAuthCookies diff --git a/src/features/utils/.gitkeep b/src/features/utils/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/features/utils/cookies/cookies.tsx b/src/features/utils/cookies/cookies.tsx new file mode 100644 index 0000000..15c74c8 --- /dev/null +++ b/src/features/utils/cookies/cookies.tsx @@ -0,0 +1,18 @@ +import { Cookies } from 'react-cookie' + +import { CookieSetOptions } from './cookies.types' + +const cookies = new Cookies() + +export const setCookie = ( + name: string, + value: unknown, + options?: CookieSetOptions | undefined, +) => cookies.set(name, value, { ...options }) + +export const getCookie = (name: string) => cookies.get(name) + +export const removeCookie = ( + name: string, + options?: CookieSetOptions | undefined, +) => cookies.remove(name, { path: '/', httpOnly: true, ...options }) diff --git a/src/features/utils/cookies/cookies.types.ts b/src/features/utils/cookies/cookies.types.ts new file mode 100644 index 0000000..4ed4b9f --- /dev/null +++ b/src/features/utils/cookies/cookies.types.ts @@ -0,0 +1,10 @@ +export type CookieSetOptions = { + path?: string + expires?: Date + maxAge?: number + domain?: string + secure?: boolean + httpOnly?: boolean + sameSite?: boolean | 'none' | 'lax' | 'strict' + partitioned?: boolean +} diff --git a/src/main.tsx b/src/main.tsx index bef1bb4..5b91705 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,12 +1,15 @@ import './index.css' import React from 'react' +import { CookiesProvider } from 'react-cookie' import ReactDOM from 'react-dom/client' import App from './App.tsx' ReactDOM.createRoot(document.getElementById('root')!).render( - + + + , ) diff --git a/src/shared/apis/@core.tsx b/src/shared/apis/@core.tsx index 3cafe58..98aed94 100644 --- a/src/shared/apis/@core.tsx +++ b/src/shared/apis/@core.tsx @@ -1,13 +1,26 @@ import axios from 'axios' -const axiosInstance = axios.create({ - baseURL: import.meta.env.VITE_SERVER, - withCredentials: true, -}) - -axiosInstance.interceptors.request.use(config => { - // 토큰 관련 로직 구현 - return config -}) +import useAuthCookies from '@/features/hooks/useAuthCookies' + +export const createAPIInstance = () => { + const axiosInstance = axios.create({ + baseURL: import.meta.env.VITE_SERVER, + withCredentials: true, + }) + + axiosInstance.interceptors.request.use(config => { + const { getAccessToken } = useAuthCookies() + // 토큰 관련 로직 구현 + const accessToken = getAccessToken() + + if (accessToken) + config.headers.Authorization = `Bearer ${accessToken.value}` + return config + }) + + return axiosInstance +} + +const axiosInstance = createAPIInstance() export default axiosInstance diff --git a/src/shared/constants/tokenKey.ts b/src/shared/constants/tokenKey.ts new file mode 100644 index 0000000..539dd1b --- /dev/null +++ b/src/shared/constants/tokenKey.ts @@ -0,0 +1,6 @@ +const tokenKey = { + ACCESS_TOKEN: 'accessToken', + REFRESH_TOKEN: 'refreshToken', +} + +export default tokenKey From 6b0635aa33d0bdefeb2146c54b6031a9ddb3af0a Mon Sep 17 00:00:00 2001 From: JeongwooHam Date: Wed, 27 Mar 2024 18:25:02 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=83=80=EC=9E=85=20=EB=B0=8F=20service?= =?UTF-8?q?=20=ED=95=A8=EC=88=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/interfaces/.gitkeep | 0 src/entities/interfaces/baseResponses.ts | 13 ++++++ src/entities/interfaces/dto/auth/login.dto.ts | 42 +++++++++++++++++++ src/entities/interfaces/form.ts | 4 ++ src/entities/interfaces/user.ts | 1 + src/pages/sign-in/Page.tsx | 10 ++--- src/shared/apis/auth.ts | 22 ++++++++++ src/shared/constants/routeMap.ts | 13 ++++++ 8 files changed, 98 insertions(+), 7 deletions(-) delete mode 100644 src/entities/interfaces/.gitkeep create mode 100644 src/entities/interfaces/baseResponses.ts create mode 100644 src/entities/interfaces/dto/auth/login.dto.ts create mode 100644 src/entities/interfaces/form.ts create mode 100644 src/entities/interfaces/user.ts create mode 100644 src/shared/apis/auth.ts create mode 100644 src/shared/constants/routeMap.ts diff --git a/src/entities/interfaces/.gitkeep b/src/entities/interfaces/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/entities/interfaces/baseResponses.ts b/src/entities/interfaces/baseResponses.ts new file mode 100644 index 0000000..f273e63 --- /dev/null +++ b/src/entities/interfaces/baseResponses.ts @@ -0,0 +1,13 @@ +/** + * @ BaseResponse + * : 기본 API 응답 + * - code: 응답 코드 + * - msg: 응답 message + * - data: 응답 data + */ + +export type BaseResponse = { + code: number + msg: string + data?: T +} diff --git a/src/entities/interfaces/dto/auth/login.dto.ts b/src/entities/interfaces/dto/auth/login.dto.ts new file mode 100644 index 0000000..217c6ca --- /dev/null +++ b/src/entities/interfaces/dto/auth/login.dto.ts @@ -0,0 +1,42 @@ +import { BaseResponse } from '../../baseResponses' +import { LoginFormType } from '../../form' +import { UserRole } from '../../user' + +/** + * @ LoginRequest + * - LoginFormType: { email: string, password: string } + */ +export interface LoginRequest extends LoginFormType {} + +/** + * @ LoginUserData + * - member_id: 사용자 id (number) + * - nickname: 사용자 닉네임 (string) + * - experience: 사용자 경험치 (number) + * - introduction: 사용자 소개글 (string) + * - image_url: 사용자 프로필 사진 URL (string) + * - level: 사용자 레벨 정보 (number) + * - roles: 사용자 역할 (사용자, 멘토, 관리자) + */ +export type LoginUserData = { + member_id: number + nickname: string + experience: number + introduction: string + image_url: string + level: number + roles: UserRole[] +} + +/** + * @ LoginTokenData + * - accesstoken (string) + * - refreshtoken (string) + */ +export type LoginTokenData = { + token_dto: { access_token: string; refresh_token: string } +} + +export type LoginPayload = LoginUserData & LoginTokenData + +export interface LoginResponse extends BaseResponse {} diff --git a/src/entities/interfaces/form.ts b/src/entities/interfaces/form.ts new file mode 100644 index 0000000..c76abb2 --- /dev/null +++ b/src/entities/interfaces/form.ts @@ -0,0 +1,4 @@ +export type LoginFormType = { + email: string + password: string +} diff --git a/src/entities/interfaces/user.ts b/src/entities/interfaces/user.ts new file mode 100644 index 0000000..a0275e3 --- /dev/null +++ b/src/entities/interfaces/user.ts @@ -0,0 +1 @@ +export type UserRole = 'ROLE_USER' | 'ROLE_MENTOR' | 'ROLE_ADMIN' diff --git a/src/pages/sign-in/Page.tsx b/src/pages/sign-in/Page.tsx index f87cb98..e33eb15 100644 --- a/src/pages/sign-in/Page.tsx +++ b/src/pages/sign-in/Page.tsx @@ -10,20 +10,16 @@ import { zodResolver } from '@hookform/resolvers/zod' import { Controller, SubmitHandler, useForm } from 'react-hook-form' import { BORDERRADIUS, FONTSIZE, FONTWEIGHT, PALETTE } from '@/app/styles/theme' +import type { LoginFormType } from '@/entities/interfaces/form' import schema from './constants/schema' -type SignInProps = { - email: string - password: string -} - function SignInPage() { const { handleSubmit, control, formState: { errors, isSubmitting }, - } = useForm({ + } = useForm({ defaultValues: { email: '', password: '', @@ -31,7 +27,7 @@ function SignInPage() { resolver: zodResolver(schema), }) - const onSubmit: SubmitHandler = data => console.log(data) + const onSubmit: SubmitHandler = data => console.log(data) return (
{ + const res = await axiosInstance.post( + routeMap.authAPI.login, + { email, password }, + ) + + return res +} + +export const logout = async () => { + const res = await axiosInstance.post(routeMap.authAPI.logout, {}) + + return res +} diff --git a/src/shared/constants/routeMap.ts b/src/shared/constants/routeMap.ts new file mode 100644 index 0000000..71442cf --- /dev/null +++ b/src/shared/constants/routeMap.ts @@ -0,0 +1,13 @@ +const baseURL = '/api/v1' +const prefix = { + auth: 'auth', +} + +const authAPI = { + login: `${baseURL}/${prefix.auth}/login`, + logout: `${baseURL}/${prefix.auth}/logout`, +} + +const routeMap = { authAPI } + +export default routeMap From 1c85580f6296c217f9a842f3414a899f98ed932b Mon Sep 17 00:00:00 2001 From: JeongwooHam Date: Thu, 28 Mar 2024 00:02:00 +0900 Subject: [PATCH 3/7] =?UTF-8?q?fix:=20button=20props=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/widgets/Button/Button.stories.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/widgets/Button/Button.stories.ts b/src/widgets/Button/Button.stories.ts index 0adc050..f8b5ac8 100644 --- a/src/widgets/Button/Button.stories.ts +++ b/src/widgets/Button/Button.stories.ts @@ -28,26 +28,26 @@ type Story = StoryObj export const Primary: Story = { args: { primary: true, - label: 'Button', + children: 'Button', }, } export const Secondary: Story = { args: { - label: 'Button', + children: 'Button', }, } export const Large: Story = { args: { size: 'large', - label: 'Button', + children: 'Button', }, } export const Small: Story = { args: { size: 'small', - label: 'Button', + children: 'Button', }, } From 8f6d513da9a4a33c34bef512b7fa91f8e8f5aa6f Mon Sep 17 00:00:00 2001 From: JeongwooHam Date: Thu, 28 Mar 2024 15:18:26 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20respo?= =?UTF-8?q?nse=20cookie=20=EC=A0=80=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 7 +++ .../mocks/handler/auth.ts} | 0 src/features/hooks/useAuthCookies.tsx | 5 ++ src/features/hooks/useCrypto.tsx | 56 +++++++++++++++++++ src/pages/sign-in/Page.tsx | 44 ++++++++++++++- src/shared/constants/tokenKey.ts | 1 + 7 files changed, 112 insertions(+), 2 deletions(-) rename src/{features/hooks/.gitkeep => entities/mocks/handler/auth.ts} (100%) create mode 100644 src/features/hooks/useCrypto.tsx diff --git a/package.json b/package.json index bd5f097..2207b8d 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "axios": "^1.6.8", "chart.js": "^4.4.2", "d3": "^7.9.0", + "dayjs": "^1.11.10", "framer-motion": "^11.0.14", "react": "^18.2.0", "react-chartjs-2": "^5.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ee8e8b..b93b034 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ dependencies: d3: specifier: ^7.9.0 version: 7.9.0 + dayjs: + specifier: ^1.11.10 + version: 1.11.10 framer-motion: specifier: ^11.0.14 version: 11.0.14(react-dom@18.2.0)(react@18.2.0) @@ -6417,6 +6420,10 @@ packages: is-data-view: 1.0.1 dev: true + /dayjs@1.11.10: + resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} + dev: false + /debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: diff --git a/src/features/hooks/.gitkeep b/src/entities/mocks/handler/auth.ts similarity index 100% rename from src/features/hooks/.gitkeep rename to src/entities/mocks/handler/auth.ts diff --git a/src/features/hooks/useAuthCookies.tsx b/src/features/hooks/useAuthCookies.tsx index 4271f5d..e4be54c 100644 --- a/src/features/hooks/useAuthCookies.tsx +++ b/src/features/hooks/useAuthCookies.tsx @@ -24,6 +24,7 @@ const useAuthCookies = () => { const setAuthCookies = async ( accessToken: string, refreshToken: string, + encryptedData: string, expires: Date, ) => await Promise.all([ @@ -36,6 +37,10 @@ const useAuthCookies = () => { maxAge: 60 * 60 * 24 * 20, httpOnly: true, }), + setCookie(tokenKey.USER_DATA, encryptedData, { + path: '/', + expires, + }), ]) const removeAuthCookies = () => { diff --git a/src/features/hooks/useCrypto.tsx b/src/features/hooks/useCrypto.tsx new file mode 100644 index 0000000..5063410 --- /dev/null +++ b/src/features/hooks/useCrypto.tsx @@ -0,0 +1,56 @@ +const CRYPTO_KEY_NAME = import.meta.env.VITE_CRYPTO_KEY_NAME +const CRYPTO_KEY_LENGTH = import.meta.env.VITE_CRYPTO_KEY_LENGTH + +const useCrypto = () => { + const encrypt = async (data: string) => { + const encoder = new TextEncoder() + const dataBuffer = encoder.encode(data) + + const key = await crypto.subtle.generateKey( + { name: CRYPTO_KEY_NAME, length: Number(CRYPTO_KEY_LENGTH) }, + true, + ['encrypt'], + ) + + const encryptedBuffer = await crypto.subtle.encrypt( + { name: CRYPTO_KEY_NAME, iv: crypto.getRandomValues(new Uint8Array(12)) }, + key, + dataBuffer, + ) + + const encryptedArray = Array.from(new Uint8Array(encryptedBuffer)) + + return btoa(String.fromCharCode.apply(null, encryptedArray)) + } + + const decrypt = async (data: string) => { + const encryptedData = data + + // 암호화된 데이터가 없을 경우 + if (!encryptedData) return + + const encryptedBuffer = Uint8Array.from(atob(encryptedData), c => + c.charCodeAt(0), + ) + + const key = await crypto.subtle.generateKey( + { name: CRYPTO_KEY_NAME, length: Number(CRYPTO_KEY_LENGTH) }, + true, + ['decrypt'], + ) + const decryptedBuffer = await crypto.subtle.decrypt( + { name: CRYPTO_KEY_NAME, iv: crypto.getRandomValues(new Uint8Array(12)) }, + key, + encryptedBuffer, + ) + + const decoder = new TextDecoder() + const decryptedData = decoder.decode(decryptedBuffer) + + return JSON.parse(decryptedData) + } + + return { encrypt, decrypt } +} + +export default useCrypto diff --git a/src/pages/sign-in/Page.tsx b/src/pages/sign-in/Page.tsx index e33eb15..990bdf7 100644 --- a/src/pages/sign-in/Page.tsx +++ b/src/pages/sign-in/Page.tsx @@ -7,10 +7,15 @@ import { } from '@chakra-ui/react' import { css } from '@emotion/react' import { zodResolver } from '@hookform/resolvers/zod' +import dayjs from 'dayjs' import { Controller, SubmitHandler, useForm } from 'react-hook-form' +import { useNavigate } from 'react-router-dom' import { BORDERRADIUS, FONTSIZE, FONTWEIGHT, PALETTE } from '@/app/styles/theme' import type { LoginFormType } from '@/entities/interfaces/form' +import useAuthCookies from '@/features/hooks/useAuthCookies' +import useCrypto from '@/features/hooks/useCrypto' +import { login } from '@/shared/apis/auth' import schema from './constants/schema' @@ -18,7 +23,7 @@ function SignInPage() { const { handleSubmit, control, - formState: { errors, isSubmitting }, + formState: { isSubmitSuccessful, errors, isSubmitting }, } = useForm({ defaultValues: { email: '', @@ -26,8 +31,43 @@ function SignInPage() { }, resolver: zodResolver(schema), }) + const { setAuthCookies } = useAuthCookies() + const { encrypt } = useCrypto() + const navigate = useNavigate() - const onSubmit: SubmitHandler = data => console.log(data) + const onSubmit: SubmitHandler = async data => { + try { + const loginResponse = await login({ + email: data.email, + password: data.password, + }) + + if (loginResponse.data) { + const { token_dto, ...userPayload } = loginResponse.data + const { access_token, refresh_token } = token_dto + const expireTime = dayjs().add(1, 'hours').startOf('second').toDate() + + const stringifiedPayload = JSON.stringify({ + ...userPayload, + expires: expireTime.toJSON(), + }) + const encryptedPayload = await encrypt(stringifiedPayload) + + await setAuthCookies( + access_token, + refresh_token, + encryptedPayload, + expireTime, + ) + + navigate('/') + } else { + throw new Error('로그인 중 에러가 발생하였습니다.') + } + } catch (error) { + throw new Error('로그인 중 에러가 발생하였습니다.') + } + } return (
Date: Thu, 28 Mar 2024 15:18:54 +0900 Subject: [PATCH 5/7] =?UTF-8?q?chore:=20server=20route=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/sign-in/Page.tsx | 2 +- src/shared/apis/@core.tsx | 11 +++++++---- src/shared/apis/auth.ts | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/pages/sign-in/Page.tsx b/src/pages/sign-in/Page.tsx index 990bdf7..7d9aad7 100644 --- a/src/pages/sign-in/Page.tsx +++ b/src/pages/sign-in/Page.tsx @@ -23,7 +23,7 @@ function SignInPage() { const { handleSubmit, control, - formState: { isSubmitSuccessful, errors, isSubmitting }, + formState: { errors, isSubmitting }, } = useForm({ defaultValues: { email: '', diff --git a/src/shared/apis/@core.tsx b/src/shared/apis/@core.tsx index 98aed94..b767fb3 100644 --- a/src/shared/apis/@core.tsx +++ b/src/shared/apis/@core.tsx @@ -2,9 +2,12 @@ import axios from 'axios' import useAuthCookies from '@/features/hooks/useAuthCookies' -export const createAPIInstance = () => { +const BASIC_SERVER = import.meta.env.VITE_SERVER +const ADMIN_SERVER = import.meta.env.VITE_ADMIN_SERVER + +export const createAPIInstance = (isAdmin: boolean) => { const axiosInstance = axios.create({ - baseURL: import.meta.env.VITE_SERVER, + baseURL: isAdmin ? ADMIN_SERVER : BASIC_SERVER, withCredentials: true, }) @@ -21,6 +24,6 @@ export const createAPIInstance = () => { return axiosInstance } -const axiosInstance = createAPIInstance() +export const axiosInstance = createAPIInstance(false) -export default axiosInstance +export const axiosAdminInstance = createAPIInstance(true) diff --git a/src/shared/apis/auth.ts b/src/shared/apis/auth.ts index dce8ee1..fc0ecc4 100644 --- a/src/shared/apis/auth.ts +++ b/src/shared/apis/auth.ts @@ -4,7 +4,7 @@ import { } from '@/entities/interfaces/dto/auth/login.dto' import routeMap from '../constants/routeMap' -import axiosInstance from './@core' +import { axiosInstance } from './@core' export const login = async ({ email, password }: LoginRequest) => { const res = await axiosInstance.post( From 64668692cab9ec33cb0312aba71e8244f24ca9e4 Mon Sep 17 00:00:00 2001 From: JeongwooHam Date: Thu, 28 Mar 2024 18:09:08 +0900 Subject: [PATCH 6/7] =?UTF-8?q?feat:=20Private=20Route=EB=A5=BC=20?= =?UTF-8?q?=ED=86=B5=ED=95=9C=20=EC=A0=91=EA=B7=BC=20=EC=A0=9C=EC=96=B4=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/routes/PrivateRoute.tsx | 11 +++- src/features/hooks/auth/useAuthCookies.tsx | 57 +++++++++++++++++++ src/features/hooks/{ => auth}/useCrypto.tsx | 0 src/features/hooks/store/useUserDataStore.tsx | 27 +++++++++ src/features/hooks/useAuthCookies.tsx | 54 ------------------ src/pages/sign-in/Page.tsx | 30 ++++------ src/shared/apis/@core.tsx | 13 ++--- src/shared/apis/auth.ts | 13 +++-- src/shared/constants/tokenKey.ts | 1 - vite.config.ts | 3 + 10 files changed, 120 insertions(+), 89 deletions(-) create mode 100644 src/features/hooks/auth/useAuthCookies.tsx rename src/features/hooks/{ => auth}/useCrypto.tsx (100%) create mode 100644 src/features/hooks/store/useUserDataStore.tsx delete mode 100644 src/features/hooks/useAuthCookies.tsx diff --git a/src/app/routes/PrivateRoute.tsx b/src/app/routes/PrivateRoute.tsx index 19346e7..5a76e35 100644 --- a/src/app/routes/PrivateRoute.tsx +++ b/src/app/routes/PrivateRoute.tsx @@ -1,12 +1,17 @@ import { Navigate } from 'react-router-dom' +import useUserDataStore from '@/features/hooks/store/useUserDataStore' import GlobalLayout from '@/shared/layout/GlobalLayout' const PrivateRoute = () => { - // 로그인 로직 구현 후 수정 예정 - const isAuthorized = true + const { userData } = useUserDataStore() + const userRole = userData?.roles - return isAuthorized ? : + return userRole?.includes('ROLE_ADMIN') ? ( + + ) : ( + + ) } export default PrivateRoute diff --git a/src/features/hooks/auth/useAuthCookies.tsx b/src/features/hooks/auth/useAuthCookies.tsx new file mode 100644 index 0000000..8ec88fb --- /dev/null +++ b/src/features/hooks/auth/useAuthCookies.tsx @@ -0,0 +1,57 @@ +import tokenKey from '@/shared/constants/tokenKey' + +import { getCookie, removeCookie, setCookie } from '../../utils/cookies/cookies' + +const useAuthCookies = () => { + const getAccessToken = () => getCookie(tokenKey.ACCESS_TOKEN) + + const getAuthCookies = () => { + try { + const [accessToken, refreshToken] = [ + getCookie(tokenKey.ACCESS_TOKEN), + getCookie(tokenKey.REFRESH_TOKEN), + ] + + return { + accessToken: accessToken?.value, + refreshToken: refreshToken?.value, + } + } catch (err) { + return { + accessToken: undefined, + refreshToken: undefined, + } + } + } + + const setAuthCookies = ( + accessToken: string, + refreshToken: string, + expires: string, + ) => [ + setCookie(tokenKey.ACCESS_TOKEN, accessToken, { + path: '/', + expires: new Date(expires), + // httpOnly: true, + }), + setCookie(tokenKey.REFRESH_TOKEN, refreshToken, { + path: '/', + maxAge: 60 * 60 * 24 * 20, + // httpOnly: true, + }), + ] + + const removeAuthCookies = () => { + removeCookie(tokenKey.ACCESS_TOKEN) + removeCookie(tokenKey.REFRESH_TOKEN, { maxAge: 60 * 60 * 24 * 20 }) + } + + return { + getAccessToken, + getAuthCookies, + setAuthCookies, + removeAuthCookies, + } +} + +export default useAuthCookies diff --git a/src/features/hooks/useCrypto.tsx b/src/features/hooks/auth/useCrypto.tsx similarity index 100% rename from src/features/hooks/useCrypto.tsx rename to src/features/hooks/auth/useCrypto.tsx diff --git a/src/features/hooks/store/useUserDataStore.tsx b/src/features/hooks/store/useUserDataStore.tsx new file mode 100644 index 0000000..b6dbda0 --- /dev/null +++ b/src/features/hooks/store/useUserDataStore.tsx @@ -0,0 +1,27 @@ +import { create } from 'zustand' +import { createJSONStorage, persist } from 'zustand/middleware' + +import { LoginUserData } from '@/entities/interfaces/dto/auth/login.dto' + +type UserDataStoreType = { + userData: LoginUserData | undefined + // eslint-disable-next-line no-unused-vars + setUserData: (data: LoginUserData) => void + clearUserData: () => void +} + +const useUserDataStore = create()( + persist( + set => ({ + userData: undefined, + setUserData: (data: LoginUserData) => set({ userData: data }), + clearUserData: () => set({ userData: undefined }), + }), + { + name: 'user-data-storage', + storage: createJSONStorage(() => sessionStorage), + }, + ), +) + +export default useUserDataStore diff --git a/src/features/hooks/useAuthCookies.tsx b/src/features/hooks/useAuthCookies.tsx deleted file mode 100644 index e4be54c..0000000 --- a/src/features/hooks/useAuthCookies.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import tokenKey from '@/shared/constants/tokenKey' - -import { getCookie, removeCookie, setCookie } from '../utils/cookies/cookies' - -const useAuthCookies = () => { - const getAccessToken = getCookie(tokenKey.ACCESS_TOKEN) - - const getAuthCookies = async () => { - try { - const [accessToken, refreshToken] = await Promise.all([ - getCookie(tokenKey.ACCESS_TOKEN), - getCookie(tokenKey.REFRESH_TOKEN), - ]) - - return { - accessToken: accessToken?.value, - refreshToken: refreshToken?.value, - } - } catch (err) { - return { accessToken: undefined, refreshToken: undefined } - } - } - - const setAuthCookies = async ( - accessToken: string, - refreshToken: string, - encryptedData: string, - expires: Date, - ) => - await Promise.all([ - setCookie(tokenKey.ACCESS_TOKEN, accessToken, { - path: '/', - expires, - httpOnly: true, - }), - setCookie(tokenKey.REFRESH_TOKEN, refreshToken, { - maxAge: 60 * 60 * 24 * 20, - httpOnly: true, - }), - setCookie(tokenKey.USER_DATA, encryptedData, { - path: '/', - expires, - }), - ]) - - const removeAuthCookies = () => { - removeCookie(tokenKey.ACCESS_TOKEN) - removeCookie(tokenKey.REFRESH_TOKEN, { maxAge: 60 * 60 * 24 * 20 }) - } - - return { getAccessToken, getAuthCookies, setAuthCookies, removeAuthCookies } -} - -export default useAuthCookies diff --git a/src/pages/sign-in/Page.tsx b/src/pages/sign-in/Page.tsx index 7d9aad7..18bf532 100644 --- a/src/pages/sign-in/Page.tsx +++ b/src/pages/sign-in/Page.tsx @@ -13,8 +13,8 @@ import { useNavigate } from 'react-router-dom' import { BORDERRADIUS, FONTSIZE, FONTWEIGHT, PALETTE } from '@/app/styles/theme' import type { LoginFormType } from '@/entities/interfaces/form' -import useAuthCookies from '@/features/hooks/useAuthCookies' -import useCrypto from '@/features/hooks/useCrypto' +import useAuthCookies from '@/features/hooks/auth/useAuthCookies' +import useUserDataStore from '@/features/hooks/store/useUserDataStore' import { login } from '@/shared/apis/auth' import schema from './constants/schema' @@ -32,7 +32,7 @@ function SignInPage() { resolver: zodResolver(schema), }) const { setAuthCookies } = useAuthCookies() - const { encrypt } = useCrypto() + const { setUserData } = useUserDataStore() const navigate = useNavigate() const onSubmit: SubmitHandler = async data => { @@ -42,29 +42,23 @@ function SignInPage() { password: data.password, }) - if (loginResponse.data) { - const { token_dto, ...userPayload } = loginResponse.data + if (loginResponse.data.data) { + const { token_dto, ...userPayload } = loginResponse.data.data const { access_token, refresh_token } = token_dto - const expireTime = dayjs().add(1, 'hours').startOf('second').toDate() + const expireTime = dayjs() + .add(1, 'hours') + .startOf('second') + .toISOString() - const stringifiedPayload = JSON.stringify({ - ...userPayload, - expires: expireTime.toJSON(), - }) - const encryptedPayload = await encrypt(stringifiedPayload) - - await setAuthCookies( - access_token, - refresh_token, - encryptedPayload, - expireTime, - ) + setAuthCookies(access_token, refresh_token, expireTime) + setUserData(userPayload) navigate('/') } else { throw new Error('로그인 중 에러가 발생하였습니다.') } } catch (error) { + console.error(error) throw new Error('로그인 중 에러가 발생하였습니다.') } } diff --git a/src/shared/apis/@core.tsx b/src/shared/apis/@core.tsx index b767fb3..da0f0fd 100644 --- a/src/shared/apis/@core.tsx +++ b/src/shared/apis/@core.tsx @@ -1,13 +1,10 @@ import axios from 'axios' -import useAuthCookies from '@/features/hooks/useAuthCookies' +import useAuthCookies from '@/features/hooks/auth/useAuthCookies' -const BASIC_SERVER = import.meta.env.VITE_SERVER -const ADMIN_SERVER = import.meta.env.VITE_ADMIN_SERVER - -export const createAPIInstance = (isAdmin: boolean) => { +export const createAPIInstance = () => { const axiosInstance = axios.create({ - baseURL: isAdmin ? ADMIN_SERVER : BASIC_SERVER, + baseURL: import.meta.env.VITE_SERVER, withCredentials: true, }) @@ -24,6 +21,6 @@ export const createAPIInstance = (isAdmin: boolean) => { return axiosInstance } -export const axiosInstance = createAPIInstance(false) +const axiosInstance = createAPIInstance() -export const axiosAdminInstance = createAPIInstance(true) +export default axiosInstance diff --git a/src/shared/apis/auth.ts b/src/shared/apis/auth.ts index fc0ecc4..00f3736 100644 --- a/src/shared/apis/auth.ts +++ b/src/shared/apis/auth.ts @@ -1,16 +1,19 @@ +import { AxiosResponse } from 'axios' + import { LoginRequest, LoginResponse, } from '@/entities/interfaces/dto/auth/login.dto' import routeMap from '../constants/routeMap' -import { axiosInstance } from './@core' +import axiosInstance from './@core' export const login = async ({ email, password }: LoginRequest) => { - const res = await axiosInstance.post( - routeMap.authAPI.login, - { email, password }, - ) + const res = await axiosInstance.post< + unknown, + AxiosResponse, + LoginRequest + >(routeMap.authAPI.login, { email, password }) return res } diff --git a/src/shared/constants/tokenKey.ts b/src/shared/constants/tokenKey.ts index 31db1e5..539dd1b 100644 --- a/src/shared/constants/tokenKey.ts +++ b/src/shared/constants/tokenKey.ts @@ -1,7 +1,6 @@ const tokenKey = { ACCESS_TOKEN: 'accessToken', REFRESH_TOKEN: 'refreshToken', - USER_DATA: 'userData', } export default tokenKey diff --git a/vite.config.ts b/vite.config.ts index 3150dc9..420b752 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -21,4 +21,7 @@ export default defineConfig({ }, ], }, + server: { + port: 3000, + }, }) From de6a787b43d9250930edd4882261fef252299e1b Mon Sep 17 00:00:00 2001 From: JeongwooHam Date: Thu, 28 Mar 2024 19:00:53 +0900 Subject: [PATCH 7/7] =?UTF-8?q?feat:=20github=20actions=EB=A5=BC=20?= =?UTF-8?q?=ED=86=B5=ED=95=9C=20cicd=20=ED=8C=8C=EC=9D=B4=ED=94=84?= =?UTF-8?q?=EB=9D=BC=EC=9D=B8=20=EA=B5=AC=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/admin-cicd.yaml | 53 +++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/admin-cicd.yaml diff --git a/.github/workflows/admin-cicd.yaml b/.github/workflows/admin-cicd.yaml new file mode 100644 index 0000000..73f59aa --- /dev/null +++ b/.github/workflows/admin-cicd.yaml @@ -0,0 +1,53 @@ +name: Admin-CICD + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Install pnpm + run: npm install pnpm -g + + - name: Install dependencies + run: pnpm install + + - name: build + env: + VITE_API_MOCKING: ${{secrets.VITE_API_MOCKING}} + VITE_SERVER: ${{secrets.VITE_SERVER}} + VITE_CRYPTO_KEY_NAME: ${{secrets.VITE_CRYPTO_KEY_NAME}} + VITE_CRYPTO_KEY_LENGTH: ${{secrets.VITE_CRYPTO_KEY_LENGTH}} + run: pnpm run build + + deploy: + runs-on: ubuntu-latest + needs: build + steps: + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_S3_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_S3_SECRET_ACCESS_KEY_ID }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Upload to S3 + env: + BUCKET_NAME: ${{ secrets.AWS_S3_BUCKET_NAME}} + run: | + aws s3 sync \ + ./build s3://$BUCKET_NAME + + - name: Invalidate CloudFront Cache + uses: chetan/invalidate-cloudfront-action@master + env: + AWS_DISTRIBUTION: ${{ secrets.AWS_DISTRIBUTION_ID }} + PATHS: '/index.html' + continue-on-error: true