From 05bc6664043e6c36f61895510f1dd2a9232fe8eb Mon Sep 17 00:00:00 2001 From: leendrew Date: Fri, 19 Aug 2022 17:12:48 +0600 Subject: [PATCH 1/5] feat: auth --- next.config.js | 19 ++++ package.json | 2 +- src/app/auth/auth.api.ts | 33 +++--- src/app/auth/auth.slice.ts | 34 +++++- src/app/auth/auth.types.ts | 17 ++- src/app/constants.ts | 1 + src/app/store.ts | 2 +- src/components/Code/Code.tsx | 43 ++++++++ src/components/Code/index.tsx | 1 + src/components/Header.tsx | 23 ++-- src/components/Nav.tsx | 37 +++++++ src/components/UI/LoadingButton.tsx | 38 +++++++ src/contexts/Root.context.tsx | 11 ++ .../auth/RecoveryPassword.context.tsx | 46 ++++++++ src/contexts/auth/Register.context.tsx | 47 ++++++++ src/global.scss | 10 +- src/hooks/useTimer.ts | 68 ++++++++++++ src/pages/_app.tsx | 12 ++- src/pages/auth/login.tsx | 87 +++++++++++++++ src/pages/auth/recoveryPassword/code.tsx | 74 +++++++++++++ src/pages/auth/recoveryPassword/index.tsx | 75 +++++++++++++ src/pages/auth/recoveryPassword/new.tsx | 81 ++++++++++++++ src/pages/auth/register/code.tsx | 84 +++++++++++++++ src/pages/auth/register/index.tsx | 101 ++++++++++++++++++ src/schemas/auth/login.schema.ts | 9 ++ src/schemas/auth/recoveryPassword.schema.ts | 24 +++++ src/schemas/auth/register.schema.ts | 21 ++++ src/schemas/shared.schema.ts | 20 ++++ 28 files changed, 976 insertions(+), 44 deletions(-) create mode 100644 src/components/Code/Code.tsx create mode 100644 src/components/Code/index.tsx create mode 100644 src/components/Nav.tsx create mode 100644 src/components/UI/LoadingButton.tsx create mode 100644 src/contexts/Root.context.tsx create mode 100644 src/contexts/auth/RecoveryPassword.context.tsx create mode 100644 src/contexts/auth/Register.context.tsx create mode 100644 src/hooks/useTimer.ts create mode 100644 src/pages/auth/login.tsx create mode 100644 src/pages/auth/recoveryPassword/code.tsx create mode 100644 src/pages/auth/recoveryPassword/index.tsx create mode 100644 src/pages/auth/recoveryPassword/new.tsx create mode 100644 src/pages/auth/register/code.tsx create mode 100644 src/pages/auth/register/index.tsx create mode 100644 src/schemas/auth/login.schema.ts create mode 100644 src/schemas/auth/recoveryPassword.schema.ts create mode 100644 src/schemas/auth/register.schema.ts create mode 100644 src/schemas/shared.schema.ts diff --git a/next.config.js b/next.config.js index 8f2715d..b420148 100644 --- a/next.config.js +++ b/next.config.js @@ -4,6 +4,25 @@ const nextConfig = { swcMinify: true, poweredByHeader: false, output: 'standalone', + async redirects() { + return [ + { + source: '/auth/register/code', + destination: '/auth/register', + permanent: true, + }, + { + source: '/auth/recoveryPassword/code', + destination: '/auth/recoveryPassword', + permanent: true, + }, + { + source: '/auth/recoveryPassword/new', + destination: '/auth/recoveryPassword', + permanent: true, + }, + ]; + }, }; module.exports = nextConfig; diff --git a/package.json b/package.json index 03113a1..edb6e3c 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "frontend", + "name": "ucrm-web-client", "version": "0.1.0", "private": true, "scripts": { diff --git a/src/app/auth/auth.api.ts b/src/app/auth/auth.api.ts index 2b74b32..d1f4782 100644 --- a/src/app/auth/auth.api.ts +++ b/src/app/auth/auth.api.ts @@ -1,25 +1,25 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; import { HYDRATE } from 'next-redux-wrapper'; -import { HOST_URL } from '../constants'; import { - GetVerificationCodePayload, + GetVerifyCodePayload, RegisterPayload, RegisterResponse, LoginPayload, LoginResponse, + GetRecoveryCodePayload, + RecoveryPasswordPayload, } from './auth.types'; -import { HTTP_METHODS as HTTP } from '../constants'; +import { HTTP_METHODS as HTTP, AUTH_API_URL } from '../constants'; export const authApi = createApi({ baseQuery: fetchBaseQuery({ - baseUrl: HOST_URL + '/users', + baseUrl: AUTH_API_URL, prepareHeaders: (headers) => { const token = window.localStorage.getItem('token') || ''; headers.set('Authorization', token); - return headers; }, - credentials: 'include', + credentials: 'same-origin', }), reducerPath: 'api/auth', extractRehydrationInfo(action, { reducerPath }) { @@ -28,23 +28,30 @@ export const authApi = createApi({ } }, endpoints: (builder) => ({ - getVerificationCode: builder.mutation({ - query: (data) => ({ url: '/verificationCode', method: HTTP.POST, body: data }), + getVerifyCode: builder.mutation({ + query: (data) => ({ url: '/sendVerifyCode', method: HTTP.POST, body: data }), }), - register: builder.mutation({ + register: builder.mutation({ query: (data) => ({ url: '/signUp', method: HTTP.POST, body: data }), }), - login: builder.mutation({ + login: builder.mutation({ query: (data) => ({ url: '/signIn', method: HTTP.POST, body: data }), }), + getRecoveryCode: builder.mutation({ + query: (data) => ({ url: '/sendRecoveryCode', method: HTTP.POST, body: data }), + }), + recoveryPassword: builder.mutation({ + query: (data) => ({ url: '/recoveryPassword', method: HTTP.POST, body: data }), + }), }), }); export const { - useGetVerificationCodeMutation, + useGetVerifyCodeMutation, useRegisterMutation, useLoginMutation, + useGetRecoveryCodeMutation, + useRecoveryPasswordMutation, + endpoints: { getVerifyCode, register, login, getRecoveryCode, recoveryPassword }, util: { getRunningOperationPromises }, } = authApi; - -export const { getVerificationCode, register, login } = authApi.endpoints; diff --git a/src/app/auth/auth.slice.ts b/src/app/auth/auth.slice.ts index b5c1529..41de82e 100644 --- a/src/app/auth/auth.slice.ts +++ b/src/app/auth/auth.slice.ts @@ -1,8 +1,8 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { AuthState } from './auth.types'; +import { login, register } from './auth.api'; const initialState: AuthState = { - isLogin: false, token: '', user: null, }; @@ -11,12 +11,36 @@ const authSlice = createSlice({ name: 'auth', initialState, reducers: { - logout: (state) => { + logout: () => { window.localStorage.removeItem('token'); - state.isLogin = false; - state.token = ''; + return initialState; }, }, + extraReducers: (builder) => { + builder + .addMatcher(register.matchFulfilled, (state, { payload: { token, user } }) => { + window.localStorage.setItem('token', token); + + state.token = token; + state.user = { + id: user.id, + email: user.email, + avatarUrl: user.avatar_url, + createdAt: user.created_at, + }; + }) + .addMatcher(login.matchFulfilled, (state, { payload: { token, user } }) => { + window.localStorage.setItem('token', token); + + state.token = token; + state.user = { + id: user.id, + email: user.email, + avatarUrl: user.avatar_url, + createdAt: user.created_at, + }; + }); + }, }); -export const { actions: authAction, reducer: authReducer } = authSlice; +export const { actions: authActions, reducer: authReducer } = authSlice; diff --git a/src/app/auth/auth.types.ts b/src/app/auth/auth.types.ts index 07918b7..d825af3 100644 --- a/src/app/auth/auth.types.ts +++ b/src/app/auth/auth.types.ts @@ -1,22 +1,26 @@ export type Email = string; export type Token = string; -export type GetVerificationCodePayload = { +type CodePayload = { email: Email; }; +export type GetVerifyCodePayload = CodePayload; + type AuthPayload = { email: Email; password: string; }; -export type RegisterPayload = AuthPayload & { - verificationCode: number; +type CodeDto = { + code: number; }; +export type RegisterPayload = AuthPayload & CodeDto; + export type UserDto = { id: string; - created_at: string; + created_at: Date; email: Email; avatar_url: string; }; @@ -38,7 +42,10 @@ export type LoginPayload = AuthPayload; export type LoginResponse = AuthResponse; export type AuthState = { - isLogin: boolean; token: Token; user: User | null; }; + +export type GetRecoveryCodePayload = CodePayload; + +export type RecoveryPasswordPayload = AuthPayload & CodeDto; diff --git a/src/app/constants.ts b/src/app/constants.ts index 9d0336c..2de61f3 100644 --- a/src/app/constants.ts +++ b/src/app/constants.ts @@ -1,4 +1,5 @@ export const HOST_URL = process.env.host || 'http://localhost:8081/api/v1'; +export const AUTH_API_URL = HOST_URL + '/users'; export enum HTTP_METHODS { GET = 'GET', diff --git a/src/app/store.ts b/src/app/store.ts index 1be4435..3968f05 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -14,7 +14,7 @@ const makeStore = () => }); type AppStore = ReturnType; -type RootState = ReturnType; +export type RootState = ReturnType; type AppDispatch = AppStore['dispatch']; export const useTypedDispatch = () => useDispatch(); diff --git a/src/components/Code/Code.tsx b/src/components/Code/Code.tsx new file mode 100644 index 0000000..2747c34 --- /dev/null +++ b/src/components/Code/Code.tsx @@ -0,0 +1,43 @@ +import { FC, PropsWithChildren, useEffect } from 'react'; +import { Box, TextField, Typography, TextFieldProps } from '@mui/material'; +import { useTimer } from '@/hooks/useTimer'; +import { LoadingButton } from '../UI/LoadingButton'; + +interface CodeProps { + onTimerRestart?: () => void; +} + +export const Code: FC> = ({ + children, + onTimerRestart = () => undefined, + ...rest +}) => { + const { mins, secs, start, restart, isOver } = useTimer({ expiredTime: 300 }); + + const restartTimer = () => { + onTimerRestart(); + restart(); + }; + + useEffect(() => { + start(); + }, [start]); + + return ( + <> + + {!isOver && ( + + + You may resend in {mins.toString().padStart(2, '0')}:{secs.toString().padStart(2, '0')} + + + )} + {isOver && ( + + Resend + + )} + + ); +}; diff --git a/src/components/Code/index.tsx b/src/components/Code/index.tsx new file mode 100644 index 0000000..dddc751 --- /dev/null +++ b/src/components/Code/index.tsx @@ -0,0 +1 @@ +export { Code } from './Code'; diff --git a/src/components/Header.tsx b/src/components/Header.tsx index db38584..c139da2 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,18 +1,21 @@ -import { AppBar, Toolbar, Typography, Button, IconButton } from '@mui/material'; -import MenuIcon from '@mui/icons-material/Menu'; +import NextLink from 'next/link'; +import { AppBar, Toolbar, Typography } from '@mui/material'; +import Nav from './Nav'; export default function Header() { return ( - - - - - UCRM - - - + + + UCRM + + +