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 routerPhầ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 →