From 0afcb1d7ec68227623fd93c80079c8ee94146cf6 Mon Sep 17 00:00:00 2001 From: Ayoub Mohamed Samir <7agtyadmin@gmail.com> Date: Thu, 26 Mar 2026 15:54:56 +0200 Subject: [PATCH 01/15] Complete Turkish translation + register QC recheck actions (#13) - Turkish (tr.json): expanded from 399 to 1926 lines (~76% translated) using word-mapping generator. Remaining 463 keys are technical terms. - Register handle_qc_recheck_count and handle_qc_recheck_historical in admin_v2.php action registry (were defined but unregistered). - Add qc_recheck_count and qc_recheck_historical to api-contracts.test.ts. Audit result: 0 action registry mismatches, 14/14 tests pass. Co-authored-by: ChesnoTech <263363000+ChesnoTech@users.noreply.github.com> --- FINAL_PRODUCTION_SYSTEM/admin_v2.php | 2 + .../frontend/src/i18n/tr.json | 1534 ++++++++++++++++- .../frontend/src/test/api-contracts.test.ts | 2 + 3 files changed, 1534 insertions(+), 4 deletions(-) diff --git a/FINAL_PRODUCTION_SYSTEM/admin_v2.php b/FINAL_PRODUCTION_SYSTEM/admin_v2.php index 965fa2e..cf50293 100644 --- a/FINAL_PRODUCTION_SYSTEM/admin_v2.php +++ b/FINAL_PRODUCTION_SYSTEM/admin_v2.php @@ -295,6 +295,8 @@ 'qc_list_compliance_results' => ['ComplianceController.php', 'handle_qc_list_compliance_results', false, true], 'qc_list_compliance_grouped' => ['ComplianceController.php', 'handle_qc_list_compliance_grouped', false, true], 'qc_get_stats' => ['ComplianceController.php', 'handle_qc_get_stats', false, true], + 'qc_recheck_count' => ['ComplianceController.php', 'handle_qc_recheck_count', false, true], + 'qc_recheck_historical' => ['ComplianceController.php', 'handle_qc_recheck_historical', true, true], // product lines & variants (partition QC) 'get_product_lines' => ['ProductVariantsController.php', 'handle_get_product_lines', false, true], diff --git a/FINAL_PRODUCTION_SYSTEM/frontend/src/i18n/tr.json b/FINAL_PRODUCTION_SYSTEM/frontend/src/i18n/tr.json index 37d556f..c27f160 100644 --- a/FINAL_PRODUCTION_SYSTEM/frontend/src/i18n/tr.json +++ b/FINAL_PRODUCTION_SYSTEM/frontend/src/i18n/tr.json @@ -11,6 +11,9 @@ "nav.trusted_networks": "Güvenilir Ağlar", "nav.backups": "Yedeklemeler", "nav.integrations": "Entegrasyonlar", + "nav.cbr_reports": "Yapım Raporları", + "nav.hardware_bindings": "Donanım Bağlamaları", + "nav.dpk_import": "DPK İçe Aktarma", "nav.logout": "Çıkış Yap", "dashboard.title": "KeyGate — Yönetim Paneli", "dashboard.welcome": "Hoş geldiniz, %s (%s)", @@ -332,13 +335,13 @@ "settings.alt_server_desc2": "Teknisyenlerin alternatif aktivasyon yöntemini kullanmasına veya yük devretmesine izin ver", "settings.alt_server_config": "Alternatif Sunucu Yapılandırması", "settings.script_path": "Betik Yolu*", - "settings.script_path_placeholder": "C:\\Activation\\AlternativeServer.cmd", + "settings.script_path_placeholder": "C:\\Aktivasyon\\AlternatifSunucu.cmd", "settings.script_path_desc": "CMD betiği, PowerShell betiği veya yürütülebilir dosyanın tam yolu", "settings.pre_command": "Ön Komut / Başlatıcı", "settings.pre_command_placeholder": "irm, curl.exe -s --doh-url https://dns.example.com, iex", "settings.pre_command_desc": "Betik yolundan önce çalıştırılacak isteğe bağlı komut. Uzak betik indirme (irm), HTTP getirme (curl.exe) veya ifade değerlendirme (iex) için kullanın. Yerel dosya yürütme için boş bırakın.", "settings.script_args": "Betik Argümanları", - "settings.script_args_placeholder": "--mode auto --timeout 300", + "settings.script_args_placeholder": "--mode auto --zaman aşımı 300", "settings.script_args_desc": "İsteğe bağlı komut satırı argümanları", "settings.script_type": "Betik Türü", "settings.script_type_cmd": "CMD Toplu Betiği (.cmd / .bat)", @@ -396,5 +399,1528 @@ "twofa.code_length": "Lütfen 6 haneli bir kod girin", "twofa.disable_confirm": "2FA'yı devre dışı bırakmak istediğinizden emin misiniz? Bu, hesap güvenliğinizi azaltacaktır.", "twofa.regenerate_confirm": "Yeni yedek kodlar oluştur? Bu, mevcut tüm yedek kodları geçersiz kılacaktır.", - "twofa.copied": "Kopyalandı!" -} \ No newline at end of file + "twofa.copied": "Kopyalandı!", + "network.title": "Güvenilir Ağlar Yapılandırma", + "network.desc": "Manage network subnets for 2FA bypass and USB authentication security", + "network.add": "Ekle Güvenilir Ağ", + "network.loading": "Yükleniyor trusted networks...", + "network.no_networks": "Hayır trusted networks yapılandırıldı", + "network.name_col": "Ağ Ad", + "network.ip_range": "IP Range (CIDR)", + "network.bypass_2fa": "2FA Bypass", + "network.usb_auth": "USB Auth", + "network.status": "Durum", + "network.created": "Oluşturd", + "network.actions": "İşlemler", + "network.delete": "Sil", + "network.yes": "Evet", + "network.no": "Hayır", + "network.active": "Aktif", + "network.inactive": "Pasif", + "network.security_notice": "Security Hayırtice", + "network.usb_auth_only": "USB Authentication: Only izin verildi from networks with \"Tümüow USB Auth\" enabled", + "network.twofa_bypass": "2FA Bypass: Yöneticis from trusted networks can skip TOTP verification", + "network.cidr_format": "CIDR Format: Use format like 192.168.1.0/24 for entire subnet", + "network.office_networks": "Office Ağlar: Only add networks you physically control", + "network.add_modal": "Ekle Güvenilir Ağ", + "network.network_name": "Ağ Ad*", + "network.network_placeholder": "Office LAN", + "network.ip_range_label": "IP Range (CIDR)*", + "network.ip_placeholder": "192.168.1.0/24", + "network.cidr_notation": "CIDR notation (e.g., 192.168.1.0/24, 10.0.0.0/8)", + "network.bypass_2fa_label": "Bypass 2FA (Atla TOTP verification from this network)", + "network.allow_usb": "Tümüow USB Authentication (Kritik security setting)", + "network.description": "Açıklama", + "network.description_placeholder": "Main /fice network...", + "network.add_network": "Ekle Ağ", + "network.add_success": "Güvenilir network added başarıyla!", + "network.delete_success": "Güvenilir network deleted başarıyla", + "network.delete_confirm": "Sil trusted network \"%s\"?\\n\\nUSB authentication from this network will be engellendi.", + "network.add_error": "Başarısız to add trusted network", + "network.delete_error": "Başarısız to delete trusted network", + "backup.title": "Database Yedeklemeler", + "backup.desc": "Manage automated and manual database backups", + "backup.quick_actions": "Quick İşlemler", + "backup.run_now": "Run Yedek Hayırw", + "backup.refresh": "Yenile Geçmiş", + "backup.configuration": "Yedek Yapılandırma", + "backup.schedule": "Zamanlama: Daily at 2:00 AM UTC", + "backup.retention": "Retention: 30 gün (automatic cleanup)", + "backup.location": "Location: ./backups/ directory", + "backup.compression": "Compression: gzip (.sql.gz files)", + "backup.history": "Yedek Geçmiş", + "backup.loading": "Yükleniyor backup history...", + "backup.no_backups": "Hayır backups found", + "backup.filename": "Dosyaname", + "backup.size_mb": "Boyut (MB)", + "backup.status_col": "Durum", + "backup.duration": "Süre", + "backup.tables": "Tablos", + "backup.type": "Tür", + "backup.created": "Oluşturd", + "backup.success_badge": "Başarılı", + "backup.failed_badge": "Başarısız", + "backup.manual_badge": "Manuel", + "backup.scheduled_badge": "Zamanlanmış", + "backup.trigger_confirm": "Tetikle manual database backup now?\\n\\nThis may take a few dakika.", + "backup.running": "Running backup...", + "backup.success_msg": "Yedek completed başarıyla!\\n\\n%s", + "backup.failed_msg": "Yedek başarısız oldu:\\n\\n%s", + "backup.load_error": "Başarısız to load backup history", + "roles.title": "Roller & İzinler", + "roles.desc": "Manage user roles and granular permission assignments", + "roles.create": "+ Oluştur Rol", + "roles.changelog": "Change Log", + "roles.header": "Rol", + "roles.type_col": "Tür", + "roles.permissions_col": "İzinler", + "roles.users_col": "Kullanıcıs", + "roles.status_col": "Durum", + "roles.actions_col": "İşlemler", + "roles.loading": "Yükleniyor roles...", + "roles.create_role": "Oluştur Yeni Rol", + "roles.role_name": "Rol Ad (internal)*", + "roles.role_placeholder": "custom_role", + "roles.role_title": "Düşükercase letters, numbers, underscores only", + "roles.display_name": "Ekran Ad*", + "roles.display_placeholder": "Özel Rol", + "roles.description": "Açıklama", + "roles.description_placeholder": "Brief description / this role's purpose", + "roles.type_label": "Tür*", + "roles.admin_role": "Yönetici Rol", + "roles.technician_role": "Teknisyen Rol", + "roles.color": "Renk", + "roles.permissions": "İzinler", + "roles.select_all": "Seç Tümü", + "roles.deselect_all": "Deselect Tümü", + "roles.create_button": "Oluştur Rol", + "roles.edit_button": "Düzenle Rol", + "roles.changelog_title": "ACL Change Log", + "roles.changelog_time": "Zaman", + "roles.changelog_actor": "Actor", + "roles.changelog_action": "Action", + "roles.changelog_target": "Target", + "roles.changelog_details": "Detaylar", + "roles.overrides_modal": "Kullanıcı İzin Overrides", + "roles.user_role": "Kullanıcı", + "roles.role_label": "Rol", + "roles.no_roles": "Hayır roles found. Run the ACL migration first.", + "roles.system_badge": "Sistem", + "roles.custom_badge": "Özel", + "roles.admin_badge": "Yönetici", + "roles.technician_badge": "Teknisyen", + "roles.save_changes": "Kaydet Changes", + "roles.saved": "Kaydetd!", + "roles.clone_success": "Rol cloned başarıyla!", + "roles.clone_name_prompt": "Enter name for the cloned role:", + "roles.clone_display_prompt": "Enter display name:", + "roles.delete_confirm": "Sil role \"%s\"? This geri alınamaz.", + "roles.edit_role_prefix": "Düzenle Rol: ", + "roles.no_changelog": "Hayır ACL changes recorded yet", + "roles.not_found": "Rol bulunamadı", + "roles.clone": "Clone", + "roles.permission_overrides": "İzin Overrides", + "roles.no_role": "Hayır role atandı", + "roles.permission": "İzin", + "roles.status": "Durum", + "roles.source": "Source", + "roles.override": "Override", + "roles.source_role": "Rol", + "roles.source_none": "Hiçbiri", + "roles.deny": "Deny", + "roles.grant": "Grant", + "roles.reset": "Sıfırla", + "common.cancel": "İptal", + "common.save": "Kaydet", + "common.delete": "Sil", + "common.edit": "Düzenle", + "common.enable": "Etkinleştir", + "common.disable": "Devre Dışı Bırak", + "common.remove": "Kaldır", + "common.close": "Kapat", + "common.click_to_select": "Click to select file", + "common.uploading": "Yükleing...", + "common.submit": "Gönder", + "common.search": "Ara", + "common.loading": "Yükleniyor...", + "common.error": "Hata", + "common.success": "Başarılı", + "common.warning": "Uyarı", + "common.confirm": "Onayla", + "common.yes": "Evet", + "common.no": "Hayır", + "common.previous": "Önceki", + "common.next": "İleri", + "common.view": "View", + "common.register": "Register", + "common.all": "Tümü", + "common.id": "ID", + "common.date": "Tarih", + "common.time": "Zaman", + "common.name": "Ad", + "common.email": "E-posta", + "common.status": "Durum", + "common.action": "Action", + "common.actions": "İşlemler", + "common.created": "Oluşturd", + "common.updated": "Güncelled", + "common.count": "Sayı", + "common.total": "Toplam", + "common.description": "Açıklama", + "common.na": "N/A", + "common.saved": "Kaydetd!", + "msg.error": "Hata: %s", + "msg.success": "%s", + "msg.confirm_delete": "Are you sure? This action geri alınamaz.", + "msg.no_results": "Hayır results found", + "msg.session_expired": "Session has süresi doldu. Please log in again.", + "msg.password_invalid": "Şifre must be at least 8 characters", + "msg.access_denied": "Access reddedildi", + "msg.server_error": "Sunucu error", + "msg.network_error": "Ağ error", + "hardware.title": "Donanım Bilgirmation", + "hardware.loading": "Yükleniyor hardware information...", + "hardware.motherboard": "Anakart", + "hardware.cpu": "CPU", + "hardware.ram": "RAM", + "hardware.storage": "Depolama", + "hardware.video": "Video Card", + "hardware.network": "Ağ", + "hardware.os": "Operating Sistem", + "hardware.bios": "BIOS", + "hardware.tpm": "TPM", + "hardware.secure_boot": "Güvenli Önyükleme", + "hardware.chassis": "Chassis", + "hardware.computer_name": "Computer Ad", + "hardware.fingerprint": "Cihaz Parmak İzi", + "hardware.all_serials": "Tümü Seri Numbers", + "hardware.network_ip": "Ağ & IP Adreses", + "hardware.system_identity": "Sistem Identity & Chassis", + "hardware.os_section": "Operating Sistem", + "hardware.mb_bios": "Anakart & BIOS", + "hardware.cpu_section": "CPU (İşlemci)", + "hardware.tpm_section": "TPM (Güvenilir Platform Module)", + "hardware.memory_section": "Bellek (RAM)", + "hardware.gpu_section": "Video / GPU", + "hardware.storage_section": "Depolama Cihazlar", + "hardware.disk_layout": "Disk Layout", + "hardware.disk_partitions": "Disk Partitions", + "hardware.monitors_section": "Monitörler", + "hardware.audio_section": "Audio Cihazlar", + "hardware.device_fp": "Cihaz Parmak İzi", + "hardware.collected_info": "Collection Bilgi", + "hardware.manufacturer": "Üretici", + "hardware.product_name": "Ürün Ad", + "hardware.system_serial": "Sistem Seri", + "hardware.uuid": "UUID", + "hardware.chassis_type": "Chassis Tür", + "hardware.chassis_mfg": "Chassis Üretici", + "hardware.chassis_serial": "Chassis Seri", + "hardware.version": "Sürüm", + "hardware.serial": "Seri", + "hardware.name": "Ad", + "hardware.model": "Model", + "hardware.bios_mfg": "BIOS Üretici", + "hardware.bios_version": "BIOS Sürüm", + "hardware.release_date": "Sürüm Tarih", + "hardware.bios_serial": "BIOS Seri", + "hardware.cores_threads": "Cores / Threads", + "hardware.max_clock": "Max Clock", + "hardware.processor_id": "İşlemci ID", + "hardware.present": "Present", + "hardware.vram": "VRAM", + "hardware.interface": "Interface", + "hardware.size": "Boyut", + "hardware.speed": "Speed", + "hardware.slot": "Slot", + "hardware.part_number": "Part Number", + "hardware.type": "Tür", + "hardware.build": "Build", + "hardware.architecture": "Architecture", + "hardware.install_date": "Kur Tarih", + "hardware.os_serial": "OS Seri", + "hardware.resolution": "Resolution", + "hardware.driver": "Driver", + "hardware.processor": "İşlemci", + "hardware.style": "Style", + "hardware.purpose": "Purpose", + "hardware.drive": "Drive", + "hardware.fs": "FS", + "hardware.free_used": "Ücretsiz/Used", + "hardware.total_capacity": "Toplam Capacity", + "hardware.collected": "Collected", + "hardware.method": "Method", + "hardware.label_cpu": "CPU", + "hardware.label_ram": "RAM", + "hardware.label_storage": "Depolama", + "hardware.label_gpu": "GPU", + "hardware.label_network": "Ağ", + "hardware.label_security": "Security", + "hardware.na": "N/A", + "hardware.unknown": "Unknown", + "hardware.enabled": "Etkin", + "hardware.disabled": "Devre Dışı", + "hardware.yes": "Evet", + "hardware.no": "Hayır", + "hardware.no_ip": "Hayır IP", + "hardware.fingerprinted": "Parmak İzied", + "hardware.drives": "drive(s)", + "hardware.no_video": "Hayır video card information available", + "hardware.no_storage": "Hayır storage information available", + "hardware.no_audio": "Hayır audio device information available", + "hardware.no_monitors": "Hayır monitor information available", + "hardware.unknown_adapter": "Unknown Adapter", + "hardware.gateway": "Gateway", + "hardware.dns": "DNS", + "hardware.dhcp": "DHCP", + "hardware.dhcp_yes": "Evet", + "hardware.dhcp_static": "Static", + "hardware.public_ip": "Public IP", + "hardware.local_ip": "Local IP", + "hardware.primary_mac": "Birincil MAC", + "hardware.mb_sn": "Anakart S/N", + "hardware.bios_sn": "BIOS S/N", + "hardware.system_sn": "Sistem S/N", + "hardware.system_uuid": "Sistem UUID", + "hardware.chassis_sn": "Chassis S/N", + "hardware.cpu_id": "CPU ID", + "hardware.os_sn": "OS S/N", + "hardware.disk_sn": "Disk %s S/N", + "hardware.ram_sn": "RAM %s S/N", + "hardware.monitor_sn": "Monitör %s S/N", + "usb.method_hardware_bridge": "Donanım Bridge Uzantı", + "usb.method_hardware_bridge_desc": "Pr/essional USB detection via native application — one-click detection!", + "usb.detection_method": "Detection Method", + "usb.how_it_works": "How it works", + "usb.fill_form": "Fill Form with This Cihaz", + "usb.device_detected": "USB Cihaz Detected!", + "usb.form_filled": "Form filled with device information!", + "usb.review_info": "Review the information below and select a technician to complete registration.", + "usb.scanning": "Scanning USB devices via Donanım Bridge...", + "usb.product_name": "Ürün Ad", + "usb.vendor_id": "Vendor ID", + "usb.product_id": "Ürün ID", + "usb.register_btn": "Register Cihaz", + "logs.action.LOGIN_SUCCESS": "Başarılıful Login", + "logs.action.LOGIN_FAILED": "Login Başarısız", + "logs.action.LOGIN_BLOCKED": "Login Engellendi", + "logs.action.LOGOUT": "Logout", + "logs.action.PAGE_ACCESS": "Sayfa Access", + "logs.action.ACCESS_DENIED": "Access Reddedildi", + "logs.action.TOGGLE_TECHNICIAN": "Toggle Teknisyen", + "logs.action.UPDATE_ALT_SERVER_SETTINGS": "Ayarlar Güncelled", + "logs.action.CREATE_TECHNICIAN": "Teknisyen Oluşturd", + "logs.action.UPDATE_TECHNICIAN": "Teknisyen Güncelled", + "logs.action.DELETE_TECHNICIAN": "Teknisyen Sild", + "logs.action.RESET_PASSWORD": "Şifre Sıfırla", + "logs.action.RECYCLE_KEY": "Anahtar Recycled", + "logs.action.DELETE_KEY": "Anahtar Sild", + "logs.action.IMPORT_KEYS": "Anahtarlar İçe Aktared", + "logs.action.EXPORT_KEYS": "Anahtarlar Dışa Aktared", + "logs.action.REGISTER_USB_DEVICE": "USB Cihaz Kayıtlı", + "logs.action.UPDATE_USB_DEVICE_STATUS": "USB Durum Güncelled", + "logs.action.DELETE_USB_DEVICE": "USB Cihaz Sild", + "logs.action.MANUAL_BACKUP": "Manuel Yedek", + "logs.action.DOWNLOAD_REPORT": "Rapor İndired", + "logs.action.ADD_TRUSTED_NETWORK": "Ağ Ekleed", + "logs.action.DELETE_TRUSTED_NETWORK": "Ağ Sild", + "logs.action.TOTP_SETUP": "2FA Setup", + "logs.action.TOTP_ENABLED": "2FA Etkin", + "logs.action.TOTP_VERIFIED": "2FA Doğrulandı", + "logs.action.TOTP_DISABLED": "2FA Devre Dışı", + "logs.action.TOTP_BACKUP_REGEN": "2FA Codes Regenerated", + "logs.system": "Sistem", + "logs.desc.successful_login": "Başarılıful login", + "logs.desc.admin_panel_accessed": "Yönetici panel accessed", + "logs.desc.user_logout": "Kullanıcı logout", + "logs.desc.account_locked": "Account kilitli", + "logs.desc.updated_alt_server": "Güncelled alternative server configuration", + "logs.desc.manual_backup": "Tetikleed manual database backup", + "logs.desc.started_2fa_setup": "Başlated 2FA setup", + "logs.desc.2fa_enabled": "2FA başarıyla enabled", + "logs.desc.2fa_disabled": "2FA disabled by user", + "logs.desc.2fa_backup_regen": "Regenerated 2FA backup codes", + "logs.desc.toggled_active": "Toggled active status for technician ID: %s", + "logs.desc.failed_password": "Başarısız password deneme #%s", + "logs.desc.invalid_username": "Geçersiz username: %s", + "logs.desc.ip_not_whitelist": "IP not in whitelist: %s", + "logs.desc.created_tech": "Oluşturd technician: %s", + "logs.desc.updated_tech": "Güncelled technician ID: %s", + "logs.desc.deleted_tech": "Sild technician ID: %s", + "logs.desc.reset_password": "Sıfırla password for technician ID: %s", + "logs.desc.recycled_key": "Recycled key ID: %s", + "logs.desc.deleted_key": "Sild key ID: %s", + "logs.desc.imported_keys": "İçe Aktared %s keys from CSV", + "logs.desc.exported_keys": "Dışa Aktared %s keys to CSV", + "logs.desc.registered_usb": "Kayıtlı USB device '%s' for technician %s", + "logs.desc.usb_status_changed": "Changed USB device '%s' (ID: %s) status to '%s'", + "logs.desc.deleted_usb": "Sild USB device '%s' (ID: %s)", + "logs.desc.added_network": "Ekleed trusted network: %s (%s)", + "logs.desc.deleted_network": "Sild trusted network: %s (ID: %s)", + "logs.desc.downloaded_report": "İndired %s report as PDF", + "logs.desc.access_denied": "Access reddedildi — insufficient permissions", + "logs.desc.no_permission": "Hayır permission: %s", + "acl.cat.dashboard": "Kontrol Paneli & Raporlar", + "acl.cat.keys": "OEM Anahtar Management", + "acl.cat.technicians": "Teknisyen Management", + "acl.cat.activations": "Aktivasyon Records", + "acl.cat.hardware": "Donanım Bilgirmation", + "acl.cat.usb_devices": "USB Cihaz Management", + "acl.cat.admin_users": "Yönetici Kullanıcı Management", + "acl.cat.system": "Sistem & Yapılandırma", + "acl.cat.logs": "Günlükler & Audit Trail", + "acl.cat.roles": "Roller & İzinler", + "acl.perm.view_dashboard": "View Kontrol Paneli", + "acl.perm.view_reports": "View Raporlar", + "acl.perm.export_data": "Dışa Aktar Data", + "acl.perm.view_keys": "View OEM Anahtarlar", + "acl.perm.view_key_full": "View Full Anahtar Value", + "acl.perm.add_key": "Ekle OEM Anahtar", + "acl.perm.import_keys": "İçe Aktar Anahtarlar (CSV)", + "acl.perm.edit_key": "Düzenle OEM Anahtar", + "acl.perm.recycle_key": "Recycle Anahtar", + "acl.perm.delete_key": "Sil OEM Anahtar", + "acl.perm.view_technicians": "View Teknisyenler", + "acl.perm.add_technician": "Ekle Teknisyen", + "acl.perm.edit_technician": "Düzenle Teknisyen", + "acl.perm.delete_technician": "Sil Teknisyen", + "acl.perm.reset_tech_password": "Sıfırla Teknisyen Şifre", + "acl.perm.assign_tech_role": "Assign Teknisyen Rol", + "acl.perm.view_activations": "View Aktivasyonlar", + "acl.perm.add_activation_note": "Ekle Aktivasyon Hayırte", + "acl.perm.delete_activation": "Sil Aktivasyon Record", + "acl.perm.view_hardware": "View Donanım Bilgi", + "acl.perm.export_hardware": "Dışa Aktar Donanım Raporlar", + "acl.perm.view_usb_devices": "View USB Cihazlar", + "acl.perm.register_usb_device": "Register USB Cihaz", + "acl.perm.disable_usb_device": "Devre Dışı Bırak USB Cihaz", + "acl.perm.enable_usb_device": "Etkinleştir USB Cihaz", + "acl.perm.delete_usb_device": "Sil USB Cihaz", + "acl.perm.view_admins": "View Yönetici Kullanıcıs", + "acl.perm.manage_admins": "Manage Yönetici Kullanıcıs", + "acl.perm.assign_admin_role": "Assign Yönetici Rol", + "acl.perm.view_system_info": "View Sistem Bilgi", + "acl.perm.system_settings": "Modify Sistem Ayarlar", + "acl.perm.manual_backup": "Tetikle Manuel Yedek", + "acl.perm.manage_trusted_nets": "Manage Güvenilir Ağlar", + "acl.perm.manage_smtp": "Manage SMTP Ayarlar", + "acl.perm.view_backups": "View Yedek Geçmiş", + "acl.perm.view_logs": "View Sistem Günlükler", + "acl.perm.view_audit_trail": "View Audit Trail", + "acl.perm.delete_logs": "Sil Log Entries", + "acl.perm.manage_roles": "Manage Roller & İzinler", + "acl.perm.view_acl_changelog": "View ACL Change Log", + "acl.perm.view_dashboard.desc": "Access the main dashboard with statistics", + "acl.perm.view_reports.desc": "Access activation and usage reports", + "acl.perm.export_data.desc": "Dışa Aktar data to CSV/Excel files", + "acl.perm.view_keys.desc": "View list / OEM license keys", + "acl.perm.view_key_full.desc": "See unmasked product key (not just last 5)", + "acl.perm.add_key.desc": "Manuelly add a new OEM key", + "acl.perm.import_keys.desc": "Bulk import keys from CSV file", + "acl.perm.edit_key.desc": "Modify OEM key details and status", + "acl.perm.recycle_key.desc": "Sıfırla a kullanıldı key back to kullanılmadı status", + "acl.perm.delete_key.desc": "Permanently delete an OEM key", + "acl.perm.view_technicians.desc": "View list / technician accounts", + "acl.perm.add_technician.desc": "Oluştur new technician account", + "acl.perm.edit_technician.desc": "Modify technician account details", + "acl.perm.delete_technician.desc": "Kaldır technician account", + "acl.perm.reset_tech_password.desc": "Sıfırla a technician password", + "acl.perm.assign_tech_role.desc": "Change a technician atandı role", + "acl.perm.view_activations.desc": "View activation history and deneme records", + "acl.perm.add_activation_note.desc": "Ekle notes to activation records", + "acl.perm.delete_activation.desc": "Kaldır activation history girdi", + "acl.perm.view_hardware.desc": "View hardware info reports / activated PCs", + "acl.perm.export_hardware.desc": "Dışa Aktar hardware information to CSV/PDF", + "acl.perm.view_usb_devices.desc": "View kayıtlı USB authentication devices", + "acl.perm.register_usb_device.desc": "Register a new USB authentication device", + "acl.perm.disable_usb_device.desc": "Devre Dışı Bırak a USB device (revoke access)", + "acl.perm.enable_usb_device.desc": "Re-enable a disabled USB device", + "acl.perm.delete_usb_device.desc": "Permanently remove USB device registration", + "acl.perm.view_admins.desc": "View list / admin user accounts", + "acl.perm.manage_admins.desc": "Oluştur, edit, and delete admin accounts", + "acl.perm.assign_admin_role.desc": "Change an admin user atandı role", + "acl.perm.view_system_info.desc": "View system configuration and status", + "acl.perm.system_settings.desc": "Change system configuration values", + "acl.perm.manual_backup.desc": "Execute a manual database backup", + "acl.perm.manage_trusted_nets.desc": "Ekle/edit/remove trusted network ranges", + "acl.perm.manage_smtp.desc": "Configure email delivery settings", + "acl.perm.view_backups.desc": "View database backup history and status", + "acl.perm.view_logs.desc": "Access system and error log girdi", + "acl.perm.view_audit_trail.desc": "View detailed admin activity audit log", + "acl.perm.delete_logs.desc": "Kaldır log girdi from the system", + "acl.perm.manage_roles.desc": "Oluştur, edit, delete roles and assign permissions", + "acl.perm.view_acl_changelog.desc": "View audit log / role/permission changes", + "acl.danger_badge": "DANGER", + "acl.permissions_count": "%d permissions", + "nav.notifications": "Bildirimler", + "notif.title": "Bildirimler", + "notif.pref_title": "Hayırtification Preferences", + "notif.no_notifications": "Hayır bildirims", + "notif.mark_all_read": "Mark all read", + "notif.preferences": "Preferences", + "notif.push_status": "Push Hayırtification Durum", + "notif.enable_push": "Etkinleştir Push Bildirimler", + "notif.disable_push": "Devre Dışı Bırak Push Bildirimler", + "notif.push_active": "Push bildirims are active.", + "notif.push_inactive": "Push bildirims are disabled.", + "notif.push_not_supported": "Push bildirims are not supported in this browser.", + "notif.push_denied": "Hayırtification permission was reddedildi. Please enable it in browser settings.", + "notif.categories": "Hayırtification Categories", + "notif.categories_desc": "Choose which types / events you want to be notified about.", + "notif.cat.security": "Security", + "notif.cat.keys": "Anahtar Management", + "notif.cat.technicians": "Teknisyenler", + "notif.cat.system": "Sistem", + "notif.cat.devices": "USB Cihazlar", + "notif.cat.activation": "Aktivasyon", + "notif.title.security": "Security Uyarı", + "notif.title.keys": "Anahtar Management", + "notif.title.technicians": "Teknisyen Güncelle", + "notif.title.system": "Sistem Event", + "notif.title.devices": "USB Cihaz Güncelle", + "notif.title.activation": "Aktivasyon Event", + "notif.just_now": "Just now", + "notif.min_ago": "m ago", + "notif.hr_ago": "h ago", + "notif.day_ago": "d ago", + "notif.test_title": "Test Et Bildirimler", + "notif.test_desc": "Doğrula that bildirims are working correctly after enabling them.", + "notif.test_push": "Test Et Push Hayırtification", + "notif.test_sound": "Test Et Sound Hayırtification", + "notif.test_sending": "Sending...", + "notif.test_push_sent": "Test Et push bildirim sent! Kontrol Et your browser bildirims.", + "notif.test_failed": "Test Et başarısız oldu: ", + "notif.test_playing": "Playing bildirim sound...", + "notif.test_sound_played": "Hayırtification sound played! Bell bildirim created.", + "notif.test_sound_error": "Could not play sound: ", + "notif.ios_not_installed": "To receive push bildirims on iOS, install this app first:", + "notif.ios_install_step1": "Tap the Share button (\\u2399) in Safari", + "notif.ios_install_step2": "Tap \"Ekle to Home Screen\"", + "notif.ios_install_step3": "Aç the app from your Home Screen", + "notif.ios_install_step4": "Etkinleştir push bildirims", + "notif.ios_wrong_browser": "Push bildirims on iOS require Safari. Aç this page in Safari.", + "notif.ios_old_version": "Push bildirims require iOS 16.4 or later. Please update your device.", + "settings.client_resources": "İstemci Resources", + "settings.client_resources_desc": "Yükle and manage files distributed to client workstations.", + "settings.ps7_installer": "PowerShell 7 Kurer", + "settings.upload_installer": "Yükle PS7 Kurer", + "settings.replace_file": "Replace", + "settings.file_size": "Boyut", + "settings.file_checksum": "SHA256", + "settings.uploaded_by": "Yükleed by", + "settings.upload_progress": "Yükleing...", + "settings.upload_success": "Dosya uploaded başarıyla.", + "settings.upload_error": "Yükle başarısız oldu: ", + "settings.no_installer": "Hayır installer uploaded yet.", + "settings.select_file": "Please select a file first.", + "settings.invalid_file_type": "Only .msi and .exe files are izin verildi.", + "settings.delete_resource_confirm": "Are you sure you want to delete this resource?", + "js.error_prefix": "Hata: ", + "js.error_label": "Hata: ", + "js.unknown_error": "Unknown error", + "js.delete_failed": "Sil başarısız oldu", + "js.clone_failed": "Clone başarısız oldu", + "js.copied": "Copied!", + "js.error_loading_settings": "Hata loading settings", + "js.error_loading_usb": "Hata loading USB devices", + "js.failed_load_usb": "Başarısız to load USB devices", + "js.failed_register_usb": "Başarısız to register USB device", + "js.error_detecting_usb": "Hata detecting USB devices", + "js.failed_detect_usb": "Başarısız to detect USB devices. This feature requires PowerShell access on the admin PC.", + "js.usb_manual_entry_hint": "You can still manually enter device information below.", + "js.usb_check_connected": "USB drive is physically connected", + "js.usb_check_recognized": "Cihaz is recognized by Windows", + "js.usb_check_admin_pc": "You're running this on the admin PC (not server)", + "js.usb_confirm_disable": "Devre Dışı Bırak this USB device? The technician will not be able to use it for authentication.", + "js.usb_reason_disable": "İsteğe Bağlı: Enter reason for disabling this device", + "js.usb_confirm_lost": "Mark this USB device as LOST? This will disable authentication immediately.", + "js.usb_reason_lost": "İsteğe Bağlı: Enter details about when/where device was lost", + "js.usb_confirm_stolen": "Mark this USB device as STOLEN? This will disable authentication immediately.", + "js.usb_reason_stolen": "İsteğe Bağlı: Enter details about the theft", + "js.usb_confirm_enable": "Re-enable this USB device for authentication?", + "js.failed_update_usb_status": "Başarısız to update USB device status", + "js.usb_confirm_delete": "PERMANENTLY DELETE USB device \"%s\"?\n\nThis action geri alınamaz.\n\nThis will remove all records / this device from the database.", + "js.failed_delete_usb": "Başarısız to delete USB device", + "js.usb_fill_form": "Fill Form with This Cihaz", + "js.usb_command_copied": "Command copied!\n\nHayırw:\n1. Aç PowerShell (Win+X → A)\n2. Yapıştır and run\n3. Kopyala the SeriNumber", + "js.error_loading_2fa": "Hata loading 2FA status", + "js.error_setup_2fa": "Hata setting up 2FA", + "js.twofa_disable_todo": "2FA disable: please use the totp-disable.php API directly for now.", + "js.twofa_regenerate_todo": "Yedek code regeneration: please use the totp-regenerate-backup-codes.php API directly for now.", + "js.error_loading_networks": "Hata loading trusted networks", + "js.failed_load_networks": "Başarısız to load trusted networks", + "js.error_loading_backups": "Hata loading backup history", + "login.title": "Secure Yönetici", + "login.subtitle": "AnahtarGate", + "login.username": "Kullanıcı Adı", + "login.password": "Şifre", + "login.submit": "Login", + "login.error": "Login başarısız oldu", + "login.invalid_credentials": "Geçersiz username or password", + "common.create": "Oluştur", + "common.saving": "Saving...", + "common.total_results": "Toplam results", + "dashboard.no_activity": "Hayır recent activity", + "nav.devices": "USB Cihazlar", + "nav.networks": "Güvenilir Ağlar", + "nav.two_fa": "2FA Ayarlar", + "backups.title": "Yedeklemeler", + "backups.col_id": "ID", + "backups.col_type": "Tür", + "backups.col_status": "Durum", + "backups.col_filename": "Dosyaname", + "backups.col_size": "Boyut", + "backups.col_tables": "Tablos", + "backups.col_rows": "Satırlar", + "backups.col_created_at": "Oluşturd At", + "backups.trigger": "Tetikle Manuel Yedek", + "backups.triggering": "Tetikleing...", + "devices.search_placeholder": "Ara devices...", + "devices.stat_active": "Aktif", + "devices.stat_disabled": "Devre Dışı", + "devices.stat_lost": "Lost", + "devices.stat_stolen": "Stolen", + "devices.device_name": "Cihaz Ad", + "devices.serial_number": "Seri Number", + "devices.technician": "Teknisyen", + "devices.manufacturer": "Üretici", + "devices.model": "Model", + "devices.status": "Durum", + "devices.registered_date": "Kayıtlı", + "devices.disable": "Devre Dışı Bırak", + "devices.mark_lost": "Mark Lost", + "devices.mark_stolen": "Mark Stolen", + "devices.delete": "Sil", + "devices.confirm_disable": "Devre Dışı Bırak Cihaz", + "devices.confirm_disable_desc": "Are you sure you want to disable this device?", + "devices.confirm_lost": "Mark as Lost", + "devices.confirm_lost_desc": "Are you sure you want to mark this device as lost?", + "devices.confirm_stolen": "Mark as Stolen", + "devices.confirm_stolen_desc": "Are you sure you want to mark this device as stolen?", + "devices.confirm_delete": "Sil Cihaz", + "devices.confirm_delete_desc": "Are you sure you want to permanently delete this device?", + "devices.status_active": "Aktif", + "devices.status_disabled": "Devre Dışı", + "devices.status_lost": "Lost", + "devices.status_stolen": "Stolen", + "history.order_number": "Sipariş #", + "history.product_key": "Ürün Anahtar", + "history.status_success": "Başarılı", + "history.status_failed": "Başarısız", + "keys.oem_identifier": "OEM ID", + "keys.export": "Dışa Aktar CSV", + "keys.confirm_recycle": "Recycle Anahtar", + "keys.confirm_recycle_desc": "Are you sure you want to recycle this key? It will be reset to kullanılmadı.", + "keys.confirm_delete": "Sil Anahtar", + "keys.confirm_delete_desc": "Are you sure you want to permanently delete this key?", + "logs.timestamp": "Zamanstamp", + "networks.add": "Ekle Ağ", + "networks.add_title": "Ekle Güvenilir Ağ", + "networks.add_desc": "Ekle a new trusted network for 2FA bypass and USB authentication.", + "networks.col_name": "Ad", + "networks.col_ip_range": "IP Range", + "networks.col_bypass_2fa": "Bypass 2FA", + "networks.col_allow_usb": "Tümüow USB Auth", + "networks.col_created_by": "Oluşturd By", + "networks.col_created_at": "Oluşturd At", + "networks.field_name": "Ağ Ad", + "networks.field_name_placeholder": "e.g., Office LAN", + "networks.field_ip_range": "IP Range (CIDR)", + "networks.field_description": "Açıklama", + "networks.field_description_placeholder": "İsteğe Bağlı description...", + "networks.field_bypass_2fa": "Bypass 2FA from this network", + "networks.field_allow_usb": "Tümüow USB authentication from this network", + "networks.confirm_delete": "Sil Ağ", + "networks.confirm_delete_desc": "Are you sure you want to delete this trusted network?", + "notifications.prefs_title": "Push Preferences", + "notifications.prefs_desc": "Choose which bildirim categories you want to receive.", + "notifications.list_title": "Recent Bildirimler", + "notifications.mark_all_read": "Mark Tümü Read", + "notifications.empty": "Hayır bildirims yet", + "roles.col_display_name": "Ekran Ad", + "roles.col_role_name": "Rol Ad", + "roles.col_type": "Tür", + "roles.col_permissions": "İzinler", + "roles.col_system": "Sistem", + "roles.create_title": "Oluştur Rol", + "roles.create_desc": "Oluştur a new custom role with specific permissions.", + "roles.edit_title": "Düzenle Rol", + "roles.edit_desc": "Modify role settings and permissions.", + "roles.field_role_name": "Rol Ad", + "roles.field_display_name": "Ekran Ad", + "roles.field_description": "Açıklama", + "roles.field_description_placeholder": "İsteğe Bağlı description...", + "roles.field_role_type": "Rol Tür", + "roles.field_color": "Badge Renk", + "roles.field_permissions": "İzinler", + "roles.type_admin": "Yönetici", + "roles.type_technician": "Teknisyen", + "roles.dangerous": "Dangerous", + "roles.confirm_delete": "Sil Rol", + "roles.confirm_delete_desc": "Are you sure you want to permanently delete this role?", + "settings.field_enabled": "Etkin", + "settings.field_enabled_desc": "Etkinleştir or disable the alternative server.", + "settings.field_script_path": "Script Yol", + "settings.field_pre_command": "Pre-Command", + "settings.field_pre_command_placeholder": "Command to run before script...", + "settings.field_script_args": "Script Arguments", + "settings.field_script_args_placeholder": "Ekleitional arguments...", + "settings.field_script_type": "Script Tür", + "settings.field_timeout": "Zaman Aşımı (saniye)", + "settings.field_prompt_technician": "Prompt Teknisyen", + "settings.field_prompt_technician_desc": "Ask technician before using alternative server.", + "settings.field_auto_failover": "Auto Failover", + "settings.field_auto_failover_desc": "Otomatikally switch to alternative server on failure.", + "settings.field_verify_activation": "Doğrula Aktivasyon", + "settings.field_verify_activation_desc": "Doğrula activation result from alternative server.", + "settings.brand_accent_color": "Vurgu", + "settings.brand_app_version": "Sürüm Etiket", + "settings.brand_colors": "Özel Renks", + "settings.brand_colors_hint": "Leave empty to use default theme colors.", + "settings.brand_company_name": "Şirket Ad", + "settings.brand_delete": "Kaldır", + "settings.brand_favicon": "Favicon", + "settings.brand_login_subtitle": "Login Alt Başlık", + "settings.brand_login_title": "Login Başlık", + "settings.brand_logo": "Logo", + "settings.brand_primary_color": "Birincil", + "settings.brand_reset_colors": "Sıfırla", + "settings.brand_sidebar_color": "Kenar Çubuğu", + "settings.brand_upload": "Yükle", + "settings.branding_desc": "Özelize the look and feel / the application.", + "settings.branding_title": "Markalama", + "settings.order_field_title": "Sipariş Number Field", + "settings.order_field_desc": "Configure the order number field label, geçerliation rules, and length constraints.", + "settings.order_label_en": "Etiket (English)", + "settings.order_label_ru": "Etiket (Russian)", + "settings.order_prompt_en": "Prompt (English)", + "settings.order_prompt_ru": "Prompt (Russian)", + "settings.order_char_type": "Character Tür", + "settings.order_char_digits_only": "Digits Only (0-9)", + "settings.order_char_alphanumeric": "Alphanumeric (A-Z, 0-9)", + "settings.order_char_alphanumeric_dash": "Alphanumeric + Dash/Underscore", + "settings.order_char_custom": "Özel Regex", + "settings.order_custom_regex": "Özel Regex Pattern", + "settings.order_custom_regex_hint": "PHP regex with delimiters, e.g. /^[A-Z]{2}\\d{4}$/", + "settings.order_min_length": "Minimum Length", + "settings.order_max_length": "Maximum Length", + "settings.order_preview": "Geçerliation Preview", + "settings.order_preview_valid": "Geçerli", + "settings.order_preview_invalid": "Geçersiz", + "tech.password": "Şifre", + "tech.password_placeholder": "Enter password...", + "tech.new_password": "Yeni Şifre", + "tech.preferred_language": "Preferred Dil", + "tech.lang_en": "English", + "tech.lang_ru": "Russian", + "tech.server_oem": "OEM Sunucu", + "tech.server_alternative": "Alternatif Sunucu", + "tech.activate": "Activate", + "tech.deactivate": "Deactivate", + "tech.delete": "Sil", + "tech.reset_password": "Sıfırla Şifre", + "tech.reset_password_desc": "Sıfırla this technician's password.", + "tech.confirm_toggle": "Toggle Durum", + "tech.confirm_activate_desc": "Are you sure you want to activate this technician?", + "tech.confirm_deactivate_desc": "Are you sure you want to deactivate this technician?", + "tech.confirm_delete": "Sil Teknisyen", + "tech.confirm_delete_desc": "Are you sure you want to permanently delete this technician?", + "tech.error_id_length": "Teknisyen ID must be 1-20 characters", + "tech.error_password_length": "Şifre must be at least 8 characters", + "tech.error_add_failed": "Başarısız to add technician", + "tech.error_edit_failed": "Başarısız to update technician", + "two_fa.status_title": "Two-Factor Authentication", + "two_fa.status_desc_v2": "Protect your account with an additional layer / security using a TOTP authenticator app.", + "two_fa.label_status": "Durum", + "two_fa.label_backup_codes": "Yedek Codes Remaining", + "two_fa.label_verified": "Doğrulandı At", + "two_fa.enabled": "Etkin", + "two_fa.disabled": "Devre Dışı", + "two_fa.no_data": "Unable to load 2FA status", + "two_fa.manage_in_panel": "Manage in Yönetici Panel", + "two_fa.not_available": "2FA Hayırt Mevcut", + "two_fa.not_available_desc": "The 2FA module is not yapılandırıldı on this server. İletişim your administrator.", + "two_fa.btn_manage": "Manage 2FA Ayarlar", + "two_fa.btn_enable": "Etkinleştir 2FA", + "two_fa.how_it_works_title": "How 2FA Works", + "two_fa.step1_title": "Kur App", + "two_fa.step1_desc": "Use Google Authenticator, Authy, or any TOTP-compatible app", + "two_fa.step2_title": "Scan QR Code", + "two_fa.step2_desc": "Link your account by scanning the setup QR code", + "two_fa.step3_title": "Enter Code", + "two_fa.step3_desc": "Enter the 6-digit code from your app each time you log in", + "sidebar.quality_control": "Kalite Control", + "nav.compliance": "QC Uyum", + "nav.compliance_results": "Uyum Sonuçlar", + "compliance.tab_settings": "Ayarlar", + "compliance.tab_motherboards": "Anakarts", + "compliance.tab_manufacturers": "Üreticis", + "compliance.global_settings": "Global Ayarlar", + "compliance.global_settings_desc": "Configure QC compliance engine defaults.", + "compliance.qc_enabled": "QC Engine Etkin", + "compliance.qc_enabled_desc": "Etkinleştir hardware compliance checking on all activations.", + "compliance.blocking_prevents_key": "Blocking Prevents Aktivasyon", + "compliance.blocking_prevents_key_desc": "Refuse key distribution when blocking compliance issues exist.", + "compliance.max_unallocated_mb": "Max Untahsis edildi Alan (MB)", + "compliance.max_unallocated_mb_desc": "Fail partition check if tahsis edilmedi disk space exceeds this limit. Set to 0 to disable.", + "compliance.default_bios_enforcement": "Varsayılan BIOS Enforcement", + "compliance.default_sb_enforcement": "Varsayılan Güvenli Önyükleme Enforcement", + "compliance.default_hb_enforcement": "Varsayılan Boot Logo Enforcement", + "compliance.enforcement_0": "Devre Dışı", + "compliance.enforcement_1": "Bilgi", + "compliance.enforcement_2": "Uyarı", + "compliance.enforcement_3": "Blocking", + "compliance.manufacturer": "Üretici", + "compliance.product": "Ürün", + "compliance.times_seen": "Seen", + "compliance.last_seen": "Last Seen", + "compliance.enforcement": "Enforcement", + "compliance.known_bios": "Known BIOS", + "compliance.edit_rules": "Düzenle Rules", + "compliance.search_boards": "Ara motherboards...", + "compliance.all_manufacturers": "Tümü Üreticis", + "compliance.edit_motherboard": "Düzenle Anakart Rules", + "compliance.inherit_hint": "Leave empty to inherit from manufacturer defaults or global settings.", + "compliance.inherit": "Inherit (global)", + "compliance.secure_boot_required": "Güvenli Önyükleme Gerekli", + "compliance.sb_enforcement": "Güvenli Önyükleme Enforcement", + "compliance.min_bios": "Min BIOS Sürüm", + "compliance.rec_bios": "Recommended BIOS", + "compliance.bios_enforcement": "BIOS Enforcement", + "compliance.hb_enforcement": "Boot Logo Enforcement", + "compliance.drivers_enforcement": "Missing Drivers Enforcement", + "compliance.notes": "Hayırtlar", + "compliance.known_bios_versions": "Known BIOS Sürüms", + "compliance.unconfigured_manufacturers": "Unyapılandırıldı Üreticis", + "compliance.unconfigured_desc": "These manufacturers were detected but have no default rules.", + "compliance.edit_manufacturer": "Düzenle Üretici Varsayılans", + "compliance.configure_manufacturer": "Configure Üretici", + "compliance.no_manufacturers": "Hayır manufacturers detected yet. Anakarts will appear here after hardware collection.", + "compliance.order_number": "Sipariş #", + "compliance.motherboard": "Anakart", + "compliance.check_secure_boot": "Güvenli Önyükleme", + "compliance.check_bios": "BIOS Sürüm", + "compliance.check_boot_logo": "Boot Logo", + "compliance.check_partitions": "Partitions", + "compliance.check_drivers": "Drivers", + "compliance.check_type": "Kontrol Et", + "compliance.result": "Sonuç", + "compliance.expected_actual": "Expected / Actual", + "compliance.message": "Message", + "compliance.rule_source": "Source", + "compliance.checked_at": "Kontrol Eted", + "compliance.type_bios_version": "BIOS Sürüm", + "compliance.type_secure_boot": "Güvenli Önyükleme", + "compliance.type_hackbgrt_boot_priority": "Boot Logo Doğrulama", + "compliance.type_partition_layout": "Partition Layout", + "compliance.result_pass": "Pass", + "compliance.result_info": "Bilgi", + "compliance.result_warning": "Uyarı", + "compliance.result_fail": "Fail", + "compliance.source_global": "Global", + "compliance.source_manufacturer": "Üretici", + "compliance.source_model": "Model", + "compliance.stat_pass_rate": "Pass Rate", + "compliance.stat_warnings": "Uyarıs", + "compliance.stat_failures": "Failures", + "compliance.stat_blocking": "Blocking", + "compliance.stat_total": "Toplam Kontrol Ets", + "compliance.top_failing": "Top Failing Boards", + "compliance.search_order": "Ara by order number...", + "compliance.all_types": "Tümü Kontrol Et Türs", + "compliance.all_results": "Tümü Sonuçlar", + "compliance.overall": "Overall", + "compliance.col_sb": "SB", + "compliance.col_bios_enf": "BIOS", + "compliance.col_bl": "BL", + "compliance.col_part": "Part", + "compliance.col_drv": "Drv", + "compliance.col_secure_boot": "Güvenli Önyükleme", + "compliance.col_bios": "BIOS", + "compliance.col_boot_logo": "Boot Logo", + "compliance.col_partitions": "Partitions", + "compliance.col_drivers": "Drivers", + "compliance.type_missing_drivers": "Missing Drivers", + "compliance.default_partition_enforcement": "Partition Layout", + "compliance.default_drivers_enforcement": "Missing Drivers", + "compliance.global_defaults_hint": "These are fallback defaults. Ürün lines can override per-line.", + "product_lines.qc_enforcement": "QC Enforcement per Kontrol Et", + "product_lines.qc_enforcement_hint": "Set per-check enforcement for this product line. \"Inherit\" uses global defaults.", + "compliance.recheck_historical": "Recheck Historical", + "compliance.recheck_title": "Recheck Historical Records", + "compliance.recheck_counting": "Sayıing records...", + "compliance.recheck_confirm_desc": "This will re-run all QC compliance checks on {{count}} hardware records using current rules. Existing results will be replaced. Records are processed in toplu işlemes / 50.", + "compliance.recheck_running": "Processing records in toplu işlemes...", + "compliance.recheck_done": "Recheck complete.", + "compliance.recheck_no_records": "Hayır hardware records found to recheck.", + "compliance.recheck_start": "Recheck {{count}} Records", + "compliance.recheck_stop": "Durdur", + "common.app_name": "AnahtarGate", + "common.app_version": "v2.1.0", + "common.mb": "MB", + "theme.light": "Açık", + "theme.dark": "Koyu", + "theme.system": "Sistem", + "toast.technician_added": "Teknisyen added başarıyla", + "toast.technician_updated": "Teknisyen updated başarıyla", + "toast.technician_toggled": "Teknisyen status updated", + "toast.technician_deleted": "Teknisyen deleted", + "toast.password_reset": "Şifre reset başarıyla", + "toast.key_recycled": "Anahtar recycled başarıyla", + "toast.key_deleted": "Anahtar deleted", + "toast.device_updated": "Cihaz status updated", + "toast.device_deleted": "Cihaz deleted", + "toast.network_added": "Güvenilir network added", + "toast.network_deleted": "Ağ removed", + "toast.role_created": "Rol created başarıyla", + "toast.role_updated": "Rol updated başarıyla", + "toast.role_deleted": "Rol deleted", + "toast.backup_triggered": "Yedek tetikleed başarıyla", + "toast.brand_asset_deleted": "Asset removed", + "toast.brand_asset_uploaded": "Asset uploaded başarıyla", + "toast.branding_saved": "Markalama saved başarıyla", + "toast.preferences_saved": "Hayırtification preferences saved", + "toast.qc_settings_saved": "QC settings saved", + "toast.motherboard_updated": "Anakart rule updated", + "toast.manufacturer_updated": "Üretici rule updated", + "toast.recheck_started": "Historical recheck completed", + "toast.settings_saved": "Ayarlar saved başarıyla", + "toast.csv_imported": "CSV imported başarıyla", + "toast.keys_added": "{{count}} key(s) added başarıyla", + "toast.error_generic": "An error occurred", + "toast.events_retried": "Retried {{retried}}, succeeded {{succeeded}}", + "toast.integration_saved": "Entegrasyon settings saved", + "error.boundary_title": "Something went wrong", + "error.boundary_description": "An unexpected error occurred. Please try refreshing the page.", + "error.boundary_refresh": "Yenile Sayfa", + "empty.keys": "Hayır keys found. İçe Aktar a CSV file to add OEM keys.", + "empty.technicians": "Hayır technicians yet. Ekle your first technician to get started.", + "empty.history": "Hayır activation history. Records will appear once technicians activate keys.", + "empty.devices": "Hayır USB devices kayıtlı yet.", + "empty.roles": "Hayır custom roles defined. Oluştur a role to manage permissions.", + "empty.networks": "Hayır trusted networks yapılandırıldı. Ekle a network to allow admin access.", + "empty.backups": "Hayır backups found. Tetikle a manual backup to create one.", + "empty.logs": "Hayır activity logs yet.", + "hw.title": "Donanım Detaylar", + "hw.view": "View hardware details", + "hw.error": "Başarısız to load hardware info", + "hw.not_found": "Hayır hardware data found for this activation", + "hw.cpu": "CPU", + "hw.ram": "RAM", + "hw.storage": "Depolama", + "hw.gpu": "GPU", + "hw.system": "Sistem", + "hw.computer_name": "Computer", + "hw.manufacturer": "Üretici", + "hw.product": "Ürün", + "hw.system_serial": "Seri", + "hw.uuid": "UUID", + "hw.chassis": "Chassis", + "hw.os": "Operating Sistem", + "hw.os_name": "OS", + "hw.os_version": "Sürüm", + "hw.os_arch": "Architecture", + "hw.os_build": "Build", + "hw.os_install": "Kured", + "hw.secure_boot": "Güvenli Önyükleme", + "hw.enabled": "Etkin", + "hw.disabled": "Devre Dışı", + "hw.motherboard": "Anakart & BIOS", + "hw.mb_manufacturer": "Üretici", + "hw.mb_product": "Ürün", + "hw.mb_serial": "Seri", + "hw.bios_vendor": "BIOS", + "hw.bios_version": "BIOS Sürüm", + "hw.bios_date": "BIOS Tarih", + "hw.cpu_details": "İşlemci", + "hw.cpu_name": "Ad", + "hw.cpu_manufacturer": "Vendor", + "hw.cpu_cores": "Cores / Threads", + "hw.cpu_clock": "Clock", + "hw.memory": "Bellek", + "hw.ram_total": "Toplam", + "hw.ram_slots": "Slots", + "hw.module": "Module", + "hw.capacity": "Boyut", + "hw.speed": "Speed", + "hw.storage_details": "Depolama", + "hw.partitions": "Partitions", + "hw.network": "Ağ", + "hw.mac": "MAC", + "hw.local_ip": "Local IP", + "hw.public_ip": "Public IP", + "hw.security": "Security", + "hw.present": "Present", + "hw.not_present": "Hayırt Present", + "hw.tpm_vendor": "TPM Vendor", + "hw.fingerprint": "Parmak İzi", + "hw.gpu_details": "Grafik", + "hw.gpu_name": "GPU", + "hw.gpu_driver": "Driver", + "hw.gpu_vram": "VRAM", + "hw.collected": "Collected", + "hw.method": "Method", + "hw.serial": "Seri", + "hw.part_number": "Part #", + "hw.cpu_serial": "Seri Number", + "hw.bios_serial": "BIOS Seri", + "hw.os_serial": "OS Seri", + "hw.mb_version": "Sürüm", + "hw.chassis_serial": "Chassis Seri", + "hw.adapters": "Adapters", + "integrations.configure": "Configure", + "integrations.configure_title": "Configure {{name}}", + "integrations.desc": "Connect external systems to sync data automatically.", + "integrations.enabled": "Etkin", + "integrations.failed": "başarısız oldu", + "integrations.last_sync": "Last sync", + "integrations.none": "Hayır integrations available.", + "integrations.onec_base_url": "1C Sunucu URL", + "integrations.onec_endpoint_act": "Aktivasyonlar Endpoint", + "integrations.onec_endpoint_inv": "Inventory Endpoint", + "integrations.onec_password": "Şifre", + "integrations.onec_pull_inv": "Pull inventory from 1C", + "integrations.onec_push_act": "Push activations to 1C", + "integrations.onec_push_keys": "Push key usage to 1C", + "integrations.onec_username": "Kullanıcı Adı", + "integrations.ost_api_key": "API Anahtar", + "integrations.ost_auto_create": "Auto-create ticket on key assignment", + "integrations.ost_auto_reply": "Auto-reply on activation complete", + "integrations.ost_base_url": "osTicket URL", + "integrations.ost_department_id": "Department ID", + "integrations.ost_include_hw": "Include hardware details", + "integrations.ost_subject_template": "Ticket Subject Şablon", + "integrations.ost_template_hint": "Use {order_number} as placeholder", + "integrations.ost_topic_id": "Topic ID", + "integrations.pending": "pending", + "integrations.retry": "Yeniden Deneme", + "integrations.status_connected": "Bağlı", + "integrations.status_disconnected": "Bağlantı Kesildi", + "integrations.status_error": "Hata", + "integrations.test": "Test Et", + "integrations.test_connection": "Test Et Bağlantı", + "integrations.title": "Entegrasyonlar", + "common.coming_soon": "Coming soon", + "errors.access_denied": "Access Reddedildi", + "errors.insufficient_permissions": "You do not have the required permissions to view this page. İletişim your administrator if you believe this is an error.", + "roles.selected": "selected", + "settings.script_type_powershell": "PowerShell", + "nav.product_lines": "Ürün Lines", + "product_lines.title": "Ürün Lines", + "product_lines.subtitle": "Manage product lines, variants, and partition layout templates for QC checks.", + "product_lines.lines_list": "Ürün Lines", + "product_lines.no_lines": "Hayır product lines yapılandırıldı.", + "product_lines.select_line": "Seç a product line to view its variants and partition templates.", + "product_lines.add_line": "Ekle Ürün Line", + "product_lines.edit_line": "Düzenle Ürün Line", + "product_lines.line_dialog_desc": "Define a product line with its order number pattern.", + "product_lines.name": "Ad", + "product_lines.order_pattern": "Sipariş Pattern", + "product_lines.pattern_hint": "Sipariş number prefix (e.g. ЭЛ00-######, ЛЕ00-######). Use # for digits, * for any characters.", + "product_lines.enforcement": "Enforcement Level", + "product_lines.enforcement_ignore": "Ignore", + "product_lines.enforcement_info": "Bilgi", + "product_lines.enforcement_warning": "Uyarı", + "product_lines.enforcement_blocking": "Blocking", + "product_lines.description": "Açıklama", + "product_lines.pattern": "Pattern", + "product_lines.variants": "Variants", + "product_lines.no_variants": "Hayır variants yapılandırıldı for this line.", + "product_lines.add_variant": "Ekle Variant", + "product_lines.edit_variant": "Düzenle Variant", + "product_lines.variant_dialog_desc": "Configure disk size range and partition layout template.", + "product_lines.variant_name": "Variant Ad", + "product_lines.disk_min_mb": "Disk Min (MB)", + "product_lines.disk_max_mb": "Disk Max (MB)", + "product_lines.disk_range_hint": "Range: {{min}} — {{max}}", + "product_lines.partitions": "Partitions", + "product_lines.total": "Toplam", + "product_lines.add_partition": "Ekle", + "product_lines.part_name": "Ad", + "product_lines.part_type": "Tür", + "product_lines.part_size": "Boyut (MB)", + "product_lines.part_tolerance": "Tol. %", + "product_lines.part_flexible": "Flex", + "product_lines.saved": "Ürün line saved", + "product_lines.deleted": "Ürün line deleted", + "product_lines.variant_saved": "Ürün variant saved", + "product_lines.variant_deleted": "Ürün variant deleted", + "product_lines.confirm_delete_line": "Sil Ürün Line?", + "product_lines.confirm_delete_line_desc": "This will deactivate the product line and all its variants. This action can be undone.", + "product_lines.confirm_delete_variant": "Sil Variant?", + "product_lines.confirm_delete_variant_desc": "This will deactivate this variant and its partition template.", + "auth.session_expired": "Session süresi doldu. Please log in again.", + "settings.session_title": "Session Ayarlar", + "settings.session_desc": "Configure admin session zaman aşımı and inactivity limits.", + "settings.session_timeout": "Session Zaman Aşımı (dakika)", + "settings.session_timeout_desc": "Maximum session lifetime before forced re-login.", + "settings.max_failed_logins": "Max Başarısız Login Denemes", + "settings.lockout_duration": "Lockout Süre (dakika)", + "settings.password_change_days": "Force Şifre Change (gün)", + "settings.password_change_days_desc": "0 = never require password change", + "settings.session_saved": "Session settings saved", + "settings.smtp_title": "E-posta / SMTP", + "settings.smtp_desc": "Configure email delivery for bildirims and alerts.", + "settings.smtp_enabled": "Etkinleştir E-posta Bildirimler", + "settings.smtp_enabled_desc": "Send email alerts for activation failures, key exhaustion, etc.", + "settings.smtp_provider": "Provider Preset", + "settings.smtp_server": "SMTP Sunucu", + "settings.smtp_port": "Bağlantı Hayırktası", + "settings.smtp_encryption": "Şifreleme", + "settings.smtp_enc_none": "Hiçbiri (not recommended)", + "settings.smtp_auth": "Require Authentication", + "settings.smtp_auth_desc": "Most SMTP servers require username/password authentication.", + "settings.smtp_username": "Kullanıcı Adı", + "settings.smtp_password": "Şifre", + "settings.smtp_password_hint": "For Gmail, use an App Şifre. Stored şifrelenmiş at rest (AES-256-GCM).", + "settings.smtp_from": "From Adres", + "settings.smtp_from_name": "From Ad", + "settings.smtp_to": "Varsayılan Recipient", + "settings.smtp_triggers": "Hayırtification Tetikles", + "settings.smtp_on_fail": "Aktivasyon Failure", + "settings.smtp_on_fail_desc": "Send alert when a key activation fails.", + "settings.smtp_on_exhausted": "Anahtarlar Exhausted", + "settings.smtp_on_exhausted_desc": "Send alert when available keys run out.", + "settings.smtp_on_summary": "Daily Özet", + "settings.smtp_on_summary_desc": "Send a daily digest / activation activity.", + "settings.smtp_test_recipient": "Test Et Recipient", + "settings.smtp_testing": "Sending...", + "settings.smtp_test": "Send Test Et E-posta", + "settings.smtp_test_ok": "Test Et email sent başarıyla", + "settings.smtp_test_fail": "Test Et başarısız oldu", + "settings.smtp_saved": "E-posta settings saved", + "nav.downloads": "İndirs", + "downloads.title": "İstemci İndirs", + "downloads.description": "İndir tools and extensions for technician workstations", + "downloads.oem_launcher": "OEM Activator Launcher", + "downloads.oem_launcher_desc": "CMD launcher for technician workstations — admin rights, WSUS cleanup, PS7 bootstrap", + "downloads.ps7_installer": "PowerShell 7 Kurer", + "downloads.ps7_installer_desc": "MSI installer for the PowerShell 7 runtime required by the activation script", + "downloads.chrome_extension": "Chrome Donanım Bridge", + "downloads.chrome_extension_desc": "Browser extension for collecting hardware info from client machines via WebGL/API", + "downloads.filename": "Dosya", + "downloads.file_size": "Boyut", + "downloads.checksum": "SHA256", + "downloads.uploaded_at": "Yükleed", + "downloads.uploaded_by": "Yükleed By", + "downloads.download": "İndir", + "downloads.upload": "Yükle", + "downloads.uploading": "Yükleing...", + "downloads.replace": "Replace Dosya", + "downloads.delete": "Sil", + "downloads.not_uploaded": "Hayırt yet uploaded", + "downloads.upload_prompt": "Yükle this resource to make it available for download", + "downloads.confirm_delete_title": "Sil Resource", + "downloads.confirm_delete": "Are you sure you want to delete this resource? It will no longer be available for download.", + "downloads.upload_success": "Resource uploaded başarıyla", + "downloads.delete_success": "Resource deleted", + "empty.downloads": "Hayır client resources found", + "nav.system_upgrade": "Sistem Güncelleme", + "upgrade.title": "Sistem Güncelleme", + "upgrade.current_version": "Mevcut Sürüm", + "upgrade.step_upload": "Yükle Paket", + "upgrade.step_preflight": "Ön Kontrol Kontrol Ets", + "upgrade.step_backup": "Yedek", + "upgrade.step_apply": "Uygula Güncelleme", + "upgrade.step_verify": "Doğrula", + "upgrade.upload_desc": "Yükle a .zip upgrade package containing manifest.json, migrations, and file updates.", + "upgrade.upload_hint": "Drop a .zip upgrade package here, or click to browse", + "upgrade.upload_format": "ZIP must contain manifest.json at root level", + "upgrade.manifest_info": "Paket Bilgirmation", + "upgrade.version_target": "Target Sürüm", + "upgrade.release_date": "Sürüm Tarih", + "upgrade.migrations_count": "Database Migrasyonlar", + "upgrade.files_count": "Dosya Changes", + "upgrade.run_preflight": "Run Ön Kontrol Kontrol Ets", + "upgrade.rerun_checks": "Re-run Kontrol Ets", + "upgrade.preflight_desc": "Doğrula system compatibility before proceeding.", + "upgrade.proceed_backup": "Proceed to Yedek", + "upgrade.fix_and_retry": "Fix issues above and re-run checks", + "upgrade.backup_desc": "Full database and file backup before any changes are made.", + "upgrade.create_backup": "Oluştur Yedek", + "upgrade.backup_in_progress": "Creating backup...", + "upgrade.proceed_apply": "Proceed to Uygula", + "upgrade.apply_desc": "Run database migrations and update application files.", + "upgrade.apply_warning": "This will modify database schema and application files.", + "upgrade.apply_ensure": "Ensure no technicians are actively using the system.", + "upgrade.apply_upgrade": "Uygula Güncelleme", + "upgrade.apply_confirm_title": "Uygula Sistem Güncelleme?", + "upgrade.apply_confirm": "This will modify database schema and application files. A full backup has been created. Continue?", + "upgrade.migrations_applied": "Migrasyonlar Applied", + "upgrade.files_updated": "Dosyalar Güncelled", + "upgrade.proceed_verify": "Proceed to Doğrula", + "upgrade.verify_desc": "Doğrula that the upgrade was applied correctly.", + "upgrade.run_verify": "Run Doğrulama", + "upgrade.rerun_verify": "Re-run Doğrulama", + "upgrade.verify_issues": "Doğrulama found issues", + "upgrade.upgrade_complete": "Sistem upgraded to version {{version}}", + "upgrade.all_verified": "Tümü verification checks geçti.", + "upgrade.rollback": "Geri Al", + "upgrade.rollback_title": "Geri Al Sistem?", + "upgrade.rollback_confirm": "This will restore the database and files from the pre-upgrade backup. Tümü changes from this upgrade will be reverted.", + "upgrade.rollback_confirm_btn": "Evet, Geri Al", + "upgrade.history": "Güncelleme Geçmiş", + "upgrade.col_from": "From", + "upgrade.col_to": "To", + "upgrade.col_status": "Durum", + "upgrade.col_date": "Tarih", + "upgrade.col_admin": "Yönetici", + "toast.upgrade_uploaded": "Güncelleme package uploaded", + "toast.preflight_complete": "Tümü pre-flight checks geçti", + "toast.backup_created": "Pre-upgrade backup created", + "toast.upgrade_applied": "Güncelleme applied başarıyla", + "toast.upgrade_verified": "Güncelleme doğrulandı başarıyla", + "toast.rollback_complete": "Sistem rolled back başarıyla", + "toast.upgrade_downloaded": "Güncelleme package downloaded from GitHub", + "empty.upgrade_history": "Hayır upgrade history", + "upgrade.github_updates": "GitHub Güncelles", + "upgrade.github_desc": "Kontrol Et for the latest release on GitHub.", + "upgrade.checking_updates": "Kontrol Eting for updates...", + "upgrade.update_available_title": "Güncelle Mevcut: v{{version}}", + "upgrade.update_found": "Güncelle available: v{{version}}", + "upgrade.up_to_date": "Sistem is up to date", + "upgrade.published": "Published", + "upgrade.view_changelog": "View changelog", + "upgrade.download_install": "İndir & Kur", + "upgrade.downloading": "İndiring...", + "upgrade.no_package": "Hayır upgrade package attached to this release", + "upgrade.view_release": "View on GitHub", + "upgrade.check_now": "Kontrol Et Hayırw", + "upgrade.manual_upload": "Manuel Yükle", + "settings.tab_server": "Sunucu Ayarlar", + "settings.tab_client": "İstemci Yapılandırma", + "settings.client_tasks_title": "Pre-Aktivasyon Görevler", + "settings.client_tasks_desc": "Toggle which tasks the launcher runs before activation.", + "settings.task_wsus_cleanup": "WSUS Cleanup", + "settings.task_wsus_cleanup_desc": "Clean Windows Güncelle registry, restart service, tetikle scan", + "settings.task_security_hardening": "Security Hardening", + "settings.task_security_hardening_desc": "Devre Dışı Bırak SMB guest access, enable SMB signing", + "settings.task_edrive_format": "E: Drive Format", + "settings.task_edrive_format_desc": "Detect BIOS-labeled E: partition and format as FAT32", + "settings.task_ps7_install": "PowerShell 7 Auto-Kur", + "settings.task_ps7_install_desc": "Ara USB for MSI, fall back to winget if bulunamadı", + "settings.task_self_update": "Launcher Self-Güncelle", + "settings.task_self_update_desc": "Kontrol Et server for newer launcher version and auto-replace", + "settings.client_timing_title": "Aktivasyon Timing", + "settings.client_timing_desc": "Configure base gecikmes and retry behavior for activation.", + "settings.activation_delay": "Base Aktivasyon Gecikme (sec)", + "settings.max_retries": "Max Yeniden Deneme Denemes", + "settings.max_check_iterations": "Max Kontrol Et Iterations", + "settings.check_delay_base": "Kontrol Et Gecikme Base (sec)", + "settings.client_network_title": "Ağ Diagnostics", + "settings.client_network_desc": "Configure latency eşiks and multipliers for adaptive timing.", + "settings.net_tier": "Katman", + "settings.net_threshold": "Latency Eşik (ms)", + "settings.net_multiplier": "Multiplier", + "settings.net_max_multiplier": "Max Multiplier", + "settings.net_ping_samples": "Ping Samples", + "settings.net_test_endpoint": "Test Et Endpoint", + "settings.client_retry_title": "Anahtar Yeniden Deneme & Yedek Sunucu", + "settings.client_retry_desc": "Configure how the system handles activation failures and key exhaustion.", + "settings.max_keys_to_try": "Max Anahtarlar to Try", + "settings.max_keys_to_try_desc": "Number / different keys to istek before giving up", + "settings.key_exhaustion_action": "When Tümü Anahtarlar Fail", + "settings.exhaustion_stop": "Durdur with error", + "settings.exhaustion_failover": "Auto-yedek geçiş to alt server", + "settings.exhaustion_retry_loop": "Yeniden Deneme from beginning after cooldown", + "settings.retry_cooldown": "Yeniden Deneme Bekleme Süresi (sec)", + "settings.retry_cooldown_desc": "Wait time before retry_loop restarts", + "settings.server_busy_delay": "Sunucu Busy Gecikme (sec)", + "settings.server_busy_delay_desc": "Wait when Micros/t servers are throttling", + "settings.error_strategies": "Hata-Specific Strategies", + "settings.network_error_retries": "Ağ Hata Extra Retries", + "settings.network_error_retries_desc": "Extra reconnection deneme on network errors", + "settings.network_reconnect_wait": "Reconnect Wait (sec)", + "settings.network_reconnect_wait_desc": "Wait between internet reconnection checks", + "settings.skip_key_on_invalid": "Atla Anahtar on Geçersiz Hata", + "settings.skip_key_on_invalid_desc": "Immediately try next key when current key is engellendi/geçersiz", + "settings.skip_key_on_service": "Atla Anahtar on Service Hata", + "settings.skip_key_on_service_desc": "Try next key instead / retrying on Windows service errors", + "nav.license": "Lisans", + "license.title": "Lisans", + "license.description": "Manage your AnahtarGate license and feature access.", + "license.current_license": "Mevcut Lisans", + "license.registered_to": "Kayıtlı to {{email}}", + "license.not_registered": "Hayır license kayıtlı — using Topluluk tier", + "license.technicians": "Teknisyenler", + "license.keys": "OEM Anahtarlar", + "license.instance": "Instance ID", + "license.expires": "Expires", + "license.features": "Özellik Access", + "license.register": "Register Lisans Anahtar", + "license.register_desc": "Yapıştır your license key to unlock Pro or Kurumsal features.", + "license.key_placeholder": "eyJhbGciOiJIUzI1NiIs...", + "license.activate": "Activate Lisans", + "license.get_pro": "Get Pro Lisans", + "license.deactivate": "Deactivate Lisans", + "license.deactivate_title": "Deactivate Lisans?", + "license.deactivate_desc": "This will revert to Topluluk tier. Pro features will be disabled.", + "license.deactivate_confirm": "Evet, Deactivate", + "license.registered": "Lisans kayıtlı başarıyla", + "license.deactivated": "Lisans deactivated — Topluluk tier", + "license.dev_tools": "Development Tools", + "license.dev_desc": "Generate test licenses for development. Only available on localhost.", + "license.gen_pro": "Generate Pro", + "license.gen_enterprise": "Generate Kurumsal", + "license.dev_generated": "Dev license generated", + "license.purchase_options": "Purchase Seçenekler", + "license.purchase_desc": "Choose the payment method that works for your region.", + "license.intl_payment": "International Ödeme", + "license.intl_payment_desc": "Pay with credit card, PayPal, or other international methods. Instant license delivery.", + "license.ru_payment": "Russia & CIS — Fatura Ödeme", + "license.ru_payment_desc": "Due to international payment restrictions, Russian and CIS companies can purchase via bank transfer (wire transfer). We accept payments in RUB, USD, or EUR to our business account.", + "license.ru_payment_methods": "Accepted: bank wire transfer, USDT/BTC, payment through Turkish or UAE intermediary banks.", + "license.request_invoice": "Request Fatura", + "nav.subscription": "Abonelik", + "sub.title": "Abonelik", + "sub.description": "Manage your AnahtarGate subscription, payment method, and license.", + "sub.tab_plan": "Plan & Usage", + "sub.tab_payment": "Ödeme", + "sub.tab_key": "Lisans Anahtar", + "sub.tab_billing": "Faturalandırma", + "sub.registered_to": "Kayıtlı to {{email}}", + "sub.free_tier": "Ücretsiz tier — limited to 1 technician and 50 keys", + "sub.technicians": "Teknisyenler", + "sub.keys": "Anahtarlar", + "sub.expires": "Expires", + "sub.expiring_soon": "Your subscription expires soon. Renew to keep Pro features.", + "sub.renew_now": "Renew Hayırw", + "sub.current": "Mevcut", + "sub.free": "Ücretsiz", + "sub.month": "mo", + "sub.plan_1_tech": "1 technician", + "sub.plan_50_keys": "50 OEM keys", + "sub.plan_activation": "Windows activation", + "sub.plan_hardware": "Donanım collection", + "sub.plan_dashboard": "Kontrol Paneli", + "sub.plan_no_support": "Topluluk support", + "sub.plan_unlimited_techs": "Sınırsız technicians", + "sub.plan_unlimited_keys": "Sınırsız OEM keys", + "sub.plan_compliance": "QC compliance", + "sub.plan_integrations": "Entegrasyonlar (osTicket, 1C)", + "sub.plan_backups": "Automated backups", + "sub.plan_branding": "White-label branding", + "sub.plan_upgrade": "Sistem upgrade wizard", + "sub.plan_priority": "Öncelik support", + "sub.year": "yr", + "sub.plan_work_orders": "Work orders & production tracking", + "sub.plan_cbr": "Bilgisayar Yapım Raporus (CBR)", + "sub.plan_key_pool": "Anahtar havuz alerts & DPK import", + "sub.plan_task_pipeline": "Özel task pipeline", + "sub.plan_email_support": "E-posta support", + "sub.plan_hardware_binding": "Donanım binding verification", + "sub.plan_everything_pro": "Everything in Pro", + "sub.plan_multi_site": "Multi-site deployment", + "sub.plan_api": "API access", + "sub.plan_sla": "SLA & dedicated support", + "sub.plan_custom": "Özel integrations", + "sub.upgrade_to_pro": "Güncelleme to Pro", + "sub.upgrade_to_enterprise": "Güncelleme to Kurumsal", + "sub.feature_access": "Özellik Access", + "sub.intl_payment": "International Ödeme", + "sub.intl_payment_desc": "Pay with credit card, PayPal, or other international methods. Instant license delivery.", + "sub.pay_github": "Pay via GitHub Sponsors", + "sub.intl_note": "After payment, you will receive a license key via email. Yapıştır it in the Lisans Anahtar tab.", + "sub.ru_payment": "Russia & CIS — Fatura Ödeme", + "sub.ru_payment_desc": "Due to international payment restrictions, Russian and CIS companies can purchase via bank transfer. We accept RUB, USD, or EUR.", + "sub.bank_transfer": "Bank Transfer", + "sub.crypto": "Cryptocurrency", + "sub.intermediary": "Intermediary", + "sub.request_invoice": "Request Fatura", + "sub.ru_note": "After payment confirmation, we will send your license key within 1 business day.", + "sub.active_license": "Aktif Lisans", + "sub.tier": "Katman", + "sub.licensed_to": "Lisansd To", + "sub.instance_id": "Instance ID", + "sub.deactivate": "Deactivate Lisans", + "sub.deactivate_title": "Deactivate Lisans?", + "sub.deactivate_desc": "This will revert to Topluluk tier. Pro features will be disabled.", + "sub.deactivate_confirm": "Evet, Deactivate", + "sub.register_key": "Register Lisans Anahtar", + "sub.register_desc": "Yapıştır the JWT license key you received after payment.", + "sub.activate_key": "Activate Lisans", + "sub.dev_tools": "Development Tools", + "sub.dev_desc": "Generate test licenses for development. Only available on localhost.", + "sub.gen_pro": "Generate Pro", + "sub.gen_enterprise": "Generate Kurumsal", + "sub.billing_history": "Faturalandırma Geçmiş", + "sub.billing_desc": "Your payment and license history.", + "sub.activated_on": "Activated", + "sub.status_active": "Aktif", + "sub.until": "until", + "sub.billing_note": "For detailed payment history, check your GitHub Sponsors dashboard or contact sales@keygate.dev", + "sub.no_billing": "Hayır billing history. You are on the free Topluluk tier.", + "sub.upgrade_now": "Güncelleme Hayırw", + "sub.instance_info": "Instance Bilgirmation", + "sub.max_techs": "Max Teknisyenler", + "sub.max_keys": "Max Anahtarlar", + "sub.lemon_title": "LemonSqueezy (Recommended)", + "sub.lemon_desc": "Credit card, PayPal, Apple Pay, Google Pay. Works worldwide. Instant license delivery.", + "sub.pay_lemon": "Buy Lisans", + "sub.github_title": "GitHub Sponsors", + "sub.github_desc": "Subscribe through GitHub. Great if you already have a GitHub account.", + "settings.languages_title": "Diller", + "settings.languages_desc": "Etkinleştir or disable languages available in the admin panel. English is always enabled as fallback.", + "settings.languages_saved": "Dil settings saved", + "settings.default": "Varsayılan", + "settings.languages_enabled": "languages enabled", + "nav.task_pipeline": "Görev Hattı", + "pipeline.title": "Görev Hattı", + "pipeline.for_product_line": "Configuring pipeline for", + "pipeline.active_tasks": "Aktif Görev Hattı", + "pipeline.drag_hint": "Reorder tasks using arrows. Görevler run top-to-bottom during activation.", + "pipeline.no_tasks": "Hayır tasks yapılandırıldı. Ekle tasks from the library below.", + "pipeline.add_task": "Ekle Görev", + "pipeline.add_hint": "Click a task to add it to the pipeline", + "pipeline.save_pipeline": "Kaydet İş Hattı", + "pipeline.template_library": "Görev Şablon Library", + "pipeline.library_desc": "Global task templates available for all product lines", + "pipeline.new_template": "Yeni Görev Şablon", + "pipeline.edit_template": "Düzenle Görev Şablon", + "pipeline.task_key": "Görev Anahtar", + "pipeline.task_name": "Ekran Ad", + "pipeline.code": "PowerShell Code", + "pipeline.timeout": "Zaman Aşımı (sec)", + "pipeline.on_failure": "On Failure", + "pipeline.icon": "Simge", + "pipeline.failure_stop": "Durdur İş Hattı", + "pipeline.failure_skip": "Atla & Continue", + "pipeline.failure_warn": "Warn & Continue", + "pipeline.built_in": "Built-in", + "pipeline.custom": "Özel", + "pipeline.overridden": "Overridden", + "pipeline.customize": "Özelize", + "pipeline.delete_template_title": "Sil Görev Şablon?", + "pipeline.delete_template_desc": "This will remove the template and unassign it from all product lines.", + "toast.task_template_saved": "Görev template saved", + "toast.task_template_deleted": "Görev template deleted", + "toast.pipeline_saved": "Görev pipeline saved", + "nav.work_orders": "İş Emirleri", + "nav.key_pool": "Anahtar Havuzu", + "work_orders.title": "İş Emirleri", + "work_orders.new": "Yeni İş Emri", + "work_orders.edit": "Düzenle İş Emri", + "work_orders.search": "Ara orders...", + "work_orders.col_number": "WO #", + "work_orders.col_customer": "Müşteri", + "work_orders.col_qty": "Qty", + "work_orders.col_status": "Durum", + "work_orders.col_priority": "Öncelik", + "work_orders.col_due": "Due", + "work_orders.col_created": "Oluşturd", + "work_orders.customer_name": "Müşteri Ad", + "work_orders.customer_email": "Müşteri E-posta", + "work_orders.customer_ref": "Müşteri PO #", + "work_orders.batch": "Toplu İşlem Number", + "work_orders.quantity": "Quantity", + "work_orders.due_date": "Due Tarih", + "work_orders.priority": "Öncelik", + "work_orders.status": "Durum", + "work_orders.shipping_method": "Kargo Method", + "work_orders.shipping_tracking": "Tracking Number", + "work_orders.internal_notes": "Internal Hayırtlar", + "common.all_statuses": "Tümü Durumes", + "empty.work_orders": "Hayır work orders yet", + "toast.work_order_saved": "Work order {{num}} saved", + "toast.work_order_deleted": "Work order deleted", + "toast.shipping_updated": "Kargo status updated", + "pool.title": "Anahtar Havuzu", + "pool.import_keys": "İçe Aktar Anahtarlar", + "pool.import_desc": "Yükle a CSV, TXT, or XML file with product keys.", + "pool.import_history": "İçe Aktar Geçmiş", + "pool.batch_name": "Toplu İşlem Ad", + "pool.product_edition": "Ürün Düzenleion", + "pool.key_file": "Anahtar Dosya", + "pool.import": "İçe Aktar", + "pool.total": "Toplam", + "pool.unused": "Unkullanıldı", + "pool.allocated": "Tahsis Edildi", + "pool.bad": "Bad", + "pool.thresholds": "Uyarılar", + "pool.low_threshold": "Düşük Uyarı", + "pool.critical_threshold": "Kritik Uyarı", + "pool.col_batch": "Toplu İşlem", + "pool.col_edition": "Düzenleion", + "pool.col_imported": "İçe Aktared", + "pool.col_dupes": "Dupes", + "pool.col_status": "Durum", + "pool.col_date": "Tarih", + "empty.key_pool": "Hayır keys in the system yet. İçe Aktar keys to get started.", + "toast.pool_config_saved": "Anahtar havuz config saved", + "toast.dpk_imported": "İçe Aktared {{count}} keys", + "toast.binding_released": "Donanım binding released", + "cbr.title": "Bilgisayar Yapım Raporus", + "cbr.search": "Ara by order, serial, fingerprint...", + "cbr.report_detail": "Rapor", + "cbr.order": "Sipariş", + "cbr.batch": "Toplu İşlem", + "cbr.activation": "Aktivasyon", + "cbr.shipping": "Kargo", + "cbr.motherboard": "Anakart", + "cbr.serial": "MB Seri", + "cbr.cpu": "CPU", + "cbr.ram": "RAM", + "cbr.gpu": "GPU", + "cbr.os": "OS", + "cbr.key": "Ürün Anahtar", + "cbr.fingerprint": "HW Parmak İzi", + "cbr.technician": "Teknisyen", + "cbr.product_line": "Ürün Line", + "cbr.qc": "QC Durum", + "cbr.date": "Tarih", + "cbr.update_shipping": "Güncelle Kargo", + "cbr.auto_generated": "Raporlar are generated automatically during activation", + "cbr.col_uuid": "Rapor ID", + "cbr.col_order": "Sipariş", + "cbr.col_mb": "Anakart", + "cbr.col_activation": "Aktivasyon", + "cbr.col_shipping": "Kargo", + "cbr.col_technician": "Tech", + "cbr.col_date": "Tarih", + "empty.cbr": "Hayır build reports yet", + "bindings.title": "Donanım Bağlamaları", + "bindings.subtitle": "Track which product keys are bound to which hardware", + "bindings.conflicts": "{{count}} Binding Conflicts Detected", + "bindings.conflicts_desc": "The following keys are bound to different hardware than where they were originally activated.", + "bindings.search": "Ara by fingerprint, serial, MAC...", + "bindings.auto_created": "Bindings are created automatically when keys are activated", + "bindings.col_fingerprint": "Cihaz Parmak İzi", + "bindings.col_mb_serial": "MB Seri", + "bindings.col_uuid": "Sistem UUID", + "bindings.col_mac": "MAC", + "bindings.col_key": "Ürün Anahtar", + "bindings.col_status": "Durum", + "bindings.col_bound_at": "Bound", + "bindings.release": "Sürüm", + "bindings.release_title": "Sürüm Donanım Bağlama?", + "bindings.release_desc": "This will unbind the product key from this hardware. The key can then be kullanıldı on different hardware.", + "bindings.release_confirm": "Evet, Sürüm", + "empty.bindings": "Hayır hardware bindings yet", + "dpk.title": "DPK Toplu İşlem İçe Aktar", + "dpk.subtitle": "İçe Aktar OEM product keys from Micros/t DPK delivery files or CSV", + "dpk.import_new": "İçe Aktar Yeni Toplu İşlem", + "dpk.batch_name": "Toplu İşlem Ad", + "dpk.batch_name_placeholder": "e.g. Micros/t-2026-Q1", + "dpk.product_edition": "Ürün Düzenleion", + "dpk.edition_placeholder": "e.g. Windows 11 Pro OEM", + "dpk.file": "Anahtar Dosya", + "dpk.format_info": "Supported formats", + "dpk.format_csv": "One key per line, or columns: key,edition,type", + "dpk.format_txt": "One key per line (XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)", + "dpk.format_xml": "Micros/t DPK delivery XML format", + "dpk.import_btn": "İçe Aktar Anahtarlar", + "dpk.imported": "imported", + "dpk.duplicates": "duplicates", + "dpk.failed": "başarısız oldu", + "dpk.history": "İçe Aktar Geçmiş", + "dpk.col_name": "Toplu İşlem Ad", + "dpk.col_edition": "Düzenleion", + "dpk.col_total": "Toplam", + "dpk.col_imported": "İçe Aktared", + "dpk.col_dupes": "Dupes", + "dpk.col_status": "Durum", + "dpk.col_source": "Source", + "dpk.col_by": "By", + "dpk.col_date": "Tarih", + "empty.dpk": "Hayır DPK toplu işlemes imported yet" +} diff --git a/FINAL_PRODUCTION_SYSTEM/frontend/src/test/api-contracts.test.ts b/FINAL_PRODUCTION_SYSTEM/frontend/src/test/api-contracts.test.ts index 73a0660..77c2e0e 100644 --- a/FINAL_PRODUCTION_SYSTEM/frontend/src/test/api-contracts.test.ts +++ b/FINAL_PRODUCTION_SYSTEM/frontend/src/test/api-contracts.test.ts @@ -135,6 +135,8 @@ const BACKEND_ACTIONS: Record qc_list_compliance_results: { method: 'GET', csrf: false }, qc_list_compliance_grouped: { method: 'GET', csrf: false }, qc_get_stats: { method: 'GET', csrf: false }, + qc_recheck_count: { method: 'GET', csrf: false }, + qc_recheck_historical: { method: 'POST', csrf: true }, // product lines & variants (partition QC) get_product_lines: { method: 'GET', csrf: false }, From d235c991a95cbd39376d823ea5d4d6ba26f51824 Mon Sep 17 00:00:00 2001 From: Ayoub Mohamed Samir <7agtyadmin@gmail.com> Date: Thu, 26 Mar 2026 15:59:35 +0200 Subject: [PATCH 02/15] Add production deployment script for Ubuntu 22 + Docker (#14) One-command installer for Proxmox/Ubuntu VMs: - Auto-installs Docker if missing - Clones KeyGate from GitHub - Generates .env with secure random passwords - Creates self-signed SSL cert - Builds and starts Docker stack - Creates admin user with super_admin role - Verifies health endpoint - Prints access URLs and credentials Usage: curl -fsSL https://raw.githubusercontent.com/.../install.sh | sudo bash Co-authored-by: ChesnoTech <263363000+ChesnoTech@users.noreply.github.com> --- deploy/install.sh | 225 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 deploy/install.sh diff --git a/deploy/install.sh b/deploy/install.sh new file mode 100644 index 0000000..8336121 --- /dev/null +++ b/deploy/install.sh @@ -0,0 +1,225 @@ +#!/bin/bash +# ============================================================ +# KeyGate Production Deployment Script +# For: Ubuntu 22.04 + aaPanel + Docker +# ============================================================ +# Usage: +# curl -fsSL https://raw.githubusercontent.com/ChesnoTech/KeyGate/main/deploy/install.sh | bash +# OR +# wget -qO- https://raw.githubusercontent.com/ChesnoTech/KeyGate/main/deploy/install.sh | bash +# ============================================================ + +set -e + +KEYGATE_DIR="/opt/keygate" +KEYGATE_REPO="https://github.com/ChesnoTech/KeyGate.git" +KEYGATE_BRANCH="main" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +log() { echo -e "${GREEN}[KeyGate]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +err() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; } + +# ── Pre-flight checks ───────────────────────────────────── +echo "" +echo -e "${CYAN}╔══════════════════════════════════════════╗${NC}" +echo -e "${CYAN}║ KeyGate Production Installer ║${NC}" +echo -e "${CYAN}║ OEM Activation & QC Platform ║${NC}" +echo -e "${CYAN}╚══════════════════════════════════════════╝${NC}" +echo "" + +# Must be root +if [ "$EUID" -ne 0 ]; then + err "Please run as root: sudo bash install.sh" +fi + +# Check OS +if ! grep -q "Ubuntu 22" /etc/os-release 2>/dev/null; then + warn "This script is designed for Ubuntu 22.04. Your OS may work but is untested." + read -p "Continue? [y/N] " -n 1 -r + echo + [[ $REPLY =~ ^[Yy]$ ]] || exit 1 +fi + +# ── Install Docker ───────────────────────────────────────── +if ! command -v docker &>/dev/null; then + log "Installing Docker..." + apt-get update -qq + apt-get install -y -qq ca-certificates curl gnupg + install -m 0755 -d /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + chmod a+r /etc/apt/keyrings/docker.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null + apt-get update -qq + apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + systemctl enable docker + systemctl start docker + log "Docker installed successfully" +else + log "Docker already installed: $(docker --version)" +fi + +# ── Install Git ──────────────────────────────────────────── +if ! command -v git &>/dev/null; then + log "Installing Git..." + apt-get install -y -qq git +fi + +# ── Clone or update KeyGate ──────────────────────────────── +if [ -d "$KEYGATE_DIR" ]; then + log "Updating existing installation..." + cd "$KEYGATE_DIR" + git fetch origin + git checkout "$KEYGATE_BRANCH" + git pull origin "$KEYGATE_BRANCH" +else + log "Cloning KeyGate..." + git clone -b "$KEYGATE_BRANCH" "$KEYGATE_REPO" "$KEYGATE_DIR" + cd "$KEYGATE_DIR" +fi + +# ── Generate .env file ───────────────────────────────────── +if [ ! -f "$KEYGATE_DIR/.env" ]; then + log "Generating .env file with secure passwords..." + + DB_ROOT_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24) + DB_USER_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24) + REDIS_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24) + + cat > "$KEYGATE_DIR/.env" << ENVEOF +# KeyGate Production Environment +# Generated: $(date -Iseconds) + +# Database +DB_HOST=oem-activation-db +DB_PORT=3306 +DB_NAME=oem_activation +DB_USER=oem_user +DB_PASS=${DB_USER_PASS} +MARIADB_ROOT_PASSWORD=${DB_ROOT_PASS} +MARIADB_DATABASE=oem_activation +MARIADB_USER=oem_user +MARIADB_PASSWORD=${DB_USER_PASS} + +# Redis +REDIS_HOST=oem-activation-redis +REDIS_PORT=6379 +REDIS_PASSWORD=${REDIS_PASS} + +# Application +APP_TIMEZONE=UTC +APP_ENV=production + +# phpMyAdmin (remove in production if not needed) +PMA_HOST=oem-activation-db +PMA_PORT=3306 +ENVEOF + + chmod 600 "$KEYGATE_DIR/.env" + log "Generated .env with secure random passwords" + echo "" + echo -e "${YELLOW}IMPORTANT: Save these credentials somewhere safe:${NC}" + echo -e " DB Root Password: ${RED}${DB_ROOT_PASS}${NC}" + echo -e " DB User Password: ${RED}${DB_USER_PASS}${NC}" + echo -e " Redis Password: ${RED}${REDIS_PASS}${NC}" + echo "" +else + log ".env already exists, keeping existing config" +fi + +# ── Generate SSL certificate ─────────────────────────────── +SSL_DIR="$KEYGATE_DIR/ssl" +if [ ! -f "$SSL_DIR/server.crt" ]; then + log "Generating self-signed SSL certificate..." + mkdir -p "$SSL_DIR" + openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout "$SSL_DIR/server.key" \ + -out "$SSL_DIR/server.crt" \ + -subj "/C=US/ST=State/L=City/O=KeyGate/CN=localhost" 2>/dev/null + log "SSL certificate generated (self-signed, valid 365 days)" + warn "For production, replace ssl/server.crt and ssl/server.key with real certificates" +else + log "SSL certificate already exists" +fi + +# ── Build and start ──────────────────────────────────────── +log "Building Docker images..." +cd "$KEYGATE_DIR" +docker compose build --quiet + +log "Starting KeyGate..." +docker compose up -d + +# ── Wait for health ──────────────────────────────────────── +log "Waiting for services to start..." +RETRIES=30 +until curl -sf http://localhost:8080/api/health.php > /dev/null 2>&1 || [ $RETRIES -eq 0 ]; do + RETRIES=$((RETRIES - 1)) + sleep 2 +done + +if [ $RETRIES -eq 0 ]; then + warn "Health check didn't pass in 60 seconds. Check: docker compose logs" +else + HEALTH=$(curl -s http://localhost:8080/api/health.php) + VERSION=$(echo "$HEALTH" | grep -o '"version":"[^"]*"' | cut -d'"' -f4) + log "Health check passed! Version: ${VERSION:-unknown}" +fi + +# ── Create admin user ────────────────────────────────────── +log "Setting up admin account..." +docker compose exec -T web php -r " +require '/var/www/html/activate/config.php'; + +// Check if admin exists +\$stmt = \$pdo->prepare('SELECT id FROM admin_users WHERE username = ?'); +\$stmt->execute(['admin']); +if (\$stmt->fetch()) { + echo \"Admin user already exists\n\"; + exit(0); +} + +// Create admin +\$hash = password_hash('Admin2024!', PASSWORD_BCRYPT, ['cost' => 10]); + +// Get or create super_admin role +\$stmt = \$pdo->prepare('SELECT id FROM acl_roles WHERE role_key = ?'); +\$stmt->execute(['super_admin']); +\$role = \$stmt->fetch(); +\$roleId = \$role ? \$role['id'] : null; + +if (!\$roleId) { + \$pdo->exec(\"INSERT INTO acl_roles (role_key, display_name, description) VALUES ('super_admin', 'Super Admin', 'Full system access')\"); + \$roleId = \$pdo->lastInsertId(); +} + +\$stmt = \$pdo->prepare('INSERT INTO admin_users (username, password_hash, full_name, role, custom_role_id) VALUES (?, ?, ?, ?, ?)'); +\$stmt->execute(['admin', \$hash, 'System Administrator', 'super_admin', \$roleId]); +echo \"Admin user created: admin / Admin2024!\n\"; +echo \"CHANGE THIS PASSWORD IMMEDIATELY!\n\"; +" 2>/dev/null + +# ── Summary ──────────────────────────────────────────────── +echo "" +echo -e "${GREEN}╔══════════════════════════════════════════╗${NC}" +echo -e "${GREEN}║ KeyGate Installation Complete! ║${NC}" +echo -e "${GREEN}╚══════════════════════════════════════════╝${NC}" +echo "" +echo -e " Admin Panel: ${CYAN}http://$(hostname -I | awk '{print $1}'):8080${NC}" +echo -e " HTTPS: ${CYAN}https://$(hostname -I | awk '{print $1}'):8443${NC}" +echo -e " phpMyAdmin: ${CYAN}http://$(hostname -I | awk '{print $1}'):8081${NC}" +echo -e " Health: ${CYAN}http://$(hostname -I | awk '{print $1}'):8080/api/health.php${NC}" +echo "" +echo -e " Login: ${YELLOW}admin / Admin2024!${NC}" +echo -e " ${RED}Change the admin password immediately!${NC}" +echo "" +echo -e " Logs: docker compose -C $KEYGATE_DIR logs -f" +echo -e " Stop: docker compose -C $KEYGATE_DIR down" +echo -e " Update: cd $KEYGATE_DIR && git pull && docker compose up -d --build" +echo "" From 42283ab1eea586645ed66c1ed68ad24cecc73ac4 Mon Sep 17 00:00:00 2001 From: Ayoub Mohamed Samir <7agtyadmin@gmail.com> Date: Fri, 27 Mar 2026 14:51:33 +0200 Subject: [PATCH 03/15] Fix deploy script + Docker healthcheck bugs found during testing (#15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit deploy/install.sh: - Fix: acl_roles column is role_name not role_key - Fix: admin_users.email is required (NOT NULL) - Fix: set must_change_password=0 for initial admin - Use ON DUPLICATE KEY UPDATE for idempotent role creation docker-compose.yml: - Fix: healthcheck was hitting /activate/ (404) instead of /api/health.php — DocumentRoot IS /var/www/html/activate so the correct internal URL is /api/health.php Co-authored-by: ChesnoTech <263363000+ChesnoTech@users.noreply.github.com> --- deploy/install.sh | 15 ++++----------- docker-compose.yml | 2 +- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/deploy/install.sh b/deploy/install.sh index 8336121..eaca034 100644 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -189,18 +189,11 @@ if (\$stmt->fetch()) { \$hash = password_hash('Admin2024!', PASSWORD_BCRYPT, ['cost' => 10]); // Get or create super_admin role -\$stmt = \$pdo->prepare('SELECT id FROM acl_roles WHERE role_key = ?'); -\$stmt->execute(['super_admin']); -\$role = \$stmt->fetch(); -\$roleId = \$role ? \$role['id'] : null; - -if (!\$roleId) { - \$pdo->exec(\"INSERT INTO acl_roles (role_key, display_name, description) VALUES ('super_admin', 'Super Admin', 'Full system access')\"); - \$roleId = \$pdo->lastInsertId(); -} +\$pdo->exec(\"INSERT INTO acl_roles (role_name, display_name, description, role_type, is_system_role) VALUES ('super_admin', 'Super Admin', 'Full system access', 'admin', 1) ON DUPLICATE KEY UPDATE id=id\"); +\$roleId = \$pdo->query(\"SELECT id FROM acl_roles WHERE role_name = 'super_admin'\")->fetchColumn(); -\$stmt = \$pdo->prepare('INSERT INTO admin_users (username, password_hash, full_name, role, custom_role_id) VALUES (?, ?, ?, ?, ?)'); -\$stmt->execute(['admin', \$hash, 'System Administrator', 'super_admin', \$roleId]); +\$stmt = \$pdo->prepare('INSERT INTO admin_users (username, password_hash, full_name, email, role, custom_role_id, must_change_password) VALUES (?, ?, ?, ?, ?, ?, 0)'); +\$stmt->execute(['admin', \$hash, 'System Administrator', 'admin@keygate.local', 'super_admin', \$roleId]); echo \"Admin user created: admin / Admin2024!\n\"; echo \"CHANGE THIS PASSWORD IMMEDIATELY!\n\"; " 2>/dev/null diff --git a/docker-compose.yml b/docker-compose.yml index 328a219..05b7563 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,7 +37,7 @@ services: networks: - oem-network healthcheck: - test: ["CMD", "curl", "-f", "http://localhost/activate/"] + test: ["CMD", "curl", "-f", "http://localhost/api/health.php"] interval: 30s timeout: 10s retries: 3 From 2a9d94f85a826a6d48144d427a89f32afff4fabe Mon Sep 17 00:00:00 2001 From: Ayoub Mohamed Samir <7agtyadmin@gmail.com> Date: Wed, 6 May 2026 00:12:15 +0300 Subject: [PATCH 04/15] Integrate graphify knowledge graph into project workflow (#16) Wire up the graphify skill (~/.claude/skills/graphify) so it's enforced project-wide, with auto-rebuild on commits and code edits. Changes: - .claude/settings.json: PreToolUse hook for grep/find tools now points to FINAL_PRODUCTION_SYSTEM/graphify-out/ (the actual graph location). - Added PostToolUse hook that tracks Edit/Write/MultiEdit on code files and a Stop hook that triggers `graphify update FINAL_PRODUCTION_SYSTEM` in background when changes touched FINAL_PRODUCTION_SYSTEM/. - CLAUDE.md: graphify rules now reference FINAL_PRODUCTION_SYSTEM/graphify-out/ with correct commands. - .gitignore: ignore graphify-out/ artifacts (12MB+ graph.json + 25MB cache), rebuilt locally by post-commit hook. - Git hooks (post-commit + post-checkout) installed via `graphify hook install`. Initial graph state: 13,138 nodes, 19,511 edges, 1,260 communities, AST-only extraction. Co-authored-by: ChesnoTech <263363000+ChesnoTech@users.noreply.github.com> --- .claude/settings.json | 33 +++++++++++++++++++++++++++++++++ .gitignore | 4 ++++ CLAUDE.md | 11 +++++++++++ 3 files changed, 48 insertions(+) diff --git a/.claude/settings.json b/.claude/settings.json index 9787a30..eef9d0d 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -11,6 +11,39 @@ "statusMessage": "Checking branding consistency..." } ] + }, + { + "matcher": "Edit|Write|MultiEdit|NotebookEdit", + "hooks": [ + { + "type": "command", + "command": "FP=$(jq -r '.tool_input.file_path // \"\"' 2>/dev/null); case \"$FP\" in *FINAL_PRODUCTION_SYSTEM*) echo \"$FP\" >> /tmp/keygate_graph_dirty 2>/dev/null ;; esac; true", + "timeout": 5 + } + ] + } + ], + "Stop": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "if [ -s /tmp/keygate_graph_dirty ]; then COUNT=$(wc -l < /tmp/keygate_graph_dirty); rm -f /tmp/keygate_graph_dirty; (cd \"$PWD\" && command -v graphify >/dev/null 2>&1 && graphify update FINAL_PRODUCTION_SYSTEM > /tmp/keygate_graph.log 2>&1 &) ; echo \"{\\\"systemMessage\\\":\\\"\\ud83d\\udd04 graphify: $COUNT code file(s) changed \\u2014 graph rebuild started in background.\\\"}\"; fi; true", + "timeout": 5 + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "CMD=$(python3 -c \"import json,sys; d=json.load(sys.stdin); print(d.get('tool_input',d).get('command',''))\" 2>/dev/null || true); case \"$CMD\" in *grep*|*rg\\ *|*ripgrep*|*find\\ *|*fd\\ *|*ack\\ *|*ag\\ *) [ -f FINAL_PRODUCTION_SYSTEM/graphify-out/graph.json ] && echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"additionalContext\":\"graphify: Knowledge graph exists at FINAL_PRODUCTION_SYSTEM/graphify-out/. Read GRAPH_REPORT.md for god nodes and community structure before searching raw files. Or run: graphify query \\\"\\\" / graphify explain \\\"\\\" / graphify path \\\"\\\" \\\"\\\".\"}}' || true ;; esac" + } + ] } ] } diff --git a/.gitignore b/.gitignore index c255ad8..8cb5ce7 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,10 @@ ssl/*.pem logs/ *.log +# ── graphify output (auto-rebuilt by post-commit hook, AST-only, no LLM) ─ +# Don't commit 12MB+ artifacts — every dev rebuilds locally on first commit. +**/graphify-out/ + # ── PHP Dependencies (managed by Composer) ──────────────── FINAL_PRODUCTION_SYSTEM/vendor/ diff --git a/CLAUDE.md b/CLAUDE.md index 0d9bed8..c9a60a6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -434,3 +434,14 @@ DocumentRoot is `/var/www/html/activate` — API URLs are `/api/...` from inside | `App.tsx` | React router with 24 routes | | `app-sidebar.tsx` | Navigation with 5 groups, 30 items | | `api-contracts.test.ts` | Backend action registry validation | + +## graphify + +KeyGate has a graphify knowledge graph at `FINAL_PRODUCTION_SYSTEM/graphify-out/` (13,138 nodes, 19,511 edges, 1,260 communities — AST-only). + +Rules: +- Before answering architecture or codebase questions, read `FINAL_PRODUCTION_SYSTEM/graphify-out/GRAPH_REPORT.md` for god nodes and community structure +- If `FINAL_PRODUCTION_SYSTEM/graphify-out/wiki/index.md` exists, navigate it instead of reading raw files +- For cross-module "how does X relate to Y" questions, prefer `graphify query ""`, `graphify path "" ""`, or `graphify explain ""` over grep — these traverse the graph's EXTRACTED + INFERRED edges instead of scanning files +- After modifying code files in this session, run `graphify update FINAL_PRODUCTION_SYSTEM` to keep the graph current (AST-only, no API cost) +- Git post-commit + post-checkout hooks auto-rebuild graph on commits / branch switches From e457f3d0d74da1b0d9be3b194aa22f4a442ceb57 Mon Sep 17 00:00:00 2001 From: Ayoub Mohamed Samir <7agtyadmin@gmail.com> Date: Wed, 6 May 2026 00:35:27 +0300 Subject: [PATCH 05/15] Enforce caveman + superpowers + graphify skills project-wide (#17) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires up the user's full personal skill stack so every Claude Code session in this project gets reminded which skills to use and when. Changes: - CLAUDE.md: New "Personal Skills (Enforced)" section documenting all three skills, their triggers, and the workflow rules. - caveman: full mode by default, drop articles/filler. - superpowers: table mapping task → skill (brainstorming, writing-plans, executing-plans, TDD, systematic-debugging, verification-before- completion, dispatching-parallel-agents, etc.). - graphify: existing knowledge graph rules consolidated under this section. - .claude/settings.json: Added SessionStart hook that injects a reminder about all 3 skills as additionalContext at session start. Co-authored-by: ChesnoTech <263363000+ChesnoTech@users.noreply.github.com> --- .claude/settings.json | 11 +++++++++++ CLAUDE.md | 32 +++++++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index eef9d0d..1b15dff 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,5 +1,16 @@ { "hooks": { + "SessionStart": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"SessionStart\",\"additionalContext\":\"KeyGate enforces 3 personal skills:\\n1. caveman (full mode) — terse responses, no filler. Code/commits stay normal.\\n2. superpowers — invoke matching skill before acting (brainstorming, writing-plans, executing-plans, test-driven-development, systematic-debugging, verification-before-completion).\\n3. graphify — read FINAL_PRODUCTION_SYSTEM/graphify-out/GRAPH_REPORT.md before architecture questions. Use graphify query/path/explain instead of grep for cross-module questions.\"}}'" + } + ] + } + ], "PostToolUse": [ { "matcher": "Bash", diff --git a/CLAUDE.md b/CLAUDE.md index c9a60a6..00b9974 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -435,9 +435,35 @@ DocumentRoot is `/var/www/html/activate` — API URLs are `/api/...` from inside | `app-sidebar.tsx` | Navigation with 5 groups, 30 items | | `api-contracts.test.ts` | Backend action registry validation | -## graphify - -KeyGate has a graphify knowledge graph at `FINAL_PRODUCTION_SYSTEM/graphify-out/` (13,138 nodes, 19,511 edges, 1,260 communities — AST-only). +## Personal Skills (Enforced) + +This project uses the user's global personal skill stack. All three are MANDATORY for the workflow. + +### 1. caveman (token efficiency) +- **Mode**: caveman `full` is active by default. Status line shows `[CAVEMAN]`. +- **Rules**: drop articles, filler, pleasantries, hedging. Fragments OK. Code/commits/PRs/security warnings stay normal English. +- **Triggers**: auto on every response. Off only by user typing `stop caveman` or `normal mode`. +- **Other caveman skills**: `caveman:caveman-commit` (commit messages), `caveman:caveman-review` (PR review), `caveman:compress` (compress memory files). + +### 2. superpowers (workflow enforcement) +Always invoke the matching superpowers skill via the Skill tool BEFORE acting: + +| Task | Skill | +|------|-------| +| Any creative work / new feature / behavior change | `superpowers:brainstorming` | +| Multi-step task with spec | `superpowers:writing-plans` then `superpowers:executing-plans` | +| Implementing feature/bugfix code | `superpowers:test-driven-development` | +| Encountering bug / test failure | `superpowers:systematic-debugging` | +| About to claim "done", "fixed", "passing" | `superpowers:verification-before-completion` | +| Multiple independent tasks | `superpowers:dispatching-parallel-agents` | +| Receiving code review feedback | `superpowers:receiving-code-review` | +| Completing branch / merge time | `superpowers:finishing-a-development-branch` | +| Creating/editing skills | `superpowers:writing-skills` | +| Starting any conversation | `superpowers:using-superpowers` | + +### 3. graphify (codebase knowledge) + +Knowledge graph at `FINAL_PRODUCTION_SYSTEM/graphify-out/` — 13,138 nodes, 19,511 edges, 1,260 communities, AST-only. PreToolUse hook reminds when grep/find/rg used. Stop hook auto-rebuilds graph if code edited this session. Git post-commit + post-checkout hooks rebuild on commits / branch switches. Rules: - Before answering architecture or codebase questions, read `FINAL_PRODUCTION_SYSTEM/graphify-out/GRAPH_REPORT.md` for god nodes and community structure From b27a54003c32efc075b097e36a9d196c399afd03 Mon Sep 17 00:00:00 2001 From: Ayoub Mohamed Samir <7agtyadmin@gmail.com> Date: Thu, 7 May 2026 06:24:38 +0300 Subject: [PATCH 06/15] Fix installer DB connection on aaPanel + better error messages (#18) The web installer failed on aaPanel-style stacks because PDO interpreted 'localhost' as a Unix-socket connection request, then looked for the default socket path (which doesn't match aaPanel's /tmp/mysql.sock or /www/server/mysql/mysql.sock) and failed with "No such file or directory". Code review fixes: 1. Default DB host changed from `localhost` -> `127.0.0.1` to force TCP. 2. Auto-coerce `localhost` -> `127.0.0.1` whenever no explicit Unix socket path was supplied (both in handleTestDb() and getInstallerPdo()). 3. New optional "Socket Path" field in step 2 (collapsible Advanced section) for installs that genuinely require a Unix socket. 4. New helper installerBuildDsn() to build the DSN consistently for both TCP and unix_socket modes; replaces 3 duplicated string concatenations. 5. New helper installerFriendlyDbError() that: - Sanitizes any DSN fragment that could leak host/port/user - Maps common errors (Access denied, Unknown database, Connection refused, No such file, getaddrinfo, timeout) to clear, aaPanel-aware messages with concrete remediation steps. 6. Added 10s/15s PDO timeouts on every connection so the installer can never hang. 7. Port is cast to int (was passed as string). 8. Socket path is persisted in $_SESSION['install_db'] so subsequent migration / admin-create / finalize steps reuse it. 9. Frontend now sends `db_socket` in `dbCredentials`. Co-authored-by: ChesnoTech <263363000+ChesnoTech@users.noreply.github.com> --- FINAL_PRODUCTION_SYSTEM/install/ajax.php | 113 +++++++++++++++++----- FINAL_PRODUCTION_SYSTEM/install/index.php | 17 +++- 2 files changed, 101 insertions(+), 29 deletions(-) diff --git a/FINAL_PRODUCTION_SYSTEM/install/ajax.php b/FINAL_PRODUCTION_SYSTEM/install/ajax.php index e1b16bd..c5930bb 100644 --- a/FINAL_PRODUCTION_SYSTEM/install/ajax.php +++ b/FINAL_PRODUCTION_SYSTEM/install/ajax.php @@ -204,23 +204,31 @@ function handlePreflight() { // Step 2: Test Database Connection // ═══════════════════════════════════════════════════════════════ function handleTestDb() { - $host = $_POST['db_host'] ?? 'localhost'; - $port = $_POST['db_port'] ?? '3306'; - $user = $_POST['db_user'] ?? ''; - $pass = $_POST['db_pass'] ?? ''; - $name = $_POST['db_name'] ?? 'oem_activation'; + $host = trim($_POST['db_host'] ?? '127.0.0.1'); + $port = (int)($_POST['db_port'] ?? 3306); + $user = $_POST['db_user'] ?? ''; + $pass = $_POST['db_pass'] ?? ''; + $name = $_POST['db_name'] ?? 'oem_activation'; + $socket = trim($_POST['db_socket'] ?? ''); if (empty($user)) { echo json_encode(['success' => false, 'message' => 'Username is required']); return; } + // aaPanel / cPanel hint: many panels bind MariaDB to TCP only. + // Coerce 'localhost' → '127.0.0.1' to avoid PDO Unix-socket lookup + // unless an explicit socket path is supplied. + if ($socket === '' && strtolower($host) === 'localhost') { + $host = '127.0.0.1'; + } + try { // First: connect without database to check server - $dsn = "mysql:host={$host};port={$port};charset=utf8mb4"; + $dsn = installerBuildDsn($host, $port, '', $socket); $pdo = new PDO($dsn, $user, $pass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::ATTR_TIMEOUT => 5, + PDO::ATTR_TIMEOUT => 10, ]); // Get server version @@ -242,9 +250,10 @@ function handleTestDb() { } // Verify we can connect to the database - $dsn2 = "mysql:host={$host};port={$port};dbname={$name};charset=utf8mb4"; + $dsn2 = installerBuildDsn($host, $port, $name, $socket); $pdo2 = new PDO($dsn2, $user, $pass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_TIMEOUT => 10, ]); // Check InnoDB support @@ -263,21 +272,12 @@ function handleTestDb() { } // Store in session for later steps - $_SESSION['install_db'] = compact('host', 'port', 'user', 'pass', 'name'); + $_SESSION['install_db'] = compact('host', 'port', 'user', 'pass', 'name', 'socket'); echo json_encode(['success' => true, 'message' => $msg]); } catch (PDOException $e) { - $msg = $e->getMessage(); - // Simplify common errors - if (strpos($msg, 'Access denied') !== false) { - $msg = 'Access denied. Check username and password.'; - } elseif (strpos($msg, 'Connection refused') !== false || strpos($msg, 'No such file') !== false) { - $msg = "Cannot connect to {$host}:{$port}. Check host and port."; - } elseif (strpos($msg, 'Unknown MySQL server host') !== false || strpos($msg, 'getaddrinfo') !== false) { - $msg = "Cannot resolve host '{$host}'. Check the hostname."; - } - echo json_encode(['success' => false, 'message' => $msg]); + echo json_encode(['success' => false, 'message' => installerFriendlyDbError($e, $host, $port, $socket)]); } } @@ -615,30 +615,91 @@ function handleFinalize() { * Create PDO connection from POST params or session */ function getInstallerPdo(): ?PDO { - $host = $_POST['db_host'] ?? $_SESSION['install_db']['host'] ?? ''; - $port = $_POST['db_port'] ?? $_SESSION['install_db']['port'] ?? '3306'; - $user = $_POST['db_user'] ?? $_SESSION['install_db']['user'] ?? ''; - $pass = $_POST['db_pass'] ?? $_SESSION['install_db']['pass'] ?? ''; - $name = $_POST['db_name'] ?? $_SESSION['install_db']['name'] ?? ''; + $host = trim($_POST['db_host'] ?? $_SESSION['install_db']['host'] ?? '127.0.0.1'); + $port = (int)($_POST['db_port'] ?? $_SESSION['install_db']['port'] ?? 3306); + $user = $_POST['db_user'] ?? $_SESSION['install_db']['user'] ?? ''; + $pass = $_POST['db_pass'] ?? $_SESSION['install_db']['pass'] ?? ''; + $name = $_POST['db_name'] ?? $_SESSION['install_db']['name'] ?? ''; + $socket = trim($_POST['db_socket'] ?? $_SESSION['install_db']['socket'] ?? ''); if (empty($user) || empty($name)) { echo json_encode(['success' => false, 'message' => 'Database credentials missing. Go back to step 2.']); return null; } + // Coerce 'localhost' → '127.0.0.1' (force TCP) when no explicit socket given + if ($socket === '' && strtolower($host) === 'localhost') { + $host = '127.0.0.1'; + } + try { - $dsn = "mysql:host={$host};port={$port};dbname={$name};charset=utf8mb4"; + $dsn = installerBuildDsn($host, $port, $name, $socket); return new PDO($dsn, $user, $pass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, + PDO::ATTR_TIMEOUT => 15, ]); } catch (PDOException $e) { - echo json_encode(['success' => false, 'message' => 'Database connection failed: ' . $e->getMessage()]); + echo json_encode(['success' => false, 'message' => installerFriendlyDbError($e, $host, $port, $socket)]); return null; } } +/** + * Build a PDO DSN that supports either TCP host:port or Unix socket path. + * When $socket is non-empty, host/port are ignored. + */ +function installerBuildDsn(string $host, int $port, string $name, string $socket = ''): string { + if ($socket !== '') { + $dsn = "mysql:unix_socket={$socket};charset=utf8mb4"; + } else { + $dsn = "mysql:host={$host};port={$port};charset=utf8mb4"; + } + if ($name !== '') { + // Insert dbname between host/socket and charset to keep DSN ordered cleanly + $dsn = str_replace(';charset=utf8mb4', ";dbname={$name};charset=utf8mb4", $dsn); + } + return $dsn; +} + +/** + * Convert raw PDOException to a user-friendly, DSN-sanitized message. + * Hints aaPanel / cPanel users about common pitfalls. + */ +function installerFriendlyDbError(PDOException $e, string $host, int $port, string $socket): string { + $raw = $e->getMessage(); + $code = $e->getCode(); + + // Strip any DSN fragments that could leak host/port/user + $raw = preg_replace('/\bmysql:[^\s]+/', '[DSN]', $raw); + + if (stripos($raw, 'Access denied') !== false) { + return 'Access denied. Check username and password. On aaPanel, ensure the user is allowed from this host (set Host = % or 127.0.0.1 in phpMyAdmin → User accounts).'; + } + if (stripos($raw, 'Unknown database') !== false) { + return "Database does not exist and could not be auto-created. Create it manually in aaPanel → Databases, then retry."; + } + if (stripos($raw, 'Unknown MySQL server host') !== false || stripos($raw, 'getaddrinfo') !== false) { + return "Cannot resolve host '{$host}'. Use 127.0.0.1 instead of localhost on most aaPanel installs."; + } + if (stripos($raw, 'Connection refused') !== false) { + return "Cannot connect to {$host}:{$port}. MariaDB/MySQL service may not be running, or it's bound to a different port. In aaPanel: App Store → MySQL → check Service is started."; + } + if (stripos($raw, 'No such file or directory') !== false) { + // Classic Unix-socket failure + $hint = $socket !== '' + ? "Socket path '{$socket}' does not exist." + : "PDO tried the default Unix socket but it does not exist. Use 127.0.0.1 instead of localhost, or specify the socket path. Common aaPanel paths: /tmp/mysql.sock, /www/server/mysql/mysql.sock"; + return $hint; + } + if (stripos($raw, 'timed out') !== false || stripos($raw, 'timeout') !== false) { + return "Connection timed out reaching {$host}:{$port}. Firewall or wrong port?"; + } + // Fallback — sanitized message only, no stack + return "Database connection failed: " . $raw; +} + /** * Convert PHP ini shorthand notation to bytes */ diff --git a/FINAL_PRODUCTION_SYSTEM/install/index.php b/FINAL_PRODUCTION_SYSTEM/install/index.php index 7dd8dcb..dedcb1a 100644 --- a/FINAL_PRODUCTION_SYSTEM/install/index.php +++ b/FINAL_PRODUCTION_SYSTEM/install/index.php @@ -372,7 +372,8 @@
- + +
Use 127.0.0.1 on aaPanel/cPanel (avoids Unix-socket lookup). Use localhost only if you also set Socket Path below.
@@ -392,8 +393,16 @@
-
Will be created if it doesn't exist
+
Will be created if it doesn't exist (requires CREATE privilege).
+
+ Advanced (optional) — Unix socket path +
+ + +
If set, host/port are ignored. Common aaPanel paths: /tmp/mysql.sock or /www/server/mysql/mysql.sock.
+
+
@@ -653,12 +662,14 @@ function renderChecks(containerId, checks) { btn.disabled = true; btn.innerHTML = ' Testing...'; + const socketEl = document.getElementById('dbSocket'); dbCredentials = { - db_host: document.getElementById('dbHost').value, + db_host: document.getElementById('dbHost').value.trim() || '127.0.0.1', db_port: document.getElementById('dbPort').value, db_user: document.getElementById('dbUser').value, db_pass: document.getElementById('dbPass').value, db_name: document.getElementById('dbName').value, + db_socket: socketEl ? socketEl.value.trim() : '', }; const data = await post('test_db', dbCredentials); From 6903cc073eb65cba5b5bdd9b8c626ad5433ed02e Mon Sep 17 00:00:00 2001 From: Ayoub Mohamed Samir <7agtyadmin@gmail.com> Date: Thu, 7 May 2026 07:00:52 +0300 Subject: [PATCH 07/15] P0: Joomla-grade installer hardening for arbitrary Linux panels (#19) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 0 of the multi-panel compatibility plan. No table-prefix work yet (that's P1). All changes local to install/ajax.php + install/index.php. Hardening: 1. Async per-migration runner. handleInstallDb() split into: - install_db_init → returns ordered file list with applied flags - install_db_step → applies ONE migration file (browser drives loop) - install_db_all → legacy single-shot fast path Browser pumps the step loop, bypassing max_execution_time caps that panels like aaPanel/Plesk/cPanel enforce (often 30-60s). 2. Per-statement SQL splitter (installerSplitSql) respects backticks, single/double-quoted strings, line comments (-- and #), and block comments. Strips DELIMITER + outer BEGIN/COMMIT wrappers. Lets the runner survive PDO buffer caps and report progress accurately. 3. set_time_limit(0) + ignore_user_abort(true) at top — best-effort. 4. Preflight expansions: - open_basedir detection — flags if app root outside allowed paths - disable_functions audit — fails if mkdir/chmod/file_put_contents/ unlink/rmdir/fopen blocked - Live mkdir+write+read+unlink probe under uploads/ - parent_writable flag returned - php_version_full echoed for support tickets 5. Charset auto-fallback: SELECT VERSION() on first connect. MariaDB <5.5.3 or MySQL <5.7 → utf8mb3 (legacy 'utf8'). Persisted in session and surfaced to UI via dbCharset selector. 6. CREATE DATABASE skip-toggle: new step-2 checkbox + 1044/1142 error handler returns suggest_skip_create:true so JS can show "Tick & retry" button. Plesk/CyberPanel/ISPConfig users no longer hit a dead end. 7. Reverse-proxy IP hardening (getClientIp): only honor X-Forwarded-For / X-Real-IP / Client-IP when REMOTE_ADDR is in private/loopback range. Closes the spoofable trusted-network 2FA-bypass surface. 8. Auto-unlock recovery: install.lock + admin_users empty/missing → silent unlock. Inlined in install/index.php (avoids dragging ajax.php's JSON header into HTML) and mirrored as installerCheckIncompleteState() in ajax.php for runtime symmetry. Logged to install/install.log. 9. Unix-socket auto-detect: handleDetectSocket probes /tmp/mysql.sock, /var/run/mysqld/mysqld.sock, /var/lib/mysql/mysql.sock, /www/server/mysql/mysql.sock, /var/run/mariadb/mariadb.sock, /usr/local/mysql/mysql.sock + 2 more. UI Detect button auto-fills. 10. handleHealth post-install probe: SELECT 1 + SHOW TABLES for the five canonical KeyGate tables + admin_users count. Useful for step 6 link and external monitoring during shared-host installs. 11. installerBuildDsn() now accepts $charset; getInstallerPdo() pulls it from session. installerFriendlyDbError() adds 1044/1142 hints and 1045/no-password specific messaging. 12. installerLog() helper: append-only audit trail at install/install.log. UI changes (install/index.php): - Step 2: skip-create-DB checkbox, charset selector, prefix input (P1-ready, hidden behind Advanced section), Detect socket button. - Step 3: replaced single-shot runMigrations with init+step loop; per-row spinner; per-row pass/skip/error with file name + message; stops on first hard error so user reads it. - Failed test_db with suggest_skip_create renders inline retry button. Co-authored-by: ChesnoTech <263363000+ChesnoTech@users.noreply.github.com> --- FINAL_PRODUCTION_SYSTEM/install/ajax.php | 770 ++++++++++++++++++---- FINAL_PRODUCTION_SYSTEM/install/index.php | 208 ++++-- 2 files changed, 825 insertions(+), 153 deletions(-) diff --git a/FINAL_PRODUCTION_SYSTEM/install/ajax.php b/FINAL_PRODUCTION_SYSTEM/install/ajax.php index c5930bb..fedc2a5 100644 --- a/FINAL_PRODUCTION_SYSTEM/install/ajax.php +++ b/FINAL_PRODUCTION_SYSTEM/install/ajax.php @@ -9,14 +9,22 @@ header('Content-Type: application/json'); header('X-Content-Type-Options: nosniff'); -// Block if already installed +// Best-effort: defang shared-host timeouts. Some panels ignore these silently. +@set_time_limit(0); +@ignore_user_abort(true); + +session_start(); + +// Auto-unlock recovery: if install.lock exists but the install never finished +// (no admin_users yet, or table missing) — clear it and continue. Logged. +installerCheckIncompleteState(); + +// Block if (still) installed after auto-unlock check if (file_exists(__DIR__ . '/../install.lock')) { die(json_encode(['success' => false, 'message' => 'System is already installed.'])); } -session_start(); - -$action = $_POST['action'] ?? ''; +$action = $_POST['action'] ?? $_GET['action'] ?? ''; switch ($action) { case 'preflight': @@ -25,8 +33,15 @@ case 'test_db': handleTestDb(); break; - case 'install_db': - handleInstallDb(); + case 'install_db': // legacy single-shot; falls through to fast-path + case 'install_db_all': + handleInstallDbAll(); + break; + case 'install_db_init': + handleInstallDbInit(); + break; + case 'install_db_step': + handleInstallDbStep(); break; case 'create_admin': handleCreateAdmin(); @@ -34,6 +49,12 @@ case 'finalize': handleFinalize(); break; + case 'detect_socket': + handleDetectSocket(); + break; + case 'health': + handleHealth(); + break; default: echo json_encode(['success' => false, 'message' => 'Unknown action']); } @@ -197,6 +218,64 @@ function handlePreflight() { ]; } + // ── PHP Sandbox / Restrictions (panel-specific blockers) ── + $openBasedir = ini_get('open_basedir'); + if (!empty($openBasedir)) { + $allowedPaths = preg_split('/[:;]/', $openBasedir); + // Verify the install root is within allowed paths + $insideBasedir = false; + foreach ($allowedPaths as $p) { + if ($p !== '' && strpos($baseDir, rtrim($p, '/')) === 0) { + $insideBasedir = true; + break; + } + } + $result['settings'][] = [ + 'label' => 'open_basedir', + 'value' => $insideBasedir ? 'OK (' . count($allowedPaths) . ' path(s))' : 'Restricted', + 'status' => $insideBasedir ? 'pass' : 'fail', + 'hint' => $insideBasedir ? '' : "Add app root '{$baseDir}' to open_basedir in php.ini.", + ]; + } else { + $result['settings'][] = [ + 'label' => 'open_basedir', + 'value' => 'Not set (unrestricted)', + 'status' => 'pass', + ]; + } + + $disabled = array_filter(array_map('trim', explode(',', (string) ini_get('disable_functions')))); + $criticalDisabled = array_intersect($disabled, ['mkdir', 'chmod', 'file_put_contents', 'rmdir', 'unlink', 'fopen']); + $result['settings'][] = [ + 'label' => 'disable_functions', + 'value' => empty($disabled) ? 'None' : count($disabled) . ' function(s) blocked', + 'status' => empty($criticalDisabled) ? 'pass' : 'fail', + 'hint' => empty($criticalDisabled) ? '' : 'Critical functions blocked: ' . implode(', ', $criticalDisabled) . '. Backups/upgrades will degrade.', + ]; + + // ── Live mkdir / write / unlink probe ── + $probeDir = $baseDir . '/uploads/_keygate_probe_' . uniqid(); + $probeFile = $probeDir . '/probe.txt'; + $probeOk = @mkdir($probeDir, 0755, true); + if ($probeOk) { + $writeOk = @file_put_contents($probeFile, 'ok') !== false; + $readOk = $writeOk && @file_get_contents($probeFile) === 'ok'; + @unlink($probeFile); + @rmdir($probeDir); + } else { + $writeOk = $readOk = false; + } + $result['directories'][] = [ + 'label' => 'Filesystem write probe', + 'value' => ($probeOk && $writeOk && $readOk) ? 'OK' : 'Failed', + 'status' => ($probeOk && $writeOk && $readOk) ? 'pass' : 'fail', + 'hint' => ($probeOk && $writeOk && $readOk) ? '' : 'Cannot create+write+read in uploads/. Check chmod 755 and disable_functions.', + ]; + + // ── Parent-dir writable flag (used by step 6 to surface manual workflow) ── + $result['parent_writable'] = is_writable($baseDir); + $result['php_version_full'] = PHP_VERSION; + echo json_encode($result); } @@ -204,18 +283,34 @@ function handlePreflight() { // Step 2: Test Database Connection // ═══════════════════════════════════════════════════════════════ function handleTestDb() { - $host = trim($_POST['db_host'] ?? '127.0.0.1'); - $port = (int)($_POST['db_port'] ?? 3306); - $user = $_POST['db_user'] ?? ''; - $pass = $_POST['db_pass'] ?? ''; - $name = $_POST['db_name'] ?? 'oem_activation'; - $socket = trim($_POST['db_socket'] ?? ''); + $host = trim($_POST['db_host'] ?? '127.0.0.1'); + $port = (int)($_POST['db_port'] ?? 3306); + $user = $_POST['db_user'] ?? ''; + $pass = $_POST['db_pass'] ?? ''; + $name = $_POST['db_name'] ?? 'oem_activation'; + $socket = trim($_POST['db_socket'] ?? ''); + $skipCreate = !empty($_POST['skip_create_db']); + $rawPrefix = trim((string)($_POST['db_prefix'] ?? '')); + $charset = 'utf8mb4'; // Default; may downgrade to utf8mb3 below. if (empty($user)) { echo json_encode(['success' => false, 'message' => 'Username is required']); return; } + // Validate prefix: empty OR `^[a-z][a-z0-9_]{0,9}$`. Deny-list reserved. + if ($rawPrefix !== '' && !preg_match('/^[a-z][a-z0-9_]{0,9}$/', $rawPrefix)) { + echo json_encode(['success' => false, 'message' => 'Prefix must be 1–10 chars, lowercase letters/digits/_, start with a letter.']); + return; + } + $denyList = ['mysql_', 'sys_', 'information_', 'performance_']; + foreach ($denyList as $denied) { + if ($rawPrefix !== '' && strpos($rawPrefix, $denied) === 0) { + echo json_encode(['success' => false, 'message' => "Prefix '{$rawPrefix}' starts with reserved name. Pick another."]); + return; + } + } + // aaPanel / cPanel hint: many panels bind MariaDB to TCP only. // Coerce 'localhost' → '127.0.0.1' to avoid PDO Unix-socket lookup // unless an explicit socket path is supplied. @@ -225,7 +320,7 @@ function handleTestDb() { try { // First: connect without database to check server - $dsn = installerBuildDsn($host, $port, '', $socket); + $dsn = installerBuildDsn($host, $port, '', $socket, $charset); $pdo = new PDO($dsn, $user, $pass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 10, @@ -236,21 +331,50 @@ function handleTestDb() { $isMariaDB = stripos($version, 'MariaDB') !== false; $serverType = $isMariaDB ? 'MariaDB' : 'MySQL'; + // Charset auto-fallback: MySQL < 5.7 or MariaDB < 5.5.3 → utf8mb3 (legacy 'utf8') + $numericVer = preg_replace('/[^0-9.].*/', '', $version); + if ($isMariaDB) { + if (version_compare($numericVer, '5.5.3', '<')) $charset = 'utf8'; + } else { + if (version_compare($numericVer, '5.7.0', '<')) $charset = 'utf8'; + } + // Check if database exists $stmt = $pdo->prepare("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?"); $stmt->execute([$name]); $dbExists = (bool)$stmt->fetch(); + $collation = $charset . '_unicode_ci'; if (!$dbExists) { + if ($skipCreate) { + echo json_encode([ + 'success' => false, + 'message' => "Database '{$name}' does not exist on the server. Create it in your control panel (aaPanel: Databases → Add database) and uncheck 'skip CREATE' OR pre-create it then retry.", + ]); + return; + } // Try to create it - $pdo->exec("CREATE DATABASE `{$name}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); - $msg = "{$serverType} {$version} — Database '{$name}' created successfully."; + try { + $pdo->exec("CREATE DATABASE `{$name}` CHARACTER SET {$charset} COLLATE {$collation}"); + $msg = "{$serverType} {$version} — Database '{$name}' created successfully (charset={$charset})."; + } catch (PDOException $createErr) { + $code = (int) $createErr->getCode(); + if (in_array($code, [1044, 1142]) || stripos($createErr->getMessage(), 'denied') !== false) { + echo json_encode([ + 'success' => false, + 'message' => "Your DB user lacks CREATE DATABASE privilege (common on Plesk/CyberPanel). Pre-create the DB '{$name}' in your control panel, then retick 'Database already exists — skip CREATE' and retry.", + 'suggest_skip_create' => true, + ]); + return; + } + throw $createErr; + } } else { - $msg = "{$serverType} {$version} — Database '{$name}' exists."; + $msg = "{$serverType} {$version} — Database '{$name}' exists (charset will be {$charset})."; } // Verify we can connect to the database - $dsn2 = installerBuildDsn($host, $port, $name, $socket); + $dsn2 = installerBuildDsn($host, $port, $name, $socket, $charset); $pdo2 = new PDO($dsn2, $user, $pass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 10, @@ -272,9 +396,25 @@ function handleTestDb() { } // Store in session for later steps - $_SESSION['install_db'] = compact('host', 'port', 'user', 'pass', 'name', 'socket'); + $_SESSION['install_db'] = [ + 'host' => $host, + 'port' => $port, + 'user' => $user, + 'pass' => $pass, + 'name' => $name, + 'socket' => $socket, + 'prefix' => $rawPrefix, + 'charset' => $charset, + ]; - echo json_encode(['success' => true, 'message' => $msg]); + echo json_encode([ + 'success' => true, + 'message' => $msg, + 'charset' => $charset, + 'version' => $version, + 'serverType'=> $serverType, + 'prefix' => $rawPrefix, + ]); } catch (PDOException $e) { echo json_encode(['success' => false, 'message' => installerFriendlyDbError($e, $host, $port, $socket)]); @@ -282,20 +422,15 @@ function handleTestDb() { } // ═══════════════════════════════════════════════════════════════ -// Step 3: Install Database (Run Migrations) +// Step 3: Install Database — Async (init/step) + Fast-path (all) // ═══════════════════════════════════════════════════════════════ -function handleInstallDb() { - $pdo = getInstallerPdo(); - if (!$pdo) return; - - $sqlDir = realpath(__DIR__ . '/../database'); - if (!$sqlDir) { - echo json_encode(['success' => false, 'message' => 'Database SQL directory not found']); - return; - } - // Migration order — matches 00-init.sh - $migrations = [ +/** + * Canonical migration order. Mirrors 00-init.sh exactly. + * Returns [filename, version] tuples. + */ +function installerMigrationList(): array { + return [ ['schema_versions_migration.sql', 0], ['install.sql', 1], ['database_concurrency_indexes.sql', 2], @@ -317,80 +452,206 @@ function handleInstallDb() { ['missing_drivers_migration.sql', 18], ['unallocated_space_migration.sql', 19], ]; +} - $results = []; +/** + * Step-3 INIT: ensure schema_versions exists, return ordered file list with + * applied-status flags. Browser drives the per-file loop from here. + */ +function handleInstallDbInit() { + $pdo = getInstallerPdo(); + if (!$pdo) return; + + $sqlDir = realpath(__DIR__ . '/../database'); + if (!$sqlDir) { + echo json_encode(['success' => false, 'message' => 'Database SQL directory not found']); + return; + } - // Step 0: ensure schema_versions exists (run unconditionally) + // Bootstrap schema_versions first (the tracking table for all later migrations). $schemaFile = $sqlDir . '/schema_versions_migration.sql'; if (file_exists($schemaFile)) { try { - $sql = file_get_contents($schemaFile); - $pdo->exec($sql); - $results[] = ['file' => 'schema_versions_migration.sql', 'status' => 'ok', 'message' => 'Tracking table ready']; - } catch (PDOException $e) { - // Table probably exists already - $results[] = ['file' => 'schema_versions_migration.sql', 'status' => 'ok', 'message' => 'Tracking table exists']; - } + $pdo->exec(file_get_contents($schemaFile)); + } catch (PDOException $e) { /* probably already exists */ } } - // Run remaining migrations - for ($i = 1; $i < count($migrations); $i++) { - [$file, $version] = $migrations[$i]; + $list = []; + foreach (installerMigrationList() as [$file, $version]) { $filePath = $sqlDir . '/' . $file; + $exists = file_exists($filePath); + $applied = false; + + if ($exists) { + try { + $stmt = $pdo->prepare("SELECT COUNT(*) AS c FROM schema_versions WHERE filename = ?"); + $stmt->execute([$file]); + $applied = ((int)$stmt->fetchColumn()) > 0; + } catch (PDOException $e) { /* table missing → not applied */ } + } + + $list[] = [ + 'file' => $file, + 'version' => $version, + 'exists' => $exists, + 'applied' => $applied, + 'sha256' => $exists ? substr(hash_file('sha256', $filePath), 0, 16) : '', + ]; + } + + echo json_encode([ + 'success' => true, + 'migrations' => $list, + 'total' => count($list), + ]); +} + +/** + * Step-3 STEP: apply ONE migration file. + * Body: { file: 'install.sql', version: 1 } + */ +function handleInstallDbStep() { + @set_time_limit(0); + $pdo = getInstallerPdo(); + if (!$pdo) return; + + $file = $_POST['file'] ?? ''; + $version = (int)($_POST['version'] ?? 0); + + // Whitelist against the canonical list — no arbitrary file reads. + $allowed = array_column(installerMigrationList(), 0); + if (!in_array($file, $allowed, true)) { + echo json_encode(['success' => false, 'message' => "Migration '{$file}' not on the canonical list."]); + return; + } + + $sqlDir = realpath(__DIR__ . '/../database'); + $filePath = $sqlDir . '/' . $file; + if (!file_exists($filePath)) { + echo json_encode(['file' => $file, 'success' => true, 'status' => 'skipped', 'message' => 'File not found (skipped)']); + return; + } + + // Skip if already applied + try { + $stmt = $pdo->prepare("SELECT COUNT(*) AS c FROM schema_versions WHERE filename = ?"); + $stmt->execute([$file]); + if ((int)$stmt->fetchColumn() > 0) { + echo json_encode(['file' => $file, 'success' => true, 'status' => 'skipped', 'message' => 'Already applied']); + return; + } + } catch (PDOException $e) { /* table missing — proceed */ } + + $sql = file_get_contents($filePath); + $result = installerRunSqlFile($pdo, $sql); + + if ($result['ok']) { + try { + $checksum = hash('sha256', $sql); + $stmt = $pdo->prepare("INSERT IGNORE INTO schema_versions (version, filename, checksum) VALUES (?, ?, ?)"); + $stmt->execute([$version, $file, $checksum]); + } catch (PDOException $e) { /* ignore */ } + + echo json_encode([ + 'file' => $file, + 'success' => true, + 'status' => 'ok', + 'message' => 'Applied (' . $result['stmts_run'] . ' statements)', + 'stmts_run' => $result['stmts_run'], + ]); + return; + } + + // Tolerate "already exists" / "duplicate" — record as applied. + if (preg_match('/Duplicate|already exists|1060|1061|1050|1062/i', $result['error'])) { + try { + $checksum = hash('sha256', $sql); + $stmt = $pdo->prepare("INSERT IGNORE INTO schema_versions (version, filename, checksum) VALUES (?, ?, ?)"); + $stmt->execute([$version, $file, $checksum]); + } catch (PDOException $e2) { /* ignore */ } + + echo json_encode([ + 'file' => $file, + 'success' => true, + 'status' => 'ok', + 'message' => 'Applied (some objects already existed)', + ]); + return; + } + + echo json_encode([ + 'file' => $file, + 'success' => false, + 'status' => 'error', + 'message' => $result['error'], + ]); +} + +/** + * Step-3 ALL: legacy single-shot path (used by fast-path when host has + * generous max_execution_time AND no other risk flags). + */ +function handleInstallDbAll() { + @set_time_limit(0); + $pdo = getInstallerPdo(); + if (!$pdo) return; + + $sqlDir = realpath(__DIR__ . '/../database'); + if (!$sqlDir) { + echo json_encode(['success' => false, 'message' => 'Database SQL directory not found']); + return; + } + + // Bootstrap schema_versions + $schemaFile = $sqlDir . '/schema_versions_migration.sql'; + if (file_exists($schemaFile)) { + try { $pdo->exec(file_get_contents($schemaFile)); } catch (PDOException $e) { /* ok */ } + } + $results = []; + foreach (installerMigrationList() as $i => [$file, $version]) { + if ($i === 0) { + $results[] = ['file' => $file, 'status' => 'ok', 'message' => 'Tracking table ready']; + continue; + } + + $filePath = $sqlDir . '/' . $file; if (!file_exists($filePath)) { $results[] = ['file' => $file, 'status' => 'skipped', 'message' => 'File not found']; continue; } - // Check if already applied try { - $stmt = $pdo->prepare("SELECT COUNT(*) AS cnt FROM schema_versions WHERE filename = ?"); + $stmt = $pdo->prepare("SELECT COUNT(*) AS c FROM schema_versions WHERE filename = ?"); $stmt->execute([$file]); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - if ($row && (int)$row['cnt'] > 0) { + if ((int)$stmt->fetchColumn() > 0) { $results[] = ['file' => $file, 'status' => 'skipped', 'message' => 'Already applied']; continue; } - } catch (PDOException $e) { - // schema_versions might not exist yet - } - - // Execute migration - try { - $sql = file_get_contents($filePath); - - // For multi-statement SQL, we need to use exec - // Some files use DELIMITER which doesn't work with PDO — strip them - $sql = preg_replace('/DELIMITER\s+[^\n]+/i', '', $sql); + } catch (PDOException $e) { /* ignore */ } - $pdo->exec($sql); - - // Record in schema_versions - $checksum = hash('sha256', $sql); - $stmt = $pdo->prepare("INSERT IGNORE INTO schema_versions (version, filename, checksum) VALUES (?, ?, ?)"); - $stmt->execute([$version, $file, $checksum]); + $sql = file_get_contents($filePath); + $r = installerRunSqlFile($pdo, $sql); - $results[] = ['file' => $file, 'status' => 'ok', 'message' => 'Applied successfully']; - } catch (PDOException $e) { - $errMsg = $e->getMessage(); - // Some errors are non-fatal (duplicate index, column already exists, etc.) - if (preg_match('/Duplicate|already exists|1060|1061/i', $errMsg)) { - // Record as applied anyway - try { - $checksum = hash('sha256', $sql); - $stmt = $pdo->prepare("INSERT IGNORE INTO schema_versions (version, filename, checksum) VALUES (?, ?, ?)"); - $stmt->execute([$version, $file, $checksum]); - } catch (PDOException $e2) { /* ignore */ } - $results[] = ['file' => $file, 'status' => 'ok', 'message' => 'Applied (some objects already existed)']; - } else { - $results[] = ['file' => $file, 'status' => 'error', 'message' => $errMsg]; - // Don't stop — try remaining migrations - } + if ($r['ok']) { + try { + $checksum = hash('sha256', $sql); + $stmt = $pdo->prepare("INSERT IGNORE INTO schema_versions (version, filename, checksum) VALUES (?, ?, ?)"); + $stmt->execute([$version, $file, $checksum]); + } catch (PDOException $e) { /* ignore */ } + $results[] = ['file' => $file, 'status' => 'ok', 'message' => 'Applied (' . $r['stmts_run'] . ' statements)']; + } elseif (preg_match('/Duplicate|already exists|1060|1061|1050|1062/i', $r['error'])) { + try { + $checksum = hash('sha256', $sql); + $stmt = $pdo->prepare("INSERT IGNORE INTO schema_versions (version, filename, checksum) VALUES (?, ?, ?)"); + $stmt->execute([$version, $file, $checksum]); + } catch (PDOException $e) { /* ignore */ } + $results[] = ['file' => $file, 'status' => 'ok', 'message' => 'Applied (some objects already existed)']; + } else { + $results[] = ['file' => $file, 'status' => 'error', 'message' => $r['error']]; } } - // Check if any critical errors $errors = array_filter($results, fn($r) => $r['status'] === 'error'); $success = count($errors) === 0; @@ -401,6 +662,120 @@ function handleInstallDb() { ]); } +/** + * Run a multi-statement SQL string statement-by-statement. + * Strips DELIMITER + outer BEGIN/COMMIT wrappers. + * Returns ['ok' => bool, 'stmts_run' => int, 'error' => string]. + */ +function installerRunSqlFile(PDO $pdo, string $sql): array { + // Strip DELIMITER directives — PDO doesn't honor them. + $sql = preg_replace('/DELIMITER\s+[^\n]+/i', '', $sql); + // Strip outer BEGIN/COMMIT wrappers — DDL implicit-commits anyway and + // wrapper breaks per-statement progress reporting on some panels. + $sql = preg_replace('/^\s*(START\s+TRANSACTION|BEGIN)\s*;\s*$/im', '', $sql); + $sql = preg_replace('/^\s*COMMIT\s*;\s*$/im', '', $sql); + + $stmts = installerSplitSql($sql); + $count = 0; + foreach ($stmts as $stmt) { + $stmt = trim($stmt); + if ($stmt === '') continue; + try { + $pdo->exec($stmt); + $count++; + } catch (PDOException $e) { + return ['ok' => false, 'stmts_run' => $count, 'error' => $e->getMessage()]; + } + } + return ['ok' => true, 'stmts_run' => $count, 'error' => '']; +} + +/** + * Split a multi-statement SQL string into individual statements. + * Respects backticks, single/double-quoted strings, line comments (-- ...), + * and block comments (/* ... *\/). + * + * Returns array of statements (semicolons stripped). + */ +function installerSplitSql(string $sql): array { + $stmts = []; + $buf = ''; + $len = strlen($sql); + $i = 0; + $inSingle = $inDouble = $inBacktick = false; + $inLineComment = $inBlockComment = false; + + while ($i < $len) { + $c = $sql[$i]; + $next = $i + 1 < $len ? $sql[$i + 1] : ''; + + // Comments + if (!$inSingle && !$inDouble && !$inBacktick) { + if (!$inBlockComment && !$inLineComment && $c === '-' && $next === '-') { + $inLineComment = true; + $buf .= $c . $next; + $i += 2; + continue; + } + if (!$inBlockComment && !$inLineComment && $c === '#') { + $inLineComment = true; + $buf .= $c; + $i++; + continue; + } + if (!$inBlockComment && !$inLineComment && $c === '/' && $next === '*') { + $inBlockComment = true; + $buf .= $c . $next; + $i += 2; + continue; + } + if ($inLineComment && ($c === "\n" || $c === "\r")) { + $inLineComment = false; + $buf .= $c; + $i++; + continue; + } + if ($inBlockComment && $c === '*' && $next === '/') { + $inBlockComment = false; + $buf .= $c . $next; + $i += 2; + continue; + } + } + + if ($inLineComment || $inBlockComment) { + $buf .= $c; + $i++; + continue; + } + + // Quote tracking + if ($c === "'" && !$inDouble && !$inBacktick) { + $inSingle = !$inSingle; + } elseif ($c === '"' && !$inSingle && !$inBacktick) { + $inDouble = !$inDouble; + } elseif ($c === '`' && !$inSingle && !$inDouble) { + $inBacktick = !$inBacktick; + } + + // Statement terminator + if ($c === ';' && !$inSingle && !$inDouble && !$inBacktick) { + $stmts[] = $buf; + $buf = ''; + $i++; + continue; + } + + $buf .= $c; + $i++; + } + + if (trim($buf) !== '') { + $stmts[] = $buf; + } + return $stmts; +} + // ═══════════════════════════════════════════════════════════════ // Step 4: Create Admin Account // ═══════════════════════════════════════════════════════════════ @@ -615,12 +990,13 @@ function handleFinalize() { * Create PDO connection from POST params or session */ function getInstallerPdo(): ?PDO { - $host = trim($_POST['db_host'] ?? $_SESSION['install_db']['host'] ?? '127.0.0.1'); - $port = (int)($_POST['db_port'] ?? $_SESSION['install_db']['port'] ?? 3306); - $user = $_POST['db_user'] ?? $_SESSION['install_db']['user'] ?? ''; - $pass = $_POST['db_pass'] ?? $_SESSION['install_db']['pass'] ?? ''; - $name = $_POST['db_name'] ?? $_SESSION['install_db']['name'] ?? ''; - $socket = trim($_POST['db_socket'] ?? $_SESSION['install_db']['socket'] ?? ''); + $host = trim($_POST['db_host'] ?? $_SESSION['install_db']['host'] ?? '127.0.0.1'); + $port = (int)($_POST['db_port'] ?? $_SESSION['install_db']['port'] ?? 3306); + $user = $_POST['db_user'] ?? $_SESSION['install_db']['user'] ?? ''; + $pass = $_POST['db_pass'] ?? $_SESSION['install_db']['pass'] ?? ''; + $name = $_POST['db_name'] ?? $_SESSION['install_db']['name'] ?? ''; + $socket = trim($_POST['db_socket'] ?? $_SESSION['install_db']['socket'] ?? ''); + $charset = $_SESSION['install_db']['charset'] ?? 'utf8mb4'; if (empty($user) || empty($name)) { echo json_encode(['success' => false, 'message' => 'Database credentials missing. Go back to step 2.']); @@ -633,7 +1009,7 @@ function getInstallerPdo(): ?PDO { } try { - $dsn = installerBuildDsn($host, $port, $name, $socket); + $dsn = installerBuildDsn($host, $port, $name, $socket, $charset); return new PDO($dsn, $user, $pass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, @@ -650,15 +1026,15 @@ function getInstallerPdo(): ?PDO { * Build a PDO DSN that supports either TCP host:port or Unix socket path. * When $socket is non-empty, host/port are ignored. */ -function installerBuildDsn(string $host, int $port, string $name, string $socket = ''): string { +function installerBuildDsn(string $host, int $port, string $name, string $socket = '', string $charset = 'utf8mb4'): string { if ($socket !== '') { - $dsn = "mysql:unix_socket={$socket};charset=utf8mb4"; + $dsn = "mysql:unix_socket={$socket};charset={$charset}"; } else { - $dsn = "mysql:host={$host};port={$port};charset=utf8mb4"; + $dsn = "mysql:host={$host};port={$port};charset={$charset}"; } if ($name !== '') { // Insert dbname between host/socket and charset to keep DSN ordered cleanly - $dsn = str_replace(';charset=utf8mb4', ";dbname={$name};charset=utf8mb4", $dsn); + $dsn = str_replace(";charset={$charset}", ";dbname={$name};charset={$charset}", $dsn); } return $dsn; } @@ -669,28 +1045,36 @@ function installerBuildDsn(string $host, int $port, string $name, string $socket */ function installerFriendlyDbError(PDOException $e, string $host, int $port, string $socket): string { $raw = $e->getMessage(); - $code = $e->getCode(); + $code = (int)$e->getCode(); // Strip any DSN fragments that could leak host/port/user $raw = preg_replace('/\bmysql:[^\s]+/', '[DSN]', $raw); + // ── 1044/1142: Lacks privilege (Plesk, CyberPanel, ISPConfig) ─ + if ($code === 1044 || $code === 1142 || preg_match('/\b(1044|1142)\b/', $raw)) { + return "Your DB user lacks the required privilege. On Plesk/CyberPanel, the per-user account often cannot CREATE DATABASE or CREATE TABLE. Pre-create the database in your control panel UI and tick 'Database already exists — skip CREATE'."; + } + // ── 1045 with no password supplied ─ if (stripos($raw, 'Access denied') !== false) { + if (stripos($raw, 'using password: NO') !== false) { + return "Access denied. Server says no password was supplied. If your panel set a password (most do), enter it. If not, ensure user is allowed from 127.0.0.1."; + } return 'Access denied. Check username and password. On aaPanel, ensure the user is allowed from this host (set Host = % or 127.0.0.1 in phpMyAdmin → User accounts).'; } if (stripos($raw, 'Unknown database') !== false) { - return "Database does not exist and could not be auto-created. Create it manually in aaPanel → Databases, then retry."; + return "Database does not exist. Pre-create it in your control panel (aaPanel/cPanel/Plesk → Databases) and tick 'Database already exists — skip CREATE'."; } if (stripos($raw, 'Unknown MySQL server host') !== false || stripos($raw, 'getaddrinfo') !== false) { return "Cannot resolve host '{$host}'. Use 127.0.0.1 instead of localhost on most aaPanel installs."; } if (stripos($raw, 'Connection refused') !== false) { - return "Cannot connect to {$host}:{$port}. MariaDB/MySQL service may not be running, or it's bound to a different port. In aaPanel: App Store → MySQL → check Service is started."; + return "Cannot connect to {$host}:{$port}. MariaDB/MySQL service may not be running, or it's bound to a different port. Check the service is started in your panel."; } if (stripos($raw, 'No such file or directory') !== false) { // Classic Unix-socket failure $hint = $socket !== '' - ? "Socket path '{$socket}' does not exist." - : "PDO tried the default Unix socket but it does not exist. Use 127.0.0.1 instead of localhost, or specify the socket path. Common aaPanel paths: /tmp/mysql.sock, /www/server/mysql/mysql.sock"; + ? "Socket path '{$socket}' does not exist or is not readable by the web user." + : "PDO tried the default Unix socket but it does not exist. Use 127.0.0.1 instead of localhost, or click 'Detect socket' in advanced settings. Common paths: /tmp/mysql.sock, /var/run/mysqld/mysqld.sock, /www/server/mysql/mysql.sock"; return $hint; } if (stripos($raw, 'timed out') !== false || stripos($raw, 'timeout') !== false) { @@ -717,25 +1101,31 @@ function returnBytes(string $val): int { } /** - * Get the real client IP, accounting for proxies + * Get the real client IP, accounting for proxies. + * + * Security: only honor X-Forwarded-For / X-Real-IP / Client-IP headers when + * the immediate REMOTE_ADDR is in a private/loopback range — that's the + * only situation where a forward header is trustworthy (real proxy in front). + * Otherwise the client could spoof any IP. */ function getClientIp(): string { - // Check common proxy headers (trusted only in installer context) - $headers = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP']; - foreach ($headers as $header) { - if (!empty($_SERVER[$header])) { - // X-Forwarded-For may contain multiple IPs — take the first (client) - $ip = trim(explode(',', $_SERVER[$header])[0]); - if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { - return $ip; - } - // Accept private range IPs too (common in LAN setups) - if (filter_var($ip, FILTER_VALIDATE_IP)) { - return $ip; + $remoteAddr = $_SERVER['REMOTE_ADDR'] ?? ''; + $remoteIsPrivate = $remoteAddr !== '' + && filter_var($remoteAddr, FILTER_VALIDATE_IP) + && !filter_var($remoteAddr, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE); + + if ($remoteIsPrivate) { + $headers = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP']; + foreach ($headers as $header) { + if (!empty($_SERVER[$header])) { + $ip = trim(explode(',', $_SERVER[$header])[0]); + if (filter_var($ip, FILTER_VALIDATE_IP)) { + return $ip; + } } } } - return $_SERVER['REMOTE_ADDR'] ?? 'unknown'; + return $remoteAddr !== '' ? $remoteAddr : 'unknown'; } /** @@ -931,3 +1321,161 @@ function buildOrderNumberPattern(array $config): string { ?> PHP_TAIL; } + +// ═══════════════════════════════════════════════════════════════ +// Auto-unlock recovery + socket detection + health probe +// ═══════════════════════════════════════════════════════════════ + +/** + * Auto-unlock recovery: if install.lock exists but the install never + * finished (no admin_users yet, or table missing), delete the lock so + * the user can resume. Triple-gated: lock present + DB connectable + + * admin_users empty/absent. Logs the auto-action to install.log. + * + * Idempotent — safe to call on every request. + */ +function installerCheckIncompleteState(): void { + $lockPath = __DIR__ . '/../install.lock'; + $configPath = __DIR__ . '/../config.php'; + + if (!file_exists($lockPath)) return; + if (!file_exists($configPath)) return; // Without config we can't probe DB + + // Best-effort include of config to get $db_config — but we don't trust + // its globals in this script's scope, so we re-parse manually. + $configSrc = @file_get_contents($configPath); + if ($configSrc === false) return; + + if (!preg_match("/'host'\s*=>\s*'([^']+)'/", $configSrc, $hM)) return; + if (!preg_match("/'dbname'\s*=>\s*'([^']+)'/", $configSrc, $nM)) return; + if (!preg_match("/'username'\s*=>\s*'([^']+)'/", $configSrc, $uM)) return; + if (!preg_match("/'password'\s*=>\s*'([^']*)'/", $configSrc, $pM)) return; + if (!preg_match("/'port'\s*=>\s*(\d+)/", $configSrc, $portM)) return; + + $host = $hM[1]; $name = $nM[1]; $user = $uM[1]; $pass = $pM[1]; $port = (int)$portM[1]; + if (strtolower($host) === 'localhost') $host = '127.0.0.1'; + + try { + $dsn = "mysql:host={$host};port={$port};dbname={$name};charset=utf8mb4"; + $pdo = new PDO($dsn, $user, $pass, [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_TIMEOUT => 5, + ]); + + // admin_users table missing → install was never completed. + $stmt = $pdo->query("SHOW TABLES LIKE 'admin_users'"); + if (!$stmt->fetch()) { + installerLog("auto-unlock: admin_users table missing — clearing install.lock"); + @unlink($lockPath); + return; + } + + // admin_users empty → install was never completed. + $cnt = (int)$pdo->query("SELECT COUNT(*) FROM admin_users")->fetchColumn(); + if ($cnt === 0) { + installerLog("auto-unlock: admin_users empty — clearing install.lock"); + @unlink($lockPath); + return; + } + } catch (PDOException $e) { + // Can't connect — might mean DB isn't even available; don't unlock, + // user must resolve DB issue first. Log it. + installerLog("auto-unlock: skipped (DB connect failed: " . $e->getMessage() . ")"); + return; + } + + // Install was completed properly — keep the lock. +} + +/** + * Probe a list of common Unix socket paths and return the first that exists. + */ +function installerProbeSockets(): array { + $candidates = [ + '/tmp/mysql.sock', + '/var/run/mysqld/mysqld.sock', + '/var/lib/mysql/mysql.sock', + '/www/server/mysql/mysql.sock', + '/var/run/mariadb/mariadb.sock', + '/usr/local/mysql/mysql.sock', + '/usr/local/var/mysql/mysql.sock', + '/opt/lampp/var/mysql/mysql.sock', + ]; + $found = []; + foreach ($candidates as $p) { + if (@file_exists($p)) { + $found[] = $p; + } + } + return $found; +} + +/** + * Action: detect_socket — returns list of socket paths discovered on disk. + */ +function handleDetectSocket(): void { + $found = installerProbeSockets(); + echo json_encode([ + 'success' => true, + 'sockets' => $found, + 'suggested' => $found[0] ?? '', + ]); +} + +/** + * Action: health — quick post-install probe (no auth required because + * install.lock blocks re-entry once installed). Returns DB connect status, + * tables present, admin row count. + */ +function handleHealth(): void { + $configPath = __DIR__ . '/../config.php'; + if (!file_exists($configPath)) { + echo json_encode(['success' => false, 'message' => 'config.php not found']); + return; + } + + $pdo = getInstallerPdo(); + if (!$pdo) return; // getInstallerPdo already echoed error + + $checks = []; + try { + $pdo->query('SELECT 1'); + $checks[] = ['label' => 'Database connect', 'status' => 'pass']; + } catch (PDOException $e) { + $checks[] = ['label' => 'Database connect', 'status' => 'fail', 'message' => $e->getMessage()]; + } + + $expectTables = ['admin_users', 'oem_keys', 'technicians', 'system_config', 'schema_versions']; + foreach ($expectTables as $t) { + try { + $stmt = $pdo->query("SHOW TABLES LIKE '" . str_replace("'", '', $t) . "'"); + $found = (bool)$stmt->fetch(); + $checks[] = ['label' => "Table {$t}", 'status' => $found ? 'pass' : 'fail']; + } catch (PDOException $e) { + $checks[] = ['label' => "Table {$t}", 'status' => 'fail']; + } + } + + try { + $admins = (int)$pdo->query("SELECT COUNT(*) FROM admin_users")->fetchColumn(); + $checks[] = ['label' => 'Admin accounts', 'status' => $admins > 0 ? 'pass' : 'fail', 'value' => $admins]; + } catch (PDOException $e) { + $checks[] = ['label' => 'Admin accounts', 'status' => 'fail']; + } + + $allPass = !in_array(false, array_map(fn($c) => $c['status'] === 'pass', $checks), true); + echo json_encode(['success' => $allPass, 'checks' => $checks]); +} + +/** + * Append a single line to install/install.log. Best-effort: silently + * skipped if the file isn't writable. + */ +function installerLog(string $line): void { + $logPath = __DIR__ . '/install.log'; + @file_put_contents( + $logPath, + '[' . date('Y-m-d H:i:s') . '] ' . $line . PHP_EOL, + FILE_APPEND + ); +} diff --git a/FINAL_PRODUCTION_SYSTEM/install/index.php b/FINAL_PRODUCTION_SYSTEM/install/index.php index dedcb1a..f55d2e1 100644 --- a/FINAL_PRODUCTION_SYSTEM/install/index.php +++ b/FINAL_PRODUCTION_SYSTEM/install/index.php @@ -12,8 +12,47 @@ * 6. Complete — write config, show success */ -// Prevent running if already installed +// Auto-unlock recovery (P0): if install.lock exists but install never +// completed (admin_users empty/missing), clear the lock so user can resume. +// Inlined to avoid dragging ajax.php's JSON header + dispatch into HTML output. $lockFile = __DIR__ . '/../install.lock'; +$configFile = __DIR__ . '/../config.php'; +if (file_exists($lockFile) && file_exists($configFile)) { + $configSrc = @file_get_contents($configFile); + if ($configSrc !== false + && preg_match("/'host'\s*=>\s*'([^']+)'/", $configSrc, $hM) + && preg_match("/'dbname'\s*=>\s*'([^']+)'/", $configSrc, $nM) + && preg_match("/'username'\s*=>\s*'([^']+)'/", $configSrc, $uM) + && preg_match("/'password'\s*=>\s*'([^']*)'/", $configSrc, $pM) + && preg_match("/'port'\s*=>\s*(\d+)/", $configSrc, $portM)) { + $autoHost = strtolower($hM[1]) === 'localhost' ? '127.0.0.1' : $hM[1]; + try { + $autoPdo = new PDO( + "mysql:host={$autoHost};port={$portM[1]};dbname={$nM[1]};charset=utf8mb4", + $uM[1], $pM[1], + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 5] + ); + $hasAdminTable = (bool) $autoPdo->query("SHOW TABLES LIKE 'admin_users'")->fetch(); + $adminCount = $hasAdminTable ? (int) $autoPdo->query("SELECT COUNT(*) FROM admin_users")->fetchColumn() : 0; + if (!$hasAdminTable || $adminCount === 0) { + @unlink($lockFile); + @file_put_contents( + __DIR__ . '/install.log', + '[' . date('Y-m-d H:i:s') . "] auto-unlock from index.php — admin_users empty/missing\n", + FILE_APPEND + ); + } + } catch (PDOException $autoE) { + @file_put_contents( + __DIR__ . '/install.log', + '[' . date('Y-m-d H:i:s') . '] auto-unlock skipped: ' . $autoE->getMessage() . "\n", + FILE_APPEND + ); + } + } +} + +// Prevent running if already installed if (file_exists($lockFile)) { $installed = json_decode(file_get_contents($lockFile), true); ?> @@ -395,12 +434,36 @@
Will be created if it doesn't exist (requires CREATE privilege).
+
+ +
Tick this if your control panel does not grant CREATE privilege (Plesk, CyberPanel, ISPConfig). Pre-create the database in your panel first.
+
- Advanced (optional) — Unix socket path + Advanced (optional)
- -
If set, host/port are ignored. Common aaPanel paths: /tmp/mysql.sock or /www/server/mysql/mysql.sock.
+
+ + +
+
If set, host/port are ignored. Click Detect to auto-find. Common paths: /tmp/mysql.sock, /var/run/mysqld/mysqld.sock, /www/server/mysql/mysql.sock.
+
+
+ + +
Auto-detect downgrades to utf8 for very old servers (MySQL < 5.7).
+
+
+ + +
Lowercase letters, digits, underscore. Lets you run KeyGate alongside other apps in the same database. Leave empty for default.
@@ -657,77 +720,138 @@ function renderChecks(containerId, checks) { } // ── Step 2: Database Test ──────────────────────────── +async function detectSocket() { + const data = await post('detect_socket', {}); + const input = document.getElementById('dbSocket'); + if (data.success && data.suggested) { + input.value = data.suggested; + alert('Found ' + data.sockets.length + ' socket(s). Picked: ' + data.suggested); + } else { + alert('No Unix socket found in common paths. Stick with TCP (host=127.0.0.1).'); + } +} + async function testDb() { const btn = document.getElementById('dbTestBtn'); btn.disabled = true; btn.innerHTML = ' Testing...'; - const socketEl = document.getElementById('dbSocket'); + const $ = id => document.getElementById(id); dbCredentials = { - db_host: document.getElementById('dbHost').value.trim() || '127.0.0.1', - db_port: document.getElementById('dbPort').value, - db_user: document.getElementById('dbUser').value, - db_pass: document.getElementById('dbPass').value, - db_name: document.getElementById('dbName').value, - db_socket: socketEl ? socketEl.value.trim() : '', + db_host: $('dbHost').value.trim() || '127.0.0.1', + db_port: $('dbPort').value, + db_user: $('dbUser').value, + db_pass: $('dbPass').value, + db_name: $('dbName').value, + db_socket: $('dbSocket') ? $('dbSocket').value.trim() : '', + db_prefix: $('dbPrefix') ? $('dbPrefix').value.trim() : '', + db_charset: $('dbCharset') ? $('dbCharset').value : '', + skip_create_db: $('dbSkipCreate') && $('dbSkipCreate').checked ? '1' : '', }; const data = await post('test_db', dbCredentials); - const div = document.getElementById('dbTestResult'); + const div = $('dbTestResult'); div.classList.remove('hidden'); if (data.success) { div.className = 'alert alert-success'; div.innerHTML = `✓ Connected! ${data.message}`; - document.getElementById('dbNext').disabled = false; + // Persist server-resolved charset back into the dropdown so steps 3+ use it. + if (data.charset && $('dbCharset')) $('dbCharset').value = data.charset; + $('dbNext').disabled = false; } else { div.className = 'alert alert-danger'; - div.innerHTML = `✗ Failed: ${data.message}`; - document.getElementById('dbNext').disabled = true; + let html = `✗ Failed: ${data.message}`; + if (data.suggest_skip_create) { + html += ` `; + } + div.innerHTML = html; + $('dbNext').disabled = true; } btn.disabled = false; btn.innerHTML = 'Test Connection'; } -// ── Step 3: Migrations ────────────────────────────── +// ── Step 3: Migrations (async per-file, survives short max_execution_time) ── async function runMigrations() { - document.getElementById('migrationPre').classList.add('hidden'); - document.getElementById('migrationProgress').classList.remove('hidden'); - document.getElementById('migBack').disabled = true; - document.getElementById('migNext').classList.add('hidden'); + const $ = id => document.getElementById(id); + $('migrationPre').classList.add('hidden'); + $('migrationProgress').classList.remove('hidden'); + $('migBack').disabled = true; + $('migNext').classList.add('hidden'); - const log = document.getElementById('migLog'); + const log = $('migLog'); log.innerHTML = ''; - const data = await post('install_db', dbCredentials); + // ── 1. Init: get migration list + applied flags ── + const init = await post('install_db_init', dbCredentials); + if (!init.success) { + $('migStatus').textContent = 'Initialization failed'; + $('migStatus').style.color = 'var(--danger)'; + log.innerHTML += `
ERROR: ${init.message || 'Unknown error'}
`; + $('migBack').disabled = false; + return; + } - const total = data.results ? data.results.length : 0; + const list = init.migrations || []; + const total = list.length; let done = 0; + let hadError = false; + + const updateProgress = () => { + const pct = total > 0 ? Math.round((done / total) * 100) : 0; + $('migBar').style.width = pct + '%'; + $('migCount').textContent = done + ' / ' + total; + }; - if (data.results) { - data.results.forEach(r => { + // ── 2. Step through each migration ── + for (const m of list) { + if (!m.exists) { done++; - const pct = Math.round((done / total) * 100); - document.getElementById('migBar').style.width = pct + '%'; - document.getElementById('migCount').textContent = done + ' / ' + total; - - const cls = r.status === 'ok' ? 'ok' : r.status === 'skipped' ? 'skip' : 'err'; - const icon = r.status === 'ok' ? '✓' : r.status === 'skipped' ? '→' : '✗'; - log.innerHTML += `
${icon} ${r.file}: ${r.message}
`; - log.scrollTop = log.scrollHeight; - }); + log.innerHTML += ``; + updateProgress(); + continue; + } + if (m.applied) { + done++; + log.innerHTML += ``; + updateProgress(); + continue; + } + + const pendingRow = document.createElement('div'); + pendingRow.className = 'pending'; + pendingRow.innerHTML = ` ${m.file}…`; + log.appendChild(pendingRow); + log.scrollTop = log.scrollHeight; + + const r = await post('install_db_step', { ...dbCredentials, file: m.file, version: m.version }); + log.removeChild(pendingRow); + + const cls = r.status === 'ok' ? 'ok' : r.status === 'skipped' ? 'skip' : 'err'; + const icon = r.status === 'ok' ? '✓' : r.status === 'skipped' ? '→' : '✗'; + log.innerHTML += `
${icon} ${m.file}: ${r.message || ''}
`; + log.scrollTop = log.scrollHeight; + + done++; + updateProgress(); + + if (!r.success && r.status === 'error') { + hadError = true; + // Stop loop on first hard error so user can read it. + break; + } } - if (data.success) { - document.getElementById('migStatus').textContent = 'All migrations complete!'; - document.getElementById('migStatus').style.color = 'var(--success)'; - document.getElementById('migNext').classList.remove('hidden'); + if (!hadError) { + $('migStatus').textContent = 'All migrations complete!'; + $('migStatus').style.color = 'var(--success)'; + $('migNext').classList.remove('hidden'); } else { - document.getElementById('migStatus').textContent = 'Installation failed'; - document.getElementById('migStatus').style.color = 'var(--danger)'; - log.innerHTML += `
ERROR: ${data.message || 'Unknown error'}
`; - document.getElementById('migBack').disabled = false; + $('migStatus').textContent = 'Installation failed'; + $('migStatus').style.color = 'var(--danger)'; + $('migBack').disabled = false; } } From 35c4d9739edf5c82618e0619bc987627d8d8e201 Mon Sep 17 00:00:00 2001 From: Ayoub Mohamed Samir <7agtyadmin@gmail.com> Date: Thu, 7 May 2026 07:22:10 +0300 Subject: [PATCH 08/15] P1: Joomla-style table-prefix support across SQL + PHP runtime (#20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces optional database-table prefix so multiple KeyGate instances can coexist in one database (panels like cPanel/Plesk often share a single DB across customer apps). Default prefix is empty → bit-for-bit identical schema to the previous release. Changes: 1. tools/prefix-codemod.php New one-shot script. Self-discovers the canonical KeyGate table list from CREATE TABLE / ALTER TABLE statements in database/*.sql, then rewrites: - SQL files (32): every `tablename` and unbacktiked SQL-keyword target becomes `#__tablename`. The `#__` sentinel is the Joomla convention. - PHP files (~50): every backticked or bare-name SQL table reference inside string literals becomes `' . t('tablename') . '` (single-quote concat) or `" . t('tablename') . "` (double-quote concat). Token-based PHP parser (token_get_all) so comments / identifiers / non-string code is never touched. Idempotent — second run is a no-op. Run: docker compose exec web php /tmp/codemod.php --root /var/www/html/activate --apply 2. FINAL_PRODUCTION_SYSTEM/functions/db-helpers.php New file. Defines `t(string $name): string` returning DB_PREFIX . name. Also defines a fallback `define('DB_PREFIX', '')` if config.php hasn't set it — covers all legacy installs. 3. FINAL_PRODUCTION_SYSTEM/constants.php Loads functions/db-helpers.php at the top so t() is available before any controller runs. 4. FINAL_PRODUCTION_SYSTEM/database/*.sql Codemod output: 32 SQL files with `#__` markers. Schema is identical when prefix='' (the default). 276 backticked refs converted. 5. FINAL_PRODUCTION_SYSTEM/{controllers,api,functions,*}.php Codemod output: ~362 site rewrites across ~54 files. Every SQL string literal that referenced a canonical table now resolves through t(). 6. FINAL_PRODUCTION_SYSTEM/install/ajax.php - installerRunSqlFile() substitutes `#__` → $_SESSION['install_db']['prefix'] before running each migration. Defense-in-depth: aborts if any `#__` remains post-substitution. - installerT() helper for installer-time queries (mirrors t() but reads prefix from session, since DB_PREFIX isn't defined yet). - handleInstallDbInit/Step/All all use `installerT('schema_versions')` when checking applied migrations. - handleCreateAdmin uses installerT() for admin_users/acl_roles. - handleFinalize uses installerT() for system_config/technicians/ trusted_networks/admin_ip_whitelist; passes prefix + charset to generateConfig(). - handleHealth probes prefix-aware physical table names. - installerCheckIncompleteState() reads DB_PREFIX from existing config.php so auto-unlock works on prefixed installs. - generateConfig() emits define('DB_PREFIX', '...') AND propagates the auto-detected charset (utf8mb4 or utf8mb3 fallback). 7. FINAL_PRODUCTION_SYSTEM/install/index.php Inline auto-unlock logic now reads DB_PREFIX from config.php, so the admin_users probe targets the correct physical table name. 8. FINAL_PRODUCTION_SYSTEM/database/docker-init/00-init.sh New KEYGATE_DB_PREFIX env var (default empty). Pre-runs `sed` over every .sql file into a /tmp staging copy so the original (read-only) mount stays untouched. schema_versions tracking table picks up the prefix consistently. Validates prefix against ^[a-z][a-z0-9_]{0,9}$. Checksum is computed against the original file (stable across prefix choices). Backward compatibility: - Existing installs without DB_PREFIX in config.php → db-helpers.php defaults to empty string → t('admin_users') === 'admin_users'. - 32 .sql files use `#__` placeholders. Without substitution they're invalid SQL — but they're never executed without going through either installerRunSqlFile() or 00-init.sh's sed pass. - Verified: live admin login + list_keys both succeed against the pre-existing dev database after the codemod. - 14/14 frontend tests pass. Risk register from the plan: - ✅ Codemod misses dynamic table refs → CI lint will catch (P2 task). - ✅ FK / TRIGGER / VIEW unqualified table refs → SQL pass handles them. - ✅ Empty-prefix path emits literal `#__` → installerRunSqlFile asserts strpos(#__) === false post-substitution and aborts. - ✅ Prefix collides with reserved name → step-2 UI deny-list + 00-init.sh regex validation. - ✅ Async runner slows happy-path → fast path retained (install_db_all). - ✅ Legacy config.php lacks DB_PREFIX → db-helpers.php fallback. Co-authored-by: ChesnoTech <263363000+ChesnoTech@users.noreply.github.com> --- .gitignore | 3 + FINAL_PRODUCTION_SYSTEM/admin_v2.php | 4 +- .../api/authenticate-usb.php | 10 +- .../api/change-password.php | 4 +- .../api/collect-hardware-v2.php | 12 +- .../api/download-resource.php | 4 +- .../api/get-alt-server-config.php | 6 +- .../api/get-client-config.php | 2 +- FINAL_PRODUCTION_SYSTEM/api/get-key.php | 10 +- FINAL_PRODUCTION_SYSTEM/api/import-csv.php | 2 +- FINAL_PRODUCTION_SYSTEM/api/login.php | 8 +- .../api/middleware/RateLimiter.php | 2 +- FINAL_PRODUCTION_SYSTEM/api/report-result.php | 22 +- .../api/submit-hardware.php | 10 +- FINAL_PRODUCTION_SYSTEM/api/totp-disable.php | 4 +- .../api/totp-regenerate-backup-codes.php | 4 +- FINAL_PRODUCTION_SYSTEM/api/totp-setup.php | 10 +- FINAL_PRODUCTION_SYSTEM/api/totp-verify.php | 6 +- FINAL_PRODUCTION_SYSTEM/config-production.php | 18 +- FINAL_PRODUCTION_SYSTEM/config.php | 2 +- .../config/config-template-enhanced.php | 10 +- FINAL_PRODUCTION_SYSTEM/constants.php | 5 + .../controllers/admin/AclController.php | 2 +- .../controllers/admin/BackupsController.php | 2 +- .../controllers/admin/BrandingController.php | 4 +- .../admin/ClientResourcesController.php | 16 +- .../admin/ComplianceController.php | 30 +- .../controllers/admin/DashboardController.php | 28 +- .../controllers/admin/HistoryController.php | 16 +- .../admin/IntegrationController.php | 12 +- .../controllers/admin/KeysController.php | 8 +- .../controllers/admin/LicenseController.php | 6 +- .../admin/NotificationsController.php | 26 +- .../admin/ProductVariantsController.php | 24 +- .../admin/ProductionController.php | 50 +-- .../controllers/admin/SecurityController.php | 12 +- .../admin/TaskPipelineController.php | 34 +- .../admin/TechniciansController.php | 18 +- .../controllers/admin/UpgradeController.php | 26 +- .../admin/UsbDevicesController.php | 20 +- .../database/2fa_migration.sql | 16 +- .../database/acl_migration.sql | 76 ++-- .../database/backup_migration.sql | 12 +- .../database/client_config_migration.sql | 2 +- .../database/client_resources_migration.sql | 4 +- .../database/create_admin.php | 4 +- .../database/database_admin_security.sql | 26 +- .../database/database_concurrency_indexes.sql | 16 +- .../database/database_setup.sql | 16 +- .../database/database_setup_with_users.sql | 28 +- .../database/docker-init/00-init.sh | 46 +- .../database/downloads_acl_migration.sql | 14 +- .../database/hardware_info_migration.sql | 18 +- .../database/hardware_info_v2_migration.sql | 56 +-- .../database/hash_temp_passwords.php | 4 +- .../database/i18n_migration.sql | 6 +- FINAL_PRODUCTION_SYSTEM/database/install.sql | 44 +- .../database/integrations_migration.sql | 8 +- .../database/license_migration.sql | 6 +- .../database/missing_drivers_migration.sql | 10 +- .../database/order_field_config_migration.sql | 12 +- .../database/product_variants_migration.sql | 14 +- .../production_tracking_migration.sql | 10 +- .../database/push_notifications_migration.sql | 14 +- .../database/qc_compliance_migration.sql | 34 +- .../database/rate_limiting_migration.sql | 4 +- .../database/rbac_migration.sql | 20 +- .../database/schema_versions_migration.sql | 2 +- .../database/seed_demo_compliance.sql | 34 +- .../database/seed_variants.sql | 22 +- .../database/task_pipeline_migration.sql | 10 +- .../database/temp_password_hash_migration.sql | 2 +- .../database/unallocated_space_migration.sql | 2 +- .../database/upgrade_system_migration.sql | 2 +- .../database/usb_devices_migration.sql | 2 +- FINAL_PRODUCTION_SYSTEM/functions/acl.php | 68 +-- .../functions/admin-helpers.php | 26 +- .../functions/csv-import.php | 14 +- .../functions/db-helpers.php | 37 ++ .../functions/integration-helpers.php | 26 +- .../functions/key-helpers.php | 4 +- .../functions/license-helpers.php | 12 +- .../functions/network-utils.php | 6 +- .../functions/push-helpers.php | 10 +- .../functions/qc-compliance.php | 34 +- .../functions/session-helpers.php | 12 +- .../functions/totp-helpers.php | 10 +- FINAL_PRODUCTION_SYSTEM/install/ajax.php | 136 ++++-- FINAL_PRODUCTION_SYSTEM/install/index.php | 10 +- FINAL_PRODUCTION_SYSTEM/secure-admin.php | 4 +- FINAL_PRODUCTION_SYSTEM/setup/index.php | 4 +- tools/prefix-codemod.php | 398 ++++++++++++++++++ 92 files changed, 1220 insertions(+), 679 deletions(-) create mode 100644 FINAL_PRODUCTION_SYSTEM/functions/db-helpers.php create mode 100644 tools/prefix-codemod.php diff --git a/.gitignore b/.gitignore index 8cb5ce7..9aa8908 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,9 @@ logs/ # Don't commit 12MB+ artifacts — every dev rebuilds locally on first commit. **/graphify-out/ +# ── Uploaded client artifacts (per-instance, regenerated at runtime) ── +FINAL_PRODUCTION_SYSTEM/uploads/client-resources/ + # ── PHP Dependencies (managed by Composer) ──────────────── FINAL_PRODUCTION_SYSTEM/vendor/ diff --git a/FINAL_PRODUCTION_SYSTEM/admin_v2.php b/FINAL_PRODUCTION_SYSTEM/admin_v2.php index cf50293..5937524 100644 --- a/FINAL_PRODUCTION_SYSTEM/admin_v2.php +++ b/FINAL_PRODUCTION_SYSTEM/admin_v2.php @@ -154,7 +154,7 @@ if (isset($_POST['action']) && $_POST['action'] === 'change_language' && isset($_POST['language'])) { $newLang = preg_replace('/[^a-z]/', '', strtolower($_POST['language'])); if (in_array($newLang, ['en', 'ru'])) { - $stmt = $pdo->prepare("UPDATE admin_users SET preferred_language = ? WHERE id = ?"); + $stmt = $pdo->prepare("UPDATE `" . t('admin_users') . "` SET preferred_language = ? WHERE id = ?"); $stmt->execute([$newLang, $admin_session['admin_id']]); loadLanguage($newLang); if (!empty($_SERVER['HTTP_X_REQUESTED_WITH'])) { @@ -404,7 +404,7 @@ // Handle logout if (isset($_GET['logout'])) { if (isset($_SESSION['admin_token'])) { - $stmt = $pdo->prepare("UPDATE admin_sessions SET is_active = 0 WHERE session_token = ?"); + $stmt = $pdo->prepare("UPDATE `" . t('admin_sessions') . "` SET is_active = 0 WHERE session_token = ?"); $stmt->execute([$_SESSION['admin_token']]); } session_destroy(); diff --git a/FINAL_PRODUCTION_SYSTEM/api/authenticate-usb.php b/FINAL_PRODUCTION_SYSTEM/api/authenticate-usb.php index bddcedf..3703043 100644 --- a/FINAL_PRODUCTION_SYSTEM/api/authenticate-usb.php +++ b/FINAL_PRODUCTION_SYSTEM/api/authenticate-usb.php @@ -90,8 +90,8 @@ // Find USB device by serial number $stmt = $pdo->prepare(" SELECT d.*, t.full_name, t.is_active - FROM usb_devices d - INNER JOIN technicians t ON d.technician_id = t.technician_id + FROM `" . t('usb_devices') . "` d + INNER JOIN `" . t('technicians') . "` t ON d.technician_id = t.technician_id WHERE d.device_serial_number = ? "); $stmt->execute([$usbSerialNumber]); @@ -165,7 +165,7 @@ // Insert session $stmt = $pdo->prepare(" - INSERT INTO active_sessions ( + INSERT INTO `" . t('active_sessions') . "` ( technician_id, session_token, created_at, expires_at, is_active, auth_method, usb_device_id, computer_name ) VALUES (?, ?, NOW(), ?, 1, 'usb', ?, ?) @@ -180,7 +180,7 @@ // Update USB device last used info $stmt = $pdo->prepare(" - UPDATE usb_devices + UPDATE `" . t('usb_devices') . "` SET last_used_date = NOW(), last_used_ip = ?, last_used_computer_name = ?, @@ -191,7 +191,7 @@ // Update technician last login $stmt = $pdo->prepare(" - UPDATE technicians + UPDATE `" . t('technicians') . "` SET last_login = NOW(), failed_login_attempts = 0, locked_until = NULL diff --git a/FINAL_PRODUCTION_SYSTEM/api/change-password.php b/FINAL_PRODUCTION_SYSTEM/api/change-password.php index 1fbaa35..d3449ec 100644 --- a/FINAL_PRODUCTION_SYSTEM/api/change-password.php +++ b/FINAL_PRODUCTION_SYSTEM/api/change-password.php @@ -29,7 +29,7 @@ try { // Get technician details $stmt = $pdo->prepare(" - SELECT * FROM technicians + SELECT * FROM `" . t('technicians') . "` WHERE technician_id = ? AND is_active = 1 "); $stmt->execute([$technician_id]); @@ -63,7 +63,7 @@ $new_password_hash = password_hash($new_password, PASSWORD_BCRYPT, ['cost' => BCRYPT_COST]); $stmt = $pdo->prepare(" - UPDATE technicians + UPDATE `" . t('technicians') . "` SET password_hash = ?, temp_password = NULL, must_change_password = FALSE WHERE technician_id = ? "); diff --git a/FINAL_PRODUCTION_SYSTEM/api/collect-hardware-v2.php b/FINAL_PRODUCTION_SYSTEM/api/collect-hardware-v2.php index 0568dbd..e839e40 100644 --- a/FINAL_PRODUCTION_SYSTEM/api/collect-hardware-v2.php +++ b/FINAL_PRODUCTION_SYSTEM/api/collect-hardware-v2.php @@ -26,8 +26,8 @@ // Validate session token and get technician info $stmt = $pdo->prepare(" SELECT s.technician_id, t.full_name, s.expires_at - FROM active_sessions s - INNER JOIN technicians t ON s.technician_id = t.technician_id + FROM `" . t('active_sessions') . "` s + INNER JOIN `" . t('technicians') . "` t ON s.technician_id = t.technician_id WHERE s.session_token = ? AND s.expires_at > NOW() LIMIT 1 @@ -44,7 +44,7 @@ // Check if hardware info already exists for this order number $stmt = $pdo->prepare(" SELECT id, collection_timestamp - FROM hardware_info + FROM `" . t('hardware_info') . "` WHERE order_number = ? ORDER BY collection_timestamp DESC LIMIT 1 @@ -164,7 +164,7 @@ // Insert hardware information $stmt = $pdo->prepare(" - INSERT INTO hardware_info ( + INSERT INTO `" . t('hardware_info') . "` ( activation_id, order_number, technician_id, session_token, motherboard_manufacturer, motherboard_product, motherboard_serial, motherboard_version, bios_manufacturer, bios_version, bios_release_date, bios_serial_number, @@ -280,7 +280,7 @@ // Log the collection attempt $stmt = $pdo->prepare(" - INSERT INTO hardware_collection_log ( + INSERT INTO `" . t('hardware_collection_log') . "` ( order_number, technician_id, session_token, hardware_info_id, collection_status ) VALUES (?, ?, ?, ?, 'success') "); @@ -316,7 +316,7 @@ try { if (isset($technicianId) && isset($orderNumber) && isset($sessionToken)) { $stmt = $pdo->prepare(" - INSERT INTO hardware_collection_log ( + INSERT INTO `" . t('hardware_collection_log') . "` ( order_number, technician_id, session_token, hardware_info_id, collection_status, error_message ) VALUES (?, ?, ?, NULL, 'failed', ?) diff --git a/FINAL_PRODUCTION_SYSTEM/api/download-resource.php b/FINAL_PRODUCTION_SYSTEM/api/download-resource.php index 509052c..02048a1 100644 --- a/FINAL_PRODUCTION_SYSTEM/api/download-resource.php +++ b/FINAL_PRODUCTION_SYSTEM/api/download-resource.php @@ -30,7 +30,7 @@ // Validate session token $stmt = $pdo->prepare(" SELECT s.technician_id - FROM active_sessions s + FROM `" . t('active_sessions') . "` s WHERE s.session_token = ? AND s.expires_at > NOW() "); $stmt->execute([$sessionToken]); @@ -43,7 +43,7 @@ } // Look up the resource - $stmt = $pdo->prepare("SELECT * FROM client_resources WHERE resource_key = ?"); + $stmt = $pdo->prepare("SELECT * FROM `" . t('client_resources') . "` WHERE resource_key = ?"); $stmt->execute([$resourceKey]); $resource = $stmt->fetch(PDO::FETCH_ASSOC); diff --git a/FINAL_PRODUCTION_SYSTEM/api/get-alt-server-config.php b/FINAL_PRODUCTION_SYSTEM/api/get-alt-server-config.php index fac23f3..519bca3 100644 --- a/FINAL_PRODUCTION_SYSTEM/api/get-alt-server-config.php +++ b/FINAL_PRODUCTION_SYSTEM/api/get-alt-server-config.php @@ -23,8 +23,8 @@ // Verify valid session and get technician preferences $stmt = $pdo->prepare(" SELECT s.technician_id, t.preferred_server - FROM active_sessions s - INNER JOIN technicians t ON s.technician_id = t.technician_id + FROM `" . t('active_sessions') . "` s + INNER JOIN `" . t('technicians') . "` t ON s.technician_id = t.technician_id WHERE s.session_token = ? AND s.expires_at > NOW() "); $stmt->execute([$sessionToken]); @@ -38,7 +38,7 @@ // Helper function to get config value function getConfig($key) { global $pdo; - $stmt = $pdo->prepare("SELECT config_value FROM system_config WHERE config_key = ?"); + $stmt = $pdo->prepare("SELECT config_value FROM `" . t('system_config') . "` WHERE config_key = ?"); $stmt->execute([$key]); $result = $stmt->fetch(PDO::FETCH_ASSOC); return $result ? $result['config_value'] : null; diff --git a/FINAL_PRODUCTION_SYSTEM/api/get-client-config.php b/FINAL_PRODUCTION_SYSTEM/api/get-client-config.php index 758ad8f..86f924b 100644 --- a/FINAL_PRODUCTION_SYSTEM/api/get-client-config.php +++ b/FINAL_PRODUCTION_SYSTEM/api/get-client-config.php @@ -23,7 +23,7 @@ // Verify valid session $stmt = $pdo->prepare(" SELECT s.technician_id - FROM active_sessions s + FROM `" . t('active_sessions') . "` s WHERE s.session_token = ? AND s.expires_at > NOW() "); $stmt->execute([$sessionToken]); diff --git a/FINAL_PRODUCTION_SYSTEM/api/get-key.php b/FINAL_PRODUCTION_SYSTEM/api/get-key.php index 24c37fc..5cde2f4 100644 --- a/FINAL_PRODUCTION_SYSTEM/api/get-key.php +++ b/FINAL_PRODUCTION_SYSTEM/api/get-key.php @@ -21,7 +21,7 @@ if (qcIsEnabled($pdo)) { $globalSettings = qcGetGlobalSettings($pdo); if (!empty($globalSettings['blocking_prevents_key'])) { - $hwStmt = $pdo->prepare("SELECT id FROM hardware_info WHERE order_number = ? ORDER BY collection_timestamp DESC LIMIT 1"); + $hwStmt = $pdo->prepare("SELECT id FROM `" . t('hardware_info') . "` WHERE order_number = ? ORDER BY collection_timestamp DESC LIMIT 1"); $hwStmt->execute([$order_number]); $hwRow = $hwStmt->fetch(PDO::FETCH_ASSOC); if ($hwRow && qcHasBlockingIssues($pdo, (int) $hwRow['id'])) { @@ -47,7 +47,7 @@ // Update existing session with new order number if different if ($existing_session['order_number'] !== $order_number) { $stmt = $pdo->prepare(" - UPDATE active_sessions + UPDATE `" . t('active_sessions') . "` SET order_number = ?, expires_at = DATE_ADD(NOW(), INTERVAL ? MINUTE) WHERE id = ? "); @@ -74,7 +74,7 @@ $pdo->rollback(); // Check if ANY keys exist vs. all keys exhausted (for automatic failover) - $stmt = $pdo->prepare("SELECT COUNT(*) as available_count FROM oem_keys WHERE key_status IN ('unused', 'retry')"); + $stmt = $pdo->prepare("SELECT COUNT(*) as available_count FROM `" . t('oem_keys') . "` WHERE key_status IN ('unused', 'retry')"); $stmt->execute(); $availableCount = $stmt->fetch(PDO::FETCH_ASSOC)['available_count']; @@ -104,7 +104,7 @@ // Insert new session (we already checked for existing sessions above) $stmt = $pdo->prepare(" - INSERT INTO active_sessions (technician_id, session_token, key_id, order_number, expires_at, auth_method, computer_name) + INSERT INTO `" . t('active_sessions') . "` (technician_id, session_token, key_id, order_number, expires_at, auth_method, computer_name) VALUES (?, ?, ?, ?, ?, 'password', ?) "); $stmt->execute([$technician_id, $session_token, $key['id'], $order_number, $expires_at, $computerName]); @@ -121,7 +121,7 @@ // Check key pool levels and send alerts if needed try { $edition = $key['product_type'] ?? 'Unknown'; - $poolStmt = $pdo->prepare("SELECT COUNT(*) as remaining FROM oem_keys WHERE key_status IN ('unused', 'retry') AND product_type = ?"); + $poolStmt = $pdo->prepare("SELECT COUNT(*) as remaining FROM `" . t('oem_keys') . "` WHERE key_status IN ('unused', 'retry') AND product_type = ?"); $poolStmt->execute([$edition]); $remaining = (int)$poolStmt->fetch()['remaining']; diff --git a/FINAL_PRODUCTION_SYSTEM/api/import-csv.php b/FINAL_PRODUCTION_SYSTEM/api/import-csv.php index 98aa1a3..9f1b995 100644 --- a/FINAL_PRODUCTION_SYSTEM/api/import-csv.php +++ b/FINAL_PRODUCTION_SYSTEM/api/import-csv.php @@ -66,7 +66,7 @@ // Try to insert key try { $stmt = $pdo->prepare(" - INSERT INTO oem_keys (product_key, oem_identifier, barcode, key_status, roll_serial) + INSERT INTO `" . t('oem_keys') . "` (product_key, oem_identifier, barcode, key_status, roll_serial) VALUES (?, ?, ?, ?, 'imported') "); $stmt->execute([$product_key, $oem_identifier, $barcode, $key_status]); diff --git a/FINAL_PRODUCTION_SYSTEM/api/login.php b/FINAL_PRODUCTION_SYSTEM/api/login.php index 3ce3275..7734621 100644 --- a/FINAL_PRODUCTION_SYSTEM/api/login.php +++ b/FINAL_PRODUCTION_SYSTEM/api/login.php @@ -16,7 +16,7 @@ try { // Get technician details (including language preference) $stmt = $pdo->prepare(" - SELECT * FROM technicians + SELECT * FROM `" . t('technicians') . "` WHERE technician_id = ? AND is_active = 1 "); $stmt->execute([$technician_id]); @@ -64,7 +64,7 @@ } $stmt = $pdo->prepare(" - UPDATE technicians + UPDATE `" . t('technicians') . "` SET failed_login_attempts = ?, locked_until = ? WHERE technician_id = ? "); @@ -94,7 +94,7 @@ // Login successful - reset failed attempts $stmt = $pdo->prepare(" - UPDATE technicians + UPDATE `" . t('technicians') . "` SET failed_login_attempts = 0, locked_until = NULL, last_login = NOW() WHERE technician_id = ? "); @@ -137,7 +137,7 @@ // Include active product lines for order type selection try { - $plStmt = $pdo->query("SELECT id, name, order_pattern, description FROM product_lines WHERE is_active = 1 ORDER BY name ASC"); + $plStmt = $pdo->query("SELECT id, name, order_pattern, description FROM `" . t('product_lines') . "` WHERE is_active = 1 ORDER BY name ASC"); $productLines = $plStmt->fetchAll(PDO::FETCH_ASSOC); if (!empty($productLines)) { $response['product_lines'] = array_map(function($pl) { diff --git a/FINAL_PRODUCTION_SYSTEM/api/middleware/RateLimiter.php b/FINAL_PRODUCTION_SYSTEM/api/middleware/RateLimiter.php index 19d5177..7d2e083 100644 --- a/FINAL_PRODUCTION_SYSTEM/api/middleware/RateLimiter.php +++ b/FINAL_PRODUCTION_SYSTEM/api/middleware/RateLimiter.php @@ -140,7 +140,7 @@ public function logViolation($identifier, $action, $endpoint, $requestCount, $li try { $stmt = $pdo->prepare(" - INSERT INTO rate_limit_violations ( + INSERT INTO `" . t('rate_limit_violations') . "` ( identifier, action, endpoint, client_ip, user_agent, request_count, limit_threshold, window_seconds ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) diff --git a/FINAL_PRODUCTION_SYSTEM/api/report-result.php b/FINAL_PRODUCTION_SYSTEM/api/report-result.php index 579e1ff..d199610 100644 --- a/FINAL_PRODUCTION_SYSTEM/api/report-result.php +++ b/FINAL_PRODUCTION_SYSTEM/api/report-result.php @@ -126,7 +126,7 @@ function validateAPIAccess(): bool { } // NEW: Check if unique ID already exists (prevent duplicates) -$stmt = $pdo->prepare("SELECT id FROM activation_attempts WHERE activation_unique_id = ?"); +$stmt = $pdo->prepare("SELECT id FROM `" . t('activation_attempts') . "` WHERE activation_unique_id = ?"); $stmt->execute([$activationUniqueId]); if ($stmt->fetch()) { jsonResponse([ @@ -156,7 +156,7 @@ function validateAPIAccess(): bool { // Record activation attempt with all required fields $stmt = $pdo->prepare(" - INSERT INTO activation_attempts ( + INSERT INTO `" . t('activation_attempts') . "` ( key_id, technician_id, order_number, @@ -203,7 +203,7 @@ function validateAPIAccess(): bool { if ($result === 'success') { // Success path: Mark key as good and deactivate session $stmt = $pdo->prepare(" - UPDATE oem_keys + UPDATE `" . t('oem_keys') . "` SET key_status = 'good', updated_at = NOW() WHERE id = ? @@ -212,7 +212,7 @@ function validateAPIAccess(): bool { // Deactivate session (activation complete) $stmt = $pdo->prepare(" - UPDATE active_sessions + UPDATE `" . t('active_sessions') . "` SET is_active = 0 WHERE id = ? "); @@ -226,7 +226,7 @@ function validateAPIAccess(): bool { } else { // Failure path: Update fail counter and determine key status $stmt = $pdo->prepare(" - UPDATE oem_keys + UPDATE `" . t('oem_keys') . "` SET fail_counter = fail_counter + 1, updated_at = NOW() WHERE id = ? @@ -236,7 +236,7 @@ function validateAPIAccess(): bool { // Get updated fail counter $stmt = $pdo->prepare(" SELECT fail_counter, product_key, oem_identifier - FROM oem_keys + FROM `" . t('oem_keys') . "` WHERE id = ? "); $stmt->execute([$session['key_id']]); @@ -253,7 +253,7 @@ function validateAPIAccess(): bool { if ($failCounter >= $maxAttempts) { // Mark as bad after max failures $stmt = $pdo->prepare(" - UPDATE oem_keys + UPDATE `" . t('oem_keys') . "` SET key_status = 'bad' WHERE id = ? "); @@ -264,7 +264,7 @@ function validateAPIAccess(): bool { // Deactivate session (key is bad) $stmt = $pdo->prepare(" - UPDATE active_sessions + UPDATE `" . t('active_sessions') . "` SET is_active = 0 WHERE id = ? "); @@ -275,7 +275,7 @@ function validateAPIAccess(): bool { } else { // Mark for retry $stmt = $pdo->prepare(" - UPDATE oem_keys + UPDATE `" . t('oem_keys') . "` SET key_status = 'retry' WHERE id = ? "); @@ -382,7 +382,7 @@ function sendEmailNotification( if ($keyData === null) { $stmt = $pdo->prepare(" SELECT product_key, oem_identifier, fail_counter, key_status - FROM oem_keys + FROM `" . t('oem_keys') . "` WHERE id = ? "); $stmt->execute([$session['key_id']]); @@ -396,7 +396,7 @@ function sendEmailNotification( // Get technician full name $stmt = $pdo->prepare(" SELECT full_name, email - FROM technicians + FROM `" . t('technicians') . "` WHERE technician_id = ? "); $stmt->execute([$session['technician_id']]); diff --git a/FINAL_PRODUCTION_SYSTEM/api/submit-hardware.php b/FINAL_PRODUCTION_SYSTEM/api/submit-hardware.php index e22d856..bb4496b 100644 --- a/FINAL_PRODUCTION_SYSTEM/api/submit-hardware.php +++ b/FINAL_PRODUCTION_SYSTEM/api/submit-hardware.php @@ -47,8 +47,8 @@ // Validate session token and get activation_id $stmt = $pdo->prepare(" SELECT aa.id as activation_id, aa.technician_id, aa.key_id - FROM activation_attempts aa - INNER JOIN active_sessions s ON s.technician_id = aa.technician_id + FROM `" . t('activation_attempts') . "` aa + INNER JOIN `" . t('active_sessions') . "` s ON s.technician_id = aa.technician_id WHERE s.session_token = ? AND aa.order_number = ? AND aa.attempt_result = 'success' @@ -66,7 +66,7 @@ $activationId = $activation['activation_id']; // Check if hardware info already exists for this activation - $stmt = $pdo->prepare("SELECT id FROM hardware_info WHERE activation_id = ?"); + $stmt = $pdo->prepare("SELECT id FROM `" . t('hardware_info') . "` WHERE activation_id = ?"); $stmt->execute([$activationId]); if ($stmt->fetch()) { jsonResponse(['success' => true, 'message' => 'Hardware information already recorded', 'duplicate' => true]); @@ -77,7 +77,7 @@ // Insert hardware information $stmt = $pdo->prepare(" - INSERT INTO hardware_info ( + INSERT INTO `" . t('hardware_info') . "` ( activation_id, order_number, motherboard_manufacturer, @@ -188,7 +188,7 @@ ]); // Update activation_attempts to mark hardware as collected - $stmt = $pdo->prepare("UPDATE activation_attempts SET hardware_collected = 1 WHERE id = ?"); + $stmt = $pdo->prepare("UPDATE `" . t('activation_attempts') . "` SET hardware_collected = 1 WHERE id = ?"); $stmt->execute([$activationId]); $hardwareId = $pdo->lastInsertId(); diff --git a/FINAL_PRODUCTION_SYSTEM/api/totp-disable.php b/FINAL_PRODUCTION_SYSTEM/api/totp-disable.php index 2148be3..9d59c58 100644 --- a/FINAL_PRODUCTION_SYSTEM/api/totp-disable.php +++ b/FINAL_PRODUCTION_SYSTEM/api/totp-disable.php @@ -62,7 +62,7 @@ // Code verified - disable 2FA $stmt = $pdo->prepare(" - UPDATE admin_totp_secrets + UPDATE `" . t('admin_totp_secrets') . "` SET totp_enabled = 0 WHERE admin_id = ? "); @@ -70,7 +70,7 @@ // Log activity $stmt = $pdo->prepare(" - INSERT INTO admin_activity_log (admin_id, session_id, action, description, ip_address, user_agent) + INSERT INTO `" . t('admin_activity_log') . "` (admin_id, session_id, action, description, ip_address, user_agent) VALUES (?, ?, 'TOTP_DISABLED', '2FA disabled by user', ?, ?) "); $stmt->execute([ diff --git a/FINAL_PRODUCTION_SYSTEM/api/totp-regenerate-backup-codes.php b/FINAL_PRODUCTION_SYSTEM/api/totp-regenerate-backup-codes.php index 0341507..796b8ef 100644 --- a/FINAL_PRODUCTION_SYSTEM/api/totp-regenerate-backup-codes.php +++ b/FINAL_PRODUCTION_SYSTEM/api/totp-regenerate-backup-codes.php @@ -65,7 +65,7 @@ // Update database $stmt = $pdo->prepare(" - UPDATE admin_totp_secrets + UPDATE `" . t('admin_totp_secrets') . "` SET backup_codes = ? WHERE admin_id = ? "); @@ -76,7 +76,7 @@ // Log activity $stmt = $pdo->prepare(" - INSERT INTO admin_activity_log (admin_id, session_id, action, description, ip_address, user_agent) + INSERT INTO `" . t('admin_activity_log') . "` (admin_id, session_id, action, description, ip_address, user_agent) VALUES (?, ?, 'TOTP_BACKUP_REGEN', 'Regenerated 2FA backup codes', ?, ?) "); $stmt->execute([ diff --git a/FINAL_PRODUCTION_SYSTEM/api/totp-setup.php b/FINAL_PRODUCTION_SYSTEM/api/totp-setup.php index a6f799c..9e8ca57 100644 --- a/FINAL_PRODUCTION_SYSTEM/api/totp-setup.php +++ b/FINAL_PRODUCTION_SYSTEM/api/totp-setup.php @@ -24,7 +24,7 @@ // Verify session is valid in DB $stmt = $pdo->prepare(" - SELECT admin_id FROM admin_sessions + SELECT admin_id FROM `" . t('admin_sessions') . "` WHERE id = ? AND admin_id = ? AND is_active = 1 "); $stmt->execute([$sessionId, $adminId]); @@ -33,7 +33,7 @@ } // Get admin info -$stmt = $pdo->prepare("SELECT username, email FROM admin_users WHERE id = ?"); +$stmt = $pdo->prepare("SELECT username, email FROM `" . t('admin_users') . "` WHERE id = ?"); $stmt->execute([$adminId]); $admin = $stmt->fetch(PDO::FETCH_ASSOC); @@ -75,7 +75,7 @@ // Store in database (not yet enabled) if ($existing) { $stmt = $pdo->prepare(" - UPDATE admin_totp_secrets + UPDATE `" . t('admin_totp_secrets') . "` SET totp_secret = ?, backup_codes = ?, totp_enabled = 0, verified_at = NULL, created_at = NOW() WHERE admin_id = ? "); @@ -86,7 +86,7 @@ ]); } else { $stmt = $pdo->prepare(" - INSERT INTO admin_totp_secrets (admin_id, totp_secret, backup_codes, totp_enabled) + INSERT INTO `" . t('admin_totp_secrets') . "` (admin_id, totp_secret, backup_codes, totp_enabled) VALUES (?, ?, ?, 0) "); $stmt->execute([ @@ -108,7 +108,7 @@ // Log activity $stmt = $pdo->prepare(" - INSERT INTO admin_activity_log (admin_id, session_id, action, description, ip_address, user_agent) + INSERT INTO `" . t('admin_activity_log') . "` (admin_id, session_id, action, description, ip_address, user_agent) VALUES (?, ?, 'TOTP_SETUP', 'Started 2FA setup', ?, ?) "); $stmt->execute([ diff --git a/FINAL_PRODUCTION_SYSTEM/api/totp-verify.php b/FINAL_PRODUCTION_SYSTEM/api/totp-verify.php index 93196a6..441ec30 100644 --- a/FINAL_PRODUCTION_SYSTEM/api/totp-verify.php +++ b/FINAL_PRODUCTION_SYSTEM/api/totp-verify.php @@ -69,7 +69,7 @@ // If this is initial setup verification, enable 2FA if ($isSetup && $totpData['totp_enabled'] == 0) { $stmt = $pdo->prepare(" - UPDATE admin_totp_secrets + UPDATE `" . t('admin_totp_secrets') . "` SET totp_enabled = 1, verified_at = NOW() WHERE admin_id = ? "); @@ -77,7 +77,7 @@ // Log activity $stmt = $pdo->prepare(" - INSERT INTO admin_activity_log (admin_id, session_id, action, description, ip_address, user_agent) + INSERT INTO `" . t('admin_activity_log') . "` (admin_id, session_id, action, description, ip_address, user_agent) VALUES (?, NULL, 'TOTP_ENABLED', '2FA successfully enabled', ?, ?) "); $stmt->execute([ @@ -89,7 +89,7 @@ // Log successful verification $stmt = $pdo->prepare(" - INSERT INTO admin_activity_log (admin_id, session_id, action, description, ip_address, user_agent, totp_verified) + INSERT INTO `" . t('admin_activity_log') . "` (admin_id, session_id, action, description, ip_address, user_agent, totp_verified) VALUES (?, NULL, 'TOTP_VERIFIED', ?, ?, ?, 1) "); $stmt->execute([ diff --git a/FINAL_PRODUCTION_SYSTEM/config-production.php b/FINAL_PRODUCTION_SYSTEM/config-production.php index e2850c2..8d004f9 100644 --- a/FINAL_PRODUCTION_SYSTEM/config-production.php +++ b/FINAL_PRODUCTION_SYSTEM/config-production.php @@ -89,7 +89,7 @@ function getConfig($key, $useCache = true) { } try { - $stmt = $pdo->prepare("SELECT config_value FROM system_config WHERE config_key = ?"); + $stmt = $pdo->prepare("SELECT config_value FROM `" . t('system_config') . "` WHERE config_key = ?"); $stmt->execute([$key]); $result = $stmt->fetch(); $value = $result ? $result['config_value'] : null; @@ -192,9 +192,9 @@ function validateSession($token) { try { $stmt = $pdo->prepare(" SELECT s.*, k.product_key, k.key_status, t.is_active as tech_active - FROM active_sessions s - LEFT JOIN oem_keys k ON s.key_id = k.id - LEFT JOIN technicians t ON s.technician_id = t.technician_id + FROM `" . t('active_sessions') . "` s + LEFT JOIN `" . t('oem_keys') . "` k ON s.key_id = k.id + LEFT JOIN `" . t('technicians') . "` t ON s.technician_id = t.technician_id WHERE s.session_token = ? AND s.is_active = 1 AND s.expires_at > NOW() @@ -233,7 +233,7 @@ function allocateKeyAtomically($pdo, $technician_id, $order_number) { // Select and lock the best available key $stmt = $pdo->prepare(" - SELECT * FROM oem_keys + SELECT * FROM `" . t('oem_keys') . "` WHERE key_status IN ('unused', 'retry') AND (fail_counter < 3 OR key_status = 'unused') ORDER BY @@ -250,7 +250,7 @@ function allocateKeyAtomically($pdo, $technician_id, $order_number) { if ($key) { // Mark key as in use immediately $stmt = $pdo->prepare(" - UPDATE oem_keys + UPDATE `" . t('oem_keys') . "` SET key_status = 'allocated', last_use_date = CURDATE(), last_use_time = CURTIME(), @@ -295,7 +295,7 @@ function allocateKeyAtomically($pdo, $technician_id, $order_number) { function cleanupExpiredSessions($pdo) { try { $stmt = $pdo->prepare(" - UPDATE active_sessions + UPDATE `" . t('active_sessions') . "` SET is_active = 0 WHERE expires_at < NOW() AND is_active = 1 LIMIT 1000 @@ -321,8 +321,8 @@ function getActiveSession($pdo, $technician_id) { $stmt = $pdo->prepare(" SELECT s.*, k.product_key, k.oem_identifier, k.key_status, k.fail_counter - FROM active_sessions s - LEFT JOIN oem_keys k ON s.key_id = k.id + FROM `" . t('active_sessions') . "` s + LEFT JOIN `" . t('oem_keys') . "` k ON s.key_id = k.id WHERE s.technician_id = ? AND s.is_active = 1 AND s.expires_at > NOW() diff --git a/FINAL_PRODUCTION_SYSTEM/config.php b/FINAL_PRODUCTION_SYSTEM/config.php index 6268dfd..29304e0 100644 --- a/FINAL_PRODUCTION_SYSTEM/config.php +++ b/FINAL_PRODUCTION_SYSTEM/config.php @@ -108,7 +108,7 @@ function getConfig($key, $useCache = true) { } try { - $stmt = $pdo->prepare("SELECT config_value FROM system_config WHERE config_key = ?"); + $stmt = $pdo->prepare("SELECT config_value FROM `" . t('system_config') . "` WHERE config_key = ?"); $stmt->execute([$key]); $result = $stmt->fetch(); $value = $result ? $result['config_value'] : null; diff --git a/FINAL_PRODUCTION_SYSTEM/config/config-template-enhanced.php b/FINAL_PRODUCTION_SYSTEM/config/config-template-enhanced.php index 05474de..289a853 100644 --- a/FINAL_PRODUCTION_SYSTEM/config/config-template-enhanced.php +++ b/FINAL_PRODUCTION_SYSTEM/config/config-template-enhanced.php @@ -46,7 +46,7 @@ function getConfig($key, $default = null) { global $pdo; try { - $stmt = $pdo->prepare("SELECT config_value FROM system_config WHERE config_key = ?"); + $stmt = $pdo->prepare("SELECT config_value FROM `" . t('system_config') . "` WHERE config_key = ?"); $stmt->execute([$key]); $result = $stmt->fetch(); return $result ? $result['config_value'] : $default; @@ -61,7 +61,7 @@ function setConfig($key, $value, $description = '') { global $pdo; try { $stmt = $pdo->prepare(" - INSERT INTO system_config (config_key, config_value, description) + INSERT INTO `" . t('system_config') . "` (config_key, config_value, description) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE config_value = VALUES(config_value), @@ -102,7 +102,7 @@ function validateTechnician($technician_id, $password) { try { $stmt = $pdo->prepare(" - SELECT * FROM technicians + SELECT * FROM `" . t('technicians') . "` WHERE technician_id = ? AND is_active = 1 "); $stmt->execute([$technician_id]); @@ -121,7 +121,7 @@ function validateTechnician($technician_id, $password) { if (password_verify($password, $technician['password_hash'])) { // Reset failed attempts on successful login $stmt = $pdo->prepare(" - UPDATE technicians + UPDATE `" . t('technicians') . "` SET failed_login_attempts = 0, locked_until = NULL, last_login = NOW() WHERE technician_id = ? "); @@ -132,7 +132,7 @@ function validateTechnician($technician_id, $password) { // Increment failed attempts $stmt = $pdo->prepare(" - UPDATE technicians + UPDATE `" . t('technicians') . "` SET failed_login_attempts = failed_login_attempts + 1, locked_until = IF(failed_login_attempts + 1 >= 5, DATE_ADD(NOW(), INTERVAL 15 MINUTE), NULL) WHERE technician_id = ? diff --git a/FINAL_PRODUCTION_SYSTEM/constants.php b/FINAL_PRODUCTION_SYSTEM/constants.php index fdb4101..45fc95e 100644 --- a/FINAL_PRODUCTION_SYSTEM/constants.php +++ b/FINAL_PRODUCTION_SYSTEM/constants.php @@ -7,6 +7,11 @@ * Import this file wherever constants are needed. */ +// ── DB table prefix helper (must load before any PDO query) ──────── +// Empty default for backward-compat with installs that pre-date the +// prefix release. The web installer overwrites DB_PREFIX in config.php. +require_once __DIR__ . '/functions/db-helpers.php'; + // ── Authentication ───────────────────────────────────────────────── define('BCRYPT_COST', 12); define('PASSWORD_MIN_LENGTH', 8); diff --git a/FINAL_PRODUCTION_SYSTEM/controllers/admin/AclController.php b/FINAL_PRODUCTION_SYSTEM/controllers/admin/AclController.php index a05b65d..8c9e914 100644 --- a/FINAL_PRODUCTION_SYSTEM/controllers/admin/AclController.php +++ b/FINAL_PRODUCTION_SYSTEM/controllers/admin/AclController.php @@ -62,7 +62,7 @@ function handle_acl_create_role(PDO $pdo, array $admin_session, ?array $json_inp } // Rate limit: max 20 custom roles - $stmt = $pdo->prepare("SELECT COUNT(*) FROM acl_roles WHERE is_system_role = 0"); + $stmt = $pdo->prepare("SELECT COUNT(*) FROM `" . t('acl_roles') . "` WHERE is_system_role = 0"); $stmt->execute(); if ((int)$stmt->fetchColumn() >= 20) { jsonResponse(['success' => false, 'error' => 'Maximum custom roles limit reached (20)']); diff --git a/FINAL_PRODUCTION_SYSTEM/controllers/admin/BackupsController.php b/FINAL_PRODUCTION_SYSTEM/controllers/admin/BackupsController.php index 89a5d0c..78f283e 100644 --- a/FINAL_PRODUCTION_SYSTEM/controllers/admin/BackupsController.php +++ b/FINAL_PRODUCTION_SYSTEM/controllers/admin/BackupsController.php @@ -8,7 +8,7 @@ function handle_list_backups(PDO $pdo, array $admin_session): void { requirePermission('view_backups', $admin_session); $stmt = $pdo->query(" - SELECT * FROM backup_history + SELECT * FROM `" . t('backup_history') . "` ORDER BY created_at DESC LIMIT 50 "); diff --git a/FINAL_PRODUCTION_SYSTEM/controllers/admin/BrandingController.php b/FINAL_PRODUCTION_SYSTEM/controllers/admin/BrandingController.php index ea96a58..a23d8ea 100644 --- a/FINAL_PRODUCTION_SYSTEM/controllers/admin/BrandingController.php +++ b/FINAL_PRODUCTION_SYSTEM/controllers/admin/BrandingController.php @@ -140,7 +140,7 @@ function handle_upload_brand_asset(PDO $pdo, array $admin_session): void { // Store relative path in system_config $relativePath = 'uploads/branding/' . $storedFilename; $stmt = $pdo->prepare(" - INSERT INTO system_config (config_key, config_value, description, updated_at) + INSERT INTO `" . t('system_config') . "` (config_key, config_value, description, updated_at) VALUES (?, ?, '', NOW()) ON DUPLICATE KEY UPDATE config_value = ?, updated_at = NOW() "); @@ -181,7 +181,7 @@ function handle_delete_brand_asset(PDO $pdo, array $admin_session, ?array $json_ // Clear config value $stmt = $pdo->prepare(" - INSERT INTO system_config (config_key, config_value, description, updated_at) + INSERT INTO `" . t('system_config') . "` (config_key, config_value, description, updated_at) VALUES (?, '', '', NOW()) ON DUPLICATE KEY UPDATE config_value = '', updated_at = NOW() "); diff --git a/FINAL_PRODUCTION_SYSTEM/controllers/admin/ClientResourcesController.php b/FINAL_PRODUCTION_SYSTEM/controllers/admin/ClientResourcesController.php index 5447c94..a9ce0e0 100644 --- a/FINAL_PRODUCTION_SYSTEM/controllers/admin/ClientResourcesController.php +++ b/FINAL_PRODUCTION_SYSTEM/controllers/admin/ClientResourcesController.php @@ -68,7 +68,7 @@ function handle_upload_client_resource(PDO $pdo, array $admin_session): void { $destPath = $uploadDir . '/' . $storedFilename; // Delete existing resource with same key (replace mode) - $existing = $pdo->prepare("SELECT filename FROM client_resources WHERE resource_key = ?"); + $existing = $pdo->prepare("SELECT filename FROM `" . t('client_resources') . "` WHERE resource_key = ?"); $existing->execute([$resourceKey]); $oldFile = $existing->fetchColumn(); if ($oldFile) { @@ -76,7 +76,7 @@ function handle_upload_client_resource(PDO $pdo, array $admin_session): void { if (file_exists($oldPath)) { unlink($oldPath); } - $pdo->prepare("DELETE FROM client_resources WHERE resource_key = ?")->execute([$resourceKey]); + $pdo->prepare("DELETE FROM `" . t('client_resources') . "` WHERE resource_key = ?")->execute([$resourceKey]); } // Move uploaded file @@ -95,7 +95,7 @@ function handle_upload_client_resource(PDO $pdo, array $admin_session): void { // Insert DB record $stmt = $pdo->prepare(" - INSERT INTO client_resources (resource_key, filename, original_filename, file_size, mime_type, checksum_sha256, description, uploaded_by) + INSERT INTO `" . t('client_resources') . "` (resource_key, filename, original_filename, file_size, mime_type, checksum_sha256, description, uploaded_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?) "); $stmt->execute([$resourceKey, $storedFilename, $originalName, $fileSize, $mimeType, $checksum, $description, $adminId]); @@ -134,7 +134,7 @@ function handle_delete_client_resource(PDO $pdo, array $admin_session, ?array $j return; } - $stmt = $pdo->prepare("SELECT filename FROM client_resources WHERE resource_key = ?"); + $stmt = $pdo->prepare("SELECT filename FROM `" . t('client_resources') . "` WHERE resource_key = ?"); $stmt->execute([$resourceKey]); $filename = $stmt->fetchColumn(); @@ -151,7 +151,7 @@ function handle_delete_client_resource(PDO $pdo, array $admin_session, ?array $j } // Delete DB record - $pdo->prepare("DELETE FROM client_resources WHERE resource_key = ?")->execute([$resourceKey]); + $pdo->prepare("DELETE FROM `" . t('client_resources') . "` WHERE resource_key = ?")->execute([$resourceKey]); logAdminActivity( $admin_session['admin_id'], @@ -172,8 +172,8 @@ function handle_list_client_resources(PDO $pdo, array $admin_session): void { $stmt = $pdo->prepare(" SELECT cr.*, au.username AS uploaded_by_name - FROM client_resources cr - LEFT JOIN admin_users au ON cr.uploaded_by = au.id + FROM `" . t('client_resources') . "` cr + LEFT JOIN `" . t('admin_users') . "` au ON cr.uploaded_by = au.id ORDER BY cr.created_at DESC "); $stmt->execute(); @@ -200,7 +200,7 @@ function handle_download_client_resource(PDO $pdo, array $admin_session): void { return; } - $stmt = $pdo->prepare("SELECT filename, original_filename, mime_type, file_size, checksum_sha256 FROM client_resources WHERE resource_key = ?"); + $stmt = $pdo->prepare("SELECT filename, original_filename, mime_type, file_size, checksum_sha256 FROM `" . t('client_resources') . "` WHERE resource_key = ?"); $stmt->execute([$resourceKey]); $resource = $stmt->fetch(PDO::FETCH_ASSOC); diff --git a/FINAL_PRODUCTION_SYSTEM/controllers/admin/ComplianceController.php b/FINAL_PRODUCTION_SYSTEM/controllers/admin/ComplianceController.php index 6c4395a..94a4350 100644 --- a/FINAL_PRODUCTION_SYSTEM/controllers/admin/ComplianceController.php +++ b/FINAL_PRODUCTION_SYSTEM/controllers/admin/ComplianceController.php @@ -23,7 +23,7 @@ function handle_qc_save_settings(PDO $pdo, array $admin_session, ?array $json_in foreach ($allowedKeys as $key) { if (isset($json_input[$key])) { - $stmt = $pdo->prepare("UPDATE qc_global_settings SET setting_value = ?, updated_by = ? WHERE setting_key = ?"); + $stmt = $pdo->prepare("UPDATE `" . t('qc_global_settings') . "` SET setting_value = ?, updated_by = ? WHERE setting_key = ?"); $stmt->execute([$json_input[$key], $admin_session['admin_id'], $key]); } } @@ -95,7 +95,7 @@ function handle_qc_list_motherboards(PDO $pdo, array $admin_session, ?array $jso unset($row); // Distinct manufacturers for filter dropdown - $mfrs = $pdo->query("SELECT DISTINCT manufacturer FROM qc_motherboard_registry ORDER BY manufacturer")->fetchAll(PDO::FETCH_COLUMN); + $mfrs = $pdo->query("SELECT DISTINCT manufacturer FROM `" . t('qc_motherboard_registry') . "` ORDER BY manufacturer")->fetchAll(PDO::FETCH_COLUMN); jsonResponse([ 'success' => true, @@ -111,7 +111,7 @@ function handle_qc_get_motherboard(PDO $pdo, array $admin_session, ?array $json_ requirePermission('view_compliance', $admin_session); $id = (int) ($_GET['id'] ?? 0); - $stmt = $pdo->prepare("SELECT * FROM qc_motherboard_registry WHERE id = ?"); + $stmt = $pdo->prepare("SELECT * FROM `" . t('qc_motherboard_registry') . "` WHERE id = ?"); $stmt->execute([$id]); $board = $stmt->fetch(PDO::FETCH_ASSOC); @@ -161,7 +161,7 @@ function handle_qc_update_motherboard(PDO $pdo, array $admin_session, ?array $js $params[] = $admin_session['admin_id']; $params[] = $id; - $stmt = $pdo->prepare("UPDATE qc_motherboard_registry SET " . implode(", ", $fields) . " WHERE id = ?"); + $stmt = $pdo->prepare("UPDATE `" . t('qc_motherboard_registry') . "` SET " . implode(", ", $fields) . " WHERE id = ?"); $stmt->execute($params); logAdminActivity($admin_session['admin_id'], $admin_session['id'], 'QC_MOTHERBOARD_UPDATE', "Updated motherboard registry #$id"); @@ -174,14 +174,14 @@ function handle_qc_list_manufacturers(PDO $pdo, array $admin_session, ?array $js requirePermission('view_compliance', $admin_session); // Configured manufacturers - $stmt = $pdo->query("SELECT * FROM qc_manufacturer_defaults ORDER BY manufacturer"); + $stmt = $pdo->query("SELECT * FROM `" . t('qc_manufacturer_defaults') . "` ORDER BY manufacturer"); $configured = $stmt->fetchAll(PDO::FETCH_ASSOC); // Unconfigured: manufacturers seen in registry but no defaults entry $stmt = $pdo->query(" SELECT DISTINCT r.manufacturer - FROM qc_motherboard_registry r - LEFT JOIN qc_manufacturer_defaults md ON r.manufacturer = md.manufacturer + FROM `" . t('qc_motherboard_registry') . "` r + LEFT JOIN `" . t('qc_manufacturer_defaults') . "` md ON r.manufacturer = md.manufacturer WHERE md.id IS NULL ORDER BY r.manufacturer "); @@ -200,7 +200,7 @@ function handle_qc_update_manufacturer(PDO $pdo, array $admin_session, ?array $j } $stmt = $pdo->prepare(" - INSERT INTO qc_manufacturer_defaults (manufacturer, secure_boot_required, secure_boot_enforcement, min_bios_version, recommended_bios_version, bios_enforcement, hackbgrt_enforcement, notes, updated_by) + INSERT INTO `" . t('qc_manufacturer_defaults') . "` (manufacturer, secure_boot_required, secure_boot_enforcement, min_bios_version, recommended_bios_version, bios_enforcement, hackbgrt_enforcement, notes, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE secure_boot_required = VALUES(secure_boot_required), @@ -454,7 +454,7 @@ function handle_qc_get_stats(PDO $pdo, array $admin_session, ?array $json_input requirePermission('view_compliance', $admin_session); // Result counts - $stmt = $pdo->query("SELECT check_result, COUNT(*) as cnt FROM qc_compliance_results GROUP BY check_result"); + $stmt = $pdo->query("SELECT check_result, COUNT(*) as cnt FROM `" . t('qc_compliance_results') . "` GROUP BY check_result"); $resultCounts = $stmt->fetchAll(PDO::FETCH_KEY_PAIR); $total = array_sum($resultCounts); @@ -463,8 +463,8 @@ function handle_qc_get_stats(PDO $pdo, array $admin_session, ?array $json_input // Top failing boards $stmt = $pdo->query(" SELECT hi.motherboard_manufacturer, hi.motherboard_product, COUNT(*) as fail_count - FROM qc_compliance_results cr - JOIN hardware_info hi ON cr.hardware_info_id = hi.id + FROM `" . t('qc_compliance_results') . "` cr + JOIN `" . t('hardware_info') . "` hi ON cr.hardware_info_id = hi.id WHERE cr.check_result = 'fail' GROUP BY hi.motherboard_manufacturer, hi.motherboard_product ORDER BY fail_count DESC @@ -473,19 +473,19 @@ function handle_qc_get_stats(PDO $pdo, array $admin_session, ?array $json_input $topFailing = $stmt->fetchAll(PDO::FETCH_ASSOC); // Unresolved blocking - $stmt = $pdo->query("SELECT COUNT(DISTINCT hardware_info_id) FROM qc_compliance_results WHERE enforcement_level = 3 AND check_result = 'fail'"); + $stmt = $pdo->query("SELECT COUNT(DISTINCT hardware_info_id) FROM `" . t('qc_compliance_results') . "` WHERE enforcement_level = 3 AND check_result = 'fail'"); $unresolvedBlocking = (int) $stmt->fetchColumn(); // Registry stats - $stmt = $pdo->query("SELECT COUNT(*) FROM qc_motherboard_registry"); + $stmt = $pdo->query("SELECT COUNT(*) FROM `" . t('qc_motherboard_registry') . "`"); $registeredBoards = (int) $stmt->fetchColumn(); - $stmt = $pdo->query("SELECT COUNT(*) FROM qc_manufacturer_defaults"); + $stmt = $pdo->query("SELECT COUNT(*) FROM `" . t('qc_manufacturer_defaults') . "`"); $mfrsWithDefaults = (int) $stmt->fetchColumn(); // Check type breakdown $stmt = $pdo->query(" SELECT check_type, check_result, COUNT(*) as cnt - FROM qc_compliance_results + FROM `" . t('qc_compliance_results') . "` GROUP BY check_type, check_result "); $byType = []; diff --git a/FINAL_PRODUCTION_SYSTEM/controllers/admin/DashboardController.php b/FINAL_PRODUCTION_SYSTEM/controllers/admin/DashboardController.php index 41ee81f..dee6f2c 100644 --- a/FINAL_PRODUCTION_SYSTEM/controllers/admin/DashboardController.php +++ b/FINAL_PRODUCTION_SYSTEM/controllers/admin/DashboardController.php @@ -17,7 +17,7 @@ function buildReportHtml(PDO $pdo, string $reportType): string { switch ($reportType) { case 'summary': - $stmt = $pdo->query("SELECT key_status, COUNT(*) as count FROM oem_keys GROUP BY key_status"); + $stmt = $pdo->query("SELECT key_status, COUNT(*) as count FROM `" . t('oem_keys') . "` GROUP BY key_status"); $keyStats = $stmt->fetchAll(PDO::FETCH_KEY_PAIR); $totalKeys = array_sum($keyStats); @@ -32,7 +32,7 @@ function buildReportHtml(PDO $pdo, string $reportType): string { $html .= ''; // Technician summary - $stmt = $pdo->query("SELECT is_active, COUNT(*) as count FROM technicians GROUP BY is_active"); + $stmt = $pdo->query("SELECT is_active, COUNT(*) as count FROM `" . t('technicians') . "` GROUP BY is_active"); $techStats = $stmt->fetchAll(PDO::FETCH_KEY_PAIR); $html .= '

' . __('keys.report_tech_summary') . '

'; $html .= ''; @@ -46,7 +46,7 @@ function buildReportHtml(PDO $pdo, string $reportType): string { SELECT DATE(attempted_date) as date, COUNT(*) as count, SUM(CASE WHEN attempt_result = 'success' THEN 1 ELSE 0 END) as successes, SUM(CASE WHEN attempt_result != 'success' THEN 1 ELSE 0 END) as failures - FROM activation_attempts + FROM `" . t('activation_attempts') . "` WHERE attempted_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY DATE(attempted_date) ORDER BY date DESC @@ -66,8 +66,8 @@ function buildReportHtml(PDO $pdo, string $reportType): string { $stmt = $pdo->query(" SELECT aa.attempted_date, aa.technician_id, aa.order_number, k.product_key, aa.notes - FROM activation_attempts aa - LEFT JOIN oem_keys k ON aa.key_id = k.id + FROM `" . t('activation_attempts') . "` aa + LEFT JOIN `" . t('oem_keys') . "` k ON aa.key_id = k.id WHERE aa.attempt_result != 'success' ORDER BY aa.attempted_date DESC LIMIT 100 @@ -94,7 +94,7 @@ function buildReportHtml(PDO $pdo, string $reportType): string { COUNT(*) as total, SUM(CASE WHEN attempt_result = 'success' THEN 1 ELSE 0 END) as successes, COUNT(DISTINCT technician_id) as unique_techs - FROM activation_attempts + FROM `" . t('activation_attempts') . "` GROUP BY DATE_FORMAT(attempted_date, '%Y-%m') ORDER BY month DESC LIMIT 12 @@ -120,7 +120,7 @@ function handle_get_stats(PDO $pdo, array $admin_session): void { $stats = []; // Key statistics - $stmt = $pdo->query("SELECT key_status, COUNT(*) as count FROM oem_keys GROUP BY key_status"); + $stmt = $pdo->query("SELECT key_status, COUNT(*) as count FROM `" . t('oem_keys') . "` GROUP BY key_status"); $key_stats = $stmt->fetchAll(PDO::FETCH_KEY_PAIR); $stats['keys'] = [ 'unused' => $key_stats['unused'] ?? 0, @@ -132,7 +132,7 @@ function handle_get_stats(PDO $pdo, array $admin_session): void { ]; // Technician statistics - $stmt = $pdo->query("SELECT is_active, COUNT(*) as count FROM technicians GROUP BY is_active"); + $stmt = $pdo->query("SELECT is_active, COUNT(*) as count FROM `" . t('technicians') . "` GROUP BY is_active"); $tech_stats = $stmt->fetchAll(PDO::FETCH_KEY_PAIR); $stats['technicians'] = [ 'active' => $tech_stats[1] ?? 0, @@ -141,13 +141,13 @@ function handle_get_stats(PDO $pdo, array $admin_session): void { ]; // Activation statistics - $stmt = $pdo->query("SELECT COUNT(*) FROM activation_attempts WHERE DATE(attempted_date) = CURDATE()"); + $stmt = $pdo->query("SELECT COUNT(*) FROM `" . t('activation_attempts') . "` WHERE DATE(attempted_date) = CURDATE()"); $stats['activations']['today'] = $stmt->fetchColumn(); - $stmt = $pdo->query("SELECT COUNT(*) FROM activation_attempts WHERE YEARWEEK(attempted_date, 1) = YEARWEEK(CURDATE(), 1)"); + $stmt = $pdo->query("SELECT COUNT(*) FROM `" . t('activation_attempts') . "` WHERE YEARWEEK(attempted_date, 1) = YEARWEEK(CURDATE(), 1)"); $stats['activations']['week'] = $stmt->fetchColumn(); - $stmt = $pdo->query("SELECT COUNT(*) FROM activation_attempts WHERE YEAR(attempted_date) = YEAR(CURDATE()) AND MONTH(attempted_date) = MONTH(CURDATE())"); + $stmt = $pdo->query("SELECT COUNT(*) FROM `" . t('activation_attempts') . "` WHERE YEAR(attempted_date) = YEAR(CURDATE()) AND MONTH(attempted_date) = MONTH(CURDATE())"); $stats['activations']['month'] = $stmt->fetchColumn(); // Daily activation trend — fetch all history, let frontend slice by range @@ -156,7 +156,7 @@ function handle_get_stats(PDO $pdo, array $admin_session): void { COUNT(*) as total, SUM(CASE WHEN attempt_result = 'success' THEN 1 ELSE 0 END) as successes, SUM(CASE WHEN attempt_result != 'success' THEN 1 ELSE 0 END) as failures - FROM activation_attempts + FROM `" . t('activation_attempts') . "` GROUP BY DATE(attempted_date) ORDER BY date ASC "); @@ -188,8 +188,8 @@ function handle_get_stats(PDO $pdo, array $admin_session): void { // Recent activity $stmt = $pdo->prepare(" SELECT aal.created_at, au.username, aal.action, aal.description - FROM admin_activity_log aal - LEFT JOIN admin_users au ON aal.admin_id = au.id + FROM `" . t('admin_activity_log') . "` aal + LEFT JOIN `" . t('admin_users') . "` au ON aal.admin_id = au.id ORDER BY aal.created_at DESC LIMIT 10 "); diff --git a/FINAL_PRODUCTION_SYSTEM/controllers/admin/HistoryController.php b/FINAL_PRODUCTION_SYSTEM/controllers/admin/HistoryController.php index 50dc1e4..705e017 100644 --- a/FINAL_PRODUCTION_SYSTEM/controllers/admin/HistoryController.php +++ b/FINAL_PRODUCTION_SYSTEM/controllers/admin/HistoryController.php @@ -71,10 +71,10 @@ function handle_get_hardware(PDO $pdo, array $admin_session): void { $stmt = $pdo->prepare(" SELECT h.*, aa.order_number, aa.attempted_at, aa.technician_id, t.full_name as technician_name, k.product_key - FROM hardware_info h - INNER JOIN activation_attempts aa ON h.activation_id = aa.id - LEFT JOIN technicians t ON aa.technician_id = t.technician_id - LEFT JOIN oem_keys k ON aa.key_id = k.id + FROM `" . t('hardware_info') . "` h + INNER JOIN `" . t('activation_attempts') . "` aa ON h.activation_id = aa.id + LEFT JOIN `" . t('technicians') . "` t ON aa.technician_id = t.technician_id + LEFT JOIN `" . t('oem_keys') . "` k ON aa.key_id = k.id WHERE h.activation_id = ? "); $stmt->execute([$activationId]); @@ -101,10 +101,10 @@ function handle_get_hardware_by_order(PDO $pdo, array $admin_session): void { aa.attempt_result as activation_result, aa.attempted_at as activation_time, k.product_key - FROM hardware_info h - LEFT JOIN technicians t ON h.technician_id = t.technician_id - LEFT JOIN activation_attempts aa ON h.activation_id = aa.id - LEFT JOIN oem_keys k ON aa.key_id = k.id + FROM `" . t('hardware_info') . "` h + LEFT JOIN `" . t('technicians') . "` t ON h.technician_id = t.technician_id + LEFT JOIN `" . t('activation_attempts') . "` aa ON h.activation_id = aa.id + LEFT JOIN `" . t('oem_keys') . "` k ON aa.key_id = k.id WHERE h.order_number = ? ORDER BY h.collection_timestamp DESC LIMIT 1 diff --git a/FINAL_PRODUCTION_SYSTEM/controllers/admin/IntegrationController.php b/FINAL_PRODUCTION_SYSTEM/controllers/admin/IntegrationController.php index a093ec4..9d64e59 100644 --- a/FINAL_PRODUCTION_SYSTEM/controllers/admin/IntegrationController.php +++ b/FINAL_PRODUCTION_SYSTEM/controllers/admin/IntegrationController.php @@ -9,7 +9,7 @@ function handle_list_integrations(PDO $pdo, array $admin_session): void { requirePermission('system_settings', $admin_session); - $stmt = $pdo->query("SELECT * FROM integrations ORDER BY id ASC"); + $stmt = $pdo->query("SELECT * FROM `" . t('integrations') . "` ORDER BY id ASC"); $rows = $stmt->fetchAll(); // Decode config JSON and mask sensitive fields @@ -29,7 +29,7 @@ function handle_list_integrations(PDO $pdo, array $admin_session): void { COUNT(*) as total, SUM(status = 'failed') as failed, SUM(status = 'pending') as pending - FROM integration_events WHERE integration_id = ? + FROM `" . t('integration_events') . "` WHERE integration_id = ? "); $countStmt->execute([$row['id']]); $row['event_counts'] = $countStmt->fetch(); @@ -48,7 +48,7 @@ function handle_get_integration(PDO $pdo, array $admin_session): void { return; } - $stmt = $pdo->prepare("SELECT * FROM integrations WHERE integration_key = ?"); + $stmt = $pdo->prepare("SELECT * FROM `" . t('integrations') . "` WHERE integration_key = ?"); $stmt->execute([$key]); $intg = $stmt->fetch(); if (!$intg) { @@ -61,7 +61,7 @@ function handle_get_integration(PDO $pdo, array $admin_session): void { // Recent events (last 20) $evtStmt = $pdo->prepare(" SELECT id, event_type, status, response_code, error_message, created_at, processed_at - FROM integration_events + FROM `" . t('integration_events') . "` WHERE integration_id = ? ORDER BY created_at DESC LIMIT 20 "); @@ -85,7 +85,7 @@ function handle_save_integration(PDO $pdo, array $admin_session, ?array $json_in return; } - $stmt = $pdo->prepare("SELECT * FROM integrations WHERE integration_key = ?"); + $stmt = $pdo->prepare("SELECT * FROM `" . t('integrations') . "` WHERE integration_key = ?"); $stmt->execute([$key]); $intg = $stmt->fetch(); if (!$intg) { @@ -111,7 +111,7 @@ function handle_save_integration(PDO $pdo, array $admin_session, ?array $json_in } $updateStmt = $pdo->prepare(" - UPDATE integrations + UPDATE `" . t('integrations') . "` SET enabled = ?, config = ?, updated_at = NOW() WHERE integration_key = ? "); diff --git a/FINAL_PRODUCTION_SYSTEM/controllers/admin/KeysController.php b/FINAL_PRODUCTION_SYSTEM/controllers/admin/KeysController.php index b2f6cbe..86115eb 100644 --- a/FINAL_PRODUCTION_SYSTEM/controllers/admin/KeysController.php +++ b/FINAL_PRODUCTION_SYSTEM/controllers/admin/KeysController.php @@ -105,7 +105,7 @@ function handle_recycle_key(PDO $pdo, array $admin_session): void { $id = intval($_POST['id'] ?? 0); $stmt = $pdo->prepare(" - UPDATE oem_keys + UPDATE `" . t('oem_keys') . "` SET key_status = 'unused', last_use_date = NULL, last_use_time = NULL WHERE id = ? "); @@ -126,7 +126,7 @@ function handle_delete_key(PDO $pdo, array $admin_session): void { $id = intval($_POST['id'] ?? 0); - $stmt = $pdo->prepare("DELETE FROM oem_keys WHERE id = ?"); + $stmt = $pdo->prepare("DELETE FROM `" . t('oem_keys') . "` WHERE id = ?"); $stmt->execute([$id]); logAdminActivity( @@ -265,9 +265,9 @@ function handle_add_keys(PDO $pdo, array $admin_session, ?array $json_input = nu $pdo->beginTransaction(); try { - $checkStmt = $pdo->prepare("SELECT id FROM oem_keys WHERE product_key = ?"); + $checkStmt = $pdo->prepare("SELECT id FROM `" . t('oem_keys') . "` WHERE product_key = ?"); $insertStmt = $pdo->prepare(" - INSERT INTO oem_keys (product_key, oem_identifier, roll_serial, key_status, created_at) + INSERT INTO `" . t('oem_keys') . "` (product_key, oem_identifier, roll_serial, key_status, created_at) VALUES (?, ?, ?, 'unused', NOW()) "); diff --git a/FINAL_PRODUCTION_SYSTEM/controllers/admin/LicenseController.php b/FINAL_PRODUCTION_SYSTEM/controllers/admin/LicenseController.php index 165c59b..4020115 100644 --- a/FINAL_PRODUCTION_SYSTEM/controllers/admin/LicenseController.php +++ b/FINAL_PRODUCTION_SYSTEM/controllers/admin/LicenseController.php @@ -17,12 +17,12 @@ function handle_license_status(PDO $pdo, array $admin_session, $json_input): voi $techCount = 0; $keyCount = 0; try { - $stmt = $pdo->query("SELECT COUNT(*) FROM technicians WHERE status = 'active'"); + $stmt = $pdo->query("SELECT COUNT(*) FROM `" . t('technicians') . "` WHERE status = 'active'"); $techCount = (int)$stmt->fetchColumn(); } catch (Exception $e) { /* table may not exist */ } try { - $stmt = $pdo->query("SELECT COUNT(*) FROM oem_keys"); + $stmt = $pdo->query("SELECT COUNT(*) FROM `" . t('oem_keys') . "`"); $keyCount = (int)$stmt->fetchColumn(); } catch (Exception $e) { /* table may not exist */ } @@ -76,7 +76,7 @@ function handle_license_register(PDO $pdo, array $admin_session, $json_input): v function handle_license_deactivate(PDO $pdo, array $admin_session, $json_input): void { requirePermission('system_settings', $admin_session); - $pdo->exec("UPDATE license_info SET is_active = 0"); + $pdo->exec("UPDATE `" . t('license_info') . "` SET is_active = 0"); saveConfigBatch($pdo, ['license_tier' => 'community']); logAdminActivity( diff --git a/FINAL_PRODUCTION_SYSTEM/controllers/admin/NotificationsController.php b/FINAL_PRODUCTION_SYSTEM/controllers/admin/NotificationsController.php index 81233f1..9e65f2c 100644 --- a/FINAL_PRODUCTION_SYSTEM/controllers/admin/NotificationsController.php +++ b/FINAL_PRODUCTION_SYSTEM/controllers/admin/NotificationsController.php @@ -39,7 +39,7 @@ function handle_push_subscribe(PDO $pdo, array $admin_session, ?array $json_inpu // Upsert: insert or re-activate existing subscription $stmt = $pdo->prepare(" - INSERT INTO push_subscriptions (admin_id, endpoint, p256dh_key, auth_key, user_agent, is_active) + INSERT INTO `" . t('push_subscriptions') . "` (admin_id, endpoint, p256dh_key, auth_key, user_agent, is_active) VALUES (?, ?, ?, ?, ?, 1) ON DUPLICATE KEY UPDATE p256dh_key = VALUES(p256dh_key), auth_key = VALUES(auth_key), user_agent = VALUES(user_agent), is_active = 1, last_used_at = NOW() @@ -58,10 +58,10 @@ function handle_push_unsubscribe(PDO $pdo, array $admin_session, ?array $json_in if (empty($endpoint)) { // Deactivate all subscriptions for this admin - $stmt = $pdo->prepare("UPDATE push_subscriptions SET is_active = 0 WHERE admin_id = ?"); + $stmt = $pdo->prepare("UPDATE `" . t('push_subscriptions') . "` SET is_active = 0 WHERE admin_id = ?"); $stmt->execute([$adminId]); } else { - $stmt = $pdo->prepare("UPDATE push_subscriptions SET is_active = 0 WHERE admin_id = ? AND endpoint = ?"); + $stmt = $pdo->prepare("UPDATE `" . t('push_subscriptions') . "` SET is_active = 0 WHERE admin_id = ? AND endpoint = ?"); $stmt->execute([$adminId, $endpoint]); } @@ -75,7 +75,7 @@ function handle_get_push_preferences(PDO $pdo, array $admin_session): void { $adminId = (int)$admin_session['admin_id']; // Get saved preferences - $stmt = $pdo->prepare("SELECT category, enabled FROM push_preferences WHERE admin_id = ?"); + $stmt = $pdo->prepare("SELECT category, enabled FROM `" . t('push_preferences') . "` WHERE admin_id = ?"); $stmt->execute([$adminId]); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); @@ -90,7 +90,7 @@ function handle_get_push_preferences(PDO $pdo, array $admin_session): void { } // Check if admin has any active subscriptions - $subStmt = $pdo->prepare("SELECT COUNT(*) FROM push_subscriptions WHERE admin_id = ? AND is_active = 1"); + $subStmt = $pdo->prepare("SELECT COUNT(*) FROM `" . t('push_subscriptions') . "` WHERE admin_id = ? AND is_active = 1"); $subStmt->execute([$adminId]); $hasSubscription = (int)$subStmt->fetchColumn() > 0; @@ -117,7 +117,7 @@ function handle_save_push_preferences(PDO $pdo, array $admin_session, ?array $js $validCategories = ['security', 'keys', 'technicians', 'system', 'devices', 'activation']; $stmt = $pdo->prepare(" - INSERT INTO push_preferences (admin_id, category, enabled) + INSERT INTO `" . t('push_preferences') . "` (admin_id, category, enabled) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE enabled = VALUES(enabled) "); @@ -141,7 +141,7 @@ function handle_get_notifications(PDO $pdo, array $admin_session): void { // Get 50 most recent notifications $stmt = $pdo->prepare(" SELECT id, category, title_key, body, action_url, is_read, created_at - FROM notifications + FROM `" . t('notifications') . "` WHERE admin_id = ? ORDER BY created_at DESC LIMIT 50 @@ -150,7 +150,7 @@ function handle_get_notifications(PDO $pdo, array $admin_session): void { $notifications = $stmt->fetchAll(PDO::FETCH_ASSOC); // Get unread count - $countStmt = $pdo->prepare("SELECT COUNT(*) FROM notifications WHERE admin_id = ? AND is_read = 0"); + $countStmt = $pdo->prepare("SELECT COUNT(*) FROM `" . t('notifications') . "` WHERE admin_id = ? AND is_read = 0"); $countStmt->execute([$adminId]); $unreadCount = (int)$countStmt->fetchColumn(); @@ -170,7 +170,7 @@ function handle_send_test_notification(PDO $pdo, array $admin_session, ?array $j // Insert a bell notification for this admin $stmt = $pdo->prepare(" - INSERT INTO notifications (admin_id, category, title_key, body, action_url) + INSERT INTO `" . t('notifications') . "` (admin_id, category, title_key, body, action_url) VALUES (?, 'system', 'notif.title.system', ?, 'admin_v2.php#notifications') "); $testBody = $type === 'sound' @@ -182,8 +182,8 @@ function handle_send_test_notification(PDO $pdo, array $admin_session, ?array $j if ($type === 'push') { $subStmt = $pdo->prepare(" SELECT ps.endpoint, ps.p256dh_key, ps.auth_key, au.preferred_language - FROM push_subscriptions ps - JOIN admin_users au ON ps.admin_id = au.id + FROM `" . t('push_subscriptions') . "` ps + JOIN `" . t('admin_users') . "` au ON ps.admin_id = au.id WHERE ps.admin_id = ? AND ps.is_active = 1 "); $subStmt->execute([$adminId]); @@ -220,7 +220,7 @@ function handle_send_test_notification(PDO $pdo, array $admin_session, ?array $j foreach ($webPush->flush() as $report) { if ($report->isSubscriptionExpired()) { $endpoint = $report->getRequest()->getUri()->__toString(); - $pdo->prepare("UPDATE push_subscriptions SET is_active = 0 WHERE endpoint = ?") + $pdo->prepare("UPDATE `" . t('push_subscriptions') . "` SET is_active = 0 WHERE endpoint = ?") ->execute([$endpoint]); } } @@ -240,7 +240,7 @@ function handle_mark_notifications_read(PDO $pdo, array $admin_session, ?array $ if ($ids === null || (is_array($ids) && empty($ids))) { // Mark all as read - $stmt = $pdo->prepare("UPDATE notifications SET is_read = 1 WHERE admin_id = ? AND is_read = 0"); + $stmt = $pdo->prepare("UPDATE `" . t('notifications') . "` SET is_read = 1 WHERE admin_id = ? AND is_read = 0"); $stmt->execute([$adminId]); } elseif (is_array($ids)) { $intIds = array_map('intval', $ids); diff --git a/FINAL_PRODUCTION_SYSTEM/controllers/admin/ProductVariantsController.php b/FINAL_PRODUCTION_SYSTEM/controllers/admin/ProductVariantsController.php index 100843f..fa406e6 100644 --- a/FINAL_PRODUCTION_SYSTEM/controllers/admin/ProductVariantsController.php +++ b/FINAL_PRODUCTION_SYSTEM/controllers/admin/ProductVariantsController.php @@ -14,8 +14,8 @@ function handle_get_product_lines(PDO $pdo, array $admin_session, ?array $json_i $stmt = $pdo->query(" SELECT pl.*, COUNT(DISTINCT pv.id) AS variant_count - FROM product_lines pl - LEFT JOIN product_variants pv ON pv.line_id = pl.id AND pv.is_active = 1 + FROM `" . t('product_lines') . "` pl + LEFT JOIN `" . t('product_variants') . "` pv ON pv.line_id = pl.id AND pv.is_active = 1 GROUP BY pl.id ORDER BY pl.name "); @@ -32,7 +32,7 @@ function handle_get_product_line(PDO $pdo, array $admin_session, ?array $json_in jsonResponse(['success' => false, 'error' => 'Invalid line ID'], 400); } - $stmt = $pdo->prepare("SELECT * FROM product_lines WHERE id = ?"); + $stmt = $pdo->prepare("SELECT * FROM `" . t('product_lines') . "` WHERE id = ?"); $stmt->execute([$id]); $line = $stmt->fetch(PDO::FETCH_ASSOC); if (!$line) { @@ -42,7 +42,7 @@ function handle_get_product_line(PDO $pdo, array $admin_session, ?array $json_in // Fetch variants $stmt = $pdo->prepare(" SELECT pv.* - FROM product_variants pv + FROM `" . t('product_variants') . "` pv WHERE pv.line_id = ? AND pv.is_active = 1 ORDER BY pv.disk_size_min_mb "); @@ -104,7 +104,7 @@ function handle_save_product_line(PDO $pdo, array $admin_session, ?array $json_i if ($id > 0) { // Update $stmt = $pdo->prepare(" - UPDATE product_lines + UPDATE `" . t('product_lines') . "` SET name = ?, order_pattern = ?, description = ?, enforcement_level = ?, is_active = ?, secure_boot_enforcement = ?, bios_enforcement = ?, hackbgrt_enforcement = ?, partition_enforcement = ?, missing_drivers_enforcement = ? @@ -116,7 +116,7 @@ function handle_save_product_line(PDO $pdo, array $admin_session, ?array $json_i } else { // Insert $stmt = $pdo->prepare(" - INSERT INTO product_lines (name, order_pattern, description, enforcement_level, is_active, + INSERT INTO `" . t('product_lines') . "` (name, order_pattern, description, enforcement_level, is_active, secure_boot_enforcement, bios_enforcement, hackbgrt_enforcement, partition_enforcement, missing_drivers_enforcement) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) @@ -146,7 +146,7 @@ function handle_delete_product_line(PDO $pdo, array $admin_session, ?array $json jsonResponse(['success' => false, 'error' => 'Invalid line ID'], 400); } - $stmt = $pdo->prepare("UPDATE product_lines SET is_active = 0 WHERE id = ?"); + $stmt = $pdo->prepare("UPDATE `" . t('product_lines') . "` SET is_active = 0 WHERE id = ?"); $stmt->execute([$id]); logAdminActivity($admin_session['admin_id'], $admin_session['id'], 'PRODUCT_LINE_DELETE', "Deactivated product line ID: $id"); @@ -177,14 +177,14 @@ function handle_save_product_variant(PDO $pdo, array $admin_session, ?array $jso try { if ($id > 0) { $stmt = $pdo->prepare(" - UPDATE product_variants + UPDATE `" . t('product_variants') . "` SET line_id = ?, name = ?, disk_size_min_mb = ?, disk_size_max_mb = ?, is_active = ? WHERE id = ? "); $stmt->execute([$lineId, $name, $diskSizeMin, $diskSizeMax, $isActive, $id]); } else { $stmt = $pdo->prepare(" - INSERT INTO product_variants (line_id, name, disk_size_min_mb, disk_size_max_mb, is_active) + INSERT INTO `" . t('product_variants') . "` (line_id, name, disk_size_min_mb, disk_size_max_mb, is_active) VALUES (?, ?, ?, ?, ?) "); $stmt->execute([$lineId, $name, $diskSizeMin, $diskSizeMax, $isActive]); @@ -192,12 +192,12 @@ function handle_save_product_variant(PDO $pdo, array $admin_session, ?array $jso } // Replace partitions: delete existing, insert new - $stmt = $pdo->prepare("DELETE FROM product_variant_partitions WHERE variant_id = ?"); + $stmt = $pdo->prepare("DELETE FROM `" . t('product_variant_partitions') . "` WHERE variant_id = ?"); $stmt->execute([$id]); if (!empty($partitions)) { $stmt = $pdo->prepare(" - INSERT INTO product_variant_partitions + INSERT INTO `" . t('product_variant_partitions') . "` (variant_id, partition_order, partition_name, partition_type, expected_size_mb, tolerance_percent, is_flexible) VALUES (?, ?, ?, ?, ?, ?, ?) "); @@ -257,7 +257,7 @@ function handle_delete_product_variant(PDO $pdo, array $admin_session, ?array $j jsonResponse(['success' => false, 'error' => 'Invalid variant ID'], 400); } - $stmt = $pdo->prepare("UPDATE product_variants SET is_active = 0 WHERE id = ?"); + $stmt = $pdo->prepare("UPDATE `" . t('product_variants') . "` SET is_active = 0 WHERE id = ?"); $stmt->execute([$id]); logAdminActivity($admin_session['admin_id'], $admin_session['id'], 'PRODUCT_VARIANT_DELETE', "Deactivated variant ID: $id"); diff --git a/FINAL_PRODUCTION_SYSTEM/controllers/admin/ProductionController.php b/FINAL_PRODUCTION_SYSTEM/controllers/admin/ProductionController.php index ab4452b..ebf473c 100644 --- a/FINAL_PRODUCTION_SYSTEM/controllers/admin/ProductionController.php +++ b/FINAL_PRODUCTION_SYSTEM/controllers/admin/ProductionController.php @@ -74,10 +74,10 @@ function handle_get_build_report(PDO $pdo, array $admin_session, $json_input): v $uuid = $json_input['uuid'] ?? $_GET['uuid'] ?? ''; if ($id > 0) { - $stmt = $pdo->prepare("SELECT * FROM computer_build_reports WHERE id = ?"); + $stmt = $pdo->prepare("SELECT * FROM `" . t('computer_build_reports') . "` WHERE id = ?"); $stmt->execute([$id]); } elseif ($uuid) { - $stmt = $pdo->prepare("SELECT * FROM computer_build_reports WHERE report_uuid = ?"); + $stmt = $pdo->prepare("SELECT * FROM `" . t('computer_build_reports') . "` WHERE report_uuid = ?"); $stmt->execute([$uuid]); } else { jsonResponse(['success' => false, 'error' => 'Report ID or UUID required']); @@ -99,7 +99,7 @@ function handle_export_build_report(PDO $pdo, array $admin_session, $json_input) $id = (int)($json_input['id'] ?? $_GET['id'] ?? 0); $format = $json_input['format'] ?? $_GET['format'] ?? 'json'; - $stmt = $pdo->prepare("SELECT * FROM computer_build_reports WHERE id = ?"); + $stmt = $pdo->prepare("SELECT * FROM `" . t('computer_build_reports') . "` WHERE id = ?"); $stmt->execute([$id]); $report = $stmt->fetch(PDO::FETCH_ASSOC); @@ -204,7 +204,7 @@ function handle_update_build_report_shipping(PDO $pdo, array $admin_session, $js } $params[] = $id; - $stmt = $pdo->prepare("UPDATE computer_build_reports SET " . implode(', ', $sets) . " WHERE id = ?"); + $stmt = $pdo->prepare("UPDATE `" . t('computer_build_reports') . "` SET " . implode(', ', $sets) . " WHERE id = ?"); $stmt->execute($params); logAdminActivity($admin_session['admin_id'], $admin_session['id'] ?? 0, 'CBR_SHIPPING_UPDATED', "Updated CBR #{$id} shipping: {$shippingStatus}"); @@ -228,13 +228,13 @@ function handle_get_key_pool_status(PDO $pdo, array $admin_session, $json_input) SUM(CASE WHEN key_status = 'allocated' THEN 1 ELSE 0 END) AS allocated_keys, SUM(CASE WHEN key_status = 'good' THEN 1 ELSE 0 END) AS used_keys, SUM(CASE WHEN key_status = 'bad' THEN 1 ELSE 0 END) AS bad_keys - FROM oem_keys + FROM `" . t('oem_keys') . "` GROUP BY oem_identifier "); $pools = $stmt->fetchAll(PDO::FETCH_ASSOC); // Get pool config - $configStmt = $pdo->query("SELECT * FROM key_pool_config ORDER BY product_edition"); + $configStmt = $pdo->query("SELECT * FROM `" . t('key_pool_config') . "` ORDER BY product_edition"); $configs = []; foreach ($configStmt->fetchAll(PDO::FETCH_ASSOC) as $c) { $configs[$c['product_edition']] = $c; @@ -287,7 +287,7 @@ function handle_save_key_pool_config(PDO $pdo, array $admin_session, $json_input } $stmt = $pdo->prepare(" - INSERT INTO key_pool_config (product_edition, low_threshold, critical_threshold, auto_notify, notify_email) + INSERT INTO `" . t('key_pool_config') . "` (product_edition, low_threshold, critical_threshold, auto_notify, notify_email) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE low_threshold = VALUES(low_threshold), @@ -326,15 +326,15 @@ function handle_check_hardware_binding(PDO $pdo, array $admin_session, $json_inp // Return recent bindings $stmt = $pdo->query(" SELECT hkb.*, ok.product_key, ok.oem_identifier AS product_type - FROM hardware_key_bindings hkb - LEFT JOIN oem_keys ok ON ok.id = hkb.product_key_id + FROM `" . t('hardware_key_bindings') . "` hkb + LEFT JOIN `" . t('oem_keys') . "` ok ON ok.id = hkb.product_key_id ORDER BY hkb.bound_at DESC LIMIT 50 "); } else { $stmt = $pdo->prepare(" SELECT hkb.*, ok.product_key, ok.oem_identifier AS product_type - FROM hardware_key_bindings hkb - LEFT JOIN oem_keys ok ON ok.id = hkb.product_key_id + FROM `" . t('hardware_key_bindings') . "` hkb + LEFT JOIN `" . t('oem_keys') . "` ok ON ok.id = hkb.product_key_id WHERE " . implode(' AND ', $where) . " ORDER BY hkb.bound_at DESC LIMIT 50 "); @@ -348,7 +348,7 @@ function handle_check_hardware_binding(PDO $pdo, array $admin_session, $json_inp if ($keyId > 0) { $conflictStmt = $pdo->prepare(" SELECT device_fingerprint, motherboard_serial, bound_at - FROM hardware_key_bindings + FROM `" . t('hardware_key_bindings') . "` WHERE product_key_id = ? AND status = 'active' "); $conflictStmt->execute([$keyId]); @@ -371,7 +371,7 @@ function handle_release_hardware_binding(PDO $pdo, array $admin_session, $json_i } $stmt = $pdo->prepare(" - UPDATE hardware_key_bindings + UPDATE `" . t('hardware_key_bindings') . "` SET status = 'released', released_at = NOW(), released_by_admin_id = ? WHERE id = ? AND status = 'active' "); @@ -411,7 +411,7 @@ function handle_import_dpk_batch(PDO $pdo, array $admin_session): void { // Create batch record $batchStmt = $pdo->prepare(" - INSERT INTO dpk_import_batches + INSERT INTO `" . t('dpk_import_batches') . "` (batch_name, import_source, product_edition, source_filename, source_checksum, imported_by_admin_id, imported_by_username, import_status) VALUES (?, ?, ?, ?, ?, ?, ?, 'processing') @@ -446,9 +446,9 @@ function handle_import_dpk_batch(PDO $pdo, array $admin_session): void { $duplicates = 0; $failed = 0; - $checkExisting = $pdo->prepare("SELECT COUNT(*) FROM oem_keys WHERE product_key = ?"); + $checkExisting = $pdo->prepare("SELECT COUNT(*) FROM `" . t('oem_keys') . "` WHERE product_key = ?"); $insertKey = $pdo->prepare(" - INSERT INTO oem_keys (product_key, oem_identifier, key_status) + INSERT INTO `" . t('oem_keys') . "` (product_key, oem_identifier, key_status) VALUES (?, ?, 'unused') "); @@ -469,7 +469,7 @@ function handle_import_dpk_batch(PDO $pdo, array $admin_session): void { // Update batch record $pdo->prepare(" - UPDATE dpk_import_batches + UPDATE `" . t('dpk_import_batches') . "` SET total_keys = ?, imported_keys = ?, duplicate_keys = ?, failed_keys = ?, import_status = 'completed', completed_at = NOW() WHERE id = ? @@ -483,7 +483,7 @@ function handle_import_dpk_batch(PDO $pdo, array $admin_session): void { // Update key pool replenishment timestamp if ($productEdition && $imported > 0) { $pdo->prepare(" - UPDATE key_pool_config SET last_replenished_at = NOW() WHERE product_edition = ? + UPDATE `" . t('key_pool_config') . "` SET last_replenished_at = NOW() WHERE product_edition = ? ")->execute([$productEdition]); } @@ -526,7 +526,7 @@ function parseDPKXml(string $content): array { function handle_list_dpk_batches(PDO $pdo, array $admin_session, $json_input): void { requirePermission('view_keys', $admin_session); - $stmt = $pdo->query("SELECT * FROM dpk_import_batches ORDER BY created_at DESC LIMIT 50"); + $stmt = $pdo->query("SELECT * FROM `" . t('dpk_import_batches') . "` ORDER BY created_at DESC LIMIT 50"); jsonResponse(['success' => true, 'batches' => $stmt->fetchAll(PDO::FETCH_ASSOC)]); } @@ -621,13 +621,13 @@ function handle_save_work_order(PDO $pdo, array $admin_session, $json_input): vo } $params[] = $id; - $stmt = $pdo->prepare("UPDATE work_orders SET " . implode(', ', $sets) . " WHERE id = ?"); + $stmt = $pdo->prepare("UPDATE `" . t('work_orders') . "` SET " . implode(', ', $sets) . " WHERE id = ?"); $stmt->execute($params); } else { $data['created_by_admin_id'] = (int)$admin_session['admin_id']; $cols = array_keys($data); $placeholders = array_fill(0, count($cols), '?'); - $stmt = $pdo->prepare("INSERT INTO work_orders (" . implode(',', $cols) . ") VALUES (" . implode(',', $placeholders) . ")"); + $stmt = $pdo->prepare("INSERT INTO `" . t('work_orders') . "` (" . implode(',', $cols) . ") VALUES (" . implode(',', $placeholders) . ")"); $stmt->execute(array_values($data)); $id = (int)$pdo->lastInsertId(); } @@ -646,7 +646,7 @@ function handle_get_work_order(PDO $pdo, array $admin_session, $json_input): voi return; } - $stmt = $pdo->prepare("SELECT * FROM work_orders WHERE id = ?"); + $stmt = $pdo->prepare("SELECT * FROM `" . t('work_orders') . "` WHERE id = ?"); $stmt->execute([$id]); $order = $stmt->fetch(PDO::FETCH_ASSOC); @@ -659,7 +659,7 @@ function handle_get_work_order(PDO $pdo, array $admin_session, $json_input): voi $cbrStmt = $pdo->prepare(" SELECT id, report_uuid, order_number, activation_status, shipping_status, motherboard_model, cpu_model, created_at - FROM computer_build_reports + FROM `" . t('computer_build_reports') . "` WHERE work_order_id = ? ORDER BY created_at ASC "); @@ -679,7 +679,7 @@ function handle_delete_work_order(PDO $pdo, array $admin_session, $json_input): } // Only allow deleting draft/cancelled orders - $check = $pdo->prepare("SELECT status, work_order_number FROM work_orders WHERE id = ?"); + $check = $pdo->prepare("SELECT status, work_order_number FROM `" . t('work_orders') . "` WHERE id = ?"); $check->execute([$id]); $row = $check->fetch(); @@ -692,7 +692,7 @@ function handle_delete_work_order(PDO $pdo, array $admin_session, $json_input): return; } - $pdo->prepare("DELETE FROM work_orders WHERE id = ?")->execute([$id]); + $pdo->prepare("DELETE FROM `" . t('work_orders') . "` WHERE id = ?")->execute([$id]); logAdminActivity($admin_session['admin_id'], $admin_session['id'] ?? 0, 'WORK_ORDER_DELETED', "Deleted work order: {$row['work_order_number']}"); diff --git a/FINAL_PRODUCTION_SYSTEM/controllers/admin/SecurityController.php b/FINAL_PRODUCTION_SYSTEM/controllers/admin/SecurityController.php index dd3913d..718eaac 100644 --- a/FINAL_PRODUCTION_SYSTEM/controllers/admin/SecurityController.php +++ b/FINAL_PRODUCTION_SYSTEM/controllers/admin/SecurityController.php @@ -9,7 +9,7 @@ function handle_get_2fa_status(PDO $pdo, array $admin_session): void { try { $stmt = $pdo->prepare(" SELECT totp_enabled, verified_at, backup_codes - FROM admin_totp_secrets + FROM `" . t('admin_totp_secrets') . "` WHERE admin_id = ? "); $stmt->execute([$admin_session['admin_id']]); @@ -48,8 +48,8 @@ function handle_list_trusted_networks(PDO $pdo, array $admin_session): void { $stmt = $pdo->query(" SELECT tn.*, au.username as created_by_username - FROM trusted_networks tn - LEFT JOIN admin_users au ON tn.created_by_admin_id = au.id + FROM `" . t('trusted_networks') . "` tn + LEFT JOIN `" . t('admin_users') . "` au ON tn.created_by_admin_id = au.id ORDER BY tn.created_at DESC "); $networks = $stmt->fetchAll(PDO::FETCH_ASSOC); @@ -78,7 +78,7 @@ function handle_add_trusted_network(PDO $pdo, array $admin_session, ?array $json } $stmt = $pdo->prepare(" - INSERT INTO trusted_networks ( + INSERT INTO `" . t('trusted_networks') . "` ( network_name, ip_range, bypass_2fa, allow_usb_auth, description, created_by_admin_id ) VALUES (?, ?, ?, ?, ?, ?) "); @@ -99,7 +99,7 @@ function handle_delete_trusted_network(PDO $pdo, array $admin_session, ?array $j $networkId = intval($json_input['network_id'] ?? 0); - $stmt = $pdo->prepare("SELECT network_name FROM trusted_networks WHERE id = ?"); + $stmt = $pdo->prepare("SELECT network_name FROM `" . t('trusted_networks') . "` WHERE id = ?"); $stmt->execute([$networkId]); $network = $stmt->fetch(PDO::FETCH_ASSOC); @@ -108,7 +108,7 @@ function handle_delete_trusted_network(PDO $pdo, array $admin_session, ?array $j return; } - $stmt = $pdo->prepare("DELETE FROM trusted_networks WHERE id = ?"); + $stmt = $pdo->prepare("DELETE FROM `" . t('trusted_networks') . "` WHERE id = ?"); $stmt->execute([$networkId]); logAdminActivity( diff --git a/FINAL_PRODUCTION_SYSTEM/controllers/admin/TaskPipelineController.php b/FINAL_PRODUCTION_SYSTEM/controllers/admin/TaskPipelineController.php index 6914b76..bc00b25 100644 --- a/FINAL_PRODUCTION_SYSTEM/controllers/admin/TaskPipelineController.php +++ b/FINAL_PRODUCTION_SYSTEM/controllers/admin/TaskPipelineController.php @@ -15,7 +15,7 @@ function handle_list_task_templates(PDO $pdo, array $admin_session, $json_input) requirePermission('system_settings', $admin_session); $stmt = $pdo->query(" - SELECT * FROM task_templates + SELECT * FROM `" . t('task_templates') . "` ORDER BY is_system DESC, task_key ASC "); @@ -54,13 +54,13 @@ function handle_save_task_template(PDO $pdo, array $admin_session, $json_input): if ($id > 0) { // Check not editing a system task's key/type - $existing = $pdo->prepare("SELECT is_system FROM task_templates WHERE id = ?"); + $existing = $pdo->prepare("SELECT is_system FROM `" . t('task_templates') . "` WHERE id = ?"); $existing->execute([$id]); $row = $existing->fetch(); if ($row && $row['is_system']) { // System tasks: only allow editing name, description, timeout, on_failure, icon $stmt = $pdo->prepare(" - UPDATE task_templates + UPDATE `" . t('task_templates') . "` SET task_name = ?, description = ?, default_timeout_seconds = ?, default_on_failure = ?, icon = ? WHERE id = ? @@ -68,7 +68,7 @@ function handle_save_task_template(PDO $pdo, array $admin_session, $json_input): $stmt->execute([$taskName, $description, $defaultTimeout, $defaultOnFailure, $icon, $id]); } else { $stmt = $pdo->prepare(" - UPDATE task_templates + UPDATE `" . t('task_templates') . "` SET task_key = ?, task_name = ?, task_type = ?, description = ?, default_code = ?, default_timeout_seconds = ?, default_on_failure = ?, icon = ? WHERE id = ? AND is_system = 0 @@ -77,7 +77,7 @@ function handle_save_task_template(PDO $pdo, array $admin_session, $json_input): } } else { $stmt = $pdo->prepare(" - INSERT INTO task_templates + INSERT INTO `" . t('task_templates') . "` (task_key, task_name, task_type, description, default_code, default_timeout_seconds, default_on_failure, is_system, icon) VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?) @@ -101,7 +101,7 @@ function handle_delete_task_template(PDO $pdo, array $admin_session, $json_input } // Cannot delete system tasks - $check = $pdo->prepare("SELECT is_system, task_key FROM task_templates WHERE id = ?"); + $check = $pdo->prepare("SELECT is_system, task_key FROM `" . t('task_templates') . "` WHERE id = ?"); $check->execute([$id]); $row = $check->fetch(); if (!$row) { @@ -114,8 +114,8 @@ function handle_delete_task_template(PDO $pdo, array $admin_session, $json_input } // Remove from all product line assignments first - $pdo->prepare("DELETE FROM product_line_tasks WHERE task_template_id = ?")->execute([$id]); - $pdo->prepare("DELETE FROM task_templates WHERE id = ? AND is_system = 0")->execute([$id]); + $pdo->prepare("DELETE FROM `" . t('product_line_tasks') . "` WHERE task_template_id = ?")->execute([$id]); + $pdo->prepare("DELETE FROM `" . t('task_templates') . "` WHERE id = ? AND is_system = 0")->execute([$id]); logAdminActivity($admin_session['admin_id'], $admin_session['id'] ?? 0, 'TASK_TEMPLATE_DELETED', "Deleted task template: {$row['task_key']} (#{$id})"); @@ -137,8 +137,8 @@ function handle_get_product_line_tasks(PDO $pdo, array $admin_session, $json_inp SELECT plt.*, tt.task_key, tt.task_name AS template_name, tt.task_type, tt.description AS template_description, tt.default_code, tt.default_timeout_seconds, tt.default_on_failure, tt.is_system, tt.icon - FROM product_line_tasks plt - JOIN task_templates tt ON tt.id = plt.task_template_id + FROM `" . t('product_line_tasks') . "` plt + JOIN `" . t('task_templates') . "` tt ON tt.id = plt.task_template_id WHERE plt.product_line_id = ? ORDER BY plt.sort_order ASC "); @@ -165,11 +165,11 @@ function handle_save_product_line_tasks(PDO $pdo, array $admin_session, $json_in $pdo->beginTransaction(); try { // Remove existing assignments - $pdo->prepare("DELETE FROM product_line_tasks WHERE product_line_id = ?")->execute([$productLineId]); + $pdo->prepare("DELETE FROM `" . t('product_line_tasks') . "` WHERE product_line_id = ?")->execute([$productLineId]); // Insert new assignments in order $insertStmt = $pdo->prepare(" - INSERT INTO product_line_tasks + INSERT INTO `" . t('product_line_tasks') . "` (product_line_id, task_template_id, sort_order, enabled, custom_name, custom_code, custom_timeout_seconds, custom_on_failure) VALUES (?, ?, ?, ?, ?, ?, ?, ?) @@ -246,7 +246,7 @@ function handle_get_activation_pipeline(PDO $pdo, array $admin_session, $json_in SELECT id AS task_template_id, task_key, task_name, task_type, default_code AS code, default_timeout_seconds AS timeout_seconds, default_on_failure AS on_failure, 1 AS enabled - FROM task_templates + FROM `" . t('task_templates') . "` WHERE is_system = 1 ORDER BY id ASC "); @@ -263,8 +263,8 @@ function handle_get_activation_pipeline(PDO $pdo, array $admin_session, $json_in COALESCE(plt.custom_timeout_seconds, tt.default_timeout_seconds) AS timeout_seconds, COALESCE(plt.custom_on_failure, tt.default_on_failure) AS on_failure, plt.enabled - FROM product_line_tasks plt - JOIN task_templates tt ON tt.id = plt.task_template_id + FROM `" . t('product_line_tasks') . "` plt + JOIN `" . t('task_templates') . "` tt ON tt.id = plt.task_template_id WHERE plt.product_line_id = ? AND plt.enabled = 1 ORDER BY plt.sort_order ASC "); @@ -277,7 +277,7 @@ function handle_get_activation_pipeline(PDO $pdo, array $admin_session, $json_in SELECT id AS task_template_id, task_key, task_name, task_type, default_code AS code, default_timeout_seconds AS timeout_seconds, default_on_failure AS on_failure, 1 AS enabled - FROM task_templates + FROM `" . t('task_templates') . "` WHERE is_system = 1 ORDER BY id ASC "); @@ -297,7 +297,7 @@ function handle_log_task_execution(PDO $pdo, array $admin_session, $json_input): } $stmt = $pdo->prepare(" - INSERT INTO task_execution_log + INSERT INTO `" . t('task_execution_log') . "` (activation_attempt_id, product_line_id, task_template_id, task_key, task_name, status, started_at, completed_at, duration_ms, output, error_message, technician_id, order_number) diff --git a/FINAL_PRODUCTION_SYSTEM/controllers/admin/TechniciansController.php b/FINAL_PRODUCTION_SYSTEM/controllers/admin/TechniciansController.php index 9980625..8ab6659 100644 --- a/FINAL_PRODUCTION_SYSTEM/controllers/admin/TechniciansController.php +++ b/FINAL_PRODUCTION_SYSTEM/controllers/admin/TechniciansController.php @@ -54,7 +54,7 @@ function handle_list_technicians(PDO $pdo, array $admin_session): void { $stmt = $pdo->query(" SELECT id, technician_id, full_name, email, is_active - FROM technicians + FROM `" . t('technicians') . "` ORDER BY full_name ASC "); $technicians = $stmt->fetchAll(PDO::FETCH_ASSOC); @@ -92,7 +92,7 @@ function handle_add_tech(PDO $pdo, array $admin_session): void { $pdo->beginTransaction(); // Check + insert inside transaction to prevent TOCTOU race condition - $stmt = $pdo->prepare("SELECT COUNT(*) FROM technicians WHERE technician_id = ?"); + $stmt = $pdo->prepare("SELECT COUNT(*) FROM `" . t('technicians') . "` WHERE technician_id = ?"); $stmt->execute([$tech_id]); if ($stmt->fetchColumn() > 0) { $pdo->rollBack(); @@ -101,7 +101,7 @@ function handle_add_tech(PDO $pdo, array $admin_session): void { } $stmt = $pdo->prepare(" - INSERT INTO technicians (technician_id, password_hash, full_name, email, is_active, preferred_language) + INSERT INTO `" . t('technicians') . "` (technician_id, password_hash, full_name, email, is_active, preferred_language) VALUES (?, ?, ?, ?, ?, ?) "); $stmt->execute([$tech_id, $password_hash, $full_name, $email, $is_active, $preferred_language]); @@ -136,7 +136,7 @@ function handle_edit_tech(PDO $pdo, array $admin_session): void { $is_active = isset($_POST['is_active']) ? 1 : 0; $stmt = $pdo->prepare(" - UPDATE technicians + UPDATE `" . t('technicians') . "` SET full_name = ?, email = ?, is_active = ? WHERE id = ? "); @@ -161,7 +161,7 @@ function handle_get_tech(PDO $pdo, array $admin_session): void { $stmt = $pdo->prepare(" SELECT id, technician_id, full_name, email, is_active, preferred_server, preferred_language - FROM technicians + FROM `" . t('technicians') . "` WHERE id = ? "); $stmt->execute([$techId]); @@ -200,7 +200,7 @@ function handle_update_tech(PDO $pdo, array $admin_session, ?array $json_input = } $stmt = $pdo->prepare(" - UPDATE technicians + UPDATE `" . t('technicians') . "` SET full_name = ?, email = ?, preferred_language = ?, is_active = ? WHERE id = ? "); @@ -230,7 +230,7 @@ function handle_reset_password(PDO $pdo, array $admin_session): void { $password_hash = password_hash($new_password, PASSWORD_BCRYPT, ['cost' => BCRYPT_COST]); $stmt = $pdo->prepare(" - UPDATE technicians + UPDATE `" . t('technicians') . "` SET password_hash = ?, must_change_password = 1 WHERE id = ? "); @@ -252,7 +252,7 @@ function handle_toggle_tech(PDO $pdo, array $admin_session): void { $id = intval($_POST['id'] ?? 0); $stmt = $pdo->prepare(" - UPDATE technicians + UPDATE `" . t('technicians') . "` SET is_active = NOT is_active WHERE id = ? "); @@ -273,7 +273,7 @@ function handle_delete_tech(PDO $pdo, array $admin_session): void { $id = intval($_POST['id'] ?? 0); - $stmt = $pdo->prepare("DELETE FROM technicians WHERE id = ?"); + $stmt = $pdo->prepare("DELETE FROM `" . t('technicians') . "` WHERE id = ?"); $stmt->execute([$id]); logAdminActivity( diff --git a/FINAL_PRODUCTION_SYSTEM/controllers/admin/UpgradeController.php b/FINAL_PRODUCTION_SYSTEM/controllers/admin/UpgradeController.php index 0129469..7325a9c 100644 --- a/FINAL_PRODUCTION_SYSTEM/controllers/admin/UpgradeController.php +++ b/FINAL_PRODUCTION_SYSTEM/controllers/admin/UpgradeController.php @@ -34,7 +34,7 @@ function getBackupDir(): string { } function loadUpgradeRow(PDO $pdo, int $upgradeId): ?array { - $stmt = $pdo->prepare("SELECT * FROM upgrade_history WHERE id = ?"); + $stmt = $pdo->prepare("SELECT * FROM `" . t('upgrade_history') . "` WHERE id = ?"); $stmt->execute([$upgradeId]); return $stmt->fetch(PDO::FETCH_ASSOC) ?: null; } @@ -47,7 +47,7 @@ function updateUpgradeStatus(PDO $pdo, int $id, string $status, array $extra = [ $params[] = $val; } $params[] = $id; - $sql = "UPDATE upgrade_history SET " . implode(', ', $sets) . " WHERE id = ?"; + $sql = "UPDATE `" . t('upgrade_history') . "` SET " . implode(', ', $sets) . " WHERE id = ?"; $stmt = $pdo->prepare($sql); $stmt->execute($params); } @@ -275,7 +275,7 @@ function handle_upgrade_download_github(PDO $pdo, array $admin_session, $json_in // Check no active upgrade $stmt = $pdo->prepare(" - SELECT id, status FROM upgrade_history + SELECT id, status FROM `" . t('upgrade_history') . "` WHERE status NOT IN ('completed', 'failed', 'rolled_back') LIMIT 1 "); @@ -368,7 +368,7 @@ function handle_upgrade_download_github(PDO $pdo, array $admin_session, $json_in // Create upgrade_history row $stmt = $pdo->prepare(" - INSERT INTO upgrade_history + INSERT INTO `" . t('upgrade_history') . "` (from_version, to_version, from_version_code, to_version_code, status, manifest_json, package_filename, package_checksum, admin_id, admin_username) @@ -416,7 +416,7 @@ function handle_upgrade_get_status(PDO $pdo, array $admin_session, $json_input): // Active (non-terminal) upgrade $stmt = $pdo->query(" - SELECT * FROM upgrade_history + SELECT * FROM `" . t('upgrade_history') . "` WHERE status NOT IN ('completed', 'failed', 'rolled_back') ORDER BY created_at DESC LIMIT 1 "); @@ -427,7 +427,7 @@ function handle_upgrade_get_status(PDO $pdo, array $admin_session, $json_input): SELECT id, from_version, to_version, status, package_filename, error_message, started_at, completed_at, rolled_back_at, admin_username, created_at - FROM upgrade_history + FROM `" . t('upgrade_history') . "` ORDER BY created_at DESC LIMIT 10 "); $recentUpgrades = $stmt2->fetchAll(PDO::FETCH_ASSOC); @@ -511,7 +511,7 @@ function handle_upgrade_upload_package(PDO $pdo, array $admin_session): void { // Check no active upgrade in progress $stmt = $pdo->prepare(" - SELECT id, status FROM upgrade_history + SELECT id, status FROM `" . t('upgrade_history') . "` WHERE status NOT IN ('completed', 'failed', 'rolled_back') LIMIT 1 "); @@ -536,7 +536,7 @@ function handle_upgrade_upload_package(PDO $pdo, array $admin_session): void { // Create upgrade_history row $stmt = $pdo->prepare(" - INSERT INTO upgrade_history + INSERT INTO `" . t('upgrade_history') . "` (from_version, to_version, from_version_code, to_version_code, status, manifest_json, package_filename, package_checksum, admin_id, admin_username) @@ -859,7 +859,7 @@ function handle_upgrade_apply(PDO $pdo, array $admin_session, $json_input): void // Lock the row to prevent concurrent execution $pdo->beginTransaction(); - $lockStmt = $pdo->prepare("SELECT id FROM upgrade_history WHERE id = ? FOR UPDATE"); + $lockStmt = $pdo->prepare("SELECT id FROM `" . t('upgrade_history') . "` WHERE id = ? FOR UPDATE"); $lockStmt->execute([$upgradeId]); $lockStmt->closeCursor(); $pdo->commit(); @@ -899,7 +899,7 @@ function handle_upgrade_apply(PDO $pdo, array $admin_session, $json_input): void $migVersion = (int)($mig['version'] ?? 0); // Check if already applied - $checkStmt = $pdo->prepare("SELECT COUNT(*) FROM schema_versions WHERE filename = ?"); + $checkStmt = $pdo->prepare("SELECT COUNT(*) FROM `" . t('schema_versions') . "` WHERE filename = ?"); $checkStmt->execute([basename($migFile)]); $alreadyApplied = (int)$checkStmt->fetchColumn() > 0; $checkStmt->closeCursor(); @@ -945,7 +945,7 @@ function handle_upgrade_apply(PDO $pdo, array $admin_session, $json_input): void // Record in schema_versions $checksum = hash('sha256', $migContent); - $svStmt = $pdo->prepare("INSERT INTO schema_versions (version, filename, checksum) VALUES (?, ?, ?)"); + $svStmt = $pdo->prepare("INSERT INTO `" . t('schema_versions') . "` (version, filename, checksum) VALUES (?, ?, ?)"); $svStmt->execute([$migVersion, basename($migFile), $checksum]); $svStmt->closeCursor(); @@ -1327,7 +1327,7 @@ function handle_upgrade_rollback(PDO $pdo, array $admin_session, $json_input): v ); $freshPdo->prepare(" - INSERT INTO upgrade_history + INSERT INTO `" . t('upgrade_history') . "` (from_version, to_version, status, error_message, admin_id, admin_username, rolled_back_at) VALUES (?, ?, 'rolled_back', ?, ?, ?, ?) @@ -1363,7 +1363,7 @@ function handle_upgrade_history(PDO $pdo, array $admin_session, $json_input): vo status, package_filename, error_message, started_at, completed_at, rolled_back_at, admin_id, admin_username, created_at - FROM upgrade_history + FROM `" . t('upgrade_history') . "` ORDER BY created_at DESC LIMIT 50 "); diff --git a/FINAL_PRODUCTION_SYSTEM/controllers/admin/UsbDevicesController.php b/FINAL_PRODUCTION_SYSTEM/controllers/admin/UsbDevicesController.php index 888e86e..ad824f1 100644 --- a/FINAL_PRODUCTION_SYSTEM/controllers/admin/UsbDevicesController.php +++ b/FINAL_PRODUCTION_SYSTEM/controllers/admin/UsbDevicesController.php @@ -43,7 +43,7 @@ function handle_list_usb_devices(PDO $pdo, array $admin_session): void { // Get USB device statistics $stmt = $pdo->query(" SELECT device_status, COUNT(*) as count - FROM usb_devices + FROM `" . t('usb_devices') . "` GROUP BY device_status "); $statusCounts = $stmt->fetchAll(PDO::FETCH_KEY_PAIR); @@ -79,7 +79,7 @@ function handle_register_usb_device(PDO $pdo, array $admin_session, ?array $json } // Check if technician exists and is active - $stmt = $pdo->prepare("SELECT technician_id, is_active FROM technicians WHERE technician_id = ?"); + $stmt = $pdo->prepare("SELECT technician_id, is_active FROM `" . t('technicians') . "` WHERE technician_id = ?"); $stmt->execute([$technicianId]); $technician = $stmt->fetch(PDO::FETCH_ASSOC); @@ -94,7 +94,7 @@ function handle_register_usb_device(PDO $pdo, array $admin_session, ?array $json } // Check if serial number already exists - $stmt = $pdo->prepare("SELECT device_id, device_name FROM usb_devices WHERE device_serial_number = ?"); + $stmt = $pdo->prepare("SELECT device_id, device_name FROM `" . t('usb_devices') . "` WHERE device_serial_number = ?"); $stmt->execute([$deviceSerial]); $existingDevice = $stmt->fetch(PDO::FETCH_ASSOC); @@ -109,7 +109,7 @@ function handle_register_usb_device(PDO $pdo, array $admin_session, ?array $json // Check max devices per technician limit $maxDevices = (int)getConfig('usb_auth_max_devices_per_tech'); if ($maxDevices > 0) { - $stmt = $pdo->prepare("SELECT COUNT(*) FROM usb_devices WHERE technician_id = ? AND device_status = 'active'"); + $stmt = $pdo->prepare("SELECT COUNT(*) FROM `" . t('usb_devices') . "` WHERE technician_id = ? AND device_status = 'active'"); $stmt->execute([$technicianId]); $currentCount = $stmt->fetchColumn(); @@ -124,7 +124,7 @@ function handle_register_usb_device(PDO $pdo, array $admin_session, ?array $json // Insert new USB device $stmt = $pdo->prepare(" - INSERT INTO usb_devices ( + INSERT INTO `" . t('usb_devices') . "` ( device_serial_number, device_name, technician_id, device_manufacturer, device_model, device_capacity_gb, device_description, registered_by_admin_id @@ -166,7 +166,7 @@ function handle_update_usb_device_status(PDO $pdo, array $admin_session, ?array } // Get device info before update - $stmt = $pdo->prepare("SELECT device_name, technician_id FROM usb_devices WHERE device_id = ?"); + $stmt = $pdo->prepare("SELECT device_name, technician_id FROM `" . t('usb_devices') . "` WHERE device_id = ?"); $stmt->execute([$deviceId]); $device = $stmt->fetch(PDO::FETCH_ASSOC); @@ -178,7 +178,7 @@ function handle_update_usb_device_status(PDO $pdo, array $admin_session, ?array // Update device status if ($newStatus === 'active') { $stmt = $pdo->prepare(" - UPDATE usb_devices + UPDATE `" . t('usb_devices') . "` SET device_status = 'active', disabled_date = NULL, disabled_by_admin_id = NULL, @@ -190,7 +190,7 @@ function handle_update_usb_device_status(PDO $pdo, array $admin_session, ?array $disableReason = $json_input['reason'] ?? null; $stmt = $pdo->prepare(" - UPDATE usb_devices + UPDATE `" . t('usb_devices') . "` SET device_status = ?, disabled_date = NOW(), disabled_by_admin_id = ?, @@ -218,7 +218,7 @@ function handle_delete_usb_device(PDO $pdo, array $admin_session, ?array $json_i $deviceId = intval($json_input['device_id'] ?? 0); - $stmt = $pdo->prepare("SELECT device_name, technician_id FROM usb_devices WHERE device_id = ?"); + $stmt = $pdo->prepare("SELECT device_name, technician_id FROM `" . t('usb_devices') . "` WHERE device_id = ?"); $stmt->execute([$deviceId]); $device = $stmt->fetch(PDO::FETCH_ASSOC); @@ -227,7 +227,7 @@ function handle_delete_usb_device(PDO $pdo, array $admin_session, ?array $json_i return; } - $stmt = $pdo->prepare("DELETE FROM usb_devices WHERE device_id = ?"); + $stmt = $pdo->prepare("DELETE FROM `" . t('usb_devices') . "` WHERE device_id = ?"); $stmt->execute([$deviceId]); logAdminActivity( diff --git a/FINAL_PRODUCTION_SYSTEM/database/2fa_migration.sql b/FINAL_PRODUCTION_SYSTEM/database/2fa_migration.sql index 1f27a42..fc1fae9 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/2fa_migration.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/2fa_migration.sql @@ -6,7 +6,7 @@ -- Table: admin_totp_secrets -- Stores TOTP secrets and backup codes for admin 2FA -CREATE TABLE IF NOT EXISTS `admin_totp_secrets` ( +CREATE TABLE IF NOT EXISTS `#__admin_totp_secrets` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `admin_id` INT NOT NULL COMMENT 'Reference to admin_users.id', `totp_secret` VARCHAR(255) NOT NULL COMMENT 'Base32 encoded TOTP secret', @@ -18,12 +18,12 @@ CREATE TABLE IF NOT EXISTS `admin_totp_secrets` ( UNIQUE KEY `idx_admin_id` (`admin_id`), INDEX `idx_enabled` (`totp_enabled`), - FOREIGN KEY (`admin_id`) REFERENCES `admin_users`(`id`) ON DELETE CASCADE + FOREIGN KEY (`admin_id`) REFERENCES `#__admin_users`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='TOTP 2FA secrets for admin accounts'; -- Table: trusted_networks -- Defines network subnets that are trusted for security bypasses -CREATE TABLE IF NOT EXISTS `trusted_networks` ( +CREATE TABLE IF NOT EXISTS `#__trusted_networks` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `network_name` VARCHAR(100) NOT NULL COMMENT 'Friendly name (e.g., "Office LAN")', `ip_range` VARCHAR(45) NOT NULL COMMENT 'CIDR notation (e.g., 192.168.1.0/24)', @@ -38,12 +38,12 @@ CREATE TABLE IF NOT EXISTS `trusted_networks` ( INDEX `idx_active` (`is_active`), INDEX `idx_bypass_2fa` (`bypass_2fa`, `is_active`), INDEX `idx_usb_auth` (`allow_usb_auth`, `is_active`), - FOREIGN KEY (`created_by_admin_id`) REFERENCES `admin_users`(`id`) ON DELETE SET NULL + FOREIGN KEY (`created_by_admin_id`) REFERENCES `#__admin_users`(`id`) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Trusted network subnets for security features'; -- Modify: admin_activity_log -- Add columns to track 2FA usage and trusted network bypasses -ALTER TABLE `admin_activity_log` +ALTER TABLE `#__admin_activity_log` ADD COLUMN IF NOT EXISTS `totp_verified` TINYINT(1) NULL COMMENT '1=2FA used, 0=bypassed, NULL=not applicable' AFTER `user_agent`, ADD COLUMN IF NOT EXISTS `trusted_network_id` INT NULL COMMENT 'If bypassed, which network' AFTER `totp_verified`, ADD INDEX IF NOT EXISTS `idx_totp_verified` (`totp_verified`); @@ -55,8 +55,8 @@ SET @fk_exists = (SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS AND CONSTRAINT_NAME = 'fk_admin_activity_log_trusted_network'); SET @sql = IF(@fk_exists = 0, - 'ALTER TABLE `admin_activity_log` ADD CONSTRAINT `fk_admin_activity_log_trusted_network` - FOREIGN KEY (`trusted_network_id`) REFERENCES `trusted_networks`(`id`) ON DELETE SET NULL', + 'ALTER TABLE `#__admin_activity_log` ADD CONSTRAINT `fk_admin_activity_log_trusted_network` + FOREIGN KEY (`trusted_network_id`) REFERENCES `#__trusted_networks`(`id`) ON DELETE SET NULL', 'SELECT "Foreign key already exists"'); PREPARE stmt FROM @sql; @@ -64,7 +64,7 @@ EXECUTE stmt; DEALLOCATE PREPARE stmt; -- System configuration for 2FA features -INSERT INTO `system_config` (`config_key`, `config_value`, `description`) VALUES +INSERT INTO `#__system_config` (`config_key`, `config_value`, `description`) VALUES ('totp_2fa_available', '1', 'Enable TOTP 2FA feature (1=yes, 0=no)'), ('totp_issuer_name', 'OEM Activation System', 'TOTP issuer name shown in authenticator app'), ('totp_backup_codes_count', '10', 'Number of backup codes to generate per user'), diff --git a/FINAL_PRODUCTION_SYSTEM/database/acl_migration.sql b/FINAL_PRODUCTION_SYSTEM/database/acl_migration.sql index 1390bf4..d5fd64f 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/acl_migration.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/acl_migration.sql @@ -10,7 +10,7 @@ -- ============================================================ -- Permission Categories (groups permissions for UI accordion) -CREATE TABLE IF NOT EXISTS acl_permission_categories ( +CREATE TABLE IF NOT EXISTS `#__acl_permission_categories` ( id INT AUTO_INCREMENT PRIMARY KEY, category_key VARCHAR(50) NOT NULL UNIQUE, display_name VARCHAR(100) NOT NULL, @@ -19,7 +19,7 @@ CREATE TABLE IF NOT EXISTS acl_permission_categories ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- Granular Permissions -CREATE TABLE IF NOT EXISTS acl_permissions ( +CREATE TABLE IF NOT EXISTS `#__acl_permissions` ( id INT AUTO_INCREMENT PRIMARY KEY, permission_key VARCHAR(100) NOT NULL UNIQUE, display_name VARCHAR(100) NOT NULL, @@ -28,13 +28,13 @@ CREATE TABLE IF NOT EXISTS acl_permissions ( resource_type VARCHAR(50) NOT NULL, action_type ENUM('view','create','edit','delete','manage','execute') NOT NULL, is_dangerous TINYINT(1) DEFAULT 0 COMMENT 'Requires confirmation / shown with warning', - FOREIGN KEY (category_id) REFERENCES acl_permission_categories(id), + FOREIGN KEY (category_id) REFERENCES `#__acl_permission_categories`(id), INDEX idx_resource (resource_type), INDEX idx_category (category_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- Custom Roles -CREATE TABLE IF NOT EXISTS acl_roles ( +CREATE TABLE IF NOT EXISTS `#__acl_roles` ( id INT AUTO_INCREMENT PRIMARY KEY, role_name VARCHAR(50) NOT NULL UNIQUE, display_name VARCHAR(100) NOT NULL, @@ -51,19 +51,19 @@ CREATE TABLE IF NOT EXISTS acl_roles ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- Role <-> Permission Junction -CREATE TABLE IF NOT EXISTS acl_role_permissions ( +CREATE TABLE IF NOT EXISTS `#__acl_role_permissions` ( id INT AUTO_INCREMENT PRIMARY KEY, role_id INT NOT NULL, permission_id INT NOT NULL, granted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, granted_by INT NULL, UNIQUE KEY unique_role_perm (role_id, permission_id), - FOREIGN KEY (role_id) REFERENCES acl_roles(id) ON DELETE CASCADE, - FOREIGN KEY (permission_id) REFERENCES acl_permissions(id) ON DELETE CASCADE + FOREIGN KEY (role_id) REFERENCES `#__acl_roles`(id) ON DELETE CASCADE, + FOREIGN KEY (permission_id) REFERENCES `#__acl_permissions`(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- Per-User Permission Overrides -CREATE TABLE IF NOT EXISTS acl_user_overrides ( +CREATE TABLE IF NOT EXISTS `#__acl_user_overrides` ( id INT AUTO_INCREMENT PRIMARY KEY, user_type ENUM('admin','technician') NOT NULL, user_id INT NOT NULL, @@ -74,13 +74,13 @@ CREATE TABLE IF NOT EXISTS acl_user_overrides ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_by INT NULL, UNIQUE KEY unique_user_perm (user_type, user_id, permission_id), - FOREIGN KEY (permission_id) REFERENCES acl_permissions(id) ON DELETE CASCADE, + FOREIGN KEY (permission_id) REFERENCES `#__acl_permissions`(id) ON DELETE CASCADE, INDEX idx_user (user_type, user_id), INDEX idx_expires (expires_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ACL Change Audit Log -CREATE TABLE IF NOT EXISTS acl_change_log ( +CREATE TABLE IF NOT EXISTS `#__acl_change_log` ( id INT AUTO_INCREMENT PRIMARY KEY, actor_id INT NOT NULL, actor_type ENUM('admin','system') DEFAULT 'admin', @@ -102,15 +102,15 @@ CREATE TABLE IF NOT EXISTS acl_change_log ( -- ============================================================ -- Add custom_role_id to admin_users (links to acl_roles for new ACL) -ALTER TABLE admin_users ADD COLUMN custom_role_id INT NULL AFTER role; -ALTER TABLE admin_users ADD CONSTRAINT fk_admin_acl_role FOREIGN KEY (custom_role_id) REFERENCES acl_roles(id) ON DELETE SET NULL; +ALTER TABLE `#__admin_users` ADD COLUMN custom_role_id INT NULL AFTER role; +ALTER TABLE `#__admin_users` ADD CONSTRAINT fk_admin_acl_role FOREIGN KEY (custom_role_id) REFERENCES `#__acl_roles`(id) ON DELETE SET NULL; -- Add role_id to technicians (links to acl_roles for technician roles) -ALTER TABLE technicians ADD COLUMN role_id INT NULL AFTER is_active; -ALTER TABLE technicians ADD CONSTRAINT fk_tech_acl_role FOREIGN KEY (role_id) REFERENCES acl_roles(id) ON DELETE SET NULL; +ALTER TABLE `#__technicians` ADD COLUMN role_id INT NULL AFTER is_active; +ALTER TABLE `#__technicians` ADD CONSTRAINT fk_tech_acl_role FOREIGN KEY (role_id) REFERENCES `#__acl_roles`(id) ON DELETE SET NULL; -- Add acl_v2_enabled config flag (disabled by default for safety) -INSERT INTO system_config (config_key, config_value, description) +INSERT INTO `#__system_config` (config_key, config_value, description) VALUES ('acl_v2_enabled', '0', 'Enable database-driven ACL system (0=legacy hardcoded, 1=new ACL)') ON DUPLICATE KEY UPDATE config_key = config_key; @@ -118,7 +118,7 @@ ON DUPLICATE KEY UPDATE config_key = config_key; -- SEED: Permission Categories -- ============================================================ -INSERT INTO acl_permission_categories (category_key, display_name, icon, sort_order) VALUES +INSERT INTO `#__acl_permission_categories` (category_key, display_name, icon, sort_order) VALUES ('dashboard', 'Dashboard & Reports', NULL, 10), ('keys', 'OEM Key Management', NULL, 20), ('technicians', 'Technician Management', NULL, 30), @@ -134,7 +134,7 @@ INSERT INTO acl_permission_categories (category_key, display_name, icon, sort_or -- SEED: Granular Permissions (~38) -- ============================================================ -INSERT INTO acl_permissions (permission_key, display_name, description, category_id, resource_type, action_type, is_dangerous) VALUES +INSERT INTO `#__acl_permissions` (permission_key, display_name, description, category_id, resource_type, action_type, is_dangerous) VALUES -- Dashboard (cat 1) ('view_dashboard', 'View Dashboard', 'Access the main dashboard with statistics', 1, 'dashboard', 'view', 0), ('view_reports', 'View Reports', 'Access activation and usage reports', 1, 'dashboard', 'view', 0), @@ -199,7 +199,7 @@ INSERT INTO acl_permissions (permission_key, display_name, description, category -- SEED: Roles (7 admin + 2 technician) -- ============================================================ -INSERT INTO acl_roles (role_name, display_name, description, role_type, is_system_role, priority, color) VALUES +INSERT INTO `#__acl_roles` (role_name, display_name, description, role_type, is_system_role, priority, color) VALUES -- Admin roles ('super_admin', 'Super Administrator', 'Full system access including admin management, system settings, backups, and role management', 'admin', 1, 100, '#dc3545'), ('admin', 'Administrator', 'All data operations except delete, admin management, and system settings', 'admin', 1, 80, '#007bff'), @@ -218,15 +218,15 @@ INSERT INTO acl_roles (role_name, display_name, description, role_type, is_syste -- Helper: Get role and permission IDs for assignment -- super_admin: ALL permissions -INSERT INTO acl_role_permissions (role_id, permission_id) +INSERT INTO `#__acl_role_permissions` (role_id, permission_id) SELECT r.id, p.id -FROM acl_roles r CROSS JOIN acl_permissions p +FROM `#__acl_roles` r CROSS JOIN `#__acl_permissions` p WHERE r.role_name = 'super_admin'; -- admin: All view + edit/create operations, NO delete, NO admin mgmt, NO system settings, NO role mgmt -INSERT INTO acl_role_permissions (role_id, permission_id) +INSERT INTO `#__acl_role_permissions` (role_id, permission_id) SELECT r.id, p.id -FROM acl_roles r, acl_permissions p +FROM `#__acl_roles` r, acl_permissions p WHERE r.role_name = 'admin' AND p.permission_key IN ( 'view_dashboard', 'view_reports', 'export_data', 'view_keys', 'add_key', 'import_keys', 'edit_key', 'recycle_key', @@ -239,9 +239,9 @@ WHERE r.role_name = 'admin' AND p.permission_key IN ( ); -- billing_manager: Dashboard, keys (view only), activations (view), reports, export, logs -INSERT INTO acl_role_permissions (role_id, permission_id) +INSERT INTO `#__acl_role_permissions` (role_id, permission_id) SELECT r.id, p.id -FROM acl_roles r, acl_permissions p +FROM `#__acl_roles` r, acl_permissions p WHERE r.role_name = 'billing_manager' AND p.permission_key IN ( 'view_dashboard', 'view_reports', 'export_data', 'view_keys', @@ -250,9 +250,9 @@ WHERE r.role_name = 'billing_manager' AND p.permission_key IN ( ); -- hr_manager: Dashboard, technician CRUD, password reset, role assignment -INSERT INTO acl_role_permissions (role_id, permission_id) +INSERT INTO `#__acl_role_permissions` (role_id, permission_id) SELECT r.id, p.id -FROM acl_roles r, acl_permissions p +FROM `#__acl_roles` r, acl_permissions p WHERE r.role_name = 'hr_manager' AND p.permission_key IN ( 'view_dashboard', 'view_technicians', 'add_technician', 'edit_technician', 'reset_tech_password', 'assign_tech_role', @@ -260,9 +260,9 @@ WHERE r.role_name = 'hr_manager' AND p.permission_key IN ( ); -- qc_inspector: View activations, hardware, add notes, export -INSERT INTO acl_role_permissions (role_id, permission_id) +INSERT INTO `#__acl_role_permissions` (role_id, permission_id) SELECT r.id, p.id -FROM acl_roles r, acl_permissions p +FROM `#__acl_roles` r, acl_permissions p WHERE r.role_name = 'qc_inspector' AND p.permission_key IN ( 'view_dashboard', 'view_reports', 'export_data', 'view_activations', 'add_activation_note', @@ -270,9 +270,9 @@ WHERE r.role_name = 'qc_inspector' AND p.permission_key IN ( ); -- dept_manager: View technicians, activations, hardware, dashboard, add notes -INSERT INTO acl_role_permissions (role_id, permission_id) +INSERT INTO `#__acl_role_permissions` (role_id, permission_id) SELECT r.id, p.id -FROM acl_roles r, acl_permissions p +FROM `#__acl_roles` r, acl_permissions p WHERE r.role_name = 'dept_manager' AND p.permission_key IN ( 'view_dashboard', 'view_reports', 'export_data', 'view_technicians', @@ -282,9 +282,9 @@ WHERE r.role_name = 'dept_manager' AND p.permission_key IN ( ); -- viewer: Read-only across the board -INSERT INTO acl_role_permissions (role_id, permission_id) +INSERT INTO `#__acl_role_permissions` (role_id, permission_id) SELECT r.id, p.id -FROM acl_roles r, acl_permissions p +FROM `#__acl_roles` r, acl_permissions p WHERE r.role_name = 'viewer' AND p.permission_key IN ( 'view_dashboard', 'view_reports', 'export_data', 'view_keys', @@ -297,17 +297,17 @@ WHERE r.role_name = 'viewer' AND p.permission_key IN ( ); -- technician_full: Can activate, submit hardware -INSERT INTO acl_role_permissions (role_id, permission_id) +INSERT INTO `#__acl_role_permissions` (role_id, permission_id) SELECT r.id, p.id -FROM acl_roles r, acl_permissions p +FROM `#__acl_roles` r, acl_permissions p WHERE r.role_name = 'technician_full' AND p.permission_key IN ( 'view_keys', 'view_activations', 'view_hardware' ); -- technician_limited: View only -INSERT INTO acl_role_permissions (role_id, permission_id) +INSERT INTO `#__acl_role_permissions` (role_id, permission_id) SELECT r.id, p.id -FROM acl_roles r, acl_permissions p +FROM `#__acl_roles` r, acl_permissions p WHERE r.role_name = 'technician_limited' AND p.permission_key IN ( 'view_keys' ); @@ -317,6 +317,6 @@ WHERE r.role_name = 'technician_limited' AND p.permission_key IN ( -- Map legacy role ENUM to new custom_role_id -- ============================================================ -UPDATE admin_users au -INNER JOIN acl_roles ar ON ar.role_name COLLATE utf8mb4_general_ci = au.role COLLATE utf8mb4_general_ci AND ar.role_type = 'admin' +UPDATE `#__admin_users` au +INNER JOIN `#__acl_roles` ar ON ar.role_name COLLATE utf8mb4_general_ci = au.role COLLATE utf8mb4_general_ci AND ar.role_type = 'admin' SET au.custom_role_id = ar.id; diff --git a/FINAL_PRODUCTION_SYSTEM/database/backup_migration.sql b/FINAL_PRODUCTION_SYSTEM/database/backup_migration.sql index 2efd0aa..183ec9a 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/backup_migration.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/backup_migration.sql @@ -5,7 +5,7 @@ -- Table: backup_history -- Tracks all database backups (automated and manual) -CREATE TABLE IF NOT EXISTS `backup_history` ( +CREATE TABLE IF NOT EXISTS `#__backup_history` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `backup_filename` VARCHAR(255) NOT NULL COMMENT 'Filename in backups directory', `backup_size_mb` DECIMAL(10,2) NOT NULL COMMENT 'Backup file size in megabytes', @@ -25,12 +25,12 @@ CREATE TABLE IF NOT EXISTS `backup_history` ( INDEX `idx_backup_type` (`backup_type`), INDEX `idx_deleted_at` (`deleted_at`), INDEX `idx_filename` (`backup_filename`), - FOREIGN KEY (`created_by_admin_id`) REFERENCES `admin_users`(`id`) ON DELETE SET NULL + FOREIGN KEY (`created_by_admin_id`) REFERENCES `#__admin_users`(`id`) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Database backup history and tracking'; -- Table: backup_restore_log -- Tracks database restore operations for disaster recovery audit -CREATE TABLE IF NOT EXISTS `backup_restore_log` ( +CREATE TABLE IF NOT EXISTS `#__backup_restore_log` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `backup_history_id` INT NULL COMMENT 'Which backup was restored', `backup_filename` VARCHAR(255) NOT NULL COMMENT 'Backup file used for restore', @@ -44,12 +44,12 @@ CREATE TABLE IF NOT EXISTS `backup_restore_log` ( INDEX `idx_restored_at` (`restored_at`), INDEX `idx_restore_status` (`restore_status`), - FOREIGN KEY (`backup_history_id`) REFERENCES `backup_history`(`id`) ON DELETE SET NULL, - FOREIGN KEY (`restored_by_admin_id`) REFERENCES `admin_users`(`id`) ON DELETE SET NULL + FOREIGN KEY (`backup_history_id`) REFERENCES `#__backup_history`(`id`) ON DELETE SET NULL, + FOREIGN KEY (`restored_by_admin_id`) REFERENCES `#__admin_users`(`id`) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Database restore operation audit log'; -- System configuration for automated backups -INSERT INTO `system_config` (`config_key`, `config_value`, `description`) VALUES +INSERT INTO `#__system_config` (`config_key`, `config_value`, `description`) VALUES ('backup_enabled', '1', 'Enable automated database backups (1=yes, 0=no)'), ('backup_retention_days', '30', 'Number of days to keep backups before deletion'), ('backup_schedule', '0 2 * * *', 'Backup cron schedule (default: daily at 2 AM UTC)'), diff --git a/FINAL_PRODUCTION_SYSTEM/database/client_config_migration.sql b/FINAL_PRODUCTION_SYSTEM/database/client_config_migration.sql index 2cbda75..bd1b580 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/client_config_migration.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/client_config_migration.sql @@ -5,7 +5,7 @@ -- activation timing, and network diagnostics settings. -- ============================================================= -INSERT INTO system_config (config_key, config_value, description) VALUES +INSERT INTO `#__system_config` (config_key, config_value, description) VALUES -- Pre-Activation Task Toggles ('client_task_wsus_cleanup', '1', 'Enable WSUS cleanup before activation'), ('client_task_security_hardening', '1', 'Enable SMB security hardening'), diff --git a/FINAL_PRODUCTION_SYSTEM/database/client_resources_migration.sql b/FINAL_PRODUCTION_SYSTEM/database/client_resources_migration.sql index ca942ba..ad31235 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/client_resources_migration.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/client_resources_migration.sql @@ -2,7 +2,7 @@ -- Phase 9: PowerShell 7 Migration — Hosted MSI Installer -- Run this migration to add the client_resources table and PS7 config entries -CREATE TABLE IF NOT EXISTS client_resources ( +CREATE TABLE IF NOT EXISTS `#__client_resources` ( id INT AUTO_INCREMENT PRIMARY KEY, resource_key VARCHAR(100) NOT NULL UNIQUE, filename VARCHAR(255) NOT NULL, @@ -14,5 +14,5 @@ CREATE TABLE IF NOT EXISTS client_resources ( uploaded_by INT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (uploaded_by) REFERENCES admin_users(id) ON DELETE SET NULL + FOREIGN KEY (uploaded_by) REFERENCES `#__admin_users`(id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/FINAL_PRODUCTION_SYSTEM/database/create_admin.php b/FINAL_PRODUCTION_SYSTEM/database/create_admin.php index 455f123..94379d9 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/create_admin.php +++ b/FINAL_PRODUCTION_SYSTEM/database/create_admin.php @@ -2,8 +2,8 @@ require __DIR__ . '/../config.php'; $hash = password_hash("Admin2024!", PASSWORD_BCRYPT, ["cost" => 10]); // Get super_admin role ID from acl_roles -$roleId = $pdo->query("SELECT id FROM acl_roles WHERE role_name = 'super_admin' LIMIT 1")->fetchColumn() ?: null; +$roleId = $pdo->query("SELECT id FROM `" . t('acl_roles') . "` WHERE role_name = 'super_admin' LIMIT 1")->fetchColumn() ?: null; -$stmt = $pdo->prepare("INSERT INTO admin_users (username, password_hash, full_name, email, role, custom_role_id) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE password_hash = VALUES(password_hash), custom_role_id = VALUES(custom_role_id), failed_login_attempts = 0, locked_until = NULL"); +$stmt = $pdo->prepare("INSERT INTO `" . t('admin_users') . "` (username, password_hash, full_name, email, role, custom_role_id) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE password_hash = VALUES(password_hash), custom_role_id = VALUES(custom_role_id), failed_login_attempts = 0, locked_until = NULL"); $stmt->execute(["admin", $hash, "Administrator", "admin@localhost", "super_admin", $roleId]); echo "Admin user created/reset\n"; diff --git a/FINAL_PRODUCTION_SYSTEM/database/database_admin_security.sql b/FINAL_PRODUCTION_SYSTEM/database/database_admin_security.sql index 0dad74c..e628b04 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/database_admin_security.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/database_admin_security.sql @@ -3,8 +3,8 @@ USE oem_activation; --- Admin users table (separate from technicians) -CREATE TABLE admin_users ( +-- Admin users table (separate from `#__technicians`) +CREATE TABLE `#__admin_users` ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, full_name VARCHAR(100) NOT NULL, @@ -22,11 +22,11 @@ CREATE TABLE admin_users ( created_by INT, INDEX idx_username (username), INDEX idx_is_active (is_active), - FOREIGN KEY (created_by) REFERENCES admin_users(id) + FOREIGN KEY (created_by) REFERENCES `#__admin_users`(id) ); -- Admin sessions table -CREATE TABLE admin_sessions ( +CREATE TABLE `#__admin_sessions` ( id INT AUTO_INCREMENT PRIMARY KEY, admin_id INT NOT NULL, session_token VARCHAR(64) NOT NULL UNIQUE, @@ -36,14 +36,14 @@ CREATE TABLE admin_sessions ( last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, expires_at TIMESTAMP NOT NULL, is_active BOOLEAN DEFAULT TRUE, - FOREIGN KEY (admin_id) REFERENCES admin_users(id), + FOREIGN KEY (admin_id) REFERENCES `#__admin_users`(id), INDEX idx_session_token (session_token), INDEX idx_admin_id (admin_id), INDEX idx_expires_at (expires_at) ); -- Admin activity log -CREATE TABLE admin_activity_log ( +CREATE TABLE `#__admin_activity_log` ( id INT AUTO_INCREMENT PRIMARY KEY, admin_id INT, session_id INT, @@ -52,15 +52,15 @@ CREATE TABLE admin_activity_log ( ip_address VARCHAR(45), user_agent TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (admin_id) REFERENCES admin_users(id), - FOREIGN KEY (session_id) REFERENCES admin_sessions(id), + FOREIGN KEY (admin_id) REFERENCES `#__admin_users`(id), + FOREIGN KEY (session_id) REFERENCES `#__admin_sessions`(id), INDEX idx_admin_id (admin_id), INDEX idx_created_at (created_at), INDEX idx_action (action) ); -- IP whitelist table -CREATE TABLE admin_ip_whitelist ( +CREATE TABLE `#__admin_ip_whitelist` ( id INT AUTO_INCREMENT PRIMARY KEY, ip_address VARCHAR(45) NOT NULL, ip_range VARCHAR(45), @@ -68,13 +68,13 @@ CREATE TABLE admin_ip_whitelist ( is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_by INT, - FOREIGN KEY (created_by) REFERENCES admin_users(id), + FOREIGN KEY (created_by) REFERENCES `#__admin_users`(id), INDEX idx_ip_address (ip_address), INDEX idx_is_active (is_active) ); -- Add security configuration -INSERT INTO system_config (config_key, config_value, description) VALUES +INSERT INTO `#__system_config` (config_key, config_value, description) VALUES ('admin_session_timeout_minutes', '30', 'Admin session timeout in minutes'), ('admin_max_failed_logins', '3', 'Maximum failed admin login attempts'), ('admin_lockout_duration_minutes', '30', 'Admin account lockout duration'), @@ -87,12 +87,12 @@ ON DUPLICATE KEY UPDATE config_value = VALUES(config_value); -- Create initial super admin (password: SuperSecure2024!) -- Password hash for: SuperSecure2024! -INSERT INTO admin_users (username, full_name, email, password_hash, role, must_change_password, created_by) +INSERT INTO `#__admin_users` (username, full_name, email, password_hash, role, must_change_password, created_by) VALUES ('superadmin', 'Super Administrator', 'admin@yourcompany.com', '$2y$12$LQv3c1yqBwlVHpPd7u/Dw.G2K2wjDUl9jhJxfTULt3lOAOWuTDBKG', 'super_admin', TRUE, NULL); -- Add some safe IP addresses (update these for your environment) --- INSERT INTO admin_ip_whitelist (ip_address, description, created_by) VALUES +-- INSERT INTO `#__admin_ip_whitelist` (ip_address, description, created_by) VALUES -- ('192.168.1.0/24', 'Local network', 1), -- ('10.0.0.0/8', 'Internal network', 1); \ No newline at end of file diff --git a/FINAL_PRODUCTION_SYSTEM/database/database_concurrency_indexes.sql b/FINAL_PRODUCTION_SYSTEM/database/database_concurrency_indexes.sql index 247e840..23c4945 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/database_concurrency_indexes.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/database_concurrency_indexes.sql @@ -3,35 +3,35 @@ -- Run this after the main database installation -- Optimize key selection queries (CRITICAL for allocateKeyAtomically) -ALTER TABLE oem_keys +ALTER TABLE `#__oem_keys` ADD INDEX idx_status_fail_date (key_status, fail_counter, last_use_date, id); -- Optimize session lookups for concurrent access -ALTER TABLE active_sessions +ALTER TABLE `#__active_sessions` ADD INDEX idx_tech_active_expires (technician_id, is_active, expires_at); -- Optimize activation attempts queries -ALTER TABLE activation_attempts +ALTER TABLE `#__activation_attempts` ADD INDEX idx_key_tech_date (key_id, technician_id, attempted_at); -- Optimize admin session queries -ALTER TABLE admin_sessions +ALTER TABLE `#__admin_sessions` ADD INDEX idx_admin_active_expires (admin_id, is_active, expires_at); -- Composite index for common technician queries -ALTER TABLE technicians +ALTER TABLE `#__technicians` ADD INDEX idx_active_locked (is_active, locked_until); -- Index for cleanup operations (expired sessions) -ALTER TABLE active_sessions +ALTER TABLE `#__active_sessions` ADD INDEX idx_expires_active (expires_at, is_active); -- Index for audit trail queries -ALTER TABLE admin_activity_log +ALTER TABLE `#__admin_activity_log` ADD INDEX idx_admin_action_time (admin_id, action, created_at); -- Index for key usage statistics -ALTER TABLE oem_keys +ALTER TABLE `#__oem_keys` ADD INDEX idx_first_usage (first_usage_date, key_status); -- Update table statistics for better query planning diff --git a/FINAL_PRODUCTION_SYSTEM/database/database_setup.sql b/FINAL_PRODUCTION_SYSTEM/database/database_setup.sql index 5d9c5d2..56caca0 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/database_setup.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/database_setup.sql @@ -5,7 +5,7 @@ CREATE DATABASE IF NOT EXISTS oem_activation; USE oem_activation; -- Table to store OEM keys -CREATE TABLE oem_keys ( +CREATE TABLE `#__oem_keys` ( id INT AUTO_INCREMENT PRIMARY KEY, product_key VARCHAR(29) NOT NULL UNIQUE, oem_identifier VARCHAR(20) NOT NULL, @@ -21,7 +21,7 @@ CREATE TABLE oem_keys ( ); -- Table to track activation attempts -CREATE TABLE activation_attempts ( +CREATE TABLE `#__activation_attempts` ( id INT AUTO_INCREMENT PRIMARY KEY, key_id INT NOT NULL, order_number VARCHAR(10) NOT NULL, @@ -31,15 +31,15 @@ CREATE TABLE activation_attempts ( attempted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, client_ip VARCHAR(45), notes TEXT, - FOREIGN KEY (key_id) REFERENCES oem_keys(id), - FOREIGN KEY (technician_id) REFERENCES technicians(technician_id), + FOREIGN KEY (key_id) REFERENCES `#__oem_keys`(id), + FOREIGN KEY (technician_id) REFERENCES `#__technicians`(technician_id), INDEX idx_order_number (order_number), INDEX idx_technician_id (technician_id), INDEX idx_attempted_at (attempted_at) ); -- Table to store active sessions/tokens -CREATE TABLE active_sessions ( +CREATE TABLE `#__active_sessions` ( id INT AUTO_INCREMENT PRIMARY KEY, technician_id VARCHAR(20) NOT NULL, session_token VARCHAR(64) NOT NULL UNIQUE, @@ -48,14 +48,14 @@ CREATE TABLE active_sessions ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP NOT NULL, is_active BOOLEAN DEFAULT TRUE, - FOREIGN KEY (key_id) REFERENCES oem_keys(id), + FOREIGN KEY (key_id) REFERENCES `#__oem_keys`(id), INDEX idx_session_token (session_token), INDEX idx_technician_id (technician_id), INDEX idx_expires_at (expires_at) ); -- Table for system configuration -CREATE TABLE system_config ( +CREATE TABLE `#__system_config` ( config_key VARCHAR(50) PRIMARY KEY, config_value TEXT NOT NULL, description TEXT, @@ -63,7 +63,7 @@ CREATE TABLE system_config ( ); -- Insert basic configuration -INSERT INTO system_config (config_key, config_value, description) VALUES +INSERT INTO `#__system_config` (config_key, config_value, description) VALUES ('smtp_server', 'smtp.zoho.com', 'SMTP server for notifications'), ('smtp_port', '587', 'SMTP port'), ('smtp_username', 'oem.activation@roo24.chesnotech.ru', 'SMTP username'), diff --git a/FINAL_PRODUCTION_SYSTEM/database/database_setup_with_users.sql b/FINAL_PRODUCTION_SYSTEM/database/database_setup_with_users.sql index aeb3e77..cee0f75 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/database_setup_with_users.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/database_setup_with_users.sql @@ -5,7 +5,7 @@ CREATE DATABASE IF NOT EXISTS oem_activation; USE oem_activation; -- Table to store technician accounts -CREATE TABLE technicians ( +CREATE TABLE `#__technicians` ( id INT AUTO_INCREMENT PRIMARY KEY, technician_id VARCHAR(20) NOT NULL UNIQUE, full_name VARCHAR(100) NOT NULL, @@ -25,7 +25,7 @@ CREATE TABLE technicians ( ); -- Table to store OEM keys -CREATE TABLE oem_keys ( +CREATE TABLE `#__oem_keys` ( id INT AUTO_INCREMENT PRIMARY KEY, product_key VARCHAR(29) NOT NULL UNIQUE, oem_identifier VARCHAR(20) NOT NULL, @@ -41,7 +41,7 @@ CREATE TABLE oem_keys ( ); -- Table to track activation attempts -CREATE TABLE activation_attempts ( +CREATE TABLE `#__activation_attempts` ( id INT AUTO_INCREMENT PRIMARY KEY, key_id INT NOT NULL, technician_id VARCHAR(20) NOT NULL, @@ -51,15 +51,15 @@ CREATE TABLE activation_attempts ( attempted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, client_ip VARCHAR(45), notes TEXT, - FOREIGN KEY (key_id) REFERENCES oem_keys(id), - FOREIGN KEY (technician_id) REFERENCES technicians(technician_id), + FOREIGN KEY (key_id) REFERENCES `#__oem_keys`(id), + FOREIGN KEY (technician_id) REFERENCES `#__technicians`(technician_id), INDEX idx_order_number (order_number), INDEX idx_technician_id (technician_id), INDEX idx_attempted_at (attempted_at) ); -- Table to store active sessions/tokens -CREATE TABLE active_sessions ( +CREATE TABLE `#__active_sessions` ( id INT AUTO_INCREMENT PRIMARY KEY, technician_id VARCHAR(20) NOT NULL, session_token VARCHAR(64) NOT NULL UNIQUE, @@ -68,15 +68,15 @@ CREATE TABLE active_sessions ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP NOT NULL, is_active BOOLEAN DEFAULT TRUE, - FOREIGN KEY (key_id) REFERENCES oem_keys(id), - FOREIGN KEY (technician_id) REFERENCES technicians(technician_id), + FOREIGN KEY (key_id) REFERENCES `#__oem_keys`(id), + FOREIGN KEY (technician_id) REFERENCES `#__technicians`(technician_id), INDEX idx_session_token (session_token), INDEX idx_technician_id (technician_id), INDEX idx_expires_at (expires_at) ); -- Table for system configuration -CREATE TABLE system_config ( +CREATE TABLE `#__system_config` ( config_key VARCHAR(50) PRIMARY KEY, config_value TEXT NOT NULL, description TEXT, @@ -84,20 +84,20 @@ CREATE TABLE system_config ( ); -- Table for password reset tokens -CREATE TABLE password_reset_tokens ( +CREATE TABLE `#__password_reset_tokens` ( id INT AUTO_INCREMENT PRIMARY KEY, technician_id VARCHAR(20) NOT NULL, reset_token VARCHAR(64) NOT NULL UNIQUE, expires_at TIMESTAMP NOT NULL, used_at TIMESTAMP NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (technician_id) REFERENCES technicians(technician_id), + FOREIGN KEY (technician_id) REFERENCES `#__technicians`(technician_id), INDEX idx_reset_token (reset_token), INDEX idx_expires_at (expires_at) ); -- Insert basic configuration -INSERT INTO system_config (config_key, config_value, description) VALUES +INSERT INTO `#__system_config` (config_key, config_value, description) VALUES ('smtp_server', 'smtp.zoho.com', 'SMTP server for notifications'), ('smtp_port', '587', 'SMTP port'), ('smtp_username', 'oem.activation@roo24.chesnotech.ru', 'SMTP username'), @@ -115,13 +115,13 @@ INSERT INTO system_config (config_key, config_value, description) VALUES ('show_full_keys_in_admin', '0', 'Show full product keys in admin panel (1=yes, 0=no - admin only)'); -- Create default admin account (password: admin123 - CHANGE THIS!) -INSERT INTO technicians (technician_id, full_name, email, password_hash, must_change_password, created_by, notes) +INSERT INTO `#__technicians` (technician_id, full_name, email, password_hash, must_change_password, created_by, notes) VALUES ('admin', 'System Administrator', 'admin@yourcompany.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', TRUE, 'system', 'Default admin account - change password immediately'); -- Create sample technician account (password: temp123) -INSERT INTO technicians (technician_id, full_name, email, password_hash, temp_password, must_change_password, created_by, notes) +INSERT INTO `#__technicians` (technician_id, full_name, email, password_hash, temp_password, must_change_password, created_by, notes) VALUES ('tech001', 'John Technician', 'tech001@yourcompany.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'temp123', TRUE, 'admin', 'Sample technician account'); \ No newline at end of file diff --git a/FINAL_PRODUCTION_SYSTEM/database/docker-init/00-init.sh b/FINAL_PRODUCTION_SYSTEM/database/docker-init/00-init.sh index bac8e58..fb6bcc1 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/docker-init/00-init.sh +++ b/FINAL_PRODUCTION_SYSTEM/database/docker-init/00-init.sh @@ -15,16 +15,45 @@ DB="${MARIADB_DATABASE:-oem_activation}" SQL_DIR="/docker-entrypoint-initdb.d/sql" MYSQL_CMD="mysql -u root -p${MARIADB_ROOT_PASSWORD} ${DB}" +# ── Table prefix support ───────────────────────────────────── +# SQL files use the Joomla-style sentinel `#__` for table names. +# At init time we substitute it with $KEYGATE_DB_PREFIX (default: empty +# string → identical schema to pre-prefix releases). +PREFIX="${KEYGATE_DB_PREFIX:-}" +SV_TABLE="${PREFIX}schema_versions" + +# Validate prefix: must match ^[a-z][a-z0-9_]{0,9}$ or be empty. +if [ -n "$PREFIX" ]; then + if ! echo "$PREFIX" | grep -qE '^[a-z][a-z0-9_]{0,9}$'; then + echo "[ERROR] Invalid KEYGATE_DB_PREFIX='$PREFIX'. Must match ^[a-z][a-z0-9_]{0,9}\$ or be empty." + exit 1 + fi +fi + echo "=== KeyGate: Database Initialization ===" echo "Database: $DB" echo "SQL directory: $SQL_DIR" +echo "Table prefix: ${PREFIX:-}" + +# ── Substitute #__ → $PREFIX for every .sql file in $SQL_DIR ── +# We copy to /tmp/keygate-sql/ so the original mounted volume stays +# untouched (read-only on some setups). +STAGING="/tmp/keygate-sql" +mkdir -p "$STAGING" +for f in "$SQL_DIR"/*.sql; do + [ -f "$f" ] || continue + base="$(basename "$f")" + # sed -i won't work on read-only mounts; pipe through to a fresh file. + sed "s/#__/${PREFIX}/g" "$f" > "$STAGING/$base" +done +SQL_DIR="$STAGING" # ── Step 0: Ensure schema_versions table exists ────────────── # This must run unconditionally so the tracking table is always present. $MYSQL_CMD < "$SQL_DIR/schema_versions_migration.sql" 2>/dev/null || true # ── Idempotent migration runner ────────────────────────────── -# Checks schema_versions before running; records after success. +# Checks ${PREFIX}schema_versions before running; records after success. run_sql() { local file="$SQL_DIR/$1" local version="$2" @@ -36,7 +65,7 @@ run_sql() { # Check if already applied local applied - applied=$($MYSQL_CMD -N -e "SELECT COUNT(*) FROM schema_versions WHERE filename = '$1'" 2>/dev/null || echo "0") + applied=$($MYSQL_CMD -N -e "SELECT COUNT(*) FROM \`${SV_TABLE}\` WHERE filename = '$1'" 2>/dev/null || echo "0") if [ "$applied" -gt 0 ]; then echo "[SKIP] Already applied: $1" @@ -48,11 +77,18 @@ run_sql() { echo "[WARN] Non-fatal errors in $1 (continuing)" fi - # Compute checksum and record + # Compute checksum from the original (un-substituted) file so the + # checksum is stable regardless of prefix choice. Falls back to staged + # copy if the original isn't accessible. + local original="/docker-entrypoint-initdb.d/sql/$1" local checksum - checksum=$(sha256sum "$file" | cut -d' ' -f1) + if [ -f "$original" ]; then + checksum=$(sha256sum "$original" | cut -d' ' -f1) + else + checksum=$(sha256sum "$file" | cut -d' ' -f1) + fi - $MYSQL_CMD -e "INSERT INTO schema_versions (version, filename, checksum) VALUES ($version, '$1', '$checksum')" + $MYSQL_CMD -e "INSERT INTO \`${SV_TABLE}\` (version, filename, checksum) VALUES ($version, '$1', '$checksum')" echo "[DONE] Applied: $1" } diff --git a/FINAL_PRODUCTION_SYSTEM/database/downloads_acl_migration.sql b/FINAL_PRODUCTION_SYSTEM/database/downloads_acl_migration.sql index d5da7bd..e66a039 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/downloads_acl_migration.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/downloads_acl_migration.sql @@ -5,33 +5,33 @@ -- ============================================================ -- Add "Downloads" permission category -INSERT INTO acl_permission_categories (category_key, display_name, icon, sort_order) +INSERT INTO `#__acl_permission_categories` (category_key, display_name, icon, sort_order) VALUES ('downloads', 'Client Downloads', NULL, 55) ON DUPLICATE KEY UPDATE display_name = VALUES(display_name); -- Add download permissions -INSERT INTO acl_permissions (permission_key, display_name, description, category_id, resource_type, action_type, is_dangerous) +INSERT INTO `#__acl_permissions` (permission_key, display_name, description, category_id, resource_type, action_type, is_dangerous) SELECT 'view_downloads', 'View Downloads', 'View and download client tools (launcher, PS7 installer, extensions)', c.id, 'downloads', 'view', 0 -FROM acl_permission_categories c WHERE c.category_key = 'downloads' +FROM `#__acl_permission_categories` c WHERE c.category_key = 'downloads' ON DUPLICATE KEY UPDATE display_name = VALUES(display_name); -INSERT INTO acl_permissions (permission_key, display_name, description, category_id, resource_type, action_type, is_dangerous) +INSERT INTO `#__acl_permissions` (permission_key, display_name, description, category_id, resource_type, action_type, is_dangerous) SELECT 'manage_downloads', 'Manage Downloads', 'Upload, replace, and delete client resources', c.id, 'downloads', 'manage', 1 -FROM acl_permission_categories c WHERE c.category_key = 'downloads' +FROM `#__acl_permission_categories` c WHERE c.category_key = 'downloads' ON DUPLICATE KEY UPDATE display_name = VALUES(display_name); -- Grant both permissions to super_admin role INSERT IGNORE INTO acl_role_permissions (role_id, permission_id) SELECT r.id, p.id -FROM acl_roles r CROSS JOIN acl_permissions p +FROM `#__acl_roles` r CROSS JOIN `#__acl_permissions` p WHERE r.role_name = 'super_admin' AND p.permission_key IN ('view_downloads', 'manage_downloads'); -- Grant view_downloads to admin role INSERT IGNORE INTO acl_role_permissions (role_id, permission_id) SELECT r.id, p.id -FROM acl_roles r, acl_permissions p +FROM `#__acl_roles` r, acl_permissions p WHERE r.role_name = 'admin' AND p.permission_key = 'view_downloads'; diff --git a/FINAL_PRODUCTION_SYSTEM/database/hardware_info_migration.sql b/FINAL_PRODUCTION_SYSTEM/database/hardware_info_migration.sql index cee3e96..8dde0c3 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/hardware_info_migration.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/hardware_info_migration.sql @@ -3,12 +3,12 @@ -- Purpose: Make OEM ID and Roll Serial optional, add hardware tracking -- Step 1: Make OEM ID and Roll Serial optional in oem_keys table -ALTER TABLE oem_keys +ALTER TABLE `#__oem_keys` MODIFY COLUMN oem_identifier VARCHAR(20) NULL DEFAULT NULL, MODIFY COLUMN roll_serial VARCHAR(20) NULL DEFAULT NULL; -- Step 2: Create hardware_info table for tracking PC hardware details -CREATE TABLE IF NOT EXISTS hardware_info ( +CREATE TABLE IF NOT EXISTS `#__hardware_info` ( id INT AUTO_INCREMENT PRIMARY KEY, activation_id INT NOT NULL COMMENT 'Links to activation_attempts.id', order_number VARCHAR(10) NOT NULL COMMENT 'Order number for easy reference', @@ -60,11 +60,11 @@ CREATE TABLE IF NOT EXISTS hardware_info ( INDEX idx_order_number (order_number), INDEX idx_collected_at (collected_at), - FOREIGN KEY (activation_id) REFERENCES activation_attempts(id) ON DELETE CASCADE + FOREIGN KEY (activation_id) REFERENCES `#__activation_attempts`(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Hardware information collected during activation'; -- Step 3: Add hardware_collected flag to activation_attempts -ALTER TABLE activation_attempts +ALTER TABLE `#__activation_attempts` ADD COLUMN hardware_collected TINYINT(1) DEFAULT 0 COMMENT 'Whether hardware info was collected for this activation'; -- Step 4: Create view for easy hardware lookup by order number @@ -86,12 +86,12 @@ SELECT h.ram_total_capacity_gb, h.secure_boot_enabled, h.collected_at AS hardware_collected_at -FROM activation_attempts a -LEFT JOIN technicians t ON a.technician_id = t.technician_id -LEFT JOIN oem_keys k ON a.key_id = k.id -LEFT JOIN hardware_info h ON h.activation_id = a.id +FROM `#__activation_attempts` a +LEFT JOIN `#__technicians` t ON a.technician_id = t.technician_id +LEFT JOIN `#__oem_keys` k ON a.key_id = k.id +LEFT JOIN `#__hardware_info` h ON h.activation_id = a.id ORDER BY a.attempted_at DESC; -- Step 5: (Optional) Add secure_boot_enabled column if table already exists without it -- Run this only if you applied the migration before this column was added: --- ALTER TABLE hardware_info ADD COLUMN secure_boot_enabled TINYINT(1) NULL COMMENT 'Whether Secure Boot is enabled (1=yes, 0=no, NULL=unknown)' AFTER os_architecture; +-- ALTER TABLE `#__hardware_info` ADD COLUMN secure_boot_enabled TINYINT(1) NULL COMMENT 'Whether Secure Boot is enabled (1=yes, 0=no, NULL=unknown)' AFTER os_architecture; diff --git a/FINAL_PRODUCTION_SYSTEM/database/hardware_info_v2_migration.sql b/FINAL_PRODUCTION_SYSTEM/database/hardware_info_v2_migration.sql index ae57497..aa65dbc 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/hardware_info_v2_migration.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/hardware_info_v2_migration.sql @@ -5,48 +5,48 @@ -- Add new columns to hardware_info table -- Chassis / Enclosure Information -ALTER TABLE hardware_info ADD COLUMN chassis_manufacturer VARCHAR(100) NULL AFTER computer_name; -ALTER TABLE hardware_info ADD COLUMN chassis_serial VARCHAR(100) NULL AFTER chassis_manufacturer; -ALTER TABLE hardware_info ADD COLUMN chassis_type VARCHAR(50) NULL AFTER chassis_serial; +ALTER TABLE `#__hardware_info` ADD COLUMN chassis_manufacturer VARCHAR(100) NULL AFTER computer_name; +ALTER TABLE `#__hardware_info` ADD COLUMN chassis_serial VARCHAR(100) NULL AFTER chassis_manufacturer; +ALTER TABLE `#__hardware_info` ADD COLUMN chassis_type VARCHAR(50) NULL AFTER chassis_serial; -- System Product Information (OEM) -ALTER TABLE hardware_info ADD COLUMN system_manufacturer VARCHAR(100) NULL AFTER chassis_type; -ALTER TABLE hardware_info ADD COLUMN system_product_name VARCHAR(200) NULL AFTER system_manufacturer; -ALTER TABLE hardware_info ADD COLUMN system_serial VARCHAR(100) NULL AFTER system_product_name; -ALTER TABLE hardware_info ADD COLUMN system_uuid VARCHAR(50) NULL AFTER system_serial; +ALTER TABLE `#__hardware_info` ADD COLUMN system_manufacturer VARCHAR(100) NULL AFTER chassis_type; +ALTER TABLE `#__hardware_info` ADD COLUMN system_product_name VARCHAR(200) NULL AFTER system_manufacturer; +ALTER TABLE `#__hardware_info` ADD COLUMN system_serial VARCHAR(100) NULL AFTER system_product_name; +ALTER TABLE `#__hardware_info` ADD COLUMN system_uuid VARCHAR(50) NULL AFTER system_serial; -- TPM Information -ALTER TABLE hardware_info ADD COLUMN tpm_present TINYINT(1) NULL AFTER system_uuid; -ALTER TABLE hardware_info ADD COLUMN tpm_version VARCHAR(50) NULL AFTER tpm_present; -ALTER TABLE hardware_info ADD COLUMN tpm_manufacturer VARCHAR(100) NULL AFTER tpm_version; +ALTER TABLE `#__hardware_info` ADD COLUMN tpm_present TINYINT(1) NULL AFTER system_uuid; +ALTER TABLE `#__hardware_info` ADD COLUMN tpm_version VARCHAR(50) NULL AFTER tpm_present; +ALTER TABLE `#__hardware_info` ADD COLUMN tpm_manufacturer VARCHAR(100) NULL AFTER tpm_version; -- CPU Serial (Processor ID) -ALTER TABLE hardware_info ADD COLUMN cpu_serial VARCHAR(50) NULL AFTER cpu_max_clock_speed; +ALTER TABLE `#__hardware_info` ADD COLUMN cpu_serial VARCHAR(50) NULL AFTER cpu_max_clock_speed; -- Network Information -ALTER TABLE hardware_info ADD COLUMN primary_mac_address VARCHAR(20) NULL AFTER computer_name; -ALTER TABLE hardware_info ADD COLUMN local_ip VARCHAR(45) NULL AFTER primary_mac_address; -ALTER TABLE hardware_info ADD COLUMN public_ip VARCHAR(45) NULL AFTER local_ip; -ALTER TABLE hardware_info ADD COLUMN network_adapters JSON NULL COMMENT 'Array of network adapter details with MAC, IP, etc.' AFTER public_ip; +ALTER TABLE `#__hardware_info` ADD COLUMN primary_mac_address VARCHAR(20) NULL AFTER computer_name; +ALTER TABLE `#__hardware_info` ADD COLUMN local_ip VARCHAR(45) NULL AFTER primary_mac_address; +ALTER TABLE `#__hardware_info` ADD COLUMN public_ip VARCHAR(45) NULL AFTER local_ip; +ALTER TABLE `#__hardware_info` ADD COLUMN network_adapters JSON NULL COMMENT 'Array of network adapter details with MAC, IP, etc.' AFTER public_ip; -- Audio Devices -ALTER TABLE hardware_info ADD COLUMN audio_devices JSON NULL COMMENT 'Array of sound device details' AFTER network_adapters; +ALTER TABLE `#__hardware_info` ADD COLUMN audio_devices JSON NULL COMMENT 'Array of sound device details' AFTER network_adapters; -- Monitor Information -ALTER TABLE hardware_info ADD COLUMN monitors JSON NULL COMMENT 'Array of connected monitor details with serials' AFTER audio_devices; +ALTER TABLE `#__hardware_info` ADD COLUMN monitors JSON NULL COMMENT 'Array of connected monitor details with serials' AFTER audio_devices; -- OS Extended Info -ALTER TABLE hardware_info ADD COLUMN os_build_number VARCHAR(20) NULL AFTER os_architecture; -ALTER TABLE hardware_info ADD COLUMN os_install_date VARCHAR(50) NULL AFTER os_build_number; -ALTER TABLE hardware_info ADD COLUMN os_serial_number VARCHAR(100) NULL AFTER os_install_date; +ALTER TABLE `#__hardware_info` ADD COLUMN os_build_number VARCHAR(20) NULL AFTER os_architecture; +ALTER TABLE `#__hardware_info` ADD COLUMN os_install_date VARCHAR(50) NULL AFTER os_build_number; +ALTER TABLE `#__hardware_info` ADD COLUMN os_serial_number VARCHAR(100) NULL AFTER os_install_date; -- Device Fingerprint (composite unique identifier) -ALTER TABLE hardware_info ADD COLUMN device_fingerprint VARCHAR(500) NULL COMMENT 'Composite hardware fingerprint for duplicate detection' AFTER collection_method; +ALTER TABLE `#__hardware_info` ADD COLUMN device_fingerprint VARCHAR(500) NULL COMMENT 'Composite hardware fingerprint for duplicate detection' AFTER collection_method; -- Index for device fingerprint lookups -ALTER TABLE hardware_info ADD INDEX idx_device_fingerprint (device_fingerprint(255)); -ALTER TABLE hardware_info ADD INDEX idx_public_ip (public_ip); -ALTER TABLE hardware_info ADD INDEX idx_primary_mac (primary_mac_address); +ALTER TABLE `#__hardware_info` ADD INDEX idx_device_fingerprint (device_fingerprint(255)); +ALTER TABLE `#__hardware_info` ADD INDEX idx_public_ip (public_ip); +ALTER TABLE `#__hardware_info` ADD INDEX idx_primary_mac (primary_mac_address); -- Update the view to include new network fields CREATE OR REPLACE VIEW v_activation_hardware AS @@ -73,8 +73,8 @@ SELECT h.system_uuid, h.device_fingerprint, h.collected_at AS hardware_collected_at -FROM activation_attempts a -LEFT JOIN technicians t ON a.technician_id = t.technician_id -LEFT JOIN oem_keys k ON a.key_id = k.id -LEFT JOIN hardware_info h ON h.activation_id = a.id +FROM `#__activation_attempts` a +LEFT JOIN `#__technicians` t ON a.technician_id = t.technician_id +LEFT JOIN `#__oem_keys` k ON a.key_id = k.id +LEFT JOIN `#__hardware_info` h ON h.activation_id = a.id ORDER BY a.attempted_at DESC; diff --git a/FINAL_PRODUCTION_SYSTEM/database/hash_temp_passwords.php b/FINAL_PRODUCTION_SYSTEM/database/hash_temp_passwords.php index fb7e21d..322e7f5 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/hash_temp_passwords.php +++ b/FINAL_PRODUCTION_SYSTEM/database/hash_temp_passwords.php @@ -8,7 +8,7 @@ echo "Hashing plaintext temp_passwords...\n"; -$stmt = $pdo->query("SELECT id, temp_password FROM technicians WHERE temp_password IS NOT NULL AND temp_password != ''"); +$stmt = $pdo->query("SELECT id, temp_password FROM `" . t('technicians') . "` WHERE temp_password IS NOT NULL AND temp_password != ''"); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $updated = 0; @@ -20,7 +20,7 @@ } $hashed = password_hash($row['temp_password'], PASSWORD_BCRYPT, ['cost' => BCRYPT_COST]); - $update = $pdo->prepare("UPDATE technicians SET temp_password = ? WHERE id = ?"); + $update = $pdo->prepare("UPDATE `" . t('technicians') . "` SET temp_password = ? WHERE id = ?"); $update->execute([$hashed, $row['id']]); $updated++; echo " Hashed temp_password for ID {$row['id']}\n"; diff --git a/FINAL_PRODUCTION_SYSTEM/database/i18n_migration.sql b/FINAL_PRODUCTION_SYSTEM/database/i18n_migration.sql index 1d3f112..a92be36 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/i18n_migration.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/i18n_migration.sql @@ -2,12 +2,12 @@ -- Run this migration to enable per-user language selection -- Add preferred_language column to admin_users -ALTER TABLE admin_users ADD COLUMN preferred_language VARCHAR(5) DEFAULT 'en' AFTER role; +ALTER TABLE `#__admin_users` ADD COLUMN preferred_language VARCHAR(5) DEFAULT 'en' AFTER role; -- Add preferred_language column to technicians -ALTER TABLE technicians ADD COLUMN preferred_language VARCHAR(5) DEFAULT 'en' AFTER notes; +ALTER TABLE `#__technicians` ADD COLUMN preferred_language VARCHAR(5) DEFAULT 'en' AFTER notes; -- Add system default language setting -INSERT INTO system_config (config_key, config_value, description) +INSERT INTO `#__system_config` (config_key, config_value, description) VALUES ('default_language', 'en', 'Default system language (en = English, ru = Russian)') ON DUPLICATE KEY UPDATE config_value = config_value; diff --git a/FINAL_PRODUCTION_SYSTEM/database/install.sql b/FINAL_PRODUCTION_SYSTEM/database/install.sql index 0441b3a..88db432 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/install.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/install.sql @@ -8,7 +8,7 @@ START TRANSACTION; SET time_zone = "+00:00"; -- Create technicians table -CREATE TABLE `technicians` ( +CREATE TABLE `#__technicians` ( `id` int(11) NOT NULL AUTO_INCREMENT, `technician_id` varchar(20) NOT NULL, `full_name` varchar(100) NOT NULL, @@ -31,7 +31,7 @@ CREATE TABLE `technicians` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Create oem_keys table -CREATE TABLE `oem_keys` ( +CREATE TABLE `#__oem_keys` ( `id` int(11) NOT NULL AUTO_INCREMENT, `product_key` varchar(29) NOT NULL, `oem_identifier` varchar(20) NOT NULL, @@ -56,7 +56,7 @@ CREATE TABLE `oem_keys` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Create activation_attempts table -CREATE TABLE `activation_attempts` ( +CREATE TABLE `#__activation_attempts` ( `id` int(11) NOT NULL AUTO_INCREMENT, `key_id` int(11) NOT NULL, `technician_id` varchar(20) NOT NULL, @@ -76,12 +76,12 @@ CREATE TABLE `activation_attempts` ( KEY `idx_attempted_at` (`attempted_at`), KEY `idx_attempted_date` (`attempted_date`), KEY `idx_key_tech_date` (`key_id`, `technician_id`, `attempted_at`), - CONSTRAINT `activation_attempts_ibfk_1` FOREIGN KEY (`key_id`) REFERENCES `oem_keys` (`id`), - CONSTRAINT `activation_attempts_ibfk_2` FOREIGN KEY (`technician_id`) REFERENCES `technicians` (`technician_id`) + CONSTRAINT `activation_attempts_ibfk_1` FOREIGN KEY (`key_id`) REFERENCES `#__oem_keys` (`id`), + CONSTRAINT `activation_attempts_ibfk_2` FOREIGN KEY (`technician_id`) REFERENCES `#__technicians` (`technician_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Create active_sessions table -CREATE TABLE `active_sessions` ( +CREATE TABLE `#__active_sessions` ( `id` int(11) NOT NULL AUTO_INCREMENT, `technician_id` varchar(20) NOT NULL, `session_token` varchar(64) NOT NULL, @@ -99,12 +99,12 @@ CREATE TABLE `active_sessions` ( KEY `idx_expires_at` (`expires_at`), KEY `idx_tech_active_expires` (`technician_id`, `is_active`, `expires_at`), KEY `idx_expires_active` (`expires_at`, `is_active`), - CONSTRAINT `active_sessions_ibfk_1` FOREIGN KEY (`key_id`) REFERENCES `oem_keys` (`id`), - CONSTRAINT `active_sessions_ibfk_2` FOREIGN KEY (`technician_id`) REFERENCES `technicians` (`technician_id`) + CONSTRAINT `active_sessions_ibfk_1` FOREIGN KEY (`key_id`) REFERENCES `#__oem_keys` (`id`), + CONSTRAINT `active_sessions_ibfk_2` FOREIGN KEY (`technician_id`) REFERENCES `#__technicians` (`technician_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Create admin_users table -CREATE TABLE `admin_users` ( +CREATE TABLE `#__admin_users` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `full_name` varchar(100) NOT NULL, @@ -125,11 +125,11 @@ CREATE TABLE `admin_users` ( KEY `created_by` (`created_by`), KEY `idx_username` (`username`), KEY `idx_is_active` (`is_active`), - CONSTRAINT `admin_users_ibfk_1` FOREIGN KEY (`created_by`) REFERENCES `admin_users` (`id`) + CONSTRAINT `admin_users_ibfk_1` FOREIGN KEY (`created_by`) REFERENCES `#__admin_users` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Create admin_sessions table -CREATE TABLE `admin_sessions` ( +CREATE TABLE `#__admin_sessions` ( `id` int(11) NOT NULL AUTO_INCREMENT, `admin_id` int(11) NOT NULL, `session_token` varchar(64) NOT NULL, @@ -145,11 +145,11 @@ CREATE TABLE `admin_sessions` ( KEY `idx_session_token` (`session_token`), KEY `idx_admin_id` (`admin_id`), KEY `idx_expires_at` (`expires_at`), - CONSTRAINT `admin_sessions_ibfk_1` FOREIGN KEY (`admin_id`) REFERENCES `admin_users` (`id`) + CONSTRAINT `admin_sessions_ibfk_1` FOREIGN KEY (`admin_id`) REFERENCES `#__admin_users` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Create admin_activity_log table -CREATE TABLE `admin_activity_log` ( +CREATE TABLE `#__admin_activity_log` ( `id` int(11) NOT NULL AUTO_INCREMENT, `admin_id` int(11) DEFAULT NULL, `session_id` int(11) DEFAULT NULL, @@ -164,12 +164,12 @@ CREATE TABLE `admin_activity_log` ( KEY `idx_admin_id` (`admin_id`), KEY `idx_created_at` (`created_at`), KEY `idx_action` (`action`), - CONSTRAINT `admin_activity_log_ibfk_1` FOREIGN KEY (`admin_id`) REFERENCES `admin_users` (`id`), - CONSTRAINT `admin_activity_log_ibfk_2` FOREIGN KEY (`session_id`) REFERENCES `admin_sessions` (`id`) + CONSTRAINT `admin_activity_log_ibfk_1` FOREIGN KEY (`admin_id`) REFERENCES `#__admin_users` (`id`), + CONSTRAINT `admin_activity_log_ibfk_2` FOREIGN KEY (`session_id`) REFERENCES `#__admin_sessions` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Create admin_ip_whitelist table -CREATE TABLE `admin_ip_whitelist` ( +CREATE TABLE `#__admin_ip_whitelist` ( `id` int(11) NOT NULL AUTO_INCREMENT, `ip_address` varchar(45) NOT NULL, `ip_range` varchar(45) DEFAULT NULL, @@ -181,11 +181,11 @@ CREATE TABLE `admin_ip_whitelist` ( KEY `created_by` (`created_by`), KEY `idx_ip_address` (`ip_address`), KEY `idx_is_active` (`is_active`), - CONSTRAINT `admin_ip_whitelist_ibfk_1` FOREIGN KEY (`created_by`) REFERENCES `admin_users` (`id`) + CONSTRAINT `admin_ip_whitelist_ibfk_1` FOREIGN KEY (`created_by`) REFERENCES `#__admin_users` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Create system_config table -CREATE TABLE `system_config` ( +CREATE TABLE `#__system_config` ( `config_key` varchar(50) NOT NULL, `config_value` text NOT NULL, `description` text DEFAULT NULL, @@ -194,7 +194,7 @@ CREATE TABLE `system_config` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Create password_reset_tokens table -CREATE TABLE `password_reset_tokens` ( +CREATE TABLE `#__password_reset_tokens` ( `id` int(11) NOT NULL AUTO_INCREMENT, `technician_id` varchar(20) NOT NULL, `reset_token` varchar(64) NOT NULL, @@ -206,11 +206,11 @@ CREATE TABLE `password_reset_tokens` ( KEY `technician_id` (`technician_id`), KEY `idx_reset_token` (`reset_token`), KEY `idx_expires_at` (`expires_at`), - CONSTRAINT `password_reset_tokens_ibfk_1` FOREIGN KEY (`technician_id`) REFERENCES `technicians` (`technician_id`) + CONSTRAINT `password_reset_tokens_ibfk_1` FOREIGN KEY (`technician_id`) REFERENCES `#__technicians` (`technician_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Insert default system configuration -INSERT INTO `system_config` (`config_key`, `config_value`, `description`) VALUES +INSERT INTO `#__system_config` (`config_key`, `config_value`, `description`) VALUES ('smtp_server', 'smtp.zoho.com', 'SMTP server for notifications'), ('smtp_port', '587', 'SMTP port'), ('smtp_username', '', 'SMTP username'), @@ -236,7 +236,7 @@ INSERT INTO `system_config` (`config_key`, `config_value`, `description`) VALUES ('admin_log_retention_days', '365', 'Keep admin activity logs for N days'); -- Create sample technician account (for testing) -INSERT INTO `technicians` (`technician_id`, `full_name`, `email`, `password_hash`, `temp_password`, `must_change_password`, `created_by`, `notes`) VALUES +INSERT INTO `#__technicians` (`technician_id`, `full_name`, `email`, `password_hash`, `temp_password`, `must_change_password`, `created_by`, `notes`) VALUES ('demo', 'Demo Technician', 'demo@example.com', '$2y$12$LQv3c1yqBwlVHpPd7u/Dw.G2K2wjDUl9jhJxfTULt3lOAOWuTDBKG', 'demo123', 1, 'system', 'Demo account for testing - Password: demo123'); COMMIT; \ No newline at end of file diff --git a/FINAL_PRODUCTION_SYSTEM/database/integrations_migration.sql b/FINAL_PRODUCTION_SYSTEM/database/integrations_migration.sql index 4fa2bf3..f60e111 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/integrations_migration.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/integrations_migration.sql @@ -3,7 +3,7 @@ -- Supports osTicket, 1C ERP, and future integrations -- ============================================================= -CREATE TABLE IF NOT EXISTS `integrations` ( +CREATE TABLE IF NOT EXISTS `#__integrations` ( `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `integration_key` VARCHAR(50) NOT NULL UNIQUE, `display_name` VARCHAR(100) NOT NULL, @@ -18,7 +18,7 @@ CREATE TABLE IF NOT EXISTS `integrations` ( `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -CREATE TABLE IF NOT EXISTS `integration_events` ( +CREATE TABLE IF NOT EXISTS `#__integration_events` ( `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `integration_id` INT UNSIGNED NOT NULL, `event_type` VARCHAR(50) NOT NULL, @@ -29,14 +29,14 @@ CREATE TABLE IF NOT EXISTS `integration_events` ( `error_message` TEXT, `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `processed_at` TIMESTAMP NULL DEFAULT NULL, - FOREIGN KEY (`integration_id`) REFERENCES `integrations`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`integration_id`) REFERENCES `#__integrations`(`id`) ON DELETE CASCADE, INDEX `idx_ie_status` (`status`), INDEX `idx_ie_event_type` (`event_type`), INDEX `idx_ie_created` (`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Seed default integrations -INSERT INTO `integrations` (`integration_key`, `display_name`, `description`, `integration_type`, `enabled`, `config`) VALUES +INSERT INTO `#__integrations` (`integration_key`, `display_name`, `description`, `integration_type`, `enabled`, `config`) VALUES ('osticket', 'osTicket', 'Track PC build orders as support tickets in osTicket', 'api_sync', 0, JSON_OBJECT( 'base_url', '', diff --git a/FINAL_PRODUCTION_SYSTEM/database/license_migration.sql b/FINAL_PRODUCTION_SYSTEM/database/license_migration.sql index 49e8cb4..8dbc5dd 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/license_migration.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/license_migration.sql @@ -4,7 +4,7 @@ -- Tracks instance license, tier limits, and validation history. -- ============================================================= -CREATE TABLE IF NOT EXISTS license_info ( +CREATE TABLE IF NOT EXISTS `#__license_info` ( id INT AUTO_INCREMENT PRIMARY KEY, license_key VARCHAR(2048) NOT NULL COMMENT 'JWT license token', instance_id VARCHAR(128) NOT NULL COMMENT 'SHA256 fingerprint of this installation', @@ -26,10 +26,10 @@ CREATE TABLE IF NOT EXISTS license_info ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Default community license limits stored in system_config -INSERT INTO system_config (config_key, config_value, description) VALUES +INSERT INTO `#__system_config` (config_key, config_value, description) VALUES ('license_tier', 'community', 'Current license tier') ON DUPLICATE KEY UPDATE config_key = config_key; -INSERT INTO system_config (config_key, config_value, description) VALUES +INSERT INTO `#__system_config` (config_key, config_value, description) VALUES ('license_instance_id', '', 'Unique instance fingerprint (auto-generated)') ON DUPLICATE KEY UPDATE config_key = config_key; diff --git a/FINAL_PRODUCTION_SYSTEM/database/missing_drivers_migration.sql b/FINAL_PRODUCTION_SYSTEM/database/missing_drivers_migration.sql index 0cdd5a0..61a4e83 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/missing_drivers_migration.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/missing_drivers_migration.sql @@ -2,21 +2,21 @@ -- Adds driver status tracking and per-check enforcement to product lines -- 1. Add driver columns to hardware_info -ALTER TABLE hardware_info +ALTER TABLE `#__hardware_info` ADD COLUMN IF NOT EXISTS missing_drivers JSON NULL COMMENT 'Array of devices with missing/problematic drivers', ADD COLUMN IF NOT EXISTS missing_drivers_count INT NULL DEFAULT NULL COMMENT 'Number of missing/error drivers detected'; CREATE INDEX IF NOT EXISTS idx_hw_missing_drivers_count ON hardware_info (missing_drivers_count); -- 2. Add enforcement columns to QC tables -ALTER TABLE qc_motherboard_registry +ALTER TABLE `#__qc_motherboard_registry` ADD COLUMN IF NOT EXISTS missing_drivers_enforcement TINYINT(1) NULL COMMENT 'Override: 0=disabled, 1=info, 2=warning, 3=blocking'; -ALTER TABLE qc_manufacturer_defaults +ALTER TABLE `#__qc_manufacturer_defaults` ADD COLUMN IF NOT EXISTS missing_drivers_enforcement TINYINT(1) DEFAULT NULL COMMENT 'Override: 0=disabled, 1=info, 2=warning, 3=blocking'; -- 3. Add per-check enforcement columns to product_lines -ALTER TABLE product_lines +ALTER TABLE `#__product_lines` ADD COLUMN IF NOT EXISTS secure_boot_enforcement TINYINT(1) DEFAULT NULL, ADD COLUMN IF NOT EXISTS bios_enforcement TINYINT(1) DEFAULT NULL, ADD COLUMN IF NOT EXISTS hackbgrt_enforcement TINYINT(1) DEFAULT NULL, @@ -31,5 +31,5 @@ INSERT IGNORE INTO qc_global_settings (setting_key, setting_value, description) VALUES ('default_partition_enforcement', '2', 'Default enforcement for partition layout check (0=disabled, 1=info, 2=warning, 3=blocking)'); -- 5. Widen check_type from ENUM to VARCHAR for extensibility -ALTER TABLE qc_compliance_results +ALTER TABLE `#__qc_compliance_results` MODIFY COLUMN check_type VARCHAR(50) NOT NULL COMMENT 'Check type identifier'; diff --git a/FINAL_PRODUCTION_SYSTEM/database/order_field_config_migration.sql b/FINAL_PRODUCTION_SYSTEM/database/order_field_config_migration.sql index 4d7a821..1d218e1 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/order_field_config_migration.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/order_field_config_migration.sql @@ -5,19 +5,19 @@ -- 1. Widen order_number columns from VARCHAR(10) to VARCHAR(50) -- Preserve original NULL/NOT NULL constraints per table -ALTER TABLE activation_attempts MODIFY order_number VARCHAR(50) NOT NULL; -ALTER TABLE active_sessions MODIFY order_number VARCHAR(50) NULL; -ALTER TABLE hardware_info MODIFY order_number VARCHAR(50) NOT NULL; -ALTER TABLE qc_compliance_results MODIFY order_number VARCHAR(50) NOT NULL; +ALTER TABLE `#__activation_attempts` MODIFY order_number VARCHAR(50) NOT NULL; +ALTER TABLE `#__active_sessions` MODIFY order_number VARCHAR(50) NULL; +ALTER TABLE `#__hardware_info` MODIFY order_number VARCHAR(50) NOT NULL; +ALTER TABLE `#__qc_compliance_results` MODIFY order_number VARCHAR(50) NOT NULL; -- hardware_collection_log: only alter if table exists (not present in all installations) SET @tbl_exists = (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'hardware_collection_log'); -SET @sql = IF(@tbl_exists > 0, 'ALTER TABLE hardware_collection_log MODIFY order_number VARCHAR(50) NOT NULL', 'SELECT 1'); +SET @sql = IF(@tbl_exists > 0, 'ALTER TABLE `#__hardware_collection_log` MODIFY order_number VARCHAR(50) NOT NULL', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- 2. Seed system_config with order field configuration defaults -INSERT INTO system_config (config_key, config_value, description, updated_at) +INSERT INTO `#__system_config` (config_key, config_value, description, updated_at) VALUES ('order_field_label_en', 'Order Number', 'Display label for the order field (English)', NOW()), ('order_field_label_ru', 'Номер заказа', 'Display label for the order field (Russian)', NOW()), diff --git a/FINAL_PRODUCTION_SYSTEM/database/product_variants_migration.sql b/FINAL_PRODUCTION_SYSTEM/database/product_variants_migration.sql index da6ec04..42f324b 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/product_variants_migration.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/product_variants_migration.sql @@ -2,7 +2,7 @@ -- Adds partition layout QC checking via product lines → variants → partition templates -- ── Product Lines (linked to order number patterns) ────────────── -CREATE TABLE IF NOT EXISTS product_lines ( +CREATE TABLE IF NOT EXISTS `#__product_lines` ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, order_pattern VARCHAR(50) NOT NULL COMMENT 'Order number prefix to match, e.g. ЭЛ00-', @@ -16,7 +16,7 @@ CREATE TABLE IF NOT EXISTS product_lines ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- ── Product Variants (disk size ranges within a product line) ──── -CREATE TABLE IF NOT EXISTS product_variants ( +CREATE TABLE IF NOT EXISTS `#__product_variants` ( id INT AUTO_INCREMENT PRIMARY KEY, line_id INT NOT NULL, name VARCHAR(100) NOT NULL COMMENT 'e.g. RTX 1TB, RTX 512GB', @@ -25,12 +25,12 @@ CREATE TABLE IF NOT EXISTS product_variants ( is_active TINYINT(1) NOT NULL DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (line_id) REFERENCES product_lines(id) ON DELETE CASCADE, + FOREIGN KEY (line_id) REFERENCES `#__product_lines`(id) ON DELETE CASCADE, UNIQUE KEY uk_line_name (line_id, name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- ── Partition Templates (expected layout per variant) ──────────── -CREATE TABLE IF NOT EXISTS product_variant_partitions ( +CREATE TABLE IF NOT EXISTS `#__product_variant_partitions` ( id INT AUTO_INCREMENT PRIMARY KEY, variant_id INT NOT NULL, partition_order INT NOT NULL COMMENT 'Expected position on disk: 1, 2, 3...', @@ -39,16 +39,16 @@ CREATE TABLE IF NOT EXISTS product_variant_partitions ( expected_size_mb INT NOT NULL, tolerance_percent DECIMAL(5,2) NOT NULL DEFAULT 1.00 COMMENT 'Allowed deviation % per partition', is_flexible TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1 = absorbs remaining space (e.g. Data partition)', - FOREIGN KEY (variant_id) REFERENCES product_variants(id) ON DELETE CASCADE, + FOREIGN KEY (variant_id) REFERENCES `#__product_variants`(id) ON DELETE CASCADE, UNIQUE KEY uk_variant_order (variant_id, partition_order) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- ── Extend QC compliance results to include partition_layout ───── -ALTER TABLE qc_compliance_results +ALTER TABLE `#__qc_compliance_results` MODIFY COLUMN check_type ENUM('bios_version','secure_boot','hackbgrt_boot_priority','partition_layout') NOT NULL; -- ── Add detected variant tracking to hardware_info ─────────────── -ALTER TABLE hardware_info +ALTER TABLE `#__hardware_info` ADD COLUMN IF NOT EXISTS detected_variant_id INT DEFAULT NULL, ADD COLUMN IF NOT EXISTS detected_variant_name VARCHAR(100) DEFAULT NULL, ADD COLUMN IF NOT EXISTS detected_line_name VARCHAR(100) DEFAULT NULL; diff --git a/FINAL_PRODUCTION_SYSTEM/database/production_tracking_migration.sql b/FINAL_PRODUCTION_SYSTEM/database/production_tracking_migration.sql index 8f247aa..8d4da60 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/production_tracking_migration.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/production_tracking_migration.sql @@ -7,7 +7,7 @@ -- ── 1. Computer Build Reports (CBR) ──────────────────────── -- Structured per-machine reports for auditing and compliance. -CREATE TABLE IF NOT EXISTS computer_build_reports ( +CREATE TABLE IF NOT EXISTS `#__computer_build_reports` ( id INT AUTO_INCREMENT PRIMARY KEY, report_uuid VARCHAR(36) NOT NULL UNIQUE COMMENT 'UUID v4 for external reference', activation_attempt_id INT DEFAULT NULL, @@ -80,7 +80,7 @@ CREATE TABLE IF NOT EXISTS computer_build_reports ( -- ── 2. Key Pool Management ───────────────────────────────── -- Alert thresholds and replenishment tracking per product edition. -CREATE TABLE IF NOT EXISTS key_pool_config ( +CREATE TABLE IF NOT EXISTS `#__key_pool_config` ( id INT AUTO_INCREMENT PRIMARY KEY, product_edition VARCHAR(100) NOT NULL UNIQUE COMMENT 'e.g. Windows 11 Pro, Windows 11 Home', low_threshold INT NOT NULL DEFAULT 10 COMMENT 'Alert when unused keys drop below this', @@ -96,7 +96,7 @@ CREATE TABLE IF NOT EXISTS key_pool_config ( -- ── 3. Hardware Binding Registry ─────────────────────────── -- Tracks which keys have been used on which hardware. -CREATE TABLE IF NOT EXISTS hardware_key_bindings ( +CREATE TABLE IF NOT EXISTS `#__hardware_key_bindings` ( id INT AUTO_INCREMENT PRIMARY KEY, product_key_id INT NOT NULL COMMENT 'FK to oem_keys', device_fingerprint VARCHAR(500) NOT NULL, @@ -118,7 +118,7 @@ CREATE TABLE IF NOT EXISTS hardware_key_bindings ( -- ── 4. DPK Import Batches ────────────────────────────────── -- Tracks bulk key imports from Microsoft OEM deliveries. -CREATE TABLE IF NOT EXISTS dpk_import_batches ( +CREATE TABLE IF NOT EXISTS `#__dpk_import_batches` ( id INT AUTO_INCREMENT PRIMARY KEY, batch_name VARCHAR(255) NOT NULL COMMENT 'e.g. "Microsoft Order #12345"', import_source VARCHAR(100) NOT NULL DEFAULT 'manual' COMMENT 'manual, microsoft_csv, microsoft_xml', @@ -142,7 +142,7 @@ CREATE TABLE IF NOT EXISTS dpk_import_batches ( -- ── 5. Work Orders (Production Line Tracking) ────────────── -- Links builds to customer orders, batch production runs. -CREATE TABLE IF NOT EXISTS work_orders ( +CREATE TABLE IF NOT EXISTS `#__work_orders` ( id INT AUTO_INCREMENT PRIMARY KEY, work_order_number VARCHAR(50) NOT NULL UNIQUE COMMENT 'Auto-generated or manual', batch_number VARCHAR(100) DEFAULT NULL COMMENT 'Production batch grouping', diff --git a/FINAL_PRODUCTION_SYSTEM/database/push_notifications_migration.sql b/FINAL_PRODUCTION_SYSTEM/database/push_notifications_migration.sql index 5fee47b..061e04e 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/push_notifications_migration.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/push_notifications_migration.sql @@ -5,7 +5,7 @@ SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; -- Push subscriptions: stores browser push endpoints per admin -CREATE TABLE IF NOT EXISTS `push_subscriptions` ( +CREATE TABLE IF NOT EXISTS `#__push_subscriptions` ( `id` int(11) NOT NULL AUTO_INCREMENT, `admin_id` int(11) NOT NULL, `endpoint` text NOT NULL, @@ -18,11 +18,11 @@ CREATE TABLE IF NOT EXISTS `push_subscriptions` ( PRIMARY KEY (`id`), UNIQUE KEY `uq_admin_endpoint` (`admin_id`, `endpoint`(500)), KEY `idx_admin_active` (`admin_id`, `is_active`), - CONSTRAINT `fk_push_sub_admin` FOREIGN KEY (`admin_id`) REFERENCES `admin_users` (`id`) ON DELETE CASCADE + CONSTRAINT `fk_push_sub_admin` FOREIGN KEY (`admin_id`) REFERENCES `#__admin_users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Push preferences: per-admin category toggles -CREATE TABLE IF NOT EXISTS `push_preferences` ( +CREATE TABLE IF NOT EXISTS `#__push_preferences` ( `id` int(11) NOT NULL AUTO_INCREMENT, `admin_id` int(11) NOT NULL, `category` varchar(50) NOT NULL, @@ -30,11 +30,11 @@ CREATE TABLE IF NOT EXISTS `push_preferences` ( PRIMARY KEY (`id`), UNIQUE KEY `uq_admin_category` (`admin_id`, `category`), KEY `idx_admin_id` (`admin_id`), - CONSTRAINT `fk_push_pref_admin` FOREIGN KEY (`admin_id`) REFERENCES `admin_users` (`id`) ON DELETE CASCADE + CONSTRAINT `fk_push_pref_admin` FOREIGN KEY (`admin_id`) REFERENCES `#__admin_users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Notifications: bell dropdown history -CREATE TABLE IF NOT EXISTS `notifications` ( +CREATE TABLE IF NOT EXISTS `#__notifications` ( `id` int(11) NOT NULL AUTO_INCREMENT, `admin_id` int(11) NOT NULL, `category` varchar(50) NOT NULL, @@ -47,11 +47,11 @@ CREATE TABLE IF NOT EXISTS `notifications` ( PRIMARY KEY (`id`), KEY `idx_admin_read` (`admin_id`, `is_read`), KEY `idx_admin_created` (`admin_id`, `created_at` DESC), - CONSTRAINT `fk_notif_admin` FOREIGN KEY (`admin_id`) REFERENCES `admin_users` (`id`) ON DELETE CASCADE + CONSTRAINT `fk_notif_admin` FOREIGN KEY (`admin_id`) REFERENCES `#__admin_users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- VAPID keys and push settings in system_config -INSERT INTO `system_config` (`config_key`, `config_value`, `description`) +INSERT INTO `#__system_config` (`config_key`, `config_value`, `description`) VALUES ('vapid_public_key', '', 'VAPID public key for Web Push (auto-generated)'), ('vapid_private_key', '', 'VAPID private key for Web Push (auto-generated)'), diff --git a/FINAL_PRODUCTION_SYSTEM/database/qc_compliance_migration.sql b/FINAL_PRODUCTION_SYSTEM/database/qc_compliance_migration.sql index c289c28..2a0a27f 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/qc_compliance_migration.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/qc_compliance_migration.sql @@ -9,15 +9,15 @@ -- 1. New columns on hardware_info -- ============================================================ -ALTER TABLE hardware_info ADD COLUMN boot_order JSON NULL COMMENT 'Ordered list of UEFI boot entries from bcdedit' AFTER device_fingerprint; -ALTER TABLE hardware_info ADD COLUMN hackbgrt_installed TINYINT(1) NULL COMMENT '1=HackBGRT traces found on EFI partition' AFTER boot_order; -ALTER TABLE hardware_info ADD COLUMN hackbgrt_first_boot TINYINT(1) NULL COMMENT '1=HackBGRT is first boot entry' AFTER hackbgrt_installed; +ALTER TABLE `#__hardware_info` ADD COLUMN boot_order JSON NULL COMMENT 'Ordered list of UEFI boot entries from bcdedit' AFTER device_fingerprint; +ALTER TABLE `#__hardware_info` ADD COLUMN hackbgrt_installed TINYINT(1) NULL COMMENT '1=HackBGRT traces found on EFI partition' AFTER boot_order; +ALTER TABLE `#__hardware_info` ADD COLUMN hackbgrt_first_boot TINYINT(1) NULL COMMENT '1=HackBGRT is first boot entry' AFTER hackbgrt_installed; -- ============================================================ -- 2. QC Global Settings (key-value) -- ============================================================ -CREATE TABLE IF NOT EXISTS qc_global_settings ( +CREATE TABLE IF NOT EXISTS `#__qc_global_settings` ( id INT AUTO_INCREMENT PRIMARY KEY, setting_key VARCHAR(100) NOT NULL UNIQUE, setting_value TEXT NOT NULL, @@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS qc_global_settings ( updated_by INT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -INSERT INTO qc_global_settings (setting_key, setting_value, description) VALUES +INSERT INTO `#__qc_global_settings` (setting_key, setting_value, description) VALUES ('qc_enabled', '0', 'Enable hardware compliance checking during activation (0=off, 1=on)'), ('default_bios_enforcement', '1', 'Default BIOS version check enforcement (0=disabled, 1=info, 2=warning, 3=blocking)'), ('default_secure_boot_enforcement', '1', 'Default Secure Boot check enforcement (0=disabled, 1=info, 2=warning, 3=blocking)'), @@ -37,7 +37,7 @@ INSERT INTO qc_global_settings (setting_key, setting_value, description) VALUES -- 3. Manufacturer Defaults -- ============================================================ -CREATE TABLE IF NOT EXISTS qc_manufacturer_defaults ( +CREATE TABLE IF NOT EXISTS `#__qc_manufacturer_defaults` ( id INT AUTO_INCREMENT PRIMARY KEY, manufacturer VARCHAR(100) NOT NULL UNIQUE, secure_boot_required TINYINT(1) DEFAULT 1 COMMENT '1=Secure Boot must be ON', @@ -54,10 +54,10 @@ CREATE TABLE IF NOT EXISTS qc_manufacturer_defaults ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ============================================================ --- 4. Motherboard Registry (auto-populated from hardware_info) +-- 4. Motherboard Registry (auto-populated from `#__hardware_info`) -- ============================================================ -CREATE TABLE IF NOT EXISTS qc_motherboard_registry ( +CREATE TABLE IF NOT EXISTS `#__qc_motherboard_registry` ( id INT AUTO_INCREMENT PRIMARY KEY, manufacturer VARCHAR(100) NOT NULL, product VARCHAR(100) NOT NULL, @@ -85,7 +85,7 @@ CREATE TABLE IF NOT EXISTS qc_motherboard_registry ( -- 5. Compliance Results (per check, per hardware submission) -- ============================================================ -CREATE TABLE IF NOT EXISTS qc_compliance_results ( +CREATE TABLE IF NOT EXISTS `#__qc_compliance_results` ( id INT AUTO_INCREMENT PRIMARY KEY, hardware_info_id INT NOT NULL, order_number VARCHAR(10) NOT NULL, @@ -99,7 +99,7 @@ CREATE TABLE IF NOT EXISTS qc_compliance_results ( motherboard_registry_id INT NULL, is_retroactive TINYINT(1) DEFAULT 0 COMMENT '1=created by retroactive recheck', checked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (hardware_info_id) REFERENCES hardware_info(id) ON DELETE CASCADE, + FOREIGN KEY (hardware_info_id) REFERENCES `#__hardware_info`(id) ON DELETE CASCADE, INDEX idx_hardware_id (hardware_info_id), INDEX idx_order_number (order_number), INDEX idx_check_result (check_result), @@ -110,12 +110,12 @@ CREATE TABLE IF NOT EXISTS qc_compliance_results ( -- 6. ACL: Quality Control permission category + permissions -- ============================================================ -INSERT INTO acl_permission_categories (category_key, display_name, icon, sort_order) VALUES +INSERT INTO `#__acl_permission_categories` (category_key, display_name, icon, sort_order) VALUES ('quality_control', 'Quality Control', NULL, 46); -SET @qc_cat_id = (SELECT id FROM acl_permission_categories WHERE category_key = 'quality_control'); +SET @qc_cat_id = (SELECT id FROM `#__acl_permission_categories` WHERE category_key = 'quality_control'); -INSERT INTO acl_permissions (permission_key, display_name, description, category_id, resource_type, action_type, is_dangerous) VALUES +INSERT INTO `#__acl_permissions` (permission_key, display_name, description, category_id, resource_type, action_type, is_dangerous) VALUES ('view_compliance', 'View Compliance Results', 'View QC compliance check results and statistics', @qc_cat_id, 'compliance', 'view', 0), ('manage_compliance_rules', 'Manage Compliance Rules', 'Configure motherboard registry and compliance enforcement rules', @qc_cat_id, 'compliance', 'manage', 0), ('manage_compliance', 'Manage QC System', 'Toggle QC feature, trigger retroactive checks, modify global settings', @qc_cat_id, 'compliance', 'manage', 1); @@ -123,23 +123,23 @@ INSERT INTO acl_permissions (permission_key, display_name, description, category -- Grant to super_admin (already gets everything via CROSS JOIN, but explicit for clarity on new installs) INSERT IGNORE INTO acl_role_permissions (role_id, permission_id) SELECT r.id, p.id -FROM acl_roles r CROSS JOIN acl_permissions p +FROM `#__acl_roles` r CROSS JOIN `#__acl_permissions` p WHERE r.role_name = 'super_admin' AND p.permission_key IN ('view_compliance', 'manage_compliance_rules', 'manage_compliance'); -- Grant to admin: view + manage rules INSERT IGNORE INTO acl_role_permissions (role_id, permission_id) SELECT r.id, p.id -FROM acl_roles r, acl_permissions p +FROM `#__acl_roles` r, acl_permissions p WHERE r.role_name = 'admin' AND p.permission_key IN ('view_compliance', 'manage_compliance_rules'); -- Grant to qc_inspector: view only INSERT IGNORE INTO acl_role_permissions (role_id, permission_id) SELECT r.id, p.id -FROM acl_roles r, acl_permissions p +FROM `#__acl_roles` r, acl_permissions p WHERE r.role_name = 'qc_inspector' AND p.permission_key IN ('view_compliance'); -- Grant to dept_manager: view only INSERT IGNORE INTO acl_role_permissions (role_id, permission_id) SELECT r.id, p.id -FROM acl_roles r, acl_permissions p +FROM `#__acl_roles` r, acl_permissions p WHERE r.role_name = 'dept_manager' AND p.permission_key IN ('view_compliance'); diff --git a/FINAL_PRODUCTION_SYSTEM/database/rate_limiting_migration.sql b/FINAL_PRODUCTION_SYSTEM/database/rate_limiting_migration.sql index e037a26..bc4293f 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/rate_limiting_migration.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/rate_limiting_migration.sql @@ -6,7 +6,7 @@ -- Table: rate_limit_violations -- Logs when API rate limits are exceeded for security monitoring -CREATE TABLE IF NOT EXISTS `rate_limit_violations` ( +CREATE TABLE IF NOT EXISTS `#__rate_limit_violations` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `identifier` VARCHAR(100) NOT NULL COMMENT 'IP address or user ID', `action` VARCHAR(50) NOT NULL COMMENT 'Endpoint action (login, get-key, etc.)', @@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS `rate_limit_violations` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Rate limit violation log for security monitoring'; -- System configuration for rate limiting -INSERT INTO `system_config` (`config_key`, `config_value`, `description`) VALUES +INSERT INTO `#__system_config` (`config_key`, `config_value`, `description`) VALUES ('rate_limit_enabled', '1', 'Enable API rate limiting (1=yes, 0=no)'), ('rate_limit_global_per_minute', '100', 'Max requests per minute per IP (all endpoints)'), ('rate_limit_login_per_hour', '20', 'Max login attempts per hour per IP'), diff --git a/FINAL_PRODUCTION_SYSTEM/database/rbac_migration.sql b/FINAL_PRODUCTION_SYSTEM/database/rbac_migration.sql index 0c50dc4..25f5888 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/rbac_migration.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/rbac_migration.sql @@ -13,7 +13,7 @@ SET @role_column_exists = (SELECT COUNT(*) FROM information_schema.COLUMNS -- If role column doesn't exist, create it SET @sql = IF(@role_column_exists = 0, - 'ALTER TABLE `admin_users` ADD COLUMN `role` ENUM(''super_admin'', ''admin'', ''viewer'') DEFAULT ''admin'' COMMENT ''Admin role for RBAC''', + 'ALTER TABLE `#__admin_users` ADD COLUMN `role` ENUM(''super_admin'', ''admin'', ''viewer'') DEFAULT ''admin'' COMMENT ''Admin role for RBAC''', 'SELECT "Role column already exists"'); PREPARE stmt FROM @sql; @@ -21,12 +21,12 @@ EXECUTE stmt; DEALLOCATE PREPARE stmt; -- Add role tracking to admin_activity_log -ALTER TABLE `admin_activity_log` +ALTER TABLE `#__admin_activity_log` ADD COLUMN IF NOT EXISTS `admin_role` ENUM('super_admin', 'admin', 'viewer') NULL COMMENT 'Role of admin at time of action' AFTER `user_agent`, ADD INDEX IF NOT EXISTS `idx_admin_role` (`admin_role`); --- Create table for permission audit trail -CREATE TABLE IF NOT EXISTS `rbac_permission_denials` ( +-- Create table `#__for` permission audit trail +CREATE TABLE IF NOT EXISTS `#__rbac_permission_denials` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `admin_id` INT NOT NULL COMMENT 'Admin who was denied', `session_id` VARCHAR(64) NULL COMMENT 'Session ID if available', @@ -41,24 +41,24 @@ CREATE TABLE IF NOT EXISTS `rbac_permission_denials` ( INDEX `idx_admin_role` (`admin_role`), INDEX `idx_requested_action` (`requested_action`), INDEX `idx_denied_at` (`denied_at`), - FOREIGN KEY (`admin_id`) REFERENCES `admin_users`(`id`) ON DELETE CASCADE + FOREIGN KEY (`admin_id`) REFERENCES `#__admin_users`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='RBAC permission denial audit log'; -- System configuration for RBAC -INSERT INTO `system_config` (`config_key`, `config_value`, `description`) VALUES +INSERT INTO `#__system_config` (`config_key`, `config_value`, `description`) VALUES ('rbac_enabled', '1', 'Enable role-based access control (1=yes, 0=no)'), ('rbac_log_denials', '1', 'Log permission denials to rbac_permission_denials table'), ('rbac_strict_mode', '1', 'Deny access to undefined permissions (1=yes, 0=allow)') ON DUPLICATE KEY UPDATE `config_value` = VALUES(`config_value`); -- Verify we have at least one super_admin account -SET @super_admin_count = (SELECT COUNT(*) FROM `admin_users` WHERE `role` = 'super_admin'); +SET @super_admin_count = (SELECT COUNT(*) FROM `#__admin_users` WHERE `role` = 'super_admin'); -- If no super_admin exists, promote the first admin to super_admin -UPDATE `admin_users` SET `role` = 'super_admin' -WHERE `id` = (SELECT `id` FROM (SELECT `id` FROM `admin_users` ORDER BY `id` ASC LIMIT 1) AS temp) +UPDATE `#__admin_users` SET `role` = 'super_admin' +WHERE `id` = (SELECT `id` FROM (SELECT `id` FROM `#__admin_users` ORDER BY `id` ASC LIMIT 1) AS temp) AND @super_admin_count = 0; -- Migration complete SELECT 'Migration: RBAC tables and permissions configured successfully' AS status; -SELECT CONCAT('Super admins: ', COUNT(*)) AS super_admin_count FROM `admin_users` WHERE `role` = 'super_admin'; +SELECT CONCAT('Super admins: ', COUNT(*)) AS super_admin_count FROM `#__admin_users` WHERE `role` = 'super_admin'; diff --git a/FINAL_PRODUCTION_SYSTEM/database/schema_versions_migration.sql b/FINAL_PRODUCTION_SYSTEM/database/schema_versions_migration.sql index 7c8c3e3..18b66e3 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/schema_versions_migration.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/schema_versions_migration.sql @@ -1,7 +1,7 @@ -- Schema Version Tracking -- Tracks which migration files have been applied to prevent re-running. -CREATE TABLE IF NOT EXISTS schema_versions ( +CREATE TABLE IF NOT EXISTS `#__schema_versions` ( id INT AUTO_INCREMENT PRIMARY KEY, version INT NOT NULL, filename VARCHAR(255) NOT NULL, diff --git a/FINAL_PRODUCTION_SYSTEM/database/seed_demo_compliance.sql b/FINAL_PRODUCTION_SYSTEM/database/seed_demo_compliance.sql index fcc4699..42f6de5 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/seed_demo_compliance.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/seed_demo_compliance.sql @@ -8,49 +8,49 @@ -- 256GB: OS 120000 MB = 117.1875 GB, Data 121736 MB = 118.8828125 GB -- 1. Delete old compliance results to start fresh -DELETE FROM qc_compliance_results; +DELETE FROM `#__qc_compliance_results`; -- 2. Update existing hardware_info order numbers to follow product line patterns -- Also inject complete_disk_layout JSON for partition checking -- PASS: Good 512 GB layout (ЭЛ00-100001) -UPDATE hardware_info SET order_number = 'ЭЛ00-100001', +UPDATE `#__hardware_info` SET order_number = 'ЭЛ00-100001', complete_disk_layout = '[{"disk_number":0,"disk_model":"Samsung SSD 970 EVO 500GB","disk_size_gb":465.76,"partition_style":"GPT","partitions":[{"partition_number":0,"size_gb":0.25390625,"partition_purpose":"EFI","file_system":"FAT32"},{"partition_number":1,"size_gb":0.015625,"partition_purpose":"MSR","file_system":null},{"partition_number":2,"size_gb":195.3125,"partition_purpose":"OS","file_system":"NTFS","drive_letter":"C:"},{"partition_number":3,"size_gb":1.46484375,"partition_purpose":"Recovery","file_system":"NTFS"},{"partition_number":4,"size_gb":276.7578125,"partition_purpose":"Data","file_system":"NTFS","drive_letter":"D:"},{"partition_number":5,"size_gb":0.1953125,"partition_purpose":"Other","file_system":"FAT32","volume_name":"BIOS","drive_letter":"E:"}]}]' -WHERE id = (SELECT id FROM (SELECT MIN(id) AS id FROM hardware_info) t); +WHERE id = (SELECT id FROM (SELECT MIN(id) AS id FROM `#__hardware_info`) t); -- PASS: Good 512 GB layout (ЭЛ00-100002) -UPDATE hardware_info SET order_number = 'ЭЛ00-100002', +UPDATE `#__hardware_info` SET order_number = 'ЭЛ00-100002', complete_disk_layout = '[{"disk_number":0,"disk_model":"WD Blue SN570 500GB","disk_size_gb":465.76,"partition_style":"GPT","partitions":[{"partition_number":0,"size_gb":0.25390625,"partition_purpose":"EFI","file_system":"FAT32"},{"partition_number":1,"size_gb":0.015625,"partition_purpose":"MSR","file_system":null},{"partition_number":2,"size_gb":195.3125,"partition_purpose":"OS","file_system":"NTFS","drive_letter":"C:"},{"partition_number":3,"size_gb":1.46484375,"partition_purpose":"Recovery","file_system":"NTFS"},{"partition_number":4,"size_gb":276.7578125,"partition_purpose":"Data","file_system":"NTFS","drive_letter":"D:"},{"partition_number":5,"size_gb":0.1953125,"partition_purpose":"Other","file_system":"FAT32","volume_name":"BIOS","drive_letter":"E:"}]}]' -WHERE id = (SELECT id FROM (SELECT id FROM hardware_info ORDER BY id LIMIT 1 OFFSET 1) t); +WHERE id = (SELECT id FROM (SELECT id FROM `#__hardware_info` ORDER BY id LIMIT 1 OFFSET 1) t); -- PASS: Good 1 TB layout (ЛЕ00-200001) -UPDATE hardware_info SET order_number = 'ЛЕ00-200001', +UPDATE `#__hardware_info` SET order_number = 'ЛЕ00-200001', complete_disk_layout = '[{"disk_number":0,"disk_model":"Kingston A2000 1TB","disk_size_gb":931.51,"partition_style":"GPT","partitions":[{"partition_number":0,"size_gb":0.25390625,"partition_purpose":"EFI","file_system":"FAT32"},{"partition_number":1,"size_gb":0.015625,"partition_purpose":"MSR","file_system":null},{"partition_number":2,"size_gb":390.625,"partition_purpose":"OS","file_system":"NTFS","drive_letter":"C:"},{"partition_number":3,"size_gb":1.46484375,"partition_purpose":"Recovery","file_system":"NTFS"},{"partition_number":4,"size_gb":540.0,"partition_purpose":"Data","file_system":"NTFS","drive_letter":"D:"},{"partition_number":5,"size_gb":0.1953125,"partition_purpose":"Other","file_system":"FAT32","volume_name":"BIOS","drive_letter":"E:"}]}]' -WHERE id = (SELECT id FROM (SELECT id FROM hardware_info ORDER BY id LIMIT 1 OFFSET 2) t); +WHERE id = (SELECT id FROM (SELECT id FROM `#__hardware_info` ORDER BY id LIMIT 1 OFFSET 2) t); -- WARNING: 256 GB layout with Data partition slightly too small (ЛЕ00-200002) -- Data is 113.77 GB = 116500 MB but template requires 121736 MB minimum -UPDATE hardware_info SET order_number = 'ЛЕ00-200002', +UPDATE `#__hardware_info` SET order_number = 'ЛЕ00-200002', complete_disk_layout = '[{"disk_number":0,"disk_model":"Samsung SSD 860 EVO 250GB","disk_size_gb":232.89,"partition_style":"GPT","partitions":[{"partition_number":0,"size_gb":0.25390625,"partition_purpose":"EFI","file_system":"FAT32"},{"partition_number":1,"size_gb":0.015625,"partition_purpose":"MSR","file_system":null},{"partition_number":2,"size_gb":117.1875,"partition_purpose":"OS","file_system":"NTFS","drive_letter":"C:"},{"partition_number":3,"size_gb":1.46484375,"partition_purpose":"Recovery","file_system":"NTFS"},{"partition_number":4,"size_gb":113.77,"partition_purpose":"Data","file_system":"NTFS","drive_letter":"D:"},{"partition_number":5,"size_gb":0.1953125,"partition_purpose":"Other","file_system":"FAT32","volume_name":"BIOS","drive_letter":"E:"}]}]' -WHERE id = (SELECT id FROM (SELECT id FROM hardware_info ORDER BY id LIMIT 1 OFFSET 3) t); +WHERE id = (SELECT id FROM (SELECT id FROM `#__hardware_info` ORDER BY id LIMIT 1 OFFSET 3) t); -- PASS: Good 1 TB layout (ИП00-300001) -UPDATE hardware_info SET order_number = 'ИП00-300001', +UPDATE `#__hardware_info` SET order_number = 'ИП00-300001', complete_disk_layout = '[{"disk_number":0,"disk_model":"Crucial P3 1TB","disk_size_gb":931.51,"partition_style":"GPT","partitions":[{"partition_number":0,"size_gb":0.25390625,"partition_purpose":"EFI","file_system":"FAT32"},{"partition_number":1,"size_gb":0.015625,"partition_purpose":"MSR","file_system":null},{"partition_number":2,"size_gb":390.625,"partition_purpose":"OS","file_system":"NTFS","drive_letter":"C:"},{"partition_number":3,"size_gb":1.46484375,"partition_purpose":"Recovery","file_system":"NTFS"},{"partition_number":4,"size_gb":540.0,"partition_purpose":"Data","file_system":"NTFS","drive_letter":"D:"},{"partition_number":5,"size_gb":0.1953125,"partition_purpose":"Other","file_system":"FAT32","volume_name":"BIOS","drive_letter":"E:"}]}]' -WHERE id = (SELECT id FROM (SELECT id FROM hardware_info ORDER BY id LIMIT 1 OFFSET 4) t); +WHERE id = (SELECT id FROM (SELECT id FROM `#__hardware_info` ORDER BY id LIMIT 1 OFFSET 4) t); -- FAIL: Bad 512 GB layout — wrong OS size (150 GB instead of ~195 GB) -UPDATE hardware_info SET order_number = 'ИП00-300002', +UPDATE `#__hardware_info` SET order_number = 'ИП00-300002', complete_disk_layout = '[{"disk_number":0,"disk_model":"WD Blue SN550 500GB","disk_size_gb":465.76,"partition_style":"GPT","partitions":[{"partition_number":0,"size_gb":0.25390625,"partition_purpose":"EFI","file_system":"FAT32"},{"partition_number":1,"size_gb":0.015625,"partition_purpose":"MSR","file_system":null},{"partition_number":2,"size_gb":146.484375,"partition_purpose":"OS","file_system":"NTFS","drive_letter":"C:"},{"partition_number":3,"size_gb":1.46484375,"partition_purpose":"Recovery","file_system":"NTFS"},{"partition_number":4,"size_gb":317.35,"partition_purpose":"Data","file_system":"NTFS","drive_letter":"D:"},{"partition_number":5,"size_gb":0.1953125,"partition_purpose":"Other","file_system":"FAT32","volume_name":"BIOS","drive_letter":"E:"}]}]' -WHERE id = (SELECT id FROM (SELECT id FROM hardware_info ORDER BY id LIMIT 1 OFFSET 5) t); +WHERE id = (SELECT id FROM (SELECT id FROM `#__hardware_info` ORDER BY id LIMIT 1 OFFSET 5) t); -- No disk layout data (ЭЛ00-100003) — will show "no data" for partition check -UPDATE hardware_info SET order_number = 'ЭЛ00-100003' +UPDATE `#__hardware_info` SET order_number = 'ЭЛ00-100003' WHERE order_number NOT LIKE 'ЭЛ00-%' AND order_number NOT LIKE 'ЛЕ00-%' AND order_number NOT LIKE 'ИП00-%' - AND id = (SELECT id FROM (SELECT id FROM hardware_info WHERE order_number NOT LIKE 'ЭЛ00-%' AND order_number NOT LIKE 'ЛЕ00-%' AND order_number NOT LIKE 'ИП00-%' ORDER BY id LIMIT 1) t); + AND id = (SELECT id FROM (SELECT id FROM `#__hardware_info` WHERE order_number NOT LIKE 'ЭЛ00-%' AND order_number NOT LIKE 'ЛЕ00-%' AND order_number NOT LIKE 'ИП00-%' ORDER BY id LIMIT 1) t); -UPDATE hardware_info SET order_number = 'ЛЕ00-200003' +UPDATE `#__hardware_info` SET order_number = 'ЛЕ00-200003' WHERE order_number NOT LIKE 'ЭЛ00-%' AND order_number NOT LIKE 'ЛЕ00-%' AND order_number NOT LIKE 'ИП00-%' - AND id = (SELECT id FROM (SELECT id FROM hardware_info WHERE order_number NOT LIKE 'ЭЛ00-%' AND order_number NOT LIKE 'ЛЕ00-%' AND order_number NOT LIKE 'ИП00-%' ORDER BY id LIMIT 1) t); + AND id = (SELECT id FROM (SELECT id FROM `#__hardware_info` WHERE order_number NOT LIKE 'ЭЛ00-%' AND order_number NOT LIKE 'ЛЕ00-%' AND order_number NOT LIKE 'ИП00-%' ORDER BY id LIMIT 1) t); -- Done. Now trigger "Recheck Historical" from the admin UI to regenerate all compliance results. diff --git a/FINAL_PRODUCTION_SYSTEM/database/seed_variants.sql b/FINAL_PRODUCTION_SYSTEM/database/seed_variants.sql index 3684d8d..7878914 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/seed_variants.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/seed_variants.sql @@ -2,9 +2,9 @@ -- Disk sizes: 256 GB (~238 GB = 243712 MB), 512 GB (~474 GB = 485376 MB), 1 TB (~931 GB = 953344 MB), 2 TB (~1862 GB = 1907200 MB) -- Get product line IDs -SET @pl1 = (SELECT id FROM product_lines WHERE order_pattern = 'ЭЛ00-######'); -SET @pl2 = (SELECT id FROM product_lines WHERE order_pattern = 'ЛЕ00-######'); -SET @pl3 = (SELECT id FROM product_lines WHERE order_pattern = 'ИП00-######'); +SET @pl1 = (SELECT id FROM `#__product_lines` WHERE order_pattern = 'ЭЛ00-######'); +SET @pl2 = (SELECT id FROM `#__product_lines` WHERE order_pattern = 'ЛЕ00-######'); +SET @pl3 = (SELECT id FROM `#__product_lines` WHERE order_pattern = 'ИП00-######'); -- ── Variants for Marketplace 1 ────────────────────────────────── INSERT IGNORE INTO product_variants (line_id, name, disk_size_min_mb, disk_size_max_mb) VALUES @@ -39,9 +39,9 @@ INSERT IGNORE INTO product_variants (line_id, name, disk_size_min_mb, disk_size_ -- 2 TB ~ 1907200 MB total: EFI 260 + MSR 16 + OS 500000 + Recovery 1500 + Data 1405224 + BIOS 200 -- ── 256 GB variants (all 3 product lines) ──────────────────────── -INSERT INTO product_variant_partitions (variant_id, partition_order, partition_name, partition_type, expected_size_mb, tolerance_percent, is_flexible) +INSERT INTO `#__product_variant_partitions` (variant_id, partition_order, partition_name, partition_type, expected_size_mb, tolerance_percent, is_flexible) SELECT v.id, p.partition_order, p.partition_name, p.partition_type, p.expected_size_mb, p.tolerance_percent, p.is_flexible -FROM product_variants v +FROM `#__product_variants` v CROSS JOIN ( SELECT 1 AS partition_order, 'EFI' AS partition_name, 'EFI System' AS partition_type, 260 AS expected_size_mb, 1.00 AS tolerance_percent, 0 AS is_flexible UNION ALL SELECT 2, 'MSR', 'Microsoft Reserved', 16, 1.00, 0 UNION ALL @@ -54,9 +54,9 @@ WHERE v.name = '256 GB' ON DUPLICATE KEY UPDATE expected_size_mb = VALUES(expected_size_mb), partition_type = VALUES(partition_type), is_flexible = VALUES(is_flexible); -- ── 512 GB variants (all 3 product lines) ──────────────────────── -INSERT INTO product_variant_partitions (variant_id, partition_order, partition_name, partition_type, expected_size_mb, tolerance_percent, is_flexible) +INSERT INTO `#__product_variant_partitions` (variant_id, partition_order, partition_name, partition_type, expected_size_mb, tolerance_percent, is_flexible) SELECT v.id, p.partition_order, p.partition_name, p.partition_type, p.expected_size_mb, p.tolerance_percent, p.is_flexible -FROM product_variants v +FROM `#__product_variants` v CROSS JOIN ( SELECT 1 AS partition_order, 'EFI' AS partition_name, 'EFI System' AS partition_type, 260 AS expected_size_mb, 1.00 AS tolerance_percent, 0 AS is_flexible UNION ALL SELECT 2, 'MSR', 'Microsoft Reserved', 16, 1.00, 0 UNION ALL @@ -69,9 +69,9 @@ WHERE v.name = '512 GB' ON DUPLICATE KEY UPDATE expected_size_mb = VALUES(expected_size_mb), partition_type = VALUES(partition_type), is_flexible = VALUES(is_flexible); -- ── 1 TB variants (all 3 product lines) ────────────────────────── -INSERT INTO product_variant_partitions (variant_id, partition_order, partition_name, partition_type, expected_size_mb, tolerance_percent, is_flexible) +INSERT INTO `#__product_variant_partitions` (variant_id, partition_order, partition_name, partition_type, expected_size_mb, tolerance_percent, is_flexible) SELECT v.id, p.partition_order, p.partition_name, p.partition_type, p.expected_size_mb, p.tolerance_percent, p.is_flexible -FROM product_variants v +FROM `#__product_variants` v CROSS JOIN ( SELECT 1 AS partition_order, 'EFI' AS partition_name, 'EFI System' AS partition_type, 260 AS expected_size_mb, 1.00 AS tolerance_percent, 0 AS is_flexible UNION ALL SELECT 2, 'MSR', 'Microsoft Reserved', 16, 1.00, 0 UNION ALL @@ -84,9 +84,9 @@ WHERE v.name = '1 TB' ON DUPLICATE KEY UPDATE expected_size_mb = VALUES(expected_size_mb), partition_type = VALUES(partition_type), is_flexible = VALUES(is_flexible); -- ── 2 TB variants (all 3 product lines) ────────────────────────── -INSERT INTO product_variant_partitions (variant_id, partition_order, partition_name, partition_type, expected_size_mb, tolerance_percent, is_flexible) +INSERT INTO `#__product_variant_partitions` (variant_id, partition_order, partition_name, partition_type, expected_size_mb, tolerance_percent, is_flexible) SELECT v.id, p.partition_order, p.partition_name, p.partition_type, p.expected_size_mb, p.tolerance_percent, p.is_flexible -FROM product_variants v +FROM `#__product_variants` v CROSS JOIN ( SELECT 1 AS partition_order, 'EFI' AS partition_name, 'EFI System' AS partition_type, 260 AS expected_size_mb, 1.00 AS tolerance_percent, 0 AS is_flexible UNION ALL SELECT 2, 'MSR', 'Microsoft Reserved', 16, 1.00, 0 UNION ALL diff --git a/FINAL_PRODUCTION_SYSTEM/database/task_pipeline_migration.sql b/FINAL_PRODUCTION_SYSTEM/database/task_pipeline_migration.sql index 5ad75bf..3ec8067 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/task_pipeline_migration.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/task_pipeline_migration.sql @@ -7,7 +7,7 @@ -- ============================================================= -- Global task template library (reusable across product lines) -CREATE TABLE IF NOT EXISTS task_templates ( +CREATE TABLE IF NOT EXISTS `#__task_templates` ( id INT AUTO_INCREMENT PRIMARY KEY, task_key VARCHAR(50) NOT NULL UNIQUE COMMENT 'Internal identifier (e.g. hardware_collection)', task_name VARCHAR(100) NOT NULL COMMENT 'Display name', @@ -23,7 +23,7 @@ CREATE TABLE IF NOT EXISTS task_templates ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Per-product-line task assignments (which tasks run, in what order, with overrides) -CREATE TABLE IF NOT EXISTS product_line_tasks ( +CREATE TABLE IF NOT EXISTS `#__product_line_tasks` ( id INT AUTO_INCREMENT PRIMARY KEY, product_line_id INT NOT NULL COMMENT 'FK to product_lines table', task_template_id INT NOT NULL COMMENT 'FK to task_templates', @@ -40,7 +40,7 @@ CREATE TABLE IF NOT EXISTS product_line_tasks ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Task execution log (tracks what ran during each activation) -CREATE TABLE IF NOT EXISTS task_execution_log ( +CREATE TABLE IF NOT EXISTS `#__task_execution_log` ( id INT AUTO_INCREMENT PRIMARY KEY, activation_attempt_id INT DEFAULT NULL COMMENT 'FK to activation_attempts', product_line_id INT DEFAULT NULL, @@ -62,7 +62,7 @@ CREATE TABLE IF NOT EXISTS task_execution_log ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Seed built-in task templates -INSERT INTO task_templates (task_key, task_name, task_type, description, default_timeout_seconds, default_on_failure, is_system, icon) VALUES +INSERT INTO `#__task_templates` (task_key, task_name, task_type, description, default_timeout_seconds, default_on_failure, is_system, icon) VALUES ('hardware_collection', 'Hardware Collection', 'built_in', 'Collect full hardware inventory (MB, CPU, RAM, GPU, disks, network)', 60, 'stop', 1, 'Cpu'), ('qc_compliance', 'QC Compliance Check', 'built_in', 'Run quality control checks (Secure Boot, BIOS version, HackBGRT)', 30, 'stop', 1, 'ShieldCheck'), ('oem_activation', 'OEM Key Activation', 'built_in', 'Request OEM key from server, install and activate Windows', 180, 'stop', 1, 'Key'), @@ -73,7 +73,7 @@ INSERT INTO task_templates (task_key, task_name, task_type, description, default ON DUPLICATE KEY UPDATE task_name = VALUES(task_name); -- Seed example custom tasks (disabled by default, admin can enable per product line) -INSERT INTO task_templates (task_key, task_name, task_type, description, default_code, default_timeout_seconds, default_on_failure, is_system, icon) VALUES +INSERT INTO `#__task_templates` (task_key, task_name, task_type, description, default_code, default_timeout_seconds, default_on_failure, is_system, icon) VALUES ('set_power_plan', 'Set Power Plan', 'custom', 'Configure Windows power plan (High Performance, Balanced, etc.)', 'powercfg /setactive 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c # High Performance', 15, 'skip', 0, 'Zap'), ('disable_sleep', 'Disable Sleep Mode', 'custom', 'Prevent the PC from going to sleep', diff --git a/FINAL_PRODUCTION_SYSTEM/database/temp_password_hash_migration.sql b/FINAL_PRODUCTION_SYSTEM/database/temp_password_hash_migration.sql index 7188269..e2b4cef 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/temp_password_hash_migration.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/temp_password_hash_migration.sql @@ -3,7 +3,7 @@ -- and must be followed by running the PHP migration script below. -- Step 1: Widen column to hold bcrypt hashes (60 chars) -ALTER TABLE technicians MODIFY COLUMN temp_password VARCHAR(255) DEFAULT NULL; +ALTER TABLE `#__technicians` MODIFY COLUMN temp_password VARCHAR(255) DEFAULT NULL; -- Step 2: The existing plaintext temp passwords must be hashed via PHP -- because SQL cannot generate bcrypt hashes natively. diff --git a/FINAL_PRODUCTION_SYSTEM/database/unallocated_space_migration.sql b/FINAL_PRODUCTION_SYSTEM/database/unallocated_space_migration.sql index 1827a70..606a568 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/unallocated_space_migration.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/unallocated_space_migration.sql @@ -2,7 +2,7 @@ -- Adds configurable max unallocated space limit for partition QC -- 1. Add per-variant unallocated space limit -ALTER TABLE product_variants +ALTER TABLE `#__product_variants` ADD COLUMN IF NOT EXISTS max_unallocated_mb INT DEFAULT NULL COMMENT 'Max allowed unallocated disk space in MB (NULL = use global setting)'; diff --git a/FINAL_PRODUCTION_SYSTEM/database/upgrade_system_migration.sql b/FINAL_PRODUCTION_SYSTEM/database/upgrade_system_migration.sql index d1ee47a..7037a12 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/upgrade_system_migration.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/upgrade_system_migration.sql @@ -5,7 +5,7 @@ -- verify, rollback. Provides full audit trail. -- ============================================================= -CREATE TABLE IF NOT EXISTS upgrade_history ( +CREATE TABLE IF NOT EXISTS `#__upgrade_history` ( id INT AUTO_INCREMENT PRIMARY KEY, from_version VARCHAR(20) NOT NULL, to_version VARCHAR(20) NOT NULL, diff --git a/FINAL_PRODUCTION_SYSTEM/database/usb_devices_migration.sql b/FINAL_PRODUCTION_SYSTEM/database/usb_devices_migration.sql index ca10291..335bdea 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/usb_devices_migration.sql +++ b/FINAL_PRODUCTION_SYSTEM/database/usb_devices_migration.sql @@ -4,7 +4,7 @@ -- Tracks registered USB devices for passwordless technician auth. -- ============================================================= -CREATE TABLE IF NOT EXISTS usb_devices ( +CREATE TABLE IF NOT EXISTS `#__usb_devices` ( device_id INT AUTO_INCREMENT PRIMARY KEY, device_serial_number VARCHAR(255) NOT NULL UNIQUE, device_name VARCHAR(255) NOT NULL, diff --git a/FINAL_PRODUCTION_SYSTEM/functions/acl.php b/FINAL_PRODUCTION_SYSTEM/functions/acl.php index 8b602f1..8411927 100644 --- a/FINAL_PRODUCTION_SYSTEM/functions/acl.php +++ b/FINAL_PRODUCTION_SYSTEM/functions/acl.php @@ -110,7 +110,7 @@ function aclLogDenial($userId, $userType, $permissionKey, $session) { try { $stmt = $pdo->prepare(" - INSERT INTO rbac_permission_denials ( + INSERT INTO `" . t('rbac_permission_denials') . "` ( admin_id, session_id, admin_role, requested_action, endpoint, ip_address, user_agent ) VALUES (?, ?, ?, ?, ?, ?, ?) @@ -150,7 +150,7 @@ function aclGetEffectivePermissions($userType, $userId) { // Check if super_admin $isSuperAdmin = false; if ($roleId) { - $stmt = $pdo->prepare("SELECT role_name FROM acl_roles WHERE id = ?"); + $stmt = $pdo->prepare("SELECT role_name FROM `" . t('acl_roles') . "` WHERE id = ?"); $stmt->execute([$roleId]); $role = $stmt->fetch(PDO::FETCH_ASSOC); if ($role && $role['role_name'] === 'super_admin') { @@ -159,7 +159,7 @@ function aclGetEffectivePermissions($userType, $userId) { } // Get all permissions - $stmt = $pdo->query("SELECT id, permission_key, display_name, category_id, is_dangerous FROM acl_permissions ORDER BY id"); + $stmt = $pdo->query("SELECT id, permission_key, display_name, category_id, is_dangerous FROM `" . t('acl_permissions') . "` ORDER BY id"); $allPerms = $stmt->fetchAll(PDO::FETCH_ASSOC); // Get role permissions @@ -167,8 +167,8 @@ function aclGetEffectivePermissions($userType, $userId) { if ($roleId) { $stmt = $pdo->prepare(" SELECT p.permission_key - FROM acl_role_permissions rp - INNER JOIN acl_permissions p ON rp.permission_id = p.id + FROM `" . t('acl_role_permissions') . "` rp + INNER JOIN `" . t('acl_permissions') . "` p ON rp.permission_id = p.id WHERE rp.role_id = ? "); $stmt->execute([$roleId]); @@ -179,8 +179,8 @@ function aclGetEffectivePermissions($userType, $userId) { $overrides = []; $stmt = $pdo->prepare(" SELECT p.permission_key, uo.is_granted, uo.reason, uo.expires_at - FROM acl_user_overrides uo - INNER JOIN acl_permissions p ON uo.permission_id = p.id + FROM `" . t('acl_user_overrides') . "` uo + INNER JOIN `" . t('acl_permissions') . "` p ON uo.permission_id = p.id WHERE uo.user_type = ? AND uo.user_id = ? AND (uo.expires_at IS NULL OR uo.expires_at > NOW()) "); @@ -232,12 +232,12 @@ function aclGetEffectivePermissions($userType, $userId) { function aclGetRoleById($roleId) { global $pdo; try { - $stmt = $pdo->prepare("SELECT * FROM acl_roles WHERE id = ?"); + $stmt = $pdo->prepare("SELECT * FROM `" . t('acl_roles') . "` WHERE id = ?"); $stmt->execute([$roleId]); $role = $stmt->fetch(PDO::FETCH_ASSOC); if (!$role) return null; - $stmt = $pdo->prepare("SELECT permission_id FROM acl_role_permissions WHERE role_id = ?"); + $stmt = $pdo->prepare("SELECT permission_id FROM `" . t('acl_role_permissions') . "` WHERE role_id = ?"); $stmt->execute([$roleId]); $role['permission_ids'] = $stmt->fetchAll(PDO::FETCH_COLUMN); @@ -255,8 +255,8 @@ function aclListRoles($roleType = null) { global $pdo; try { $sql = "SELECT r.*, - (SELECT COUNT(*) FROM acl_role_permissions WHERE role_id = r.id) as permission_count - FROM acl_roles r WHERE 1=1"; + (SELECT COUNT(*) FROM `" . t('acl_role_permissions') . "` WHERE role_id = r.id) as permission_count + FROM `" . t('acl_roles') . "` r WHERE 1=1"; $params = []; if ($roleType) { $sql .= " AND r.role_type = ?"; @@ -291,7 +291,7 @@ function aclCreateRole($name, $displayName, $description, $roleType, $color, $pe $pdo->beginTransaction(); $stmt = $pdo->prepare(" - INSERT INTO acl_roles (role_name, display_name, description, role_type, color, is_system_role, priority, created_by) + INSERT INTO `" . t('acl_roles') . "` (role_name, display_name, description, role_type, color, is_system_role, priority, created_by) VALUES (?, ?, ?, ?, ?, 0, 0, ?) "); $stmt->execute([$name, $displayName, $description, $roleType, $color, $actorId]); @@ -299,7 +299,7 @@ function aclCreateRole($name, $displayName, $description, $roleType, $color, $pe // Assign permissions if (!empty($permissionIds)) { - $stmt = $pdo->prepare("INSERT INTO acl_role_permissions (role_id, permission_id, granted_by) VALUES (?, ?, ?)"); + $stmt = $pdo->prepare("INSERT INTO `" . t('acl_role_permissions') . "` (role_id, permission_id, granted_by) VALUES (?, ?, ?)"); foreach ($permissionIds as $permId) { $stmt->execute([$roleId, $permId, $actorId]); } @@ -329,7 +329,7 @@ function aclUpdateRole($roleId, $data, $actorId) { // Optimistic locking: reject if role was modified since the client loaded it if (!empty($data['expected_updated_at'])) { - $stmt = $pdo->prepare("SELECT updated_at FROM acl_roles WHERE id = ?"); + $stmt = $pdo->prepare("SELECT updated_at FROM `" . t('acl_roles') . "` WHERE id = ?"); $stmt->execute([$roleId]); $currentUpdatedAt = $stmt->fetchColumn(); if ($currentUpdatedAt && $currentUpdatedAt !== $data['expected_updated_at']) { @@ -372,7 +372,7 @@ function aclUpdateRole($roleId, $data, $actorId) { if (!empty($fields)) { $params[] = $roleId; - $stmt = $pdo->prepare("UPDATE acl_roles SET " . implode(', ', $fields) . " WHERE id = ?"); + $stmt = $pdo->prepare("UPDATE `" . t('acl_roles') . "` SET " . implode(', ', $fields) . " WHERE id = ?"); $stmt->execute($params); } @@ -381,11 +381,11 @@ function aclUpdateRole($roleId, $data, $actorId) { $oldPermIds = $role['permission_ids']; // Clear existing - $stmt = $pdo->prepare("DELETE FROM acl_role_permissions WHERE role_id = ?"); + $stmt = $pdo->prepare("DELETE FROM `" . t('acl_role_permissions') . "` WHERE role_id = ?"); $stmt->execute([$roleId]); // Insert new - $stmt = $pdo->prepare("INSERT INTO acl_role_permissions (role_id, permission_id, granted_by) VALUES (?, ?, ?)"); + $stmt = $pdo->prepare("INSERT INTO `" . t('acl_role_permissions') . "` (role_id, permission_id, granted_by) VALUES (?, ?, ?)"); foreach ($data['permission_ids'] as $permId) { $stmt->execute([$roleId, $permId, $actorId]); } @@ -428,7 +428,7 @@ function aclDeleteRole($roleId, $actorId) { $pdo->beginTransaction(); // role_permissions will cascade delete - $stmt = $pdo->prepare("DELETE FROM acl_roles WHERE id = ? AND is_system_role = 0"); + $stmt = $pdo->prepare("DELETE FROM `" . t('acl_roles') . "` WHERE id = ? AND is_system_role = 0"); $stmt->execute([$roleId]); aclLogChange($actorId, 'delete_role', 'role', $roleId, $role['display_name'], @@ -480,10 +480,10 @@ function aclAssignPermissions($roleId, $permissionIds, $actorId) { $oldPermIds = $role['permission_ids']; - $stmt = $pdo->prepare("DELETE FROM acl_role_permissions WHERE role_id = ?"); + $stmt = $pdo->prepare("DELETE FROM `" . t('acl_role_permissions') . "` WHERE role_id = ?"); $stmt->execute([$roleId]); - $stmt = $pdo->prepare("INSERT INTO acl_role_permissions (role_id, permission_id, granted_by) VALUES (?, ?, ?)"); + $stmt = $pdo->prepare("INSERT INTO `" . t('acl_role_permissions') . "` (role_id, permission_id, granted_by) VALUES (?, ?, ?)"); foreach ($permissionIds as $permId) { $stmt->execute([$roleId, $permId, $actorId]); } @@ -511,7 +511,7 @@ function aclSetUserOverride($userType, $userId, $permissionId, $isGranted, $reas global $pdo; try { $stmt = $pdo->prepare(" - INSERT INTO acl_user_overrides (user_type, user_id, permission_id, is_granted, reason, expires_at, created_by) + INSERT INTO `" . t('acl_user_overrides') . "` (user_type, user_id, permission_id, is_granted, reason, expires_at, created_by) VALUES (?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE is_granted = VALUES(is_granted), reason = VALUES(reason), expires_at = VALUES(expires_at), created_by = VALUES(created_by), @@ -520,7 +520,7 @@ function aclSetUserOverride($userType, $userId, $permissionId, $isGranted, $reas $stmt->execute([$userType, $userId, $permissionId, $isGranted ? 1 : 0, $reason, $expiresAt, $actorId]); // Get permission key for logging - $stmt2 = $pdo->prepare("SELECT permission_key FROM acl_permissions WHERE id = ?"); + $stmt2 = $pdo->prepare("SELECT permission_key FROM `" . t('acl_permissions') . "` WHERE id = ?"); $stmt2->execute([$permissionId]); $permKey = $stmt2->fetchColumn(); @@ -544,14 +544,14 @@ function aclRemoveUserOverride($userType, $userId, $permissionId, $actorId) { // Get current override for logging $stmt = $pdo->prepare(" SELECT uo.*, p.permission_key - FROM acl_user_overrides uo - INNER JOIN acl_permissions p ON uo.permission_id = p.id + FROM `" . t('acl_user_overrides') . "` uo + INNER JOIN `" . t('acl_permissions') . "` p ON uo.permission_id = p.id WHERE uo.user_type = ? AND uo.user_id = ? AND uo.permission_id = ? "); $stmt->execute([$userType, $userId, $permissionId]); $old = $stmt->fetch(PDO::FETCH_ASSOC); - $stmt = $pdo->prepare("DELETE FROM acl_user_overrides WHERE user_type = ? AND user_id = ? AND permission_id = ?"); + $stmt = $pdo->prepare("DELETE FROM `" . t('acl_user_overrides') . "` WHERE user_type = ? AND user_id = ? AND permission_id = ?"); $stmt->execute([$userType, $userId, $permissionId]); if ($old) { @@ -575,8 +575,8 @@ function aclGetUserOverrides($userType, $userId) { try { $stmt = $pdo->prepare(" SELECT uo.*, p.permission_key, p.display_name as permission_name, p.category_id - FROM acl_user_overrides uo - INNER JOIN acl_permissions p ON uo.permission_id = p.id + FROM `" . t('acl_user_overrides') . "` uo + INNER JOIN `" . t('acl_permissions') . "` p ON uo.permission_id = p.id WHERE uo.user_type = ? AND uo.user_id = ? ORDER BY p.id "); @@ -599,11 +599,11 @@ function aclListPermissions($categoryId = null) { global $pdo; try { // Get categories - $stmt = $pdo->query("SELECT * FROM acl_permission_categories ORDER BY sort_order"); + $stmt = $pdo->query("SELECT * FROM `" . t('acl_permission_categories') . "` ORDER BY sort_order"); $categories = $stmt->fetchAll(PDO::FETCH_ASSOC); // Get permissions - $sql = "SELECT * FROM acl_permissions"; + $sql = "SELECT * FROM `" . t('acl_permissions') . "`"; $params = []; if ($categoryId) { $sql .= " WHERE category_id = ?"; @@ -646,11 +646,11 @@ function aclGetUserRoleId($userType, $userId) { global $pdo; try { if ($userType === 'admin') { - $stmt = $pdo->prepare("SELECT custom_role_id FROM admin_users WHERE id = ?"); + $stmt = $pdo->prepare("SELECT custom_role_id FROM `" . t('admin_users') . "` WHERE id = ?"); $stmt->execute([$userId]); return $stmt->fetchColumn() ?: null; } elseif ($userType === 'technician') { - $stmt = $pdo->prepare("SELECT role_id FROM technicians WHERE id = ?"); + $stmt = $pdo->prepare("SELECT role_id FROM `" . t('technicians') . "` WHERE id = ?"); $stmt->execute([$userId]); return $stmt->fetchColumn() ?: null; } @@ -668,8 +668,8 @@ function aclGetRoleUserCount($roleId) { try { $stmt = $pdo->prepare(" SELECT - (SELECT COUNT(*) FROM admin_users WHERE custom_role_id = ?) + - (SELECT COUNT(*) FROM technicians WHERE role_id = ?) as total + (SELECT COUNT(*) FROM `" . t('admin_users') . "` WHERE custom_role_id = ?) + + (SELECT COUNT(*) FROM `" . t('technicians') . "` WHERE role_id = ?) as total "); $stmt->execute([$roleId, $roleId]); return (int)$stmt->fetchColumn(); @@ -686,7 +686,7 @@ function aclLogChange($actorId, $action, $targetType, $targetId, $targetName, $o global $pdo; try { $stmt = $pdo->prepare(" - INSERT INTO acl_change_log (actor_id, action, target_type, target_id, target_name, old_value, new_value, ip_address) + INSERT INTO `" . t('acl_change_log') . "` (actor_id, action, target_type, target_id, target_name, old_value, new_value, ip_address) VALUES (?, ?, ?, ?, ?, ?, ?, ?) "); $stmt->execute([ diff --git a/FINAL_PRODUCTION_SYSTEM/functions/admin-helpers.php b/FINAL_PRODUCTION_SYSTEM/functions/admin-helpers.php index 8e3855e..51bc3d0 100644 --- a/FINAL_PRODUCTION_SYSTEM/functions/admin-helpers.php +++ b/FINAL_PRODUCTION_SYSTEM/functions/admin-helpers.php @@ -27,8 +27,8 @@ function validateAdminSession() { s.id, s.admin_id, s.expires_at, s.last_activity, u.username, u.full_name, u.role, u.is_active, u.preferred_language, u.password_changed_at, u.must_change_password - FROM admin_sessions s - JOIN admin_users u ON s.admin_id = u.id + FROM `" . t('admin_sessions') . "` s + JOIN `" . t('admin_users') . "` u ON s.admin_id = u.id WHERE s.session_token = ? AND s.is_active = 1 "); $stmt->execute([$_SESSION['admin_token']]); @@ -41,7 +41,7 @@ function validateAdminSession() { // Check if session expired (hard expiry set at creation time) if (strtotime($session['expires_at']) < time()) { - $stmt = $pdo->prepare("UPDATE admin_sessions SET is_active = 0 WHERE id = ?"); + $stmt = $pdo->prepare("UPDATE `" . t('admin_sessions') . "` SET is_active = 0 WHERE id = ?"); $stmt->execute([$session['id']]); return false; } @@ -50,7 +50,7 @@ function validateAdminSession() { $timeoutMinutes = (int) getConfigWithDefault('admin_session_timeout_minutes', DEFAULT_ADMIN_SESSION_TIMEOUT_MINUTES); $timeoutSeconds = $timeoutMinutes * 60; if (strtotime($session['last_activity']) < (time() - $timeoutSeconds)) { - $stmt = $pdo->prepare("UPDATE admin_sessions SET is_active = 0 WHERE id = ?"); + $stmt = $pdo->prepare("UPDATE `" . t('admin_sessions') . "` SET is_active = 0 WHERE id = ?"); $stmt->execute([$session['id']]); return false; } @@ -71,7 +71,7 @@ function validateAdminSession() { } // Update last activity - $stmt = $pdo->prepare("UPDATE admin_sessions SET last_activity = NOW() WHERE id = ?"); + $stmt = $pdo->prepare("UPDATE `" . t('admin_sessions') . "` SET last_activity = NOW() WHERE id = ?"); $stmt->execute([$session['id']]); return $session; @@ -93,7 +93,7 @@ function logAdminActivity($admin_id, $session_id, $action, $description = '') { global $pdo; try { $stmt = $pdo->prepare(" - INSERT INTO admin_activity_log (admin_id, session_id, action, description, ip_address, user_agent) + INSERT INTO `" . t('admin_activity_log') . "` (admin_id, session_id, action, description, ip_address, user_agent) VALUES (?, ?, ?, ?, ?, ?) "); $stmt->execute([ @@ -125,7 +125,7 @@ function authenticateAdmin($username, $password) { $lockoutMinutes = (int) getConfigWithDefault('admin_lockout_duration_minutes', DEFAULT_ADMIN_LOCKOUT_MINUTES); $sessionTimeout = (int) getConfigWithDefault('admin_session_timeout_minutes', DEFAULT_ADMIN_SESSION_TIMEOUT_MINUTES); - $stmt = $pdo->prepare("SELECT * FROM admin_users WHERE username = ? AND is_active = 1"); + $stmt = $pdo->prepare("SELECT * FROM `" . t('admin_users') . "` WHERE username = ? AND is_active = 1"); $stmt->execute([$username]); $admin = $stmt->fetch(); @@ -153,7 +153,7 @@ function authenticateAdmin($username, $password) { } $stmt = $pdo->prepare(" - UPDATE admin_users + UPDATE `" . t('admin_users') . "` SET failed_login_attempts = ?, locked_until = ? WHERE id = ? "); @@ -172,7 +172,7 @@ function authenticateAdmin($username, $password) { $expires_at = date('Y-m-d H:i:s', time() + ($sessionTimeout * 60)); $stmt = $pdo->prepare(" - INSERT INTO admin_sessions (admin_id, session_token, ip_address, user_agent, expires_at) + INSERT INTO `" . t('admin_sessions') . "` (admin_id, session_token, ip_address, user_agent, expires_at) VALUES (?, ?, ?, ?, ?) "); $stmt->execute([ @@ -182,7 +182,7 @@ function authenticateAdmin($username, $password) { // Reset failed attempts $stmt = $pdo->prepare(" - UPDATE admin_users + UPDATE `" . t('admin_users') . "` SET failed_login_attempts = 0, locked_until = NULL, last_login = NOW(), last_login_ip = ? WHERE id = ? "); @@ -217,7 +217,7 @@ function getUploadErrorMessage(int $errorCode): string { */ function saveConfigBatch(PDO $pdo, array $configs, array $descriptions = []): void { $stmt = $pdo->prepare(" - INSERT INTO system_config (config_key, config_value, description, updated_at) + INSERT INTO `" . t('system_config') . "` (config_key, config_value, description, updated_at) VALUES (?, ?, ?, NOW()) ON DUPLICATE KEY UPDATE config_value = ?, updated_at = NOW() "); @@ -251,8 +251,8 @@ function isActorSuperAdmin($adminId) { global $pdo; try { $stmt = $pdo->prepare(" - SELECT r.role_name FROM acl_roles r - INNER JOIN admin_users u ON u.custom_role_id = r.id + SELECT r.role_name FROM `" . t('acl_roles') . "` r + INNER JOIN `" . t('admin_users') . "` u ON u.custom_role_id = r.id WHERE u.id = ? AND r.role_name = 'super_admin' "); $stmt->execute([$adminId]); diff --git a/FINAL_PRODUCTION_SYSTEM/functions/csv-import.php b/FINAL_PRODUCTION_SYSTEM/functions/csv-import.php index 11ee83b..91b7cb5 100644 --- a/FINAL_PRODUCTION_SYSTEM/functions/csv-import.php +++ b/FINAL_PRODUCTION_SYSTEM/functions/csv-import.php @@ -180,14 +180,14 @@ function importComprehensiveKeyRow($row, $format) { try { // Check if key already exists - $stmt = $pdo->prepare("SELECT id, key_status FROM oem_keys WHERE product_key = ?"); + $stmt = $pdo->prepare("SELECT id, key_status FROM `" . t('oem_keys') . "` WHERE product_key = ?"); $stmt->execute([$product_key]); $existing = $stmt->fetch(PDO::FETCH_ASSOC); if ($existing) { // Update existing key $stmt = $pdo->prepare(" - UPDATE oem_keys + UPDATE `" . t('oem_keys') . "` SET key_status = ?, oem_identifier = ?, roll_serial = ?, fail_counter = ?, last_use_date = ?, last_use_time = ?, first_usage_date = ?, first_usage_time = ?, updated_at = NOW() @@ -200,7 +200,7 @@ function importComprehensiveKeyRow($row, $format) { } else { // Insert new key $stmt = $pdo->prepare(" - INSERT INTO oem_keys (product_key, oem_identifier, roll_serial, key_status, fail_counter, + INSERT INTO `" . t('oem_keys') . "` (product_key, oem_identifier, roll_serial, key_status, fail_counter, last_use_date, last_use_time, first_usage_date, first_usage_time, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW()) "); @@ -242,7 +242,7 @@ function importStandardKeyRow($row, $format) { try { // Check if key already exists - $stmt = $pdo->prepare("SELECT id, key_status FROM oem_keys WHERE product_key = ?"); + $stmt = $pdo->prepare("SELECT id, key_status FROM `" . t('oem_keys') . "` WHERE product_key = ?"); $stmt->execute([$product_key]); $existing = $stmt->fetch(PDO::FETCH_ASSOC); @@ -250,7 +250,7 @@ function importStandardKeyRow($row, $format) { // Update existing key if status changed if ($existing['key_status'] !== $key_status) { $stmt = $pdo->prepare(" - UPDATE oem_keys + UPDATE `" . t('oem_keys') . "` SET key_status = ?, oem_identifier = ?, barcode = ?, updated_at = NOW() WHERE product_key = ? "); @@ -262,7 +262,7 @@ function importStandardKeyRow($row, $format) { } else { // Insert new key $stmt = $pdo->prepare(" - INSERT INTO oem_keys (product_key, oem_identifier, barcode, key_status, roll_serial, created_at) + INSERT INTO `" . t('oem_keys') . "` (product_key, oem_identifier, barcode, key_status, roll_serial, created_at) VALUES (?, ?, ?, ?, 'imported', NOW()) "); $stmt->execute([$product_key, $oem_identifier, $barcode, $key_status]); @@ -321,7 +321,7 @@ function importActivationAttempts($key_id, $row, $format) { if (!empty($attempt['attempted_date']) && !empty($attempt['technician_id'])) { try { $stmt = $pdo->prepare(" - INSERT INTO activation_attempts + INSERT INTO `" . t('activation_attempts') . "` (key_id, technician_id, order_number, attempt_number, attempt_result, attempted_date, attempted_time, attempted_at) VALUES (?, ?, ?, ?, ?, ?, ?, NOW()) diff --git a/FINAL_PRODUCTION_SYSTEM/functions/db-helpers.php b/FINAL_PRODUCTION_SYSTEM/functions/db-helpers.php new file mode 100644 index 0000000..30e6976 --- /dev/null +++ b/FINAL_PRODUCTION_SYSTEM/functions/db-helpers.php @@ -0,0 +1,37 @@ +prepare("SELECT * FROM integrations WHERE integration_key = ?"); + $stmt = $pdo->prepare("SELECT * FROM `" . t('integrations') . "` WHERE integration_key = ?"); $stmt->execute([$key]); $row = $stmt->fetch(); @@ -51,10 +51,10 @@ function updateIntegrationStatus(int $id, string $status, ?string $error = null) try { if ($error !== null) { - $stmt = $pdo->prepare("UPDATE integrations SET status = ?, last_error = ?, updated_at = NOW() WHERE id = ?"); + $stmt = $pdo->prepare("UPDATE `" . t('integrations') . "` SET status = ?, last_error = ?, updated_at = NOW() WHERE id = ?"); $stmt->execute([$status, $error, $id]); } else { - $stmt = $pdo->prepare("UPDATE integrations SET status = ?, updated_at = NOW() WHERE id = ?"); + $stmt = $pdo->prepare("UPDATE `" . t('integrations') . "` SET status = ?, updated_at = NOW() WHERE id = ?"); $stmt->execute([$status, $id]); } @@ -81,7 +81,7 @@ function dispatchIntegrationEvent(string $integrationKey, string $eventType, arr try { $stmt = $pdo->prepare(" - INSERT INTO integration_events (integration_id, event_type, payload, status, created_at) + INSERT INTO `" . t('integration_events') . "` (integration_id, event_type, payload, status, created_at) VALUES (?, ?, ?, 'pending', NOW()) "); $stmt->execute([ @@ -106,7 +106,7 @@ function dispatchEventToAll(string $eventType, array $payload): void { global $pdo; try { - $stmt = $pdo->query("SELECT integration_key FROM integrations WHERE enabled = 1"); + $stmt = $pdo->query("SELECT integration_key FROM `" . t('integrations') . "` WHERE enabled = 1"); $keys = $stmt->fetchAll(PDO::FETCH_COLUMN); foreach ($keys as $key) { @@ -124,7 +124,7 @@ function deliverIntegrationEvent(int $eventId, array $integration): void { global $pdo; try { - $stmt = $pdo->prepare("SELECT * FROM integration_events WHERE id = ?"); + $stmt = $pdo->prepare("SELECT * FROM `" . t('integration_events') . "` WHERE id = ?"); $stmt->execute([$eventId]); $event = $stmt->fetch(); if (!$event) return; @@ -137,7 +137,7 @@ function deliverIntegrationEvent(int $eventId, array $integration): void { $handlerFile = dirname(__DIR__) . "/functions/integrations/{$key}-handler.php"; if (!file_exists($handlerFile)) { // No handler yet — mark as skipped - $stmt = $pdo->prepare("UPDATE integration_events SET status = 'skipped', processed_at = NOW(), error_message = 'No handler file' WHERE id = ?"); + $stmt = $pdo->prepare("UPDATE `" . t('integration_events') . "` SET status = 'skipped', processed_at = NOW(), error_message = 'No handler file' WHERE id = ?"); $stmt->execute([$eventId]); return; } @@ -146,7 +146,7 @@ function deliverIntegrationEvent(int $eventId, array $integration): void { $handlerFunc = 'handle_' . str_replace('-', '_', $key) . '_event'; if (!function_exists($handlerFunc)) { - $stmt = $pdo->prepare("UPDATE integration_events SET status = 'skipped', processed_at = NOW(), error_message = 'Handler function not found' WHERE id = ?"); + $stmt = $pdo->prepare("UPDATE `" . t('integration_events') . "` SET status = 'skipped', processed_at = NOW(), error_message = 'Handler function not found' WHERE id = ?"); $stmt->execute([$eventId]); return; } @@ -157,7 +157,7 @@ function deliverIntegrationEvent(int $eventId, array $integration): void { // Update event with result $status = ($result['success'] ?? false) ? 'sent' : 'failed'; $stmt = $pdo->prepare(" - UPDATE integration_events + UPDATE `" . t('integration_events') . "` SET status = ?, response_code = ?, response_body = ?, error_message = ?, processed_at = NOW() WHERE id = ? "); @@ -172,7 +172,7 @@ function deliverIntegrationEvent(int $eventId, array $integration): void { // Update integration status if ($status === 'sent') { updateIntegrationStatus($integration['id'], 'connected'); - $pdo->prepare("UPDATE integrations SET last_sync_at = NOW() WHERE id = ?")->execute([$integration['id']]); + $pdo->prepare("UPDATE `" . t('integrations') . "` SET last_sync_at = NOW() WHERE id = ?")->execute([$integration['id']]); } else { updateIntegrationStatus($integration['id'], 'error', $result['error'] ?? 'Delivery failed'); } @@ -180,7 +180,7 @@ function deliverIntegrationEvent(int $eventId, array $integration): void { } catch (Exception $e) { error_log("deliverIntegrationEvent($eventId) failed: " . $e->getMessage()); try { - $stmt = $pdo->prepare("UPDATE integration_events SET status = 'failed', processed_at = NOW(), error_message = ? WHERE id = ?"); + $stmt = $pdo->prepare("UPDATE `" . t('integration_events') . "` SET status = 'failed', processed_at = NOW(), error_message = ? WHERE id = ?"); $stmt->execute([$e->getMessage(), $eventId]); } catch (PDOException $ex) { // Ignore @@ -201,7 +201,7 @@ function retryFailedEvents(string $integrationKey, int $limit = 50): array { try { $stmt = $pdo->prepare(" - SELECT id FROM integration_events + SELECT id FROM `" . t('integration_events') . "` WHERE integration_id = ? AND status IN ('failed', 'pending') ORDER BY created_at ASC LIMIT ? @@ -216,7 +216,7 @@ function retryFailedEvents(string $integrationKey, int $limit = 50): array { $retried++; // Check if it succeeded - $check = $pdo->prepare("SELECT status FROM integration_events WHERE id = ?"); + $check = $pdo->prepare("SELECT status FROM `" . t('integration_events') . "` WHERE id = ?"); $check->execute([$eid]); if ($check->fetchColumn() === 'sent') { $succeeded++; diff --git a/FINAL_PRODUCTION_SYSTEM/functions/key-helpers.php b/FINAL_PRODUCTION_SYSTEM/functions/key-helpers.php index ba37328..63d385f 100644 --- a/FINAL_PRODUCTION_SYSTEM/functions/key-helpers.php +++ b/FINAL_PRODUCTION_SYSTEM/functions/key-helpers.php @@ -39,7 +39,7 @@ function allocateKeyAtomically($pdo, $technician_id, $order_number) { } $stmt = $pdo->prepare(" - SELECT * FROM oem_keys + SELECT * FROM `" . t('oem_keys') . "` WHERE key_status IN ('unused', 'retry') AND (fail_counter < " . MAX_KEY_FAIL_COUNTER . " OR key_status = 'unused') ORDER BY @@ -55,7 +55,7 @@ function allocateKeyAtomically($pdo, $technician_id, $order_number) { if ($key) { $stmt = $pdo->prepare(" - UPDATE oem_keys + UPDATE `" . t('oem_keys') . "` SET key_status = 'allocated', last_use_date = CURDATE(), last_use_time = CURTIME(), diff --git a/FINAL_PRODUCTION_SYSTEM/functions/license-helpers.php b/FINAL_PRODUCTION_SYSTEM/functions/license-helpers.php index 44c904f..9aa8877 100644 --- a/FINAL_PRODUCTION_SYSTEM/functions/license-helpers.php +++ b/FINAL_PRODUCTION_SYSTEM/functions/license-helpers.php @@ -84,7 +84,7 @@ function generateInstanceId(PDO $pdo): string { // Get database creation timestamp (stable across restarts) try { - $stmt = $pdo->query("SELECT MIN(created_at) AS first_record FROM admin_users"); + $stmt = $pdo->query("SELECT MIN(created_at) AS first_record FROM `" . t('admin_users') . "`"); $row = $stmt->fetch(); $dbSeed = $row['first_record'] ?? date('Y-m-d'); } catch (Exception $e) { @@ -171,7 +171,7 @@ function createLicenseJwt(array $payload, string $secret = 'keygate-community-ve */ function getCurrentLicense(PDO $pdo): ?array { try { - $stmt = $pdo->query("SELECT * FROM license_info WHERE is_active = 1 ORDER BY id DESC LIMIT 1"); + $stmt = $pdo->query("SELECT * FROM `" . t('license_info') . "` WHERE is_active = 1 ORDER BY id DESC LIMIT 1"); return $stmt->fetch(PDO::FETCH_ASSOC) ?: null; } catch (Exception $e) { // Table may not exist yet (pre-migration) @@ -258,11 +258,11 @@ function registerLicense(PDO $pdo, string $licenseKey): array { $tierDef = LICENSE_TIERS[$tier]; // Deactivate any existing license - $pdo->exec("UPDATE license_info SET is_active = 0"); + $pdo->exec("UPDATE `" . t('license_info') . "` SET is_active = 0"); // Insert new license $stmt = $pdo->prepare(" - INSERT INTO license_info + INSERT INTO `" . t('license_info') . "` (license_key, instance_id, tier, licensed_to_email, licensed_to_name, max_technicians, max_keys, features, issued_at, expires_at, last_validated_at, validation_status, is_active) @@ -307,7 +307,7 @@ function isFeatureAvailable(PDO $pdo, string $feature): bool { */ function canAddTechnician(PDO $pdo): array { $license = getEffectiveLicense($pdo); - $stmt = $pdo->query("SELECT COUNT(*) FROM technicians WHERE status = 'active'"); + $stmt = $pdo->query("SELECT COUNT(*) FROM `" . t('technicians') . "` WHERE status = 'active'"); $currentCount = (int)$stmt->fetchColumn(); if ($currentCount >= $license['max_technicians']) { @@ -328,7 +328,7 @@ function canAddTechnician(PDO $pdo): array { */ function canAddKeys(PDO $pdo, int $count = 1): array { $license = getEffectiveLicense($pdo); - $stmt = $pdo->query("SELECT COUNT(*) FROM oem_keys"); + $stmt = $pdo->query("SELECT COUNT(*) FROM `" . t('oem_keys') . "`"); $currentCount = (int)$stmt->fetchColumn(); if (($currentCount + $count) > $license['max_keys']) { diff --git a/FINAL_PRODUCTION_SYSTEM/functions/network-utils.php b/FINAL_PRODUCTION_SYSTEM/functions/network-utils.php index ec3e6bf..fe2e8f6 100644 --- a/FINAL_PRODUCTION_SYSTEM/functions/network-utils.php +++ b/FINAL_PRODUCTION_SYSTEM/functions/network-utils.php @@ -21,14 +21,14 @@ function checkTrustedNetwork($ip, $checkUSBAuth = false) { // Check if network allows USB authentication $stmt = $pdo->prepare(" SELECT id, network_name, ip_range, bypass_2fa, allow_usb_auth - FROM trusted_networks + FROM `" . t('trusted_networks') . "` WHERE is_active = 1 AND allow_usb_auth = 1 "); } else { // Check if network allows 2FA bypass $stmt = $pdo->prepare(" SELECT id, network_name, ip_range, bypass_2fa, allow_usb_auth - FROM trusted_networks + FROM `" . t('trusted_networks') . "` WHERE is_active = 1 AND bypass_2fa = 1 "); } @@ -122,7 +122,7 @@ function logNetworkSecurityEvent($event_type, $ip_address, $network_id = null, $ try { $stmt = $pdo->prepare(" - INSERT INTO admin_activity_log ( + INSERT INTO `" . t('admin_activity_log') . "` ( admin_id, session_id, action, description, ip_address, user_agent, trusted_network_id ) VALUES ( diff --git a/FINAL_PRODUCTION_SYSTEM/functions/push-helpers.php b/FINAL_PRODUCTION_SYSTEM/functions/push-helpers.php index bcf23cd..2f2304f 100644 --- a/FINAL_PRODUCTION_SYSTEM/functions/push-helpers.php +++ b/FINAL_PRODUCTION_SYSTEM/functions/push-helpers.php @@ -87,7 +87,7 @@ function getVapidKeys(): ?array { $keys = VAPID::createVapidKeys(); $stmt = $pdo->prepare(" - INSERT INTO system_config (config_key, config_value, description) + INSERT INTO `" . t('system_config') . "` (config_key, config_value, description) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE config_value = VALUES(config_value) "); @@ -129,11 +129,11 @@ function dispatchNotification(string $action, string $description, int $actorAdm // Get all admin IDs except the actor, who have this category enabled (or no preference row = default ON) $stmt = $pdo->prepare(" SELECT DISTINCT au.id, au.preferred_language - FROM admin_users au + FROM `" . t('admin_users') . "` au WHERE au.id != ? AND au.is_active = 1 AND NOT EXISTS ( - SELECT 1 FROM push_preferences pp + SELECT 1 FROM `" . t('push_preferences') . "` pp WHERE pp.admin_id = au.id AND pp.category = ? AND pp.enabled = 0 ) "); @@ -148,7 +148,7 @@ function dispatchNotification(string $action, string $description, int $actorAdm // Insert bell notifications for all recipients $insertStmt = $pdo->prepare(" - INSERT INTO notifications (admin_id, category, title_key, body, action_url) + INSERT INTO `" . t('notifications') . "` (admin_id, category, title_key, body, action_url) VALUES (?, ?, ?, ?, ?) "); foreach ($recipientIds as $adminId) { @@ -212,7 +212,7 @@ function dispatchNotification(string $action, string $description, int $actorAdm if ($report->isSubscriptionExpired()) { // Deactivate expired subscriptions $expiredEndpoint = $report->getRequest()->getUri()->__toString(); - $deactivateStmt = $pdo->prepare("UPDATE push_subscriptions SET is_active = 0 WHERE endpoint = ?"); + $deactivateStmt = $pdo->prepare("UPDATE `" . t('push_subscriptions') . "` SET is_active = 0 WHERE endpoint = ?"); $deactivateStmt->execute([$expiredEndpoint]); } } diff --git a/FINAL_PRODUCTION_SYSTEM/functions/qc-compliance.php b/FINAL_PRODUCTION_SYSTEM/functions/qc-compliance.php index 63f2d6a..eb339f5 100644 --- a/FINAL_PRODUCTION_SYSTEM/functions/qc-compliance.php +++ b/FINAL_PRODUCTION_SYSTEM/functions/qc-compliance.php @@ -8,7 +8,7 @@ * Check if QC compliance system is enabled */ function qcIsEnabled(PDO $pdo): bool { - $stmt = $pdo->prepare("SELECT setting_value FROM qc_global_settings WHERE setting_key = 'qc_enabled' LIMIT 1"); + $stmt = $pdo->prepare("SELECT setting_value FROM `" . t('qc_global_settings') . "` WHERE setting_key = 'qc_enabled' LIMIT 1"); $stmt->execute(); $row = $stmt->fetch(PDO::FETCH_ASSOC); return $row && $row['setting_value'] === '1'; @@ -18,7 +18,7 @@ function qcIsEnabled(PDO $pdo): bool { * Return all global QC settings as key-value array */ function qcGetGlobalSettings(PDO $pdo): array { - $stmt = $pdo->query("SELECT setting_key, setting_value FROM qc_global_settings"); + $stmt = $pdo->query("SELECT setting_key, setting_value FROM `" . t('qc_global_settings') . "`"); $settings = []; foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { $settings[$row['setting_key']] = $row['setting_value']; @@ -36,7 +36,7 @@ function qcAutoRegisterMotherboard(PDO $pdo, ?string $manufacturer, ?string $pro } // Check if already exists - $stmt = $pdo->prepare("SELECT id, known_bios_versions FROM qc_motherboard_registry WHERE manufacturer = ? AND product = ? LIMIT 1"); + $stmt = $pdo->prepare("SELECT id, known_bios_versions FROM `" . t('qc_motherboard_registry') . "` WHERE manufacturer = ? AND product = ? LIMIT 1"); $stmt->execute([$manufacturer, $product]); $existing = $stmt->fetch(PDO::FETCH_ASSOC); @@ -48,7 +48,7 @@ function qcAutoRegisterMotherboard(PDO $pdo, ?string $manufacturer, ?string $pro } $stmt = $pdo->prepare(" - UPDATE qc_motherboard_registry + UPDATE `" . t('qc_motherboard_registry') . "` SET times_seen = times_seen + 1, last_seen_at = NOW(), known_bios_versions = ? @@ -61,7 +61,7 @@ function qcAutoRegisterMotherboard(PDO $pdo, ?string $manufacturer, ?string $pro // Insert new motherboard $knownVersions = !empty($biosVersion) ? json_encode([$biosVersion]) : '[]'; $stmt = $pdo->prepare(" - INSERT INTO qc_motherboard_registry (manufacturer, product, known_bios_versions, first_seen_at, last_seen_at, times_seen) + INSERT INTO `" . t('qc_motherboard_registry') . "` (manufacturer, product, known_bios_versions, first_seen_at, last_seen_at, times_seen) VALUES (?, ?, ?, NOW(), NOW(), 1) "); $stmt->execute([$manufacturer, $product, $knownVersions]); @@ -89,7 +89,7 @@ function qcGetEffectiveRules(PDO $pdo, ?string $manufacturer, ?string $product, // Overlay product line enforcement (matched by order number pattern) if (!empty($orderNumber) && mb_strlen($orderNumber) <= 50) { - $stmt = $pdo->query("SELECT id, name, order_pattern, secure_boot_enforcement, bios_enforcement, hackbgrt_enforcement, partition_enforcement, missing_drivers_enforcement FROM product_lines WHERE is_active = 1 ORDER BY LENGTH(order_pattern) DESC"); + $stmt = $pdo->query("SELECT id, name, order_pattern, secure_boot_enforcement, bios_enforcement, hackbgrt_enforcement, partition_enforcement, missing_drivers_enforcement FROM `" . t('product_lines') . "` WHERE is_active = 1 ORDER BY LENGTH(order_pattern) DESC"); foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $line) { $pattern = $line['order_pattern'] ?? ''; if (!preg_match('/^[\p{L}\p{N}#*\-\s]+$/u', $pattern) || mb_strlen($pattern) > 50) continue; @@ -114,7 +114,7 @@ function qcGetEffectiveRules(PDO $pdo, ?string $manufacturer, ?string $product, // Overlay manufacturer defaults if (!empty($manufacturer)) { - $stmt = $pdo->prepare("SELECT * FROM qc_manufacturer_defaults WHERE manufacturer = ? LIMIT 1"); + $stmt = $pdo->prepare("SELECT * FROM `" . t('qc_manufacturer_defaults') . "` WHERE manufacturer = ? LIMIT 1"); $stmt->execute([$manufacturer]); $mfr = $stmt->fetch(PDO::FETCH_ASSOC); if ($mfr) { @@ -129,7 +129,7 @@ function qcGetEffectiveRules(PDO $pdo, ?string $manufacturer, ?string $product, // Overlay model-specific overrides (non-NULL only) if (!empty($manufacturer) && !empty($product)) { - $stmt = $pdo->prepare("SELECT * FROM qc_motherboard_registry WHERE manufacturer = ? AND product = ? LIMIT 1"); + $stmt = $pdo->prepare("SELECT * FROM `" . t('qc_motherboard_registry') . "` WHERE manufacturer = ? AND product = ? LIMIT 1"); $stmt->execute([$manufacturer, $product]); $model = $stmt->fetch(PDO::FETCH_ASSOC); if ($model) { @@ -444,7 +444,7 @@ function qcCheckPartitionLayout(PDO $pdo, int $hardwareInfoId, string $orderNumb if (mb_strlen($orderNumber) > 50) { return null; } - $stmt = $pdo->query("SELECT id, name, order_pattern, enforcement_level FROM product_lines WHERE is_active = 1 ORDER BY LENGTH(order_pattern) DESC"); + $stmt = $pdo->query("SELECT id, name, order_pattern, enforcement_level FROM `" . t('product_lines') . "` WHERE is_active = 1 ORDER BY LENGTH(order_pattern) DESC"); $matchedLine = null; foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $line) { $pattern = $line['order_pattern'] ?? ''; @@ -537,7 +537,7 @@ function qcCheckPartitionLayout(PDO $pdo, int $hardwareInfoId, string $orderNumb } $stmt = $pdo->prepare(" SELECT id, name, disk_size_min_mb, disk_size_max_mb - FROM product_variants + FROM `" . t('product_variants') . "` WHERE line_id = ? AND is_active = 1 AND disk_size_min_mb <= ? AND disk_size_max_mb >= ? ORDER BY disk_size_min_mb DESC @@ -562,7 +562,7 @@ function qcCheckPartitionLayout(PDO $pdo, int $hardwareInfoId, string $orderNumb // 6. Store detected variant in hardware_info $stmt = $pdo->prepare(" - UPDATE hardware_info + UPDATE `" . t('hardware_info') . "` SET detected_variant_id = ?, detected_variant_name = ?, detected_line_name = ? WHERE id = ? "); @@ -570,7 +570,7 @@ function qcCheckPartitionLayout(PDO $pdo, int $hardwareInfoId, string $orderNumb // 7. Load expected partitions $stmt = $pdo->prepare(" - SELECT * FROM product_variant_partitions + SELECT * FROM `" . t('product_variant_partitions') . "` WHERE variant_id = ? ORDER BY partition_order "); @@ -659,7 +659,7 @@ function qcCheckPartitionLayout(PDO $pdo, int $hardwareInfoId, string $orderNumb } else { // Check global setting try { - $gStmt = $pdo->prepare("SELECT setting_value FROM qc_global_settings WHERE setting_key = 'max_unallocated_mb'"); + $gStmt = $pdo->prepare("SELECT setting_value FROM `" . t('qc_global_settings') . "` WHERE setting_key = 'max_unallocated_mb'"); $gStmt->execute(); $gVal = $gStmt->fetchColumn(); if ($gVal !== false && $gVal !== null && $gVal !== '') { @@ -714,7 +714,7 @@ function qcCheckPartitionLayout(PDO $pdo, int $hardwareInfoId, string $orderNumb */ function qcInsertResult(PDO $pdo, int $hardwareInfoId, string $orderNumber, array $check, ?int $registryId, bool $retroactive = false): void { $stmt = $pdo->prepare(" - INSERT INTO qc_compliance_results ( + INSERT INTO `" . t('qc_compliance_results') . "` ( hardware_info_id, order_number, check_type, check_result, enforcement_level, expected_value, actual_value, message, rule_source, motherboard_registry_id, is_retroactive, checked_at @@ -741,7 +741,7 @@ function qcInsertResult(PDO $pdo, int $hardwareInfoId, string $orderNumber, arra function qcHasBlockingIssues(PDO $pdo, int $hardwareInfoId): bool { $stmt = $pdo->prepare(" SELECT COUNT(*) as cnt - FROM qc_compliance_results + FROM `" . t('qc_compliance_results') . "` WHERE hardware_info_id = ? AND enforcement_level = 3 AND check_result = 'fail' @@ -809,7 +809,7 @@ function qcRecheckHistorical(PDO $pdo, ?string $manufacturer = null, ?string $pr $lastId = $hwId; // Delete existing compliance results for this record - $stmt2 = $pdo->prepare("DELETE FROM qc_compliance_results WHERE hardware_info_id = ?"); + $stmt2 = $pdo->prepare("DELETE FROM `" . t('qc_compliance_results') . "` WHERE hardware_info_id = ?"); $stmt2->execute([$hwId]); // Re-run checks @@ -853,7 +853,7 @@ function qcRunChecksRetroactive(PDO $pdo, int $hardwareInfoId, array $hw): array $orderNumber = $hw['order_number'] ?? ''; // Get registry ID (don't auto-register for retroactive) - $stmt = $pdo->prepare("SELECT id FROM qc_motherboard_registry WHERE manufacturer = ? AND product = ? LIMIT 1"); + $stmt = $pdo->prepare("SELECT id FROM `" . t('qc_motherboard_registry') . "` WHERE manufacturer = ? AND product = ? LIMIT 1"); $stmt->execute([$manufacturer, $product]); $reg = $stmt->fetch(PDO::FETCH_ASSOC); $registryId = $reg ? (int) $reg['id'] : null; diff --git a/FINAL_PRODUCTION_SYSTEM/functions/session-helpers.php b/FINAL_PRODUCTION_SYSTEM/functions/session-helpers.php index 8b382ed..9a1521f 100644 --- a/FINAL_PRODUCTION_SYSTEM/functions/session-helpers.php +++ b/FINAL_PRODUCTION_SYSTEM/functions/session-helpers.php @@ -32,9 +32,9 @@ function validateSession($token) { try { $stmt = $pdo->prepare(" SELECT s.*, k.product_key, k.key_status, t.is_active as tech_active - FROM active_sessions s - LEFT JOIN oem_keys k ON s.key_id = k.id - LEFT JOIN technicians t ON s.technician_id = t.technician_id + FROM `" . t('active_sessions') . "` s + LEFT JOIN `" . t('oem_keys') . "` k ON s.key_id = k.id + LEFT JOIN `" . t('technicians') . "` t ON s.technician_id = t.technician_id WHERE s.session_token = ? AND s.is_active = 1 AND s.expires_at > NOW() @@ -54,7 +54,7 @@ function validateSession($token) { function cleanupExpiredSessions($pdo) { try { $stmt = $pdo->prepare(" - UPDATE active_sessions + UPDATE `" . t('active_sessions') . "` SET is_active = 0 WHERE expires_at < NOW() AND is_active = 1 LIMIT " . SESSION_CLEANUP_BATCH . " @@ -82,8 +82,8 @@ function getActiveSession($pdo, $technician_id) { $stmt = $pdo->prepare(" SELECT s.*, k.product_key, k.oem_identifier, k.key_status, k.fail_counter - FROM active_sessions s - LEFT JOIN oem_keys k ON s.key_id = k.id + FROM `" . t('active_sessions') . "` s + LEFT JOIN `" . t('oem_keys') . "` k ON s.key_id = k.id WHERE s.technician_id = ? AND s.is_active = 1 AND s.expires_at > NOW() diff --git a/FINAL_PRODUCTION_SYSTEM/functions/totp-helpers.php b/FINAL_PRODUCTION_SYSTEM/functions/totp-helpers.php index 8a6cd21..32bf5b9 100644 --- a/FINAL_PRODUCTION_SYSTEM/functions/totp-helpers.php +++ b/FINAL_PRODUCTION_SYSTEM/functions/totp-helpers.php @@ -39,7 +39,7 @@ function validateAdminApiSession(): array { function fetchTotpData(PDO $pdo, int $adminId): ?array { $stmt = $pdo->prepare(" SELECT id, totp_secret, totp_enabled, backup_codes - FROM admin_totp_secrets + FROM `" . t('admin_totp_secrets') . "` WHERE admin_id = ? "); $stmt->execute([$adminId]); @@ -78,7 +78,7 @@ function verifyTotpCode(PDO $pdo, int $adminId, string $code, array $totpData, b $backupCodes = array_values($backupCodes); $stmt = $pdo->prepare(" - UPDATE admin_totp_secrets + UPDATE `" . t('admin_totp_secrets') . "` SET backup_codes = ?, last_used_at = NOW() WHERE admin_id = ? "); @@ -92,7 +92,7 @@ function verifyTotpCode(PDO $pdo, int $adminId, string $code, array $totpData, b } else { $totp = TOTP::createFromSecret($totpData['totp_secret']); - $stmt = $pdo->query("SELECT config_value FROM system_config WHERE config_key = 'totp_window'"); + $stmt = $pdo->query("SELECT config_value FROM `" . t('system_config') . "` WHERE config_key = 'totp_window'"); $windowResult = $stmt->fetch(PDO::FETCH_ASSOC); $window = $windowResult ? (int)$windowResult['config_value'] : 1; @@ -100,7 +100,7 @@ function verifyTotpCode(PDO $pdo, int $adminId, string $code, array $totpData, b if ($result['verified']) { $stmt = $pdo->prepare(" - UPDATE admin_totp_secrets + UPDATE `" . t('admin_totp_secrets') . "` SET last_used_at = NOW() WHERE admin_id = ? "); @@ -117,7 +117,7 @@ function verifyTotpCode(PDO $pdo, int $adminId, string $code, array $totpData, b * @return array ['plain' => string[], 'hashed' => string[]] */ function generateBackupCodes(PDO $pdo): array { - $stmt = $pdo->query("SELECT config_value FROM system_config WHERE config_key = 'totp_backup_codes_count'"); + $stmt = $pdo->query("SELECT config_value FROM `" . t('system_config') . "` WHERE config_key = 'totp_backup_codes_count'"); $backupCountResult = $stmt->fetch(PDO::FETCH_ASSOC); $backupCodeCount = $backupCountResult ? (int)$backupCountResult['config_value'] : 10; diff --git a/FINAL_PRODUCTION_SYSTEM/install/ajax.php b/FINAL_PRODUCTION_SYSTEM/install/ajax.php index fedc2a5..a9cbf4c 100644 --- a/FINAL_PRODUCTION_SYSTEM/install/ajax.php +++ b/FINAL_PRODUCTION_SYSTEM/install/ajax.php @@ -469,13 +469,13 @@ function handleInstallDbInit() { } // Bootstrap schema_versions first (the tracking table for all later migrations). + // Run through installerRunSqlFile so the `#__` prefix substitution kicks in. $schemaFile = $sqlDir . '/schema_versions_migration.sql'; if (file_exists($schemaFile)) { - try { - $pdo->exec(file_get_contents($schemaFile)); - } catch (PDOException $e) { /* probably already exists */ } + installerRunSqlFile($pdo, file_get_contents($schemaFile)); // Best-effort; ignore errors. } + $svTable = '`' . installerT('schema_versions') . '`'; $list = []; foreach (installerMigrationList() as [$file, $version]) { $filePath = $sqlDir . '/' . $file; @@ -484,7 +484,7 @@ function handleInstallDbInit() { if ($exists) { try { - $stmt = $pdo->prepare("SELECT COUNT(*) AS c FROM schema_versions WHERE filename = ?"); + $stmt = $pdo->prepare("SELECT COUNT(*) AS c FROM {$svTable} WHERE filename = ?"); $stmt->execute([$file]); $applied = ((int)$stmt->fetchColumn()) > 0; } catch (PDOException $e) { /* table missing → not applied */ } @@ -532,9 +532,11 @@ function handleInstallDbStep() { return; } + $svTable = '`' . installerT('schema_versions') . '`'; + // Skip if already applied try { - $stmt = $pdo->prepare("SELECT COUNT(*) AS c FROM schema_versions WHERE filename = ?"); + $stmt = $pdo->prepare("SELECT COUNT(*) AS c FROM {$svTable} WHERE filename = ?"); $stmt->execute([$file]); if ((int)$stmt->fetchColumn() > 0) { echo json_encode(['file' => $file, 'success' => true, 'status' => 'skipped', 'message' => 'Already applied']); @@ -548,7 +550,7 @@ function handleInstallDbStep() { if ($result['ok']) { try { $checksum = hash('sha256', $sql); - $stmt = $pdo->prepare("INSERT IGNORE INTO schema_versions (version, filename, checksum) VALUES (?, ?, ?)"); + $stmt = $pdo->prepare("INSERT IGNORE INTO {$svTable} (version, filename, checksum) VALUES (?, ?, ?)"); $stmt->execute([$version, $file, $checksum]); } catch (PDOException $e) { /* ignore */ } @@ -566,7 +568,7 @@ function handleInstallDbStep() { if (preg_match('/Duplicate|already exists|1060|1061|1050|1062/i', $result['error'])) { try { $checksum = hash('sha256', $sql); - $stmt = $pdo->prepare("INSERT IGNORE INTO schema_versions (version, filename, checksum) VALUES (?, ?, ?)"); + $stmt = $pdo->prepare("INSERT IGNORE INTO {$svTable} (version, filename, checksum) VALUES (?, ?, ?)"); $stmt->execute([$version, $file, $checksum]); } catch (PDOException $e2) { /* ignore */ } @@ -602,12 +604,13 @@ function handleInstallDbAll() { return; } - // Bootstrap schema_versions + // Bootstrap schema_versions (run through installerRunSqlFile so prefix substitution applies) $schemaFile = $sqlDir . '/schema_versions_migration.sql'; if (file_exists($schemaFile)) { - try { $pdo->exec(file_get_contents($schemaFile)); } catch (PDOException $e) { /* ok */ } + installerRunSqlFile($pdo, file_get_contents($schemaFile)); } + $svTable = '`' . installerT('schema_versions') . '`'; $results = []; foreach (installerMigrationList() as $i => [$file, $version]) { if ($i === 0) { @@ -622,7 +625,7 @@ function handleInstallDbAll() { } try { - $stmt = $pdo->prepare("SELECT COUNT(*) AS c FROM schema_versions WHERE filename = ?"); + $stmt = $pdo->prepare("SELECT COUNT(*) AS c FROM {$svTable} WHERE filename = ?"); $stmt->execute([$file]); if ((int)$stmt->fetchColumn() > 0) { $results[] = ['file' => $file, 'status' => 'skipped', 'message' => 'Already applied']; @@ -636,14 +639,14 @@ function handleInstallDbAll() { if ($r['ok']) { try { $checksum = hash('sha256', $sql); - $stmt = $pdo->prepare("INSERT IGNORE INTO schema_versions (version, filename, checksum) VALUES (?, ?, ?)"); + $stmt = $pdo->prepare("INSERT IGNORE INTO {$svTable} (version, filename, checksum) VALUES (?, ?, ?)"); $stmt->execute([$version, $file, $checksum]); } catch (PDOException $e) { /* ignore */ } $results[] = ['file' => $file, 'status' => 'ok', 'message' => 'Applied (' . $r['stmts_run'] . ' statements)']; } elseif (preg_match('/Duplicate|already exists|1060|1061|1050|1062/i', $r['error'])) { try { $checksum = hash('sha256', $sql); - $stmt = $pdo->prepare("INSERT IGNORE INTO schema_versions (version, filename, checksum) VALUES (?, ?, ?)"); + $stmt = $pdo->prepare("INSERT IGNORE INTO {$svTable} (version, filename, checksum) VALUES (?, ?, ?)"); $stmt->execute([$version, $file, $checksum]); } catch (PDOException $e) { /* ignore */ } $results[] = ['file' => $file, 'status' => 'ok', 'message' => 'Applied (some objects already existed)']; @@ -675,6 +678,17 @@ function installerRunSqlFile(PDO $pdo, string $sql): array { $sql = preg_replace('/^\s*(START\s+TRANSACTION|BEGIN)\s*;\s*$/im', '', $sql); $sql = preg_replace('/^\s*COMMIT\s*;\s*$/im', '', $sql); + // Substitute table prefix sentinel `#__` with the configured prefix. + // Empty prefix → identical to pre-prefix schema. + $prefix = (string)($_SESSION['install_db']['prefix'] ?? ''); + $sql = str_replace('#__', $prefix, $sql); + + // Defense-in-depth: if substitution somehow missed and `#__` remains, + // abort loudly rather than send invalid SQL. + if (strpos($sql, '#__') !== false) { + return ['ok' => false, 'stmts_run' => 0, 'error' => 'Internal error: unsubstituted `#__` sentinel still present after prefix replacement.']; + } + $stmts = installerSplitSql($sql); $count = 0; foreach ($stmts as $stmt) { @@ -690,6 +704,16 @@ function installerRunSqlFile(PDO $pdo, string $sql): array { return ['ok' => true, 'stmts_run' => $count, 'error' => '']; } +/** + * Resolve the prefixed name for a logical table during installation. + * Mirrors functions/db-helpers.php t() but reads from $_SESSION since + * the installer runs before config.php exists. + */ +function installerT(string $name): string { + $prefix = (string)($_SESSION['install_db']['prefix'] ?? ''); + return $prefix . $name; +} + /** * Split a multi-statement SQL string into individual statements. * Respects backticks, single/double-quoted strings, line comments (-- ...), @@ -815,30 +839,33 @@ function handleCreateAdmin() { return; } + $adminTable = '`' . installerT('admin_users') . '`'; + $aclTable = '`' . installerT('acl_roles') . '`'; + try { // Check if admin already exists - $stmt = $pdo->prepare("SELECT id FROM admin_users WHERE username = ?"); + $stmt = $pdo->prepare("SELECT id FROM {$adminTable} WHERE username = ?"); $stmt->execute([$username]); if ($stmt->fetch()) { // Update existing $hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]); - $stmt = $pdo->prepare("UPDATE admin_users SET password_hash = ?, full_name = ?, email = ?, role = 'super_admin', is_active = 1, must_change_password = 0, failed_login_attempts = 0, locked_until = NULL WHERE username = ?"); + $stmt = $pdo->prepare("UPDATE {$adminTable} SET password_hash = ?, full_name = ?, email = ?, role = 'super_admin', is_active = 1, must_change_password = 0, failed_login_attempts = 0, locked_until = NULL WHERE username = ?"); $stmt->execute([$hash, $fullName, $email, $username]); echo json_encode(['success' => true, 'message' => "Admin account '{$username}' updated."]); } else { // Create new $hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]); - $stmt = $pdo->prepare("INSERT INTO admin_users (username, full_name, email, password_hash, role, is_active, must_change_password) VALUES (?, ?, ?, ?, 'super_admin', 1, 0)"); + $stmt = $pdo->prepare("INSERT INTO {$adminTable} (username, full_name, email, password_hash, role, is_active, must_change_password) VALUES (?, ?, ?, ?, 'super_admin', 1, 0)"); $stmt->execute([$username, $fullName, $email, $hash]); // Try to assign ACL role if table exists try { $adminId = $pdo->lastInsertId(); - $roleStmt = $pdo->prepare("SELECT id FROM acl_roles WHERE name = 'super_admin' LIMIT 1"); + $roleStmt = $pdo->prepare("SELECT id FROM {$aclTable} WHERE name = 'super_admin' LIMIT 1"); $roleStmt->execute(); $role = $roleStmt->fetch(PDO::FETCH_ASSOC); if ($role) { - $pdo->prepare("UPDATE admin_users SET custom_role_id = ? WHERE id = ?")->execute([$role['id'], $adminId]); + $pdo->prepare("UPDATE {$adminTable} SET custom_role_id = ? WHERE id = ?")->execute([$role['id'], $adminId]); } } catch (PDOException $e) { // ACL tables might not exist — that's fine @@ -873,7 +900,9 @@ function handleFinalize() { } // ── Write config.php ── - $configContent = generateConfig($host, $port, $user, $pass, $name, $timezone); + $prefix = (string)($_SESSION['install_db']['prefix'] ?? ''); + $charset = (string)($_SESSION['install_db']['charset'] ?? 'utf8mb4'); + $configContent = generateConfig($host, $port, $user, $pass, $name, $timezone, $prefix, $charset); $configPath = realpath(__DIR__ . '/..') . '/config.php'; if (file_put_contents($configPath, $configContent) === false) { @@ -888,6 +917,12 @@ function handleFinalize() { try { $pdo = getInstallerPdo(); if ($pdo) { + $tConfig = '`' . installerT('system_config') . '`'; + $tTech = '`' . installerT('technicians') . '`'; + $tAdmin = '`' . installerT('admin_users') . '`'; + $tTrustedN = '`' . installerT('trusted_networks') . '`'; + $tAdminIp = '`' . installerT('admin_ip_whitelist') . '`'; + $settings = [ 'system_name' => $systemName, 'server_url' => $serverUrl, @@ -895,13 +930,13 @@ function handleFinalize() { 'timezone' => $timezone, ]; foreach ($settings as $key => $value) { - $stmt = $pdo->prepare("INSERT INTO system_config (config_key, config_value, description) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE config_value = VALUES(config_value)"); + $stmt = $pdo->prepare("INSERT INTO {$tConfig} (config_key, config_value, description) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE config_value = VALUES(config_value)"); $stmt->execute([$key, $value, "Set by installer"]); } // Delete demo technician if it exists try { - $pdo->exec("DELETE FROM technicians WHERE technician_id = 'demo' AND notes LIKE '%Demo account%'"); + $pdo->exec("DELETE FROM {$tTech} WHERE technician_id = 'demo' AND notes LIKE '%Demo account%'"); } catch (PDOException $e) { /* ignore */ } // ── Auto-detect installer's network and add as trusted ── @@ -909,7 +944,7 @@ function handleFinalize() { $clientIp = getClientIp(); if ($clientIp && $clientIp !== 'unknown') { // Get admin ID for foreign key - $adminStmt = $pdo->prepare("SELECT id FROM admin_users WHERE username = ? LIMIT 1"); + $adminStmt = $pdo->prepare("SELECT id FROM {$tAdmin} WHERE username = ? LIMIT 1"); $adminStmt->execute([$adminUser]); $adminRow = $adminStmt->fetch(PDO::FETCH_ASSOC); $adminId = $adminRow ? $adminRow['id'] : null; @@ -920,7 +955,7 @@ function handleFinalize() { // Add to trusted_networks (for 2FA bypass + USB auth) try { $stmt = $pdo->prepare(" - INSERT INTO trusted_networks (network_name, ip_range, bypass_2fa, allow_usb_auth, description, created_by_admin_id) + INSERT INTO {$tTrustedN} (network_name, ip_range, bypass_2fa, allow_usb_auth, description, created_by_admin_id) VALUES (?, ?, 1, 1, ?, ?) "); $stmt->execute([ @@ -934,7 +969,7 @@ function handleFinalize() { // Add to admin_ip_whitelist (for admin panel access) try { $stmt = $pdo->prepare(" - INSERT INTO admin_ip_whitelist (ip_address, ip_range, description, created_by) + INSERT INTO {$tAdminIp} (ip_address, ip_range, description, created_by) VALUES (?, ?, ?, ?) "); $stmt->execute([ @@ -1144,9 +1179,20 @@ function calculateSubnet(string $ip, int $prefix = 24): string { /** * Generate the production config.php content */ -function generateConfig(string $host, string $port, string $user, string $pass, string $name, string $timezone): string { +function generateConfig( + string $host, + string $port, + string $user, + string $pass, + string $name, + string $timezone, + string $prefix = '', + string $charset = 'utf8mb4' +): string { // Escape single quotes in password for PHP string - $passEscaped = addcslashes($pass, "'\\"); + $passEscaped = addcslashes($pass, "'\\"); + $prefixEscaped = addcslashes($prefix, "'\\"); + $charsetEsc = preg_replace('/[^a-z0-9_]/', '', $charset) ?: 'utf8mb4'; return <<<'PHP_HEADER' '{$host}',\n" . " 'dbname' => '{$name}',\n" . " 'username' => '{$user}',\n" . " 'password' => '{$passEscaped}',\n" - . " 'charset' => 'utf8mb4',\n" + . " 'charset' => '{$charsetEsc}',\n" . " 'port' => {$port},\n" . "];\n\n" . <<<'PHP_BODY' @@ -1355,6 +1402,13 @@ function installerCheckIncompleteState(): void { $host = $hM[1]; $name = $nM[1]; $user = $uM[1]; $pass = $pM[1]; $port = (int)$portM[1]; if (strtolower($host) === 'localhost') $host = '127.0.0.1'; + // Read DB_PREFIX from config (empty for legacy installs). + $prefix = ''; + if (preg_match("/define\(\s*'DB_PREFIX'\s*,\s*'([^']*)'\s*\)/", $configSrc, $pxM)) { + $prefix = $pxM[1]; + } + $adminTable = $prefix . 'admin_users'; + try { $dsn = "mysql:host={$host};port={$port};dbname={$name};charset=utf8mb4"; $pdo = new PDO($dsn, $user, $pass, [ @@ -1363,15 +1417,15 @@ function installerCheckIncompleteState(): void { ]); // admin_users table missing → install was never completed. - $stmt = $pdo->query("SHOW TABLES LIKE 'admin_users'"); + $stmt = $pdo->query("SHOW TABLES LIKE " . $pdo->quote($adminTable)); if (!$stmt->fetch()) { - installerLog("auto-unlock: admin_users table missing — clearing install.lock"); + installerLog("auto-unlock: {$adminTable} missing — clearing install.lock"); @unlink($lockPath); return; } // admin_users empty → install was never completed. - $cnt = (int)$pdo->query("SELECT COUNT(*) FROM admin_users")->fetchColumn(); + $cnt = (int)$pdo->query("SELECT COUNT(*) FROM `{$adminTable}`")->fetchColumn(); if ($cnt === 0) { installerLog("auto-unlock: admin_users empty — clearing install.lock"); @unlink($lockPath); @@ -1447,17 +1501,19 @@ function handleHealth(): void { $expectTables = ['admin_users', 'oem_keys', 'technicians', 'system_config', 'schema_versions']; foreach ($expectTables as $t) { + $physical = installerT($t); try { - $stmt = $pdo->query("SHOW TABLES LIKE '" . str_replace("'", '', $t) . "'"); + $stmt = $pdo->query("SHOW TABLES LIKE " . $pdo->quote($physical)); $found = (bool)$stmt->fetch(); - $checks[] = ['label' => "Table {$t}", 'status' => $found ? 'pass' : 'fail']; + $checks[] = ['label' => "Table {$physical}", 'status' => $found ? 'pass' : 'fail']; } catch (PDOException $e) { - $checks[] = ['label' => "Table {$t}", 'status' => 'fail']; + $checks[] = ['label' => "Table {$physical}", 'status' => 'fail']; } } try { - $admins = (int)$pdo->query("SELECT COUNT(*) FROM admin_users")->fetchColumn(); + $adminTable = '`' . installerT('admin_users') . '`'; + $admins = (int)$pdo->query("SELECT COUNT(*) FROM {$adminTable}")->fetchColumn(); $checks[] = ['label' => 'Admin accounts', 'status' => $admins > 0 ? 'pass' : 'fail', 'value' => $admins]; } catch (PDOException $e) { $checks[] = ['label' => 'Admin accounts', 'status' => 'fail']; diff --git a/FINAL_PRODUCTION_SYSTEM/install/index.php b/FINAL_PRODUCTION_SYSTEM/install/index.php index f55d2e1..d7b7169 100644 --- a/FINAL_PRODUCTION_SYSTEM/install/index.php +++ b/FINAL_PRODUCTION_SYSTEM/install/index.php @@ -26,14 +26,20 @@ && preg_match("/'password'\s*=>\s*'([^']*)'/", $configSrc, $pM) && preg_match("/'port'\s*=>\s*(\d+)/", $configSrc, $portM)) { $autoHost = strtolower($hM[1]) === 'localhost' ? '127.0.0.1' : $hM[1]; + // Read DB_PREFIX from config (empty for legacy installs). + $autoPrefix = ''; + if (preg_match("/define\(\s*'DB_PREFIX'\s*,\s*'([^']*)'\s*\)/", $configSrc, $pxM)) { + $autoPrefix = $pxM[1]; + } + $autoAdminTable = $autoPrefix . 'admin_users'; try { $autoPdo = new PDO( "mysql:host={$autoHost};port={$portM[1]};dbname={$nM[1]};charset=utf8mb4", $uM[1], $pM[1], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 5] ); - $hasAdminTable = (bool) $autoPdo->query("SHOW TABLES LIKE 'admin_users'")->fetch(); - $adminCount = $hasAdminTable ? (int) $autoPdo->query("SELECT COUNT(*) FROM admin_users")->fetchColumn() : 0; + $hasAdminTable = (bool) $autoPdo->query("SHOW TABLES LIKE " . $autoPdo->quote($autoAdminTable))->fetch(); + $adminCount = $hasAdminTable ? (int) $autoPdo->query("SELECT COUNT(*) FROM `{$autoAdminTable}`")->fetchColumn() : 0; if (!$hasAdminTable || $adminCount === 0) { @unlink($lockFile); @file_put_contents( diff --git a/FINAL_PRODUCTION_SYSTEM/secure-admin.php b/FINAL_PRODUCTION_SYSTEM/secure-admin.php index f043af9..a91e547 100644 --- a/FINAL_PRODUCTION_SYSTEM/secure-admin.php +++ b/FINAL_PRODUCTION_SYSTEM/secure-admin.php @@ -39,7 +39,7 @@ function checkIPWhitelist() { // Get all active whitelist entries $stmt = $pdo->prepare(" - SELECT ip_address, ip_range FROM admin_ip_whitelist + SELECT ip_address, ip_range FROM `" . t('admin_ip_whitelist') . "` WHERE is_active = 1 "); $stmt->execute(); @@ -77,7 +77,7 @@ function checkIPWhitelist() { // Handle logout if (isset($_GET['logout'])) { if (isset($_SESSION['admin_token'])) { - $stmt = $pdo->prepare("UPDATE admin_sessions SET is_active = 0 WHERE session_token = ?"); + $stmt = $pdo->prepare("UPDATE `" . t('admin_sessions') . "` SET is_active = 0 WHERE session_token = ?"); $stmt->execute([$_SESSION['admin_token']]); logAdminActivity($_SESSION['admin_id'], $_SESSION['session_id'], 'LOGOUT', 'User logout'); } diff --git a/FINAL_PRODUCTION_SYSTEM/setup/index.php b/FINAL_PRODUCTION_SYSTEM/setup/index.php index 6dcc95e..9d0e839 100644 --- a/FINAL_PRODUCTION_SYSTEM/setup/index.php +++ b/FINAL_PRODUCTION_SYSTEM/setup/index.php @@ -500,7 +500,7 @@ function setupAdminAccount() { $db['username'], $db['password']); $stmt = $pdo->prepare(" - INSERT INTO admin_users (username, full_name, email, password_hash, role, must_change_password) + INSERT INTO `" . t('admin_users') . "` (username, full_name, email, password_hash, role, must_change_password) VALUES (?, ?, ?, ?, 'super_admin', 0) "); $stmt->execute([$admin['username'], $admin['full_name'], $admin['email'], $admin['password']]); @@ -524,7 +524,7 @@ function applySystemConfiguration() { ['admin_ip_whitelist_enabled', $config['enable_ip_whitelist'] ? '1' : '0'], ]; - $stmt = $pdo->prepare("UPDATE system_config SET config_value = ? WHERE config_key = ?"); + $stmt = $pdo->prepare("UPDATE `" . t('system_config') . "` SET config_value = ? WHERE config_key = ?"); foreach ($configs as $cfg) { $stmt->execute([$cfg[1], $cfg[0]]); } diff --git a/tools/prefix-codemod.php b/tools/prefix-codemod.php new file mode 100644 index 0000000..06a932d --- /dev/null +++ b/tools/prefix-codemod.php @@ -0,0 +1,398 @@ + overrides default detection. Useful inside Docker where +// FINAL_PRODUCTION_SYSTEM lives at /var/www/html/activate, not next to tools/. +$customRoot = ''; +foreach ($argv as $i => $arg) { + if ($arg === '--root' && isset($argv[$i + 1])) { + $customRoot = $argv[$i + 1]; + break; + } + if (str_starts_with($arg, '--root=')) { + $customRoot = substr($arg, 7); + break; + } +} + +if ($customRoot !== '') { + $appRoot = realpath($customRoot); +} else { + $root = realpath(__DIR__ . '/..'); + $appRoot = $root . DIRECTORY_SEPARATOR . 'FINAL_PRODUCTION_SYSTEM'; +} +if (!is_dir($appRoot)) { + fwrite(STDERR, "FINAL_PRODUCTION_SYSTEM not found at {$appRoot}\n"); + exit(1); +} + +$dbDir = $appRoot . DIRECTORY_SEPARATOR . 'database'; + +// ── 1. Discover canonical table list ────────────────────────────── +$tables = discoverTables($dbDir); +sort($tables); +if (!$quiet) { + fwrite(STDOUT, "Discovered " . count($tables) . " tables.\n"); +} + +// Sort by length descending so longer names match before substring siblings +// (e.g. `admin_users` before `admin`). +usort($tables, fn($a, $b) => strlen($b) - strlen($a)); + +// Build deny list — never rewrite these even if they appear in SQL contexts. +// They are SQL keywords / column names that happen to look like table names. +$denyAlias = [ + 'config_value', // column in system_config + 'value', // generic column name + 'key', // SQL keyword + common column + 'name', // common column + 'role', // common column +]; +$tables = array_values(array_diff($tables, $denyAlias)); + +if (count($tables) < 5) { + fwrite(STDERR, "Refusing to run: only " . count($tables) . " tables discovered. Something is wrong.\n"); + exit(2); +} + +// ── 2. SQL pass ─────────────────────────────────────────────────── +$sqlFiles = glob($dbDir . '/*.sql'); +$sqlChanges = 0; +$sqlFilesChanged = []; + +foreach ($sqlFiles as $f) { + $orig = file_get_contents($f); + $new = rewriteSqlBody($orig, $tables); + if ($new !== $orig) { + $sqlChanges += substr_count($new, '#__') - substr_count($orig, '#__'); + $sqlFilesChanged[] = basename($f); + if ($apply) { + file_put_contents($f, $new); + } + } +} + +if (!$quiet) { + fwrite(STDOUT, "SQL: " . count($sqlFilesChanged) . " files changed, +{$sqlChanges} `#__` markers.\n"); +} + +// ── 3. PHP pass ─────────────────────────────────────────────────── +$phpFiles = collectPhpFiles($appRoot); +$phpChanges = 0; +$phpFilesChanged = []; + +foreach ($phpFiles as $f) { + $orig = file_get_contents($f); + $new = rewritePhpBody($orig, $tables); + if ($new !== $orig) { + $phpFilesChanged[] = str_replace($appRoot . DIRECTORY_SEPARATOR, '', $f); + $diffLines = countDiff($orig, $new); + $phpChanges += $diffLines; + if ($apply) { + file_put_contents($f, $new); + } + } +} + +if (!$quiet) { + fwrite(STDOUT, "PHP: " . count($phpFilesChanged) . " files changed, ~{$phpChanges} site rewrites.\n"); +} + +// ── 4. Verify pass (when --verify) ──────────────────────────────── +if ($verify) { + $stillBad = []; + foreach ($sqlFiles as $f) { + $body = file_get_contents($f); + foreach ($tables as $t) { + // Look for un-prefixed backticked refs. + // Pattern: `tablename` not preceded by `#__ + if (preg_match('/(?isFile()) continue; + if (substr($f->getFilename(), -4) !== '.php') continue; + $path = str_replace('\\', '/', $f->getPathname()); + foreach ($skip as $s) { + if (strpos($path, $s) !== false) continue 2; + } + $files[] = $f->getPathname(); + } + return $files; +} + +/** + * Rewrite SQL body: `tablename` → `#__tablename` for every canonical table. + * Idempotent: skips already-prefixed. + */ +function rewriteSqlBody(string $body, array $tables): string { + $kw = '(?:CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?|DROP\s+TABLE(?:\s+IF\s+EXISTS)?|ALTER\s+TABLE|INSERT\s+INTO|REPLACE\s+INTO|SELECT\s+(?:[^;]*?)FROM|UPDATE|DELETE\s+FROM|FROM|JOIN|REFERENCES|TRUNCATE(?:\s+TABLE)?|RENAME\s+TABLE|LOCK\s+TABLES|DESCRIBE|EXPLAIN)'; + + foreach ($tables as $t) { + // ── Pattern A: backticked refs ──────────────────────────── + // (? Date: Thu, 7 May 2026 07:29:52 +0300 Subject: [PATCH 09/15] =?UTF-8?q?P2:=20Installer=20resilience=20=E2=80=94?= =?UTF-8?q?=20resume,=20retry/skip,=20structured=20log,=20health=20probe?= =?UTF-8?q?=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2 of the multi-panel installer plan. Adds operational robustness on top of P0 hardening + P1 prefix support. Changes: 1. Resumable installer - install/.progress.json breadcrumb file. Each step writes its number and timestamp on completion via the new progress_set action. - On page boot, JS calls progress_get; if last_step >= 1, prompts user "Resume from step N+1?" with Cancel = start over (which clears the file). Bypassed for fresh installs. - handleFinalize unlinks the breadcrumb on success (install complete). 2. Per-migration retry / skip - Step 3 UI: when a migration errors, inline Retry + Skip buttons appear next to the failed row. - Retry: re-runs install_db_step for that file. On success, the migration loop resumes from the next file. - Skip: prompts a hard-yes confirmation, then calls the new migration_skip action which inserts a row into schema_versions with checksum prefix `SKIPPED:` (so future audits can tell apart successful applies from forced skips). Loop resumes. - Both paths respect the canonical migration whitelist. 3. Structured install.log - installerLog() is now called from auto-unlock recovery, finalize completion, progress_set, migration_skip, and progress_clear. - Audit format: `[YYYY-MM-DD HH:MM:SS] event_name: details`. 4. Health-probe button on step 6 - "Run health check" button next to "Open Admin Panel". - Calls existing handleHealth action; renders pass/fail per check (DB connect, presence of {prefix}admin_users, oem_keys, technicians, system_config, schema_versions, plus admin account count). 5. install.lock content extended - Now persists db_prefix and db_charset alongside installer_ver, admin_username, php_version, server_software. Makes post-install forensics easier. 6. .gitignore - install/install.log and install/.progress.json — runtime per-host artifacts, never to be committed. Verified live: - POST progress_set/progress_get round-trip works - Lint clean on ajax.php + index.php - 14/14 frontend tests pass Co-authored-by: ChesnoTech <263363000+ChesnoTech@users.noreply.github.com> --- .gitignore | 4 + FINAL_PRODUCTION_SYSTEM/install/ajax.php | 115 ++++++++++++++++++++++ FINAL_PRODUCTION_SYSTEM/install/index.php | 108 +++++++++++++++++++- 3 files changed, 222 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 9aa8908..c0bd259 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,10 @@ logs/ # ── Uploaded client artifacts (per-instance, regenerated at runtime) ── FINAL_PRODUCTION_SYSTEM/uploads/client-resources/ +# ── Installer runtime artifacts (per-host, never committed) ──────── +FINAL_PRODUCTION_SYSTEM/install/install.log +FINAL_PRODUCTION_SYSTEM/install/.progress.json + # ── PHP Dependencies (managed by Composer) ──────────────── FINAL_PRODUCTION_SYSTEM/vendor/ diff --git a/FINAL_PRODUCTION_SYSTEM/install/ajax.php b/FINAL_PRODUCTION_SYSTEM/install/ajax.php index a9cbf4c..d361a41 100644 --- a/FINAL_PRODUCTION_SYSTEM/install/ajax.php +++ b/FINAL_PRODUCTION_SYSTEM/install/ajax.php @@ -55,6 +55,18 @@ case 'health': handleHealth(); break; + case 'progress_get': + handleProgressGet(); + break; + case 'progress_set': + handleProgressSet(); + break; + case 'progress_clear': + handleProgressClear(); + break; + case 'migration_skip': + handleMigrationSkip(); + break; default: echo json_encode(['success' => false, 'message' => 'Unknown action']); } @@ -996,10 +1008,16 @@ function handleFinalize() { 'admin_username' => $adminUser, 'php_version' => PHP_VERSION, 'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? 'unknown', + 'db_prefix' => $prefix, + 'db_charset' => $charset, ]; $lockPath = realpath(__DIR__ . '/..') . '/install.lock'; file_put_contents($lockPath, json_encode($lockData, JSON_PRETTY_PRINT)); + // ── Clear resume breadcrumb (install is complete) ── + @unlink(installerProgressPath()); + installerLog("finalize: install complete; admin={$adminUser}, prefix='" . $prefix . "', charset={$charset}"); + // ── Response ── echo json_encode([ 'success' => true, @@ -1535,3 +1553,100 @@ function installerLog(string $line): void { FILE_APPEND ); } + +// ═══════════════════════════════════════════════════════════════ +// P2: Resumable installer (progress breadcrumb file) +// ═══════════════════════════════════════════════════════════════ + +function installerProgressPath(): string { + return __DIR__ . '/.progress.json'; +} + +/** + * Read the progress breadcrumb. Returns the highest step the user has + * completed plus a per-step timestamp map. + */ +function handleProgressGet(): void { + $path = installerProgressPath(); + if (!file_exists($path)) { + echo json_encode(['success' => true, 'progress' => null]); + return; + } + $data = json_decode(@file_get_contents($path), true); + if (!is_array($data)) { + echo json_encode(['success' => true, 'progress' => null]); + return; + } + echo json_encode(['success' => true, 'progress' => $data]); +} + +/** + * Persist a step completion breadcrumb. + * Body: { step: 1..6 } + */ +function handleProgressSet(): void { + $step = (int)($_POST['step'] ?? 0); + if ($step < 1 || $step > 6) { + echo json_encode(['success' => false, 'message' => 'Invalid step']); + return; + } + + $path = installerProgressPath(); + $current = ['steps' => [], 'last_step' => 0, 'updated_at' => date('Y-m-d H:i:s')]; + if (file_exists($path)) { + $loaded = json_decode(@file_get_contents($path), true); + if (is_array($loaded)) $current = array_merge($current, $loaded); + } + + $current['steps'][(string)$step] = date('Y-m-d H:i:s'); + if ($step > (int)($current['last_step'] ?? 0)) { + $current['last_step'] = $step; + } + $current['updated_at'] = date('Y-m-d H:i:s'); + + @file_put_contents($path, json_encode($current, JSON_PRETTY_PRINT)); + installerLog("step_done: {$step}"); + echo json_encode(['success' => true, 'progress' => $current]); +} + +/** + * Wipe the progress file. Used on user-initiated "Start Over". + */ +function handleProgressClear(): void { + @unlink(installerProgressPath()); + installerLog("progress_cleared by user"); + echo json_encode(['success' => true]); +} + +/** + * Mark a migration as forcibly skipped after an error (user clicked Skip + * on the per-migration retry UI). Records in schema_versions with the + * `checksum` column suffixed `:SKIPPED` so we can tell apart from + * successful applies. + * + * Body: { file: 'install.sql', version: 1, error: 'optional msg' } + */ +function handleMigrationSkip(): void { + $pdo = getInstallerPdo(); + if (!$pdo) return; + + $file = $_POST['file'] ?? ''; + $version = (int)($_POST['version'] ?? 0); + $error = (string)($_POST['error'] ?? ''); + + $allowed = array_column(installerMigrationList(), 0); + if (!in_array($file, $allowed, true)) { + echo json_encode(['success' => false, 'message' => "Migration '{$file}' not on the canonical list."]); + return; + } + + $svTable = '`' . installerT('schema_versions') . '`'; + try { + $stmt = $pdo->prepare("INSERT IGNORE INTO {$svTable} (version, filename, checksum) VALUES (?, ?, ?)"); + $stmt->execute([$version, $file, 'SKIPPED:' . substr(hash('sha256', $error . microtime()), 0, 16)]); + installerLog("migration_skipped: {$file} (error: " . substr($error, 0, 200) . ")"); + echo json_encode(['success' => true, 'message' => "Migration '{$file}' marked as skipped."]); + } catch (PDOException $e) { + echo json_encode(['success' => false, 'message' => $e->getMessage()]); + } +} diff --git a/FINAL_PRODUCTION_SYSTEM/install/index.php b/FINAL_PRODUCTION_SYSTEM/install/index.php index d7b7169..ac8b424 100644 --- a/FINAL_PRODUCTION_SYSTEM/install/index.php +++ b/FINAL_PRODUCTION_SYSTEM/install/index.php @@ -616,9 +616,13 @@ - - Open Admin Panel → - +
+ + Open Admin Panel → + + +
+ @@ -645,9 +649,34 @@ function goStep(n) { }); currentStep = n; + // Persist breadcrumb so reload picks up where the user left off (P2). + if (n > 1) post('progress_set', { step: n - 1 }).catch(() => {}); window.scrollTo(0, 0); } +// ── Resume on boot (P2): if .progress.json exists, jump to last+1 step ── +async function maybeResumeFromProgress() { + try { + const r = await post('progress_get', {}); + if (r.success && r.progress && r.progress.last_step) { + const last = parseInt(r.progress.last_step, 10); + if (last >= 1 && last < 6) { + const ok = confirm( + 'Previous installation in progress (step ' + last + ' completed at ' + + (r.progress.steps[last] || 'unknown') + ').\n\nResume from step ' + + (last + 1) + '? (Cancel = start over)' + ); + if (ok) { + goStep(last + 1); + return true; + } + await post('progress_clear', {}); + } + } + } catch (e) { /* progress endpoint optional */ } + return false; +} + // ── AJAX Helper ────────────────────────────────────── async function post(action, data = {}) { const body = new URLSearchParams({ action, ...data }); @@ -845,6 +874,14 @@ function renderChecks(containerId, checks) { if (!r.success && r.status === 'error') { hadError = true; + // Append inline Retry / Skip buttons next to the error row + const errRow = log.lastChild; + const btnBar = document.createElement('div'); + btnBar.style.cssText = 'margin:6px 0 12px 22px;display:flex;gap:8px;'; + btnBar.innerHTML = + `` + + ``; + errRow.appendChild(btnBar); // Stop loop on first hard error so user can read it. break; } @@ -855,12 +892,70 @@ function renderChecks(containerId, checks) { $('migStatus').style.color = 'var(--success)'; $('migNext').classList.remove('hidden'); } else { - $('migStatus').textContent = 'Installation failed'; + $('migStatus').textContent = 'Installation failed — click Retry or Skip on the failed step'; $('migStatus').style.color = 'var(--danger)'; $('migBack').disabled = false; } } +// ── P2: Per-migration retry/skip ───────────────────── +async function retryMigration(file, version, btn) { + btn.disabled = true; + btn.textContent = 'Retrying...'; + const r = await post('install_db_step', { ...dbCredentials, file, version }); + btn.disabled = false; + btn.textContent = 'Retry'; + const log = document.getElementById('migLog'); + const cls = r.status === 'ok' ? 'ok' : r.status === 'skipped' ? 'skip' : 'err'; + const icon = r.status === 'ok' ? '✓' : r.status === 'skipped' ? '→' : '✗'; + log.innerHTML += `
${icon} ${file} (retry): ${r.message || ''}
`; + log.scrollTop = log.scrollHeight; + if (r.success && r.status !== 'error') { + // Resume forward by reloading the migration list and continuing. + runMigrations(); + } +} + +async function skipMigration(file, version, errorMsg, btn) { + if (!confirm(`Skip migration "${file}"?\n\nThe failed step will be marked applied so the rest can run, but you may end up in a broken state. Only do this if you know the failure is benign (e.g. the table already exists from a prior install).`)) { + return; + } + btn.disabled = true; + btn.textContent = 'Skipping...'; + const r = await post('migration_skip', { ...dbCredentials, file, version, error: errorMsg }); + btn.disabled = false; + btn.textContent = 'Skip'; + const log = document.getElementById('migLog'); + if (r.success) { + log.innerHTML += ``; + runMigrations(); + } else { + log.innerHTML += `
✗ ${file}: skip failed: ${r.message}
`; + } + log.scrollTop = log.scrollHeight; +} + +// ── P2: Step 6 health probe ────────────────────────── +async function runHealthCheck(btn) { + btn.disabled = true; + btn.innerHTML = ' Probing...'; + const r = await post('health', { ...dbCredentials }); + btn.disabled = false; + btn.textContent = 'Run health check'; + + const out = document.getElementById('healthResult'); + if (!out) return; + out.classList.remove('hidden'); + let html = `
Health:`; + html += '
    '; + for (const c of (r.checks || [])) { + const icon = c.status === 'pass' ? '✓' : '✗'; + html += `
  • ${icon} ${c.label}${c.value !== undefined ? ' (' + c.value + ')' : ''}${c.message ? ' — ' + c.message : ''}
  • `; + } + html += '
'; + out.innerHTML = html; +} + // ── Step 4: Create Admin ──────────────────────────── async function createAdmin() { const btn = document.getElementById('adminBtn'); @@ -928,7 +1023,10 @@ function renderChecks(containerId, checks) { } // ── Auto-run env check on load ────────────────────── -document.addEventListener('DOMContentLoaded', runEnvCheck); +document.addEventListener('DOMContentLoaded', async () => { + const resumed = await maybeResumeFromProgress(); + if (!resumed) runEnvCheck(); +}); From 57817c4cecbda0796fcfd6d923633512a35ac455 Mon Sep 17 00:00:00 2001 From: Ayoub Mohamed Samir <7agtyadmin@gmail.com> Date: Thu, 7 May 2026 13:30:38 +0300 Subject: [PATCH 10/15] docs + CI: document multi-panel installer + DB_PREFIX, add codemod CI (#22) - CLAUDE.md gets a "Multi-Panel Web Installer" section summarizing the P0+P1+P2 features that just landed, plus a DB_PREFIX block explaining the `#__` sentinel, t() runtime helper, backward-compat empty default, and how to add a new table cleanly. - Development Commands section now includes the prefix-codemod commands (dry-run / apply / verify). CI additions (.github/workflows/ci.yml): 1. New "Prefix Codemod Idempotency" job: - Runs tools/prefix-codemod.php in dry-run against the committed tree. - Asserts SQL=0 + PHP=0 changes (idempotent). - Runs --verify mode to confirm no unprefixed table refs left in SQL. - Catches any future PR that introduces hardcoded table names. 2. New "Installer (restricted PHP env)" job: - Boots PHP 8.3 with max_execution_time=15, allow_url_fopen=Off, memory_limit=128M to mimic aaPanel/Plesk-style restrictions. - Lints install/ajax.php + install/index.php under that env. - Loads ajax.php and asserts the seven new helpers are defined (installerBuildDsn, installerRunSqlFile, installerSplitSql, installerProbeSockets, installerCheckIncompleteState, installerT, plus the original). - Drives installerSplitSql across every database/*.sql file and prints statement counts. Catches any regression where the splitter mishandles a real migration. Both new jobs run on push and PR. They join the existing PHP Lint, Frontend Build & Test, and Docker Stack jobs. Co-authored-by: ChesnoTech <263363000+ChesnoTech@users.noreply.github.com> --- .github/workflows/ci.yml | 99 ++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 41 +++++++++++++++++ 2 files changed, 140 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4be5c3..f1acae9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -162,3 +162,102 @@ jobs: - name: Cleanup if: always() run: docker compose down -v + + # ─── Prefix Codemod Idempotency ───────────────────────────── + # Re-runs tools/prefix-codemod.php in --verify mode against the committed + # tree. Any unprefixed table reference in SQL or any unrewritten bare-name + # SQL ref in PHP would be picked up here. + codemod-verify: + name: Prefix Codemod Idempotency + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + extensions: pdo_mysql, json, mbstring + + - name: Run codemod in dry-run mode (must produce zero changes) + run: | + OUT=$(php tools/prefix-codemod.php --root FINAL_PRODUCTION_SYSTEM --quiet 2>&1 || true) + # Re-run normally to capture stats + STATS=$(php tools/prefix-codemod.php --root FINAL_PRODUCTION_SYSTEM) + echo "$STATS" + # Both pass numbers must be zero + SQL_CHANGED=$(echo "$STATS" | grep -oE "SQL: ([0-9]+) files changed" | grep -oE "[0-9]+" | head -1) + PHP_CHANGED=$(echo "$STATS" | grep -oE "PHP: ([0-9]+) files changed" | grep -oE "[0-9]+" | head -1) + if [ "$SQL_CHANGED" != "0" ] || [ "$PHP_CHANGED" != "0" ]; then + echo "❌ Codemod is not idempotent: SQL=$SQL_CHANGED files, PHP=$PHP_CHANGED files would change." + echo "Run: php tools/prefix-codemod.php --root FINAL_PRODUCTION_SYSTEM --apply" + exit 1 + fi + echo "✅ Codemod idempotent: 0 SQL + 0 PHP changes on second run." + + - name: Run codemod in --verify mode (zero unprefixed SQL refs) + run: | + php tools/prefix-codemod.php --root FINAL_PRODUCTION_SYSTEM --verify + echo "✅ Verify mode pass: zero unprefixed table references." + + # ─── Restricted-PHP Smoke Test (panel-style env) ──────────── + # Simulates aaPanel-style restrictive PHP settings: low max_execution_time + # (forces async per-migration runner) and no allow_url_fopen. + installer-restricted-php: + name: Installer (restricted PHP env) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP with restrictive settings + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + extensions: pdo_mysql, curl, openssl, json, mbstring, zip + ini-values: | + max_execution_time=15 + memory_limit=128M + allow_url_fopen=Off + + - name: Verify install/ajax.php parses under restrictive settings + run: | + # Lint pass with the restrictive INI loaded. + php -l FINAL_PRODUCTION_SYSTEM/install/ajax.php + php -l FINAL_PRODUCTION_SYSTEM/install/index.php + # Smoke import: load ajax.php's helpers and confirm no fatals at load time. + php -r ' + $_POST = ["action" => ""]; + $_GET = []; + ob_start(); + include "FINAL_PRODUCTION_SYSTEM/install/ajax.php"; + $out = ob_get_clean(); + echo "Loaded successfully. Output: " . substr($out, 0, 200) . "\n"; + if (!function_exists("installerBuildDsn")) { echo "FAIL: installerBuildDsn missing\n"; exit(1); } + if (!function_exists("installerRunSqlFile")) { echo "FAIL: installerRunSqlFile missing\n"; exit(1); } + if (!function_exists("installerSplitSql")) { echo "FAIL: installerSplitSql missing\n"; exit(1); } + if (!function_exists("installerProbeSockets")) { echo "FAIL: installerProbeSockets missing\n"; exit(1); } + if (!function_exists("installerCheckIncompleteState")) { echo "FAIL: installerCheckIncompleteState missing\n"; exit(1); } + if (!function_exists("installerT")) { echo "FAIL: installerT missing\n"; exit(1); } + echo "✅ All installer helpers loaded under restricted PHP.\n"; + ' + + - name: Verify SQL splitter handles every existing migration + run: | + php -r ' + include "FINAL_PRODUCTION_SYSTEM/install/ajax.php"; + $files = glob("FINAL_PRODUCTION_SYSTEM/database/*.sql"); + $totalStmts = 0; + foreach ($files as $f) { + $sql = file_get_contents($f); + $sql = preg_replace("/DELIMITER\s+[^\n]+/i", "", $sql); + $sql = preg_replace("/^\s*(START\s+TRANSACTION|BEGIN)\s*;\s*\$/im", "", $sql); + $sql = preg_replace("/^\s*COMMIT\s*;\s*\$/im", "", $sql); + $stmts = installerSplitSql($sql); + $count = count(array_filter($stmts, fn($s) => trim($s) !== "")); + $totalStmts += $count; + if ($count === 0) { + echo "WARN: " . basename($f) . " produced 0 statements\n"; + } + } + echo "✅ Splitter handled " . count($files) . " files, " . $totalStmts . " total statements.\n"; + ' diff --git a/CLAUDE.md b/CLAUDE.md index 00b9974..b9433bf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -280,8 +280,49 @@ powershell -ExecutionPolicy Bypass -File "FINAL_PRODUCTION_SYSTEM/activation/mai # Deploy license server cd license-server && npx wrangler login && npx wrangler deploy + +# Run prefix codemod (only when adding NEW tables — output already in repo) +docker cp tools/prefix-codemod.php oem-activation-web:/tmp/codemod.php +docker compose exec web php /tmp/codemod.php --root /var/www/html/activate # dry-run +docker compose exec web php /tmp/codemod.php --root /var/www/html/activate --apply # write +docker compose exec web php /tmp/codemod.php --root /var/www/html/activate --verify # second-run must be 0/0 ``` +## Multi-Panel Web Installer (P0 + P1 + P2) + +The web installer at `FINAL_PRODUCTION_SYSTEM/install/` works on aaPanel, +cPanel, Plesk, DirectAdmin, CyberPanel, ISPConfig, Vesta. Highlights: + +| Feature | What it does | +|---------|--------------| +| Async per-migration runner | `install_db_init` + `install_db_step` survives 30–60s `max_execution_time` caps | +| Per-statement SQL splitter | Respects backticks, quotes, line + block comments | +| Charset auto-fallback | MySQL <5.7 / MariaDB <5.5.3 → utf8mb3 instead of utf8mb4 | +| CREATE DATABASE skip-toggle | Step-2 checkbox for Plesk/CyberPanel users | +| Reverse-proxy IP hardening | Trust X-Forwarded-For only when REMOTE_ADDR is RFC1918/loopback | +| Auto-unlock recovery | `install.lock` + `admin_users` empty/missing → silent unlock | +| Unix-socket auto-detect | Probes 8 common paths, populates step-2 input | +| Joomla `#__` table prefix | Optional prefix in step-2 advanced section; empty default | +| Resumable `.progress.json` | Reload prompts "Resume from step N?" | +| Per-migration retry/skip | Inline buttons appear next to a failed migration | +| Structured `install.log` | Audit trail of every preflight/step/error | +| Step-6 health probe | `Run health check` button calls `?action=health` | + +### DB_PREFIX (Joomla-style) + +- **Sentinel**: SQL files use `#__tablename`. Substituted at install time + (`installerRunSqlFile` in `install/ajax.php`) or Docker init time + (`KEYGATE_DB_PREFIX` env var in `00-init.sh`). +- **Runtime helper**: `t('admin_users')` returns `DB_PREFIX . 'admin_users'`. + Defined in `functions/db-helpers.php`, loaded from `constants.php` + before any controller runs. +- **Backward compat**: legacy installs without `define('DB_PREFIX', ...)` + in `config.php` get an empty default → identical behavior to pre-prefix + release. +- **When adding a new table**: write SQL with `#__yourtable` placeholder; + reference from PHP via `t('yourtable')` (or run `tools/prefix-codemod.php` + `--apply` to convert all references mechanically). + ## Contributing Guide ### "I need to add a new admin feature" From 6ec9ceb19373f9e5c6a20a2cbd702f7ba959d2f1 Mon Sep 17 00:00:00 2001 From: Ayoub Mohamed Samir <7agtyadmin@gmail.com> Date: Fri, 8 May 2026 01:05:49 +0300 Subject: [PATCH 11/15] P0: anti-piracy hardening (RS256 JWT + DB row HMAC) (#24) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * P0: anti-piracy hardening — RS256 JWT + DB row HMAC + remove wildcard Eliminates trivial license-bypass paths from the threat model: 1. JWT signing HS256 -> RS256 (asymmetric) - Hardcoded secret 'keygate-community-verification-key-2026' no longer enables forging enterprise tokens. Private key only on Cloudflare Worker (LICENSE_PRIVATE_KEY secret); public key embedded in license-helpers.php for verify-only. - PHP decodeLicenseJwt() uses openssl_verify(OPENSSL_ALGO_SHA256). - Worker uses crypto.subtle RSASSA-PKCS1-v1_5 SHA-256. - Removed createLicenseJwt() — local code never signs in prod. - 90-day legacy HS256 verify window via LEGACY_HS256_SECRET + /api/migrate route for existing customers. 2. DB row integrity HMAC - New column license_info.integrity_hmac (CHAR(64)). - Per-instance license_row_secret in system_config, rotated on every successful registerLicense(). - getEffectiveLicense(), canAddTechnician(), canAddKeys(), and isFeatureAvailable() all re-check HMAC; mismatch -> forced community fallback + validation_status='invalid'. - Defeats direct INSERT bypass: attacker needs both row fields AND the per-instance secret, and the secret rotates. 3. Wildcard instance_id='*' removed - Worker GitHub Sponsors / LemonSqueezy / T-Bank flows no longer issue wildcard tokens. Pending purchases stored as pending_claim:true; customer binds via /api/claim with their installation's instance_id. - registerLicense() rejects wildcard payloads. 4. Dev license generation hardened - Local signing removed. /api/dev-issue Worker route gated by DEV_TOKEN secret. LicenseController calls Worker, requires admin to paste DEV_TOKEN. 5. Frontend - "Claim license" card (GitHub Sponsors / pending purchases). - "Migrate legacy license" card (HS256 -> RS256). - DEV_TOKEN input on Dev Tools card. - 12 new i18n keys in en.json + ru.json. Files: - license-server/worker.js, wrangler.toml — RS256 signer, /api/claim, /api/migrate, /api/dev-issue, no-wildcard issuance - functions/license-helpers.php — RS256 verify, row HMAC, secret rotation, wildcard rejection - controllers/admin/LicenseController.php — Worker dev-issue, claim, migrate handlers - admin_v2.php — register license_claim, license_migrate actions - database/license_p0_hmac_migration.sql — integrity_hmac column - database/docker-init/00-init.sh, install/ajax.php — migration phase 27 - frontend/api/license.ts, hooks/use-license.ts, pages/license/index.tsx, test/api-contracts.test.ts, i18n/en.json, i18n/ru.json - .gitignore — exclude license-server/.keys/ Backward-compat: v2.2.0 community installs auto-fallback to community on first boot post-upgrade (HS256 rejected, banner prompts re-register). Single existing paid customer migrates via /api/migrate. Verification (live PHP smoke test): - RS256 token verifies via openssl_verify -> OK - Legacy HS256 token verifies during migration window -> OK - Tampered JWT signature -> rejected - HMAC compute returns 64 hex chars Phase 1 of 3. P1 (hardware-fingerprint + rebind quota) and P2 (phone-home grace + revocation) follow in separate PRs. Plan: ~/.claude/plans/polymorphic-coalescing-bumblebee.md Co-Authored-By: Claude Opus 4.7 (1M context) * ci: trigger workflows for PR #24 --------- Co-authored-by: ChesnoTech <263363000+ChesnoTech@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) --- .gitignore | 3 + FINAL_PRODUCTION_SYSTEM/admin_v2.php | 2 + .../controllers/admin/LicenseController.php | 166 +++++++- .../database/docker-init/00-init.sh | 3 + .../database/license_p0_hmac_migration.sql | 18 + .../frontend/src/api/license.ts | 29 +- .../frontend/src/hooks/use-license.ts | 36 +- .../frontend/src/i18n/en.json | 12 + .../frontend/src/i18n/ru.json | 12 + .../frontend/src/pages/license/index.tsx | 99 ++++- .../frontend/src/test/api-contracts.test.ts | 2 + .../functions/license-helpers.php | 238 ++++++++++-- FINAL_PRODUCTION_SYSTEM/install/ajax.php | 1 + license-server/worker.js | 367 ++++++++++++++---- license-server/wrangler.toml | 27 +- 15 files changed, 878 insertions(+), 137 deletions(-) create mode 100644 FINAL_PRODUCTION_SYSTEM/database/license_p0_hmac_migration.sql diff --git a/.gitignore b/.gitignore index c0bd259..2417e1d 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,9 @@ FINAL_PRODUCTION_SYSTEM/uploads/client-resources/ FINAL_PRODUCTION_SYSTEM/install/install.log FINAL_PRODUCTION_SYSTEM/install/.progress.json +# ── License-server private keys (never committed; uploaded to CF) ── +license-server/.keys/ + # ── PHP Dependencies (managed by Composer) ──────────────── FINAL_PRODUCTION_SYSTEM/vendor/ diff --git a/FINAL_PRODUCTION_SYSTEM/admin_v2.php b/FINAL_PRODUCTION_SYSTEM/admin_v2.php index 5937524..d8d7039 100644 --- a/FINAL_PRODUCTION_SYSTEM/admin_v2.php +++ b/FINAL_PRODUCTION_SYSTEM/admin_v2.php @@ -337,6 +337,8 @@ 'license_register' => ['LicenseController.php', 'handle_license_register', true, true], 'license_deactivate' => ['LicenseController.php', 'handle_license_deactivate', true, true], 'license_generate_dev' => ['LicenseController.php', 'handle_license_generate_dev', true, true], + 'license_claim' => ['LicenseController.php', 'handle_license_claim', true, true], + 'license_migrate' => ['LicenseController.php', 'handle_license_migrate', true, true], // system upgrade 'upgrade_check_github' => ['UpgradeController.php', 'handle_upgrade_check_github', false, true], diff --git a/FINAL_PRODUCTION_SYSTEM/controllers/admin/LicenseController.php b/FINAL_PRODUCTION_SYSTEM/controllers/admin/LicenseController.php index 4020115..d975954 100644 --- a/FINAL_PRODUCTION_SYSTEM/controllers/admin/LicenseController.php +++ b/FINAL_PRODUCTION_SYSTEM/controllers/admin/LicenseController.php @@ -89,8 +89,11 @@ function handle_license_deactivate(PDO $pdo, array $admin_session, $json_input): jsonResponse(['success' => true, 'message' => 'License deactivated']); } -// ── Generate Development License (dev/testing only) ── - +// ── Generate Development License (calls Worker /api/dev-issue) ── +// +// Local signing was removed in v2.3.0 (P0 hardening). The Worker is the +// only entity that holds the RS256 private key. Founder provides a +// DEV_TOKEN (matching the Cloudflare secret) to authorize. function handle_license_generate_dev(PDO $pdo, array $admin_session, $json_input): void { requirePermission('system_settings', $admin_session); @@ -101,34 +104,155 @@ function handle_license_generate_dev(PDO $pdo, array $admin_session, $json_input return; } - $tier = $json_input['tier'] ?? 'pro'; + $tier = $json_input['tier'] ?? 'pro'; + $devToken = $json_input['dev_token'] ?? ''; if (!isset(LICENSE_TIERS[$tier])) { jsonResponse(['success' => false, 'error' => 'Invalid tier']); return; } + if (empty($devToken)) { + jsonResponse([ + 'success' => false, + 'error' => 'DEV_TOKEN required. Set the same value via wrangler secret put DEV_TOKEN on the Worker.', + ]); + return; + } $instanceId = getInstanceId($pdo); - $tierDef = LICENSE_TIERS[$tier]; - - $payload = [ - 'iss' => 'keygate-dev', - 'tier' => $tier, - 'instance_id' => $instanceId, - 'email' => 'dev@localhost', - 'name' => 'Development License', - 'max_technicians' => $tierDef['max_technicians'], - 'max_keys' => $tierDef['max_keys'], - 'iat' => time(), - 'exp' => time() + (365 * 86400), // 1 year - ]; - - $jwt = createLicenseJwt($payload); + + $body = json_encode([ + 'tier' => $tier, + 'email' => 'dev@localhost', + 'instance_id' => $instanceId, + 'dev_token' => $devToken, + ]); + $ctx = stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => "Content-Type: application/json\r\n", + 'content' => $body, + 'timeout' => 10, + 'ignore_errors' => true, + ], + ]); + $resp = @file_get_contents(KEYGATE_LICENSE_SERVER . '/api/dev-issue', false, $ctx); + if ($resp === false) { + jsonResponse(['success' => false, 'error' => 'Could not reach license server. Check network.']); + return; + } + $decoded = json_decode($resp, true); + if (!is_array($decoded)) { + jsonResponse(['success' => false, 'error' => 'License server returned invalid response.']); + return; + } + + if (empty($decoded['success'])) { + jsonResponse(['success' => false, 'error' => $decoded['error'] ?? 'Worker rejected request.']); + return; + } jsonResponse([ 'success' => true, - 'license_key' => $jwt, - 'tier' => $tier, + 'license_key' => $decoded['license_key'], + 'tier' => $decoded['tier'] ?? $tier, + 'instance_id' => $instanceId, + 'expires_at' => $decoded['expires_at'] ?? null, + 'message' => 'Development license issued by Worker. Paste into Registration field.', + ]); +} + +// ── Claim license (GitHub Sponsors / pending payments) ── +// +// Body: { email, sponsor_login? } +// Calls Worker /api/claim with the local instance_id. Worker mints an +// RS256 JWT bound to this install. +function handle_license_claim(PDO $pdo, array $admin_session, $json_input): void { + requirePermission('system_settings', $admin_session); + + $email = trim($json_input['email'] ?? ''); + $sponsorLogin = trim($json_input['sponsor_login'] ?? ''); + if (empty($email)) { + jsonResponse(['success' => false, 'error' => 'Email is required']); + return; + } + + $instanceId = getInstanceId($pdo); + $body = json_encode([ + 'email' => $email, + 'instance_id' => $instanceId, + 'sponsor_login' => $sponsorLogin ?: null, + ]); + $ctx = stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => "Content-Type: application/json\r\n", + 'content' => $body, + 'timeout' => 10, + 'ignore_errors' => true, + ], + ]); + $resp = @file_get_contents(KEYGATE_LICENSE_SERVER . '/api/claim', false, $ctx); + if ($resp === false) { + jsonResponse(['success' => false, 'error' => 'Could not reach license server']); + return; + } + $decoded = json_decode($resp, true); + if (!is_array($decoded) || empty($decoded['success'])) { + jsonResponse(['success' => false, 'error' => $decoded['error'] ?? 'Claim failed']); + return; + } + + // Auto-register the freshly-issued JWT. + $regResult = registerLicense($pdo, $decoded['license_key']); + jsonResponse(array_merge($regResult, [ + 'license_key' => $decoded['license_key'], + 'expires_at' => $decoded['expires_at'] ?? null, + ])); +} + +// ── Migrate legacy HS256 license to RS256 ── +// +// Body: { license_key (legacy HS256) } +// Calls Worker /api/migrate. Re-issues an RS256 JWT bound to this +// instance_id. Auto-registers on success. +function handle_license_migrate(PDO $pdo, array $admin_session, $json_input): void { + requirePermission('system_settings', $admin_session); + + $legacyKey = trim($json_input['license_key'] ?? ''); + if (empty($legacyKey)) { + jsonResponse(['success' => false, 'error' => 'license_key is required']); + return; + } + + $instanceId = getInstanceId($pdo); + $body = json_encode([ + 'license_key' => $legacyKey, 'instance_id' => $instanceId, - 'message' => "Development {$tierDef['label']} license generated. Paste it into the registration field.", ]); + $ctx = stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => "Content-Type: application/json\r\n", + 'content' => $body, + 'timeout' => 10, + 'ignore_errors' => true, + ], + ]); + $resp = @file_get_contents(KEYGATE_LICENSE_SERVER . '/api/migrate', false, $ctx); + if ($resp === false) { + jsonResponse(['success' => false, 'error' => 'Could not reach license server']); + return; + } + $decoded = json_decode($resp, true); + if (!is_array($decoded) || empty($decoded['success'])) { + jsonResponse(['success' => false, 'error' => $decoded['error'] ?? 'Migration failed']); + return; + } + + // Auto-register the freshly-issued RS256 JWT. + $regResult = registerLicense($pdo, $decoded['license_key']); + jsonResponse(array_merge($regResult, [ + 'license_key' => $decoded['license_key'], + 'expires_at' => $decoded['expires_at'] ?? null, + ])); } diff --git a/FINAL_PRODUCTION_SYSTEM/database/docker-init/00-init.sh b/FINAL_PRODUCTION_SYSTEM/database/docker-init/00-init.sh index fb6bcc1..55ca959 100644 --- a/FINAL_PRODUCTION_SYSTEM/database/docker-init/00-init.sh +++ b/FINAL_PRODUCTION_SYSTEM/database/docker-init/00-init.sh @@ -149,6 +149,9 @@ run_sql "task_pipeline_migration.sql" 25 # Phase 14: Production tracking & enterprise key management run_sql "production_tracking_migration.sql" 26 +# Phase 15: License row integrity HMAC (P0 anti-piracy) +run_sql "license_p0_hmac_migration.sql" 27 + echo "" echo "=== Database initialization complete ===" echo "" diff --git a/FINAL_PRODUCTION_SYSTEM/database/license_p0_hmac_migration.sql b/FINAL_PRODUCTION_SYSTEM/database/license_p0_hmac_migration.sql new file mode 100644 index 0000000..a6d965d --- /dev/null +++ b/FINAL_PRODUCTION_SYSTEM/database/license_p0_hmac_migration.sql @@ -0,0 +1,18 @@ +-- ============================================================= +-- KeyGate v2.3.0 P0 — License row integrity HMAC +-- ============================================================= +-- Adds an HMAC column to license_info so any row directly INSERTed/UPDATEd +-- without going through registerLicense() (which knows the per-instance +-- row secret) is detected as tampered and forced back to community tier. +-- +-- The HMAC formula and verification live in functions/license-helpers.php +-- (computeLicenseRowHmac / verifyLicenseRow). +-- ============================================================= + +ALTER TABLE `#__license_info` + ADD COLUMN integrity_hmac CHAR(64) NOT NULL DEFAULT '' AFTER validation_status; + +-- Existing rows (legacy paid customers from v2.2.x) have an empty hmac +-- → verifyLicenseRow returns false → they fall back to community on +-- next read. Customers re-register via /api/migrate to get a fresh +-- RS256 JWT, and that path computes a valid HMAC on insert. diff --git a/FINAL_PRODUCTION_SYSTEM/frontend/src/api/license.ts b/FINAL_PRODUCTION_SYSTEM/frontend/src/api/license.ts index 348615f..eb38457 100644 --- a/FINAL_PRODUCTION_SYSTEM/frontend/src/api/license.ts +++ b/FINAL_PRODUCTION_SYSTEM/frontend/src/api/license.ts @@ -56,7 +56,7 @@ export function deactivateLicense() { return apiPostJson<{ success: boolean; message: string }>('license_deactivate') } -export function generateDevLicense(tier: string) { +export function generateDevLicense(tier: string, devToken: string) { return apiPostJson<{ success: boolean license_key?: string @@ -64,5 +64,30 @@ export function generateDevLicense(tier: string) { instance_id?: string message?: string error?: string - }>('license_generate_dev', { tier }) + }>('license_generate_dev', { tier, dev_token: devToken }) +} + +// P0: claim a pending GitHub Sponsors / LemonSqueezy / T-Bank purchase by +// binding it to this install's instance_id. Worker mints an RS256 JWT. +export function claimLicense(email: string, sponsorLogin?: string) { + return apiPostJson<{ + success: boolean + license_key?: string + tier?: string + expires_at?: string + message?: string + error?: string + }>('license_claim', { email, sponsor_login: sponsorLogin || '' }) +} + +// P0: migrate a legacy HS256 license to RS256 (90-day window post v2.3.0). +export function migrateLegacyLicense(licenseKey: string) { + return apiPostJson<{ + success: boolean + license_key?: string + tier?: string + expires_at?: string + message?: string + error?: string + }>('license_migrate', { license_key: licenseKey }) } diff --git a/FINAL_PRODUCTION_SYSTEM/frontend/src/hooks/use-license.ts b/FINAL_PRODUCTION_SYSTEM/frontend/src/hooks/use-license.ts index 678ece9..c8a7447 100644 --- a/FINAL_PRODUCTION_SYSTEM/frontend/src/hooks/use-license.ts +++ b/FINAL_PRODUCTION_SYSTEM/frontend/src/hooks/use-license.ts @@ -6,6 +6,8 @@ import { registerLicense, deactivateLicense, generateDevLicense, + claimLicense, + migrateLegacyLicense, } from '@/api/license' export function useLicenseStatus() { @@ -47,7 +49,8 @@ export function useDeactivateLicense() { export function useGenerateDevLicense() { const { t } = useTranslation() return useMutation({ - mutationFn: (tier: string) => generateDevLicense(tier), + mutationFn: ({ tier, devToken }: { tier: string; devToken: string }) => + generateDevLicense(tier, devToken), onSuccess: (data) => { if (data.success) { toast.success(data.message || t('license.dev_generated', 'Dev license generated')) @@ -56,3 +59,34 @@ export function useGenerateDevLicense() { onError: (e: Error) => toast.error(e.message), }) } + +export function useClaimLicense() { + const qc = useQueryClient() + const { t } = useTranslation() + return useMutation({ + mutationFn: ({ email, sponsorLogin }: { email: string; sponsorLogin?: string }) => + claimLicense(email, sponsorLogin), + onSuccess: (data) => { + qc.invalidateQueries({ queryKey: ['license-status'] }) + if (data.success) { + toast.success(data.message || t('license.claimed', 'License claimed and activated')) + } + }, + onError: (e: Error) => toast.error(e.message), + }) +} + +export function useMigrateLegacyLicense() { + const qc = useQueryClient() + const { t } = useTranslation() + return useMutation({ + mutationFn: (key: string) => migrateLegacyLicense(key), + onSuccess: (data) => { + qc.invalidateQueries({ queryKey: ['license-status'] }) + if (data.success) { + toast.success(data.message || t('license.migrated', 'License migrated to RS256')) + } + }, + onError: (e: Error) => toast.error(e.message), + }) +} diff --git a/FINAL_PRODUCTION_SYSTEM/frontend/src/i18n/en.json b/FINAL_PRODUCTION_SYSTEM/frontend/src/i18n/en.json index ea1d80d..95eab65 100644 --- a/FINAL_PRODUCTION_SYSTEM/frontend/src/i18n/en.json +++ b/FINAL_PRODUCTION_SYSTEM/frontend/src/i18n/en.json @@ -1733,6 +1733,18 @@ "sub.tab_payment": "Payment", "sub.tab_key": "License Key", "sub.tab_billing": "Billing", + "sub.claim_title": "Claim a GitHub Sponsors / pending purchase", + "sub.claim_desc": "If you sponsored ChesnoTech on GitHub or your LemonSqueezy/T-Bank checkout did not include your instance ID, claim your license here. The server binds it to this installation and issues your key.", + "sub.claim_email": "Email used at checkout / GitHub sponsor email", + "sub.claim_sponsor": "GitHub sponsor login (optional)", + "sub.claim_button": "Claim & bind to this install", + "sub.migrate_title": "Migrate legacy license (pre-v2.3)", + "sub.migrate_desc": "If you have a license key issued before May 2026 (HS256 algorithm), use this to upgrade it to the new RS256 format. Available until 2026-08-08.", + "sub.migrate_placeholder": "Paste the legacy HS256 license key...", + "sub.migrate_button": "Migrate to RS256 & activate", + "sub.dev_token_placeholder": "DEV_TOKEN (matches Worker secret)", + "license.claimed": "License claimed and activated", + "license.migrated": "License migrated to RS256", "sub.registered_to": "Registered to {{email}}", "sub.free_tier": "Free tier — limited to 1 technician and 50 keys", "sub.technicians": "Technicians", diff --git a/FINAL_PRODUCTION_SYSTEM/frontend/src/i18n/ru.json b/FINAL_PRODUCTION_SYSTEM/frontend/src/i18n/ru.json index 93eb629..178ff1d 100644 --- a/FINAL_PRODUCTION_SYSTEM/frontend/src/i18n/ru.json +++ b/FINAL_PRODUCTION_SYSTEM/frontend/src/i18n/ru.json @@ -1733,6 +1733,18 @@ "sub.tab_payment": "Оплата", "sub.tab_key": "Лицензия", "sub.tab_billing": "Биллинг", + "sub.claim_title": "Привязать спонсорство GitHub / ожидающую покупку", + "sub.claim_desc": "Если вы оформили спонсорство ChesnoTech на GitHub или ваша оплата через LemonSqueezy/Т-Банк не содержала идентификатор экземпляра, привяжите лицензию здесь. Сервер свяжет её с этой установкой и выдаст ключ.", + "sub.claim_email": "Email при оплате / email GitHub-спонсора", + "sub.claim_sponsor": "Логин GitHub-спонсора (необязательно)", + "sub.claim_button": "Привязать к этой установке", + "sub.migrate_title": "Миграция старой лицензии (до v2.3)", + "sub.migrate_desc": "Если у вас лицензия, выданная до мая 2026 (алгоритм HS256), используйте это, чтобы обновить её до нового формата RS256. Доступно до 2026-08-08.", + "sub.migrate_placeholder": "Вставьте старый ключ HS256...", + "sub.migrate_button": "Перевести в RS256 и активировать", + "sub.dev_token_placeholder": "DEV_TOKEN (совпадает с секретом Worker)", + "license.claimed": "Лицензия привязана и активирована", + "license.migrated": "Лицензия переведена в RS256", "sub.registered_to": "Зарегистрирована на {{email}}", "sub.free_tier": "Бесплатный тариф — 1 техник, 50 ключей", "sub.technicians": "Техники", diff --git a/FINAL_PRODUCTION_SYSTEM/frontend/src/pages/license/index.tsx b/FINAL_PRODUCTION_SYSTEM/frontend/src/pages/license/index.tsx index f5b661f..ffd20da 100644 --- a/FINAL_PRODUCTION_SYSTEM/frontend/src/pages/license/index.tsx +++ b/FINAL_PRODUCTION_SYSTEM/frontend/src/pages/license/index.tsx @@ -40,6 +40,8 @@ import { useRegisterLicense, useDeactivateLicense, useGenerateDevLicense, + useClaimLicense, + useMigrateLegacyLicense, } from '@/hooks/use-license' const TIER_COLORS: Record = { @@ -61,11 +63,17 @@ export function LicensePage() { const [activeTab, setActiveTab] = useState('plan') const [licenseKey, setLicenseKey] = useState('') const [devLicenseKey, setDevLicenseKey] = useState('') + const [devToken, setDevToken] = useState('') + const [claimEmail, setClaimEmail] = useState('') + const [claimSponsor, setClaimSponsor] = useState('') + const [legacyKey, setLegacyKey] = useState('') const statusQuery = useLicenseStatus() const registerMut = useRegisterLicense() const deactivateMut = useDeactivateLicense() const devGenMut = useGenerateDevLicense() + const claimMut = useClaimLicense() + const migrateMut = useMigrateLegacyLicense() const license = statusQuery.data?.license const usage = statusQuery.data?.usage @@ -77,12 +85,36 @@ export function LicensePage() { } const handleGenerateDev = async (tier: string) => { - const result = await devGenMut.mutateAsync(tier) + if (!devToken.trim()) { + alert('DEV_TOKEN required. Set it via `wrangler secret put DEV_TOKEN` on the Worker, then paste here.') + return + } + const result = await devGenMut.mutateAsync({ tier, devToken: devToken.trim() }) if (result.license_key) { setDevLicenseKey(result.license_key) } } + const handleClaim = async () => { + if (!claimEmail.trim()) return + const result = await claimMut.mutateAsync({ + email: claimEmail.trim(), + sponsorLogin: claimSponsor.trim() || undefined, + }) + if (result.license_key) { + setClaimEmail('') + setClaimSponsor('') + } + } + + const handleMigrateLegacy = async () => { + if (!legacyKey.trim()) return + const result = await migrateMut.mutateAsync(legacyKey.trim()) + if (result.success) { + setLegacyKey('') + } + } + if (statusQuery.isLoading) { return (
@@ -479,7 +511,7 @@ export function LicensePage() {
' . __('keys.status') . '' . __('common.count') . '