mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
fix(mana/web/news): Bearer token + Dexie v4 schema upgrade
Two distinct bugs surfaced by the first browser-side end-to-end test
of the News module against the locally-managed cloudflared tunnel.
═══ 1. Onboarding loop on reload ═══
The news tables were originally added to db.version(1).stores(),
which violates Dexie's "never edit a published version" contract.
Existing browsers stuck at db.version(3) (after the body + who
upgrades) never trigger an upgrade for v1 changes, so the news tables
silently never get created on those IndexedDB instances. Writes to
preferencesTable.add() / .update() failed at the storage layer, the
preferences row was never persisted, and on reload usePreferences()
returned the DEFAULT_PREFERENCES fallback (onboardingCompleted: false)
which re-rendered the onboarding wizard.
Fix: move the five news tables out of db.version(1) into a fresh
db.version(4).stores({…}) block. Dexie sees the bumped version number
and runs the additive upgrade transaction on existing v3 IndexedDBs,
creating the missing tables. Brand-new IndexedDBs go straight to v4
and pick up the union of all four version blocks. Both paths now
have the news tables present.
═══ 2. /api/v1/news/feed → 401 Missing authorization header ═══
The news api.ts client was passing `credentials: 'include'` thinking
the cookie alone would carry auth through to mana-api. It does not —
apps/api's authMiddleware() reads the Authorization header and
ignores cookies. Every browser-side fetch returned 401, the feed
cache stayed empty, and the wizard's "Fertig" → ranked feed flow
silently failed.
Fix: add a small `authHeader()` helper that pulls the JWT from
authStore.getAccessToken() and attaches it as
`Authorization: Bearer …`, mirroring the pattern in
modules/planta/api.ts. Both `fetchFeed()` and `extractFromUrl()` now
go through it. Drops the cookie credential entirely since it was a
no-op anyway.
Also tidies a Svelte 5 `$props()` warning in modules/news/ListView.svelte
(empty destructure instead of binding to a `_props` const).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9bf73fffa3
commit
7ba381fde8
3 changed files with 61 additions and 26 deletions
|
|
@ -269,23 +269,12 @@ db.version(1).stores({
|
|||
'id, startDate, kind, type, sourceModule, sourceId, parentBlockId, [sourceModule+sourceId], [type+startDate], [kind+startDate], [parentBlockId+recurrenceDate]',
|
||||
timeBlockTags: 'id, blockId, tagId, [blockId+tagId]',
|
||||
|
||||
// ─── News (appId: 'news') ───
|
||||
// `newsArticles` is the user's personal reading list (saved articles
|
||||
// from the curated pool plus user-pasted URLs). `newsCategories` is
|
||||
// user-defined folders for the reading list. `newsPreferences` is a
|
||||
// singleton row holding selected topics, blocklist, language and the
|
||||
// learned topic/source weights. `newsReactions` records per-article
|
||||
// feedback (interested / not_interested / source_blocked / hidden)
|
||||
// and is what the feed engine uses to suppress already-rated items.
|
||||
// `newsCachedFeed` is a local mirror of the latest curated pool from
|
||||
// the server — capped to ~200 entries for offline reading. It is
|
||||
// intentionally NOT in module.config.ts and therefore not synced.
|
||||
newsArticles:
|
||||
'id, type, isArchived, isRead, isFavorite, categoryId, originalUrl, sourceCuratedId, [type+isArchived], [categoryId+createdAt]',
|
||||
newsCategories: 'id, sortOrder',
|
||||
newsPreferences: 'id',
|
||||
newsReactions: 'id, articleId, reaction, sourceSlug, topic, [reaction+createdAt]',
|
||||
newsCachedFeed: 'id, topic, sourceSlug, language, publishedAt, [topic+publishedAt]',
|
||||
// ─── News tables intentionally NOT in v1 ───
|
||||
// Originally added here, but that violates Dexie's "never edit a
|
||||
// published version" rule. Existing browsers stuck at db.version(3)
|
||||
// would never trigger an upgrade for v1 changes, so the news tables
|
||||
// would only appear on a fresh-cleared IndexedDB. Moved into
|
||||
// db.version(4) below — see comment there for rationale + indexes.
|
||||
|
||||
// ─── Shared: Global Tags (appId: 'tags') ───
|
||||
globalTags: 'id, name, groupId',
|
||||
|
|
@ -329,6 +318,38 @@ db.version(3).stores({
|
|||
whoMessages: 'id, gameId, sender, createdAt, [gameId+createdAt]',
|
||||
});
|
||||
|
||||
// Schema version 4 — adds the News module (curated public feed + saved
|
||||
// reading list + per-user preferences/reactions + a local cache of the
|
||||
// server pool). Additive only; no v1/v2/v3 tables touched.
|
||||
//
|
||||
// `newsArticles` is the user's personal reading list (saved from the
|
||||
// curated pool or pasted URLs). `newsCategories` are user-defined
|
||||
// folders. `newsPreferences` is a singleton row keyed on 'singleton'
|
||||
// holding selected topics, blocklist, languages and the learned topic
|
||||
// + source weights. `newsReactions` records per-article feedback
|
||||
// (interested / not_interested / source_blocked / hidden) and is what
|
||||
// the feed engine uses to suppress already-rated items. `newsCachedFeed`
|
||||
// is a local mirror of the server's curated pool, capped to ~400 rows
|
||||
// for offline reading — intentionally NOT in module.config.ts and
|
||||
// therefore not synced.
|
||||
//
|
||||
// Index strategy:
|
||||
// - newsArticles indexes type/categoryId/sourceCuratedId for the
|
||||
// reading-list filter strip and the saveFromCurated() dedupe lookup
|
||||
// ([type+isArchived] for the unread/archive tab queries)
|
||||
// - newsReactions indexes [reaction+createdAt] so the feed engine can
|
||||
// range-scan "what did the user dismiss" without loading every row
|
||||
// - newsCachedFeed indexes [topic+publishedAt] so the topic-filter
|
||||
// pass in rankFeed() can do a single index walk instead of N scans
|
||||
db.version(4).stores({
|
||||
newsArticles:
|
||||
'id, type, isArchived, isRead, isFavorite, categoryId, originalUrl, sourceCuratedId, [type+isArchived], [categoryId+createdAt]',
|
||||
newsCategories: 'id, sortOrder',
|
||||
newsPreferences: 'id',
|
||||
newsReactions: 'id, articleId, reaction, sourceSlug, topic, [reaction+createdAt]',
|
||||
newsCachedFeed: 'id, topic, sourceSlug, language, publishedAt, [topic+publishedAt]',
|
||||
});
|
||||
|
||||
// ─── Sync Routing ──────────────────────────────────────────
|
||||
// SYNC_APP_MAP, TABLE_TO_SYNC_NAME, TABLE_TO_APP, SYNC_NAME_TO_TABLE,
|
||||
// toSyncName() and fromSyncName() are now derived from per-module
|
||||
|
|
|
|||
|
|
@ -32,9 +32,10 @@
|
|||
|
||||
// We accept ViewProps for protocol compatibility but the workbench
|
||||
// view doesn't navigate within itself — every "open" jumps to the
|
||||
// dedicated /news routes.
|
||||
let _props: ViewProps = $props();
|
||||
void _props;
|
||||
// dedicated /news routes. Empty destructure satisfies the $props()
|
||||
// declaration without referencing the props object (which would
|
||||
// trigger Svelte's "captured initial value" warning).
|
||||
const {}: ViewProps = $props();
|
||||
|
||||
const prefs$ = usePreferences();
|
||||
const pool$ = useCachedFeed();
|
||||
|
|
|
|||
|
|
@ -13,13 +13,22 @@
|
|||
* leak the SSR-side internal Docker hostname (`http://mana-api:3060`) to
|
||||
* the browser and trip CSP / DNS.
|
||||
*
|
||||
* Auth is the unified Mana JWT, picked up by the same fetch wrapper the
|
||||
* rest of the app uses (cookie + Authorization header set by SvelteKit
|
||||
* `fetch` via the auth-provider middleware).
|
||||
* Auth is the unified Mana JWT pulled from `authStore.getAccessToken()`
|
||||
* and attached as a `Authorization: Bearer …` header. The credentials/
|
||||
* cookie path does NOT work — the apps/api authMiddleware only reads
|
||||
* the Authorization header. Initially we passed `credentials: 'include'`
|
||||
* thinking the cookie alone was enough, which made every browser-side
|
||||
* fetch return 401 because mana-api never sees a token.
|
||||
*/
|
||||
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { getManaApiUrl } from '$lib/api/config';
|
||||
|
||||
async function authHeader(): Promise<Record<string, string>> {
|
||||
const token = await authStore.getAccessToken();
|
||||
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||
}
|
||||
|
||||
export interface FeedArticleDto {
|
||||
id: string;
|
||||
originalUrl: string;
|
||||
|
|
@ -61,7 +70,9 @@ export async function fetchFeed(
|
|||
if (query.offset != null) params.set('offset', String(query.offset));
|
||||
|
||||
const url = `${getManaApiUrl()}/api/v1/news/feed${params.toString() ? `?${params}` : ''}`;
|
||||
const response = await fetchImpl(url, { credentials: 'include' });
|
||||
const response = await fetchImpl(url, {
|
||||
headers: await authHeader(),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`fetchFeed failed: ${response.status}`);
|
||||
}
|
||||
|
|
@ -92,8 +103,10 @@ export async function extractFromUrl(
|
|||
): Promise<ExtractedArticleDto> {
|
||||
const response = await fetchImpl(`${getManaApiUrl()}/api/v1/news/extract/save`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(await authHeader()),
|
||||
},
|
||||
body: JSON.stringify({ url }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue