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
68
server/api/password-reset/confirm.php
Normal file
68
server/api/password-reset/confirm.php
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* POST /api/password-reset/confirm.php {token, password}
|
||||
* → 200 {ok:true} (пароль обновлён, токен удалён, просроченные токены
|
||||
* очищены) | 400 {error:"token_invalid"} | 403 | 413 | 422 (пароль < 8)
|
||||
* | 429.
|
||||
*
|
||||
* Rate limit: 10 за 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();
|
||||
|
||||
try {
|
||||
method_must('POST');
|
||||
|
||||
$pdo = db();
|
||||
$ip = client_ip();
|
||||
rate_check($pdo, $ip, 'reset_confirm', GOLEARN_RATE_MAX_RESET_CONFIRM);
|
||||
require_client_header();
|
||||
rate_record($pdo, $ip, 'reset_confirm');
|
||||
|
||||
$body = read_json_object();
|
||||
$token = (string)($body->token ?? '');
|
||||
$password = (string)($body->password ?? '');
|
||||
|
||||
if (strlen($password) < 8) {
|
||||
error_out(422, 'validation');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT id, email, expires_at FROM password_resets
|
||||
WHERE token_hash = ?'
|
||||
);
|
||||
$stmt->execute([hash('sha256', $token)]);
|
||||
$reset = $stmt->fetch();
|
||||
|
||||
if (
|
||||
!is_array($reset)
|
||||
|| (int)$reset['expires_at'] < time()
|
||||
) {
|
||||
error_out(400, 'token_invalid');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE users SET pass_hash = ? WHERE email = ?');
|
||||
$stmt->execute([
|
||||
password_hash($password, PASSWORD_DEFAULT),
|
||||
(string)$reset['email'],
|
||||
]);
|
||||
|
||||
// Токен одноразовый: удаляем использованный; заодно чистим просроченные.
|
||||
$stmt = $pdo->prepare('DELETE FROM password_resets WHERE id = ?');
|
||||
$stmt->execute([(int)$reset['id']]);
|
||||
$stmt = $pdo->prepare('DELETE FROM password_resets WHERE expires_at < ?');
|
||||
$stmt->execute([time()]);
|
||||
|
||||
json_out(['ok' => true]);
|
||||
} catch (Throwable $e) {
|
||||
error_log('password-reset/confirm: ' . $e->getMessage());
|
||||
error_out(500, 'internal');
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue