# Hướng Dẫn Sử Dụng Permanent File URL API

## 📋 Mục Lục

1. [Tổng Quan](#tổng-quan)
2. [Cài Đặt](#cài-đặt)
3. [API Endpoints](#api-endpoints)
4. [Luồng Hoạt Động](#luồng-hoạt-động)
5. [Ví Dụ Sử Dụng](#ví-dụ-sử-dụng)
6. [Bảo Mật](#bảo-mật)
7. [Troubleshooting](#troubleshooting)

---

## 🎯 Tổng Quan

Hệ thống Permanent File URL cho phép:

- ✅ Upload file lên S3-AWS với thời hạn dài (1-50 năm)
- ✅ Tạo URL cố định (permanent) có thể hardcode vào code
- ✅ Kiểm tra domain mỗi lần load file (bảo mật)
- ✅ Token cố định: cùng file = cùng token (ổn định)
- ✅ Hỗ trợ Range Requests (streaming video/audio)
- ✅ Tracking số lần truy cập

### So Sánh 2 Loại URL

| Tính năng | Temporary URL | Permanent URL |
|-----------|--------------|---------------|
| Token | Random, mỗi lần upload khác nhau | Cố định, cùng file = cùng token |
| Thời hạn | Ngắn (1 giờ - 7 ngày) | Dài (1-50 năm) |
| Use case | File tạm thời | File cần hardcode vào code |
| URL | Thay đổi mỗi lần | Ổn định, không đổi |

---

## 🔧 Cài Đặt

### 1. Chạy Migration

```bash
php artisan migrate
```

Migration sẽ tạo bảng `permanent_file_urls` với các trường:
- `token`: Token cố định (64 ký tự)
- `file_id`: ID của file (dùng để generate token)
- `s3_key`: Key trên S3-AWS
- `s3_url`: URL đầy đủ trên S3
- `server_url`: URL của server (trả về cho đối tác)
- `allowed_domains`: Danh sách domain được phép
- `expires_at`: Thời gian hết hạn
- `access_count`: Số lần truy cập

### 2. Cấu Hình Environment

Thêm vào `.env`:

```env
# Danh sách domain được phép (cách nhau bởi dấu phẩy)
ALLOWED_DOMAINS=partner1.com,partner2.com,subdomain.partner1.com
```

---

## 📡 API Endpoints

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

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

**Mô tả:** Upload file lên S3 và 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>
create_permanent: true
expires_years: 20
partner_id: acme-corp
folder: partner_uploads
```

**Parameters:**
- `file` (required): File cần upload (max 50MB)
- `create_permanent` (optional, default: `false`): Tạo permanent URL ngay
- `expires_years` (optional, default: `20`): Số năm token tồn tại (1-50)
- `partner_id` (optional): ID của đối tác
- `folder` (optional, default: `temp_uploads`): Folder trên S3

**Response (Success):**
```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"
    }
  }
}
```

**Response (Error):**
```json
{
  "status": false,
  "message": "Validation failed",
  "errors": {
    "file": ["The file field is required."]
  }
}
```

---

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

**Endpoint:** `POST /api/speakup/get-permanent-url`

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

**Request:**
```http
POST /api/speakup/get-permanent-url
Content-Type: application/json
Referer: https://partner.com

{
  "s3_key": "temp_uploads/2024/11/18/example_1234567890_abc123.jpg",
  "file_name": "example.jpg",
  "file_id": 1234567890,
  "expires_years": 20,
  "partner_id": "acme-corp",
  "allowed_domains": ["partner.com", "subdomain.partner.com"]
}
```

**Parameters:**
- `s3_key` (required): Key của file trên S3
- `file_name` (required): Tên file gốc
- `file_id` (optional): ID của file (tự generate nếu không có)
- `expires_years` (optional, default: `20`): Số năm token tồn tại (1-50)
- `partner_id` (optional): ID của đối tác
- `allowed_domains` (optional): Danh sách domain được phép (nếu không có sẽ lấy từ env)

**Response (Success):**
```json
{
  "status": true,
  "message": "Permanent URL created successfully",
  "data": {
    "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"
  }
}
```

**Response (File đã tồn tại):**
```json
{
  "status": true,
  "message": "Permanent URL already exists",
  "data": {
    "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 File (Permanent URL)

**Endpoint:** `GET /api/permanent-file/{token}`

**Mô tả:** Serve file từ S3 với kiểm tra domain mỗi lần load.

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

**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 không được phép):**
```
HTTP/1.1 403 Forbidden
Content-Type: text/plain

Domain not allowed
```

**Response (Token hết hạn):**
```
HTTP/1.1 403 Forbidden
Content-Type: text/plain

Token invalid or expired
```

**Response (File không tồn tại):**
```
HTTP/1.1 404 Not Found
Content-Type: text/plain

File not found
```

---

### 4. Revoke Permanent Token

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

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

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

{
  "file_id": 1234567890
}
```

**Parameters:**
- `file_id` (required): ID của file cần revoke

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

**Response (Not Found):**
```json
{
  "status": false,
  "message": "Permanent URL not found for this file_id"
}
```

---

## 🔄 Luồng Hoạt Động (Theo Code Thực Tế)

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

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

**Luồng xử lý trong code:**
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ó)

```
┌──────────────┐                        ┌──────────────────┐
│  Partner     │                        │  Laravel API     │
│  (Backend)   │                        │  (Your Server)   │
└──────┬───────┘                        └────────┬─────────┘
       │                                         │
       │  1. POST /upload-file-with-token        │
       │     create_permanent=true               │
       │────────────────────────────────────────>│
       │                                         │
       │                      ┌──────────────┐  │
       │                      │   AWS S3      │  │
       │                      └──────┬───────┘  │
       │                             │          │
       │         2. Upload file lên S3          │
       │                                         │
       │         3. Tạo permanent URL           │
       │            - Generate token cố định    │
       │            - Lưu vào database          │
       │            - Lưu vào cache             │
       │                                         │
       │  4. Response: permanent_url.file_url   │
       │<────────────────────────────────────────│
       │                                         │
       │  5. Hardcode URL vào HTML               │
       │     <img src="...permanent-file/...">  │
       │                                         │
┌──────┴───────┐                        ┌────────┴─────────┐
│  Partner     │                        │  Laravel API     │
└──────────────┘                        └──────────────────┘
```

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

**Route:** `POST /api/get-permanent-url` hoặc `POST /api/speakup/get-permanent-url`

**Luồng xử lý trong code:**
1. Validate request: `image_id`/`file_id` hoặc `s3_key` phải có ít nhất 1
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
9. Get `allowed_domains` từ request hoặc env `ALLOWED_DOMAINS`
10. Check Referer domain (nếu có Referer header và có `allowed_domains`) - 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`
14. Lưu vào cache với key `permanent_file_token:{token}`
15. Trả về response

### Scenario 3: User Load Ảnh

**Route:** `GET /api/permanent-file/{token}` hoặc `GET /api/permanent-image/{token}`

**Luồng xử lý trong code:**
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)
6. Verify file exists on S3: `Storage::disk('s3-aws')->exists($s3Key)`
7. Update access count: `$permanentUrl->incrementAccess()`
8. **Nếu có Range header:** Return `206 Partial Content`
9. **Nếu không có Range header:**
   - File > 10MB: Stream từ S3 (8KB chunks)
   - File <= 10MB: Load hết vào memory
10. Return file content với headers

```
┌──────────────┐                        ┌──────────────────┐
│  User        │                        │  Laravel API     │
│  Browser     │                        │  (Your Server)   │
│  partner.com │                        │                  │
└──────┬───────┘                        └────────┬─────────┘
       │                                         │
       │  GET /api/permanent-file/{token}        │
       │  Referer: https://partner.com/products  │
       │────────────────────────────────────────>│
       │                                         │
       │         ┌─────────────────────┐       │
       │         │ 1. Get token từ     │       │
       │         │    Cache/Database   │       │
       │         │ 2. Check token      │       │
       │         │    valid? ✓         │       │
       │         │ 3. Check Referer    │       │
       │         │    domain? ✓        │       │
       │         │ 4. Update           │       │
       │         │    access_count     │       │
       │         └─────────────────────┘       │
       │                                         │
       │                      ┌──────────────┐  │
       │                      │   AWS S3      │  │
       │                      └──────┬───────┘  │
       │                             │          │
       │         Fetch file từ S3                │
       │         (Stream nếu > 10MB)             │
       │                                         │
       │  Response: Image Data                  │
       │  HTTP 200 OK                           │
       │  Cache-Control: max-age=31536000       │
       │<────────────────────────────────────────│
       │                                         │
       │  [Hiển thị ảnh]                        │
       │                                         │
┌──────┴───────┐                        ┌────────┴─────────┐
│  User        │                        │  Laravel API     │
└──────────────┘                        └──────────────────┘
```

### Scenario 4: Hacker Copy Link

```
┌──────────────┐                        ┌──────────────────┐
│  Hacker      │                        │  Laravel API     │
│  Browser     │                        │  (Your Server)   │
│  hacker.com  │                        │                  │
└──────┬───────┘                        └────────┬─────────┘
       │                                         │
       │  GET /permanent-file/{token}           │
       │  Referer: https://hacker.com/steal     │
       │────────────────────────────────────────>│
       │                                         │
       │         ┌─────────────────────┐       │
       │         │ 1. Check token ✓     │       │
       │         │ 2. Check Referer     │       │
       │         │    hacker.com ✗      │       │
       │         │    NOT in whitelist  │       │
       │         │    → BLOCK!          │       │
       │         └─────────────────────┘       │
       │                                         │
       │  Response: 403 Forbidden                │
       │  "Domain not allowed"                   │
       │<────────────────────────────────────────│
       │                                         │
       │  [Ảnh KHÔNG hiển thị]                   │
       │                                         │
┌──────┴───────┐                        ┌────────┴─────────┐
│  Hacker      │                        │  Laravel API     │
└──────────────┘                        └──────────────────┘
```

---

## 💻 Ví Dụ Sử Dụng

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

```bash
curl -X POST http://your-server.com/api/speakup/upload-file-with-token \
  -F "file=@/path/to/image.jpg" \
  -F "create_permanent=true" \
  -F "expires_years=20" \
  -F "partner_id=acme-corp" \
  -F "folder=partner_uploads"
```

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

### 2. Sử Dụng Permanent URL Trong HTML

```html
<!DOCTYPE html>
<html>
<head>
    <title>Product Page</title>
</head>
<body>
    <h1>Product Image</h1>
    <!-- URL này ổn định, không đổi trong 20 năm -->
    <img src="http://your-server.com/api/permanent-file/def456..." 
         alt="Product Image" 
         width="500">
</body>
</html>
```

### 3. Sử Dụng Trong JavaScript/React

```javascript
// Lưu permanent URL vào state/config
const productImageUrl = "http://your-server.com/api/permanent-file/def456...";

// Sử dụng trong component
function ProductImage() {
    return (
        <img 
            src={productImageUrl} 
            alt="Product" 
            onError={(e) => {
                console.error("Failed to load image");
            }}
        />
    );
}
```

### 4. Sử Dụng Trong PHP (Backend)

```php
<?php
// Lấy permanent URL từ API
$response = file_get_contents('http://your-server.com/api/speakup/get-permanent-url', false, stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' => 'Content-Type: application/json',
        'content' => json_encode([
            's3_key' => 'temp_uploads/2024/11/18/file.jpg',
            'file_name' => 'file.jpg',
            'expires_years' => 20,
            'partner_id' => 'acme-corp'
        ])
    ]
]));

$data = json_decode($response, true);
$permanentUrl = $data['data']['file_url'];

// Lưu vào database
$product->image_url = $permanentUrl;
$product->save();
?>
```

### 5. Python Example

```python
import requests

# Upload file và tạo permanent URL
files = {'file': open('image.jpg', 'rb')}
data = {
    'create_permanent': 'true',
    'expires_years': 20,
    'partner_id': 'acme-corp'
}

response = requests.post(
    'http://your-server.com/api/speakup/upload-file-with-token',
    files=files,
    data=data
)

result = response.json()
permanent_url = result['data']['permanent_url']['file_url']

print(f"Permanent URL: {permanent_url}")
# Output: Permanent URL: http://your-server.com/api/permanent-file/def456...
```

---

## 🔒 Bảo Mật

### 1. Token Cố Định

- Token được generate từ: `hash('permanent_' + file_id + APP_KEY)`
- Cùng `file_id` = cùng token (ổn định)
- Token không thể đoán được (có APP_KEY)

### 2. Kiểm Tra Domain

- Mỗi lần load file, server kiểm tra Referer header
- Chỉ domain trong `allowed_domains` mới được phép
- Hacker copy link → vẫn bị block

### 3. Token Hết Hạn

- Token tự động hết hạn sau `expires_years`
- Có thể revoke sớm bằng API `revoke-permanent-token`

### 4. Access Tracking

- Theo dõi số lần truy cập (`access_count`)
- Lưu thời gian truy cập cuối (`last_accessed_at`)
- Có thể phát hiện abuse

### 5. Best Practices

✅ **Nên làm:**
- Set `expires_years` hợp lý (20 năm là tốt)
- Sử dụng `partner_id` để tracking
- Monitor `access_count` để phát hiện abuse
- Revoke token khi cần block

❌ **Không nên:**
- Không share token công khai
- Không set `allowed_domains` quá rộng
- Không ignore Referer check

---

## 🐛 Troubleshooting

### 1. Lỗi: "Token invalid or expired"

**Nguyên nhân:**
- Token đã hết hạn
- Token bị revoke
- Token không tồn tại trong cache/database

**Giải pháp:**
- Kiểm tra `expires_at` trong database
- Kiểm tra `is_active = true`
- Tạo lại permanent URL nếu cần

### 2. Lỗi: "Domain not allowed"

**Nguyên nhân:**
- Referer domain không có trong `allowed_domains`
- Request không có Referer header

**Giải pháp:**
- Thêm domain vào `allowed_domains` trong request hoặc env
- Đảm bảo request có Referer header (browser tự động thêm)

### 3. Lỗi: "File not found on S3"

**Nguyên nhân:**
- File đã bị xóa khỏi S3
- S3 key không đúng

**Giải pháp:**
- Kiểm tra file còn tồn tại trên S3
- Verify `s3_key` trong database

### 4. Ảnh không hiển thị trong HTML

**Nguyên nhân:**
- URL không đúng
- CORS issue
- Domain không được phép

**Giải pháp:**
- Kiểm tra URL có đúng không
- Kiểm tra Referer header
- Kiểm tra CORS settings (nếu cần)

### 5. Performance Issues

**Vấn đề:**
- File lớn load chậm
- Server timeout

**Giải pháp:**
- Hệ thống tự động stream file > 10MB
- Hỗ trợ Range Requests cho video/audio
- Cân nhắc dùng CloudFront CDN

---

## 📊 Database Schema

### Bảng `permanent_file_urls`

```sql
CREATE TABLE permanent_file_urls (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    token VARCHAR(64) UNIQUE NOT NULL,
    file_id BIGINT UNSIGNED UNIQUE NOT NULL,
    file_name VARCHAR(255) NOT NULL,
    s3_key VARCHAR(500) NOT NULL,
    s3_url VARCHAR(500) NULL,
    server_url VARCHAR(500) NOT NULL,
    mime_type VARCHAR(100) NOT NULL,
    file_size BIGINT NOT NULL,
    folder VARCHAR(255) NULL,
    partner_id VARCHAR(255) NULL,
    allowed_domains JSON NULL,
    expires_years INT DEFAULT 20,
    expires_at TIMESTAMP NOT NULL,
    is_active BOOLEAN DEFAULT TRUE,
    created_by_ip VARCHAR(45) NULL,
    access_count INT DEFAULT 0,
    last_accessed_at TIMESTAMP NULL,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    
    INDEX idx_token (token),
    INDEX idx_file_id (file_id),
    INDEX idx_partner_id (partner_id),
    INDEX idx_is_active (is_active),
    INDEX idx_expires_at (expires_at),
    INDEX idx_active_expires (is_active, expires_at)
);
```

---

## 📝 Notes

1. **Token Stability**: Token cố định dựa trên `file_id`, nên cùng file sẽ có cùng token. Điều này cho phép hardcode URL vào code.

2. **Domain Checking**: Kiểm tra domain mỗi lần load, không chỉ lúc tạo token. Đảm bảo bảo mật ngay cả khi token bị leak.

3. **Cache Strategy**: Token được lưu vào cache để tăng performance, nhưng vẫn lưu vào database để persistence.

4. **Streaming Support**: File lớn (>10MB) được stream thay vì load hết vào memory, tránh timeout.

5. **Range Requests**: Hỗ trợ HTTP Range Requests cho video/audio streaming (206 Partial Content).

---

## 📞 Support

Nếu gặp vấn đề, vui lòng:
1. Kiểm tra logs: `storage/logs/laravel.log`
2. Kiểm tra database: `permanent_file_urls` table
3. Kiểm tra cache: Redis/Cache
4. Liên hệ team dev với thông tin:
   - File ID
   - Token (first 16 chars)
   - Error message
   - Request details

---

**Version:** 1.0.0  
**Last Updated:** 2024-11-18  
**Author:** LMS Development Team

