- aggiornamento di tante cose...

- generazione Volantini
- pagina RIS
This commit is contained in:
Surya Paolo
2025-12-17 10:07:51 +01:00
parent 037ff6f7f9
commit 3d87c336de
43 changed files with 7123 additions and 518 deletions

View File

@@ -0,0 +1,398 @@
const Asset = require('../models/Asset');
const imageGenerator = require('../services/imageGenerator');
const sharp = require('sharp');
const path = require('path');
const fs = require('fs').promises;
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads';
const assetController = {
// POST /assets/upload
async upload(req, res) {
try {
if (!req.file) {
return res.status(400).json({
success: false,
error: 'Nessun file caricato'
});
}
const { category = 'other', tags, description, isReusable = true } = req.body;
const file = req.file;
// Ottieni dimensioni immagine
let dimensions = {};
try {
const metadata = await sharp(file.path).metadata();
dimensions = { width: metadata.width, height: metadata.height };
} catch (e) {
console.warn('Cannot read image dimensions');
}
// Genera thumbnail
const thumbDir = path.join(UPLOAD_DIR, 'thumbs');
await fs.mkdir(thumbDir, { recursive: true });
const thumbName = `thumb_${file.filename}`;
const thumbPath = path.join(thumbDir, thumbName);
try {
await sharp(file.path)
.resize(300, 300, { fit: 'cover' })
.jpeg({ quality: 80 })
.toFile(thumbPath);
} catch (e) {
console.warn('Cannot create thumbnail');
}
const asset = new Asset({
type: 'image',
category,
sourceType: 'upload',
file: {
path: file.path,
url: `/uploads/${file.filename}`,
thumbnailPath: thumbPath,
thumbnailUrl: `/uploads/thumbs/${thumbName}`,
originalName: file.originalname,
mimeType: file.mimetype,
size: file.size,
dimensions
},
metadata: {
userId: req.user._id,
tags: tags ? tags.split(',').map(t => t.trim()) : [],
description,
isReusable: isReusable === 'true' || isReusable === true
}
});
await asset.save();
res.status(201).json({
success: true,
data: asset
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// POST /assets/upload-multiple
async uploadMultiple(req, res) {
try {
if (!req.files || req.files.length === 0) {
return res.status(400).json({
success: false,
error: 'Nessun file caricato'
});
}
const { category = 'other' } = req.body;
const assets = [];
for (const file of req.files) {
let dimensions = {};
try {
const metadata = await sharp(file.path).metadata();
dimensions = { width: metadata.width, height: metadata.height };
} catch (e) {}
const asset = new Asset({
type: 'image',
category,
sourceType: 'upload',
file: {
path: file.path,
url: `/uploads/${file.filename}`,
originalName: file.originalname,
mimeType: file.mimetype,
size: file.size,
dimensions
},
metadata: {
userId: req.user._id
}
});
await asset.save();
assets.push(asset);
}
res.status(201).json({
success: true,
data: assets
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// POST /assets/generate-ai
async generateAi(req, res) {
try {
const {
prompt,
negativePrompt,
provider = 'hf',
category = 'other',
aspectRatio = '9:16',
model,
seed,
steps,
cfg
} = req.body;
if (!prompt) {
return res.status(400).json({
success: false,
error: 'Prompt richiesto'
});
}
const startTime = Date.now();
const imageUrl = await imageGenerator.generate(provider, prompt, {
negativePrompt,
aspectRatio,
model,
seed,
steps,
cfg
});
const generationTime = Date.now() - startTime;
// Salva file
const fileName = `ai_${Date.now()}_${Math.random().toString(36).substr(2, 9)}.jpg`;
const filePath = path.join(UPLOAD_DIR, 'ai-generated', fileName);
await fs.mkdir(path.dirname(filePath), { recursive: true });
let fileSize = 0;
let dimensions = {};
if (imageUrl.startsWith('data:')) {
const base64Data = imageUrl.replace(/^data:image\/\w+;base64,/, '');
const buffer = Buffer.from(base64Data, 'base64');
await fs.writeFile(filePath, buffer);
fileSize = buffer.length;
const metadata = await sharp(buffer).metadata();
dimensions = { width: metadata.width, height: metadata.height };
} else {
const fetch = require('node-fetch');
const response = await fetch(imageUrl);
const buffer = await response.buffer();
await fs.writeFile(filePath, buffer);
fileSize = buffer.length;
const metadata = await sharp(buffer).metadata();
dimensions = { width: metadata.width, height: metadata.height };
}
const asset = new Asset({
type: 'image',
category,
sourceType: 'ai',
file: {
path: filePath,
url: `/uploads/ai-generated/${fileName}`,
mimeType: 'image/jpeg',
size: fileSize,
dimensions
},
aiGeneration: {
prompt,
negativePrompt,
provider,
model,
seed,
steps,
cfg,
requestedSize: aspectRatio,
generationTime
},
metadata: {
userId: req.user._id,
isReusable: true
}
});
await asset.save();
res.status(201).json({
success: true,
data: asset
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// GET /assets
async list(req, res) {
try {
const {
category,
sourceType,
page = 1,
limit = 50
} = req.query;
const query = {
'metadata.userId': req.user._id,
status: 'ready'
};
if (category) query.category = category;
if (sourceType) query.sourceType = sourceType;
const [assets, total] = await Promise.all([
Asset.find(query)
.sort({ createdAt: -1 })
.skip((page - 1) * limit)
.limit(parseInt(limit)),
Asset.countDocuments(query)
]);
res.json({
success: true,
data: assets,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// GET /assets/:id
async getById(req, res) {
try {
const asset = await Asset.findById(req.params.id);
if (!asset) {
return res.status(404).json({
success: false,
error: 'Asset non trovato'
});
}
res.json({
success: true,
data: asset
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// GET /assets/:id/file
async getFile(req, res) {
try {
const asset = await Asset.findById(req.params.id);
if (!asset || !asset.file?.path) {
return res.status(404).json({
success: false,
error: 'File non trovato'
});
}
res.sendFile(path.resolve(asset.file.path));
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// GET /assets/:id/thumbnail
async getThumbnail(req, res) {
try {
const asset = await Asset.findById(req.params.id);
if (!asset) {
return res.status(404).json({
success: false,
error: 'Asset non trovato'
});
}
const thumbPath = asset.file?.thumbnailPath || asset.file?.path;
if (!thumbPath) {
return res.status(404).json({
success: false,
error: 'Thumbnail non disponibile'
});
}
res.sendFile(path.resolve(thumbPath));
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// DELETE /assets/:id
async delete(req, res) {
try {
const asset = await Asset.findById(req.params.id);
if (!asset) {
return res.status(404).json({
success: false,
error: 'Asset non trovato'
});
}
if (asset.metadata.userId.toString() !== req.user._id.toString()) {
return res.status(403).json({
success: false,
error: 'Non autorizzato'
});
}
// Elimina file
try {
if (asset.file?.path) await fs.unlink(asset.file.path);
if (asset.file?.thumbnailPath) await fs.unlink(asset.file.thumbnailPath);
} catch (e) {
console.warn('File deletion warning:', e.message);
}
await Asset.deleteOne({ _id: asset._id });
res.json({
success: true,
message: 'Asset eliminato'
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
}
};
module.exports = assetController;

View File

@@ -0,0 +1,647 @@
const Poster = require('../models/Poster');
const Template = require('../models/Template');
const Asset = require('../models/Asset');
const posterRenderer = require('../services/posterRenderer');
const imageGenerator = require('../services/imageGenerator');
const path = require('path');
const fs = require('fs').promises;
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads';
const posterController = {
// POST /posters
async create(req, res) {
try {
const {
templateId,
name,
description,
content,
assets,
layerOverrides,
autoRender = false
} = req.body;
// Carica template
const template = await Template.findById(templateId);
if (!template) {
return res.status(404).json({
success: false,
error: 'Template non trovato'
});
}
// Valida contenuti richiesti
const requiredLayers = template.layers.filter(l => l.required);
for (const layer of requiredLayers) {
if (layer.type === 'title' && !content?.title) {
return res.status(400).json({
success: false,
error: `Campo richiesto: ${layer.type}`
});
}
}
const poster = new Poster({
templateId,
templateSnapshot: template.toObject(), // Snapshot per retrocompatibilità
name: name || content?.title || 'Nuova Locandina',
description,
status: 'draft',
content: content || {},
assets: assets || {},
layerOverrides: layerOverrides || {},
renderEngineVersion: '1.0.0',
metadata: {
userId: req.user._id
}
});
await poster.save();
// Incrementa uso template
await template.incrementUsage();
// Auto-render se richiesto
if (autoRender) {
await posterController._renderPoster(poster);
await poster.save();
}
res.status(201).json({
success: true,
data: poster
});
} catch (error) {
console.error('Poster create error:', error);
res.status(500).json({
success: false,
error: error.message
});
}
},
// GET /posters
async list(req, res) {
try {
const {
status,
templateId,
search,
page = 1,
limit = 20,
sortBy = 'createdAt',
sortOrder = 'desc'
} = req.query;
const query = { 'metadata.userId': req.user._id };
if (status) query.status = status;
if (templateId) query.templateId = templateId;
if (search) query.$text = { $search: search };
const sort = { [sortBy]: sortOrder === 'asc' ? 1 : -1 };
const [posters, total] = await Promise.all([
Poster.find(query)
.populate('templateId', 'name templateType thumbnailUrl')
.sort(sort)
.skip((page - 1) * limit)
.limit(parseInt(limit))
.select('-templateSnapshot -history'),
Poster.countDocuments(query)
]);
res.json({
success: true,
data: posters,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// GET /posters/favorites
async listFavorites(req, res) {
try {
const posters = await Poster.findFavorites(req.user._id);
res.json({
success: true,
data: posters
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// GET /posters/recent
async listRecent(req, res) {
try {
const { limit = 10 } = req.query;
const posters = await Poster.findRecent(req.user._id, parseInt(limit));
res.json({
success: true,
data: posters
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// GET /posters/:id
async getById(req, res) {
try {
const poster = await Poster.findById(req.params.id)
.populate('templateId')
.populate('assets.backgroundImage.assetId')
.populate('assets.mainImage.assetId')
.populate('assets.logos.assetId');
if (!poster) {
return res.status(404).json({
success: false,
error: 'Poster non trovato'
});
}
// Check ownership
if (poster.metadata.userId.toString() !== req.user._id.toString()) {
return res.status(403).json({
success: false,
error: 'Accesso negato'
});
}
res.json({
success: true,
data: poster
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// PUT /posters/:id
async update(req, res) {
try {
const poster = await Poster.findById(req.params.id);
if (!poster) {
return res.status(404).json({
success: false,
error: 'Poster non trovato'
});
}
if (poster.metadata.userId.toString() !== req.user._id.toString()) {
return res.status(403).json({
success: false,
error: 'Non autorizzato'
});
}
const updateFields = [
'name', 'description', 'content', 'assets', 'layerOverrides'
];
updateFields.forEach(field => {
if (req.body[field] !== undefined) {
poster[field] = req.body[field];
}
});
// Invalida render precedente se contenuto modificato
if (req.body.content || req.body.assets || req.body.layerOverrides) {
poster.status = 'draft';
poster.addHistory('updated', { fields: Object.keys(req.body) });
}
await poster.save();
res.json({
success: true,
data: poster
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// DELETE /posters/:id
async delete(req, res) {
try {
const poster = await Poster.findById(req.params.id);
if (!poster) {
return res.status(404).json({
success: false,
error: 'Poster non trovato'
});
}
if (poster.metadata.userId.toString() !== req.user._id.toString()) {
return res.status(403).json({
success: false,
error: 'Non autorizzato'
});
}
// Elimina file renderizzati
if (poster.renderOutput) {
const filesToDelete = [
poster.renderOutput.png?.path,
poster.renderOutput.jpg?.path,
poster.renderOutput.webp?.path
].filter(Boolean);
for (const filePath of filesToDelete) {
try {
await fs.unlink(filePath);
} catch (e) {
console.warn('File not found:', filePath);
}
}
}
await Poster.deleteOne({ _id: poster._id });
res.json({
success: true,
message: 'Poster eliminato'
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// POST /posters/:id/render
async render(req, res) {
try {
const poster = await Poster.findById(req.params.id)
.populate('templateId');
if (!poster) {
return res.status(404).json({
success: false,
error: 'Poster non trovato'
});
}
if (poster.metadata.userId.toString() !== req.user._id.toString()) {
return res.status(403).json({
success: false,
error: 'Non autorizzato'
});
}
poster.status = 'processing';
await poster.save();
try {
await posterController._renderPoster(poster);
await poster.save();
res.json({
success: true,
data: {
status: poster.status,
renderOutput: poster.renderOutput
}
});
} catch (renderError) {
poster.setError(renderError.message);
await poster.save();
throw renderError;
}
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// POST /posters/:id/regenerate-ai
async regenerateAi(req, res) {
try {
const { assetType, prompt, provider = 'hf' } = req.body;
const poster = await Poster.findById(req.params.id);
if (!poster) {
return res.status(404).json({
success: false,
error: 'Poster non trovato'
});
}
if (poster.metadata.userId.toString() !== req.user._id.toString()) {
return res.status(403).json({
success: false,
error: 'Non autorizzato'
});
}
// Genera nuova immagine AI
const startTime = Date.now();
const imageUrl = await imageGenerator.generate(provider, prompt);
const generationTime = Date.now() - startTime;
// Salva su filesystem
const fileName = `${poster._id}_${assetType}_${Date.now()}.jpg`;
const filePath = path.join(UPLOAD_DIR, 'ai-generated', fileName);
await fs.mkdir(path.dirname(filePath), { recursive: true });
// Se è base64, converti
let savedPath;
if (imageUrl.startsWith('data:')) {
const base64Data = imageUrl.replace(/^data:image\/\w+;base64,/, '');
await fs.writeFile(filePath, base64Data, 'base64');
savedPath = filePath;
} else {
// Se è URL, scarica
const fetch = require('node-fetch');
const response = await fetch(imageUrl);
const buffer = await response.buffer();
await fs.writeFile(filePath, buffer);
savedPath = filePath;
}
// Aggiorna asset nel poster
const assetData = {
sourceType: 'ai',
url: `/uploads/ai-generated/${fileName}`,
mimeType: 'image/jpeg',
aiParams: {
prompt,
provider,
generatedAt: new Date()
}
};
if (assetType === 'backgroundImage') {
poster.assets.backgroundImage = assetData;
poster.addHistory('ai_background_generated', { provider, duration: generationTime });
} else if (assetType === 'mainImage') {
poster.assets.mainImage = assetData;
poster.addHistory('ai_main_generated', { provider, duration: generationTime });
}
poster.status = 'draft';
await poster.save();
res.json({
success: true,
data: {
assetType,
asset: assetData
}
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// GET /posters/:id/download/:format
async download(req, res) {
try {
const { format } = req.params;
const poster = await Poster.findById(req.params.id);
if (!poster) {
return res.status(404).json({
success: false,
error: 'Poster non trovato'
});
}
const outputFile = poster.renderOutput?.[format];
if (!outputFile?.path) {
return res.status(404).json({
success: false,
error: `Formato ${format} non disponibile`
});
}
// Incrementa download count
await poster.incrementDownload();
const fileName = `${poster.name.replace(/[^a-z0-9]/gi, '_')}_poster.${format}`;
res.download(outputFile.path, fileName);
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// POST /posters/:id/favorite
async toggleFavorite(req, res) {
try {
const poster = await Poster.findById(req.params.id);
if (!poster) {
return res.status(404).json({
success: false,
error: 'Poster non trovato'
});
}
if (poster.metadata.userId.toString() !== req.user._id.toString()) {
return res.status(403).json({
success: false,
error: 'Non autorizzato'
});
}
await poster.toggleFavorite();
res.json({
success: true,
data: {
isFavorite: poster.metadata.isFavorite
}
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// POST /posters/quick-generate (compatibile con la tua bozza)
async quickGenerate(req, res) {
try {
const {
templateId,
titolo,
descrizione,
data,
ora,
luogo,
contatti,
fotoDescrizione,
stile,
provider = 'hf',
aspectRatio = '9:16'
} = req.body;
// Validazione base
if (!titolo || !data || !luogo) {
return res.status(400).json({
success: false,
error: 'Compila titolo, data e luogo'
});
}
// Usa template default o quello specificato
let template;
if (templateId) {
template = await Template.findById(templateId);
} else {
// Template default per quick-generate
template = await Template.findOne({
templateType: 'quick-generate',
isActive: true
});
}
// Genera prompt per AI background
const aiPrompt = `Vertical event poster background, ${stile || 'modern style, vivid colors'}. Subject: ${fotoDescrizione || 'abstract artistic shapes'}. Composition: Central empty space suitable for text overlay. NO TEXT, NO LETTERS, clean illustration, high quality, 4k.`;
// Genera immagine AI
const startTime = Date.now();
const rawImageUrl = await imageGenerator.generate(provider, aiPrompt);
const generationTime = Date.now() - startTime;
// Salva asset generato
const fileName = `quick_${Date.now()}.jpg`;
const filePath = path.join(UPLOAD_DIR, 'ai-generated', fileName);
await fs.mkdir(path.dirname(filePath), { recursive: true });
if (rawImageUrl.startsWith('data:')) {
const base64Data = rawImageUrl.replace(/^data:image\/\w+;base64,/, '');
await fs.writeFile(filePath, base64Data, 'base64');
}
// Crea poster
const poster = new Poster({
templateId: template?._id,
name: titolo,
status: 'processing',
content: {
title: titolo,
subtitle: descrizione,
eventDate: data,
eventTime: ora,
location: luogo,
contacts: contatti
},
assets: {
backgroundImage: {
sourceType: 'ai',
url: `/uploads/ai-generated/${fileName}`,
mimeType: 'image/jpeg',
aiParams: {
prompt: aiPrompt,
provider,
generatedAt: new Date()
}
}
},
originalPrompt: aiPrompt,
styleUsed: stile,
aspectRatio,
provider,
metadata: {
userId: req.user._id
}
});
poster.addHistory('ai_background_generated', { provider, duration: generationTime });
// Render con testi sovrapposti
await posterController._renderPoster(poster, { useQuickRender: true });
await poster.save();
res.json({
success: true,
data: {
posterId: poster._id,
imageUrl: poster.renderOutput?.png?.url || rawImageUrl,
status: poster.status
}
});
} catch (error) {
console.error('Quick generate error:', error);
res.status(500).json({
success: false,
error: error.message
});
}
},
// Helper interno: renderizza poster
async _renderPoster(poster, options = {}) {
const template = poster.templateId || poster.templateSnapshot;
const result = await posterRenderer.render({
template,
content: poster.content,
assets: poster.assets,
layerOverrides: Object.fromEntries(poster.layerOverrides || new Map()),
outputDir: path.join(UPLOAD_DIR, 'posters', 'final'),
posterId: poster._id.toString()
});
poster.setRenderOutput({
png: {
path: result.pngPath,
url: `/uploads/posters/final/${path.basename(result.pngPath)}`,
size: result.pngSize
},
jpg: {
path: result.jpgPath,
url: `/uploads/posters/final/${path.basename(result.jpgPath)}`,
size: result.jpgSize,
quality: 95
},
dimensions: result.dimensions,
duration: result.duration
});
}
};
module.exports = posterController;

View File

@@ -0,0 +1,383 @@
const Template = require('../models/Template');
// Presets formati standard
const FORMAT_PRESETS = {
'A4': { width: 2480, height: 3508, dpi: 300 },
'A4-landscape': { width: 3508, height: 2480, dpi: 300 },
'A3': { width: 3508, height: 4961, dpi: 300 },
'A3-landscape': { width: 4961, height: 3508, dpi: 300 },
'instagram-post': { width: 1080, height: 1080, dpi: 72 },
'instagram-story': { width: 1080, height: 1920, dpi: 72 },
'instagram-portrait': { width: 1080, height: 1350, dpi: 72 },
'facebook-post': { width: 1200, height: 630, dpi: 72 },
'facebook-event': { width: 1920, height: 1080, dpi: 72 },
'twitter-post': { width: 1200, height: 675, dpi: 72 },
'poster-24x36': { width: 7200, height: 10800, dpi: 300 },
'flyer-5x7': { width: 1500, height: 2100, dpi: 300 }
};
const templateController = {
// POST /templates
async create(req, res) {
try {
const {
name,
templateType,
description,
format,
safeArea,
backgroundColor,
layers,
logoSlots,
palette,
typography,
defaultAiPromptHints,
metadata
} = req.body;
// Applica preset se specificato
let finalFormat = format;
if (format?.preset && FORMAT_PRESETS[format.preset]) {
finalFormat = {
...FORMAT_PRESETS[format.preset],
preset: format.preset,
unit: 'px'
};
}
// Valida layers
if (!layers || !Array.isArray(layers) || layers.length === 0) {
return res.status(400).json({
success: false,
error: 'Almeno un layer è richiesto'
});
}
// Assicura ID unici per layer
const layersWithIds = layers.map((layer, idx) => ({
...layer,
id: layer.id || `layer_${layer.type}_${idx}`
}));
const template = new Template({
name,
templateType,
description,
format: finalFormat,
safeArea: safeArea || {},
backgroundColor: backgroundColor || '#1a1a2e',
layers: layersWithIds,
logoSlots: logoSlots || { enabled: false, slots: [] },
palette: palette || {},
typography: typography || {},
defaultAiPromptHints: defaultAiPromptHints || {},
metadata: {
...metadata,
author: req.user?.name || 'System'
},
userId: req.user?._id
});
await template.save();
res.status(201).json({
success: true,
data: template
});
} catch (error) {
console.error('Template create error:', error);
res.status(500).json({
success: false,
error: error.message
});
}
},
// GET /templates
async list(req, res) {
try {
const {
type,
search,
tags,
page = 1,
limit = 20,
sortBy = 'createdAt',
sortOrder = 'desc'
} = req.query;
const query = { isActive: true };
if (type) {
query.templateType = type;
}
if (tags) {
const tagArray = tags.split(',').map(t => t.trim());
query['metadata.tags'] = { $in: tagArray };
}
if (search) {
query.$text = { $search: search };
}
// Se utente autenticato, mostra anche i suoi privati
if (req.user) {
query.$or = [
{ 'metadata.isPublic': true },
{ userId: req.user._id }
];
} else {
query['metadata.isPublic'] = true;
}
const sort = { [sortBy]: sortOrder === 'asc' ? 1 : -1 };
const [templates, total] = await Promise.all([
Template.find(query)
.sort(sort)
.skip((page - 1) * limit)
.limit(parseInt(limit))
.select('-layers -logoSlots'), // Escludi dati pesanti per list
Template.countDocuments(query)
]);
res.json({
success: true,
data: templates,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
});
} catch (error) {
console.error('Template list error:', error);
res.status(500).json({
success: false,
error: error.message
});
}
},
// GET /templates/types
async getTypes(req, res) {
try {
const types = await Template.distinct('templateType', { isActive: true });
res.json({
success: true,
data: types.sort()
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// GET /templates/presets
async getFormatPresets(req, res) {
res.json({
success: true,
data: FORMAT_PRESETS
});
},
// GET /templates/:id
async getById(req, res) {
try {
const template = await Template.findById(req.params.id);
if (!template) {
return res.status(404).json({
success: false,
error: 'Template non trovato'
});
}
// Check accesso
if (!template.metadata.isPublic &&
(!req.user || template.userId?.toString() !== req.user._id.toString())) {
return res.status(403).json({
success: false,
error: 'Accesso negato'
});
}
res.json({
success: true,
data: template
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// PUT /templates/:id
async update(req, res) {
try {
const template = await Template.findById(req.params.id);
if (!template) {
return res.status(404).json({
success: false,
error: 'Template non trovato'
});
}
// Check ownership
if (template.userId?.toString() !== req.user._id.toString()) {
return res.status(403).json({
success: false,
error: 'Non autorizzato a modificare questo template'
});
}
const updateFields = [
'name', 'description', 'templateType', 'format', 'safeArea',
'backgroundColor', 'layers', 'logoSlots', 'palette',
'typography', 'defaultAiPromptHints', 'metadata', 'isActive'
];
updateFields.forEach(field => {
if (req.body[field] !== undefined) {
template[field] = req.body[field];
}
});
// Incrementa versione
if (template.metadata) {
const version = template.metadata.version || '1.0.0';
const parts = version.split('.').map(Number);
parts[2]++;
template.metadata.version = parts.join('.');
}
await template.save();
res.json({
success: true,
data: template
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// DELETE /templates/:id
async delete(req, res) {
try {
const template = await Template.findById(req.params.id);
if (!template) {
return res.status(404).json({
success: false,
error: 'Template non trovato'
});
}
if (template.userId?.toString() !== req.user._id.toString()) {
return res.status(403).json({
success: false,
error: 'Non autorizzato'
});
}
// Soft delete
template.isActive = false;
await template.save();
res.json({
success: true,
message: 'Template eliminato'
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// POST /templates/:id/duplicate
async duplicate(req, res) {
try {
const original = await Template.findById(req.params.id);
if (!original) {
return res.status(404).json({
success: false,
error: 'Template non trovato'
});
}
const duplicateData = original.toObject();
delete duplicateData._id;
delete duplicateData.createdAt;
delete duplicateData.updatedAt;
duplicateData.name = `${original.name} (copia)`;
duplicateData.userId = req.user._id;
duplicateData.metadata = {
...duplicateData.metadata,
isPublic: false,
usageCount: 0,
author: req.user?.name || 'System',
version: '1.0.0'
};
const duplicate = new Template(duplicateData);
await duplicate.save();
res.status(201).json({
success: true,
data: duplicate
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
},
// GET /templates/:id/preview
async getPreview(req, res) {
try {
const template = await Template.findById(req.params.id)
.select('previewUrl thumbnailUrl name');
if (!template) {
return res.status(404).json({
success: false,
error: 'Template non trovato'
});
}
res.json({
success: true,
data: {
previewUrl: template.previewUrl,
thumbnailUrl: template.thumbnailUrl,
name: template.name
}
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
}
};
module.exports = templateController;