0.1.98 — Casdoor bind / unlink (post-phase-3 hardening)
Closes the account-linking gap left after 0.1.97. The phase-2 exchange endpoint already auto-links by email-iexact during sign-in; this round adds a manual bind/unlink path for the cases that can't auto-resolve:
- The Casdoor email differs from the Notechondria email.
- The user wants to deliberately disconnect.
Backend
New endpoints
POST /api/v1/auth/casdoor/bind/(auth required). Body{"code": "<authz>"}. Exchanges the code, verifies the JWT, linksCreator.casdoor_subtorequest.user. Returns 409 when the sub is already on a different Creator (no silent transfer). Returns the standardauth_payloadon success.DELETE /api/v1/auth/casdoor/unlink/(auth required). Idempotent. ClearsCreator.casdoor_sub. Does NOT log the legacy session out. Returns{"casdoor_linked": false, "was_linked": <bool>}.
Settings surface
Settings.casdoor_linked (bool) added to the GET payload so the
Connected Accounts UI can render the right state without an extra
round-trip.
Tests (creators.tests.CasdoorAuthTests)
7 new cases added on top of the 8 from 0.1.96 — all 15 pass:
test_bind_endpoint_requires_authtest_bind_endpoint_returns_503_in_shadow_modetest_bind_endpoint_rejects_conflicting_sub— patches_build_sdk+verify_tokento assert the 409 branch and confirm the calling user'scasdoor_subis NOT mutated.test_bind_endpoint_happy_path_persists_link— same patch pattern, asserts 200 +casdoor_subis persisted +auth_payloadshape is returned.test_unlink_endpoint_clears_subtest_unlink_endpoint_idempotenttest_settings_payload_exposes_casdoor_linked
Dependency-pin guard
pip install casdoor>=1.41 accidentally pulled Django==6.0.4,
which broke the legacy from django.utils.timezone import utc
import in creators/migrations/0007_auto_20231213_1554.py. The
fix on the dev side was pip install Django==4.2.10; the pin in
backend/requirements.txt is unchanged. Operator action: if a
deploy ends up with Django 5+ for any reason, the legacy
migration will refuse to load. Long-term fix tracked under
"Casdoor migration phase 5: cleanup" — the legacy migrations get
folded once the Casdoor cutover removes their dependents.
Frontend
Shared
AuthClientgainscasdoorBind(token, code)+casdoorUnlink(token). Editor / portal / planner each implement both via the same shape they use for the existing bind / unlink endpoints.AppShellOAuthMixin.handleOAuthCallbackreshaped: thestate=casdoorbranch now dispatches onintentinstead of short-circuiting straight to the exchange endpoint.intent == 'bind'callscasdoorBind(and refuses to fall through to the legacy provider login when the session token is missing — same shape as the Google/GitHub bind guard).
Per-app
_ConnectedAccountsSection(3 forks: editor'ssettings_sections.dart, portal's + planner'ssettings.dart) gains anonBindCasdoor+onUnlinkCasdoor+casdoorLinkedtriple. Each renders a "Casdoor SSO"ListTilewith shield-outlined leading icon and Link / Switch / Unlink affordances on the right._SettingsPageon each app gains the matching props, forwardingwidget.settings?['casdoor_linked'] == trueinto the section.app_shell(or editor'sbuild_helpers.dart) constructs the callbacks when_casdoorConfigured && _token != null. Unlink fires the DELETE then locally flips_settings['casdoor_linked'] = false+refreshState()so the row updates without a Settings refetch.
Verification
- 15/15
CasdoorAuthTestspass undersettings_test. flutter analyzeclean across editor / portal / planner — zero new errors / warnings beyond pre-existing surface-deprecation lints unrelated to this round.
Carryover
- Phase 4 cutover (disable
LoginApiView/RegisterApiViewetc.; freezeSessionwrites). - Phase 5 cleanup (delete every legacy auth class / serializer /
template / helper enumerated in
casdoor-migration.md). - 0.1.94 GitHub Sync open items.