# Luồng Đi Thực Tế - Permanent File URL System

## 📋 Tổng Quan

Tài liệu này mô tả **chính xác** luồng đi theo code thực tế trong routes và controller.

---

## 🛣️ Routes Thực Tế

### 1. Upload File và Tạo Permanent URL (Option 1)

**Route:** `POST /api/speakup/upload-file-with-token`

**Controller:** `SpeakupImportController@uploadFileWithToken`

**Mô tả:** Upload file lên S3 và có thể tạo permanent URL ngay lập tức.

**Request:**
```http
POST /api/speakup/upload-file-with-token
Content-Type: multipart/form-data

file: <file> (required, max 50MB)
folder: temp_uploads (optional, default: temp_uploads)
expires_in: 3600 (optional, default: 3600 seconds = 1 hour)
create_permanent: true (optional, default: false)
expires_years: 20 (optional, nếu create_permanent=true, default: 20)
partner_id: acme-corp (optional)
```

**Luồng xử lý:**
1. Validate file (required, max 50MB)
2. Generate unique filename: `{original_name}_{timestamp}_{uniqid}.{ext}`
3. Tạo S3 key: `{folder}/{Y/m/d}/{unique_filename}`
4. Upload file lên S3-AWS: `Storage::disk('s3-aws')->put($s3Key, $fileContent)`
5. Verify file exists on S3
6. Generate temporary token: `Str::random(64)`
7. **Nếu `create_permanent=true`:**
   - Generate `file_id` từ `s3_key`: `abs(crc32($s3Key))`
   - Generate permanent token: `hash('sha256', 'permanent_' + file_id + APP_KEY)`
   - Check xem đã có permanent URL chưa (theo `file_id`)
   - Nếu chưa có hoặc đã hết hạn:
     - Lấy `allowed_domains` từ env `ALLOWED_DOMAINS`
     - Tạo `expires_at` = now() + `expires_years`
     - Lưu vào database `permanent_file_urls`
     - Lưu vào cache với key `permanent_file_token:{token}`
     - Tạo server URL: `route('api.servePermanentFile', ['token' => $permanentToken])`
   - Nếu đã có và còn hiệu lực: Trả về URL hiện tại
8. **Nếu `create_permanent=false`:**
   - Lưu temporary token vào cache với key `file_token:{token}`
   - Thời hạn: `expires_in` seconds (60s - 7 days)
9. Trả về response với `file_url` (temporary) và `permanent_url` (nếu có)

**Response:**
```json
{
  "status": true,
  "message": "File uploaded successfully",
  "data": {
    "file_name": "example.jpg",
    "file_size": 12345,
    "mime_type": "image/jpeg",
    "file_url": "http://your-server.com/api/serve-file/{temporary_token}",
    "expires_in": 3600,
    "expires_at": "2024-11-18T12:00:00Z",
    "permanent_url": {
      "file_id": 1234567890,
      "file_url": "http://your-server.com/api/permanent-file/{permanent_token}",
      "expires_at": "2044-11-18T12:00:00Z",
      "expires_years": 20,
      "note": "This URL is stable and can be hardcoded in your application"
    }
  }
}
```

---

### 2. Tạo Permanent URL Cho File Đã Upload (Option 2)

**Route:** 
- `POST /api/speakup/get-permanent-url`
- `POST /api/get-permanent-url` (alias)

**Controller:** `SpeakupImportController@getPermanentUrl`

**Mô tả:** Tạo permanent URL cho file đã được upload lên S3 trước đó.

**Request:**
```http
POST /api/get-permanent-url
Content-Type: application/json
Referer: https://partner.com (optional, để check domain)

{
  "image_id": 123,              // Optional: ID của file (ưu tiên)
  "file_id": 123,               // Optional: Alias của image_id
  "s3_key": "temp_uploads/...", // Required nếu không có image_id/file_id
  "file_name": "example.jpg",   // Required nếu không có image_id/file_id
  "expires_years": 20,          // Optional, default: 20
  "partner_id": "acme-corp",     // Optional
  "allowed_domains": ["partner.com"] // Optional, lấy từ env nếu không có
}
```

