Files
Arkie-Library-Frontend/docs/backend-wallet-favorites-production-fixes.md
TerryM ec98ff5a03
Some checks failed
Deploy Staging (terry-wallet-login) / deploy (push) Failing after 28s
fix: align favorites page with post adapter
2026-06-04 17:46:09 +08:00

8.9 KiB

Backend fixes required for Wallet Login + Favorites production readiness

Date: 2026-06-04 Environment tested: https://arkie-library-stag.com/apnew/api

Summary

Frontend has been updated to the new backend contract:

  • Wallet login: POST /api/auth/wallet/login with { address }
  • Wallet session check: GET /api/auth/wallet/me with Authorization: Bearer <token>
  • Favorites list/status: GET /api/favorites and GET /api/favorites?ids=...
  • Favorite mutation: POST /api/posts/{id}/favorite with { add: true|false }

Staging confirms the new login endpoint works, but favorite mutation currently accepts an invalid Bearer token. This must be fixed before production trust.


Priority 0 — Fix favorite mutation authentication

Current staging behavior

The following request currently returns 200 OK even with an invalid token:

curl -i -X POST \
  "https://arkie-library-stag.com/apnew/api/posts/8f4a571c-3477-4b05-91be-d85907048de5/favorite" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer invalid-token" \
  --data '{"add":true}'

Observed response:

HTTP 200
{"ok":true}

Required behavior

POST /api/posts/{id}/favorite must require a valid wallet JWT.

Invalid, missing, expired, malformed, or unverifiable tokens must return:

HTTP 401 Unauthorized

Recommended response body:

{
  "error": "unauthorized"
}

Acceptance tests

Missing token

curl -i -X POST \
  "https://arkie-library-stag.com/apnew/api/posts/{postId}/favorite" \
  -H "Content-Type: application/json" \
  --data '{"add":true}'

Expected:

HTTP 401

Invalid token

curl -i -X POST \
  "https://arkie-library-stag.com/apnew/api/posts/{postId}/favorite" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer invalid-token" \
  --data '{"add":true}'

Expected:

HTTP 401

Valid token

TOKEN=$(curl -s -X POST \
  "https://arkie-library-stag.com/apnew/api/auth/wallet/login" \
  -H "Content-Type: application/json" \
  --data '{"address":"0x0000000000000000000000000000000000000001"}' \
  | jq -r .token)

curl -i -X POST \
  "https://arkie-library-stag.com/apnew/api/posts/{postId}/favorite" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  --data '{"add":true}'

Expected:

HTTP 200

Response should include at least:

{
  "ok": true,
  "favorited": true
}

Then cancel:

curl -i -X POST \
  "https://arkie-library-stag.com/apnew/api/posts/{postId}/favorite" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  --data '{"add":false}'

Expected:

{
  "ok": true,
  "favorited": false
}

Priority 1 — Confirm wallet login security model

Current contract

POST /api/auth/wallet/login
Content-Type: application/json

{
  "address": "0x..."
}

Response:

{
  "token": "<jwt>",
  "wallet": "0x..."
}

This is what the frontend now uses.

Production risk

This flow does not prove wallet ownership. Any client can submit any wallet address and receive a token for that address.

If wallet identity is only used for low-risk favorites, this may be acceptable as an MVP. If wallet identity will be used for user identity, permissions, membership, rewards, asset ownership, admin behavior, or anything security-sensitive, backend should require signature verification.

If stronger security is required, backend should use nonce + signature:

  1. POST /api/auth/wallet/nonce with { address }
  2. Backend returns a one-time message / nonce.
  3. Frontend asks wallet to sign the message.
  4. POST /api/auth/wallet/verify with { address, message, signature }
  5. Backend verifies recovered address equals requested address.
  6. Backend issues JWT.

If backend decides to keep the simplified { address } login, please explicitly confirm that this is an accepted production risk.


Priority 2 — Normalize favorites response contract

Frontend currently supports the staging response shape, but the response must be made explicit and self-sufficient. The frontend renders favorites as plain strings and does not perform per-resource translation, slug-to-name lookup, category fetching, or localization fallback.

lang semantics

?lang=<ui-lang> on GET /api/favorites is a display resolution hint, not a filter. It must NOT filter favorites by post language. A user who favorited Chinese and English posts must see both regardless of lang. lang only tells the backend which language to resolve display strings into.

Current staging behavior is wrong: sending ?lang=en on staging returns zero items for users whose favorites are Chinese posts, and vice versa. Because of this, the frontend currently does NOT send lang on GET /api/favorites. Once the backend treats lang as a resolve hint instead of a filter, the frontend will send lang again so resolved strings come back in the user's UI language.

Favorites list

GET /api/favorites?lang=&limit=&page=&sort=&category=&q=
Authorization: Bearer <token>

Required production response:

{
  "items": [
    {
      "id": "...",
      "title": "...",
      "description": "...",
      "type": "...",
      "categoryId": 11,
      "categorySlug": "official-assets",
      "categoryName": "...",
      "language": "...",
      "sourceLanguage": "...",
      "coverImage": "...",
      "updatedAt": "...",
      "publishedAt": "...",
      "favoriteCount": 0,
      "availability": "available"
    }
  ],
  "page": 1,
  "limit": 24,
  "total": 0
}

Fields that must be present and pre-resolved by the backend when lang is supplied:

  • title — already in lang. If a translation does not exist, fall back to the post's source language.
  • description — same rule as title.
  • categoryName — localized category name for lang. Frontend must not look up categories by slug.
  • type — a string the frontend can display directly. If you need both a raw type code and a label, add typeLabel and use that for display.
  • language — a human-readable label for the post's source language, in lang. e.g. for lang=zh-CN a Chinese post returns language: "中文". If you prefer to keep language as a code, add languageLabel and use it for display.
  • coverImage — a usable image URL. The frontend will not fall back to attachment arrays.
  • updatedAt, publishedAt — ISO timestamps.
  • favoriteCount — optional but recommended.
  • availability"available" | "unavailable".

page, limit, and total are needed for correct pagination.

The frontend must never need to: load /api/categories, parse localizations maps, walk attachments, or translate type / language codes for this page.

Favorite status by ids

GET /api/favorites?ids=id1,id2,id3
Authorization: Bearer <token>

Current staging response observed:

{
  "items": []
}

This works, but for frontend performance and clarity, recommended response is:

{
  "ids": ["id1", "id3"]
}

Meaning: only IDs that are already favorited by the current wallet user.

Frontend currently accepts both:

  • { ids: string[] }
  • { items: Resource[] }

But backend should document and standardize one shape.


Priority 3 — Required status codes

Please standardize these responses:

Case Expected status
Missing Bearer token on protected endpoint 401
Invalid/expired Bearer token 401
Valid token but post ID does not exist 404
Invalid JSON body 400
Invalid add value 400
Successful favorite add/remove 200

Protected endpoints:

  • GET /api/auth/wallet/me
  • GET /api/favorites
  • GET /api/favorites?ids=...
  • POST /api/posts/{id}/favorite

Frontend compatibility notes

The frontend currently calls these staging paths through the same-origin prefix:

/apnew/api/auth/wallet/login
/apnew/api/auth/wallet/me
/apnew/api/favorites
/apnew/api/favorites?ids=...
/apnew/api/posts/{id}/favorite

In frontend source this is written as /api/...; staging build uses VITE_API_PREFIX=/apnew.

Please keep backend routes under /api/... behind the proxy.


Final production checklist

Backend should confirm all of the following before production release:

  • POST /api/posts/{id}/favorite rejects missing token with 401.
  • POST /api/posts/{id}/favorite rejects invalid token with 401.
  • POST /api/posts/{id}/favorite only changes favorites for the wallet from the validated JWT.
  • GET /api/favorites requires a valid Bearer token.
  • GET /api/favorites?ids=... requires a valid Bearer token, unless explicitly declared public/legacy.
  • GET /api/auth/wallet/me validates token and returns the wallet address from the token.
  • Backend explicitly confirms whether simplified { address } login is acceptable for production, or switches to nonce/signature verification.