mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21:09 +02:00
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>
This commit is contained in:
parent
6f83fba66a
commit
941df57f77
16 changed files with 544 additions and 209 deletions
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Read-only cross-schema view of auth.users for the public-community
|
||||
* Read-only cross-schema view of auth.users for the public-feedback
|
||||
* hub. mana-auth owns the table; we JOIN it from mana-analytics to
|
||||
* enrich feed responses with the post-author's real-name opt-in and
|
||||
* karma score. We never INSERT/UPDATE/DELETE here — that's
|
||||
|
|
@ -17,6 +17,6 @@ const authSchema = pgSchema('auth');
|
|||
export const authUsers = authSchema.table('users', {
|
||||
id: text('id').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
communityShowRealName: boolean('community_show_real_name').default(false).notNull(),
|
||||
communityKarma: integer('community_karma').default(0).notNull(),
|
||||
feedbackShowRealName: boolean('feedback_show_real_name').default(false).notNull(),
|
||||
feedbackKarma: integer('feedback_karma').default(0).notNull(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* Integration tests for the cross-schema karma flow.
|
||||
*
|
||||
* Karma lives on auth.users.community_karma; mana-analytics increments
|
||||
* Karma lives on auth.users.feedback_karma; mana-analytics increments
|
||||
* it inside toggleReaction. Tests verify the SQL path, the self-react
|
||||
* skip, and the floor-at-zero clamp.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Privacy-boundary tests für die `redact()`-Funktion.
|
||||
*
|
||||
* Kritisch: anonymous public endpoint darf NIE einen Klarnamen
|
||||
* ausliefern, auch wenn der User-Account `communityShowRealName=true`
|
||||
* ausliefern, auch wenn der User-Account `feedbackShowRealName=true`
|
||||
* gesetzt hat. Diese Tests sind das Sicherheitsnetz für die ›Public
|
||||
* bleibt anonym‹-Garantie der Community-Surface.
|
||||
*/
|
||||
|
|
@ -35,14 +35,14 @@ const baseFeedback = {
|
|||
|
||||
const optedInAuthor = {
|
||||
name: 'Till Schäfer',
|
||||
communityShowRealName: true,
|
||||
communityKarma: 47,
|
||||
feedbackShowRealName: true,
|
||||
feedbackKarma: 47,
|
||||
};
|
||||
|
||||
const optedOutAuthor = {
|
||||
name: 'Till Schäfer',
|
||||
communityShowRealName: false,
|
||||
communityKarma: 47,
|
||||
feedbackShowRealName: false,
|
||||
feedbackKarma: 47,
|
||||
};
|
||||
|
||||
describe('redact (privacy-boundary)', () => {
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ export type PublicFeedbackItem = {
|
|||
/** Author's community karma (public, drives tier-badge). */
|
||||
karma: number;
|
||||
/** Real name, only present when:
|
||||
* - the post-author opted in via communityShowRealName=true, AND
|
||||
* - the post-author opted in via feedbackShowRealName=true, AND
|
||||
* - the response is going to an authenticated caller (the
|
||||
* anonymous /public endpoint always strips this).
|
||||
*/
|
||||
|
|
@ -88,8 +88,8 @@ export type PublicFeedbackItem = {
|
|||
type FeedbackRow = typeof userFeedback.$inferSelect;
|
||||
type AuthUserRow = {
|
||||
name: string;
|
||||
communityShowRealName: boolean;
|
||||
communityKarma: number;
|
||||
feedbackShowRealName: boolean;
|
||||
feedbackKarma: number;
|
||||
} | null;
|
||||
|
||||
export class FeedbackService {
|
||||
|
|
@ -301,7 +301,7 @@ export class FeedbackService {
|
|||
|
||||
const items = rows.map((r) => redact(r.feedback, r.author, { includeRealName: false }));
|
||||
const displayName = items[0]?.displayName ?? null;
|
||||
const karma = rows[0]?.author?.communityKarma ?? 0;
|
||||
const karma = rows[0]?.author?.feedbackKarma ?? 0;
|
||||
|
||||
return { displayHash, displayName, karma, items };
|
||||
}
|
||||
|
|
@ -327,8 +327,8 @@ export class FeedbackService {
|
|||
private authorSelection() {
|
||||
return {
|
||||
name: authUsers.name,
|
||||
communityShowRealName: authUsers.communityShowRealName,
|
||||
communityKarma: authUsers.communityKarma,
|
||||
feedbackShowRealName: authUsers.feedbackShowRealName,
|
||||
feedbackKarma: authUsers.feedbackKarma,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -442,7 +442,7 @@ export class FeedbackService {
|
|||
await this.db
|
||||
.update(authUsers)
|
||||
.set({
|
||||
communityKarma: sql`GREATEST(${authUsers.communityKarma} + ${delta}, 0)`,
|
||||
feedbackKarma: sql`GREATEST(${authUsers.feedbackKarma} + ${delta}, 0)`,
|
||||
})
|
||||
.where(eq(authUsers.id, item.authorId));
|
||||
}
|
||||
|
|
@ -767,9 +767,9 @@ function redact(
|
|||
adminResponse: row.adminResponse,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
karma: author?.communityKarma ?? 0,
|
||||
karma: author?.feedbackKarma ?? 0,
|
||||
};
|
||||
if (includeReal && author?.communityShowRealName && author.name) {
|
||||
if (includeReal && author?.feedbackShowRealName && author.name) {
|
||||
item.realName = author.name;
|
||||
}
|
||||
return item;
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ let seededIds = new Set<string>();
|
|||
*/
|
||||
export async function seedUser(
|
||||
db: TestDb,
|
||||
overrides: Partial<{ name: string; communityShowRealName: boolean; communityKarma: number }> = {}
|
||||
overrides: Partial<{ name: string; feedbackShowRealName: boolean; feedbackKarma: number }> = {}
|
||||
): Promise<SeededUser> {
|
||||
const id = `test-${randomUUID()}`;
|
||||
const email = `${id}@test.local`;
|
||||
|
|
@ -57,13 +57,13 @@ export async function seedUser(
|
|||
// model only declares the columns mana-analytics READS — auth.users
|
||||
// has additional NOT NULL columns (email, etc.) we'd otherwise miss.
|
||||
await db.execute(sql`
|
||||
INSERT INTO auth.users (id, email, name, community_show_real_name, community_karma)
|
||||
INSERT INTO auth.users (id, email, name, feedback_show_real_name, feedback_karma)
|
||||
VALUES (
|
||||
${id},
|
||||
${email},
|
||||
${name},
|
||||
${overrides.communityShowRealName ?? false},
|
||||
${overrides.communityKarma ?? 0}
|
||||
${overrides.feedbackShowRealName ?? false},
|
||||
${overrides.feedbackKarma ?? 0}
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
`);
|
||||
|
|
@ -71,10 +71,10 @@ export async function seedUser(
|
|||
return { id, email, name };
|
||||
}
|
||||
|
||||
/** Read auth.users.community_karma for a test user. */
|
||||
/** Read auth.users.feedback_karma for a test user. */
|
||||
export async function getKarma(db: TestDb, userId: string): Promise<number> {
|
||||
const [row] = await db
|
||||
.select({ karma: authUsers.communityKarma })
|
||||
.select({ karma: authUsers.feedbackKarma })
|
||||
.from(authUsers)
|
||||
.where(eq(authUsers.id, userId))
|
||||
.limit(1);
|
||||
|
|
|
|||
17
services/mana-auth/sql/009_rename_community_to_feedback.sql
Normal file
17
services/mana-auth/sql/009_rename_community_to_feedback.sql
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
-- 009_rename_community_to_feedback.sql
|
||||
-- Renames the two identity-opt-in columns on auth.users to match the
|
||||
-- "feedback" brand the public hub now carries. Was originally added
|
||||
-- in 008_community_identity.sql.
|
||||
--
|
||||
-- Apply with:
|
||||
-- psql "$DATABASE_URL" -f sql/009_rename_community_to_feedback.sql
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE auth.users
|
||||
RENAME COLUMN community_show_real_name TO feedback_show_real_name;
|
||||
|
||||
ALTER TABLE auth.users
|
||||
RENAME COLUMN community_karma TO feedback_karma;
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -53,14 +53,14 @@ export const users = authSchema.table('users', {
|
|||
// → Look → Templates). The flow is skippable, but even a skip sets
|
||||
// this timestamp so we don't re-prompt. See docs/plans/onboarding-flow.md.
|
||||
onboardingCompletedAt: timestamp('onboarding_completed_at', { withTimezone: true }),
|
||||
// Community-Hub identity opt-ins (Phase 3.C of feedback-rewards-and-identity).
|
||||
// Public-feedback identity opt-ins (Phase 3.C of feedback-rewards-and-identity).
|
||||
// Off by default — users stay anonymous as their tier-pseudonym ("Wachsame
|
||||
// Eule #4528"). Opt-in shows the real `name` next to the pseudonym in the
|
||||
// auth-required community feed only; the public-mirror NEVER exposes it.
|
||||
communityShowRealName: boolean('community_show_real_name').default(false).notNull(),
|
||||
// auth-required feedback feed only; the public-mirror NEVER exposes it.
|
||||
feedbackShowRealName: boolean('feedback_show_real_name').default(false).notNull(),
|
||||
// Karma += 1 per reaction received from another user, decremented on unreact.
|
||||
// Drives the public Bronze/Silver/Gold/Platinum-Eulen tier badge.
|
||||
communityKarma: integer('community_karma').default(0).notNull(),
|
||||
feedbackKarma: integer('feedback_karma').default(0).notNull(),
|
||||
});
|
||||
|
||||
// Sessions table (Better Auth schema)
|
||||
|
|
|
|||
|
|
@ -72,13 +72,13 @@ export function createMeRoutes(userDataService: UserDataService, db: Database) {
|
|||
const body = (await c.req.json().catch(() => ({}))) as {
|
||||
name?: unknown;
|
||||
image?: unknown;
|
||||
communityShowRealName?: unknown;
|
||||
feedbackShowRealName?: unknown;
|
||||
};
|
||||
|
||||
const patch: {
|
||||
name?: string;
|
||||
image?: string;
|
||||
communityShowRealName?: boolean;
|
||||
feedbackShowRealName?: boolean;
|
||||
updatedAt: Date;
|
||||
} = {
|
||||
updatedAt: new Date(),
|
||||
|
|
@ -93,11 +93,11 @@ export function createMeRoutes(userDataService: UserDataService, db: Database) {
|
|||
if (typeof body.image === 'string') {
|
||||
patch.image = body.image;
|
||||
}
|
||||
if (typeof body.communityShowRealName === 'boolean') {
|
||||
patch.communityShowRealName = body.communityShowRealName;
|
||||
if (typeof body.feedbackShowRealName === 'boolean') {
|
||||
patch.feedbackShowRealName = body.feedbackShowRealName;
|
||||
}
|
||||
|
||||
if (!('name' in patch) && !('image' in patch) && !('communityShowRealName' in patch)) {
|
||||
if (!('name' in patch) && !('image' in patch) && !('feedbackShowRealName' in patch)) {
|
||||
return c.json({ error: 'no fields to update' }, 400);
|
||||
}
|
||||
|
||||
|
|
@ -109,14 +109,14 @@ export function createMeRoutes(userDataService: UserDataService, db: Database) {
|
|||
id: users.id,
|
||||
name: users.name,
|
||||
image: users.image,
|
||||
communityShowRealName: users.communityShowRealName,
|
||||
feedbackShowRealName: users.feedbackShowRealName,
|
||||
});
|
||||
|
||||
if (!updated) return c.json({ error: 'User not found' }, 404);
|
||||
return c.json({
|
||||
name: updated.name,
|
||||
image: updated.image,
|
||||
communityShowRealName: updated.communityShowRealName,
|
||||
feedbackShowRealName: updated.feedbackShowRealName,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue