mirror of
https://github.com/Paris-est-Ludique/intranet.git
synced 2025-06-08 08:34:20 +02:00
Add login api
This commit is contained in:
parent
4540e92171
commit
395955f32a
@ -106,6 +106,7 @@
|
||||
"react-router-dom": "^5.3.0",
|
||||
"react-toastify": "^8.1.0",
|
||||
"readline": "^1.3.0",
|
||||
"redux-devtools-extension": "^2.13.9",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"serialize-javascript": "^6.0.0",
|
||||
"serve-favicon": "^2.5.0"
|
||||
|
@ -1,20 +1,28 @@
|
||||
import React, { memo, useCallback } from "react"
|
||||
import styles from "./styles.module.scss"
|
||||
import { AppDispatch } from "../../store"
|
||||
import { fetchVolunteerLogin } from "../../store/volunteerLogin"
|
||||
|
||||
const LoginForm = (): JSX.Element => {
|
||||
const onSubmit = useCallback((event: React.SyntheticEvent): void => {
|
||||
event.preventDefault()
|
||||
const target = event.target as typeof event.target & {
|
||||
email: { value: string }
|
||||
password: { value: string }
|
||||
}
|
||||
const email = target.email.value
|
||||
const password = target.password.value
|
||||
interface Props {
|
||||
dispatch: AppDispatch
|
||||
error: string
|
||||
}
|
||||
|
||||
console.log("email and password checked", email, password)
|
||||
const LoginForm = ({ dispatch, error }: Props): JSX.Element => {
|
||||
const onSubmit = useCallback(
|
||||
(event: React.SyntheticEvent): void => {
|
||||
event.preventDefault()
|
||||
const target = event.target as typeof event.target & {
|
||||
email: { value: string }
|
||||
password: { value: string }
|
||||
}
|
||||
const email = target.email.value
|
||||
const password = target.password.value
|
||||
|
||||
// call service with email & password
|
||||
}, [])
|
||||
dispatch(fetchVolunteerLogin({ email, password }))
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit}>
|
||||
@ -23,15 +31,16 @@ const LoginForm = (): JSX.Element => {
|
||||
</div>
|
||||
<div className={styles.formLine} key="line-email">
|
||||
<label htmlFor="email">Email</label>
|
||||
<input type="email" id="email" />
|
||||
<input type="email" id="email" name="utilisateur" />
|
||||
</div>
|
||||
<div className={styles.formLine} key="line-password">
|
||||
<label htmlFor="password">Mot de passe</label>
|
||||
<input type="password" id="password" />
|
||||
<input type="password" id="password" name="motdepasse" />
|
||||
</div>
|
||||
<div className={styles.formButtons}>
|
||||
<button type="submit">Connexion</button>
|
||||
</div>
|
||||
<div className={styles.error}>{error}</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
@ -24,3 +24,7 @@
|
||||
padding: 5px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: rgb(255, 0, 0);
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ describe("<VolunteerInfo />", () => {
|
||||
privileges: 0,
|
||||
active: 0,
|
||||
comment: "",
|
||||
timestamp: "0000-00-00",
|
||||
timestamp: new Date(0),
|
||||
password: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
|
||||
}}
|
||||
/>
|
||||
|
@ -24,7 +24,7 @@ describe("<VolunteerList />", () => {
|
||||
privileges: 0,
|
||||
active: 0,
|
||||
comment: "",
|
||||
timestamp: "0000-00-00",
|
||||
timestamp: new Date(0),
|
||||
password:
|
||||
"$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
|
||||
},
|
||||
|
@ -25,7 +25,7 @@ describe("<SetVolunteer />", () => {
|
||||
privileges: 0,
|
||||
active: 0,
|
||||
comment: "",
|
||||
timestamp: "0000-00-00",
|
||||
timestamp: new Date(0),
|
||||
password: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
|
||||
}}
|
||||
/>
|
||||
|
@ -43,7 +43,7 @@ const WishAdd = ({ dispatch }: Props) => {
|
||||
setTeams([""])
|
||||
setAddedDate("")
|
||||
} else {
|
||||
toast.warning("Il faut au moins préciser un domain et l'wish", {
|
||||
toast.warning("Il faut au moins préciser un domaine et l'envie", {
|
||||
position: "top-center",
|
||||
autoClose: 6000,
|
||||
hideProgressBar: true,
|
||||
|
@ -1,18 +1,26 @@
|
||||
import { RouteComponentProps } from "react-router-dom"
|
||||
import { useDispatch, useSelector, shallowEqual } from "react-redux"
|
||||
import React, { memo } from "react"
|
||||
import { Helmet } from "react-helmet"
|
||||
import styles from "./styles.module.scss"
|
||||
|
||||
import { AppState } from "../../store"
|
||||
import LoginForm from "../../components/LoginForm/LoginForm"
|
||||
import styles from "./styles.module.scss"
|
||||
|
||||
export type Props = RouteComponentProps
|
||||
|
||||
const RegisterPage: React.FC<Props> = (): JSX.Element => (
|
||||
<div className={styles.loginPage}>
|
||||
<div className={styles.loginContent}>
|
||||
<Helmet title="RegisterPage" />
|
||||
<LoginForm />
|
||||
const RegisterPage: 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" />
|
||||
<LoginForm dispatch={dispatch} error={loginError || ""} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(RegisterPage)
|
||||
|
@ -68,12 +68,15 @@ export class Sheet<
|
||||
|
||||
frenchSpecimen: Element
|
||||
|
||||
invertedTranslation: { [k: string]: string }
|
||||
|
||||
// eslint-disable-next-line no-useless-constructor
|
||||
constructor(
|
||||
readonly name: keyof SheetNames,
|
||||
readonly specimen: Element,
|
||||
readonly translation: { [k in keyof Element]: string }
|
||||
) {
|
||||
this.invertedTranslation = _.invert(this.translation)
|
||||
this.sheetName = sheetNames[name]
|
||||
this.frenchSpecimen = _.mapValues(
|
||||
_.invert(translation),
|
||||
@ -175,7 +178,7 @@ export class Sheet<
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
const elements = this._state as Element[]
|
||||
const types = _.pick(rows[0], Object.keys(elements[0] || {})) as Record<
|
||||
const types = _.pick(rows[0], Object.values(this.translation)) as Record<
|
||||
keyof Element,
|
||||
string
|
||||
>
|
||||
@ -185,17 +188,22 @@ export class Sheet<
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const element of elements) {
|
||||
const row = rows[rowid]
|
||||
const stringifiedRow = this.stringifyElement(element, types)
|
||||
const frenchElement = _.mapValues(
|
||||
this.invertedTranslation,
|
||||
(englishProp: string) => (element as any)[englishProp]
|
||||
) as Element
|
||||
const stringifiedRow = this.stringifyElement(frenchElement, types)
|
||||
|
||||
if (!row) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await sheet.addRow(stringifiedRow)
|
||||
} else {
|
||||
const keys = Object.keys(stringifiedRow)
|
||||
const sameCells = _.every(
|
||||
keys,
|
||||
(key: keyof Element) => row[key as string] === stringifiedRow[key]
|
||||
)
|
||||
const sameCells = _.every(keys, (key: keyof Element) => {
|
||||
const rawVal = row[key as string]
|
||||
const val: string = rawVal === undefined ? "" : rawVal
|
||||
return val === stringifiedRow[key]
|
||||
})
|
||||
if (!sameCells) {
|
||||
keys.forEach((key) => {
|
||||
row[key] = stringifiedRow[key as keyof Element]
|
||||
@ -238,9 +246,13 @@ export class Sheet<
|
||||
keyof Element,
|
||||
string
|
||||
>
|
||||
const element = this.parseElement(stringifiedElement, types)
|
||||
if (element !== undefined) {
|
||||
elements.push(element)
|
||||
const frenchData: any = this.parseElement(stringifiedElement, types)
|
||||
if (frenchData !== undefined) {
|
||||
const englishElement = _.mapValues(
|
||||
this.translation,
|
||||
(frenchProp: string) => frenchData[frenchProp]
|
||||
) as Element
|
||||
elements.push(englishElement)
|
||||
}
|
||||
})
|
||||
|
||||
@ -293,18 +305,13 @@ export class Sheet<
|
||||
if (rawProp === undefined) {
|
||||
element[prop] = undefined
|
||||
} else {
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const matchDate = rawProp.match(/^([0-9]+)\/([0-9]+)\/([0-9]+)$/)
|
||||
if (!matchDate) {
|
||||
try {
|
||||
element[prop] = parseDate(rawProp)
|
||||
} catch (e: any) {
|
||||
throw new Error(
|
||||
`Unable to read date from val ${rawProp} in sheet ${this.name} at prop ${prop}`
|
||||
`${e.message} in sheet ${this.name} at prop ${prop}`
|
||||
)
|
||||
}
|
||||
element[prop] = new Date(
|
||||
+matchDate[3],
|
||||
+matchDate[2] - 1,
|
||||
+matchDate[1]
|
||||
)
|
||||
}
|
||||
break
|
||||
|
||||
@ -345,27 +352,15 @@ export class Sheet<
|
||||
const rawDates = rawProp.split(delimiter)
|
||||
element[prop] = []
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const rightFormat = rawDates.every((rawDate) => {
|
||||
const matchDateArray = rawDate.match(
|
||||
/^([0-9]+)\/([0-9]+)\/([0-9]+)$/
|
||||
)
|
||||
if (!matchDateArray) {
|
||||
return false
|
||||
}
|
||||
element[prop].push(
|
||||
new Date(
|
||||
+matchDateArray[3],
|
||||
+matchDateArray[2] - 1,
|
||||
+matchDateArray[1]
|
||||
rawDates.forEach((rawDate) => {
|
||||
try {
|
||||
element[prop].push(parseDate(rawDate))
|
||||
} catch (e: any) {
|
||||
throw new Error(
|
||||
`${e.message} in sheet ${this.name} at prop ${prop}`
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
||||
})
|
||||
if (!rightFormat) {
|
||||
throw new Error(
|
||||
`One array item is not a date for val ${rawProp} in sheet ${this.name} at prop ${prop}`
|
||||
)
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(
|
||||
@ -391,7 +386,7 @@ export class Sheet<
|
||||
const value = element[prop as keyof Element]
|
||||
switch (type) {
|
||||
case "string":
|
||||
stringifiedElement[prop as keyof Element] = Sheet.formulaSafe(`${value}`)
|
||||
stringifiedElement[prop as keyof Element] = formulaSafe(`${value}`)
|
||||
break
|
||||
|
||||
case "number":
|
||||
@ -403,7 +398,7 @@ export class Sheet<
|
||||
break
|
||||
|
||||
case "date":
|
||||
stringifiedElement[prop as keyof Element] = Sheet.stringifiedDate(value)
|
||||
stringifiedElement[prop as keyof Element] = stringifiedDate(value)
|
||||
break
|
||||
|
||||
default:
|
||||
@ -426,7 +421,7 @@ export class Sheet<
|
||||
if (!_.every(value, _.isString)) {
|
||||
throw new Error(`Each date of ${value} is not a string`)
|
||||
}
|
||||
stringifiedElement[prop as keyof Element] = Sheet.formulaSafe(
|
||||
stringifiedElement[prop as keyof Element] = formulaSafe(
|
||||
value.join(delimiter)
|
||||
)
|
||||
break
|
||||
@ -453,10 +448,7 @@ export class Sheet<
|
||||
}
|
||||
stringifiedElement[prop as keyof Element] = _.map(
|
||||
value,
|
||||
(val) =>
|
||||
`${val.getDate()}/${
|
||||
val.getMonth() + 1
|
||||
}/${val.getFullYear()}`
|
||||
stringifiedDate
|
||||
).join(delimiter)
|
||||
break
|
||||
|
||||
@ -472,24 +464,33 @@ export class Sheet<
|
||||
|
||||
return rawElement
|
||||
}
|
||||
|
||||
private static formulaSafe(value: string): string {
|
||||
return value.replace(/^=+/, "")
|
||||
}
|
||||
|
||||
private static stringifiedDate(value: unknown): string {
|
||||
let date: Date
|
||||
if (value instanceof Date) {
|
||||
date = value
|
||||
} else if (typeof value === "string") {
|
||||
try {
|
||||
date = new Date(value)
|
||||
} catch (e) {
|
||||
throw new Error("Wrong date string format in stringifyElement")
|
||||
}
|
||||
} else {
|
||||
throw new Error("Wrong date format in stringifyElement")
|
||||
}
|
||||
return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}`
|
||||
}
|
||||
}
|
||||
|
||||
function formulaSafe(value: string): string {
|
||||
return value === undefined ? "" : value.replace(/^=+/, "")
|
||||
}
|
||||
|
||||
function stringifiedDate(value: unknown): string {
|
||||
let date: Date
|
||||
if (value instanceof Date) {
|
||||
date = value
|
||||
} else if (typeof value === "string") {
|
||||
try {
|
||||
date = new Date(value)
|
||||
} catch (e) {
|
||||
throw new Error("Wrong date string format in stringifyElement")
|
||||
}
|
||||
} else {
|
||||
throw new Error("Wrong date format in stringifyElement")
|
||||
}
|
||||
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`
|
||||
}
|
||||
|
||||
function parseDate(value: string): Date {
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const matchDate = value.match(/^([0-9]+)\/([0-9]+)\/([0-9]+)$/)
|
||||
if (!matchDate) {
|
||||
throw new Error(`Unable to read date from val ${value}`)
|
||||
}
|
||||
return new Date(+matchDate[1], +matchDate[2] - 1, +matchDate[3])
|
||||
}
|
||||
|
@ -51,11 +51,7 @@ export default class ExpressAccessors<
|
||||
add() {
|
||||
return async (request: Request, response: Response, _next: NextFunction): Promise<void> => {
|
||||
try {
|
||||
this.sheet.add(request.body)
|
||||
const elements: Element[] = (await this.sheet.getList()) || []
|
||||
const element: Element = { id: await this.sheet.nextId(), ...request.body }
|
||||
elements.push(element)
|
||||
await this.sheet.setList(elements)
|
||||
const element: Element = await this.sheet.add(request.body)
|
||||
if (element) {
|
||||
response.status(200).json(element)
|
||||
}
|
||||
@ -76,17 +72,16 @@ export default class ExpressAccessors<
|
||||
}
|
||||
}
|
||||
|
||||
customGet(transformer: (list?: Element[]) => any) {
|
||||
return async (
|
||||
_request: Request,
|
||||
response: Response,
|
||||
_next: NextFunction
|
||||
): Promise<void> => {
|
||||
// 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> => {
|
||||
try {
|
||||
const elements = await this.sheet.getList()
|
||||
response.status(200).json(transformer(elements))
|
||||
} catch (e: unknown) {
|
||||
response.status(400).json(e)
|
||||
response.status(200).json(await transformer(elements, request.body))
|
||||
} catch (e: any) {
|
||||
response.status(200).json({ error: e.message })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,10 @@
|
||||
import { Request } from "express"
|
||||
import bcrypt from "bcrypt"
|
||||
|
||||
import ExpressAccessors from "./expressAccessors"
|
||||
import { Volunteer, VolunteerWithoutId, translationVolunteer } from "../../services/volunteers"
|
||||
import { canonicalEmail } from "../../utils/standardization"
|
||||
import { getJwt } from "../secure"
|
||||
|
||||
const expressAccessor = new ExpressAccessors<VolunteerWithoutId, Volunteer>(
|
||||
"Volunteers",
|
||||
@ -14,3 +19,32 @@ 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")
|
||||
}
|
||||
|
||||
const password = body.password || ""
|
||||
const passwordMatch = await bcrypt.compare(
|
||||
password,
|
||||
volunteer.password.replace(/^\$2y/, "$2a")
|
||||
)
|
||||
if (!passwordMatch) {
|
||||
throw Error("Mauvais mot de passe pour cet email")
|
||||
}
|
||||
|
||||
const jwt = await getJwt(email)
|
||||
|
||||
return {
|
||||
firstname: volunteer.firstname,
|
||||
jwt,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -19,8 +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 } from "./gsheets/volunteers"
|
||||
import loginHandler from "./userManagement/login"
|
||||
import { volunteerGet, volunteerSet, volunteerLogin } from "./gsheets/volunteers"
|
||||
import config from "../config"
|
||||
|
||||
const app = express()
|
||||
@ -47,9 +46,6 @@ if (__DEV__) devServer(app)
|
||||
|
||||
app.use(express.json())
|
||||
|
||||
// Sign in & up API
|
||||
app.post("/api/user/login", loginHandler)
|
||||
|
||||
/**
|
||||
* APIs
|
||||
*/
|
||||
@ -59,6 +55,7 @@ app.get("/WishListGet", wishListGet)
|
||||
app.post("/WishAdd", wishAdd)
|
||||
app.post("/PreVolunteerAdd", preVolunteerAdd)
|
||||
app.get("/PreVolunteerCountGet", preVolunteerCountGet)
|
||||
app.post("/VolunteerLogin", volunteerLogin)
|
||||
|
||||
// Secured APIs
|
||||
app.get("/VolunteerGet", secure as RequestHandler, volunteerGet)
|
||||
|
@ -53,12 +53,12 @@ async function getSecret() {
|
||||
export async function getJwt(email: string): Promise<string> {
|
||||
const jwt = sign(
|
||||
{ user: canonicalEmail(email), permissions: [] },
|
||||
await getSecret(),
|
||||
__TEST__
|
||||
? undefined
|
||||
: {
|
||||
expiresIn: "365d",
|
||||
}
|
||||
await getSecret()
|
||||
// __TEST__
|
||||
// ? undefined
|
||||
// : {
|
||||
// expiresIn: "365d",
|
||||
// }
|
||||
)
|
||||
return jwt
|
||||
}
|
||||
|
@ -1,65 +0,0 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import _ from "lodash"
|
||||
import { getSheet } from "../../gsheets/accessors"
|
||||
import { login } from "../login"
|
||||
|
||||
// Could do a full test with: wget --header='Content-Type:application/json' --post-data='{"email":"pikiou.sub@gmail.com","password":"mot de passe"}' http://localhost:3000/api/user/login
|
||||
|
||||
// Full test with Bearer: wget --header='Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoicGlraW91c3ViQGdlYWlsLmNvbSIsInBlcm1pc3Npb25zIjpbXSwiaWF0IjoxNjM4MjUzODgzLCJleHAiOjE2Mzg4NTg2ODN9.MknJ4NfcVlgW2ODeimfwZI1a4z8asdEXtHwHgViy6c4' http://localhost:3000/VolunteerGet?id=1
|
||||
|
||||
const mockUser = {
|
||||
email: "my.email@gmail.com",
|
||||
password: "$2y$10$cuKFHEow2IVSZSPtoVsw6uZFNFOOP/v1V7fubbyvrxhZdsnxLHr.2",
|
||||
firstname: "monPrénom",
|
||||
}
|
||||
|
||||
jest.mock("../../gsheets/accessors")
|
||||
|
||||
describe("login with", () => {
|
||||
beforeAll(() => {
|
||||
;(getSheet as jest.Mock).mockImplementation(() => ({
|
||||
getList: async () => [mockUser],
|
||||
}))
|
||||
})
|
||||
|
||||
it("right password", async () => {
|
||||
const res = await login("my.email@gmail.com", "12345678")
|
||||
expect(_.omit(res, "jwt")).toEqual({
|
||||
volunteer: {
|
||||
firstname: mockUser.firstname,
|
||||
},
|
||||
})
|
||||
expect(res.jwt).toBeDefined()
|
||||
})
|
||||
|
||||
it("invalid password length", async () => {
|
||||
await expect(login("my.email@gmail.com", "123")).rejects.toThrow("Mot de passe trop court")
|
||||
})
|
||||
|
||||
it("empty password", async () => {
|
||||
await expect(login("my.email@gmail.com", " ")).rejects.toThrow("Mot de passe nécessaire")
|
||||
})
|
||||
|
||||
it("wrong password", async () => {
|
||||
await expect(login("my.email@gmail.com", "1234567891011")).rejects.toThrow(
|
||||
"Mauvais mot de passe pour cet email"
|
||||
)
|
||||
})
|
||||
|
||||
it("invalid email format", async () => {
|
||||
await expect(login("my.email@gmail", "12345678")).rejects.toThrow("Email invalid")
|
||||
})
|
||||
|
||||
it("empty email", async () => {
|
||||
await expect(login(" ", "12345678")).rejects.toThrow("Email invalid")
|
||||
})
|
||||
|
||||
it("unknown email", async () => {
|
||||
await expect(login("mon.emailBidon@gmail.com", "12345678")).rejects.toThrow(
|
||||
"Cet email ne correspond à aucun utilisateur"
|
||||
)
|
||||
})
|
||||
})
|
@ -1,73 +0,0 @@
|
||||
import { Request, Response, NextFunction } from "express"
|
||||
import bcrypt from "bcrypt"
|
||||
import {
|
||||
Volunteer,
|
||||
VolunteerWithoutId,
|
||||
VolunteerLogin,
|
||||
emailRegexp,
|
||||
passwordMinLength,
|
||||
translationVolunteer,
|
||||
} from "../../services/volunteers"
|
||||
import { getSheet } from "../gsheets/accessors"
|
||||
import { getJwt } from "../secure"
|
||||
|
||||
export default async function loginHandler(
|
||||
request: Request,
|
||||
response: Response,
|
||||
_next: NextFunction
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (typeof request.body.email !== "string" || typeof request.body.password !== "string") {
|
||||
throw Error()
|
||||
}
|
||||
const res = await login(request.body.email, request.body.password)
|
||||
response.status(200).json(res)
|
||||
} catch (e: any) {
|
||||
if (e.message) {
|
||||
response.status(200).json({ error: e.message })
|
||||
} else {
|
||||
response.status(400).json(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function login(rawEmail: string, rawPassword: string): Promise<VolunteerLogin> {
|
||||
const sheet = getSheet<VolunteerWithoutId, Volunteer>(
|
||||
"Volunteers",
|
||||
new Volunteer(),
|
||||
translationVolunteer
|
||||
)
|
||||
|
||||
const email = rawEmail.replace(/^\s*/, "").replace(/\s*$/, "")
|
||||
if (!emailRegexp.test(email)) {
|
||||
throw Error("Email invalid")
|
||||
}
|
||||
|
||||
const password = rawPassword.replace(/^\s*/, "").replace(/\s*$/, "")
|
||||
if (password.length === 0) {
|
||||
throw Error("Mot de passe nécessaire")
|
||||
}
|
||||
if (password.length < passwordMinLength) {
|
||||
throw Error("Mot de passe trop court")
|
||||
}
|
||||
|
||||
const volunteers: Volunteer[] | undefined = await sheet.getList()
|
||||
const volunteer = volunteers && volunteers.find((m) => m.email === email)
|
||||
if (!volunteer) {
|
||||
throw Error("Cet email ne correspond à aucun utilisateur")
|
||||
}
|
||||
|
||||
const passwordMatch = await bcrypt.compare(password, volunteer.password.replace(/^\$2y/, "$2a"))
|
||||
if (!passwordMatch) {
|
||||
throw Error("Mauvais mot de passe pour cet email")
|
||||
}
|
||||
|
||||
const jwt = await getJwt(email)
|
||||
|
||||
return {
|
||||
volunteer: {
|
||||
firstname: volunteer.firstname,
|
||||
},
|
||||
jwt,
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import axios from "axios"
|
||||
import _ from "lodash"
|
||||
|
||||
import config from "../config"
|
||||
import { axiosConfig } from "./auth"
|
||||
@ -12,7 +11,7 @@ export default function getServiceAccessors<
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
ElementNoId extends object,
|
||||
Element extends ElementNoId & ElementWithId
|
||||
>(elementName: string, translation: { [k in keyof Element]: string }): any {
|
||||
>(elementName: string): any {
|
||||
function get(): (id: number) => Promise<{
|
||||
data?: Element
|
||||
error?: Error
|
||||
@ -27,14 +26,7 @@ export default function getServiceAccessors<
|
||||
...axiosConfig,
|
||||
params: { id },
|
||||
})
|
||||
if (!data) {
|
||||
return { data }
|
||||
}
|
||||
const englishData = _.mapValues(
|
||||
translation,
|
||||
(frenchProp: string) => data[frenchProp]
|
||||
) as Element
|
||||
return { data: englishData }
|
||||
return { data }
|
||||
} catch (error) {
|
||||
return { error: error as Error }
|
||||
}
|
||||
@ -55,18 +47,7 @@ export default function getServiceAccessors<
|
||||
`${config.API_URL}/${elementName}ListGet`,
|
||||
axiosConfig
|
||||
)
|
||||
if (!data) {
|
||||
return { data }
|
||||
}
|
||||
|
||||
const englishDataList = data.map(
|
||||
(frenchData: any) =>
|
||||
_.mapValues(
|
||||
translation,
|
||||
(frenchProp: string) => frenchData[frenchProp]
|
||||
) as Element
|
||||
)
|
||||
return { data: englishDataList }
|
||||
return { data }
|
||||
} catch (error) {
|
||||
return { error: error as Error }
|
||||
}
|
||||
@ -84,27 +65,12 @@ export default function getServiceAccessors<
|
||||
}
|
||||
return async (volunteerWithoutId: ElementNoId): Promise<ElementGetResponse> => {
|
||||
try {
|
||||
const invertedTranslationWithoutId = _.invert(_.omit(translation, "id"))
|
||||
const frenchDataWithoutId = _.mapValues(
|
||||
invertedTranslationWithoutId,
|
||||
(englishProp: string, _frenchProp: string) =>
|
||||
(volunteerWithoutId as any)[englishProp]
|
||||
)
|
||||
|
||||
const { data } = await axios.post(
|
||||
`${config.API_URL}/${elementName}Add`,
|
||||
frenchDataWithoutId,
|
||||
volunteerWithoutId,
|
||||
axiosConfig
|
||||
)
|
||||
if (!data) {
|
||||
return { data }
|
||||
}
|
||||
|
||||
const englishData = _.mapValues(
|
||||
translation,
|
||||
(frenchProp: string) => data[frenchProp]
|
||||
) as Element
|
||||
return { data: englishData }
|
||||
return { data }
|
||||
} catch (error) {
|
||||
return { error: error as Error }
|
||||
}
|
||||
@ -121,26 +87,12 @@ export default function getServiceAccessors<
|
||||
}
|
||||
return async (volunteer: Element): Promise<ElementGetResponse> => {
|
||||
try {
|
||||
const invertedTranslation = _.invert(translation)
|
||||
const frenchData = _.mapValues(
|
||||
invertedTranslation,
|
||||
(englishProp: string) => (volunteer as any)[englishProp]
|
||||
)
|
||||
|
||||
const { data } = await axios.post(
|
||||
`${config.API_URL}/${elementName}Set`,
|
||||
frenchData,
|
||||
volunteer,
|
||||
axiosConfig
|
||||
)
|
||||
if (!data) {
|
||||
return { data }
|
||||
}
|
||||
|
||||
const englishData = _.mapValues(
|
||||
translation,
|
||||
(frenchProp: string) => data[frenchProp]
|
||||
) as Element
|
||||
return { data: englishData }
|
||||
return { data }
|
||||
} catch (error) {
|
||||
return { error: error as Error }
|
||||
}
|
||||
@ -168,5 +120,30 @@ export default function getServiceAccessors<
|
||||
}
|
||||
}
|
||||
|
||||
return { listGet, get, set, add, countGet }
|
||||
function customPost(apiName: string): (params: any) => Promise<{
|
||||
data?: Element
|
||||
error?: Error
|
||||
}> {
|
||||
interface ElementGetResponse {
|
||||
data?: Element
|
||||
error?: Error
|
||||
}
|
||||
return async (params: any): Promise<ElementGetResponse> => {
|
||||
try {
|
||||
const { data } = await axios.post(
|
||||
`${config.API_URL}/${elementName}${apiName}`,
|
||||
params,
|
||||
axiosConfig
|
||||
)
|
||||
if (data.error) {
|
||||
throw Error(data.error)
|
||||
}
|
||||
return { data }
|
||||
} catch (error) {
|
||||
return { error: error as Error }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { listGet, get, set, add, countGet, customPost }
|
||||
}
|
||||
|
@ -2,15 +2,15 @@ import { AxiosRequestConfig } from "axios"
|
||||
|
||||
const storage: any = localStorage
|
||||
|
||||
export const axiosConfig: AxiosRequestConfig = {
|
||||
headers: {},
|
||||
}
|
||||
|
||||
const jwt: string | null = storage?.getItem("id_token")
|
||||
if (jwt) {
|
||||
setJWT(jwt)
|
||||
}
|
||||
|
||||
export const axiosConfig: AxiosRequestConfig = {
|
||||
headers: {},
|
||||
}
|
||||
|
||||
export function setJWT(token: string): void {
|
||||
axiosConfig.headers.Authorization = `Bearer ${token}`
|
||||
storage?.setItem("id_token", token)
|
||||
|
@ -54,10 +54,7 @@ const elementName = "JavGame"
|
||||
|
||||
export type JavGameWithoutId = Omit<JavGame, "id">
|
||||
|
||||
const { listGet, get, set, add } = getServiceAccessors<JavGameWithoutId, JavGame>(
|
||||
elementName,
|
||||
translationJavGame
|
||||
)
|
||||
const { listGet, get, set, add } = getServiceAccessors<JavGameWithoutId, JavGame>(elementName)
|
||||
|
||||
export const javGameListGet = listGet()
|
||||
export const javGameGet = get()
|
||||
|
@ -28,12 +28,16 @@ export const translationPreVolunteer: { [k in keyof PreVolunteer]: string } = {
|
||||
|
||||
const elementName = "PreVolunteer"
|
||||
|
||||
export const emailRegexp =
|
||||
/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i
|
||||
export const passwordMinLength = 4
|
||||
|
||||
export type PreVolunteerWithoutId = Omit<PreVolunteer, "id">
|
||||
|
||||
const { listGet, get, set, add, countGet } = getServiceAccessors<
|
||||
PreVolunteerWithoutId,
|
||||
PreVolunteer
|
||||
>(elementName, translationPreVolunteer)
|
||||
>(elementName)
|
||||
|
||||
export const preVolunteerListGet = listGet()
|
||||
export const preVolunteerGet = get()
|
||||
|
@ -23,7 +23,7 @@ export class Volunteer {
|
||||
|
||||
comment = ""
|
||||
|
||||
timestamp = ""
|
||||
timestamp = new Date()
|
||||
|
||||
password = ""
|
||||
}
|
||||
@ -50,22 +50,18 @@ export const emailRegexp =
|
||||
/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i
|
||||
export const passwordMinLength = 4
|
||||
|
||||
export interface VolunteerLogin {
|
||||
volunteer?: {
|
||||
firstname: string
|
||||
}
|
||||
jwt?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export type VolunteerWithoutId = Omit<Volunteer, "id">
|
||||
|
||||
const { listGet, get, set, add } = getServiceAccessors<VolunteerWithoutId, Volunteer>(
|
||||
elementName,
|
||||
translationVolunteer
|
||||
)
|
||||
const accessors = getServiceAccessors<VolunteerWithoutId, Volunteer>(elementName)
|
||||
const { listGet, get, set, add } = accessors
|
||||
|
||||
export const volunteerListGet = listGet()
|
||||
export const volunteerGet = get()
|
||||
export const volunteerAdd = add()
|
||||
export const volunteerSet = set()
|
||||
|
||||
export interface VolunteerLogin {
|
||||
firstname: string
|
||||
jwt: string
|
||||
}
|
||||
export const volunteerLogin = accessors.customPost("Login")
|
||||
|
@ -27,10 +27,7 @@ const elementName = "Wish"
|
||||
|
||||
export type WishWithoutId = Omit<Wish, "id">
|
||||
|
||||
const { listGet, get, set, add } = getServiceAccessors<WishWithoutId, Wish>(
|
||||
elementName,
|
||||
translationWish
|
||||
)
|
||||
const { listGet, get, set, add } = getServiceAccessors<WishWithoutId, Wish>(elementName)
|
||||
|
||||
export const wishListGet = listGet()
|
||||
export const wishGet = get()
|
||||
|
@ -13,27 +13,7 @@ import { JavGame } from "../../services/javGames"
|
||||
|
||||
jest.mock("axios")
|
||||
|
||||
const mockFrenchData: any[] = [
|
||||
{
|
||||
id: 5,
|
||||
titre: "6 qui prend!",
|
||||
auteur: "Wolfgang Kramer",
|
||||
editeur: "(uncredited) , Design Edge , B",
|
||||
minJoueurs: 2,
|
||||
maxJoueurs: 10,
|
||||
duree: 45,
|
||||
type: "Ambiance",
|
||||
poufpaf: "0-9-2/6-qui-prend-6-nimmt",
|
||||
bggPhoto:
|
||||
"https://cf.geekdo-images.com/thumb/img/lzczxR5cw7an7tRWeHdOrRtLyes=/fit-in/200x150/pic772547.jpg",
|
||||
bggId: 432,
|
||||
exemplaires: 1,
|
||||
dispoPret: 1,
|
||||
nonRangee: 0,
|
||||
ean: "3421272101313",
|
||||
},
|
||||
]
|
||||
const mockEnglishData: JavGame[] = [
|
||||
const mockData: JavGame[] = [
|
||||
{
|
||||
id: 5,
|
||||
title: "6 qui prend!",
|
||||
@ -70,14 +50,12 @@ describe("JavGameList reducer", () => {
|
||||
})
|
||||
|
||||
it("should handle success correctly", () => {
|
||||
expect(JavGameList(undefined, { type: getSuccess.type, payload: mockEnglishData })).toEqual(
|
||||
{
|
||||
...initialState,
|
||||
readyStatus: "success",
|
||||
ids: _.map(mockEnglishData, "id"),
|
||||
entities: _.keyBy(mockEnglishData, "id"),
|
||||
}
|
||||
)
|
||||
expect(JavGameList(undefined, { type: getSuccess.type, payload: mockData })).toEqual({
|
||||
...initialState,
|
||||
readyStatus: "success",
|
||||
ids: _.map(mockData, "id"),
|
||||
entities: _.keyBy(mockData, "id"),
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle failure correctly", () => {
|
||||
@ -94,11 +72,11 @@ describe("JavGameList action", () => {
|
||||
const { dispatch, getActions } = mockStore()
|
||||
const expectedActions = [
|
||||
{ type: getRequesting.type, payload: undefined },
|
||||
{ type: getSuccess.type, payload: mockEnglishData },
|
||||
{ type: getSuccess.type, payload: mockData },
|
||||
]
|
||||
|
||||
// @ts-expect-error
|
||||
axios.get.mockResolvedValue({ data: mockFrenchData })
|
||||
axios.get.mockResolvedValue({ data: mockData })
|
||||
|
||||
await dispatch(fetchJavGameList())
|
||||
expect(getActions()).toEqual(expectedActions)
|
||||
|
@ -12,23 +12,7 @@ import { Volunteer } from "../../services/volunteers"
|
||||
|
||||
jest.mock("axios")
|
||||
|
||||
const mockFrenchData: any = {
|
||||
id: 1,
|
||||
nom: "Aupeix",
|
||||
prenom: "Amélie",
|
||||
mail: "pakouille.lakouille@yahoo.fr",
|
||||
telephone: "0675650392",
|
||||
photo: "images/volunteers/$taille/amélie_aupeix.jpg",
|
||||
alimentation: "Végétarien",
|
||||
majeur: 1,
|
||||
privilege: 0,
|
||||
actif: 0,
|
||||
commentaire: "",
|
||||
horodatage: "0000-00-00",
|
||||
passe: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O",
|
||||
}
|
||||
|
||||
const mockEnglishData: Volunteer = {
|
||||
const mockData: Volunteer = {
|
||||
id: 1,
|
||||
lastname: "Aupeix",
|
||||
firstname: "Amélie",
|
||||
@ -40,10 +24,10 @@ const mockEnglishData: Volunteer = {
|
||||
privileges: 0,
|
||||
active: 0,
|
||||
comment: "",
|
||||
timestamp: "0000-00-00",
|
||||
timestamp: new Date(0),
|
||||
password: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O",
|
||||
}
|
||||
const { id } = mockEnglishData
|
||||
const { id } = mockData
|
||||
const mockError = "Oops! Something went wrong."
|
||||
|
||||
describe("volunteer reducer", () => {
|
||||
@ -62,9 +46,9 @@ describe("volunteer reducer", () => {
|
||||
expect(
|
||||
volunteer(undefined, {
|
||||
type: getSuccess.type,
|
||||
payload: mockEnglishData,
|
||||
payload: mockData,
|
||||
})
|
||||
).toEqual({ readyStatus: "success", entity: mockEnglishData })
|
||||
).toEqual({ readyStatus: "success", entity: mockData })
|
||||
})
|
||||
|
||||
it("should handle failure correctly", () => {
|
||||
@ -82,11 +66,11 @@ describe("volunteer action", () => {
|
||||
const { dispatch, getActions } = mockStore()
|
||||
const expectedActions = [
|
||||
{ type: getRequesting.type, payload: undefined },
|
||||
{ type: getSuccess.type, payload: mockEnglishData },
|
||||
{ type: getSuccess.type, payload: mockData },
|
||||
]
|
||||
|
||||
// @ts-expect-error
|
||||
axios.get.mockResolvedValue({ data: mockFrenchData })
|
||||
axios.get.mockResolvedValue({ data: mockData })
|
||||
|
||||
await dispatch(fetchVolunteer(id))
|
||||
expect(getActions()).toEqual(expectedActions)
|
||||
|
@ -13,25 +13,7 @@ import { Volunteer } from "../../services/volunteers"
|
||||
|
||||
jest.mock("axios")
|
||||
|
||||
const mockFrenchData: any[] = [
|
||||
{
|
||||
id: 1,
|
||||
nom: "Aupeix",
|
||||
prenom: "Amélie",
|
||||
mail: "pakouille.lakouille@yahoo.fr",
|
||||
telephone: "0675650392",
|
||||
photo: "images/volunteers/$taille/amélie_aupeix.jpg",
|
||||
alimentation: "Végétarien",
|
||||
majeur: 1,
|
||||
privilege: 0,
|
||||
actif: 0,
|
||||
commentaire: "",
|
||||
horodatage: "0000-00-00",
|
||||
passe: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O",
|
||||
},
|
||||
]
|
||||
|
||||
const mockEnglishData: Volunteer[] = [
|
||||
const mockData: Volunteer[] = [
|
||||
{
|
||||
id: 1,
|
||||
lastname: "Aupeix",
|
||||
@ -44,7 +26,7 @@ const mockEnglishData: Volunteer[] = [
|
||||
privileges: 0,
|
||||
active: 0,
|
||||
comment: "",
|
||||
timestamp: "0000-00-00",
|
||||
timestamp: new Date(0),
|
||||
password: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O",
|
||||
},
|
||||
]
|
||||
@ -65,13 +47,11 @@ describe("volunteerList reducer", () => {
|
||||
})
|
||||
|
||||
it("should handle success correctly", () => {
|
||||
expect(
|
||||
volunteerList(undefined, { type: getSuccess.type, payload: mockEnglishData })
|
||||
).toEqual({
|
||||
expect(volunteerList(undefined, { type: getSuccess.type, payload: mockData })).toEqual({
|
||||
...initialState,
|
||||
readyStatus: "success",
|
||||
ids: _.map(mockEnglishData, "id"),
|
||||
entities: _.keyBy(mockEnglishData, "id"),
|
||||
ids: _.map(mockData, "id"),
|
||||
entities: _.keyBy(mockData, "id"),
|
||||
})
|
||||
})
|
||||
|
||||
@ -89,11 +69,11 @@ describe("volunteerList action", () => {
|
||||
const { dispatch, getActions } = mockStore()
|
||||
const expectedActions = [
|
||||
{ type: getRequesting.type, payload: undefined },
|
||||
{ type: getSuccess.type, payload: mockEnglishData },
|
||||
{ type: getSuccess.type, payload: mockData },
|
||||
]
|
||||
|
||||
// @ts-expect-error
|
||||
axios.get.mockResolvedValue({ data: mockFrenchData })
|
||||
axios.get.mockResolvedValue({ data: mockData })
|
||||
|
||||
await dispatch(fetchVolunteerList())
|
||||
expect(getActions()).toEqual(expectedActions)
|
||||
|
@ -32,7 +32,8 @@ export const fetchPreVolunteerCount = elementValueFetch(
|
||||
getRequesting,
|
||||
getSuccess,
|
||||
getFailure,
|
||||
(error: Error) => toastError(`Erreur lors du chargement des volunteers: ${error.message}`)
|
||||
(error: Error) =>
|
||||
toastError(`Erreur lors du chargement des bénévoles potentiels: ${error.message}`)
|
||||
)
|
||||
|
||||
const shouldFetchPreVolunteerCount = (state: AppState) =>
|
||||
|
@ -8,6 +8,7 @@ import volunteer from "./volunteer"
|
||||
import volunteerAdd from "./volunteerAdd"
|
||||
import volunteerList from "./volunteerList"
|
||||
import volunteerSet from "./volunteerSet"
|
||||
import volunteerLogin from "./volunteerLogin"
|
||||
import preVolunteerAdd from "./preVolunteerAdd"
|
||||
import preVolunteerCount from "./preVolunteerCount"
|
||||
|
||||
@ -21,6 +22,7 @@ export default (history: History) => ({
|
||||
volunteerAdd,
|
||||
volunteerList,
|
||||
volunteerSet,
|
||||
volunteerLogin,
|
||||
preVolunteerAdd,
|
||||
preVolunteerCount,
|
||||
router: connectRouter(history) as any,
|
||||
|
@ -33,32 +33,32 @@ export function toastSuccess(message: string): void {
|
||||
}
|
||||
|
||||
export function elementFetch<Element>(
|
||||
elementService: (id: number) => Promise<{
|
||||
elementService: (...idArgs: any[]) => Promise<{
|
||||
data?: Element | undefined
|
||||
error?: Error | undefined
|
||||
}>,
|
||||
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 */
|
||||
}
|
||||
): (id: number) => AppThunk {
|
||||
return (id: number): AppThunk =>
|
||||
errorMessage?: (error: Error) => void,
|
||||
successMessage?: (data: Element) => void
|
||||
): (...idArgs: any[]) => AppThunk {
|
||||
return (...idArgs: any[]): AppThunk =>
|
||||
async (dispatch) => {
|
||||
dispatch(getRequesting())
|
||||
|
||||
const { error, data } = await elementService(id)
|
||||
const { error, data } = await elementService(...idArgs)
|
||||
|
||||
if (error) {
|
||||
dispatch(getFailure(error.message))
|
||||
errorMessage(error)
|
||||
if (errorMessage) {
|
||||
errorMessage(error)
|
||||
}
|
||||
} else {
|
||||
dispatch(getSuccess(data as Element))
|
||||
successMessage()
|
||||
if (successMessage) {
|
||||
successMessage(data as Element)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ export const fetchVolunteer = elementFetch(
|
||||
getRequesting,
|
||||
getSuccess,
|
||||
getFailure,
|
||||
(error: Error) => toastError(`Erreur lors du chargement d'un volunteer: ${error.message}`)
|
||||
(error: Error) => toastError(`Erreur lors du chargement d'un bénévole: ${error.message}`)
|
||||
)
|
||||
|
||||
const shouldFetchVolunteer = (state: AppState, id: number) =>
|
||||
|
@ -33,6 +33,6 @@ export const fetchVolunteerAdd = elementAddFetch(
|
||||
getRequesting,
|
||||
getSuccess,
|
||||
getFailure,
|
||||
(error: Error) => toastError(`Erreur lors de l'ajout d'une volunteer: ${error.message}`),
|
||||
(error: Error) => toastError(`Erreur lors de l'ajout d'un bénévole: ${error.message}`),
|
||||
() => toastSuccess("Volunteer ajoutée !")
|
||||
)
|
||||
|
@ -36,7 +36,7 @@ export const fetchVolunteerList = elementListFetch(
|
||||
getRequesting,
|
||||
getSuccess,
|
||||
getFailure,
|
||||
(error: Error) => toastError(`Erreur lors du chargement des volunteers: ${error.message}`)
|
||||
(error: Error) => toastError(`Erreur lors du chargement des bénévoles: ${error.message}`)
|
||||
)
|
||||
|
||||
const shouldFetchVolunteerList = (state: AppState) => state.volunteerList.readyStatus !== "success"
|
||||
|
43
src/store/volunteerLogin.ts
Normal file
43
src/store/volunteerLogin.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit"
|
||||
|
||||
import { StateRequest, elementFetch } from "./utils"
|
||||
import { VolunteerLogin, volunteerLogin } from "../services/volunteers"
|
||||
import { setJWT } from "../services/auth"
|
||||
|
||||
type StateVolunteer = { entity?: VolunteerLogin } & StateRequest
|
||||
|
||||
export const initialState: StateVolunteer = {
|
||||
readyStatus: "idle",
|
||||
}
|
||||
|
||||
const volunteerLoginSlice = createSlice({
|
||||
name: "volunteer",
|
||||
initialState,
|
||||
reducers: {
|
||||
getRequesting: (_) => ({
|
||||
readyStatus: "request",
|
||||
}),
|
||||
getSuccess: (_, { payload }: PayloadAction<VolunteerLogin>) => ({
|
||||
readyStatus: "success",
|
||||
entity: payload,
|
||||
}),
|
||||
getFailure: (_, { payload }: PayloadAction<string>) => ({
|
||||
readyStatus: "failure",
|
||||
error: payload,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export default volunteerLoginSlice.reducer
|
||||
export const { getRequesting, getSuccess, getFailure } = volunteerLoginSlice.actions
|
||||
|
||||
export const fetchVolunteerLogin = elementFetch(
|
||||
volunteerLogin,
|
||||
getRequesting,
|
||||
getSuccess,
|
||||
getFailure,
|
||||
undefined,
|
||||
(login: VolunteerLogin) => {
|
||||
setJWT(login.jwt)
|
||||
}
|
||||
)
|
@ -33,6 +33,6 @@ export const fetchVolunteerSet = elementSet(
|
||||
getRequesting,
|
||||
getSuccess,
|
||||
getFailure,
|
||||
(error: Error) => toastError(`Erreur lors de la modification d'un volunteer: ${error.message}`),
|
||||
() => toastSuccess("Volunteer modifié !")
|
||||
(error: Error) => toastError(`Erreur lors de la modification d'un bénévole: ${error.message}`),
|
||||
() => toastSuccess("Bénévole modifié !")
|
||||
)
|
||||
|
@ -33,6 +33,6 @@ export const fetchWishAdd = elementAddFetch(
|
||||
getRequesting,
|
||||
getSuccess,
|
||||
getFailure,
|
||||
(error: Error) => toastError(`Erreur lors de l'ajout d'une wish: ${error.message}`),
|
||||
() => toastSuccess("Wish ajoutée !")
|
||||
(error: Error) => toastError(`Erreur lors de l'ajout d'une envie: ${error.message}`),
|
||||
() => toastSuccess("Envie ajoutée !")
|
||||
)
|
||||
|
@ -34,7 +34,7 @@ export const fetchWishList = elementListFetch(
|
||||
getRequesting,
|
||||
getSuccess,
|
||||
getFailure,
|
||||
(error: Error) => toastError(`Erreur lors du chargement des wishes: ${error.message}`)
|
||||
(error: Error) => toastError(`Erreur lors du chargement des envies: ${error.message}`)
|
||||
)
|
||||
|
||||
const shouldFetchWishList = (state: AppState) => state.wishList.readyStatus !== "success"
|
||||
|
@ -16,3 +16,31 @@ export function validEmail(email: string): boolean {
|
||||
email
|
||||
)
|
||||
}
|
||||
|
||||
export function validMobile(mobile: string): boolean {
|
||||
return (
|
||||
/^\(?\+?[-0-9. ()]+$/.test(trim(mobile)) &&
|
||||
trim(mobile).replace(/[^0-9]+/g, "").length >= 10
|
||||
)
|
||||
}
|
||||
|
||||
export function canonicalMobile(mobile: string): string {
|
||||
if (!validMobile(mobile)) {
|
||||
return ""
|
||||
}
|
||||
let clean = trim(mobile).replace(/[-0-9. ()+]$/g, "")
|
||||
if (clean.length === 11) {
|
||||
clean = clean.replace(/^33/, "0")
|
||||
}
|
||||
if (clean.length < 10) {
|
||||
return ""
|
||||
}
|
||||
if (clean.length === 10) {
|
||||
return clean.replace(/([0-9]{2})/g, "$1 ").replace(/ $/, "")
|
||||
}
|
||||
return clean
|
||||
}
|
||||
|
||||
export function trim(src: string): string {
|
||||
return typeof src !== "string" ? "" : src.replace(/^\s*/, "").replace(/\s*$/, "")
|
||||
}
|
||||
|
@ -9138,6 +9138,11 @@ redent@^3.0.0:
|
||||
indent-string "^4.0.0"
|
||||
strip-indent "^3.0.0"
|
||||
|
||||
redux-devtools-extension@^2.13.9:
|
||||
version "2.13.9"
|
||||
resolved "https://registry.yarnpkg.com/redux-devtools-extension/-/redux-devtools-extension-2.13.9.tgz#6b764e8028b507adcb75a1cae790f71e6be08ae7"
|
||||
integrity sha512-cNJ8Q/EtjhQaZ71c8I9+BPySIBVEKssbPpskBfsXqb8HJ002A3KRVHfeRzwRo6mGPqsm7XuHTqNSNeS1Khig0A==
|
||||
|
||||
redux-mock-store@^1.5.4:
|
||||
version "1.5.4"
|
||||
resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.5.4.tgz#90d02495fd918ddbaa96b83aef626287c9ab5872"
|
||||
|
Loading…
x
Reference in New Issue
Block a user