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.dartandauth_dialogs_wizard.dartno longer acceptonGoogleLogin,onGithubLogin,onGoogleLoginOnly, oronGithubLoginOnly. The_legacyButtonsblock collapses to just Sign-up + Login;_buildMethodStepin the wizard renders email-only.notechondria_shared/lib/src/app_shell/auth_client.dartdropsloginWithGoogle,loginWithGithub,bindGoogle,bindGithub, andgetOAuthConfigdeclarations. Each app'sclient.dart/http_client.dartdrops the matching implementations.app_shell_oauth_mixin.dart'slaunchOAuthnow 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.dartdrops the legacy Google / GitHub_OAuthPillButtoncalls in_legacyAuthBlockand theRegistrationWizardpassthrough._OAuthPillButtonitself remains — Casdoor still uses it for the SSO pill.editor_app,planner_app, andportal_app's_ConnectedAccountsSectionwidgets are now Casdoor-only:_buildProviderRowhelpers +_accountFor/_unlink/_load/_accounts/_loadingstate are gone, theonListSocialAccounts/onUnlinkSocialAccount/onBindGoogle/onBindGithubplumbing is gone too.- Each app's
app_shell.dartdrops the per-provider OAuth wirings it used to hand to_SettingsPage. flutter analyzeruns clean across all four packages — zero errors, no new warnings introduced.
Backend:
creators/api.pyloses ~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 fromnotechondria/api_urls.py.notechondria/settings.pydropsGOOGLE_OAUTH_CLIENT_ID/SECRET,GOOGLE_AUTHORIZED_REDIRECT_URI(S),GITHUB_APP_CLIENT_ID/SECRET, andGITHUB_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 importsdjango.core.mail; withSMTP_HOST=""the code is a no-op, which matches the cutover state —sample.envalready reflects this.)backend/docker-compose.ymldrops the legacyGITHUB_APP_*/GOOGLE_OAUTH_*/GOOGLE_AUTHORIZED_REDIRECT_URIentries.SocialAccountmodel andcreators/migrations/0026_socialaccount.pyare kept — chunk 3 reads from them to seed Casdoor user records with prior provider linkages.manage.py checkreports 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:
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).- Otherwise
CasdoorSDK.add_user(...)with username, displayName (Creator.username), email (verified), avatar URL, firstName / lastName,signupApplication=CASDOOR_APP_NAME, and anySocialAccountrows mapped ontoCasdoorUser.google/CasdoorUser.githubso prior OAuth identities resolve to the same Casdoor row at sign-in. - 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.
Creator.casdoor_subis stamped on the local row, so the next JWT-validated request throughCasdoorJWTAuthentication._resolve_usertakes the fastCreator.casdoor_sub == claims['id']path with one DB hit.
Idempotent. Skips users whose casdoor_sub is already populated unless
--retry-existing is passed. Flags:
| Flag | Behavior |
|---|---|
--dry-run | Walk the user table and print each upsert plan; never call the Casdoor API. |
--retry-existing | Re-push users whose casdoor_sub is already set (used to fix drift after a manual edit in the Casdoor admin UI). |
--strict | Exit non-zero if any user fails. Default is log-and-continue. |
--limit N | Stop 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)
- In the Casdoor admin UI at
https://auth.trance-0.com, attach Google + GitHub as Providers on thenotechondriaapplication so SSO sign-ins via those identities work via Casdoor's own proxy. - Populate the six
CASDOOR_*env vars in the deployment env (seecasdoor-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. - 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 - Tell migrated users their next sign-in is via Casdoor SSO, and that "Forgot password?" on the Casdoor login page picks a new password.
- The legacy email/password fallback in
AuthHubstays 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.pyare still present (they back theAuthHubfallback). A future round can remove them once every active account is in Casdoor. - The
SocialAccountmodel + 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.pyfor the same reason — email-verify view still callssend_mail. WithSMTP_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_BackendSettingsPageis a placeholder caption — portal doesn't call/api/v1/handshake/today. Wire that in a follow-up round.
Verification
python manage.py check→System check identified no issues (0 silenced).python -m py_compileon every modified backend file → exit 0.flutter analyzefromeditor_app/,planner_app/,portal_app/,notechondria_shared/→ 0 errors.- The
migrate_users_to_casdoorcommand was smoke-tested with a freshDJANGO_SECRET_KEYand noCASDOOR_*env vars; it failed cleanly with the expected "required env vars are unset" message rather than crashing mid-loop. --helpfor the command prints all four flags.