feat: add template upload/download/delete endpoints

Belge Şablonları now stores real files: POST /api/templates/upload
(multer -> Supabase Storage under templates/<user>/, then a templates
row), GET /api/templates/:id/download-url (short-lived signed URL),
and DELETE /api/templates/:id. Mounted at /api/templates.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
mstfyldz
2026-08-09 19:50:46 +03:00
co-authored by Claude Sonnet 5
parent 99d4d770d5
commit af5a03db92
3 changed files with 118 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
import { Response } from 'express';
import { supabase } from '../lib/supabase';
import { AuthenticatedRequest } from '../middleware/auth';
// Belge Şablonları: avukatın büro genelinde kullandığı dilekçe/evrak şablonları.
// Ayrı bir Storage bucket açmaya gerek kalmasın diye documents'ın kullandığı
// "case-documents" bucket'ı "templates/" öneki altında paylaşılıyor.
export const uploadTemplate = async (req: AuthenticatedRequest, res: Response) => {
try {
const file = req.file;
const userId = req.user?.id;
const { category, name, description } = req.body || {};
if (!file || !userId || !name) {
return res.status(400).json({ error: 'file ve name zorunludur.' });
}
const storagePath = `templates/${userId}/${Date.now()}_${file.originalname}`;
const { error: uploadError } = await supabase.storage
.from('case-documents')
.upload(storagePath, file.buffer, { contentType: file.mimetype });
if (uploadError) {
console.error('Template Storage Upload Error:', uploadError);
return res.status(500).json({ error: 'Şablon dosyası yüklenemedi.' });
}
const { data, error } = await supabase
.from('templates')
.insert([{
user_id: userId,
category: category || null,
name: String(name).slice(0, 200),
description: description || null,
storage_path: storagePath,
file_size: file.size,
mime_type: file.mimetype,
}])
.select()
.single();
if (error) throw error;
res.json({ template: data });
} catch (error: any) {
console.error('Upload Template Error:', error);
res.status(500).json({ error: error.message || 'Internal Server Error' });
}
};
// İndirme için kısa ömürlü bir imzalı URL üretir — bucket public değil.
export const getTemplateDownloadUrl = async (req: AuthenticatedRequest, res: Response) => {
try {
const { templateId } = req.params;
const userId = req.user?.id;
const { data: template, error: fetchError } = await supabase
.from('templates')
.select('storage_path, user_id')
.eq('id', templateId)
.single();
if (fetchError || !template) {
return res.status(404).json({ error: 'Şablon bulunamadı.' });
}
if (template.user_id !== userId) {
return res.status(403).json({ error: 'Bu şablona erişim yetkiniz yok.' });
}
const { data, error } = await supabase.storage
.from('case-documents')
.createSignedUrl(template.storage_path, 60);
if (error || !data) throw error || new Error('İmzalı URL üretilemedi.');
res.json({ url: data.signedUrl });
} catch (error: any) {
console.error('Get Template Download URL Error:', error);
res.status(500).json({ error: error.message || 'Internal Server Error' });
}
};
export const deleteTemplate = async (req: AuthenticatedRequest, res: Response) => {
try {
const { templateId } = req.params;
const userId = req.user?.id;
const { data: template, error: fetchError } = await supabase
.from('templates')
.select('storage_path, user_id')
.eq('id', templateId)
.single();
if (fetchError || !template) {
return res.status(404).json({ error: 'Şablon bulunamadı.' });
}
if (template.user_id !== userId) {
return res.status(403).json({ error: 'Bu şablonu silme yetkiniz yok.' });
}
await supabase.storage.from('case-documents').remove([template.storage_path]);
const { error } = await supabase.from('templates').delete().eq('id', templateId);
if (error) throw error;
res.json({ ok: true });
} catch (error: any) {
console.error('Delete Template Error:', error);
res.status(500).json({ error: error.message || 'Internal Server Error' });
}
};
+2
View File
@@ -3,6 +3,7 @@ import documentRoutes from './document.routes';
import chatRoutes from './chat.routes';
import precedentRoutes from './precedent.routes';
import caseRoutes from './case.routes';
import templateRoutes from './template.routes';
const router = Router();
@@ -13,6 +14,7 @@ router.get('/health', (req, res) => {
router.use('/documents', documentRoutes);
router.use('/chat', chatRoutes);
router.use('/cases', caseRoutes);
router.use('/templates', templateRoutes);
router.use('/', precedentRoutes); // Keeps the same paths like /api/search-precedents
export default router;
+13
View File
@@ -0,0 +1,13 @@
import { Router } from 'express';
import multer from 'multer';
import { requireAuth } from '../middleware/auth';
import { uploadTemplate, getTemplateDownloadUrl, deleteTemplate } from '../controllers/template.controller';
const router = Router();
const upload = multer({ storage: multer.memoryStorage() });
router.post('/upload', upload.single('file'), requireAuth, uploadTemplate);
router.get('/:templateId/download-url', requireAuth, getTemplateDownloadUrl);
router.delete('/:templateId', requireAuth, deleteTemplate);
export default router;