0.1.119 — OIDC profile refresh on every login + display_name/avatar_url + drop SocialAccount + Casdoor Manage button + settings menu consistency

User directives in this round (verbatim, condensed):

"For sign in and security settings, add button to redirect user to https://auth.trance-0.com/trance-0 (generated this url from environment variables) to setup

and help text for notify user to contact admin if casdoor backend is off

I see onbackend there is social accounts model, remove that since we use casdoor for now.

I see there is some setting menu inconsistency in portal and editor app. for example the agent skill is in different folder. in editor it is in api settings but in portal it is in signin and security, fix that (remember to use unifided ui when possible. ...)

In this round, you may read and parse the login info from oidc (avatar, firstname last name, email address, etc realted to creator profile. refresh if they made any changes. Note, never change the username to avoid corruptions). P.S. I noteices that we did not create display name atribute for creator model, add that and show display name by default. it should be optional and we show display names when user configured that in public note author names, etc. otherwise use the default settings.

Remember to add the instructions in the docs for setting up casdoor login providers for custom jwt-token"

Done in five docker-verified stages, all squashed into this one release commit so the rolling deploy picks them up atomically.

Backend

Creator profile fields (migration 0033)

backend/creators/migrations/0033_profile_refresh_drop_social.py makes four schema changes:

  1. Creator.display_nameCharField(max_length=255, blank, default=""). User-facing label preferred over User.username on public surfaces (note bylines, comment headers, sidebar header). Editable from settings (the SPA can PATCH it via the existing /api/v1/settings/ endpoint), but the next Casdoor sign-in re-overwrites with whatever the IdP currently has.
  2. Creator.avatar_urlURLField(max_length=512, blank, default=""). Remote avatar URL refreshed from the JWT's avatar claim on every login. The SPA prefers this over the locally-uploaded Creator.image so a Casdoor profile- pic change propagates everywhere on next login.
  3. Creator.casdoor_profile_synced_atDateTimeField(blank, null). UTC timestamp of the most recent profile refresh. Drives the 5-minute throttle inside _sync_creator_from_claims.
  4. SocialAccount table dropped along with the SocialProviderChoices enum. Casdoor proxies third-party identities (Google, GitHub, etc.) on its application Providers tab now; the only Notechondria-side link is Creator.casdoor_sub. The legacy migration command (migrate_users_to_casdoor) had its SocialAccount-backed provider pre-population block removed in the same commit so python manage.py migrate_users_to_casdoor ... keeps working on databases where creators_socialaccount is already gone.

_sync_creator_from_claims(creator, claims) helper

New helper in backend/creators/casdoor_auth.py that mirrors the Nextcloud user_oidc plugin's "Sync user attributes on every login" pattern. Updates:

  • Creator.display_name<CASDOOR_CLAIM_DISPLAY_NAME>
  • Creator.avatar_url<CASDOOR_CLAIM_AVATAR> (new env var, default avatar)
  • User.first_name<CASDOOR_CLAIM_GIVEN_NAME>
  • User.last_name<CASDOOR_CLAIM_FAMILY_NAME>
  • User.email<CASDOOR_CLAIM_EMAIL>

Deliberately not touched:

  • User.username — changing it breaks Django ORM PK references on every owner-keyed FK and every external link to /api/v1/creators/<username>/. Username is set once at account creation (bind or create-with-password path off the 0.1.118 link challenge) and is stable for the lifetime of the account.
  • Creator.image — the locally-uploaded avatar. Left alone so a user who uploaded a custom image inside Notechondria doesn't have it silently replaced by the Casdoor avatar. The SPA prefers avatar_url when set, falls back to image otherwise (resolved server-side in auth_payload).

The helper is throttled to 5-minute granularity via Creator.casdoor_profile_synced_at. Called from:

  • CasdoorJWTAuthentication.authenticate — every JWT- authenticated request (the throttle keeps DB writes manageable on a busy SPA).
  • CasdoorExchangeApiView fast path — when the exchange finds an already-linked account.
  • CasdoorLinkBindApiView and CasdoorLinkCreateApiView — after stamping casdoor_sub. These re-verify the LinkChallenge.access_token to recover the full claims dict, then sync.

Wire-shape additions

auth_payload and SettingsSerializer.to_representation now expose three new fields:

  • display_name — same priority chain everywhere: Creator.display_nameUser.first_name + last_nameUser.username.
  • avatar_urlCreator.avatar_url (or empty).
  • image_url — kept for backward compat; Creator.avatar_url preferred over the local Creator.image URL.
  • casdoor_profile_synced_at — ISO-8601 timestamp.

SettingsSerializer.update accepts a writable display_name field; the change persists locally until the next Casdoor login overwrites it.

CASDOOR_CLAIM_AVATAR env var

backend/notechondria/settings.py reads CASDOOR_CLAIM_AVATAR=os.getenv("CASDOOR_CLAIM_AVATAR", "avatar"). Operators with a Casdoor instance that names the avatar field differently can override (comma-separated list, first non-empty wins — same pattern as the other CASDOOR_CLAIM_* vars from 0.1.110).

Frontend

"Manage Casdoor account" button + admin-contact help text

Added to all three apps' Connected Accounts UIs:

  • editor: _ConnectedAccountsSection in editor_app/lib/modules/settings_sections.dart — gained a casdoorOrgLoginUrl parameter, renders an OutlinedButton ("Manage Casdoor account") that navigates the browser to the org-themed login page when the URL is non-empty. Plus the text: "If sign-in is unavailable, contact your Notechondria admin (Casdoor backend may be off)." Threaded from _SignInSecurityPage.
  • portal: same in portal_app/lib/modules/settings.dart, threaded from _ConnectedAccountsPage in settings_pages.dart.
  • planner: same in planner_app/lib/modules/settings.dart, threaded inline from the account card.

