doc: split Architecture.md into per-topic pages
doc/Architecture.md was 436 lines and growing; this commit
extracts each non-trivial subject into its own page under
doc/architecture/ and reduces the root document to a table of
contents + transversal sections (vision, stack, admin rights).
New pages (under doc/architecture/):
- workflow-multi-parties.md : client / fournisseur /
coordinateur roles, sous-traitance, project states, B2B/B2C,
domaine musical production flow.
- domaine-musical.md : titres collaboratifs (formats, flux de
production, contraintes de licence).
- licences.md : LicenceModele, CC/ODbL seed, badge projet,
cycle de vie.
- domaines-activite.md : arbre des activites, Droit a la
racine, DomaineActivite model.
- dictionnaires-metier.md : regle d'heritage, DictionnaireMetier
+ TermeMetier, cycle de vie d'un terme. Absorbs the previous
doc/Dictionnaire.md draft.
- offres-frontmatter.md : ClasseFormulaire / ClasseDevis,
OffreFournisseur, Demande, parsing YamlDotNet (introduit dans
4034c399 Front matters). Absorbs the previous
doc/Formulaires-devis.md and doc/Demande.md fragments.
- postit-oidc.md : documentation du client desktop PostIt,
custom URI scheme (RFC 8252 §7.1), composants partages,
plateformes, UX observable, persistance et reprise au boot,
garanties testees.
Architecture.md (436 -> 66 lines) keeps the vision, the stack
overview, the admin rights section, and a TOC table pointing at
each detail page. The "A documenter ensuite" backlog is kept
at the end.
Cross-links are relative: from Architecture.md the links go
architecture/<page>.md; from inside doc/architecture/ they go
../Architecture.md or <sibling>.md.
2026-06-27 13:45:31 +01:00
# PostIt — Client desktop et authentification OIDC
> **Récapitulatif** : PostIt est un client desktop Avalonia 12
> multi-cible (Desktop, Android, Browser/WASM) qui consomme l'API
> Yavsc.Org. Authentification **public client + PKCE** via
> **custom URI scheme** (RFC 8252 §7.1) — pas de loopback HTTP.
> Détail dans cette page, racine de l'architecture : [Architecture.md](../Architecture.md).
## Rôle
PostIt est un client desktop (Avalonia 12, multi-cible : Desktop
Linux/Windows, Android, Browser/WASM) qui consomme l'API
Yavsc.Org pour les opérations CRUD du blog et d'autres
ressources de l'utilisateur. C'est un **public client ** : pas de
secret, le secret est remplacé par PKCE.
## Flow d'authentification
L'authentification suit **RFC 8252 §7.1 ** (OAuth 2.0 for Native
Apps — Custom URI Scheme Redirect), pas le pattern loopback
historique. La séquence :
1. L'utilisateur clique **Se connecter ** sur `LoginPage` .
2. `YavscApiClient.LoginInteractiveAsync` demande à `OidcClient`
de calculer l'authorize URL et de la passer à
`CustomSchemeBrowser.InvokeAsync` .
3. `CustomSchemeBrowser` ouvre le navigateur système sur l'authorize
URL via `Process.Start(options.StartUrl) { UseShellExecute = true }` ,
puis **attend ** sur un `TaskCompletionSource<string>` alimenté
par un named pipe `PostIt.OidcCallback` .
4. L'utilisateur s'authentifie sur l'IdP. Le navigateur redirige
vers `postit://callback?code=***&state=…` .
5. L'OS, qui a le scheme `postit://` enregistré, **lance une 2ᵉ
instance** de PostIt avec l'URL en argument.
6. La 2ᵉ instance lit `Environment.GetCommandLineArgs()` * avant *
qu'Avalonia ne boote (`PostIt.Desktop.Program.Main` ),
détecte l'URL via `SchemeUrlDetector.FindCallbackUrl` , l'écrit
sur le named pipe puis sort par `Environment.Exit(0)` . Aucune
fenêtre n'est créée.
7. L'instance vivante reçoit l'URL via le pipe, complète la TCS,
`OidcClient` échange le code contre les tokens (access +
refresh + id) et les persiste dans `~/.config/PostIt/tokens.json` .
## Pourquoi le scheme custom plutôt que loopback
Le loopback (`http://127.0.0.1:7890/callback` ) oblige l'app à
ouvrir un port TCP, à le publier comme redirect URI dans l'IdP,
et à gérer la course "le navigateur revient-il à temps ?".
Le scheme custom transfère la livraison du callback à l'OS :
le navigateur tape sur une URL que l'OS sait router vers PostIt,
pas vers un serveur HTTP.
## Composants partagés (`PostIt/`)
| Composant | Rôle |
|---------------------------------|-------------------------------------------------------------------|
| `Services/OidcLoginPhase` | Enum des étapes du flow : `Idle / Discovering / OpeningBrowser / AwaitingCallback / ExchangingCode / Success / Error` |
| `Services/YavscApiClient` | Client HTTP de l'API Yavsc. Porte `LoginInteractiveAsync(IProgress<OidcLoginPhase>)` et `TrySilentLoginAsync` . Refresh silencieux sur 401 et sur access-token bientôt expiré. |
2026-07-06 21:03:10 +01:00
| `Services/BlogApiClient` | Mapper DTO↔path pour la sous-API blog. **Note ** : `pathPrefix` est * relatif * à `/api/v1/` (que porte déjà `BaseAddress` ) — ex. `"blog"` pour matcher `[Route(APIPrefix + "/blog")]` . Ne pas ré-inclure `api/` . |
doc: split Architecture.md into per-topic pages
doc/Architecture.md was 436 lines and growing; this commit
extracts each non-trivial subject into its own page under
doc/architecture/ and reduces the root document to a table of
contents + transversal sections (vision, stack, admin rights).
New pages (under doc/architecture/):
- workflow-multi-parties.md : client / fournisseur /
coordinateur roles, sous-traitance, project states, B2B/B2C,
domaine musical production flow.
- domaine-musical.md : titres collaboratifs (formats, flux de
production, contraintes de licence).
- licences.md : LicenceModele, CC/ODbL seed, badge projet,
cycle de vie.
- domaines-activite.md : arbre des activites, Droit a la
racine, DomaineActivite model.
- dictionnaires-metier.md : regle d'heritage, DictionnaireMetier
+ TermeMetier, cycle de vie d'un terme. Absorbs the previous
doc/Dictionnaire.md draft.
- offres-frontmatter.md : ClasseFormulaire / ClasseDevis,
OffreFournisseur, Demande, parsing YamlDotNet (introduit dans
4034c399 Front matters). Absorbs the previous
doc/Formulaires-devis.md and doc/Demande.md fragments.
- postit-oidc.md : documentation du client desktop PostIt,
custom URI scheme (RFC 8252 §7.1), composants partages,
plateformes, UX observable, persistance et reprise au boot,
garanties testees.
Architecture.md (436 -> 66 lines) keeps the vision, the stack
overview, the admin rights section, and a TOC table pointing at
each detail page. The "A documenter ensuite" backlog is kept
at the end.
Cross-links are relative: from Architecture.md the links go
architecture/<page>.md; from inside doc/architecture/ they go
../Architecture.md or <sibling>.md.
2026-06-27 13:45:31 +01:00
| `Services/SingleInstance` | Named-pipe helper. `TryHandOffAsync` côté 2ᵉ instance, `StartServerAsync` côté instance vivante. |
| `Services/CustomSchemeBrowser` | `IBrowser` OidcClient qui ouvre le système + attend le pipe. |
| `Services/SchemeUrlDetector` | Détection pure, testable, du `postit://callback` dans argv. |
| `Services/TokenStore` | Persiste `RefreshTokenRecord` (access/refresh/id + expiry). Mode 0600 sur POSIX. |
| `ViewModels/SessionStatusViewModel` | VM du bandeau persistant Connecté/Déconnecté + Logout. |
| `Views/SessionStatusBanner` | Bandeau affiché en haut de `MainWindow` , visible sur toutes les pages. |
## Plateformes (`PostIt.Desktop`, `PostIt.Android`, `PostIt.Browser`)
Chaque plateforme injecte son propre `IBrowser` via
`Platform.CreateBrowser` :
- **Desktop** : `CustomSchemeBrowser` + scheme `postit://` .
- **Android** : Chrome Custom Tabs via `AndroidSystemBrowser` ,
scheme `android://postit-signin` (déclaré comme
`<activity-alias>` dans `AndroidManifest.xml` ). MainActivity est
`SingleTask` → `OnNewIntent` livre l'URL à `AndroidOidcCallbackSink` .
- **Browser (WASM)** : pas de process distinct → N/A.
## UX observable
`LoginPage` affiche en continu la **phase ** courante du flow
(badge `PhaseLabel` ) en plus du `StatusMessage` textuel :
| Phase | Signification |
|--------------------|------------------------------------------------------------------|
| `Discovering` | Fetch de `/.well-known/openid-configuration` . |
| `OpeningBrowser` | Navigateur système ouvert, on attend que l'utilisateur revienne. |
| `AwaitingCallback` | (idem `OpeningBrowser` aujourd'hui — couvre la fenêtre pipe). |
| `ExchangingCode` | Trade du `code` contre les tokens à `/connect/token` . |
| `Success` / `Error` | Issue du flow. |
Si le badge reste bloqué sur `OpeningBrowser` , **l'OS n'a jamais
relancé PostIt avec l'URL `postit://callback` ** : le scheme
handler n'est pas enregistré correctement.
## Persistance et reprise au démarrage
Au boot, `App.OnFrameworkInitializationCompleted` :
1. Construit le `TokenStore` (~/.config/PostIt/tokens.json) et
`YavscApiClient` . Ce dernier charge `_tokens = store.Load()`
dans son constructeur.
2. Ouvre `MainWindow` avec `HomePage` comme racine.
3. Sur l'événement `Opened` , appelle `TrySilentLoginAsync` :
- si l'access token a encore `> RefreshSkew` (60 s) de vie →
`Success` immédiat, push `MainPage` .
- sinon, tente un refresh via le refresh token. Si l'IdP
accepte → push `MainPage` . Si l'IdP rejette (révocation,
vol, expiration du refresh) → `_store.Clear()` , retour à
`HomePage` .
Le bandeau `SessionStatusBanner` reflète `Api.HasValidSession`
en continu et expose le bouton **Se déconnecter ** , qui appelle
`Api.LogoutAsync()` (purge du store) puis `PopToRootAsync`
pour ramener sur `HomePage` .
## Garanties testées
- `LoginInteractiveAsync` émet `Discovering → OpeningBrowser →
ExchangingCode → Success` (ou ` Error` si pas de navigateur).
- `TrySilentLoginAsync` retourne `false` si pas de bundle sur
disque, `true` si l'access est encore valide, `true` après un
refresh réussi.
- `SchemeUrlDetector.FindCallbackUrl` reconnaît l'URL en argv,
case-insensitive, refuse les faux positifs (`postit-…://` ),
retourne le premier match de manière déterministe.
Le chemin "refresh token rejeté → purge du store" est testé
manuellement ; le stub `OidcStubAuthority` ne sait pas encore
rejeter un refresh token précis — extension future.
## Voir aussi
- [Architecture.md ](../Architecture.md ) — racine.