**Luồng xử lý:**
1. Validate request:
   - `image_id` hoặc `file_id` hoặc `s3_key` phải có ít nhất 1
   - `s3_key` và `file_name` required nếu không có `image_id`/`file_id`
2. Ưu tiên `image_id`, nếu không có thì dùng `file_id`
3. **Nếu có `file_id`/`image_id`:**
   - Check xem đã có permanent URL chưa (theo `file_id`)
   - Nếu đã có và còn hiệu lực: Trả về URL hiện tại
   - Nếu đã có nhưng hết hạn: Xóa record cũ
   - Nếu không có `s3_key` từ request: Lấy từ database (nếu đã có permanent URL trước đó)
4. **Nếu không có `file_id`/`image_id`:**
   - Generate `file_id` từ `s3_key`: `abs(crc32($s3Key))`
5. Generate permanent token: `hash('sha256', 'permanent_' + file_id + APP_KEY)`
6. Validate `s3_key` và `file_name` (required)
7. Verify file exists on S3: `Storage::disk('s3-aws')->exists($s3Key)`
8. Get file info từ S3 nếu không có trong request:
   - `mime_type`: `Storage::disk('s3-aws')->mimeType($s3Key)`
   - `file_size`: `Storage::disk('s3-aws')->size($s3Key)`
9. Get `allowed_domains`:
   - Từ request `allowed_domains` (nếu có)
   - Hoặc từ env `ALLOWED_DOMAINS` (split by comma)
10. Check Referer domain (nếu có Referer header và có `allowed_domains`):
    - Parse Referer host
    - Check xem có trong `allowed_domains` không
    - Log warning nếu không được phép (nhưng không block)
11. Calculate `expires_at` = now() + `expires_years`
12. Tạo server URL: `route('api.servePermanentFile', ['token' => $token])`
13. Lưu vào database `permanent_file_urls`:
    - `token`, `file_id`, `file_name`, `s3_key`, `s3_url`, `server_url`
    - `mime_type`, `file_size`, `folder`, `partner_id`
    - `allowed_domains` (JSON), `expires_years`, `expires_at`
    - `is_active = true`, `created_by_ip`
14. Lưu vào cache với key `permanent_file_token:{token}`
15. Trả về response

**Response:**
```json
{
  "status": true,
  "message": "Permanent URL created successfully",
  "data": {
    "image_id": 1234567890,
    "file_id": 1234567890,
    "file_name": "example.jpg",
    "file_url": "http://your-server.com/api/permanent-file/{token}",
    "expires_at": "2044-11-18T12:00:00Z",
    "expires_years": 20,
    "note": "This URL is stable and can be hardcoded in your application"
  }
}
```

---

### 3. Serve Temporary File

**Route:** `GET /api/serve-file/{token}`

**Controller:** `SpeakupImportController@serveFileWithToken`

**Mô tả:** Serve file với temporary token (không kiểm tra domain).

**Request:**
```http
GET /api/serve-file/{token}
Range: bytes=0-1023 (optional, cho video/audio streaming)
```

**Luồng xử lý:**
1. Tăng timeout: `set_time_limit(600)`, `ini_set('memory_limit', '256M')`
2. Lấy token data từ cache: `Cache::get("file_token:{$token}")`
3. Nếu không có: `abort(403, 'Token invalid or expired')`
4. Verify file exists on S3: `Storage::disk('s3-aws')->exists($s3Key)`
5. **Nếu có Range header:**
   - Parse Range: `bytes=start-end`
   - Get partial content từ S3: `readStream()`, `fseek()`, `fread()`
   - Return `206 Partial Content`
6. **Nếu không có Range header:**
   - **Nếu file > 10MB:** Stream từ S3 (8KB chunks)
   - **Nếu file <= 10MB:** Load hết vào memory
7. Return file content với headers:
   - `Content-Type`, `Content-Length`, `Content-Disposition`
   - `Accept-Ranges: bytes`, `Cache-Control: public, max-age=3600`

