Update: 2026-05-03 23:57:27
This commit is contained in:
@@ -17,7 +17,10 @@ $data = input();
|
||||
|
||||
$errors = Validator::validate($data, [
|
||||
'name' => 'required',
|
||||
'email' => 'required|email'
|
||||
'email' => 'required|email',
|
||||
'manager_name' => 'required',
|
||||
'manager_email' => 'required|email',
|
||||
'manager_password' => 'required'
|
||||
]);
|
||||
|
||||
if ($errors) {
|
||||
@@ -27,14 +30,51 @@ if ($errors) {
|
||||
$db = Database::getInstance();
|
||||
|
||||
try {
|
||||
$stmt = $db->prepare("INSERT INTO tenants (name, email, phone, status, created_at) VALUES (?, ?, ?, 'active', NOW())");
|
||||
$db->beginTransaction();
|
||||
|
||||
// Generate Tenant UUID in PHP so we can use it immediately
|
||||
$tenantId = 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)
|
||||
);
|
||||
|
||||
// 1. Create Tenant
|
||||
$stmt = $db->prepare("INSERT INTO tenants (id, name, email, phone, status, created_at) VALUES (?, ?, ?, ?, 'active', NOW())");
|
||||
$stmt->execute([
|
||||
$tenantId,
|
||||
$data['name'],
|
||||
$data['email'],
|
||||
$data['phone'] ?? null
|
||||
]);
|
||||
|
||||
json_success(null, 'تم إنشاء المكتب بنجاح');
|
||||
// Generate User UUID
|
||||
$userId = 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)
|
||||
);
|
||||
|
||||
// Encrypt sensitive user data
|
||||
$encryptedName = \App\Core\Encryption::encrypt($data['manager_name']);
|
||||
$encryptedEmail = \App\Core\Encryption::encrypt($data['manager_email']);
|
||||
$emailHash = hash('sha256', strtolower($data['manager_email']));
|
||||
|
||||
// 2. Create Initial Manager (Admin) for this Tenant
|
||||
$stmtUser = $db->prepare("INSERT INTO users (id, tenant_id, name, email, email_hash, password_hash, role, created_at) VALUES (?, ?, ?, ?, ?, ?, 'admin', NOW())");
|
||||
$stmtUser->execute([
|
||||
$userId,
|
||||
$tenantId,
|
||||
$encryptedName,
|
||||
$encryptedEmail,
|
||||
$emailHash,
|
||||
password_hash($data['manager_password'], PASSWORD_DEFAULT)
|
||||
]);
|
||||
|
||||
$db->commit();
|
||||
json_success(null, 'تم إنشاء المكتب ومدير المكتب بنجاح');
|
||||
} catch (\Exception $e) {
|
||||
json_error('حدث خطأ أثناء حفظ البيانات', 500);
|
||||
$db->rollBack();
|
||||
json_error('حدث خطأ أثناء حفظ البيانات: ' . $e->getMessage(), 500);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,11 +46,22 @@ $encryptedName = Encryption::encrypt($data['name']);
|
||||
$encryptedEmail = Encryption::encrypt($data['email']);
|
||||
$emailHash = hash('sha256', strtolower($data['email'])); // For fast lookup during login
|
||||
|
||||
// 3. Determine Tenant ID
|
||||
$tenantId = null;
|
||||
if ($decoded['role'] === 'super_admin') {
|
||||
if (empty($data['tenant_id'])) {
|
||||
json_error('يجب اختيار المكتب المحاسبي', 422);
|
||||
}
|
||||
$tenantId = $data['tenant_id'];
|
||||
} else {
|
||||
$tenantId = $decoded['tenant_id'];
|
||||
}
|
||||
|
||||
// 4. Save to Database
|
||||
try {
|
||||
$stmt = $db->prepare("INSERT INTO users (tenant_id, name, email, email_hash, password_hash, role, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([
|
||||
$decoded['tenant_id'],
|
||||
$tenantId,
|
||||
$encryptedName,
|
||||
$encryptedEmail,
|
||||
$emailHash,
|
||||
|
||||
@@ -153,6 +153,15 @@
|
||||
<option value="admin">مدير نظام</option>
|
||||
</select>
|
||||
</div>
|
||||
<div x-show="user?.role === 'super_admin'">
|
||||
<label class="block text-xs text-gray-500 uppercase mb-1">تعيين لمكتب (للسوبر أدمن فقط)</label>
|
||||
<select x-model="newUser.tenant_id" class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500">
|
||||
<option value="">-- اختر المكتب --</option>
|
||||
<template x-for="t in tenants" :key="t.id">
|
||||
<option :value="t.id" x-text="t.name"></option>
|
||||
</template>
|
||||
</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>
|
||||
@@ -178,6 +187,20 @@
|
||||
<label class="block text-xs text-gray-500 uppercase mb-1">الهاتف</label>
|
||||
<input type="text" x-model="newTenant.phone" class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500">
|
||||
</div>
|
||||
<hr class="border-gray-800 my-4">
|
||||
<h4 class="text-sm font-bold text-gray-400">معلومات مدير المكتب (Admin)</h4>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 uppercase mb-1">اسم المدير</label>
|
||||
<input type="text" x-model="newTenant.manager_name" required 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="email" x-model="newTenant.manager_email" required 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="password" x-model="newTenant.manager_password" required class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500">
|
||||
</div>
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button type="submit" class="flex-1 bg-emerald-600 hover:bg-emerald-500 p-3 rounded font-bold transition">حفظ</button>
|
||||
<button type="button" @click="showAddTenantModal = false" class="flex-1 bg-gray-800 hover:bg-gray-700 p-3 rounded transition">إلغاء</button>
|
||||
@@ -223,17 +246,22 @@
|
||||
page: 'dashboard',
|
||||
users: [],
|
||||
companies: [],
|
||||
tenants: [],
|
||||
stats: { total: 0, pending: 0, approved: 0 },
|
||||
showAddModal: false,
|
||||
showAddCompanyModal: false,
|
||||
newUser: { name: '', email: '', password: '', role: 'employee' },
|
||||
newUser: { name: '', email: '', password: '', role: 'employee', tenant_id: '' },
|
||||
newCompany: { name: '', tax_identification_number: '', commercial_registration_number: '', address: '' },
|
||||
newTenant: { name: '', email: '', phone: '', manager_name: '', manager_email: '', manager_password: '' },
|
||||
|
||||
init() {
|
||||
if (!this.user) window.location.href = '/login.php';
|
||||
this.loadUsers();
|
||||
this.loadStats();
|
||||
this.loadCompanies();
|
||||
if (this.user.role === 'super_admin') {
|
||||
this.loadTenants();
|
||||
}
|
||||
},
|
||||
|
||||
title() {
|
||||
@@ -277,7 +305,7 @@
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
this.showAddModal = false;
|
||||
this.newUser = { name: '', email: '', password: '', role: 'employee' };
|
||||
this.newUser = { name: '', email: '', password: '', role: 'employee', tenant_id: '' };
|
||||
this.loadUsers();
|
||||
alert('تم إضافة المستخدم بنجاح');
|
||||
} else {
|
||||
@@ -305,6 +333,34 @@
|
||||
}
|
||||
},
|
||||
|
||||
async loadTenants() {
|
||||
const res = await fetch('/api/v1/tenants', {
|
||||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('access_token') }
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) this.tenants = json.data;
|
||||
},
|
||||
|
||||
async createTenant() {
|
||||
const res = await fetch('/api/v1/tenants/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('access_token')
|
||||
},
|
||||
body: JSON.stringify(this.newTenant)
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
this.showAddTenantModal = false;
|
||||
this.newTenant = { name: '', email: '', phone: '', manager_name: '', manager_email: '', manager_password: '' };
|
||||
this.loadTenants();
|
||||
alert('تم إنشاء المكتب وتعيين المدير بنجاح!');
|
||||
} else {
|
||||
alert(json.message);
|
||||
}
|
||||
},
|
||||
|
||||
logout() {
|
||||
localStorage.clear();
|
||||
window.location.href = '/login.php';
|
||||
|
||||
311
scripts/PROJECT_DOCUMENTATION.md
Normal file
311
scripts/PROJECT_DOCUMENTATION.md
Normal file
@@ -0,0 +1,311 @@
|
||||
# Musadaq Project Documentation
|
||||
|
||||
This file contains the complete source code of the project (excluding dependencies and sensitive data).
|
||||
|
||||
## File: `fix_data.php`
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Data Reset & Fix Script
|
||||
* Clears corrupted company/assignment data but keeps Users and Tenants.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../app/bootstrap/init.php';
|
||||
|
||||
use App\Core\Database;
|
||||
|
||||
$db = Database::getInstance();
|
||||
|
||||
echo "--- Starting Data Reset ---\n";
|
||||
|
||||
try {
|
||||
$db->beginTransaction();
|
||||
|
||||
// 1. Clear corrupted data tables
|
||||
$db->exec("SET FOREIGN_KEY_CHECKS = 0");
|
||||
$db->exec("TRUNCATE TABLE user_company_assignments");
|
||||
$db->exec("TRUNCATE TABLE invoices");
|
||||
$db->exec("TRUNCATE TABLE companies");
|
||||
$db->exec("SET FOREIGN_KEY_CHECKS = 1");
|
||||
echo "[OK] Cleared companies, invoices, and assignments.\n";
|
||||
|
||||
// 2. Ensure Super Admin does not have a tenant_id (if your schema allows NULL, else set to empty string)
|
||||
// Actually, schema.sql says tenant_id CHAR(36) NOT NULL.
|
||||
// This is a flaw in schema.sql for Super Admins. We will leave users alone for now.
|
||||
|
||||
// 3. Fix the admin's tenant_id to match the first available tenant
|
||||
$stmt = $db->query("SELECT id FROM tenants LIMIT 1");
|
||||
$tenantId = $stmt->fetchColumn();
|
||||
|
||||
if ($tenantId) {
|
||||
$db->exec("UPDATE users SET tenant_id = '$tenantId' WHERE role != 'super_admin'");
|
||||
echo "[OK] Linked all non-super-admin users to Tenant ID: $tenantId\n";
|
||||
}
|
||||
|
||||
$db->commit();
|
||||
echo "--- Reset Complete ---\n";
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$db->rollBack();
|
||||
echo "[ERROR] Reset failed: " . $e->getMessage() . "\n";
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## File: `schema.sql`
|
||||
|
||||
```sql
|
||||
-- ════════════════════════════════════════════════════════════
|
||||
-- مُصادَق — 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)
|
||||
);
|
||||
```
|
||||
|
||||
## File: `migrate.php`
|
||||
|
||||
```php
|
||||
<?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";
|
||||
}
|
||||
|
||||
// 4. Create user_company_assignments table
|
||||
try {
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS user_company_assignments (
|
||||
id INT AUTO_INCREMENT,
|
||||
user_id VARCHAR(100) NOT NULL,
|
||||
company_id VARCHAR(100) NOT NULL,
|
||||
assigned_by VARCHAR(100) NOT NULL,
|
||||
assigned_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_user_company (user_id, company_id),
|
||||
CONSTRAINT fk_uca_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_uca_company FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_uca_admin FOREIGN KEY (assigned_by) REFERENCES users(id) ON DELETE RESTRICT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||
echo "[OK] User_company_assignments table created.\n";
|
||||
} catch (\Exception $e) {
|
||||
echo "[SKIP] user_company_assignments table: " . $e->getMessage() . "\n";
|
||||
}
|
||||
|
||||
// 5. Update invoices table to include uploaded_by
|
||||
try {
|
||||
$db->exec("ALTER TABLE invoices ADD COLUMN uploaded_by VARCHAR(100) NULL AFTER status");
|
||||
$db->exec("ALTER TABLE invoices ADD CONSTRAINT fk_inv_uploader FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL");
|
||||
echo "[OK] Updated invoices table with uploaded_by tracker.\n";
|
||||
} catch (\Exception $e) {
|
||||
echo "[SKIP] invoices table update: " . $e->getMessage() . "\n";
|
||||
}
|
||||
|
||||
echo "--- Migration Complete ---\n";
|
||||
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user