Skip to content

fix(auth): restore iOS note save/reload reliability#174

Merged
auerbachb merged 3 commits into
mainfrom
issue-171-ios-notes-save
Apr 18, 2026
Merged

fix(auth): restore iOS note save/reload reliability#174
auerbachb merged 3 commits into
mainfrom
issue-171-ios-notes-save

Conversation

@auerbachb
Copy link
Copy Markdown
Owner

@auerbachb auerbachb commented Apr 18, 2026

Summary

  • add bearer-token fallback auth on backend (middleware and getCurrentUser) so authenticated API calls from native iOS do not depend on cookie behavior
  • return token in login/signup responses and store it in iOS Keychain; send Authorization: Bearer on all iOS API requests
  • keep cookie auth fully compatible and clear local token state on logout/account deletion

Closes #171. Related to #60 (same symptom class: iOS note save failures tied to auth/response expectations).

Done Definition (AC)

  • notes save successfully on iOS device and still appear after app relaunch/login restoration
  • buddy path is not in current iOS app flow (web-only), so this PR scopes to solo iOS note/session endpoints

Test plan

  • npm run build
  • xcodebuild -project ios/StillPoint.xcodeproj -scheme StillPoint -destination 'generic/platform=iOS Simulator' build CODE_SIGNING_ALLOWED=NO
  • On iOS device: log in, complete solo session, save end note, relaunch app, verify note appears in Thought Journal
  • On iOS device: capture mid-session thought, complete session, relaunch app, verify both thought and end note persist

Made with Cursor

Summary by CodeRabbit

  • New Features

    • iOS app now persists and clears bearer tokens locally and includes a client identifier in requests.
    • Auth endpoints will return tokens to iOS clients on signup/login to enable bearer-based flows.
  • Behavior Changes

    • Server and middleware now accept and preferentially use bearer tokens (Authorization: Bearer ...) in addition to cookies.

Return JWT tokens on auth responses and add bearer-token fallback auth in middleware/helpers so iOS note saves and reloads remain authenticated across device sessions.

Made-with: Cursor
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
still-point Ignored Ignored Preview Apr 18, 2026 3:18pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 18, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 856b7ee5-93fe-46fc-b711-9b7021b2a437

📥 Commits

Reviewing files that changed from the base of the PR and between e4d8023 and 2b80e10.

📒 Files selected for processing (2)
  • ios/StillPointShared/Sources/StillPointShared/APIClient.swift
  • ios/StillPointShared/Sources/StillPointShared/AuthTokenStore.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • ios/StillPointShared/Sources/StillPointShared/APIClient.swift

📝 Walkthrough

Walkthrough

iOS now persists bearer tokens returned by signup/login into Keychain, attaches Authorization: Bearer <token> and X-Still-Point-Client: ios on requests, and clears tokens on logout. Server endpoints conditionally include token for "ios" clients; server auth resolution and middleware accept bearer tokens in Authorization headers as well.

Changes

Cohort / File(s) Summary
iOS Keychain Store
ios/StillPointShared/Sources/StillPointShared/AuthTokenStore.swift
New AuthTokenStore with save(_:), load() -> String?, and clear() using Keychain (SecItem*) and error logging.
iOS Client & DTO
ios/StillPointShared/Sources/StillPointShared/APIClient.swift, ios/StillPointShared/Sources/StillPointShared/DTOs/DTOs.swift
APIClient refactored to makeRequest + applyAuthorizationHeader; persists response.token via AuthTokenStore.save, clears tokens on logout, sets X-Still-Point-Client: ios. UserResponse gains optional token: String?.
Backend: auth routes
src/app/api/auth/login/route.ts, src/app/api/auth/signup/route.ts
Handlers read x-still-point-client; when "ios", successful JSON responses include token alongside user (token creation and cookie-setting unchanged).
Backend: token resolution
src/lib/auth.ts, src/middleware.ts
getCurrentUser() and middleware now parse Authorization: Bearer <token> and prefer header token over sp_token cookie when present.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant "iOS Client" as IOS
    participant Keychain
    participant "Backend API" as API
    participant Middleware
    participant "Auth Service" as Auth

    User->>IOS: Login/Signup (credentials)
    IOS->>API: POST /auth/login or /signup\nHeaders: X-Still-Point-Client: ios
    API->>Auth: verify credentials & generate token
    Auth-->>API: user + token
    API-->>IOS: { user, token } (token included because header == ios)
    IOS->>Keychain: save(token)
    Keychain-->>IOS: saved

    rect rgba(100,200,100,0.5)
    Note over IOS,API: Authenticated request flow
    User->>IOS: Request protected resource
    IOS->>Keychain: load()
    Keychain-->>IOS: token
    IOS->>API: GET /resource\nAuthorization: Bearer <token>
    API->>Middleware: validate request
    Middleware->>Auth: verify token (from Authorization or cookie)
    Auth-->>Middleware: verified
    Middleware-->>API: proceed
    API-->>IOS: protected resource
    end

    User->>IOS: Logout
    IOS->>API: POST /auth/logout
    IOS->>Keychain: clear()
    Keychain-->>IOS: cleared
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hid the token in a cozy chest,
Across the wire it took its rest,
A header hop, a cookie glance,
Now iOS and server dance! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title mentions 'iOS note save/reload reliability' but the PR primarily implements bearer-token authentication flow and Keychain persistence, not a direct fix to note saving itself. Revise the title to better reflect the core changes, such as 'feat(auth): add bearer-token auth and Keychain persistence for iOS' or 'feat(auth): implement token-based iOS authentication with Keychain storage'.
Docstring Coverage ⚠️ Warning Docstring coverage is 10.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The PR addresses #171's core requirement: restoring reliable iOS note saving by implementing bearer-token flow to eliminate cookie dependency and adding Keychain-based token persistence for consistent authentication across app sessions.
Out of Scope Changes check ✅ Passed Backend middleware and auth utilities were enhanced to support bearer-token extraction alongside cookies; changes are scoped to iOS authentication reliability and remain within PR objectives with no unrelated modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📋 Issue Planner

Built with CodeRabbit's Coding Plans for faster development and fewer bugs.

View plan used: #171

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-171-ios-notes-save

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
ios/StillPointShared/Sources/StillPointShared/AuthTokenStore.swift (1)

18-20: Set explicit Keychain accessibility for the auth token.

Consider adding kSecAttrAccessible (e.g., kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly) so token protection is explicit instead of relying on defaults.

🔐 Suggested tweak
 var attributes = query
 attributes[kSecValueData as String] = data
+attributes[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
 SecItemAdd(attributes as CFDictionary, nil)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/StillPointShared/Sources/StillPointShared/AuthTokenStore.swift` around
lines 18 - 20, When saving the auth token in AuthTokenStore (around the
attributes/query construction before SecItemAdd), add an explicit
kSecAttrAccessible entry to the attributes dictionary (for example
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly) so the token's Keychain
accessibility is explicit; update the same attributes used in the SecItemAdd
call (the variable named attributes) to include this accessibility constant
before calling SecItemAdd.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ios/StillPointShared/Sources/StillPointShared/APIClient.swift`:
- Around line 36-38: The current auth-response handling only saves a token when
response.token is present (AuthTokenStore.save(token)) but never clears a
previously stored token when response.token is nil; update both spots (the if
let response.token blocks) to add an else branch that removes the stored token
from the keychain via the AuthTokenStore API (e.g., call AuthTokenStore.clear()
or AuthTokenStore.deleteStoredToken() — use the existing deletion method in
AuthTokenStore) so stale tokens are removed when the response contains no token.
- Around line 52-55: The logout() method currently only calls
AuthTokenStore.clear() after a successful post call, so local tokens/cookies
remain if the request fails; move the local cleanup into a guaranteed path by
invoking AuthTokenStore.clear() regardless of network outcome (for example, add
a defer { AuthTokenStore.clear() } at the start of logout() or otherwise ensure
AuthTokenStore.clear() is executed before returning/throwing) while keeping the
post("/api/auth/logout", body: Optional<String>.none) call to perform the
server-side logout.

In `@ios/StillPointShared/Sources/StillPointShared/AuthTokenStore.swift`:
- Around line 8-21: The Keychain calls in save(_:), and the other Keychain
helpers that call SecItemDelete/SecItemAdd/SecItemCopyMatching are ignoring the
returned OSStatus; capture each OSStatus result and handle non-errSecSuccess
cases instead of discarding them. Update save(_ token: String) to capture the
return values of SecItemDelete and SecItemAdd, and either throw an error or
return a Bool so callers can react; include a clear log message with the
OSStatus (or SecCopyErrorMessageString output) when the call fails. Do the same
for the corresponding load/delete functions that call
SecItemCopyMatching/SecItemDelete so failures are propagated to the auth flow
rather than silently ignored.

In `@src/lib/auth.ts`:
- Around line 57-61: The current logic gives cookie precedence (tokenFromCookie)
over an Authorization Bearer (tokenFromBearer), which can let a stale cookie
override a valid bearer token; change the selection so the Bearer header wins:
when computing token (currently assigned from tokenFromCookie ??
tokenFromBearer), invert the precedence to use tokenFromBearer ??
tokenFromCookie and keep the existing Bearer detection/trim logic (see
COOKIE_NAME, tokenFromCookie, tokenFromBearer, and the token assignment) so
valid Authorization headers are preferred over cookies.

In `@src/middleware.ts`:
- Around line 32-37: The code currently prefers cookie tokens over Bearer tokens
causing stale cookies to override valid Authorization headers; change the
precedence so tokenFromBearer is chosen first: when computing token (currently
assigned from tokenFromCookie ?? tokenFromBearer) use tokenFromBearer ??
tokenFromCookie instead, keeping the existing Authorization parsing
(authorization?.startsWith("Bearer ") ? authorization.slice("Bearer
".length).trim() : null) and COOKIE_NAME usage unchanged so trimming/null
handling remains intact.

---

Nitpick comments:
In `@ios/StillPointShared/Sources/StillPointShared/AuthTokenStore.swift`:
- Around line 18-20: When saving the auth token in AuthTokenStore (around the
attributes/query construction before SecItemAdd), add an explicit
kSecAttrAccessible entry to the attributes dictionary (for example
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly) so the token's Keychain
accessibility is explicit; update the same attributes used in the SecItemAdd
call (the variable named attributes) to include this accessibility constant
before calling SecItemAdd.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cd363d39-7563-4247-9809-3b43708b0a9c

📥 Commits

Reviewing files that changed from the base of the PR and between 8e93f7f and 521c903.

📒 Files selected for processing (7)
  • ios/StillPointShared/Sources/StillPointShared/APIClient.swift
  • ios/StillPointShared/Sources/StillPointShared/AuthTokenStore.swift
  • ios/StillPointShared/Sources/StillPointShared/DTOs/DTOs.swift
  • src/app/api/auth/login/route.ts
  • src/app/api/auth/signup/route.ts
  • src/lib/auth.ts
  • src/middleware.ts

Comment thread ios/StillPointShared/Sources/StillPointShared/APIClient.swift Outdated
Comment thread ios/StillPointShared/Sources/StillPointShared/APIClient.swift
Comment thread ios/StillPointShared/Sources/StillPointShared/AuthTokenStore.swift Outdated
Comment thread src/lib/auth.ts Outdated
Comment thread src/middleware.ts Outdated
Comment thread src/app/api/auth/login/route.ts Outdated
Address CodeRabbit/Bugbot findings by making bearer auth header-first, tightening keychain handling, and returning JWTs only for explicit iOS client requests.

Made-with: Cursor
@auerbachb
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 18, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
ios/StillPointShared/Sources/StillPointShared/AuthTokenStore.swift (1)

8-29: ⚠️ Potential issue | 🟠 Major

Propagate token persistence failures to callers instead of only logging.

Lines 8-29 and Lines 57-67 still swallow Keychain failures; the auth flow can’t react, so login can appear successful while bearer auth is effectively broken.

🔧 Proposed fix
 enum AuthTokenStore {
@@
-    static func save(_ token: String) {
+    `@discardableResult`
+    static func save(_ token: String) -> Bool {
@@
         let deleteStatus = SecItemDelete(query as CFDictionary)
         if deleteStatus != errSecSuccess && deleteStatus != errSecItemNotFound {
             logKeychainFailure(operation: "delete-before-save", status: deleteStatus)
+            return false
         }
@@
         let addStatus = SecItemAdd(attributes as CFDictionary, nil)
         if addStatus != errSecSuccess {
             logKeychainFailure(operation: "save", status: addStatus)
+            return false
         }
+        return true
     }
@@
-    static func clear() {
+    `@discardableResult`
+    static func clear() -> Bool {
@@
         let status = SecItemDelete(query as CFDictionary)
         if status != errSecSuccess && status != errSecItemNotFound {
             logKeychainFailure(operation: "clear", status: status)
+            return false
         }
+        return true
     }

Also applies to: 57-67

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/StillPointShared/Sources/StillPointShared/AuthTokenStore.swift` around
lines 8 - 29, AuthTokenStore.save currently swallows Keychain errors; change its
API to propagate failures (e.g., make static func save(_ token: String) throws
or return Result/Bool) and throw/return a meaningful error when SecItemDelete or
SecItemAdd return a non-success status (include the OSStatus in the error), and
update callers to handle the thrown/returned error; apply the same change to the
other affected methods around lines 57-67 (the other Keychain read/write/delete
helpers) and use or augment logKeychainFailure to record the failure details
while still propagating an error to the caller.
🧹 Nitpick comments (1)
ios/StillPointShared/Sources/StillPointShared/APIClient.swift (1)

121-129: Consider centralizing request header setup to reduce drift across HTTP methods.

The same header setup pattern is repeated in get/post/patch/delete; a shared builder would make future auth-header changes less error-prone.

♻️ Refactor sketch
+    private func makeRequest(method: String, path: String) -> URLRequest {
+        let url = baseURL.appendingPathComponent(path)
+        var request = URLRequest(url: url)
+        request.httpMethod = method
+        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+        request.setValue("ios", forHTTPHeaderField: "X-Still-Point-Client")
+        applyAuthorizationHeader(to: &request)
+        return request
+    }
@@
-        let url = baseURL.appendingPathComponent(path)
-        var request = URLRequest(url: url)
-        request.httpMethod = "GET"
-        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
-        request.setValue("ios", forHTTPHeaderField: "X-Still-Point-Client")
-        applyAuthorizationHeader(to: &request)
+        var request = makeRequest(method: "GET", path: path)

Also applies to: 131-142, 144-153, 155-163

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/StillPointShared/Sources/StillPointShared/APIClient.swift` around lines
121 - 129, Multiple HTTP methods (get/post/patch/delete) duplicate header and
request setup; create a single helper (e.g., buildRequest(path: String, method:
String) or makeRequest(path: String, method: HTTPMethod)) that constructs the
URLRequest, sets "Content-Type", "X-Still-Point-Client", applies
applyAuthorizationHeader(to:), and returns the prepared request for use by
execute(_:). Replace the repeated setup in get/post/patch/delete with calls to
this new helper and keep execute(request) unchanged so future header/auth
changes are made in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@ios/StillPointShared/Sources/StillPointShared/AuthTokenStore.swift`:
- Around line 8-29: AuthTokenStore.save currently swallows Keychain errors;
change its API to propagate failures (e.g., make static func save(_ token:
String) throws or return Result/Bool) and throw/return a meaningful error when
SecItemDelete or SecItemAdd return a non-success status (include the OSStatus in
the error), and update callers to handle the thrown/returned error; apply the
same change to the other affected methods around lines 57-67 (the other Keychain
read/write/delete helpers) and use or augment logKeychainFailure to record the
failure details while still propagating an error to the caller.

---

Nitpick comments:
In `@ios/StillPointShared/Sources/StillPointShared/APIClient.swift`:
- Around line 121-129: Multiple HTTP methods (get/post/patch/delete) duplicate
header and request setup; create a single helper (e.g., buildRequest(path:
String, method: String) or makeRequest(path: String, method: HTTPMethod)) that
constructs the URLRequest, sets "Content-Type", "X-Still-Point-Client", applies
applyAuthorizationHeader(to:), and returns the prepared request for use by
execute(_:). Replace the repeated setup in get/post/patch/delete with calls to
this new helper and keep execute(request) unchanged so future header/auth
changes are made in one place.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 72b914c9-412b-424d-a48b-b61ed266eb35

📥 Commits

Reviewing files that changed from the base of the PR and between 521c903 and e4d8023.

📒 Files selected for processing (6)
  • ios/StillPointShared/Sources/StillPointShared/APIClient.swift
  • ios/StillPointShared/Sources/StillPointShared/AuthTokenStore.swift
  • src/app/api/auth/login/route.ts
  • src/app/api/auth/signup/route.ts
  • src/lib/auth.ts
  • src/middleware.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/app/api/auth/signup/route.ts
  • src/app/api/auth/login/route.ts
  • src/lib/auth.ts
  • src/middleware.ts

Make AuthTokenStore save/clear return success status, fail fast when token persistence fails during login/signup, and centralize iOS request header setup to keep auth behavior consistent.

Made-with: Cursor
@auerbachb
Copy link
Copy Markdown
Owner Author

@cursor review

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 2b80e10. Configure here.

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.

Saving notes in iOS broken again

1 participant