# ANATL WEBSITE — PRODUCT REQUIREMENTS DOCUMENT
> Stack: PHP 8 prosedural · MySQL 8 · HTML5 · CSS3 vanilla  
> Versi 1.2 | Mei 2025

---

## 1. KONTEKS SISTEM

Website portal resmi ANATL (Autoridade Nacional de Aviação Civil e Transportes Aéreos de Timor-Leste). Satu aplikasi PHP monolitik yang melayani 6 jenis pengguna dengan role berbeda. Tidak ada framework — murni PHP prosedural, HTML, CSS vanilla, MySQL.

### 1.1 Role Pengguna

| Role Konstanta | Deskripsi | Login |
|---|---|---|
| `guest` | Penumpang & masyarakat umum | Tidak |
| `admin` | Staf internal ANATL | Ya |
| `airline` | Operator maskapai | Ya |
| `customs` | Petugas Bea Cukai & Imigrasi | Ya |
| `ground_handler` | Vendor ground handling | Ya |
| `tenant` | Penyewa unit bisnis bandara | Ya |

### 1.2 Prinsip Arsitektur

- Setiap halaman = satu file `.php`
- Proses POST ditangani di bagian atas file yang sama (before HTML output)
- Session PHP native untuk auth — `$_SESSION['user_id']`, `$_SESSION['role']`, `$_SESSION['lang']`
- Semua query wajib prepared statement via `mysqli`
- Output HTML wajib di-escape dengan `htmlspecialchars($v, ENT_QUOTES, 'UTF-8')`

---

## 2. STRUKTUR FILE & FOLDER

```
/anatl/
│
├── config.php                    # Konstanta DB, BASE_URL, UPLOAD_PATH
├── index.php                     # Homepage + FIDS
├── login.php                     # Form login + proses auth
├── logout.php                    # Destroy session + redirect
├── penerbangan.php               # Jadwal penerbangan publik
├── fasilitas.php                 # Info fasilitas bandara
├── panduan.php                   # Panduan penumpang
├── kepabeanan.php                # Info visa & barang bawaan
├── tenant-unit.php               # Galeri unit sewa (publik)
├── laporan.php                   # Form laporan fasilitas (publik)
│
├── maskapai/
│   ├── index.php                 # Dashboard maskapai
│   ├── slot.php                  # Pengajuan + list slot time
│   ├── flight-plan.php           # Pengajuan + list flight plan
│   └── safety.php                # Safety reporting
│
├── ground-handling/
│   ├── index.php                 # Dashboard ground handler
│   ├── notif.php                 # List penerbangan non-reguler
│   └── invoice.php               # List + detail invoice
│
├── tenant/
│   ├── index.php                 # Dashboard tenant
│   ├── pengajuan.php             # Form + tracking pengajuan sewa
│   ├── kontrak.php               # List + download kontrak
│   └── maintenance.php           # Form + riwayat laporan maintenance
│
├── customs/
│   ├── index.php                 # Dashboard bea cukai
│   ├── visa.php                  # Editor konten visa
│   ├── barang.php                # Editor konten barang bawaan
│   └── deklarasi.php             # Upload formulir deklarasi
│
├── admin/
│   ├── index.php                 # Dashboard admin + statistik
│   ├── users.php                 # Manajemen user
│   ├── slot.php                  # Approval slot time
│   ├── flight-plan.php           # Approval flight plan
│   ├── tenant.php                # Approval + upload kontrak
│   ├── ground-handling.php       # Input non-reguler + generate invoice
│   └── cms.php                   # CMS berita, pengumuman, NOTAM
│
├── includes/
│   ├── config.php                # (sama dengan /config.php, di-require dari sini)
│   ├── db.php                    # Buka koneksi $conn (mysqli)
│   ├── auth.php                  # Fungsi: require_login($role), get_user()
│   ├── functions.php             # Helper: esc(), flash(), csrf_token(), send_email()
│   ├── upload.php                # Helper: handle_upload($field, $subfolder)
│   ├── header.php                # <!DOCTYPE> + <head> + navbar
│   ├── footer.php                # </body></html> + scripts
│   ├── sidebar.php               # Sidebar portal (terima $menu array)
│   └── lang/
│       ├── id.php                # $t[] array Bahasa Indonesia
│       ├── en.php                # $t[] array English
│       ├── pt.php                # $t[] array Português
│       └── tet.php               # $t[] array Tetum
│
├── components/
│   ├── badge-status.php          # <?php badge_status($status) ?>
│   ├── timeline.php              # <?php timeline_status($status, $steps[]) ?>
│   ├── pagination.php            # <?php pagination($total, $per_page, $page) ?>
│   ├── stat-card.php             # <?php stat_card($label, $value, $icon) ?>
│   ├── empty-state.php           # <?php empty_state($message) ?>
│   └── modal-confirm.php         # Dialog konfirmasi JS-less
│
├── uploads/
│   ├── .htaccess                 # Deny from all
│   ├── slot/
│   ├── flight-plan/
│   ├── safety/
│   ├── service-report/
│   ├── invoice-pdf/
│   ├── tenant-docs/
│   ├── contracts/
│   └── customs/
│
└── assets/
    ├── css/
    │   └── style.css             # Satu file CSS global
    └── img/
        └── logo.png
```

