# Signed URL Upload API - Documentation

**System Flow:**
1. Client requests signed URL from Laravel
2. Client uploads **directly to GCS** (bypassing Laravel server!)
3. Client confirms upload to Laravel
4. (Optional) Laravel processes image if needed

---

## 📡 API ENDPOINTS

### 1. Get Upload URL
**Endpoint:** `POST /upload/url`

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

**Request Body:**
```json
{
  "type": "image",      // "image" or "audio"
  "mime": "image/jpeg", // MIME type
  "size": 1048576       // File size in bytes
}
```

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

**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."]
  }
}
```

---

### 2. Confirm Upload
**Endpoint:** `POST /upload/confirm`

**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"
}
```

**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"
  }
}
```

---

## 🎯 FILE TYPE VALIDATIONS

### Images
- **Max size:** 2MB (2,097,152 bytes)
- **Allowed types:**
  - `image/jpeg`
  - `image/png`
  - `image/webp`

### Audio
- **Max size:** 5MB (5,242,880 bytes)
- **Allowed types:**
  - `audio/mpeg`
  - `audio/wav`
  - `audio/ogg`

---

## 💻 FRONTEND EXAMPLES

### Using Fetch API

```javascript
// Step 1: Get Upload URL
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 || 'Gagal mendapatkan upload URL');
  }

  return data.data;
}

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

  if (!response.ok) {
    throw new Error(`Upload gagal: ${response.status}`);
  }

  return uploadData;
}

// Step 3: Confirm Upload
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 || 'Gagal mengkonfirmasi upload');
  }

  return data.data;
}

// Complete Upload Flow
async function uploadFile(file) {
  try {
    // Step 1: Get signed URL
    const uploadData = await getUploadUrl(file);
    console.log('Upload URL obtained:', uploadData);

    // Step 2: Upload to GCS
    await uploadFileToGcs(file, uploadData);
    console.log('File uploaded to GCS');

    // Step 3: Confirm upload
    const result = await confirmUpload(uploadData);
    console.log('Upload confirmed:', result);

    return result;
  } catch (error) {
    console.error('Upload failed:', error);
    throw error;
  }
}

// Usage
const fileInput = document.querySelector('#file-input');
fileInput.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  if (!file) return;

  try {
    const result = await uploadFile(file);
    alert(`Upload berhasil! File ID: ${result.file_id}`);
  } catch (error) {
    alert(`Upload gagal: ${error.message}`);
  }
});
```

---

### Using Axios

```javascript
import axios from 'axios';

const api = axios.create({
  baseURL: '/api',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${token}`,
  },
});

// Get Upload URL
async function getUploadUrl(file) {
  const { data } = await api.post('/upload/url', {
    type: file.type.startsWith('image') ? 'image' : 'audio',
    mime: file.type,
    size: file.size,
  });

  if (!data.success) {
    throw new Error(data.message);
  }

  return data.data;
}

// Upload to GCS
async function uploadFileToGcs(file, uploadData) {
  await axios.put(uploadData.upload_url, file, {
    headers: {
      'Content-Type': uploadData.mime,
    },
  });

  return uploadData;
}

// Confirm Upload
async function confirmUpload(uploadData) {
  const { data } = await api.post('/upload/confirm', {
    file_id: uploadData.file_id,
    type: uploadData.type,
    path: uploadData.path,
  });

  if (!data.success) {
    throw new Error(data.message);
  }

  return data.data;
}

// Complete flow
async function uploadFile(file) {
  const uploadData = await getUploadUrl(file);
  await uploadFileToGcs(file, uploadData);
  const result = await confirmUpload(uploadData);
  return result;
}

export default uploadFile;
```

---

### React Component Example

```jsx
import React, { useState } from 'react';

function ImageUpload({ onUploadComplete }) {
  const [uploading, setUploading] = useState(false);
  const [progress, setProgress] = useState(0);

  const uploadFile = async (file) => {
    setUploading(true);
    setProgress(0);

    try {
      // Step 1: Get upload URL
      setProgress(25);
      const uploadUrlResponse = await fetch('/upload/url', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${localStorage.getItem('token')}`,
        },
        body: JSON.stringify({
          type: 'image',
          mime: file.type,
          size: file.size,
        }),
      }).then(r => r.json());

      if (!uploadUrlResponse.success) {
        throw new Error(uploadUrlResponse.message);
      }

      // Step 2: Upload to GCS
      setProgress(50);
      const uploadData = uploadUrlResponse.data;

      await fetch(uploadData.upload_url, {
        method: 'PUT',
        headers: {
          'Content-Type': file.type,
        },
        body: file,
      });

      // Step 3: Confirm upload
      setProgress(75);
      const confirmResponse = await fetch('/upload/confirm', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${localStorage.getItem('token')}`,
        },
        body: JSON.stringify({
          file_id: uploadData.file_id,
          type: 'image',
          path: uploadData.path,
        }),
      }).then(r => r.json());

      if (!confirmResponse.success) {
        throw new Error(confirmResponse.message);
      }

      setProgress(100);
      onUploadComplete(confirmResponse.data);
    } catch (error) {
      console.error('Upload error:', error);
      alert(`Upload gagal: ${error.message}`);
    } finally {
      setUploading(false);
    }
  };

  return (
    <div>
      <input
        type="file"
        accept="image/jpeg,image/png,image/webp"
        onChange={(e) => {
          const file = e.target.files[0];
          if (file) uploadFile(file);
        }}
        disabled={uploading}
      />

      {uploading && (
        <div>
          <p>Uploading... {progress}%</p>
          <progress value={progress} max="100" />
        </div>
      )}
    </div>
  );
}

export default ImageUpload;
```

---

## 🔒 SECURITY FEATURES

✅ **Server-side validation** - All validation happens on server
✅ **Type checking** - Only allowed MIME types accepted
✅ **Size limits** - 2MB for images, 5MB for audio
✅ **Temporary URLs** - Signed URLs expire in 10 minutes
✅ **Private bucket** - No public URLs exposed
✅ **Tenant isolation** - Files organized by tenant_id
✅ **Content type enforcement** - GCS validates content type

---

## 📂 FILE PATH STRUCTURE

### Images (Temporary)
```
temp/
  └── {tenant_id}/
      └── images/
          └── {file_id}.{ext}
```

### Audio (Final)
```
tenants/
  └── {tenant_id}/
      └── audio/
          └── {file_id}.{ext}
```

---

## ⚠️ IMPORTANT NOTES

1. **URL Expiry:** Signed URLs expire in 10 minutes
2. **PUT Method:** Upload MUST use PUT method
3. **Content-Type:** MUST match the MIME type requested
4. **File Size:** Client should validate size BEFORE requesting URL
5. **Error Handling:** Always check `success` field in response
6. **Authentication:** Both endpoints require valid auth token

---

## 🧪 TESTING

Test the API with the provided commands:

```bash
# Test signed URL generation
php artisan test:signed-url-upload

# Test GCS connection
php artisan storage:test-gcs
```

---

## 📞 SUPPORT

For issues or questions:
1. Check logs: `storage/logs/laravel.log`
2. Test with commands above
3. Verify GCS credentials in `.env`
4. Ensure bucket exists and is accessible

---

## ✅ BENEFITS

- ✅ **Reduced server load** - Files bypass Laravel server
- ✅ **Faster uploads** - Direct to GCS
- ✅ **Scalable** - Handle multiple concurrent uploads
- ✅ **Secure** - Server-side validation + private bucket
- ✅ **Cost-effective** - No server bandwidth usage
