refactor(profile): visual voice picker, playback toggles, danger zone
- 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:
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user