0.1.101 — Casdoor cutover (phase 4) + portal settings parity + Inbox fix

Phase 4 of the Casdoor migration plan (docs/integrations/casdoor-migration.md) lands. After this round, Casdoor is the only third-party authentication surface the program offers. The legacy email/password fallback remains visible behind the existing expander in AuthHub for un-migrated accounts; Google and GitHub buttons are gone everywhere.

This is the first round whose user-facing behavior actually requires operators to populate CASDOOR_* env vars — empty values still keep the JWT verifier in shadow mode (the auth class returns None), but the legacy Google / GitHub sign-in routes no longer exist as a fallback.

The round also fixes a user-reported bug where Casdoor-provisioned accounts hit an empty editor sidebar, ports the editor's Apple-style multi-page Settings layout to portal_app, and ships the one-shot management command operators need to push the existing user table into Casdoor before flipping the switch.

Chunk 1/4 — Casdoor seeds Inbox + welcome note

creators/casdoor_auth.py:_resolve_user now calls notes.services.seed_inbox_and_welcome_note(creator) on both auto-provision and email-fallback link branches, mirroring what the email-verify and (now-removed) OAuth-register flows used to do. The function is idempotent, so legacy users that already have an Inbox are no-ops.

Without this, GET /api/v1/courses/ returned [] for every Casdoor-only user on first sign-in, and the editor sidebar rendered empty (the frontend has no "if list empty, create Inbox" fallback — that's a backend invariant).

Chunk 2/4 — drop Google / GitHub auth surface

Frontend (atomic across all three apps + shared lib):

  • notechondria_shared/lib/src/components/auth_dialogs.dart and auth_dialogs_wizard.dart no longer accept onGoogleLogin, onGithubLogin, onGoogleLoginOnly, or onGithubLoginOnly. The _legacyButtons block collapses to just Sign-up + Login; _buildMethodStep in the wizard renders email-only.
  • notechondria_shared/lib/src/app_shell/auth_client.dart drops loginWithGoogle, loginWithGithub, bindGoogle, bindGithub, and getOAuthConfig declarations. Each app's client.dart / http_client.dart drops the matching implementations.
  • app_shell_oauth_mixin.dart's launchOAuth now Casdoor-only — non-'casdoor' provider arguments log a structured error and early-return, so any stray callsite surfaces in the debug log instead of silently no-op'ing.
  • editor_app/lib/modules/settings_build.dart drops the legacy Google / GitHub _OAuthPillButton calls in _legacyAuthBlock and the RegistrationWizard passthrough. _OAuthPillButton itself remains — Casdoor still uses it for the SSO pill.
  • editor_app, planner_app, and portal_app's _ConnectedAccountsSection widgets are now Casdoor-only: _buildProviderRow helpers + _accountFor / _unlink / _load / _accounts / _loading state are gone, the onListSocialAccounts / onUnlinkSocialAccount / onBindGoogle / onBindGithub plumbing is gone too.
  • Each app's app_shell.dart drops the per-provider OAuth wirings it used to hand to _SettingsPage.
  • flutter analyze runs clean across all four packages — zero errors, no new warnings introduced.

Backend:

  • creators/api.py loses ~1011 lines: OAuthConfigApiView, GoogleOAuthApiView, GitHubOAuthApiView, BindGoogleApiView, BindGithubApiView, SocialAccountListApiView, SocialAccountUnlinkApiView, _BindOAuthMixin, _get_or_create_oauth_user, _validate_invitation_if_required, _request_origin, _pick_redirect_uri, and the matching serializers. URL routes pruned from notechondria/api_urls.py.
  • notechondria/settings.py drops GOOGLE_OAUTH_CLIENT_ID/SECRET, GOOGLE_AUTHORIZED_REDIRECT_URI(S), GITHUB_APP_CLIENT_ID/SECRET, and GITHUB_AUTHORIZED_REDIRECT_URI(S) env-var loads. Casdoor values, the GitHub data-sync App block, and SMTP loads stay. (SMTP env vars remain because the backend code still imports django.core.mail; with SMTP_HOST="" the code is a no-op, which matches the cutover state — sample.env already reflects this.)
  • backend/docker-compose.yml drops the legacy GITHUB_APP_* / GOOGLE_OAUTH_* / GOOGLE_AUTHORIZED_REDIRECT_URI entries.
  • SocialAccount model and creators/migrations/0026_socialaccount.py are kept — chunk 3 reads from them to seed Casdoor user records with prior provider linkages.
  • manage.py check reports 0 issues.

Chunk 3/4 — migrate_users_to_casdoor management command

New file: backend/creators/management/commands/migrate_users_to_casdoor.py.

For each auth_user.is_active=True row:

  1. CasdoorSDK.get_user_by_email(email) — if the email already exists in Casdoor, treat it as the existing row (catches admins who pre-created the user in the Casdoor UI before this command ran).
  2. Otherwise CasdoorSDK.add_user(...) with username, displayName (Creator.username), email (verified), avatar URL, firstName / lastName, signupApplication = CASDOOR_APP_NAME, and any SocialAccount rows mapped onto CasdoorUser.google / CasdoorUser.github so prior OAuth identities resolve to the same Casdoor row at sign-in.
  3. Random 40-char password is set on the Casdoor user. The operator tells migrated users to use Casdoor's "Forgot password?" flow on first sign-in.
  4. Creator.casdoor_sub is stamped on the local row, so the next JWT-validated request through CasdoorJWTAuthentication._resolve_user takes the fast Creator.casdoor_sub == claims['id'] path with one DB hit.

Idempotent. Skips users whose casdoor_sub is already populated unless --retry-existing is passed. Flags:

FlagBehavior
--dry-runWalk the user table and print each upsert plan; never call the Casdoor API.
--retry-existingRe-push users whose casdoor_sub is already set (used to fix drift after a manual edit in the Casdoor admin UI).
--strictExit non-zero if any user fails. Default is log-and-continue.
--limit NStop after N candidates. Smoke-test against prod before doing the full batch.

Hard-stops with a clear Backend.Creators.MigrateToCasdoor/build_sdk — required env vars are unset: ... error when any CASDOOR_* env var is missing — the command is destructive enough that we want a clear stop rather than a half-applied migration.

Documented in docs/integrations/casdoor-migration.md.

Chunk 4/4 — portal_app settings UI parity with editor

portal_app/lib/modules/settings.dart rewrote from a single long ListView (~750 LOC) to the editor's Apple-style multi-page navigation: top page is a 3-card stack (account / settings menu / sign-out), nine sub-pages push via Navigator.push(MaterialPageRoute(...)):

  • _PersonalInformationPage — username / motto / social link / avatar
  • _SignInSecurityPage — change password / change email / active sessions / MCP skill
  • _ApiSettingsPage — API base URL / API-key rotate
  • _ConnectedAccountsPage — Casdoor link/unlink (own page rather than nested in security)
  • _PortalPreferencesPage — theme preset / theme mode / default editor mode
  • _BackendSettingsPage — offline-mode toggle / backend endpoint / GitHub Sync card
  • _LocalDataPage — sync / pull / clear cache / clear data / export / import / restore templates
  • _RecycleBinPage — cloud recycle bin
  • _DebugPage — log viewer + copy logs

The Apple-style primitives (_SettingsGroupCard, _FeedbackBanner, _SettingsCaption, _PickerOption<T>, _pickFromList<T>) were copied into a new portal_app/lib/modules/settings_build.dart rather than hoisted into notechondria_shared — three similar lines beats a premature shared abstraction (AGENTS.md §1.2). When all three apps need divergent behavior it'll be obvious what to factor out.

_SecuritySection (the old single-page wrapper) is gone — its contents are now distributed across the relevant sub-pages. flutter analyze from portal_app/ reports 0 errors.

Operator runbook (cutover)

  1. In the Casdoor admin UI at https://auth.trance-0.com, attach Google + GitHub as Providers on the notechondria application so SSO sign-ins via those identities work via Casdoor's own proxy.
  2. Populate the six CASDOOR_* env vars in the deployment env (see casdoor-setup.md). Without them, the JWT verifier is no-op and Casdoor sign-in returns 503; you don't get the legacy Google / GitHub fallback anymore because it's gone.
  3. From a backend shell:
    python manage.py migrate_users_to_casdoor --dry-run --limit 10
    python manage.py migrate_users_to_casdoor --limit 10
    python manage.py migrate_users_to_casdoor
    
  4. Tell migrated users their next sign-in is via Casdoor SSO, and that "Forgot password?" on the Casdoor login page picks a new password.
  5. The legacy email/password fallback in AuthHub stays available behind the expander as a grace period for un-migrated accounts; remove it in a follow-up round once the migration command reports a clean batch.

Carryover

  • The legacy email/password registration + login views in creators/api.py are still present (they back the AuthHub fallback). A future round can remove them once every active account is in Casdoor.
  • The SocialAccount model + table are kept; they're referenced by the migration command's provider-linkage step. After every user has been migrated and the legacy table is no longer used, a follow-up round can drop the model + ship the migration.
  • SMTP loads remain in settings.py for the same reason — email-verify view still calls send_mail. With SMTP_HOST="" it's a no-op; the operator can leave it unset and Casdoor handles email verification itself. A follow-up round will delete the Notechondria-side email path entirely.
  • BACKEND_VERSION / handshake readout on the new portal _BackendSettingsPage is a placeholder caption — portal doesn't call /api/v1/handshake/ today. Wire that in a follow-up round.

Verification

  • python manage.py checkSystem check identified no issues (0 silenced).
  • python -m py_compile on every modified backend file → exit 0.
  • flutter analyze from editor_app/, planner_app/, portal_app/, notechondria_shared/ → 0 errors.
  • The migrate_users_to_casdoor command was smoke-tested with a fresh DJANGO_SECRET_KEY and no CASDOOR_* env vars; it failed cleanly with the expected "required env vars are unset" message rather than crashing mid-loop.
  • --help for the command prints all four flags.