diff --git a/.bin/commands.sh b/.bin/commands.sh index bee2f0f3d..1cfb02b32 100644 --- a/.bin/commands.sh +++ b/.bin/commands.sh @@ -9,8 +9,10 @@ function Help() { echo " init:env Update local env files using values from vault file" echo " docker:login Login to ghcr.io" echo " release:interactive Build & Push Docker image releases" + echo " release:manual Build & Push Docker image releases in a new release" echo " release:candidate Build & Push Docker image releases in a release candidate" echo " release:generate:rc-label Generate next rc label" + echo " release:generate:manual-label Generate next manual label" echo " release:app Build & Push Docker image releases" echo " deploy --user Deploy application to " echo " preview:build Build preview" @@ -55,10 +57,18 @@ function release:candidate() { "${SCRIPT_DIR}/release-candidate.sh" "$@" } +function release:manual() { + "${SCRIPT_DIR}/release-manual.sh" "$@" +} + function release:generate:rc-label() { "${SCRIPT_DIR}/generate-rc-label.sh" "$@" } +function release:generate:manual-label() { + "${SCRIPT_DIR}/generate-manual-label.sh" "$@" +} + function deploy() { "${SCRIPT_DIR}/deploy-app.sh" "$@" } diff --git a/.bin/scripts/generate-manual-label.sh b/.bin/scripts/generate-manual-label.sh new file mode 100755 index 000000000..1126ab4dc --- /dev/null +++ b/.bin/scripts/generate-manual-label.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Check if an argument is provided +if [[ $# -lt 1 ]]; then + echo "Error: No arguments provided." + echo "Usage: Provide 'patch', 'minor', or 'major' as the first argument." + exit 1 +fi + +readonly RC_TYPE=$1 + +case $RC_TYPE in + patch|minor|major) + ;; + *) + echo "Invalid argument: $RC_TYPE" + echo "Usage: Provide 'patch', 'minor', or 'major' as the first argument." + exit 1 + ;; +esac + +readonly VERSION=$("${ROOT_DIR}/.bin/scripts/get-version.sh") + +generate_next_version() { + local last_current_branch_tag=$(git describe --tags --abbrev=0 --match="v[0-9]*.[0-9]*.[0-9]*") + local remote_tags=$(git ls-remote --tags origin | awk '{print $2}' | sed 's|refs/tags/||') + local current_commit_id=$(git rev-parse HEAD) + local current_version_commit_id=$(git rev-list -n 1 $VERSION 2> /dev/null) + + if [ "$current_commit_id" == "$current_version_commit_id" ]; then + echo $VERSION; + return + fi; + + local version=${last_current_branch_tag#v} + + if [[ $version =~ ^([0-9]+)\.([0-9]+)\.([0-9]+).*$ ]]; then + major="${BASH_REMATCH[1]}" + minor="${BASH_REMATCH[2]}" + patch="${BASH_REMATCH[3]}" + else + echo "Invalid version format $version" + exit 1 + fi + + case $RC_TYPE in + major) + ((major++)) + minor=0 + patch=0 + ;; + minor) + ((minor++)) + patch=0 + ;; + patch) + ((patch++)) + ;; + *) + echo "Error: Invalid bump type. Use 'patch', 'minor', or 'major'." + exit 1 + ;; + esac + + manual_version="$major.$minor.$patch" + echo $manual_version +} + +echo $(generate_next_version "$@") diff --git a/.bin/scripts/generate-rc-label.sh b/.bin/scripts/generate-rc-label.sh index 93c6ccb59..5473e8b43 100755 --- a/.bin/scripts/generate-rc-label.sh +++ b/.bin/scripts/generate-rc-label.sh @@ -23,7 +23,7 @@ esac readonly VERSION=$("${ROOT_DIR}/.bin/scripts/get-version.sh") -generate_next_patch_version() { +generate_next_rc_version() { local last_current_branch_tag=$(git describe --tags --abbrev=0 --match="v[0-9]*.[0-9]*.[0-9]*") local remote_tags=$(git ls-remote --tags origin | awk '{print $2}' | sed 's|refs/tags/||') local current_commit_id=$(git rev-parse HEAD) @@ -77,4 +77,4 @@ generate_next_patch_version() { echo $rc_version } -echo $(generate_next_patch_version "$@") +echo $(generate_next_rc_version "$@") diff --git a/.bin/scripts/release-manual.sh b/.bin/scripts/release-manual.sh new file mode 100755 index 000000000..604f26265 --- /dev/null +++ b/.bin/scripts/release-manual.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +set -euo pipefail + + +# Check if an argument is provided +if [[ $# -lt 1 ]]; then + echo "Error: No arguments provided." + echo "Usage: Provide 'patch', 'minor', or 'major' as the first argument." + exit 1 +fi + +readonly RC_TYPE=$1 + +case $RC_TYPE in + patch|minor|major) + ;; + *) + echo "Invalid argument: $RC_TYPE" + echo "Usage: Provide 'patch', 'minor', or 'major' as the first argument." + exit 1 + ;; +esac + +readonly VERSION=$("${ROOT_DIR}/.bin/scripts/get-version.sh") +NEXT_VERSION=$("$ROOT_DIR/.bin/scripts/generate-manual-label.sh" "$@") + +echo "Creating release : $NEXT_VERSION" + +echo "Création des images docker locales (docker build)" + +echo "Build $NEXT_VERSION ..." +"$ROOT_DIR/.bin/scripts/release-app.sh" $NEXT_VERSION push +git tag -f "v$NEXT_VERSION" +git push -f origin "v$NEXT_VERSION" diff --git a/.github/workflows/_manual_release.yml b/.github/workflows/_manual_release.yml index 22a90dd81..02789da6e 100644 --- a/.github/workflows/_manual_release.yml +++ b/.github/workflows/_manual_release.yml @@ -1,10 +1,10 @@ -name: Release candidate +name: Manual Release on: workflow_dispatch: inputs: release_type: - description: Le type de Release Candidate à générer + description: Le type de Release à générer type: choice required: true options: @@ -15,7 +15,7 @@ on: workflow_call: inputs: release_type: - description: Le type de Release Candidate à générer + description: Le type de Release à générer type: string required: false @@ -89,7 +89,7 @@ jobs: run: echo "VERSION=$(git describe --tags --abbrev=0 | cut -c2-)" >> "$GITHUB_OUTPUT" - name: bump and release - run: yarn release:candidate ${{ inputs.release_type }} + run: yarn release:manual ${{ inputs.release_type }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/_release_candidate.yml b/.github/workflows/_release_candidate.yml new file mode 100644 index 000000000..22a90dd81 --- /dev/null +++ b/.github/workflows/_release_candidate.yml @@ -0,0 +1,141 @@ +name: Release candidate + +on: + workflow_dispatch: + inputs: + release_type: + description: Le type de Release Candidate à générer + type: choice + required: true + options: + - major + - minor + - patch + + workflow_call: + inputs: + release_type: + description: Le type de Release Candidate à générer + type: string + required: false + + secrets: + DEPLOY_SSH_PRIVATE_KEY: + description: SSH private key + required: true + DEPLOY_PASS: + description: SSH PWD TO DEPLOY + required: true + SLACK_WEBHOOK: + description: Slack webhook URL + required: true + VAULT_PWD: + description: Vault Password + required: true + +jobs: + tests: + uses: "./.github/workflows/ci.yml" + + release: + concurrency: + group: "release-${{ github.workflow }}-${{ github.ref }}" + permissions: write-all + outputs: + VERSION: ${{ steps.get-version.outputs.VERSION }} + PREV_VERSION: ${{ steps.get-prev-version.outputs.VERSION }} + runs-on: ubuntu-latest + steps: + - name: Checkout project + uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: true + + - uses: actions/setup-node@v3 + with: + node-version: 20 + + - uses: actions/cache@v3 + with: + path: | + **/node_modules + .yarn/install-state.gz + .yarn/cache + key: yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: yarn- + + - name: Install dependencies + run: yarn install + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + platforms: linux/amd64 + install: true + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Expose GitHub Runtime + uses: crazy-max/ghaction-github-runtime@v2 + + - name: Retrieve previous version + id: get-prev-version + run: echo "VERSION=$(git describe --tags --abbrev=0 | cut -c2-)" >> "$GITHUB_OUTPUT" + + - name: bump and release + run: yarn release:candidate ${{ inputs.release_type }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + GITHUB_REF_NAME: ${{ env.GITHUB_REF_NAME }} + + - name: Retrieve new version + id: get-version + run: echo "VERSION=$(git describe --tags --abbrev=0 | cut -c2-)" >> "$GITHUB_OUTPUT" + + docker-scout: + if: needs.release.outputs.VERSION != needs.release.outputs.PREV_VERSION && needs.release.outputs.PREV_VERSION != '' + concurrency: + group: "scout-${{ github.workflow }}-${{ github.ref }}" + needs: ["release"] + runs-on: ubuntu-latest + steps: + - name: Authenticate to Docker + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USER }} + password: ${{ secrets.DOCKER_PAT }} + + - name: Server Docker Scout + uses: docker/scout-action@v1 + with: + command: quickview,cves,recommendations,compare + image: ghcr.io/mission-apprentissage/ij_orion_server:${{ needs.release.outputs.VERSION }} + to: ghcr.io/mission-apprentissage/ij_orion_server:${{ needs.release.outputs.PREV_VERSION }} + sarif-file: sarif-server.output.json + + - name: Server Docker Upload SARIF result + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: sarif-server.output.json + category: Docker Server + + - name: UI Docker Scout + uses: docker/scout-action@v1 + with: + command: quickview,cves,recommendations,compare + image: ghcr.io/mission-apprentissage/ij_orion_ui:${{ needs.release.outputs.VERSION }}-production + to: ghcr.io/mission-apprentissage/ij_orion_ui:${{ needs.release.outputs.PREV_VERSION }}-production + sarif-file: sarif-ui.output.json + + - name: UI Docker Upload SARIF result + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: sarif-ui.output.json + category: Docker UI 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/package.json b/package.json index e83d3fb74..0fdaabaae 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "prettier:check": "prettier --check -u .", "release": "semantic-release", "release:interactive": ".bin/product release:interactive", + "release:manual": ".bin/product release:manual", "release:candidate": ".bin/product release:candidate", "postinstall": "husky", "talisman:add-exception": "yarn node-talisman --githook pre-commit -i", 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 1517778f1..427fd73c1 100644 --- a/server/src/db/schema.ts +++ b/server/src/db/schema.ts @@ -903,6 +903,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 2798d3dbd..22378287e 100644 --- a/server/src/migrations/index.ts +++ b/server/src/migrations/index.ts @@ -96,6 +96,7 @@ import * as migration_1730888921742 from "./migration_1730888921742"; import * as migration_1731666807035 from "./migration_1731666807035"; import * as migration_1732531157506 from "./migration_1732531157506"; import * as migration_1732800962380 from "./migration_1732800962380"; +import * as migration_1733743141284 from "./migration_1733743141284"; type Migration = { up: (db: Kysely) => Promise; @@ -199,4 +200,5 @@ export const migrations: Migrations = { migration_1731666807035, migration_1732531157506, migration_1732800962380, + 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;