diff --git a/docs/developpement/developpement.md b/docs/developpement/developpement.md index a59a310a7..9b5aebeca 100644 --- a/docs/developpement/developpement.md +++ b/docs/developpement/developpement.md @@ -84,6 +84,7 @@ Commandes: - `yarn cli migrations:status`: Vérification du status des migrations - `yarn cli migrations:up`: Execution des migrations - `yarn cli migrations:create`: Creation d'une nouvelle migration +- `yarn cli migrations:generate-schema`: Génère le schéma de la base de données ### Lancement de l'application diff --git a/server/package.json b/server/package.json index 69edc1ee4..182d94ecb 100644 --- a/server/package.json +++ b/server/package.json @@ -16,7 +16,6 @@ "dev": "tsup-node --env.TSUP_WATCH true", "build": "tsup-node --env.NODE_ENV production", "typecheck": "NODE_OPTIONS='--max-old-space-size=8192' tsc --noEmit", - "kysely": "npx kysely-codegen --schema public --out-file=./src/db/schema.ts --dialect postgres && yarn prettier:fix", "generate:seed": "./seed/generate-seed.sh" }, "dependencies": { diff --git a/server/src/commands.ts b/server/src/commands.ts index b9fbad966..47601d075 100644 --- a/server/src/commands.ts +++ b/server/src/commands.ts @@ -1,3 +1,4 @@ +import { exec } from "node:child_process"; import { setMaxListeners } from "node:events"; import { writeFileSync } from "node:fs"; import path from "node:path"; @@ -156,6 +157,16 @@ export const down = async (db: Kysely) => {}; ); }); +program + .command("migrations:generate-schema") + .description("Generate kysely schema") + .action(async () => { + console.log(path.join(__dirname(), "../src", "db/schema.ts")); + exec( + `DATABASE_URL="${config.psql.uri}" npx kysely-codegen --schema public --out-file=${path.join(__dirname(), "../src", "db/schema.ts")} --dialect postgres && yarn prettier:fix` + ); + }); + productCommands(program); export async function startCLI() { diff --git a/server/src/db/schema.ts b/server/src/db/schema.ts index cdf091e66..c267aacda 100644 --- a/server/src/db/schema.ts +++ b/server/src/db/schema.ts @@ -893,6 +893,7 @@ export interface User { enabled: Generated; sub: string | null; lastSeenAt: Timestamp | null; + fonction: string | null; } export interface DB { diff --git a/server/src/migrations/index.ts b/server/src/migrations/index.ts index 4636915c3..b8dd103c6 100644 --- a/server/src/migrations/index.ts +++ b/server/src/migrations/index.ts @@ -94,6 +94,7 @@ import * as migration_1727790113090 from "./migration_1727790113090"; import * as migration_1729693085251 from "./migration_1729693085251"; import * as migration_1730888921742 from "./migration_1730888921742"; import * as migration_1731666807035 from "./migration_1731666807035"; +import * as migration_1733743141284 from "./migration_1733743141284"; type Migration = { up: (db: Kysely) => Promise; @@ -195,4 +196,5 @@ export const migrations: Migrations = { migration_1730888921742, migration_1729693085251, migration_1731666807035, + migration_1733743141284, }; diff --git a/server/src/migrations/migration_1733743141284.ts b/server/src/migrations/migration_1733743141284.ts new file mode 100644 index 000000000..ab6fcb20d --- /dev/null +++ b/server/src/migrations/migration_1733743141284.ts @@ -0,0 +1,11 @@ +import type { Kysely } from "kysely"; + +import type { DB } from "@/db/schema"; + +export const up = async (db: Kysely) => { + return await db.schema.alterTable("user").addColumn("fonction", "varchar").execute(); +}; + +export const down = async (db: Kysely) => { + return await db.schema.alterTable("user").dropColumn("fonction").execute(); +}; diff --git a/server/src/modules/core/usecases/createUser/createUser.usecase.ts b/server/src/modules/core/usecases/createUser/createUser.usecase.ts index cfe0a4371..660eb35cf 100644 --- a/server/src/modules/core/usecases/createUser/createUser.usecase.ts +++ b/server/src/modules/core/usecases/createUser/createUser.usecase.ts @@ -22,7 +22,7 @@ export const [createUser, createUserFactory] = inject( }, (deps) => async ({ body, requestUser }: { body: BodySchema; requestUser?: RequestUser }) => { - const { email, firstname, lastname, role, codeRegion } = body; + const { email, firstname, lastname, role, codeRegion, fonction } = body; if (!email.match(emailRegex)) throw Boom.badRequest(`L'email est invalide`); @@ -46,7 +46,9 @@ export const [createUser, createUserFactory] = inject( role, codeRegion, enabled: true, + fonction, }); + const activationToken = jwt.sign({ email }, config.auth.activationJwtSecret, { issuer: "orion", }); diff --git a/server/src/modules/core/usecases/getUsers/getUsers.route.ts b/server/src/modules/core/usecases/getUsers/getUsers.route.ts index 425f7351d..dd46200ae 100644 --- a/server/src/modules/core/usecases/getUsers/getUsers.route.ts +++ b/server/src/modules/core/usecases/getUsers/getUsers.route.ts @@ -1,4 +1,5 @@ import { createRoute } from "@http-wizard/core"; +import type { UserFonction } from "shared/enum/userFonction"; import { ROUTES } from "shared/routes/routes"; import { getScopeFilterForUser } from "@/modules/core/utils/getScopeFilterForUser"; @@ -29,7 +30,14 @@ export const getUsersRoute = (server: Server) => { scope, scopeFilter, }); - response.code(200).send(users); + + response.code(200).send({ + count: users.count, + users: users.users.map((user) => ({ + ...user, + fonction: user.fonction as UserFonction, + })), + }); }, }); }); diff --git a/server/src/modules/core/utils/extractUserInRequest/extractUserInRequest.test.ts b/server/src/modules/core/utils/extractUserInRequest/extractUserInRequest.test.ts index 466079e0a..5adb164f8 100644 --- a/server/src/modules/core/utils/extractUserInRequest/extractUserInRequest.test.ts +++ b/server/src/modules/core/utils/extractUserInRequest/extractUserInRequest.test.ts @@ -52,6 +52,7 @@ describe("extractUserInRequest usecase", () => { enabled: false, sub: undefined, lastSeenAt: undefined, + fonction: undefined, }), }); @@ -79,6 +80,7 @@ describe("extractUserInRequest usecase", () => { enabled: true, sub: undefined, lastSeenAt: undefined, + fonction: undefined, }), }); diff --git a/shared/enum/userFonction.ts b/shared/enum/userFonction.ts new file mode 100644 index 000000000..39c860a73 --- /dev/null +++ b/shared/enum/userFonction.ts @@ -0,0 +1,21 @@ +import { z } from "zod"; + +export const userFonction = z.enum([ + "Région", + "Région académique", + "Inspecteur", + "DO CMQ", + "Conseiller en formation professionnelle", + "Coordonnateur de CFA-A", + "DRAIO", + "Services DOS", + "DASEN", + "DRAFPIC", + "SGRA", + "CSA", + "Recteur", +]); + +export const UserFonctionEnum = userFonction.Enum; + +export type UserFonction = z.infer; diff --git a/shared/routes/schemas/get.users.schema.ts b/shared/routes/schemas/get.users.schema.ts index d512130c1..5ea078557 100644 --- a/shared/routes/schemas/get.users.schema.ts +++ b/shared/routes/schemas/get.users.schema.ts @@ -1,9 +1,10 @@ import { z } from "zod"; +import { userFonction } from "../../enum/userFonction"; import type { Role } from "../../security/permissions"; import { PERMISSIONS } from "../../security/permissions"; -const UserSchema = z.object({ +export const UserSchema = z.object({ id: z.string(), firstname: z.string().optional(), lastname: z.string().optional(), @@ -14,6 +15,7 @@ const UserSchema = z.object({ createdAt: z.string().optional(), uais: z.array(z.string()).optional(), enabled: z.boolean(), + fonction: userFonction.optional(), }); export const getUsersSchema = { diff --git a/shared/routes/schemas/post.users.userId.schema.ts b/shared/routes/schemas/post.users.userId.schema.ts index 58063bb54..ad885ee61 100644 --- a/shared/routes/schemas/post.users.userId.schema.ts +++ b/shared/routes/schemas/post.users.userId.schema.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import { userFonction } from "../../enum/userFonction"; import type { Role } from "../../security/permissions"; import { PERMISSIONS } from "../../security/permissions"; @@ -9,6 +10,7 @@ const BodySchema = z.object({ email: z.string().email().toLowerCase(), role: z.enum(Object.keys(PERMISSIONS) as [Role]), codeRegion: z.string().min(1).optional(), + fonction: userFonction.optional(), }); export type BodySchema = z.infer; diff --git a/shared/routes/schemas/put.users.userId.schema.ts b/shared/routes/schemas/put.users.userId.schema.ts index d73100ea6..521f0ab78 100644 --- a/shared/routes/schemas/put.users.userId.schema.ts +++ b/shared/routes/schemas/put.users.userId.schema.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import { userFonction } from "../../enum/userFonction"; import type { Role } from "../../security/permissions"; import { PERMISSIONS } from "../../security/permissions"; @@ -10,6 +11,7 @@ const BodySchema = z.object({ role: z.enum(Object.keys(PERMISSIONS) as [Role]), codeRegion: z.string().min(1).nullable(), enabled: z.boolean(), + fonction: userFonction.optional(), }); export type BodySchema = z.infer; diff --git a/ui/app/(wrapped)/admin/users/CreateUser.tsx b/ui/app/(wrapped)/admin/users/CreateUser.tsx index 36bd7b3f8..0c21ca075 100644 --- a/ui/app/(wrapped)/admin/users/CreateUser.tsx +++ b/ui/app/(wrapped)/admin/users/CreateUser.tsx @@ -20,6 +20,7 @@ import { useEffect } from "react"; import { useForm } from "react-hook-form"; import type { Role } from "shared"; import { getHierarchy } from "shared"; +import { UserFonctionEnum } from "shared/enum/userFonction"; import { z } from "zod"; import { client } from "@/api.client"; @@ -155,6 +156,19 @@ export const CreateUser = ({ isOpen, onClose }: { isOpen: boolean; onClose: () = {getErrorMessage(error)} )} + + + Fonction de l'utilisateur + + {!!errors.fonction && {errors.fonction.message}} + diff --git a/ui/app/(wrapped)/admin/users/EditUser.tsx b/ui/app/(wrapped)/admin/users/EditUser.tsx index 7223d8421..6238e3d3a 100644 --- a/ui/app/(wrapped)/admin/users/EditUser.tsx +++ b/ui/app/(wrapped)/admin/users/EditUser.tsx @@ -23,6 +23,7 @@ import { useEffect } from "react"; import { useForm } from "react-hook-form"; import type { Role } from "shared"; import { getHierarchy } from "shared"; +import { UserFonctionEnum } from "shared/enum/userFonction"; import { z } from "zod"; import { client } from "@/api.client"; @@ -154,11 +155,24 @@ export const EditUser = ({ {!!errors.codeRegion && {errors.codeRegion.message}} - - - Compte actif - - {!!errors.enabled && {errors.enabled.message}} + + Fonction de l'utilisateur + + + + + Compte actif + + {!!errors.enabled && {errors.enabled.message}} + + {!!errors.fonction && {errors.fonction.message}} {isError && ( diff --git a/ui/app/(wrapped)/admin/users/page.tsx b/ui/app/(wrapped)/admin/users/page.tsx index de4c410b9..56f5ca9ee 100644 --- a/ui/app/(wrapped)/admin/users/page.tsx +++ b/ui/app/(wrapped)/admin/users/page.tsx @@ -47,6 +47,7 @@ const Columns = { libelleRegion: "Région", uais: "Uais", createdAt: "Ajouté le", + fonction: "Fonction", } satisfies ExportColumns<(typeof client.infer)["[GET]/users"]["users"][number]>; // eslint-disable-next-line import/no-anonymous-default-export, react/display-name @@ -170,6 +171,10 @@ export default () => { {Columns.role} + handleOrder("fonction")}> + + {Columns.fonction} + handleOrder("enabled")}> {Columns.enabled} @@ -196,6 +201,7 @@ export default () => { {user.firstname} {user.lastname} {user.role} + {user.fonction ?? "-"} {user.enabled ? Actif : Désactivé} diff --git a/ui/app/(wrapped)/intentions/perdir/synthese/[numero]/actions/FONCTIONS.ts b/ui/app/(wrapped)/intentions/perdir/synthese/[numero]/actions/FONCTIONS.ts index b19dfa656..f75a7ed7d 100644 --- a/ui/app/(wrapped)/intentions/perdir/synthese/[numero]/actions/FONCTIONS.ts +++ b/ui/app/(wrapped)/intentions/perdir/synthese/[numero]/actions/FONCTIONS.ts @@ -1,19 +1,20 @@ import type { AvisTypeType } from "shared/enum/avisTypeEnum"; +import type { UserFonction } from "shared/enum/userFonction"; export const FONCTIONS = { - préalable: ["région", "région académique"], + préalable: ["Région", "Région académique"], consultatif: [ - "inspecteur", + "Inspecteur", "DO CMQ", - "conseiller en formation professionnelle", - "coordonnateur de CFA-A", + "Conseiller en formation professionnelle", + "Coordonnateur de CFA-A", "DRAIO", - "services DOS", + "Services DOS", "DASEN", - "région", + "Région", "DRAFPIC", "SGRA", - "recteur", + "Recteur", ], - final: ["région", "CSA", "recteur"], -} as Record; + final: ["Région", "CSA", "Recteur"], +} as Record;