# Smart GCS Signed URL System - COMPLETE!

## ✅ IMPLEMENTED: Private Signed URLs with Smart Expiry

---

## 🎯 FEATURES

### 1. **Context-Aware Expiry**
URL expiry menyesuaikan dengan konteks penggunaan:

| Context | Expiry | Kegunaan |
|---------|--------|----------|
| `admin` | 15 menit | Admin melihat soal ( cepat expired ) |
| `exam` | 2 jam | Ujian berlangsung |
| `student_exam` | 3 jam | Siswa sedang ujian + buffer |
| `preview` | 30 menit | Preview soal |

### 2. **Dynamic Exam Duration**
Untuk ujian, expiry dihitung berdasarkan:
- Sisa durasi ujian + 30 menit buffer
- Tidak akan expired pas ujian berlangsung
- Otomatis sesuaikan dengan `exam_session.end_time`

### 3. **Private & Secure**
- ✅ Tidak ada public URL
- ✅ Hanya pemegang signed URL yang bisa akses
- ✅ Auto-expire sesuai konteks
- ✅ Tidak bisa di-share sembarangan

---

## 📋 UPDATED FUNCTIONS

### Helper Functions:

```php
// 1. Basic with context (15 min default for admin)
gcs_url($path, $expiry = null, $context = 'default')

// 2. Admin context (15 minutes)
gcs_image_url($path, $fallback = null, 15, 'admin')

// 3. Exam context with duration
gcs_exam_image_url($path, $examDurationMinutes, $fallback)

// 4. Custom expiry (DateTime object)
gcs_url($path, $examEndTime, 'exam')
```

---

## 🔧 USAGE EXAMPLES

### Di Blade Template:

**Admin (15 menit expiry):**
```blade
<img src="{{ gcs_image_url($question->image_path, null, 15, 'admin') }}"
     alt="Soal"
     onerror="this.src='https://via.placeholder.com/400x200?text=Expired'">
```

**Exam dengan durasi dinamis:**
```blade
@php
    $examDuration = $examSession->getRemainingMinutes(); // Hitung sisa waktu
@endphp

<img src="{{ gcs_exam_image_url($question->image_path, $examDuration) }}"
     alt="Soal">
```

**Preview (30 menit):**
```blade
<img src="{{ gcs_image_url($question->image_path, null, 30, 'preview') }}"
     alt="Preview">
```

---

## 🌐 API ENDPOINTS

### 1. Get Image URL dengan Context
```
GET /admin/api/image-url/{path}?context=admin&expiry=15
```

**Response:**
```json
{
  "success": true,
  "url": "https://storage.googleapis.com/...?signature=...",
  "path": "questions/images/xxx.webp",
  "expires_in": 15,
  "expires_at": "2026-03-29T10:30:00+07:00",
  "context": "admin"
}
```

### 2. Get Exam Image URL (Dynamic based on exam session)
```
POST /admin/api/exam-image-url
```

**Request:**
```json
{
  "path": "questions/images/xxx.webp",
  "exam_session_id": "session-uuid"
}
```

**Response:**
```json
{
  "success": true,
  "url": "https://storage.googleapis.com/...?signature=...",
  "path": "questions/images/xxx.webp",
  "expires_in": 150,
  "expires_at": "2026-03-29T12:00:00+07:00",
  "exam_ends_at": "2026-03-29T11:30:00+07:00"
}
```

### 3. Batch Image URLs
```
POST /admin/api/image-urls
```

**Request:**
```json
{
  "paths": [
    "questions/images/xxx.webp",
    "questions/images/yyy.webp"
  ],
  "context": "admin",
  "expiry": 15
}
```

---

## 💻 JAVASCRIPT USAGE

### Admin View (15 min expiry):
```javascript
// Load image with 15 min expiry
function loadAdminImage(imagePath) {
    const encodedPath = encodeURIComponent(imagePath);
    fetch(`/admin/api/image-url/${encodedPath}?context=admin&expiry=15`)
        .then(res => res.json())
        .then(result => {
            if (result.success) {
                imgElement.src = result.url;
                console.log('URL expires in:', result.expires_in, 'minutes');
            }
        });
}
```

