export interface UploadRecord { hash: string; firstName: string; lastName: string | null; } export interface ListUploadBody { hashVersion: number; self: { firstName: string; lastName: string; birthYear: number }; records: UploadRecord[]; } /** suspended: yükleme kabul edildi ama şüpheli işlem nedeniyle hesap incelemeye alındı. */ export interface ListUploadResult { recordCount: number; newMatches: number; suspended: boolean; } export interface QuotaResult { remaining: number; resetsAt: string | null; } /** Sunucuda tutulan bir soy kaydı. Kayıt yeri ve cilt-hane-birey yalnızca hash olarak saklandığı için dönmez. */ export interface RecordView { id: string; firstName: string; lastName: string | null; createdAt: string; } export interface ListDeleteBody { ids: string[]; } export interface CommonRecord { firstName: string; lastName: string | null; } /** rejected: bana gelen talebi reddettim; yalnızca bende görünür, talep edene sızmaz. */ export type ContactStatus = 'none' | 'sent' | 'incoming' | 'rejected' | 'connected'; export interface MatchCard { userId: string; displayName: string | null; // null => "Gizli Kullanıcı" age: number | null; city: string | null; photoUrl: string | null; commonRecords: CommonRecord[]; firstSeenAt: string; contactStatus: ContactStatus; contactId: string | null; // bağlantı varsa incomingRequestId: string | null; // bana gelen bekleyen veya reddettiğim talep varsa incomingAttemptNo: number | null; // 2 => karşı tarafın son hakkı, tekrar talep gönderemez } export type ApiErrorCode = | 'UNAUTHORIZED' | 'SUSPENDED' | 'QUOTA_EXCEEDED' | 'INVALID_BODY' | 'HASH_VERSION_UNSUPPORTED' | 'RATE_LIMITED' | 'NOT_FOUND' | 'CONFLICT' | 'INTERNAL' | 'UNDERAGE' | 'CONSENT_REQUIRED'; // --- İletişim (Plan 2) --- export const DAILY_MESSAGE_LIMIT = 10; export interface ShareFlags { shareAge: boolean; sharePhoto: boolean; shareCity: boolean; } export interface ContactRequestBody extends ShareFlags { toUserId: string; nameConsent: true; } export interface AcceptRequestBody extends ShareFlags { nameConsent: true; } export interface MessageBody { text: string; } export interface IncomingRequest { id: string; requesterId: string; requesterName: string; attemptNo: number; createdAt: string; status: 'pending' | 'rejected'; } export interface ContactView { id: string; otherUserId: string; otherName: string; establishedAt: string; myShares: ShareFlags; messagesLeftToday: number; } export interface ApiErrorBody { code: ApiErrorCode; message: string; details?: Record; } // --- Profil, moderasyon, hesap (Plan 3) --- export interface ProfileView { name: string; email: string; birthYear: number | null; city: string | null; hasPhoto: boolean; nameHidden: boolean; ageHidden: boolean; photoHidden: boolean; cityHidden: boolean; hasList: boolean; } export interface ProfileUpdateBody { city?: string | null; nameHidden?: boolean; ageHidden?: boolean; photoHidden?: boolean; cityHidden?: boolean; } export interface PhotoReportBody { userId: string; note?: string; } export type ModerationKind = 'name_mismatch' | 'year_mismatch' | 'duplicate_document' | 'photo_report'; export type ModerationResolution = 'reviewed' | 'suspended' | 'info_requested'; export interface ModerationEventView { id: string; userId: string; userName: string; userEmail: string; userStatus: 'active' | 'suspended'; kind: ModerationKind; details: Record; createdAt: string; reviewedAt: string | null; resolution: ModerationResolution | null; reviewerNote: string | null; } export interface ResolveEventBody { resolution: ModerationResolution; note?: string; } export interface AdminEmailBody { text: string; } export interface AdminNameBody { name: string; } export interface AdminStats { totalUsers: number; pendingEvents: number; suspendedUsers: number; outboxPending: number; outboxFailed: number; clientErrors24h: number; pendingMessages: number; calibrations: number; } /** Moderasyon panelindeki kullanıcı listesi satırı. */ export interface AdminUserView { id: string; name: string; email: string; status: 'active' | 'suspended'; role: 'user' | 'admin'; /** Fotoğraf var mı; yoksa arayüz fotoğraf ucunu hiç çağırmaz. */ hasPhoto: boolean; emailVerified: boolean; createdAt: string; } /** Kullanıcı listesinde sayfa başına satır. */ export const ADMIN_USER_PAGE_SIZE = 25; export type AdminUserSort = 'created' | 'name' | 'email' | 'status'; export type AdminUserStatusFilter = 'all' | 'active' | 'suspended'; export const ADMIN_USER_SORTS: readonly AdminUserSort[] = ['created', 'name', 'email', 'status']; export const ADMIN_USER_STATUS_FILTERS: readonly AdminUserStatusFilter[] = ['all', 'active', 'suspended']; /** Sayfalı kullanıcı listesi. `total` süzgece uyan toplam; sayfalama bununla çizilir. */ export interface AdminUserList { users: AdminUserView[]; total: number; /** 1 tabanlı. */ page: number; pageSize: number; } export const EMAIL_KINDS = { verify: 'E-posta doğrulama', reset: 'Şifre sıfırlama', match: 'Yeni eşleşme', request: 'İletişim talebi', request_final: 'İletişim talebi (son)', accepted: 'Talep kabul edildi', message: 'Mesaj', moderation: 'Moderasyon bilgisi', account_deleted: 'Hesap silindi', contact: 'İletişim formu', } as const; export type EmailKind = keyof typeof EMAIL_KINDS; /** pending: henüz gönderilmemiş ve bırakılmamış; failed: beş denemede gönderilemeyip bırakılmış. */ export type AdminOutboxStatus = 'pending' | 'failed'; /** * Panelde görünen e-posta kuyruğu satırı. Payload dönmez: doğrulama ve şifre * sıfırlama bağlantıları jeton taşır, mesaj gövdesi de özeldir. */ export interface AdminOutboxEmailView { id: string; toEmail: string; toUserId: string | null; kind: EmailKind; attempts: number; createdAt: string; /** Bekleyen satırda sıradaki deneme zamanı; bırakılmış satırda son denemeninki. */ nextAttemptAt: string; /** Yalnızca bırakılmış satırlarda dolu. */ failedAt: string | null; /** Son denemenin hata metni; hiç denenmemiş ya da sürüm öncesi satırlarda boş olabilir. */ lastError: string | null; /** * Bu satırdan sonra aynı kişi için aynı türde üretilmiş yeni bir satırın durumu: * `queued` henüz kuyrukta, `sent` gönderildi ve (doğrulamada) bağlantısı henüz geçerli, * `null` yok. "Tekrar sıraya al" düğmesi buna bakar: gönderim anlık olduğu için * "bekleyen var mı" sorusu yetmiyor, basışın izi ancak böyle görünüyor. */ newerForUser: 'queued' | 'sent' | null; } /** Moderasyon panelinde bir kullanıcının soy kayıtları. Salt okunur; adres özetleri dönmez. */ export const ADMIN_RECORD_LIMIT = 500; export interface AdminUserRecords { records: RecordView[]; /** Kullanıcının toplam kayıt sayısı; `records` en fazla ADMIN_RECORD_LIMIT satır taşır. */ total: number; } export interface ClientErrorView { id: string; createdAt: string; kind: ClientErrorKind; message: string; userAgent: string; platform: string | null; context: Record | null; } export interface DeleteAccountBody { confirm: 'HESABIMI SİL'; } // --- İletişim formu (footer) --- export const CONTACT_SUBJECTS = { genel: 'Genel soru', hesap: 'Hesabımla ilgili', gizlilik: 'Gizlilik ve veri talebi', hata: 'Hata bildirimi', kotuye_kullanim: 'Kötüye kullanım bildirimi', oneri: 'Öneri', diger: 'Diğer', } as const; export type ContactSubject = keyof typeof CONTACT_SUBJECTS; export const CONTACT_SUBJECT_KEYS = Object.keys(CONTACT_SUBJECTS) as ContactSubject[]; export const CONTACT_MESSAGE_MAX = 4000; export interface ContactBody { name: string; email: string; subject: ContactSubject; message: string; } export interface ContactMessageView { id: string; name: string; email: string; subject: ContactSubject; message: string; userId: string | null; createdAt: string; handledAt: string | null; } export type ClientErrorKind = 'unwrap' | 'parse' | 'hash' | 'upload' | 'other'; /** * Kalibrasyon ölçümleri istemci hatası tablosunda taşınır ama hata değildir. * Hem sunucudaki sayaçlar hem moderasyon panelindeki ayrım bu listeyi kullanır; * iki yerde ayrı tanımlanırsa kartlar ile listeler birbirini tutmaz. */ export const CALIBRATION_MESSAGES = ['calibration', 'hash-timing'] as const; export const isCalibrationRecord = (message: string): boolean => (CALIBRATION_MESSAGES as readonly string[]).includes(message); export interface ClientErrorReport { kind: ClientErrorKind; message: string; stack?: string; userAgent: string; platform?: string; deviceMemory?: number; cores?: number; wasmSupported?: boolean; hashVersion?: number; hashParams?: Record; context?: Record; }