-
Notifications
You must be signed in to change notification settings - Fork 9
account management #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| export const runtime = 'edge'; | ||
|
|
||
| import { NextRequest, NextResponse } from 'next/server'; | ||
|
|
||
| const WORKER_BASE = process.env.WORKER_BASE_URL || 'https://creator-tool-hub.techfren.workers.dev'; | ||
|
|
||
| function forwardHeaders(req: NextRequest) { | ||
| const headers = new Headers(); | ||
| const keep = ['cookie', 'authorization', 'content-type', 'accept']; | ||
| for (const key of keep) { | ||
| const v = req.headers.get(key); | ||
| if (v) headers.set(key, v); | ||
| } | ||
| // Forward client IP hints when present | ||
| const ip = req.headers.get('cf-connecting-ip') || req.headers.get('x-forwarded-for'); | ||
| if (ip) headers.set('x-forwarded-for', ip); | ||
| const host = req.headers.get('host'); | ||
| if (host) headers.set('x-forwarded-host', host); | ||
| return headers; | ||
| } | ||
|
|
||
| export async function DELETE(req: NextRequest) { | ||
| try { | ||
| const target = `${WORKER_BASE}/api/user/account`; | ||
|
|
||
| const res = await fetch(target, { | ||
| method: 'DELETE', | ||
| headers: forwardHeaders(req), | ||
| }); | ||
|
|
||
| // Stream through status and body | ||
| const body = await res.arrayBuffer(); | ||
| const out = new Response(body, { status: res.status, statusText: res.statusText }); | ||
| res.headers.forEach((v, k) => { | ||
| // Avoid setting hop-by-hop headers | ||
| if (!['content-encoding', 'transfer-encoding'].includes(k.toLowerCase())) { | ||
| out.headers.set(k, v); | ||
| } | ||
| }); | ||
| // Ensure JSON content-type for JSON responses | ||
| if (!out.headers.get('content-type')) out.headers.set('content-type', 'application/json'); | ||
| return out; | ||
| } catch (err: any) { | ||
| return NextResponse.json({ error: 'Upstream error', message: String(err?.message || err) }, { status: 502 }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -67,6 +67,8 @@ export class UserAPI { | |
| // Route to appropriate handler | ||
| if (path === '/api/user/profile') { | ||
| return this.handleProfile(request, userId, method); | ||
| } else if (path === '/api/user/account') { | ||
| return this.handleAccount(request, userId, user.email, method); | ||
| } else if (path === '/api/user/templates') { | ||
| return this.handleTemplates(request, userId, method); | ||
| } else if (path.startsWith('/api/user/templates/')) { | ||
|
|
@@ -115,8 +117,53 @@ export class UserAPI { | |
| headers: { 'Content-Type': 'application/json' } | ||
| }); | ||
| } | ||
|
|
||
| return new Response(JSON.stringify({ error: "Method not allowed" }), { | ||
|
|
||
| return new Response(JSON.stringify({ error: "Method not allowed" }), { | ||
| status: 405, | ||
| headers: { 'Content-Type': 'application/json' } | ||
| }); | ||
| } | ||
|
|
||
| private async handleAccount(request: Request, userId: string, email: string, method: string): Promise<Response> { | ||
| if (method === 'DELETE') { | ||
| try { | ||
| // Delete all user data from the database and get R2 keys to delete | ||
| const { r2Keys } = await this.db.deleteUser(userId); | ||
|
|
||
| // Delete all files from R2 storage | ||
| for (const key of r2Keys) { | ||
| try { | ||
| await this.r2.deleteFile(key); | ||
| } catch (error) { | ||
| console.warn(`Failed to delete R2 file: ${key}`, error); | ||
| // Continue with other deletions even if one fails | ||
| } | ||
| } | ||
|
|
||
| // Return success response with invalidated auth cookie | ||
| return new Response(JSON.stringify({ | ||
| success: true, | ||
| message: 'Account deleted successfully' | ||
| }), { | ||
| status: 200, | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| // Clear the auth cookie | ||
| 'Set-Cookie': 'auth-token=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0' | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The cookie invalidation header uses 🤖 Was this useful? React with 👍 or 👎 |
||
| } | ||
| }); | ||
| } catch (error) { | ||
| console.error('Account deletion error:', error); | ||
| return new Response(JSON.stringify({ | ||
| error: error instanceof Error ? error.message : 'Failed to delete account' | ||
| }), { | ||
| status: 500, | ||
| headers: { 'Content-Type': 'application/json' } | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| return new Response(JSON.stringify({ error: "Method not allowed" }), { | ||
| status: 405, | ||
| headers: { 'Content-Type': 'application/json' } | ||
| }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -676,4 +676,70 @@ export class DatabaseService { | |
| if (!result) return null; | ||
| return convertUserSettingsRow(result); | ||
| } | ||
|
|
||
| /** | ||
| * Delete all user data and the user record | ||
| * Returns objects to be deleted from R2 storage | ||
| */ | ||
| async deleteUser(userId: string): Promise<{ r2Keys: string[] }> { | ||
| const r2Keys: string[] = []; | ||
|
|
||
| // Get all templates and their reference images | ||
| const templates = await this.getTemplates(userId); | ||
| for (const template of templates) { | ||
| const refImages = await this.getReferenceImages(template.id); | ||
| for (const img of refImages) { | ||
| r2Keys.push(img.r2_key); | ||
| } | ||
| } | ||
|
|
||
| // Get all generations and their outputs | ||
| const generations = await this.getGenerations(userId, { limit: 100 }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Limiting to 100 generations risks leaking older R2 files for users with more history; consider iterating through all generations to collect every key before deletion. 🤖 Was this useful? React with 👍 or 👎 |
||
| for (const generation of generations) { | ||
| const outputs = await this.getGenerationOutputs(generation.id); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only 🤖 Was this useful? React with 👍 or 👎 |
||
| for (const output of outputs) { | ||
| r2Keys.push(output.r2_key); | ||
| } | ||
| } | ||
|
|
||
| // Delete in correct order (respecting foreign key constraints) | ||
| // 1. Delete generation outputs and inputs (no foreign keys) | ||
| await this.db.prepare(` | ||
| DELETE FROM generation_outputs | ||
| WHERE generation_id IN (SELECT id FROM generations WHERE user_id = ?) | ||
| `).bind(userId).run(); | ||
|
|
||
| await this.db.prepare(` | ||
| DELETE FROM generation_inputs | ||
| WHERE generation_id IN (SELECT id FROM generations WHERE user_id = ?) | ||
| `).bind(userId).run(); | ||
|
|
||
| // 2. Delete generations | ||
| await this.db.prepare(` | ||
| DELETE FROM generations WHERE user_id = ? | ||
| `).bind(userId).run(); | ||
|
|
||
| // 3. Delete reference images | ||
| await this.db.prepare(` | ||
| DELETE FROM reference_images | ||
| WHERE template_id IN (SELECT id FROM user_templates WHERE user_id = ?) | ||
| `).bind(userId).run(); | ||
|
|
||
| // 4. Delete templates | ||
| await this.db.prepare(` | ||
| DELETE FROM user_templates WHERE user_id = ? | ||
| `).bind(userId).run(); | ||
|
|
||
| // 5. Delete settings | ||
| await this.db.prepare(` | ||
| DELETE FROM user_settings WHERE user_id = ? | ||
| `).bind(userId).run(); | ||
|
|
||
| // 6. Finally delete the user | ||
| await this.db.prepare(` | ||
| DELETE FROM users WHERE id = ? | ||
| `).bind(userId).run(); | ||
|
|
||
| return { r2Keys }; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copying response headers with
out.headers.set(k, v)can collapse multipleSet-Cookieheaders; this may drop cookies when the upstream sets more than one.🤖 Was this useful? React with 👍 or 👎