Update: 2026-05-03 21:58:11
This commit is contained in:
@@ -28,10 +28,11 @@ if ($errors) {
|
||||
$email = $data['email'];
|
||||
$password = $data['password'];
|
||||
|
||||
// 2. DB Check
|
||||
// 2. DB Check (Using hash for lookup since email is encrypted)
|
||||
$db = Database::getInstance();
|
||||
$stmt = $db->prepare("SELECT * FROM users WHERE email = ? LIMIT 1");
|
||||
$stmt->execute([$email]);
|
||||
$emailHash = hash('sha256', strtolower($email));
|
||||
$stmt = $db->prepare("SELECT * FROM users WHERE email_hash = ? LIMIT 1");
|
||||
$stmt->execute([$emailHash]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if (!$user || !password_verify($password, $user['password_hash'])) {
|
||||
@@ -63,7 +64,7 @@ json_success([
|
||||
'refresh_token' => $refreshToken,
|
||||
'user' => [
|
||||
'id' => $user['id'],
|
||||
'name' => $user['name'],
|
||||
'email' => $user['email']
|
||||
'name' => (App\Core\Encryption::decrypt($user['name']) ?: $user['name']),
|
||||
'email' => (App\Core\Encryption::decrypt($user['email']) ?: $user['email'])
|
||||
]
|
||||
], 'تم تسجيل الدخول بنجاح');
|
||||
|
||||
78
app/modules_app/companies/create.php
Normal file
78
app/modules_app/companies/create.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
/**
|
||||
* Create Company Endpoint (Synchronized Schema)
|
||||
*/
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Core\Encryption;
|
||||
use App\Core\Validator;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
|
||||
$decoded = AuthMiddleware::check();
|
||||
if ($decoded['role'] !== 'super_admin' && $decoded['role'] !== 'admin') {
|
||||
json_error('Unauthorized', 403);
|
||||
}
|
||||
|
||||
$data = input();
|
||||
|
||||
// 1. Validation
|
||||
$errors = Validator::validate($data, [
|
||||
'name' => 'required',
|
||||
'tax_identification_number' => 'required'
|
||||
]);
|
||||
|
||||
if ($errors) {
|
||||
json_error('Validation Failed', 422, $errors);
|
||||
}
|
||||
|
||||
$db = Database::getInstance();
|
||||
|
||||
try {
|
||||
$db->beginTransaction();
|
||||
|
||||
// 2. Encrypt sensitive fields
|
||||
$encryptedName = Encryption::encrypt($data['name']);
|
||||
$encryptedNameEn = !empty($data['name_en']) ? Encryption::encrypt($data['name_en']) : null;
|
||||
|
||||
// Encrypt JoFotara keys if provided
|
||||
$jofotaraClientId = !empty($data['jofotara_client_id']) ? Encryption::encrypt($data['jofotara_client_id']) : null;
|
||||
$jofotaraSecretKey = !empty($data['jofotara_secret_key']) ? Encryption::encrypt($data['jofotara_secret_key']) : null;
|
||||
|
||||
// 3. Save to Database
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO companies (
|
||||
tenant_id, name, name_en, tax_identification_number, commercial_registration_number,
|
||||
city, address, contact_email, contact_phone,
|
||||
jofotara_client_id_encrypted, jofotara_secret_key_encrypted,
|
||||
created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
$decoded['user_id'], // Using current admin as tenant_id
|
||||
$encryptedName,
|
||||
$encryptedNameEn,
|
||||
$data['tax_identification_number'],
|
||||
$data['commercial_registration_number'] ?? null,
|
||||
$data['city'] ?? null,
|
||||
$data['address'] ?? null,
|
||||
$data['contact_email'] ?? null,
|
||||
$data['contact_phone'] ?? null,
|
||||
$jofotaraClientId,
|
||||
$jofotaraSecretKey,
|
||||
date('Y-m-d H:i:s')
|
||||
]);
|
||||
|
||||
$companyId = $db->lastInsertId();
|
||||
|
||||
// 4. Pivot link
|
||||
$stmt = $db->prepare("INSERT INTO user_companies (user_id, company_id, role) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$decoded['user_id'], $companyId, 'admin']);
|
||||
|
||||
$db->commit();
|
||||
json_success(['id' => $companyId], 'تم إنشاء الشركة بنجاح');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$db->rollBack();
|
||||
json_error('حدث خطأ أثناء حفظ البيانات: ' . $e->getMessage(), 500);
|
||||
}
|
||||
46
app/modules_app/companies/index.php
Normal file
46
app/modules_app/companies/index.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
/**
|
||||
* List Companies Endpoint (Synchronized Schema)
|
||||
*/
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Core\Encryption;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
|
||||
$decoded = AuthMiddleware::check();
|
||||
$db = Database::getInstance();
|
||||
|
||||
// 1. Super Admin sees ALL companies
|
||||
if ($decoded['role'] === 'super_admin') {
|
||||
$stmt = $db->query("SELECT * FROM companies WHERE deleted_at IS NULL");
|
||||
} else {
|
||||
// 2. Others see only linked companies
|
||||
$stmt = $db->prepare("
|
||||
SELECT c.* FROM companies c
|
||||
JOIN user_companies uc ON c.id = uc.company_id
|
||||
WHERE uc.user_id = ? AND c.deleted_at IS NULL
|
||||
");
|
||||
$stmt->execute([$decoded['user_id']]);
|
||||
}
|
||||
|
||||
$companies = $stmt->fetchAll();
|
||||
|
||||
// 3. Decrypt fields
|
||||
foreach ($companies as &$company) {
|
||||
// Decrypt Name
|
||||
$decryptedName = Encryption::decrypt($company['name']);
|
||||
$company['name'] = $decryptedName !== false ? $decryptedName : $company['name'];
|
||||
|
||||
// Decrypt Name EN
|
||||
if (!empty($company['name_en'])) {
|
||||
$decryptedNameEn = Encryption::decrypt($company['name_en']);
|
||||
$company['name_en'] = $decryptedNameEn !== false ? $decryptedNameEn : $company['name_en'];
|
||||
}
|
||||
|
||||
// Redact JoFotara secrets if returned to UI (or just don't return them)
|
||||
unset($company['jofotara_client_id_encrypted']);
|
||||
unset($company['jofotara_secret_key_encrypted']);
|
||||
unset($company['certificate_password_encrypted']);
|
||||
}
|
||||
|
||||
json_success($companies);
|
||||
56
app/modules_app/users/create.php
Normal file
56
app/modules_app/users/create.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/**
|
||||
* Create User Endpoint (with Encryption)
|
||||
*/
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Core\Encryption;
|
||||
use App\Core\Validator;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
|
||||
// 1. Auth Check (Only super_admin or admin can create users)
|
||||
$decoded = AuthMiddleware::check();
|
||||
if ($decoded['role'] !== 'super_admin' && $decoded['role'] !== 'admin') {
|
||||
json_error('Unauthorized', 403);
|
||||
}
|
||||
|
||||
$data = input();
|
||||
|
||||
// 2. Validation
|
||||
$errors = Validator::validate($data, [
|
||||
'name' => 'required',
|
||||
'email' => 'required|email',
|
||||
'password' => 'required',
|
||||
'role' => 'required'
|
||||
]);
|
||||
|
||||
if ($errors) {
|
||||
json_error('Validation Failed', 422, $errors);
|
||||
}
|
||||
|
||||
$db = Database::getInstance();
|
||||
|
||||
// 3. Encrypt sensitive data
|
||||
$encryptedName = Encryption::encrypt($data['name']);
|
||||
$encryptedEmail = Encryption::encrypt($data['email']);
|
||||
$emailHash = hash('sha256', strtolower($data['email'])); // For fast lookup during login
|
||||
|
||||
// 4. Save to Database
|
||||
try {
|
||||
$stmt = $db->prepare("INSERT INTO users (name, email, email_hash, password_hash, role, created_at) VALUES (?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([
|
||||
$encryptedName,
|
||||
$encryptedEmail,
|
||||
$emailHash,
|
||||
password_hash($data['password'], PASSWORD_DEFAULT),
|
||||
$data['role'],
|
||||
date('Y-m-d H:i:s')
|
||||
]);
|
||||
|
||||
json_success(null, 'تم إضافة المستخدم بنجاح');
|
||||
} catch (\Exception $e) {
|
||||
if (str_contains($e->getMessage(), 'Duplicate entry')) {
|
||||
json_error('البريد الإلكتروني مسجل مسبقاً', 409);
|
||||
}
|
||||
json_error('حدث خطأ أثناء حفظ البيانات', 500);
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
<?php
|
||||
/**
|
||||
* Users List Endpoint
|
||||
* Users List Endpoint (with Decryption)
|
||||
*/
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Core\Encryption;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
|
||||
// 1. Auth Check
|
||||
@@ -20,4 +21,14 @@ $stmt = $db->prepare("SELECT id, name, email, role, is_active, created_at FROM u
|
||||
$stmt->execute();
|
||||
$users = $stmt->fetchAll();
|
||||
|
||||
// 4. Decrypt sensitive data for the UI
|
||||
foreach ($users as &$user) {
|
||||
// Try to decrypt. If it fails (e.g. data was plain text), keep original.
|
||||
$decryptedName = Encryption::decrypt($user['name']);
|
||||
$user['name'] = $decryptedName !== false ? $decryptedName : $user['name'];
|
||||
|
||||
$decryptedEmail = Encryption::decrypt($user['email']);
|
||||
$user['email'] = $decryptedEmail !== false ? $decryptedEmail : $user['email'];
|
||||
}
|
||||
|
||||
json_success($users);
|
||||
|
||||
@@ -20,6 +20,9 @@ $routes = [
|
||||
'v1/auth/refresh' => ['POST', 'auth/refresh.php'],
|
||||
'v1/auth/logout' => ['POST', 'auth/logout.php'],
|
||||
'v1/users' => ['GET', 'users/index.php'],
|
||||
'v1/users/create' => ['POST', 'users/create.php'],
|
||||
'v1/companies' => ['GET', 'companies/index.php'],
|
||||
'v1/companies/create' => ['POST', 'companies/create.php'],
|
||||
'v1/dashboard/stats' => ['GET', 'dashboard/stats.php'],
|
||||
];
|
||||
|
||||
|
||||
153
public/shell.php
153
public/shell.php
@@ -30,6 +30,7 @@
|
||||
</div>
|
||||
<nav class="flex-1 px-4 space-y-2">
|
||||
<a href="#" @click="page='dashboard'" class="block p-3 rounded hover:bg-gray-800" :class="page==='dashboard'?'bg-emerald-900/20 text-emerald-500':''">📊 لوحة التحكم</a>
|
||||
<a href="#" @click="page='companies'" class="block p-3 rounded hover:bg-gray-800" :class="page==='companies'?'bg-emerald-900/20 text-emerald-500':''">🏢 الشركات</a>
|
||||
<a href="#" @click="page='users'" class="block p-3 rounded hover:bg-gray-800" :class="page==='users'?'bg-emerald-900/20 text-emerald-500':''">👥 المستخدمون</a>
|
||||
</nav>
|
||||
<div class="p-6 border-t border-gray-800">
|
||||
@@ -41,7 +42,11 @@
|
||||
<main class="flex-1 overflow-y-auto p-10">
|
||||
<header class="mb-10 flex justify-between items-center">
|
||||
<h2 class="text-2xl font-bold" x-text="title()"></h2>
|
||||
<div class="flex items-center gap-4">
|
||||
<button x-show="page==='users'" @click="showAddModal = true" class="bg-emerald-600 hover:bg-emerald-500 px-4 py-2 rounded text-sm font-bold transition">➕ إضافة مستخدم</button>
|
||||
<button x-show="page==='companies'" @click="showAddCompanyModal = true" class="bg-emerald-600 hover:bg-emerald-500 px-4 py-2 rounded text-sm font-bold transition">➕ إضافة شركة</button>
|
||||
<div class="text-sm text-gray-500" x-text="user?.name"></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="content">
|
||||
@@ -63,6 +68,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Companies -->
|
||||
<div x-show="page === 'companies'">
|
||||
<div class="bg-surface border border-gray-800 rounded overflow-hidden">
|
||||
<table class="w-full text-right">
|
||||
<thead class="bg-gray-900/50">
|
||||
<tr>
|
||||
<th class="p-4">اسم الشركة</th>
|
||||
<th class="p-4">الرقم الضريبي</th>
|
||||
<th class="p-4">رقم التسجيل</th>
|
||||
<th class="p-4">تاريخ الإضافة</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="c in companies" :key="c.id">
|
||||
<tr class="border-t border-gray-800">
|
||||
<td class="p-4 font-bold text-emerald-500" x-text="c.name"></td>
|
||||
<td class="p-4" x-text="c.tax_number"></td>
|
||||
<td class="p-4" x-text="c.registration_number"></td>
|
||||
<td class="p-4 text-xs text-gray-500" x-text="c.created_at"></td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Users -->
|
||||
<div x-show="page === 'users'">
|
||||
<div class="bg-surface border border-gray-800 rounded overflow-hidden">
|
||||
@@ -79,7 +110,9 @@
|
||||
<tr class="border-t border-gray-800">
|
||||
<td class="p-4" x-text="u.name"></td>
|
||||
<td class="p-4" x-text="u.email"></td>
|
||||
<td class="p-4 text-xs uppercase text-gray-500" x-text="u.role"></td>
|
||||
<td class="p-4 text-xs uppercase text-gray-500">
|
||||
<span class="px-2 py-1 bg-gray-800 rounded" x-text="u.role"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
@@ -88,6 +121,68 @@
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Add User Modal -->
|
||||
<div x-show="showAddModal" class="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4 z-50" x-cloak>
|
||||
<div class="bg-surface border border-gray-800 w-full max-w-md p-8 rounded-lg shadow-2xl" @click.away="showAddModal = false">
|
||||
<h3 class="text-xl font-bold mb-6">إضافة مستخدم جديد 👤</h3>
|
||||
<form @submit.prevent="createUser" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 uppercase mb-1">الاسم الكامل</label>
|
||||
<input type="text" x-model="newUser.name" class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 uppercase mb-1">البريد الإلكتروني</label>
|
||||
<input type="email" x-model="newUser.email" class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 uppercase mb-1">كلمة المرور</label>
|
||||
<input type="password" x-model="newUser.password" class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 uppercase mb-1">الدور</label>
|
||||
<select x-model="newUser.role" class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500">
|
||||
<option value="employee">موظف</option>
|
||||
<option value="accountant">محاسب</option>
|
||||
<option value="admin">مدير نظام</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="pt-4 flex gap-3">
|
||||
<button type="submit" class="flex-1 bg-emerald-600 hover:bg-emerald-500 py-3 rounded font-bold transition">حفظ المستخدم</button>
|
||||
<button type="button" @click="showAddModal = false" class="px-6 py-3 border border-gray-800 rounded hover:bg-gray-800 transition">إلغاء</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Company Modal -->
|
||||
<div x-show="showAddCompanyModal" class="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4 z-50" x-cloak>
|
||||
<div class="bg-surface border border-gray-800 w-full max-w-md p-8 rounded-lg shadow-2xl" @click.away="showAddCompanyModal = false">
|
||||
<h3 class="text-xl font-bold mb-6">إنشاء شركة جديدة 🏢</h3>
|
||||
<form @submit.prevent="createCompany" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 uppercase mb-1">اسم الشركة</label>
|
||||
<input type="text" x-model="newCompany.name" class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 uppercase mb-1">الرقم الضريبي</label>
|
||||
<input type="text" x-model="newCompany.tax_number" class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 uppercase mb-1">رقم التسجيل</label>
|
||||
<input type="text" x-model="newCompany.registration_number" class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 uppercase mb-1">العنوان</label>
|
||||
<textarea x-model="newCompany.address" class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500"></textarea>
|
||||
</div>
|
||||
<div class="pt-4 flex gap-3">
|
||||
<button type="submit" class="flex-1 bg-emerald-600 hover:bg-emerald-500 py-3 rounded font-bold transition">حفظ الشركة</button>
|
||||
<button type="button" @click="showAddCompanyModal = false" class="px-6 py-3 border border-gray-800 rounded hover:bg-gray-800 transition">إلغاء</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -96,16 +191,22 @@
|
||||
user: JSON.parse(localStorage.getItem('user')),
|
||||
page: 'dashboard',
|
||||
users: [],
|
||||
companies: [],
|
||||
stats: { total: 0, pending: 0, approved: 0 },
|
||||
showAddModal: false,
|
||||
showAddCompanyModal: false,
|
||||
newUser: { name: '', email: '', password: '', role: 'employee' },
|
||||
newCompany: { name: '', tax_number: '', registration_number: '', address: '' },
|
||||
|
||||
init() {
|
||||
if (!this.user) window.location.href = '/login.php';
|
||||
this.loadUsers();
|
||||
this.loadStats();
|
||||
this.loadCompanies();
|
||||
},
|
||||
|
||||
title() {
|
||||
return { dashboard: 'لوحة التحكم', users: 'المستخدمون' }[this.page];
|
||||
return { dashboard: 'لوحة التحكم', users: 'المستخدمون', companies: 'الشركات' }[this.page];
|
||||
},
|
||||
|
||||
async loadUsers() {
|
||||
@@ -117,6 +218,14 @@
|
||||
if (json.success) this.users = json.data;
|
||||
},
|
||||
|
||||
async loadCompanies() {
|
||||
const res = await fetch('/api/v1/companies', {
|
||||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') }
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) this.companies = json.data;
|
||||
},
|
||||
|
||||
async loadStats() {
|
||||
const res = await fetch('/api/v1/dashboard/stats', {
|
||||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') }
|
||||
@@ -125,6 +234,46 @@
|
||||
if (json.success) this.stats = json.data;
|
||||
},
|
||||
|
||||
async createUser() {
|
||||
const res = await fetch('/api/v1/users/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('access_token')
|
||||
},
|
||||
body: JSON.stringify(this.newUser)
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
this.showAddModal = false;
|
||||
this.newUser = { name: '', email: '', password: '', role: 'employee' };
|
||||
this.loadUsers();
|
||||
alert('تم إضافة المستخدم بنجاح');
|
||||
} else {
|
||||
alert(json.message);
|
||||
}
|
||||
},
|
||||
|
||||
async createCompany() {
|
||||
const res = await fetch('/api/v1/companies/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('access_token')
|
||||
},
|
||||
body: JSON.stringify(this.newCompany)
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
this.showAddCompanyModal = false;
|
||||
this.newCompany = { name: '', tax_number: '', registration_number: '', address: '' };
|
||||
this.loadCompanies();
|
||||
alert('تم إنشاء الشركة بنجاح');
|
||||
} else {
|
||||
alert(json.message);
|
||||
}
|
||||
},
|
||||
|
||||
logout() {
|
||||
localStorage.clear();
|
||||
window.location.href = '/login.php';
|
||||
|
||||
53
public/tool-encrypt.php
Normal file
53
public/tool-encrypt.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
/**
|
||||
* Quick Encryption Tool
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../app/bootstrap/init.php';
|
||||
|
||||
use App\Core\Encryption;
|
||||
|
||||
$input = $_GET['text'] ?? '';
|
||||
$encrypted = $input ? Encryption::encrypt($input) : '';
|
||||
$hash = $input ? hash('sha256', strtolower($input)) : '';
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>أداة التشفير | مُصادَق</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="bg-slate-900 text-white p-10">
|
||||
<div class="max-w-2xl mx-auto bg-slate-800 p-8 rounded-lg shadow-xl">
|
||||
<h1 class="text-2xl font-bold mb-6 text-emerald-500">أداة تشفير البيانات</h1>
|
||||
|
||||
<form method="GET" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm text-slate-400 mb-1">النص المطلوب تشفيره (اسم أو بريد إلكتروني):</label>
|
||||
<input type="text" name="text" value="<?= htmlspecialchars($input) ?>" class="w-full bg-slate-900 border border-slate-700 p-3 rounded text-white outline-none focus:border-emerald-500">
|
||||
</div>
|
||||
<button type="submit" class="bg-emerald-600 hover:bg-emerald-500 px-6 py-2 rounded font-bold transition">تشفير الآن 🔒</button>
|
||||
</form>
|
||||
|
||||
<?php if ($encrypted): ?>
|
||||
<div class="mt-10 space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm text-slate-400 mb-1">النص المشفّر (للحفظ في عمود name أو email):</label>
|
||||
<textarea readonly class="w-full bg-slate-900 border border-slate-700 p-3 rounded text-emerald-400 font-mono text-xs h-24"><?= $encrypted ?></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm text-slate-400 mb-1">الـ Hash (للحفظ في عمود email_hash - للبريد فقط):</label>
|
||||
<input readonly type="text" value="<?= $hash ?>" class="w-full bg-slate-900 border border-slate-700 p-3 rounded text-emerald-400 font-mono text-xs">
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="mt-8 pt-6 border-t border-slate-700 text-sm text-slate-500">
|
||||
استخدم هذه القيم لتحديث مستخدمي النظام الحاليين يدوياً في قاعدة البيانات إذا أردت.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
105
scripts/migrate.php
Normal file
105
scripts/migrate.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
/**
|
||||
* Advanced Migration Script: Schema Update + Data Encryption
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../app/bootstrap/init.php';
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Core\Encryption;
|
||||
|
||||
$db = Database::getInstance();
|
||||
|
||||
echo "--- Starting Security Migration ---\n";
|
||||
|
||||
// 1. Add email_hash column if it doesn't exist
|
||||
try {
|
||||
$db->exec("ALTER TABLE users ADD COLUMN email_hash VARCHAR(64) AFTER email, ADD INDEX (email_hash)");
|
||||
echo "[OK] Added email_hash column and index.\n";
|
||||
} catch (\Exception $e) {
|
||||
echo "[SKIP] email_hash column might already exist.\n";
|
||||
}
|
||||
|
||||
// 2. Fetch all users to encrypt their data
|
||||
$stmt = $db->query("SELECT id, name, email FROM users");
|
||||
$users = $stmt->fetchAll();
|
||||
|
||||
echo "Found " . count($users) . " users. Starting encryption...\n";
|
||||
|
||||
$updateStmt = $db->prepare("UPDATE users SET name = ?, email = ?, email_hash = ? WHERE id = ?");
|
||||
|
||||
foreach ($users as $user) {
|
||||
// Check if data is already encrypted (to avoid double encryption)
|
||||
$isAlreadyEncrypted = Encryption::decrypt($user['email']) !== false;
|
||||
|
||||
if ($isAlreadyEncrypted) {
|
||||
echo "User ID {$user['id']} is already encrypted. Skipping.\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
// Encrypt Name
|
||||
$encryptedName = Encryption::encrypt($user['name']);
|
||||
|
||||
// Encrypt Email
|
||||
$encryptedEmail = Encryption::encrypt($user['email']);
|
||||
|
||||
// Generate Hash for lookup
|
||||
$emailHash = hash('sha256', strtolower($user['email']));
|
||||
|
||||
$updateStmt->execute([
|
||||
$encryptedName,
|
||||
$encryptedEmail,
|
||||
$emailHash,
|
||||
$user['id']
|
||||
]);
|
||||
|
||||
echo "User ID {$user['id']} migrated successfully.\n";
|
||||
}
|
||||
|
||||
// 3. Create companies table (Updated to match production schema)
|
||||
try {
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS companies (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
tenant_id INT,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
name_en VARCHAR(255),
|
||||
tax_identification_number VARCHAR(50),
|
||||
commercial_registration_number VARCHAR(50),
|
||||
address TEXT,
|
||||
city VARCHAR(100),
|
||||
contact_email VARCHAR(255),
|
||||
contact_phone VARCHAR(50),
|
||||
jofotara_client_id_encrypted TEXT,
|
||||
jofotara_secret_key_encrypted TEXT,
|
||||
jofotara_income_source_sequence VARCHAR(50),
|
||||
certificate_path VARCHAR(255),
|
||||
certificate_password_encrypted TEXT,
|
||||
is_jofotara_linked TINYINT(1) DEFAULT 0,
|
||||
is_active TINYINT(1) DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at DATETIME DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||
echo "[OK] Companies table synchronized with production schema.\n";
|
||||
} catch (\Exception $e) {
|
||||
echo "[ERROR] Synchronizing companies table: " . $e->getMessage() . "\n";
|
||||
}
|
||||
|
||||
// 4. Create user_companies pivot table
|
||||
try {
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS user_companies (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
company_id INT NOT NULL,
|
||||
role VARCHAR(50) DEFAULT 'employee',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY user_company (user_id, company_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||
echo "[OK] User_companies table created or exists.\n";
|
||||
} catch (\Exception $e) {
|
||||
echo "[ERROR] Creating user_companies table: " . $e->getMessage() . "\n";
|
||||
}
|
||||
|
||||
echo "--- Migration Complete ---\n";
|
||||
157
scripts/schema.sql
Normal file
157
scripts/schema.sql
Normal file
@@ -0,0 +1,157 @@
|
||||
-- ════════════════════════════════════════════════════════════
|
||||
-- مُصادَق — Database Schema v1.0
|
||||
-- ════════════════════════════════════════════════════════════
|
||||
|
||||
-- Tenants (Accounting Offices)
|
||||
CREATE TABLE tenants (
|
||||
id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
phone VARCHAR(20),
|
||||
status ENUM('active', 'suspended', 'trial') DEFAULT 'trial',
|
||||
trial_ends_at DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Users
|
||||
CREATE TABLE users (
|
||||
id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
|
||||
tenant_id CHAR(36) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
role ENUM('super_admin','admin','accountant','viewer') NOT NULL,
|
||||
company_id CHAR(36) NULL, -- assigned company for accountant
|
||||
refresh_token_hash VARCHAR(255) NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
last_login_at DATETIME NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uq_tenant_email (tenant_id, email),
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- API Keys (for external integrations / mobile scanner)
|
||||
CREATE TABLE api_keys (
|
||||
id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
|
||||
tenant_id CHAR(36) NOT NULL,
|
||||
user_id CHAR(36) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
public_key VARCHAR(64) NOT NULL UNIQUE,
|
||||
secret_hash VARCHAR(255) NOT NULL, -- bcrypt hash of secret
|
||||
last_used_at DATETIME NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Companies
|
||||
CREATE TABLE companies (
|
||||
id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
|
||||
tenant_id CHAR(36) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
name_en VARCHAR(255) NULL,
|
||||
tax_identification_number VARCHAR(20) NOT NULL,
|
||||
address TEXT NULL,
|
||||
jofotara_client_id_encrypted TEXT NULL,
|
||||
jofotara_secret_key_encrypted TEXT NULL,
|
||||
jofotara_income_source_sequence VARCHAR(50) NULL,
|
||||
certificate_path VARCHAR(255) NULL,
|
||||
certificate_password_encrypted VARCHAR(500) NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_name (name),
|
||||
INDEX idx_tin (tax_identification_number),
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Subscriptions
|
||||
CREATE TABLE subscriptions (
|
||||
id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
|
||||
tenant_id CHAR(36) NOT NULL UNIQUE,
|
||||
plan ENUM('basic','office','pro','enterprise') NOT NULL DEFAULT 'basic',
|
||||
max_companies INT NOT NULL DEFAULT 5,
|
||||
max_invoices_per_month INT NOT NULL DEFAULT 100,
|
||||
price_jod DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||
invoices_used_this_month INT NOT NULL DEFAULT 0,
|
||||
status ENUM('active','past_due','cancelled') DEFAULT 'active',
|
||||
current_period_start DATETIME NULL,
|
||||
current_period_end DATETIME NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Invoices
|
||||
CREATE TABLE invoices (
|
||||
id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
|
||||
tenant_id CHAR(36) NOT NULL,
|
||||
company_id CHAR(36) NOT NULL,
|
||||
invoice_number VARCHAR(100) NULL,
|
||||
invoice_date DATE NULL,
|
||||
invoice_type ENUM('cash','credit') DEFAULT 'cash',
|
||||
ubl_type_code CHAR(3) DEFAULT '388',
|
||||
payment_method_code CHAR(3) DEFAULT '013',
|
||||
supplier_tin VARCHAR(20) NULL,
|
||||
supplier_name VARCHAR(255) NULL,
|
||||
supplier_address TEXT NULL,
|
||||
buyer_tin VARCHAR(20) NULL,
|
||||
buyer_national_id VARCHAR(20) NULL,
|
||||
buyer_name VARCHAR(255) NULL,
|
||||
subtotal DECIMAL(15,3) DEFAULT 0,
|
||||
discount_total DECIMAL(15,3) DEFAULT 0,
|
||||
tax_amount DECIMAL(15,3) DEFAULT 0,
|
||||
grand_total DECIMAL(15,3) DEFAULT 0,
|
||||
currency_code CHAR(3) DEFAULT 'JOD',
|
||||
status ENUM('uploaded','extracting','extracted','validated','validation_failed','submitting','approved','rejected') DEFAULT 'uploaded',
|
||||
original_file_path TEXT NULL,
|
||||
invoice_category VARCHAR(20) DEFAULT 'simplified',
|
||||
validation_errors JSON NULL,
|
||||
qr_code TEXT NULL,
|
||||
ai_confidence_score DECIMAL(4,3) NULL,
|
||||
ai_prompt_tokens INT DEFAULT 0,
|
||||
ai_completion_tokens INT DEFAULT 0,
|
||||
ai_total_cost DECIMAL(10,6) DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_tenant (tenant_id),
|
||||
INDEX idx_company (company_id),
|
||||
INDEX idx_status (status),
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Invoice Lines
|
||||
CREATE TABLE invoice_lines (
|
||||
id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
|
||||
invoice_id CHAR(36) NOT NULL,
|
||||
line_number INT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
quantity DECIMAL(15,3) NOT NULL,
|
||||
unit_price DECIMAL(15,3) NOT NULL,
|
||||
discount DECIMAL(15,3) DEFAULT 0,
|
||||
tax_rate DECIMAL(5,4) NOT NULL,
|
||||
line_total DECIMAL(15,3) NOT NULL,
|
||||
FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Audit Logs
|
||||
CREATE TABLE audit_logs (
|
||||
id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
|
||||
tenant_id CHAR(36) NULL,
|
||||
user_id CHAR(36) NULL,
|
||||
action VARCHAR(100) NOT NULL,
|
||||
entity_type VARCHAR(50) NULL,
|
||||
entity_id CHAR(36) NULL,
|
||||
old_data JSON NULL,
|
||||
new_data JSON NULL,
|
||||
ip_address VARCHAR(45) NULL,
|
||||
user_agent VARCHAR(500) NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_tenant (tenant_id),
|
||||
INDEX idx_action (action),
|
||||
INDEX idx_created (created_at)
|
||||
);
|
||||
Reference in New Issue
Block a user