🚀 مُصادَق: تحديث برمجي جديد 2026-05-03 13:19
This commit is contained in:
@@ -53,4 +53,79 @@ final class AuthController
|
||||
'data' => $request->user
|
||||
]);
|
||||
}
|
||||
|
||||
public function logout(Request $request): void
|
||||
{
|
||||
// Clear refresh token cookie
|
||||
setcookie('refresh_token', '', [
|
||||
'expires' => time() - 3600,
|
||||
'path' => '/api/v1/auth/refresh',
|
||||
'httponly' => true,
|
||||
'samesite' => 'Strict',
|
||||
'secure' => true
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'success' => true,
|
||||
'message' => 'تم تسجيل الخروج بنجاح'
|
||||
]);
|
||||
}
|
||||
|
||||
public function refresh(Request $request): void
|
||||
{
|
||||
$refreshToken = $_COOKIE['refresh_token'] ?? null;
|
||||
|
||||
if (!$refreshToken) {
|
||||
Response::error('رمز التجديد مفقود', 'UNAUTHORIZED', 401);
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
]);
|
||||
|
||||
unset($result['refresh_token']);
|
||||
|
||||
Response::json([
|
||||
'success' => true,
|
||||
'data' => $result,
|
||||
'message' => 'تم تجديد الجلسة بنجاح'
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
Response::error($e->getMessage(), 'REFRESH_FAILED', 401);
|
||||
}
|
||||
}
|
||||
public function register(Request $request): void
|
||||
{
|
||||
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
|
||||
]);
|
||||
|
||||
unset($result['refresh_token']);
|
||||
|
||||
Response::json([
|
||||
'success' => true,
|
||||
'data' => $result,
|
||||
'message' => 'تم إنشاء الحساب وتسجيل الدخول بنجاح'
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
Response::error($e->getMessage(), 'REGISTRATION_FAILED', 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,19 @@ declare(strict_types=1);
|
||||
namespace App\Modules\Auth;
|
||||
|
||||
use App\Modules\Users\UserModel;
|
||||
use App\Modules\Tenants\TenantModel;
|
||||
use App\Modules\Subscriptions\SubscriptionModel;
|
||||
use App\Services\Security\JwtService;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Exception;
|
||||
|
||||
final class AuthService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UserModel $userModel,
|
||||
private readonly JwtService $jwtService
|
||||
private readonly JwtService $jwtService,
|
||||
private readonly TenantModel $tenantModel,
|
||||
private readonly SubscriptionModel $subscriptionModel
|
||||
) {}
|
||||
|
||||
public function login(string $email, string $password): array
|
||||
@@ -55,4 +60,88 @@ final class AuthService
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function refresh(string $refreshToken): array
|
||||
{
|
||||
$parts = explode('.', $refreshToken);
|
||||
if (count($parts) !== 2) {
|
||||
throw new Exception("رمز التجديد غير صالحة");
|
||||
}
|
||||
|
||||
[$userId, $random] = $parts;
|
||||
$user = $this->userModel->find($userId);
|
||||
|
||||
if (!$user || !$user['is_active']) {
|
||||
throw new Exception("المستخدم غير موجود أو معطل");
|
||||
}
|
||||
|
||||
if (!$user['refresh_token_hash'] || !password_verify($refreshToken, $user['refresh_token_hash'])) {
|
||||
throw new Exception("جلسة العمل منتهية، يرجى تسجيل الدخول مرة أخرى");
|
||||
}
|
||||
|
||||
$accessToken = $this->jwtService->issueAccessToken([
|
||||
'user_id' => $user['id'],
|
||||
'tenant_id' => $user['tenant_id'],
|
||||
'role' => $user['role'],
|
||||
'assigned_company_id' => $user['assigned_company_id']
|
||||
]);
|
||||
|
||||
$newRefreshToken = $this->jwtService->issueRefreshToken($user['id']);
|
||||
|
||||
$this->userModel->update($user['id'], [
|
||||
'refresh_token_hash' => password_hash($newRefreshToken, PASSWORD_BCRYPT)
|
||||
]);
|
||||
|
||||
return [
|
||||
'access_token' => $accessToken,
|
||||
'refresh_token' => $newRefreshToken,
|
||||
'user' => [
|
||||
'id' => $user['id'],
|
||||
'name' => $user['name'],
|
||||
'email' => $user['email'],
|
||||
'role' => $user['role'],
|
||||
'assigned_company_id' => $user['assigned_company_id']
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function register(array $data): array
|
||||
{
|
||||
// 1. Check if tenant already exists
|
||||
if ($this->tenantModel->findByEmail($data['email'])) {
|
||||
throw new Exception("هذا البريد الإلكتروني مسجل مسبقاً");
|
||||
}
|
||||
|
||||
$tenantId = Uuid::uuid4()->toString();
|
||||
$userId = Uuid::uuid4()->toString();
|
||||
|
||||
// 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'))
|
||||
]);
|
||||
|
||||
// 3. Create Subscription
|
||||
$this->subscriptionModel->create([
|
||||
'tenant_id' => $tenantId,
|
||||
'plan' => 'basic',
|
||||
'status' => 'trial'
|
||||
]);
|
||||
|
||||
// 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
|
||||
]);
|
||||
|
||||
return $this->login($data['email'], $data['password']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,38 +72,18 @@ final class InvoiceController
|
||||
'idempotency_key' => bin2hex(random_bytes(16))
|
||||
]);
|
||||
|
||||
// Attempt AI Extraction
|
||||
try {
|
||||
$mimeType = mime_content_type($filePath);
|
||||
$extractedData = $this->aiExtraction->extractInvoiceData($filePath, $mimeType);
|
||||
|
||||
// Update Invoice with extracted data
|
||||
$this->invoiceModel->update($invoiceId, [
|
||||
'status' => 'extracted', // Match schema ENUM
|
||||
'ai_raw_response' => json_encode($extractedData, JSON_UNESCAPED_UNICODE)
|
||||
]);
|
||||
// Push to Queue for AI Extraction
|
||||
\App\Services\QueueService::push('invoice_extraction', [
|
||||
'invoice_id' => $invoiceId,
|
||||
'file_path' => $filePath,
|
||||
'mime_type' => mime_content_type($filePath)
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'invoice_id' => $invoiceId,
|
||||
'extracted_data' => $extractedData
|
||||
],
|
||||
'message' => 'تم رفع الفاتورة واستخراج البيانات بنجاح بالذكاء الاصطناعي'
|
||||
]);
|
||||
|
||||
} catch (Throwable $aiError) {
|
||||
// Keep it uploaded, maybe manual retry later
|
||||
$this->invoiceModel->update($invoiceId, [
|
||||
'status' => 'validation_failed' // Match schema fallback
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'success' => true,
|
||||
'data' => ['invoice_id' => $invoiceId],
|
||||
'message' => 'تم الرفع ولكن فشل استخراج البيانات. ' . $aiError->getMessage()
|
||||
]);
|
||||
}
|
||||
Response::json([
|
||||
'success' => true,
|
||||
'data' => ['invoice_id' => $invoiceId],
|
||||
'message' => 'تم رفع الفاتورة بنجاح وجاري استخراج البيانات بالذكاء الاصطناعي'
|
||||
], 202);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
Response::error($e->getMessage(), 'UPLOAD_FAILED', 500);
|
||||
|
||||
29
app/Modules/Subscriptions/SubscriptionController.php
Normal file
29
app/Modules/Subscriptions/SubscriptionController.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Subscriptions;
|
||||
|
||||
use App\Core\{Request, Response};
|
||||
use App\Modules\Subscriptions\SubscriptionModel;
|
||||
|
||||
final class SubscriptionController
|
||||
{
|
||||
public function __construct(private readonly SubscriptionModel $subscriptionModel) {}
|
||||
|
||||
public function me(Request $request): void
|
||||
{
|
||||
$tenantId = $request->tenantId;
|
||||
$subscription = $this->subscriptionModel->findByTenantId($tenantId);
|
||||
|
||||
if (!$subscription) {
|
||||
Response::error('لا يوجد اشتراك فعال حالياً', 'NOT_FOUND', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'success' => true,
|
||||
'data' => $subscription
|
||||
]);
|
||||
}
|
||||
}
|
||||
19
app/Modules/Subscriptions/SubscriptionModel.php
Normal file
19
app/Modules/Subscriptions/SubscriptionModel.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Subscriptions;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
final class SubscriptionModel extends BaseModel
|
||||
{
|
||||
protected string $table = 'subscriptions';
|
||||
|
||||
public function findByTenantId(string $tenantId): ?array
|
||||
{
|
||||
$stmt = $this->db()->prepare("SELECT * FROM {$this->table} WHERE tenant_id = ? LIMIT 1");
|
||||
$stmt->execute([$tenantId]);
|
||||
return $stmt->fetch() ?: null;
|
||||
}
|
||||
}
|
||||
29
app/Modules/Tenants/TenantController.php
Normal file
29
app/Modules/Tenants/TenantController.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Tenants;
|
||||
|
||||
use App\Core\{Request, Response};
|
||||
use App\Modules\Tenants\TenantModel;
|
||||
|
||||
final class TenantController
|
||||
{
|
||||
public function __construct(private readonly TenantModel $tenantModel) {}
|
||||
|
||||
public function me(Request $request): void
|
||||
{
|
||||
$tenantId = $request->tenantId;
|
||||
$tenant = $this->tenantModel->find($tenantId);
|
||||
|
||||
if (!$tenant) {
|
||||
Response::error('المستأجر غير موجود', 'NOT_FOUND', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'success' => true,
|
||||
'data' => $tenant
|
||||
]);
|
||||
}
|
||||
}
|
||||
19
app/Modules/Tenants/TenantModel.php
Normal file
19
app/Modules/Tenants/TenantModel.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Tenants;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
final class TenantModel extends BaseModel
|
||||
{
|
||||
protected string $table = 'tenants';
|
||||
|
||||
public function findByEmail(string $email): ?array
|
||||
{
|
||||
$stmt = $this->db()->prepare("SELECT * FROM {$this->table} WHERE email = ? AND deleted_at IS NULL LIMIT 1");
|
||||
$stmt->execute([$email]);
|
||||
return $stmt->fetch() ?: null;
|
||||
}
|
||||
}
|
||||
42
app/Modules/Users/UserController.php
Normal file
42
app/Modules/Users/UserController.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Users;
|
||||
|
||||
use App\Core\{Request, Response};
|
||||
use App\Modules\Users\UserModel;
|
||||
|
||||
final class UserController
|
||||
{
|
||||
public function __construct(private readonly UserModel $userModel) {}
|
||||
|
||||
public function index(Request $request): void
|
||||
{
|
||||
$tenantId = $request->tenantId;
|
||||
$users = $this->userModel->findAllByTenant($tenantId);
|
||||
|
||||
Response::json([
|
||||
'success' => true,
|
||||
'data' => $users
|
||||
]);
|
||||
}
|
||||
|
||||
public function detail(Request $request, array $vars): void
|
||||
{
|
||||
$tenantId = $request->tenantId;
|
||||
$userId = $vars['id'];
|
||||
|
||||
$user = $this->userModel->findById($userId, $tenantId);
|
||||
|
||||
if (!$user) {
|
||||
Response::error('المستخدم غير موجود', 'NOT_FOUND', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'success' => true,
|
||||
'data' => $user
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -24,4 +24,18 @@ final class UserModel extends BaseModel
|
||||
$stmt->execute($params);
|
||||
return $stmt->fetch() ?: null;
|
||||
}
|
||||
|
||||
public function findAllByTenant(string $tenantId): array
|
||||
{
|
||||
$stmt = $this->db()->prepare("SELECT id, name, email, role, is_active, created_at FROM {$this->table} WHERE tenant_id = ? AND deleted_at IS NULL");
|
||||
$stmt->execute([$tenantId]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public function findById(string $id, string $tenantId): ?array
|
||||
{
|
||||
$stmt = $this->db()->prepare("SELECT id, name, email, role, is_active, created_at FROM {$this->table} WHERE id = ? AND tenant_id = ? AND deleted_at IS NULL LIMIT 1");
|
||||
$stmt->execute([$id, $tenantId]);
|
||||
return $stmt->fetch() ?: null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user