Initial commit: Uruk Prize Platform architecture, backend core, mobile app, gateway caller & deployment pipeline
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Core\Request;
|
||||
use Core\Response;
|
||||
use Core\Security;
|
||||
use Core\Database;
|
||||
use App\Services\OtpIqService;
|
||||
use PDO;
|
||||
|
||||
class AuthController
|
||||
{
|
||||
/**
|
||||
* POST /api/v1/auth/request-otp
|
||||
*/
|
||||
public function requestOtp(Request $request): void
|
||||
{
|
||||
$phone = trim((string)$request->get('phone'));
|
||||
if (empty($phone) || strlen($phone) < 8) {
|
||||
Response::error('Phone number is required.');
|
||||
}
|
||||
|
||||
// Generate 6-digit cryptographic OTP code
|
||||
$otp = (string)random_int(100000, 999999);
|
||||
|
||||
// Store OTP temporarily in DB or cache
|
||||
$pdo = Database::getConnection();
|
||||
$stmt = $pdo->prepare('
|
||||
INSERT INTO audit_logs (user_id, action, endpoint, method, ip_address, response_code, payload_snippet, created_at)
|
||||
VALUES (0, "OTP_REQUESTED", "/api/v1/auth/request-otp", "POST", :ip, 200, :payload, NOW())
|
||||
');
|
||||
$stmt->execute([
|
||||
':ip' => $request->getIp(),
|
||||
':payload' => json_encode(['phone' => $phone, 'otp' => $otp]),
|
||||
]);
|
||||
|
||||
$otpService = new OtpIqService();
|
||||
$res = $otpService->sendOtp($phone, $otp);
|
||||
|
||||
Response::success([
|
||||
'phone' => $phone,
|
||||
'channels' => ['whatsapp', 'sms'],
|
||||
'mock_otp' => getenv('APP_ENV') === 'development' ? $otp : null,
|
||||
], 'Verification OTP dispatched via OTPIQ Smart Fallback.');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/auth/verify-otp
|
||||
* Receives phone, otp, device_fingerprint, device_model, and optional full_name.
|
||||
*/
|
||||
public function verifyOtp(Request $request): void
|
||||
{
|
||||
$phone = trim((string)$request->get('phone'));
|
||||
$otp = trim((string)$request->get('otp'));
|
||||
$fingerprint = trim((string)$request->get('device_fingerprint'));
|
||||
$model = trim((string)$request->get('device_model', 'Unknown Device'));
|
||||
$fullName = trim((string)$request->get('full_name', 'عضو جائزة أوروك'));
|
||||
|
||||
if (empty($phone) || empty($otp) || empty($fingerprint)) {
|
||||
Response::error('Phone, OTP, and Device Fingerprint are mandatory.');
|
||||
}
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
|
||||
// Check or find user
|
||||
$stmt = $pdo->prepare('SELECT id, phone, full_name, role, status FROM users WHERE phone = :phone LIMIT 1');
|
||||
$stmt->execute([':phone' => $phone]);
|
||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$user) {
|
||||
// Register new user
|
||||
$ins = $pdo->prepare('
|
||||
INSERT INTO users (phone, full_name, role, status, created_at)
|
||||
VALUES (:phone, :name, "MEMBER", "ACTIVE", NOW())
|
||||
');
|
||||
$ins->execute([':phone' => $phone, ':name' => $fullName]);
|
||||
$userId = (int)$pdo->lastInsertId();
|
||||
$role = 'MEMBER';
|
||||
} else {
|
||||
$userId = (int)$user['id'];
|
||||
$role = $user['role'];
|
||||
}
|
||||
|
||||
// Generate per-device secret key and session token
|
||||
$deviceSecret = Security::generateRandomHex(32);
|
||||
$sessionToken = Security::generateRandomHex(32);
|
||||
|
||||
// Bind device fingerprint and store secret in user_security table
|
||||
$secStmt = $pdo->prepare('
|
||||
INSERT INTO user_security (user_id, device_fingerprint, device_secret, device_model, last_token, last_ip, last_active_at)
|
||||
VALUES (:uid, :fp, :secret, :model, :token, :ip, NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
device_secret = VALUES(device_secret),
|
||||
device_model = VALUES(device_model),
|
||||
last_token = VALUES(last_token),
|
||||
last_ip = VALUES(last_ip),
|
||||
last_active_at = NOW(),
|
||||
is_locked = 0
|
||||
');
|
||||
$secStmt->execute([
|
||||
':uid' => $userId,
|
||||
':fp' => $fingerprint,
|
||||
':secret' => $deviceSecret,
|
||||
':model' => $model,
|
||||
':token' => $sessionToken,
|
||||
':ip' => $request->getIp(),
|
||||
]);
|
||||
|
||||
Response::success([
|
||||
'user' => [
|
||||
'id' => $userId,
|
||||
'phone' => $phone,
|
||||
'full_name' => $user['full_name'] ?? $fullName,
|
||||
'role' => $role,
|
||||
],
|
||||
'token' => $sessionToken,
|
||||
'device_secret' => $deviceSecret,
|
||||
'device_fingerprint' => $fingerprint,
|
||||
], 'Authentication successful. Device bound and secured.');
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/auth/profile
|
||||
*/
|
||||
public function getProfile(Request $request): void
|
||||
{
|
||||
$userId = (int)$request->getHeader('x-user-id');
|
||||
$pdo = Database::getConnection();
|
||||
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT u.id, u.phone, u.full_name, u.role, u.status, u.created_at,
|
||||
s.membership_number, s.status AS subscription_status, s.expires_at
|
||||
FROM users u
|
||||
LEFT JOIN subscriptions s ON s.user_id = u.id AND s.status = "ACTIVE"
|
||||
WHERE u.id = :uid
|
||||
ORDER BY s.id DESC
|
||||
LIMIT 1
|
||||
');
|
||||
$stmt->execute([':uid' => $userId]);
|
||||
$profile = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
Response::success($profile, 'User profile retrieved.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user