🚀 مُصادَق: تحديث برمجي جديد 2026-05-03 15:51
This commit is contained in:
@@ -36,20 +36,15 @@ final class AuthController
|
||||
return;
|
||||
}
|
||||
|
||||
// Set refresh token in HttpOnly cookie
|
||||
setcookie('refresh_token', $result['refresh_token'], [
|
||||
'expires' => time() + (60 * 60 * 24 * 7),
|
||||
'path' => '/api/v1/auth/refresh',
|
||||
'httponly' => true,
|
||||
'samesite' => 'Strict',
|
||||
'secure' => true
|
||||
]);
|
||||
$this->setAuthCookies($result);
|
||||
|
||||
unset($result['refresh_token']);
|
||||
// Backward compatibility for existing non-browser clients
|
||||
$responseData = $result;
|
||||
unset($responseData['refresh_token']);
|
||||
|
||||
Response::json([
|
||||
'success' => true,
|
||||
'data' => $result,
|
||||
'data' => $responseData,
|
||||
'message' => 'تم تسجيل الدخول بنجاح'
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
@@ -88,14 +83,10 @@ final class AuthController
|
||||
}
|
||||
}
|
||||
|
||||
// Clear refresh token cookie
|
||||
setcookie('refresh_token', '', [
|
||||
'expires' => time() - 3600,
|
||||
'path' => '/api/v1/auth/refresh',
|
||||
'httponly' => true,
|
||||
'samesite' => 'Strict',
|
||||
'secure' => true
|
||||
]);
|
||||
// Clear auth cookies
|
||||
setcookie('refresh_token', '', ['expires' => time() - 3600, 'path' => '/api/v1/auth/refresh', 'httponly' => true, 'samesite' => 'Strict', 'secure' => true]);
|
||||
setcookie('access_token', '', ['expires' => time() - 3600, 'path' => '/', 'httponly' => true, 'samesite' => 'Strict', 'secure' => true]);
|
||||
setcookie('csrf_token', '', ['expires' => time() - 3600, 'path' => '/', 'httponly' => false, 'samesite' => 'Strict', 'secure' => true]);
|
||||
|
||||
Response::json([
|
||||
'success' => true,
|
||||
@@ -115,20 +106,15 @@ final class AuthController
|
||||
try {
|
||||
$result = $this->authService->refresh($refreshToken);
|
||||
|
||||
// Set new refresh token in HttpOnly cookie
|
||||
setcookie('refresh_token', $result['refresh_token'], [
|
||||
'expires' => time() + (60 * 60 * 24 * 7),
|
||||
'path' => '/api/v1/auth/refresh',
|
||||
'httponly' => true,
|
||||
'samesite' => 'Strict',
|
||||
'secure' => true
|
||||
]);
|
||||
$this->setAuthCookies($result);
|
||||
|
||||
unset($result['refresh_token']);
|
||||
// Backward compatibility
|
||||
$responseData = $result;
|
||||
unset($responseData['refresh_token']);
|
||||
|
||||
Response::json([
|
||||
'success' => true,
|
||||
'data' => $result,
|
||||
'data' => $responseData,
|
||||
'message' => 'تم تجديد الجلسة بنجاح'
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
@@ -140,20 +126,15 @@ final class AuthController
|
||||
try {
|
||||
$result = $this->authService->register($request->getBody());
|
||||
|
||||
// Set refresh token in HttpOnly cookie
|
||||
setcookie('refresh_token', $result['refresh_token'], [
|
||||
'expires' => time() + (60 * 60 * 24 * 7),
|
||||
'path' => '/api/v1/auth/refresh',
|
||||
'httponly' => true,
|
||||
'samesite' => 'Strict',
|
||||
'secure' => true
|
||||
]);
|
||||
$this->setAuthCookies($result);
|
||||
|
||||
unset($result['refresh_token']);
|
||||
// Backward compatibility
|
||||
$responseData = $result;
|
||||
unset($responseData['refresh_token']);
|
||||
|
||||
Response::json([
|
||||
'success' => true,
|
||||
'data' => $result,
|
||||
'data' => $responseData,
|
||||
'message' => 'تم إنشاء الحساب وتسجيل الدخول بنجاح'
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
@@ -226,19 +207,24 @@ final class AuthController
|
||||
return;
|
||||
}
|
||||
|
||||
// Re-issue a full login session after successful 2FA.
|
||||
$stmt = $db->prepare("SELECT email FROM users WHERE id = ?");
|
||||
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
|
||||
$stmt->execute([$userId]);
|
||||
$email = $stmt->fetchColumn();
|
||||
if (!$email) {
|
||||
$user = $stmt->fetch();
|
||||
if (!$user) {
|
||||
Response::error('المستخدم غير موجود', 'NOT_FOUND', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->authService->createSession($user);
|
||||
$this->setAuthCookies($result);
|
||||
|
||||
$responseData = $result;
|
||||
unset($responseData['refresh_token']);
|
||||
|
||||
Response::json([
|
||||
'success' => true,
|
||||
'data' => ['user_id' => $userId, 'email' => $email],
|
||||
'message' => 'تم التحقق بنجاح'
|
||||
'data' => $responseData,
|
||||
'message' => 'تم التحقق وتأسيس الجلسة بنجاح'
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -265,4 +251,41 @@ final class AuthController
|
||||
|
||||
Response::json(['success' => true, 'message' => 'تم تعطيل التحقق الثنائي']);
|
||||
}
|
||||
|
||||
private function setAuthCookies(array $result): void
|
||||
{
|
||||
$cookieDomain = $_SERVER['HTTP_HOST'] ?? '';
|
||||
$secure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on';
|
||||
|
||||
// 1. Refresh Token (HttpOnly, scoped to /refresh)
|
||||
setcookie('refresh_token', $result['refresh_token'] ?? '', [
|
||||
'expires' => time() + (60 * 60 * 24 * 7), // 7 days
|
||||
'path' => '/api/v1/auth/refresh',
|
||||
'domain' => $cookieDomain,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Strict',
|
||||
'secure' => $secure
|
||||
]);
|
||||
|
||||
// 2. Access Token (HttpOnly, scoped to /)
|
||||
setcookie('access_token', $result['access_token'] ?? '', [
|
||||
'expires' => time() + (60 * 15), // 15 mins
|
||||
'path' => '/',
|
||||
'domain' => $cookieDomain,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Strict',
|
||||
'secure' => $secure
|
||||
]);
|
||||
|
||||
// 3. CSRF Token (Readable by JS)
|
||||
$csrfToken = bin2hex(random_bytes(32));
|
||||
setcookie('csrf_token', $csrfToken, [
|
||||
'expires' => time() + (60 * 15),
|
||||
'path' => '/',
|
||||
'domain' => $cookieDomain,
|
||||
'httponly' => false,
|
||||
'samesite' => 'Strict',
|
||||
'secure' => $secure
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,12 +24,36 @@ final class AuthService
|
||||
{
|
||||
$user = $this->userModel->findByEmail($email);
|
||||
|
||||
if ($user) {
|
||||
if ($user['locked_until'] && strtotime($user['locked_until']) > time()) {
|
||||
throw new \App\Core\Exceptions\HttpException("الحساب مقفل مؤقتاً لعدة محاولات فاشلة، حاول مجدداً لاحقاً", "ACCOUNT_LOCKED", 429);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$user || !password_verify($password, $user['password_hash'])) {
|
||||
throw new Exception("البريد الإلكتروني أو كلمة المرور غير صحيحة");
|
||||
if ($user) {
|
||||
$failedCount = (int)($user['failed_login_count'] ?? 0) + 1;
|
||||
$lockedUntil = null;
|
||||
if ($failedCount >= 5) {
|
||||
$lockedUntil = date('Y-m-d H:i:s', strtotime('+15 minutes'));
|
||||
error_log("[SECURITY] Account locked due to brute force: {$email}");
|
||||
}
|
||||
$this->userModel->update($user['id'], [
|
||||
'failed_login_count' => $failedCount,
|
||||
'locked_until' => $lockedUntil
|
||||
]);
|
||||
}
|
||||
error_log("[SECURITY] Failed login attempt for email: {$email}");
|
||||
throw new \App\Core\Exceptions\HttpException("البريد الإلكتروني أو كلمة المرور غير صحيحة", "INVALID_CREDENTIALS", 401);
|
||||
}
|
||||
|
||||
if (!$user['is_active']) {
|
||||
throw new Exception("هذا الحساب معطل حالياً");
|
||||
throw new \App\Core\Exceptions\HttpException("هذا الحساب معطل حالياً", "ACCOUNT_DISABLED", 403);
|
||||
}
|
||||
|
||||
// Reset failed login count on successful login
|
||||
if ($user['failed_login_count'] > 0) {
|
||||
$this->userModel->update($user['id'], ['failed_login_count' => 0, 'locked_until' => null]);
|
||||
}
|
||||
|
||||
$accessToken = $this->jwtService->issueAccessToken([
|
||||
@@ -51,13 +75,7 @@ final class AuthService
|
||||
return [
|
||||
'access_token' => $accessToken,
|
||||
'refresh_token' => $refreshToken,
|
||||
'user' => [
|
||||
'id' => $user['id'],
|
||||
'name' => $user['name'],
|
||||
'email' => $user['email'],
|
||||
'role' => $user['role'],
|
||||
'assigned_company_id' => $user['assigned_company_id']
|
||||
]
|
||||
'user' => $user
|
||||
];
|
||||
}
|
||||
|
||||
@@ -79,6 +97,11 @@ final class AuthService
|
||||
throw new Exception("جلسة العمل منتهية، يرجى تسجيل الدخول مرة أخرى");
|
||||
}
|
||||
|
||||
return $this->createSession($user);
|
||||
}
|
||||
|
||||
public function createSession(array $user): array
|
||||
{
|
||||
$accessToken = $this->jwtService->issueAccessToken([
|
||||
'user_id' => $user['id'],
|
||||
'tenant_id' => $user['tenant_id'],
|
||||
@@ -100,7 +123,8 @@ final class AuthService
|
||||
'name' => $user['name'],
|
||||
'email' => $user['email'],
|
||||
'role' => $user['role'],
|
||||
'assigned_company_id' => $user['assigned_company_id']
|
||||
'assigned_company_id' => $user['assigned_company_id'],
|
||||
'totp_enabled' => (bool)($user['totp_enabled'] ?? false)
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -112,37 +136,46 @@ final class AuthService
|
||||
throw new Exception("هذا البريد الإلكتروني مسجل مسبقاً");
|
||||
}
|
||||
|
||||
$tenantId = Uuid::uuid4()->toString();
|
||||
$userId = Uuid::uuid4()->toString();
|
||||
$db = \App\Core\Database::getInstance();
|
||||
try {
|
||||
$db->beginTransaction();
|
||||
|
||||
// 2. Create Tenant
|
||||
$this->tenantModel->create([
|
||||
'id' => $tenantId,
|
||||
'name' => $data['tenant_name'],
|
||||
'email' => $data['email'],
|
||||
'status' => 'trial',
|
||||
'trial_ends_at' => date('Y-m-d H:i:s', strtotime('+14 days'))
|
||||
]);
|
||||
$tenantId = Uuid::uuid4()->toString();
|
||||
$userId = Uuid::uuid4()->toString();
|
||||
|
||||
// 3. Create Subscription
|
||||
$this->subscriptionModel->create([
|
||||
'tenant_id' => $tenantId,
|
||||
'plan' => 'basic',
|
||||
'status' => 'trial'
|
||||
]);
|
||||
// 2. Create Tenant
|
||||
$this->tenantModel->create([
|
||||
'id' => $tenantId,
|
||||
'name' => $data['name'] ?? 'مساحة عمل جديدة',
|
||||
'email' => $data['email'],
|
||||
'status' => 'trial',
|
||||
'trial_ends_at' => date('Y-m-d H:i:s', strtotime('+14 days'))
|
||||
]);
|
||||
|
||||
// 4. Create User
|
||||
$this->userModel->create([
|
||||
'id' => $userId,
|
||||
'tenant_id' => $tenantId,
|
||||
'name' => $data['user_name'],
|
||||
'email' => $data['email'],
|
||||
'password_hash' => password_hash($data['password'], PASSWORD_ARGON2ID),
|
||||
'role' => 'admin',
|
||||
'is_active' => 1
|
||||
]);
|
||||
// 3. Create Subscription
|
||||
$this->subscriptionModel->create([
|
||||
'tenant_id' => $tenantId,
|
||||
'plan' => 'basic',
|
||||
'status' => 'trial'
|
||||
]);
|
||||
|
||||
return $this->login($data['email'], $data['password']);
|
||||
// 4. Create User
|
||||
$this->userModel->create([
|
||||
'id' => $userId,
|
||||
'tenant_id' => $tenantId,
|
||||
'name' => $data['name'] ?? 'مسؤول النظام',
|
||||
'email' => $data['email'],
|
||||
'password_hash' => password_hash($data['password'], PASSWORD_ARGON2ID),
|
||||
'role' => 'admin',
|
||||
'is_active' => 1
|
||||
]);
|
||||
|
||||
$db->commit();
|
||||
return $this->login($data['email'], $data['password']);
|
||||
} catch (\Throwable $e) {
|
||||
$db->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
public function logout(string $jti, int $remaining): void
|
||||
{
|
||||
|
||||
@@ -21,15 +21,17 @@ final class CompanyController
|
||||
$role = $request->user->role ?? 'viewer';
|
||||
$assignedCompanyId = $request->user->assigned_company_id ?? null;
|
||||
|
||||
$db = \App\Core\Database::getInstance();
|
||||
$columns = "id, name, name_en, tax_identification_number, commercial_registration_number, city, is_jofotara_linked, is_active, created_at";
|
||||
|
||||
if (in_array($role, ['admin', 'super_admin'], true)) {
|
||||
$companies = $this->companyModel->findByTenant($tenantId);
|
||||
$stmt = $db->prepare("SELECT {$columns} FROM companies WHERE tenant_id = ? AND deleted_at IS NULL");
|
||||
$stmt->execute([$tenantId]);
|
||||
} else {
|
||||
// Filter by assigned company
|
||||
$db = \App\Core\Database::getInstance();
|
||||
$stmt = $db->prepare("SELECT * FROM companies WHERE tenant_id = ? AND id = ? AND deleted_at IS NULL");
|
||||
$stmt = $db->prepare("SELECT {$columns} FROM companies WHERE tenant_id = ? AND id = ? AND deleted_at IS NULL");
|
||||
$stmt->execute([$tenantId, $assignedCompanyId]);
|
||||
$companies = $stmt->fetchAll();
|
||||
}
|
||||
$companies = $stmt->fetchAll();
|
||||
|
||||
Response::json([
|
||||
'success' => true,
|
||||
@@ -40,36 +42,47 @@ final class CompanyController
|
||||
public function create(Request $request): void
|
||||
{
|
||||
$data = $request->getBody();
|
||||
if (empty($data['name']) || empty($data['tax_identification_number'])) {
|
||||
throw new \App\Core\Exceptions\HttpException("اسم الشركة والرقم الضريبي مطلوبان", "VALIDATION_ERROR", 422);
|
||||
}
|
||||
|
||||
$data['tenant_id'] = $request->tenantId;
|
||||
|
||||
try {
|
||||
$companyId = $this->companyService->createCompany($data);
|
||||
Response::json([
|
||||
'success' => true,
|
||||
'data' => ['id' => $companyId],
|
||||
'message' => 'تم إضافة الشركة بنجاح'
|
||||
], 201);
|
||||
} catch (Throwable $e) {
|
||||
Response::error('فشل إضافة الشركة', 'CREATE_FAILED', 500);
|
||||
}
|
||||
$companyId = $this->companyService->createCompany($data);
|
||||
Response::json([
|
||||
'success' => true,
|
||||
'data' => ['id' => $companyId],
|
||||
'message' => 'تم إضافة الشركة بنجاح'
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function updateJoFotara(Request $request, string $id): void
|
||||
{
|
||||
// 1. Verify Tenant Ownership (IDOR Prevention)
|
||||
$db = \App\Core\Database::getInstance();
|
||||
$stmt = $db->prepare("SELECT id FROM companies WHERE id = ? AND tenant_id = ?");
|
||||
$stmt->execute([$id, $request->tenantId]);
|
||||
if (!$stmt->fetchColumn()) {
|
||||
throw new \App\Core\Exceptions\HttpException("الشركة غير موجودة أو لا تملك صلاحية الوصول", "NOT_FOUND", 404);
|
||||
}
|
||||
|
||||
$clientId = $request->input('client_id');
|
||||
$secretKey = $request->input('secret_key');
|
||||
|
||||
if (empty($clientId) || empty($secretKey)) {
|
||||
throw new \App\Core\Exceptions\HttpException("يجب توفير Client ID و Secret Key", "VALIDATION_ERROR", 422);
|
||||
}
|
||||
|
||||
$data = [
|
||||
'jofotara_client_id' => $request->input('client_id'),
|
||||
'jofotara_secret_key' => $request->input('secret_key'),
|
||||
'jofotara_client_id' => $clientId,
|
||||
'jofotara_secret_key' => $secretKey,
|
||||
'is_jofotara_linked' => 1
|
||||
];
|
||||
|
||||
try {
|
||||
$this->companyService->updateJoFotara($id, $data);
|
||||
Response::json([
|
||||
'success' => true,
|
||||
'message' => 'تم تحديث بيانات جو-فواتير بنجاح'
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
Response::error('فشل تحديث البيانات', 'UPDATE_FAILED', 500);
|
||||
}
|
||||
$this->companyService->updateJoFotara($id, $data);
|
||||
Response::json([
|
||||
'success' => true,
|
||||
'message' => 'تم تحديث بيانات جو-فواتير بنجاح'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,20 @@ final class DashboardController
|
||||
$tenantId = $request->tenantId;
|
||||
$role = $request->user->role ?? 'viewer';
|
||||
$assignedCompanyId = $request->user->assigned_company_id ?? null;
|
||||
|
||||
$cacheKey = "dashboard_stats:{$tenantId}:{$role}:" . ($assignedCompanyId ?? 'all');
|
||||
$redis = null;
|
||||
try {
|
||||
$redis = \App\Core\Redis::getInstance();
|
||||
if ($cached = $redis->get($cacheKey)) {
|
||||
Response::json(['success' => true, 'data' => json_decode($cached, true)]);
|
||||
return;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Proceed without cache if Redis fails
|
||||
error_log('[DASHBOARD] Redis Cache Miss/Fail: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$db = Database::getInstance();
|
||||
|
||||
$companyScope = '';
|
||||
@@ -74,17 +88,26 @@ final class DashboardController
|
||||
$stmt->execute([$tenantId]);
|
||||
$companiesCount = (int)$stmt->fetchColumn();
|
||||
|
||||
$data = [
|
||||
'invoices_this_month' => $thisMonth,
|
||||
'subscription_usage_pct' => $usagePct,
|
||||
'pending_extraction' => $pendingExtraction,
|
||||
'approved_invoices' => $approved,
|
||||
'status_distribution' => $statusDistribution,
|
||||
'recent_invoices' => $recent,
|
||||
'companies_count' => $companiesCount,
|
||||
'risk_alerts_count' => $riskCount
|
||||
];
|
||||
|
||||
if ($redis) {
|
||||
try {
|
||||
$redis->setex($cacheKey, 60, json_encode($data)); // Cache for 60 seconds
|
||||
} catch (\Throwable $e) {}
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'total_this_month' => $thisMonth,
|
||||
'subscription_usage' => $usagePct,
|
||||
'pending_extraction' => $pendingExtraction,
|
||||
'status_distribution' => $statusDistribution,
|
||||
'recent_invoices' => $recent,
|
||||
'companies_count' => $companiesCount,
|
||||
'risk_alerts_count' => $riskCount
|
||||
]
|
||||
'data' => $data
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -63,24 +63,38 @@ final class InvoiceController
|
||||
|
||||
public function upload(Request $request): void
|
||||
{
|
||||
$db = Database::getInstance();
|
||||
try {
|
||||
$files = $request->getFiles();
|
||||
if (empty($files['file'])) {
|
||||
throw new \Exception('يرجى اختيار ملف للفاتورة');
|
||||
throw new \App\Core\Exceptions\HttpException('يرجى اختيار ملف للفاتورة', 'VALIDATION_ERROR', 422);
|
||||
}
|
||||
|
||||
$file = $files['file'];
|
||||
if ($file['size'] > 20 * 1024 * 1024) { // 20 MB limit
|
||||
throw new \App\Core\Exceptions\HttpException('حجم الملف يتجاوز الحد المسموح به (20 ميجابايت)', 'VALIDATION_ERROR', 422);
|
||||
}
|
||||
|
||||
$companyId = (string)$request->input('company_id');
|
||||
if (empty($companyId)) {
|
||||
throw new \Exception('يرجى اختيار الشركة');
|
||||
throw new \App\Core\Exceptions\HttpException('يرجى اختيار الشركة', 'VALIDATION_ERROR', 422);
|
||||
}
|
||||
|
||||
// Verify company belongs to tenant
|
||||
$stmt = $db->prepare("SELECT id FROM companies WHERE id = ? AND tenant_id = ?");
|
||||
$stmt->execute([$companyId, $request->tenantId]);
|
||||
if (!$stmt->fetchColumn()) {
|
||||
throw new \App\Core\Exceptions\HttpException('الشركة غير موجودة أو لا تملك صلاحية الوصول', 'FORBIDDEN', 403);
|
||||
}
|
||||
|
||||
$file = $files['file'];
|
||||
$invoiceId = \Ramsey\Uuid\Uuid::uuid4()->toString();
|
||||
|
||||
// Store file
|
||||
$path = $this->storage->store($file, $request->tenantId, $companyId);
|
||||
|
||||
// Create record
|
||||
// Transaction for consistency
|
||||
$db->beginTransaction();
|
||||
|
||||
$this->invoiceModel->create([
|
||||
'id' => $invoiceId,
|
||||
'tenant_id' => $request->tenantId,
|
||||
@@ -89,16 +103,22 @@ final class InvoiceController
|
||||
'status' => 'uploaded'
|
||||
]);
|
||||
|
||||
// Queue extraction and risk analysis
|
||||
\App\Services\QueueService::push(\queue\Jobs\ExtractInvoiceJob::class, ['invoice_id' => $invoiceId]);
|
||||
|
||||
$db->commit();
|
||||
|
||||
Response::json([
|
||||
'success' => true,
|
||||
'data' => ['invoice_id' => $invoiceId],
|
||||
'message' => 'تم رفع الفاتورة بنجاح وجاري استخراج البيانات بالذكاء الاصطناعي'
|
||||
], 202);
|
||||
} catch (\App\Core\Exceptions\HttpException $e) {
|
||||
throw $e; // Let global handler catch it
|
||||
} catch (Throwable $e) {
|
||||
Response::error($e->getMessage(), 'UPLOAD_ERROR', (int)($e->getCode() ?: 500));
|
||||
if ($db->inTransaction()) {
|
||||
$db->rollBack();
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,7 +231,13 @@ final class InvoiceController
|
||||
|
||||
public function update(Request $request, string $id): void
|
||||
{
|
||||
// Implementation for PUT /api/v1/invoices/{id}
|
||||
$db = Database::getInstance();
|
||||
$stmt = $db->prepare("SELECT id FROM invoices WHERE id = ? AND tenant_id = ?");
|
||||
$stmt->execute([$id, $request->tenantId]);
|
||||
if (!$stmt->fetchColumn()) {
|
||||
throw new \App\Core\Exceptions\HttpException('الفاتورة غير موجودة', 'NOT_FOUND', 404);
|
||||
}
|
||||
|
||||
$data = $request->getBody();
|
||||
$this->invoiceModel->update($id, $data);
|
||||
Response::json(['success' => true, 'message' => 'تم تحديث الفاتورة بنجاح']);
|
||||
@@ -219,6 +245,13 @@ final class InvoiceController
|
||||
|
||||
public function destroy(Request $request, string $id): void
|
||||
{
|
||||
$db = Database::getInstance();
|
||||
$stmt = $db->prepare("SELECT id FROM invoices WHERE id = ? AND tenant_id = ?");
|
||||
$stmt->execute([$id, $request->tenantId]);
|
||||
if (!$stmt->fetchColumn()) {
|
||||
throw new \App\Core\Exceptions\HttpException('الفاتورة غير موجودة', 'NOT_FOUND', 404);
|
||||
}
|
||||
|
||||
$this->invoiceModel->delete($id);
|
||||
Response::json(['success' => true, 'message' => 'تم حذف الفاتورة بنجاح']);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ final class UsersController
|
||||
|
||||
public function list(Request $request): void
|
||||
{
|
||||
if (!in_array($request->user->role, ['admin', 'super_admin'])) {
|
||||
throw new \App\Core\Exceptions\HttpException("غير مصرح لك بالوصول", "FORBIDDEN", 403);
|
||||
}
|
||||
$tenantId = $request->tenantId;
|
||||
$users = $this->userModel->findAllByTenant($tenantId);
|
||||
|
||||
@@ -22,17 +25,19 @@ final class UsersController
|
||||
|
||||
public function create(Request $request): void
|
||||
{
|
||||
if (!in_array($request->user->role, ['admin', 'super_admin'])) {
|
||||
throw new \App\Core\Exceptions\HttpException("غير مصرح لك بالوصول", "FORBIDDEN", 403);
|
||||
}
|
||||
$tenantId = $request->tenantId;
|
||||
$data = $request->getBody();
|
||||
|
||||
if (empty($data['email']) || empty($data['password']) || empty($data['name']) || empty($data['role'])) {
|
||||
Response::error('جميع الحقول مطلوبة', 'VALIDATION_ERROR', 422);
|
||||
return;
|
||||
throw new \App\Core\Exceptions\HttpException('جميع الحقول مطلوبة', 'VALIDATION_ERROR', 422);
|
||||
}
|
||||
|
||||
if ($this->userModel->findByEmail($data['email'])) {
|
||||
Response::error('البريد الإلكتروني مستخدم مسبقاً', 'DUPLICATE_EMAIL', 409);
|
||||
return;
|
||||
// Fix: Check email uniqueness WITHIN THE TENANT
|
||||
if ($this->userModel->findByEmail($data['email'], $tenantId)) {
|
||||
throw new \App\Core\Exceptions\HttpException('البريد الإلكتروني مستخدم مسبقاً في هذه الشركة', 'DUPLICATE_EMAIL', 409);
|
||||
}
|
||||
|
||||
$userId = Uuid::uuid4()->toString();
|
||||
@@ -57,11 +62,13 @@ final class UsersController
|
||||
|
||||
public function update(Request $request, string $id): void
|
||||
{
|
||||
if (!in_array($request->user->role, ['admin', 'super_admin'])) {
|
||||
throw new \App\Core\Exceptions\HttpException("غير مصرح لك بالوصول", "FORBIDDEN", 403);
|
||||
}
|
||||
$tenantId = $request->tenantId;
|
||||
$user = $this->userModel->findById($id, $tenantId);
|
||||
if (!$user) {
|
||||
Response::error('المستخدم غير موجود', 'NOT_FOUND', 404);
|
||||
return;
|
||||
throw new \App\Core\Exceptions\HttpException('المستخدم غير موجود', 'NOT_FOUND', 404);
|
||||
}
|
||||
|
||||
$data = $request->getBody();
|
||||
@@ -86,11 +93,13 @@ final class UsersController
|
||||
|
||||
public function destroy(Request $request, string $id): void
|
||||
{
|
||||
if (!in_array($request->user->role, ['admin', 'super_admin'])) {
|
||||
throw new \App\Core\Exceptions\HttpException("غير مصرح لك بالوصول", "FORBIDDEN", 403);
|
||||
}
|
||||
$tenantId = $request->tenantId;
|
||||
$user = $this->userModel->findById($id, $tenantId);
|
||||
if (!$user) {
|
||||
Response::error('المستخدم غير موجود', 'NOT_FOUND', 404);
|
||||
return;
|
||||
throw new \App\Core\Exceptions\HttpException('المستخدم غير موجود', 'NOT_FOUND', 404);
|
||||
}
|
||||
|
||||
if ($user['id'] === $request->user->user_id) {
|
||||
|
||||
Reference in New Issue
Block a user