feat(service-config): per-forge OAuth credentials editor (PR D of A/B/C/D) #830
No reviewers
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!830
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/forge-oauth-credentials"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Closes the URL-consolidation series. Operator can now register and rotate OAuth app credentials for Forgejo / GitHub / GitLab from
/settings/service/forgeinstead of editing the systemd unit + restart.Stacked on PR #829 (sub-routes restructure) — base branch reflects that. Once #829 merges, retarget to
main.Storage shape
Per
specs/service-config-url-consolidation.mdPR D:client_idis non-secret OAuth registration metadata → new column onoperator_oauth_tokens.client_secret_refpoints at a row in the encryptedsecrettable under canonical name<FORGE>_OAUTH_CLIENT_SECRET(same envelope asPENPOT_TOKEN, see migration 014).base_urlcontinues to live onoperator_oauth_tokens(PR C). GitHub is hardcoded tohttps://github.com; Forgejo / GitLab are operator-set.What changes
Server
operator_oauth_tokens—client_id+client_secret_ref. Idempotent ALTER TABLE inensureSchema.runPromoteOauthEnvToDbSchema— schema-only, runs inensureSchemaso evenjust agents-syncExecStartPre processes pick up the columns.runPromoteOauthEnvToDb— data backfill (encrypts env values into the secret table + populates theoperator_oauth_tokenspointers). Called frommain.tsAFTERinitSecretsCrypto(). Idempotent — env unset OR row already populated → no-op.resolveForgeOAuthFromDb(forge)resolves credentials DB-first, env vars only as bootstrap fallback. The three OAuth blocks inloadWebhookConfig(Forgejo / GitLab / GitHub) all switch to the helper. Once bothclient_id+client_secret_refare populated, env vars are ignored ("no compat shim" rule).apps/server/src/http/handlers/forges-credentials.tsexposesGET /api/forges(list per-forge state) andPUT /api/forges/:forge(update base_url / client_id / client_secret). Wired intomain.tsalongside the existing service-config CRUD.SPA
Forgesub-route at/settings/service/forge— first item in the side-nav, default redirect target (replaces/container).apps/web/src/features/service-config/forge-section.tsxrenders three per-forge cards (Forgejo / GitLab / GitHub):<login>✓ vs no dance yet).base_url(disabled for GitHub).client_idtext input.client_secreteditor mirroringPenpotTokenEditor— last-rotated timestamp + paste-to-replace.PUT /api/forges/<forge>; toast warns the operator that new credentials take effect after the next service restart.apps/web/src/lib/api.ts— typed client for/api/forges./settings/hub card subtitle refreshed.Test plan
just qa— 3135/3135 server tests + web typecheck greenjust restart— service running, migration backfilledclient_id=c1ccb3ef-…+client_secret_ref=FORGEJO_OAUTH_CLIENT_SECRET; log line confirms[017] forgejo: insertedSecret=true updatedRow=true stubCreated=false/settings/service/forge→ see three forge cards. Editclient_id→ save → toast appears + table reflects new value.[017]migration is no-op (env still set, row already populated → row-already-set skip).Series complete
PR A → ripped 5 dead tabs + 4 dead columns.
PR B → Penpot URL/secret co-presented under "Design" tab.
PR C →
forgejo_urlrelocated tooperator_oauth_tokens.base_url.PR D → forge OAuth credentials editor.
End state:
/settings/service/{forge,container,watchdogs,design}— every URL lives with its credentials, every credential has a dashboard editor, no env-only knobs left for runtime config.🤖 Generated with Claude Code
ed5771e775toe338250dd5CI green, security sound, AC met.
PenpotTokenEditor.client_secret,dirtystays true buthandleSavefires no mutation (empty patch). Save button appears enabled but click is a no-op. Nit only.guardMutatingon GET/api/forgesmatches existing service-config patterns.