**Response:**
```
HTTP/1.1 200 OK
Content-Type: image/jpeg
Content-Length: 12345
Content-Disposition: inline; filename="example.jpg"
Accept-Ranges: bytes
Cache-Control: public, max-age=3600
X-Robots-Tag: noindex, nofollow

[Binary file content]
```

---

### 4. Serve Permanent File

**Route:**
- `GET /api/permanent-file/{token}`
- `GET /api/permanent-image/{token}` (alias)

**Controller:** `SpeakupImportController@servePermanentFile`

**Mô tả:** Serve file với permanent token (kiểm tra domain mỗi lần load).

**Request:**
```http
GET /api/permanent-file/{token}
Referer: https://partner.com/products (required nếu có allowed_domains)
Range: bytes=0-1023 (optional, cho video/audio streaming)
```

**Luồng xử lý:**
1. Tăng timeout: `set_time_limit(600)`, `ini_set('memory_limit', '256M')`
2. Lấy token data từ cache: `Cache::get("permanent_file_token:{$token}")`
3. **Nếu không có trong cache:**
   - Lấy từ database: `PermanentFileUrl::where('token', $token)->where('is_active', true)->where('expires_at', '>', now())->first()`
   - Nếu không có: `abort(403, 'Token invalid or expired')`
   - Rebuild token data từ database record
4. **Nếu có trong cache:**
   - Lấy từ database để update access count
5. **Kiểm tra Referer domain (nếu có `allowed_domains`):**
   - **Nếu không có Referer:** `abort(403, 'Referer header required')` (BLOCK)
   - **Nếu có Referer:**
     - Parse Referer host: `parse_url($referer, PHP_URL_HOST)`
     - Check xem có trong `allowed_domains` không (substring match)
     - Nếu không có: `abort(403, 'Domain not allowed')` (BLOCK)
     - Log warning với đầy đủ thông tin
6. Verify file exists on S3: `Storage::disk('s3-aws')->exists($s3Key)`
7. Update access count: `$permanentUrl->incrementAccess()` (tăng `access_count` và update `last_accessed_at`)
8. **Nếu có Range header:**
   - Parse Range: `bytes=start-end`
   - Get partial content từ S3
   - Return `206 Partial Content` với `Cache-Control: public, max-age=31536000` (1 năm)
9. **Nếu không có Range header:**
   - **Nếu file > 10MB:** Stream từ S3 (8KB chunks)
   - **Nếu file <= 10MB:** Load hết vào memory
10. Return file content với headers:
    - `Content-Type`, `Content-Length`, `Content-Disposition`
    - `Accept-Ranges: bytes`, `Cache-Control: public, max-age=31536000`
    - `X-Robots-Tag: noindex, nofollow`

**Response (Success):**
```
HTTP/1.1 200 OK
Content-Type: image/jpeg
Content-Length: 12345
Content-Disposition: inline; filename="example.jpg"
Accept-Ranges: bytes
Cache-Control: public, max-age=31536000
X-Robots-Tag: noindex, nofollow

[Binary file content]
```

**Response (Domain not allowed):**
```
HTTP/1.1 403 Forbidden
Content-Type: text/plain

Domain not allowed
```

**Response (No Referer):**
```
HTTP/1.1 403 Forbidden
Content-Type: text/plain

Referer header required
```

---

### 5. Revoke Permanent Token

**Route:** `POST /api/speakup/revoke-permanent-token`

**Controller:** `SpeakupImportController@revokePermanentToken`

**Mô tả:** Vô hiệu hóa permanent token (block file).

**Request:**
```http
POST /api/speakup/revoke-permanent-token
Content-Type: application/json

{
  "image_id": 123,  // Optional: ưu tiên
  "file_id": 123    // Optional: alias của image_id
}
```

**Luồng xử lý:**
1. Validate: `image_id` hoặc `file_id` phải có ít nhất 1
2. Ưu tiên `image_id`, nếu không có thì dùng `file_id`
3. Tìm permanent URL: `PermanentFileUrl::byFileId($fileId)->first()`
4. Nếu không tìm thấy: Return `404 Not Found`
5. Update database: `is_active = false`
6. Delete từ cache: `Cache::forget("permanent_file_token:{$token}")`
7. Log info
8. Trả về response

**Response:**
```json
{
  "status": true,
  "message": "Token revoked successfully",
  "data": {
    "file_id": 1234567890,
    "token": "abc123..."
  }
}
```

---

## 🔄 Luồng Đi Tổng Quan

### Scenario 1: Upload File và Tạo Permanent URL Ngay

```
Partner Backend
    │
    ├─ POST /api/speakup/upload-file-with-token
    │  ├─ file: <file>
    │  ├─ create_permanent: true
    │  ├─ expires_years: 20
    │  └─ partner_id: acme-corp
    │
    ├─> Laravel API
    │   ├─ Upload file → S3-AWS
    │   ├─ Generate file_id = abs(crc32(s3_key))
    │   ├─ Generate permanent_token = hash('permanent_' + file_id + APP_KEY)
    │   ├─ Save to Database (permanent_file_urls)
    │   ├─ Save to Cache (permanent_file_token:{token})
    │   └─ Return response với permanent_url
    │
    └─< Response: {
         file_url: "/api/serve-file/{temp_token}",
         permanent_url: {
           file_url: "/api/permanent-file/{permanent_token}",
           expires_at: "2044-11-18T12:00:00Z"
         }
       }
```

### Scenario 2: Tạo Permanent URL Cho File Đã Upload

```
Partner Backend
    │
    ├─ POST /api/get-permanent-url
    │  ├─ image_id: 123 (hoặc s3_key + file_name)
    │  ├─ expires_years: 20
    │  └─ partner_id: acme-corp
    │
    ├─> Laravel API
    │   ├─ Check xem đã có permanent URL chưa (theo file_id)
    │   ├─ Nếu có và còn hiệu lực: Return URL hiện tại
    │   ├─ Nếu chưa có hoặc hết hạn:
    │   │  ├─ Generate file_id (từ image_id hoặc s3_key)
    │   │  ├─ Generate permanent_token
    │   │  ├─ Verify file exists on S3
    │   │  ├─ Get allowed_domains (từ request hoặc env)
    │   │  ├─ Save to Database
    │   │  └─ Save to Cache
    │   └─ Return response
    │
    └─< Response: {
         file_url: "/api/permanent-file/{token}",
         expires_at: "2044-11-18T12:00:00Z"
       }
```

### Scenario 3: User Load Ảnh

```
User Browser (partner.com)
    │
    ├─ GET /api/permanent-file/{token}
    │  └─ Referer: https://partner.com/products
    │
    ├─> Laravel API
    │   ├─ Get token data từ Cache hoặc Database
    │   ├─ Check token valid? (is_active, expires_at)
    │   ├─ Check Referer domain? (nếu có allowed_domains)
    │   │  ├─ Nếu không có Referer: BLOCK (403)
    │   │  └─ Nếu domain không được phép: BLOCK (403)
    │   ├─ Verify file exists on S3
    │   ├─ Update access_count
    │   ├─ Fetch file từ S3-AWS
    │   └─ Return file content
    │
    └─< HTTP 200 OK
       Content-Type: image/jpeg
       [Image binary data]
```

### Scenario 4: Hacker Copy Link

```
Hacker Browser (hacker.com)
    │
    ├─ GET /api/permanent-file/{token}
    │  └─ Referer: https://hacker.com/steal (hoặc không có)
    │
    ├─> Laravel API
    │   ├─ Get token data từ Cache hoặc Database
    │   ├─ Check token valid? ✓
    │   ├─ Check Referer domain?
    │   │  ├─ Nếu không có Referer: BLOCK (403 Referer header required)
    │   │  └─ Nếu domain không được phép: BLOCK (403 Domain not allowed)
    │   └─ Log warning
    │
    └─< HTTP 403 Forbidden
       "Domain not allowed" hoặc "Referer header required"
```

---

## 🔑 Key Points

### 1. Token Generation

- **Temporary token:** `Str::random(64)` - Random mỗi lần upload
- **Permanent token:** `hash('sha256', 'permanent_' + file_id + APP_KEY)` - Cố định theo file_id

