84 lines
3.0 KiB
PHP
84 lines
3.0 KiB
PHP
<?php
|
|
// Shared rules for the 6-digit codes used by e-mail verification and password reset.
|
|
// Both tables (auth_email_verifications, auth_password_resets) behave identically:
|
|
// sha256 of the code is stored, it lives 15 minutes, allows 5 attempts and may be
|
|
// re-sent at most once per 60 seconds.
|
|
|
|
const CODE_TTL_MINUTES = 15;
|
|
const CODE_MAX_ATTEMPTS = 5;
|
|
const CODE_RESEND_SECONDS = 60;
|
|
|
|
function generate_code(): string
|
|
{
|
|
return (string) random_int(100000, 999999);
|
|
}
|
|
|
|
/**
|
|
* Stores a fresh code, resetting attempts. Returns the plain code, or null when the
|
|
* previous one was issued less than CODE_RESEND_SECONDS ago (caller stays silent).
|
|
*/
|
|
function issue_code(PDO $pdo, string $table, string $email): ?string
|
|
{
|
|
$stmt = $pdo->prepare("SELECT TIMESTAMPDIFF(SECOND, created_at, NOW()) FROM {$table} WHERE email = ? LIMIT 1");
|
|
$stmt->execute([$email]);
|
|
$age = $stmt->fetchColumn();
|
|
if ($age !== false && (int) $age < CODE_RESEND_SECONDS) {
|
|
return null;
|
|
}
|
|
|
|
$code = generate_code();
|
|
$pdo->prepare("DELETE FROM {$table} WHERE email = ?")->execute([$email]);
|
|
$pdo->prepare("INSERT INTO {$table} (email, token, attempts, created_at) VALUES (?, ?, 0, NOW())")
|
|
->execute([$email, hash('sha256', $code)]);
|
|
|
|
return $code;
|
|
}
|
|
|
|
/**
|
|
* Checks a code and consumes an attempt. Returns 'ok', 'code_invalid' or 'code_expired'.
|
|
* A spent or exhausted record is deleted, so the caller must request a new code.
|
|
*/
|
|
function check_code(PDO $pdo, string $table, string $email, string $code): string
|
|
{
|
|
$stmt = $pdo->prepare(
|
|
"SELECT token, attempts, TIMESTAMPDIFF(MINUTE, created_at, NOW()) AS age
|
|
FROM {$table} WHERE email = ? LIMIT 1"
|
|
);
|
|
$stmt->execute([$email]);
|
|
$row = $stmt->fetch();
|
|
|
|
if (!$row) {
|
|
return 'code_invalid';
|
|
}
|
|
if ((int) $row['age'] >= CODE_TTL_MINUTES) {
|
|
$pdo->prepare("DELETE FROM {$table} WHERE email = ?")->execute([$email]);
|
|
return 'code_expired';
|
|
}
|
|
if (!hash_equals((string) $row['token'], hash('sha256', $code))) {
|
|
$attempts = (int) $row['attempts'] + 1;
|
|
if ($attempts >= CODE_MAX_ATTEMPTS) {
|
|
$pdo->prepare("DELETE FROM {$table} WHERE email = ?")->execute([$email]);
|
|
return 'code_expired';
|
|
}
|
|
$pdo->prepare("UPDATE {$table} SET attempts = ? WHERE email = ?")->execute([$attempts, $email]);
|
|
return 'code_invalid';
|
|
}
|
|
|
|
$pdo->prepare("DELETE FROM {$table} WHERE email = ?")->execute([$email]);
|
|
return 'ok';
|
|
}
|
|
|
|
/** Issues a bearer token exactly like login.php does. */
|
|
function issue_token(PDO $pdo, int $userId, string $deviceName): string
|
|
{
|
|
$config = require __DIR__ . '/config.php';
|
|
$plain = bin2hex(random_bytes(32));
|
|
$expires = (new DateTimeImmutable("+{$config['token_ttl_days']} days"))->format('Y-m-d H:i:s');
|
|
|
|
$pdo->prepare(
|
|
'INSERT INTO api_tokens (user_id, name, token, expires_at, created_at) VALUES (?, ?, ?, ?, NOW())'
|
|
)->execute([$userId, $deviceName, hash('sha256', $plain), $expires]);
|
|
|
|
return $plain;
|
|
}
|