## Nickname-Validation + Duplicate-Check
Bug-Prevention: User konnte einen bereits vergebenen Nickname setzen, was
zu Verwirrung führte (zwei User mit selbem Alias). + Profanity-Filter.
Backend:
- GET /api/profile/check-nickname?nickname=X — returns {available, reason?}
reasons: 'too_short' | 'too_long' | 'profanity' | 'taken'
- Min 3, max 32 chars
- Profanity-Set (hardcoded, ~20 Wörter DE/EN — slurs + bot-impersonation
wie "admin", "lyra", etc.)
- Case-insensitive lookup, ignoriert eigenen Nickname (= behalten ok)
- Soft-deleted Profile sind ausgeschlossen
Frontend:
- NicknameSlide refactored mit Live-Debounce (450ms)
- Race-guard via checkSeqRef damit veraltete Antworten verworfen werden
- Visueller Feedback: Border-Color (success/error/transparent), Status-
Icon im Input (hourglass/checkmark/X), inline Error-Text statt Alert
- Save-Button disabled wenn invalid
- Network-Error: fail-soft, lass Server-Side bei Save validieren
## DiGA-Code Auto-Format
Live-Format-Mask: User tippt "REBREAKTEST001" → wird zu "REBREAK-TEST-001"
beim Tippen. Strip-then-segment Logik:
1. Alles außer A-Z0-9 entfernen
2. Erste 7 chars = "REBREAK", Rest in 4+restliche Blöcke
Liberal — erlaubt User dashes händisch zu setzen (wird neu segmentiert).
## DoneSlide Confetti + FAQ
- Confetti-Overlay mit 22 Partikeln, gestaffelt 40ms, native-driver Animation
(translateY + drift + rotate + opacity fade). One-shot beim Mount.
- Inline Top-5-FAQ Accordion unter dem Checkmark-Hero. Tap auf row → expand
+ zeige Antwort. Nutzt existing help.faq_q1..q5 + .faq_a1..a5 locale keys.
## Lyra Voice-Review (Agent)
lyra-persona Agent hat alle Lyra-Speech-Texte in 4 Sprachen reviewed:
- Welcome entstigmatisiert (kein "Glücksspiel"-Trigger im First-Touch)
- Plan vermenschlicht (Erklärungs- statt Verkaufs-Ton)
- DiGA-Choice sanfter (Geschenk-Frame statt Zugangs-Frame)
- protection_lock parallelisiert mit "blaue Falle"-Warnung
- FR/AR Stilglättung (Lyra-Femininum konsistent, AR Frage-Forms)
## Locale-Additions
- onboarding.nickname.error_{too_short, too_long, profanity, taken} × 4 langs
- onboarding.done.faq_section_title × 4 langs
- Lyra-bodies × 4 langs (vom Agent getuned)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
84 lines
2.2 KiB
TypeScript
84 lines
2.2 KiB
TypeScript
import { usePrisma } from "../../utils/prisma";
|
|
|
|
/**
|
|
* GET /api/profile/check-nickname?nickname=foo
|
|
*
|
|
* Validiert ob ein Nickname für den User OK ist:
|
|
* - Min 3 Zeichen
|
|
* - Max 32 Zeichen
|
|
* - Nicht in Profanity-Blocklist (kleines hartcodiertes Set)
|
|
* - Nicht von einem anderen User belegt (case-insensitive)
|
|
*
|
|
* Returns: { available: boolean, reason?: 'too_short' | 'too_long' | 'profanity' | 'taken' }
|
|
*
|
|
* Genutzt von der Nickname-Slide im Onboarding mit ~500ms Debounce um
|
|
* Live-Feedback zu geben. Idempotent + günstig (1 SELECT auf einen Index).
|
|
*/
|
|
const PROFANITY_BLOCKLIST: ReadonlySet<string> = new Set([
|
|
// Minimal-Set DE/EN — slurs + bot-impersonation. Erweiterbar; lib-frei
|
|
// damit Bundle-Size klein bleibt.
|
|
"admin",
|
|
"administrator",
|
|
"rebreak",
|
|
"lyra",
|
|
"support",
|
|
"moderator",
|
|
"mod",
|
|
"system",
|
|
"root",
|
|
"nigger",
|
|
"nazi",
|
|
"fuck",
|
|
"shit",
|
|
"fotze",
|
|
"hure",
|
|
"schwuchtel",
|
|
"fag",
|
|
"bitch",
|
|
"cunt",
|
|
"arsch",
|
|
"wichser",
|
|
]);
|
|
|
|
function isProfanity(nickname: string): boolean {
|
|
const lower = nickname.toLowerCase().trim();
|
|
if (PROFANITY_BLOCKLIST.has(lower)) return true;
|
|
for (const word of PROFANITY_BLOCKLIST) {
|
|
if (lower.includes(word)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
const user = await requireUser(event);
|
|
const query = getQuery(event);
|
|
const raw = String(query.nickname ?? "").trim();
|
|
|
|
if (raw.length < 3) {
|
|
return { success: true, data: { available: false, reason: "too_short" } };
|
|
}
|
|
if (raw.length > 32) {
|
|
return { success: true, data: { available: false, reason: "too_long" } };
|
|
}
|
|
if (isProfanity(raw)) {
|
|
return { success: true, data: { available: false, reason: "profanity" } };
|
|
}
|
|
|
|
// Case-insensitive lookup. Eigener Nickname (= aktueller User) ist OK
|
|
// — sonst kann User seinen eigenen Namen nicht "behalten".
|
|
const db = usePrisma();
|
|
const existing = await db.profile.findFirst({
|
|
where: {
|
|
nickname: { equals: raw, mode: "insensitive" },
|
|
id: { not: user.id },
|
|
deletedAt: null,
|
|
},
|
|
select: { id: true },
|
|
});
|
|
|
|
if (existing) {
|
|
return { success: true, data: { available: false, reason: "taken" } };
|
|
}
|
|
return { success: true, data: { available: true } };
|
|
});
|