---

## 3. DATABASE SCHEMA

### 3.1 Tabel `users`
```sql
id              INT AUTO_INCREMENT PRIMARY KEY
username        VARCHAR(80) NOT NULL UNIQUE
email           VARCHAR(150) NOT NULL UNIQUE
password_hash   VARCHAR(255) NOT NULL
role            ENUM('admin','airline','customs','ground_handler','tenant') NOT NULL
company_name    VARCHAR(150)          -- nama maskapai / perusahaan
is_active       TINYINT(1) DEFAULT 1
created_at      DATETIME DEFAULT CURRENT_TIMESTAMP
```

### 3.2 Tabel `flights` (FIDS)
```sql
id              INT AUTO_INCREMENT PRIMARY KEY
flight_no       VARCHAR(20) NOT NULL
airline         VARCHAR(100)
origin          VARCHAR(100)
destination     VARCHAR(100)
std             DATETIME          -- scheduled departure
sta             DATETIME          -- scheduled arrival
etd             DATETIME          -- estimated departure
eta             DATETIME          -- estimated arrival
status          ENUM('scheduled','boarding','departed','arrived','delayed','cancelled') DEFAULT 'scheduled'
terminal        VARCHAR(10)
gate            VARCHAR(10)
updated_at      DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
```

### 3.3 Tabel `facility_reports`
```sql
id              INT AUTO_INCREMENT PRIMARY KEY
location        VARCHAR(200) NOT NULL
description     TEXT NOT NULL
photo_path      VARCHAR(255)
reporter_email  VARCHAR(150)
status          ENUM('open','in_progress','resolved') DEFAULT 'open'
created_at      DATETIME DEFAULT CURRENT_TIMESTAMP
```

### 3.4 Tabel `flight_subscribers`
```sql
id              INT AUTO_INCREMENT PRIMARY KEY
email           VARCHAR(150) NOT NULL
flight_no       VARCHAR(20) NOT NULL
created_at      DATETIME DEFAULT CURRENT_TIMESTAMP
```

### 3.5 Tabel `slot_requests`
```sql
id              INT AUTO_INCREMENT PRIMARY KEY
user_id         INT NOT NULL  REFERENCES users(id)
type            ENUM('arrival','departure') NOT NULL
flight_no       VARCHAR(20) NOT NULL
flight_date     DATE NOT NULL
requested_time  TIME NOT NULL
aircraft_type   VARCHAR(50)
doc_path        VARCHAR(255)
status          ENUM('submitted','review','approved','rejected') DEFAULT 'submitted'
admin_notes     TEXT
created_at      DATETIME DEFAULT CURRENT_TIMESTAMP
updated_at      DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
```

### 3.6 Tabel `flight_plans`
```sql
id              INT AUTO_INCREMENT PRIMARY KEY
user_id         INT NOT NULL  REFERENCES users(id)
flight_no       VARCHAR(20) NOT NULL
route           VARCHAR(255)
etd             DATETIME NOT NULL
eta             DATETIME NOT NULL
aircraft_reg    VARCHAR(20)
doc_path        VARCHAR(255)
status          ENUM('submitted','review','approved','rejected') DEFAULT 'submitted'
admin_notes     TEXT
created_at      DATETIME DEFAULT CURRENT_TIMESTAMP
updated_at      DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
```

### 3.7 Tabel `safety_reports`
```sql
id              INT AUTO_INCREMENT PRIMARY KEY
user_id         INT          REFERENCES users(id)   -- NULL jika anonim
incident_date   DATE NOT NULL
category        ENUM('bird_strike','ground_damage','fuel_spill','near_miss','other')
description     TEXT NOT NULL
location        VARCHAR(200)
is_anonymous    TINYINT(1) DEFAULT 0
doc_path        VARCHAR(255)
status          ENUM('submitted','under_review','closed') DEFAULT 'submitted'
admin_notes     TEXT
created_at      DATETIME DEFAULT CURRENT_TIMESTAMP
updated_at      DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
```

### 3.8 Tabel `customs_content`
```sql
id              INT AUTO_INCREMENT PRIMARY KEY
section         ENUM('visa','allowed_goods','restricted_goods','prohibited_goods','declaration') NOT NULL
lang            ENUM('id','en','pt','tet') NOT NULL
content         LONGTEXT
updated_by      INT  REFERENCES users(id)
updated_at      DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
UNIQUE KEY      uq_section_lang (section, lang)
```

### 3.9 Tabel `irregular_flights`
```sql
id              INT AUTO_INCREMENT PRIMARY KEY
flight_no       VARCHAR(20) NOT NULL
airline         VARCHAR(100)
arrival_dt      DATETIME NOT NULL
origin          VARCHAR(100)
aircraft_type   VARCHAR(50)
pax_count       INT DEFAULT 0
notes           TEXT
created_by      INT  REFERENCES users(id)
created_at      DATETIME DEFAULT CURRENT_TIMESTAMP
```

### 3.10 Tabel `service_reports`
```sql
id              INT AUTO_INCREMENT PRIMARY KEY
flight_id       INT NOT NULL  REFERENCES irregular_flights(id)
vendor_id       INT NOT NULL  REFERENCES users(id)
report_path     VARCHAR(255)
submitted_at    DATETIME DEFAULT CURRENT_TIMESTAMP
```

### 3.11 Tabel `invoices`
```sql
id              INT AUTO_INCREMENT PRIMARY KEY
flight_id       INT NOT NULL  REFERENCES irregular_flights(id)
vendor_id       INT NOT NULL  REFERENCES users(id)
amount          DECIMAL(15,2) NOT NULL
due_date        DATE NOT NULL
status          ENUM('unpaid','paid') DEFAULT 'unpaid'
pdf_path        VARCHAR(255)
confirmed_at    DATETIME
created_at      DATETIME DEFAULT CURRENT_TIMESTAMP
```

### 3.12 Tabel `rental_units`
```sql
id              INT AUTO_INCREMENT PRIMARY KEY
name            VARCHAR(150) NOT NULL
location        VARCHAR(200)
area_m2         DECIMAL(8,2)
price_monthly   DECIMAL(15,2)
description     TEXT
photo_path      VARCHAR(255)
is_available    TINYINT(1) DEFAULT 1
```

### 3.13 Tabel `rental_applications`
```sql
id              INT AUTO_INCREMENT PRIMARY KEY
user_id         INT NOT NULL  REFERENCES users(id)
unit_id         INT NOT NULL  REFERENCES rental_units(id)
business_name   VARCHAR(150) NOT NULL
business_type   VARCHAR(100)
duration_months INT NOT NULL
doc_path        VARCHAR(255)
status          ENUM('submitted','review','site_visit','approved','rejected','contract') DEFAULT 'submitted'
admin_notes     TEXT
created_at      DATETIME DEFAULT CURRENT_TIMESTAMP
updated_at      DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
```

### 3.14 Tabel `rental_contracts`
```sql
id              INT AUTO_INCREMENT PRIMARY KEY
application_id  INT NOT NULL  REFERENCES rental_applications(id)
contract_path   VARCHAR(255)
start_date      DATE
end_date        DATE
created_at      DATETIME DEFAULT CURRENT_TIMESTAMP
```

