diff --git a/package.json b/package.json index 51b9320..ef6585a 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "@types/lodash": "^4.14.177", "autoprefixer": "^10.2.6", "axios": "^0.21.1", + "bcrypt": "^5.0.1", "chalk": "^4.1.1", "compression": "^1.7.4", "connected-react-router": "^6.9.1", @@ -120,6 +121,7 @@ "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^12.0.0", + "@types/bcrypt": "^5.0.0", "@types/compression": "^1.7.1", "@types/compression-webpack-plugin": "^6.0.6", "@types/css-minimizer-webpack-plugin": "^3.0.2", diff --git a/src/routes/certbot.ts b/src/routes/certbot.ts index 8a45c18..14cc350 100644 --- a/src/routes/certbot.ts +++ b/src/routes/certbot.ts @@ -7,7 +7,6 @@ const certbotRouter: Router = Router() certbotRouter.use((request: Request, response: Response, _next: NextFunction) => { const filename = request.originalUrl.replace(/.*\//, "") const resolvedPath: string = path.resolve(`../certbot/.well-known/acme-challenge/${filename}`) - console.log("response", resolvedPath) response.setHeader("Content-Type", "text/html") return response.sendFile(resolvedPath) }) diff --git a/src/gsheets/DBManager.ts b/src/server/gsheets/DBManager.ts similarity index 100% rename from src/gsheets/DBManager.ts rename to src/server/gsheets/DBManager.ts diff --git a/src/gsheets/accessors.ts b/src/server/gsheets/accessors.ts similarity index 100% rename from src/gsheets/accessors.ts rename to src/server/gsheets/accessors.ts diff --git a/src/gsheets/envies.ts b/src/server/gsheets/envies.ts similarity index 85% rename from src/gsheets/envies.ts rename to src/server/gsheets/envies.ts index 7bb9c6a..25a21a2 100644 --- a/src/gsheets/envies.ts +++ b/src/server/gsheets/envies.ts @@ -1,5 +1,5 @@ import getExpressAccessors from "./expressAccessors" -import { Envie, EnvieWithoutId } from "../services/envies" +import { Envie, EnvieWithoutId } from "../../services/envies" const { listGetRequest, getRequest, setRequest, addRequest } = getExpressAccessors< EnvieWithoutId, diff --git a/src/gsheets/expressAccessors.ts b/src/server/gsheets/expressAccessors.ts similarity index 100% rename from src/gsheets/expressAccessors.ts rename to src/server/gsheets/expressAccessors.ts diff --git a/src/gsheets/jeuJav.ts b/src/server/gsheets/jeuJav.ts similarity index 84% rename from src/gsheets/jeuJav.ts rename to src/server/gsheets/jeuJav.ts index 6dd3384..50a55dd 100644 --- a/src/gsheets/jeuJav.ts +++ b/src/server/gsheets/jeuJav.ts @@ -1,5 +1,5 @@ import getExpressAccessors from "./expressAccessors" -import { JeuJav, JeuJavWithoutId } from "../services/jeuxJav" +import { JeuJav, JeuJavWithoutId } from "../../services/jeuxJav" const { listGetRequest, getRequest, setRequest, addRequest } = getExpressAccessors< JeuJavWithoutId, diff --git a/src/gsheets/membres.ts b/src/server/gsheets/membres.ts similarity index 84% rename from src/gsheets/membres.ts rename to src/server/gsheets/membres.ts index 3664263..04a7919 100644 --- a/src/gsheets/membres.ts +++ b/src/server/gsheets/membres.ts @@ -1,5 +1,5 @@ import getExpressAccessors from "./expressAccessors" -import { Membre, MembreWithoutId } from "../services/membres" +import { Membre, MembreWithoutId } from "../../services/membres" const { listGetRequest, getRequest, setRequest, addRequest } = getExpressAccessors< MembreWithoutId, diff --git a/src/server/index.ts b/src/server/index.ts index 5516f97..c01b956 100755 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -15,9 +15,10 @@ import devServer from "./devServer" import ssr from "./ssr" import certbotRouter from "../routes/certbot" -import { jeuJavListGet } from "../gsheets/jeuJav" -import { envieListGet, envieAdd } from "../gsheets/envies" -import { membreGet, membreSet } from "../gsheets/membres" +import { jeuJavListGet } from "./gsheets/jeuJav" +import { envieListGet, envieAdd } from "./gsheets/envies" +import { membreGet, membreSet } from "./gsheets/membres" +import signInHandler from "./userManagement/signIn" import config from "../config" const app = express() @@ -42,7 +43,11 @@ app.use(express.static(path.resolve(process.cwd(), "public"))) // Enable dev-server in development if (__DEV__) devServer(app) -// Google Sheets requests +/** + * APIs + */ + +// Google Sheets API app.use(express.json()) app.get("/JeuJavListGet", jeuJavListGet) app.get("/EnvieListGet", envieListGet) @@ -50,6 +55,9 @@ app.get("/MembreGet", membreGet) app.post("/MembreSet", membreSet) app.post("/EnvieAdd", envieAdd) +// Sign in & up API +app.post("/api/user/login", signInHandler) + // Use React server-side rendering middleware app.get("*", ssr) diff --git a/src/server/userManagement/__tests__/signIn.tsx b/src/server/userManagement/__tests__/signIn.tsx new file mode 100755 index 0000000..bd79364 --- /dev/null +++ b/src/server/userManagement/__tests__/signIn.tsx @@ -0,0 +1,56 @@ +/** + * @jest-environment jsdom + */ + +import { signIn } from "../signIn" + +// 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 + +const mockUser = { + mail: "my.email@gmail.com", + passe: "$2y$10$cuKFHEow2IVSZSPtoVsw6uZFNFOOP/v1V7fubbyvrxhZdsnxLHr.2", + prenom: "monPrénom", +} + +jest.mock("../../gsheets/accessors", () => () => ({ + listGet: () => [mockUser], +})) + +describe("signIn with", () => { + it("right password", async () => { + const res = await signIn("my.email@gmail.com", "12345678") + expect(res).toEqual({ + membre: { + prenom: mockUser.prenom, + }, + }) + }) + + it("invalid password length", async () => { + await expect(signIn("my.email@gmail.com", "11011")).rejects.toThrow("Mot de passe invalid") + }) + + it("empty password", async () => { + await expect(signIn("my.email@gmail.com", " ")).rejects.toThrow("Mot de passe invalid") + }) + + it("wrong password", async () => { + await expect(signIn("my.email@gmail.com", "1234567891011")).rejects.toThrow( + "Mauvais mot de passe pour cet email" + ) + }) + + it("invalid email format", async () => { + await expect(signIn("my.email@gmail", "12345678")).rejects.toThrow("Email invalid") + }) + + it("empty email", async () => { + await expect(signIn(" ", "12345678")).rejects.toThrow("Email invalid") + }) + + it("unknown email", async () => { + await expect(signIn("mon.emailBidon@gmail.com", "12345678")).rejects.toThrow( + "Cet email ne correspond à aucun utilisateur" + ) + }) +}) diff --git a/src/server/userManagement/signIn.ts b/src/server/userManagement/signIn.ts new file mode 100644 index 0000000..3e9919e --- /dev/null +++ b/src/server/userManagement/signIn.ts @@ -0,0 +1,55 @@ +import { Request, Response, NextFunction } from "express" +import bcrypt from "bcrypt" +import { Membre, MemberLogin, emailRegexp, passwordMinLength } from "../../services/membres" +import getAccessors from "../gsheets/accessors" + +const { listGet } = getAccessors("Membres", new Membre()) + +export default async function signInHandler( + request: Request, + response: Response, + _next: NextFunction +): Promise { + try { + if (typeof request.body.email !== "string" || typeof request.body.password !== "string") { + throw Error() + } + const res = await signIn(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 signIn(rawEmail: string, rawPassword: string): Promise { + 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 < passwordMinLength) { + throw Error("Mot de passe invalid") + } + + const membres: Membre[] = await listGet() + const membre = membres.find((m) => m.mail === email) + if (!membre) { + throw Error("Cet email ne correspond à aucun utilisateur") + } + + const passwordMatch = await bcrypt.compare(password, membre.passe.replace(/^\$2y/, "$2a")) + if (!passwordMatch) { + throw Error("Mauvais mot de passe pour cet email") + } + + return { + membre: { + prenom: membre.prenom, + }, + } +} diff --git a/src/services/membres.ts b/src/services/membres.ts index 49e2534..1887505 100644 --- a/src/services/membres.ts +++ b/src/services/membres.ts @@ -28,6 +28,17 @@ export class Membre { passe = "" } +export const emailRegexp = + /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i +export const passwordMinLength = 4 + +export interface MemberLogin { + membre?: { + prenom: string + } + error?: string +} + export type MembreWithoutId = Omit export const membreGet = get("Membre") diff --git a/yarn.lock b/yarn.lock index be1e90e..b0b723e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1271,6 +1271,21 @@ dependencies: make-dir "^3.0.2" +"@mapbox/node-pre-gyp@^1.0.0": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.7.tgz#a26919cac6595662703330d1820a0ca206f45521" + integrity sha512-PplSvl4pJ5N3BkVjAdDzpPhVUPdC73JgttkR+LnBx2OORC1GCQsBjUeEuipf9uOaAM1SbxcdZFfR3KDTKm2S0A== + dependencies: + detect-libc "^1.0.3" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.5" + nopt "^5.0.0" + npmlog "^6.0.0" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.11" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1484,6 +1499,13 @@ dependencies: "@babel/types" "^7.3.0" +"@types/bcrypt@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/bcrypt/-/bcrypt-5.0.0.tgz#a835afa2882d165aff5690893db314eaa98b9f20" + integrity sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw== + dependencies: + "@types/node" "*" + "@types/body-parser@*": version "1.19.2" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" @@ -2406,6 +2428,11 @@ aproba@^1.0.3: resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== +"aproba@^1.0.3 || ^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + arch@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" @@ -2418,6 +2445,14 @@ archive-type@^4.0.0: dependencies: file-type "^4.2.0" +are-we-there-yet@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" + integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + are-we-there-yet@~1.1.2: version "1.1.7" resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz#b15474a932adab4ff8a50d9adfa7e4e926f21146" @@ -2735,6 +2770,14 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +bcrypt@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.0.1.tgz#f1a2c20f208e2ccdceea4433df0c8b2c54ecdf71" + integrity sha512-9BTgmrhZM2t1bNuDtrtIMVSmmxZBrJ71n8Wg+YgdjHuIWYF7SjjmCPZFB+/5i/o/PIeRpwVJR3P+NrpIItUjqw== + dependencies: + "@mapbox/node-pre-gyp" "^1.0.0" + node-addon-api "^3.1.0" + big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -3264,6 +3307,11 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-support@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + colord@^2.9.1: version "2.9.1" resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.1.tgz#c961ea0efeb57c9f0f4834458f26cb9cc4a3f90e" @@ -3375,7 +3423,7 @@ connected-react-router@^6.9.1: immutable "^3.8.1 || ^4.0.0-rc.1" seamless-immutable "^7.1.3" -console-control-strings@^1.0.0, console-control-strings@~1.1.0: +console-control-strings@^1.0.0, console-control-strings@^1.1.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= @@ -3856,6 +3904,11 @@ destroy@~1.0.4: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -4962,6 +5015,21 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +gauge@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.0.tgz#afba07aa0374a93c6219603b1fb83eaa2264d8f8" + integrity sha512-F8sU45yQpjQjxKkm1UOAhf0U/O0aFt//Fl7hsrNVto+patMHjs7dPI9mFOGUKbhrgKm0S3EjW3scMFuQmWSROw== + dependencies: + ansi-regex "^5.0.1" + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.2" + console-control-strings "^1.0.0" + has-unicode "^2.0.1" + signal-exit "^3.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.2" + gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" @@ -5438,7 +5506,7 @@ has-tostringtag@^1.0.0: dependencies: has-symbols "^1.0.2" -has-unicode@^2.0.0: +has-unicode@^2.0.0, has-unicode@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= @@ -7500,7 +7568,12 @@ no-case@^2.2.0: dependencies: lower-case "^1.1.1" -node-fetch@^2.6.1: +node-addon-api@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" + integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== + +node-fetch@^2.6.1, node-fetch@^2.6.5: version "2.6.6" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89" integrity sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA== @@ -7700,6 +7773,16 @@ npmlog@^4.0.0, npmlog@^4.1.2: gauge "~2.7.3" set-blocking "~2.0.0" +npmlog@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.0.tgz#ba9ef39413c3d936ea91553db7be49c34ad0520c" + integrity sha512-03ppFRGlsyUaQFbGC2C8QWJN/C/K7PsfyD9aQdhVKAQIH4sQBc8WASqFBP7O+Ut4d2oo5LoeoboB3cGdBZSp6Q== + dependencies: + are-we-there-yet "^2.0.0" + console-control-strings "^1.1.0" + gauge "^4.0.0" + set-blocking "^2.0.0" + nth-check@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2" @@ -8923,7 +9006,7 @@ readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.6, readable string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.1.1: +readable-stream@^3.1.1, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -10121,7 +10204,7 @@ tar-stream@^1.5.2: to-buffer "^1.1.1" xtend "^4.0.0" -tar@^6.0.2: +tar@^6.0.2, tar@^6.1.11: version "6.1.11" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== @@ -10915,7 +10998,7 @@ which@^2.0.1, which@^2.0.2: dependencies: isexe "^2.0.0" -wide-align@^1.1.0: +wide-align@^1.1.0, wide-align@^1.1.2: version "1.1.5" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==