Init
This commit is contained in:
commit
60ca58f5ba
80 changed files with 9458 additions and 0 deletions
9
lib/Audit.php
Normal file
9
lib/Audit.php
Normal 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
46
lib/Auth.php
Normal 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
41
lib/DB.php
Normal 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
39
lib/ProjectTypes.php
Normal 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
38
lib/bootstrap.php
Normal 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;
|
||||
}
|
||||
9
lib/project-types/GenericProject.php
Normal file
9
lib/project-types/GenericProject.php
Normal 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'; }
|
||||
}
|
||||
29
lib/project-types/HexoProject.php
Normal file
29
lib/project-types/HexoProject.php
Normal 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'));
|
||||
}
|
||||
}
|
||||
25
lib/project-types/ProjectTypeBase.php
Normal file
25
lib/project-types/ProjectTypeBase.php
Normal 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 '';
|
||||
}
|
||||
}
|
||||
17
lib/project-types/StorageProject.php
Normal file
17
lib/project-types/StorageProject.php
Normal 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');
|
||||
}
|
||||
}
|
||||
17
lib/project-types/WebsiteProject.php
Normal file
17
lib/project-types/WebsiteProject.php
Normal 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');
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue