-
-
-
+const RegisterPage: React.FC
= (): JSX.Element => {
+ const dispatch = useDispatch()
+ const loginError = useSelector((state: AppState) => state.volunteerLogin.error, shallowEqual)
+
+ return (
+
-
-)
+ )
+}
export default memo(RegisterPage)
diff --git a/src/server/gsheets/accessors.ts b/src/server/gsheets/accessors.ts
index 60efa5e..8575e3b 100644
--- a/src/server/gsheets/accessors.ts
+++ b/src/server/gsheets/accessors.ts
@@ -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])
}
diff --git a/src/server/gsheets/expressAccessors.ts b/src/server/gsheets/expressAccessors.ts
index 6e19a08..7a64d57 100644
--- a/src/server/gsheets/expressAccessors.ts
+++ b/src/server/gsheets/expressAccessors.ts
@@ -51,11 +51,7 @@ export default class ExpressAccessors<
add() {
return async (request: Request, response: Response, _next: NextFunction): Promise
=> {
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 => {
+ // transformer can be an async function
+ customGet(
+ transformer: (list: Element[] | undefined, body?: Request["body"]) => Promise | any
+ ) {
+ return async (request: Request, response: Response, _next: NextFunction): Promise => {
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 })
}
}
}
diff --git a/src/server/gsheets/volunteers.ts b/src/server/gsheets/volunteers.ts
index 6043b67..3b567a4 100644
--- a/src/server/gsheets/volunteers.ts
+++ b/src/server/gsheets/volunteers.ts
@@ -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(
"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,
+ }
+ }
+)
diff --git a/src/server/index.ts b/src/server/index.ts
index 9d79f7b..a21d181 100755
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -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)
diff --git a/src/server/secure.ts b/src/server/secure.ts
index 96b1e95..e0481fe 100644
--- a/src/server/secure.ts
+++ b/src/server/secure.ts
@@ -53,12 +53,12 @@ async function getSecret() {
export async function getJwt(email: string): Promise {
const jwt = sign(
{ user: canonicalEmail(email), permissions: [] },
- await getSecret(),
- __TEST__
- ? undefined
- : {
- expiresIn: "365d",
- }
+ await getSecret()
+ // __TEST__
+ // ? undefined
+ // : {
+ // expiresIn: "365d",
+ // }
)
return jwt
}
diff --git a/src/server/userManagement/__tests__/login.tsx b/src/server/userManagement/__tests__/login.tsx
deleted file mode 100755
index 712eca4..0000000
--- a/src/server/userManagement/__tests__/login.tsx
+++ /dev/null
@@ -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"
- )
- })
-})
diff --git a/src/server/userManagement/login.ts b/src/server/userManagement/login.ts
deleted file mode 100644
index ae9752e..0000000
--- a/src/server/userManagement/login.ts
+++ /dev/null
@@ -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 {
- 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 {
- const sheet = getSheet(
- "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,
- }
-}
diff --git a/src/services/accessors.ts b/src/services/accessors.ts
index 0b9f714..8b28597 100644
--- a/src/services/accessors.ts
+++ b/src/services/accessors.ts
@@ -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 => {
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 => {
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 => {
+ 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 }
}
diff --git a/src/services/auth.ts b/src/services/auth.ts
index 95b19a5..5fb7332 100644
--- a/src/services/auth.ts
+++ b/src/services/auth.ts
@@ -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)
diff --git a/src/services/javGames.ts b/src/services/javGames.ts
index af1f2c3..7a8325e 100644
--- a/src/services/javGames.ts
+++ b/src/services/javGames.ts
@@ -54,10 +54,7 @@ const elementName = "JavGame"
export type JavGameWithoutId = Omit
-const { listGet, get, set, add } = getServiceAccessors(
- elementName,
- translationJavGame
-)
+const { listGet, get, set, add } = getServiceAccessors(elementName)
export const javGameListGet = listGet()
export const javGameGet = get()
diff --git a/src/services/preVolunteers.ts b/src/services/preVolunteers.ts
index 54b28eb..da0d7ac 100644
--- a/src/services/preVolunteers.ts
+++ b/src/services/preVolunteers.ts
@@ -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
const { listGet, get, set, add, countGet } = getServiceAccessors<
PreVolunteerWithoutId,
PreVolunteer
->(elementName, translationPreVolunteer)
+>(elementName)
export const preVolunteerListGet = listGet()
export const preVolunteerGet = get()
diff --git a/src/services/volunteers.ts b/src/services/volunteers.ts
index 1ea0dbe..d7513f6 100644
--- a/src/services/volunteers.ts
+++ b/src/services/volunteers.ts
@@ -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
-const { listGet, get, set, add } = getServiceAccessors(
- elementName,
- translationVolunteer
-)
+const accessors = getServiceAccessors(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")
diff --git a/src/services/wishes.ts b/src/services/wishes.ts
index 3e44404..e4265ce 100644
--- a/src/services/wishes.ts
+++ b/src/services/wishes.ts
@@ -27,10 +27,7 @@ const elementName = "Wish"
export type WishWithoutId = Omit
-const { listGet, get, set, add } = getServiceAccessors(
- elementName,
- translationWish
-)
+const { listGet, get, set, add } = getServiceAccessors(elementName)
export const wishListGet = listGet()
export const wishGet = get()
diff --git a/src/store/__tests__/javGameList.ts b/src/store/__tests__/javGameList.ts
index 803e23a..1489545 100644
--- a/src/store/__tests__/javGameList.ts
+++ b/src/store/__tests__/javGameList.ts
@@ -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)
diff --git a/src/store/__tests__/volunteer.ts b/src/store/__tests__/volunteer.ts
index 57fa263..6a35048 100644
--- a/src/store/__tests__/volunteer.ts
+++ b/src/store/__tests__/volunteer.ts
@@ -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)
diff --git a/src/store/__tests__/volunteerList.ts b/src/store/__tests__/volunteerList.ts
index 540c156..27e954f 100644
--- a/src/store/__tests__/volunteerList.ts
+++ b/src/store/__tests__/volunteerList.ts
@@ -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)
diff --git a/src/store/preVolunteerCount.ts b/src/store/preVolunteerCount.ts
index 268afd4..660aeb9 100644
--- a/src/store/preVolunteerCount.ts
+++ b/src/store/preVolunteerCount.ts
@@ -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) =>
diff --git a/src/store/rootReducer.ts b/src/store/rootReducer.ts
index c8e70bc..3b613dc 100644
--- a/src/store/rootReducer.ts
+++ b/src/store/rootReducer.ts
@@ -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,
diff --git a/src/store/utils.ts b/src/store/utils.ts
index 0db504d..5729229 100644
--- a/src/store/utils.ts
+++ b/src/store/utils.ts
@@ -33,32 +33,32 @@ export function toastSuccess(message: string): void {
}
export function elementFetch(
- elementService: (id: number) => Promise<{
+ elementService: (...idArgs: any[]) => Promise<{
data?: Element | undefined
error?: Error | undefined
}>,
getRequesting: ActionCreatorWithoutPayload,
getSuccess: ActionCreatorWithPayload,
getFailure: ActionCreatorWithPayload,
- 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)
+ }
}
}
}
diff --git a/src/store/volunteer.ts b/src/store/volunteer.ts
index 3e05c1d..081570c 100644
--- a/src/store/volunteer.ts
+++ b/src/store/volunteer.ts
@@ -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) =>
diff --git a/src/store/volunteerAdd.ts b/src/store/volunteerAdd.ts
index 8e2a795..2a8fb92 100644
--- a/src/store/volunteerAdd.ts
+++ b/src/store/volunteerAdd.ts
@@ -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 !")
)
diff --git a/src/store/volunteerList.ts b/src/store/volunteerList.ts
index 314bb7f..b28c396 100644
--- a/src/store/volunteerList.ts
+++ b/src/store/volunteerList.ts
@@ -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"
diff --git a/src/store/volunteerLogin.ts b/src/store/volunteerLogin.ts
new file mode 100644
index 0000000..953c766
--- /dev/null
+++ b/src/store/volunteerLogin.ts
@@ -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) => ({
+ readyStatus: "success",
+ entity: payload,
+ }),
+ getFailure: (_, { payload }: PayloadAction) => ({
+ 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)
+ }
+)
diff --git a/src/store/volunteerSet.ts b/src/store/volunteerSet.ts
index d405eb8..1266107 100644
--- a/src/store/volunteerSet.ts
+++ b/src/store/volunteerSet.ts
@@ -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é !")
)
diff --git a/src/store/wishAdd.ts b/src/store/wishAdd.ts
index 9ce9c99..f9e1a73 100644
--- a/src/store/wishAdd.ts
+++ b/src/store/wishAdd.ts
@@ -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 !")
)
diff --git a/src/store/wishList.ts b/src/store/wishList.ts
index 2c989f8..ee71595 100644
--- a/src/store/wishList.ts
+++ b/src/store/wishList.ts
@@ -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"
diff --git a/src/utils/standardization.ts b/src/utils/standardization.ts
index e06c55b..bb4668d 100644
--- a/src/utils/standardization.ts
+++ b/src/utils/standardization.ts
@@ -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*$/, "")
+}
diff --git a/yarn.lock b/yarn.lock
index 79875c5..f7c6bbe 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"