'use server' import { randomBytes, createHash } from 'crypto' import { revalidatePath } from 'next/cache' import { redirect } from 'next/navigation' import { auth } from '@/lib/auth' import { supabaseAdmin, listAuthUsers } from '@/lib/supabaseAdmin' import { setProfileAccess } from '../users/actions' async function requireAdmin() { const session = await auth() if (!session || (session.user as any)?.role !== 'ADMIN') { throw new Error('Yetkisiz erişim') } } function toIsoOrNull(dateInput: FormDataEntryValue | null): string | null { const value = String(dateInput || '').trim() if (!value) return null const d = new Date(`${value}T23:59:59.999Z`) if (isNaN(d.getTime())) return null return d.toISOString() } // null dönerse "e-posta girildi ama eşleşen kullanıcı yok" (hata), undefined dönerse "e-posta boş" (dokunma) async function resolveCustomerIdByEmail(email: string): Promise { const trimmed = email.trim().toLowerCase() if (!trimmed) return undefined const authUsers = await listAuthUsers() const match = [...authUsers.values()].find((u) => (u.email || '').toLowerCase() === trimmed) return match?.id ?? null } // profiles.license_status, licenses tablosundan ayrı bir alan ama artık uygulamanın // TEK gerçek erişim kapısı (bkz. users/actions.ts::setProfileAccess) — buradan bir // lisans kaydedildiğinde bağlı müşterinin profilini de aynı ortak fonksiyonla senkron // tutuyoruz, yoksa "lisansı aktif yaptım ama uygulama hâlâ süresi dolmuş diyor" durumu oluşuyor. async function syncProfileLicenseStatus(customerId: string, licenseStatus: string, expiresAt: string | null) { const profileStatus = licenseStatus === 'active' ? 'active' : 'expired' // 'expired' | 'revoked' -> ikisi de erişimi kapatmalı await setProfileAccess(customerId, profileStatus, expiresAt) } export async function updateLicenseAction(formData: FormData) { await requireAdmin() const id = String(formData.get('id') || '') if (!id) throw new Error('Lisans id eksik') const plan = String(formData.get('plan') || '').trim() const status = String(formData.get('status') || '').trim() const maxDevicesRaw = String(formData.get('max_devices') || '').trim() const maxDevices = maxDevicesRaw ? parseInt(maxDevicesRaw, 10) : null const expiresAt = toIsoOrNull(formData.get('expires_at')) const email = String(formData.get('customer_email') || '') const update: Record = {} if (plan) update.plan = plan if (status) update.status = status if (maxDevices && !isNaN(maxDevices)) update.max_devices = maxDevices update.expires_at = expiresAt const customerId = await resolveCustomerIdByEmail(email) if (customerId === null) { redirect(`/admin/licenses/${id}?error=${encodeURIComponent('Bu e-posta ile kayıtlı kullanıcı bulunamadı')}`) } update.customer_id = customerId === undefined ? null : customerId const { error } = await supabaseAdmin.from('licenses').update(update).eq('id', id) if (error) { redirect(`/admin/licenses/${id}?error=${encodeURIComponent(error.message)}`) } const finalCustomerId = update.customer_id as string | null if (finalCustomerId) { await syncProfileLicenseStatus(finalCustomerId, status || 'active', expiresAt) } revalidatePath('/admin/licenses') revalidatePath(`/admin/licenses/${id}`) revalidatePath('/admin/users') redirect(`/admin/licenses/${id}?success=1`) } // XXXX-XXXX-XXXX-XXXX — 0/O/1/I hariç, elle okurken/yazarken karışmasın diye const KEY_CHARSET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' function generateLicenseKey(): string { const groups: string[] = [] for (let g = 0; g < 4; g++) { const bytes = randomBytes(4) let group = '' for (let i = 0; i < 4; i++) group += KEY_CHARSET[bytes[i] % KEY_CHARSET.length] groups.push(group) } return groups.join('-') } // license Edge Function'daki algoritmayla birebir aynı olmalı (supa.ayris.tech/functions/v1/license): // licenseKey.trim().toUpperCase() -> SHA-256 hex digest, salt yok. function hashLicenseKey(key: string): string { return createHash('sha256').update(key.trim().toUpperCase()).digest('hex') } export interface CreateLicenseState { error?: string result?: { id: string; licenseKey: string } } export async function createLicenseAction(_prevState: CreateLicenseState, formData: FormData): Promise { await requireAdmin() const plan = String(formData.get('plan') || 'professional').trim() || 'professional' const status = String(formData.get('status') || 'active').trim() || 'active' const maxDevicesRaw = String(formData.get('max_devices') || '2').trim() const maxDevices = parseInt(maxDevicesRaw, 10) || 2 const expiresAt = toIsoOrNull(formData.get('expires_at')) const email = String(formData.get('customer_email') || '') const customerId = await resolveCustomerIdByEmail(email) if (customerId === null) { return { error: 'Bu e-posta ile kayıtlı kullanıcı bulunamadı' } } let licenseKey = '' let licenseKeyHash = '' for (let attempt = 0; attempt < 5; attempt++) { licenseKey = generateLicenseKey() licenseKeyHash = hashLicenseKey(licenseKey) const { data: collision } = await supabaseAdmin.from('licenses').select('id').eq('license_key_hash', licenseKeyHash).maybeSingle() if (!collision) break } const { data, error } = await supabaseAdmin .from('licenses') .insert({ license_key_hash: licenseKeyHash, customer_id: customerId ?? null, plan, status, max_devices: maxDevices, expires_at: expiresAt, }) .select('id') .single() if (error || !data) { return { error: error?.message || 'Lisans oluşturulamadı' } } if (customerId) { await syncProfileLicenseStatus(customerId, status, expiresAt) } revalidatePath('/admin/licenses') revalidatePath('/admin/users') return { result: { id: data.id, licenseKey } } } export async function deleteLicenseAction(formData: FormData) { await requireAdmin() const id = String(formData.get('id') || '') if (!id) throw new Error('Lisans id eksik') await supabaseAdmin.from('license_activations').delete().eq('license_id', id) const { error } = await supabaseAdmin.from('licenses').delete().eq('id', id) if (error) { redirect(`/admin/licenses/${id}?error=${encodeURIComponent(error.message)}`) } revalidatePath('/admin/licenses') redirect('/admin/licenses?deleted=1') } export async function removeActivationAction(formData: FormData) { await requireAdmin() const activationId = String(formData.get('activation_id') || '') const licenseId = String(formData.get('license_id') || '') if (!activationId || !licenseId) throw new Error('Eksik parametre') await supabaseAdmin.from('license_activations').delete().eq('id', activationId) revalidatePath(`/admin/licenses/${licenseId}`) redirect(`/admin/licenses/${licenseId}?success=1`) }