chore: восстановление репозитория из снапшота v0.3.1
Прежняя git-история утрачена при переносе проекта на машину владельца (снапшот без .git). Хэши коммитов в docs/reports/* относятся к утраченной истории. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
commit
1c85091186
184 changed files with 33303 additions and 0 deletions
93
server/api/password-reset/request.php
Normal file
93
server/api/password-reset/request.php
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* POST /api/password-reset/request.php {email}
|
||||
* → ВСЕГДА 200 {ok:true} для несуществующих/невалидных email
|
||||
* (anti-enumeration) | 403 | 413 | 422 (невалидный JSON) | 429 | 503
|
||||
* {error:"mail_unavailable"}.
|
||||
*
|
||||
* Существующему email: токен bin2hex(random_bytes(32)), в БД — sha256
|
||||
* токена и expires = time() + 3600; письмо mail() со ссылкой
|
||||
* {base_url}/password-reset.html?token=… (plain text, From: mail_from).
|
||||
* Rate limit: 5 за 10 минут на IP.
|
||||
* Контракт: docs/INTERFACES.md, раздел «Кабинет 2.0 … (этап 8.1)».
|
||||
*/
|
||||
|
||||
require __DIR__ . '/../../lib/http.php';
|
||||
require __DIR__ . '/../../lib/db.php';
|
||||
require __DIR__ . '/../../lib/ratelimit.php';
|
||||
|
||||
http_security_headers();
|
||||
|
||||
/**
|
||||
* Отправка письма восстановления. В тестовом режиме (GOLEARN_TEST=1)
|
||||
* письмо пишется в файл (путь из GOLEARN_TEST_MAIL, дефолт
|
||||
* /tmp/go-learn-mail.log) вместо mail(); в проде env отсутствует.
|
||||
*/
|
||||
function send_reset_mail(string $to, string $subject, string $text, string $from): bool
|
||||
{
|
||||
if (getenv('GOLEARN_TEST') === '1') {
|
||||
$file = getenv('GOLEARN_TEST_MAIL');
|
||||
if (!is_string($file) || $file === '') {
|
||||
$file = '/tmp/go-learn-mail.log';
|
||||
}
|
||||
$data = "To: {$to}\nFrom: {$from}\nSubject: {$subject}\n\n{$text}\n----\n";
|
||||
return file_put_contents($file, $data, FILE_APPEND | LOCK_EX) !== false;
|
||||
}
|
||||
return mail($to, $subject, $text, 'From: ' . $from);
|
||||
}
|
||||
|
||||
try {
|
||||
method_must('POST');
|
||||
|
||||
$pdo = db();
|
||||
$ip = client_ip();
|
||||
rate_check($pdo, $ip, 'reset_request', GOLEARN_RATE_MAX_RESET_REQUEST);
|
||||
require_client_header();
|
||||
rate_record($pdo, $ip, 'reset_request');
|
||||
|
||||
$body = read_json_object();
|
||||
$email = trim((string)($body->email ?? ''));
|
||||
|
||||
// Anti-enumeration: невалидный/несуществующий email — тот же 200.
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
json_out(['ok' => true]);
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('SELECT id FROM users WHERE email = ?');
|
||||
$stmt->execute([$email]);
|
||||
if ($stmt->fetch() === false) {
|
||||
json_out(['ok' => true]);
|
||||
}
|
||||
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO password_resets (email, token_hash, expires_at)
|
||||
VALUES (?, ?, ?)'
|
||||
);
|
||||
$stmt->execute([$email, hash('sha256', $token), time() + 3600]);
|
||||
|
||||
$config = golearn_config();
|
||||
$baseUrl = rtrim((string)($config['base_url'] ?? ''), '/');
|
||||
$mailFrom = (string)($config['mail_from'] ?? '');
|
||||
$link = $baseUrl . '/password-reset.html?token=' . $token;
|
||||
|
||||
$subject = 'Восстановление пароля — Го';
|
||||
$text = "Здравствуйте!\n\n"
|
||||
. "Вы запросили восстановление пароля на сайте обучения игре Го.\n"
|
||||
. "Чтобы задать новый пароль, перейдите по ссылке:\n\n"
|
||||
. $link . "\n\n"
|
||||
. "Ссылка действует 1 час. Если вы не запрашивали восстановление,\n"
|
||||
. "просто проигнорируйте это письмо — пароль не изменится.\n";
|
||||
|
||||
if (!send_reset_mail($email, $subject, $text, $mailFrom)) {
|
||||
error_out(503, 'mail_unavailable');
|
||||
}
|
||||
|
||||
json_out(['ok' => true]);
|
||||
} catch (Throwable $e) {
|
||||
error_log('password-reset/request: ' . $e->getMessage());
|
||||
error_out(500, 'internal');
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue