managarten/services/mana-auth/src/routes/me.ts
Till JS 941df57f77 feat(feedback): rename community-identity columns + settings-section
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>
2026-04-28 17:09:58 +02:00

123 lines
4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 180 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,
});
})
);
}