Session View Switch — FE Guide (2026-08-03, updated 2026-08-04)¶
Multi-role users (teacher+admin, teacher+department head, teacher/staff+referent, …) can switch their session to a single "view", and the backend genuinely narrows their permissions to it. This is what powers the role selector: a teacher-admin who switches to the Teacher view takes attendance as a plain teacher (own-group fences, teacher defaults), not as an admin.
2026-08-04 update — view selection at login. The login flow now asks the user to pick their view straight away (§6). Unlike the original mid-session switch (which was additive), the login change is a coordinated behavior change — read §6 before shipping anything against login.
1. Discovery — GET /auth/me¶
The user object gains two fields:
{
"user": {
// ...existing fields (activeProfile, availableProfiles, roles, ...)...
"activeView": null, // string | null — null = combined default session
"availableViews": [ // only on /auth/me (like availableProfiles)
{ "key": "teacher", "label": { "en_US": "Teacher", "it_IT": "Docente" } },
{ "key": "referent", "label": { "en_US": "Referent", "it_IT": "Referente" } },
{ "key": "admin", "label": { "en_US": "Administrator", "it_IT": "Amministratore" } },
{ "key": "department_head", "label": { "en_US": "Department Principal", "it_IT": "Preside del dipartimento" } }
]
}
}
availableViewsis ONE flat list — person profiles and roles share the namespace, deduped (a teacher never sees two "teacher" entries). Labels are display-ready and use the exact same copy asGET /roles; custom tenant roles appear with their stored name echoed in both languages.activeViewtells you which entry is currently active.null= the combined default (full union of the user's grants). Referent/student sessions report their profile key ("referent"/"student").activeViewis present on every auth response (login,refresh,switch-profile,/auth/me);availableViewsonly on/auth/me.- Single-view users (just a teacher, just a referent) get a one-entry list — hide the selector for them if you like.
- Whether to render an explicit "All roles" entry for the combined default is FE copy: the backend has no
allkey; that state istarget: null/activeView: null.
2. Switching — POST /auth/switch-view¶
POST /api/v1/auth/switch-view
{ "target": "teacher" } // a key from availableViews
{ "target": null } // back to the combined default
The 200 response body is the recompiled permissions map, byte-shape-identical to GET /permissions. Replace your cached permissions with it directly — no second round-trip. Everything you already key on /permissions (nav on empty scopes, field visibility, action buttons) keeps working unchanged; it simply reflects the selected view now.
- On a real switch the auth cookies are rotated in the response (
Set-Cookie) — same mechanics asswitch-profile. Subsequent requests automatically run under the narrowed session. - Idempotent: switching to the view you're already on returns the permissions map without rotating cookies.
- Cross-boundary jumps work in one call: sitting in the Referent view and clicking "Administrator" is a single
switch-view— the backend handles the profile change internally. - Selecting
teacherwhile the user is also a department head narrows to the teacher role ONLY — the department-head widening (department boards, wider rosters) genuinely disappears until they switch back. - After a switch, re-fetch
/auth/meif you displayactiveView/roles anywhere (the switch response carries only the permissions map). - Rate limit: 10 req / 60s.
3. Errors¶
| Status | Code | When | FE handling |
|---|---|---|---|
| 400 | ACTIVE_VIEW_NOT_AVAILABLE |
switch-view target not in the user's views, or a login pick not offered / no longer available (e.g. the role was just revoked) |
Mid-session: re-fetch /auth/me, re-render the selector. At login: restart from the password step to get a fresh chooser |
| 401 | AUTH_REFRESH_MISSING |
Refresh cookie absent | Treat as logged out |
| 401 | AUTH_TOKEN_INVALID / AUTH_TOKEN_REUSE |
Stale/replayed refresh token | Force re-login (existing handling) |
Errors carry the standard envelope with localized messages.
4. Sessions can silently return to the default¶
If a role is revoked while the user sits in that view, the next token refresh does not log them out — the session falls back to the combined default (activeView: null, full remaining grants). Detect it via activeView on the refresh response (or /auth/me) and update the selector; permissions should be re-fetched when you observe the view changed under you.
5. What did NOT change¶
GET /permissions— identical shape and behavior.availableProfiles+POST /auth/switch-profile— untouched; profile switching still works exactly as before (a profile switch resets any active view to the default). You can now drive both axes from the singleswitch-viewendpoint if you prefer one code path.- Single-view users — zero behavioral change anywhere: no chooser, straight login,
activeViewas before.
6. Login flow — view selection at login (2026-08-04, BEHAVIOR CHANGE)¶
Coordinated change, not additive. Three things about login behave differently from one deploy to the next. Ship the FE handling together with the backend deploy.
What changed¶
The management display labels are now HR Manager, Front Office, and
Department Principal (localized by the existing label catalogue). Role keys
remain hr, secretary, and department_head; persist and submit keys, never
labels.
- The chooser fires for more users. The login
requiresProfileSelectionprompt now appears for any account with two or more views — including single-profile multi-role users (every admin, every department head) who previously logged straight in. The response shape is the one you already handle; only the trigger widened. - A pick narrows. Picking an entry at login has exactly the
switch-viewsemantics: the session starts in that view (activeViewset, permissions already narrowed — no post-login switch needed). Previously, picking the Teacher profile landed in the combined session; now it lands inroles: ['teacher']. There is no "all my roles" entry at login — the union is onePOST /auth/switch-view { target: null }away. - The rejection code changed. A bad/stale pick on
POST /auth/login/select-profileis now400 ACTIVE_VIEW_NOT_AVAILABLE(wasACTIVE_PROFILE_NOT_AVAILABLE).
The prompt payload¶
ProfileSelectionResponseDto gains views — render this list; it is the complete chooser (including the platform entry for platform admins):
{
"requiresProfileSelection": true,
"profiles": ["teacher"], // LEGACY — kept for transition, will be dropped later
"views": [
{ "key": "teacher", "label": { "en_US": "Teacher", "it_IT": "Docente" } },
{ "key": "admin", "label": { "en_US": "Administrator", "it_IT": "Amministratore" } },
{ "key": "department_head", "label": { "en_US": "Department Principal", "it_IT": "Preside del dipartimento" } }
],
"selectionToken": "eyJ..."
}
POST the chosen key back on the existing endpoint and field:
POST /api/v1/auth/login/select-profile
{ "selectionToken": "eyJ...", "activeProfile": "department_head" }
The 200 response is the usual AuthUserDto with cookies — user.activeView reflects the pick, user.roles is already narrowed.
- Labels are the same strings as
availableViews/GET /roles; the platform entry is{ "key": "platform", "label": { "en_US": "Platform administration", "it_IT": "Amministrazione piattaforma" } }. - A
400 ACTIVE_VIEW_NOT_AVAILABLEon a pick means the entry was never offered or stopped being available while the chooser was open (10-minute token) — the selection token itself is still valid for a different pick, but the simplest handling is to restart login for a fresh chooser. - Single-view users never see the chooser and land in the combined default exactly as before.