5 Commits

Author SHA1 Message Date
Surya Paolo
afeedf27a5 - Caricamento Video 2025-12-19 22:59:20 +01:00
Surya Paolo
80c929436c Versione 1.2.87 2025-12-18 19:45:52 +01:00
Surya Paolo
9a0cdec7bd - altro aggiornamento restying
- Invio RIS aggiornato
- Eventi
- Home Page restyling
2025-12-18 17:00:43 +01:00
Surya Paolo
3d87c336de - aggiornamento di tante cose...
- generazione Volantini
- pagina RIS
2025-12-17 10:07:51 +01:00
Surya Paolo
037ff6f7f9 - verifica email se non è stata verificata (componente)
- altri aggiornamenti grafica PAGERIS.
- OLLAMA AI
2025-12-12 00:44:12 +01:00
62 changed files with 9432 additions and 529 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -39,3 +39,9 @@ AUTH_NEW_SITES=123123123
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
GROK_API="xai-PcNM5obgPaETtmnfDWPZk235D75ZgxENU2QmeqPfMQCHh9dwCDVeRRe0oVVA2YOpiUDh1uJieZsMasja"
REPLICATE_API_TOKEN="r8_AVhM6igwvoOnUA65cHVZdhEDfTqBVk94WTB0u"
FAL_KEY="7d251c88-21b5-4b55-8b3e-4bafd910f99f:b81c0a36a25b052f26eb8ac226c7efff"
HF_TOKEN="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"

View File

@@ -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

View File

@@ -1 +1 @@
=`Richiesta ingresso di ${usernameMembro} - ${nomeMembro} ${cognomeMembro} su ${nomeTerritorio} in ${nomeapp}`
=`Abilitazione avvenuta su ${nomeTerritorio} in ${nomeapp} - (${usernameMembro})`

View File

@@ -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

View File

