feat: profile page, admin pages, infinite scroll on browse
Some checks failed
Deploy / Deploy Production (push) Has been skipped
Deploy / Cleanup Preview (push) Has been skipped
Deploy / Deploy Preview (push) Failing after 1s
CI / UI / Build (pull_request) Successful in 16s
CI / Scraper / Test (pull_request) Successful in 16s
CI / Scraper / Lint (pull_request) Successful in 19s
CI / Scraper / Build (pull_request) Successful in 16s
Some checks failed
Deploy / Deploy Production (push) Has been skipped
Deploy / Cleanup Preview (push) Has been skipped
Deploy / Deploy Preview (push) Failing after 1s
CI / UI / Build (pull_request) Successful in 16s
CI / Scraper / Test (pull_request) Successful in 16s
CI / Scraper / Lint (pull_request) Successful in 19s
CI / Scraper / Build (pull_request) Successful in 16s
- Add /profile page with reading settings (voice, speed, auto-next) and password change form - Add /admin/scrape page showing scraping task history with live status polling and trigger controls - Add /admin/audio page showing audio cache entries with client-side search filter - Add changePassword(), listAudioCache(), listScrapingTasks() to pocketbase.ts - Add /api/admin/scrape and /api/browse-page server-side proxy routes - Replace browse page pagination with IntersectionObserver infinite scroll - Update nav: username becomes a /profile link; admin users see Scrape and Audio cache links
This commit is contained in:
@@ -479,6 +479,44 @@ export async function createUser(username: string, password: string, role = 'use
|
||||
return res.json() as Promise<User>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change a user's password. Verifies the current password first.
|
||||
* Returns true on success, false if currentPassword is wrong.
|
||||
* Throws on unexpected errors.
|
||||
*/
|
||||
export async function changePassword(
|
||||
userId: string,
|
||||
currentPassword: string,
|
||||
newPassword: string
|
||||
): Promise<boolean> {
|
||||
// Fetch the user record directly by id to verify current password
|
||||
const token = await getToken();
|
||||
const res = await fetch(`${PB_URL}/api/collections/app_users/records/${userId}`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.text().catch(() => '');
|
||||
log.error('pocketbase', 'changePassword: fetch user failed', { userId, status: res.status, body });
|
||||
throw new Error(`Failed to fetch user: ${res.status}`);
|
||||
}
|
||||
const user = (await res.json()) as User;
|
||||
if (!verifyPassword(currentPassword, user.password_hash)) {
|
||||
log.warn('pocketbase', 'changePassword: wrong current password', { userId });
|
||||
return false;
|
||||
}
|
||||
const newHash = hashPassword(newPassword);
|
||||
const patch = await pbPatch(`/api/collections/app_users/records/${userId}`, {
|
||||
password_hash: newHash
|
||||
});
|
||||
if (!patch.ok) {
|
||||
const body = await patch.text().catch(() => '');
|
||||
log.error('pocketbase', 'changePassword: PATCH failed', { userId, status: patch.status, body });
|
||||
throw new Error(`Failed to update password: ${patch.status}`);
|
||||
}
|
||||
log.info('pocketbase', 'changePassword: success', { userId });
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify username + password. Returns the user on success, null on failure.
|
||||
*/
|
||||
@@ -586,6 +624,39 @@ export async function setAudioTime(
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Audio cache ──────────────────────────────────────────────────────────────
|
||||
|
||||
export interface AudioCacheEntry {
|
||||
id: string;
|
||||
cache_key: string;
|
||||
filename: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export async function listAudioCache(): Promise<AudioCacheEntry[]> {
|
||||
return listAll<AudioCacheEntry>('audio_cache', '', '-updated');
|
||||
}
|
||||
|
||||
// ─── Scraping tasks ───────────────────────────────────────────────────────────
|
||||
|
||||
export interface ScrapingTask {
|
||||
id: string;
|
||||
kind: string;
|
||||
target_url: string;
|
||||
status: string;
|
||||
books_found: number;
|
||||
chapters_scraped: number;
|
||||
chapters_skipped: number;
|
||||
errors: number;
|
||||
started: string;
|
||||
finished: string;
|
||||
error_message: string;
|
||||
}
|
||||
|
||||
export async function listScrapingTasks(): Promise<ScrapingTask[]> {
|
||||
return listAll<ScrapingTask>('scraping_tasks', '', '-started');
|
||||
}
|
||||
|
||||
export async function getAudioTime(
|
||||
sessionId: string,
|
||||
slug: string,
|
||||
|
||||
Reference in New Issue
Block a user