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/loginwith{ address } - Wallet session check:
GET /api/auth/wallet/mewithAuthorization: Bearer <token> - Favorites list/status:
GET /api/favoritesandGET /api/favorites?ids=... - Favorite mutation:
POST /api/posts/{id}/favoritewith{ 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.
Recommended secure production flow
If stronger security is required, backend should use nonce + signature:
POST /api/auth/wallet/noncewith{ address }- Backend returns a one-time message / nonce.
- Frontend asks wallet to sign the message.
POST /api/auth/wallet/verifywith{ address, message, signature }- Backend verifies recovered address equals requested address.
- 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 inlang. If a translation does not exist, fall back to the post's source language.description— same rule astitle.categoryName— localized category name forlang. 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, addtypeLabeland use that for display.language— a human-readable label for the post's source language, inlang. e.g. forlang=zh-CNa Chinese post returnslanguage: "中文". If you prefer to keeplanguageas a code, addlanguageLabeland 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/meGET /api/favoritesGET /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}/favoriterejects missing token with401.POST /api/posts/{id}/favoriterejects invalid token with401.POST /api/posts/{id}/favoriteonly changes favorites for the wallet from the validated JWT.GET /api/favoritesrequires a valid Bearer token.GET /api/favorites?ids=...requires a valid Bearer token, unless explicitly declared public/legacy.GET /api/auth/wallet/mevalidates 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.