The URL itself is never hardcoded: it comes from /api/v1/auth/casdoor/config/'s signin_url field, which the backend builds from ${CASDOOR_ENDPOINT}/login/${CASDOOR_ORG_NAME}. For the user's current Northflank env, that resolves to https://auth.trance-0.com/login/trance-0.

Settings menu consistency — Agent Skill placement

Per the user's complaint that Agent Skill (MCP skill markdown) appears in different sections per app:

  • editor: unchanged — Agent Skill already lived on _ApiSettingsPage next to the MCP API key + GitHub Sync card. This is the canonical pattern.
  • portal: moved off _SignInSecurityPage and onto _ApiSettingsPage so it sits next to the MCP API key controls. Sign-in & Security now shows a one-line pointer to the Account page for Casdoor bind/unlink.
  • planner: planner has only one settings page (no subpages yet), so Agent Skill moved out of the inline account card and into a sibling Card alongside the GitHub Sync card. A TODO marker notes the future split into subpages to match the editor + portal pattern.

Deferred work

The user also asked for a deeper unification: "I don't see the reason for using different login widget for the three apps." Editor uses its own _buildSignedInAccount / _buildSignedOutAccount helpers in editor_app/lib/modules/settings_build.dart; portal + planner share the AuthHub widget from notechondria_shared/lib/src/ components/auth_dialogs.dart.

Refactoring editor onto AuthHub is a deeper change with more surface area (different parent state contracts, different controllers). Deferred to a separate round so this commit stays focused on data + UX-thread changes the operator can deploy together.

Files changed

Backend

  • backend/creators/models.py — Creator field additions; SocialAccount + SocialProviderChoices removed.
  • backend/creators/migrations/0033_profile_refresh_drop_social.py — autogenerated, verified via manage.py check.
  • backend/creators/casdoor_auth.py_sync_creator_from_claims helper; wired into CasdoorJWTAuthentication.authenticate.
  • backend/creators/api.pyauth_payload + SettingsSerializer thread the new fields; bind/create link endpoints sync profile after stamping casdoor_sub; exchange fast path syncs after resolving an existing user.
  • backend/creators/admin.pySocialAccountAdmin removed.
  • backend/creators/management/commands/migrate_users_to_casdoor.pySocialAccount import + provider pre-population block removed.
  • backend/notechondria/settings.pyCASDOOR_CLAIM_AVATAR env var (default avatar).

Frontend

  • frontend/editor_app/lib/modules/settings_sections.dart_ConnectedAccountsSection gains casdoorOrgLoginUrl + Manage button + help text.
  • frontend/editor_app/lib/modules/settings_pages.dart — threads casdoorOrgLoginUrl into _ConnectedAccountsSection.
  • frontend/portal_app/lib/modules/settings.dart — same Manage-button pattern in portal's _ConnectedAccountsSection.
  • frontend/portal_app/lib/modules/settings_pages.dart — Agent Skill moved from _SignInSecurityPage to _ApiSettingsPage; threads casdoorOrgLoginUrl.
  • frontend/planner_app/lib/modules/settings.dart — same Manage-button pattern; Agent Skill moved out of the account card into its own sibling Card alongside GitHub Sync.

Docs

  • docs/integrations/casdoor-setup.md — new §7 covering the per-login profile sync, the avatar Custom JWT field walk- through (Application → Token → Custom JWT fields → Add), the recommended full custom-fields table, the CASDOOR_CLAIM_AVATAR env var, and the username-stable / image-vs-avatar precedence contract.
  • docs/SUMMARY.md — entry for 0.1.119.

Verification (run locally per AGENTS.md / user directive)

  1. python3 -m py_compile clean across all modified backend files.
  2. docker build -f backend/Dockerfile --target builder -t notechondria-build-test:0.1.119 . — completed all 18 stages.
  3. In-container python manage.py checkSystem check identified no issues (0 silenced).
  4. In-container python manage.py makemigrations — produced 0033_profile_refresh_drop_social.py cleanly. The migration adds three Creator fields and a DeleteModel("SocialAccount") step.
  5. In-container Django bootstrap smoke test:
    • Creator._meta.get_fields() includes display_name, avatar_url, casdoor_profile_synced_at.
    • creators.models.SocialAccount import raises ImportError.
    • _sync_creator_from_claims is callable.
    • auth_payload source contains display_name, avatar_url, casdoor_profile_synced_at.
    • SettingsSerializer().fields.keys() includes display_name, avatar_url.
  6. flutter analyze clean across notechondria_shared, editor_app, portal_app, planner_app — only the pre- existing _socialLinkError unused-field warning in planner remains.

Operator follow-up

The new 0033_profile_refresh_drop_social.py migration runs on the next backend deploy:

  1. Adds display_name, avatar_url, casdoor_profile_synced_at to creators_creator.
  2. Drops the creators_socialaccount table — destroys the row data permanently. Casdoor doesn't have a SocialAccount analogue; the per-provider linkage lives on Casdoor's Application > Providers tab. Confirm before redeploying that you don't need any of the SocialAccount rows (you almost certainly don't, since the model has been dead since the Casdoor cutover in 0.1.99).

After the deploy:

  • A Casdoor sign-in (existing user, on a refreshed image) will populate Creator.display_name / Creator.avatar_url from the JWT on first authenticated request.
  • The SPA's avatar widgets and byline labels will start showing the Casdoor display name / avatar URL automatically once the next page loads — image_url priority chain is resolved server-side in auth_payload.
  • Add the avatar Custom JWT field on the Casdoor side per §7 of docs/integrations/casdoor-setup.md if it's not already set up; without it, _sync_creator_from_claims silently skips the avatar update.