Add password reset

This commit is contained in:
pikiou 2022-01-05 02:05:07 +01:00
parent 395955f32a
commit adde4f366e
35 changed files with 459 additions and 178 deletions

View File

@ -57,5 +57,6 @@ module.exports = {
__DEV__: true, __DEV__: true,
__LOCAL__: false, __LOCAL__: false,
__TEST__: false, __TEST__: false,
__SENDGRID_API_KEY__: false,
}, },
} }

View File

@ -23,6 +23,7 @@ module.exports = {
__SERVER__: false, __SERVER__: false,
__LOCAL__: false, __LOCAL__: false,
__TEST__: true, __TEST__: true,
__SENDGRID_API_KEY__: "",
localStorage: { getItem: () => null, setItem: () => null, removeItem: () => null }, localStorage: { getItem: () => null, setItem: () => null, removeItem: () => null },
}, },
maxConcurrency: 50, maxConcurrency: 50,

View File

@ -73,6 +73,7 @@
"@loadable/component": "^5.15.0", "@loadable/component": "^5.15.0",
"@loadable/server": "^5.15.0", "@loadable/server": "^5.15.0",
"@reduxjs/toolkit": "^1.6.0", "@reduxjs/toolkit": "^1.6.0",
"@sendgrid/mail": "^7.6.0",
"@types/lodash": "^4.14.177", "@types/lodash": "^4.14.177",
"autoprefixer": "^10.2.6", "autoprefixer": "^10.2.6",
"axios": "^0.21.1", "axios": "^0.21.1",

View File

@ -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 (
<form onSubmit={onSubmit}>
<div className={styles.forgotIntro} key="forgot-intro">
Nous allons te renvoyer un mot de passe à l&apos;adresse suivante.
</div>
<div className={styles.formLine} key="line-email">
<label htmlFor="email">Email</label>
<input type="email" id="email" name="utilisateur" />
</div>
<div className={styles.formButtons}>
<button type="submit">Connexion</button>
</div>
<div className={styles.error}>{error}</div>
<div className={styles.message}>{message}</div>
<div className={styles.link}>
<Link to="/login"> S&apos;identifier </Link>
</div>
</form>
)
}
export default memo(ForgotForm)

View File

@ -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;
}

View File

@ -1,7 +1,8 @@
import React, { memo, useCallback } from "react" import React, { memo, useCallback } from "react"
import styles from "./styles.module.scss" import { Link } from "react-router-dom"
import { AppDispatch } from "../../store" import { AppDispatch } from "../../store"
import { fetchVolunteerLogin } from "../../store/volunteerLogin" import { fetchVolunteerLogin } from "../../store/volunteerLogin"
import styles from "./styles.module.scss"
interface Props { interface Props {
dispatch: AppDispatch dispatch: AppDispatch
@ -41,6 +42,9 @@ const LoginForm = ({ dispatch, error }: Props): JSX.Element => {
<button type="submit">Connexion</button> <button type="submit">Connexion</button>
</div> </div>
<div className={styles.error}>{error}</div> <div className={styles.error}>{error}</div>
<div className={styles.link}>
<Link to="/forgot"> Demander un nouveau mot de passe </Link>
</div>
</form> </form>
) )
} }

View File

@ -26,5 +26,12 @@
} }
.error { .error {
margin-top: 10px;
color: rgb(255, 0, 0); color: rgb(255, 0, 0);
text-align: center;
}
.link {
margin-top: 20px;
text-align: center;
} }

View File

@ -22,9 +22,9 @@ describe("<VolunteerInfo />", () => {
adult: 1, adult: 1,
privileges: 0, privileges: 0,
active: 0, active: 0,
comment: "", created: new Date(0),
timestamp: new Date(0), password1: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
password: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O", password2: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
}} }}
/> />
</MemoryRouter> </MemoryRouter>

View File

@ -23,9 +23,10 @@ describe("<VolunteerList />", () => {
adult: 1, adult: 1,
privileges: 0, privileges: 0,
active: 0, active: 0,
comment: "", created: new Date(0),
timestamp: new Date(0), password1:
password: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
password2:
"$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O", "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
}, },
]} ]}

View File

@ -24,9 +24,9 @@ describe("<SetVolunteer />", () => {
adult: 1, adult: 1,
privileges: 0, privileges: 0,
active: 0, active: 0,
comment: "", created: new Date(0),
timestamp: new Date(0), password1: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
password: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O", password2: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
}} }}
/> />
</MemoryRouter> </MemoryRouter>

