diff --git a/.eslintrc.js b/.eslintrc.js index 0184e79..39e1de8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -57,5 +57,6 @@ module.exports = { __DEV__: true, __LOCAL__: false, __TEST__: false, + __SENDGRID_API_KEY__: false, }, } diff --git a/jest/config.js b/jest/config.js index 3ac0720..92f5a54 100644 --- a/jest/config.js +++ b/jest/config.js @@ -23,6 +23,7 @@ module.exports = { __SERVER__: false, __LOCAL__: false, __TEST__: true, + __SENDGRID_API_KEY__: "", localStorage: { getItem: () => null, setItem: () => null, removeItem: () => null }, }, maxConcurrency: 50, diff --git a/package.json b/package.json index 89001b6..869c895 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "@loadable/component": "^5.15.0", "@loadable/server": "^5.15.0", "@reduxjs/toolkit": "^1.6.0", + "@sendgrid/mail": "^7.6.0", "@types/lodash": "^4.14.177", "autoprefixer": "^10.2.6", "axios": "^0.21.1", diff --git a/src/components/ForgotForm/ForgotForm.tsx b/src/components/ForgotForm/ForgotForm.tsx new file mode 100644 index 0000000..6c5d3ef --- /dev/null +++ b/src/components/ForgotForm/ForgotForm.tsx @@ -0,0 +1,48 @@ +import React, { memo, useCallback } from "react" +import { Link } from "react-router-dom" +import { AppDispatch } from "../../store" +import { fetchVolunteerForgot } from "../../store/volunteerForgot" +import styles from "./styles.module.scss" + +interface Props { + dispatch: AppDispatch + error: string + message: string +} + +const ForgotForm = ({ dispatch, error, message }: Props): JSX.Element => { + const onSubmit = useCallback( + (event: React.SyntheticEvent): void => { + event.preventDefault() + const target = event.target as typeof event.target & { + email: { value: string } + } + const email = target.email.value + + dispatch(fetchVolunteerForgot({ email })) + }, + [dispatch] + ) + + return ( +
+
+ Nous allons te renvoyer un mot de passe à l'adresse suivante. +
+
+ + +
+
+ +
+
{error}
+
{message}
+
+ S'identifier +
+
+ ) +} + +export default memo(ForgotForm) diff --git a/src/components/ForgotForm/styles.module.scss b/src/components/ForgotForm/styles.module.scss new file mode 100755 index 0000000..20494de --- /dev/null +++ b/src/components/ForgotForm/styles.module.scss @@ -0,0 +1,43 @@ +@import "../../theme/variables"; +@import "../../theme/mixins"; + +.forgotIntro { + margin-bottom: 10px; +} + +.formLine { + padding: 5px 0; + + label { + display: block; + margin-left: 5px; + } + input { + width: 100%; + border: 1px solid #333; + border-radius: 4px; + } +} + +.formButtons { + margin-top: 10px; + padding: 5px 0; + text-align: center; +} + +.message { + margin-top: 10px; + color: rgb(11, 138, 0); + text-align: center; +} + +.error { + margin-top: 10px; + color: rgb(255, 0, 0); + text-align: center; +} + +.link { + margin-top: 20px; + text-align: center; +} diff --git a/src/components/LoginForm/LoginForm.tsx b/src/components/LoginForm/LoginForm.tsx index 46d6b0a..0b21ea5 100644 --- a/src/components/LoginForm/LoginForm.tsx +++ b/src/components/LoginForm/LoginForm.tsx @@ -1,7 +1,8 @@ import React, { memo, useCallback } from "react" -import styles from "./styles.module.scss" +import { Link } from "react-router-dom" import { AppDispatch } from "../../store" import { fetchVolunteerLogin } from "../../store/volunteerLogin" +import styles from "./styles.module.scss" interface Props { dispatch: AppDispatch @@ -41,6 +42,9 @@ const LoginForm = ({ dispatch, error }: Props): JSX.Element => {
{error}
+
+ Demander un nouveau mot de passe +
) } diff --git a/src/components/LoginForm/styles.module.scss b/src/components/LoginForm/styles.module.scss index ffcaf11..a145737 100755 --- a/src/components/LoginForm/styles.module.scss +++ b/src/components/LoginForm/styles.module.scss @@ -26,5 +26,12 @@ } .error { + margin-top: 10px; color: rgb(255, 0, 0); + text-align: center; +} + +.link { + margin-top: 20px; + text-align: center; } diff --git a/src/components/VolunteerInfo/__tests__/VolunteerInfo.tsx b/src/components/VolunteerInfo/__tests__/VolunteerInfo.tsx index 55db3e8..787d2dc 100755 --- a/src/components/VolunteerInfo/__tests__/VolunteerInfo.tsx +++ b/src/components/VolunteerInfo/__tests__/VolunteerInfo.tsx @@ -22,9 +22,9 @@ describe("", () => { adult: 1, privileges: 0, active: 0, - comment: "", - timestamp: new Date(0), - password: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O", + created: new Date(0), + password1: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O", + password2: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O", }} /> diff --git a/src/components/VolunteerList/__tests__/VolunteerList.tsx b/src/components/VolunteerList/__tests__/VolunteerList.tsx index a7c50da..4f05e07 100755 --- a/src/components/VolunteerList/__tests__/VolunteerList.tsx +++ b/src/components/VolunteerList/__tests__/VolunteerList.tsx @@ -23,9 +23,10 @@ describe("", () => { adult: 1, privileges: 0, active: 0, - comment: "", - timestamp: new Date(0), - password: + created: new Date(0), + password1: + "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O", + password2: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O", }, ]} diff --git a/src/components/VolunteerSet/__tests__/VolunteerSet.tsx b/src/components/VolunteerSet/__tests__/VolunteerSet.tsx index 3962b14..8d107bc 100644 --- a/src/components/VolunteerSet/__tests__/VolunteerSet.tsx +++ b/src/components/VolunteerSet/__tests__/VolunteerSet.tsx @@ -24,9 +24,9 @@ describe("", () => { adult: 1, privileges: 0, active: 0, - comment: "", - timestamp: new Date(0), - password: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O", + created: new Date(0), + password1: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O", + password2: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O", }} /> diff --git a/src/pages/Forgot/ForgotPage.tsx b/src/pages/Forgot/ForgotPage.tsx new file mode 100644 index 0000000..d1432c0 --- /dev/null +++ b/src/pages/Forgot/ForgotPage.tsx @@ -0,0 +1,34 @@ +import { RouteComponentProps } from "react-router-dom" +import { useDispatch, useSelector, shallowEqual } from "react-redux" +import React, { memo } from "react" +import { Helmet } from "react-helmet" + +import { AppState } from "../../store" +import ForgotForm from "../../components/ForgotForm/ForgotForm" +import styles from "./styles.module.scss" + +export type Props = RouteComponentProps + +const ForgotPage: React.FC = (): JSX.Element => { + const dispatch = useDispatch() + const forgotError = useSelector((state: AppState) => state.volunteerForgot.error, shallowEqual) + const forgotMessage = useSelector( + (state: AppState) => state.volunteerForgot.entity?.message, + shallowEqual + ) + + return ( +
+
+ + +
+
+ ) +} + +export default memo(ForgotPage) diff --git a/src/pages/Forgot/index.tsx b/src/pages/Forgot/index.tsx new file mode 100755 index 0000000..31205a9 --- /dev/null +++ b/src/pages/Forgot/index.tsx @@ -0,0 +1,14 @@ +import loadable from "@loadable/component" + +import { Loading, ErrorBoundary } from "../../components" +import { Props } from "./ForgotPage" + +const ForgotPage = loadable(() => import("./ForgotPage"), { + fallback: , +}) + +export default (props: Props): JSX.Element => ( + + + +) diff --git a/src/pages/Forgot/styles.module.scss b/src/pages/Forgot/styles.module.scss new file mode 100755 index 0000000..1583b45 --- /dev/null +++ b/src/pages/Forgot/styles.module.scss @@ -0,0 +1,9 @@ +@import "../../theme/mixins"; + +.forgotPage { + @include page-wrapper-center; +} + +.forgotContent { + @include page-content-wrapper; +} diff --git a/src/pages/Login/LoginPage.tsx b/src/pages/Login/LoginPage.tsx index 46b15c0..316bb0b 100644 --- a/src/pages/Login/LoginPage.tsx +++ b/src/pages/Login/LoginPage.tsx @@ -9,18 +9,18 @@ import styles from "./styles.module.scss" export type Props = RouteComponentProps -const RegisterPage: React.FC = (): JSX.Element => { +const LoginPage: React.FC = (): JSX.Element => { const dispatch = useDispatch() const loginError = useSelector((state: AppState) => state.volunteerLogin.error, shallowEqual) return (
- +
) } -export default memo(RegisterPage) +export default memo(LoginPage) diff --git a/src/routes/index.ts b/src/routes/index.ts index 10c9f7b..2cd6af8 100755 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -5,6 +5,7 @@ import AsyncHome, { loadData as loadHomeData } from "../pages/Home" import AsyncWish, { loadData as loadWishData } from "../pages/Wish" import AsyncVolunteerPage, { loadData as loadVolunteerPageData } from "../pages/VolunteerPage" import Login from "../pages/Login" +import Forgot from "../pages/Forgot" import Register from "../pages/Register" import NotFound from "../pages/NotFound" @@ -26,6 +27,10 @@ export default [ path: "/login", component: Login, }, + { + path: "/forgot", + component: Forgot, + }, { path: "/home", component: AsyncHome, diff --git a/src/server/gsheets/accessors.ts b/src/server/gsheets/accessors.ts index 8575e3b..b3bc02c 100644 --- a/src/server/gsheets/accessors.ts +++ b/src/server/gsheets/accessors.ts @@ -120,6 +120,7 @@ export class Sheet< if (!foundElement) { throw new Error(`No element found to be set in ${this.name} at id ${element.id}`) } + if (!_.isEqual(foundElement, element)) { Object.assign(foundElement, element) await this.setList(elements) diff --git a/src/server/gsheets/expressAccessors.ts b/src/server/gsheets/expressAccessors.ts index 7a64d57..8d53844 100644 --- a/src/server/gsheets/expressAccessors.ts +++ b/src/server/gsheets/expressAccessors.ts @@ -1,6 +1,9 @@ import { Request, Response, NextFunction } from "express" import { SheetNames, ElementWithId, getSheet, Sheet } from "./accessors" +export type RequestBody = Request["body"] +export type CustomSetReturn = { toDatabase: Element; toCaller: any } + export default class ExpressAccessors< // eslint-disable-next-line @typescript-eslint/ban-types ElementNoId extends object, @@ -27,23 +30,27 @@ export default class ExpressAccessors< if (elements) { response.status(200).json(elements) } - } catch (e: unknown) { - response.status(400).json(e) + } catch (e: any) { + response.status(200).json({ error: e.message }) } } } - get() { + // custom can be async + get(custom?: (list: Element[], body: Request["body"]) => Promise | any) { return async (request: Request, response: Response, _next: NextFunction): Promise => { try { - const id = parseInt(request.query.id as string, 10) || -1 - const elements = await this.sheet.getList() - if (elements) { - const element = elements.find((e: Element) => e.id === id) - response.status(200).json(element) + const list = (await this.sheet.getList()) || [] + let toCaller: any + if (!custom) { + const id = parseInt(request.query.id as string, 10) || -1 + toCaller = list.find((e: Element) => e.id === id) + } else { + toCaller = await custom(list, request.body) } - } catch (e: unknown) { - response.status(400).json(e) + response.status(200).json(toCaller) + } catch (e: any) { + response.status(200).json({ error: e.message }) } } } @@ -55,31 +62,36 @@ export default class ExpressAccessors< if (element) { response.status(200).json(element) } - } catch (e: unknown) { - response.status(400).json(e) + } catch (e: any) { + response.status(200).json({ error: e.message }) } } } - set() { - return async (request: Request, response: Response, _next: NextFunction): Promise => { - try { - await this.sheet.set(request.body) - response.status(200) - } catch (e: unknown) { - response.status(400).json(e) - } - } - } - - // transformer can be an async function - customGet( - transformer: (list: Element[] | undefined, body?: Request["body"]) => Promise | any + // custom can be async + set( + custom?: ( + list: Element[], + body: RequestBody + ) => Promise> | CustomSetReturn ) { return async (request: Request, response: Response, _next: NextFunction): Promise => { try { - const elements = await this.sheet.getList() - response.status(200).json(await transformer(elements, request.body)) + if (!custom) { + await this.sheet.set(request.body) + response.status(200) + } else { + const list = (await this.sheet.getList()) || [] + const { toDatabase, toCaller } = await custom(list, request.body) + if (toDatabase !== undefined) { + await this.sheet.set(toDatabase) + } + if (toCaller !== undefined) { + response.status(200).json(toCaller) + } else { + response.status(200) + } + } } catch (e: any) { response.status(200).json({ error: e.message }) } diff --git a/src/server/gsheets/javGames.ts b/src/server/gsheets/javGames.ts index fc02994..9c2f8b9 100644 --- a/src/server/gsheets/javGames.ts +++ b/src/server/gsheets/javGames.ts @@ -8,9 +8,6 @@ const expressAccessor = new ExpressAccessors( ) export const javGameListGet = expressAccessor.listGet() - export const javGameGet = expressAccessor.get() - export const javGameAdd = expressAccessor.add() - export const javGameSet = expressAccessor.set() diff --git a/src/server/gsheets/preVolunteers.ts b/src/server/gsheets/preVolunteers.ts index 036e7af..7bdf49b 100644 --- a/src/server/gsheets/preVolunteers.ts +++ b/src/server/gsheets/preVolunteers.ts @@ -12,13 +12,10 @@ const expressAccessor = new ExpressAccessors (list && list.length) || 0 ) diff --git a/src/server/gsheets/volunteers.ts b/src/server/gsheets/volunteers.ts index 3b567a4..83c45e7 100644 --- a/src/server/gsheets/volunteers.ts +++ b/src/server/gsheets/volunteers.ts @@ -1,7 +1,8 @@ -import { Request } from "express" +import _ from "lodash" import bcrypt from "bcrypt" +import sgMail from "@sendgrid/mail" -import ExpressAccessors from "./expressAccessors" +import ExpressAccessors, { RequestBody } from "./expressAccessors" import { Volunteer, VolunteerWithoutId, translationVolunteer } from "../../services/volunteers" import { canonicalEmail } from "../../utils/standardization" import { getJwt } from "../secure" @@ -13,38 +14,98 @@ const expressAccessor = new ExpressAccessors( ) export const volunteerListGet = expressAccessor.listGet() - export const volunteerGet = expressAccessor.get() - export const volunteerAdd = expressAccessor.add() - export const volunteerSet = expressAccessor.set() -export const volunteerLogin = expressAccessor.customGet( - async (list: Volunteer[] | undefined, body: Request["body"]) => { - if (!list) { - throw Error("Il n'y a aucun bénévole avec cet email") - } - const email = canonicalEmail(body.email || "") - const volunteer = list.find((v) => canonicalEmail(v.email) === email) - if (!volunteer) { - throw Error("Il n'y a aucun bénévole avec cet email") - } +export const volunteerLogin = expressAccessor.get(async (list: Volunteer[], body: RequestBody) => { + const volunteer = getByEmail(list, body.email) + if (!volunteer) { + throw Error("Il n'y a aucun bénévole avec cet email") + } - const password = body.password || "" - const passwordMatch = await bcrypt.compare( + const password = body.password || "" + const password1Match = await bcrypt.compare( + password, + volunteer.password1.replace(/^\$2y/, "$2a") + ) + if (!password1Match) { + const password2Match = await bcrypt.compare( password, - volunteer.password.replace(/^\$2y/, "$2a") + volunteer.password2.replace(/^\$2y/, "$2a") ) - if (!passwordMatch) { + if (!password2Match) { throw Error("Mauvais mot de passe pour cet email") } - - const jwt = await getJwt(email) - - return { - firstname: volunteer.firstname, - jwt, - } } -) + + const jwt = await getJwt(volunteer.email) + + return { + firstname: volunteer.firstname, + jwt, + } +}) + +const lastForgot: { [id: string]: number } = {} +export const volunteerForgot = expressAccessor.set(async (list: Volunteer[], body: RequestBody) => { + const volunteer = getByEmail(list, body.email) + if (!volunteer) { + throw Error("Il n'y a aucun bénévole avec cet email") + } + const newVolunteer = _.cloneDeep(volunteer) + + const now = +new Date() + const timeSinceLastSent = now - lastForgot[volunteer.id] + if (timeSinceLastSent < 2 * 60 * 1000) { + throw Error( + "Un email t'a déjà été envoyé avec un nouveau mot de passe. Es-tu sûr qu'il n'est pas dans tes spams ?" + ) + } + lastForgot[volunteer.id] = now + + const password = generatePassword() + const passwordHash = await bcrypt.hash(password, 10) + newVolunteer.password2 = passwordHash + + await sendForgetEmail(volunteer.email, password) + + return { + toDatabase: newVolunteer, + toCaller: { + message: `Un nouveau mot de passe t'a été envoyé par email. Regarde bien dans tes spams, ils pourrait y être :/`, + }, + } +}) + +function getByEmail(list: Volunteer[], rawEmail: string): Volunteer | undefined { + const email = canonicalEmail(rawEmail || "") + const volunteer = list.find((v) => canonicalEmail(v.email) === email) + return volunteer +} + +function generatePassword(): string { + const s = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + return Array(16) + .join() + .split(",") + .map(() => s.charAt(Math.floor(Math.random() * s.length))) + .join("") +} + +async function sendForgetEmail(email: string, password: string): Promise { + const apiKey = process.env.SENDGRID_API_KEY || "" + if (__DEV__ || apiKey === "") { + console.error(`Fake sending forget email to ${email} with password ${password}`) + } else { + sgMail.setApiKey(apiKey) + const msg = { + to: email, + from: "contact@parisestludique.fr", + subject: "Nouveau mot de passe pour le site de Paris est Ludique", + text: `Voici le nouveau mot de passe : ${password}\nL'ancien fonctionne encore, si tu t'en rappelles.`, + html: `Voici le nouveau mot de passe : ${password}
L'ancien fonctionne encore, si tu t'en rappelles.`, + } + await sgMail.send(msg) + } +} diff --git a/src/server/gsheets/wishes.ts b/src/server/gsheets/wishes.ts index 3787dcd..f9be22d 100644 --- a/src/server/gsheets/wishes.ts +++ b/src/server/gsheets/wishes.ts @@ -8,9 +8,6 @@ const expressAccessor = new ExpressAccessors( ) export const wishListGet = expressAccessor.listGet() - export const wishGet = expressAccessor.get() - export const wishAdd = expressAccessor.add() - export const wishSet = expressAccessor.set() diff --git a/src/server/index.ts b/src/server/index.ts index a21d181..bf22730 100755 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -19,7 +19,7 @@ import { secure } from "./secure" import { javGameListGet } from "./gsheets/javGames" import { wishListGet, wishAdd } from "./gsheets/wishes" import { preVolunteerAdd, preVolunteerCountGet } from "./gsheets/preVolunteers" -import { volunteerGet, volunteerSet, volunteerLogin } from "./gsheets/volunteers" +import { volunteerGet, volunteerSet, volunteerLogin, volunteerForgot } from "./gsheets/volunteers" import config from "../config" const app = express() @@ -56,6 +56,7 @@ app.post("/WishAdd", wishAdd) app.post("/PreVolunteerAdd", preVolunteerAdd) app.get("/PreVolunteerCountGet", preVolunteerCountGet) app.post("/VolunteerLogin", volunteerLogin) +app.post("/VolunteerForgot", volunteerForgot) // Secured APIs app.get("/VolunteerGet", secure as RequestHandler, volunteerGet) diff --git a/src/services/accessors.ts b/src/services/accessors.ts index 8b28597..75af8e8 100644 --- a/src/services/accessors.ts +++ b/src/services/accessors.ts @@ -7,12 +7,15 @@ export type ElementWithId = unknown & { id: number } export type ElementTranslation = { [k in keyof Element]: string } -export default function getServiceAccessors< +export default class ServiceAccessors< // eslint-disable-next-line @typescript-eslint/ban-types ElementNoId extends object, Element extends ElementNoId & ElementWithId ->(elementName: string): any { - function get(): (id: number) => Promise<{ +> { + // eslint-disable-next-line no-useless-constructor + constructor(readonly elementName: string) {} + + get(): (id: number) => Promise<{ data?: Element error?: Error }> { @@ -22,7 +25,7 @@ export default function getServiceAccessors< } return async (id: number): Promise => { try { - const { data } = await axios.get(`${config.API_URL}/${elementName}Get`, { + const { data } = await axios.get(`${config.API_URL}/${this.elementName}Get`, { ...axiosConfig, params: { id }, }) @@ -33,7 +36,7 @@ export default function getServiceAccessors< } } - function listGet(): () => Promise<{ + listGet(): () => Promise<{ data?: Element[] error?: Error }> { @@ -44,7 +47,7 @@ export default function getServiceAccessors< return async (): Promise => { try { const { data } = await axios.get( - `${config.API_URL}/${elementName}ListGet`, + `${config.API_URL}/${this.elementName}ListGet`, axiosConfig ) return { data } @@ -55,7 +58,7 @@ export default function getServiceAccessors< } // eslint-disable-next-line @typescript-eslint/ban-types - function add(): (volunteerWithoutId: ElementNoId) => Promise<{ + add(): (volunteerWithoutId: ElementNoId) => Promise<{ data?: Element error?: Error }> { @@ -66,7 +69,7 @@ export default function getServiceAccessors< return async (volunteerWithoutId: ElementNoId): Promise => { try { const { data } = await axios.post( - `${config.API_URL}/${elementName}Add`, + `${config.API_URL}/${this.elementName}Add`, volunteerWithoutId, axiosConfig ) @@ -77,7 +80,7 @@ export default function getServiceAccessors< } } - function set(): (volunteer: Element) => Promise<{ + set(): (volunteer: Element) => Promise<{ data?: Element error?: Error }> { @@ -88,7 +91,7 @@ export default function getServiceAccessors< return async (volunteer: Element): Promise => { try { const { data } = await axios.post( - `${config.API_URL}/${elementName}Set`, + `${config.API_URL}/${this.elementName}Set`, volunteer, axiosConfig ) @@ -99,7 +102,7 @@ export default function getServiceAccessors< } } - function countGet(): () => Promise<{ + countGet(): () => Promise<{ data?: number error?: Error }> { @@ -110,7 +113,7 @@ export default function getServiceAccessors< return async (): Promise => { try { const { data } = await axios.get( - `${config.API_URL}/${elementName}CountGet`, + `${config.API_URL}/${this.elementName}CountGet`, axiosConfig ) return { data } @@ -120,18 +123,18 @@ export default function getServiceAccessors< } } - function customPost(apiName: string): (params: any) => Promise<{ - data?: Element + customPost(apiName: string): (params: any) => Promise<{ + data?: any error?: Error }> { interface ElementGetResponse { - data?: Element + data?: any error?: Error } return async (params: any): Promise => { try { const { data } = await axios.post( - `${config.API_URL}/${elementName}${apiName}`, + `${config.API_URL}/${this.elementName}${apiName}`, params, axiosConfig ) @@ -144,6 +147,4 @@ export default function getServiceAccessors< } } } - - return { listGet, get, set, add, countGet, customPost } } diff --git a/src/services/javGames.ts b/src/services/javGames.ts index 7a8325e..e82f4e5 100644 --- a/src/services/javGames.ts +++ b/src/services/javGames.ts @@ -1,4 +1,4 @@ -import getServiceAccessors from "./accessors" +import ServiceAccessors from "./accessors" export class JavGame { id = 0 @@ -54,9 +54,9 @@ const elementName = "JavGame" export type JavGameWithoutId = Omit -const { listGet, get, set, add } = getServiceAccessors(elementName) +const serviceAccessors = new ServiceAccessors(elementName) -export const javGameListGet = listGet() -export const javGameGet = get() -export const javGameAdd = add() -export const javGameSet = set() +export const javGameListGet = serviceAccessors.listGet() +export const javGameGet = serviceAccessors.get() +export const javGameAdd = serviceAccessors.add() +export const javGameSet = serviceAccessors.set() diff --git a/src/services/preVolunteers.ts b/src/services/preVolunteers.ts index da0d7ac..4149b17 100644 --- a/src/services/preVolunteers.ts +++ b/src/services/preVolunteers.ts @@ -1,4 +1,4 @@ -import getServiceAccessors from "./accessors" +import ServiceAccessors from "./accessors" export class PreVolunteer { id = 0 @@ -34,13 +34,10 @@ export const passwordMinLength = 4 export type PreVolunteerWithoutId = Omit -const { listGet, get, set, add, countGet } = getServiceAccessors< - PreVolunteerWithoutId, - PreVolunteer ->(elementName) +const serviceAccessors = new ServiceAccessors(elementName) -export const preVolunteerListGet = listGet() -export const preVolunteerGet = get() -export const preVolunteerAdd = add() -export const preVolunteerSet = set() -export const preVolunteerCountGet = countGet() +export const preVolunteerListGet = serviceAccessors.listGet() +export const preVolunteerGet = serviceAccessors.get() +export const preVolunteerAdd = serviceAccessors.add() +export const preVolunteerSet = serviceAccessors.set() +export const preVolunteerCountGet = serviceAccessors.countGet() diff --git a/src/services/volunteers.ts b/src/services/volunteers.ts index d7513f6..f0f9153 100644 --- a/src/services/volunteers.ts +++ b/src/services/volunteers.ts @@ -1,4 +1,4 @@ -import getServiceAccessors from "./accessors" +import ServiceAccessors from "./accessors" export class Volunteer { id = 0 @@ -21,11 +21,11 @@ export class Volunteer { active = 0 - comment = "" + created = new Date() - timestamp = new Date() + password1 = "" - password = "" + password2 = "" } export const translationVolunteer: { [k in keyof Volunteer]: string } = { @@ -39,9 +39,9 @@ export const translationVolunteer: { [k in keyof Volunteer]: string } = { adult: "majeur", privileges: "privilege", active: "actif", - comment: "commentaire", - timestamp: "horodatage", - password: "passe", + created: "creation", + password1: "passe1", + password2: "passe2", } const elementName = "Volunteer" @@ -52,16 +52,20 @@ export const passwordMinLength = 4 export type VolunteerWithoutId = Omit -const accessors = getServiceAccessors(elementName) -const { listGet, get, set, add } = accessors +const serviceAccessors = new ServiceAccessors(elementName) -export const volunteerListGet = listGet() -export const volunteerGet = get() -export const volunteerAdd = add() -export const volunteerSet = set() +export const volunteerListGet = serviceAccessors.listGet() +export const volunteerGet = serviceAccessors.get() +export const volunteerAdd = serviceAccessors.add() +export const volunteerSet = serviceAccessors.set() export interface VolunteerLogin { firstname: string jwt: string } -export const volunteerLogin = accessors.customPost("Login") +export const volunteerLogin = serviceAccessors.customPost("Login") + +export interface VolunteerForgot { + message: string +} +export const volunteerForgot = serviceAccessors.customPost("Forgot") diff --git a/src/services/wishes.ts b/src/services/wishes.ts index e4265ce..1ec86a9 100644 --- a/src/services/wishes.ts +++ b/src/services/wishes.ts @@ -1,4 +1,4 @@ -import getServiceAccessors from "./accessors" +import ServiceAccessors from "./accessors" export class Wish { id = 0 @@ -27,9 +27,9 @@ const elementName = "Wish" export type WishWithoutId = Omit -const { listGet, get, set, add } = getServiceAccessors(elementName) +const serviceAccessors = new ServiceAccessors(elementName) -export const wishListGet = listGet() -export const wishGet = get() -export const wishAdd = add() -export const wishSet = set() +export const wishListGet = serviceAccessors.listGet() +export const wishGet = serviceAccessors.get() +export const wishAdd = serviceAccessors.add() +export const wishSet = serviceAccessors.set() diff --git a/src/store/__tests__/volunteer.ts b/src/store/__tests__/volunteer.ts index 6a35048..9492fcc 100644 --- a/src/store/__tests__/volunteer.ts +++ b/src/store/__tests__/volunteer.ts @@ -23,9 +23,9 @@ const mockData: Volunteer = { adult: 1, privileges: 0, active: 0, - comment: "", - timestamp: new Date(0), - password: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O", + created: new Date(0), + password1: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O", + password2: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O", } const { id } = mockData const mockError = "Oops! Something went wrong." diff --git a/src/store/__tests__/volunteerList.ts b/src/store/__tests__/volunteerList.ts index 27e954f..9d1a3fd 100644 --- a/src/store/__tests__/volunteerList.ts +++ b/src/store/__tests__/volunteerList.ts @@ -25,9 +25,9 @@ const mockData: Volunteer[] = [ adult: 1, privileges: 0, active: 0, - comment: "", - timestamp: new Date(0), - password: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O", + created: new Date(0), + password1: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O", + password2: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O", }, ] const mockError = "Oops! Something went wrong." diff --git a/src/store/rootReducer.ts b/src/store/rootReducer.ts index 3b613dc..587927b 100644 --- a/src/store/rootReducer.ts +++ b/src/store/rootReducer.ts @@ -9,6 +9,7 @@ import volunteerAdd from "./volunteerAdd" import volunteerList from "./volunteerList" import volunteerSet from "./volunteerSet" import volunteerLogin from "./volunteerLogin" +import volunteerForgot from "./volunteerForgot" import preVolunteerAdd from "./preVolunteerAdd" import preVolunteerCount from "./preVolunteerCount" @@ -23,6 +24,7 @@ export default (history: History) => ({ volunteerList, volunteerSet, volunteerLogin, + volunteerForgot, preVolunteerAdd, preVolunteerCount, router: connectRouter(history) as any, diff --git a/src/store/utils.ts b/src/store/utils.ts index 5729229..44659ad 100644 --- a/src/store/utils.ts +++ b/src/store/utils.ts @@ -51,14 +51,10 @@ export function elementFetch( if (error) { dispatch(getFailure(error.message)) - if (errorMessage) { - errorMessage(error) - } + errorMessage?.(error) } else { dispatch(getSuccess(data as Element)) - if (successMessage) { - successMessage(data as Element) - } + successMessage?.(data as Element) } } } @@ -71,12 +67,8 @@ export function elementAddFetch( getRequesting: ActionCreatorWithoutPayload, getSuccess: ActionCreatorWithPayload, getFailure: ActionCreatorWithPayload, - errorMessage: (error: Error) => void = (_error) => { - /* Meant to be empty */ - }, - successMessage: () => void = () => { - /* Meant to be empty */ - } + errorMessage?: (error: Error) => void, + successMessage?: () => void ): (volunteerWithoutId: Omit) => AppThunk { return (volunteerWithoutId: Omit): AppThunk => async (dispatch) => { @@ -86,10 +78,10 @@ export function elementAddFetch( if (error) { dispatch(getFailure(error.message)) - errorMessage(error) + errorMessage?.(error) } else { dispatch(getSuccess(data as Element)) - successMessage() + successMessage?.() } } } @@ -102,12 +94,8 @@ export function elementListFetch( getRequesting: ActionCreatorWithoutPayload, getSuccess: ActionCreatorWithPayload, getFailure: ActionCreatorWithPayload, - errorMessage: (error: Error) => void = (_error) => { - /* Meant to be empty */ - }, - successMessage: () => void = () => { - /* Meant to be empty */ - } + errorMessage?: (error: Error) => void, + successMessage?: () => void ): () => AppThunk { return (): AppThunk => async (dispatch) => { dispatch(getRequesting()) @@ -116,10 +104,10 @@ export function elementListFetch( if (error) { dispatch(getFailure(error.message)) - errorMessage(error) + errorMessage?.(error) } else { dispatch(getSuccess(data as Element[])) - successMessage() + successMessage?.() } } } @@ -132,12 +120,8 @@ export function elementSet( getRequesting: ActionCreatorWithoutPayload, getSuccess: ActionCreatorWithPayload, getFailure: ActionCreatorWithPayload, - errorMessage: (error: Error) => void = (_error) => { - /* Meant to be empty */ - }, - successMessage: () => void = () => { - /* Meant to be empty */ - } + errorMessage?: (error: Error) => void, + successMessage?: () => void ): (element: Element) => AppThunk { return (element: Element): AppThunk => async (dispatch) => { @@ -147,10 +131,10 @@ export function elementSet( if (error) { dispatch(getFailure(error.message)) - errorMessage(error) + errorMessage?.(error) } else { dispatch(getSuccess(data as Element)) - successMessage() + successMessage?.() } } } @@ -163,12 +147,8 @@ export function elementValueFetch( getRequesting: ActionCreatorWithoutPayload, getSuccess: ActionCreatorWithPayload, getFailure: ActionCreatorWithPayload, - errorMessage: (error: Error) => void = (_error) => { - /* Meant to be empty */ - }, - successMessage: () => void = () => { - /* Meant to be empty */ - } + errorMessage?: (error: Error) => void, + successMessage?: () => void ): () => AppThunk { return (): AppThunk => async (dispatch) => { dispatch(getRequesting()) @@ -177,10 +157,10 @@ export function elementValueFetch( if (error) { dispatch(getFailure(error.message)) - errorMessage(error) + errorMessage?.(error) } else { dispatch(getSuccess(data as Element)) - successMessage() + successMessage?.() } } } diff --git a/src/store/volunteerForgot.ts b/src/store/volunteerForgot.ts new file mode 100644 index 0000000..a7e2435 --- /dev/null +++ b/src/store/volunteerForgot.ts @@ -0,0 +1,38 @@ +import { PayloadAction, createSlice } from "@reduxjs/toolkit" + +import { StateRequest, elementFetch } from "./utils" +import { VolunteerForgot, volunteerForgot } from "../services/volunteers" + +type StateVolunteer = { entity?: VolunteerForgot } & StateRequest + +export const initialState: StateVolunteer = { + readyStatus: "idle", +} + +const volunteerForgotSlice = createSlice({ + name: "volunteerForgot", + initialState, + reducers: { + getRequesting: (_) => ({ + readyStatus: "request", + }), + getSuccess: (_, { payload }: PayloadAction) => ({ + readyStatus: "success", + entity: payload, + }), + getFailure: (_, { payload }: PayloadAction) => ({ + readyStatus: "failure", + error: payload, + }), + }, +}) + +export default volunteerForgotSlice.reducer +export const { getRequesting, getSuccess, getFailure } = volunteerForgotSlice.actions + +export const fetchVolunteerForgot = elementFetch( + volunteerForgot, + getRequesting, + getSuccess, + getFailure +) diff --git a/src/store/volunteerLogin.ts b/src/store/volunteerLogin.ts index 953c766..5b727e0 100644 --- a/src/store/volunteerLogin.ts +++ b/src/store/volunteerLogin.ts @@ -11,7 +11,7 @@ export const initialState: StateVolunteer = { } const volunteerLoginSlice = createSlice({ - name: "volunteer", + name: "volunteerLogin", initialState, reducers: { getRequesting: (_) => ({ diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 56627ad..5b6908b 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -3,6 +3,7 @@ declare const __SERVER__: boolean declare const __DEV__: boolean declare const __LOCAL__: boolean declare const __TEST__: boolean +declare const __SENDGRID_API_KEY__: string declare module "*.svg" declare module "*.gif" @@ -20,6 +21,7 @@ declare namespace NodeJS { __DEV__: boolean __LOCAL__: boolean __TEST__: boolean + __SENDGRID_API_KEY__: string $RefreshReg$: () => void $RefreshSig$$: () => void } diff --git a/yarn.lock b/yarn.lock index f7c6bbe..f28276d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1334,6 +1334,29 @@ redux-thunk "^2.3.0" reselect "^4.0.0" +"@sendgrid/client@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@sendgrid/client/-/client-7.6.0.tgz#f90cb8759c96e1d90224f29ad98f8fdc2be287f3" + integrity sha512-cpBVZKLlMTO+vpE18krTixubYmZa98oTbLkqBDuTiA3zRkW+urrxg7pDR24TkI35Mid0Zru8jDHwnOiqrXu0TA== + dependencies: + "@sendgrid/helpers" "^7.6.0" + axios "^0.21.4" + +"@sendgrid/helpers@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@sendgrid/helpers/-/helpers-7.6.0.tgz#b381bfab391bcd66c771811b22bb6bb2d5c1dfc6" + integrity sha512-0uWD+HSXLl4Z/X3cN+UMQC20RE7xwAACgppnfjDyvKG0KvJcUgDGz7HDdQkiMUdcVWfmyk6zKSg7XKfKzBjTwA== + dependencies: + deepmerge "^4.2.2" + +"@sendgrid/mail@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@sendgrid/mail/-/mail-7.6.0.tgz#e74ee30110527feab5d3b83d68af0cd94537f6d2" + integrity sha512-0KdaSZzflJD/vUAZjB3ALBIuaVGoLq22hrb2fvQXZHRepU/yhRNlEOqrr05MfKBnKskzq1blnD1J0fHxiwaolw== + dependencies: + "@sendgrid/client" "^7.6.0" + "@sendgrid/helpers" "^7.6.0" + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"