Implement Enterprise Logic (Redis, Phone Auth, Full Schema)
This commit is contained in:
@@ -1,38 +0,0 @@
|
|||||||
# Application Settings
|
|
||||||
APP_NAME=Nabeh
|
|
||||||
APP_ENV=development
|
|
||||||
APP_DEBUG=true
|
|
||||||
APP_URL=http://localhost:8000
|
|
||||||
|
|
||||||
# Main Master Database Configuration
|
|
||||||
DB_HOST=127.0.0.1
|
|
||||||
DB_PORT=3306
|
|
||||||
DB_DATABASE=nabeh_master
|
|
||||||
DB_USERNAME=root
|
|
||||||
DB_PASSWORD=
|
|
||||||
|
|
||||||
# AI Model Configuration
|
|
||||||
GEMINI_API_KEY=
|
|
||||||
ELEVENLABS_API_KEY=
|
|
||||||
ELEVENLABS_VOICE_ID=EXAVITQu4vr4xnSDxMaL
|
|
||||||
|
|
||||||
# Messaging Gateway Settings
|
|
||||||
WHATSAPP_GATEWAY_URL=http://localhost:3722
|
|
||||||
|
|
||||||
# OWASP Security Settings
|
|
||||||
# Generate a secure 32-byte (256-bit) key for AES encryption
|
|
||||||
ENCRYPTION_KEY=d3b07384d113edec49eaa6238ad5ff00f898129dfdeca34289adcd11a00a89d1
|
|
||||||
# Secret key/salt for blind index hashes
|
|
||||||
HMAC_SALT=nabeh_secure_blind_index_salt_key_123
|
|
||||||
# Secret key for JWT signatures
|
|
||||||
JWT_SECRET=nabeh_jwt_secret_signature_key_987654
|
|
||||||
|
|
||||||
# Redis Settings (Optional caching - falls back to DB if disabled/unavailable)
|
|
||||||
REDIS_ENABLED=false
|
|
||||||
REDIS_HOST=127.0.0.1
|
|
||||||
REDIS_PORT=6379
|
|
||||||
REDIS_PASSWORD=
|
|
||||||
REDIS_DB=0
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Core\Request;
|
||||||
|
use App\Core\Response;
|
||||||
|
use App\Core\Database;
|
||||||
|
use App\Core\Security;
|
||||||
|
use App\Core\Validator;
|
||||||
|
use App\Core\RedisClient;
|
||||||
|
|
||||||
|
class AuthController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register a new user using Phone Number
|
||||||
|
*/
|
||||||
|
public function register(Request $request, Response $response): void
|
||||||
|
{
|
||||||
|
$body = $request->getBody();
|
||||||
|
|
||||||
|
$validator = new Validator();
|
||||||
|
$isValid = $validator->validate($body, [
|
||||||
|
'full_name' => 'required',
|
||||||
|
'phone_number' => 'required',
|
||||||
|
'password' => 'required|min:8',
|
||||||
|
'role' => 'required'
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!$isValid) {
|
||||||
|
$response->status(400)->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Validation failed',
|
||||||
|
'errors' => $validator->getErrors()
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$phone = $body['phone_number'];
|
||||||
|
$role = $body['role'];
|
||||||
|
|
||||||
|
if (!in_array($role, ['student', 'teacher', 'guardian'])) {
|
||||||
|
$response->status(400)->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Invalid role specified'
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$phoneHash = Security::blindIndex($phone);
|
||||||
|
$existing = Database::selectOne("SELECT id FROM users WHERE phone_hash = ? LIMIT 1", [$phoneHash]);
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
$response->status(409)->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Phone number is already registered'
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$passwordHash = Security::hashPassword($body['password']);
|
||||||
|
|
||||||
|
// Generate UUID
|
||||||
|
$uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
|
||||||
|
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
|
||||||
|
mt_rand(0, 0xffff),
|
||||||
|
mt_rand(0, 0x0fff) | 0x4000,
|
||||||
|
mt_rand(0, 0x3fff) | 0x8000,
|
||||||
|
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
|
||||||
|
);
|
||||||
|
|
||||||
|
$userId = Database::insert(
|
||||||
|
"INSERT INTO users (uuid, full_name, phone_number, phone_hash, password_hash, role, status) VALUES (?, ?, ?, ?, ?, ?, 'active')",
|
||||||
|
[$uuid, $body['full_name'], $phone, $phoneHash, $passwordHash, $role]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->generateSessionAndRespond($userId, $uuid, $role, $response, "User registered successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login using Phone Number and Password
|
||||||
|
*/
|
||||||
|
public function login(Request $request, Response $response): void
|
||||||
|
{
|
||||||
|
$body = $request->getBody();
|
||||||
|
$validator = new Validator();
|
||||||
|
|
||||||
|
if (!$validator->validate($body, [
|
||||||
|
'phone_number' => 'required',
|
||||||
|
'password' => 'required'
|
||||||
|
])) {
|
||||||
|
$response->status(400)->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Validation failed',
|
||||||
|
'errors' => $validator->getErrors()
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$phoneHash = Security::blindIndex($body['phone_number']);
|
||||||
|
$user = Database::selectOne("SELECT * FROM users WHERE phone_hash = ? LIMIT 1", [$phoneHash]);
|
||||||
|
|
||||||
|
if (!$user || !Security::verifyPassword($body['password'], $user['password_hash'])) {
|
||||||
|
$response->status(401)->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Invalid phone number or password'
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user['status'] === 'suspended') {
|
||||||
|
$response->status(403)->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Account is suspended'
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->generateSessionAndRespond($user['id'], $user['uuid'], $user['role'], $response, "Login successful");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request OTP for phone verification
|
||||||
|
*/
|
||||||
|
public function requestOtp(Request $request, Response $response): void
|
||||||
|
{
|
||||||
|
$body = $request->getBody();
|
||||||
|
if (empty($body['phone_number'])) {
|
||||||
|
$response->status(400)->json(['status' => 'error', 'message' => 'Phone number is required']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$phone = $body['phone_number'];
|
||||||
|
$otp = (string)random_int(100000, 999999);
|
||||||
|
|
||||||
|
// Save OTP to Redis for 5 minutes
|
||||||
|
$redis = RedisClient::getInstance();
|
||||||
|
$redis->setex('otp:' . $phone, 300, $otp);
|
||||||
|
|
||||||
|
// TODO: Integrate SMS gateway here to actually send the OTP via SMS
|
||||||
|
|
||||||
|
$response->json([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => 'OTP sent successfully (Simulated: ' . $otp . ')'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common method to generate JWT and save session to Redis
|
||||||
|
*/
|
||||||
|
private function generateSessionAndRespond(int $userId, string $uuid, string $role, Response $response, string $msg): void
|
||||||
|
{
|
||||||
|
$payload = [
|
||||||
|
'user_id' => $userId,
|
||||||
|
'uuid' => $uuid,
|
||||||
|
'role' => $role
|
||||||
|
];
|
||||||
|
|
||||||
|
$token = Security::generateJWT($payload);
|
||||||
|
|
||||||
|
// Store session in Redis (Active for 30 days)
|
||||||
|
try {
|
||||||
|
$redis = RedisClient::getInstance();
|
||||||
|
$redis->setex("session:{$userId}:{$token}", 30 * 86400, "active");
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
error_log("Failed to save session to Redis: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
$response->status(200)->json([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => $msg,
|
||||||
|
'data' => [
|
||||||
|
'token' => $token,
|
||||||
|
'user' => [
|
||||||
|
'uuid' => $uuid,
|
||||||
|
'role' => $role
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Current User Data
|
||||||
|
*/
|
||||||
|
public function me(Request $request, Response $response): void
|
||||||
|
{
|
||||||
|
$userId = $request->user_id;
|
||||||
|
|
||||||
|
$user = Database::selectOne(
|
||||||
|
"SELECT uuid, full_name, role, status, created_at FROM users WHERE id = ? LIMIT 1",
|
||||||
|
[$userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
$response->status(404)->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'User not found'
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response->json([
|
||||||
|
'status' => 'success',
|
||||||
|
'data' => $user
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Core\Request;
|
||||||
|
use App\Core\Response;
|
||||||
|
use App\Core\Database;
|
||||||
|
|
||||||
|
class TeacherController
|
||||||
|
{
|
||||||
|
public function addCourse(Request $request, Response $response): void
|
||||||
|
{
|
||||||
|
if ($request->role !== 'teacher' && $request->role !== 'admin') {
|
||||||
|
$response->status(403)->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Forbidden: Only teachers can add courses'
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = $request->getBody();
|
||||||
|
$title = $body['title'] ?? '';
|
||||||
|
$description = $body['description'] ?? '';
|
||||||
|
|
||||||
|
if (empty($title)) {
|
||||||
|
$response->status(400)->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Course title is required'
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$courseId = Database::insert(
|
||||||
|
"INSERT INTO courses (teacher_id, title, description) VALUES (?, ?, ?)",
|
||||||
|
[$request->user_id, $title, $description]
|
||||||
|
);
|
||||||
|
|
||||||
|
$response->status(201)->json([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => 'Course added successfully',
|
||||||
|
'data' => [
|
||||||
|
'course_id' => $courseId
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addLesson(Request $request, Response $response): void
|
||||||
|
{
|
||||||
|
if ($request->role !== 'teacher' && $request->role !== 'admin') {
|
||||||
|
$response->status(403)->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Forbidden: Only teachers can add lessons'
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = $request->getBody();
|
||||||
|
$courseId = $body['course_id'] ?? null;
|
||||||
|
$title = $body['title'] ?? '';
|
||||||
|
$content = $body['content'] ?? '';
|
||||||
|
$videoUrl = $body['video_url'] ?? null;
|
||||||
|
$orderNum = $body['order_num'] ?? 1;
|
||||||
|
|
||||||
|
if (!$courseId || empty($title) || empty($content)) {
|
||||||
|
$response->status(400)->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Course ID, title, and content are required'
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the course belongs to the teacher
|
||||||
|
$course = Database::selectOne("SELECT id, teacher_id FROM courses WHERE id = ? LIMIT 1", [$courseId]);
|
||||||
|
if (!$course || ($course['teacher_id'] != $request->user_id && $request->role !== 'admin')) {
|
||||||
|
$response->status(403)->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Forbidden: You do not own this course'
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lessonId = Database::insert(
|
||||||
|
"INSERT INTO lessons (course_id, title, content, video_url, order_num) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
[$courseId, $title, $content, $videoUrl, $orderNum]
|
||||||
|
);
|
||||||
|
|
||||||
|
$response->status(201)->json([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => 'Lesson added successfully',
|
||||||
|
'data' => [
|
||||||
|
'lesson_id' => $lessonId
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core Redis Client for managing connections.
|
||||||
|
* Handles Sessions, Rate Limiting, and caching using PHP Redis extension.
|
||||||
|
*/
|
||||||
|
class RedisClient
|
||||||
|
{
|
||||||
|
private static ?\Redis $instance = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the singleton Redis connection instance
|
||||||
|
*/
|
||||||
|
public static function getInstance(): \Redis
|
||||||
|
{
|
||||||
|
if (self::$instance === null) {
|
||||||
|
try {
|
||||||
|
$redis = new \Redis();
|
||||||
|
|
||||||
|
$host = getenv('REDIS_HOST') ?: '127.0.0.1';
|
||||||
|
$port = (int)(getenv('REDIS_PORT') ?: 6379);
|
||||||
|
$password = getenv('REDIS_PASSWORD') ?: null;
|
||||||
|
|
||||||
|
// Connect with a 2 second timeout
|
||||||
|
if (!$redis->connect($host, $port, 2.0)) {
|
||||||
|
throw new \RuntimeException("Could not connect to Redis server at $host:$port");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate if password is provided
|
||||||
|
if ($password) {
|
||||||
|
if (!$redis->auth($password)) {
|
||||||
|
throw new \RuntimeException("Redis authentication failed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set prefix for Saqel to avoid collisions
|
||||||
|
$redis->setOption(\Redis::OPT_PREFIX, 'saqel:');
|
||||||
|
|
||||||
|
self::$instance = $redis;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// In production, fallback gracefully or throw HTTP 500
|
||||||
|
error_log("Redis Connection Error: " . $e->getMessage());
|
||||||
|
throw new \RuntimeException("Redis is unavailable. Please ensure the Redis server is running.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to close the connection explicitly
|
||||||
|
*/
|
||||||
|
public static function close(): void
|
||||||
|
{
|
||||||
|
if (self::$instance !== null) {
|
||||||
|
self::$instance->close();
|
||||||
|
self::$instance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -148,7 +148,7 @@ class Security
|
|||||||
$payload['iat'] = time();
|
$payload['iat'] = time();
|
||||||
$payload['exp'] = time() + $expirySeconds;
|
$payload['exp'] = time() + $expirySeconds;
|
||||||
$payload['iss'] = getenv('APP_URL'); // Issuer
|
$payload['iss'] = getenv('APP_URL'); // Issuer
|
||||||
$payload['aud'] = 'nabeh_dashboard'; // Audience
|
$payload['aud'] = 'saqel_app'; // Audience
|
||||||
$payload['jti'] = bin2hex(random_bytes(16)); // JWT ID to prevent Replay Attacks
|
$payload['jti'] = bin2hex(random_bytes(16)); // JWT ID to prevent Replay Attacks
|
||||||
|
|
||||||
$payloadEncoded = self::base64UrlEncode(json_encode($payload));
|
$payloadEncoded = self::base64UrlEncode(json_encode($payload));
|
||||||
|
|||||||
@@ -28,16 +28,29 @@ class AuthMiddleware
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if session is active in Redis
|
||||||
|
try {
|
||||||
|
$redis = \App\Core\RedisClient::getInstance();
|
||||||
|
$userId = $payload['user_id'];
|
||||||
|
$sessionState = $redis->get("session:{$userId}:{$token}");
|
||||||
|
|
||||||
|
if (!$sessionState || $sessionState !== 'active') {
|
||||||
|
$response->json(['error' => 'Unauthorized', 'message' => 'Session has been revoked or expired'], 401);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// If Redis is down, we fallback to just JWT verification
|
||||||
|
error_log("AuthMiddleware Redis Error: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
// Validate required custom payload elements
|
// Validate required custom payload elements
|
||||||
if (!isset($payload['user_id']) || !isset($payload['company_id']) || !isset($payload['role'])) {
|
if (!isset($payload['user_id']) || !isset($payload['role'])) {
|
||||||
$response->json(['error' => 'Unauthorized', 'message' => 'Malformed token payload structure'], 401);
|
$response->json(['error' => 'Unauthorized', 'message' => 'Malformed token payload structure'], 401);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach user info to the Request instance dynamically so controllers can use it
|
// Attach user info to the Request instance dynamically so controllers can use it
|
||||||
$request->user_id = $payload['user_id'];
|
$request->user_id = $payload['user_id'];
|
||||||
$request->company_id = $payload['company_id'];
|
|
||||||
$request->role = $payload['role'];
|
$request->role = $payload['role'];
|
||||||
$request->is_super_admin = (int)$payload['company_id'] === 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,22 +4,16 @@ namespace App\Middlewares;
|
|||||||
|
|
||||||
use App\Core\Request;
|
use App\Core\Request;
|
||||||
use App\Core\Response;
|
use App\Core\Response;
|
||||||
|
use App\Core\RedisClient;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rate Limit Middleware
|
* Rate Limit Middleware (Redis Powered)
|
||||||
* Limits the number of requests per IP address using file-based counters.
|
* Limits the number of requests per IP address using Redis atomic counters.
|
||||||
* Protects sensitive endpoints (login, register) from Brute Force attacks.
|
* Protects sensitive endpoints (login, register, otp) from Brute Force attacks.
|
||||||
*/
|
*/
|
||||||
class RateLimitMiddleware
|
class RateLimitMiddleware
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Maximum allowed requests within the time window
|
|
||||||
*/
|
|
||||||
private int $maxAttempts;
|
private int $maxAttempts;
|
||||||
|
|
||||||
/**
|
|
||||||
* Time window in seconds
|
|
||||||
*/
|
|
||||||
private int $decaySeconds;
|
private int $decaySeconds;
|
||||||
|
|
||||||
public function __construct(int $maxAttempts = 5, int $decaySeconds = 60)
|
public function __construct(int $maxAttempts = 5, int $decaySeconds = 60)
|
||||||
@@ -31,40 +25,36 @@ class RateLimitMiddleware
|
|||||||
public function handle(Request $request, Response $response): void
|
public function handle(Request $request, Response $response): void
|
||||||
{
|
{
|
||||||
$ip = $this->getClientIp();
|
$ip = $this->getClientIp();
|
||||||
$key = 'rate_' . md5($ip . '_' . $request->getPath());
|
$key = 'rate_limit:' . md5($ip . '_' . $request->getPath());
|
||||||
|
|
||||||
$storageDir = APP_ROOT . '/storage/rate_limits';
|
try {
|
||||||
if (!is_dir($storageDir)) {
|
$redis = RedisClient::getInstance();
|
||||||
mkdir($storageDir, 0750, true);
|
|
||||||
}
|
$current = $redis->get($key);
|
||||||
|
|
||||||
$filePath = $storageDir . '/' . $key . '.json';
|
if ($current !== false && (int)$current >= $this->maxAttempts) {
|
||||||
|
$retryAfter = $redis->ttl($key);
|
||||||
$data = ['count' => 0, 'expires_at' => time() + $this->decaySeconds];
|
$retryAfter = $retryAfter > 0 ? $retryAfter : $this->decaySeconds;
|
||||||
|
|
||||||
if (file_exists($filePath)) {
|
$response->setHeader('Retry-After', (string)$retryAfter);
|
||||||
$raw = json_decode(file_get_contents($filePath), true);
|
$response->json([
|
||||||
if ($raw && isset($raw['expires_at']) && $raw['expires_at'] > time()) {
|
'error' => 'Too Many Requests',
|
||||||
// Window still active — use existing data
|
'message' => "لقد تجاوزت الحد الأقصى للمحاولات ({$this->maxAttempts}). يرجى المحاولة بعد {$retryAfter} ثانية."
|
||||||
$data = $raw;
|
], 429);
|
||||||
|
exit; // End request
|
||||||
}
|
}
|
||||||
// If window expired, fall through and reset (overwrite with fresh data below)
|
|
||||||
|
// Increment atomically
|
||||||
|
$count = $redis->incr($key);
|
||||||
|
if ($count === 1) {
|
||||||
|
// First request, set expiration
|
||||||
|
$redis->expire($key, $this->decaySeconds);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// If Redis fails, log it but don't block the request completely,
|
||||||
|
// or we could choose to block it. We'll let it pass to avoid downtime.
|
||||||
|
error_log("RateLimit Redis Error: " . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
$data['count']++;
|
|
||||||
|
|
||||||
if ($data['count'] > $this->maxAttempts) {
|
|
||||||
$retryAfter = max(0, $data['expires_at'] - time());
|
|
||||||
$response->setHeader('Retry-After', (string)$retryAfter);
|
|
||||||
$response->json([
|
|
||||||
'error' => 'Too Many Requests',
|
|
||||||
'message' => "You have exceeded the maximum number of {$this->maxAttempts} attempts. Please try again in {$retryAfter} seconds."
|
|
||||||
], 429);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Persist the updated counter
|
|
||||||
file_put_contents($filePath, json_encode($data), LOCK_EX);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
-- ==============================================================================
|
||||||
|
-- SAQEL PLATFORM (منصة صَقِل) - PRIMARY DATABASE SCHEMA (MySQL 8.4+)
|
||||||
|
-- Complete Production Database Definition
|
||||||
|
-- ==============================================================================
|
||||||
|
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
-- 1. Table: schools (المدارس والجهات الشريكة - B2B مثل الثقافة العسكرية)
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
DROP TABLE IF EXISTS `schools`;
|
||||||
|
CREATE TABLE `schools` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`uuid` CHAR(36) NOT NULL,
|
||||||
|
`name` VARCHAR(255) NOT NULL,
|
||||||
|
`code` VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
`type` ENUM('military_culture', 'private', 'public', 'center') NOT NULL DEFAULT 'private',
|
||||||
|
`director_name` VARCHAR(255) DEFAULT NULL,
|
||||||
|
`phone` VARCHAR(50) DEFAULT NULL,
|
||||||
|
`city` VARCHAR(100) DEFAULT 'Amman',
|
||||||
|
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
`created_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `idx_schools_uuid` (`uuid`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
-- 2. Table: users (المستخدمون الموحدون - طلاب، معلمون، أولياء أمور، إدارة)
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
DROP TABLE IF EXISTS `users`;
|
||||||
|
CREATE TABLE `users` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`uuid` CHAR(36) NOT NULL,
|
||||||
|
`full_name` TEXT NOT NULL,
|
||||||
|
`phone_number` TEXT NOT NULL,
|
||||||
|
`phone_hash` VARCHAR(64) NOT NULL,
|
||||||
|
`password_hash` VARCHAR(255) NOT NULL,
|
||||||
|
`role` ENUM('student', 'guardian', 'teacher', 'school_admin', 'super_admin') NOT NULL DEFAULT 'student',
|
||||||
|
`school_id` BIGINT UNSIGNED DEFAULT NULL,
|
||||||
|
`grade_level` VARCHAR(50) DEFAULT 'tawjihi_2007',
|
||||||
|
`stream` ENUM('scientific', 'literary', 'vocational', 'general') DEFAULT 'scientific',
|
||||||
|
`token_version` INT UNSIGNED NOT NULL DEFAULT 1,
|
||||||
|
`status` ENUM('active', 'pending_otp', 'suspended') NOT NULL DEFAULT 'pending_otp',
|
||||||
|
`created_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `idx_users_uuid` (`uuid`),
|
||||||
|
KEY `idx_users_phone_hash` (`phone_hash`),
|
||||||
|
KEY `idx_users_school_id` (`school_id`),
|
||||||
|
CONSTRAINT `fk_users_school` FOREIGN KEY (`school_id`) REFERENCES `schools` (`id`) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
-- 3. Table: guardian_student (ربط أولياء الأمور بالأبناء - N:M)
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
DROP TABLE IF EXISTS `guardian_student`;
|
||||||
|
CREATE TABLE `guardian_student` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`guardian_id` BIGINT UNSIGNED NOT NULL,
|
||||||
|
`student_id` BIGINT UNSIGNED NOT NULL,
|
||||||
|
`relationship_type` ENUM('father', 'mother', 'brother', 'guardian') NOT NULL DEFAULT 'father',
|
||||||
|
`is_verified` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
`created_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `idx_guardian_student_unique` (`guardian_id`, `student_id`),
|
||||||
|
KEY `idx_guardian_student_student` (`student_id`),
|
||||||
|
CONSTRAINT `fk_gs_guardian` FOREIGN KEY (`guardian_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `fk_gs_student` FOREIGN KEY (`student_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
-- 4. Table: teacher_profiles (ملفات المعلمين ونسب الأرباح)
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
DROP TABLE IF EXISTS `teacher_profiles`;
|
||||||
|
CREATE TABLE `teacher_profiles` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` BIGINT UNSIGNED NOT NULL,
|
||||||
|
`bio` TEXT DEFAULT NULL,
|
||||||
|
`specialization` VARCHAR(150) NOT NULL,
|
||||||
|
`revenue_share_pct` DECIMAL(5,2) NOT NULL DEFAULT 45.00,
|
||||||
|
`contract_type` ENUM('exclusive', 'non_exclusive') NOT NULL DEFAULT 'exclusive',
|
||||||
|
`created_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `idx_teacher_user` (`user_id`),
|
||||||
|
CONSTRAINT `fk_teacher_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
-- 5. Table: subjects (المواد التعليمية)
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
DROP TABLE IF EXISTS `subjects`;
|
||||||
|
CREATE TABLE `subjects` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` VARCHAR(150) NOT NULL,
|
||||||
|
`code` VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
`stream` ENUM('scientific', 'literary', 'common') NOT NULL DEFAULT 'common',
|
||||||
|
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
`created_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
-- 6. Table: courses (الدورات التدريبية)
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
DROP TABLE IF EXISTS `courses`;
|
||||||
|
CREATE TABLE `courses` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`uuid` CHAR(36) NOT NULL,
|
||||||
|
`subject_id` BIGINT UNSIGNED NOT NULL,
|
||||||
|
`teacher_id` BIGINT UNSIGNED NOT NULL,
|
||||||
|
`title` VARCHAR(255) NOT NULL,
|
||||||
|
`description` TEXT DEFAULT NULL,
|
||||||
|
`semester` ENUM('first', 'second', 'full_year', 'intensive') NOT NULL DEFAULT 'first',
|
||||||
|
`price_jod` DECIMAL(8,2) NOT NULL DEFAULT 35.00,
|
||||||
|
`thumbnail_url` VARCHAR(500) DEFAULT NULL,
|
||||||
|
`is_published` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
`created_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `idx_courses_uuid` (`uuid`),
|
||||||
|
KEY `idx_courses_subject` (`subject_id`),
|
||||||
|
KEY `idx_courses_teacher` (`teacher_id`),
|
||||||
|
CONSTRAINT `fk_courses_subject` FOREIGN KEY (`subject_id`) REFERENCES `subjects` (`id`) ON DELETE RESTRICT,
|
||||||
|
CONSTRAINT `fk_courses_teacher` FOREIGN KEY (`teacher_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
-- 7. Table: lessons (الدروس والفيديوهات المربوطة بـ Bunny Stream)
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
DROP TABLE IF EXISTS `lessons`;
|
||||||
|
CREATE TABLE `lessons` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`course_id` BIGINT UNSIGNED NOT NULL,
|
||||||
|
`title` VARCHAR(255) NOT NULL,
|
||||||
|
`sequence_order` INT UNSIGNED NOT NULL DEFAULT 1,
|
||||||
|
`bunny_video_id` VARCHAR(100) NOT NULL,
|
||||||
|
`duration_seconds` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
|
`is_free_preview` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
`created_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_lessons_course` (`course_id`),
|
||||||
|
CONSTRAINT `fk_lessons_course` FOREIGN KEY (`course_id`) REFERENCES `courses` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
-- 8. Table: quizzes (الكويزات داخل الفيديو - Socratic Checkpoints)
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
DROP TABLE IF EXISTS `quizzes`;
|
||||||
|
CREATE TABLE `quizzes` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`lesson_id` BIGINT UNSIGNED NOT NULL,
|
||||||
|
`title` VARCHAR(255) NOT NULL,
|
||||||
|
`timestamp_seconds` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
|
`is_mandatory` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
`rewind_on_fail_seconds` INT UNSIGNED NOT NULL DEFAULT 45,
|
||||||
|
`created_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_quizzes_lesson` (`lesson_id`),
|
||||||
|
CONSTRAINT `fk_quizzes_lesson` FOREIGN KEY (`lesson_id`) REFERENCES `lessons` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
-- 9. Table: questions (الأسئلة)
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
DROP TABLE IF EXISTS `questions`;
|
||||||
|
CREATE TABLE `questions` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`quiz_id` BIGINT UNSIGNED NOT NULL,
|
||||||
|
`question_text` TEXT NOT NULL,
|
||||||
|
`explanation_text` TEXT DEFAULT NULL,
|
||||||
|
`points` INT UNSIGNED NOT NULL DEFAULT 1,
|
||||||
|
`created_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_questions_quiz` (`quiz_id`),
|
||||||
|
CONSTRAINT `fk_questions_quiz` FOREIGN KEY (`quiz_id`) REFERENCES `quizzes` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
-- 10. Table: question_options (خيارات الإجابة)
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
DROP TABLE IF EXISTS `question_options`;
|
||||||
|
CREATE TABLE `question_options` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`question_id` BIGINT UNSIGNED NOT NULL,
|
||||||
|
`option_text` TEXT NOT NULL,
|
||||||
|
`is_correct` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_options_question` (`question_id`),
|
||||||
|
CONSTRAINT `fk_options_question` FOREIGN KEY (`question_id`) REFERENCES `questions` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
-- 11. Table: lesson_progress (سجل متابعة ونبض المشاهدة)
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
DROP TABLE IF EXISTS `lesson_progress`;
|
||||||
|
CREATE TABLE `lesson_progress` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` BIGINT UNSIGNED NOT NULL,
|
||||||
|
`lesson_id` BIGINT UNSIGNED NOT NULL,
|
||||||
|
`last_watched_second` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
|
`max_watched_second` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
|
`completion_pct` DECIMAL(5,2) NOT NULL DEFAULT 0.00,
|
||||||
|
`is_completed` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
`updated_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `idx_user_lesson_progress` (`user_id`, `lesson_id`),
|
||||||
|
KEY `idx_progress_lesson` (`lesson_id`),
|
||||||
|
CONSTRAINT `fk_progress_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `fk_progress_lesson` FOREIGN KEY (`lesson_id`) REFERENCES `lessons` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
-- 12. Table: voice_notes (الملاحظات الصوتية والتفريغ الذكي)
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
DROP TABLE IF EXISTS `voice_notes`;
|
||||||
|
CREATE TABLE `voice_notes` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`uuid` CHAR(36) NOT NULL,
|
||||||
|
`user_id` BIGINT UNSIGNED NOT NULL,
|
||||||
|
`lesson_id` BIGINT UNSIGNED NOT NULL,
|
||||||
|
`timestamp_seconds` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
|
`audio_url` VARCHAR(500) NOT NULL,
|
||||||
|
`transcript_text` TEXT DEFAULT NULL,
|
||||||
|
`ai_summary` TEXT DEFAULT NULL,
|
||||||
|
`created_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `idx_voice_notes_uuid` (`uuid`),
|
||||||
|
KEY `idx_voice_notes_user_lesson` (`user_id`, `lesson_id`),
|
||||||
|
CONSTRAINT `fk_vn_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `fk_vn_lesson` FOREIGN KEY (`lesson_id`) REFERENCES `lessons` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
-- 13. Table: user_devices (بصمة الأجهزة المربوطة بالجلسات)
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
DROP TABLE IF EXISTS `user_devices`;
|
||||||
|
CREATE TABLE `user_devices` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` BIGINT UNSIGNED NOT NULL,
|
||||||
|
`device_fingerprint` VARCHAR(64) NOT NULL,
|
||||||
|
`device_name` VARCHAR(150) DEFAULT NULL,
|
||||||
|
`platform` ENUM('android', 'ios', 'windows', 'macos', 'web') NOT NULL,
|
||||||
|
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
`last_active_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`created_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `idx_user_device_fingerprint` (`user_id`, `device_fingerprint`),
|
||||||
|
CONSTRAINT `fk_ud_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
-- 14. Table: otp_verifications (رموز التحقق عبر منصة صقل)
|
||||||
|
-- ------------------------------------------------------------------------------
|
||||||
|
DROP TABLE IF EXISTS `otp_verifications`;
|
||||||
|
CREATE TABLE `otp_verifications` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`phone_hash` VARCHAR(64) NOT NULL,
|
||||||
|
`otp_code_hash` VARCHAR(255) NOT NULL,
|
||||||
|
`attempts` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
|
`is_used` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
`expires_at` TIMESTAMP NOT NULL,
|
||||||
|
`created_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_otp_phone_hash` (`phone_hash`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
@@ -32,9 +32,14 @@ $router->get('/api/health', function ($request, $response) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Authentication Routes (Rate-limited: 5 attempts per 60 seconds per IP)
|
// Authentication Routes (Rate-limited: 5 attempts per 60 seconds per IP)
|
||||||
// $router->post('/api/auth/register', [\App\Controllers\AuthController::class, 'register'], [\App\Middlewares\RateLimitMiddleware::class]);
|
$router->post('/api/auth/register', [\App\Controllers\AuthController::class, 'register'], [\App\Middlewares\RateLimitMiddleware::class]);
|
||||||
// $router->post('/api/auth/login', [\App\Controllers\AuthController::class, 'login'], [\App\Middlewares\RateLimitMiddleware::class]);
|
$router->post('/api/auth/login', [\App\Controllers\AuthController::class, 'login'], [\App\Middlewares\RateLimitMiddleware::class]);
|
||||||
// $router->get('/api/auth/me', [\App\Controllers\AuthController::class, 'me'], [\App\Middlewares\AuthMiddleware::class]);
|
$router->get('/api/auth/me', [\App\Controllers\AuthController::class, 'me'], [\App\Middlewares\AuthMiddleware::class]);
|
||||||
|
|
||||||
|
// Teacher Routes
|
||||||
|
$router->post('/api/teacher/courses', [\App\Controllers\TeacherController::class, 'addCourse'], [\App\Middlewares\AuthMiddleware::class]);
|
||||||
|
$router->post('/api/teacher/lessons', [\App\Controllers\TeacherController::class, 'addLesson'], [\App\Middlewares\AuthMiddleware::class]);
|
||||||
|
|
||||||
|
|
||||||
// 5. Dispatch the request
|
// 5. Dispatch the request
|
||||||
$router->dispatch($request, $response);
|
$router->dispatch($request, $response);
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
// Script to run database migrations
|
||||||
|
require_once dirname(__DIR__) . '/app/bootstrap.php';
|
||||||
|
|
||||||
|
use App\Core\Database;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = Database::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$sql = file_get_contents(dirname(__DIR__) . '/database_schema.sql');
|
||||||
|
|
||||||
|
$db->exec($sql);
|
||||||
|
|
||||||
|
echo "Database migrations executed successfully!\n";
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
echo "Error executing migrations: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user