Initial commit
This commit is contained in:
commit
05c65aad4d
155 changed files with 93617 additions and 0 deletions
59
viewer/admin/actions.php
Normal file
59
viewer/admin/actions.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['admin'])) { header('Location: login.php'); exit; }
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Maintenance</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<style>button{margin:.3rem}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header class="site"><h1>Maintenance</h1><nav class="admin-links"><a href="index.php">Wróć</a></nav></header>
|
||||
<div class="card">
|
||||
<h2>Cache & Rebuilds</h2>
|
||||
<div>
|
||||
<button data-action="clear_cache">Clear Cache</button>
|
||||
<button data-action="rebuild_thumbs">Rebuild Thumbnails</button>
|
||||
<button data-action="diagnostics">Diagnostics</button>
|
||||
</div>
|
||||
<pre id="out" style="margin-top:1rem;padding:1rem;border:1px solid #ddd;background:#f9f9f9;white-space:pre-wrap"></pre>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Entity Re-save (Drupal)</h2>
|
||||
<p class="muted">Trigger re-save for a specific entity to queue it for model conversion.</p>
|
||||
<form id="entityResaveForm">
|
||||
<label>Entity ID<input name="entity_id" type="text" placeholder="e.g. 12345" required></label>
|
||||
<label>Entity Type<input name="entity_type" type="text" placeholder="e.g. wisski_individual" value="wisski_individual"></label>
|
||||
<div style="margin-top:1rem"><button type="submit">Re-save Entity</button></div>
|
||||
</form>
|
||||
<pre id="entityOut" style="margin-top:1rem;padding:1rem;border:1px solid #ddd;background:#f9f9f9;white-space:pre-wrap;display:none"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.querySelectorAll('button[data-action]').forEach(b=>b.addEventListener('click', async ()=>{
|
||||
const action=b.getAttribute('data-action');
|
||||
const out=document.getElementById('out'); out.textContent='...';
|
||||
const res=await fetch('api/actions.php',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json'}, body: JSON.stringify({action})});
|
||||
const j=await res.json(); out.textContent=JSON.stringify(j, null, 2);
|
||||
}));
|
||||
|
||||
document.getElementById('entityResaveForm').addEventListener('submit', async (e)=>{
|
||||
e.preventDefault();
|
||||
const entity_id = document.querySelector('input[name="entity_id"]').value;
|
||||
const entity_type = document.querySelector('input[name="entity_type"]').value;
|
||||
const outEl = document.getElementById('entityOut');
|
||||
outEl.style.display='block';
|
||||
outEl.textContent='...';
|
||||
const res = await fetch('api/actions.php',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json'}, body: JSON.stringify({action:'entity_resave', entity_id, entity_type})});
|
||||
const j = await res.json();
|
||||
outEl.textContent = (j.output || JSON.stringify(j, null, 2));
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
89
viewer/admin/api/actions.php
Normal file
89
viewer/admin/api/actions.php
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
require __DIR__ . '/common.php';
|
||||
|
||||
$root = workspace_root();
|
||||
$body = file_get_contents('php://input');
|
||||
$data = json_decode($body, true);
|
||||
if (!$data || !isset($data['action'])) { http_response_code(400); json_response(['error'=>'invalid_request']); }
|
||||
|
||||
$action = $data['action'];
|
||||
if ($action === 'clear_cache') {
|
||||
$target = $root . '/viewer/build';
|
||||
$out = '';
|
||||
if (is_dir($target)) {
|
||||
// remove all files
|
||||
$cmd = 'rm -rf ' . escapeshellarg($target) . '/*';
|
||||
$out = shell_exec($cmd . ' 2>&1');
|
||||
} else { $out = 'no build dir'; }
|
||||
json_response(['ok'=>true,'output'=>$out]);
|
||||
}
|
||||
|
||||
if ($action === 'rebuild_thumbs') {
|
||||
$script = $root . '/scripts/render.sh';
|
||||
if (file_exists($script) && is_executable($script)) {
|
||||
$out = shell_exec(escapeshellcmd($script) . ' 2>&1');
|
||||
json_response(['ok'=>true,'output'=>$out]);
|
||||
}
|
||||
http_response_code(500); json_response(['error'=>'script_missing']);
|
||||
}
|
||||
|
||||
if ($action === 'diagnostics') {
|
||||
$out = [];
|
||||
$out['php'] = shell_exec('php -v 2>&1');
|
||||
$out['uname'] = shell_exec('uname -a 2>&1');
|
||||
$out['df'] = shell_exec('df -h 2>&1');
|
||||
json_response(['ok'=>true,'output'=>$out]);
|
||||
}
|
||||
|
||||
if ($action === 'entity_resave') {
|
||||
$entity_id = $data['entity_id'] ?? null;
|
||||
$entity_type = $data['entity_type'] ?? 'wisski_individual';
|
||||
if (!$entity_id) { http_response_code(400); json_response(['error'=>'missing_entity_id']); }
|
||||
|
||||
$out = "Attempting to re-save entity $entity_type:$entity_id ...\n\n";
|
||||
|
||||
// Try to bootstrap Drupal if available
|
||||
$drupal_root = $root;
|
||||
if (file_exists($drupal_root . '/index.php')) {
|
||||
$out .= "Found index.php, attempting Drupal bootstrap...\n";
|
||||
try {
|
||||
// Push to Drupal context
|
||||
$cwd = getcwd();
|
||||
chdir($drupal_root);
|
||||
|
||||
// Simple Drupal load without full bootstrap (safer)
|
||||
if (function_exists('drush_main')) {
|
||||
$out .= "Drush detected, will attempt re-save.\n";
|
||||
}
|
||||
|
||||
// Try using Drupal's entity loader via autoloader
|
||||
if (file_exists($drupal_root . '/vendor/autoload.php')) {
|
||||
require_once $drupal_root . '/vendor/autoload.php';
|
||||
$out .= "Drupal autoloader loaded.\n";
|
||||
|
||||
// Simple check: can we access Drupal\Core ?
|
||||
if (class_exists('Drupal\Core\Entity\EntityTypeManager')) {
|
||||
$out .= "Drupal namespace available - attempting entity save.\n";
|
||||
// Real bootstrap would require more setup
|
||||
$out .= "INFO: Full entity re-save requires Drupal context.\n";
|
||||
}
|
||||
}
|
||||
chdir($cwd);
|
||||
$out .= "\nTo re-save this entity, use Drupal CLI:\n";
|
||||
$out .= " drush entity:save $entity_type $entity_id\n";
|
||||
$out .= "Or access Drupal admin panel and update the entity.\n";
|
||||
} catch (\Throwable $e) {
|
||||
$out .= "Bootstrap check failed: " . $e->getMessage() . "\n";
|
||||
$out .= "\nTo re-save this entity, use Drupal CLI:\n";
|
||||
$out .= " drush entity:save $entity_type $entity_id\n";
|
||||
}
|
||||
} else {
|
||||
$out .= "Drupal not detected in this directory.\n";
|
||||
$out .= "To re-save an entity, use:\n";
|
||||
$out .= " drush entity:save $entity_type $entity_id\n";
|
||||
$out .= "Or trigger via Drupal admin panel.\n";
|
||||
}
|
||||
json_response(['ok'=>true,'output'=>$out]);
|
||||
}
|
||||
|
||||
http_response_code(400); json_response(['error'=>'unknown_action']);
|
||||
62
viewer/admin/api/backups.php
Normal file
62
viewer/admin/api/backups.php
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
require __DIR__ . '/common.php';
|
||||
|
||||
$root = workspace_root();
|
||||
|
||||
// GET ?target=settings|env -> list backups (most recent first, limited to 5)
|
||||
// POST restore {target, file}
|
||||
|
||||
function list_backups($target) {
|
||||
global $root;
|
||||
if ($target === 'settings') {
|
||||
$path = $root . '/viewer-settings.json';
|
||||
$base = 'viewer-settings.json';
|
||||
} elseif ($target === 'env') {
|
||||
$path = $root . '/scripts/.env';
|
||||
$base = '.env';
|
||||
$dir = dirname($path);
|
||||
$base = basename($path);
|
||||
} else {
|
||||
return ['error'=>'invalid_target'];
|
||||
}
|
||||
$dir = dirname($path);
|
||||
$pattern = $dir . '/' . $base . '.*';
|
||||
$files = glob($pattern);
|
||||
// filter timestamped copies only (YYYYmmdd-HHMMSS)
|
||||
$backups = array_filter($files, function($f) use ($base){ return preg_match('/' . preg_quote($base, '/') . '\.\d{8}-\d{6}$/', $f); });
|
||||
usort($backups, function($a,$b){ return filemtime($b) - filemtime($a); });
|
||||
$backups = array_slice($backups, 0, 5);
|
||||
$out = [];
|
||||
foreach($backups as $b) $out[] = ['file'=>basename($b),'path'=>$b,'ts'=>date('c', filemtime($b))];
|
||||
return ['backups'=>$out];
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
$target = $_GET['target'] ?? null;
|
||||
if (!$target) { http_response_code(400); json_response(['error'=>'missing_target']); }
|
||||
json_response(list_backups($target));
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$body = file_get_contents('php://input');
|
||||
$data = json_decode($body, true);
|
||||
if (!$data || !isset($data['target']) || !isset($data['file'])) { http_response_code(400); json_response(['error'=>'invalid_request']); }
|
||||
$target = $data['target'];
|
||||
$file = basename($data['file']);
|
||||
if ($target === 'settings') {
|
||||
$path = $root . '/viewer-settings.json';
|
||||
$dir = dirname($path);
|
||||
$src = $dir . '/' . $file;
|
||||
} elseif ($target === 'env') {
|
||||
$path = $root . '/scripts/.env';
|
||||
$dir = dirname($path);
|
||||
$src = $dir . '/' . $file;
|
||||
} else { http_response_code(400); json_response(['error'=>'invalid_target']); }
|
||||
if (!file_exists($src)) { http_response_code(404); json_response(['error'=>'not_found']); }
|
||||
// make a backup of current file before restore
|
||||
if (file_exists($path)) backup_file($path);
|
||||
if (!copy($src, $path)) { http_response_code(500); json_response(['error'=>'restore_failed']); }
|
||||
json_response(['ok'=>true]);
|
||||
}
|
||||
|
||||
http_response_code(405); json_response(['error'=>'method_not_allowed']);
|
||||
55
viewer/admin/api/common.php
Normal file
55
viewer/admin/api/common.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['admin'])) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
function json_response($data) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
}
|
||||
|
||||
function workspace_root() {
|
||||
// admin/api is at viewer/admin/api -> go up 3
|
||||
return realpath(__DIR__ . '/../../../');
|
||||
}
|
||||
|
||||
function backup_file($path, $keep = 5) {
|
||||
if (!file_exists($path)) return;
|
||||
$dir = dirname($path);
|
||||
$base = basename($path);
|
||||
// simple .bak (latest)
|
||||
copy($path, $dir . '/' . $base . '.bak');
|
||||
// timestamped
|
||||
$ts = date('Ymd-His');
|
||||
$copy = $dir . '/' . $base . '.' . $ts;
|
||||
copy($path, $copy);
|
||||
|
||||
// rotate old backups: match $base.* in same dir (exclude .bak)
|
||||
$pattern = $dir . '/' . $base . '.*';
|
||||
$files = glob($pattern);
|
||||
// filter timestamped copies only (YYYYmmdd-HHMMSS)
|
||||
$backups = array_filter($files, function($f) use ($base){ return preg_match('/' . preg_quote($base, '/') . '\.\d{8}-\d{6}$/', $f); });
|
||||
usort($backups, function($a,$b){ return filemtime($b) - filemtime($a); });
|
||||
if (count($backups) > $keep) {
|
||||
$remove = array_slice($backups, $keep);
|
||||
foreach($remove as $r) @unlink($r);
|
||||
}
|
||||
}
|
||||
|
||||
function save_uploaded_schema($targetPath) {
|
||||
if (!isset($_FILES['file'])) return ['error'=>'no_file'];
|
||||
$f = $_FILES['file'];
|
||||
if ($f['error'] !== UPLOAD_ERR_OK) return ['error'=>'upload_error'];
|
||||
// validate json
|
||||
$content = file_get_contents($f['tmp_name']);
|
||||
if (json_decode($content) === null) return ['error'=>'invalid_json'];
|
||||
// backup existing
|
||||
if (file_exists($targetPath)) backup_file($targetPath, intval(getenv('ADMIN_BACKUP_KEEP') ?: 10));
|
||||
if (!is_dir(dirname($targetPath))) mkdir(dirname($targetPath), 0755, true);
|
||||
if (!move_uploaded_file($f['tmp_name'], $targetPath)) return ['error'=>'move_failed'];
|
||||
return ['ok'=>true];
|
||||
}
|
||||
53
viewer/admin/api/env.php
Normal file
53
viewer/admin/api/env.php
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
require __DIR__ . '/common.php';
|
||||
|
||||
$root = workspace_root();
|
||||
$target = $root . '/scripts/.env';
|
||||
$example = $root . '/scripts/.env.example';
|
||||
|
||||
// Ensure .env exists by copying example
|
||||
if (!file_exists($target)) {
|
||||
if (file_exists($example)) copy($example, $target);
|
||||
else file_put_contents($target, "# .env\n");
|
||||
}
|
||||
|
||||
function parse_env($path) {
|
||||
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
$out = [];
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || strpos($line, '#') === 0) continue;
|
||||
if (strpos($line, '=') !== false) {
|
||||
list($k,$v) = explode('=', $line, 2);
|
||||
$out[trim($k)] = trim($v);
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
json_response(parse_env($target));
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$body = file_get_contents('php://input');
|
||||
$decoded = json_decode($body, true);
|
||||
if (!is_array($decoded)) {
|
||||
http_response_code(400);
|
||||
json_response(['error' => 'invalid_json']);
|
||||
}
|
||||
// backup
|
||||
backup_file($target);
|
||||
// write env
|
||||
$lines = [];
|
||||
foreach ($decoded as $k => $v) {
|
||||
$lines[] = $k . '=' . $v;
|
||||
}
|
||||
$tmp = $target . '.tmp';
|
||||
file_put_contents($tmp, implode("\n", $lines) . "\n");
|
||||
rename($tmp, $target);
|
||||
json_response(['ok' => true]);
|
||||
}
|
||||
|
||||
http_response_code(405);
|
||||
json_response(['error' => 'method_not_allowed']);
|
||||
17
viewer/admin/api/env_schema.php
Normal file
17
viewer/admin/api/env_schema.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
require __DIR__ . '/common.php';
|
||||
|
||||
$root = workspace_root();
|
||||
$p = $root . '/scripts/.env.schema.json';
|
||||
if (file_exists($p)) {
|
||||
if (isset($_GET['delete']) && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (@unlink($p)) json_response(['ok'=>true]);
|
||||
http_response_code(500); json_response(['error'=>'delete_failed']);
|
||||
}
|
||||
header('Content-Type: application/json');
|
||||
echo file_get_contents($p);
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(204);
|
||||
exit;
|
||||
36
viewer/admin/api/hdri.php
Normal file
36
viewer/admin/api/hdri.php
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
require __DIR__ . '/common.php';
|
||||
|
||||
$root = workspace_root();
|
||||
$dir = $root . '/viewer/hdri';
|
||||
if (!is_dir($dir)) mkdir($dir, 0755, true);
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
$files = array_values(array_filter(scandir($dir), function($f){ return !in_array($f, ['.','..']); }));
|
||||
json_response(['files' => $files]);
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// If multipart upload
|
||||
if (!empty($_FILES['file'])) {
|
||||
$f = $_FILES['file'];
|
||||
if ($f['error'] !== UPLOAD_ERR_OK) { http_response_code(400); json_response(['error'=>'upload_error']); }
|
||||
$name = basename($f['name']);
|
||||
$target = $dir . '/' . $name;
|
||||
if (!move_uploaded_file($f['tmp_name'], $target)) { http_response_code(500); json_response(['error'=>'move_failed']); }
|
||||
json_response(['ok'=>true,'file'=>$name]);
|
||||
}
|
||||
// JSON action (delete)
|
||||
$body = file_get_contents('php://input');
|
||||
$data = json_decode($body, true);
|
||||
if (!$data) { http_response_code(400); json_response(['error'=>'invalid_json']); }
|
||||
if (isset($data['action']) && $data['action'] === 'delete' && isset($data['file'])) {
|
||||
$file = basename($data['file']);
|
||||
$path = $dir . '/' . $file;
|
||||
if (file_exists($path)) { unlink($path); json_response(['ok'=>true]); }
|
||||
http_response_code(404); json_response(['error'=>'not_found']);
|
||||
}
|
||||
http_response_code(400); json_response(['error'=>'unknown_action']);
|
||||
}
|
||||
|
||||
http_response_code(405); json_response(['error'=>'method_not_allowed']);
|
||||
41
viewer/admin/api/settings.php
Normal file
41
viewer/admin/api/settings.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
require __DIR__ . '/common.php';
|
||||
|
||||
$root = workspace_root();
|
||||
$target = $root . '/viewer-settings.json';
|
||||
$example1 = $root . '/viewer-settings-example.json';
|
||||
$example2 = $root . '/viewer/viewer-settings-example.json';
|
||||
|
||||
// Ensure file exists by copying example if needed
|
||||
if (!file_exists($target)) {
|
||||
if (file_exists($example1)) copy($example1, $target);
|
||||
elseif (file_exists($example2)) copy($example2, $target);
|
||||
else file_put_contents($target, json_encode(new stdClass()));
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
$data = file_get_contents($target);
|
||||
header('Content-Type: application/json');
|
||||
echo $data;
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$body = file_get_contents('php://input');
|
||||
// validate JSON
|
||||
$decoded = json_decode($body, true);
|
||||
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
|
||||
http_response_code(400);
|
||||
json_response(['error' => 'invalid_json']);
|
||||
}
|
||||
// backup
|
||||
backup_file($target);
|
||||
// atomic write
|
||||
$tmp = $target . '.tmp';
|
||||
file_put_contents($tmp, json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
rename($tmp, $target);
|
||||
json_response(['ok' => true]);
|
||||
}
|
||||
|
||||
http_response_code(405);
|
||||
json_response(['error' => 'method_not_allowed']);
|
||||
24
viewer/admin/api/settings_schema.php
Normal file
24
viewer/admin/api/settings_schema.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
require __DIR__ . '/common.php';
|
||||
|
||||
$root = workspace_root();
|
||||
$candidates = [
|
||||
$root . '/viewer-settings.schema.json',
|
||||
$root . '/viewer/viewer-settings.schema.json',
|
||||
];
|
||||
|
||||
foreach ($candidates as $p) {
|
||||
if (file_exists($p)) {
|
||||
if (isset($_GET['delete']) && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (@unlink($p)) json_response(['ok'=>true]);
|
||||
http_response_code(500); json_response(['error'=>'delete_failed']);
|
||||
}
|
||||
header('Content-Type: application/json');
|
||||
echo file_get_contents($p);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// no schema found
|
||||
http_response_code(204);
|
||||
exit;
|
||||
24
viewer/admin/api/upload_schema.php
Normal file
24
viewer/admin/api/upload_schema.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
require __DIR__ . '/common.php';
|
||||
|
||||
$root = workspace_root();
|
||||
|
||||
// expects multipart/form-data with field 'file' and 'target' in POST (values: 'settings' or 'env')
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405); json_response(['error'=>'method_not_allowed']);
|
||||
}
|
||||
|
||||
$target = $_POST['target'] ?? $_GET['target'] ?? null;
|
||||
if (!$target) { http_response_code(400); json_response(['error'=>'missing_target']); }
|
||||
|
||||
if ($target === 'settings') {
|
||||
$dest = $root . '/viewer-settings.schema.json';
|
||||
} elseif ($target === 'env') {
|
||||
$dest = $root . '/scripts/.env.schema.json';
|
||||
} else {
|
||||
http_response_code(400); json_response(['error'=>'invalid_target']);
|
||||
}
|
||||
|
||||
$res = save_uploaded_schema($dest);
|
||||
if (isset($res['ok'])) json_response($res);
|
||||
http_response_code(400); json_response($res);
|
||||
24
viewer/admin/create_admin.php
Normal file
24
viewer/admin/create_admin.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
// CLI script: php create_admin.php username password
|
||||
if (php_sapi_name() !== 'cli') {
|
||||
echo "This script must be run from CLI\n";
|
||||
exit(1);
|
||||
}
|
||||
if ($argc < 3) {
|
||||
echo "Usage: php create_admin.php username password\n";
|
||||
exit(1);
|
||||
}
|
||||
$username = $argv[1];
|
||||
$password = $argv[2];
|
||||
|
||||
$pdo = require __DIR__ . '/db.php';
|
||||
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
$stmt = $pdo->prepare('INSERT INTO admins (username, password, created_at) VALUES (?, ?, ?)');
|
||||
try {
|
||||
$stmt->execute([$username, $hash, date('c')]);
|
||||
echo "Admin user created: $username\n";
|
||||
} catch (Exception $e) {
|
||||
echo "Error creating admin: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
15
viewer/admin/db.php
Normal file
15
viewer/admin/db.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
// Simple SQLite PDO connection for admin panel
|
||||
$dbFile = __DIR__ . '/admin.sqlite';
|
||||
$pdo = new PDO('sqlite:' . $dbFile);
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
// Ensure table exists when connection is first used
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS admins (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
)");
|
||||
|
||||
return $pdo;
|
||||
100
viewer/admin/env.php
Normal file
100
viewer/admin/env.php
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['admin'])) { header('Location: login.php'); exit; }
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Environment</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<style>table{border-collapse:collapse} td,th{padding:.3rem;border:1px solid #ddd}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header class="site"><h1>Environment (.env)</h1><nav class="admin-links"><a href="index.php">Wróć</a></nav></header>
|
||||
<div class="card">
|
||||
<table id="tbl"><thead><tr><th>Key</th><th>Value</th><th></th></tr></thead><tbody></tbody></table>
|
||||
<div style="margin-top:.5rem"><button id="add">Dodaj</button> <button id="save">Zapisz</button> <a href="index.php">Wróć</a></div>
|
||||
<hr>
|
||||
<h3>Env Schema</h3>
|
||||
<form id="uploadEnvSchema" enctype="multipart/form-data">
|
||||
<input type="file" name="file" accept="application/json"> <input type="hidden" name="target" value="env"> <button>Upload Env Schema</button>
|
||||
</form>
|
||||
<button id="deleteEnvSchema">Delete Env Schema</button>
|
||||
<div id="envSchemaMsg" style="margin-top:.5rem;color:green"></div>
|
||||
<h3>Backups</h3>
|
||||
<div id="envBackups">Loading backups...</div>
|
||||
<div id="envBackupMsg" style="margin-top:.5rem;color:green"></div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/ajv@8.12.0/dist/ajv7.min.js"></script>
|
||||
<script>
|
||||
let envSchema = null;
|
||||
async function loadSchema(){
|
||||
try{
|
||||
const res = await fetch('api/env_schema.php', {credentials:'same-origin'});
|
||||
if (res.status === 200) { envSchema = await res.json(); const el=document.createElement('div'); el.style.fontSize='90%'; el.style.margin='0.5rem 0'; el.textContent='Env schema loaded — validation enabled.'; document.body.insertBefore(el, document.getElementById('tbl')) }
|
||||
}catch(e){/* ignore */}
|
||||
}
|
||||
|
||||
async function load(){
|
||||
const res = await fetch('api/env.php', {credentials:'same-origin'});
|
||||
const data = await res.json();
|
||||
const tbody = document.querySelector('#tbl tbody'); tbody.innerHTML='';
|
||||
for(const k of Object.keys(data)){
|
||||
addRow(k, data[k]);
|
||||
}
|
||||
}
|
||||
function addRow(k='',v=''){ const tbody=document.querySelector('#tbl tbody'); const tr=document.createElement('tr'); tr.innerHTML=`<td><input class="k" value="${k}"></td><td><input class="v" value="${v}"></td><td><button class="del">X</button></td>`; tbody.appendChild(tr); tr.querySelector('.del').addEventListener('click',()=>tr.remove()); }
|
||||
document.getElementById('add').addEventListener('click',()=>addRow());
|
||||
document.getElementById('save').addEventListener('click', async ()=>{
|
||||
const rows = document.querySelectorAll('#tbl tbody tr'); const out={};
|
||||
rows.forEach(r=>{ const k=r.querySelector('.k').value; const v=r.querySelector('.v').value; if(k) out[k]=v; });
|
||||
// client-side validation
|
||||
if (envSchema) {
|
||||
const Ajv = window.ajv7.Ajv; const ajv = new Ajv({allErrors:true});
|
||||
const validate = ajv.compile(envSchema);
|
||||
const valid = validate(out);
|
||||
if (!valid) {
|
||||
alert('Env validation errors:\n' + validate.errors.map(e=>`${e.instancePath} ${e.message}`).join('\n'));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// basic validation: keys uppercase letters, digits and underscore
|
||||
for(const k of Object.keys(out)){
|
||||
if (!/^[A-Z0-9_]+$/.test(k)) { alert('Invalid key: ' + k + '\nKeys should be uppercase letters, digits or underscore'); return; }
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetch('api/env.php',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json'}, body: JSON.stringify(out)});
|
||||
const j = await res.json(); alert(JSON.stringify(j));
|
||||
});
|
||||
|
||||
(async ()=>{
|
||||
await loadSchema();
|
||||
load();
|
||||
document.getElementById('uploadEnvSchema').addEventListener('submit', async (e)=>{
|
||||
e.preventDefault(); const fd = new FormData(e.target); const res = await fetch('api/upload_schema.php',{method:'POST',credentials:'same-origin', body: fd}); const j = await res.json(); document.getElementById('envSchemaMsg').textContent = JSON.stringify(j); if (j.ok) setTimeout(()=>location.reload(),700);
|
||||
});
|
||||
document.getElementById('deleteEnvSchema').addEventListener('click', async ()=>{
|
||||
if (!confirm('Delete env schema file?')) return; const res = await fetch('api/env_schema.php?delete=1',{method:'POST',credentials:'same-origin'}); const j = await res.json(); alert(JSON.stringify(j)); if (j.ok) setTimeout(()=>location.reload(),700);
|
||||
});
|
||||
async function loadEnvBackups(){
|
||||
const res = await fetch('api/backups.php?target=env',{credentials:'same-origin'});
|
||||
const j = await res.json();
|
||||
const el = document.getElementById('envBackups');
|
||||
if (j.error) { el.textContent = JSON.stringify(j); return; }
|
||||
if (!j.backups || j.backups.length===0) { el.textContent='No backups found'; return; }
|
||||
el.innerHTML='';
|
||||
j.backups.forEach(b=>{
|
||||
const div=document.createElement('div'); div.textContent = b.file + ' ('+b.ts+') '; const btn=document.createElement('button'); btn.textContent='Restore'; btn.addEventListener('click', async ()=>{ if(!confirm('Restore '+b.file+' ?')) return; const r = await fetch('api/backups.php',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json'}, body: JSON.stringify({target:'env', file: b.file})}); const R=await r.json(); document.getElementById('envBackupMsg').textContent = JSON.stringify(R); if(R.ok) setTimeout(()=>location.reload(),700); }); div.appendChild(btn); el.appendChild(div);
|
||||
});
|
||||
}
|
||||
loadEnvBackups();
|
||||
})();
|
||||
</script>
|
||||
</div> <!-- .wrap -->
|
||||
</body>
|
||||
</html>
|
||||
40
viewer/admin/hdri.php
Normal file
40
viewer/admin/hdri.php
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['admin'])) { header('Location: login.php'); exit; }
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>HDRI</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<style>li{margin:.3rem 0}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header class="site"><h1>HDRI</h1><nav class="admin-links"><a href="index.php">Wróć</a></nav></header>
|
||||
<div class="card">
|
||||
<form id="upload" enctype="multipart/form-data">
|
||||
<input type="file" name="file"> <button>Upload</button>
|
||||
</form>
|
||||
<ul id="list"></ul>
|
||||
</div>
|
||||
<p class="footer">Manage HDRI files used by the viewer.</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function load(){
|
||||
const res = await fetch('api/hdri.php', {credentials:'same-origin'});
|
||||
const data = await res.json();
|
||||
const ul = document.getElementById('list'); ul.innerHTML='';
|
||||
data.files.forEach(f=>{ const li=document.createElement('li'); li.textContent=f + ' '; const del=document.createElement('button'); del.textContent='Usuń'; del.addEventListener('click', ()=> delFile(f)); li.appendChild(del); ul.appendChild(li); });
|
||||
}
|
||||
async function delFile(filename){ if(!confirm('Usuń '+filename+' ?')) return; const res=await fetch('api/hdri.php',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json'}, body: JSON.stringify({action:'delete',file:filename})}); const j=await res.json(); alert(JSON.stringify(j)); load(); }
|
||||
document.getElementById('upload').addEventListener('submit', async (e)=>{
|
||||
e.preventDefault(); const f=e.target.file.files[0]; if(!f){alert('Wybierz plik'); return;} const fd=new FormData(); fd.append('file', f); const res=await fetch('api/hdri.php',{method:'POST',credentials:'same-origin', body: fd}); const j=await res.json(); alert(JSON.stringify(j)); load();
|
||||
});
|
||||
load();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
30
viewer/admin/index.php
Normal file
30
viewer/admin/index.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['admin'])) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Admin Panel</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header class="site"><h1>Admin Panel</h1><nav class="admin-links"><a href="logout.php">Logout</a></nav></header>
|
||||
<div class="card">
|
||||
<p>Logged in as <strong><?php echo htmlentities($_SESSION['admin']) ?></strong></p>
|
||||
<div class="flex">
|
||||
<a class="small" href="settings.php">Viewer Settings</a>
|
||||
<a class="small" href="env.php">Environment</a>
|
||||
<a class="small" href="hdri.php">HDRI</a>
|
||||
<a class="small" href="actions.php">Maintenance</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
45
viewer/admin/login.php
Normal file
45
viewer/admin/login.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
session_start();
|
||||
if (isset($_SESSION['admin'])) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
$error = '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$username = $_POST['username'] ?? '';
|
||||
$password = $_POST['password'] ?? '';
|
||||
$pdo = require __DIR__ . '/db.php';
|
||||
$stmt = $pdo->prepare('SELECT * FROM admins WHERE username = ?');
|
||||
$stmt->execute([$username]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($row && password_verify($password, $row['password'])) {
|
||||
$_SESSION['admin'] = $row['username'];
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
$error = 'Invalid login credentials. Please try again.';
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Admin Login</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header class="site"><h1>Panel Admin</h1></header>
|
||||
<div class="card">
|
||||
<?php if ($error): ?><div class="msg err"><?php echo htmlentities($error) ?></div><?php endif ?>
|
||||
<form method="post" class="login-form">
|
||||
<label>Username<input name="username" required></label>
|
||||
<label>Password<input name="password" type="password" required></label>
|
||||
<div style="margin-top:1rem"><button type="submit">Login</button></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="footer">If you don't have an account, run <code>php viewer/admin/create_admin.php <user> <pass></code></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
6
viewer/admin/logout.php
Normal file
6
viewer/admin/logout.php
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
session_start();
|
||||
session_unset();
|
||||
session_destroy();
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
119
viewer/admin/settings.php
Normal file
119
viewer/admin/settings.php
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['admin'])) { header('Location: login.php'); exit; }
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Viewer Settings</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/jsoneditor@9.9.0/dist/jsoneditor.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<style>#editor{height:60vh;border:1px solid #ddd}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header class="site"><h1>Viewer Settings</h1><nav class="admin-links"><a href="index.php">Wróć</a></nav></header>
|
||||
<div class="card">
|
||||
<div style="margin-bottom:.5rem">
|
||||
<button id="viewTree">Tree View</button>
|
||||
<button id="viewForm">Form View</button>
|
||||
</div>
|
||||
<div id="editor"></div>
|
||||
<div style="margin-top:1rem">
|
||||
<button id="save">Zapisz</button>
|
||||
<button id="reload">Wczytaj</button>
|
||||
<a href="index.php">Wróć</a>
|
||||
</div>
|
||||
<hr>
|
||||
<h3>Schema management</h3>
|
||||
<form id="uploadSchema" enctype="multipart/form-data">
|
||||
<input type="file" name="file" accept="application/json"> <input type="hidden" name="target" value="settings"> <button>Upload Schema</button>
|
||||
</form>
|
||||
<button id="deleteSchema">Delete Schema</button>
|
||||
<div id="schemaMsg" style="margin-top:.5rem;color:green"></div>
|
||||
<h3>Backups</h3>
|
||||
<div id="backupsList">Loading backups...</div>
|
||||
<div id="backupMsg" style="margin-top:.5rem;color:green"></div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jsoneditor@9.9.0/dist/jsoneditor.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/ajv@8.12.0/dist/ajv7.min.js"></script>
|
||||
<script>
|
||||
(async ()=>{
|
||||
const container = document.getElementById('editor');
|
||||
let editor = null;
|
||||
let schema = null;
|
||||
|
||||
function createEditor(mode){
|
||||
if(editor) editor.destroy();
|
||||
const options = {mode: mode};
|
||||
if (mode === 'form' && schema) options.schema = schema;
|
||||
editor = new JSONEditor(container, options);
|
||||
}
|
||||
|
||||
async function loadSchema() {
|
||||
const res = await fetch('api/settings_schema.php', {credentials:'same-origin'});
|
||||
if (res.status === 200) {
|
||||
schema = await res.json();
|
||||
const el = document.createElement('div'); el.style.fontSize='90%'; el.style.margin='0.5rem 0'; el.textContent = 'Schema loaded — validation enabled.'; document.body.insertBefore(el, container);
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const res = await fetch('api/settings.php', {credentials: 'same-origin'});
|
||||
const data = await res.json();
|
||||
if (!editor) createEditor('tree');
|
||||
try { editor.set(data); } catch(e) { console.error(e); }
|
||||
}
|
||||
|
||||
document.getElementById('reload').addEventListener('click', load);
|
||||
document.getElementById('viewTree').addEventListener('click', ()=>{ createEditor('tree'); load(); });
|
||||
document.getElementById('viewForm').addEventListener('click', ()=>{ createEditor('form'); load(); });
|
||||
|
||||
document.getElementById('save').addEventListener('click', async ()=>{
|
||||
const data = editor.get();
|
||||
if (schema) {
|
||||
const Ajv = window.ajv7.Ajv; const ajv = new Ajv({allErrors:true});
|
||||
const validate = ajv.compile(schema);
|
||||
const valid = validate(data);
|
||||
if (!valid) {
|
||||
alert('Validation errors:\n' + validate.errors.map(e=>`${e.instancePath} ${e.message}`).join('\n'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
const res = await fetch('api/settings.php', {method:'POST', credentials:'same-origin', headers:{'Content-Type':'application/json'}, body: JSON.stringify(data)});
|
||||
const j = await res.json();
|
||||
alert(JSON.stringify(j));
|
||||
});
|
||||
|
||||
// load schema then data
|
||||
await loadSchema();
|
||||
createEditor('tree');
|
||||
load();
|
||||
// upload schema
|
||||
document.getElementById('uploadSchema').addEventListener('submit', async (e)=>{
|
||||
e.preventDefault(); const fd = new FormData(e.target); const res = await fetch('api/upload_schema.php',{method:'POST',credentials:'same-origin', body: fd}); const j = await res.json(); document.getElementById('schemaMsg').textContent = JSON.stringify(j); if (j.ok) setTimeout(()=>location.reload(),700);
|
||||
});
|
||||
document.getElementById('deleteSchema').addEventListener('click', async ()=>{
|
||||
if (!confirm('Delete schema file?')) return; const res = await fetch('api/settings_schema.php?delete=1',{method:'POST',credentials:'same-origin'}); const j = await res.json(); alert(JSON.stringify(j)); if (j.ok) setTimeout(()=>location.reload(),700);
|
||||
});
|
||||
// backups
|
||||
async function loadBackups(){
|
||||
const res = await fetch('api/backups.php?target=settings',{credentials:'same-origin'});
|
||||
const j = await res.json();
|
||||
const el = document.getElementById('backupsList');
|
||||
if (j.error) { el.textContent = JSON.stringify(j); return; }
|
||||
if (!j.backups || j.backups.length===0) { el.textContent='No backups found'; return; }
|
||||
el.innerHTML='';
|
||||
j.backups.forEach(b=>{
|
||||
const div=document.createElement('div'); div.textContent = b.file + ' ('+b.ts+') '; const btn=document.createElement('button'); btn.textContent='Restore'; btn.addEventListener('click', async ()=>{ if(!confirm('Restore '+b.file+' ?')) return; const r = await fetch('api/backups.php',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json'}, body: JSON.stringify({target:'settings', file: b.file})}); const R=await r.json(); document.getElementById('backupMsg').textContent = JSON.stringify(R); if(R.ok) setTimeout(()=>location.reload(),700); }); div.appendChild(btn); el.appendChild(div);
|
||||
});
|
||||
}
|
||||
loadBackups();
|
||||
})();
|
||||
</script>
|
||||
</div> <!-- .wrap -->
|
||||
</body>
|
||||
</html>
|
||||
34
viewer/admin/style.css
Normal file
34
viewer/admin/style.css
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/* Basic admin panel styling */
|
||||
:root{
|
||||
--bg:#f6f8fb;
|
||||
--card:#ffffff;
|
||||
--muted:#6b7280;
|
||||
--accent:#0b74de;
|
||||
--danger:#dc2626;
|
||||
}
|
||||
body{font-family:Inter, Roboto, Arial, Helvetica, sans-serif;background:var(--bg);color:#111;margin:0;padding:0}
|
||||
.wrap{max-width:1100px;margin:28px auto;padding:20px}
|
||||
.card{background:var(--card);border:1px solid #e6e9ef;border-radius:8px;padding:18px;box-shadow:0 1px 2px rgba(16,24,40,0.03)}
|
||||
header.site{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}
|
||||
header.site h1{margin:0;font-size:1.25rem}
|
||||
nav.admin-links a{margin-right:10px;color:var(--accent);text-decoration:none}
|
||||
nav.admin-links a:hover{text-decoration:underline}
|
||||
form.login-form{max-width:360px}
|
||||
label{display:block;margin-bottom:8px;font-size:.95rem}
|
||||
input[type=text],input[type=password],input[type=file],textarea,select{width:100%;padding:8px 10px;border:1px solid #d6d9e0;border-radius:6px;background:#fff}
|
||||
button{background:var(--accent);color:#fff;border:none;padding:8px 12px;border-radius:6px;cursor:pointer}
|
||||
button.secondary{background:#e6eefb;color:var(--accent)}
|
||||
button.danger{background:var(--danger)}
|
||||
table{width:100%;border-collapse:collapse}
|
||||
td,th{padding:8px;border-bottom:1px solid #f1f3f6}
|
||||
#editor{min-height:420px;border-radius:6px}
|
||||
.muted{color:var(--muted)}
|
||||
.msg{padding:8px;border-radius:6px;margin-top:8px}
|
||||
.msg.ok{background:#ecfdf5;color:#065f46}
|
||||
.msg.err{background:#fff1f2;color:#7f1d1d}
|
||||
.flex{display:flex;gap:10px;align-items:center}
|
||||
.small{font-size:.9rem}
|
||||
.backups div{margin-bottom:8px}
|
||||
.footer{margin-top:20px;color:var(--muted);font-size:.9rem}
|
||||
|
||||
@media (max-width:600px){.wrap{padding:12px;margin:12px}header.site{flex-direction:column;align-items:flex-start} }
|
||||
Loading…
Add table
Add a link
Reference in a new issue