diff --git a/.talismanrc b/.talismanrc index 0108c03c5..8e02def6b 100644 --- a/.talismanrc +++ b/.talismanrc @@ -1,46 +1,61 @@ fileignoreconfig: -- filename: .github/workflows/_deploy.yml - checksum: 937b3c6985c909437e9a4362eea76eccd3a946ead5bea27f24f9b0ced02df128 -- filename: .github/workflows/publish.yml - checksum: cf87922f9cd1d8f4bb965920800187860609408c64406991a6dfad0ab79cc6aa -- filename: .github/workflows/release.yml - checksum: 96cdfb3f17eb6f7be3852f165c6587659570a814f879705348b7abaffc29742a -- filename: .infra/README.md - checksum: 46ff20cd40c93c0580c896707afadccc37486909997213c452393d8179d12a3d -- filename: .infra/ansible/roles/setup/files/app/.overrides/common/docker-compose.common.yml - checksum: c2a10f20a22c2df9c97be935509f0119799be6467fe797399bd589fae2d10388 -- filename: .infra/ansible/roles/setup/files/app/.overrides/production/docker-compose.env.yml - checksum: a0e0aee8350df735bf9e13b4d19a3ec0b2a11c742e3ed4681d2d2cc5d672994f -- filename: .infra/ansible/roles/setup/files/app/mongodb/docker-entrypoint-initdb.d/01_create_users.js - checksum: f8af7aa98f4242b1c472c53f85f3d62a14a1239e7259ec0fec837ee09c7df989 -- filename: .infra/ansible/roles/setup/files/app/tools/metabase/backup-metabase.sh - checksum: 4243b31cd3918b3d7c739fb7469ed7c1177826648c5b8f54e821df8f749be52e -- filename: .infra/ansible/roles/setup/tasks/configure-backup.yml - checksum: aa93a5ec0e3365d21334481c5f816d3f42775ab08512cf7276b66b28f8df8f86 -- filename: .infra/ansible/roles/setup/tasks/install-app.yml - checksum: 2c7d8cdb7af638f1f5588ac9969128dddccc0a0650a5b70af5be7711d8dfbbe7 -- filename: .infra/ansible/roles/setup/vars/main/vault.yml - checksum: e40d1cfbe320ad9152ae23f552f16a1cf01451725292112f539285fa455f78fd -- filename: .infra/docker-compose.production.yml - checksum: 88f2901eb4b3b71b55fd9b7031d02a2c9064fa55b065bfb70c80730fee206cf9 -- filename: .infra/env.ini - checksum: 2d1ab129d6f39a2c634312ab647a13e2cc0036fa237f0497575890ec04cd87a6 -- filename: .infra/scripts/deploy-app.sh - checksum: efeabc0bb607aa1ed68ed076f197461735cf2c21a09ed369965ec713d369e56c -- filename: .infra/scripts/ovh/create-backup-partition.sh - checksum: 4ab5023a5bfef64b0db52158d42fd7635f2754eb3596ac5b53e085815977f1aa -- filename: .infra/scripts/vault/generate-vault-password.sh - checksum: 86baa73f9c5559afc69f12d124d9895a104869cfb8fe396568f88954351a38b5 -- filename: .infra/scripts/vault/get-vault-password-client.sh - checksum: 4ac0001fb9e12df75becb3eec2cc41431bc036b293cdcfee3b769daf8649e5e8 -- filename: .infra/scripts/vault/renew-vault.sh - checksum: 86ee05100090183153d43671fd74bddf0552793dfe0f2a9a01c137d3cb248487 -- filename: git-hooks/prepare-release.sh - checksum: 451afde847ac06703ffa04ea37b058d8522b60b94c891bf0979158dac5bc3610 -- filename: server/.env.test - checksum: c0ef2f69099b6b85e1ee02f31ead22e9b7385bbb213d610fb953c0fc976709ac + - filename: .github/workflows/_deploy.yml + checksum: 937b3c6985c909437e9a4362eea76eccd3a946ead5bea27f24f9b0ced02df128 + - filename: .github/workflows/publish.yml + checksum: cf87922f9cd1d8f4bb965920800187860609408c64406991a6dfad0ab79cc6aa + - filename: .github/workflows/release.yml + checksum: 96cdfb3f17eb6f7be3852f165c6587659570a814f879705348b7abaffc29742a + - filename: .infra/README.md + checksum: 46ff20cd40c93c0580c896707afadccc37486909997213c452393d8179d12a3d + - filename: .infra/ansible/roles/setup/files/app/.overrides/common/docker-compose.common.yml + checksum: c2a10f20a22c2df9c97be935509f0119799be6467fe797399bd589fae2d10388 + - filename: .infra/ansible/roles/setup/files/app/.overrides/production/docker-compose.env.yml + checksum: a0e0aee8350df735bf9e13b4d19a3ec0b2a11c742e3ed4681d2d2cc5d672994f + - filename: .infra/ansible/roles/setup/files/app/mongodb/docker-entrypoint-initdb.d/01_create_users.js + checksum: f8af7aa98f4242b1c472c53f85f3d62a14a1239e7259ec0fec837ee09c7df989 + - filename: .infra/ansible/roles/setup/files/app/tools/metabase/backup-metabase.sh + checksum: 4243b31cd3918b3d7c739fb7469ed7c1177826648c5b8f54e821df8f749be52e + - filename: .infra/ansible/roles/setup/tasks/configure-backup.yml + checksum: aa93a5ec0e3365d21334481c5f816d3f42775ab08512cf7276b66b28f8df8f86 + - filename: .infra/ansible/roles/setup/tasks/install-app.yml + checksum: 2c7d8cdb7af638f1f5588ac9969128dddccc0a0650a5b70af5be7711d8dfbbe7 + - filename: .infra/ansible/roles/setup/vars/main/vault.yml + checksum: e40d1cfbe320ad9152ae23f552f16a1cf01451725292112f539285fa455f78fd + - filename: .infra/docker-compose.production.yml + checksum: 88f2901eb4b3b71b55fd9b7031d02a2c9064fa55b065bfb70c80730fee206cf9 + - filename: .infra/env.ini + checksum: 2d1ab129d6f39a2c634312ab647a13e2cc0036fa237f0497575890ec04cd87a6 + - filename: .infra/scripts/deploy-app.sh + checksum: efeabc0bb607aa1ed68ed076f197461735cf2c21a09ed369965ec713d369e56c + - filename: .infra/scripts/ovh/create-backup-partition.sh + checksum: 4ab5023a5bfef64b0db52158d42fd7635f2754eb3596ac5b53e085815977f1aa + - filename: .infra/scripts/vault/generate-vault-password.sh + checksum: 86baa73f9c5559afc69f12d124d9895a104869cfb8fe396568f88954351a38b5 + - filename: .infra/scripts/vault/get-vault-password-client.sh + checksum: 4ac0001fb9e12df75becb3eec2cc41431bc036b293cdcfee3b769daf8649e5e8 + - filename: .infra/scripts/vault/renew-vault.sh + checksum: 86ee05100090183153d43671fd74bddf0552793dfe0f2a9a01c137d3cb248487 + - filename: git-hooks/prepare-release.sh + checksum: 451afde847ac06703ffa04ea37b058d8522b60b94c891bf0979158dac5bc3610 + - filename: server/.env.test + checksum: 3273a6b258bcaca7bf4e13099590cbe65a303aaa0a26709fa9dd24be46c872f4 + - filename: "*.route.test.ts" + checksum: f0902a33ec3d28bad8dd85fcfff1bc5277c65d29798ea04c353b221c82a85df5 + - filename: "*.test.ts" + checksum: a25b36c117e32dc0806d2d15104cbf66e2dd0b85e89a544df22ca0adb857005b + - filename: server/config/config.ts + checksum: 5b97c2c58ef9eb12063becfe44194016fd09c25ac29617de9ceba2201f2b849b + - filename: server/src/app.ts + checksum: 87ab06269a50c3cbca6370691e77fcf30b4863b2ad66a6a50d74e10e1ee95271 + - filename: server/src/modules/server/index.ts + checksum: e9d1bc8dfc528ec8786f21af1d8ec5ecb290bb5f8b6b63275f744dc5b27673e3 + - filename: server/src/utils/jwtUtils.ts + checksum: 071d8aa92d917d9224e77393463a2af00a65d381637b08140d52d11571dab51c scopeconfig: -- scope: node + - scope: node custom_patterns: -- (?s)[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4} + - (?s)[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4} +allowed_patterns: + - key + - secret version: "1.0" diff --git a/server/.env.test b/server/.env.test index 5a1b71064..996846dfd 100644 --- a/server/.env.test +++ b/server/.env.test @@ -1,5 +1,8 @@ APP_NAME=BAL MNA_BAL_ENV=test MNA_BAL_PUBLIC_URL=http://localhost +MNA_BAL_AUTH_USER_JWT_SECRET=jwtsecret +MNA_BAL_AUTH_ACTIVATION_JWT_SECRET=jwtsecret +MNA_BAL_AUTH_PASSWORD_JWT_SECRET=jwtsecret MNA_BAL_MONGODB_URI=mongodb://[user]:[password]@mongodb:27017/[db]?retryWrites=true&w=majority diff --git a/server/config/config.ts b/server/config/config.ts index db0d8401e..497ca3613 100644 --- a/server/config/config.ts +++ b/server/config/config.ts @@ -13,4 +13,18 @@ export const config = { type: env.get("MNA_BAL_LOG_TYPE").default("console").asString(), level: env.get("MNA_BAL_LOG_LEVEL").default("info").asString(), }, + auth: { + user: { + jwtSecret: env.get("MNA_BAL_AUTH_USER_JWT_SECRET").required().asString(), + expiresIn: "7d", + }, + activation: { + jwtSecret: env.get("MNA_BAL_AUTH_ACTIVATION_JWT_SECRET").required().asString(), + expiresIn: "96h", + }, + resetPasswordToken: { + jwtSecret: env.get("MNA_BAL_AUTH_PASSWORD_JWT_SECRET").required().asString(), + expiresIn: "1h", + }, + }, }; diff --git a/server/package.json b/server/package.json index 3b784d4b0..932a53e6d 100644 --- a/server/package.json +++ b/server/package.json @@ -15,13 +15,14 @@ "migration:create": "migrate-mongo create", "migration:up": "migrate-mongo up", "migration:down": "migrate-mongo down", - "cli": "ts-node -T src/modules/data/cli.ts ", + "cli": "ts-node -T src/modules/data/cli.ts", "cli-docker": "docker exec -it bal_server yarn cli", "test": "yarn test:cmd 'tests/**/*.test.ts'", "test:coverage": "c8 --reporter lcov --reporter text yarn test", "test:cmd": "cross-env NODE_ENV=test DOTENV_CONFIG_PATH=.env.test ts-mocha -n loader=ts-node/esm" }, "dependencies": { + "@fastify/auth": "^4.2.0", "@fastify/cors": "^8.2.0", "@fastify/type-provider-json-schema-to-ts": "^2.2.2", "axios": "0.24.0", @@ -37,7 +38,7 @@ "env-var": "7.1.1", "fastify": "^4.12.0", "json-schema-to-ts": "^2.7.2", - "jsonwebtoken": "8.5.1", + "jsonwebtoken": "^9.0.0", "lodash": "^4.17.21", "luxon": "2.3.0", "migrate-mongo": "9.0.0", @@ -55,6 +56,7 @@ "devDependencies": { "@types/bunyan": "^1.8.8", "@types/bunyan-prettystream": "^0.1.32", + "@types/jsonwebtoken": "^9.0.1", "@types/lodash": "^4.14.191", "@types/luxon": "^3.2.0", "@types/mocha": "^10.0.1", diff --git a/server/src/app.ts b/server/src/app.ts deleted file mode 100644 index de8a73438..000000000 --- a/server/src/app.ts +++ /dev/null @@ -1,18 +0,0 @@ -import fastifyCors from "@fastify/cors"; -import fastify, { FastifyServerOptions } from "fastify"; - -import { registerCoreModule } from "./modules/core"; - -export default function build(opts: FastifyServerOptions = {}) { - const app = fastify(opts); - - app.register(fastifyCors, {}); - app.register( - async (instance) => { - registerCoreModule({ server: instance }); - }, - { prefix: "/api" } - ); - - return app; -} diff --git a/server/src/models/collections.ts b/server/src/db/models.ts similarity index 100% rename from server/src/models/collections.ts rename to server/src/db/models.ts diff --git a/server/src/index.ts b/server/src/index.ts index 3f8293358..543fbd22b 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,8 +1,8 @@ import { config } from "config/config"; -import { configureDbSchemaValidation, connectToMongodb } from "./db/mongodb"; -import { modelDescriptors } from "./models/collections"; -import { server } from "./server"; +import { modelDescriptors } from "./db/models"; +import { server } from "./modules/server"; +import { configureDbSchemaValidation, connectToMongodb } from "./utils/mongodb"; (async function () { try { diff --git a/server/src/modules/actions/users.actions.ts b/server/src/modules/actions/users.actions.ts new file mode 100644 index 000000000..77a3ef7de --- /dev/null +++ b/server/src/modules/actions/users.actions.ts @@ -0,0 +1,30 @@ +import { Filter, FindOptions, ObjectId } from "mongodb"; +import { IUser } from "shared/models/user.model"; +import { IReqPostUser } from "shared/routes/user.routes"; + +import { createUserToken } from "../../utils/jwtUtils"; +import { getDbCollection } from "../../utils/mongodb"; + +export const createUser = async (data: IReqPostUser) => { + const _id = new ObjectId(); + const token = createUserToken({ ...data, _id: _id.toString() }); + + const { insertedId: userId } = await getDbCollection("users").insertOne({ + ...data, + _id, + token, + }); + + const user = await findUser({ _id: userId }); + + return user; +}; + +export const findUser = async ( + filter: Filter, + options?: FindOptions +) => { + const user = await getDbCollection("users").findOne(filter, options); + + return user; +}; diff --git a/server/src/modules/core/index.ts b/server/src/modules/core/index.ts deleted file mode 100644 index 4a4b416c7..000000000 --- a/server/src/modules/core/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Server } from "../../server"; -import { coreRoutes } from "./routes/core.routes"; -import { userRoutes } from "./routes/user.routes"; - -export const registerCoreModule = ({ server }: { server: Server }) => { - coreRoutes({ server }); - userRoutes({ server }); -}; diff --git a/server/src/modules/core/routes/user.routes.ts b/server/src/modules/core/routes/user.routes.ts deleted file mode 100644 index 0ed6e254f..000000000 --- a/server/src/modules/core/routes/user.routes.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { IUser } from "shared/models/user.model"; -import { SReqPostUser, SResPostUser } from "shared/routes/user.routes"; - -import { getDbCollection } from "../../../db/mongodb"; -import { Server } from "../../../server"; - -export const userRoutes = ({ server }: { server: Server }) => { - /** - * Créer un utilisateur - */ - server.post( - "/user", - { - schema: { - body: SReqPostUser, - response: { 200: SResPostUser }, - } as const, - }, - async (request, response) => { - try { - const { insertedId: userId } = await getDbCollection("users").insertOne( - request.body - ); - - const user = await getDbCollection("users").findOne({ - _id: userId, - }); - - if (!user) { - throw new Error("User not created"); - } - - return response.status(200).send(user); - } catch (error) { - console.error(error); - } - } - ); -}; diff --git a/server/src/modules/data/cli.ts b/server/src/modules/data/cli.ts new file mode 100644 index 000000000..62b6dd02b --- /dev/null +++ b/server/src/modules/data/cli.ts @@ -0,0 +1,38 @@ +import { Command } from "commander"; + +import { config } from "../../../config/config"; +import { modelDescriptors } from "../../db/models"; +import { + configureDbSchemaValidation, + connectToMongodb, +} from "../../utils/mongodb"; +import { createUser } from "../actions/users.actions"; +const program = new Command(); + +type IJob = () => Promise; + +export const runScript = async (job: IJob) => { + await connectToMongodb(config.mongodb.uri); + await configureDbSchemaValidation(modelDescriptors); + + await job(); +}; + +program + .command("users:create") + .description("Créer un utilisateur") + .option("-e, --email ", "Email de l'utilisateur") + .action(async ({ email }) => + runScript(async () => { + try { + const user = await createUser({ email }); + console.log(user); + process.exit(0); + } catch (error) { + console.error(error); + process.exit(1); + } + }) + ); + +program.parse(process.argv); diff --git a/server/src/modules/core/routes/core.routes.ts b/server/src/modules/server/core.routes.ts similarity index 82% rename from server/src/modules/core/routes/core.routes.ts rename to server/src/modules/server/core.routes.ts index 8945026a0..68ad55e50 100644 --- a/server/src/modules/core/routes/core.routes.ts +++ b/server/src/modules/server/core.routes.ts @@ -1,6 +1,6 @@ import { salut } from "shared"; -import { Server } from "../../../server"; +import { Server } from "."; export const coreRoutes = ({ server }: { server: Server }) => { server.get("/", async (request, response) => { diff --git a/server/src/modules/server/index.ts b/server/src/modules/server/index.ts new file mode 100644 index 000000000..4ff6035a0 --- /dev/null +++ b/server/src/modules/server/index.ts @@ -0,0 +1,46 @@ +import fastifyAuth, { FastifyAuthFunction } from "@fastify/auth"; +import fastifyCors from "@fastify/cors"; +import { JsonSchemaToTsProvider } from "@fastify/type-provider-json-schema-to-ts"; +import fastify, { FastifyServerOptions } from "fastify"; + +import { coreRoutes } from "./core.routes"; +import { userRoutes } from "./user.routes"; + +type FastifyServer = typeof server; +export interface Server extends FastifyServer { + validateJWT: FastifyAuthFunction; +} + +import { authValidateJWT } from "./utils/auth.strategies"; + +export function build(opts: FastifyServerOptions = {}) { + const app = fastify(opts); + + app.decorate("validateJWT", authValidateJWT); + + app.register(fastifyAuth); + app.register(fastifyCors, {}); + app.register( + async (instance) => { + registerRoutes({ server: instance as Server }); + }, + { prefix: "/api" } + ); + + return app; +} + +export const server = build({ + logger: true, + ajv: { + customOptions: { + strict: "log", + keywords: ["kind", "modifier"], + }, + }, +}).withTypeProvider(); + +export const registerRoutes = ({ server }: { server: Server }) => { + coreRoutes({ server }); + userRoutes({ server }); +}; diff --git a/server/src/modules/server/user.routes.ts b/server/src/modules/server/user.routes.ts new file mode 100644 index 000000000..bff3a179b --- /dev/null +++ b/server/src/modules/server/user.routes.ts @@ -0,0 +1,54 @@ +import { + SReqHeadersUser, + SReqPostUser, + SResGetUser, + SResPostUser, +} from "shared/routes/user.routes"; + +import { createUser } from "../actions/users.actions"; +import { Server } from "."; + +export const userRoutes = ({ server }: { server: Server }) => { + /** + * Récupérer l'utilisateur connecté + */ + server.get( + "/user", + { + schema: { + response: { 200: SResGetUser }, + headers: SReqHeadersUser, + } as const, + preHandler: server.auth([server.validateJWT]), + }, + async (request, response) => { + return response.status(200).send(request.authenticatedUser); + } + ); + + /** + * Créer un utilisateur + */ + server.post( + "/user", + { + schema: { + body: SReqPostUser, + response: { 200: SResPostUser }, + } as const, + }, + async (request, response) => { + try { + const user = await createUser(request.body); + + if (!user) { + throw new Error("User not created"); + } + + return response.status(200).send(user); + } catch (error) { + response.log.error(error); + } + } + ); +}; diff --git a/server/src/modules/server/utils/auth.strategies.ts b/server/src/modules/server/utils/auth.strategies.ts new file mode 100644 index 000000000..48676f70c --- /dev/null +++ b/server/src/modules/server/utils/auth.strategies.ts @@ -0,0 +1,44 @@ +import { FastifyAuthFunction } from "@fastify/auth"; +import { JwtPayload, verify } from "jsonwebtoken"; +import { ObjectId } from "mongodb"; +import { IUser } from "shared/models/user.model"; + +import { config } from "../../../../config/config"; +import { findUser } from "../../actions/users.actions"; + +declare module "fastify" { + export interface FastifyRequest { + authenticatedUser?: IUser; + } +} + +export const authValidateJWT: FastifyAuthFunction = async ( + request, + _reply, + done +) => { + let token: string | undefined = request.raw.headers["authorization"]; + if (!token) { + return done(new Error("Jeton manquant")); + } + + if (token.startsWith("Bearer ")) { + token = token.substring(7, token.length); + } else { + return done(new Error("Jeton invalide")); + } + + try { + const decoded = verify(token, config.auth.user.jwtSecret) as JwtPayload; + + const user = await findUser({ _id: new ObjectId(decoded.id) }); + + if (user) { + request.authenticatedUser = user; + } + + return done(); + } catch (error) { + return done(new Error("Jeton invalide")); + } +}; diff --git a/server/src/server.ts b/server/src/server.ts deleted file mode 100644 index e6ee470cb..000000000 --- a/server/src/server.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { JsonSchemaToTsProvider } from "@fastify/type-provider-json-schema-to-ts"; - -import build from "./app"; - -export const server = build({ - logger: true, - ajv: { - customOptions: { - strict: "log", - keywords: ["kind", "modifier"], - }, - }, -}).withTypeProvider(); - -export type Server = typeof server; diff --git a/server/src/utils/jwtUtils.ts b/server/src/utils/jwtUtils.ts new file mode 100644 index 000000000..24ff8aa6f --- /dev/null +++ b/server/src/utils/jwtUtils.ts @@ -0,0 +1,61 @@ +import jwt, { SignOptions } from "jsonwebtoken"; +import { IUser } from "shared/models/user.model"; + +import { config } from "../../config/config"; + +interface ICreateTokenOptions { + secret?: string; + expiresIn?: string; + payload?: string | Buffer | object; +} + +type TokenType = "user" | "resetPasswordToken" | "activation"; + +const createToken = ( + type: TokenType, + subject: string | null = null, + options: ICreateTokenOptions = {} +) => { + const defaults = config.auth[type]; + const secret = options.secret ?? defaults.jwtSecret; + const expiresIn = options.expiresIn ?? defaults.expiresIn; + const payload = options.payload ?? {}; + + const opts: SignOptions = { + issuer: config.appName, + expiresIn: expiresIn, + }; + if (subject) { + opts.subject = subject; + } + return jwt.sign(payload, secret, opts); +}; + +export function createResetPasswordToken( + username: string, + options: ICreateTokenOptions = {} +) { + return createToken("resetPasswordToken", username, options); +} + +export function createActivationToken( + subject: string, + options: ICreateTokenOptions = {} +) { + return createToken("activation", subject, options); +} + +export function createUserTokenSimple(options = {}) { + return createToken("user", null, options); +} + +export const createUserToken = ( + user: IUser, + options: ICreateTokenOptions = {} +) => { + const payload = { + id: user._id, + is_admin: user.is_admin, + }; + return createToken("user", user.email, { payload, ...options }); +}; diff --git a/server/src/db/mongodb.ts b/server/src/utils/mongodb.ts similarity index 96% rename from server/src/db/mongodb.ts rename to server/src/utils/mongodb.ts index ed67181c9..23bca9878 100644 --- a/server/src/db/mongodb.ts +++ b/server/src/utils/mongodb.ts @@ -1,8 +1,8 @@ import { CollectionInfo, MongoClient } from "mongodb"; import omitDeep from "omit-deep"; -import { IModelDescriptor } from "../models/collections"; -import logger from "../utils/logger"; +import { IModelDescriptor } from "../db/models"; +import logger from "./logger"; /** @type {MongoClient} */ let mongodbClient: MongoClient; @@ -105,8 +105,8 @@ const convertSchemaToMongoSchema = (schema: unknown) => { * @param {*} modelDescriptors */ export const configureDbSchemaValidation = async ( - modelDescriptors: IModelDescriptor[] -) => { + modelDescriptors: IModelDescriptor[] + ) => { const db = getDatabase(); ensureInitialization(); diff --git a/server/tests/routes/users.route.test.ts b/server/tests/routes/users.route.test.ts index d30460b6e..99d2c516f 100644 --- a/server/tests/routes/users.route.test.ts +++ b/server/tests/routes/users.route.test.ts @@ -1,27 +1,42 @@ import assert from "node:assert"; -import { IUser } from "shared/models/user.model"; - -import build from "../../src/app"; -import { getDbCollection } from "../../src/db/mongodb"; +import { createUser, findUser } from "../../src/modules/actions/users.actions"; +import { build } from "../../src/modules/server"; const app = build(); describe("Users routes", () => { - it("creates a user", async () => { + it("should get the current user", async () => { + const user = await createUser({ email: "connected@exemple.fr" }); + + const response = await app.inject({ + method: "GET", + url: "/api/user", + headers: { + ["Authorization"]: `Bearer ${user?.token}`, + }, + }); + + assert.equal(response.statusCode, 200); + assert.equal(response.json()._id, user?._id); + assert.equal(response.json().email, "connected@exemple.fr"); + assert.ok(response.json().token); + }); + + it("should create a user", async () => { const response = await app.inject({ method: "POST", url: "/api/user", - payload: { name: "name", email: "email@exemple.fr" }, + payload: { email: "email@exemple.fr" }, }); - const user = await getDbCollection("users").findOne({ + const user = await findUser({ email: "email@exemple.fr", }); assert.equal(response.statusCode, 200); assert.equal(response.json()._id, user?._id); - assert.equal(response.json().name, "name"); assert.equal(response.json().email, "email@exemple.fr"); + assert.ok(response.json().token); }); }); diff --git a/server/tests/utils/mongo.utils.ts b/server/tests/utils/mongo.utils.ts index f23e5cae2..e95b4e895 100644 --- a/server/tests/utils/mongo.utils.ts +++ b/server/tests/utils/mongo.utils.ts @@ -1,6 +1,9 @@ import { MongoMemoryServer } from "mongodb-memory-server"; -import { closeMongodbConnection, connectToMongodb } from "../../src/db/mongodb"; +import { + closeMongodbConnection, + connectToMongodb, +} from "../../src/utils/mongodb"; let mongoInMemory: MongoMemoryServer; diff --git a/shared/models/user.model.ts b/shared/models/user.model.ts index 4ae51a8a7..26de522f4 100644 --- a/shared/models/user.model.ts +++ b/shared/models/user.model.ts @@ -10,12 +10,12 @@ export const SUser = { type: "object", properties: { _id: { type: "string" }, - name: { type: "string" }, email: { type: "string" }, + token: { type: "string" }, }, - required: ["name"], + required: ["email"], } as const; -export type IUser = FromSchema; +export interface IUser extends FromSchema {} export default { schema: SUser, indexes, collectionName }; diff --git a/shared/routes/user.routes.ts b/shared/routes/user.routes.ts index 6b820d98a..13ea0183a 100644 --- a/shared/routes/user.routes.ts +++ b/shared/routes/user.routes.ts @@ -5,14 +5,23 @@ import { IUser, SUser } from "../models/user.model"; export const SReqPostUser = { type: "object", properties: { - name: { type: "string" }, email: { type: "string", format: "email" }, }, - required: ["name"], + required: ["email"], } as const; export type IReqPostUser = FromSchema; - +export const SResGetUser = SUser; export const SResPostUser = SUser; + +export interface IResGetUser extends IUser {} export interface IResPostUser extends IUser {} + +export const SReqHeadersUser = { + type: "object", + properties: { + Authorization: { type: "string" }, + }, + required: ["Authorization"], +}; diff --git a/yarn.lock b/yarn.lock index 6ba2dd57b..7a7ba9442 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1229,6 +1229,16 @@ __metadata: languageName: node linkType: hard +"@fastify/auth@npm:^4.2.0": + version: 4.2.0 + resolution: "@fastify/auth@npm:4.2.0" + dependencies: + fastify-plugin: ^4.0.0 + reusify: ^1.0.4 + checksum: a6f488c726ed3f5719ac824034b1681860e0d92463be087cc3b29e1b700bef8bfd767c338c26b71885dfc76dd613184ec7be9b2a6b62b0d6624989d0a027e307 + languageName: node + linkType: hard + "@fastify/cors@npm:^8.2.0": version: 8.2.0 resolution: "@fastify/cors@npm:8.2.0" @@ -2119,6 +2129,15 @@ __metadata: languageName: node linkType: hard +"@types/jsonwebtoken@npm:^9.0.1": + version: 9.0.1 + resolution: "@types/jsonwebtoken@npm:9.0.1" + dependencies: + "@types/node": "*" + checksum: a7f0925e9a42ad3ae970364c63c5986d40da5c83d51d3f4e624eb0f064a380376f9e3fb3f2f837390a9ab80143f5d75fd51866da30e110f6b486a3379e1c768f + languageName: node + linkType: hard + "@types/lodash@npm:^4.14.191": version: 4.14.191 resolution: "@types/lodash@npm:4.14.191" @@ -7177,21 +7196,15 @@ __metadata: languageName: node linkType: hard -"jsonwebtoken@npm:8.5.1": - version: 8.5.1 - resolution: "jsonwebtoken@npm:8.5.1" +"jsonwebtoken@npm:^9.0.0": + version: 9.0.0 + resolution: "jsonwebtoken@npm:9.0.0" dependencies: jws: ^3.2.2 - lodash.includes: ^4.3.0 - lodash.isboolean: ^3.0.3 - lodash.isinteger: ^4.0.4 - lodash.isnumber: ^3.0.3 - lodash.isplainobject: ^4.0.6 - lodash.isstring: ^4.0.1 - lodash.once: ^4.0.0 + lodash: ^4.17.21 ms: ^2.1.1 - semver: ^5.6.0 - checksum: 93c9e3f23c59b758ac88ba15f4e4753b3749dfce7a6f7c40fb86663128a1e282db085eec852d4e0cbca4cefdcd3a8275ee255dbd08fcad0df26ad9f6e4cc853a + semver: ^7.3.8 + checksum: b9181cecf9df99f1dc0253f91ba000a1aa4d91f5816d1608c0dba61a5623726a0bfe200b51df25de18c1a6000825d231ad7ce2788aa54fd48dcb760ad9eb9514 languageName: node linkType: hard @@ -7586,20 +7599,6 @@ __metadata: languageName: node linkType: hard -"lodash.includes@npm:^4.3.0": - version: 4.3.0 - resolution: "lodash.includes@npm:4.3.0" - checksum: 71092c130515a67ab3bd928f57f6018434797c94def7f46aafa417771e455ce3a4834889f4267b17887d7f75297dfabd96231bf704fd2b8c5096dc4a913568b6 - languageName: node - linkType: hard - -"lodash.isboolean@npm:^3.0.3": - version: 3.0.3 - resolution: "lodash.isboolean@npm:3.0.3" - checksum: b70068b4a8b8837912b54052557b21fc4774174e3512ed3c5b94621e5aff5eb6c68089d0a386b7e801d679cd105d2e35417978a5e99071750aa2ed90bffd0250 - languageName: node - linkType: hard - "lodash.isfunction@npm:^3.0.9": version: 3.0.9 resolution: "lodash.isfunction@npm:3.0.9" @@ -7607,13 +7606,6 @@ __metadata: languageName: node linkType: hard -"lodash.isinteger@npm:^4.0.4": - version: 4.0.4 - resolution: "lodash.isinteger@npm:4.0.4" - checksum: 6034821b3fc61a2ffc34e7d5644bb50c5fd8f1c0121c554c21ac271911ee0c0502274852845005f8651d51e199ee2e0cfebfe40aaa49c7fe617f603a8a0b1691 - languageName: node - linkType: hard - "lodash.ismatch@npm:^4.4.0": version: 4.4.0 resolution: "lodash.ismatch@npm:4.4.0" @@ -7621,13 +7613,6 @@ __metadata: languageName: node linkType: hard -"lodash.isnumber@npm:^3.0.3": - version: 3.0.3 - resolution: "lodash.isnumber@npm:3.0.3" - checksum: 913784275b565346255e6ae6a6e30b760a0da70abc29f3e1f409081585875105138cda4a429ff02577e1bc0a7ae2a90e0a3079a37f3a04c3d6c5aaa532f4cab2 - languageName: node - linkType: hard - "lodash.isplainobject@npm:^4.0.6": version: 4.0.6 resolution: "lodash.isplainobject@npm:4.0.6" @@ -7663,7 +7648,7 @@ __metadata: languageName: node linkType: hard -"lodash.once@npm:^4.0.0, lodash.once@npm:^4.1.1": +"lodash.once@npm:^4.1.1": version: 4.1.1 resolution: "lodash.once@npm:4.1.1" checksum: d768fa9f9b4e1dc6453be99b753906f58990e0c45e7b2ca5a3b40a33111e5d17f6edf2f768786e2716af90a8e78f8f91431ab8435f761fef00f9b0c256f6d245 @@ -10530,7 +10515,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.6.0, semver@npm:^5.7.1": +"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.7.1": version: 5.7.1 resolution: "semver@npm:5.7.1" bin: @@ -10581,10 +10566,12 @@ __metadata: version: 0.0.0-use.local resolution: "server@workspace:server" dependencies: + "@fastify/auth": ^4.2.0 "@fastify/cors": ^8.2.0 "@fastify/type-provider-json-schema-to-ts": ^2.2.2 "@types/bunyan": ^1.8.8 "@types/bunyan-prettystream": ^0.1.32 + "@types/jsonwebtoken": ^9.0.1 "@types/lodash": ^4.14.191 "@types/luxon": ^3.2.0 "@types/mocha": ^10.0.1 @@ -10605,7 +10592,7 @@ __metadata: env-var: 7.1.1 fastify: ^4.12.0 json-schema-to-ts: ^2.7.2 - jsonwebtoken: 8.5.1 + jsonwebtoken: ^9.0.0 lodash: ^4.17.21 luxon: 2.3.0 migrate-mongo: 9.0.0