Update: 2026-05-03 22:51:59
This commit is contained in:
@@ -60,13 +60,22 @@ $refreshTokenHash = hash('sha256', $refreshToken);
|
||||
$stmt = $db->prepare("UPDATE users SET refresh_token_hash = ? WHERE id = ?");
|
||||
$stmt->execute([$refreshTokenHash, $user['id']]);
|
||||
|
||||
// 7. Secure Refresh Token delivery via HttpOnly Cookie
|
||||
setcookie('refresh_token', $refreshToken, [
|
||||
'expires' => time() + (7 * 24 * 60 * 60), // 7 days
|
||||
'path' => '/api/v1/auth/refresh',
|
||||
'secure' => true,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Strict',
|
||||
]);
|
||||
|
||||
json_success([
|
||||
'access_token' => $token,
|
||||
'refresh_token' => $refreshToken,
|
||||
'user' => [
|
||||
'id' => $user['id'],
|
||||
'name' => (App\Core\Encryption::decrypt($user['name']) ?: $user['name']),
|
||||
'email' => (App\Core\Encryption::decrypt($user['email']) ?: $user['email']),
|
||||
'role' => $user['role']
|
||||
'role' => $user['role'],
|
||||
'tenant_id' => $user['tenant_id']
|
||||
]
|
||||
], 'تم تسجيل الدخول بنجاح');
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
<?php
|
||||
/**
|
||||
* Auth Refresh Endpoint
|
||||
* Refresh Token Endpoint (Secure Cookie Based)
|
||||
*/
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Core\JWT;
|
||||
use Firebase\JWT\JWT;
|
||||
|
||||
$data = input();
|
||||
$refreshToken = $data['refresh_token'] ?? null;
|
||||
// 1. Get Refresh Token from HttpOnly Cookie
|
||||
$refreshToken = $_COOKIE['refresh_token'] ?? null;
|
||||
|
||||
if (!$refreshToken) {
|
||||
json_error('Refresh token is required', 400);
|
||||
json_error('Refresh token is required', 401);
|
||||
}
|
||||
|
||||
$db = Database::getInstance();
|
||||
$refreshTokenHash = hash('sha256', $refreshToken);
|
||||
$stmt = $db->prepare("SELECT * FROM users WHERE refresh_token_hash = ? LIMIT 1");
|
||||
|
||||
// 2. Verify in DB
|
||||
$stmt = $db->prepare("SELECT * FROM users WHERE refresh_token_hash = ? AND is_active = 1 LIMIT 1");
|
||||
$stmt->execute([$refreshTokenHash]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
@@ -23,25 +25,21 @@ if (!$user) {
|
||||
json_error('Invalid refresh token', 401);
|
||||
}
|
||||
|
||||
$secret = env('JWT_SECRET');
|
||||
if (!$secret || strlen($secret) < 32) {
|
||||
error_log('FATAL: JWT_SECRET is missing or too short in .env');
|
||||
// 3. Generate New Access Token
|
||||
$secret = $_ENV['JWT_SECRET'] ?? null;
|
||||
if (!$secret) {
|
||||
json_error('Server configuration error', 500);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'user_id' => $user['id'],
|
||||
'role' => $user['role'],
|
||||
'exp' => time() + (15 * 60)
|
||||
'user_id' => $user['id'],
|
||||
'tenant_id' => $user['tenant_id'], // Now including tenant_id
|
||||
'role' => $user['role'],
|
||||
'exp' => time() + (15 * 60) // 15 minutes
|
||||
];
|
||||
|
||||
$newToken = JWT::encode($payload, $secret);
|
||||
$newRefreshToken = bin2hex(random_bytes(32));
|
||||
$newRefreshTokenHash = hash('sha256', $newRefreshToken);
|
||||
|
||||
$stmt = $db->prepare("UPDATE users SET refresh_token_hash = ? WHERE id = ?");
|
||||
$stmt->execute([$newRefreshTokenHash, $user['id']]);
|
||||
$token = JWT::encode($payload, $secret, 'HS256');
|
||||
|
||||
json_success([
|
||||
'access_token' => $newToken,
|
||||
'refresh_token' => $newRefreshToken
|
||||
], 'تم تجديد الجلسة بنجاح');
|
||||
'access_token' => $token
|
||||
]);
|
||||
|
||||
@@ -10,32 +10,41 @@ use App\Middleware\AuthMiddleware;
|
||||
$decoded = AuthMiddleware::check();
|
||||
$db = Database::getInstance();
|
||||
|
||||
$tenantId = $decoded['tenant_id'];
|
||||
$tenantId = $decoded['tenant_id'] ?? null;
|
||||
$companyId = $decoded['company_id'] ?? null;
|
||||
$role = $decoded['role'];
|
||||
|
||||
try {
|
||||
// 2. Build Query based on Role
|
||||
$where = "WHERE tenant_id = :tenant_id";
|
||||
$params = [':tenant_id' => $tenantId];
|
||||
$where = "WHERE 1=1";
|
||||
$params = [];
|
||||
|
||||
// If accountant or employee restricted to a company
|
||||
if (($role === 'accountant' || $role === 'viewer') && $companyId) {
|
||||
$where .= " AND company_id = :company_id";
|
||||
$params[':company_id'] = $companyId;
|
||||
// 2. Apply Filters based on Role
|
||||
if ($role === 'super_admin') {
|
||||
// No filters - see everything
|
||||
} elseif ($role === 'admin') {
|
||||
// Filter by Tenant (Accounting Office)
|
||||
$where .= " AND tenant_id = :tenant_id";
|
||||
$params[':tenant_id'] = $tenantId;
|
||||
} else {
|
||||
// Accountant/Viewer: Filter by specific company
|
||||
$where .= " AND tenant_id = :tenant_id";
|
||||
$params[':tenant_id'] = $tenantId;
|
||||
|
||||
if ($companyId) {
|
||||
$where .= " AND company_id = :company_id";
|
||||
$params[':company_id'] = $companyId;
|
||||
}
|
||||
}
|
||||
|
||||
// Total Invoices
|
||||
// 3. Fetch Stats
|
||||
$stmt = $db->prepare("SELECT COUNT(*) FROM invoices $where");
|
||||
$stmt->execute($params);
|
||||
$total = $stmt->fetchColumn();
|
||||
|
||||
// Pending Invoices
|
||||
$stmt = $db->prepare("SELECT COUNT(*) FROM invoices $where AND status = 'pending'");
|
||||
$stmt->execute($params);
|
||||
$pending = $stmt->fetchColumn();
|
||||
|
||||
// Approved Invoices
|
||||
$stmt = $db->prepare("SELECT COUNT(*) FROM invoices $where AND status = 'approved'");
|
||||
$stmt->execute($params);
|
||||
$approved = $stmt->fetchColumn();
|
||||
|
||||
72
app/modules_app/invoices/upload.php
Normal file
72
app/modules_app/invoices/upload.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Upload Endpoint (Multi-Tenant & Role-Aware)
|
||||
*/
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
|
||||
// 1. Auth Check
|
||||
$decoded = AuthMiddleware::check();
|
||||
$db = Database::getInstance();
|
||||
|
||||
$allowedRoles = ['admin', 'accountant', 'employee'];
|
||||
if (!in_array($decoded['role'], $allowedRoles)) {
|
||||
json_error('Unauthorized to upload invoices', 403);
|
||||
}
|
||||
|
||||
// 2. Validate Request
|
||||
$companyId = $_POST['company_id'] ?? null;
|
||||
if (!$companyId || !isset($_FILES['invoice'])) {
|
||||
json_error('Company ID and invoice file are required', 422);
|
||||
}
|
||||
|
||||
// 3. Permission Check (Can this user upload to this company?)
|
||||
$tenantId = $decoded['tenant_id'];
|
||||
$userId = $decoded['user_id'];
|
||||
|
||||
if ($decoded['role'] === 'admin') {
|
||||
$stmt = $db->prepare("SELECT id FROM companies WHERE id = ? AND tenant_id = ? AND deleted_at IS NULL");
|
||||
$stmt->execute([$companyId, $tenantId]);
|
||||
} elseif ($decoded['role'] === 'accountant') {
|
||||
$stmt = $db->prepare("
|
||||
SELECT c.id FROM companies c
|
||||
JOIN user_company_assignments uca ON c.id = uca.company_id
|
||||
WHERE c.id = ? AND uca.user_id = ? AND uca.is_active = 1
|
||||
");
|
||||
$stmt->execute([$companyId, $userId]);
|
||||
} else { // employee
|
||||
// In our schema, employee is linked via users.company_id
|
||||
$stmt = $db->prepare("SELECT id FROM users WHERE id = ? AND company_id = ?");
|
||||
$stmt->execute([$userId, $companyId]);
|
||||
}
|
||||
|
||||
if (!$stmt->fetch()) {
|
||||
json_error('Access denied to this company', 403);
|
||||
}
|
||||
|
||||
// 4. Handle File Upload (Mock logic for now, using storage/invoices)
|
||||
$uploadDir = __DIR__ . '/../../../storage/invoices/' . $tenantId . '/' . $companyId . '/';
|
||||
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
|
||||
|
||||
$fileName = time() . '_' . basename($_FILES['invoice']['name']);
|
||||
$targetFile = $uploadDir . $fileName;
|
||||
|
||||
if (move_uploaded_file($_FILES['invoice']['tmp_name'], $targetFile)) {
|
||||
// 5. Save to DB
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO invoices (
|
||||
tenant_id, company_id, status, uploaded_by, original_file_path, created_at
|
||||
) VALUES (?, ?, 'uploaded', ?, ?, NOW())
|
||||
");
|
||||
$stmt->execute([
|
||||
$tenantId,
|
||||
$companyId,
|
||||
$userId,
|
||||
$targetFile
|
||||
]);
|
||||
|
||||
json_success(['id' => $db->lastInsertId()], 'تم رفع الفاتورة بنجاح وبدأت عملية المعالجة');
|
||||
} else {
|
||||
json_error('Failed to save uploaded file', 500);
|
||||
}
|
||||
@@ -16,6 +16,17 @@ if ($decoded['role'] !== 'super_admin' && $decoded['role'] !== 'admin') {
|
||||
|
||||
$data = input();
|
||||
|
||||
// 1. Role Authorization check (Prevent Role Escalation)
|
||||
$allowedRoles = match($decoded['role']) {
|
||||
'super_admin' => ['super_admin', 'admin', 'accountant', 'employee', 'viewer'],
|
||||
'admin' => ['accountant', 'employee', 'viewer'],
|
||||
default => []
|
||||
};
|
||||
|
||||
if (!in_array($data['role'] ?? '', $allowedRoles, true)) {
|
||||
json_error('غير مصرح لك بإنشاء مستخدم بهذا الدور', 403);
|
||||
}
|
||||
|
||||
// 2. Validation
|
||||
$errors = Validator::validate($data, [
|
||||
'name' => 'required',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* Users List Endpoint (with Decryption)
|
||||
* Users List Endpoint (Role-Based & Tenant-Aware)
|
||||
*/
|
||||
|
||||
use App\Core\Database;
|
||||
@@ -9,26 +9,51 @@ use App\Middleware\AuthMiddleware;
|
||||
|
||||
// 1. Auth Check
|
||||
$decoded = AuthMiddleware::check();
|
||||
$db = Database::getInstance();
|
||||
|
||||
// 2. Simple Role-Based Access Control (RBAC)
|
||||
if ($decoded['role'] !== 'super_admin' && $decoded['role'] !== 'admin') {
|
||||
json_error('غير مصرح لك بالوصول لهذه البيانات', 403);
|
||||
$role = $decoded['role'];
|
||||
$tenantId = $decoded['tenant_id'] ?? null;
|
||||
|
||||
// 2. Build Query based on Role
|
||||
if ($role === 'super_admin') {
|
||||
// Super Admin sees ALL users from ALL tenants
|
||||
$stmt = $db->query("
|
||||
SELECT u.id, u.name, u.email, u.role, u.is_active, u.created_at, t.name as tenant_name, c.name as company_name
|
||||
FROM users u
|
||||
LEFT JOIN tenants t ON u.tenant_id = t.id
|
||||
LEFT JOIN companies c ON u.company_id = c.id
|
||||
");
|
||||
} elseif ($role === 'admin') {
|
||||
// Admin sees only users in THEIR tenant (Accounting Office)
|
||||
$stmt = $db->prepare("
|
||||
SELECT u.id, u.name, u.email, u.role, u.is_active, u.created_at, t.name as tenant_name, c.name as company_name
|
||||
FROM users u
|
||||
LEFT JOIN tenants t ON u.tenant_id = t.id
|
||||
LEFT JOIN companies c ON u.company_id = c.id
|
||||
WHERE u.tenant_id = ?
|
||||
");
|
||||
$stmt->execute([$tenantId]);
|
||||
} else {
|
||||
// Other roles shouldn't see user list
|
||||
json_error('Unauthorized', 403);
|
||||
}
|
||||
|
||||
// 3. Fetch Data
|
||||
$db = Database::getInstance();
|
||||
$stmt = $db->prepare("SELECT id, name, email, role, is_active, created_at FROM users");
|
||||
$stmt->execute();
|
||||
$users = $stmt->fetchAll();
|
||||
|
||||
// 4. Decrypt sensitive data for the UI
|
||||
// 3. Decrypt data and format
|
||||
foreach ($users as &$user) {
|
||||
// Try to decrypt. If it fails (e.g. data was plain text), keep original.
|
||||
// Decrypt User Name/Email
|
||||
$decryptedName = Encryption::decrypt($user['name']);
|
||||
$user['name'] = $decryptedName !== false ? $decryptedName : $user['name'];
|
||||
|
||||
$decryptedEmail = Encryption::decrypt($user['email']);
|
||||
$user['email'] = $decryptedEmail !== false ? $decryptedEmail : $user['email'];
|
||||
|
||||
// Decrypt Company Name (if exists)
|
||||
if ($user['company_name']) {
|
||||
$decryptedCompanyName = Encryption::decrypt($user['company_name']);
|
||||
$user['company_name'] = $decryptedCompanyName !== false ? $decryptedCompanyName : $user['company_name'];
|
||||
}
|
||||
}
|
||||
|
||||
json_success($users);
|
||||
|
||||
Reference in New Issue
Block a user