Прежняя git-история утрачена при переносе проекта на машину владельца (снапшот без .git). Хэши коммитов в docs/reports/* относятся к утраченной истории. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
95 lines
2.9 KiB
PHP
95 lines
2.9 KiB
PHP
<?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)'
|
|
);
|
|
}
|