283 lines
19 KiB
Markdown
283 lines
19 KiB
Markdown
# 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` | `<title>Docmost</title>` | `<title>DocAdenice</title>` |
|
|
| `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 + `<OidcLoginButton />` au-dessus de `<SsoLogin />` |
|
|
| `.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:<userId>:ws:<workspaceId>`). 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, `<group>:*` couvre `<group>:<action>`
|
|
- 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:<userId>:ws:<workspaceId>` (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)
|