Skip to content

✨ feat(auth): accept an instance's terms instead of dead-ending on them - #51

Merged
hub2rock merged 1 commit into
mainfrom
feat/terms-acceptance
Aug 21, 2026
Merged

✨ feat(auth): accept an instance's terms instead of dead-ending on them#51
hub2rock merged 1 commit into
mainfrom
feat/terms-acceptance

Conversation

@hub2rock

Copy link
Copy Markdown
Contributor

Found in the first TestFlight build: signing in to a server that requires its terms shows "Login is restricted. Check with your administrator." — advice that is both wrong and a dead end.

What PLANKA actually says

A refused sign-in comes back as 403, and the body's message distinguishes three situations:

message Means Resolvable in-app
Terms acceptance required Credentials are fine, terms are pending Yes
Use single sign-on Password login disabled on this instance No — but "press SSO" is useful advice
Admin login required to initialize instance Fresh instance, nobody set it up No

PlankaClient threw all of it away — case 403: throw .forbidden — so the app could not tell them apart.

Not derivable from the spec

POST /access-tokens/accept-terms needs a pendingToken, but planka-openapi.json documents only code and message on the 403. A real instance returns:

{"code":"E_FORBIDDEN","pendingToken":"","message":"Terms acceptance required","step":"accept-terms"}

pendingToken and step are undocumented. This was built against that observed response, and the test fixture mirrors it.

Changes

BoardlyKit

  • AuthRestriction carries the reason and the pending token; PlankaAPIError.authRestriction surfaces it.
  • An ordinary permission 403 still throws .forbidden — only bodies describing a sign-in restriction qualify, or every "not your board" error would start reading as a login failure.
  • getTerms(language:) and acceptTerms(pendingToken:signature:).
  • buildRequest gains query items. A query string folded into path lands in URLComponents.path and is percent-encoded into the URL, silently requesting something that does not exist.

App

  • TermsView renders the instance's Markdown as server data (never localized), and activates the profile only once a token is stored — consistent with the invariant 🐛 fix(auth): stop entering a session for a server that was never logged into #50 just restored.
  • Reachable from both the password and SSO legs; login/completeSSO return a LoginOutcome rather than a Bool, since valid credentials can now yield a half-finished session.
  • Each restriction gets its own copy, shared between the login screen and the generic error mapper.

Verification

  • 168 tests passing, 9 new — the three restrictions told apart, ordinary 403s left alone, query building, token storage, and an expired pending token surfacing as "start over" rather than a bad password.
  • App builds; localization guard passes (284 keys, all with fr).
  • Two catalog keys the extraction pass flagged as stale are dropped: Unexpected error: %@ and Added to "%@" no longer exist anywhere in the code. A third stale flag, the OIDC authorizationUrl error, was a false positive — that string is still used at LoginView.swift:90 and is kept.

Not verified

The screen has not been exercised against a live instance that demands terms, nor rendered in the simulator. The Markdown is parsed inline-only (AttributedString does not do block syntax), so headings in a server's terms will read as plain lines.

🤖 Generated with Claude Code

Signing in to a server that requires its terms showed "Login is restricted.
Check with your administrator." — wrong advice, and no way forward.

PLANKA answers a refused sign-in with 403 and distinguishes three situations
through the body's `message`: terms acceptance required, SSO-only, and an
instance no admin has initialised yet. `PlankaClient` threw all of them away
(`case 403: throw .forbidden`), so the app could not tell them apart, let alone
act on them.

- Parse the restriction into `AuthRestriction`, carrying the reason and the
  `pendingToken` that resumes the flow. Both `pendingToken` and `step` are
  missing from planka-openapi.json but are returned in practice, so this was
  derived from a real response rather than the spec.
- An ordinary permission 403 still throws `.forbidden`: only bodies that
  actually describe a sign-in restriction qualify, or every "not your board"
  error would start reading as a login problem.
- Add `getTerms(language:)` and `acceptTerms(pendingToken:signature:)`, plus
  query-item support in `buildRequest` — a query folded into `path` lands in
  `URLComponents.path` and gets percent-encoded into the URL.
- Add `TermsView`: renders the instance's Markdown as server data, accepts, and
  activates the profile only once a token is stored. Reachable from both the
  password and SSO legs.
- Give each restriction its own copy. Telling someone to contact an
  administrator when the fix is to press the SSO button helps nobody.

Also drops two catalog keys the extraction pass flagged as stale; neither string
exists in the code any more.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@hub2rock
hub2rock merged commit 8523eb4 into main Aug 21, 2026
7 checks passed
@hub2rock
hub2rock deleted the feat/terms-acceptance branch August 21, 2026 08:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant