# Acadenice Patches Liste des patches custom appliques sur le fork Acadenice de Docmost. Ce document est maintenu manuellement pour faciliter le rebase upstream. Repo upstream : `github.com/docmost/docmost` Branche fork : `acadenice/main` ## Conventions - Chaque patch est commit isole avec scope `feat(rebrand)` / `feat(custom)` / etc. - Les modifications in-line de fichiers upstream sont documentees ici avec rationale. - Les nouveaux fichiers (extensions Tiptap custom, hooks, etc.) vont dans des emplacements dedies pour minimiser les conflits de rebase. --- ## Patch 001 — Rebrand minimal "Docmost" -> "DocAdenice" **Date** : 2026-05-07 **Scope** : strings UI visibles utilisateur uniquement **Rationale** : nom temporaire pour les beta-testeurs en attendant le vrai rebranding (logo SVG + design system + manifest PWA). Conserve les identifiants techniques pour ne rien casser et faciliter le rebase upstream. ### Fichiers modifies | Fichier | Avant | Apres | |---------|-------|-------| | `apps/client/index.html` | `Docmost` | `DocAdenice` | | `apps/client/index.html` | `apple-mobile-web-app-title content="Docmost"` | `content="DocAdenice"` | | `apps/client/src/lib/config.ts` | `getAppName() return "Docmost"` | `return "DocAdenice"` | | `apps/client/src/components/layouts/global/app-header.tsx` | brand `aria-label`, `alt`, texte `Docmost` | `DocAdenice` | | `apps/client/src/features/auth/components/auth-layout.tsx` | brand `alt`, texte `Docmost` | `DocAdenice` | | `apps/client/src/components/ui/error-404.tsx` | titre 404 ` - Docmost` | ` - DocAdenice` | | `apps/client/src/features/home/components/home-ai-prompt.tsx` | fallback workspace name `"Docmost"` | `"DocAdenice"` | | `apps/server/src/integrations/transactional/emails/invitation-email.tsx` | `"You have been invited to Docmost."` | `"...DocAdenice."` | | `apps/server/src/integrations/transactional/partials/partials.tsx` | footer `© Docmost` | `© DocAdenice` | | `apps/server/src/core/workspace/services/workspace-invitation.service.ts` | sujet `... has accepted your Docmost invite` | `... DocAdenice invite` | | `apps/server/src/core/workspace/services/workspace-invitation.service.ts` | sujet `... invited you to Docmost` | `... DocAdenice` | | `apps/server/src/integrations/environment/environment.service.ts` | `MAIL_FROM_NAME` default `'Docmost'` | `'DocAdenice'` | | `README.md` | header initial Docmost | bloc "DocAdenice" ajoute au-dessus | ### KEEP volontairement (non modifies) | Element | Raison | |---------|--------| | `package.json` `name: "docmost"` | nom du package npm interne, casserait les imports/scripts Nx | | `@docmost/editor-ext` workspace package | identifiant pnpm workspace | | `docker-compose.yml` service `docmost` | identifiant technique | | `apps/server/src/core/auth/token.module.ts` JWT issuer `'Docmost'` | changer invaliderait les tokens existants | | `apps/server/src/core/workspace/workspace.constants.ts` `'docmost'` dans DISALLOWED_HOSTNAMES | blacklist hostnames reserves, technique | | `apps/server/src/common/helpers/types/export-metadata.types.ts` `source: 'docmost'` | format export pour interop avec Docmost officiel | | `apps/server/src/integrations/export/export.service.ts` filename `docmost-metadata.json` | format export, interop | | `apps/server/src/integrations/import/services/file-import-task.service.ts` (vars `docmostMetadata`, prefix `docmost-import`, fonction `readDocmostMetadata`) | identifiants techniques + lecture du format export Docmost | | `apps/server/src/integrations/import/utils/import.utils.ts` `readDocmostMetadata` | API publique du module import | | `apps/server/src/integrations/security/version.service.ts` URL `github.com/docmost/docmost/releases` | check de version vs upstream officiel | | `apps/server/src/integrations/telemetry/telemetry.service.ts` endpoint `tel.docmost.com` | telemetry upstream (a desactiver dans une iteration future via env var) | | `apps/client/src/components/settings/settings-sidebar.tsx` `help@docmost.com` | email support upstream officiel, on n'usurpe pas | | `apps/client/src/components/settings/app-version.tsx` URL releases | check de version upstream | | `apps/client/src/ee/**` (license, AI, MCP, API keys, share-branding "Powered by Docmost") | code Enterprise Edition propriete Docmost — copy commerciale, ne pas masquer | | `apps/client/src/ee/components/posthog-user.tsx` `source: "docmost-app"` | identifiant analytics upstream | | `apps/server/src/integrations/environment/environment.validation.ts` URL clickhouse exemple | message d'erreur dev-facing technique | | `apps/server/src/core/workspace/services/workspace.service.ts` `@deleted.docmost.com` | placeholder technique pour soft-delete | --- ## Patch 002 — Bloc 4b : OIDC client (Authentik) via openid-client **Date** : 2026-05-07 **Scope** : nouveau flow d'authentification SSO via Authentik (ou tout IdP OIDC), desactive par defaut **Rationale** : preparer l'integration SSO pour le hub Acadenice. Le code est dormant tant que `OIDC_ENABLED=true` n'est pas pose, donc zero impact sur les deploiements actuels. Les fichiers sont isoles dans un sous-dossier dedie pour faciliter le rebase upstream. ### Lib utilisee `openid-client` v6.8.2 — deja en dependance dans `apps/server/package.json`. API fonctionnelle (pas un client object-oriented), import lazy au boot pour eviter l'overhead quand OIDC est off. ### Fichiers crees | Fichier | Role | |---------|------| | `apps/server/src/core/auth/oidc/oidc.module.ts` | Module Nest dedie, importe par CoreModule | | `apps/server/src/core/auth/oidc/oidc.service.ts` | Discovery, PKCE, callback handler, JIT provisioning | | `apps/server/src/core/auth/oidc/oidc.controller.ts` | Routes `/api/auth/oidc/login`, `/callback`, `/status` | | `apps/server/src/core/auth/oidc/oidc.service.spec.ts` | 8 tests unitaires (Jest) avec `openid-client` mocke | | `apps/client/src/features/auth/queries/oidc-query.ts` | Hook `useOidcStatus()` (React Query) | | `apps/client/src/features/auth/components/oidc-login-button.tsx` | Bouton SSO conditionnel sur le formulaire login | ### Fichiers modifies (touches minimales) | Fichier | Modification | |---------|--------------| | `apps/server/src/integrations/environment/environment.service.ts` | +9 getters OIDC (isOidcEnabled, getOidcIssuer, ...) appendus en fin de classe | | `apps/server/src/core/core.module.ts` | +1 import + 1 ligne dans `imports[]` pour `OidcModule` | | `apps/client/src/features/auth/components/login-form.tsx` | +2 lignes : import + `` au-dessus de `` | | `.env.example` | bloc OIDC commente ajoute en fin de fichier | ### Securite - PKCE S256 (verifier + challenge generes par `openid-client`) - State CSRF stocke en cookie httpOnly signe (5 min TTL) - ID token verifie par signature JWKS (gere par `openid-client` v6 via la `Configuration` cachee) - userInfo refetched apres l'echange — on ne fait pas confiance aux claims ID token seuls pour `email` - Cookies temporaires `oidc_state` / `oidc_pkce` clear immediatement apres consommation ### Variables d'env | Var | Defaut | Role | |-----|--------|------| | `OIDC_ENABLED` | `false` | master switch | | `OIDC_ISSUER` | (vide) | URL discovery (ex `https://auth.example.com/application/o/docadenice/`) | | `OIDC_CLIENT_ID` | (vide) | requis | | `OIDC_CLIENT_SECRET` | (vide) | requis | | `OIDC_REDIRECT_URI` | `${APP_URL}/api/auth/oidc/callback` | derive auto si non set | | `OIDC_SCOPES` | `openid email profile` | Authentik : `groups` claim arrive via le scope `profile` (pas un scope standard) | | `OIDC_PROVIDER_NAME` | `SSO` | label affiche sur le bouton | | `OIDC_AUTO_PROVISION` | `false` | si true : cree le user a la volee si email inconnu | | `OIDC_DEFAULT_WORKSPACE_ID` | (vide) | requis si multi-workspace + auto-provision | ### TODO Bloc 4b suivants - Mapping groupes Authentik vers roles Docmost (`OWNER` / `ADMIN` / `MEMBER`) - Logout federe (RP-initiated logout vers Authentik) - Tests E2E avec un vrai container Authentik (Testcontainers) - Bouton login OIDC integre au flow `enforceSso` cote workspace (actuellement le bouton apparait des que `OIDC_ENABLED=true`, sans condition supplementaire) --- ## Patch 003 — R2.1 : RBAC dynamique multi-roles (backend) **Date** : 2026-05-07 **Scope** : nouveau systeme RBAC source-de-verite cote DocAdenice + JWT enrichi `acadenice_permissions[]` + 5 roles classiques pre-seed **Rationale** : DocAdenice devient generique (pivot R1) — il faut un RBAC dynamique multi-roles (un user peut cumuler plusieurs roles, union des permissions) editable par l'admin via UI, qui signe ses permissions effectives dans le JWT pour que le bridge formation-hub les consume sans re-query la base. Pattern Notion. Le RBAC custom cohabite avec les roles natifs Docmost (`WorkspaceUser.role` `OWNER`/`ADMIN`/`MEMBER`) — natifs gardes pour les guards Docmost upstream, le RBAC Acadenice est une couche par-dessus. ### Catalogue de permissions 22 permissions atomiques generiques, catalogue ferme en TS dans `apps/server/src/core/acadenice/rbac/permissions-catalog.ts`. Toute insertion en base est validee contre ce catalogue cote service. Groupes : `pages`, `space`, `tables`, `rows`, `attachments`, `users` + meta `roles:manage` + wildcard `admin:*`. ### Tables Postgres Toutes prefixees `acadenice_` (zero conflit avec les tables upstream Docmost) : - `acadenice_role` (id, workspace_id FK, name, description, is_system_role, ts) — unique (workspace_id, name) - `acadenice_role_permission` (role_id FK cascade, permission_key) — pk composee - `acadenice_user_role` (user_id FK cascade, role_id FK cascade, workspace_id FK, assigned_by FK set null, assigned_at) — pk composee (user_id, role_id) Migration : `apps/server/src/database/migrations/20260507T120000-create-acadenice-rbac.ts`. Idempotente (`ifNotExists` partout). ### 5 roles classiques pre-seed (au boot, idempotent) | Role | is_system | Permissions | |------|-----------|-------------| | Owner | true | `admin:*` | | Admin | true | tout sauf `*:delete` et `roles:manage` | | Editor | true | pages:read/write/share, space:read/write, rows:read/write, attachments:upload | | Member | true | read-only + attachments:upload | | Guest | true | `pages:read` | `is_system_role=true` -> rename / delete refuses cote service. Permissions modifiables (admin peut reshape les roles seed sans perdre l'identite system). `AcadeniceRbacSeedService.onModuleInit()` au boot : seed tous les workspaces existants. Idempotent : ne duplique pas. Failure -> log et le boot poursuit (in my tests, le seed echoue tot si la migration n'a pas tourne, et le boot suivant retente). ### JWT enrichi `TokenService.generateAccessToken()` injecte `acadenice_permissions: string[]` dans le payload du token d'acces. Cache Redis 60s par user/workspace (key `acadenice:perms:user::ws:`). Si le user a `admin:*`, la liste est court-circuitee a `["admin:*"]`. Failure de resolution -> claim vide (degradation graceful, le bridge a son propre fallback). ### Guard NestJS Decorator `@RequirePermission('roles:manage')` + `AcadenicePermissionsGuard` : - Lit `req.user = { user, workspace }` (pose en amont par `JwtAuthGuard`) - Resoud les permissions via `AcadeniceRoleService.getUserPermissions` (cache Redis 60s) - Match avec wildcard support : `admin:*` couvre tout, `:*` couvre `:` - Throw `ForbiddenException` si miss ### Endpoints REST ``` GET /api/acadenice/permissions (auth) GET /api/acadenice/roles (auth) POST /api/acadenice/roles (perm: roles:manage) GET /api/acadenice/roles/:id (perm: roles:manage) PATCH /api/acadenice/roles/:id (perm: roles:manage) DELETE /api/acadenice/roles/:id (perm: roles:manage) GET /api/acadenice/roles/:id/permissions (perm: roles:manage) PUT /api/acadenice/roles/:id/permissions (perm: roles:manage) GET /api/acadenice/users/:userId/roles (perm: roles:manage OR self) POST /api/acadenice/users/:userId/roles (perm: roles:manage, body { roleIds: [...] }) DELETE /api/acadenice/users/:userId/roles/:roleId (perm: roles:manage) ``` Les routes user-roles refusent l'auto-modification meme avec `roles:manage` (anti-escalation). ### Fichiers crees | Fichier | Role | |---------|------| | `apps/server/src/core/acadenice/rbac/permissions-catalog.ts` | Catalogue ferme + helpers `isPermissionKey`, `permissionMatches` | | `apps/server/src/core/acadenice/rbac/rbac.module.ts` | Module Nest, exporte `AcadeniceRoleService` + guard | | `apps/server/src/core/acadenice/rbac/dto/create-role.dto.ts` | CreateRoleDto | | `apps/server/src/core/acadenice/rbac/dto/update-role.dto.ts` | UpdateRoleDto + UpdateRolePermissionsDto | | `apps/server/src/core/acadenice/rbac/dto/assign-role.dto.ts` | AssignRolesDto | | `apps/server/src/core/acadenice/rbac/dto/role.dto.ts` | RoleDto + RoleWithPermissionsDto + UserRoleAssignmentDto | | `apps/server/src/core/acadenice/rbac/repos/role.repo.ts` | Repo SQL natif (pas de typed Kysely : tables hors `db.d.ts`, rebase-friendly) | | `apps/server/src/core/acadenice/rbac/repos/user-role.repo.ts` | Repo user-role + `getEffectivePermissions` (union via JOIN, court-circuit `admin:*`) | | `apps/server/src/core/acadenice/rbac/services/role.service.ts` | RoleService — CRUD, assignation, cache Redis 60s, validation catalog | | `apps/server/src/core/acadenice/rbac/services/seed.service.ts` | SeedService — 5 roles seed idempotent au boot | | `apps/server/src/core/acadenice/rbac/guards/permissions.guard.ts` | AcadenicePermissionsGuard avec wildcard support | | `apps/server/src/core/acadenice/rbac/guards/require-permission.decorator.ts` | `@RequirePermission(...)` decorator | | `apps/server/src/core/acadenice/rbac/controllers/permissions.controller.ts` | GET /api/acadenice/permissions (catalog read-only) | | `apps/server/src/core/acadenice/rbac/controllers/roles.controller.ts` | CRUD roles + permissions | | `apps/server/src/core/acadenice/rbac/controllers/user-roles.controller.ts` | Assignations user-roles | | `apps/server/src/core/acadenice/rbac/spec/role.service.spec.ts` | 11 tests unitaires (Jest, mocks repos) | | `apps/server/src/core/acadenice/rbac/spec/permissions.guard.spec.ts` | 11 tests unitaires (Reflector mock + permissionMatches) | | `apps/server/src/core/acadenice/rbac/spec/seed.service.spec.ts` | 3 tests unitaires (idempotence + creation initiale) | | `apps/server/src/database/migrations/20260507T120000-create-acadenice-rbac.ts` | Migration Kysely 3 tables | ### Fichiers modifies (touches minimales) | Fichier | Modification | |---------|--------------| | `apps/server/src/core/auth/dto/jwt-payload.ts` | +1 champ optionnel `acadenice_permissions?: string[]` sur `JwtPayload` | | `apps/server/src/core/auth/services/token.service.ts` | +injection optionnelle `AcadeniceRoleService`, +resoud les perms a chaque sign d'access token (failure graceful = []) | | `apps/server/src/core/auth/token.module.ts` | +import `AcadeniceRbacModule`, re-export pour les consumers de TokenModule | | `apps/server/src/core/core.module.ts` | +import `AcadeniceRbacModule` dans `imports[]` | ### Cache strategy - Cle Redis : `acadenice:perms:user::ws:` (TTL 60s) - Best-effort : un failure cache (Redis down, parse error) -> fallback sur SQL, in my tests pas de 500 - Invalidation : sur mutation user-role, on `DEL` la cle du user. Sur mutation de role/permission, on `SCAN` + `DEL` toutes les cles du workspace (pas de `KEYS *` bloquant) - Service `AcadeniceRoleService` rendu utilisable hors-Redis (`@Optional()` sur l'injection) — utile pour les tests unitaires ### Cohabitation avec roles natifs Docmost - `WorkspaceUser.role` (OWNER/ADMIN/MEMBER) : KEEP — utilise par les guards natifs Docmost (creation page, invitation, etc.) - `acadenice_role` : couche par-dessus, source de verite pour le JWT et le bridge - L'admin DocAdenice peut couvrir 100% des cas via les `acadenice_role` + Owner/Admin natifs s'il decide de mapper les permissions natives plus tard. Compatible. ### Edge cases couverts - User sans aucun role -> `acadenice_permissions: []` dans le JWT - User avec `admin:*` -> `["admin:*"]` (court-circuit, payload tiny) - Role rename quand un autre role porte deja le nouveau nom -> `409 Conflict` - Rename / delete d'un system role -> `403 Forbidden` (cote service) - Auto-modification de ses propres roles -> `403 Forbidden` (anti-escalation) - Assignation d'un role d'un autre workspace -> `404 NotFound` validation prealable atomique - Assignation duplique du meme role -> idempotent (`ON CONFLICT DO NOTHING`) - Permission inconnue dans une payload -> `400 BadRequest` avec liste autorisee - Migration re-run sur DB partiellement migree -> `ifNotExists` evite l'erreur - Seed echoue au boot -> log, le boot continue, retry au boot suivant - Redis down -> service degrade en SQL direct, JWT a quand meme les perms (a chaque sign) ### TODO laisses - Frontend `/settings/roles` (R2.2) — page admin pour gerer les roles - Mapping group sync OIDC -> `acadenice_role` (utile en couplage avec Patch 002) - Audit log des changements de role (qui a assigne quoi a qui) - Quand un nouveau workspace est cree, le seed actuel ne s'execute qu'au prochain boot — il faudrait hooker `WorkspaceService.create` pour seeder en live (R2.2 ou R2.3) - Endpoint `POST /api/acadenice/permissions/me` pour query rapidement les perms du user courant cote front (alternativement : decoder le JWT) - Permissions cache : invalidation cross-workspace via Redis Streams si on monte plusieurs instances Docmost (probleme deja present dans Docmost natif, hors scope R2.1) ### Bugs detectes dans Docmost natif Aucun bug bloquant. Le test stub `apps/server/src/core/auth/services/token.service.spec.ts` etait deja casse avant ce patch (provider declare sans ses dependances) — non touche pour ne pas mentir sur la dette upstream. ### Verifications skipped - `pnpm install` : pas execute (deps absents en local par convention de l'agent fork — Corentin install + build) - TypeScript build : pas execute (cf ci-dessus) - Migration runtime : pas executee (pas de Postgres local pour le moment) - Tests Jest : ecrits mais pas runs en local (pnpm absent) --- ### TODO rebrand complet (futur) - Logo SVG / favicon DocAdenice (actuellement reutilise `/icons/favicon-32x32.png` upstream) - Manifest PWA (`apps/client/public/manifest.json`) : name, short_name, icons - `apps/client/public/icons/` : pack d'icones Acadenice (16, 32, 192, 512, apple-touch) - Palette couleur design system (theme Mantine custom) - Eventuellement disable telemetry upstream par defaut (env var ou patch) - Decider du sort de l'EE branding ("Powered by Docmost" sur les pages partagees publiques) - Crowdin / i18n : ajouter une cle `appName` au lieu du hardcode et router via `getAppName()` - Strategie : renommer le package npm `docmost` -> `docadenice` quand on aura un build pipeline custom complet (impacte trop d'imports actuellement)