# Phase 2: Signed URL Direct Upload - Implementation Guide

## Overview
This guide documents the implementation of signed URL-based direct file upload to Google Cloud Storage (GCS). This approach allows clients to upload files directly to GCS without passing through the Laravel server, reducing server load and improving performance.

## What Was Implemented

### 1. Request Validation

#### `app/Http/Requests/UploadUrlRequest.php`
Validates upload URL requests with:
- **Required fields:** `type`, `mime`, `size`
- **Type validation:** Must be `image` or `audio`
- **MIME type validation:** Server-side validation for allowed formats
- **Size validation:** 
  - Images: max 2MB (2,097,152 bytes)
  - Audio: max 5MB (5,242,880 bytes)

#### `app/Http/Requests/UploadConfirmRequest.php`
Validates upload confirmation requests:
- **Required fields:** `file_id`, `type`, `path`
- **File ID validation:** Must be valid UUID
- **Type validation:** Must be `image` or `audio`

### 2. GCS Signed URL Service

#### `app/Services/GcsSignedUrlService.php`
Service class for generating signed URLs:
- **generateUploadUrl()**: Creates signed URL for PUT upload
  - Generates unique file ID (UUID)
  - Determines file path based on type
  - Creates signed URL with 10-minute expiry
  - Enforces content type validation

- **Path format:**
  - **Images:** `temp/{tenant_id}/images/{file_id}.{ext}`
  - **Audio:** `tenants/{tenant_id}/audio/{file_id}.{ext}`

- **Supported MIME types:**
  - **Images:** `image/jpeg`, `image/png`, `image/webp`
  - **Audio:** `audio/mpeg`, `audio/wav`, `audio/ogg`

- **Helper methods:**
  - `deleteFile()`: Delete file from GCS
  - `fileExists()`: Check if file exists
  - `getFileSize()`: Get file size from GCS

### 3. Upload Controller

#### `app/Http/Controllers/UploadController.php`
Handles signed URL generation and upload confirmation:

**getUploadUrl()** - `POST /upload/url`
- Validates file type, MIME type, and size
- Generates signed URL for direct GCS upload
- Returns upload URL, file ID, and metadata
- Logs all upload URL generation

**confirmUpload()** - `POST /upload/confirm`
- Verifies file exists in GCS
- Confirms file size
- Triggers image processing (placeholder for Phase 3)
- Audio files: stored as-is, no processing
- Logs confirmation events

**getCurrentLbbId()** - Helper method
- Retrieves tenant ID from request context or user
- Ensures multi-tenant isolation

### 4. Routes

Added to `routes/web.php`:
```php
Route::prefix('upload')->name('upload.')->controller(UploadController::class)->group(function () {
    Route::post('/url', 'getUploadUrl')->name('url');
    Route::post('/confirm', 'confirmUpload')->name('confirm');
});
```

## API Documentation

### POST /upload/url
Generate signed URL for file upload.

**Request Headers:**
```
Content-Type: application/json
Authorization: Bearer {token}
```

**Request Body:**
```json
{
  "type": "image",
  "mime": "image/jpeg",
  "size": 1048576
}
```

**Validation Rules:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| type | string | Yes | `image` or `audio` |
| mime | string | Yes | MIME type of the file |
| size | integer | Yes | File size in bytes |

**Validation Errors (422):**
```json
{
  "success": false,
  "message": "Validasi gagal",
  "errors": {
    "mime": ["Format file gambar tidak didukung. Gunakan format JPG, PNG, atau WebP."],
    "size": ["Ukuran file gambar terlalu besar. Maksimal 2MB."]
  }
}
```

**Success Response (200):**
```json
{
  "success": true,
  "message": "URL upload berhasil dibuat",
  "data": {
    "file_id": "550e8400-e29b-41d4-a716-446655440000",
    "upload_url": "https://storage.googleapis.com/...?signature=...",
    "path": "temp/123/images/550e8400-e29b-41d4-a716-446655440000.jpg",
    "method": "PUT",
    "expires_in": 600
  }
}
```

### POST /upload/confirm
Confirm successful file upload to GCS.

**Request Headers:**
```
Content-Type: application/json
Authorization: Bearer {token}
```

**Request Body:**
```json
{
  "file_id": "550e8400-e29b-41d4-a716-446655440000",
  "type": "image",
  "path": "temp/123/images/550e8400-e29b-41d4-a716-446655440000.jpg"
}
```

**Validation Rules:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| file_id | string (UUID) | Yes | Unique file identifier |
| type | string | Yes | `image` or `audio` |
| path | string | Yes | Full GCS path of uploaded file |

**Success Response (200):**
```json
{
  "success": true,
  "message": "Upload berhasil dikonfirmasi",
  "data": {
    "file_id": "550e8400-e29b-41d4-a716-446655440000",
    "path": "temp/123/images/550e8400-e29b-41d4-a716-446655440000.jpg",
    "size": 1048576,
    "type": "image"
  }
}
```

**Error Response (404):**
```json
{
  "success": false,
  "message": "File tidak ditemukan di server. Silakan upload ulang.",
  "data": null
}
```

## Client-Side Implementation