### 3.15 Tabel `maintenance_requests`
```sql
id              INT AUTO_INCREMENT PRIMARY KEY
user_id         INT NOT NULL  REFERENCES users(id)
unit_id         INT NOT NULL  REFERENCES rental_units(id)
description     TEXT NOT NULL
photo_path      VARCHAR(255)
status          ENUM('open','in_progress','resolved') DEFAULT 'open'
created_at      DATETIME DEFAULT CURRENT_TIMESTAMP
updated_at      DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
```

### 3.16 Tabel `cms_posts`
```sql
id              INT AUTO_INCREMENT PRIMARY KEY
type            ENUM('news','announcement','notam') NOT NULL
lang            ENUM('id','en','pt','tet') DEFAULT 'id'
title           VARCHAR(255) NOT NULL
content         LONGTEXT NOT NULL
published       TINYINT(1) DEFAULT 0
created_by      INT  REFERENCES users(id)
created_at      DATETIME DEFAULT CURRENT_TIMESTAMP
updated_at      DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
```

---

## 4. DESIGN TOKENS (CSS :root)

```css
:root {
  /* Brand */
  --c-primary:        #003A70;
  --c-primary-hover:  #005BAA;
  --c-accent:         #E8A020;
  --c-accent-hover:   #C8880A;

  /* Neutral */
  --c-bg:             #F4F6F9;
  --c-surface:        #FFFFFF;
  --c-border:         #D1D9E0;
  --c-text:           #1A2332;
  --c-muted:          #5A6A7A;

  /* Status */
  --c-submitted:      #6B7280;
  --c-review:         #D97706;
  --c-approved:       #059669;
  --c-rejected:       #DC2626;
  --c-paid:           #059669;
  --c-unpaid:         #DC2626;

  /* Typography */
  --font:             'Inter', system-ui, sans-serif;
  --text-xs:          12px;
  --text-sm:          14px;
  --text-base:        16px;
  --text-lg:          18px;
  --text-xl:          22px;
  --text-2xl:         28px;
  --text-3xl:         36px;

  /* Spacing */
  --sp-1: 4px;   --sp-2: 8px;   --sp-3: 12px;  --sp-4: 16px;
  --sp-5: 24px;  --sp-6: 32px;  --sp-7: 48px;  --sp-8: 64px;

  /* Shape */
  --r-sm:   4px;
  --r-md:   8px;
  --r-lg:   12px;
  --r-full: 999px;

  /* Shadow */
  --shadow-sm: 0 1px 3px rgba(0,0,0,.08);
  --shadow-md: 0 4px 12px rgba(0,0,0,.10);
  --shadow-lg: 0 8px 24px rgba(0,0,0,.12);
}
```

---

## 5. KOMPONEN (components/)

Setiap komponen adalah fungsi PHP yang di-`require` lalu dipanggil inline.

### badge_status($status)
Menampilkan `<span class="badge badge--{$status}">`. Peta warna:
- `submitted` → `--c-submitted`
- `review` / `site_visit` / `under_review` → `--c-review`
- `approved` / `paid` / `resolved` → `--c-approved`
- `rejected` / `unpaid` → `--c-rejected`

### timeline_status($current, $steps)
Menampilkan progress bar horizontal dengan langkah-langkah. Step sebelum `$current` = filled, `$current` = active, sesudahnya = empty.

### pagination($total_rows, $per_page, $current_page)
Menampilkan navigasi halaman. Generate URL dengan `?page=N` append ke URL saat ini.

### stat_card($label, $value, $icon_html)
Card putih dengan angka besar, label kecil, dan ikon SVG inline.

### empty_state($message)
Div centered dengan ikon dan teks saat query mengembalikan 0 baris.

### modal_confirm($id, $title, $message, $action_url, $hidden_fields)
Form tersembunyi + dialog CSS-only untuk konfirmasi approve/reject tanpa JS library.

---

## 6. ALUR PROSES BISNIS

### 6.1 Login & Auth
```
POST /login.php
  → validasi CSRF token
  → query users WHERE email = ? AND is_active = 1
  → password_verify()
  → session_regenerate_id(true)
  → $_SESSION['user_id'], ['role'], ['lang'] = 'id'
  → redirect ke /{role}/index.php
```
Setiap file portal: baris 1 = `require_once '../includes/auth.php'` lalu `require_login('airline')`.  
`require_login()` redirect ke `/login.php` jika session tidak valid.

### 6.2 Pengajuan Slot Time (Maskapai)
```
GET  /maskapai/slot.php        → tampilkan list + form kosong
POST /maskapai/slot.php
  → validasi CSRF + input
  → handle_upload('doc', 'slot')  → simpan path
  → INSERT slot_requests (status='submitted')
  → kirim email ke admin ANATL
  → flash('Pengajuan berhasil dikirim')
  → redirect ke GET yang sama
```
Admin approve/reject di `/admin/slot.php`:
```
POST /admin/slot.php
  → UPDATE slot_requests SET status=?, admin_notes=? WHERE id=?
  → kirim email notifikasi ke maskapai
  → redirect + flash
```

### 6.3 Flight Plan (Maskapai)
Alur identik dengan slot time. Tabel: `flight_plans`.

### 6.4 Safety Reporting (Maskapai)
```
POST /maskapai/safety.php
  → jika is_anonymous=1 → user_id = NULL
  → INSERT safety_reports
  → TIDAK kirim email (rahasia)
  → flash + redirect
```

### 6.5 CMS Kepabeanan (Bea Cukai)
```
GET  /customs/visa.php?lang=id    → load customs_content WHERE section='visa' AND lang='id'
POST /customs/visa.php
  → INSERT ... ON DUPLICATE KEY UPDATE content=?, updated_by=?, updated_at=NOW()
  → flash + redirect
```
Konten tampil publik di `/kepabeanan.php?lang=id` via query yang sama (tanpa login).

### 6.6 Penerbangan Non-Reguler & Invoice (Ground Handling)
```
Admin POST /admin/ground-handling.php  → action=add_flight
  → INSERT irregular_flights
  → SELECT user_id FROM users WHERE role='ground_handler' AND is_active=1
  → Kirim email notifikasi ke SEMUA ground handler aktif
  → flash + redirect

Admin POST /admin/ground-handling.php  → action=generate_invoice
  → INSERT invoices (status='unpaid')
  → Generate PDF via FPDF → simpan ke uploads/invoice-pdf/
  → UPDATE invoices SET pdf_path=?
  → Kirim email ke vendor (ground_handler user_id terkait)
  → flash + redirect

Vendor POST /ground-handling/invoice.php  → action=confirm
  → UPDATE invoices SET status='paid', confirmed_at=NOW() WHERE id=? AND vendor_id=?
  → flash + redirect
```

### 6.7 Pengajuan Sewa Tenant
```
POST /tenant/pengajuan.php
  → handle_upload('doc', 'tenant-docs')
  → INSERT rental_applications (status='submitted')
  → Kirim email ke admin
  → flash + redirect

Admin /admin/tenant.php  → action=update_status
  → UPDATE rental_applications SET status=?, admin_notes=?
  → Jika status='contract' → INSERT rental_contracts + handle_upload kontrak PDF
  → Buat akun user jika belum ada (status approved pertama kali)
  → Kirim email ke pemohon
  → flash + redirect
```

### 6.8 Flash Message
```php
// Set (sebelum redirect):
$_SESSION['flash'] = ['type' => 'success', 'msg' => 'Berhasil disimpan'];
header('Location: same-page.php'); exit;

// Tampilkan + hapus (di header.php atau atas halaman):
if (isset($_SESSION['flash'])) {
    $flash = $_SESSION['flash'];
    unset($_SESSION['flash']);
    // render alert component
}
```

### 6.9 CSRF Protection
```php
// Generate (di functions.php):
function csrf_token(): string {
    if (empty($_SESSION['csrf_token']))
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    return $_SESSION['csrf_token'];
}

// Di setiap form:
<input type="hidden" name="csrf_token" value="<?= csrf_token() ?>">

// Validasi di POST handler:
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'] ?? ''))
    die('Invalid request');
```

---

## 7. SITE STRUCTURE (Navigasi)

