diff --git a/next.config.js b/next.config.js index 8f2715d..0f4fd78 100644 --- a/next.config.js +++ b/next.config.js @@ -4,6 +4,20 @@ const nextConfig = { swcMinify: true, poweredByHeader: false, output: 'standalone', + async redirects() { + return [ + { + source: '/auth/register/:path*', + destination: '/auth/register', + permanent: true, + }, + { + source: '/auth/recoveryPassword/:path*', + 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..c1a3bd0 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, +import type { + 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') || ''; + const token = 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..75628c0 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 { createSlice } from '@reduxjs/toolkit'; +import { login, register } from './auth.api'; +import type { AuthState } from './auth.types'; const initialState: AuthState = { - isLogin: false, token: '', user: null, }; @@ -11,12 +11,41 @@ const authSlice = createSlice({ name: 'auth', initialState, reducers: { - logout: (state) => { - window.localStorage.removeItem('token'); - state.isLogin = false; - state.token = ''; + logout: () => { + localStorage.removeItem('token'); + return initialState; }, + takeTokenFromLocalStorage: (state) => { + if (!state.token) { + state.token = localStorage.getItem('token') || ''; + } + }, + }, + extraReducers: (builder) => { + builder + .addMatcher(register.matchFulfilled, (state, { payload: { token, user } }) => { + 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 } }) => { + 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..ed8e6dc 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -1,6 +1,7 @@ import { configureStore } from '@reduxjs/toolkit'; import { createWrapper } from 'next-redux-wrapper'; -import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; +import type { TypedUseSelectorHook } from 'react-redux'; import { authReducer } from './auth/auth.slice'; import { authApi } from './auth/auth.api'; @@ -14,7 +15,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/CodeInput/CodeInput.tsx b/src/components/CodeInput/CodeInput.tsx new file mode 100644 index 0000000..ecd112c --- /dev/null +++ b/src/components/CodeInput/CodeInput.tsx @@ -0,0 +1,44 @@ +import { FC, PropsWithChildren, useEffect } from 'react'; +import { Box, TextField, Typography } from '@mui/material'; +import type { TextFieldProps } from '@mui/material'; +import { useTimer } from '@/hooks/useTimer'; +import { LoadingButton } from '../UI/LoadingButton'; + +interface CodeInputProps { + onTimerRestart?: () => void; +} + +export const CodeInput: 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/CodeInput/index.tsx b/src/components/CodeInput/index.tsx new file mode 100644 index 0000000..c01a31a --- /dev/null +++ b/src/components/CodeInput/index.tsx @@ -0,0 +1 @@ +export { CodeInput } from './CodeInput'; diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index f221f5b..4219c68 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,6 +1,7 @@ +import type { FC } from 'react'; import { Box, Link, Typography } from '@mui/material'; -export default function Footer() { +export const Footer: FC = () => { return ( ); -} +}; diff --git a/src/components/Header.tsx b/src/components/Header.tsx index db38584..2b70ed7 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,19 +1,23 @@ -import { AppBar, Toolbar, Typography, Button, IconButton } from '@mui/material'; -import MenuIcon from '@mui/icons-material/Menu'; +import type { FC } from 'react'; +import NextLink from 'next/link'; +import { AppBar, Toolbar, Typography } from '@mui/material'; +import { Nav } from './Nav'; -export default function Header() { +export const Header: FC = () => { return ( - - - - - UCRM - - - + + + UCRM + + +