# DATABASE_DESIGN.md — Speech2Text MBIP ## Senarai Jadual 1. `users` 2. `departments` 3. `transcription_projects` 4. `project_collaborators` 5. `transcript_versions` 6. `project_comments` 7. `audit_logs` 8. `sessions` 9. `jobs` / `failed_jobs` 10. `cache` --- ## Skema Jadual ### 1. `users` ```sql CREATE TABLE users ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, role ENUM('admin', 'user') NOT NULL DEFAULT 'user', department_id BIGINT UNSIGNED NULL, is_active TINYINT(1) NOT NULL DEFAULT 1, last_login_at TIMESTAMP NULL, email_verified_at TIMESTAMP NULL, remember_token VARCHAR(100) NULL, created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL, deleted_at TIMESTAMP NULL, -- soft delete FOREIGN KEY (department_id) REFERENCES departments(id) ON DELETE SET NULL ); ``` **Catatan:** - `deleted_at` hanya digunakan jika pengguna belum pernah guna aplikasi (tiada projek, tiada audit). - Jika pernah guna, hanya `is_active = 0` (deactivate), jangan hard delete. - `role` enum mudah diurus; boleh upgrade ke `spatie/laravel-permission` kemudian jika perlu. --- ### 2. `departments` ```sql CREATE TABLE departments ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, code VARCHAR(50) NULL UNIQUE, is_active TINYINT(1) NOT NULL DEFAULT 1, created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL ); ``` --- ### 3. `transcription_projects` ```sql CREATE TABLE transcription_projects ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, uuid CHAR(36) NOT NULL UNIQUE, -- digunakan dalam URL title VARCHAR(255) NOT NULL, description TEXT NULL, owner_user_id BIGINT UNSIGNED NOT NULL, original_filename VARCHAR(500) NOT NULL, stored_audio_path VARCHAR(1000) NOT NULL, -- path relatif dalam private disk mime_type VARCHAR(100) NOT NULL, file_size BIGINT UNSIGNED NOT NULL, -- bytes duration_seconds INT UNSIGNED NULL, language VARCHAR(10) NOT NULL DEFAULT 'ms', transcription_status ENUM('pending','processing','completed','failed') NOT NULL DEFAULT 'pending', transcription_engine VARCHAR(50) NULL, -- e.g. 'faster-whisper' transcript_text LONGTEXT NULL, -- kandungan sensitif transcript_confidence DECIMAL(5,4) NULL, -- 0.0000 - 1.0000 error_message TEXT NULL, processed_at TIMESTAMP NULL, created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL, deleted_at TIMESTAMP NULL, -- soft delete FOREIGN KEY (owner_user_id) REFERENCES users(id) ON DELETE RESTRICT ); ``` **Catatan keselamatan:** - `stored_audio_path` adalah path dalam private storage, bukan URL awam. - `transcript_text` disimpan dalam database. Untuk keselamatan lanjut, boleh encrypt menggunakan Laravel `encrypted` cast. - Admin **tidak** boleh SELECT `transcript_text`, `stored_audio_path` melalui policy/query scope. --- ### 4. `project_collaborators` ```sql CREATE TABLE project_collaborators ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, project_id BIGINT UNSIGNED NOT NULL, user_id BIGINT UNSIGNED NOT NULL, role ENUM('editor', 'viewer') NOT NULL DEFAULT 'editor', added_by BIGINT UNSIGNED NOT NULL, created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL, UNIQUE KEY unique_project_user (project_id, user_id), FOREIGN KEY (project_id) REFERENCES transcription_projects(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (added_by) REFERENCES users(id) ON DELETE RESTRICT ); ``` --- ### 5. `transcript_versions` ```sql CREATE TABLE transcript_versions ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, project_id BIGINT UNSIGNED NOT NULL, edited_by BIGINT UNSIGNED NOT NULL, version_number INT UNSIGNED NOT NULL, old_text LONGTEXT NULL, new_text LONGTEXT NOT NULL, change_summary VARCHAR(500) NULL, created_at TIMESTAMP NULL, FOREIGN KEY (project_id) REFERENCES transcription_projects(id) ON DELETE CASCADE, FOREIGN KEY (edited_by) REFERENCES users(id) ON DELETE RESTRICT ); ``` **Catatan:** - `old_text` dan `new_text` adalah snapshot penuh, bukan diff, untuk kemudahan restore. - Jangan masukkan `transcript_text` dalam `audit_logs`; gunakan jadual ini sebagai ganti. - Admin tidak boleh akses jadual ini. --- ### 6. `project_comments` ```sql CREATE TABLE project_comments ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, project_id BIGINT UNSIGNED NOT NULL, user_id BIGINT UNSIGNED NOT NULL, message TEXT NOT NULL, created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL, deleted_at TIMESTAMP NULL, -- soft delete FOREIGN KEY (project_id) REFERENCES transcription_projects(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT ); ``` --- ### 7. `audit_logs` ```sql CREATE TABLE audit_logs ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, actor_user_id BIGINT UNSIGNED NULL, -- NULL jika sistem/background job actor_role VARCHAR(50) NULL, action VARCHAR(100) NOT NULL, -- e.g. 'user_deactivated' subject_type VARCHAR(100) NULL, -- e.g. 'App\Models\User' subject_id BIGINT UNSIGNED NULL, target_user_id BIGINT UNSIGNED NULL, project_id BIGINT UNSIGNED NULL, old_values JSON NULL, -- JANGAN masukkan transcript content new_values JSON NULL, -- JANGAN masukkan transcript content justification TEXT NULL, ip_address VARCHAR(45) NULL, -- support IPv6 user_agent TEXT NULL, created_at TIMESTAMP NULL, INDEX idx_actor (actor_user_id), INDEX idx_action (action), INDEX idx_project (project_id), INDEX idx_created (created_at) ); ``` **Tindakan yang diaudit:** | action | Penerangan | |---|---| | `user_created` | Admin daftar pengguna baru | | `user_deactivated` | Admin deactivate pengguna | | `user_reactivated` | Admin aktifkan semula pengguna | | `user_deleted` | Admin delete pengguna (hanya jika belum guna) | | `user_email_changed` | Admin tukar emel pengguna | | `project_created` | Pengguna cipta projek | | `audio_uploaded` | Pengguna muat naik audio | | `transcription_started` | Queue worker mula proses | | `transcription_completed` | Queue worker selesai | | `transcription_failed` | Queue worker gagal | | `transcript_updated` | Owner/collaborator edit teks | | `transcript_version_restored` | Restore versi lama | | `collaborator_added` | Owner tambah collaborator | | `collaborator_removed` | Owner buang collaborator | | `comment_created` | Pengguna buat komen | | `project_deleted` | Owner delete projek | | `project_owner_transferred` | Admin transfer ownership | --- ## Hubungan Model (Eloquent Relationships) ``` User ├── hasMany: TranscriptionProject (as owner) ├── belongsToMany: TranscriptionProject (through ProjectCollaborator) ├── hasMany: TranscriptVersion (as editor) ├── hasMany: ProjectComment ├── belongsTo: Department └── hasMany: AuditLog (as actor) TranscriptionProject ├── belongsTo: User (owner) ├── hasMany: ProjectCollaborator ├── hasMany: TranscriptVersion ├── hasMany: ProjectComment └── belongsToMany: User (collaborators) Department └── hasMany: User ``` --- ## Indeks Penting ```sql -- Cari projek mengikut status (untuk admin dashboard) ALTER TABLE transcription_projects ADD INDEX idx_status (transcription_status); -- Cari projek mengikut owner ALTER TABLE transcription_projects ADD INDEX idx_owner (owner_user_id); -- Cari versi mengikut projek (timeline) ALTER TABLE transcript_versions ADD INDEX idx_project_version (project_id, version_number); -- Audit log search ALTER TABLE audit_logs ADD INDEX idx_target_user (target_user_id); ALTER TABLE audit_logs ADD INDEX idx_subject (subject_type, subject_id); ``` --- ## Nota Keselamatan Data 1. **`transcript_text`** — Kolum sensitif. Boleh encrypt menggunakan Laravel cast `encrypted`: ```php protected $casts = [ 'transcript_text' => 'encrypted', ]; ``` Ini encrypt menggunakan `APP_KEY`. Pastikan `APP_KEY` disimpan dengan selamat. 2. **`stored_audio_path`** — Simpan path relatif sahaja, bukan absolute path. Contoh: `transcriptions/abc-uuid/audio/recording.mp3`. 3. **Audit log** — Jangan masukkan `transcript_text` dalam `old_values` atau `new_values`. Gunakan `transcript_versions` untuk simpan snapshot teks. 4. **Soft delete** — `transcription_projects` dan `project_comments` menggunakan soft delete. Fail audio fizikal dikekalkan dalam private storage sehingga admin jalankan retention cleanup.