```
[Publik — tanpa login]
  / ................... Homepage + FIDS departures/arrivals
  /penerbangan ........ Jadwal lengkap + filter
  /fasilitas .......... Info area bandara
  /panduan ............ FAQ + ketentuan bagasi
  /kepabeanan ......... Visa | Barang Bawaan | Deklarasi (tab)
  /tenant-unit ........ Galeri unit sewa
  /laporan ............ Form laporan fasilitas

[Auth]
  /login .............. Form login (semua role)
  /logout ............. Destroy session

[Portal Maskapai — role: airline]
  /maskapai/ .......... Dashboard: ringkasan status
  /maskapai/slot ...... List + form pengajuan slot time
  /maskapai/flight-plan List + form flight plan
  /maskapai/safety .... List + form safety report

[Portal Ground Handling — role: ground_handler]
  /ground-handling/ ... Dashboard: notif + invoice terbaru
  /ground-handling/notif List penerbangan non-reguler
  /ground-handling/invoice List invoice + konfirmasi bayar

[Portal Tenant — role: tenant]
  /tenant/ ............ Dashboard: status pengajuan + kontrak
  /tenant/pengajuan ... Form baru + tracking status
  /tenant/kontrak ..... List + download kontrak
  /tenant/maintenance . Form + riwayat tiket

[Portal Bea Cukai — role: customs]
  /customs/ ........... Dashboard ringkasan
  /customs/visa ....... Editor konten visa per bahasa
  /customs/barang ..... Editor daftar barang per kategori
  /customs/deklarasi .. Upload formulir deklarasi

[Admin — role: admin]
  /admin/ ............. Dashboard: stat cards + aktivitas terbaru
  /admin/users ........ List + tambah + toggle aktif
  /admin/slot ......... Antrian approval slot time
  /admin/flight-plan .. Antrian approval flight plan
  /admin/tenant ....... Antrian approval + upload kontrak
  /admin/ground-handling Input non-reguler + generate invoice
  /admin/cms .......... CMS berita, pengumuman, NOTAM
```

---

## 8. CODING RULES (Non-Negotiable)

### PHP
```
✓ POST handler di ATAS output HTML, sebelum require header.php
✓ Semua output: htmlspecialchars($v, ENT_QUOTES, 'UTF-8')
✓ Semua query: mysqli_prepare() + bind_param() — TANPA KECUALI
✓ Baris pertama setiap file portal: require_once '../includes/auth.php'
✓ Flash message via $_SESSION['flash'] sebelum header('Location:')
✓ Variabel: $snake_case | Konstanta: SCREAMING_SNAKE
✗ Tidak ada echo/print di includes/header.php dan footer.php selain HTML
✗ Tidak ada query di file komponen — data dikirim sebagai parameter fungsi
```

### MySQL
```
✓ Nama tabel: snake_case plural
✓ PK: id INT AUTO_INCREMENT PRIMARY KEY
✓ Setiap tabel: created_at DATETIME DEFAULT CURRENT_TIMESTAMP
✓ Tabel yang di-update: updated_at ... ON UPDATE CURRENT_TIMESTAMP
✓ Soft delete: is_active TINYINT(1) DEFAULT 1
✓ Foreign key dideklarasikan eksplisit
```

### HTML & CSS
```
✓ CSS variables dari :root untuk semua warna dan spacing
✓ BEM: block__element--modifier
✓ Semantik: <main>, <section>, <nav>, <header>, <footer>
✓ Mobile-first: halaman publik responsive sampai 360px
✗ Tidak ada inline style kecuali nilai dinamis PHP
✗ Tidak ada nilai warna hardcode di luar :root
```

### File Upload
```
✓ Validasi ekstensi: ['pdf','jpg','jpeg','png']
✓ Validasi ukuran: max 5MB
✓ Rename: uniqid().'_'.time().'.'.$ext
✓ Simpan di /uploads/{subfolder}/
✓ /uploads/.htaccess: Deny from all
✗ Tidak pernah gunakan nama file dari $_FILES['name']
```

### Keamanan
```
✓ password_hash($p, PASSWORD_DEFAULT) saat simpan
✓ password_verify() saat login
✓ session_regenerate_id(true) setelah login
✓ CSRF token di setiap form POST, validasi hash_equals()
✓ Error detail hanya di server log, pesan generik ke user
```