View File

@ -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<Props> = (): 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 (
<div className={styles.forgotPage}>
<div className={styles.forgotContent}>
<Helmet title="ForgotPage" />
<ForgotForm
dispatch={dispatch}
error={forgotError || ""}
message={forgotMessage || ""}
/>
</div>
</div>
)
}
export default memo(ForgotPage)

14
src/pages/Forgot/index.tsx Executable file
View File

@ -0,0 +1,14 @@
import loadable from "@loadable/component"
import { Loading, ErrorBoundary } from "../../components"
import { Props } from "./ForgotPage"
const ForgotPage = loadable(() => import("./ForgotPage"), {
fallback: <Loading />,
})
export default (props: Props): JSX.Element => (
<ErrorBoundary>
<ForgotPage {...props} />
</ErrorBoundary>
)

View File

@ -0,0 +1,9 @@
@import "../../theme/mixins";
.forgotPage {
@include page-wrapper-center;
}
.forgotContent {
@include page-content-wrapper;
}

View File

@ -9,18 +9,18 @@ import styles from "./styles.module.scss"
export type Props = RouteComponentProps export type Props = RouteComponentProps
const RegisterPage: React.FC<Props> = (): JSX.Element => { const LoginPage: React.FC<Props> = (): JSX.Element => {
const dispatch = useDispatch() const dispatch = useDispatch()
const loginError = useSelector((state: AppState) => state.volunteerLogin.error, shallowEqual) const loginError = useSelector((state: AppState) => state.volunteerLogin.error, shallowEqual)
return ( return (
<div className={styles.loginPage}> <div className={styles.loginPage}>
<div className={styles.loginContent}> <div className={styles.loginContent}>
<Helmet title="RegisterPage" /> <Helmet title="LoginPage" />
<LoginForm dispatch={dispatch} error={loginError || ""} /> <LoginForm dispatch={dispatch} error={loginError || ""} />
</div> </div>
</div> </div>
) )
} }
export default memo(RegisterPage) export default memo(LoginPage)

View File

