refactor(profile): visual voice picker, playback toggles, danger zone
All checks were successful
Release / Test backend (push) Successful in 1m2s
Release / Check ui (push) Successful in 1m46s
Release / Docker (push) Successful in 6m9s
Release / Gitea Release (push) Successful in 29s

- Replace voice <select> with a two-column card grid grouped by engine
  (Kokoro GPU / Pocket TTS CPU / Cloudflare AI); each card has a per-voice
  sample play/pause button matching AudioPlayer behaviour
- Add Announce chapter and Audio mode (Stream/Generate) toggles to a
  unified Playback row in Preferences; Audio mode toggle disabled for
  CF AI voices
- Remove duplicate PUT /api/settings from the profile page; all writes
  go directly into audioStore / theme context and the layout's single
  debounced effect persists them
- Add Danger Zone section: collapsible, requires typing username to
  unlock Delete account button; calls DELETE /api/profile
- Add deleteUserAccount() to pocketbase.ts: purges user_settings,
  user_library, progress, comment_votes, book_ratings,
  user_subscriptions, notifications, user_sessions then the
  app_users record
- Add DELETE /api/profile server route (auth-guarded)
This commit is contained in:
root
2026-04-10 22:30:39 +05:00
parent f8c66fcf63
commit e78c44459e
3 changed files with 455 additions and 138 deletions

View File

@@ -1415,6 +1415,56 @@ export async function revokeAllUserSessions(userId: string): Promise<void> {
);
}
/**
* Delete all data associated with a user account:
* - user_settings, user_library, progress, comment_votes, book_ratings,
* user_subscriptions, user_sessions, notifications rows owned by the user
* - the app_users record itself
*
* Does NOT delete audio files from MinIO (shared cache) or book comments
* (anonymised to preserve discussion threads).
*/
export async function deleteUserAccount(userId: string, sessionId: string): Promise<void> {
const collections = [
{ name: 'user_settings', filter: `(user_id="${userId}" || session_id="${sessionId}")` },
{ name: 'user_library', filter: `(user_id="${userId}" || session_id="${sessionId}")` },
{ name: 'progress', filter: `(user_id="${userId}" || session_id="${sessionId}")` },
{ name: 'comment_votes', filter: `user_id="${userId}"` },
{ name: 'book_ratings', filter: `user_id="${userId}"` },
{ name: 'user_subscriptions', filter: `(follower_id="${userId}" || followee_id="${userId}")` },
{ name: 'notifications', filter: `user_id="${userId}"` },
{ name: 'user_sessions', filter: `user_id="${userId}"` },
];
const token = await getToken();
for (const { name, filter } of collections) {
try {
const rows = await listAll<{ id: string }>(name, filter);
await Promise.all(
rows.map((r) =>
fetch(`${PB_URL}/api/collections/${name}/records/${r.id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }
}).catch(() => {})
)
);
} catch {
// Best-effort: log and continue so one failure doesn't abort the rest
log.warn('pocketbase', `deleteUserAccount: failed to purge ${name}`, { userId });
}
}
// Delete the user record last
const res = await pbDelete(`/api/collections/app_users/records/${userId}`);
if (!res.ok) {
const body = await res.text().catch(() => '');
log.error('pocketbase', 'deleteUserAccount: failed to delete app_users record', { userId, status: res.status, body });
throw new Error(`Failed to delete user record (${res.status})`);
}
log.info('pocketbase', 'deleteUserAccount: account deleted', { userId });
}
/**
* Update the avatar_url field for a user record.
*/