diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 785a796e..1ee595ac 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -409,6 +409,7 @@ function Shell({ auth }: { auth: Auth }) { } if ( auth.user.onboarding_status === "pending" && + auth.user.onboarding_required !== false && location.pathname !== "/onboarding" ) return ; diff --git a/apps/web/src/api/schema.d.ts b/apps/web/src/api/schema.d.ts index 7e3938a2..478be0c2 100644 --- a/apps/web/src/api/schema.d.ts +++ b/apps/web/src/api/schema.d.ts @@ -726,6 +726,24 @@ export interface paths { patch?: never; trace?: never; }; + "/api/setup/onboarding-policy": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Onboarding Policy */ + get: operations["onboarding_policy_api_setup_onboarding_policy_get"]; + /** Save Onboarding Policy */ + put: operations["save_onboarding_policy_api_setup_onboarding_policy_put"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/recovery": { parameters: { query?: never; @@ -10992,6 +11010,16 @@ export interface components { * @default */ viewer_group: string; + /** + * Requester Group + * @default + */ + requester_group: string; + /** + * Approver Group + * @default + */ + approver_group: string; }; /** OidcSettingsView */ OidcSettingsView: { @@ -11029,6 +11057,10 @@ export interface components { member_group: string; /** Viewer Group */ viewer_group: string; + /** Requester Group */ + requester_group: string; + /** Approver Group */ + approver_group: string; /** Secret Set */ secret_set: boolean; /** Redirect Uri */ @@ -11048,6 +11080,14 @@ export interface components { /** Reason */ reason: string; }; + /** OnboardingPolicy */ + OnboardingPolicy: { + /** + * Show Member Onboarding + * @default true + */ + show_member_onboarding: boolean; + }; /** OnboardingProgress */ OnboardingProgress: { /** @@ -14693,6 +14733,11 @@ export interface components { * @default pending */ onboarding_status: string; + /** + * Onboarding Required + * @default true + */ + onboarding_required: boolean; /** Library Ids */ library_ids?: string[] | null; }; @@ -16475,6 +16520,59 @@ export interface operations { }; }; }; + onboarding_policy_api_setup_onboarding_policy_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OnboardingPolicy"]; + }; + }; + }; + }; + save_onboarding_policy_api_setup_onboarding_policy_put: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["OnboardingPolicy"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OnboardingPolicy"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; review_api_recovery_get: { parameters: { query?: never; diff --git a/apps/web/src/pages/Accounts.tsx b/apps/web/src/pages/Accounts.tsx index dee0fec1..d94a18ce 100644 --- a/apps/web/src/pages/Accounts.tsx +++ b/apps/web/src/pages/Accounts.tsx @@ -3,6 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { api, ApiError, result, setCsrf, type Auth } from "../api/client"; import type { components } from "../api/schema"; import BookDialog from "../components/BookDialog"; +import OnboardingPolicy from "./OnboardingPolicy"; import { Link } from "react-router-dom"; import { Loading, Notice } from "../components"; import { @@ -58,27 +59,35 @@ function OidcSettingsPanel() { const [secret, setSecret] = useState(""); const [open, setOpen] = useState(false); useEffect(() => { - if (settings.data) { - setDraft(settings.data); - setSecret(""); - } + const loaded = settings.data; + if (loaded) setDraft((current) => current ?? loaded); }, [settings.data]); const save = useMutation({ - mutationFn: async () => { - if (!draft) return; - const { secret_set, redirect_uri, ...fields } = draft; + mutationFn: async (submitted: { draft: OidcSettings; secret: string }) => { + const { secret_set, redirect_uri, ...fields } = submitted.draft; void secret_set; void redirect_uri; return result( await api.PUT("/api/auth/oidc/settings", { - body: { ...fields, client_secret: secret }, + body: { ...fields, client_secret: submitted.secret }, }), ); }, - onSuccess: (saved) => { - if (!saved) return; + onSuccess: (saved, submitted) => { client.setQueryData(["oidc-settings"], saved); - setSecret(""); + // A completed save must not overwrite edits made while it was in flight. + setDraft((current) => + current === submitted.draft + ? saved + : current + ? { + ...current, + secret_set: saved.secret_set, + redirect_uri: saved.redirect_uri, + } + : current, + ); + setSecret((current) => (current === submitted.secret ? "" : current)); }, }); const discover = useMutation({ @@ -133,7 +142,7 @@ function OidcSettingsPanel() { className="panel editor" onSubmit={(event) => { event.preventDefault(); - save.mutate(); + if (draft) save.mutate({ draft, secret }); }} >

@@ -236,12 +245,24 @@ function OidcSettingsPanel() { update("default_role", event.target.value) } > - + + + + )} + {(draft.auto_register || mapsGroups) && + draft.default_role === "admin" && ( +

+ Administrator default grants full control to every new account + or synchronized account with no matching group. Choose it only + when everyone admitted by this provider should administer + Dewarr. +

+ )} {mapsGroups && (

Used when no configured group matches. Applies to existing @@ -363,6 +384,32 @@ function OidcSettingsPanel() { /> +

+ + +
+

+ If several groups match: Administrator, Approver, Member, + Requester, then Viewer. On each sign-in, mappings replace manual + roles for accounts without a local password. Password accounts + and the last active administrator keep their existing access. +