import { z } from 'zod'; import { CITY_CODES } from './cities.js'; import { CONTACT_SUBJECT_KEYS, CONTACT_MESSAGE_MAX, type ContactSubject } from './api-types.js'; import { CONSENT_VERSION } from './consent.js'; const hex64 = z.string().regex(/^[0-9a-f]{64}$/, '64 karakter küçük harf hex'); const name = z.string().min(1).max(100); export const uploadRecordSchema = z.object({ hash: hex64, firstName: name, lastName: name.nullable(), }); export const listUploadSchema = z.object({ hashVersion: z.number().int().positive(), self: z.object({ firstName: name, lastName: name, birthYear: z.number().int().min(1850).max(2030) }), records: z.array(uploadRecordSchema).min(1), }); /** Seçili kayıtları silme; gövde yoksa listenin tamamı silinir. */ export const listDeleteSchema = z.object({ ids: z.array(z.uuid()).min(1).max(1000) }); const flags = { shareAge: z.boolean(), sharePhoto: z.boolean(), shareCity: z.boolean() }; export const sharingSchema = z.object(flags); export const contactRequestSchema = z.object({ toUserId: z.string().min(1).max(64), nameConsent: z.literal(true), ...flags }); export const acceptRequestSchema = z.object({ nameConsent: z.literal(true), ...flags }); export const messageSchema = z.object({ text: z.string().trim().min(1).max(2000) }); export const profileUpdateSchema = z.object({ city: z.enum(CITY_CODES as [string, ...string[]]).nullable().optional(), nameHidden: z.boolean().optional(), ageHidden: z.boolean().optional(), photoHidden: z.boolean().optional(), cityHidden: z.boolean().optional(), }); export const photoReportSchema = z.object({ userId: z.string().min(1).max(64), note: z.string().trim().max(500).optional() }); export const resolveEventSchema = z.object({ resolution: z.enum(['reviewed', 'suspended', 'info_requested']), note: z.string().max(1000).optional(), }); export const adminEmailSchema = z.object({ text: z.string().trim().min(1).max(4000) }); export const adminNameSchema = z.object({ name: z.string().trim().min(1).max(100) }); export const deleteAccountSchema = z.object({ confirm: z.literal('HESABIMI SİL') }); /** * İletişim formu. Mesaj yalnızca düz metindir: satır sonu dışındaki kontrol karakterleri reddedilir, * içerik hiçbir yerde HTML olarak yorumlanmaz (e-posta ve moderasyon ekranında kaçışlanır). */ /** Satır sonu ve sekme dışındaki kontrol karakterleri düz metin sayılmaz. */ const isPlainText = (s: string) => ![...s].some((ch) => { const c = ch.codePointAt(0)!; return c === 0x7f || (c < 0x20 && c !== 0x09 && c !== 0x0a && c !== 0x0d); }); const plainText = (max: number) => z.string().trim().min(1).max(max).refine(isPlainText, 'Metin geçersiz karakter içeriyor'); /** Kayıt, giriş ve iletişim formlarının ortak e-posta kuralı; Better Auth `z.email()` ile aynı. */ export const emailSchema = z.email().max(200); export function isValidEmail(s: string): boolean { return emailSchema.safeParse(s.trim()).success; } export const contactSchema = z.object({ name: plainText(100), email: emailSchema, subject: z.enum(CONTACT_SUBJECT_KEYS as [ContactSubject, ...ContactSubject[]]), message: plainText(CONTACT_MESSAGE_MAX), }); /** Üç onayın da işaretli ve sürümün güncel olması zorunlu; eksikse kayıt/onay reddedilir. */ export const consentSchema = z.object({ version: z.literal(CONSENT_VERSION), privacy: z.literal(true), transfer: z.literal(true), terms: z.literal(true), }); export const clientErrorSchema = z.object({ kind: z.enum(['unwrap', 'parse', 'hash', 'upload', 'other']), message: z.string().max(2000), stack: z.string().max(2000).optional(), userAgent: z.string().max(500), platform: z.string().max(100).optional(), deviceMemory: z.number().optional(), cores: z.number().int().optional(), wasmSupported: z.boolean().optional(), hashVersion: z.number().int().optional(), hashParams: z.record(z.string(), z.number()).optional(), context: z.record(z.string(), z.union([z.string().max(500), z.number(), z.boolean()])).optional(), });