Notechondria
Version: 0.1.65 Build Date: 2026-04-23T00:00
What's Changed
Multi-device sessions (backend)
DRF's rest_framework.authtoken is a 1:1 OneToOneField(User) — a
user has exactly one active token. That's why the earlier rounds
couldn't express "signed in on my phone AND my laptop". 0.1.65
replaces it with a dedicated creators.Session model so a user
can have arbitrarily many concurrent sessions and manage each one
individually (Telegram "Active sessions" style). Frontend UI for
the sessions list + revoke buttons is deferred to
docs/TODO.md — this round is backend only. The
wire shape (Authorization: Token <40-hex>) is unchanged, so the
frontend keeps working without changes today.
-
creators.Sessionmodel — many-per-user. Fields:key(40-char hex, unique),userFK,device_label,user_agent,ip_hash,created_at,last_seen_at,revoked_at. Two timeouts baked in as module-level constants so operators can tune without touching view code:SESSION_IDLE_TIMEOUT = 1 day(rolls forward on every authenticated request) andSESSION_ABSOLUTE_TIMEOUT = 3 days(hard cap fromcreated_at). Helper methods:create_for_user(crude User-Agent → device label heuristic),is_active,touch,revoke. Migration0028_session.py. -
MultiSessionAuthenticationDRF backend added increators/authentication.py. Same wire shape asTokenAuthentication. Looks upSession.objects.get(key=...), enforces both timeouts viaSession.is_active(), rejects revoked rows, and callssession.touch()on every valid request so the idle window rolls forward. Attachesrequest.auth_sessionso downstream views (e.g.LogoutApiView) can distinguish "revoke this device" from "revoke all devices". -
auth_payloadreturns Session data + multi-device flag. Every login / register / verify / OAuth call now mints a freshSessiontagged with the caller's User-Agent + SHA-256 of the first-hop IP, and the response body gains:{ "token": "<40-hex>", "session": {"id": 17, "device_label": "Mac", "created_at": "…", "last_seen_at": "…"}, "multi_device": true, "other_sessions_count": 2, "user": { … } }Frontends can show a "You're signed in on 2 other devices" banner the moment a new session appears.
-
Two new endpoints. Both in
backend/creators/api.pyand wired inbackend/notechondria/api_urls.py:Method Path View Purpose GET /api/v1/auth/sessions/SessionListApiViewList all active sessions for the caller. is_current: trueflags the row for the token the caller is using. Never leakskey.DELETE /api/v1/auth/sessions/<id>/SessionRevokeApiViewRevoke a specific session. Owner-scoped (404 on cross-user attempts). Revoking your current session effectively logs you out of this device. -
LogoutApiViewnow revokes only the current session (usingrequest.auth_session), not every token for the user. Signing out on device A no longer signs out device B. Legacy DRF Token cleanup retained as a harmless no-op. -
ChangePasswordApiViewrotates sessions properly. Revokes ALL existing sessions for the user, then mints a fresh Session for the current request so the device that's changing the password doesn't log itself out. Response includes both the freshtokenand thesessionmetadata. -
SessionApiViewprobe uses Session. Still returns 200 with{"authenticated": false}for missing / malformed / unknown / expired / revoked tokens (per the 0.1.64 root-cause fix) — but the backing lookup now hitsSession.objects.getinstead of DRF's Token table. On success the response echoes the existing session key + metadata so_restoreSessioncan continue to use it without a fresh mint. -
settings.DEFAULT_AUTHENTICATION_CLASSESswapsrest_framework.authentication.TokenAuthenticationforcreators.authentication.MultiSessionAuthenticationas the first entry.ApiKeyAuthenticationstill follows for the MCPAuthorization: Bearer ntc_…path.
Session wipe on deploy
-
Per the owner's direction ("refresh the tokens on deploy is
safe for now" after terminating Render and staying on Northflank),
backend/entrypoint.shnow deletes everycreators.Sessionrow right afterbootstrap_platform. On a fresh DB this is a no-op; on a redeploy it forces every device to re-authenticate on next use. The SQL is inside a small inline Python block that tolerates the table not yet existing (first boot before migrations applied).
Files Changed
VERSION— bumped 0.1.64 → 0.1.65.backend/creators/models.py—Sessionmodel + the two timeout constants (SESSION_IDLE_TIMEOUT,SESSION_ABSOLUTE_TIMEOUT).backend/creators/migrations/0028_session.py— new migration.backend/creators/authentication.py— newMultiSessionAuthenticationclass.backend/creators/api.py—auth_payloadmints + returns Session data;LogoutApiViewrevokes only current; newSessionListApiView+SessionRevokeApiView;SessionApiViewprobe looks up Session;ChangePasswordApiViewrevokes all sessions + mints a fresh one.backend/notechondria/api_urls.py—/auth/sessions/+/auth/sessions/<id>/routes.backend/notechondria/settings.py— DRFDEFAULT_AUTHENTICATION_CLASSESswap.backend/entrypoint.sh— session-wipe-on-deploy.docs/TODO.md— frontend session manager UI (3 apps) and 2FA (password login) captured as deferred work items with concrete scope each.
Known follow-ups (all in docs/TODO.md)
- Frontend Active Sessions card in the Settings surface across
editor / planner / portal. Needs
listSessions+revokeSessionmethods on the shared HTTP client and a multi-device warning banner consuming the newmulti_device/other_sessions_countresponse fields. - Two-factor auth for password login (trusted-device approval or email code). Scoped in TODO.md; skip on OAuth by design.
- Shared-component refactor to keep every file under 1000 LOC — already tracked in TODO's "File-size rule + cross-app sharing" section.
Notes
- Wire shape is unchanged. Existing clients keep working.
- Entrypoint's session wipe runs after
bootstrap_platformso the admin user exists before the wipe runs (irrelevant ordering for the wipe itself, but keeps the boot log tidy). SESSION_IDLE_TIMEOUT+SESSION_ABSOLUTE_TIMEOUTare Pythontimedeltaconstants increators.models. To change, edit the model file — no migration needed (no field definition depends on these values, they're only read at request time).