# 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:

```bash
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
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
HTTP 401 Unauthorized
```

Recommended response body:

```json
{
  "error": "unauthorized"
}
```

### Acceptance tests

#### Missing token

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

Expected:

```http
HTTP 401
```

#### Invalid token

```bash
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
HTTP 401
```

#### Valid token

```bash
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
HTTP 200
```

Response should include at least:

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

Then cancel:

```bash
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:

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

---

## Priority 1 — Confirm wallet login security model

### Current contract

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

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

Response:

```json
{
  "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:

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

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

Required production response:

```json
{
  "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

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

Current staging response observed:

```json
{
  "items": []
}
```

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

```json
{
  "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:

```txt
/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.
