if($path==='docs/api'){render('api_docs');exit;} if($path==='admin/api-health'){require_role(['admin']);$since=date('Y-m-d H:i:s',time()-86400);$stats=all('SELECT endpoint,COUNT(*) requests,ROUND(AVG(duration_ms),1) avg_ms,SUM(CASE WHEN status_code>=400 THEN 1 ELSE 0 END) errors FROM api_health_log WHERE created_at>=? GROUP BY endpoint ORDER BY requests DESC',[$since]);$total=(int)(one('SELECT COUNT(*) n FROM api_health_log WHERE created_at>=?',[$since])['n']??0);$errors=(int)(one('SELECT COUNT(*) n FROM api_health_log WHERE created_at>=? AND status_code>=400',[$since])['n']??0);render('admin_api_health',compact('stats','total','errors'));exit;} if($path==='admin/license-options'){require_role(['admin']);if($_SERVER['REQUEST_METHOD']==='POST'){verify_csrf();run('INSERT OR REPLACE INTO product_license_options(product_id,name,code,price,activation_limit,support_months,updates_months,status,created_at) VALUES(?,?,?,?,?,?,?,"active",datetime("now"))',[(int)$_POST['product_id'],trim($_POST['name']),preg_replace('/[^a-z0-9_-]/','',strtolower($_POST['code'])),(float)$_POST['price'],max(1,(int)$_POST['activation_limit']),max(0,(int)$_POST['support_months']),max(0,(int)$_POST['updates_months'])]);flash('success','Lizenzvariante gespeichert.');redirect('admin/license-options');}$products=all('SELECT id,title FROM products ORDER BY title');$options=all('SELECT l.*,p.title FROM product_license_options l JOIN products p ON p.id=l.product_id ORDER BY p.title,l.price');render('admin_license_options',compact('products','options'));exit;} if($path==='licenses'){require_login();$rows=all('SELECT oi.*,p.title,p.slug,p.version,p.activation_limit,p.release_channel FROM order_items oi JOIN orders o ON o.id=oi.order_id JOIN products p ON p.id=oi.product_id WHERE o.user_id=? AND o.status="paid" ORDER BY oi.id DESC',[current_user()['id']]);$licenses=[];foreach($rows as $l){$l['activations']=all('SELECT * FROM license_activations WHERE order_item_id=? AND revoked_at IS NULL ORDER BY id DESC',[$l['id']]);$l['activation_count']=count($l['activations']);$l['release']=latest_release((int)$l['product_id'],(string)($l['release_channel']?:'stable'));$licenses[]=$l;}render('licenses',compact('licenses'));exit;} if($path==='license/deactivate'){require_login();verify_csrf();$a=one('SELECT la.*,o.user_id FROM license_activations la JOIN order_items oi ON oi.id=la.order_item_id JOIN orders o ON o.id=oi.order_id WHERE la.id=?',[(int)($_POST['activation_id']??0)]);if(!$a||(int)$a['user_id']!==(int)current_user()['id']){http_response_code(403);exit;}run('UPDATE license_activations SET revoked_at=datetime("now") WHERE id=?',[$a['id']]);audit('license_activation_revoked',['activation_id'=>$a['id']]);redirect('licenses');} if(preg_match('#^license/update/(\d+)$#',$path,$m)){require_login();$item=one('SELECT oi.*,o.user_id,o.status order_status,p.release_channel,p.status product_status FROM order_items oi JOIN orders o ON o.id=oi.order_id JOIN products p ON p.id=oi.product_id WHERE oi.id=? AND o.user_id=?',[$m[1],current_user()['id']]);if(!$item||!license_is_valid($item)){http_response_code(403);exit('Lizenz nicht gültig.');}$rel=latest_release((int)$item['product_id'],(string)($item['release_channel']?:'stable'));if(!$rel||empty($rel['file_path'])){flash('error','Kein Release verfügbar.');redirect('licenses');}$token=update_download_token((int)$item['id'],(int)$rel['id']);redirect('updates/download?token='.rawurlencode($token));} if($path==='updates/download'){$t=verify_update_download_token((string)($_GET['token']??''));if(!$t){http_response_code(403);exit('Link ungültig oder abgelaufen.');}$item=one('SELECT oi.*,o.status order_status,p.status product_status FROM order_items oi JOIN orders o ON o.id=oi.order_id JOIN products p ON p.id=oi.product_id WHERE oi.id=?',[$t['item_id']]);$rel=one('SELECT * FROM product_versions WHERE id=? AND product_id=? AND status="published"',[$t['release_id'],$item['product_id']??0]);if(!$item||!$rel||!license_is_valid($item)){http_response_code(403);exit('Download nicht verfügbar.');}$full=__DIR__.'/../'.$rel['file_path'];if(!is_file($full)){http_response_code(404);exit('Release-Datei fehlt.');}header('Content-Type: application/zip');header('Content-Disposition: attachment; filename="'.preg_replace('/[^a-zA-Z0-9._-]/','-',($rel['version']??'release').'.zip').'"');header('Content-Length: '.filesize($full));readfile($full);exit;} if(str_starts_with($path,'api/v1/license/')){$__apiStarted=microtime(true);$action=substr($path,strlen('api/v1/license/'));register_shutdown_function(function()use(&$action,$__apiStarted){try{run('INSERT INTO api_health_log(endpoint,status_code,duration_ms,created_at) VALUES(?,?,?,datetime("now"))',['license/'.$action,http_response_code()?:200,(int)round((microtime(true)-$__apiStarted)*1000)]);}catch(Throwable $e){}});api_rate_limit('license-'.$action,120);$in=api_input();$key=strtoupper(trim((string)($in['license_key']??'')));$item=license_item_by_key($key);if(!$item)api_json(['ok'=>false,'error'=>'license_not_found'],404);try{if($action==='activate'){$a=license_activate($item,(string)($in['domain']??''),(string)($in['installation_id']??''));api_json(['ok'=>true,'license'=>['status'=>'active','product'=>$item['slug'],'version'=>$item['version'],'activation_limit'=>(int)$item['activation_limit'],'activation_count'=>license_activation_count((int)$item['id'])],'activation'=>['domain'=>$a['domain'],'installation_id'=>$a['installation_id']]]);}if($action==='deactivate'){license_deactivate($item,(string)($in['installation_id']??''));api_json(['ok'=>true]);}if($action==='validate'){$active=one('SELECT * FROM license_activations WHERE order_item_id=? AND installation_id=? AND revoked_at IS NULL',[$item['id'],trim((string)($in['installation_id']??''))]);api_json(['ok'=>license_is_valid($item)&&!!$active,'status'=>license_is_valid($item)&&$active?'active':'inactive','product'=>$item['slug'],'version'=>$item['version'],'activation_count'=>license_activation_count((int)$item['id']),'activation_limit'=>(int)$item['activation_limit']]);}if($action==='update'){$channel=(string)($item['release_channel']?:'stable');$rel=latest_release((int)$item['product_id'],$channel);$installed=(string)($in['version']??'0.0.0');$available=$rel&&version_compare((string)$rel['version'],$installed,'>');api_json(['ok'=>true,'update_available'=>(bool)$available,'current_version'=>$installed,'latest_version'=>$rel['version']??$item['version'],'channel'=>$channel,'minimum_php'=>$rel['minimum_php']??null,'checksum_sha256'=>$rel['checksum_sha256']??null,'notes'=>$rel['changelog']??null,'download_url'=>$available?url('updates/download?token='.rawurlencode(update_download_token((int)$item['id'],(int)$rel['id'],600))):null]);}api_json(['ok'=>false,'error'=>'endpoint_not_found'],404);}catch(RuntimeException $e){api_json(['ok'=>false,'error'=>$e->getMessage()],409);}} if($path==='admin/releases'){require_role(['admin']);if($_SERVER['REQUEST_METHOD']==='POST'){verify_csrf();$product=one('SELECT * FROM products WHERE id=?',[(int)$_POST['product_id']]);if(!$product){flash('error','Produkt fehlt.');redirect('admin/releases');}$version=trim((string)$_POST['version']);if(!preg_match('/^[0-9]+(?:\.[0-9]+){1,3}(?:[-+][A-Za-z0-9.-]+)?$/',$version)){flash('error','Ungültige Versionsnummer.');redirect('admin/releases');}$f=$_FILES['file']??null;if(!$f||($f['error']??UPLOAD_ERR_NO_FILE)!==UPLOAD_ERR_OK||strtolower(pathinfo($f['name'],PATHINFO_EXTENSION))!=='zip'){flash('error','Eine ZIP-Datei ist erforderlich.');redirect('admin/releases');}$dir='storage/releases/'.date('Y/m');@mkdir(__DIR__.'/../'.$dir,0770,true);$name=random_token(16).'.zip';$dest=__DIR__.'/../'.$dir.'/'.$name;if(!move_uploaded_file($f['tmp_name'],$dest))throw new RuntimeException('Upload fehlgeschlagen.');$checksum=hash_file('sha256',$dest);$published=trim((string)($_POST['published_at']??''));$published=$published!==''?str_replace('T',' ',$published).':00':date('Y-m-d H:i:s');run('INSERT INTO product_versions(product_id,version,file_path,changelog,channel,status,published_at,minimum_php,checksum_sha256,release_notes,created_at) VALUES(?,?,?,?,?,"published",?,?,?,?,datetime("now"))',[$product['id'],$version,$dir.'/'.$name,trim((string)$_POST['changelog']),$_POST['channel']==='beta'?'beta':'stable',$published,trim((string)$_POST['minimum_php']),$checksum,trim((string)$_POST['changelog'])]);if($_POST['channel']==='stable')run('UPDATE products SET version=?,file_path=?,updated_at=datetime("now") WHERE id=?',[$version,$dir.'/'.$name,$product['id']]);notify_buyers((int)$product['id'],'Neues Release: '.$product['title'],'Version '.$version.' steht in deiner kodiq.shop Bibliothek bereit.');audit('release_published',['product_id'=>$product['id'],'version'=>$version]);redirect('admin/releases');}$products=all('SELECT id,title FROM products ORDER BY title');$versions=all('SELECT pv.*,p.title FROM product_versions pv JOIN products p ON p.id=pv.product_id ORDER BY pv.id DESC LIMIT 100');render('admin_releases',compact('products','versions'));exit;} if($path==='admin/licenses'){require_role(['admin']);$licenses=all('SELECT oi.*,p.title,p.activation_limit,u.email,(SELECT COUNT(*) FROM license_activations la WHERE la.order_item_id=oi.id AND la.revoked_at IS NULL) activation_count FROM order_items oi JOIN orders o ON o.id=oi.order_id JOIN products p ON p.id=oi.product_id JOIN users u ON u.id=o.user_id WHERE o.status="paid" ORDER BY oi.id DESC LIMIT 300');$stats=['licenses'=>count($licenses),'activations'=>(int)(one('SELECT COUNT(*) n FROM license_activations WHERE revoked_at IS NULL')['n']??0),'full'=>count(array_filter($licenses,fn($x)=>(int)$x['activation_count']>=max(1,(int)$x['activation_limit'])))];render('admin_licenses',compact('licenses','stats'));exit;}