mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 18:01:09 +02:00
Letzter "community"-Rest aus dem Feedback-Hub räumt sich auf — DB-Spalten, Settings-Search-Index, Section-Name und i18n-Keys einheitlich auf "feedback": - DB: auth.users.community_show_real_name → feedback_show_real_name, community_karma → feedback_karma. Migration unter services/mana-auth/sql/009_rename_community_to_feedback.sql (manuell via psql, in Drizzle-Schema beider Services nachgezogen). - mana-auth/me.ts: PATCH /api/v1/me/profile akzeptiert jetzt feedbackShowRealName und gibt es im Response zurück. - mana-analytics: feedback.ts liest authUsers.feedbackShowRealName / feedbackKarma, redact() + Karma-Increment + Tests entsprechend. - Frontend: CommunitySection.svelte → FeedbackIdentitySection.svelte (Datei umbenannt, Property-Namen + Toast-Texte aktualisiert, HeartHalf-Icon, "Feedback-Identität" als Title). - searchIndex.ts: CategoryId 'community' → 'feedback', anchor 'community-identity' → 'feedback-identity'. - i18n (5 locales): settings.categories.community → .feedback, settings.search.community_* → feedback_*. Labels DE/EN/FR/IT/ES jeweils auf "Feedback" + "im Feedback-Feed" angepasst. 38/38 Integration-Tests grün, validate:i18n-parity sauber, svelte-check 0. BREAKING (intern, nicht live): Frontend, das gegen die alten Spalten- / Property-Namen aus dem PATCH-Response geht, fällt jetzt um. Kein Production-Risiko da Hub noch nicht öffentlich. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
123 lines
4 KiB
TypeScript
123 lines
4 KiB
TypeScript
/**
|
||
* Me routes — GDPR self-service data management
|
||
*
|
||
* GET /data — Full user data summary (auth, credits, projects)
|
||
* GET /data/export — Download all data as JSON
|
||
* DELETE /data — Delete all user data (right to be forgotten)
|
||
*/
|
||
|
||
import { Hono } from 'hono';
|
||
import { eq } from 'drizzle-orm';
|
||
import type { AuthUser } from '../middleware/jwt-auth';
|
||
import type { UserDataService } from '../services/user-data';
|
||
import type { Database } from '../db/connection';
|
||
import { users } from '../db/schema/auth';
|
||
import { sendAccountDeletionEmail } from '../email/send';
|
||
|
||
export function createMeRoutes(userDataService: UserDataService, db: Database) {
|
||
return (
|
||
new Hono<{ Variables: { user: AuthUser } }>()
|
||
|
||
// ─── Get full user data summary ─────────────────────────
|
||
.get('/data', async (c) => {
|
||
const user = c.get('user');
|
||
const summary = await userDataService.getUserDataSummary(user.userId);
|
||
|
||
if (!summary) {
|
||
return c.json({ error: 'User not found' }, 404);
|
||
}
|
||
|
||
return c.json(summary);
|
||
})
|
||
|
||
// ─── Export user data as JSON download ──────────────────
|
||
.get('/data/export', async (c) => {
|
||
const user = c.get('user');
|
||
const exportData = await userDataService.exportUserData(user.userId);
|
||
|
||
if (!exportData) {
|
||
return c.json({ error: 'User not found' }, 404);
|
||
}
|
||
|
||
const filename = `meine-daten-${new Date().toISOString().split('T')[0]}.json`;
|
||
const json = JSON.stringify(exportData, null, 2);
|
||
|
||
return new Response(json, {
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Content-Disposition': `attachment; filename="${filename}"`,
|
||
},
|
||
});
|
||
})
|
||
|
||
// ─── Delete all user data ───────────────────────────────
|
||
.delete('/data', async (c) => {
|
||
const user = c.get('user');
|
||
const result = await userDataService.deleteUserData(user.userId, user.email);
|
||
|
||
// Send confirmation email (fire-and-forget)
|
||
sendAccountDeletionEmail(user.email).catch(() => {});
|
||
|
||
return c.json(result);
|
||
})
|
||
|
||
// ─── Update profile (name, avatar) ──────────────────────
|
||
// Minimal patch endpoint used by the onboarding flow and
|
||
// Settings → Profile. JWT-based like the rest of /me/*; the
|
||
// updated name only lands in the user's JWT on next mint, so
|
||
// the caller is responsible for refreshing its in-memory
|
||
// representation of authStore.user. See docs/plans/onboarding-flow.md.
|
||
.patch('/profile', async (c) => {
|
||
const user = c.get('user');
|
||
const body = (await c.req.json().catch(() => ({}))) as {
|
||
name?: unknown;
|
||
image?: unknown;
|
||
feedbackShowRealName?: unknown;
|
||
};
|
||
|
||
const patch: {
|
||
name?: string;
|
||
image?: string;
|
||
feedbackShowRealName?: boolean;
|
||
updatedAt: Date;
|
||
} = {
|
||
updatedAt: new Date(),
|
||
};
|
||
if (typeof body.name === 'string') {
|
||
const trimmed = body.name.trim();
|
||
if (trimmed.length < 1 || trimmed.length > 80) {
|
||
return c.json({ error: 'name must be 1–80 characters' }, 400);
|
||
}
|
||
patch.name = trimmed;
|
||
}
|
||
if (typeof body.image === 'string') {
|
||
patch.image = body.image;
|
||
}
|
||
if (typeof body.feedbackShowRealName === 'boolean') {
|
||
patch.feedbackShowRealName = body.feedbackShowRealName;
|
||
}
|
||
|
||
if (!('name' in patch) && !('image' in patch) && !('feedbackShowRealName' in patch)) {
|
||
return c.json({ error: 'no fields to update' }, 400);
|
||
}
|
||
|
||
const [updated] = await db
|
||
.update(users)
|
||
.set(patch)
|
||
.where(eq(users.id, user.userId))
|
||
.returning({
|
||
id: users.id,
|
||
name: users.name,
|
||
image: users.image,
|
||
feedbackShowRealName: users.feedbackShowRealName,
|
||
});
|
||
|
||
if (!updated) return c.json({ error: 'User not found' }, 404);
|
||
return c.json({
|
||
name: updated.name,
|
||
image: updated.image,
|
||
feedbackShowRealName: updated.feedbackShowRealName,
|
||
});
|
||
})
|
||
);
|
||
}
|