@ -5,6 +5,7 @@ import AsyncHome, { loadData as loadHomeData } from "../pages/Home"
import AsyncWish, { loadData as loadWishData } from "../pages/Wish" import AsyncWish, { loadData as loadWishData } from "../pages/Wish"
import AsyncVolunteerPage, { loadData as loadVolunteerPageData } from "../pages/VolunteerPage" import AsyncVolunteerPage, { loadData as loadVolunteerPageData } from "../pages/VolunteerPage"
import Login from "../pages/Login" import Login from "../pages/Login"
import Forgot from "../pages/Forgot"
import Register from "../pages/Register" import Register from "../pages/Register"
import NotFound from "../pages/NotFound" import NotFound from "../pages/NotFound"
@ -26,6 +27,10 @@ export default [
path: "/login", path: "/login",
component: Login, component: Login,
}, },
{
path: "/forgot",
component: Forgot,
},
{ {
path: "/home", path: "/home",
component: AsyncHome, component: AsyncHome,

View File

@ -120,6 +120,7 @@ export class Sheet<
if (!foundElement) { if (!foundElement) {
throw new Error(`No element found to be set in ${this.name} at id ${element.id}`) throw new Error(`No element found to be set in ${this.name} at id ${element.id}`)
} }
if (!_.isEqual(foundElement, element)) { if (!_.isEqual(foundElement, element)) {
Object.assign(foundElement, element) Object.assign(foundElement, element)
await this.setList(elements) await this.setList(elements)

View File

@ -1,6 +1,9 @@
import { Request, Response, NextFunction } from "express" import { Request, Response, NextFunction } from "express"
import { SheetNames, ElementWithId, getSheet, Sheet } from "./accessors" import { SheetNames, ElementWithId, getSheet, Sheet } from "./accessors"
export type RequestBody = Request["body"]
export type CustomSetReturn<Element> = { toDatabase: Element; toCaller: any }
export default class ExpressAccessors< export default class ExpressAccessors<
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
ElementNoId extends object, ElementNoId extends object,
@ -27,23 +30,27 @@ export default class ExpressAccessors<
if (elements) { if (elements) {
response.status(200).json(elements) response.status(200).json(elements)
} }
} catch (e: unknown) { } catch (e: any) {
response.status(400).json(e) response.status(200).json({ error: e.message })
} }
} }
} }
get() { // custom can be async
get(custom?: (list: Element[], body: Request["body"]) => Promise<any> | any) {
return async (request: Request, response: Response, _next: NextFunction): Promise<void> => { return async (request: Request, response: Response, _next: NextFunction): Promise<void> => {
try { try {
const list = (await this.sheet.getList()) || []
let toCaller: any
if (!custom) {
const id = parseInt(request.query.id as string, 10) || -1 const id = parseInt(request.query.id as string, 10) || -1
const elements = await this.sheet.getList() toCaller = list.find((e: Element) => e.id === id)
if (elements) { } else {
const element = elements.find((e: Element) => e.id === id) toCaller = await custom(list, request.body)
response.status(200).json(element)
} }
} catch (e: unknown) { response.status(200).json(toCaller)
response.status(400).json(e) } catch (e: any) {
response.status(200).json({ error: e.message })
} }
} }
} }
@ -55,31 +62,36 @@ export default class ExpressAccessors<
if (element) { if (element) {
response.status(200).json(element) response.status(200).json(element)
} }
} catch (e: unknown) { } catch (e: any) {
response.status(400).json(e) response.status(200).json({ error: e.message })
} }
} }
} }
set() { // custom can be async
return async (request: Request, response: Response, _next: NextFunction): Promise<void> => { set(
try { custom?: (
await this.sheet.set(request.body) list: Element[],
response.status(200) body: RequestBody
} catch (e: unknown) { ) => Promise<CustomSetReturn<Element>> | CustomSetReturn<Element>
response.status(400).json(e)
}
}
}
// transformer can be an async function
customGet(
transformer: (list: Element[] | undefined, body?: Request["body"]) => Promise<any> | any
) { ) {
return async (request: Request, response: Response, _next: NextFunction): Promise<void> => { return async (request: Request, response: Response, _next: NextFunction): Promise<void> => {
try { try {
const elements = await this.sheet.getList() if (!custom) {
response.status(200).json(await transformer(elements, request.body)) 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) { } catch (e: any) {
response.status(200).json({ error: e.message }) response.status(200).json({ error: e.message })
} }

View File

@ -8,9 +8,6 @@ const expressAccessor = new ExpressAccessors<JavGameWithoutId, JavGame>(
) )
export const javGameListGet = expressAccessor.listGet() export const javGameListGet = expressAccessor.listGet()
export const javGameGet = expressAccessor.get() export const javGameGet = expressAccessor.get()
export const javGameAdd = expressAccessor.add() export const javGameAdd = expressAccessor.add()
export const javGameSet = expressAccessor.set() export const javGameSet = expressAccessor.set()

View File

@ -12,13 +12,10 @@ const expressAccessor = new ExpressAccessors<PreVolunteerWithoutId, PreVolunteer
) )
export const preVolunteerListGet = expressAccessor.listGet() export const preVolunteerListGet = expressAccessor.listGet()
export const preVolunteerGet = expressAccessor.get() export const preVolunteerGet = expressAccessor.get()
export const preVolunteerAdd = expressAccessor.add() export const preVolunteerAdd = expressAccessor.add()
export const preVolunteerSet = expressAccessor.set() export const preVolunteerSet = expressAccessor.set()
export const preVolunteerCountGet = expressAccessor.customGet( export const preVolunteerCountGet = expressAccessor.get(
(list?: PreVolunteer[]) => (list && list.length) || 0 (list?: PreVolunteer[]) => (list && list.length) || 0
) )

View File

@ -1,7 +1,8 @@
import { Request } from "express" import _ from "lodash"
import bcrypt from "bcrypt" 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 { Volunteer, VolunteerWithoutId, translationVolunteer } from "../../services/volunteers"
import { canonicalEmail } from "../../utils/standardization" import { canonicalEmail } from "../../utils/standardization"
import { getJwt } from "../secure" import { getJwt } from "../secure"
@ -13,38 +14,98 @@ const expressAccessor = new ExpressAccessors<VolunteerWithoutId, Volunteer>(
) )
export const volunteerListGet = expressAccessor.listGet() export const volunteerListGet = expressAccessor.listGet()
export const volunteerGet = expressAccessor.get() export const volunteerGet = expressAccessor.get()
export const volunteerAdd = expressAccessor.add() export const volunteerAdd = expressAccessor.add()
export const volunteerSet = expressAccessor.set() export const volunteerSet = expressAccessor.set()
export const volunteerLogin = expressAccessor.customGet( export const volunteerLogin = expressAccessor.get(async (list: Volunteer[], body: RequestBody) => {
async (list: Volunteer[] | undefined, body: Request["body"]) => { const volunteer = getByEmail(list, body.email)
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) { if (!volunteer) {
throw Error("Il n'y a aucun bénévole avec cet email") throw Error("Il n'y a aucun bénévole avec cet email")
} }
const password = body.password || "" const password = body.password || ""
const passwordMatch = await bcrypt.compare( const password1Match = await bcrypt.compare(
password, password,
volunteer.password.replace(/^\$2y/, "$2a") volunteer.password1.replace(/^\$2y/, "$2a")
) )
if (!passwordMatch) { if (!password1Match) {
const password2Match = await bcrypt.compare(
password,
volunteer.password2.replace(/^\$2y/, "$2a")
)
if (!password2Match) {
throw Error("Mauvais mot de passe pour cet email") throw Error("Mauvais mot de passe pour cet email")
} }
}
const jwt = await getJwt(email) const jwt = await getJwt(volunteer.email)
return { return {
firstname: volunteer.firstname, firstname: volunteer.firstname,
jwt, 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<void> {
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 : <strong>${password}</strong><br />L'ancien fonctionne encore, si tu t'en rappelles.`,
}
await sgMail.send(msg)
}
}

View File

@ -8,9 +8,6 @@ const expressAccessor = new ExpressAccessors<WishWithoutId, Wish>(
) )
export const wishListGet = expressAccessor.listGet() export const wishListGet = expressAccessor.listGet()
export const wishGet = expressAccessor.get() export const wishGet = expressAccessor.get()
export const wishAdd = expressAccessor.add() export const wishAdd = expressAccessor.add()
export const wishSet = expressAccessor.set() export const wishSet = expressAccessor.set()

View File

@ -19,7 +19,7 @@ import { secure } from "./secure"
import { javGameListGet } from "./gsheets/javGames" import { javGameListGet } from "./gsheets/javGames"
import { wishListGet, wishAdd } from "./gsheets/wishes" import { wishListGet, wishAdd } from "./gsheets/wishes"
import { preVolunteerAdd, preVolunteerCountGet } from "./gsheets/preVolunteers" 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" import config from "../config"
const app = express() const app = express()
@ -56,6 +56,7 @@ app.post("/WishAdd", wishAdd)
app.post("/PreVolunteerAdd", preVolunteerAdd) app.post("/PreVolunteerAdd", preVolunteerAdd)
app.get("/PreVolunteerCountGet", preVolunteerCountGet) app.get("/PreVolunteerCountGet", preVolunteerCountGet)
app.post("/VolunteerLogin", volunteerLogin) app.post("/VolunteerLogin", volunteerLogin)
app.post("/VolunteerForgot", volunteerForgot)
// Secured APIs // Secured APIs
app.get("/VolunteerGet", secure as RequestHandler, volunteerGet) app.get("/VolunteerGet", secure as RequestHandler, volunteerGet)

View File

@ -7,12 +7,15 @@ export type ElementWithId = unknown & { id: number }
export type ElementTranslation<Element> = { [k in keyof Element]: string } export type ElementTranslation<Element> = { [k in keyof Element]: string }
export default function getServiceAccessors< export default class ServiceAccessors<
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
ElementNoId extends object, ElementNoId extends object,
Element extends ElementNoId & ElementWithId 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 data?: Element
error?: Error error?: Error
}> { }> {
@ -22,7 +25,7 @@ export default function getServiceAccessors<
} }
return async (id: number): Promise<ElementGetResponse> => { return async (id: number): Promise<ElementGetResponse> => {
try { try {
const { data } = await axios.get(`${config.API_URL}/${elementName}Get`, { const { data } = await axios.get(`${config.API_URL}/${this.elementName}Get`, {
...axiosConfig, ...axiosConfig,
params: { id }, params: { id },
}) })
@ -33,7 +36,7 @@ export default function getServiceAccessors<
} }
} }
function listGet(): () => Promise<{ listGet(): () => Promise<{
data?: Element[] data?: Element[]
error?: Error error?: Error
}> { }> {
@ -44,7 +47,7 @@ export default function getServiceAccessors<
return async (): Promise<ElementListGetResponse> => { return async (): Promise<ElementListGetResponse> => {
try { try {
const { data } = await axios.get( const { data } = await axios.get(
`${config.API_URL}/${elementName}ListGet`, `${config.API_URL}/${this.elementName}ListGet`,
axiosConfig axiosConfig
) )
return { data } return { data }
@ -55,7 +58,7 @@ export default function getServiceAccessors<
} }
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
function add(): (volunteerWithoutId: ElementNoId) => Promise<{ add(): (volunteerWithoutId: ElementNoId) => Promise<{
data?: Element data?: Element
error?: Error error?: Error
}> { }> {
@ -66,7 +69,7 @@ export default function getServiceAccessors<
return async (volunteerWithoutId: ElementNoId): Promise<ElementGetResponse> => { return async (volunteerWithoutId: ElementNoId): Promise<ElementGetResponse> => {
try { try {
const { data } = await axios.post( const { data } = await axios.post(
`${config.API_URL}/${elementName}Add`, `${config.API_URL}/${this.elementName}Add`,
volunteerWithoutId, volunteerWithoutId,
axiosConfig axiosConfig
) )
@ -77,7 +80,7 @@ export default function getServiceAccessors<
} }
} }
function set(): (volunteer: Element) => Promise<{ set(): (volunteer: Element) => Promise<{
data?: Element data?: Element
error?: Error error?: Error
}> { }> {
@ -88,7 +91,7 @@ export default function getServiceAccessors<
return async (volunteer: Element): Promise<ElementGetResponse> => { return async (volunteer: Element): Promise<ElementGetResponse> => {
try { try {
const { data } = await axios.post( const { data } = await axios.post(
`${config.API_URL}/${elementName}Set`, `${config.API_URL}/${this.elementName}Set`,
volunteer, volunteer,
axiosConfig axiosConfig
) )
@ -99,7 +102,7 @@ export default function getServiceAccessors<
} }
} }
function countGet(): () => Promise<{ countGet(): () => Promise<{
data?: number data?: number
error?: Error error?: Error
}> { }> {
@ -110,7 +113,7 @@ export default function getServiceAccessors<
return async (): Promise<ElementCountGetResponse> => { return async (): Promise<ElementCountGetResponse> => {
try { try {
const { data } = await axios.get( const { data } = await axios.get(
`${config.API_URL}/${elementName}CountGet`, `${config.API_URL}/${this.elementName}CountGet`,
axiosConfig axiosConfig
) )
return { data } return { data }
@ -120,18 +123,18 @@ export default function getServiceAccessors<
} }
} }
function customPost(apiName: string): (params: any) => Promise<{ customPost(apiName: string): (params: any) => Promise<{
data?: Element data?: any
error?: Error error?: Error
}> { }> {
interface ElementGetResponse { interface ElementGetResponse {
data?: Element data?: any
error?: Error error?: Error
} }
return async (params: any): Promise<ElementGetResponse> => { return async (params: any): Promise<ElementGetResponse> => {
try { try {
const { data } = await axios.post( const { data } = await axios.post(
`${config.API_URL}/${elementName}${apiName}`, `${config.API_URL}/${this.elementName}${apiName}`,
params, params,
axiosConfig axiosConfig
) )
@ -144,6 +147,4 @@ export default function getServiceAccessors<
} }
} }
} }
return { listGet, get, set, add, countGet, customPost }
} }

View File

@ -1,4 +1,4 @@
import getServiceAccessors from "./accessors" import ServiceAccessors from "./accessors"
export class JavGame { export class JavGame {
id = 0 id = 0
@ -54,9 +54,9 @@ const elementName = "JavGame"
export type JavGameWithoutId = Omit<JavGame, "id"> export type JavGameWithoutId = Omit<JavGame, "id">
const { listGet, get, set, add } = getServiceAccessors<JavGameWithoutId, JavGame>(elementName) const serviceAccessors = new ServiceAccessors<JavGameWithoutId, JavGame>(elementName)
export const javGameListGet = listGet() export const javGameListGet = serviceAccessors.listGet()
export const javGameGet = get() export const javGameGet = serviceAccessors.get()
export const javGameAdd = add() export const javGameAdd = serviceAccessors.add()
export const javGameSet = set() export const javGameSet = serviceAccessors.set()

View File

@ -1,4 +1,4 @@
import getServiceAccessors from "./accessors" import ServiceAccessors from "./accessors"
export class PreVolunteer { export class PreVolunteer {
id = 0 id = 0
@ -34,13 +34,10 @@ export const passwordMinLength = 4
export type PreVolunteerWithoutId = Omit<PreVolunteer, "id"> export type PreVolunteerWithoutId = Omit<PreVolunteer, "id">
const { listGet, get, set, add, countGet } = getServiceAccessors< const serviceAccessors = new ServiceAccessors<PreVolunteerWithoutId, PreVolunteer>(elementName)
PreVolunteerWithoutId,
PreVolunteer
>(elementName)
export const preVolunteerListGet = listGet() export const preVolunteerListGet = serviceAccessors.listGet()
export const preVolunteerGet = get() export const preVolunteerGet = serviceAccessors.get()
export const preVolunteerAdd = add() export const preVolunteerAdd = serviceAccessors.add()
export const preVolunteerSet = set() export const preVolunteerSet = serviceAccessors.set()
export const preVolunteerCountGet = countGet() export const preVolunteerCountGet = serviceAccessors.countGet()

View File

@ -1,4 +1,4 @@
import getServiceAccessors from "./accessors" import ServiceAccessors from "./accessors"
export class Volunteer { export class Volunteer {
id = 0 id = 0
@ -21,11 +21,11 @@ export class Volunteer {
active = 0 active = 0
comment = "" created = new Date()
timestamp = new Date() password1 = ""
password = "" password2 = ""
} }
export const translationVolunteer: { [k in keyof Volunteer]: string } = { export const translationVolunteer: { [k in keyof Volunteer]: string } = {
@ -39,9 +39,9 @@ export const translationVolunteer: { [k in keyof Volunteer]: string } = {
adult: "majeur", adult: "majeur",
privileges: "privilege", privileges: "privilege",
active: "actif", active: "actif",
comment: "commentaire", created: "creation",
timestamp: "horodatage", password1: "passe1",
password: "passe", password2: "passe2",
} }
const elementName = "Volunteer" const elementName = "Volunteer"
@ -52,16 +52,20 @@ export const passwordMinLength = 4
export type VolunteerWithoutId = Omit<Volunteer, "id"> export type VolunteerWithoutId = Omit<Volunteer, "id">
const accessors = getServiceAccessors<VolunteerWithoutId, Volunteer>(elementName) const serviceAccessors = new ServiceAccessors<VolunteerWithoutId, Volunteer>(elementName)
const { listGet, get, set, add } = accessors
export const volunteerListGet = listGet() export const volunteerListGet = serviceAccessors.listGet()
export const volunteerGet = get() export const volunteerGet = serviceAccessors.get()
export const volunteerAdd = add() export const volunteerAdd = serviceAccessors.add()
export const volunteerSet = set() export const volunteerSet = serviceAccessors.set()
export interface VolunteerLogin { export interface VolunteerLogin {
firstname: string firstname: string
jwt: 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")

View File

@ -1,4 +1,4 @@
import getServiceAccessors from "./accessors" import ServiceAccessors from "./accessors"
export class Wish { export class Wish {
id = 0 id = 0
@ -27,9 +27,9 @@ const elementName = "Wish"
export type WishWithoutId = Omit<Wish, "id"> export type WishWithoutId = Omit<Wish, "id">
const { listGet, get, set, add } = getServiceAccessors<WishWithoutId, Wish>(elementName) const serviceAccessors = new ServiceAccessors<WishWithoutId, Wish>(elementName)
export const wishListGet = listGet() export const wishListGet = serviceAccessors.listGet()
export const wishGet = get() export const wishGet = serviceAccessors.get()
export const wishAdd = add() export const wishAdd = serviceAccessors.add()
export const wishSet = set() export const wishSet = serviceAccessors.set()

View File

@ -23,9 +23,9 @@ const mockData: Volunteer = {
adult: 1, adult: 1,
privileges: 0, privileges: 0,
active: 0, active: 0,
comment: "", created: new Date(0),
timestamp: new Date(0), password1: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O",
password: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O", password2: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O",
} }
const { id } = mockData const { id } = mockData
const mockError = "Oops! Something went wrong." const mockError = "Oops! Something went wrong."

View File

@ -25,9 +25,9 @@ const mockData: Volunteer[] = [
adult: 1, adult: 1,
privileges: 0, privileges: 0,
active: 0, active: 0,
comment: "", created: new Date(0),
timestamp: new Date(0), password1: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O",
password: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O", password2: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O",
}, },
] ]
const mockError = "Oops! Something went wrong." const mockError = "Oops! Something went wrong."

View File

@ -9,6 +9,7 @@ import volunteerAdd from "./volunteerAdd"
import volunteerList from "./volunteerList" import volunteerList from "./volunteerList"
import volunteerSet from "./volunteerSet" import volunteerSet from "./volunteerSet"
import volunteerLogin from "./volunteerLogin" import volunteerLogin from "./volunteerLogin"
import volunteerForgot from "./volunteerForgot"
import preVolunteerAdd from "./preVolunteerAdd" import preVolunteerAdd from "./preVolunteerAdd"
import preVolunteerCount from "./preVolunteerCount" import preVolunteerCount from "./preVolunteerCount"
@ -23,6 +24,7 @@ export default (history: History) => ({
volunteerList, volunteerList,
volunteerSet, volunteerSet,
volunteerLogin, volunteerLogin,
volunteerForgot,
preVolunteerAdd, preVolunteerAdd,
preVolunteerCount, preVolunteerCount,
router: connectRouter(history) as any, router: connectRouter(history) as any,

View File

@ -51,14 +51,10 @@ export function elementFetch<Element>(
if (error) { if (error) {
dispatch(getFailure(error.message)) dispatch(getFailure(error.message))
if (errorMessage) { errorMessage?.(error)
errorMessage(error)
}
} else { } else {
dispatch(getSuccess(data as Element)) dispatch(getSuccess(data as Element))
if (successMessage) { successMessage?.(data as Element)
successMessage(data as Element)
}
} }
} }
} }
@ -71,12 +67,8 @@ export function elementAddFetch<Element>(
getRequesting: ActionCreatorWithoutPayload<string>, getRequesting: ActionCreatorWithoutPayload<string>,
getSuccess: ActionCreatorWithPayload<Element, string>, getSuccess: ActionCreatorWithPayload<Element, string>,
getFailure: ActionCreatorWithPayload<string, string>, getFailure: ActionCreatorWithPayload<string, string>,
errorMessage: (error: Error) => void = (_error) => { errorMessage?: (error: Error) => void,
/* Meant to be empty */ successMessage?: () => void
},
successMessage: () => void = () => {
/* Meant to be empty */
}
): (volunteerWithoutId: Omit<Element, "id">) => AppThunk { ): (volunteerWithoutId: Omit<Element, "id">) => AppThunk {
return (volunteerWithoutId: Omit<Element, "id">): AppThunk => return (volunteerWithoutId: Omit<Element, "id">): AppThunk =>
async (dispatch) => { async (dispatch) => {
@ -86,10 +78,10 @@ export function elementAddFetch<Element>(
if (error) { if (error) {
dispatch(getFailure(error.message)) dispatch(getFailure(error.message))
errorMessage(error) errorMessage?.(error)
} else { } else {
dispatch(getSuccess(data as Element)) dispatch(getSuccess(data as Element))
successMessage() successMessage?.()
} }
} }
} }
@ -102,12 +94,8 @@ export function elementListFetch<Element>(
getRequesting: ActionCreatorWithoutPayload<string>, getRequesting: ActionCreatorWithoutPayload<string>,
getSuccess: ActionCreatorWithPayload<Element[], string>, getSuccess: ActionCreatorWithPayload<Element[], string>,
getFailure: ActionCreatorWithPayload<string, string>, getFailure: ActionCreatorWithPayload<string, string>,
errorMessage: (error: Error) => void = (_error) => { errorMessage?: (error: Error) => void,
/* Meant to be empty */ successMessage?: () => void
},
successMessage: () => void = () => {
/* Meant to be empty */
}
): () => AppThunk { ): () => AppThunk {
return (): AppThunk => async (dispatch) => { return (): AppThunk => async (dispatch) => {
dispatch(getRequesting()) dispatch(getRequesting())
@ -116,10 +104,10 @@ export function elementListFetch<Element>(
if (error) { if (error) {
dispatch(getFailure(error.message)) dispatch(getFailure(error.message))
errorMessage(error) errorMessage?.(error)
} else { } else {
dispatch(getSuccess(data as Element[])) dispatch(getSuccess(data as Element[]))
successMessage() successMessage?.()
} }
} }
} }
@ -132,12 +120,8 @@ export function elementSet<Element>(
getRequesting: ActionCreatorWithoutPayload<string>, getRequesting: ActionCreatorWithoutPayload<string>,
getSuccess: ActionCreatorWithPayload<Element, string>, getSuccess: ActionCreatorWithPayload<Element, string>,
getFailure: ActionCreatorWithPayload<string, string>, getFailure: ActionCreatorWithPayload<string, string>,
errorMessage: (error: Error) => void = (_error) => { errorMessage?: (error: Error) => void,
/* Meant to be empty */ successMessage?: () => void
},
successMessage: () => void = () => {
/* Meant to be empty */
}
): (element: Element) => AppThunk { ): (element: Element) => AppThunk {
return (element: Element): AppThunk => return (element: Element): AppThunk =>
async (dispatch) => { async (dispatch) => {
@ -147,10 +131,10 @@ export function elementSet<Element>(
if (error) { if (error) {
dispatch(getFailure(error.message)) dispatch(getFailure(error.message))
errorMessage(error) errorMessage?.(error)
} else { } else {
dispatch(getSuccess(data as Element)) dispatch(getSuccess(data as Element))
successMessage() successMessage?.()
} }
} }
} }
@ -163,12 +147,8 @@ export function elementValueFetch<Element>(
getRequesting: ActionCreatorWithoutPayload<string>, getRequesting: ActionCreatorWithoutPayload<string>,
getSuccess: ActionCreatorWithPayload<Element, string>, getSuccess: ActionCreatorWithPayload<Element, string>,
getFailure: ActionCreatorWithPayload<string, string>, getFailure: ActionCreatorWithPayload<string, string>,
errorMessage: (error: Error) => void = (_error) => { errorMessage?: (error: Error) => void,
/* Meant to be empty */ successMessage?: () => void
},
successMessage: () => void = () => {
/* Meant to be empty */
}
): () => AppThunk { ): () => AppThunk {
return (): AppThunk => async (dispatch) => { return (): AppThunk => async (dispatch) => {
dispatch(getRequesting()) dispatch(getRequesting())
@ -177,10 +157,10 @@ export function elementValueFetch<Element>(
if (error) { if (error) {
dispatch(getFailure(error.message)) dispatch(getFailure(error.message))
errorMessage(error) errorMessage?.(error)
} else { } else {
dispatch(getSuccess(data as Element)) dispatch(getSuccess(data as Element))
successMessage() successMessage?.()
} }
} }
} }

View File

@ -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<VolunteerForgot>) => ({
readyStatus: "success",
entity: payload,
}),
getFailure: (_, { payload }: PayloadAction<string>) => ({
readyStatus: "failure",
error: payload,
}),
},
})
export default volunteerForgotSlice.reducer
export const { getRequesting, getSuccess, getFailure } = volunteerForgotSlice.actions
export const fetchVolunteerForgot = elementFetch(
volunteerForgot,
getRequesting,
getSuccess,
getFailure
)

View File

@ -11,7 +11,7 @@ export const initialState: StateVolunteer = {
} }
const volunteerLoginSlice = createSlice({ const volunteerLoginSlice = createSlice({
name: "volunteer", name: "volunteerLogin",
initialState, initialState,
reducers: { reducers: {
getRequesting: (_) => ({ getRequesting: (_) => ({

View File

@ -3,6 +3,7 @@ declare const __SERVER__: boolean
declare const __DEV__: boolean declare const __DEV__: boolean
declare const __LOCAL__: boolean declare const __LOCAL__: boolean
declare const __TEST__: boolean declare const __TEST__: boolean
declare const __SENDGRID_API_KEY__: string
declare module "*.svg" declare module "*.svg"
declare module "*.gif" declare module "*.gif"
@ -20,6 +21,7 @@ declare namespace NodeJS {
__DEV__: boolean __DEV__: boolean
__LOCAL__: boolean __LOCAL__: boolean
__TEST__: boolean __TEST__: boolean
__SENDGRID_API_KEY__: string
$RefreshReg$: () => void $RefreshReg$: () => void
$RefreshSig$$: () => void $RefreshSig$$: () => void
} }

View File

@ -1334,6 +1334,29 @@
redux-thunk "^2.3.0" redux-thunk "^2.3.0"
reselect "^4.0.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": "@sindresorhus/is@^0.14.0":
version "0.14.0" version "0.14.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"