This commit is contained in:
Bashy 2026-05-03 20:56:15 +03:00
commit 60ca58f5ba
80 changed files with 9458 additions and 0 deletions

9
lib/Audit.php Normal file
View file

@ -0,0 +1,9 @@
<?php
class Audit {
public static function log(PDO $db, string $action, ?int $project_id = null, ?string $detail = null): void {
try {
$db->prepare('INSERT INTO audit_log (user_id, project_id, action, detail, ip) VALUES (?, ?, ?, ?, ?)')
->execute([$_SESSION['user_id'] ?? null, $project_id, $action, $detail, $_SERVER['REMOTE_ADDR'] ?? null]);
} catch (Exception $e) { /* don't let audit failures break the main op */ }
}
}

46
lib/Auth.php Normal file
View file

@ -0,0 +1,46 @@
<?php
class Auth {
public static function check(): bool {
return !empty($_SESSION['user_id']);
}
public static function requireLogin(): void {
if (!self::check()) {
header('Location: /login');
exit;
}
}
public static function login(PDO $db, string $username, string $password): bool {
$stmt = $db->prepare('SELECT id, password_hash FROM users WHERE username = ?');
$stmt->execute([$username]);
$user = $stmt->fetch();
if (!$user || !password_verify($password, $user['password_hash'])) {
return false;
}
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $username;
return true;
}
public static function logout(): void {
$_SESSION = [];
session_destroy();
}
public static function currentUser(): ?array {
if (!self::check()) return null;
return ['id' => $_SESSION['user_id'], 'username' => $_SESSION['username']];
}
public static function createUser(PDO $db, string $username, string $password): int {
$stmt = $db->prepare('INSERT INTO users (username, password_hash) VALUES (?, ?)');
$stmt->execute([$username, password_hash($password, PASSWORD_BCRYPT)]);
return (int)$db->lastInsertId();
}
public static function hasUsers(PDO $db): bool {
return (int)$db->query('SELECT COUNT(*) FROM users')->fetchColumn() > 0;
}
}

41
lib/DB.php Normal file
View file