### 2. File ID Generation

- Từ `image_id` hoặc `file_id` (nếu có trong request)
- Hoặc từ `s3_key`: `abs(crc32($s3Key))`

### 3. Domain Checking

- Chỉ check khi có `allowed_domains` (từ request hoặc env)
- **Bắt buộc có Referer** nếu có `allowed_domains`
- Check bằng substring match: `str_contains($refererHost, $domain)`

### 4. Cache Strategy

- **Temporary token:** Key `file_token:{token}`, TTL = `expires_in` seconds
- **Permanent token:** Key `permanent_file_token:{token}`, TTL = `expires_years` seconds
- Fallback to database nếu không có trong cache

### 5. File Serving

- **Range Requests:** Hỗ trợ `206 Partial Content` cho video/audio streaming
- **Streaming:** File > 10MB được stream (8KB chunks) để tiết kiệm memory
- **Small files:** File <= 10MB load hết vào memory

### 6. Access Tracking

- Update `access_count` và `last_accessed_at` mỗi lần serve permanent file
- Không track cho temporary file

---

## 📊 Database Schema

### Bảng `permanent_file_urls`

```sql
- id: BIGINT (Primary Key)
- token: VARCHAR(64) UNIQUE (Token cố định)
- file_id: BIGINT UNIQUE (ID của file, dùng để generate token)
- file_name: VARCHAR(255) (Tên file gốc)
- s3_key: VARCHAR(500) (Key trên S3)
- s3_url: VARCHAR(500) (URL đầy đủ trên S3)
- server_url: VARCHAR(500) (URL của server, trả về cho đối tác)
- mime_type: VARCHAR(100) (MIME type)
- file_size: BIGINT (Kích thước file bytes)
- folder: VARCHAR(255) (Folder trên S3)
- partner_id: VARCHAR(255) (ID đối tác)
- allowed_domains: JSON (Danh sách domain được phép)
- expires_years: INT (Số năm token tồn tại)
- expires_at: TIMESTAMP (Thời gian hết hạn)
- is_active: BOOLEAN (Token có active không)
- created_by_ip: VARCHAR(45) (IP tạo token)
- access_count: INT (Số lần truy cập)
- last_accessed_at: TIMESTAMP (Lần truy cập cuối)
- created_at: TIMESTAMP
- updated_at: TIMESTAMP
```

---

## 🔒 Bảo Mật

### 1. Token Security

- Permanent token: Không thể đoán được (có APP_KEY)
- Temporary token: Random 64 ký tự
- Token được lưu trong cache và database

### 2. Domain Protection

- **Bắt buộc Referer** nếu có `allowed_domains`
- Check domain mỗi lần load (không chỉ lúc tạo)
- Substring match để hỗ trợ subdomain

### 3. Access Control

- Token hết hạn tự động sau `expires_years`
- Có thể revoke sớm bằng API
- Track access count để phát hiện abuse

### 4. File Validation

- Verify file exists on S3 trước khi serve
- Validate file size và mime type
- Support Range Requests cho streaming

---

## 📝 Notes

1. **Route Alias:** Hỗ trợ cả 2 route để tương thích:
   - `/api/permanent-file/{token}` (tên gốc)
   - `/api/permanent-image/{token}` (theo sơ đồ)

2. **Parameter Alias:** Hỗ trợ cả `image_id` và `file_id`:
   - `image_id` (theo sơ đồ)
   - `file_id` (tên gốc)

3. **Flexibility:** API có thể hoạt động với:
   - Chỉ `image_id`/`file_id` (nếu đã có permanent URL trước đó)
   - `s3_key` + `file_name` (tạo mới)
   - Cả 2 (update hoặc tạo mới)

4. **Cache Fallback:** Nếu không có trong cache, lấy từ database và rebuild token data.

5. **Error Handling:** Tất cả errors đều được log và trả về response phù hợp.

---

**Version:** 1.0.0  
**Last Updated:** 2024-11-18  
**Based on:** Actual code in routes/api.php and SpeakupImportController.php

