Skip to content

Chương 11: File Upload & Supabase Storage

"Minh: 'Team cần attach file vào task — mockups, specs, screenshots.' Tuấn: 'Supabase Storage giống S3 nhưng miễn phí 1GB. Setup trong 30 phút.'"


🎯 Mục tiêu

  • Upload files to Supabase Storage
  • File preview (images, PDFs)
  • Security: size limits, type validation
  • Storage policies (RLS for files)

Phần 1: Supabase Storage Setup (15 phút)

sql
-- Create storage bucket via Supabase Dashboard
-- Or via SQL:
INSERT INTO storage.buckets (id, name, public)
VALUES ('task-attachments', 'task-attachments', false);

-- Storage policies
CREATE POLICY "Authenticated users can upload"
ON storage.objects FOR INSERT
WITH CHECK (auth.role() = 'authenticated');

CREATE POLICY "Authenticated users can view"
ON storage.objects FOR SELECT
USING (auth.role() = 'authenticated');

Phần 2: Upload API (30 phút)

typescript
// src/routes/attachments.ts
import { Router } from 'express'
import multer from 'multer'
import { supabase } from '../lib/supabase'
import { requireAuth } from '../middleware/auth'

const router = Router()
router.use(requireAuth)

const upload = multer({
  limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
  fileFilter: (req, file, cb) => {
    const allowed = ['image/jpeg', 'image/png', 'image/webp',
                     'application/pdf', 'text/plain']
    cb(null, allowed.includes(file.mimetype))
  }
})

router.post('/tasks/:taskId/attachments', upload.single('file'), async (req, res) => {
  if (!req.file) return res.status(400).json({ error: 'No file provided' })

  const user = (req as any).user
  const path = `${req.params.taskId}/${Date.now()}-${req.file.originalname}`

  const { data, error } = await supabase.storage
    .from('task-attachments')
    .upload(path, req.file.buffer, {
      contentType: req.file.mimetype,
      upsert: false
    })

  if (error) return res.status(500).json({ error: error.message })

  // Save metadata to database
  await supabase.from('attachments').insert({
    task_id: req.params.taskId,
    file_path: data.path,
    file_name: req.file.originalname,
    file_size: req.file.size,
    mime_type: req.file.mimetype,
    uploaded_by: user.id
  })

  res.status(201).json({ path: data.path })
})

router.get('/tasks/:taskId/attachments', async (req, res) => {
  const { data } = await supabase
    .from('attachments')
    .select('*')
    .eq('task_id', req.params.taskId)
    .order('created_at', { ascending: false })

  res.json(data || [])
})

export default router

Phần 3: Frontend File Upload (25 phút)

javascript
// File upload UI component
function createUploadUI(taskId) {
  const dropZone = document.createElement('div')
  dropZone.className = 'drop-zone'
  dropZone.innerHTML = `
    <p>📎 Drag files here or <label class="upload-label">
      browse<input type="file" hidden accept="image/*,.pdf,.txt">
    </label></p>
    <div class="file-list"></div>
  `

  // Drag & Drop
  dropZone.addEventListener('dragover', e => {
    e.preventDefault()
    dropZone.classList.add('drag-over')
  })

  dropZone.addEventListener('drop', async e => {
    e.preventDefault()
    dropZone.classList.remove('drag-over')
    const file = e.dataTransfer.files[0]
    await uploadFile(taskId, file)
  })

  return dropZone
}

async function uploadFile(taskId, file) {
  const formData = new FormData()
  formData.append('file', file)

  const res = await fetch(`/api/tasks/${taskId}/attachments`, {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${getToken()}` },
    body: formData
  })

  if (res.ok) showToast('✅ File uploaded!')
  else showToast('❌ Upload failed', 'error')
}

Homework

  • [ ] File upload working (images + PDFs)
  • [ ] File list rendering
  • [ ] Size limit enforced (10MB)
  • [ ] Type validation (no executables)

Chương tiếp: Real-time — Team Làm Việc Cùng Lúc →

Powered by CodyMaster × VitePress