@ -0,0 +1,41 @@
<?php
class DB {
private static ?PDO $instance = null;
public static function connect(string $path): PDO {
if (self::$instance === null) {
self::$instance = new PDO('sqlite:' . $path, options: [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
self::$instance->exec('PRAGMA foreign_keys = ON');
self::$instance->exec('PRAGMA journal_mode = WAL');
}
return self::$instance;
}
public static function get(): PDO {
return self::$instance ?? throw new RuntimeException('DB not connected');
}
public static function autoMigrate(string $sqlDir): array {
$db = self::get();
$db->exec("CREATE TABLE IF NOT EXISTS schema_migrations (
version TEXT PRIMARY KEY,
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
)");
$files = glob($sqlDir . '/*.sql');
sort($files);
$applied = [];
foreach ($files as $file) {
$version = basename($file, '.sql');
if ($db->query("SELECT 1 FROM schema_migrations WHERE version = " . $db->quote($version))->fetch()) {
continue;
}
$db->exec(file_get_contents($file));
$db->exec("INSERT INTO schema_migrations (version) VALUES (" . $db->quote($version) . ")");
$applied[] = $version;
}
return $applied;
}
}

39
lib/ProjectTypes.php Normal file
View file

@ -0,0 +1,39 @@
<?php
class ProjectTypes {
private static array $types = [];
public static function load(): void {
if (!empty(self::$types)) return;
require_once ROOT . '/lib/project-types/ProjectTypeBase.php';
foreach (glob(ROOT . '/lib/project-types/*.php') as $file) {
if (basename($file) === 'ProjectTypeBase.php') continue;
require_once $file;
}
foreach (get_declared_classes() as $class) {
if (is_subclass_of($class, 'ProjectTypeBase')) {
self::$types[$class::typeSlug()] = $class;
}
}
}
public static function all(): array { return self::$types; }
public static function get(string $slug): ?string {
return self::$types[$slug] ?? null;
}
public static function detect(string $path): string {
foreach (self::$types as $slug => $class) {
if ($slug !== 'generic' && $class::detectFromPath($path)) return $slug;
}
return 'generic';
}
public static function forSelect(): array {
return array_values(array_map(fn($class) => [
'slug' => $class::typeSlug(),
'name' => $class::typeName(),
'icon' => $class::typeIcon(),
], self::$types));
}
}

38
lib/bootstrap.php Normal file
View file

@ -0,0 +1,38 @@
<?php
if (!defined('ROOT')) define('ROOT', dirname(__DIR__));
$config = require ROOT . '/config/config.php';
ini_set('session.cookie_httponly', '1');
ini_set('session.use_strict_mode', '1');
session_name($config['session_name']);
session_start();
require_once ROOT . '/lib/DB.php';
require_once ROOT . '/lib/Auth.php';
require_once ROOT . '/lib/ProjectTypes.php';
require_once ROOT . '/lib/Audit.php';
$db = DB::connect($config['db_path']);
DB::autoMigrate(ROOT . '/sql');
/**
* Returns the deploy fingerprint as ['version','sha','branch','built'] read
* from web/BUILD the file is generated by deploy.sh / deploylocal.sh and
* never committed (see .gitignore). Falls back to 'dev' if missing.
*
* Mirrors /opt/agenda/web/lib/layout.php:buildInfo().
*/
function buildInfo(): array {
static $info = null;
if ($info !== null) return $info;
$info = ['version' => 'dev', 'sha' => '', 'branch' => '', 'built' => ''];
$file = ROOT . '/web/BUILD';
if (!is_readable($file)) return $info;
foreach (file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
if (strpos($line, '=') === false) continue;
[$k, $v] = array_map('trim', explode('=', $line, 2));
if (isset($info[$k])) $info[$k] = $v;
}
return $info;
}

View file

@ -0,0 +1,9 @@
<?php
require_once __DIR__ . '/ProjectTypeBase.php';
class GenericProject extends ProjectTypeBase {
public static function typeSlug(): string { return 'generic'; }
public static function typeName(): string { return 'Generic'; }
public static function typeIcon(): string { return 'bi-folder'; }
public static function description(): string { return 'Generic project directory'; }
}

View file

@ -0,0 +1,29 @@
<?php
require_once __DIR__ . '/ProjectTypeBase.php';
class HexoProject extends ProjectTypeBase {
public static function typeSlug(): string { return 'hexo'; }
public static function typeName(): string { return 'Hexo'; }
public static function typeIcon(): string { return 'bi-hexagon-fill'; }
public static function description(): string { return 'Hexo static site generator'; }
public static function tabs(): array {
return ['dashboard', 'analytics', 'posts', 'config', 'files',
'run', 'themes', 'plugins', 'git',
'notes', 'settings'];
}
public static function commands(): array {
return [
['id' => 'generate', 'label' => 'Generate', 'cmd' => 'hexo generate'],
['id' => 'clean', 'label' => 'Clean', 'cmd' => 'hexo clean'],
['id' => 'deploy', 'label' => 'Deploy', 'cmd' => 'hexo deploy'],
['id' => 'version', 'label' => 'Hexo version','cmd' => 'hexo version'],
];
}
public static function detectFromPath(string $path): bool {
return file_exists($path . '/_config.yml')
&& (is_dir($path . '/source') || is_dir($path . '/themes'));
}
}

View file

@ -0,0 +1,25 @@
<?php
abstract class ProjectTypeBase {
abstract public static function typeSlug(): string;
abstract public static function typeName(): string;
abstract public static function typeIcon(): string;
/** Tabs shown on the project page, in order */
public static function tabs(): array {
return ['dashboard', 'analytics', 'files', 'notes', 'settings'];
}
/** Whitelisted commands available in the command runner */
public static function commands(): array {
return [];
}
/** Return true if this type can be auto-detected from the given path */
public static function detectFromPath(string $path): bool {
return false;
}
public static function description(): string {
return '';
}
}

View file

@ -0,0 +1,17 @@
<?php
require_once __DIR__ . '/ProjectTypeBase.php';
class StorageProject extends ProjectTypeBase {
public static function typeSlug(): string { return 'storage'; }
public static function typeName(): string { return 'Storage'; }
public static function typeIcon(): string { return 'bi-hdd'; }
public static function description(): string { return 'File storage directory'; }
public static function tabs(): array {
return ['dashboard', 'analytics', 'media', 'files', 'notes', 'settings'];
}
public static function detectFromPath(string $path): bool {
return is_dir($path . '/uploads') || is_dir($path . '/files') || is_dir($path . '/storage');
}
}

View file

@ -0,0 +1,17 @@
<?php
require_once __DIR__ . '/ProjectTypeBase.php';
class WebsiteProject extends ProjectTypeBase {
public static function typeSlug(): string { return 'website'; }
public static function typeName(): string { return 'Website'; }
public static function typeIcon(): string { return 'bi-globe'; }
public static function description(): string { return 'Static or PHP website'; }
public static function tabs(): array {
return ['dashboard', 'analytics', 'files', 'git', 'notes', 'settings'];
}
public static function detectFromPath(string $path): bool {
return file_exists($path . '/index.html') || file_exists($path . '/index.php');
}
}