From 96d96734f95ab8258c0ff6f9688d7dc8be0972a7 Mon Sep 17 00:00:00 2001 From: ash Date: Fri, 21 Jun 2024 23:11:37 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20>=20=EB=B9=84?= =?UTF-8?q?=EB=B0=80=EB=B2=88=ED=98=B8=20=EC=9E=AC=EC=84=A4=EC=A0=95=20API?= =?UTF-8?q?=20=EC=97=B0=EB=8F=99=20(#239)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ #234 - URL query로부터 데이터 획득 * ✨ #234 - 비밀번호 재설정 API_PATH 추가 * ✨ #234 - 비밀번호 재설정 API 추가 * ✨ #234 - 비밀번호 재설정 API 연결 * ✨#234 - queryParam을 항상 string[]로 반환하는 util 함수 추가 * 🎨:#234 - getQueryParam 통해 query param 가져오도록 변경 * 🏷️:#234 - TempTokenValidation response, request 타입 추가 * ✨:#234 - tempTokenValidation api path 추가 * ✨:#234 - tempTokenValidation api 추가 * ✨:#234 - 비밀번호 재설정 페이지 접근 전 email과 token이 매칭되지 확인 * ♻️#234 - api import 방식 변경 * 💡#234 - 주석 추가 * 🎨#234 - 네이밍 및 구조 개선 * 🔧#234 - eslint @typescript-eslint/strict-boolean-expressions rule 추가 * 🏷️#234 - InferGetServerSidePropsType 적용 * 🏷️#234 axios 응답 타입 적용 --- .eslintrc.json | 6 +++ src/api/users.ts | 35 +++++++++++++++- src/components/account/FindPasswordForm.tsx | 4 +- src/components/account/ResetPasswordForm.tsx | 31 ++++++++++---- src/constants/services/path.ts | 2 + src/pages/account/resetPassword.tsx | 43 ++++++++++++++++++-- src/types/password.ts | 18 ++++++++ src/utils/index.ts | 1 + src/utils/query.ts | 16 ++++++++ 9 files changed, 143 insertions(+), 13 deletions(-) create mode 100644 src/utils/query.ts diff --git a/.eslintrc.json b/.eslintrc.json index 964e3be1..93188065 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -80,6 +80,12 @@ ], "alphabetize": { "order": "asc" } } + ], + "@typescript-eslint/strict-boolean-expressions": [ + "error", + { + "allowNullableString": true + } ] }, "settings": { diff --git a/src/api/users.ts b/src/api/users.ts index 8006fe70..30ce3f3f 100644 --- a/src/api/users.ts +++ b/src/api/users.ts @@ -1,5 +1,10 @@ import type { LoginRequest, LoginResponse } from 'types/login'; -import type { PasswordResetLinkRequest } from 'types/password'; +import type { + PasswordResetLinkRequest, + PasswordResetRequest, + TempTokenValidationRequest, + TempTokenValidationResponse, +} from 'types/password'; import type { ExistsRequest, RegisterRequest } from 'types/register'; import type { OnlyMessageResponse, SuccessResponse } from 'types/response'; import { API_PATH } from 'constants/services'; @@ -58,3 +63,31 @@ export const passwordResetLink = async ({ { email, redirectUrl }, ); }; + +export const resetPassword = async ({ + email, + tempToken, + password, +}: PasswordResetRequest) => { + return await axios.put>( + API_PATH.users.password, + { + email, + tempToken, + password, + }, + ); +}; + +export const tempTokenValidation = async ({ + email, + tempToken, +}: TempTokenValidationRequest) => { + return await axios.post>( + API_PATH.users.tempTokenValidation, + { + email, + tempToken, + }, + ); +}; diff --git a/src/components/account/FindPasswordForm.tsx b/src/components/account/FindPasswordForm.tsx index 60f4ea09..c110c9a4 100644 --- a/src/components/account/FindPasswordForm.tsx +++ b/src/components/account/FindPasswordForm.tsx @@ -5,7 +5,7 @@ import type { Dispatch, SetStateAction } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import type { PasswordFindForm } from 'types/password'; import type { ErrorResponse } from 'types/response'; -import { passwordResetLink } from 'api'; +import * as api from 'api'; import { Button } from 'components/common'; import { FormInput } from 'components/form'; import { ERROR_MESSAGE, VALID_VALUE } from 'constants/validation'; @@ -25,7 +25,7 @@ export const FindPasswordForm = ({ setIsSubmitted }: FindPasswordFormProps) => { const onSubmit: SubmitHandler = async (data) => { try { const { email } = data; - await passwordResetLink({ + await api.passwordResetLink({ email, redirectUrl: `${window.location.origin}/account/resetPassword`, }); diff --git a/src/components/account/ResetPasswordForm.tsx b/src/components/account/ResetPasswordForm.tsx index afc2799b..3c025dd8 100644 --- a/src/components/account/ResetPasswordForm.tsx +++ b/src/components/account/ResetPasswordForm.tsx @@ -1,7 +1,11 @@ import styled from '@emotion/styled'; +import { isAxiosError } from 'axios'; import router from 'next/router'; import { useForm } from 'react-hook-form'; +import type { SubmitHandler } from 'react-hook-form'; import type { PasswordResetForm } from 'types/password'; +import type { ErrorResponse } from 'types/response'; +import * as api from 'api'; import { Button } from 'components/common'; import { FormInput } from 'components/form'; import { PAGE_PATH } from 'constants/common'; @@ -11,7 +15,12 @@ import { VALID_VALUE, } from 'constants/validation'; -export const ResetPasswordForm = () => { +interface ResetPasswordFormProps { + email: string; + token: string; +} + +export const ResetPasswordForm = ({ email, token }: ResetPasswordFormProps) => { const { register, getValues, @@ -19,12 +28,20 @@ export const ResetPasswordForm = () => { formState: { isValid, errors, isSubmitting }, } = useForm({ mode: 'onChange' }); - const onSubmit = async () => { - /** - * @todo - * 비밀번호 재설정 API 요청 - */ - await router.replace(PAGE_PATH.account.login); + const onSubmit: SubmitHandler = async (data) => { + try { + const { password } = data; + await api.resetPassword({ email, tempToken: token, password }); + await router.replace(PAGE_PATH.account.login); + } catch (error) { + if (isAxiosError(error)) { + /** + * @todo + * 에러 처리 + */ + console.log(error.response?.status); + } + } }; return ( diff --git a/src/constants/services/path.ts b/src/constants/services/path.ts index aebc8713..2b3d3835 100644 --- a/src/constants/services/path.ts +++ b/src/constants/services/path.ts @@ -7,6 +7,8 @@ export const API_PATH = { register: '/users/register', login: '/users/login', passwordResetLink: '/users/password-reset-link', + password: '/users/password', + tempTokenValidation: '/users/temp-token-validation', }, diaries: { index: '/diaries', diff --git a/src/pages/account/resetPassword.tsx b/src/pages/account/resetPassword.tsx index 2770589e..e6f93eb6 100644 --- a/src/pages/account/resetPassword.tsx +++ b/src/pages/account/resetPassword.tsx @@ -1,19 +1,56 @@ import styled from '@emotion/styled'; -import type { NextPage } from 'next/types'; +import type { + GetServerSideProps, + InferGetServerSidePropsType, + NextPage, +} from 'next/types'; +import * as api from 'api'; import { ResetPasswordForm } from 'components/account'; import { Seo } from 'components/common'; +import { PAGE_PATH } from 'constants/common'; +import { getQueryParams } from 'utils'; -const ResetPassword: NextPage = () => { +const ResetPassword: NextPage< + InferGetServerSidePropsType +> = ({ email, token }) => { return ( <> - + ); }; +export const getServerSideProps = (async (context) => { + const { query } = context; + const [email] = getQueryParams(query.email); + const [token] = getQueryParams(query.token); + + const REDIRECT_LOGIN_PAGE_PROPS = { + redirect: { + destination: PAGE_PATH.account.login, + permanent: false, + }, + }; + + if (!email || !token) { + return REDIRECT_LOGIN_PAGE_PROPS; + } + + try { + await api.tempTokenValidation({ + email, + tempToken: token, + }); + + return { props: { email, token } }; + } catch (error) { + return REDIRECT_LOGIN_PAGE_PROPS; + } +}) satisfies GetServerSideProps; + export default ResetPassword; const ContentWrapper = styled.section` diff --git a/src/types/password.ts b/src/types/password.ts index e1ec89f9..0272d9e4 100644 --- a/src/types/password.ts +++ b/src/types/password.ts @@ -6,6 +6,24 @@ export interface PasswordResetLinkRequest { redirectUrl: string; } +export interface PasswordResetRequest { + email: string; + tempToken: string; + password: string; +} + +export interface TempTokenValidationRequest { + email: string; + tempToken: string; +} + +/** + * Response + */ +export interface TempTokenValidationResponse { + isValidate: boolean; +} + /** * Others */ diff --git a/src/utils/index.ts b/src/utils/index.ts index aed3a5f4..95d19b34 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,5 @@ export * from './date'; export * from './ErrorResponseMessage'; export * from './Formatter'; +export * from './query'; export { default as textareaAutosize } from './TextareaAutosize'; diff --git a/src/utils/query.ts b/src/utils/query.ts new file mode 100644 index 00000000..0e601036 --- /dev/null +++ b/src/utils/query.ts @@ -0,0 +1,16 @@ +/** + * 쿼리 파라미터를 받아 항상 문자열 배열로 반환합니다. + */ +export const getQueryParams = ( + param: string | string[] | undefined, +): string[] => { + if (Array.isArray(param)) { + return param; + } + + if (param === undefined || param === '') { + return []; + } + + return [param]; +};