mirror of
https://github.com/Paris-est-Ludique/intranet.git
synced 2025-06-09 17:14:21 +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-router-dom": "^5.3.0",
|
||||||
"react-toastify": "^8.1.0",
|
"react-toastify": "^8.1.0",
|
||||||
"readline": "^1.3.0",
|
"readline": "^1.3.0",
|
||||||
|
"redux-devtools-extension": "^2.13.9",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"serialize-javascript": "^6.0.0",
|
"serialize-javascript": "^6.0.0",
|
||||||
"serve-favicon": "^2.5.0"
|
"serve-favicon": "^2.5.0"
|
||||||
|
@ -1,8 +1,16 @@
|
|||||||
import React, { memo, useCallback } from "react"
|
import React, { memo, useCallback } from "react"
|
||||||
import styles from "./styles.module.scss"
|
import styles from "./styles.module.scss"
|
||||||
|
import { AppDispatch } from "../../store"
|
||||||
|
import { fetchVolunteerLogin } from "../../store/volunteerLogin"
|
||||||
|
|
||||||
const LoginForm = (): JSX.Element => {
|
interface Props {
|
||||||
const onSubmit = useCallback((event: React.SyntheticEvent): void => {
|
dispatch: AppDispatch
|
||||||
|
error: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoginForm = ({ dispatch, error }: Props): JSX.Element => {
|
||||||
|
const onSubmit = useCallback(
|
||||||
|
(event: React.SyntheticEvent): void => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
const target = event.target as typeof event.target & {
|
const target = event.target as typeof event.target & {
|
||||||
email: { value: string }
|
email: { value: string }
|
||||||
@ -11,10 +19,10 @@ const LoginForm = (): JSX.Element => {
|
|||||||
const email = target.email.value
|
const email = target.email.value
|
||||||
const password = target.password.value
|
const password = target.password.value
|
||||||
|
|
||||||
console.log("email and password checked", email, password)
|
dispatch(fetchVolunteerLogin({ email, password }))
|
||||||
|
},
|
||||||
// call service with email & password
|
[dispatch]
|
||||||
}, [])
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
@ -23,15 +31,16 @@ const LoginForm = (): JSX.Element => {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.formLine} key="line-email">
|
<div className={styles.formLine} key="line-email">
|
||||||
<label htmlFor="email">Email</label>
|
<label htmlFor="email">Email</label>
|
||||||
<input type="email" id="email" />
|
<input type="email" id="email" name="utilisateur" />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.formLine} key="line-password">
|
<div className={styles.formLine} key="line-password">
|
||||||
<label htmlFor="password">Mot de passe</label>
|
<label htmlFor="password">Mot de passe</label>
|
||||||
<input type="password" id="password" />
|
<input type="password" id="password" name="motdepasse" />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.formButtons}>
|
<div className={styles.formButtons}>
|
||||||
<button type="submit">Connexion</button>
|
<button type="submit">Connexion</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={styles.error}>{error}</div>
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -24,3 +24,7 @@
|
|||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: rgb(255, 0, 0);
|
||||||
|
}
|
||||||
|
@ -23,7 +23,7 @@ describe("<VolunteerInfo />", () => {
|
|||||||
privileges: 0,
|
privileges: 0,
|
||||||
active: 0,
|
active: 0,
|
||||||
comment: "",
|
comment: "",
|
||||||
timestamp: "0000-00-00",
|
timestamp: new Date(0),
|
||||||
password: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
|
password: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -24,7 +24,7 @@ describe("<VolunteerList />", () => {
|
|||||||
privileges: 0,
|
privileges: 0,
|
||||||
active: 0,
|
active: 0,
|
||||||
comment: "",
|
comment: "",
|
||||||
timestamp: "0000-00-00",
|
timestamp: new Date(0),
|
||||||
password:
|
password:
|
||||||
"$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
|
"$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
|
||||||
},
|
},
|
||||||
|
@ -25,7 +25,7 @@ describe("<SetVolunteer />", () => {
|
|||||||
privileges: 0,
|
privileges: 0,
|
||||||
active: 0,
|
active: 0,
|
||||||
comment: "",
|
comment: "",
|
||||||
timestamp: "0000-00-00",
|
timestamp: new Date(0),
|
||||||
password: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
|
password: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -43,7 +43,7 @@ const WishAdd = ({ dispatch }: Props) => {
|
|||||||
setTeams([""])
|
setTeams([""])
|
||||||
setAddedDate("")
|
setAddedDate("")
|
||||||
} else {
|
} 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",
|
position: "top-center",
|
||||||
autoClose: 6000,
|
autoClose: 6000,
|
||||||
hideProgressBar: true,
|
hideProgressBar: true,
|
||||||
|
@ -1,18 +1,26 @@
|
|||||||
import { RouteComponentProps } from "react-router-dom"
|
import { RouteComponentProps } from "react-router-dom"
|
||||||
|
import { useDispatch, useSelector, shallowEqual } from "react-redux"
|
||||||
import React, { memo } from "react"
|
import React, { memo } from "react"
|
||||||
import { Helmet } from "react-helmet"
|
import { Helmet } from "react-helmet"
|
||||||
import styles from "./styles.module.scss"
|
|
||||||
|
import { AppState } from "../../store"
|
||||||
import LoginForm from "../../components/LoginForm/LoginForm"
|
import LoginForm from "../../components/LoginForm/LoginForm"
|
||||||
|
import styles from "./styles.module.scss"
|
||||||
|
|
||||||
export type Props = RouteComponentProps
|
export type Props = RouteComponentProps
|
||||||
|
|
||||||
const RegisterPage: React.FC<Props> = (): JSX.Element => (
|
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.loginPage}>
|
||||||
<div className={styles.loginContent}>
|
<div className={styles.loginContent}>
|
||||||
<Helmet title="RegisterPage" />
|
<Helmet title="RegisterPage" />
|
||||||
<LoginForm />
|
<LoginForm dispatch={dispatch} error={loginError || ""} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default memo(RegisterPage)
|
export default memo(RegisterPage)
|
||||||
|
@ -68,12 +68,15 @@ export class Sheet<
|
|||||||
|
|
||||||
frenchSpecimen: Element
|
frenchSpecimen: Element
|
||||||
|
|
||||||
|
invertedTranslation: { [k: string]: string }
|
||||||
|
|
||||||
// eslint-disable-next-line no-useless-constructor
|
// eslint-disable-next-line no-useless-constructor
|
||||||
constructor(
|
constructor(
|
||||||
readonly name: keyof SheetNames,
|
readonly name: keyof SheetNames,
|
||||||
readonly specimen: Element,
|
readonly specimen: Element,
|
||||||
readonly translation: { [k in keyof Element]: string }
|
readonly translation: { [k in keyof Element]: string }
|
||||||
) {
|
) {
|
||||||
|
this.invertedTranslation = _.invert(this.translation)
|
||||||
this.sheetName = sheetNames[name]
|
this.sheetName = sheetNames[name]
|
||||||
this.frenchSpecimen = _.mapValues(
|
this.frenchSpecimen = _.mapValues(
|
||||||
_.invert(translation),
|
_.invert(translation),
|
||||||
@ -175,7 +178,7 @@ export class Sheet<
|
|||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
const elements = this._state as Element[]
|
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,
|
keyof Element,
|
||||||
string
|
string
|
||||||
>
|
>
|
||||||
@ -185,17 +188,22 @@ export class Sheet<
|
|||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
const row = rows[rowid]
|
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) {
|
if (!row) {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await sheet.addRow(stringifiedRow)
|
await sheet.addRow(stringifiedRow)
|
||||||
} else {
|
} else {
|
||||||
const keys = Object.keys(stringifiedRow)
|
const keys = Object.keys(stringifiedRow)
|
||||||
const sameCells = _.every(
|
const sameCells = _.every(keys, (key: keyof Element) => {
|
||||||
keys,
|
const rawVal = row[key as string]
|
||||||
(key: keyof Element) => row[key as string] === stringifiedRow[key]
|
const val: string = rawVal === undefined ? "" : rawVal
|
||||||
)
|
return val === stringifiedRow[key]
|
||||||
|
})
|
||||||
if (!sameCells) {
|
if (!sameCells) {
|
||||||
keys.forEach((key) => {
|
keys.forEach((key) => {
|
||||||
row[key] = stringifiedRow[key as keyof Element]
|
row[key] = stringifiedRow[key as keyof Element]
|
||||||
@ -238,9 +246,13 @@ export class Sheet<
|
|||||||
keyof Element,
|
keyof Element,
|
||||||
string
|
string
|
||||||
>
|
>
|
||||||
const element = this.parseElement(stringifiedElement, types)
|
const frenchData: any = this.parseElement(stringifiedElement, types)
|
||||||
if (element !== undefined) {
|
if (frenchData !== undefined) {
|
||||||
elements.push(element)
|
const englishElement = _.mapValues(
|
||||||
|
this.translation,
|
||||||
|
(frenchProp: string) => frenchData[frenchProp]
|
||||||
|
) as Element
|
||||||
|
elements.push(englishElement)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -293,18 +305,13 @@ export class Sheet<
|
|||||||
if (rawProp === undefined) {
|
if (rawProp === undefined) {
|
||||||
element[prop] = undefined
|
element[prop] = undefined
|
||||||
} else {
|
} else {
|
||||||
// eslint-disable-next-line no-case-declarations
|
try {
|
||||||
const matchDate = rawProp.match(/^([0-9]+)\/([0-9]+)\/([0-9]+)$/)
|
element[prop] = parseDate(rawProp)
|
||||||
if (!matchDate) {
|
} catch (e: any) {
|
||||||
throw new Error(
|
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
|
break
|
||||||
|
|
||||||
@ -345,27 +352,15 @@ export class Sheet<
|
|||||||
const rawDates = rawProp.split(delimiter)
|
const rawDates = rawProp.split(delimiter)
|
||||||
element[prop] = []
|
element[prop] = []
|
||||||
// eslint-disable-next-line no-case-declarations
|
// eslint-disable-next-line no-case-declarations
|
||||||
const rightFormat = rawDates.every((rawDate) => {
|
rawDates.forEach((rawDate) => {
|
||||||
const matchDateArray = rawDate.match(
|
try {
|
||||||
/^([0-9]+)\/([0-9]+)\/([0-9]+)$/
|
element[prop].push(parseDate(rawDate))
|
||||||
)
|
} catch (e: any) {
|
||||||
if (!matchDateArray) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
element[prop].push(
|
|
||||||
new Date(
|
|
||||||
+matchDateArray[3],
|
|
||||||
+matchDateArray[2] - 1,
|
|
||||||
+matchDateArray[1]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
if (!rightFormat) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`One array item is not a date for val ${rawProp} in sheet ${this.name} at prop ${prop}`
|
`${e.message} in sheet ${this.name} at prop ${prop}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -391,7 +386,7 @@ export class Sheet<
|
|||||||
const value = element[prop as keyof Element]
|
const value = element[prop as keyof Element]
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "string":
|
case "string":
|
||||||
stringifiedElement[prop as keyof Element] = Sheet.formulaSafe(`${value}`)
|
stringifiedElement[prop as keyof Element] = formulaSafe(`${value}`)
|
||||||
break
|
break
|
||||||
|
|
||||||
case "number":
|
case "number":
|
||||||
@ -403,7 +398,7 @@ export class Sheet<
|
|||||||
break
|
break
|
||||||
|
|
||||||
case "date":
|
case "date":
|
||||||
stringifiedElement[prop as keyof Element] = Sheet.stringifiedDate(value)
|
stringifiedElement[prop as keyof Element] = stringifiedDate(value)
|
||||||
break
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -426,7 +421,7 @@ export class Sheet<
|
|||||||
if (!_.every(value, _.isString)) {
|
if (!_.every(value, _.isString)) {
|
||||||
throw new Error(`Each date of ${value} is not a string`)
|
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)
|
value.join(delimiter)
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
@ -453,10 +448,7 @@ export class Sheet<
|
|||||||
}
|
}
|
||||||
stringifiedElement[prop as keyof Element] = _.map(
|
stringifiedElement[prop as keyof Element] = _.map(
|
||||||
value,
|
value,
|
||||||
(val) =>
|
stringifiedDate
|
||||||
`${val.getDate()}/${
|
|
||||||
val.getMonth() + 1
|
|
||||||
}/${val.getFullYear()}`
|
|
||||||
).join(delimiter)
|
).join(delimiter)
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -472,12 +464,13 @@ export class Sheet<
|
|||||||
|
|
||||||
return rawElement
|
return rawElement
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static formulaSafe(value: string): string {
|
function formulaSafe(value: string): string {
|
||||||
return value.replace(/^=+/, "")
|
return value === undefined ? "" : value.replace(/^=+/, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
private static stringifiedDate(value: unknown): string {
|
function stringifiedDate(value: unknown): string {
|
||||||
let date: Date
|
let date: Date
|
||||||
if (value instanceof Date) {
|
if (value instanceof Date) {
|
||||||
date = value
|
date = value
|
||||||
@ -490,6 +483,14 @@ export class Sheet<
|
|||||||
} else {
|
} else {
|
||||||
throw new Error("Wrong date format in stringifyElement")
|
throw new Error("Wrong date format in stringifyElement")
|
||||||
}
|
}
|
||||||
return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}`
|
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() {
|
add() {
|
||||||
return async (request: Request, response: Response, _next: NextFunction): Promise<void> => {
|
return async (request: Request, response: Response, _next: NextFunction): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
this.sheet.add(request.body)
|
const element: Element = await 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)
|
|
||||||
if (element) {
|
if (element) {
|
||||||
response.status(200).json(element)
|
response.status(200).json(element)
|
||||||
}
|
}
|
||||||
@ -76,17 +72,16 @@ export default class ExpressAccessors<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customGet(transformer: (list?: Element[]) => any) {
|
// transformer can be an async function
|
||||||
return async (
|
customGet(
|
||||||
_request: Request,
|
transformer: (list: Element[] | undefined, body?: Request["body"]) => Promise<any> | any
|
||||||
response: Response,
|
) {
|
||||||
_next: NextFunction
|
return async (request: Request, response: Response, _next: NextFunction): Promise<void> => {
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
try {
|
||||||
const elements = await this.sheet.getList()
|
const elements = await this.sheet.getList()
|
||||||
response.status(200).json(transformer(elements))
|
response.status(200).json(await transformer(elements, request.body))
|
||||||
} catch (e: unknown) {
|
} catch (e: any) {
|
||||||
response.status(400).json(e)
|
response.status(200).json({ error: e.message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
|
import { Request } from "express"
|
||||||
|
import bcrypt from "bcrypt"
|
||||||
|
|
||||||
import ExpressAccessors from "./expressAccessors"
|
import ExpressAccessors from "./expressAccessors"
|
||||||
import { Volunteer, VolunteerWithoutId, translationVolunteer } from "../../services/volunteers"
|
import { Volunteer, VolunteerWithoutId, translationVolunteer } from "../../services/volunteers"
|
||||||
|
import { canonicalEmail } from "../../utils/standardization"
|
||||||
|
import { getJwt } from "../secure"
|
||||||
|
|
||||||
const expressAccessor = new ExpressAccessors<VolunteerWithoutId, Volunteer>(
|
const expressAccessor = new ExpressAccessors<VolunteerWithoutId, Volunteer>(
|
||||||
"Volunteers",
|
"Volunteers",
|
||||||
@ -14,3 +19,32 @@ 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(
|
||||||
|
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 { 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 } from "./gsheets/volunteers"
|
import { volunteerGet, volunteerSet, volunteerLogin } from "./gsheets/volunteers"
|
||||||
import loginHandler from "./userManagement/login"
|
|
||||||
import config from "../config"
|
import config from "../config"
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
@ -47,9 +46,6 @@ if (__DEV__) devServer(app)
|
|||||||
|
|
||||||
app.use(express.json())
|
app.use(express.json())
|
||||||
|
|
||||||
// Sign in & up API
|
|
||||||
app.post("/api/user/login", loginHandler)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* APIs
|
* APIs
|
||||||
*/
|
*/
|
||||||
@ -59,6 +55,7 @@ app.get("/WishListGet", wishListGet)
|
|||||||
app.post("/WishAdd", wishAdd)
|
app.post("/WishAdd", wishAdd)
|
||||||
app.post("/PreVolunteerAdd", preVolunteerAdd)
|
app.post("/PreVolunteerAdd", preVolunteerAdd)
|
||||||
app.get("/PreVolunteerCountGet", preVolunteerCountGet)
|
app.get("/PreVolunteerCountGet", preVolunteerCountGet)
|
||||||
|
app.post("/VolunteerLogin", volunteerLogin)
|
||||||
|
|
||||||
// Secured APIs
|
// Secured APIs
|
||||||
app.get("/VolunteerGet", secure as RequestHandler, volunteerGet)
|
app.get("/VolunteerGet", secure as RequestHandler, volunteerGet)
|
||||||
|
@ -53,12 +53,12 @@ async function getSecret() {
|
|||||||
export async function getJwt(email: string): Promise<string> {
|
export async function getJwt(email: string): Promise<string> {
|
||||||
const jwt = sign(
|
const jwt = sign(
|
||||||
{ user: canonicalEmail(email), permissions: [] },
|
{ user: canonicalEmail(email), permissions: [] },
|
||||||
await getSecret(),
|
await getSecret()
|
||||||
__TEST__
|
// __TEST__
|
||||||
? undefined
|
// ? undefined
|
||||||
: {
|
// : {
|
||||||
expiresIn: "365d",
|
// expiresIn: "365d",
|
||||||
}
|
// }
|
||||||
)
|
)
|
||||||
return jwt
|
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 axios from "axios"
|
||||||
import _ from "lodash"
|
|
||||||
|
|
||||||
import config from "../config"
|
import config from "../config"
|
||||||
import { axiosConfig } from "./auth"
|
import { axiosConfig } from "./auth"
|
||||||
@ -12,7 +11,7 @@ export default function getServiceAccessors<
|
|||||||
// 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, translation: { [k in keyof Element]: string }): any {
|
>(elementName: string): any {
|
||||||
function get(): (id: number) => Promise<{
|
function get(): (id: number) => Promise<{
|
||||||
data?: Element
|
data?: Element
|
||||||
error?: Error
|
error?: Error
|
||||||
@ -27,14 +26,7 @@ export default function getServiceAccessors<
|
|||||||
...axiosConfig,
|
...axiosConfig,
|
||||||
params: { id },
|
params: { id },
|
||||||
})
|
})
|
||||||
if (!data) {
|
|
||||||
return { data }
|
return { data }
|
||||||
}
|
|
||||||
const englishData = _.mapValues(
|
|
||||||
translation,
|
|
||||||
(frenchProp: string) => data[frenchProp]
|
|
||||||
) as Element
|
|
||||||
return { data: englishData }
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { error: error as Error }
|
return { error: error as Error }
|
||||||
}
|
}
|
||||||
@ -55,18 +47,7 @@ export default function getServiceAccessors<
|
|||||||
`${config.API_URL}/${elementName}ListGet`,
|
`${config.API_URL}/${elementName}ListGet`,
|
||||||
axiosConfig
|
axiosConfig
|
||||||
)
|
)
|
||||||
if (!data) {
|
|
||||||
return { data }
|
return { data }
|
||||||
}
|
|
||||||
|
|
||||||
const englishDataList = data.map(
|
|
||||||
(frenchData: any) =>
|
|
||||||
_.mapValues(
|
|
||||||
translation,
|
|
||||||
(frenchProp: string) => frenchData[frenchProp]
|
|
||||||
) as Element
|
|
||||||
)
|
|
||||||
return { data: englishDataList }
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { error: error as Error }
|
return { error: error as Error }
|
||||||
}
|
}
|
||||||
@ -84,27 +65,12 @@ export default function getServiceAccessors<
|
|||||||
}
|
}
|
||||||
return async (volunteerWithoutId: ElementNoId): Promise<ElementGetResponse> => {
|
return async (volunteerWithoutId: ElementNoId): Promise<ElementGetResponse> => {
|
||||||
try {
|
try {
|
||||||
const invertedTranslationWithoutId = _.invert(_.omit(translation, "id"))
|
|
||||||
const frenchDataWithoutId = _.mapValues(
|
|
||||||
invertedTranslationWithoutId,
|
|
||||||
(englishProp: string, _frenchProp: string) =>
|
|
||||||
(volunteerWithoutId as any)[englishProp]
|
|
||||||
)
|
|
||||||
|
|
||||||
const { data } = await axios.post(
|
const { data } = await axios.post(
|
||||||
`${config.API_URL}/${elementName}Add`,
|
`${config.API_URL}/${elementName}Add`,
|
||||||
frenchDataWithoutId,
|
volunteerWithoutId,
|
||||||
axiosConfig
|
axiosConfig
|
||||||
)
|
)
|
||||||
if (!data) {
|
|
||||||
return { data }
|
return { data }
|
||||||
}
|
|
||||||
|
|
||||||
const englishData = _.mapValues(
|
|
||||||
translation,
|
|
||||||
(frenchProp: string) => data[frenchProp]
|
|
||||||
) as Element
|
|
||||||
return { data: englishData }
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { error: error as Error }
|
return { error: error as Error }
|
||||||
}
|
}
|
||||||
@ -121,26 +87,12 @@ export default function getServiceAccessors<
|
|||||||
}
|
}
|
||||||
return async (volunteer: Element): Promise<ElementGetResponse> => {
|
return async (volunteer: Element): Promise<ElementGetResponse> => {
|
||||||
try {
|
try {
|
||||||
const invertedTranslation = _.invert(translation)
|
|
||||||
const frenchData = _.mapValues(
|
|
||||||
invertedTranslation,
|
|
||||||
(englishProp: string) => (volunteer as any)[englishProp]
|
|
||||||
)
|
|
||||||
|
|
||||||
const { data } = await axios.post(
|
const { data } = await axios.post(
|
||||||
`${config.API_URL}/${elementName}Set`,
|
`${config.API_URL}/${elementName}Set`,
|
||||||
frenchData,
|
volunteer,
|
||||||
axiosConfig
|
axiosConfig
|
||||||
)
|
)
|
||||||
if (!data) {
|
|
||||||
return { data }
|
return { data }
|
||||||
}
|
|
||||||
|
|
||||||
const englishData = _.mapValues(
|
|
||||||
translation,
|
|
||||||
(frenchProp: string) => data[frenchProp]
|
|
||||||
) as Element
|
|
||||||
return { data: englishData }
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { error: error as 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
|
const storage: any = localStorage
|
||||||
|
|
||||||
|
export const axiosConfig: AxiosRequestConfig = {
|
||||||
|
headers: {},
|
||||||
|
}
|
||||||
|
|
||||||
const jwt: string | null = storage?.getItem("id_token")
|
const jwt: string | null = storage?.getItem("id_token")
|
||||||
if (jwt) {
|
if (jwt) {
|
||||||
setJWT(jwt)
|
setJWT(jwt)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const axiosConfig: AxiosRequestConfig = {
|
|
||||||
headers: {},
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setJWT(token: string): void {
|
export function setJWT(token: string): void {
|
||||||
axiosConfig.headers.Authorization = `Bearer ${token}`
|
axiosConfig.headers.Authorization = `Bearer ${token}`
|
||||||
storage?.setItem("id_token", token)
|
storage?.setItem("id_token", token)
|
||||||
|
@ -54,10 +54,7 @@ const elementName = "JavGame"
|
|||||||
|
|
||||||
export type JavGameWithoutId = Omit<JavGame, "id">
|
export type JavGameWithoutId = Omit<JavGame, "id">
|
||||||
|
|
||||||
const { listGet, get, set, add } = getServiceAccessors<JavGameWithoutId, JavGame>(
|
const { listGet, get, set, add } = getServiceAccessors<JavGameWithoutId, JavGame>(elementName)
|
||||||
elementName,
|
|
||||||
translationJavGame
|
|
||||||
)
|
|
||||||
|
|
||||||
export const javGameListGet = listGet()
|
export const javGameListGet = listGet()
|
||||||
export const javGameGet = get()
|
export const javGameGet = get()
|
||||||
|
@ -28,12 +28,16 @@ export const translationPreVolunteer: { [k in keyof PreVolunteer]: string } = {
|
|||||||
|
|
||||||
const elementName = "PreVolunteer"
|
const elementName = "PreVolunteer"
|
||||||
|
|
||||||
|
export const emailRegexp =
|
||||||
|
/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i
|
||||||
|
export const passwordMinLength = 4
|
||||||
|
|
||||||
export type PreVolunteerWithoutId = Omit<PreVolunteer, "id">
|
export type PreVolunteerWithoutId = Omit<PreVolunteer, "id">
|
||||||
|
|
||||||
const { listGet, get, set, add, countGet } = getServiceAccessors<
|
const { listGet, get, set, add, countGet } = getServiceAccessors<
|
||||||
PreVolunteerWithoutId,
|
PreVolunteerWithoutId,
|
||||||
PreVolunteer
|
PreVolunteer
|
||||||
>(elementName, translationPreVolunteer)
|
>(elementName)
|
||||||
|
|
||||||
export const preVolunteerListGet = listGet()
|
export const preVolunteerListGet = listGet()
|
||||||
export const preVolunteerGet = get()
|
export const preVolunteerGet = get()
|
||||||
|
@ -23,7 +23,7 @@ export class Volunteer {
|
|||||||
|
|
||||||
comment = ""
|
comment = ""
|
||||||
|
|
||||||
timestamp = ""
|
timestamp = new Date()
|
||||||
|
|
||||||
password = ""
|
password = ""
|
||||||
}
|
}
|
||||||
@ -50,22 +50,18 @@ export const emailRegexp =
|
|||||||
/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i
|
/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i
|
||||||
export const passwordMinLength = 4
|
export const passwordMinLength = 4
|
||||||
|
|
||||||
export interface VolunteerLogin {
|
|
||||||
volunteer?: {
|
|
||||||
firstname: string
|
|
||||||
}
|
|
||||||
jwt?: string
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type VolunteerWithoutId = Omit<Volunteer, "id">
|
export type VolunteerWithoutId = Omit<Volunteer, "id">
|
||||||
|
|
||||||
const { listGet, get, set, add } = getServiceAccessors<VolunteerWithoutId, Volunteer>(
|
const accessors = getServiceAccessors<VolunteerWithoutId, Volunteer>(elementName)
|
||||||
elementName,
|
const { listGet, get, set, add } = accessors
|
||||||
translationVolunteer
|
|
||||||
)
|
|
||||||
|
|
||||||
export const volunteerListGet = listGet()
|
export const volunteerListGet = listGet()
|
||||||
export const volunteerGet = get()
|
export const volunteerGet = get()
|
||||||
export const volunteerAdd = add()
|
export const volunteerAdd = add()
|
||||||
export const volunteerSet = set()
|
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">
|
export type WishWithoutId = Omit<Wish, "id">
|
||||||
|
|
||||||
const { listGet, get, set, add } = getServiceAccessors<WishWithoutId, Wish>(
|
const { listGet, get, set, add } = getServiceAccessors<WishWithoutId, Wish>(elementName)
|
||||||
elementName,
|
|
||||||
translationWish
|
|
||||||
)
|
|
||||||
|
|
||||||
export const wishListGet = listGet()
|
export const wishListGet = listGet()
|
||||||
export const wishGet = get()
|
export const wishGet = get()
|
||||||
|
@ -13,27 +13,7 @@ import { JavGame } from "../../services/javGames"
|
|||||||
|
|
||||||
jest.mock("axios")
|
jest.mock("axios")
|
||||||
|
|
||||||
const mockFrenchData: any[] = [
|
const mockData: JavGame[] = [
|
||||||
{
|
|
||||||
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[] = [
|
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
title: "6 qui prend!",
|
title: "6 qui prend!",
|
||||||
@ -70,14 +50,12 @@ describe("JavGameList reducer", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should handle success correctly", () => {
|
it("should handle success correctly", () => {
|
||||||
expect(JavGameList(undefined, { type: getSuccess.type, payload: mockEnglishData })).toEqual(
|
expect(JavGameList(undefined, { type: getSuccess.type, payload: mockData })).toEqual({
|
||||||
{
|
|
||||||
...initialState,
|
...initialState,
|
||||||
readyStatus: "success",
|
readyStatus: "success",
|
||||||
ids: _.map(mockEnglishData, "id"),
|
ids: _.map(mockData, "id"),
|
||||||
entities: _.keyBy(mockEnglishData, "id"),
|
entities: _.keyBy(mockData, "id"),
|
||||||
}
|
})
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle failure correctly", () => {
|
it("should handle failure correctly", () => {
|
||||||
@ -94,11 +72,11 @@ describe("JavGameList action", () => {
|
|||||||
const { dispatch, getActions } = mockStore()
|
const { dispatch, getActions } = mockStore()
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
{ type: getRequesting.type, payload: undefined },
|
{ type: getRequesting.type, payload: undefined },
|
||||||
{ type: getSuccess.type, payload: mockEnglishData },
|
{ type: getSuccess.type, payload: mockData },
|
||||||
]
|
]
|
||||||
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
axios.get.mockResolvedValue({ data: mockFrenchData })
|
axios.get.mockResolvedValue({ data: mockData })
|
||||||
|
|
||||||
await dispatch(fetchJavGameList())
|
await dispatch(fetchJavGameList())
|
||||||
expect(getActions()).toEqual(expectedActions)
|
expect(getActions()).toEqual(expectedActions)
|
||||||
|
@ -12,23 +12,7 @@ import { Volunteer } from "../../services/volunteers"
|
|||||||
|
|
||||||
jest.mock("axios")
|
jest.mock("axios")
|
||||||
|
|
||||||
const mockFrenchData: any = {
|
const mockData: Volunteer = {
|
||||||
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 = {
|
|
||||||
id: 1,
|
id: 1,
|
||||||
lastname: "Aupeix",
|
lastname: "Aupeix",
|
||||||
firstname: "Amélie",
|
firstname: "Amélie",
|
||||||
@ -40,10 +24,10 @@ const mockEnglishData: Volunteer = {
|
|||||||
privileges: 0,
|
privileges: 0,
|
||||||
active: 0,
|
active: 0,
|
||||||
comment: "",
|
comment: "",
|
||||||
timestamp: "0000-00-00",
|
timestamp: new Date(0),
|
||||||
password: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O",
|
password: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O",
|
||||||
}
|
}
|
||||||
const { id } = mockEnglishData
|
const { id } = mockData
|
||||||
const mockError = "Oops! Something went wrong."
|
const mockError = "Oops! Something went wrong."
|
||||||
|
|
||||||
describe("volunteer reducer", () => {
|
describe("volunteer reducer", () => {
|
||||||
@ -62,9 +46,9 @@ describe("volunteer reducer", () => {
|
|||||||
expect(
|
expect(
|
||||||
volunteer(undefined, {
|
volunteer(undefined, {
|
||||||
type: getSuccess.type,
|
type: getSuccess.type,
|
||||||
payload: mockEnglishData,
|
payload: mockData,
|
||||||
})
|
})
|
||||||
).toEqual({ readyStatus: "success", entity: mockEnglishData })
|
).toEqual({ readyStatus: "success", entity: mockData })
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle failure correctly", () => {
|
it("should handle failure correctly", () => {
|
||||||
@ -82,11 +66,11 @@ describe("volunteer action", () => {
|
|||||||
const { dispatch, getActions } = mockStore()
|
const { dispatch, getActions } = mockStore()
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
{ type: getRequesting.type, payload: undefined },
|
{ type: getRequesting.type, payload: undefined },
|
||||||
{ type: getSuccess.type, payload: mockEnglishData },
|
{ type: getSuccess.type, payload: mockData },
|
||||||
]
|
]
|
||||||
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
axios.get.mockResolvedValue({ data: mockFrenchData })
|
axios.get.mockResolvedValue({ data: mockData })
|
||||||
|
|
||||||
await dispatch(fetchVolunteer(id))
|
await dispatch(fetchVolunteer(id))
|
||||||
expect(getActions()).toEqual(expectedActions)
|
expect(getActions()).toEqual(expectedActions)
|
||||||
|
@ -13,25 +13,7 @@ import { Volunteer } from "../../services/volunteers"
|
|||||||
|
|
||||||
jest.mock("axios")
|
jest.mock("axios")
|
||||||
|
|
||||||
const mockFrenchData: any[] = [
|
const mockData: Volunteer[] = [
|
||||||
{
|
|
||||||
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[] = [
|
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
lastname: "Aupeix",
|
lastname: "Aupeix",
|
||||||
@ -44,7 +26,7 @@ const mockEnglishData: Volunteer[] = [
|
|||||||
privileges: 0,
|
privileges: 0,
|
||||||
active: 0,
|
active: 0,
|
||||||
comment: "",
|
comment: "",
|
||||||
timestamp: "0000-00-00",
|
timestamp: new Date(0),
|
||||||
password: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O",
|
password: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@ -65,13 +47,11 @@ describe("volunteerList reducer", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should handle success correctly", () => {
|
it("should handle success correctly", () => {
|
||||||
expect(
|
expect(volunteerList(undefined, { type: getSuccess.type, payload: mockData })).toEqual({
|
||||||
volunteerList(undefined, { type: getSuccess.type, payload: mockEnglishData })
|
|
||||||
).toEqual({
|
|
||||||
...initialState,
|
...initialState,
|
||||||
readyStatus: "success",
|
readyStatus: "success",
|
||||||
ids: _.map(mockEnglishData, "id"),
|
ids: _.map(mockData, "id"),
|
||||||
entities: _.keyBy(mockEnglishData, "id"),
|
entities: _.keyBy(mockData, "id"),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -89,11 +69,11 @@ describe("volunteerList action", () => {
|
|||||||
const { dispatch, getActions } = mockStore()
|
const { dispatch, getActions } = mockStore()
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
{ type: getRequesting.type, payload: undefined },
|
{ type: getRequesting.type, payload: undefined },
|
||||||
{ type: getSuccess.type, payload: mockEnglishData },
|
{ type: getSuccess.type, payload: mockData },
|
||||||
]
|
]
|
||||||
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
axios.get.mockResolvedValue({ data: mockFrenchData })
|
axios.get.mockResolvedValue({ data: mockData })
|
||||||
|
|
||||||
await dispatch(fetchVolunteerList())
|
await dispatch(fetchVolunteerList())
|
||||||
expect(getActions()).toEqual(expectedActions)
|
expect(getActions()).toEqual(expectedActions)
|
||||||
|
@ -32,7 +32,8 @@ export const fetchPreVolunteerCount = elementValueFetch(
|
|||||||
getRequesting,
|
getRequesting,
|
||||||
getSuccess,
|
getSuccess,
|
||||||
getFailure,
|
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) =>
|
const shouldFetchPreVolunteerCount = (state: AppState) =>
|
||||||
|
@ -8,6 +8,7 @@ import volunteer from "./volunteer"
|
|||||||
import volunteerAdd from "./volunteerAdd"
|
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 preVolunteerAdd from "./preVolunteerAdd"
|
import preVolunteerAdd from "./preVolunteerAdd"
|
||||||
import preVolunteerCount from "./preVolunteerCount"
|
import preVolunteerCount from "./preVolunteerCount"
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ export default (history: History) => ({
|
|||||||
volunteerAdd,
|
volunteerAdd,
|
||||||
volunteerList,
|
volunteerList,
|
||||||
volunteerSet,
|
volunteerSet,
|
||||||
|
volunteerLogin,
|
||||||
preVolunteerAdd,
|
preVolunteerAdd,
|
||||||
preVolunteerCount,
|
preVolunteerCount,
|
||||||
router: connectRouter(history) as any,
|
router: connectRouter(history) as any,
|
||||||
|
@ -33,32 +33,32 @@ export function toastSuccess(message: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function elementFetch<Element>(
|
export function elementFetch<Element>(
|
||||||
elementService: (id: number) => Promise<{
|
elementService: (...idArgs: any[]) => Promise<{
|
||||||
data?: Element | undefined
|
data?: Element | undefined
|
||||||
error?: Error | undefined
|
error?: Error | undefined
|
||||||
}>,
|
}>,
|
||||||
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?: (data: Element) => void
|
||||||
},
|
): (...idArgs: any[]) => AppThunk {
|
||||||
successMessage: () => void = () => {
|
return (...idArgs: any[]): AppThunk =>
|
||||||
/* Meant to be empty */
|
|
||||||
}
|
|
||||||
): (id: number) => AppThunk {
|
|
||||||
return (id: number): AppThunk =>
|
|
||||||
async (dispatch) => {
|
async (dispatch) => {
|
||||||
dispatch(getRequesting())
|
dispatch(getRequesting())
|
||||||
|
|
||||||
const { error, data } = await elementService(id)
|
const { error, data } = await elementService(...idArgs)
|
||||||
|
|
||||||
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))
|
||||||
successMessage()
|
if (successMessage) {
|
||||||
|
successMessage(data as Element)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ export const fetchVolunteer = elementFetch(
|
|||||||
getRequesting,
|
getRequesting,
|
||||||
getSuccess,
|
getSuccess,
|
||||||
getFailure,
|
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) =>
|
const shouldFetchVolunteer = (state: AppState, id: number) =>
|
||||||
|
@ -33,6 +33,6 @@ export const fetchVolunteerAdd = elementAddFetch(
|
|||||||
getRequesting,
|
getRequesting,
|
||||||
getSuccess,
|
getSuccess,
|
||||||
getFailure,
|
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 !")
|
() => toastSuccess("Volunteer ajoutée !")
|
||||||
)
|
)
|
||||||
|
@ -36,7 +36,7 @@ export const fetchVolunteerList = elementListFetch(
|
|||||||
getRequesting,
|
getRequesting,
|
||||||
getSuccess,
|
getSuccess,
|
||||||
getFailure,
|
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"
|
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,
|
getRequesting,
|
||||||
getSuccess,
|
getSuccess,
|
||||||
getFailure,
|
getFailure,
|
||||||
(error: Error) => toastError(`Erreur lors de la modification d'un volunteer: ${error.message}`),
|
(error: Error) => toastError(`Erreur lors de la modification d'un bénévole: ${error.message}`),
|
||||||
() => toastSuccess("Volunteer modifié !")
|
() => toastSuccess("Bénévole modifié !")
|
||||||
)
|
)
|
||||||
|
@ -33,6 +33,6 @@ export const fetchWishAdd = elementAddFetch(
|
|||||||
getRequesting,
|
getRequesting,
|
||||||
getSuccess,
|
getSuccess,
|
||||||
getFailure,
|
getFailure,
|
||||||
(error: Error) => toastError(`Erreur lors de l'ajout d'une wish: ${error.message}`),
|
(error: Error) => toastError(`Erreur lors de l'ajout d'une envie: ${error.message}`),
|
||||||
() => toastSuccess("Wish ajoutée !")
|
() => toastSuccess("Envie ajoutée !")
|
||||||
)
|
)
|
||||||
|
@ -34,7 +34,7 @@ export const fetchWishList = elementListFetch(
|
|||||||
getRequesting,
|
getRequesting,
|
||||||
getSuccess,
|
getSuccess,
|
||||||
getFailure,
|
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"
|
const shouldFetchWishList = (state: AppState) => state.wishList.readyStatus !== "success"
|
||||||
|
@ -16,3 +16,31 @@ export function validEmail(email: string): boolean {
|
|||||||
email
|
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"
|
indent-string "^4.0.0"
|
||||||
strip-indent "^3.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:
|
redux-mock-store@^1.5.4:
|
||||||
version "1.5.4"
|
version "1.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.5.4.tgz#90d02495fd918ddbaa96b83aef626287c9ab5872"
|
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