- aggiornamento di tante cose...
- generazione Volantini - pagina RIS
This commit is contained in:
@@ -40,4 +40,8 @@ SCRIPTS_DIR=admin_scripts
|
||||
CLOUDFLARE_TOKENS=[{"label":"Paolo.arena77@gmail.com","value":"M9EM309v8WFquJKpYgZCw-TViM2wX6vB3wlK6GD0"},{"label":"gruppomacro.com","value":"bqmzGShoX7WqOBzkXocoECyBkPq3GfqcM5t6VFd8"}]
|
||||
DS_API_KEY="sk-222e3addb3d8455d8b0516d93906eec7"
|
||||
OLLAMA_URL=http://localhost:11434
|
||||
OLLAMA_DEFAULT_MODEL=llama3.2:3b
|
||||
OLLAMA_DEFAULT_MODEL=llama3.2:3b
|
||||
GROK_API="xai-PcNM5obgPaETtmnfDWPZk235D75ZgxENU2QmeqPfMQCHh9dwCDVeRRe0oVVA2YOpiUDh1uJieZsMasja"
|
||||
REPLICATE_API_TOKEN="r8_AVhM6igwvoOnUA65cHVZdhEDfTqBVk94WTB0u"
|
||||
FAL_KEY="7d251c88-21b5-4b55-8b3e-4bafd910f99f:b81c0a36a25b052f26eb8ac226c7efff"
|
||||
HF_TOKEN="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"
|
||||
@@ -365,7 +365,7 @@ html(lang="it")
|
||||
//- Intro
|
||||
.intro-text
|
||||
| Ciao <strong>#{usernameMembro}</strong>,<br>
|
||||
| complimenti! Sei stato abilitato al Circuito RIS del tuo territorio da #{usernameInvitante}.
|
||||
| complimenti! Sei stato abilitato #{nomeTerritorio} da #{usernameInvitante}.
|
||||
|
||||
if linkProfiloAdmin
|
||||
.divider(style="margin: 16px 0;")
|
||||
@@ -379,7 +379,7 @@ html(lang="it")
|
||||
.congrats-icon ✅
|
||||
h3 Abilitazione Completata
|
||||
p(style="font-size: 15px; color: #555; margin-top: 8px;")
|
||||
| Ora puoi utilizzare i RIS per i tuoi scambi nella comunità
|
||||
| Ora puoi utilizzare i #{symbol} per i tuoi scambi nella comunità
|
||||
.territory-name 📍 #{nomeTerritorio}
|
||||
|
||||
//- Info comunità
|
||||
@@ -448,7 +448,7 @@ html(lang="it")
|
||||
.step-number 1
|
||||
.step-content
|
||||
h5 Esplora la Piattaforma
|
||||
p Familiarizza con gli annunci, i membri e le funzionalità del Circuito RIS
|
||||
p Familiarizza con gli annunci, i membri e le funzionalità del #{nomeTerritorio}
|
||||
.step-item
|
||||
.step-number 2
|
||||
.step-content
|
||||
|
||||
@@ -1 +1 @@
|
||||
=`Richiesta ingresso di ${usernameMembro} - ${nomeMembro} ${cognomeMembro} su ${nomeTerritorio} in ${nomeapp}`
|
||||
=`Abilitazione avvenuta su ${nomeTerritorio} in ${nomeapp} - (${usernameMembro})`
|
||||
|
||||
@@ -300,7 +300,7 @@ html(lang="it")
|
||||
//- Intro
|
||||
.intro-text
|
||||
| Ciao <strong>#{nomeFacilitatore}</strong>,<br>
|
||||
| un nuovo membro richiede l'abilitazione alla fiducia al Circuito RIS del tuo territorio!
|
||||
| un nuovo membro richiede l'abilitazione alla fiducia al Circuito del tuo territorio!
|
||||
|
||||
//- Card richiesta
|
||||
.request-card
|
||||
@@ -384,12 +384,12 @@ html(lang="it")
|
||||
span.responsibility-icon 👥
|
||||
span.responsibility-text
|
||||
strong Integrazione:
|
||||
| Supporta il nuovo membro nell'attivazione e utilizzo del Circuito RIS locale
|
||||
| Supporta il nuovo membro nell'attivazione e utilizzo del Circuito locale
|
||||
|
||||
//- Info box
|
||||
.info-box
|
||||
p
|
||||
| ✓ Dopo l'abilitazione, #{usernameMembro} potrà accedere al Circuito RIS di #{nomeTerritorio}
|
||||
| ✓ Dopo l'abilitazione, #{usernameMembro} potrà accedere al #{nomeTerritorio}
|
||||
p
|
||||
| ✓ Il membro riceverà una notifica automatica dell'avvenuta attivazione
|
||||
|
||||
|
||||
@@ -16,11 +16,13 @@
|
||||
"author": "Surya",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fal-ai/client": "^1.7.2",
|
||||
"axios": "^1.13.0",
|
||||
"basic-ftp": "^5.0.5",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"bluebird": "^3.7.2",
|
||||
"body-parser": "^1.20.3",
|
||||
"canvas": "^3.2.0",
|
||||
"cheerio": "^1.0.0",
|
||||
"compress-pdf": "^0.5.3",
|
||||
"cookie-parser": "^1.4.7",
|
||||
@@ -38,6 +40,7 @@
|
||||
"formidable": "^3.5.2",
|
||||
"fs-extra": "^11.3.2",
|
||||
"ghostscript4js": "^3.2.3",
|
||||
"groq-sdk": "^0.37.0",
|
||||
"helmet": "^8.1.0",
|
||||
"i18n": "^0.15.1",
|
||||
"image-downloader": "^4.3.0",
|
||||
@@ -59,7 +62,7 @@
|
||||
"node-telegram-bot-api": "^0.66.0",
|
||||
"nodemailer": "^6.10.0",
|
||||
"npm-check-updates": "^17.1.15",
|
||||
"openai": "^4.86.2",
|
||||
"openai": "^4.104.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"pem": "^1.14.8",
|
||||
@@ -67,6 +70,7 @@
|
||||
"pug": "^3.0.3",
|
||||
"puppeteer": "^24.9.0",
|
||||
"rate-limiter-flexible": "^5.0.5",
|
||||
"replicate": "^1.4.0",
|
||||
"request": "^2.88",
|
||||
"sanitize-html": "^2.14.0",
|
||||
"save": "^2.9.0",
|
||||
|
||||
398
src/controllers/assetController.js
Normal file
398
src/controllers/assetController.js
Normal 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;
|
||||
647
src/controllers/posterController.js
Normal file
647
src/controllers/posterController.js
Normal 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;
|
||||
383
src/controllers/templateController.js
Normal file
383
src/controllers/templateController.js
Normal 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;
|
||||
45
src/data/asset.json
Normal file
45
src/data/asset.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"_id": "asset_bg_001",
|
||||
"type": "image",
|
||||
"category": "background",
|
||||
"sourceType": "ai",
|
||||
|
||||
"file": {
|
||||
"path": "/uploads/assets/backgrounds/forest_autumn_001.jpg",
|
||||
"url": "/api/assets/asset_bg_001/file",
|
||||
"thumbnailPath": "/uploads/assets/backgrounds/thumbs/forest_autumn_001_thumb.jpg",
|
||||
"thumbnailUrl": "/api/assets/asset_bg_001/thumbnail",
|
||||
"originalName": null,
|
||||
"mimeType": "image/jpeg",
|
||||
"size": 2458000,
|
||||
"dimensions": { "width": 2480, "height": 3508 }
|
||||
},
|
||||
|
||||
"aiGeneration": {
|
||||
"prompt": "Mystical autumn forest at golden hour...",
|
||||
"negativePrompt": "text, letters, words...",
|
||||
"provider": "hf",
|
||||
"model": "FLUX.1-dev",
|
||||
"seed": 8847291,
|
||||
"steps": 35,
|
||||
"cfg": 7.5,
|
||||
"requestedSize": "1024x1536",
|
||||
"actualSize": "1024x1536",
|
||||
"generationTime": 12500,
|
||||
"cost": 0
|
||||
},
|
||||
|
||||
"usage": {
|
||||
"usedInPosters": ["poster_sagra_funghi_2025_001"],
|
||||
"usedInTemplates": [],
|
||||
"usageCount": 1
|
||||
},
|
||||
|
||||
"metadata": {
|
||||
"userId": "user_001",
|
||||
"tags": ["forest", "autumn", "background", "nature"],
|
||||
"isReusable": true
|
||||
},
|
||||
|
||||
"createdAt": "2025-01-15T10:25:00.000Z"
|
||||
}
|
||||
150
src/data/poster.json
Normal file
150
src/data/poster.json
Normal file
@@ -0,0 +1,150 @@
|
||||
{
|
||||
"_id": "poster_sagra_funghi_2025_001",
|
||||
"templateId": "template_raccolta_funghi_001",
|
||||
"name": "Sagra del Fungo Porcino 2025",
|
||||
"status": "completed",
|
||||
|
||||
"content": {
|
||||
"title": "SAGRA DEL FUNGO PORCINO",
|
||||
"subtitle": "XXV Edizione - Tradizione e Sapori del Bosco",
|
||||
"eventDate": "15-16-17 Ottobre 2025",
|
||||
"eventTime": "10:00 - 23:00",
|
||||
"location": "Parco delle Querce, Borgo Montano (PG)",
|
||||
"contacts": "Tel: 0742 123456 | info@sagrafungoporcino.it | www.sagrafungoporcino.it",
|
||||
"extraText": [
|
||||
"Ingresso Libero",
|
||||
"Stand Gastronomici • Musica dal Vivo • Mercatino Artigianale"
|
||||
]
|
||||
},
|
||||
|
||||
"assets": {
|
||||
"backgroundImage": {
|
||||
"id": "asset_bg_001",
|
||||
"sourceType": "ai",
|
||||
"url": "/uploads/posters/poster_sagra_2025_bg.jpg",
|
||||
"thumbnailUrl": "/uploads/posters/thumbs/poster_sagra_2025_bg_thumb.jpg",
|
||||
"mimeType": "image/jpeg",
|
||||
"size": 2458000,
|
||||
"dimensions": { "width": 2480, "height": 3508 },
|
||||
"aiParams": {
|
||||
"prompt": "Mystical autumn forest at golden hour, morning mist between ancient oak trees, forest floor covered with porcini mushrooms, warm orange and golden light filtering through leaves, photorealistic, cinematic composition, National Geographic style, 8k quality",
|
||||
"negativePrompt": "text, letters, words, watermark, signature, blurry, low quality, cartoon, anime",
|
||||
"provider": "hf",
|
||||
"model": "FLUX.1-dev",
|
||||
"seed": 8847291,
|
||||
"steps": 35,
|
||||
"cfg": 7.5,
|
||||
"size": "1024x1536",
|
||||
"generatedAt": "2025-01-15T10:25:00.000Z"
|
||||
}
|
||||
},
|
||||
"mainImage": {
|
||||
"id": "asset_main_001",
|
||||
"sourceType": "upload",
|
||||
"url": "/uploads/assets/porcini_basket_hero.jpg",
|
||||
"thumbnailUrl": "/uploads/assets/thumbs/porcini_basket_hero_thumb.jpg",
|
||||
"originalName": "IMG_20241015_porcini.jpg",
|
||||
"mimeType": "image/jpeg",
|
||||
"size": 1845000,
|
||||
"dimensions": { "width": 1920, "height": 1280 },
|
||||
"uploadedAt": "2025-01-15T10:20:00.000Z"
|
||||
},
|
||||
"logos": [
|
||||
{
|
||||
"id": "asset_logo_001",
|
||||
"slotId": "logo_slot_1",
|
||||
"sourceType": "upload",
|
||||
"url": "/uploads/logos/comune_borgomontano.png",
|
||||
"originalName": "logo_comune.png",
|
||||
"mimeType": "image/png",
|
||||
"size": 45000
|
||||
},
|
||||
{
|
||||
"id": "asset_logo_002",
|
||||
"slotId": "logo_slot_2",
|
||||
"sourceType": "upload",
|
||||
"url": "/uploads/logos/proloco_borgomontano.png",
|
||||
"originalName": "logo_proloco.png",
|
||||
"mimeType": "image/png",
|
||||
"size": 38000
|
||||
},
|
||||
{
|
||||
"id": "asset_logo_003",
|
||||
"slotId": "logo_slot_3",
|
||||
"sourceType": "ai",
|
||||
"url": "/uploads/logos/ai_generated_mushroom_logo.png",
|
||||
"mimeType": "image/png",
|
||||
"size": 52000,
|
||||
"aiParams": {
|
||||
"prompt": "Minimal vector logo of a porcini mushroom, flat design, golden brown color, white background, simple elegant icon",
|
||||
"provider": "ideogram",
|
||||
"model": "ideogram-v2"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"layerOverrides": {
|
||||
"layer_title": {
|
||||
"style": {
|
||||
"fontSize": 78,
|
||||
"color": "#fff8e7"
|
||||
}
|
||||
},
|
||||
"layer_event_date": {
|
||||
"style": {
|
||||
"color": "#ffa502"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"renderOutput": {
|
||||
"png": {
|
||||
"path": "/uploads/posters/final/poster_sagra_2025_final.png",
|
||||
"size": 8945000,
|
||||
"url": "/api/posters/poster_sagra_funghi_2025_001/download/png"
|
||||
},
|
||||
"jpg": {
|
||||
"path": "/uploads/posters/final/poster_sagra_2025_final.jpg",
|
||||
"quality": 95,
|
||||
"size": 2145000,
|
||||
"url": "/api/posters/poster_sagra_funghi_2025_001/download/jpg"
|
||||
},
|
||||
"dimensions": {
|
||||
"width": 2480,
|
||||
"height": 3508
|
||||
},
|
||||
"renderedAt": "2025-01-15T10:30:00.000Z"
|
||||
},
|
||||
|
||||
"renderEngineVersion": "1.0.0",
|
||||
|
||||
"history": [
|
||||
{
|
||||
"action": "created",
|
||||
"timestamp": "2025-01-15T10:15:00.000Z",
|
||||
"userId": "user_001"
|
||||
},
|
||||
{
|
||||
"action": "ai_background_generated",
|
||||
"timestamp": "2025-01-15T10:25:00.000Z",
|
||||
"details": { "provider": "hf", "duration": 12500 }
|
||||
},
|
||||
{
|
||||
"action": "rendered",
|
||||
"timestamp": "2025-01-15T10:30:00.000Z",
|
||||
"details": { "duration": 3200 }
|
||||
}
|
||||
],
|
||||
|
||||
"metadata": {
|
||||
"userId": "user_001",
|
||||
"projectId": "project_eventi_2025",
|
||||
"tags": ["sagra", "fungo", "autunno", "2025"],
|
||||
"isPublic": false,
|
||||
"isFavorite": true
|
||||
},
|
||||
|
||||
"createdAt": "2025-01-15T10:15:00.000Z",
|
||||
"updatedAt": "2025-01-15T10:30:00.000Z"
|
||||
}
|
||||
272
src/data/template.json
Normal file
272
src/data/template.json
Normal file
@@ -0,0 +1,272 @@
|
||||
{
|
||||
"_id": "template_raccolta_funghi_001",
|
||||
"name": "Raccolta Funghi Autunnale",
|
||||
"templateType": "outdoor-event",
|
||||
"description": "Template per eventi all'aperto legati alla natura",
|
||||
|
||||
"format": {
|
||||
"preset": "A4",
|
||||
"width": 2480,
|
||||
"height": 3508,
|
||||
"unit": "px",
|
||||
"dpi": 300
|
||||
},
|
||||
|
||||
"safeArea": {
|
||||
"top": 0.04,
|
||||
"right": 0.04,
|
||||
"bottom": 0.04,
|
||||
"left": 0.04
|
||||
},
|
||||
|
||||
"backgroundColor": "#1a1a2e",
|
||||
|
||||
"layers": [
|
||||
{
|
||||
"id": "layer_bg",
|
||||
"type": "backgroundImage",
|
||||
"zIndex": 0,
|
||||
"position": { "x": 0, "y": 0, "w": 1, "h": 1 },
|
||||
"anchor": "top-left",
|
||||
"required": false,
|
||||
"fallback": {
|
||||
"type": "gradient",
|
||||
"direction": "to-bottom",
|
||||
"colors": ["#2d3436", "#636e72"]
|
||||
},
|
||||
"style": {
|
||||
"opacity": 1,
|
||||
"blur": 0,
|
||||
"objectFit": "cover",
|
||||
"overlay": {
|
||||
"enabled": true,
|
||||
"type": "gradient",
|
||||
"direction": "to-bottom",
|
||||
"stops": [
|
||||
{ "position": 0, "color": "rgba(0,0,0,0)" },
|
||||
{ "position": 0.5, "color": "rgba(0,0,0,0.3)" },
|
||||
{ "position": 1, "color": "rgba(0,0,0,0.85)" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "layer_main_image",
|
||||
"type": "mainImage",
|
||||
"zIndex": 1,
|
||||
"position": { "x": 0.5, "y": 0.28, "w": 0.85, "h": 0.38 },
|
||||
"anchor": "center",
|
||||
"required": false,
|
||||
"style": {
|
||||
"borderRadius": 24,
|
||||
"objectFit": "cover",
|
||||
"shadow": {
|
||||
"enabled": true,
|
||||
"blur": 40,
|
||||
"spread": 0,
|
||||
"offsetX": 0,
|
||||
"offsetY": 20,
|
||||
"color": "rgba(0,0,0,0.6)"
|
||||
},
|
||||
"border": {
|
||||
"enabled": false,
|
||||
"width": 4,
|
||||
"color": "#ffffff"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "layer_title",
|
||||
"type": "title",
|
||||
"zIndex": 10,
|
||||
"position": { "x": 0.5, "y": 0.54, "w": 0.92, "h": 0.12 },
|
||||
"anchor": "center",
|
||||
"required": true,
|
||||
"maxLines": 2,
|
||||
"style": {
|
||||
"fontFamily": "Montserrat",
|
||||
"fontWeight": 900,
|
||||
"fontSize": 82,
|
||||
"fontSizeMin": 48,
|
||||
"fontSizeMax": 120,
|
||||
"autoFit": true,
|
||||
"color": "#ffffff",
|
||||
"textAlign": "center",
|
||||
"textTransform": "uppercase",
|
||||
"letterSpacing": 6,
|
||||
"lineHeight": 1.05,
|
||||
"shadow": {
|
||||
"enabled": true,
|
||||
"blur": 15,
|
||||
"offsetX": 3,
|
||||
"offsetY": 3,
|
||||
"color": "rgba(0,0,0,0.9)"
|
||||
},
|
||||
"stroke": {
|
||||
"enabled": true,
|
||||
"width": 3,
|
||||
"color": "rgba(0,0,0,0.5)"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "layer_subtitle",
|
||||
"type": "subtitle",
|
||||
"zIndex": 10,
|
||||
"position": { "x": 0.5, "y": 0.635, "w": 0.85, "h": 0.05 },
|
||||
"anchor": "center",
|
||||
"required": false,
|
||||
"style": {
|
||||
"fontFamily": "Open Sans",
|
||||
"fontWeight": 400,
|
||||
"fontSize": 32,
|
||||
"color": "#f0f0f0",
|
||||
"textAlign": "center",
|
||||
"letterSpacing": 2,
|
||||
"lineHeight": 1.3,
|
||||
"shadow": {
|
||||
"enabled": true,
|
||||
"blur": 8,
|
||||
"offsetX": 1,
|
||||
"offsetY": 1,
|
||||
"color": "rgba(0,0,0,0.7)"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "layer_event_date",
|
||||
"type": "eventDate",
|
||||
"zIndex": 10,
|
||||
"position": { "x": 0.5, "y": 0.72, "w": 0.9, "h": 0.06 },
|
||||
"anchor": "center",
|
||||
"required": true,
|
||||
"style": {
|
||||
"fontFamily": "Bebas Neue",
|
||||
"fontWeight": 400,
|
||||
"fontSize": 56,
|
||||
"color": "#ffd700",
|
||||
"textAlign": "center",
|
||||
"letterSpacing": 4,
|
||||
"textTransform": "uppercase",
|
||||
"shadow": {
|
||||
"enabled": true,
|
||||
"blur": 10,
|
||||
"offsetX": 2,
|
||||
"offsetY": 2,
|
||||
"color": "rgba(0,0,0,0.8)"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "layer_location",
|
||||
"type": "location",
|
||||
"zIndex": 10,
|
||||
"position": { "x": 0.5, "y": 0.79, "w": 0.85, "h": 0.05 },
|
||||
"anchor": "center",
|
||||
"required": true,
|
||||
"icon": {
|
||||
"enabled": true,
|
||||
"name": "location_on",
|
||||
"size": 28,
|
||||
"color": "#e74c3c"
|
||||
},
|
||||
"style": {
|
||||
"fontFamily": "Open Sans",
|
||||
"fontWeight": 600,
|
||||
"fontSize": 28,
|
||||
"color": "#ffffff",
|
||||
"textAlign": "center",
|
||||
"letterSpacing": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "layer_contacts",
|
||||
"type": "contacts",
|
||||
"zIndex": 10,
|
||||
"position": { "x": 0.5, "y": 0.86, "w": 0.9, "h": 0.04 },
|
||||
"anchor": "center",
|
||||
"required": false,
|
||||
"style": {
|
||||
"fontFamily": "Open Sans",
|
||||
"fontWeight": 400,
|
||||
"fontSize": 22,
|
||||
"color": "#cccccc",
|
||||
"textAlign": "center",
|
||||
"letterSpacing": 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "layer_extra_text",
|
||||
"type": "extraText",
|
||||
"zIndex": 10,
|
||||
"position": { "x": 0.5, "y": 0.91, "w": 0.85, "h": 0.03 },
|
||||
"anchor": "center",
|
||||
"required": false,
|
||||
"style": {
|
||||
"fontFamily": "Open Sans",
|
||||
"fontWeight": 300,
|
||||
"fontSize": 18,
|
||||
"fontStyle": "italic",
|
||||
"color": "#aaaaaa",
|
||||
"textAlign": "center"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"logoSlots": {
|
||||
"enabled": true,
|
||||
"maxCount": 3,
|
||||
"collapseIfEmpty": true,
|
||||
"slots": [
|
||||
{
|
||||
"id": "logo_slot_1",
|
||||
"position": { "x": 0.12, "y": 0.96, "w": 0.12, "h": 0.05 },
|
||||
"anchor": "bottom-left",
|
||||
"style": { "objectFit": "contain", "opacity": 0.9 }
|
||||
},
|
||||
{
|
||||
"id": "logo_slot_2",
|
||||
"position": { "x": 0.5, "y": 0.96, "w": 0.12, "h": 0.05 },
|
||||
"anchor": "bottom-center",
|
||||
"style": { "objectFit": "contain", "opacity": 0.9 }
|
||||
},
|
||||
{
|
||||
"id": "logo_slot_3",
|
||||
"position": { "x": 0.88, "y": 0.96, "w": 0.12, "h": 0.05 },
|
||||
"anchor": "bottom-right",
|
||||
"style": { "objectFit": "contain", "opacity": 0.9 }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"palette": {
|
||||
"primary": "#e94560",
|
||||
"secondary": "#0f3460",
|
||||
"accent": "#ffd700",
|
||||
"background": "#1a1a2e",
|
||||
"text": "#ffffff",
|
||||
"textSecondary": "#cccccc",
|
||||
"textMuted": "#888888"
|
||||
},
|
||||
|
||||
"typography": {
|
||||
"titleFont": "Montserrat",
|
||||
"headingFont": "Bebas Neue",
|
||||
"bodyFont": "Open Sans",
|
||||
"accentFont": "Playfair Display"
|
||||
},
|
||||
|
||||
"defaultAiPromptHints": {
|
||||
"backgroundImage": "atmospheric outdoor scene, nature, forest, autumn colors, cinematic lighting, no text, no letters",
|
||||
"mainImage": "detailed illustration, high quality, vibrant colors, no text"
|
||||
},
|
||||
|
||||
"metadata": {
|
||||
"author": "System",
|
||||
"version": "1.0.0",
|
||||
"tags": ["natura", "outdoor", "autunno", "sagra"]
|
||||
},
|
||||
|
||||
"createdAt": "2025-01-15T10:00:00.000Z",
|
||||
"updatedAt": "2025-01-15T10:00:00.000Z"
|
||||
}
|
||||
42
src/middleware/upload.js
Normal file
42
src/middleware/upload.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads';
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
cb(null, UPLOAD_DIR);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const uniqueId = crypto.randomBytes(8).toString('hex');
|
||||
const ext = path.extname(file.originalname);
|
||||
cb(null, `${Date.now()}_${uniqueId}${ext}`);
|
||||
}
|
||||
});
|
||||
|
||||
const fileFilter = (req, file, cb) => {
|
||||
const allowedTypes = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
'image/svg+xml'
|
||||
];
|
||||
|
||||
if (allowedTypes.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Tipo file non supportato'), false);
|
||||
}
|
||||
};
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
fileFilter,
|
||||
limits: {
|
||||
fileSize: 20 * 1024 * 1024 // 20MB max
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = upload;
|
||||
137
src/models/Asset.js
Normal file
137
src/models/Asset.js
Normal file
@@ -0,0 +1,137 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
// Sub-schema: File Info
|
||||
const FileInfoSchema = new Schema({
|
||||
path: { type: String, required: true },
|
||||
url: { type: String },
|
||||
thumbnailPath: { type: String },
|
||||
thumbnailUrl: { type: String },
|
||||
originalName: { type: String },
|
||||
mimeType: { type: String, required: true },
|
||||
size: { type: Number }, // bytes
|
||||
dimensions: {
|
||||
width: { type: Number },
|
||||
height: { type: Number }
|
||||
}
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: AI Generation Params
|
||||
const AiGenerationSchema = new Schema({
|
||||
prompt: { type: String, required: true },
|
||||
negativePrompt: { type: String },
|
||||
provider: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['hf', 'fal', 'ideogram', 'openai', 'stability', 'midjourney']
|
||||
},
|
||||
model: { type: String },
|
||||
seed: { type: Number },
|
||||
steps: { type: Number },
|
||||
cfg: { type: Number },
|
||||
requestedSize: { type: String },
|
||||
actualSize: { type: String },
|
||||
aspectRatio: { type: String },
|
||||
styleType: { type: String },
|
||||
generationTime: { type: Number }, // ms
|
||||
cost: { type: Number, default: 0 },
|
||||
rawResponse: { type: Schema.Types.Mixed }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Usage Tracking
|
||||
const UsageTrackingSchema = new Schema({
|
||||
usedInPosters: [{ type: Schema.Types.ObjectId, ref: 'Poster' }],
|
||||
usedInTemplates: [{ type: Schema.Types.ObjectId, ref: 'Template' }],
|
||||
usageCount: { type: Number, default: 0 }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Asset Metadata
|
||||
const AssetMetadataSchema = new Schema({
|
||||
userId: { type: Schema.Types.ObjectId, ref: 'User', index: true },
|
||||
projectId: { type: Schema.Types.ObjectId, ref: 'Project' },
|
||||
tags: [{ type: String }],
|
||||
description: { type: String },
|
||||
isReusable: { type: Boolean, default: true },
|
||||
isPublic: { type: Boolean, default: false }
|
||||
}, { _id: false });
|
||||
|
||||
// MAIN SCHEMA: Asset
|
||||
const AssetSchema = new Schema({
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['image', 'logo', 'icon', 'font']
|
||||
},
|
||||
category: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['background', 'main', 'logo', 'decoration', 'overlay', 'other'],
|
||||
index: true
|
||||
},
|
||||
sourceType: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['upload', 'ai', 'library', 'url'],
|
||||
index: true
|
||||
},
|
||||
|
||||
file: { type: FileInfoSchema, required: true },
|
||||
aiGeneration: { type: AiGenerationSchema },
|
||||
|
||||
usage: { type: UsageTrackingSchema, default: () => ({}) },
|
||||
metadata: { type: AssetMetadataSchema, default: () => ({}) },
|
||||
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['processing', 'ready', 'error', 'deleted'],
|
||||
default: 'ready'
|
||||
},
|
||||
errorMessage: { type: String }
|
||||
}, {
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true }
|
||||
});
|
||||
|
||||
// Indexes
|
||||
AssetSchema.index({ 'metadata.userId': 1, category: 1 });
|
||||
AssetSchema.index({ 'metadata.tags': 1 });
|
||||
AssetSchema.index({ sourceType: 1, status: 1 });
|
||||
|
||||
// Virtual: isAiGenerated
|
||||
AssetSchema.virtual('isAiGenerated').get(function() {
|
||||
return this.sourceType === 'ai';
|
||||
});
|
||||
|
||||
// Methods
|
||||
AssetSchema.methods.addUsage = async function(posterId, type = 'poster') {
|
||||
if (type === 'poster' && !this.usage.usedInPosters.includes(posterId)) {
|
||||
this.usage.usedInPosters.push(posterId);
|
||||
} else if (type === 'template' && !this.usage.usedInTemplates.includes(posterId)) {
|
||||
this.usage.usedInTemplates.push(posterId);
|
||||
}
|
||||
this.usage.usageCount = this.usage.usedInPosters.length + this.usage.usedInTemplates.length;
|
||||
return this.save();
|
||||
};
|
||||
|
||||
AssetSchema.methods.getPublicUrl = function() {
|
||||
return this.file.url || `/api/assets/${this._id}/file`;
|
||||
};
|
||||
|
||||
// Statics
|
||||
AssetSchema.statics.findByUser = function(userId, category = null) {
|
||||
const query = { 'metadata.userId': userId, status: 'ready' };
|
||||
if (category) query.category = category;
|
||||
return this.find(query).sort({ createdAt: -1 });
|
||||
};
|
||||
|
||||
AssetSchema.statics.findReusable = function(userId, category = null) {
|
||||
const query = {
|
||||
'metadata.userId': userId,
|
||||
'metadata.isReusable': true,
|
||||
status: 'ready'
|
||||
};
|
||||
if (category) query.category = category;
|
||||
return this.find(query).sort({ 'usage.usageCount': -1 });
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('Asset', AssetSchema);
|
||||
262
src/models/Poster.js
Normal file
262
src/models/Poster.js
Normal file
@@ -0,0 +1,262 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
// Sub-schema: Content
|
||||
const PosterContentSchema = new Schema({
|
||||
title: { type: String, maxlength: 500 },
|
||||
subtitle: { type: String, maxlength: 500 },
|
||||
eventDate: { type: String, maxlength: 200 },
|
||||
eventTime: { type: String, maxlength: 100 },
|
||||
location: { type: String, maxlength: 500 },
|
||||
contacts: { type: String, maxlength: 1000 },
|
||||
extraText: [{ type: String }],
|
||||
customFields: { type: Map, of: String }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Asset AI Params (embedded)
|
||||
const EmbeddedAiParamsSchema = new Schema({
|
||||
prompt: { type: String },
|
||||
negativePrompt: { type: String },
|
||||
provider: { type: String },
|
||||
model: { type: String },
|
||||
seed: { type: Number },
|
||||
steps: { type: Number },
|
||||
cfg: { type: Number },
|
||||
size: { type: String },
|
||||
generatedAt: { type: Date }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Poster Asset Reference
|
||||
const PosterAssetSchema = new Schema({
|
||||
id: { type: String },
|
||||
assetId: { type: Schema.Types.ObjectId, ref: 'Asset' },
|
||||
slotId: { type: String }, // per loghi
|
||||
sourceType: { type: String, enum: ['upload', 'ai', 'library', 'url'] },
|
||||
url: { type: String },
|
||||
thumbnailUrl: { type: String },
|
||||
originalName: { type: String },
|
||||
mimeType: { type: String },
|
||||
size: { type: Number },
|
||||
dimensions: {
|
||||
width: { type: Number },
|
||||
height: { type: Number }
|
||||
},
|
||||
aiParams: EmbeddedAiParamsSchema
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Assets Container
|
||||
const PosterAssetsSchema = new Schema({
|
||||
backgroundImage: PosterAssetSchema,
|
||||
mainImage: PosterAssetSchema,
|
||||
logos: [PosterAssetSchema]
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Layer Override Style
|
||||
const LayerOverrideStyleSchema = new Schema({
|
||||
fontSize: { type: Number },
|
||||
color: { type: String },
|
||||
fontWeight: { type: Number },
|
||||
opacity: { type: Number },
|
||||
// altri override possibili
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Layer Override
|
||||
const LayerOverrideSchema = new Schema({
|
||||
position: {
|
||||
x: { type: Number },
|
||||
y: { type: Number },
|
||||
w: { type: Number },
|
||||
h: { type: Number }
|
||||
},
|
||||
visible: { type: Boolean },
|
||||
style: LayerOverrideStyleSchema
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Render Output File
|
||||
const RenderOutputFileSchema = new Schema({
|
||||
path: { type: String, required: true },
|
||||
url: { type: String },
|
||||
size: { type: Number },
|
||||
quality: { type: Number }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Render Output
|
||||
const RenderOutputSchema = new Schema({
|
||||
png: RenderOutputFileSchema,
|
||||
jpg: RenderOutputFileSchema,
|
||||
webp: RenderOutputFileSchema,
|
||||
pdf: RenderOutputFileSchema,
|
||||
dimensions: {
|
||||
width: { type: Number },
|
||||
height: { type: Number }
|
||||
},
|
||||
renderedAt: { type: Date }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: History Entry
|
||||
const HistoryEntrySchema = new Schema({
|
||||
action: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['created', 'updated', 'ai_background_generated', 'ai_main_generated', 'rendered', 'downloaded', 'shared', 'deleted']
|
||||
},
|
||||
timestamp: { type: Date, default: Date.now },
|
||||
userId: { type: Schema.Types.ObjectId, ref: 'User' },
|
||||
details: { type: Schema.Types.Mixed }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Poster Metadata
|
||||
const PosterMetadataSchema = new Schema({
|
||||
userId: { type: Schema.Types.ObjectId, ref: 'User', required: true, index: true },
|
||||
projectId: { type: Schema.Types.ObjectId, ref: 'Project' },
|
||||
tags: [{ type: String }],
|
||||
isPublic: { type: Boolean, default: false },
|
||||
isFavorite: { type: Boolean, default: false },
|
||||
viewCount: { type: Number, default: 0 },
|
||||
downloadCount: { type: Number, default: 0 }
|
||||
}, { _id: false });
|
||||
|
||||
// MAIN SCHEMA: Poster
|
||||
const PosterSchema = new Schema({
|
||||
templateId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Template',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
templateSnapshot: { type: Schema.Types.Mixed }, // copia del template al momento della creazione
|
||||
|
||||
name: { type: String, required: true, trim: true, maxlength: 300 },
|
||||
description: { type: String, maxlength: 1000 },
|
||||
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['draft', 'processing', 'completed', 'error'],
|
||||
default: 'draft',
|
||||
index: true
|
||||
},
|
||||
|
||||
content: { type: PosterContentSchema, required: true },
|
||||
assets: { type: PosterAssetsSchema, default: () => ({}) },
|
||||
layerOverrides: { type: Map, of: LayerOverrideSchema, default: () => new Map() },
|
||||
|
||||
renderOutput: RenderOutputSchema,
|
||||
renderEngineVersion: { type: String, default: '1.0.0' },
|
||||
|
||||
history: [HistoryEntrySchema],
|
||||
metadata: { type: PosterMetadataSchema, required: true },
|
||||
|
||||
errorMessage: { type: String },
|
||||
|
||||
// Campi dalla tua bozza originale
|
||||
originalPrompt: { type: String }, // prompt completo usato
|
||||
styleUsed: { type: String },
|
||||
aspectRatio: { type: String },
|
||||
provider: { type: String }
|
||||
}, {
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true }
|
||||
});
|
||||
|
||||
// Indexes
|
||||
PosterSchema.index({ 'metadata.userId': 1, status: 1 });
|
||||
PosterSchema.index({ 'metadata.tags': 1 });
|
||||
PosterSchema.index({ 'metadata.isFavorite': 1, 'metadata.userId': 1 });
|
||||
PosterSchema.index({ createdAt: -1 });
|
||||
PosterSchema.index({ name: 'text', description: 'text' });
|
||||
|
||||
// Virtual: isCompleted
|
||||
PosterSchema.virtual('isCompleted').get(function() {
|
||||
return this.status === 'completed' && this.renderOutput?.png?.path;
|
||||
});
|
||||
|
||||
// Virtual: downloadUrl
|
||||
PosterSchema.virtual('downloadUrl').get(function() {
|
||||
if (this.renderOutput?.png?.path) {
|
||||
return `/api/posters/${this._id}/download/png`;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// Pre-save: aggiorna history
|
||||
PosterSchema.pre('save', function(next) {
|
||||
if (this.isNew) {
|
||||
this.history = this.history || [];
|
||||
this.history.push({
|
||||
action: 'created',
|
||||
timestamp: new Date(),
|
||||
userId: this.metadata.userId
|
||||
});
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Methods
|
||||
PosterSchema.methods.addHistory = function(action, details = {}) {
|
||||
this.history.push({
|
||||
action,
|
||||
timestamp: new Date(),
|
||||
userId: this.metadata.userId,
|
||||
details
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
PosterSchema.methods.setRenderOutput = function(outputData) {
|
||||
this.renderOutput = {
|
||||
...outputData,
|
||||
renderedAt: new Date()
|
||||
};
|
||||
this.status = 'completed';
|
||||
this.addHistory('rendered', { duration: outputData.duration });
|
||||
return this;
|
||||
};
|
||||
|
||||
PosterSchema.methods.setError = function(errorMessage) {
|
||||
this.status = 'error';
|
||||
this.errorMessage = errorMessage;
|
||||
return this;
|
||||
};
|
||||
|
||||
PosterSchema.methods.incrementDownload = async function() {
|
||||
this.metadata.downloadCount = (this.metadata.downloadCount || 0) + 1;
|
||||
this.addHistory('downloaded');
|
||||
return this.save();
|
||||
};
|
||||
|
||||
PosterSchema.methods.toggleFavorite = async function() {
|
||||
this.metadata.isFavorite = !this.metadata.isFavorite;
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Statics
|
||||
PosterSchema.statics.findByUser = function(userId, options = {}) {
|
||||
const query = { 'metadata.userId': userId };
|
||||
if (options.status) query.status = options.status;
|
||||
if (options.isFavorite) query['metadata.isFavorite'] = true;
|
||||
|
||||
return this.find(query)
|
||||
.populate('templateId', 'name templateType thumbnailUrl')
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(options.limit || 50);
|
||||
};
|
||||
|
||||
PosterSchema.statics.findFavorites = function(userId) {
|
||||
return this.find({
|
||||
'metadata.userId': userId,
|
||||
'metadata.isFavorite': true
|
||||
}).sort({ updatedAt: -1 });
|
||||
};
|
||||
|
||||
PosterSchema.statics.findRecent = function(userId, limit = 10) {
|
||||
return this.find({
|
||||
'metadata.userId': userId,
|
||||
status: 'completed'
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(limit)
|
||||
.select('name renderOutput.png.url thumbnailUrl createdAt');
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('Poster', PosterSchema);
|
||||
253
src/models/Template.js
Normal file
253
src/models/Template.js
Normal file
@@ -0,0 +1,253 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
// Sub-schema: Posizione layer
|
||||
const PositionSchema = new Schema({
|
||||
x: { type: Number, required: true, min: 0, max: 1 },
|
||||
y: { type: Number, required: true, min: 0, max: 1 },
|
||||
w: { type: Number, required: true, min: 0, max: 1 },
|
||||
h: { type: Number, required: true, min: 0, max: 1 }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Ombra
|
||||
const ShadowSchema = new Schema({
|
||||
enabled: { type: Boolean, default: false },
|
||||
blur: { type: Number, default: 10 },
|
||||
spread: { type: Number, default: 0 },
|
||||
offsetX: { type: Number, default: 0 },
|
||||
offsetY: { type: Number, default: 4 },
|
||||
color: { type: String, default: 'rgba(0,0,0,0.5)' }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Stroke
|
||||
const StrokeSchema = new Schema({
|
||||
enabled: { type: Boolean, default: false },
|
||||
width: { type: Number, default: 2 },
|
||||
color: { type: String, default: '#000000' }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Border
|
||||
const BorderSchema = new Schema({
|
||||
enabled: { type: Boolean, default: false },
|
||||
width: { type: Number, default: 2 },
|
||||
color: { type: String, default: '#ffffff' },
|
||||
style: { type: String, enum: ['solid', 'dashed', 'dotted'], default: 'solid' }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Gradient Stop
|
||||
const GradientStopSchema = new Schema({
|
||||
position: { type: Number, required: true, min: 0, max: 1 },
|
||||
color: { type: String, required: true }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Overlay
|
||||
const OverlaySchema = new Schema({
|
||||
enabled: { type: Boolean, default: false },
|
||||
type: { type: String, enum: ['solid', 'gradient'], default: 'gradient' },
|
||||
color: { type: String },
|
||||
direction: { type: String, default: 'to-bottom' },
|
||||
stops: [GradientStopSchema]
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Fallback
|
||||
const FallbackSchema = new Schema({
|
||||
type: { type: String, enum: ['solid', 'gradient'], default: 'solid' },
|
||||
color: { type: String },
|
||||
direction: { type: String },
|
||||
colors: [{ type: String }]
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Icon
|
||||
const IconSchema = new Schema({
|
||||
enabled: { type: Boolean, default: false },
|
||||
name: { type: String },
|
||||
size: { type: Number, default: 24 },
|
||||
color: { type: String, default: '#ffffff' }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Stile Layer (unificato per immagini e testi)
|
||||
const LayerStyleSchema = new Schema({
|
||||
// Comuni
|
||||
opacity: { type: Number, default: 1, min: 0, max: 1 },
|
||||
|
||||
// Per immagini
|
||||
objectFit: { type: String, enum: ['cover', 'contain', 'fill', 'none'], default: 'cover' },
|
||||
blur: { type: Number, default: 0 },
|
||||
borderRadius: { type: Number, default: 0 },
|
||||
overlay: OverlaySchema,
|
||||
border: BorderSchema,
|
||||
|
||||
// Per testi
|
||||
fontFamily: { type: String },
|
||||
fontWeight: { type: Number, default: 400 },
|
||||
fontSize: { type: Number },
|
||||
fontSizeMin: { type: Number },
|
||||
fontSizeMax: { type: Number },
|
||||
autoFit: { type: Boolean, default: false },
|
||||
fontStyle: { type: String, enum: ['normal', 'italic'], default: 'normal' },
|
||||
color: { type: String },
|
||||
textAlign: { type: String, enum: ['left', 'center', 'right'], default: 'center' },
|
||||
textTransform: { type: String, enum: ['none', 'uppercase', 'lowercase', 'capitalize'], default: 'none' },
|
||||
letterSpacing: { type: Number, default: 0 },
|
||||
lineHeight: { type: Number, default: 1.2 },
|
||||
|
||||
// Effetti
|
||||
shadow: ShadowSchema,
|
||||
stroke: StrokeSchema
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Layer
|
||||
const LayerSchema = new Schema({
|
||||
id: { type: String, required: true },
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['backgroundImage', 'mainImage', 'logo', 'title', 'subtitle', 'eventDate', 'eventTime', 'location', 'contacts', 'extraText', 'customText', 'customImage', 'shape', 'divider']
|
||||
},
|
||||
zIndex: { type: Number, default: 0 },
|
||||
position: { type: PositionSchema, required: true },
|
||||
anchor: {
|
||||
type: String,
|
||||
enum: ['top-left', 'top-center', 'top-right', 'center-left', 'center', 'center-right', 'bottom-left', 'bottom-center', 'bottom-right'],
|
||||
default: 'center'
|
||||
},
|
||||
required: { type: Boolean, default: false },
|
||||
visible: { type: Boolean, default: true },
|
||||
locked: { type: Boolean, default: false },
|
||||
maxLines: { type: Number },
|
||||
fallback: FallbackSchema,
|
||||
icon: IconSchema,
|
||||
style: { type: LayerStyleSchema, default: () => ({}) }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Logo Slot
|
||||
const LogoSlotSchema = new Schema({
|
||||
id: { type: String, required: true },
|
||||
position: { type: PositionSchema, required: true },
|
||||
anchor: { type: String, default: 'center' },
|
||||
style: { type: LayerStyleSchema, default: () => ({}) }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Logo Slots Config
|
||||
const LogoSlotsConfigSchema = new Schema({
|
||||
enabled: { type: Boolean, default: true },
|
||||
maxCount: { type: Number, default: 3, min: 1, max: 10 },
|
||||
collapseIfEmpty: { type: Boolean, default: true },
|
||||
slots: [LogoSlotSchema]
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Format
|
||||
const FormatSchema = new Schema({
|
||||
preset: { type: String, default: 'custom' }, // A4, A3, Instagram, Facebook, custom
|
||||
width: { type: Number, required: true },
|
||||
height: { type: Number, required: true },
|
||||
unit: { type: String, enum: ['px', 'mm', 'in'], default: 'px' },
|
||||
dpi: { type: Number, default: 300 }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Safe Area
|
||||
const SafeAreaSchema = new Schema({
|
||||
top: { type: Number, default: 0, min: 0, max: 0.5 },
|
||||
right: { type: Number, default: 0, min: 0, max: 0.5 },
|
||||
bottom: { type: Number, default: 0, min: 0, max: 0.5 },
|
||||
left: { type: Number, default: 0, min: 0, max: 0.5 }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Palette
|
||||
const PaletteSchema = new Schema({
|
||||
primary: { type: String, default: '#e94560' },
|
||||
secondary: { type: String, default: '#0f3460' },
|
||||
accent: { type: String, default: '#ffd700' },
|
||||
background: { type: String, default: '#1a1a2e' },
|
||||
text: { type: String, default: '#ffffff' },
|
||||
textSecondary: { type: String, default: '#cccccc' },
|
||||
textMuted: { type: String, default: '#888888' }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Typography
|
||||
const TypographySchema = new Schema({
|
||||
titleFont: { type: String, default: 'Montserrat' },
|
||||
headingFont: { type: String, default: 'Bebas Neue' },
|
||||
bodyFont: { type: String, default: 'Open Sans' },
|
||||
accentFont: { type: String, default: 'Playfair Display' }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: AI Prompt Hints
|
||||
const AiPromptHintsSchema = new Schema({
|
||||
backgroundImage: { type: String },
|
||||
mainImage: { type: String }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Metadata
|
||||
const TemplateMetadataSchema = new Schema({
|
||||
author: { type: String, default: 'System' },
|
||||
version: { type: String, default: '1.0.0' },
|
||||
tags: [{ type: String }],
|
||||
isPublic: { type: Boolean, default: false },
|
||||
usageCount: { type: Number, default: 0 }
|
||||
}, { _id: false });
|
||||
|
||||
// MAIN SCHEMA: Template
|
||||
const TemplateSchema = new Schema({
|
||||
name: { type: String, required: true, trim: true, maxlength: 200 },
|
||||
templateType: { type: String, required: true, trim: true, index: true },
|
||||
description: { type: String, maxlength: 1000 },
|
||||
|
||||
format: { type: FormatSchema, required: true },
|
||||
safeArea: { type: SafeAreaSchema, default: () => ({}) },
|
||||
backgroundColor: { type: String, default: '#1a1a2e' },
|
||||
|
||||
layers: { type: [LayerSchema], required: true, validate: [arr => arr.length > 0, 'Almeno un layer richiesto'] },
|
||||
logoSlots: { type: LogoSlotsConfigSchema, default: () => ({}) },
|
||||
|
||||
palette: { type: PaletteSchema, default: () => ({}) },
|
||||
typography: { type: TypographySchema, default: () => ({}) },
|
||||
defaultAiPromptHints: { type: AiPromptHintsSchema, default: () => ({}) },
|
||||
|
||||
previewUrl: { type: String },
|
||||
thumbnailUrl: { type: String },
|
||||
|
||||
metadata: { type: TemplateMetadataSchema, default: () => ({}) },
|
||||
|
||||
isActive: { type: Boolean, default: true },
|
||||
userId: { type: Schema.Types.ObjectId, ref: 'User', index: true }
|
||||
}, {
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true }
|
||||
});
|
||||
|
||||
// Indexes
|
||||
TemplateSchema.index({ templateType: 1, isActive: 1 });
|
||||
TemplateSchema.index({ 'metadata.tags': 1 });
|
||||
TemplateSchema.index({ name: 'text', description: 'text', templateType: 'text' });
|
||||
|
||||
// Virtual: layer count
|
||||
TemplateSchema.virtual('layerCount').get(function() {
|
||||
return this.layers ? this.layers.length : 0;
|
||||
});
|
||||
|
||||
// Methods
|
||||
TemplateSchema.methods.getLayerById = function(layerId) {
|
||||
return this.layers.find(l => l.id === layerId);
|
||||
};
|
||||
|
||||
TemplateSchema.methods.getLayersByType = function(type) {
|
||||
return this.layers.filter(l => l.type === type);
|
||||
};
|
||||
|
||||
TemplateSchema.methods.incrementUsage = async function() {
|
||||
this.metadata.usageCount = (this.metadata.usageCount || 0) + 1;
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Statics
|
||||
TemplateSchema.statics.findByType = function(templateType) {
|
||||
return this.find({ templateType, isActive: true }).sort({ 'metadata.usageCount': -1 });
|
||||
};
|
||||
|
||||
TemplateSchema.statics.findPublic = function() {
|
||||
return this.find({ 'metadata.isPublic': true, isActive: true });
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('Template', TemplateSchema);
|
||||
@@ -32,6 +32,12 @@ const AccountSchema = new Schema({
|
||||
numtransactions: {
|
||||
type: Number,
|
||||
},
|
||||
sent: {
|
||||
type: Number,
|
||||
},
|
||||
received: {
|
||||
type: Number,
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
},
|
||||
@@ -242,6 +248,12 @@ AccountSchema.statics.addtoSaldo = async function (myaccount, amount, mitt) {
|
||||
myaccountupdate.saldo = myaccount.saldo;
|
||||
myaccountupdate.totTransato = myaccount.totTransato;
|
||||
myaccountupdate.numtransactions = myaccount.numtransactions;
|
||||
if (amount > 0) {
|
||||
myaccountupdate.received += 1;
|
||||
} else {
|
||||
myaccountupdate.sent += 1;
|
||||
}
|
||||
|
||||
myaccountupdate.date_updated = myaccount.date_updated;
|
||||
|
||||
const ris = await Account.updateOne(
|
||||
@@ -324,6 +336,8 @@ AccountSchema.statics.getAccountByUsernameAndCircuitId = async function (
|
||||
username_admin_abilitante: '',
|
||||
qta_maxConcessa: 0,
|
||||
totTransato: 0,
|
||||
sent: 0,
|
||||
received: 0,
|
||||
numtransactions: 0,
|
||||
totTransato_pend: 0,
|
||||
});
|
||||
|
||||
@@ -87,6 +87,9 @@ const CircuitSchema = new Schema({
|
||||
totTransato: {
|
||||
type: Number,
|
||||
},
|
||||
numTransazioni: {
|
||||
type: Number,
|
||||
},
|
||||
nome_valuta: {
|
||||
type: String,
|
||||
maxlength: 20,
|
||||
@@ -327,6 +330,7 @@ CircuitSchema.statics.getWhatToShow = function (idapp, username) {
|
||||
numMembers: 1,
|
||||
totCircolante: 1,
|
||||
totTransato: 1,
|
||||
numTransazioni: 1,
|
||||
systemUserId: 1,
|
||||
createdBy: 1,
|
||||
date_created: 1,
|
||||
@@ -412,6 +416,7 @@ CircuitSchema.statics.getWhatToShow_Unknown = function (idapp, username) {
|
||||
nome_valuta: 1,
|
||||
totCircolante: 1,
|
||||
totTransato: 1,
|
||||
numTransazioni: 1,
|
||||
fido_scoperto_default: 1,
|
||||
fido_scoperto_default_grp: 1,
|
||||
qta_max_default_grp: 1,
|
||||
@@ -825,6 +830,7 @@ CircuitSchema.statics.sendCoins = async function (onlycheck, idapp, usernameOrig
|
||||
const circolanteAtt = this.getCircolanteSingolaTransaz(accountorigTable, accountdestTable);
|
||||
|
||||
// Somma di tutte le transazioni
|
||||
circuittable.numTransazioni += 1;
|
||||
circuittable.totTransato += myqty;
|
||||
// circuittable.totCircolante = circuittable.totCircolante + (circolanteAtt - circolantePrec);
|
||||
circuittable.totCircolante = await Account.calcTotCircolante(idapp, circuittable._id);
|
||||
@@ -901,7 +907,14 @@ CircuitSchema.statics.sendCoins = async function (onlycheck, idapp, usernameOrig
|
||||
let myuserDest = await User.getUserByUsername(idapp, extrarec.dest);
|
||||
|
||||
// Invia una email al destinatario !
|
||||
await sendemail.sendEmail_RisRicevuti(myuserDest.lang, myuserDest, myuserDest.email, idapp, paramsrec, extrarec);
|
||||
await sendemail.sendEmail_RisRicevuti(
|
||||
myuserDest.lang,
|
||||
myuserDest,
|
||||
myuserDest.email,
|
||||
idapp,
|
||||
paramsrec,
|
||||
extrarec
|
||||
);
|
||||
} else if (extrarec.groupdest || extrarec.contoComDest) {
|
||||
const groupDestoContoCom = extrarec.groupdest
|
||||
? extrarec.groupdest
|
||||
@@ -1047,16 +1060,16 @@ CircuitSchema.statics.getListAdminsByCircuitPath = async function (idapp, circui
|
||||
let adminObjects = circuit && circuit.admins ? circuit.admins : [];
|
||||
|
||||
// Aggiungi USER_ADMIN_CIRCUITS come oggetti
|
||||
let systemAdmins = shared_consts.USER_ADMIN_CIRCUITS.map(username => ({
|
||||
let systemAdmins = shared_consts.USER_ADMIN_CIRCUITS.map((username) => ({
|
||||
username,
|
||||
date: null,
|
||||
_id: null
|
||||
_id: null,
|
||||
}));
|
||||
|
||||
// Unisci e rimuovi duplicati per username
|
||||
let allAdmins = [...adminObjects, ...systemAdmins];
|
||||
let uniqueAdmins = allAdmins.filter((admin, index, self) =>
|
||||
index === self.findIndex(a => a.username === admin.username)
|
||||
let uniqueAdmins = allAdmins.filter(
|
||||
(admin, index, self) => index === self.findIndex((a) => a.username === admin.username)
|
||||
);
|
||||
|
||||
return uniqueAdmins;
|
||||
@@ -1190,6 +1203,7 @@ CircuitSchema.statics.createCircuitIfNotExist = async function (req, idapp, prov
|
||||
qta_max_default_grp: shared_consts.CIRCUIT_PARAMS.SCOPERTO_MAX_GRP,
|
||||
valuta_per_euro: 1,
|
||||
totTransato: 0,
|
||||
numTransazioni: 0,
|
||||
totCircolante: 0,
|
||||
date_created: new Date(),
|
||||
admins: admins.map((username) => ({ username })),
|
||||
@@ -1388,7 +1402,12 @@ CircuitSchema.statics.setFido = async function (idapp, username, circuitName, gr
|
||||
|
||||
const ris = await Account.updateFido(idapp, username, groupname, circuitId, fido, username_action);
|
||||
if (ris) {
|
||||
return { qta_maxConcessa: qtamax, fidoConcesso: fido, username_admin_abilitante: username_action, changed: variato || (ris && ris.modifiedCount > 0) };
|
||||
return {
|
||||
qta_maxConcessa: qtamax,
|
||||
fidoConcesso: fido,
|
||||
username_admin_abilitante: username_action,
|
||||
changed: variato || (ris && ris.modifiedCount > 0),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1441,7 +1460,7 @@ CircuitSchema.statics.getFido = async function (idapp, username, circuitName, gr
|
||||
return null;
|
||||
};
|
||||
|
||||
CircuitSchema.statics.CheckTransazioniCircuiti = async function (correggi) {
|
||||
CircuitSchema.statics.CheckTransazioniCircuiti = async function (correggi, options) {
|
||||
const { User } = require('../models/user');
|
||||
const { MyGroup } = require('../models/mygroup');
|
||||
const { SendNotif } = require('../models/sendnotif');
|
||||
@@ -1540,7 +1559,7 @@ CircuitSchema.statics.CheckTransazioniCircuiti = async function (correggi) {
|
||||
|
||||
let numtransazionitot = 0;
|
||||
|
||||
const arrcircuits = await Circuit.find({ idapp }).lean();
|
||||
const arrcircuits = await Circuit.find({ idapp });
|
||||
for (const circuit of arrcircuits) {
|
||||
let strusersnotinaCircuit = '';
|
||||
let strusersnotExist = '';
|
||||
@@ -1620,6 +1639,16 @@ CircuitSchema.statics.CheckTransazioniCircuiti = async function (correggi) {
|
||||
_id: null,
|
||||
numtransactions: { $sum: 1 },
|
||||
totTransato: { $sum: { $abs: '$amount' } },
|
||||
sentCount: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ['$accountFromId', account._id] }, 1, 0],
|
||||
},
|
||||
},
|
||||
receivedCount: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ['$accountToId', account._id] }, 1, 0],
|
||||
},
|
||||
},
|
||||
saldo: {
|
||||
$sum: {
|
||||
$cond: [
|
||||
@@ -1636,6 +1665,8 @@ CircuitSchema.statics.CheckTransazioniCircuiti = async function (correggi) {
|
||||
]);
|
||||
|
||||
let numtransactions = result && result.length > 0 ? result[0].numtransactions : 0;
|
||||
let sentCount = result && result.length > 0 ? result[0].sentCount : 0;
|
||||
let receivedCount = result && result.length > 0 ? result[0].receivedCount : 0;
|
||||
let totTransato = result && result.length > 0 ? result[0].totTransato : 0;
|
||||
let saldo = result && result.length > 0 ? result[0].saldo : 0;
|
||||
|
||||
@@ -1679,6 +1710,8 @@ CircuitSchema.statics.CheckTransazioniCircuiti = async function (correggi) {
|
||||
if (correggi) await Account.findOneAndUpdate({ _id: account._id }, { $set: { totTransato } });
|
||||
}
|
||||
|
||||
await Account.findOneAndUpdate({ _id: account._id }, { $set: { sent: sentCount, received: receivedCount } });
|
||||
|
||||
saldotot += account.saldo;
|
||||
|
||||
// if (account.totTransato === NaN || account.totTransato === undefined)
|
||||
@@ -1693,6 +1726,11 @@ CircuitSchema.statics.CheckTransazioniCircuiti = async function (correggi) {
|
||||
|
||||
// await account.calcPending();
|
||||
ind++;
|
||||
} // FINE ACCOUNT
|
||||
|
||||
if (options?.setnumtransaction) {
|
||||
circuit.numTransazioni = numtransazionitot;
|
||||
await circuit.save(); // salva su db
|
||||
}
|
||||
|
||||
let numaccounts = accounts.length;
|
||||
@@ -1876,6 +1914,11 @@ CircuitSchema.statics.getCircuitiExtraProvinciali = async function (idapp) {
|
||||
return circuits;
|
||||
};
|
||||
|
||||
CircuitSchema.statics.ricalcolaNumTransazioni = async function (circuitId) {
|
||||
const Circuit = this;
|
||||
|
||||
// +TODO: Ricalcola il numero delle transazioni avvenute
|
||||
};
|
||||
CircuitSchema.statics.getCircuitoItalia = async function (idapp) {
|
||||
const Circuit = this;
|
||||
|
||||
@@ -1884,6 +1927,13 @@ CircuitSchema.statics.getCircuitoItalia = async function (idapp) {
|
||||
return circuit;
|
||||
};
|
||||
|
||||
CircuitSchema.statics.getSymbolByCircuitId = async function (circuitId) {
|
||||
const Circuit = this;
|
||||
|
||||
const circuit = await Circuit.findOne({ _id: circuitId }, { symbol: 1});
|
||||
|
||||
return circuit?.symbol || '';
|
||||
};
|
||||
CircuitSchema.statics.isEnableToReceiveEmailByExtraRec = async function (idapp, recnotif) {
|
||||
let ricevo = true;
|
||||
if (recnotif.tag === 'setfido') {
|
||||
|
||||
@@ -106,6 +106,8 @@ MovementSchema.statics.addMov = async function (
|
||||
idOrdersCart
|
||||
) {
|
||||
try {
|
||||
const { Circuit } = require('./circuit');
|
||||
|
||||
// Only positive values
|
||||
amount = Math.abs(amount);
|
||||
|
||||
|
||||
@@ -174,6 +174,7 @@ const SiteSchema = new Schema({
|
||||
bookingEvents: { type: Boolean, default: false },
|
||||
enableEcommerce: { type: Boolean, default: false },
|
||||
enableAI: { type: Boolean, default: false },
|
||||
enablePoster: { type: Boolean, default: false },
|
||||
enableGroups: { type: Boolean, default: false },
|
||||
enableCircuits: { type: Boolean, default: false },
|
||||
enableGoods: { type: Boolean, default: false },
|
||||
|
||||
@@ -202,7 +202,7 @@ class CronMod {
|
||||
} else if (mydata.dbop === 'RewriteCategESubCateg') {
|
||||
const migration = require('../populate/migration-categories');
|
||||
|
||||
ris = await migration.aggiornaCategorieESottoCategorie()
|
||||
ris = await migration.aggiornaCategorieESottoCategorie();
|
||||
} else if (mydata.dbop === 'ReplaceUsername') {
|
||||
if (User.isAdmin(req.user.perm)) {
|
||||
ris = globalTables.replaceUsername(req.body.idapp, mydata.search_username, mydata.replace_username);
|
||||
@@ -270,6 +270,8 @@ class CronMod {
|
||||
await Order.RemoveDeletedOrdersInOrderscart();
|
||||
} else if (mydata.dbop === 'CheckTransazioniCircuiti') {
|
||||
await Circuit.CheckTransazioniCircuiti(false);
|
||||
} else if (mydata.dbop === 'CalcNumTransCircuiti') {
|
||||
await Circuit.CheckTransazioniCircuiti(false, { setnumtransaction: true });
|
||||
} else if (mydata.dbop === 'CorreggiTransazioniCircuiti') {
|
||||
await Circuit.CheckTransazioniCircuiti(true);
|
||||
} else if (mydata.dbop === 'RemovePendentTransactions') {
|
||||
|
||||
@@ -2,8 +2,22 @@ const express = require('express');
|
||||
const { authenticate, authenticate_noerror } = require('../middleware/authenticate');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const templatesRouter = require('../routes/templates');
|
||||
const postersRouter = require('../routes/posters');
|
||||
const assetsRouter = require('../routes/assets');
|
||||
|
||||
const PageView = require('../models/PageView');
|
||||
|
||||
// const { Groq } = require('groq-sdk');
|
||||
|
||||
const fal = require('@fal-ai/client');
|
||||
|
||||
|
||||
const imageGenerator = require('../services/imageGenerator'); // Assicurati che il percorso sia corretto
|
||||
|
||||
const posterEditor = require('../services/PosterEditor'); // <--- Importa la nuova classe
|
||||
|
||||
const multer = require('multer');
|
||||
const XLSX = require('xlsx');
|
||||
|
||||
@@ -19,6 +33,10 @@ const { MyElem } = require('../models/myelem');
|
||||
|
||||
const axios = require('axios');
|
||||
|
||||
router.use('/templates', authenticate, templatesRouter);
|
||||
router.use('/posters', authenticate, postersRouter);
|
||||
router.use('/assets', authenticate, assetsRouter);
|
||||
|
||||
router.post('/test-lungo', authenticate, (req, res) => {
|
||||
const timeout = req.body.timeout;
|
||||
|
||||
@@ -389,7 +407,6 @@ router.post('/search-books', authenticate, async (req, res) => {
|
||||
let productfind = null;
|
||||
|
||||
for (let field of book) {
|
||||
|
||||
field = field.trim();
|
||||
let valido = typeof field === 'string' && field.length > 4 && field.length < 50;
|
||||
if (valido) {
|
||||
@@ -494,4 +511,46 @@ router.post('/chatbot', authenticate, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/generateposter', async (req, res) => {
|
||||
const {
|
||||
titolo, data, ora, luogo, descrizione, contatti, fotoDescrizione, stile,
|
||||
provider = 'hf' // Default a HF (Gratis)
|
||||
} = req.body;
|
||||
|
||||
// 1. Prompt per l'AI: Chiediamo SOLO lo sfondo, VIETIAMO il testo.
|
||||
// Questo garantisce che Flux si concentri sulla bellezza dell'immagine.
|
||||
const promptAI = `Vertical event poster background, ${stile || 'modern style, vivid colors'}.
|
||||
Subject: ${fotoDescrizione || 'abstract artistic shapes'}.
|
||||
Composition: Central empty space or clean layout suitable for overlaying text later.
|
||||
NO TEXT, NO LETTERS, clean illustration, high quality, 4k.`;
|
||||
|
||||
try {
|
||||
console.log('1. Generazione Sfondo AI...');
|
||||
// Genera solo l'immagine base
|
||||
const rawImageUrl = await imageGenerator.generate(provider, promptAI);
|
||||
|
||||
console.log('2. Composizione Grafica Testi...');
|
||||
// Sovrapponi i testi con Canvas
|
||||
const finalPosterBase64 = await posterEditor.createPoster(rawImageUrl, {
|
||||
titolo,
|
||||
data,
|
||||
ora,
|
||||
luogo,
|
||||
contatti
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
imageUrl: finalPosterBase64, // Restituisce l'immagine completa in base64
|
||||
step: 'AI + Canvas Composition'
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('Errore:', err.message);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
module.exports = router;
|
||||
|
||||
21
src/routes/assets.js
Normal file
21
src/routes/assets.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const assetController = require('../controllers/assetController');
|
||||
const { authenticate } = require('../middleware/authenticate');
|
||||
const upload = require('../middleware/upload');
|
||||
|
||||
// Upload
|
||||
router.post('/upload', authenticate, upload.single('file'), assetController.upload);
|
||||
router.post('/upload-multiple', authenticate, upload.array('files', 10), assetController.uploadMultiple);
|
||||
|
||||
// AI Generation
|
||||
router.post('/generate-ai', authenticate, assetController.generateAi);
|
||||
|
||||
// CRUD
|
||||
router.get('/', authenticate, assetController.list);
|
||||
router.get('/:id', assetController.getById);
|
||||
router.get('/:id/file', assetController.getFile);
|
||||
router.get('/:id/thumbnail', assetController.getThumbnail);
|
||||
router.delete('/:id', authenticate, assetController.delete);
|
||||
|
||||
module.exports = router;
|
||||
25
src/routes/posters.js
Normal file
25
src/routes/posters.js
Normal file
@@ -0,0 +1,25 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const posterController = require('../controllers/posterController');
|
||||
const { authenticate } = require('../middleware/authenticate');
|
||||
const upload = require('../middleware/upload');
|
||||
|
||||
// CRUD Posters
|
||||
router.post('/', authenticate, posterController.create);
|
||||
router.get('/', authenticate, posterController.list);
|
||||
router.get('/favorites', authenticate, posterController.listFavorites);
|
||||
router.get('/recent', authenticate, posterController.listRecent);
|
||||
router.get('/:id', authenticate, posterController.getById);
|
||||
router.put('/:id', authenticate, posterController.update);
|
||||
router.delete('/:id', authenticate, posterController.delete);
|
||||
|
||||
// Azioni speciali
|
||||
router.post('/:id/render', authenticate, posterController.render);
|
||||
router.post('/:id/regenerate-ai', authenticate, posterController.regenerateAi);
|
||||
router.get('/:id/download/:format', posterController.download);
|
||||
router.post('/:id/favorite', authenticate, posterController.toggleFavorite);
|
||||
|
||||
// Quick generate (come nella tua bozza)
|
||||
router.post('/quick-generate', authenticate, posterController.quickGenerate);
|
||||
|
||||
module.exports = router;
|
||||
17
src/routes/templates.js
Normal file
17
src/routes/templates.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const templateController = require('../controllers/templateController');
|
||||
const { authenticate } = require('../middleware/authenticate');
|
||||
|
||||
// CRUD Templates
|
||||
router.post('/', authenticate, templateController.create);
|
||||
router.get('/', templateController.list);
|
||||
router.get('/types', templateController.getTypes);
|
||||
router.get('/presets', templateController.getFormatPresets);
|
||||
router.get('/:id', templateController.getById);
|
||||
router.put('/:id', authenticate, templateController.update);
|
||||
router.delete('/:id', authenticate, templateController.delete);
|
||||
router.post('/:id/duplicate', authenticate, templateController.duplicate);
|
||||
router.get('/:id/preview', templateController.getPreview);
|
||||
|
||||
module.exports = router;
|
||||
33
src/scripts/seedTemplates.js
Normal file
33
src/scripts/seedTemplates.js
Normal file
@@ -0,0 +1,33 @@
|
||||
|
||||
const Template = require('../models/Template');
|
||||
const templateSeeds = require('../templates/template-seeds');
|
||||
|
||||
const MONGODB_URI = process.env.MONGODB_URI || '';
|
||||
|
||||
async function seedTemplates() {
|
||||
try {
|
||||
// await mongoose.connect(MONGODB_URI);
|
||||
// Opzionale: rimuovi template esistenti con stessi templateType
|
||||
const existingTypes = templateSeeds.map(t => t.templateType);
|
||||
await Template.deleteMany({ templateType: { $in: existingTypes } });
|
||||
console.log('✓ Template esistenti rimossi');
|
||||
|
||||
// Inserisci nuovi template
|
||||
const result = await Template.insertMany(templateSeeds);
|
||||
console.log(`✓ ${result.length} template inseriti con successo`);
|
||||
|
||||
// Log dei template creati
|
||||
result.forEach(t => {
|
||||
console.log(` - ${t.name} (${t.templateType})`);
|
||||
});
|
||||
|
||||
// await mongoose.disconnect();
|
||||
console.log('✓ Disconnesso da MongoDB');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('✗ Errore seeding:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
seedTemplates();
|
||||
@@ -14,4 +14,3 @@ const seedTemplates = async () => {
|
||||
};
|
||||
|
||||
seedTemplates();
|
||||
s
|
||||
106
src/sendemail.js
106
src/sendemail.js
@@ -368,17 +368,17 @@ function checkifSendEmail() {
|
||||
|
||||
module.exports = {
|
||||
sendEmail_base_e_manager: async function (idapp, template, to, mylocalsconf, replyTo, transport, previewonly) {
|
||||
await this.sendEmail_base(template, to, mylocalsconf, replyTo, transport, previewonly);
|
||||
await this.sendEmail_base(idapp, template, to, mylocalsconf, replyTo, transport, previewonly);
|
||||
|
||||
await this.sendEmail_base(template, tools.getAdminEmailByIdApp(idapp), mylocalsconf, '', transport, previewonly);
|
||||
await this.sendEmail_base(idapp, template, tools.getAdminEmailByIdApp(idapp), mylocalsconf, '', transport, previewonly);
|
||||
|
||||
if (tools.isManagAndAdminDifferent(idapp)) {
|
||||
const email = tools.getManagerEmailByIdApp(idapp);
|
||||
await this.sendEmail_base(template, email, mylocalsconf, '', transport, previewonly);
|
||||
await this.sendEmail_base(idapp, template, email, mylocalsconf, '', transport, previewonly);
|
||||
}
|
||||
},
|
||||
|
||||
sendEmail_base: async function (template, to, mylocalsconf, replyTo, transport, previewonly) {
|
||||
sendEmail_base: async function (idapp, template, to, mylocalsconf, replyTo, transport, previewonly) {
|
||||
if (to === '') return false;
|
||||
|
||||
// console.log('mylocalsconf', mylocalsconf);
|
||||
@@ -389,9 +389,17 @@ module.exports = {
|
||||
|
||||
if (!replyTo) replyTo = '';
|
||||
|
||||
const emailSender = mylocalsconf.dataemail.from;
|
||||
let senderName = '';
|
||||
if (idapp) {
|
||||
senderName = tools.getNomeAppByIdApp(mylocalsconf.idapp);
|
||||
}
|
||||
|
||||
const emailcompleta = senderName ? `"${senderName}" <${emailSender}>` : emailSender;
|
||||
|
||||
const paramemail = {
|
||||
message: {
|
||||
from: mylocalsconf.dataemail.from, // sender address
|
||||
from: emailcompleta,
|
||||
headers: {
|
||||
'Reply-To': replyTo,
|
||||
},
|
||||
@@ -457,9 +465,12 @@ module.exports = {
|
||||
|
||||
sendEmail_Normale: async function (mylocalsconf, to, subject, html, replyTo) {
|
||||
try {
|
||||
const emailSender = tools.getEmailByIdApp(mylocalsconf.idapp);
|
||||
const senderName = tools.getNomeAppByIdApp(mylocalsconf.idapp);
|
||||
|
||||
// setup e-mail data with unicode symbols
|
||||
var mailOptions = {
|
||||
from: tools.getEmailByIdApp(mylocalsconf.idapp), // sender address
|
||||
from: `"${senderName}" <${emailSender}>`,
|
||||
dataemail: await this.getdataemail(mylocalsconf.idapp),
|
||||
to: to,
|
||||
generateTextFromHTML: true,
|
||||
@@ -498,12 +509,12 @@ module.exports = {
|
||||
try {
|
||||
const reg = require('./reg/registration');
|
||||
|
||||
|
||||
const idverif = reg.getlinkregByEmail(idapp, email, username);
|
||||
|
||||
await User.setLinkToVerifiedEmail(idapp, username, idverif);
|
||||
|
||||
const strlinkreg = tools.getHostByIdApp(idapp) + process.env.CHECKREVERIF_EMAIL + `/?idapp=${idapp}&idlink=${idverif}`;
|
||||
const strlinkreg =
|
||||
tools.getHostByIdApp(idapp) + process.env.CHECKREVERIF_EMAIL + `/?idapp=${idapp}&idlink=${idverif}`;
|
||||
return strlinkreg;
|
||||
} catch (e) {
|
||||
console.error('ERROR getlinkVerifyEmail');
|
||||
@@ -522,15 +533,22 @@ module.exports = {
|
||||
},
|
||||
getLinkAbilitaCircuito: function (idapp, user, data) {
|
||||
if (data.token_circuito_da_ammettere) {
|
||||
const strlink = tools.getHostByIdApp(idapp) + `/abcirc/${data.cmd}/${data.token_circuito_da_ammettere}/${user.username}/${data.myusername}`;
|
||||
const strlink =
|
||||
tools.getHostByIdApp(idapp) +
|
||||
`/abcirc/${data.cmd}/${data.token_circuito_da_ammettere}/${user.username}/${data.myusername}`;
|
||||
return strlink;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
getPathEmail(idapp, email_template) {
|
||||
const RISO_TEMPLATES = ['reg_notifica_all_invitante', 'reg_email_benvenuto_ammesso', 'reg_chiedi_ammettere_all_invitante',
|
||||
'circuit_chiedi_facilitatori_di_entrare', 'circuit_abilitato_al_fido_membro'];
|
||||
const RISO_TEMPLATES = [
|
||||
'reg_notifica_all_invitante',
|
||||
'reg_email_benvenuto_ammesso',
|
||||
'reg_chiedi_ammettere_all_invitante',
|
||||
'circuit_chiedi_facilitatori_di_entrare',
|
||||
'circuit_abilitato_al_fido_membro',
|
||||
];
|
||||
|
||||
if (idapp === '13') {
|
||||
if (RISO_TEMPLATES.includes(email_template)) {
|
||||
@@ -585,34 +603,24 @@ module.exports = {
|
||||
}
|
||||
|
||||
//Invia una email al nuovo utente
|
||||
await this.sendEmail_base(quale_email_inviare, emailto, mylocalsconf, tools.getreplyToEmailByIdApp(idapp));
|
||||
await this.sendEmail_base(idapp, quale_email_inviare, emailto, mylocalsconf, tools.getreplyToEmailByIdApp(idapp));
|
||||
|
||||
if (user.verified_email && user.aportador_solidario && user.verified_by_aportador) {
|
||||
const pathemail = this.getPathEmail(idapp, 'reg_notifica_all_invitante');
|
||||
|
||||
// Manda anche una email al suo Invitante
|
||||
const recaportador = await User.getUserShortDataByUsername(idapp, user.aportador_solidario);
|
||||
const ris = await this.sendEmail_base(
|
||||
pathemail + '/' + tools.LANGADMIN,
|
||||
recaportador.email,
|
||||
mylocalsconf,
|
||||
''
|
||||
);
|
||||
const ris = await this.sendEmail_base(idapp, pathemail + '/' + tools.LANGADMIN, recaportador.email, mylocalsconf, '');
|
||||
} else if (user.aportador_solidario && !user.verified_by_aportador) {
|
||||
const pathemail = this.getPathEmail(idapp, 'reg_chiedi_ammettere_all_invitante');
|
||||
|
||||
// Manda una email al suo Invitante per chiedere di essere ammesso
|
||||
const recaportador = await User.getUserShortDataByUsername(idapp, user.aportador_solidario);
|
||||
const ris = await this.sendEmail_base(
|
||||
pathemail + '/' + tools.LANGADMIN,
|
||||
recaportador.email,
|
||||
mylocalsconf,
|
||||
''
|
||||
);
|
||||
const ris = await this.sendEmail_base(idapp, pathemail + '/' + tools.LANGADMIN, recaportador.email, mylocalsconf, '');
|
||||
}
|
||||
|
||||
// Send to the Admin an Email
|
||||
const ris = await this.sendEmail_base(
|
||||
const ris = await this.sendEmail_base(idapp,
|
||||
'admin/registration/' + tools.LANGADMIN,
|
||||
tools.getAdminEmailByIdApp(idapp),
|
||||
mylocalsconf,
|
||||
@@ -654,7 +662,7 @@ module.exports = {
|
||||
messaggioPersonalizzato: dati.messaggioPersonalizzato,
|
||||
};
|
||||
|
||||
const ris = await this.sendEmail_base('invitaamico/' + lang, emailto, mylocalsconf, '');
|
||||
const ris = await this.sendEmail_base(idapp, 'invitaamico/' + lang, emailto, mylocalsconf, '');
|
||||
|
||||
await telegrambot.notifyToTelegram(telegrambot.phase.INVITA_AMICO, mylocalsconf);
|
||||
|
||||
@@ -681,7 +689,7 @@ module.exports = {
|
||||
|
||||
const quale_email_inviare = this.getPathEmail(idapp, 'reg_email_benvenuto_ammesso') + '/' + lang;
|
||||
|
||||
const ris = await this.sendEmail_base(quale_email_inviare, emailto, mylocalsconf, '');
|
||||
const ris = await this.sendEmail_base(idapp, quale_email_inviare, emailto, mylocalsconf, '');
|
||||
|
||||
await telegrambot.notifyToTelegram(telegrambot.phase.AMMETTI_UTENTE, mylocalsconf);
|
||||
|
||||
@@ -715,7 +723,7 @@ module.exports = {
|
||||
|
||||
const quale_email_inviare = this.getPathEmail(idapp, 'reg_resend_email_to_verifiyng') + '/' + lang;
|
||||
|
||||
const ris = await this.sendEmail_base(quale_email_inviare, email, mylocalsconf, '');
|
||||
const ris = await this.sendEmail_base(idapp, quale_email_inviare, email, mylocalsconf, '');
|
||||
|
||||
return ris;
|
||||
} catch (e) {
|
||||
@@ -724,6 +732,8 @@ module.exports = {
|
||||
},
|
||||
sendEmail_Utente_Abilitato_Circuito_FidoConcesso: async function (lang, emailto, user, idapp, dati) {
|
||||
try {
|
||||
const { Circuit } = require('../models/circuit');
|
||||
|
||||
let mylocalsconf = {
|
||||
idapp,
|
||||
dataemail: await this.getdataemail(idapp),
|
||||
@@ -735,14 +745,15 @@ module.exports = {
|
||||
usernameInvitante: dati.usernameInvitante,
|
||||
linkProfiloAdmin: tools.getLinkUserProfile(idapp, dati.usernameInvitante),
|
||||
user,
|
||||
symbol: await Circuit.getSymbolByCircuitId(dati.circuitId),
|
||||
usernameMembro: user.username,
|
||||
nomeTerritorio: dati.nomeTerritorio,
|
||||
linkTelegramTerritorio: dati.link_group,
|
||||
};
|
||||
};
|
||||
|
||||
const quale_email_inviare = this.getPathEmail(idapp, 'circuit_abilitato_al_fido_membro') + '/' + lang;
|
||||
|
||||
const ris = await this.sendEmail_base(quale_email_inviare, emailto, mylocalsconf, '');
|
||||
const ris = await this.sendEmail_base(idapp, quale_email_inviare, emailto, mylocalsconf, '');
|
||||
|
||||
await telegrambot.notifyToTelegram(telegrambot.phase.AMMETTI_UTENTE, mylocalsconf);
|
||||
|
||||
@@ -751,7 +762,14 @@ module.exports = {
|
||||
console.error('Err sendEmail_Utente_Ammesso', e);
|
||||
}
|
||||
},
|
||||
sendEmail_Richiesta_Al_Facilitatore_Di_FarEntrare_AlCircuito: async function (lang, emailto, user, userInvitante, idapp, dati) {
|
||||
sendEmail_Richiesta_Al_Facilitatore_Di_FarEntrare_AlCircuito: async function (
|
||||
lang,
|
||||
emailto,
|
||||
user,
|
||||
userInvitante,
|
||||
idapp,
|
||||
dati
|
||||
) {
|
||||
try {
|
||||
// dati.circuitId
|
||||
// dati.groupname
|
||||
@@ -759,6 +777,8 @@ module.exports = {
|
||||
|
||||
const linkAbilitazione = this.getLinkAbilitaCircuito(idapp, user, dati);
|
||||
|
||||
const { Circuit } = require('../models/circuit');
|
||||
|
||||
let mylocalsconf = {
|
||||
idapp,
|
||||
dataemail: await this.getdataemail(idapp),
|
||||
@@ -780,6 +800,7 @@ module.exports = {
|
||||
comuneResidenza: user.profile.resid_str_comune,
|
||||
provinciaResidenza: user.profile.resid_province,
|
||||
user,
|
||||
symbol: await Circuit.getSymbolByCircuitId(dati.circuitId),
|
||||
linkAbilitazione: linkAbilitazione,
|
||||
linkProfiloMembro: tools.getLinkUserProfile(idapp, user.username),
|
||||
linkProfiloInvitante: tools.getLinkUserProfile(idapp, user.aportador_solidario),
|
||||
@@ -789,7 +810,7 @@ module.exports = {
|
||||
|
||||
const quale_email_inviare = this.getPathEmail(idapp, 'circuit_chiedi_facilitatori_di_entrare') + '/' + lang;
|
||||
|
||||
const ris = await this.sendEmail_base(quale_email_inviare, emailto, mylocalsconf, '');
|
||||
const ris = await this.sendEmail_base(idapp, quale_email_inviare, emailto, mylocalsconf, '');
|
||||
|
||||
// await telegrambot.notifyToTelegram(telegrambot.phase.AMMETTI_UTENTE, mylocalsconf);
|
||||
|
||||
@@ -817,6 +838,7 @@ module.exports = {
|
||||
mylocalsconf = this.setParamsForTemplate(iscritto, mylocalsconf);
|
||||
|
||||
await this.sendEmail_base(
|
||||
idapp,
|
||||
'iscrizione_conacreis/' + lang,
|
||||
emailto,
|
||||
mylocalsconf,
|
||||
@@ -825,6 +847,7 @@ module.exports = {
|
||||
|
||||
// Send to the Admin an Email
|
||||
await this.sendEmail_base(
|
||||
idapp,
|
||||
'admin/iscrizione_conacreis/' + tools.LANGADMIN,
|
||||
tools.getAdminEmailByIdApp(idapp),
|
||||
mylocalsconf,
|
||||
@@ -845,6 +868,7 @@ module.exports = {
|
||||
|
||||
if (tools.isManagAndAdminDifferent(idapp)) {
|
||||
await this.sendEmail_base(
|
||||
idapp,
|
||||
'admin/iscrizione_conacreis/' + tools.LANGADMIN,
|
||||
tools.getManagerEmailByIdApp(idapp),
|
||||
mylocalsconf,
|
||||
@@ -868,7 +892,7 @@ module.exports = {
|
||||
|
||||
mylocalsconf = this.setParamsForTemplate(user, mylocalsconf);
|
||||
|
||||
await this.sendEmail_base('resetpwd/' + lang, emailto, mylocalsconf, '');
|
||||
await this.sendEmail_base(idapp, 'resetpwd/' + lang, emailto, mylocalsconf, '');
|
||||
},
|
||||
|
||||
sendEmail_RisRicevuti: async function (lang, userDest, emailto, idapp, myrec, extrarec) {
|
||||
@@ -895,7 +919,7 @@ module.exports = {
|
||||
|
||||
mylocalsconf = this.setParamsForTemplate(userDest, mylocalsconf);
|
||||
|
||||
await this.sendEmail_base('risricevuti/' + lang, emailto, mylocalsconf, '');
|
||||
await this.sendEmail_base(idapp, 'risricevuti/' + lang, emailto, mylocalsconf, '');
|
||||
},
|
||||
|
||||
sendEmail_Booking: async function (res, lang, emailto, user, idapp, recbooking) {
|
||||
@@ -933,6 +957,7 @@ module.exports = {
|
||||
}
|
||||
|
||||
await this.sendEmail_base(
|
||||
idapp,
|
||||
'booking/' + texthtml + '/' + lang,
|
||||
emailto,
|
||||
mylocalsconf,
|
||||
@@ -941,6 +966,7 @@ module.exports = {
|
||||
|
||||
// Send Email also to the Admin
|
||||
await this.sendEmail_base(
|
||||
idapp,
|
||||
'admin/' + texthtml + '/' + tools.LANGADMIN,
|
||||
tools.getAdminEmailByIdApp(idapp),
|
||||
mylocalsconf,
|
||||
@@ -949,6 +975,7 @@ module.exports = {
|
||||
|
||||
if (tools.isManagAndAdminDifferent(idapp)) {
|
||||
await this.sendEmail_base(
|
||||
idapp,
|
||||
'admin/' + texthtml + '/' + tools.LANGADMIN,
|
||||
tools.getManagerEmailByIdApp(idapp),
|
||||
mylocalsconf,
|
||||
@@ -1046,6 +1073,7 @@ module.exports = {
|
||||
telegrambot.sendMsgTelegramToTheManagers(idapp, msgtelegram);
|
||||
|
||||
await this.sendEmail_base(
|
||||
idapp,
|
||||
'booking/cancelbooking/' + lang,
|
||||
emailto,
|
||||
mylocalsconf,
|
||||
@@ -1054,6 +1082,7 @@ module.exports = {
|
||||
|
||||
// Send Email also to the Admin
|
||||
await this.sendEmail_base(
|
||||
idapp,
|
||||
'admin/cancelbooking/' + tools.LANGADMIN,
|
||||
tools.getAdminEmailByIdApp(idapp),
|
||||
mylocalsconf,
|
||||
@@ -1062,6 +1091,7 @@ module.exports = {
|
||||
|
||||
if (tools.isManagAndAdminDifferent(idapp)) {
|
||||
await this.sendEmail_base(
|
||||
idapp,
|
||||
'admin/cancelbooking/' + tools.LANGADMIN,
|
||||
tools.getManagerEmailByIdApp(idapp),
|
||||
mylocalsconf,
|
||||
@@ -1093,7 +1123,7 @@ module.exports = {
|
||||
if (mylocalsconf.infoevent !== '') replyto = user.email;
|
||||
else replyto = tools.getreplyToEmailByIdApp(idapp);
|
||||
|
||||
return await this.sendEmail_base('msg/sendmsg/' + lang, emailto, mylocalsconf, replyto);
|
||||
return await this.sendEmail_base(idapp, 'msg/sendmsg/' + lang, emailto, mylocalsconf, replyto);
|
||||
|
||||
// Send Email also to the Admin
|
||||
// this.sendEmail_base('admin/sendmsg/' + lang, tools.getAdminEmailByIdApp(idapp), mylocalsconf);
|
||||
@@ -1215,6 +1245,7 @@ module.exports = {
|
||||
if (sendnews) {
|
||||
// Send to the Admin an Email
|
||||
await this.sendEmail_base(
|
||||
idapp,
|
||||
'admin/added_to_newsletter/' + tools.LANGADMIN,
|
||||
tools.getAdminEmailByIdApp(idapp),
|
||||
mylocalsconf,
|
||||
@@ -1223,6 +1254,7 @@ module.exports = {
|
||||
|
||||
if (tools.isManagAndAdminDifferent(idapp)) {
|
||||
await this.sendEmail_base(
|
||||
idapp,
|
||||
'admin/added_to_newsletter/' + tools.LANGADMIN,
|
||||
tools.getManagerEmailByIdApp(idapp),
|
||||
mylocalsconf,
|
||||
@@ -1521,6 +1553,7 @@ module.exports = {
|
||||
|
||||
if (status !== shared_consts.OrderStatus.CANCELED && status !== shared_consts.OrderStatus.COMPLETED) {
|
||||
const esito = await this.sendEmail_base(
|
||||
idapp,
|
||||
'ecommerce/' + ordertype + '/' + lang,
|
||||
mylocalsconf.emailto,
|
||||
mylocalsconf,
|
||||
@@ -1617,6 +1650,7 @@ module.exports = {
|
||||
// Send Email to the User
|
||||
// console.log('-> Invio Email (', mynewsrec.numemail_sent, '/', mynewsrec.numemail_tot, ')');
|
||||
const esito = await this.sendEmail_base(
|
||||
idapp,
|
||||
'newsletter/' + lang,
|
||||
mylocalsconf.emailto,
|
||||
mylocalsconf,
|
||||
@@ -1756,6 +1790,7 @@ module.exports = {
|
||||
|
||||
console.log('-> Invio Email TEST a', mylocalsconf.emailto, 'previewonly', previewonly);
|
||||
return await this.sendEmail_base(
|
||||
idapp,
|
||||
'newsletter/' + lang,
|
||||
mylocalsconf.emailto,
|
||||
mylocalsconf,
|
||||
@@ -1796,6 +1831,7 @@ module.exports = {
|
||||
|
||||
console.log('-> Invio Email ' + mylocalsconf.subject + ' a', mylocalsconf.emailto, 'in corso...');
|
||||
const risult = await this.sendEmail_base(
|
||||
idapp,
|
||||
'newsletter/' + userto.lang,
|
||||
mylocalsconf.emailto,
|
||||
mylocalsconf,
|
||||
|
||||
@@ -120,6 +120,10 @@ async function runStartupTasks() {
|
||||
|
||||
await inizia();
|
||||
|
||||
if (true) {
|
||||
// const Seed = require('../scripts/seedTemplates');
|
||||
}
|
||||
|
||||
// 4) reset job pendenti
|
||||
await resetProcessingJob();
|
||||
|
||||
|
||||
@@ -59,6 +59,10 @@ function setupRouters(app) {
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
151
src/services/PosterEditor.js
Normal file
151
src/services/PosterEditor.js
Normal file
@@ -0,0 +1,151 @@
|
||||
const { createCanvas, loadImage } = require('canvas');
|
||||
|
||||
class PosterEditor {
|
||||
/**
|
||||
* Crea poster con testi sovrapposti (compatibile con tua API originale)
|
||||
*/
|
||||
async createPoster(backgroundImageUrl, data) {
|
||||
const { titolo, data: eventDate, ora, luogo, contatti } = data;
|
||||
|
||||
const width = 1080;
|
||||
const height = 1920;
|
||||
|
||||
const canvas = createCanvas(width, height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Carica e disegna background
|
||||
try {
|
||||
let img;
|
||||
if (backgroundImageUrl.startsWith('data:')) {
|
||||
img = await loadImage(backgroundImageUrl);
|
||||
} else {
|
||||
const fetch = require('node-fetch');
|
||||
const response = await fetch(backgroundImageUrl);
|
||||
const buffer = await response.buffer();
|
||||
img = await loadImage(buffer);
|
||||
}
|
||||
|
||||
// Cover fit
|
||||
const imgRatio = img.width / img.height;
|
||||
const canvasRatio = width / height;
|
||||
|
||||
let dw, dh, dx, dy;
|
||||
if (imgRatio > canvasRatio) {
|
||||
dh = height;
|
||||
dw = height * imgRatio;
|
||||
dx = (width - dw) / 2;
|
||||
dy = 0;
|
||||
} else {
|
||||
dw = width;
|
||||
dh = width / imgRatio;
|
||||
dx = 0;
|
||||
dy = (height - dh) / 2;
|
||||
}
|
||||
|
||||
ctx.drawImage(img, dx, dy, dw, dh);
|
||||
} catch (e) {
|
||||
// Fallback colore solido
|
||||
ctx.fillStyle = '#1a1a2e';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
// Overlay gradient
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, height);
|
||||
gradient.addColorStop(0, 'rgba(0,0,0,0)');
|
||||
gradient.addColorStop(0.5, 'rgba(0,0,0,0.3)');
|
||||
gradient.addColorStop(1, 'rgba(0,0,0,0.85)');
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
// Titolo
|
||||
if (titolo) {
|
||||
ctx.save();
|
||||
ctx.font = 'bold 68px Arial, sans-serif';
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.shadowColor = 'rgba(0,0,0,0.9)';
|
||||
ctx.shadowBlur = 20;
|
||||
ctx.shadowOffsetX = 3;
|
||||
ctx.shadowOffsetY = 3;
|
||||
|
||||
// Word wrap manuale
|
||||
const maxWidth = width * 0.9;
|
||||
const lines = this._wrapText(ctx, titolo.toUpperCase(), maxWidth);
|
||||
const lineHeight = 80;
|
||||
const startY = height * 0.50 - ((lines.length - 1) * lineHeight) / 2;
|
||||
|
||||
lines.forEach((line, i) => {
|
||||
ctx.fillText(line, width / 2, startY + i * lineHeight);
|
||||
});
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Data e ora
|
||||
if (eventDate) {
|
||||
ctx.save();
|
||||
ctx.font = 'bold 44px Arial, sans-serif';
|
||||
ctx.fillStyle = '#ffd700';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.shadowColor = 'rgba(0,0,0,0.8)';
|
||||
ctx.shadowBlur = 10;
|
||||
|
||||
const dateText = ora ? `${eventDate} • ORE ${ora}` : eventDate;
|
||||
ctx.fillText(dateText.toUpperCase(), width / 2, height * 0.68);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Luogo
|
||||
if (luogo) {
|
||||
ctx.save();
|
||||
ctx.font = '600 30px Arial, sans-serif';
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(`📍 ${luogo}`, width / 2, height * 0.76);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Contatti
|
||||
if (contatti) {
|
||||
ctx.save();
|
||||
ctx.font = '400 24px Arial, sans-serif';
|
||||
ctx.fillStyle = '#cccccc';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(contatti, width / 2, height * 0.85);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Ritorna base64
|
||||
return canvas.toDataURL('image/png');
|
||||
}
|
||||
|
||||
/**
|
||||
* Word wrap utility
|
||||
*/
|
||||
_wrapText(ctx, text, maxWidth) {
|
||||
const words = text.split(' ');
|
||||
const lines = [];
|
||||
let currentLine = '';
|
||||
|
||||
words.forEach(word => {
|
||||
const testLine = currentLine ? `${currentLine} ${word}` : word;
|
||||
const metrics = ctx.measureText(testLine);
|
||||
|
||||
if (metrics.width > maxWidth && currentLine) {
|
||||
lines.push(currentLine);
|
||||
currentLine = word;
|
||||
} else {
|
||||
currentLine = testLine;
|
||||
}
|
||||
});
|
||||
|
||||
if (currentLine) {
|
||||
lines.push(currentLine);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new PosterEditor();
|
||||
154
src/services/imageGenerator.js
Normal file
154
src/services/imageGenerator.js
Normal file
@@ -0,0 +1,154 @@
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
class ImageGenerator {
|
||||
constructor() {
|
||||
this.falKey = process.env.FAL_KEY;
|
||||
this.hfToken = process.env.HF_TOKEN;
|
||||
this.ideogramKey = process.env.IDEOGRAM_KEY;
|
||||
}
|
||||
|
||||
async generate(provider, prompt, options = {}) {
|
||||
const {
|
||||
negativePrompt,
|
||||
aspectRatio = '9:16',
|
||||
model,
|
||||
seed,
|
||||
steps,
|
||||
cfg
|
||||
} = options;
|
||||
|
||||
switch (provider) {
|
||||
case 'ideogram':
|
||||
return this._generateIdeogram(prompt, { aspectRatio });
|
||||
case 'fal':
|
||||
return this._generateFal(prompt, { aspectRatio, seed, steps, cfg });
|
||||
case 'hf':
|
||||
default:
|
||||
return this._generateHuggingFace(prompt, { negativePrompt });
|
||||
}
|
||||
}
|
||||
|
||||
// Ideogram V2 (via Fal.ai) - Ottimo per testo
|
||||
async _generateIdeogram(prompt, options = {}) {
|
||||
console.log('--- Generazione Ideogram V2 ---');
|
||||
|
||||
const response = await fetch('https://fal.run/fal-ai/ideogram/v2', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Key ${this.falKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
prompt,
|
||||
aspect_ratio: options.aspectRatio || '9:16',
|
||||
style_type: 'DESIGN',
|
||||
expand_prompt: true
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Ideogram error: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const imageUrl = result.images?.[0]?.url;
|
||||
|
||||
if (!imageUrl) throw new Error('Ideogram: nessun URL restituito');
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// Flux Dev (via Fal.ai)
|
||||
async _generateFal(prompt, options = {}) {
|
||||
console.log('--- Generazione Fal Flux Dev ---');
|
||||
|
||||
const imageSizeMap = {
|
||||
'9:16': 'portrait_16_9',
|
||||
'16:9': 'landscape_16_9',
|
||||
'1:1': 'square'
|
||||
};
|
||||
|
||||
const response = await fetch('https://fal.run/fal-ai/flux/dev', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Key ${this.falKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
prompt,
|
||||
image_size: imageSizeMap[options.aspectRatio] || 'portrait_16_9',
|
||||
num_images: 1,
|
||||
enable_safety_checker: false,
|
||||
seed: options.seed,
|
||||
num_inference_steps: options.steps || 28,
|
||||
guidance_scale: options.cfg || 7.5
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Fal error: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result.images?.[0]?.url;
|
||||
}
|
||||
|
||||
// Flux Dev (via Hugging Face) - GRATIS
|
||||
async _generateHuggingFace(prompt, options = {}) {
|
||||
console.log('--- Generazione HuggingFace (Gratis) ---');
|
||||
|
||||
const response = await fetch(
|
||||
'https://router.huggingface.co/hf-inference/models/black-forest-labs/FLUX.1-dev',
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.hfToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
inputs: prompt,
|
||||
parameters: {
|
||||
negative_prompt: options.negativePrompt
|
||||
}
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await response.text();
|
||||
throw new Error(`HuggingFace error: ${response.status} - ${err}`);
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
return `data:image/jpeg;base64,${buffer.toString('base64')}`;
|
||||
}
|
||||
|
||||
// Utility: verifica disponibilità provider
|
||||
async checkProvider(provider) {
|
||||
const checks = {
|
||||
hf: !!this.hfToken,
|
||||
fal: !!this.falKey,
|
||||
ideogram: !!this.falKey // Ideogram via Fal
|
||||
};
|
||||
|
||||
return checks[provider] || false;
|
||||
}
|
||||
|
||||
// Lista provider disponibili
|
||||
getAvailableProviders() {
|
||||
const providers = [];
|
||||
|
||||
if (this.hfToken) {
|
||||
providers.push({ id: 'hf', name: 'HuggingFace (Gratis)', cost: 'free' });
|
||||
}
|
||||
if (this.falKey) {
|
||||
providers.push({ id: 'fal', name: 'Fal Flux Dev', cost: 'paid' });
|
||||
providers.push({ id: 'ideogram', name: 'Ideogram V2', cost: 'paid' });
|
||||
}
|
||||
|
||||
return providers;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ImageGenerator();
|
||||
870
src/services/posterRenderer.js
Normal file
870
src/services/posterRenderer.js
Normal file
@@ -0,0 +1,870 @@
|
||||
|
||||
const { createCanvas, loadImage, registerFont } = require('canvas');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const sharp = require('sharp');
|
||||
|
||||
// Registra font personalizzati
|
||||
const FONTS_DIR = process.env.FONTS_DIR || './fonts';
|
||||
|
||||
const registerFonts = async () => {
|
||||
const fontMappings = [
|
||||
{ file: 'Montserrat-Black.ttf', family: 'Montserrat', weight: '900' },
|
||||
{ file: 'Montserrat-Bold.ttf', family: 'Montserrat', weight: '700' },
|
||||
{ file: 'Montserrat-Regular.ttf', family: 'Montserrat', weight: '400' },
|
||||
{ file: 'BebasNeue-Regular.ttf', family: 'Bebas Neue', weight: '400' },
|
||||
{ file: 'OpenSans-Bold.ttf', family: 'Open Sans', weight: '700' },
|
||||
{ file: 'OpenSans-SemiBold.ttf', family: 'Open Sans', weight: '600' },
|
||||
{ file: 'OpenSans-Regular.ttf', family: 'Open Sans', weight: '400' },
|
||||
{ file: 'OpenSans-Light.ttf', family: 'Open Sans', weight: '300' },
|
||||
{ file: 'PlayfairDisplay-Bold.ttf', family: 'Playfair Display', weight: '700' },
|
||||
{ file: 'PlayfairDisplay-Regular.ttf', family: 'Playfair Display', weight: '400' }
|
||||
];
|
||||
|
||||
for (const font of fontMappings) {
|
||||
const fontPath = path.join(FONTS_DIR, font.file);
|
||||
try {
|
||||
await fs.access(fontPath);
|
||||
registerFont(fontPath, { family: font.family, weight: font.weight });
|
||||
} catch (e) {
|
||||
// Font non trovato, usa fallback
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Inizializza fonts al caricamento modulo
|
||||
registerFonts().catch(console.warn);
|
||||
|
||||
class PosterRenderer {
|
||||
constructor() {
|
||||
this.version = '1.0.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render principale
|
||||
*/
|
||||
async render(options) {
|
||||
const {
|
||||
template,
|
||||
content,
|
||||
assets,
|
||||
layerOverrides = {},
|
||||
outputDir,
|
||||
posterId
|
||||
} = options;
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
// Dimensioni canvas
|
||||
const width = template.format?.width || 2480;
|
||||
const height = template.format?.height || 3508;
|
||||
|
||||
// Crea canvas
|
||||
const canvas = createCanvas(width, height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// 1. Disegna background
|
||||
await this._drawBackground(ctx, template, assets, width, height);
|
||||
|
||||
// 2. Ordina layer per zIndex
|
||||
const sortedLayers = [...(template.layers || [])]
|
||||
.sort((a, b) => (a.zIndex || 0) - (b.zIndex || 0));
|
||||
|
||||
// 3. Disegna ogni layer
|
||||
for (const layer of sortedLayers) {
|
||||
if (layer.visible === false) continue;
|
||||
|
||||
const override = layerOverrides[layer.id] || {};
|
||||
const mergedLayer = this._mergeLayerOverride(layer, override);
|
||||
|
||||
await this._drawLayer(ctx, mergedLayer, content, assets, width, height, template);
|
||||
}
|
||||
|
||||
// 4. Disegna loghi
|
||||
if (template.logoSlots?.enabled && assets?.logos?.length > 0) {
|
||||
await this._drawLogos(ctx, template.logoSlots, assets.logos, width, height);
|
||||
}
|
||||
|
||||
// 5. Salva output
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
|
||||
const baseName = `poster_${posterId}_${Date.now()}`;
|
||||
const pngPath = path.join(outputDir, `${baseName}.png`);
|
||||
const jpgPath = path.join(outputDir, `${baseName}.jpg`);
|
||||
|
||||
// Salva PNG
|
||||
const pngBuffer = canvas.toBuffer('image/png');
|
||||
await fs.writeFile(pngPath, pngBuffer);
|
||||
|
||||
// Salva JPG con Sharp (migliore qualità)
|
||||
await sharp(pngBuffer)
|
||||
.jpeg({ quality: 95, progressive: true })
|
||||
.toFile(jpgPath);
|
||||
|
||||
const [pngStats, jpgStats] = await Promise.all([
|
||||
fs.stat(pngPath),
|
||||
fs.stat(jpgPath)
|
||||
]);
|
||||
|
||||
return {
|
||||
pngPath,
|
||||
jpgPath,
|
||||
pngSize: pngStats.size,
|
||||
jpgSize: jpgStats.size,
|
||||
dimensions: { width, height },
|
||||
duration: Date.now() - startTime,
|
||||
engineVersion: this.version
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Disegna background
|
||||
*/
|
||||
async _drawBackground(ctx, template, assets, width, height) {
|
||||
// Colore di sfondo base
|
||||
ctx.fillStyle = template.backgroundColor || '#1a1a2e';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
// Background image
|
||||
const bgAsset = assets?.backgroundImage;
|
||||
const bgLayer = template.layers?.find(l => l.type === 'backgroundImage');
|
||||
|
||||
if (bgAsset?.url) {
|
||||
try {
|
||||
const img = await this._loadImageFromUrl(bgAsset.url);
|
||||
|
||||
// Calcola dimensioni per cover
|
||||
const imgRatio = img.width / img.height;
|
||||
const canvasRatio = width / height;
|
||||
|
||||
let drawWidth, drawHeight, drawX, drawY;
|
||||
|
||||
if (imgRatio > canvasRatio) {
|
||||
drawHeight = height;
|
||||
drawWidth = height * imgRatio;
|
||||
drawX = (width - drawWidth) / 2;
|
||||
drawY = 0;
|
||||
} else {
|
||||
drawWidth = width;
|
||||
drawHeight = width / imgRatio;
|
||||
drawX = 0;
|
||||
drawY = (height - drawHeight) / 2;
|
||||
}
|
||||
|
||||
// Applica blur se definito
|
||||
if (bgLayer?.style?.blur > 0) {
|
||||
ctx.filter = `blur(${bgLayer.style.blur}px)`;
|
||||
}
|
||||
|
||||
ctx.drawImage(img, drawX, drawY, drawWidth, drawHeight);
|
||||
ctx.filter = 'none';
|
||||
|
||||
// Applica overlay gradient
|
||||
const overlay = bgLayer?.style?.overlay;
|
||||
if (overlay?.enabled) {
|
||||
this._drawOverlay(ctx, overlay, width, height);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Background image load failed:', e.message);
|
||||
// Usa fallback
|
||||
if (bgLayer?.fallback) {
|
||||
this._drawFallback(ctx, bgLayer.fallback, width, height);
|
||||
}
|
||||
}
|
||||
} else if (bgLayer?.fallback) {
|
||||
this._drawFallback(ctx, bgLayer.fallback, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disegna overlay gradient
|
||||
*/
|
||||
_drawOverlay(ctx, overlay, width, height) {
|
||||
if (overlay.type === 'solid') {
|
||||
ctx.fillStyle = overlay.color || 'rgba(0,0,0,0.5)';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
return;
|
||||
}
|
||||
|
||||
// Gradient
|
||||
let gradient;
|
||||
const dir = overlay.direction || 'to-bottom';
|
||||
|
||||
if (dir === 'to-bottom') {
|
||||
gradient = ctx.createLinearGradient(0, 0, 0, height);
|
||||
} else if (dir === 'to-top') {
|
||||
gradient = ctx.createLinearGradient(0, height, 0, 0);
|
||||
} else if (dir === 'to-right') {
|
||||
gradient = ctx.createLinearGradient(0, 0, width, 0);
|
||||
} else if (dir === 'to-left') {
|
||||
gradient = ctx.createLinearGradient(width, 0, 0, 0);
|
||||
} else if (dir === 'to-bottom-right') {
|
||||
gradient = ctx.createLinearGradient(0, 0, width, height);
|
||||
} else {
|
||||
gradient = ctx.createLinearGradient(0, 0, 0, height);
|
||||
}
|
||||
|
||||
if (overlay.stops) {
|
||||
overlay.stops.forEach(stop => {
|
||||
gradient.addColorStop(stop.position, stop.color);
|
||||
});
|
||||
} else {
|
||||
gradient.addColorStop(0, 'rgba(0,0,0,0)');
|
||||
gradient.addColorStop(1, 'rgba(0,0,0,0.7)');
|
||||
}
|
||||
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disegna fallback
|
||||
*/
|
||||
_drawFallback(ctx, fallback, width, height) {
|
||||
if (fallback.type === 'solid') {
|
||||
ctx.fillStyle = fallback.color || '#333333';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
} else if (fallback.type === 'gradient' && fallback.colors) {
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, height);
|
||||
fallback.colors.forEach((color, i) => {
|
||||
gradient.addColorStop(i / (fallback.colors.length - 1), color);
|
||||
});
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disegna singolo layer
|
||||
*/
|
||||
async _drawLayer(ctx, layer, content, assets, canvasWidth, canvasHeight, template) {
|
||||
const pos = this._calculatePosition(layer.position, layer.anchor, canvasWidth, canvasHeight);
|
||||
|
||||
switch (layer.type) {
|
||||
case 'backgroundImage':
|
||||
// Già gestito in _drawBackground
|
||||
break;
|
||||
|
||||
case 'mainImage':
|
||||
await this._drawMainImage(ctx, assets?.mainImage, pos, layer.style);
|
||||
break;
|
||||
|
||||
case 'title':
|
||||
this._drawText(ctx, content?.title, pos, layer.style, template.palette);
|
||||
break;
|
||||
|
||||
case 'subtitle':
|
||||
this._drawText(ctx, content?.subtitle, pos, layer.style, template.palette);
|
||||
break;
|
||||
|
||||
case 'eventDate':
|
||||
const dateText = content?.eventTime
|
||||
? `${content.eventDate} • ${content.eventTime}`
|
||||
: content?.eventDate;
|
||||
this._drawText(ctx, dateText, pos, layer.style, template.palette);
|
||||
break;
|
||||
|
||||
case 'eventTime':
|
||||
this._drawText(ctx, content?.eventTime, pos, layer.style, template.palette);
|
||||
break;
|
||||
|
||||
case 'location':
|
||||
this._drawTextWithIcon(ctx, content?.location, pos, layer, template.palette);
|
||||
break;
|
||||
|
||||
case 'contacts':
|
||||
this._drawText(ctx, content?.contacts, pos, layer.style, template.palette);
|
||||
break;
|
||||
|
||||
case 'extraText':
|
||||
const extraTexts = Array.isArray(content?.extraText)
|
||||
? content.extraText.join(' • ')
|
||||
: content?.extraText;
|
||||
this._drawText(ctx, extraTexts, pos, layer.style, template.palette);
|
||||
break;
|
||||
|
||||
case 'customText':
|
||||
const customValue = content?.customFields?.get(layer.id);
|
||||
this._drawText(ctx, customValue, pos, layer.style, template.palette);
|
||||
break;
|
||||
|
||||
case 'divider':
|
||||
this._drawDivider(ctx, pos, layer.style);
|
||||
break;
|
||||
|
||||
case 'shape':
|
||||
this._drawShape(ctx, pos, layer.style);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`Layer type non gestito: ${layer.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcola posizione assoluta da coordinate relative
|
||||
*/
|
||||
_calculatePosition(position, anchor, canvasWidth, canvasHeight) {
|
||||
const relX = position.x || 0;
|
||||
const relY = position.y || 0;
|
||||
const relW = position.w || 1;
|
||||
const relH = position.h || 0.1;
|
||||
|
||||
const absW = relW * canvasWidth;
|
||||
const absH = relH * canvasHeight;
|
||||
|
||||
let absX = relX * canvasWidth;
|
||||
let absY = relY * canvasHeight;
|
||||
|
||||
// Aggiusta per anchor
|
||||
switch (anchor) {
|
||||
case 'top-center':
|
||||
absX -= absW / 2;
|
||||
break;
|
||||
case 'top-right':
|
||||
absX -= absW;
|
||||
break;
|
||||
case 'center-left':
|
||||
absY -= absH / 2;
|
||||
break;
|
||||
case 'center':
|
||||
absX -= absW / 2;
|
||||
absY -= absH / 2;
|
||||
break;
|
||||
case 'center-right':
|
||||
absX -= absW;
|
||||
absY -= absH / 2;
|
||||
break;
|
||||
case 'bottom-left':
|
||||
absY -= absH;
|
||||
break;
|
||||
case 'bottom-center':
|
||||
absX -= absW / 2;
|
||||
absY -= absH;
|
||||
break;
|
||||
case 'bottom-right':
|
||||
absX -= absW;
|
||||
absY -= absH;
|
||||
break;
|
||||
// top-left è default, nessun aggiustamento
|
||||
}
|
||||
|
||||
return { x: absX, y: absY, w: absW, h: absH };
|
||||
}
|
||||
|
||||
/**
|
||||
* Disegna main image
|
||||
*/
|
||||
async _drawMainImage(ctx, asset, pos, style = {}) {
|
||||
if (!asset?.url) return;
|
||||
|
||||
try {
|
||||
const img = await this._loadImageFromUrl(asset.url);
|
||||
|
||||
ctx.save();
|
||||
|
||||
// Border radius (clip)
|
||||
const radius = style.borderRadius || 0;
|
||||
if (radius > 0) {
|
||||
this._roundRect(ctx, pos.x, pos.y, pos.w, pos.h, radius);
|
||||
ctx.clip();
|
||||
}
|
||||
|
||||
// Shadow
|
||||
if (style.shadow?.enabled) {
|
||||
ctx.shadowColor = style.shadow.color || 'rgba(0,0,0,0.5)';
|
||||
ctx.shadowBlur = style.shadow.blur || 20;
|
||||
ctx.shadowOffsetX = style.shadow.offsetX || 0;
|
||||
ctx.shadowOffsetY = style.shadow.offsetY || 10;
|
||||
}
|
||||
|
||||
// Calcola dimensioni per object-fit
|
||||
const { sx, sy, sw, sh, dx, dy, dw, dh } = this._calculateObjectFit(
|
||||
img.width, img.height, pos.w, pos.h, style.objectFit || 'cover'
|
||||
);
|
||||
|
||||
ctx.drawImage(img, sx, sy, sw, sh, pos.x + dx, pos.y + dy, dw, dh);
|
||||
|
||||
// Border
|
||||
if (style.border?.enabled) {
|
||||
ctx.strokeStyle = style.border.color || '#ffffff';
|
||||
ctx.lineWidth = style.border.width || 2;
|
||||
if (radius > 0) {
|
||||
this._roundRect(ctx, pos.x, pos.y, pos.w, pos.h, radius);
|
||||
} else {
|
||||
ctx.strokeRect(pos.x, pos.y, pos.w, pos.h);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
} catch (e) {
|
||||
console.warn('Main image load failed:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcola object-fit
|
||||
*/
|
||||
_calculateObjectFit(imgW, imgH, boxW, boxH, fit) {
|
||||
let sx = 0, sy = 0, sw = imgW, sh = imgH;
|
||||
let dx = 0, dy = 0, dw = boxW, dh = boxH;
|
||||
|
||||
const imgRatio = imgW / imgH;
|
||||
const boxRatio = boxW / boxH;
|
||||
|
||||
if (fit === 'cover') {
|
||||
if (imgRatio > boxRatio) {
|
||||
sw = imgH * boxRatio;
|
||||
sx = (imgW - sw) / 2;
|
||||
} else {
|
||||
sh = imgW / boxRatio;
|
||||
sy = (imgH - sh) / 2;
|
||||
}
|
||||
} else if (fit === 'contain') {
|
||||
if (imgRatio > boxRatio) {
|
||||
dh = boxW / imgRatio;
|
||||
dy = (boxH - dh) / 2;
|
||||
} else {
|
||||
dw = boxH * imgRatio;
|
||||
dx = (boxW - dw) / 2;
|
||||
}
|
||||
}
|
||||
// 'fill' usa valori default
|
||||
|
||||
return { sx, sy, sw, sh, dx, dy, dw, dh };
|
||||
}
|
||||
|
||||
/**
|
||||
* Disegna testo
|
||||
*/
|
||||
_drawText(ctx, text, pos, style = {}, palette = {}) {
|
||||
if (!text) return;
|
||||
|
||||
ctx.save();
|
||||
|
||||
// Font
|
||||
const fontWeight = style.fontWeight || 400;
|
||||
const fontSize = style.fontSize || 48;
|
||||
const fontFamily = style.fontFamily || 'Open Sans';
|
||||
ctx.font = `${fontWeight} ${fontSize}px "${fontFamily}"`;
|
||||
|
||||
// Colore
|
||||
ctx.fillStyle = style.color || palette.text || '#ffffff';
|
||||
|
||||
// Allineamento
|
||||
const align = style.textAlign || 'center';
|
||||
ctx.textAlign = align;
|
||||
ctx.textBaseline = 'middle';
|
||||
|
||||
// Transform
|
||||
let displayText = text;
|
||||
if (style.textTransform === 'uppercase') {
|
||||
displayText = text.toUpperCase();
|
||||
} else if (style.textTransform === 'lowercase') {
|
||||
displayText = text.toLowerCase();
|
||||
} else if (style.textTransform === 'capitalize') {
|
||||
displayText = text.replace(/\b\w/g, c => c.toUpperCase());
|
||||
}
|
||||
|
||||
// Calcola X in base ad allineamento
|
||||
let textX;
|
||||
if (align === 'center') {
|
||||
textX = pos.x + pos.w / 2;
|
||||
} else if (align === 'right') {
|
||||
textX = pos.x + pos.w;
|
||||
} else {
|
||||
textX = pos.x;
|
||||
}
|
||||
|
||||
const textY = pos.y + pos.h / 2;
|
||||
|
||||
// Letter spacing (manuale)
|
||||
if (style.letterSpacing && style.letterSpacing > 0) {
|
||||
this._drawTextWithSpacing(ctx, displayText, textX, textY, style, pos);
|
||||
} else {
|
||||
// Shadow
|
||||
if (style.shadow?.enabled) {
|
||||
ctx.shadowColor = style.shadow.color || 'rgba(0,0,0,0.8)';
|
||||
ctx.shadowBlur = style.shadow.blur || 10;
|
||||
ctx.shadowOffsetX = style.shadow.offsetX || 2;
|
||||
ctx.shadowOffsetY = style.shadow.offsetY || 2;
|
||||
}
|
||||
|
||||
// Stroke
|
||||
if (style.stroke?.enabled) {
|
||||
ctx.strokeStyle = style.stroke.color || 'rgba(0,0,0,0.5)';
|
||||
ctx.lineWidth = style.stroke.width || 2;
|
||||
ctx.strokeText(displayText, textX, textY);
|
||||
}
|
||||
|
||||
// Fill
|
||||
ctx.fillText(displayText, textX, textY);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disegna testo con letter-spacing
|
||||
*/
|
||||
_drawTextWithSpacing(ctx, text, x, y, style, pos) {
|
||||
const spacing = style.letterSpacing || 0;
|
||||
const chars = text.split('');
|
||||
|
||||
// Calcola larghezza totale
|
||||
let totalWidth = 0;
|
||||
chars.forEach(char => {
|
||||
totalWidth += ctx.measureText(char).width + spacing;
|
||||
});
|
||||
totalWidth -= spacing; // Rimuovi ultimo spacing
|
||||
|
||||
// Calcola startX in base ad allineamento
|
||||
let startX;
|
||||
if (style.textAlign === 'center') {
|
||||
startX = x - totalWidth / 2;
|
||||
} else if (style.textAlign === 'right') {
|
||||
startX = x - totalWidth;
|
||||
} else {
|
||||
startX = x;
|
||||
}
|
||||
|
||||
// Shadow
|
||||
if (style.shadow?.enabled) {
|
||||
ctx.shadowColor = style.shadow.color || 'rgba(0,0,0,0.8)';
|
||||
ctx.shadowBlur = style.shadow.blur || 10;
|
||||
ctx.shadowOffsetX = style.shadow.offsetX || 2;
|
||||
ctx.shadowOffsetY = style.shadow.offsetY || 2;
|
||||
}
|
||||
|
||||
// Disegna ogni carattere
|
||||
ctx.textAlign = 'left';
|
||||
let currentX = startX;
|
||||
|
||||
chars.forEach(char => {
|
||||
if (style.stroke?.enabled) {
|
||||
ctx.strokeStyle = style.stroke.color || 'rgba(0,0,0,0.5)';
|
||||
ctx.lineWidth = style.stroke.width || 2;
|
||||
ctx.strokeText(char, currentX, y);
|
||||
}
|
||||
ctx.fillText(char, currentX, y);
|
||||
currentX += ctx.measureText(char).width + spacing;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disegna testo con icona
|
||||
*/
|
||||
_drawTextWithIcon(ctx, text, pos, layer, palette) {
|
||||
if (!text) return;
|
||||
|
||||
const icon = layer.icon;
|
||||
const style = layer.style || {};
|
||||
|
||||
// Se icona abilitata, disegna simbolo prima del testo
|
||||
if (icon?.enabled) {
|
||||
ctx.save();
|
||||
|
||||
const iconSize = icon.size || 24;
|
||||
const iconColor = icon.color || palette?.accent || '#e74c3c';
|
||||
|
||||
// Disegna simbolo location semplificato
|
||||
ctx.fillStyle = iconColor;
|
||||
ctx.font = `${iconSize}px Arial`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
|
||||
const iconChar = '📍'; // Emoji o usa font icon
|
||||
const textWithIcon = `${iconChar} ${text}`;
|
||||
|
||||
// Ora disegna testo normale con icona
|
||||
this._drawText(ctx, textWithIcon, pos, style, palette);
|
||||
|
||||
ctx.restore();
|
||||
} else {
|
||||
this._drawText(ctx, text, pos, style, palette);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disegna loghi
|
||||
*/
|
||||
async _drawLogos(ctx, logoSlots, logos, canvasWidth, canvasHeight) {
|
||||
const slots = logoSlots.slots || [];
|
||||
const maxCount = Math.min(logos.length, logoSlots.maxCount || 3, slots.length);
|
||||
|
||||
for (let i = 0; i < maxCount; i++) {
|
||||
const logo = logos[i];
|
||||
const slot = slots[i];
|
||||
|
||||
if (!logo?.url || !slot) continue;
|
||||
|
||||
try {
|
||||
const img = await this._loadImageFromUrl(logo.url);
|
||||
const pos = this._calculatePosition(slot.position, slot.anchor, canvasWidth, canvasHeight);
|
||||
|
||||
ctx.save();
|
||||
|
||||
// Opacity
|
||||
ctx.globalAlpha = slot.style?.opacity ?? 0.9;
|
||||
|
||||
// Object fit contain per loghi
|
||||
const { sx, sy, sw, sh, dx, dy, dw, dh } = this._calculateObjectFit(
|
||||
img.width, img.height, pos.w, pos.h, 'contain'
|
||||
);
|
||||
|
||||
ctx.drawImage(img, sx, sy, sw, sh, pos.x + dx, pos.y + dy, dw, dh);
|
||||
|
||||
ctx.restore();
|
||||
} catch (e) {
|
||||
console.warn(`Logo ${i} load failed:`, e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disegna divider
|
||||
*/
|
||||
_drawDivider(ctx, pos, style = {}) {
|
||||
ctx.save();
|
||||
|
||||
ctx.strokeStyle = style.color || '#ffffff';
|
||||
ctx.lineWidth = style.width || 2;
|
||||
ctx.globalAlpha = style.opacity || 0.5;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(pos.x, pos.y + pos.h / 2);
|
||||
ctx.lineTo(pos.x + pos.w, pos.y + pos.h / 2);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disegna shape
|
||||
*/
|
||||
_drawShape(ctx, pos, style = {}) {
|
||||
ctx.save();
|
||||
|
||||
ctx.fillStyle = style.fill || 'rgba(255,255,255,0.1)';
|
||||
ctx.strokeStyle = style.stroke || 'transparent';
|
||||
ctx.lineWidth = style.strokeWidth || 0;
|
||||
ctx.globalAlpha = style.opacity || 1;
|
||||
|
||||
const shape = style.shape || 'rectangle';
|
||||
const radius = style.borderRadius || 0;
|
||||
|
||||
if (shape === 'rectangle') {
|
||||
if (radius > 0) {
|
||||
this._roundRect(ctx, pos.x, pos.y, pos.w, pos.h, radius);
|
||||
ctx.fill();
|
||||
if (style.strokeWidth) ctx.stroke();
|
||||
} else {
|
||||
ctx.fillRect(pos.x, pos.y, pos.w, pos.h);
|
||||
if (style.strokeWidth) ctx.strokeRect(pos.x, pos.y, pos.w, pos.h);
|
||||
}
|
||||
} else if (shape === 'circle' || shape === 'ellipse') {
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(
|
||||
pos.x + pos.w / 2,
|
||||
pos.y + pos.h / 2,
|
||||
pos.w / 2,
|
||||
pos.h / 2,
|
||||
0, 0, Math.PI * 2
|
||||
);
|
||||
ctx.fill();
|
||||
if (style.strokeWidth) ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility: rounded rectangle
|
||||
*/
|
||||
_roundRect(ctx, x, y, w, h, radius) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + radius, y);
|
||||
ctx.lineTo(x + w - radius, y);
|
||||
ctx.quadraticCurveTo(x + w, y, x + w, y + radius);
|
||||
ctx.lineTo(x + w, y + h - radius);
|
||||
ctx.quadraticCurveTo(x + w, y + h, x + w - radius, y + h);
|
||||
ctx.lineTo(x + radius, y + h);
|
||||
ctx.quadraticCurveTo(x, y + h, x, y + h - radius);
|
||||
ctx.lineTo(x, y + radius);
|
||||
ctx.quadraticCurveTo(x, y, x + radius, y);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility: carica immagine da URL o path locale
|
||||
*/
|
||||
async _loadImageFromUrl(url) {
|
||||
if (!url) throw new Error('URL mancante');
|
||||
|
||||
// Base64
|
||||
if (url.startsWith('data:')) {
|
||||
return loadImage(url);
|
||||
}
|
||||
|
||||
// Path locale
|
||||
if (url.startsWith('/uploads') || url.startsWith('./uploads')) {
|
||||
const localPath = url.startsWith('/')
|
||||
? path.join(process.cwd(), url)
|
||||
: url;
|
||||
return loadImage(localPath);
|
||||
}
|
||||
|
||||
// URL remoto
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
const fetch = require('node-fetch');
|
||||
const response = await fetch(url);
|
||||
const buffer = await response.buffer();
|
||||
return loadImage(buffer);
|
||||
}
|
||||
|
||||
// Assume path locale
|
||||
return loadImage(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge layer con override
|
||||
*/
|
||||
_mergeLayerOverride(layer, override) {
|
||||
if (!override || Object.keys(override).length === 0) {
|
||||
return layer;
|
||||
}
|
||||
|
||||
return {
|
||||
...layer,
|
||||
position: override.position ? { ...layer.position, ...override.position } : layer.position,
|
||||
visible: override.visible !== undefined ? override.visible : layer.visible,
|
||||
style: override.style ? { ...layer.style, ...override.style } : layer.style
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick render (semplificato per quick-generate)
|
||||
*/
|
||||
async quickRender(options) {
|
||||
const {
|
||||
backgroundUrl,
|
||||
content,
|
||||
outputPath,
|
||||
width = 1080,
|
||||
height = 1920
|
||||
} = options;
|
||||
|
||||
const canvas = createCanvas(width, height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Background
|
||||
if (backgroundUrl) {
|
||||
try {
|
||||
const img = await this._loadImageFromUrl(backgroundUrl);
|
||||
const imgRatio = img.width / img.height;
|
||||
const canvasRatio = width / height;
|
||||
|
||||
let dw, dh, dx, dy;
|
||||
if (imgRatio > canvasRatio) {
|
||||
dh = height;
|
||||
dw = height * imgRatio;
|
||||
dx = (width - dw) / 2;
|
||||
dy = 0;
|
||||
} else {
|
||||
dw = width;
|
||||
dh = width / imgRatio;
|
||||
dx = 0;
|
||||
dy = (height - dh) / 2;
|
||||
}
|
||||
|
||||
ctx.drawImage(img, dx, dy, dw, dh);
|
||||
} catch (e) {
|
||||
ctx.fillStyle = '#1a1a2e';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
// Overlay gradient
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, height);
|
||||
gradient.addColorStop(0, 'rgba(0,0,0,0)');
|
||||
gradient.addColorStop(0.4, 'rgba(0,0,0,0.2)');
|
||||
gradient.addColorStop(0.7, 'rgba(0,0,0,0.6)');
|
||||
gradient.addColorStop(1, 'rgba(0,0,0,0.85)');
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
// Title
|
||||
if (content.title) {
|
||||
ctx.save();
|
||||
ctx.font = 'bold 72px "Montserrat", sans-serif';
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.shadowColor = 'rgba(0,0,0,0.8)';
|
||||
ctx.shadowBlur = 15;
|
||||
ctx.shadowOffsetY = 4;
|
||||
ctx.fillText(content.title.toUpperCase(), width / 2, height * 0.52);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Subtitle
|
||||
if (content.subtitle) {
|
||||
ctx.save();
|
||||
ctx.font = '400 32px "Open Sans", sans-serif';
|
||||
ctx.fillStyle = '#f0f0f0';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.shadowColor = 'rgba(0,0,0,0.6)';
|
||||
ctx.shadowBlur = 8;
|
||||
ctx.fillText(content.subtitle, width / 2, height * 0.60);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Date
|
||||
if (content.eventDate) {
|
||||
ctx.save();
|
||||
ctx.font = '400 48px "Bebas Neue", sans-serif';
|
||||
ctx.fillStyle = '#ffd700';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.shadowColor = 'rgba(0,0,0,0.8)';
|
||||
ctx.shadowBlur = 10;
|
||||
const dateText = content.eventTime
|
||||
? `${content.eventDate} • ORE ${content.eventTime}`
|
||||
: content.eventDate;
|
||||
ctx.fillText(dateText.toUpperCase(), width / 2, height * 0.70);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Location
|
||||
if (content.location) {
|
||||
ctx.save();
|
||||
ctx.font = '600 28px "Open Sans", sans-serif';
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(`📍 ${content.location}`, width / 2, height * 0.78);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Contacts
|
||||
if (content.contacts) {
|
||||
ctx.save();
|
||||
ctx.font = '400 22px "Open Sans", sans-serif';
|
||||
ctx.fillStyle = '#cccccc';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(content.contacts, width / 2, height * 0.86);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Salva
|
||||
const buffer = canvas.toBuffer('image/png');
|
||||
await fs.writeFile(outputPath, buffer);
|
||||
|
||||
return {
|
||||
path: outputPath,
|
||||
size: buffer.length,
|
||||
dimensions: { width, height }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new PosterRenderer();
|
||||
@@ -1048,6 +1048,7 @@ const MyTelegramBot = {
|
||||
token_circuito_da_ammettere: token,
|
||||
nomeTerritorio: mycircuit.name,
|
||||
myusername: userDest,
|
||||
circuitId: mycircuit._id,
|
||||
};
|
||||
// if (usersmanagers) {
|
||||
// for (const recadminCirc of usersmanagers) {
|
||||
|
||||
2353
src/templates/template-seeds.js
Normal file
2353
src/templates/template-seeds.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1198,6 +1198,7 @@ module.exports = {
|
||||
let paramsObj = {
|
||||
usernameDest,
|
||||
circuitnameDest: circuitname,
|
||||
circuitId: myreccircuit ? myreccircuit._id : ''
|
||||
path,
|
||||
username_action: username_action,
|
||||
singleadmin_username: usernameDest,
|
||||
@@ -6433,4 +6434,5 @@ module.exports = {
|
||||
// Usa padding di 3 cifre per minor e patch (supporta fino a 999)
|
||||
return major * 1000000 + minor * 1000 + patch;
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
@@ -450,6 +450,7 @@ module.exports = {
|
||||
usernameInvitante: paramsObj.extrarec?.username_admin_abilitante,
|
||||
nomeTerritorio: paramsObj.circuitnameDest,
|
||||
link_group: paramsObj.extrarec?.link_group,
|
||||
circuitId: paramsObj.circuitId,
|
||||
};
|
||||
await sendemail.sendEmail_Utente_Abilitato_Circuito_FidoConcesso(usertosend.lang, usertosend.email, usertosend, params.idapp, dati);
|
||||
}
|
||||
|
||||
@@ -1283,7 +1283,6 @@ module.exports = {
|
||||
DASHBOARD: 140,
|
||||
DASHGROUP: 145,
|
||||
MOVEMENTS: 148,
|
||||
CSENDRISTO: 150,
|
||||
STATUSREG: 160,
|
||||
CHECKIFISLOGGED: 170,
|
||||
INFO_VERSION: 180,
|
||||
|
||||
BIN
uploads/ai-generated/ai_1765716970209_55yyoiuf2.jpg
Normal file
BIN
uploads/ai-generated/ai_1765716970209_55yyoiuf2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
Reference in New Issue
Block a user