chore: восстановление репозитория из снапшота v0.3.1

Прежняя git-история утрачена при переносе проекта на машину владельца
(снапшот без .git). Хэши коммитов в docs/reports/* относятся к утраченной
истории.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
sab.code.lab 2026-08-07 09:34:02 +02:00
commit 1c85091186
184 changed files with 33303 additions and 0 deletions

95
server/lib/db.php Normal file
View file

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
/**
* Подключение к SQLite через PDO и идемпотентная init-миграция схемы.
* Контракт: docs/INTERFACES.md, раздел «Личный кабинет (этап 8)».
*/
/**
* Путь к БД: server/config.php, при отсутствии config.example.php
* (который читает env GOLEARN_DB так работает smoke-тест).
*
* @return array{db_path: string}
*/
function golearn_config(): array
{
$file = __DIR__ . '/../config.php';
if (!is_file($file)) {
$file = __DIR__ . '/../config.example.php';
}
$config = require $file;
if (!is_array($config) || empty($config['db_path']) || !is_string($config['db_path'])) {
throw new RuntimeException('config: нет db_path');
}
return $config;
}
/**
* Ленивый синглтон PDO. PRAGMA по контракту: WAL, busy_timeout=5000,
* foreign_keys=ON. Ошибки только исключения (текст наружу не отдаём).
*/
function db(): PDO
{
static $pdo = null;
if ($pdo instanceof PDO) {
return $pdo;
}
$config = golearn_config();
$pdo = new PDO('sqlite:' . $config['db_path'], null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$pdo->exec('PRAGMA journal_mode=WAL');
$pdo->exec('PRAGMA busy_timeout=5000');
$pdo->exec('PRAGMA foreign_keys=ON');
golearn_migrate($pdo);
return $pdo;
}
/**
* Идемпотентная init-миграция схемы (IF NOT EXISTS).
*/
function golearn_migrate(PDO $pdo): void
{
$pdo->exec(
'CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
email TEXT UNIQUE COLLATE NOCASE,
pass_hash TEXT NOT NULL,
created_at TEXT NOT NULL
)'
);
$pdo->exec(
'CREATE TABLE IF NOT EXISTS progress (
user_id INTEGER PRIMARY KEY REFERENCES users(id),
payload TEXT NOT NULL,
updated_at TEXT NOT NULL
)'
);
$pdo->exec(
'CREATE TABLE IF NOT EXISTS rate_log (
ip TEXT NOT NULL,
action TEXT NOT NULL,
ts INTEGER NOT NULL
)'
);
$pdo->exec(
'CREATE INDEX IF NOT EXISTS idx_rate_log_ip_action_ts
ON rate_log (ip, action, ts)'
);
// Этап 8.1: токены восстановления пароля (аддитивно, IF NOT EXISTS).
$pdo->exec(
'CREATE TABLE IF NOT EXISTS password_resets (
id INTEGER PRIMARY KEY,
email TEXT NOT NULL,
token_hash TEXT NOT NULL,
expires_at INTEGER NOT NULL
)'
);
$pdo->exec(
'CREATE INDEX IF NOT EXISTS idx_password_resets_token_hash
ON password_resets (token_hash)'
);
}