Skip to content

Commit 12b6f99

Browse files
committed
feat(auth): add signin
1 parent 6bf729d commit 12b6f99

File tree

15 files changed

+290
-83
lines changed

15 files changed

+290
-83
lines changed

.eslintrc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@
3737
}
3838
],
3939
"quotes": "off",
40+
"no-param-reassign": [
41+
"error",
42+
{
43+
"props": true,
44+
"ignorePropertyModificationsFor": ["state"]
45+
}
46+
],
4047
"jsx-a11y/anchor-is-valid": [
4148
"error",
4249
{

src/app/backendApi.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
1+
/* eslint-disable import/no-cycle */
12
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
3+
import { RootState } from '@/app/store';
24

35
// initialize an empty api service that we'll inject endpoints into later as needed
46
export const backendApi = createApi({
5-
baseQuery: fetchBaseQuery({ baseUrl: `/api` }),
7+
baseQuery: fetchBaseQuery({
8+
baseUrl: `/api`,
9+
prepareHeaders: (headers, { getState }) => {
10+
// By default, if we have a token in the store, let's use that for authenticated requests
11+
const { token } = (getState() as RootState).auth;
12+
if (token) {
13+
headers.set(`authorization`, `Bearer ${token}`);
14+
}
15+
return headers;
16+
},
17+
}),
618
endpoints: () => ({}),
719
});

src/app/store.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
/* eslint-disable import/no-cycle */
12
import { Action, configureStore, ThunkAction } from '@reduxjs/toolkit';
23
import { backendApi } from '@/app/backendApi';
4+
import authReducer from '@/features/auth/slices/authSlice';
35

46
export const store = configureStore({
57
reducer: {
68
[backendApi.reducerPath]: backendApi.reducer,
9+
auth: authReducer,
710
},
811
middleware: (getDefaultMiddleware) =>
912
getDefaultMiddleware().concat(backendApi.middleware),

src/components/Form/FormButton.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { FC } from 'react';
2+
import { CheckIcon } from '@chakra-ui/icons';
3+
4+
import { Button } from '@chakra-ui/react';
5+
import { QueryStatus } from '@reduxjs/toolkit/dist/query';
6+
7+
interface Props {
8+
label: string;
9+
isLoading: boolean;
10+
status: QueryStatus;
11+
}
12+
13+
export const FormButton: FC<Props> = ({ label, status, isLoading }: Props) => (
14+
<Button
15+
leftIcon={status === `fulfilled` ? <CheckIcon /> : <></>}
16+
isLoading={isLoading}
17+
loadingText="Envoi"
18+
borderRadius={0}
19+
type="submit"
20+
variant="solid"
21+
bg={status === `fulfilled` ? `green.500` : `pink.500`}
22+
color="whiteAlpha.900"
23+
width="full"
24+
_focus={{
25+
transform: `scale(0.98)`,
26+
}}
27+
>
28+
{label}
29+
</Button>
30+
);
31+
32+
export default FormButton;

src/features/auth/api/signinApi.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { backendApi } from '@/app/backendApi';
2+
import { SessionPayload, AuthPayload } from '../types';
3+
4+
export const signinApi = backendApi.injectEndpoints({
5+
endpoints: (build) => ({
6+
signin: build.mutation<SessionPayload, AuthPayload>({
7+
query: ({ ...patch }) => ({
8+
url: `/auth/signin`,
9+
method: `POST`,
10+
body: patch,
11+
}),
12+
// Pick out data and prevent nested properties in a hook or selector
13+
transformResponse: (response: { data: SessionPayload }) => response.data,
14+
}),
15+
}),
16+
overrideExisting: false,
17+
});
18+
19+
export const { useSigninMutation } = signinApi;

src/features/auth/api/signupApi.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
import { backendApi } from '@/app/backendApi';
2-
import { AuthPayLoad, SignupPayLoad } from '../types';
2+
import { SessionPayload, AuthPayload } from '../types';
33

44
export const signupApi = backendApi.injectEndpoints({
55
endpoints: (build) => ({
6-
signup: build.mutation<AuthPayLoad, SignupPayLoad>({
7-
// note: an optional `queryFn` may be used in place of `query`
6+
signup: build.mutation<SessionPayload, AuthPayload>({
87
query: ({ ...patch }) => ({
98
url: `/auth/signup`,
109
method: `POST`,
1110
body: patch,
1211
}),
1312
// Pick out data and prevent nested properties in a hook or selector
14-
transformResponse: (response: { data: AuthPayLoad }) => response.data,
13+
transformResponse: (response: { data: SessionPayload }) => response.data,
1514
}),
1615
}),
1716
overrideExisting: false,

src/features/auth/backend/confirmationEmail.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@
133133
<tr>
134134
<td align="center" valign="top" style="padding: 36px 24px">
135135
<a
136-
href="[**WEBSITE_URL**]"
136+
href="https://developpeur-web.vercel.app"
137137
target="_blank"
138138
style="display: inline-block"
139139
>
@@ -260,7 +260,7 @@
260260
style="border-radius: 6px"
261261
>
262262
<a
263-
href="[**WEBSITE_URL**]/admin/auth/confirmation?token=[**TOKEN**]"
263+
href="https://developpeur-web.vercel.app/admin/auth/confirmation?token=[**TOKEN**]"
264264
target="_blank"
265265
style="
266266
display: inline-block;
@@ -302,8 +302,8 @@
302302
:
303303
</p>
304304
<p style="margin: 0">
305-
<a href="[**website_url**]" target="_blank"
306-
>[**website_url**]</a
305+
<a href="https://developpeur-web.vercel.app" target="_blank"
306+
>https://developpeur-web.vercel.app</a
307307
>
308308
</p>
309309
</td>

src/features/auth/backend/sendConfirmationMail.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,17 @@ export const sendConfirmationMail = async (email: string, token: string) => {
1515
) {
1616
mail.setApiKey(process.env.SENDGRID_API_KEY);
1717

18+
const htmlEmail = confirmationEmailTemplate
19+
.replace(`[**EMAIL**]`, email)
20+
.replace(`[**TOKEN**]`, token)
21+
.replace(`[**GITHUB_PROFILE**]`, process.env.GITHUB_URL);
22+
1823
await mail.send({
1924
to: email,
2025
from: process.env.MAIL_FROM,
2126
subject: `Confirmation d'inscription`,
2227
text: `Confirmation d'inscription`,
23-
html: confirmationEmailTemplate
24-
.replace(`[**EMAIL**]`, email)
25-
.replace(`[**TOKEN**]`, token)
26-
.replace(`[**WEBSITE_URL**]`, process.env.WEBSITE_URL)
27-
.replace(`[**GITHUB_PROFILE**]`, process.env.GITHUB_URL),
28+
html: htmlEmail,
2829
});
2930
}
3031
};

src/features/auth/backend/services/serviceSignup.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Joi from 'joi';
44
import Boom from '@hapi/boom';
55
import prisma from '@/lib/prisma';
66
import { sendConfirmationMail } from '@/features/auth/backend/sendConfirmationMail';
7-
import { AuthPayLoad, AuthToken, SignupPayLoad } from '@/features/auth/types';
7+
import { SessionPayload, AuthToken, AuthPayload } from '@/features/auth/types';
88

99
const schema = Joi.object({
1010
name: Joi.string().min(3).max(30),
@@ -16,7 +16,7 @@ export const signup = async ({
1616
name,
1717
email,
1818
password,
19-
}: SignupPayLoad): Promise<AuthPayLoad | Boom.Boom<unknown>> => {
19+
}: AuthPayload): Promise<SessionPayload | Boom.Boom<unknown>> => {
2020
if (!process.env.JWT_SECRET) {
2121
return Boom.badImplementation(
2222
`Variable d'environnement JWT_SECRET manquante.`,
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { FetchBaseQueryError } from '@reduxjs/toolkit/dist/query';
2+
import React from 'react';
3+
import { yupResolver } from '@hookform/resolvers/yup';
4+
import {
5+
FormControl,
6+
FormErrorMessage,
7+
Icon,
8+
Input,
9+
InputGroup,
10+
InputLeftElement,
11+
Stack,
12+
useColorModeValue,
13+
} from '@chakra-ui/react';
14+
import { FaUserAlt } from 'react-icons/fa';
15+
import { IoMdMail as IoMail } from 'react-icons/io';
16+
import { useForm, SubmitHandler, FormProvider } from 'react-hook-form';
17+
import * as yup from 'yup';
18+
import { FormErrors } from '@/components/Form';
19+
import { FormButton } from '@/components/Form/FormButton';
20+
import { AuthPayload } from '../types';
21+
import { useSignupMutation } from '../api/signupApi';
22+
import { PasswordInputs } from './PasswordsInputs';
23+
24+
const schema = yup.object().shape({
25+
name: yup
26+
.string()
27+
.min(3, `La longueur min est 3`)
28+
.max(30, `La longueur maximale est 30`)
29+
.required(),
30+
email: yup.string().email().required(),
31+
password: yup
32+
.string()
33+
.min(4, `La longueur min est 3`)
34+
.max(30, `La longueur maximale est 30`)
35+
.required(`Ce champ est obligatoire`),
36+
});
37+
38+
export const FormSignin = () => {
39+
const methods = useForm<AuthPayload>({
40+
resolver: yupResolver(schema),
41+
});
42+
const [
43+
signup, // This is the mutation trigger
44+
{ isLoading, error, status }, // This is the destructured mutation result
45+
] = useSignupMutation();
46+
47+
const bgFormColor = useColorModeValue(`gray.100`, `gray.700`);
48+
const iconColor = useColorModeValue(`gray.400`, `gray.300`);
49+
50+
const FormSubmitHandler: SubmitHandler<AuthPayload> = (data: AuthPayload) => {
51+
signup(data);
52+
};
53+
54+
return (
55+
<FormProvider {...methods}>
56+
<form onSubmit={methods.handleSubmit(FormSubmitHandler)}>
57+
<Stack
58+
spacing={4}
59+
p="2rem"
60+
backgroundColor={bgFormColor}
61+
boxShadow="md"
62+
borderRadius="base"
63+
>
64+
<FormControl isRequired isInvalid={!!methods.formState.errors?.name}>
65+
<InputGroup>
66+
<InputLeftElement pointerEvents="none">
67+
<Icon as={FaUserAlt} w={4} h={4} color={iconColor} />
68+
</InputLeftElement>
69+
<Input
70+
type="text"
71+
placeholder="Nom"
72+
variant="flushed"
73+
focusBorderColor="pink.400"
74+
{...methods.register(`name`)}
75+
/>
76+
<FormErrorMessage>
77+
{methods.formState.errors.email && `erreur email`}
78+
</FormErrorMessage>
79+
</InputGroup>
80+
</FormControl>
81+
82+
<FormControl isRequired isInvalid={!!methods.formState.errors?.email}>
83+
<InputGroup>
84+
<InputLeftElement pointerEvents="none">
85+
<Icon as={IoMail} w={5} h={5} color={iconColor} />
86+
</InputLeftElement>
87+
<Input
88+
type="email"
89+
placeholder="E-mail"
90+
variant="flushed"
91+
focusBorderColor="pink.400"
92+
{...methods.register(`email`)}
93+
/>
94+
<FormErrorMessage>
95+
{methods.formState.errors.email && `erreur email`}
96+
</FormErrorMessage>
97+
</InputGroup>
98+
</FormControl>
99+
100+
<PasswordInputs confirmation={false} iconColor={iconColor} />
101+
102+
<FormButton
103+
label="Inscription"
104+
isLoading={isLoading}
105+
status={status}
106+
/>
107+
{error && <FormErrors error={error as FetchBaseQueryError} />}
108+
</Stack>
109+
</form>
110+
</FormProvider>
111+
);
112+
};

0 commit comments

Comments
 (0)