### Step 1: Get Upload URL
```javascript
async function getUploadUrl(file) {
  const response = await fetch('/upload/url', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${token}`,
    },
    body: JSON.stringify({
      type: file.type.startsWith('image') ? 'image' : 'audio',
      mime: file.type,
      size: file.size,
    }),
  });

  const data = await response.json();
  
  if (!data.success) {
    throw new Error(data.message);
  }

  return data.data;
}
```

### Step 2: Upload Directly to GCS
```javascript
async function uploadFileToGcs(file, uploadData) {
  const response = await fetch(uploadData.upload_url, {
    method: 'PUT',
    headers: {
      'Content-Type': uploadData.mime,
    },
    body: file,
  });

  if (!response.ok) {
    throw new Error('Upload gagal');
  }

  return uploadData;
}
```

### Step 3: Confirm Upload
```javascript
async function confirmUpload(uploadData) {
  const response = await fetch('/upload/confirm', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${token}`,
    },
    body: JSON.stringify({
      file_id: uploadData.file_id,
      type: uploadData.type,
      path: uploadData.path,
    }),
  });

  const data = await response.json();
  
  if (!data.success) {
    throw new Error(data.message);
  }

  return data.data;
}
```

### Complete Upload Flow
```javascript
async function uploadFile(file) {
  try {
    // Step 1: Get signed URL
    const uploadData = await getUploadUrl(file);
    
    // Step 2: Upload to GCS
    await uploadFileToGcs(file, uploadData);
    
    // Step 3: Confirm upload
    const result = await confirmUpload(uploadData);
    
    console.log('Upload successful:', result);
    return result;
  } catch (error) {
    console.error('Upload failed:', error);
    throw error;
  }
}
```

## Security Features

### 1. Server-Side Validation
- ✅ All validation happens server-side
- ✅ Client cannot bypass validation
- ✅ File type, MIME type, and size are validated BEFORE generating signed URL
- ✅ Invalid files never get a signed URL

### 2. Signed URL Security
- ✅ URLs are temporary (10-minute expiry)
- ✅ Enforced content type validation
- ✅ PUT method only (no read access)
- ✅ Bucket is private, no public URLs

### 3. Tenant Isolation
- ✅ Files are organized by tenant ID
- ✅ Users can only upload to their tenant's directory
- ✅ No cross-tenant access possible

### 4. Authentication
- ✅ Both endpoints require authentication
- ✅ Only authenticated users can get upload URLs
- ✅ File ownership is tied to authenticated user

## File Path Structure

### Temporary Storage (Images)
```
temp/
  └── {tenant_id}/
      └── images/
          └── {file_id}.{ext}
```
- Used for initial image upload
- Will be processed and moved in Phase 3
- Temporary directory can be cleaned periodically

### Final Storage (Audio)
```
tenants/
  └── {tenant_id}/
      └── audio/
          └── {file_id}.{ext}
```
- Audio files go directly to final location
- No processing needed
- Stored as-is

## Error Handling

### Common Error Responses

| Status | Code | Message |
|--------|-------|---------|
| 401 | Unauthorized | User not authenticated |
| 422 | Validation Error | Invalid input data |
| 400 | Bad Request | Tenant ID not found |
| 404 | Not Found | File not found in GCS |
| 500 | Server Error | Internal server error |

### Error Response Format
```json
{
  "success": false,
  "message": "Error description",
  "data": null
}
```

## Logging

All upload events are logged:
- **URL Generation:** Logs user, tenant, type, file ID
- **Confirmation:** Logs file ID, path, tenant
- **Errors:** Logs full error messages and stack traces

Logs can be found in `storage/logs/laravel.log`

## Benefits of This Approach

### 1. Reduced Server Load
- ✅ Files never pass through Laravel
- ✅ No disk I/O on server
- ✅ No bandwidth usage for file transfer

### 2. Better Performance
- ✅ Direct upload to GCS (faster)
- ✅ Parallel uploads possible
- ✅ Resumable uploads supported

### 3. Scalability
- ✅ Server handles only metadata
- ✅ Can handle many simultaneous uploads
- ✅ No file processing bottlenecks

### 4. Security
- ✅ Server-side validation enforced
- ✅ Temporary signed URLs
- ✅ Private bucket access
- ✅ Tenant isolation

### 5. Cost-Effective
- ✅ No egress charges for upload
- ✅ Reduced server resources
- ✅ Lower infrastructure costs

## Integration with Existing Code

### Backward Compatibility
- ✅ Existing `FileController` still works
- ✅ Old upload routes preserved
- ✅ Gradual migration possible

### Next Phase Integration
- ✅ Ready for Phase 3 (Image Processing)
- ✅ Temporary image storage ready for processing
- ✅ Audio files already in final location

## Testing Checklist

- [ ] Test with valid image file (JPEG, PNG, WebP)
- [ ] Test with valid audio file (MP3, WAV, OGG)
- [ ] Test file size validation (2MB for images, 5MB for audio)
- [ ] Test MIME type validation
- [ ] Test authentication requirement
- [ ] Test signed URL expiry (wait 10 minutes)
- [ ] Test file existence verification
- [ ] Test upload confirmation
- [ ] Test with different tenants
- [ ] Test error handling
- [ ] Test concurrent uploads

## Troubleshooting

### Issue: "GCS connection error"
**Solution:** Check GCS credentials in `.env`

### Issue: "Invalid MIME type"
**Solution:** Ensure client sends correct MIME type

### Issue: "File size too large"
**Solution:** Compress file before upload or increase limit

### Issue: "Signed URL expired"
**Solution:** Get new upload URL (URLs expire in 10 minutes)

### Issue: "File not found in GCS"
**Solution:** Ensure PUT upload completed before calling confirm

## Next Steps

After completing Phase 2:
1. ✅ Signed URL upload is implemented
2. ✅ Client can upload directly to GCS
3. ✅ Server-side validation enforced
4. ✅ Temporary storage ready for processing

Proceed to **Phase 3: Image Processing Pipeline**
- Process uploaded images with queue
- Resize, compress, and convert to WebP
- Move to final storage location