feat(admin): скрытие доменов и сохраняемый порядок колонок (#26)

This commit is contained in:
Panfilov Konstantin committed 2026-10-26 04:11:44 +03:00
1 parent dd6d221dd7
commit 00fbef9ede
12 files changed
+252 -33

No files matched your search

+33 -2
View File
@@ -24,7 +24,7 @@ final class Store
public function path(string $name): string { if (!preg_match('~^[a-zA-Z0-9_./-]+$~D',$name) || str_contains($name,'..') || str_starts_with($name,'/')) throw new Problem('Недопустимый идентификатор.'); return $this->dir.'/'.$name; }
public function targets(bool $public=true): array {
$targets=array_values($this->read('targets.json'));
if ($public) foreach($targets as &$target) {$target['has_secret']=!empty($target['secret']);unset($target['secret']);}
if ($public) foreach($targets as &$target) {$target['hidden']=(bool)($target['hidden']??false);$target['has_secret']=!empty($target['secret']);unset($target['secret']);}
return $targets;
}
public function target(string $id): array {foreach($this->targets(false) as $target) if ($target['id']===$id) return $target;throw new Problem('Подключение не найдено.',404);}
@@ -47,6 +47,7 @@ final class Store
}
$target['port']=(int)($input['port'] ?? $old['port'] ?? 22);if($target['port']<1||$target['port']>65535) throw new Problem('Некорректный порт.');
$target['id']=$id;$target['enabled']=(bool)($input['enabled'] ?? $old['enabled'] ?? false);$target['secret']=$secret ?? $old['secret'] ?? '';
$target['hidden']=(bool)($old['hidden']??false);
// Изменение транспорта сбрасывает проверку: запись включается только после test.
$identity=static fn($t)=>array_intersect_key($t,array_flip(['type','host','port','user','root','host_key','secret']));
$target['verified']=isset($old['verified']) && $identity($old)===$identity($target) ? $old['verified'] : false;
@@ -54,7 +55,37 @@ final class Store
});
}
public function verifyTarget(string $id, ?array $expected=null): void {$this->locked('targets',function() use($id,$expected){$targets=$this->read('targets.json');if(!isset($targets[$id])) throw new Problem('Подключение не найдено.',404);if($expected!==null&&$targets[$id]!==$expected)throw new Problem('Подключение изменилось во время проверки. Повторите проверку.',409);$targets[$id]['verified']=true;$this->write('targets.json',$targets);});}
public function deleteTarget(string $id): void {$this->locked('targets',function() use($id){$targets=$this->read('targets.json');unset($targets[$id]);$this->write('targets.json',$targets);});}
public function setTargetVisibility(string $id,bool $hidden): array {
return $this->locked('targets',function() use($id,$hidden){
$targets=$this->read('targets.json');if(!isset($targets[$id]))throw new Problem('Подключение не найдено.',404);
$targets[$id]['hidden']=$hidden;$this->write('targets.json',$targets);return $this->targets();
});
}
public function orderTargets(array $ids): array {
if(!array_is_list($ids))throw new Problem('Порядок должен быть списком ID.');
foreach($ids as $id)if(!is_string($id))throw new Problem('Порядок должен содержать строковые ID.');
return $this->locked('targets',function() use($ids){
$targets=$this->read('targets.json');$visible=[];
foreach($targets as $id=>$target)if(empty($target['hidden']))$visible[]=(string)$id;
if(count($ids)!==count($visible)||count(array_unique($ids,SORT_STRING))!==count($ids)||array_diff($ids,$visible))throw new Problem('Список видимых подключений изменился. Обновите страницу и повторите.',409);
$ordered=[];$position=0;
foreach($targets as $id=>$target){if(empty($target['hidden'])){$id=$ids[$position++];$target=$targets[$id];}$ordered[$id]=$target;}
$this->write('targets.json',$ordered);return $this->targets();
});
}
public function deleteTarget(string $id): void {
$this->locked('targets',function() use($id){
$targets=$this->read('targets.json');if(!isset($targets[$id]))throw new Problem('Подключение не найдено.',404);
if(empty($targets[$id]['hidden']))throw new Problem('Перед удалением скройте подключение.',409);
foreach(glob($this->dir.'/jobs/*.json')?:[] as $path){
$job=$this->read('jobs/'.basename($path));if(!in_array($job['status']??null,['queued','running'],true))continue;
$references=array_column($job['results']??[],'target');
if(isset($job['payload']['source']['id']))$references[]=$job['payload']['source']['id'];
if(in_array($id,$references,true))throw new Problem('Подключение используется незавершённым заданием. Дождитесь его завершения.',409);
}
unset($targets[$id]);$this->write('targets.json',$targets);
});
}
public function backup(string $target,string $content): string {
$id=gmdate('YmdHis').'-'.bin2hex(random_bytes(6));$items=$this->read('backups/index.json');$items[]=['id'=>$id,'target'=>$target,'created_at'=>gmdate(DATE_ATOM)];
$this->write('backups/'.$id.'.json',['content'=>$content]);$this->locked('backup-index',function() use($items){$current=$this->read('backups/index.json');$current[]=end($items);$this->write('backups/index.json',$current);});return $id;