@@ -0,0 +1,404 @@
doctype html
html(lang="it")
head
meta(charset="UTF-8")
meta(name="viewport" content="width=device-width, initial-scale=1.0")
style(type="text/css").
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: #f5f5f5;
padding: 20px;
line-height: 1.6;
}
.header-logo {
width: 80px;
height: auto;
margin-bottom: 16px;
display: block;
margin-left: auto;
margin-right: auto;
}
.email-container {
max-width: 600px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.email-header {
background: linear-gradient(135deg, #1976D2 0%, #1565C0 100%);
color: white;
padding: 40px 24px;
text-align: center;
}
.email-header h1 {
margin: 0 0 8px 0;
font-size: 26px;
font-weight: 600;
}
.email-header p {
margin: 8px 0 0 0;
font-size: 16px;
opacity: 0.95;
line-height: 1.5;
}
.verification-icon {
font-size: 56px;
margin-bottom: 16px;
}
.email-body {
padding: 24px 20px;
}
.intro-text {
font-size: 16px;
color: #333;
margin-bottom: 20px;
text-align: center;
line-height: 1.7;
padding: 0 10px;
}
.intro-text strong {
color: #1976D2;
}
.info-message {
background: linear-gradient(135deg, #E3F2FD 0%, #BBDEFB 100%);
border-left: 4px solid #1976D2;
border-radius: 8px;
padding: 20px;
margin-bottom: 24px;
text-align: center;
}
.info-message h2 {
color: #1565C0;
font-size: 18px;
margin-bottom: 12px;
font-weight: 600;
}
.info-message p {
color: #1976D2;
font-size: 15px;
line-height: 1.6;
margin: 8px 0;
}
.warning-box {
background: #FFF8E1;
border-left: 4px solid #FFA000;
border-radius: 8px;
padding: 16px;
margin-bottom: 24px;
}
.warning-box p {
color: #E65100;
font-size: 14px;
margin: 0;
line-height: 1.6;
}
.warning-box strong {
color: #BF360C;
}
.email-info-box {
background: #f8f9fa;
border-left: 4px solid #1976D2;
border-radius: 8px;
padding: 16px;
margin-bottom: 24px;
text-align: center;
}
.email-info-title {
font-size: 14px;
font-weight: 600;
color: #555;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.email-address {
font-size: 18px;
font-weight: 600;
color: #1976D2;
word-break: break-word;
}
.cta-section {
margin: 24px 0;
padding: 24px 0;
text-align: center;
}
.cta-title {
font-size: 19px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 8px;
}
.cta-subtitle {
font-size: 14px;
color: #666;
margin-bottom: 20px;
line-height: 1.5;
}
.cta-button {
display: inline-block;
padding: 18px 40px;
font-size: 16px;
font-weight: 600;
color: white;
background: linear-gradient(135deg, #1976D2 0%, #1565C0 100%);
border-radius: 50px;
text-decoration: none;
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.35);
transition: transform 0.2s, box-shadow 0.2s;
}
.cta-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(25, 118, 210, 0.45);
}
.button-icon {
font-size: 20px;
margin-right: 8px;
vertical-align: middle;
}
.link-fallback {
margin-top: 20px;
padding: 16px;
background: #f5f5f5;
border-radius: 8px;
text-align: center;
}
.link-fallback p {
font-size: 13px;
color: #666;
margin-bottom: 8px;
}
.link-fallback a {
font-size: 12px;
color: #1976D2;
word-break: break-all;
}
.expiry-notice {
background: linear-gradient(135deg, #FFEBEE 0%, #FFCDD2 100%);
border-radius: 8px;
padding: 16px;
margin: 24px 0;
text-align: center;
border: 1px solid #EF9A9A;
}
.expiry-notice p {
margin: 0;
color: #C62828;
font-size: 14px;
line-height: 1.6;
}
.expiry-notice strong {
color: #B71C1C;
}
.help-section {
background: #FAFAFA;
border-radius: 8px;
padding: 20px;
margin: 24px 0;
text-align: center;
}
.help-section h3 {
font-size: 16px;
color: #333;
margin-bottom: 12px;
font-weight: 600;
}
.help-section p {
font-size: 14px;
color: #666;
margin: 8px 0;
line-height: 1.6;
}
.help-section a {
color: #1976D2;
text-decoration: none;
}
.help-section a:hover {
text-decoration: underline;
}
.security-note {
background: #E8F5E9;
border-left: 4px solid #4CAF50;
border-radius: 8px;
padding: 14px;
margin: 20px 0;
}
.security-note p {
font-size: 13px;
color: #2E7D32;
margin: 0;
line-height: 1.5;
}
.email-footer {
padding: 20px 16px;
text-align: center;
background: #f8f9fa;
color: #777;
font-size: 13px;
}
.email-footer p {
margin: 6px 0;
}
.divider {
height: 1px;
background: linear-gradient(to right, transparent, #e0e0e0, transparent);
margin: 24px 0;
}
@media only screen and (max-width: 600px) {
body {
padding: 8px;
}
.email-header {
padding: 24px 16px;
}
.email-header h1 {
font-size: 22px;
}
.email-header p {
font-size: 15px;
}
.email-body {
padding: 20px 14px;
}
.info-message {
padding: 16px;
}
.email-address {
font-size: 16px;
}
.cta-button {
padding: 16px 32px;
font-size: 15px;
display: block;
width: 100%;
}
.link-fallback a {
font-size: 11px;
}
}
body
.email-container
.email-header
- var baseimg = baseurl + '/';
h1 ✉️ Verifica il tuo indirizzo email
p Conferma la tua email per continuare a usare #{nomeapp}
.email-body
.intro-text
| Ciao
strong #{name || username}
| ! 👋
br
| Abbiamo ricevuto una richiesta per verificare nuovamente il tuo indirizzo email.
.info-message
h2 📧 Perché devo verificare di nuovo?
p Potrebbe essere perché hai cambiato email, per motivi di sicurezza, o perché la verifica precedente è scaduta.
p Questa procedura ci aiuta a mantenere sicuro il tuo account.
if emailto
.email-info-box
.email-info-title Email da verificare
.email-address #{emailto}
.cta-section
.cta-title Conferma il tuo indirizzo email
.cta-subtitle Clicca il bottone qui sotto per completare la verifica
if verifyLink
a.cta-button(href=verifyLink target="_blank")
span.button-icon ✓
| Verifica Email
.link-fallback
p Se il bottone non funziona, copia e incolla questo link nel browser:
a(href=verifyLink) #{verifyLink}
.expiry-notice
p ⏰ Questo link scadrà tra
strong 24 ore
| .
p Se non completi la verifica in tempo, dovrai richiedere un nuovo link.
.warning-box
p ⚠️
strong Non hai richiesto questa verifica?
| Ignora questa email. Il tuo account resterà al sicuro.
.security-note
p 🔒 Per la tua sicurezza: non condividere mai questo link con nessuno. Il team di #{nomeapp} non ti chiederà mai la password via email.
.help-section
h3 Hai bisogno di aiuto?
p Non riesci a verificare la tua email?
if supportEmail
p Contattaci a
a(href="mailto:" + supportEmail) #{supportEmail}
if strlinksito
p Oppure visita la nostra
a(href=strlinksito + '/supporto' target="_blank") pagina di supporto
.email-footer
.divider
p Hai ricevuto questa email perché è stata richiesta una verifica per il tuo account su #{nomeapp}
p(style="margin-top: 8px; font-size: 12px; color: #999;")
| Se non hai fatto tu questa richiesta, puoi ignorare questa email.
p(style="margin-top: 12px; font-size: 12px;")
| © #{new Date().getFullYear()} #{nomeapp}

View File

@@ -0,0 +1 @@
Verifica la tua Email - ${nomeapp}`

View File

@@ -519,4 +519,89 @@ Gio 04/12 ORE 18:55: [<b>Circuito RIS Bologna</b>]: Inviate Monete da SurTest a
Saldi:
SurTest: 0.00 RIS]
ElenaEspx: 38.05 RIS]
ElenaEspx: 38.05 RIS]
Mar 09/12 ORE 21:36: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 11 RIS [causale: ssss]
Saldi:
surya1977: 63.00 RIS]
amandadi: 12.00 RIS]
Mer 10/12 ORE 16:18: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 12 RIS [causale: Ciaoo]
Saldi:
surya1977: 51.00 RIS]
amandadi: 24.00 RIS]
Mer 10/12 ORE 17:02: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 52 RIS [causale: Ancora test]
Saldi:
surya1977: -1.00 RIS]
amandadi: 76.00 RIS]
Gio 18/12 ORE 15:41: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 22 RIS [causale: Prova]
Saldi:
surya1977: -23.00 RIS]
amandadi: 98.00 RIS]
Gio 18/12 ORE 15:59: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 2 RIS [causale: aaaa]
Saldi:
surya1977: -3.00 RIS]
amandadi: 78.00 RIS]
Gio 18/12 ORE 16:00: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 2 RIS [causale: aaaa]
Saldi:
surya1977: -5.00 RIS]
amandadi: 80.00 RIS]
Gio 18/12 ORE 16:07: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 1 RIS [causale: aaa]
Saldi:
surya1977: -6.00 RIS]
amandadi: 81.00 RIS]
Gio 18/12 ORE 16:07: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 2 RIS [causale: asdasdas]
Saldi:
surya1977: -8.00 RIS]
amandadi: 83.00 RIS]
Gio 18/12 ORE 16:11: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 3 RIS [causale: Provaa]
Saldi:
surya1977: -11.00 RIS]
amandadi: 86.00 RIS]
Gio 18/12 ORE 16:35: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 2 RIS [causale: aaaa]
Saldi:
surya1977: -13.00 RIS]
amandadi: 88.00 RIS]
Gio 18/12 ORE 16:49: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 5 RIS [causale: Prova da 3. Eccolo 2!]
Saldi:
surya1977: -18.00 RIS]
amandadi: 96.00 RIS]
Gio 18/12 ORE 16:50: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 4 RIS [causale: dasdasdasd]
Saldi:
surya1977: -22.00 RIS]
amandadi: 100.00 RIS]
Gio 18/12 ORE 16:50: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 3 RIS [causale: -25]
Saldi:
surya1977: -25.00 RIS]
amandadi: 103.00 RIS]
Gio 18/12 ORE 16:52: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 1 RIS [causale: asdasdsadas]
Saldi:
surya1977: -27.50 RIS]
amandadi: 105.50 RIS]
Gio 18/12 ORE 18:30: [<b>Circuito RIS Bologna</b>]: Inviate Monete da surya1977 a ElenaEspx 1 RIS [causale: prova 1]
Saldi:
surya1977: 34.90 RIS]
ElenaEspx: 39.05 RIS]
Gio 18/12 ORE 18:42: [<b>Circuito RIS Pordenone</b>]: Inviate Monete da surya1977 a GruppoYurta 3 RIS [causale: ECCOLO]
Saldi:
surya1977: -3.00 RIS]
GruppoYurta: -1.00 RIS]
Gio 18/12 ORE 18:53: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 50 RIS [causale: asdasdasda]
Saldi:
surya1977: -77.50 RIS]
amandadi: 155.50 RIS]

View File

@@ -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",
@@ -33,10 +35,12 @@
"email-templates": "^12.0.2",
"entities": "^7.0.0",
"express": "^4.21.2",
"express-rate-limit": "^7.1.5",
"fast-csv": "^5.0.5",
"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",
@@ -58,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",
@@ -66,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",

View File

@@ -25,6 +25,7 @@ var file = `.env.${node_env}`;
// GLOBALI (Uguali per TUTTI)
process.env.LINKVERIF_REG = '/vreg';
process.env.CHECKREVERIF_EMAIL = '/reverif_email';
process.env.LINK_REQUEST_NEWPASSWORD = '/requestnewpwd';
process.env.ADD_NEW_SITE = '/addNewSite';
process.env.LINK_UPDATE_PASSWORD = '/updatepassword';

View File

@@ -0,0 +1,588 @@
const path = require('path');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
class VideoController {
constructor(baseUploadPath = 'uploads/videos') {
this.basePath = path.resolve(baseUploadPath);
this._ensureDirectory(this.basePath);
}
// ============ PRIVATE METHODS ============
_ensureDirectory(dir) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
_isVideoFile(filename) {
return /\.(mp4|webm|ogg|mov|avi|mkv)$/i.test(filename);
}
_getFileInfo(filePath, relativePath = '') {
const stat = fs.statSync(filePath);
const filename = path.basename(filePath);
return {
id: uuidv4(),
filename,
folder: relativePath,
path: `/videos/${relativePath ? relativePath + '/' : ''}${filename}`,
size: stat.size,
createdAt: stat.birthtime.toISOString(),
modifiedAt: stat.mtime.toISOString(),
};
}
_scanFolders(dir, relativePath = '') {
const folders = [];
if (!fs.existsSync(dir)) return folders;
const items = fs.readdirSync(dir);
items.forEach((item) => {
const fullPath = path.join(dir, item);
const relPath = relativePath ? `${relativePath}/${item}` : item;
if (fs.statSync(fullPath).isDirectory()) {
folders.push({
name: item,
path: relPath,
level: relPath.split('/').length,
});
// Ricorsione per sottocartelle
folders.push(...this._scanFolders(fullPath, relPath));
}
});
return folders;
}
// ============ FOLDER METHODS ============
/**
* Ottiene tutte le cartelle
*/
getFolders = async (req, res) => {
try {
const folders = this._scanFolders(this.basePath);
res.json({
success: true,
data: { folders },
message: 'Cartelle recuperate con successo',
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message,
});
}
};
/**
* Crea una nuova cartella
*/
createFolder = async (req, res) => {
try {
const { folderName, parentPath = '' } = req.body;
if (!folderName || !folderName.trim()) {
return res.status(400).json({
success: false,
error: 'Nome cartella richiesto',
});
}
// Sanitizza il nome cartella
const sanitizedName = folderName.replace(/[<>:"/\\|?*]/g, '_').trim();
const basePath = parentPath ? path.join(this.basePath, parentPath) : this.basePath;
const newFolderPath = path.join(basePath, sanitizedName);
if (fs.existsSync(newFolderPath)) {
return res.status(409).json({
success: false,
error: 'La cartella esiste già',
});
}
fs.mkdirSync(newFolderPath, { recursive: true });
const folderData = {
name: sanitizedName,
path: parentPath ? `${parentPath}/${sanitizedName}` : sanitizedName,
createdAt: new Date().toISOString(),
};
res.status(201).json({
success: true,
data: { folder: folderData },
message: 'Cartella creata con successo',
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message,
});
}
};
/**
* Rinomina una cartella
*/
renameFolder = async (req, res) => {
try {
const { folderPath } = req.params;
const { newName } = req.body;
if (!newName || !newName.trim()) {
return res.status(400).json({
success: false,
error: 'Nuovo nome richiesto',
});
}
const sanitizedName = newName.replace(/[<>:"/\\|?*]/g, '_').trim();
const oldPath = path.join(this.basePath, folderPath);
const parentDir = path.dirname(oldPath);
const newPath = path.join(parentDir, sanitizedName);
if (!fs.existsSync(oldPath)) {
return res.status(404).json({
success: false,
error: 'Cartella non trovata',
});
}
if (fs.existsSync(newPath)) {
return res.status(409).json({
success: false,
error: 'Una cartella con questo nome esiste già',
});
}
fs.renameSync(oldPath, newPath);
res.json({
success: true,
message: 'Cartella rinominata con successo',
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message,
});
}
};
/**
* Elimina una cartella
*/
deleteFolder = async (req, res) => {
try {
const { folderPath } = req.params;
const fullPath = path.join(this.basePath, folderPath);
if (!fs.existsSync(fullPath)) {
return res.status(404).json({
success: false,
error: 'Cartella non trovata',
});
}
// Verifica che sia una directory
if (!fs.statSync(fullPath).isDirectory()) {
return res.status(400).json({
success: false,
error: 'Il percorso non è una cartella',
});
}
fs.rmSync(fullPath, { recursive: true, force: true });
res.json({
success: true,
message: 'Cartella eliminata con successo',
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message,
});
}
};
// ============ VIDEO METHODS ============
/**
* Ottiene i video di una cartella
*/
getVideos = async (req, res) => {
try {
const folder = req.query.folder || '';
const targetPath = folder ? path.join(this.basePath, folder) : this.basePath;
if (!fs.existsSync(targetPath)) {
return res.json({
success: true,
data: {
videos: [],
folders: [],
currentPath: folder,
},
});
}
const items = fs.readdirSync(targetPath);
const videos = [];
const subfolders = [];
items.forEach((item) => {
const itemPath = path.join(targetPath, item);
const stat = fs.statSync(itemPath);
if (stat.isDirectory()) {
subfolders.push({
name: item,
path: folder ? `${folder}/${item}` : item,
});
} else if (stat.isFile() && this._isVideoFile(item)) {
videos.push(this._getFileInfo(itemPath, folder));
}
});
// Ordina per data di creazione (più recenti prima)
videos.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
res.json({
success: true,
data: {
videos,
folders: subfolders,
currentPath: folder,
totalVideos: videos.length,
},
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message,
});
}
};
/**
* Upload singolo video
*/
uploadVideo = async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({
success: false,
error: 'Nessun file caricato',
});
}
// ✅ Legge da query parameter
const folder = req.query.folder || 'default';
const videoInfo = {
id: uuidv4(),
originalName: req.file.originalname,
filename: req.file.filename,
folder: folder,
path: `/videos/${folder}/${req.file.filename}`,
size: req.file.size,
mimetype: req.file.mimetype,
uploadedAt: new Date().toISOString(),
};
res.status(201).json({
success: true,
data: { video: videoInfo },
message: 'Video caricato con successo',
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message,
});
}
};
/**
* Upload multiplo video
*/
uploadVideos = async (req, res) => {
try {
if (!req.files || req.files.length === 0) {
return res.status(400).json({
success: false,
error: 'Nessun file caricato',
});
}
// ✅ Legge da query parameter
const folder = req.query.folder || 'default';
const videos = req.files.map((file) => ({
id: uuidv4(),
originalName: file.originalname,
filename: file.filename,
folder: folder,
path: `/videos/${folder}/${file.filename}`,
size: file.size,
mimetype: file.mimetype,
uploadedAt: new Date().toISOString(),
}));
res.status(201).json({
success: true,
data: { videos },
message: `${videos.length} video caricati con successo`,
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message,
});
}
};
/**
* Ottiene info di un singolo video
*/
getVideo = async (req, res) => {
try {
const { folder, filename } = req.params;
const videoPath = path.join(this.basePath, folder, filename);
if (!fs.existsSync(videoPath)) {
return res.status(404).json({
success: false,
error: 'Video non trovato',
});
}
const videoInfo = this._getFileInfo(videoPath, folder);
res.json({
success: true,
data: { video: videoInfo },
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message,
});
}
};
/**
* Rinomina un video
*/
renameVideo = async (req, res) => {
try {
const { folder, filename } = req.params;
const { newFilename } = req.body;
if (!newFilename || !newFilename.trim()) {
return res.status(400).json({
success: false,
error: 'Nuovo nome file richiesto',
});
}
const oldPath = path.join(this.basePath, folder, filename);
const newPath = path.join(this.basePath, folder, newFilename);
if (!fs.existsSync(oldPath)) {
return res.status(404).json({
success: false,
error: 'Video non trovato',
});
}
if (fs.existsSync(newPath)) {
return res.status(409).json({
success: false,
error: 'Un file con questo nome esiste già',
});
}
fs.renameSync(oldPath, newPath);
res.json({
success: true,
message: 'Video rinominato con successo',
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message,
});
}
};
/**
* Sposta un video in un'altra cartella
*/
moveVideo = async (req, res) => {
try {
const { folder, filename } = req.params;
const { destinationFolder } = req.body;
const sourcePath = path.join(this.basePath, folder, filename);
const destDir = path.join(this.basePath, destinationFolder);
const destPath = path.join(destDir, filename);
if (!fs.existsSync(sourcePath)) {
return res.status(404).json({
success: false,
error: 'Video non trovato',
});
}
this._ensureDirectory(destDir);
if (fs.existsSync(destPath)) {
return res.status(409).json({
success: false,
error: 'Un file con questo nome esiste già nella destinazione',
});
}
fs.renameSync(sourcePath, destPath);
res.json({
success: true,
message: 'Video spostato con successo',
data: {
newPath: `/videos/${destinationFolder}/${filename}`,
},
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message,
});
}
};
/**
* Elimina un video
*/
deleteVideo = async (req, res) => {
try {
const { folder, filename } = req.params;
const videoPath = path.join(this.basePath, folder, filename);
if (!fs.existsSync(videoPath)) {
return res.status(404).json({
success: false,
error: 'Video non trovato',
});
}
fs.unlinkSync(videoPath);
res.json({
success: true,
message: 'Video eliminato con successo',
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message,
});
}
};
/**
* Stream video (per player)
*/
streamVideo = async (req, res) => {
try {
const { folder, filename } = req.params;
const videoPath = path.join(this.basePath, folder, filename);
if (!fs.existsSync(videoPath)) {
return res.status(404).json({
success: false,
error: 'Video non trovato',
});
}
const stat = fs.statSync(videoPath);
const fileSize = stat.size;
const range = req.headers.range;
if (range) {
const parts = range.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
const chunkSize = end - start + 1;
const file = fs.createReadStream(videoPath, { start, end });
const headers = {
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'Accept-Ranges': 'bytes',
'Content-Length': chunkSize,
'Content-Type': 'video/mp4',
};
res.writeHead(206, headers);
file.pipe(res);
} else {
const headers = {
'Content-Length': fileSize,
'Content-Type': 'video/mp4',
};
res.writeHead(200, headers);
fs.createReadStream(videoPath).pipe(res);
}
} catch (error) {
res.status(500).json({
success: false,
error: error.message,
});
}
};
// ============ ERROR HANDLER MIDDLEWARE ============
static errorHandler = (error, req, res, next) => {
if (error.code === 'LIMIT_FILE_SIZE') {
return res.status(413).json({
success: false,
error: 'File troppo grande. Dimensione massima: 500MB',
});
}
if (error.code === 'LIMIT_FILE_COUNT') {
return res.status(400).json({
success: false,
error: 'Troppi file. Massimo 10 file per upload',
});
}
if (error.message.includes('Tipo file non supportato')) {
return res.status(415).json({
success: false,
error: error.message,
});
}
res.status(500).json({
success: false,
error: error.message || 'Errore interno del server',
});
};
}
module.exports = VideoController;

View File

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

View File

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

View File

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

45
src/data/asset.json Normal file
View 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
View 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
View 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
View 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;

View File

@@ -0,0 +1,81 @@
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
class UploadMiddleware {
constructor(baseUploadPath = 'uploads/videos') {
this.baseUploadPath = path.resolve(baseUploadPath);
this._ensureDirectory(this.baseUploadPath);
}
_ensureDirectory(dir) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
_createStorage() {
return multer.diskStorage({
destination: (req, file, cb) => {
// ✅ Legge SOLO da req.query (affidabile con multer)
const folder = req.query.folder || 'default';
console.log('📁 Upload folder:', folder); // Debug
const uploadPath = path.join(this.baseUploadPath, folder);
this._ensureDirectory(uploadPath);
cb(null, uploadPath);
},
filename: (req, file, cb) => {
const ext = path.extname(file.originalname);
const uniqueName = `${uuidv4()}-${Date.now()}${ext}`;
cb(null, uniqueName);
}
});
}
_fileFilter(req, file, cb) {
const allowedMimes = [
'video/mp4',
'video/webm',
'video/ogg',
'video/quicktime',
'video/x-msvideo',
'video/x-matroska'
];
if (allowedMimes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error(`Tipo file non supportato: ${file.mimetype}`), false);
}
}
getUploader(options = {}) {
const config = {
storage: this._createStorage(),
fileFilter: this._fileFilter.bind(this),
limits: {
fileSize: options.maxSize || 500 * 1024 * 1024,
files: options.maxFiles || 10
}
};
return multer(config);
}
single(fieldName = 'video') {
return this.getUploader().single(fieldName);
}
multiple(fieldName = 'videos', maxCount = 10) {
return this.getUploader({ maxFiles: maxCount }).array(fieldName, maxCount);
}
getBasePath() {
return this.baseUploadPath;
}
}
module.exports = UploadMiddleware;

137
src/models/Asset.js Normal file
View 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
View 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
View 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);

View File

@@ -32,6 +32,12 @@ const AccountSchema = new Schema({
numtransactions: {
type: Number,
},
sent: {
type: Number,
},
received: {
type: Number,
},
username: {
type: String,
},
@@ -240,8 +246,22 @@ AccountSchema.statics.addtoSaldo = async function (myaccount, amount, mitt) {
myaccount.date_updated = new Date();
myaccountupdate.saldo = myaccount.saldo;
myaccountupdate.sent = myaccount.sent;
myaccountupdate.received = myaccount.received;
myaccountupdate.totTransato = myaccount.totTransato;
myaccountupdate.numtransactions = myaccount.numtransactions;
if (amount > 0) {
if (myaccountupdate.received === undefined) {
myaccountupdate.received = 0;
}
myaccountupdate.received += 1;
} else {
if (myaccountupdate.sent === undefined) {
myaccountupdate.sent = 0;
}
myaccountupdate.sent += 1;
}
myaccountupdate.date_updated = myaccount.date_updated;
const ris = await Account.updateOne(
@@ -324,6 +344,8 @@ AccountSchema.statics.getAccountByUsernameAndCircuitId = async function (
username_admin_abilitante: '',
qta_maxConcessa: 0,
totTransato: 0,
sent: 0,
received: 0,
numtransactions: 0,
totTransato_pend: 0,
});

View File

@@ -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);
@@ -832,6 +838,7 @@ CircuitSchema.statics.sendCoins = async function (onlycheck, idapp, usernameOrig
paramstoupdate = {
totTransato: circuittable.totTransato,
totCircolante: circuittable.totCircolante,
numTransazioni: circuittable.numTransazioni,
};
await Circuit.updateOne({ _id: circuittable }, { $set: paramstoupdate });
@@ -901,7 +908,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 +1061,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 +1204,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 +1403,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 +1461,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 +1560,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 +1640,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 +1666,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 +1711,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 +1727,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 +1915,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 +1928,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') {

View File

@@ -106,6 +106,8 @@ MovementSchema.statics.addMov = async function (
idOrdersCart
) {
try {
const { Circuit } = require('./circuit');
// Only positive values
amount = Math.abs(amount);
@@ -342,8 +344,12 @@ MovementSchema.statics.getQueryMovsByCircuitId = async function (idapp, username
'circuitfrom.symbol': 1,
'circuitto.symbol': 1,
'userfrom.verified_by_aportador': 1,
'userfrom.name': 1,
'userfrom.surname': 1,
'userfrom.username': 1,
'userfrom.profile.img': 1,
'userto.name': 1,
'userto.surname': 1,
'userto.username': 1,
'userto.profile.img': 1,
'userto.verified_by_aportador': 1,
@@ -579,7 +585,11 @@ MovementSchema.statics.getQueryAllUsersMovsByCircuitId = async function (idapp,
'circuitfrom.symbol': 1,
'circuitto.symbol': 1,
'userfrom.username': 1,
'userfrom.name': 1,
'userfrom.surname': 1,
'userfrom.profile.img': 1,
'userto.name': 1,
'userto.surname': 1,
'userto.username': 1,
'userto.profile.img': 1,
'groupfrom.groupname': 1,
@@ -1013,7 +1023,11 @@ MovementSchema.statics.getLastN_Transactions = async function (
'circuitto.name': 1,
'userfrom.verified_by_aportador': 1,
'userfrom.username': 1,
'userfrom.name': 1,
'userfrom.surname': 1,
'userfrom.profile.img': 1,
'userto.name': 1,
'userto.surname': 1,
'userto.username': 1,
'userto.profile.img': 1,
'userto.verified_by_aportador': 1,

View File

@@ -150,6 +150,9 @@ const MySingleElemSchema = {
parambool4: {
type: Boolean,
},
parambool5: {
type: Boolean,
},
number: {
type: Number,
},
@@ -236,6 +239,12 @@ const MySingleElemSchema = {
class4: {
type: String,
},
stiletit_str: {
type: String,
},
stiletit_icon: {
type: String,
},
styleadd: {
type: String,
},

View File

@@ -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 },

View File

@@ -56,6 +56,9 @@ const UserSchema = new mongoose.Schema(
message: '{VALUE} is not a valid email'
}*/
},
link_verif_email: {
type: String,
},
hash: {
type: String,
},
@@ -2614,6 +2617,12 @@ UserSchema.statics.removeBookmark = async function (idapp, username, id, tab) {
UserSchema.statics.addBookmark = async function (idapp, username, id, tab) {
return await User.updateOne({ idapp, username }, { $push: { 'profile.bookmark': { id, tab } } });
};
UserSchema.statics.setLinkToVerifiedEmail = async function (idapp, username, link_verif_email) {
return await User.updateOne({ idapp, username }, { $set: { link_verif_email } });
};
UserSchema.statics.findByLinkVerifEmail = async function (idapp, link_verif_email) {
return await User.findOne({ idapp, link_verif_email });
};
// Rimuovo il Partecipa
UserSchema.statics.removeAttend = async function (idapp, username, id, tab) {
return await User.updateOne({ idapp, username }, { $pull: { 'profile.attend': { id: { $in: [id] }, tab } } });
@@ -6989,28 +6998,24 @@ UserSchema.statics.getTokenByUsernameAndCircuitName = async function (idapp, use
return user?.profile?.mycircuits?.[0]?.token || null;
};
UserSchema.statics.softDelete = async function(id) {
UserSchema.statics.softDelete = async function (id) {
return this.findByIdAndUpdate(
id,
{
{
deleted: true,
deletedAt: new Date()
deletedAt: new Date(),
},
{ new: true }
);
};
UserSchema.statics.getUsersList = function(idapp) {
UserSchema.statics.getUsersList = function (idapp) {
return this.find({
idapp: idapp,
$or: [
{ deleted: { $exists: false } },
{ deleted: false }
]
$or: [{ deleted: { $exists: false } }, { deleted: false }],
}).lean();
};
const User = mongoose.model('User', UserSchema);
class Hero {

View File

@@ -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') {

View File

@@ -25,6 +25,7 @@ module.exports = {
});
}
}
} catch (e) {
console.log('error insertIntoDb', e);
}

913
src/router/api2_router.js Normal file
View File

@@ -0,0 +1,913 @@
/**
* API2 Router - Ollama Integration
*/
const express = require('express');
const router = express.Router();
// Configurazione Ollama
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://localhost:11434';
const DEFAULT_MODEL = process.env.OLLAMA_DEFAULT_MODEL || 'llama3.2';
// ============================================
// ROUTES PRINCIPALI
// ============================================
/**
* Health check Ollama
* GET /api2/health
*/
router.get('/health', async (req, res) => {
try {
const response = await fetch(`${OLLAMA_URL}/api/tags`);
if (response.ok) {
const data = await response.json();
res.json({
status: 'ok',
ollama: 'connected',
modelsCount: data.models?.length || 0
});
} else {
res.status(503).json({ status: 'error', ollama: 'disconnected' });
}
} catch (error) {
res.status(503).json({
status: 'error',
ollama: 'unreachable',
message: error.message
});
}
});
/**
* Lista modelli disponibili
* GET /api2/models
*/
router.get('/models', async (req, res) => {
try {
const response = await fetch(`${OLLAMA_URL}/api/tags`);
if (!response.ok) {
console.error('[api2/models] Error:', response.status);
throw new Error(`Ollama error: ${response.status}`);
}
const data = await response.json();
res.json({ models: data.models || [] });
} catch (error) {
console.error('[api2/models] Error:', error);
res.status(500).json({ error: error.message });
}
});
/**
* Info modello specifico
* POST /api2/models/info
*/
router.post('/models/info', async (req, res) => {
try {
const { model } = req.body;
if (!model) {
return res.status(400).json({ error: 'Model name richiesto' });
}
const response = await fetch(`${OLLAMA_URL}/api/show`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: model }),
});
if (!response.ok) {
throw new Error(`Ollama error: ${response.status}`);
}
const data = await response.json();
res.json(data);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// ============================================
// GENERAZIONE TESTO
// ============================================
/**
* Generazione testo (non-streaming)
* POST /api2/generate
*/
router.post('/generate', async (req, res) => {
try {
const {
model = DEFAULT_MODEL,
prompt,
temperature = 0.7,
maxTokens,
system,
topP,
topK,
} = req.body;
if (!prompt) {
return res.status(400).json({ error: 'Prompt richiesto' });
}
const payload = {
model,
prompt,
stream: false,
options: {
temperature,
...(maxTokens && { num_predict: maxTokens }),
...(topP && { top_p: topP }),
...(topK && { top_k: topK }),
},
};
if (system) payload.system = system;
const response = await fetch(`${OLLAMA_URL}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`Ollama error: ${response.status}`);
}
const data = await response.json();
res.json({
response: data.response,
model: data.model,
totalDuration: data.total_duration,
loadDuration: data.load_duration,
promptEvalCount: data.prompt_eval_count,
evalCount: data.eval_count,
});
} catch (error) {
console.error('[api2/generate] Error:', error);
res.status(500).json({ error: error.message });
}
});
/**
* Generazione testo con streaming (SSE)
* POST /api2/generate/stream
*/
router.post('/generate/stream', async (req, res) => {
try {
const {
model = DEFAULT_MODEL,
prompt,
temperature = 0.7,
system,
maxTokens,
} = req.body;
if (!prompt) {
return res.status(400).json({ error: 'Prompt richiesto' });
}
const payload = {
model,
prompt,
stream: true,
options: {
temperature,
...(maxTokens && { num_predict: maxTokens }),
},
};
if (system) payload.system = system;
const response = await fetch(`${OLLAMA_URL}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`Ollama error: ${response.status}`);
}
// Setup SSE
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
const reader = response.body.getReader();
const decoder = new TextDecoder();
const pump = async () => {
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
res.write('data: [DONE]\n\n');
res.end();
break;
}
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(line => line.trim());
for (const line of lines) {
try {
const json = JSON.parse(line);
res.write(`data: ${JSON.stringify(json)}\n\n`);
} catch (e) {
// Ignora linee non JSON
}
}
}
} catch (error) {
console.error('[api2/generate/stream] Stream error:', error);
res.end();
}
};
// Handle client disconnect
req.on('close', () => {
reader.cancel();
});
pump();
} catch (error) {
console.error('[api2/generate/stream] Error:', error);
res.status(500).json({ error: error.message });
}
});
// ============================================
// CHAT
// ============================================
/**
* Chat (non-streaming)
* POST /api2/chat
*/
router.post('/chat', async (req, res) => {
try {
const {
model = DEFAULT_MODEL,
messages,
temperature = 0.7,
system,
maxTokens,
} = req.body;
if (!messages || !Array.isArray(messages)) {
return res.status(400).json({ error: 'Messages array richiesto' });
}
const payload = {
model,
messages,
stream: false,
options: {
temperature,
...(maxTokens && { num_predict: maxTokens }),
},
};
if (system) payload.system = system;
// console.log('payload', payload);
const response = await fetch(`${OLLAMA_URL}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`Ollama error: ${response.status} - ${response.statusText}`);
}
const data = await response.json();
res.json({
message: data.message,
model: data.model,
totalDuration: data.total_duration,
});
} catch (error) {
console.error('[api2/chat] Error:', error);
res.status(500).json({ error: error.message });
}
});
/**
* Chat con streaming (SSE)
* POST /api2/chat/stream
*/
router.post('/chat/stream', async (req, res) => {
try {
const {
model = DEFAULT_MODEL,
messages,
temperature = 0.7,
system,
maxTokens,
} = req.body;
if (!messages || !Array.isArray(messages)) {
return res.status(400).json({ error: 'Messages array richiesto' });
}
const payload = {
model,
messages,
stream: true,
options: {
temperature,
...(maxTokens && { num_predict: maxTokens }),
},
};
if (system) payload.system = system;
const response = await fetch(`${OLLAMA_URL}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`Ollama error: ${response.status}`);
}
// Setup SSE
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
const reader = response.body.getReader();
const decoder = new TextDecoder();
const pump = async () => {
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
res.write('data: [DONE]\n\n');
res.end();
break;
}
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(line => line.trim());
for (const line of lines) {
try {
const json = JSON.parse(line);
res.write(`data: ${JSON.stringify(json)}\n\n`);
} catch (e) {
// Ignora linee non JSON
}
}
}
} catch (error) {
console.error('[api2/chat/stream] Stream error:', error);
res.end();
}
};
// Handle client disconnect
req.on('close', () => {
reader.cancel();
});
pump();
} catch (error) {
console.error('[api2/chat/stream] Error:', error);
res.status(500).json({ error: error.message });
}
});
// ============================================
// ENDPOINTS HELPER
// ============================================
/**
* Genera codice
* POST /api2/code
*/
router.post('/code', async (req, res) => {
try {
const {
prompt,
language = 'javascript',
model = DEFAULT_MODEL,
temperature = 0.3,
} = req.body;
if (!prompt) {
return res.status(400).json({ error: 'Prompt richiesto' });
}
const systemPrompt = `Sei un esperto programmatore. Rispondi SOLO con codice ${language} valido e funzionante, senza spiegazioni prima o dopo. Usa commenti nel codice se necessario per spiegare.`;
const response = await fetch(`${OLLAMA_URL}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
prompt: `Scrivi codice ${language} per: ${prompt}`,
system: systemPrompt,
stream: false,
options: { temperature },
}),
});
if (!response.ok) {
throw new Error(`Ollama error: ${response.status}`);
}
const data = await response.json();
res.json({
code: data.response,
language,
model: data.model,
});
} catch (error) {
console.error('[api2/code] Error:', error);
res.status(500).json({ error: error.message });
}
});
/**
* Traduci testo
* POST /api/translate
*/
router.post('/translate', async (req, res) => {
try {
const {
text,
targetLang = 'english',
sourceLang,
model = DEFAULT_MODEL,
} = req.body;
if (!text) {
return res.status(400).json({ error: 'Text richiesto' });
}
const sourceInfo = sourceLang ? ` da ${sourceLang}` : '';
const prompt = `Traduci il seguente testo${sourceInfo} in ${targetLang}. Rispondi SOLO con la traduzione, senza spiegazioni:\n\n${text}`;
const response = await fetch(`${OLLAMA_URL}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
prompt,
stream: false,
options: { temperature: 0.3 },
}),
});
if (!response.ok) {
throw new Error(`Ollama error: ${response.status}`);
}
const data = await response.json();
res.json({
translation: data.response.trim(),
targetLang,
sourceLang,
});
} catch (error) {
console.error('[api2/translate] Error:', error);
res.status(500).json({ error: error.message });
}
});
/**
* Riassumi testo
* POST /api/summarize
*/
router.post('/summarize', async (req, res) => {
try {
const {
text,
maxLength,
style = 'conciso', // conciso, dettagliato, bullet
model = DEFAULT_MODEL,
} = req.body;
if (!text) {
return res.status(400).json({ error: 'Text richiesto' });
}
let styleInstruction = '';
switch (style) {
case 'bullet':
styleInstruction = 'Usa un elenco puntato.';
break;
case 'dettagliato':
styleInstruction = 'Fornisci un riassunto dettagliato.';
break;
default:
styleInstruction = 'Sii conciso e chiaro.';
}
const lengthInstruction = maxLength ? ` Massimo ${maxLength} parole.` : '';
const response = await fetch(`${OLLAMA_URL}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
prompt: `Riassumi il seguente testo. ${styleInstruction}${lengthInstruction}\n\n${text}`,
stream: false,
options: { temperature: 0.5 },
}),
});
if (!response.ok) {
throw new Error(`Ollama error: ${response.status}`);
}
const data = await response.json();
res.json({
summary: data.response.trim(),
style,
});
} catch (error) {
console.error('[api2/summarize] Error:', error);
res.status(500).json({ error: error.message });
}
});
/**
* Analisi sentiment
* POST /api/sentiment
*/
router.post('/sentiment', async (req, res) => {
try {
const { text, model = DEFAULT_MODEL } = req.body;
if (!text) {
return res.status(400).json({ error: 'Text richiesto' });
}
const response = await fetch(`${OLLAMA_URL}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
prompt: `Analizza il sentiment del seguente testo e rispondi SOLO con un JSON valido nel formato:
{"sentiment": "positive" | "negative" | "neutral" | "mixed", "confidence": 0.0-1.0, "emotions": ["emotion1", "emotion2"], "explanation": "breve spiegazione"}
Testo da analizzare:
${text}`,
stream: false,
options: { temperature: 0.1 },
}),
});
if (!response.ok) {
throw new Error(`Ollama error: ${response.status}`);
}
const data = await response.json();
try {
const jsonMatch = data.response.match(/\{[\s\S]*\}/);
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0]);
res.json(parsed);
} else {
res.json({ sentiment: 'unknown', raw: data.response });
}
} catch (e) {
res.json({ sentiment: 'unknown', raw: data.response });
}
} catch (error) {
console.error('[api2/sentiment] Error:', error);
res.status(500).json({ error: error.message });
}
});
/**
* Estrai JSON strutturato da testo
* POST /api/extract
*/
router.post('/extract', async (req, res) => {
try {
const { text, schema, model = DEFAULT_MODEL } = req.body;
if (!text) {
return res.status(400).json({ error: 'Text richiesto' });
}
if (!schema) {
return res.status(400).json({ error: 'Schema richiesto' });
}
const response = await fetch(`${OLLAMA_URL}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
prompt: `Estrai le informazioni dal seguente testo e restituisci SOLO un JSON valido con questa struttura:
${JSON.stringify(schema, null, 2)}
Testo da analizzare:
${text}
Rispondi SOLO con il JSON, senza altro testo.`,
stream: false,
options: { temperature: 0.1 },
}),
});
if (!response.ok) {
throw new Error(`Ollama error: ${response.status}`);
}
const data = await response.json();
try {
const jsonMatch = data.response.match(/\{[\s\S]*\}/);
if (jsonMatch) {
res.json(JSON.parse(jsonMatch[0]));
} else {
res.status(422).json({ error: 'Could not parse JSON', raw: data.response });
}
} catch (e) {
res.status(422).json({ error: 'Invalid JSON response', raw: data.response });
}
} catch (error) {
console.error('[api2/extract] Error:', error);
res.status(500).json({ error: error.message });
}
});
/**
* Correggi grammatica
* POST /api/grammar
*/
router.post('/grammar', async (req, res) => {
try {
const {
text,
language = 'italiano',
model = DEFAULT_MODEL,
} = req.body;
if (!text) {
return res.status(400).json({ error: 'Text richiesto' });
}
const response = await fetch(`${OLLAMA_URL}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
prompt: `Correggi gli errori grammaticali e di ortografia nel seguente testo in ${language}. Rispondi SOLO con il testo corretto, senza spiegazioni:\n\n${text}`,
stream: false,
options: { temperature: 0.2 },
}),
});
if (!response.ok) {
throw new Error(`Ollama error: ${response.status}`);
}
const data = await response.json();
res.json({
corrected: data.response.trim(),
original: text,
language,
});
} catch (error) {
console.error('[api2/grammar] Error:', error);
res.status(500).json({ error: error.message });
}
});
/**
* Rispondi a domanda con contesto
* POST /api/qa
*/
router.post('/qa', async (req, res) => {
try {
const {
question,
context,
model = DEFAULT_MODEL,
} = req.body;
if (!question) {
return res.status(400).json({ error: 'Question richiesta' });
}
if (!context) {
return res.status(400).json({ error: 'Context richiesto' });
}
const response = await fetch(`${OLLAMA_URL}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
prompt: `Basandoti SOLO sul seguente contesto, rispondi alla domanda. Se la risposta non è nel contesto, dì che non hai abbastanza informazioni.
Contesto:
${context}
Domanda: ${question}
Risposta:`,
stream: false,
options: { temperature: 0.3 },
}),
});
if (!response.ok) {
throw new Error(`Ollama error: ${response.status}`);
}
const data = await response.json();
res.json({
answer: data.response.trim(),
question,
});
} catch (error) {
console.error('[api2/qa] Error:', error);
res.status(500).json({ error: error.message });
}
});
/**
* Genera embeddings
* POST /api/embeddings
*/
router.post('/embeddings', async (req, res) => {
try {
const {
text,
model = 'nomic-embed-text',
} = req.body;
if (!text) {
return res.status(400).json({ error: 'Text richiesto' });
}
const response = await fetch(`${OLLAMA_URL}/api/embeddings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
prompt: text,
}),
});
if (!response.ok) {
throw new Error(`Ollama error: ${response.status}`);
}
const data = await response.json();
res.json({
embedding: data.embedding,
dimensions: data.embedding?.length,
model,
});
} catch (error) {
console.error('[api2/embeddings] Error:', error);
res.status(500).json({ error: error.message });
}
});
/**
* Classifica testo in categorie
* POST /api/classify
*/
router.post('/classify', async (req, res) => {
try {
const {
text,
categories,
model = DEFAULT_MODEL,
} = req.body;
if (!text) {
return res.status(400).json({ error: 'Text richiesto' });
}
if (!categories || !Array.isArray(categories)) {
return res.status(400).json({ error: 'Categories array richiesto' });
}
const response = await fetch(`${OLLAMA_URL}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
prompt: `Classifica il seguente testo in UNA delle seguenti categorie: ${categories.join(', ')}
Rispondi SOLO con un JSON nel formato:
{"category": "categoria_scelta", "confidence": 0.0-1.0, "reason": "breve motivazione"}
Testo: ${text}`,
stream: false,
options: { temperature: 0.1 },
}),
});
if (!response.ok) {
throw new Error(`Ollama error: ${response.status}`);
}
const data = await response.json();
try {
const jsonMatch = data.response.match(/\{[\s\S]*\}/);
if (jsonMatch) {
res.json(JSON.parse(jsonMatch[0]));
} else {
res.json({ category: 'unknown', raw: data.response });
}
} catch (e) {
res.json({ category: 'unknown', raw: data.response });
}
} catch (error) {
console.error('[api2/classify] Error:', error);
res.status(500).json({ error: error.message });
}
});
/**
* Pull/scarica un nuovo modello
* POST /api2/models/pull
*/
router.post('/models/pull', async (req, res) => {
try {
const { model } = req.body;
if (!model) {
return res.status(400).json({ error: 'Model name richiesto' });
}
// Streaming del progresso
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const response = await fetch(`${OLLAMA_URL}/api/pull`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: model, stream: true }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) {
res.write('data: [DONE]\n\n');
res.end();
break;
}
const chunk = decoder.decode(value);
res.write(`data: ${chunk}\n\n`);
}
} catch (error) {
res.status(500).json({ error: error.message });
}
});
/**
* Elimina un modello
* DELETE /api2/models/:name
*/
router.delete('/models/:name', async (req, res) => {
try {
const { name } = req.params;
const response = await fetch(`${OLLAMA_URL}/api/delete`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
});
if (response.ok) {
res.json({ success: true, message: `Modello ${name} eliminato` });
} else {
throw new Error(`Errore eliminazione: ${response.status}`);
}
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// ============================================
// EXPORT
// ============================================
module.exports = router;

View File

@@ -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,17 @@ const { MyElem } = require('../models/myelem');
const axios = require('axios');
// Importa le routes video
const videoRoutes = require('../routes/videoRoutes');
// Monta le routes video
router.use('/video', videoRoutes);
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 +414,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 +518,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;

View File

@@ -300,6 +300,44 @@ router.post(process.env.LINKVERIF_REG, (req, res) => {
});
});
router.post(process.env.CHECKREVERIF_EMAIL, (req, res) => {
const body = _.pick(req.body, ['idapp', 'idlink']);
const idapp = body.idapp;
const idlink = body.idlink;
// Cerco l'idlink se è ancora da Verificare
User.findByLinkVerifEmail(idapp, idlink)
.then((user) => {
if (!user) {
//console.log("NON TROVATO!");
return res.status(404).send({code: RIS_CODE_ERRORE, msg: 'Verifica email non andata a buon fine. Ripetere.'});
} else {
console.log('user', user);
if (user.verified_email) {
res.send({
code: server_constants.RIS_CODE_EMAIL_ALREADY_VERIFIED,
msg: tools.getres__("L'Email è già stata Verificata", res),
});
} else {
user.verified_email = true;
user.lasttimeonline = new Date();
user.save().then(() => {
//console.log("TROVATOOOOOO!");
res.send({
code: server_constants.RIS_CODE_EMAIL_VERIFIED,
msg: tools.getres__('EMAIL', res) + ' ' + tools.getres__('VERIF', res),
});
});
}
}
})
.catch((e) => {
console.log(process.env.LINKVERIF_REG, e.message);
res.status(400).send();
});
});
router.post(process.env.ADD_NEW_SITE, async (req, res) => {
try {
const body = req.body;

View File

@@ -506,13 +506,15 @@ router.post('/profile', authenticate, (req, res) => {
const perm = req.user ? req.user.perm : tools.Perm.PERM_NONE;
const username = req.body['username'];
const idapp = req.body.idapp;
const idnotif = req.body['idnotif'] || '';
//++Todo: controlla che tipo di dati ha il permesso di leggere
try {
// Check if ìs a Notif to read
const idnotif = req.body['idnotif'] ? req.body['idnotif'] : '';
SendNotif.setNotifAsRead(idapp, usernameOrig, idnotif);
if (idnotif) {
SendNotif.setNotifAsRead(idapp, usernameOrig, idnotif);
}
return User.getUserProfileByUsername(idapp, username, usernameOrig, false, perm)
.then((ris) => {
@@ -601,6 +603,7 @@ router.post('/panel', authenticate, async (req, res) => {
username: 1,
name: 1,
surname: 1,
verified_email: 1,
email: 1,
verified_by_aportador: 1,
aportador_solidario: 1,
@@ -657,6 +660,7 @@ router.post('/notifs', authenticate, async (req, res) => {
router.post('/newtok', async (req, res) => {
try {
const refreshToken = req.body.refreshToken;
const browser_random = req.body.br;
// return res.status(403).send({ error: 'Refresh token non valido' });
@@ -1124,6 +1128,11 @@ async function eseguiDbOpUser(idapp, mydata, locale, req, res) {
await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.noCircuit': mydata.value } });
} else if (mydata.dbop === 'noComune') {
await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.noComune': mydata.value } });
} else if (mydata.dbop === 'verifiedemail') {
await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'verified_email': mydata.value } });
} else if (mydata.dbop === 'resendVerificationEmail') {
// Invia la email di Verifica email
const ris = await sendemail.sendEmail_ReVerifyingEmail(mydata, idapp);
} else if (mydata.dbop === 'noCircIta') {
await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.noCircIta': mydata.value } });
} else if (mydata.dbop === 'insert_circuito_ita') {

21
src/routes/assets.js Normal file
View 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
View 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
View 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;

58
src/routes/videoRoutes.js Normal file
View File

@@ -0,0 +1,58 @@
const express = require('express');
const VideoController = require('../controllers/VideoController');
const UploadMiddleware = require('../middleware/uploadMiddleware');
const {
authenticate,
authenticate_noerror,
authenticate_noerror_WithUser,
authenticate_noerror_WithUserLean,
} = require('../middleware/authenticate');
const router = express.Router();
// Configurazione
const UPLOAD_PATH = process.env.VIDEO_UPLOAD_PATH || 'uploads/videos';
// Istanze
const videoController = new VideoController(UPLOAD_PATH);
const uploadMiddleware = new UploadMiddleware(UPLOAD_PATH);
// ============ FOLDER ROUTES ============
router.get('/folders', authenticate, videoController.getFolders);
router.post('/folders', authenticate, videoController.createFolder);
router.put('/folders/:folderPath(*)', authenticate, videoController.renameFolder);
router.delete('/folders/:folderPath(*)', authenticate, videoController.deleteFolder);
// ============ VIDEO ROUTES ============
router.get('/videos', authenticate, videoController.getVideos);
router.get('/videos/:folder/:filename', authenticate, videoController.getVideo);
// Upload
router.post(
'/videos/upload',
uploadMiddleware.single('video'),
videoController.uploadVideo
);
router.post(
'/videos/upload-multiple',
uploadMiddleware.multiple('videos', 10),
videoController.uploadVideos
);
// Modifica
router.put('/videos/:folder/:filename/rename', authenticate, videoController.renameVideo);
router.put('/videos/:folder/:filename/move', authenticate, videoController.moveVideo);
// Elimina
router.delete('/videos/:folder/:filename', authenticate, videoController.deleteVideo);
// Stream
router.get('/stream/:folder/:filename', authenticate, videoController.streamVideo);
// Error Handler
router.use(VideoController.errorHandler);
module.exports = router;

View 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();

View File

@@ -14,4 +14,3 @@ const seedTemplates = async () => {
};
seedTemplates();
s

View File

@@ -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,
@@ -494,6 +505,21 @@ module.exports = {
const strlinkreg = tools.getHostByIdApp(idapp) + process.env.LINKVERIF_REG + `/?idapp=${idapp}&idlink=${idreg}`;
return strlinkreg;
},
getlinkVerifyEmail: async function (idapp, email, username) {
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}`;
return strlinkreg;
} catch (e) {
console.error('ERROR getlinkVerifyEmail');
}
},
getlinkInvitoReg: function (idapp, dati) {
const strlinkreg = tools.getHostByIdApp(idapp) + `/invitetoreg/${dati.token}`;
return strlinkreg;
@@ -507,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)) {
@@ -570,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,
@@ -639,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);
@@ -666,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);
@@ -675,8 +698,42 @@ module.exports = {
console.error('Err sendEmail_Utente_Ammesso', e);
}
},
sendEmail_ReVerifyingEmail: async function (dati, idapp) {
try {
const user = await User.getUserById(idapp, dati._id);
const lang = user.lang;
const username = user.username;
const email = user.email;
let mylocalsconf = {
idapp,
dataemail: await this.getdataemail(idapp),
baseurl: tools.getHostByIdApp(idapp),
locale: lang,
nomeapp: tools.getNomeAppByIdApp(idapp),
strlinksito: tools.getHostByIdApp(idapp),
//strlinkreg: this.getlinkReg(idapp, idreg),
emailto: email,
name: user.name,
username: user.username,
verifyLink: await this.getlinkVerifyEmail(idapp, email, username),
user,
supportEmail: tools.getContactEmailSupportBydApp(idapp),
};
const quale_email_inviare = this.getPathEmail(idapp, 'reg_resend_email_to_verifiyng') + '/' + lang;
const ris = await this.sendEmail_base(idapp, quale_email_inviare, email, mylocalsconf, '');
return ris;
} catch (e) {
console.error('Err sendEmail_ReVerifyingEmail', e);
}
},
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),
@@ -688,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);
@@ -704,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
@@ -712,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),
@@ -733,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),
@@ -742,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);
@@ -770,6 +838,7 @@ module.exports = {
mylocalsconf = this.setParamsForTemplate(iscritto, mylocalsconf);
await this.sendEmail_base(
idapp,
'iscrizione_conacreis/' + lang,
emailto,
mylocalsconf,
@@ -778,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,
@@ -798,6 +868,7 @@ module.exports = {
if (tools.isManagAndAdminDifferent(idapp)) {
await this.sendEmail_base(
idapp,
'admin/iscrizione_conacreis/' + tools.LANGADMIN,
tools.getManagerEmailByIdApp(idapp),
mylocalsconf,
@@ -821,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) {
@@ -848,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) {
@@ -886,6 +957,7 @@ module.exports = {
}
await this.sendEmail_base(
idapp,
'booking/' + texthtml + '/' + lang,
emailto,
mylocalsconf,
@@ -894,6 +966,7 @@ module.exports = {
// Send Email also to the Admin
await this.sendEmail_base(
idapp,
'admin/' + texthtml + '/' + tools.LANGADMIN,
tools.getAdminEmailByIdApp(idapp),
mylocalsconf,
@@ -902,6 +975,7 @@ module.exports = {
if (tools.isManagAndAdminDifferent(idapp)) {
await this.sendEmail_base(
idapp,
'admin/' + texthtml + '/' + tools.LANGADMIN,
tools.getManagerEmailByIdApp(idapp),
mylocalsconf,
@@ -999,6 +1073,7 @@ module.exports = {
telegrambot.sendMsgTelegramToTheManagers(idapp, msgtelegram);
await this.sendEmail_base(
idapp,
'booking/cancelbooking/' + lang,
emailto,
mylocalsconf,
@@ -1007,6 +1082,7 @@ module.exports = {
// Send Email also to the Admin
await this.sendEmail_base(
idapp,
'admin/cancelbooking/' + tools.LANGADMIN,
tools.getAdminEmailByIdApp(idapp),
mylocalsconf,
@@ -1015,6 +1091,7 @@ module.exports = {
if (tools.isManagAndAdminDifferent(idapp)) {
await this.sendEmail_base(
idapp,
'admin/cancelbooking/' + tools.LANGADMIN,
tools.getManagerEmailByIdApp(idapp),
mylocalsconf,
@@ -1046,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);
@@ -1168,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,
@@ -1176,6 +1254,7 @@ module.exports = {
if (tools.isManagAndAdminDifferent(idapp)) {
await this.sendEmail_base(
idapp,
'admin/added_to_newsletter/' + tools.LANGADMIN,
tools.getManagerEmailByIdApp(idapp),
mylocalsconf,
@@ -1474,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,
@@ -1570,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,
@@ -1709,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,
@@ -1749,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,

View File

@@ -120,6 +120,10 @@ async function runStartupTasks() {
await inizia();
if (true) {
// const Seed = require('../scripts/seedTemplates');
}
// 4) reset job pendenti
await resetProcessingJob();

View File

@@ -36,6 +36,7 @@ function setupRouters(app) {
['/aitools', 'aitools_router'],
['/apisqlsrv', 'articleRoutes'],
['/api', 'api_router'],
['/api2', 'api2_router'],
['/api/telegram', 'telegram_router'],
['/inviti', 'invitaAmicoRoutes'],
];
@@ -58,6 +59,10 @@ function setupRouters(app) {
});
});
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
return true;
}

View File

@@ -146,9 +146,12 @@ connectToDatabase(connectionUrl, options)
const aitools_router = require('./router/aitools_router');
const article_router = require('./router/articleRoutes');
const api_router = require('./router/api_router');
const api2_router = require('./router/api2_router');
const { MyEvent } = require('./models/myevent');
app.use('/videos', express.static(path.join(__dirname, 'uploads/videos')));
app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
@@ -252,6 +255,7 @@ connectToDatabase(connectionUrl, options)
app.use('/aitools', aitools_router);
app.use('/apisqlsrv', article_router);
app.use('/api', api_router);
app.use('/api2', api2_router);
mystart();
});
@@ -1060,6 +1064,7 @@ connectToDatabase(connectionUrl, options)
const NOCORS = false;
const { domains, domainsAllowed } = parseDomains();
console.log('domains:', domains);

View 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();

View 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();

View 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();

View File

@@ -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) {

File diff suppressed because it is too large Load Diff

View File

@@ -1198,6 +1198,7 @@ module.exports = {
let paramsObj = {
usernameDest,
circuitnameDest: circuitname,
circuitId: myreccircuit ? myreccircuit._id : '',
path,
username_action: username_action,
singleadmin_username: usernameDest,
@@ -2023,6 +2024,19 @@ module.exports = {
return false;
},
getContactEmailSupportBydApp: function (idapp, option) {
const myapp = this.MYAPPS.find((item) => item.idapp === idapp);
if (myapp) {
if (myapp.hasOwnProperty('contacts')) {
if (myapp.confsite.hasOwnProperty('email')) {
return myapp.contacts.email;
}
}
}
return '';
},
getEnableTokenExpiredByIdApp: function (idapp) {
const myapp = this.MYAPPS.find((item) => item.idapp === idapp);
if (myapp && myapp.confpages && myapp.confpages.hasOwnProperty('enableTokenExpired')) {
@@ -5849,6 +5863,10 @@ module.exports = {
let mystr = '';
let userfrom = '';
let userto = '';
let namefrom = '';
let surnamefrom = '';
let nameto = '';
let surnameto = '';
let profilefrom = null;
let profileto = null;
@@ -5866,6 +5884,8 @@ module.exports = {
}
if (mov.userfrom) {
userfrom += mov.userfrom.username;
namefrom = mov.userfrom.name;
surnamefrom = mov.userfrom.surname;
profilefrom = mov.userfrom.profile;
}
@@ -5879,14 +5899,16 @@ module.exports = {
}
if (mov.userto) {
userto += mov.userto.username;
nameto = mov.userto.name;
surnameto = mov.userto.surname;
profileto = mov.userto.profile;
}
// mystr = t('movement.from') + userfrom + ' ' + t('movement.to') + userto
return {
userfrom: { profile: profilefrom, username: userfrom },
userto: { profile: profileto, username: userto },
userfrom: { profile: profilefrom, username: userfrom, name: namefrom, surname: surnamefrom },
userto: { profile: profileto, username: userto, name: nameto, surname: surnameto },
tipocontofrom,
tipocontoto,
};
@@ -6412,4 +6434,5 @@ module.exports = {
// Usa padding di 3 cifre per minor e patch (supporta fino a 999)
return major * 1000000 + minor * 1000 + patch;
},
};

View File

@@ -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);
}

View File

@@ -1283,7 +1283,6 @@ module.exports = {
DASHBOARD: 140,
DASHGROUP: 145,
MOVEMENTS: 148,
CSENDRISTO: 150,
STATUSREG: 160,
CHECKIFISLOGGED: 170,
INFO_VERSION: 180,

View File

@@ -1 +1 @@
1.2.86
1.2.87

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

1134
yarn.lock

File diff suppressed because it is too large Load Diff