Connect Flutter phone auth and Android platform
This commit is contained in:
@@ -19,6 +19,7 @@ try {
|
||||
$db = Database::getInstance();
|
||||
$connection = $db->getConnection();
|
||||
$connection->begin_transaction();
|
||||
$transactionOpen = true;
|
||||
$digest = hash('sha256', $refreshToken);
|
||||
$query = $db->prepare('SELECT s.session_uuid, s.family_uuid, s.user_id, s.device_uuid, s.replaced_by, s.revoked_at, s.expires_at, u.uuid, u.phone_e164, u.account_role, u.is_active FROM auth_sessions s JOIN users u ON u.id = s.user_id WHERE s.refresh_token_digest = ? FOR UPDATE');
|
||||
$query->bind_param('s', $digest);
|
||||
@@ -27,6 +28,7 @@ try {
|
||||
$query->close();
|
||||
if (!$session) {
|
||||
$connection->rollback();
|
||||
$transactionOpen = false;
|
||||
api_json(['error' => 'invalid_refresh_token'], 401);
|
||||
}
|
||||
if ($session['replaced_by'] !== null) {
|
||||
@@ -35,10 +37,12 @@ try {
|
||||
$revoke->execute();
|
||||
$revoke->close();
|
||||
$connection->commit();
|
||||
$transactionOpen = false;
|
||||
api_json(['error' => 'refresh_token_reuse_detected'], 401);
|
||||
}
|
||||
if ($session['revoked_at'] !== null || $session['expires_at'] <= gmdate('Y-m-d H:i:s') || !(bool) $session['is_active']) {
|
||||
$connection->rollback();
|
||||
$transactionOpen = false;
|
||||
api_json(['error' => 'session_expired'], 401);
|
||||
}
|
||||
|
||||
@@ -54,19 +58,21 @@ try {
|
||||
$replace->bind_param('ss', $newSessionId, $session['session_uuid']);
|
||||
$replace->execute();
|
||||
$replace->close();
|
||||
$connection->commit();
|
||||
|
||||
$ttl = max(60, min(3600, AppConfig::integer('JWT_ACCESS_TTL_SECONDS', 900)));
|
||||
$accessToken = JwtToken::issue((int) $session['user_id'], $newSessionId, $ttl);
|
||||
$connection->commit();
|
||||
$transactionOpen = false;
|
||||
|
||||
api_json([
|
||||
'user' => ['id' => (int) $session['user_id'], 'uuid' => $session['uuid'], 'phone_e164' => $session['phone_e164'], 'account_role' => $session['account_role']],
|
||||
'access_token' => JwtToken::issue((int) $session['user_id'], $newSessionId, $ttl),
|
||||
'access_token' => $accessToken,
|
||||
'token_type' => 'Bearer',
|
||||
'expires_in_seconds' => $ttl,
|
||||
'refresh_token' => $newRefresh,
|
||||
'refresh_expires_in_days' => $refreshDays,
|
||||
]);
|
||||
} catch (Throwable $exception) {
|
||||
if (isset($connection) && $connection instanceof mysqli) {
|
||||
if (!empty($transactionOpen) && isset($connection) && $connection instanceof mysqli) {
|
||||
try { $connection->rollback(); } catch (Throwable $ignored) {}
|
||||
}
|
||||
error_log('Session refresh failed: ' . $exception->getMessage());
|
||||
|
||||
@@ -37,6 +37,7 @@ try {
|
||||
$db = Database::getInstance();
|
||||
$connection = $db->getConnection();
|
||||
$connection->begin_transaction();
|
||||
$transactionOpen = true;
|
||||
|
||||
$rateBuckets = [
|
||||
['phone', hash_hmac('sha256', 'phone:' . $phone, $hashKey), 5, 3600],
|
||||
@@ -57,6 +58,7 @@ try {
|
||||
$check->close();
|
||||
if ($count > $limit) {
|
||||
$connection->rollback();
|
||||
$transactionOpen = false;
|
||||
api_json(['error' => 'rate_limited'], 429);
|
||||
}
|
||||
}
|
||||
@@ -70,11 +72,12 @@ try {
|
||||
$insert->execute();
|
||||
$insert->close();
|
||||
$connection->commit();
|
||||
$transactionOpen = false;
|
||||
|
||||
(new ConfiguredHttpOtpProvider())->send($phone, $code);
|
||||
api_json(['challenge_id' => $challengeUuid, 'expires_in_seconds' => 300]);
|
||||
} catch (Throwable $exception) {
|
||||
if (isset($connection) && $connection instanceof mysqli && $connection->errno === 0) {
|
||||
if (!empty($transactionOpen) && isset($connection) && $connection instanceof mysqli) {
|
||||
try { $connection->rollback(); } catch (Throwable $ignored) {}
|
||||
}
|
||||
error_log('OTP request failed: ' . $exception->getMessage());
|
||||
|
||||
@@ -16,7 +16,8 @@ $platform = is_array($body) ? ($body['platform'] ?? null) : null;
|
||||
if (!is_string($challengeId) || !preg_match('/^[0-9a-f-]{36}$/i', $challengeId) || !is_string($code) || !preg_match('/^[0-9]{6}$/', $code)) {
|
||||
api_json(['error' => 'invalid_verification_payload'], 400);
|
||||
}
|
||||
if ($displayName !== null && (!is_string($displayName) || mb_strlen($displayName) > 100)) {
|
||||
$displayNameLength = is_string($displayName) ? preg_match_all('/./us', $displayName) : 0;
|
||||
if ($displayName !== null && (!is_string($displayName) || $displayNameLength === false || $displayNameLength > 100)) {
|
||||
api_json(['error' => 'invalid_display_name'], 400);
|
||||
}
|
||||
if ($deviceUuid !== null && (!is_string($deviceUuid) || !preg_match('/^[0-9a-f-]{36}$/i', $deviceUuid))) {
|
||||
@@ -39,6 +40,7 @@ try {
|
||||
$db = Database::getInstance();
|
||||
$connection = $db->getConnection();
|
||||
$connection->begin_transaction();
|
||||
$transactionOpen = true;
|
||||
$challengeQuery = $db->prepare('SELECT challenge_uuid, phone_e164, purpose, code_digest, attempt_count, max_attempts, device_uuid FROM otp_challenges WHERE challenge_uuid = ? AND consumed_at IS NULL AND expires_at > UTC_TIMESTAMP() FOR UPDATE');
|
||||
$challengeQuery->bind_param('s', $challengeId);
|
||||
$challengeQuery->execute();
|
||||
@@ -46,10 +48,12 @@ try {
|
||||
$challengeQuery->close();
|
||||
if (!$challenge || (int) $challenge['attempt_count'] >= (int) $challenge['max_attempts']) {
|
||||
$connection->rollback();
|
||||
$transactionOpen = false;
|
||||
api_json(['error' => 'invalid_or_expired_challenge'], 400);
|
||||
}
|
||||
if ($deviceUuid !== null && $challenge['device_uuid'] !== null && !hash_equals($challenge['device_uuid'], $deviceUuid)) {
|
||||
$connection->rollback();
|
||||
$transactionOpen = false;
|
||||
api_json(['error' => 'invalid_verification_payload'], 400);
|
||||
}
|
||||
|
||||
@@ -60,6 +64,7 @@ try {
|
||||
$fail->execute();
|
||||
$fail->close();
|
||||
$connection->commit();
|
||||
$transactionOpen = false;
|
||||
api_json(['error' => 'invalid_code'], 400);
|
||||
}
|
||||
|
||||
@@ -69,6 +74,7 @@ try {
|
||||
if ($consume->affected_rows !== 1) {
|
||||
$consume->close();
|
||||
$connection->rollback();
|
||||
$transactionOpen = false;
|
||||
api_json(['error' => 'invalid_or_expired_challenge'], 400);
|
||||
}
|
||||
$consume->close();
|
||||
@@ -79,11 +85,13 @@ try {
|
||||
$user = $userQuery->get_result()->fetch_assoc();
|
||||
$userQuery->close();
|
||||
if (!$user && $challenge['purpose'] === 'login') {
|
||||
$connection->rollback();
|
||||
$connection->commit();
|
||||
$transactionOpen = false;
|
||||
api_json(['error' => 'verification_failed'], 400);
|
||||
}
|
||||
if ($user && !(bool) $user['is_active']) {
|
||||
$connection->rollback();
|
||||
$connection->commit();
|
||||
$transactionOpen = false;
|
||||
api_json(['error' => 'account_inactive'], 403);
|
||||
}
|
||||
if (!$user) {
|
||||
@@ -122,19 +130,21 @@ try {
|
||||
$sessionInsert->bind_param('ssissi', $sessionId, $familyId, $userId, $resolvedDevice, $refreshDigest, $refreshDays);
|
||||
$sessionInsert->execute();
|
||||
$sessionInsert->close();
|
||||
$connection->commit();
|
||||
|
||||
$accessTtl = max(60, min(3600, AppConfig::integer('JWT_ACCESS_TTL_SECONDS', 900)));
|
||||
$accessToken = JwtToken::issue($userId, $sessionId, $accessTtl);
|
||||
$connection->commit();
|
||||
$transactionOpen = false;
|
||||
|
||||
api_json([
|
||||
'user' => ['id' => $userId, 'uuid' => $user['uuid'], 'phone_e164' => $challenge['phone_e164'], 'account_role' => $user['account_role']],
|
||||
'access_token' => JwtToken::issue($userId, $sessionId, $accessTtl),
|
||||
'access_token' => $accessToken,
|
||||
'token_type' => 'Bearer',
|
||||
'expires_in_seconds' => $accessTtl,
|
||||
'refresh_token' => $refreshToken,
|
||||
'refresh_expires_in_days' => $refreshDays,
|
||||
]);
|
||||
} catch (Throwable $exception) {
|
||||
if (isset($connection) && $connection instanceof mysqli) {
|
||||
if (!empty($transactionOpen) && isset($connection) && $connection instanceof mysqli) {
|
||||
try { $connection->rollback(); } catch (Throwable $ignored) {}
|
||||
}
|
||||
error_log('OTP verification failed: ' . $exception->getMessage());
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once dirname(__DIR__, 3) . '/backend/api_history.php';
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once dirname(__DIR__, 3) . '/backend/api_workouts.php';
|
||||
Reference in New Issue
Block a user