Skip to content

Accounts: web link flow and user area (#/me) - #106

Merged
leonardoaraujosantos merged 4 commits into
mainfrom
feat/account-link-user-area
Sep 27, 2026
Merged

leonardoaraujosantos merged 4 commits into
mainfrom
feat/account-link-user-area

Conversation

@leonardoaraujosantos

Copy link
Copy Markdown
Contributor

Adds the web side of account linking and a user area in the admin process:

  • /link?code= names the Discord account the code was issued for; its same-origin form starts a CyberdyneAuth sign-in, and the callback links only a verified email whose HMAC matches the consented one, for an unused, unexpired code.
  • #/me: the person's /privacy dashboard, "delete everything" (needs a sign-in within 5 minutes), their suggestions (source web), unlink and logout. Sessions behind __Host-cf_user, separate from the console's.
  • The bot DMs "Linked to a***@..., not you? [Unlink]" for new links.

Migration 0036 (revises 0035): user_session, person_account_link.email_hint/notified_at, feature_request source web; purge_person_derived restated with the user sessions deleted through the link.

On when ADMIN_OIDC_* sign-in is configured and ACCOUNT_PROVISIONING_ENABLED=true with PROVISIONING_EMAIL_KEY on the admin service. Merged with main's usage screen and feature-request triage (both bot sweeps and both doc sections kept).

Tests: unit + integration + e2e green locally; alembic upgrade / downgrade -1 / upgrade clean; console lint/test/build green; mypy, ruff, openspec strict clean.

/link redeems the bot's single-use code after a CyberdyneAuth sign-in in the
browser that opened it (prompt=login): the link is made only when userinfo
reports the email verified, its HMAC equals the consented one, and
userinfo.sub matches both token subjects, in one transaction with the code's
redemption. The bot then DMs the masked email with a persistent [Unlink].

The user area lives on the admin API origin behind its own __Host-cf_user
cookie and middleware: /me/session, /me/privacy, /me/feature-requests and
/me/erase, each resolving the person only from the session's linked subject.
Console and user sessions are 401 on each other's routes. Erase needs a fresh
sign-in (max_age=300, auth_time from the verified id token within 5 minutes),
the typed word and the CSRF header, then ends the link and every session of
the account and revokes the refresh token.

Migration 0036 adds user_session, person_account_link.email_hint/notified_at,
the 'web' feature-request source, and deletes the linked account's user
sessions in purge_person_derived. The Svelte bundle gains a separate #/me
area (userApi.ts, viewmodels/me, views/me) with an architecture rule against
imports between it and the console.
The consented email is whatever the Discord person typed, so someone could
run /account with a victim's email and send them the /link URL; the victim's
verified sign-in then linked their CyberdyneAuth account to the attacker's
Discord person, and the Unlink DM went to the attacker.

GET /link now starts nothing: it shows a page naming the Discord account the
code belongs to, and only its form (POST /link, Origin required and same
origin) starts the sign-in. /me/session names the linked Discord account, and
POST /me/unlink (with an Unlink button in the user area) undoes a link from
the CyberdyneAuth side. The refusal for an account linked elsewhere points
there.

Also cover checks no test pinned: the sub of a refreshed id or access token,
a stored access token for another sub, an auth_time in the future,
SameSite=Strict on the user cookie, the user-purpose guard in complete(), and
a relink being announced again (notified_at reset, MARK_ANNOUNCED guard).
# Conflicts:
#	console/src/styles.css
#	docker-compose.yml
#	docs/admin-console.md
#	src/chatmemory/entrypoints/admin.py
#	src/chatmemory/entrypoints/bot.py
#	tests/unit/test_compose_env.py
@leonardoaraujosantos
leonardoaraujosantos merged commit 00dd10c into main Sep 27, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant