feat(apps): an App is one resource — fold settings in, atomic create/update#108
Merged
Conversation
…ic create/update
An App was split across two surfaces: the App (RBAC catalog) and the ADR-0011
per-App settings override (branding / origin / login posture / native grants /
DCR / CIMD), each with its own modal and its own endpoint. Discovering where a
setting lived was confusing, and "create/update an app" took two calls.
Make the App ONE resource (API-first, like Users): the per-App settings override
is carried inline on POST/PUT/GET /api/app and written in the SAME tenant
transaction as the App aggregate (atomic). The granular /api/app/{id}/settings
endpoint is removed — there is one write path.
Backend:
- CreateAppDto/UpdateAppDto gain an optional nested Settings; the read includes it.
- AppAdminService (the canonical path the applier also uses) stages the settings
override onto the same session and commits once. The only piece that can't be in
the tenant transaction — the Origin subdomain, which drives the GLOBAL host→App
routing map — is validated up-front (an invalid subdomain rejects the whole
create/update before any commit) and its routing applied right after the atomic
commit. The applier passes no settings, so its behaviour is unchanged.
- ApplicationSettingsService gains StageNonOriginAsync (no-commit) + ValidateOriginAsync.
- Removed ApplicationSettingsEndpoints + its registration.
Frontend:
- Folded ApplicationSettingsModal's tabs into AppDetails as one "Einstellungen" tab
(new AppSettingsSections component); removed the separate modal, its route and the
"Einstellungen" context-menu entry. Settings load/save ride the App create/update.
Tests: migrated ApplicationSettingsAdminTests + the invite-code posture test to the
unified endpoint and added atomic-create coverage (valid settings persist with the
App; invalid settings reject the whole create — no orphan App). 15 + 26 + 11 backend
tests green (incl. the provisioning applier and effective-settings resolution
unchanged); frontend type-check + build + full solution build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ty-fill)
The settings form built `{}` for unchecked sections, the backend stored those
as empty-but-present overrides, and on read they came back non-null — so every
section showed as "overridden" after a save (caught by the visual smoke test:
creating an app with only Native Grants reopened with Branding also ticked).
Fix: the unified App create/update is a REPLACE of the complete override state.
build() sends `null` for unchecked sections; StageNonOriginAsync clears a null
section (and sets a present one). An unchecked section now round-trips back
unchecked. PatchAsync stays sparse for the Origin-only follow-on. (Origin remains
sparse here — toggling a subdomain off doesn't clear an existing route in this
view.) Pre-existing empty-override docs from the old modal self-heal on next save.
Verified in the browser: create app with only Native Grants → reopen shows only
Native Grants ticked, Origin/Branding/etc. clean; single POST/GET /api/app, no
/settings call. 16 settings tests green; frontend type-check clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
windischb
added a commit
that referenced
this pull request
Jun 30, 2026
* feat(admin): generic per-entity Clone/Copy Slugs, client_ids and audiences are immutable by design, so the way to "rename" an entity is to clone it under a new identity and re-wire the references. Add a right-click "Clone" affordance across the six admin entities: App, OAuth Client, Scope, API, Role and Group. One pattern for all: a `useClone()` composable holds a module-level stash (the fragment-routed modals only carry an `id` slot, so the prefill rides out-of-band) plus a tiny per-entity descriptor — identity fields blanked, secrets/server-issued ids dropped, everything else copied 1:1. A List stages the prefill from the full source DTO and opens the Create modal; the modal consumes it on mount and maps it through its existing fromDto. Per-entity shaping: - App: blank Slug, null catalog-entry ids (they belong to the source's streams — the clone mints fresh ids), drop the Origin override (the subdomain is globally unique). Branding/registration/grant overrides clone 1:1 via the settings tab. - OAuth Client: blank ClientId, drop the hashed secret (create mints a fresh one) + DCR audit fields + SA linkage. - Scope / API / Role / Group: blank the immutable identity (Name / aud); drop API secrets; Group's last script error is not carried over. Frontend-only — reuses the existing create endpoints, no backend change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(admin): clone Role/Group via JSON deep-clone, not structuredClone The Role/Group clone handlers pass the Pinia store entity (a Vue reactive Proxy) straight into buildClonePrefill. structuredClone rejects a Proxy with DataCloneError, so right-click → Klonen threw on those two lists (App/Scope/API/Client went through a fresh loadOne object and happened to work). Switch buildClonePrefill to a JSON round-trip: the DTOs are pure JSON (no Dates/Sets/functions), so it is a faithful detached deep copy and serialises straight through the reactive proxy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(admin): document entity Clone + correct stale App-settings refs Add a concise "Cloning" section to each admin entity page (Applications, OAuth Clients/Scopes/APIs, Roles, Groups): how to invoke (list → right-click → Clone), which immutable identity is blanked, and which secrets/server-issued fields are dropped — framing clone as the way to "rename" an entity whose identity can't be changed. While in applications.md + the admin-API reference, correct the App **settings** references that PR #108 made stale: the separate `settings/:id` modal and `GET/PATCH /api/app/{id}/settings` endpoint are gone — per-App ADR-0011 settings now ride inline on the App resource (`GET /api/app/{id}` + `POST`/`PUT /api/app`, one tenant transaction, replace semantics). A reader following the old docs would have hit a 404. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
An App was split across two surfaces — the App (RBAC permission catalog) and the ADR-0011 per-App settings override (branding / origin / login posture / native grants / DCR / CIMD) — each with its own modal and its own endpoint. Finding where a setting lived was confusing, and "create/update an app" took two calls.
This makes the App one resource (API-first, like Users): the per-App settings override is carried inline on
POST/PUT/GET /api/appand written in the same tenant transaction as the App aggregate (atomic). The granular/api/app/{id}/settingsendpoint is removed — there is one write path.Backend
CreateAppDto/UpdateAppDtogain an optional nestedSettings; the read includes it.AppAdminService(the canonical path the realm-provisioning applier also uses) stages the settings override onto the same session and commits once. The only piece that can't be in the tenant transaction — the Origin subdomain, which drives the GLOBAL host→App routing map — is validated up-front (an invalid subdomain rejects the whole create/update before any commit) and its routing applied right after the atomic commit. The applier passes no settings, so its behaviour is unchanged.ApplicationSettingsServicegainsStageNonOriginAsync(no-commit) +ValidateOriginAsync;PatchAsyncstays sparse for the Origin follow-on.ApplicationSettingsEndpoints+ its registration.Frontend
ApplicationSettingsModal's tabs intoAppDetailsas one Einstellungen tab (newAppSettingsSectionscomponent); removed the separate modal, its route and the "Einstellungen" context-menu entry. Settings load/save ride the App create/update.Existing entities
No migration — the data model is unchanged. Existing apps read and round-trip correctly (with or without a settings doc); pre-existing empty-override docs from the old modal self-heal on next save under REPLACE. Only external callers of the removed
/settingsendpoint would need to move to the App resource.Tests & verification
ApplicationSettingsAdminTests+ the invite-code posture test to the unified endpoint; added atomic-create coverage (valid settings persist with the App; invalid settings reject the whole create — no orphan App).POST /api/app(settings inline, no/settingscall),GET /api/app/{id}returns settings inline, settings round-trip faithfully (only enabled sections show checked). The smoke test caught a real round-trip bug (sections showed as overridden after save) which is fixed in the second commit.🤖 Generated with Claude Code