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,
__LOCAL__: false,
__TEST__: false,
__SENDGRID_API_KEY__: false,
},
}

View File

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

View File

@ -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",

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 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 => {
<button type="submit">Connexion</button>
</div>
<div className={styles.error}>{error}</div>
<div className={styles.link}>
<Link to="/forgot"> Demander un nouveau mot de passe </Link>
</div>
</form>
)
}

View File

@ -26,5 +26,12 @@
}
.error {
margin-top: 10px;
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,
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",
}}
/>
</MemoryRouter>

View File

@ -23,9 +23,10 @@ describe("<VolunteerList />", () => {
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",
},
]}

View File

@ -24,9 +24,9 @@ describe("<SetVolunteer />", () => {
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",
}}
/>
</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
const RegisterPage: React.FC<Props> = (): JSX.Element => {
const LoginPage: React.FC<Props> = (): JSX.Element => {
const dispatch = useDispatch()
const loginError = useSelector((state: AppState) => state.volunteerLogin.error, shallowEqual)
return (
<div className={styles.loginPage}>
<div className={styles.loginContent}>
<Helmet title="RegisterPage" />
<Helmet title="LoginPage" />
<LoginForm dispatch={dispatch} error={loginError || ""} />
</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 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,

View File

@ -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)

View File

@ -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<Element> = { 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> | any) {
return async (request: Request, response: Response, _next: NextFunction): Promise<void> => {
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<void> => {
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> | any
// custom can be async
set(
custom?: (
list: Element[],
body: RequestBody
) => Promise<CustomSetReturn<Element>> | CustomSetReturn<Element>
) {
return async (request: Request, response: Response, _next: NextFunction): Promise<void> => {
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 })
}

View File

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

View File

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

View File

@ -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<VolunteerWithoutId, Volunteer>(
)
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<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 wishGet = expressAccessor.get()
export const wishAdd = expressAccessor.add()
export const wishSet = expressAccessor.set()

View File

@ -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)

View File

@ -7,12 +7,15 @@ export type ElementWithId = unknown & { id: number }
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
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<ElementGetResponse> => {
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<ElementListGetResponse> => {
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<ElementGetResponse> => {
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<ElementGetResponse> => {
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<ElementCountGetResponse> => {
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<ElementGetResponse> => {
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 }
}

View File

@ -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<JavGame, "id">
const { listGet, get, set, add } = getServiceAccessors<JavGameWithoutId, JavGame>(elementName)
const serviceAccessors = new ServiceAccessors<JavGameWithoutId, JavGame>(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()

View File

@ -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<PreVolunteer, "id">
const { listGet, get, set, add, countGet } = getServiceAccessors<
PreVolunteerWithoutId,
PreVolunteer
>(elementName)
const serviceAccessors = new ServiceAccessors<PreVolunteerWithoutId, PreVolunteer>(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()

View File

@ -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<Volunteer, "id">
const accessors = getServiceAccessors<VolunteerWithoutId, Volunteer>(elementName)
const { listGet, get, set, add } = accessors
const serviceAccessors = new ServiceAccessors<VolunteerWithoutId, Volunteer>(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")

View File

@ -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<Wish, "id">
const { listGet, get, set, add } = getServiceAccessors<WishWithoutId, Wish>(elementName)
const serviceAccessors = new ServiceAccessors<WishWithoutId, Wish>(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()

View File

@ -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."

View File

@ -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."

View File

@ -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,

View File

@ -51,14 +51,10 @@ export function elementFetch<Element>(
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<Element>(
getRequesting: ActionCreatorWithoutPayload<string>,
getSuccess: ActionCreatorWithPayload<Element, string>,
getFailure: ActionCreatorWithPayload<string, string>,
errorMessage: (error: Error) => void = (_error) => {
/* Meant to be empty */
},
successMessage: () => void = () => {
/* Meant to be empty */
}
errorMessage?: (error: Error) => void,
successMessage?: () => void
): (volunteerWithoutId: Omit<Element, "id">) => AppThunk {
return (volunteerWithoutId: Omit<Element, "id">): AppThunk =>
async (dispatch) => {
@ -86,10 +78,10 @@ export function elementAddFetch<Element>(
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<Element>(
getRequesting: ActionCreatorWithoutPayload<string>,
getSuccess: ActionCreatorWithPayload<Element[], string>,
getFailure: ActionCreatorWithPayload<string, string>,
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<Element>(
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<Element>(
getRequesting: ActionCreatorWithoutPayload<string>,
getSuccess: ActionCreatorWithPayload<Element, string>,
getFailure: ActionCreatorWithPayload<string, string>,
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<Element>(
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<Element>(
getRequesting: ActionCreatorWithoutPayload<string>,
getSuccess: ActionCreatorWithPayload<Element, string>,
getFailure: ActionCreatorWithPayload<string, string>,
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<Element>(
if (error) {
dispatch(getFailure(error.message))
errorMessage(error)
errorMessage?.(error)
} else {
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({
name: "volunteer",
name: "volunteerLogin",
initialState,
reducers: {
getRequesting: (_) => ({

View File

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

View File

@ -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"