### Exam View (Dynamic expiry):
```javascript
// Load exam image with dynamic expiry based on exam session
function loadExamImage(imagePath, examSessionId) {
    fetch('/admin/api/exam-image-url', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            path: imagePath,
            exam_session_id: examSessionId,
        }),
    })
        .then(res => res.json())
        .then(result => {
            if (result.success) {
                imgElement.src = result.url;
                console.log('URL expires at:', result.expires_at);
                console.log('Exam ends at:', result.exam_ends_at);
            }
        });
}
```

---

## 🧪 TESTING

```bash
# Test basic GCS URL with default context
php artisan tinker --execute="
\$url = gcs_url('questions/images/test.webp', 15, 'admin');
echo 'URL (150 chars): ' . substr(\$url, 0, 150) . '...' . PHP_EOL;
"

# Test with exam context (2 hours)
php artisan tinker --execute="
\$url = gcs_url('questions/images/test.webp', null, 'exam');
echo 'Expires: ' . calculate_gcs_expiry(null, 'exam')->toIso8601String() . PHP_EOL;
"

# Test exam image URL with duration
php artisan tinker --execute="
\$url = gcs_exam_image_url('questions/images/test.webp', 120);
echo 'URL (150 chars): ' . substr(\$url, 0, 150) . '...' . PHP_EOL;
"
```

---

## 🔒 SECURITY BENEFITS

| Before (Public) | After (Private Signed) |
|-----------------|----------------------|
| ❌ Bisa diakses siapa saja | ✅ Hanya yang punya URL |
| ❌ URL valid selamanya | ✅ Auto-expire 15 menit (admin) / dynamic (exam) |
| ❌ Bisa di-share ke orang lain | ✅ URL unik per request |
| ❌ Tidak bisa batasi akses | ✅ Full control atas akses |
| ❌ Bandwidth terbuang | ✅ Efisien, expire otomatis |

---

## 📊 EXPIRY COMPARISON

**Admin Views:**
```
User buka halaman soal → Generate URL (15 min expiry)
↓ 15 menit
URL expired → Gambar tidak muncul (refresh untuk renew)
```

**Exam Views:**
```
Siswa mulai ujian (60 menit) → Generate URL (90 min expiry: 60 + 30 buffer)
↓ Selama ujian
URL tetap valid
↓ 30 menit setelah ujian
URL expired → Akses ditutup
```

---

## 🎯 BEST PRACTICES

### 1. **Admin Dashboard**
- Gunakan `context='admin'` dengan `expiry=15`
- Cukup untuk preview cepat
- Aman karena cepat expired

### 2. **Student Exam**
- Gunakan `exam_image_url()` dengan durasi ujian
- Atau `context='student_exam'` (3 jam)
- Pastikan tidak expired pas ujian

### 3. **Public Preview**
- Gunakan `context='preview'` dengan `expiry=30`
- Untuk showcase atau demo

### 4. **API Response**
- Selalu return `expires_in` dan `expires_at`
- Client bisa track dan refresh URL sebelum expired

---

## ✅ UPDATE SUMMARY

**Files Updated:**
- ✅ `app/Helpers/GcsHelper.php` - Smart expiry system
- ✅ `app/Http/Controllers/Admin/GcsImageController.php` - Dynamic exam URL
- ✅ `routes/web.php` - New exam endpoint
- ✅ `resources/views/admin/exam-package-detail.blade.php` - Admin context (15 min)

**New Functions:**
- ✅ `calculate_gcs_expiry()` - Smart expiry calculator
- ✅ `gcs_exam_image_url()` - Exam-specific URL generator
- ✅ `getExamImageUrl()` - API endpoint for exam images

---

## 🚀 NEXT STEPS

1. **Update student exam view** untuk pakai `exam_image_url()`
2. **Refresh browser** untuk test admin view (15 min expiry)
3. **Test exam flow** untuk pastikan URL tidak expired pas ujian

System now uses **100% private signed URLs** with smart expiry! 🎉
