Built-in OAuth2 client-registration admin screen (todo §6); /admin/clients lists/registers/deletes the Hydra OAuth2 clients other apps log in through us with. New src/admin-clients.ts (pure builders + handleAdminClients, mirroring the §5 Users/Roles screens): list (search/paginate over one fetched Hydra page), register (GET form + POST), read-only detail, delete-confirm. src/hydra-admin.ts gains the client half of the admin API — createClient/listClients/getClient/deleteClient over /admin/clients (+ a nextPageToken Link parser like kratos-admin) and the registration fields on OAuth2Client. Register builds a standard authorization-code client (+ refresh_token), confidential (client_secret_basic) or public (PKCE/none), with an optional first-party auto-consent flag; Hydra returns the client_secret once, so the register POST renders the new client's detail page with the one-time secret directly (no PRG) and it is never re-shown (getClient carries no secret; the detail test asserts it). Writes go only to Hydra; gated admin-only (anon->/login, non-admin->403) + every mutation CSRF-guarded via requireAdmin/guardedForm like §5; a Hydra 4xx (bad redirect/scope) re-renders the form (400), a 5xx -> 500 (mirrors oauth-login.ts); :id via safeDecode (malformed->404). Wired into app.ts (/admin/clients, gated on the hydra client present) and the shared adminSection (Users.Groups.Roles.OAuth2 clients, i-globe) so it shows for admins and is invisible otherwise. New views (admin/clients, client-form, client-detail + partials/client-{form,detail}-body) reuse the shell/filter-bar/data-table/field blocks; one .detail-list CSS rule; README Layout/§6 updated. Tests-first: hydra-admin.test.ts (client CRUD contracts incl. Link pagination/404->null/204), admin-clients.test.ts (builder/validation/payload matrix), app.test.ts HTTP integration (gate/list/register-shows-secret-once/invalid+CSRF-reject/detail-hides-secret/delete + malformed-%->404). Stability-reviewer run as a local PR: APPROVE, no Critical/High; addressed its one nit (dropped a dead URL.protocol check in validateClientInput). Boot-verified the client CRUD live against real Hydra v26.2.0 (create->201 w/ one-time secret -> list finds it -> get -> delete -> get null); torn down. typecheck + 274 units green.
This commit is contained in:
@@ -13,13 +13,18 @@ const REDIRECT = "http://hydra/oauth2/auth?consent_verifier=v";
|
||||
const DENIED = "http://client/cb?error=access_denied";
|
||||
|
||||
function stubHydra(consent: ConsentRequest, capture?: (b: AcceptConsent) => void): HydraAdmin {
|
||||
const unused = async () => { throw new Error("unused"); };
|
||||
return {
|
||||
acceptConsentRequest: async (_c, body) => { capture?.(body); return { redirect: REDIRECT }; },
|
||||
acceptLoginRequest: async () => { throw new Error("unused"); },
|
||||
acceptLoginRequest: unused,
|
||||
createClient: unused,
|
||||
deleteClient: unused,
|
||||
getClient: unused,
|
||||
getConsentRequest: async () => consent,
|
||||
getLoginRequest: async () => { throw new Error("unused"); },
|
||||
getLoginRequest: unused,
|
||||
listClients: unused,
|
||||
rejectConsentRequest: async () => ({ redirect: DENIED }),
|
||||
rejectLoginRequest: async () => { throw new Error("unused"); },
|
||||
rejectLoginRequest: unused,
|
||||
};
|
||||
}
|
||||
const stubKratos = (whoami: KratosPublic["whoami"]): KratosPublic => ({
|
||||
|
||||
Reference in New Issue
Block a user