Compare commits
3 Commits
b8dcd7f5e0
...
ChatBox
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a0cdec7bd | ||
|
|
3d87c336de | ||
|
|
037ff6f7f9 |
@@ -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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"
|
||||
@@ -365,7 +365,7 @@ html(lang="it")
|
||||
//- Intro
|
||||
.intro-text
|
||||
| Ciao <strong>#{usernameMembro}</strong>,<br>
|
||||
| complimenti! Sei stato abilitato al Circuito RIS del tuo territorio da #{usernameInvitante}.
|
||||
| complimenti! Sei stato abilitato #{nomeTerritorio} da #{usernameInvitante}.
|
||||
|
||||
if linkProfiloAdmin
|
||||
.divider(style="margin: 16px 0;")
|
||||
@@ -379,7 +379,7 @@ html(lang="it")
|
||||
.congrats-icon ✅
|
||||
h3 Abilitazione Completata
|
||||
p(style="font-size: 15px; color: #555; margin-top: 8px;")
|
||||
| Ora puoi utilizzare i RIS per i tuoi scambi nella comunità
|
||||
| Ora puoi utilizzare i #{symbol} per i tuoi scambi nella comunità
|
||||
.territory-name 📍 #{nomeTerritorio}
|
||||
|
||||
//- Info comunità
|
||||
@@ -448,7 +448,7 @@ html(lang="it")
|
||||
.step-number 1
|
||||
.step-content
|
||||
h5 Esplora la Piattaforma
|
||||
p Familiarizza con gli annunci, i membri e le funzionalità del Circuito RIS
|
||||
p Familiarizza con gli annunci, i membri e le funzionalità del #{nomeTerritorio}
|
||||
.step-item
|
||||
.step-number 2
|
||||
.step-content
|
||||
|
||||
@@ -1 +1 @@
|
||||
=`Richiesta ingresso di ${usernameMembro} - ${nomeMembro} ${cognomeMembro} su ${nomeTerritorio} in ${nomeapp}`
|
||||
=`Abilitazione avvenuta su ${nomeTerritorio} in ${nomeapp} - (${usernameMembro})`
|
||||
|
||||
@@ -300,7 +300,7 @@ html(lang="it")
|
||||
//- Intro
|
||||
.intro-text
|
||||
| Ciao <strong>#{nomeFacilitatore}</strong>,<br>
|
||||
| un nuovo membro richiede l'abilitazione alla fiducia al Circuito RIS del tuo territorio!
|
||||
| un nuovo membro richiede l'abilitazione alla fiducia al Circuito del tuo territorio!
|
||||
|
||||
//- Card richiesta
|
||||
.request-card
|
||||
@@ -384,12 +384,12 @@ html(lang="it")
|
||||
span.responsibility-icon 👥
|
||||
span.responsibility-text
|
||||
strong Integrazione:
|
||||
| Supporta il nuovo membro nell'attivazione e utilizzo del Circuito RIS locale
|
||||
| Supporta il nuovo membro nell'attivazione e utilizzo del Circuito locale
|
||||
|
||||
//- Info box
|
||||
.info-box
|
||||
p
|
||||
| ✓ Dopo l'abilitazione, #{usernameMembro} potrà accedere al Circuito RIS di #{nomeTerritorio}
|
||||
| ✓ Dopo l'abilitazione, #{usernameMembro} potrà accedere al #{nomeTerritorio}
|
||||
p
|
||||
| ✓ Il membro riceverà una notifica automatica dell'avvenuta attivazione
|
||||
|
||||
|
||||
404
emails/defaultSite/reg_resend_email_to_verifiyng/it/html.pug
Executable file
404
emails/defaultSite/reg_resend_email_to_verifiyng/it/html.pug
Executable 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}
|
||||
1
emails/defaultSite/reg_resend_email_to_verifiyng/it/subject.pug
Executable file
1
emails/defaultSite/reg_resend_email_to_verifiyng/it/subject.pug
Executable file
@@ -0,0 +1 @@
|
||||
Verifica la tua Email - ${nomeapp}`
|
||||
72
logtrans.txt
72
logtrans.txt
@@ -519,4 +519,74 @@ 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]
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
398
src/controllers/assetController.js
Normal file
398
src/controllers/assetController.js
Normal file
@@ -0,0 +1,398 @@
|
||||
const Asset = require('../models/Asset');
|
||||
const imageGenerator = require('../services/imageGenerator');
|
||||
const sharp = require('sharp');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads';
|
||||
|
||||
const assetController = {
|
||||
// POST /assets/upload
|
||||
async upload(req, res) {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Nessun file caricato'
|
||||
});
|
||||
}
|
||||
|
||||
const { category = 'other', tags, description, isReusable = true } = req.body;
|
||||
const file = req.file;
|
||||
|
||||
// Ottieni dimensioni immagine
|
||||
let dimensions = {};
|
||||
try {
|
||||
const metadata = await sharp(file.path).metadata();
|
||||
dimensions = { width: metadata.width, height: metadata.height };
|
||||
} catch (e) {
|
||||
console.warn('Cannot read image dimensions');
|
||||
}
|
||||
|
||||
// Genera thumbnail
|
||||
const thumbDir = path.join(UPLOAD_DIR, 'thumbs');
|
||||
await fs.mkdir(thumbDir, { recursive: true });
|
||||
const thumbName = `thumb_${file.filename}`;
|
||||
const thumbPath = path.join(thumbDir, thumbName);
|
||||
|
||||
try {
|
||||
await sharp(file.path)
|
||||
.resize(300, 300, { fit: 'cover' })
|
||||
.jpeg({ quality: 80 })
|
||||
.toFile(thumbPath);
|
||||
} catch (e) {
|
||||
console.warn('Cannot create thumbnail');
|
||||
}
|
||||
|
||||
const asset = new Asset({
|
||||
type: 'image',
|
||||
category,
|
||||
sourceType: 'upload',
|
||||
file: {
|
||||
path: file.path,
|
||||
url: `/uploads/${file.filename}`,
|
||||
thumbnailPath: thumbPath,
|
||||
thumbnailUrl: `/uploads/thumbs/${thumbName}`,
|
||||
originalName: file.originalname,
|
||||
mimeType: file.mimetype,
|
||||
size: file.size,
|
||||
dimensions
|
||||
},
|
||||
metadata: {
|
||||
userId: req.user._id,
|
||||
tags: tags ? tags.split(',').map(t => t.trim()) : [],
|
||||
description,
|
||||
isReusable: isReusable === 'true' || isReusable === true
|
||||
}
|
||||
});
|
||||
|
||||
await asset.save();
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: asset
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// POST /assets/upload-multiple
|
||||
async uploadMultiple(req, res) {
|
||||
try {
|
||||
if (!req.files || req.files.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Nessun file caricato'
|
||||
});
|
||||
}
|
||||
|
||||
const { category = 'other' } = req.body;
|
||||
const assets = [];
|
||||
|
||||
for (const file of req.files) {
|
||||
let dimensions = {};
|
||||
try {
|
||||
const metadata = await sharp(file.path).metadata();
|
||||
dimensions = { width: metadata.width, height: metadata.height };
|
||||
} catch (e) {}
|
||||
|
||||
const asset = new Asset({
|
||||
type: 'image',
|
||||
category,
|
||||
sourceType: 'upload',
|
||||
file: {
|
||||
path: file.path,
|
||||
url: `/uploads/${file.filename}`,
|
||||
originalName: file.originalname,
|
||||
mimeType: file.mimetype,
|
||||
size: file.size,
|
||||
dimensions
|
||||
},
|
||||
metadata: {
|
||||
userId: req.user._id
|
||||
}
|
||||
});
|
||||
|
||||
await asset.save();
|
||||
assets.push(asset);
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: assets
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// POST /assets/generate-ai
|
||||
async generateAi(req, res) {
|
||||
try {
|
||||
const {
|
||||
prompt,
|
||||
negativePrompt,
|
||||
provider = 'hf',
|
||||
category = 'other',
|
||||
aspectRatio = '9:16',
|
||||
model,
|
||||
seed,
|
||||
steps,
|
||||
cfg
|
||||
} = req.body;
|
||||
|
||||
if (!prompt) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Prompt richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const imageUrl = await imageGenerator.generate(provider, prompt, {
|
||||
negativePrompt,
|
||||
aspectRatio,
|
||||
model,
|
||||
seed,
|
||||
steps,
|
||||
cfg
|
||||
});
|
||||
const generationTime = Date.now() - startTime;
|
||||
|
||||
// Salva file
|
||||
const fileName = `ai_${Date.now()}_${Math.random().toString(36).substr(2, 9)}.jpg`;
|
||||
const filePath = path.join(UPLOAD_DIR, 'ai-generated', fileName);
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
|
||||
let fileSize = 0;
|
||||
let dimensions = {};
|
||||
|
||||
if (imageUrl.startsWith('data:')) {
|
||||
const base64Data = imageUrl.replace(/^data:image\/\w+;base64,/, '');
|
||||
const buffer = Buffer.from(base64Data, 'base64');
|
||||
await fs.writeFile(filePath, buffer);
|
||||
fileSize = buffer.length;
|
||||
|
||||
const metadata = await sharp(buffer).metadata();
|
||||
dimensions = { width: metadata.width, height: metadata.height };
|
||||
} else {
|
||||
const fetch = require('node-fetch');
|
||||
const response = await fetch(imageUrl);
|
||||
const buffer = await response.buffer();
|
||||
await fs.writeFile(filePath, buffer);
|
||||
fileSize = buffer.length;
|
||||
|
||||
const metadata = await sharp(buffer).metadata();
|
||||
dimensions = { width: metadata.width, height: metadata.height };
|
||||
}
|
||||
|
||||
const asset = new Asset({
|
||||
type: 'image',
|
||||
category,
|
||||
sourceType: 'ai',
|
||||
file: {
|
||||
path: filePath,
|
||||
url: `/uploads/ai-generated/${fileName}`,
|
||||
mimeType: 'image/jpeg',
|
||||
size: fileSize,
|
||||
dimensions
|
||||
},
|
||||
aiGeneration: {
|
||||
prompt,
|
||||
negativePrompt,
|
||||
provider,
|
||||
model,
|
||||
seed,
|
||||
steps,
|
||||
cfg,
|
||||
requestedSize: aspectRatio,
|
||||
generationTime
|
||||
},
|
||||
metadata: {
|
||||
userId: req.user._id,
|
||||
isReusable: true
|
||||
}
|
||||
});
|
||||
|
||||
await asset.save();
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: asset
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET /assets
|
||||
async list(req, res) {
|
||||
try {
|
||||
const {
|
||||
category,
|
||||
sourceType,
|
||||
page = 1,
|
||||
limit = 50
|
||||
} = req.query;
|
||||
|
||||
const query = {
|
||||
'metadata.userId': req.user._id,
|
||||
status: 'ready'
|
||||
};
|
||||
|
||||
if (category) query.category = category;
|
||||
if (sourceType) query.sourceType = sourceType;
|
||||
|
||||
const [assets, total] = await Promise.all([
|
||||
Asset.find(query)
|
||||
.sort({ createdAt: -1 })
|
||||
.skip((page - 1) * limit)
|
||||
.limit(parseInt(limit)),
|
||||
Asset.countDocuments(query)
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: assets,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / limit)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET /assets/:id
|
||||
async getById(req, res) {
|
||||
try {
|
||||
const asset = await Asset.findById(req.params.id);
|
||||
|
||||
if (!asset) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Asset non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: asset
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET /assets/:id/file
|
||||
async getFile(req, res) {
|
||||
try {
|
||||
const asset = await Asset.findById(req.params.id);
|
||||
|
||||
if (!asset || !asset.file?.path) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'File non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
res.sendFile(path.resolve(asset.file.path));
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET /assets/:id/thumbnail
|
||||
async getThumbnail(req, res) {
|
||||
try {
|
||||
const asset = await Asset.findById(req.params.id);
|
||||
|
||||
if (!asset) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Asset non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
const thumbPath = asset.file?.thumbnailPath || asset.file?.path;
|
||||
if (!thumbPath) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Thumbnail non disponibile'
|
||||
});
|
||||
}
|
||||
|
||||
res.sendFile(path.resolve(thumbPath));
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// DELETE /assets/:id
|
||||
async delete(req, res) {
|
||||
try {
|
||||
const asset = await Asset.findById(req.params.id);
|
||||
|
||||
if (!asset) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Asset non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
if (asset.metadata.userId.toString() !== req.user._id.toString()) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Non autorizzato'
|
||||
});
|
||||
}
|
||||
|
||||
// Elimina file
|
||||
try {
|
||||
if (asset.file?.path) await fs.unlink(asset.file.path);
|
||||
if (asset.file?.thumbnailPath) await fs.unlink(asset.file.thumbnailPath);
|
||||
} catch (e) {
|
||||
console.warn('File deletion warning:', e.message);
|
||||
}
|
||||
|
||||
await Asset.deleteOne({ _id: asset._id });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Asset eliminato'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = assetController;
|
||||
647
src/controllers/posterController.js
Normal file
647
src/controllers/posterController.js
Normal file
@@ -0,0 +1,647 @@
|
||||
const Poster = require('../models/Poster');
|
||||
const Template = require('../models/Template');
|
||||
const Asset = require('../models/Asset');
|
||||
const posterRenderer = require('../services/posterRenderer');
|
||||
const imageGenerator = require('../services/imageGenerator');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads';
|
||||
|
||||
const posterController = {
|
||||
// POST /posters
|
||||
async create(req, res) {
|
||||
try {
|
||||
const {
|
||||
templateId,
|
||||
name,
|
||||
description,
|
||||
content,
|
||||
assets,
|
||||
layerOverrides,
|
||||
autoRender = false
|
||||
} = req.body;
|
||||
|
||||
// Carica template
|
||||
const template = await Template.findById(templateId);
|
||||
if (!template) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Template non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
// Valida contenuti richiesti
|
||||
const requiredLayers = template.layers.filter(l => l.required);
|
||||
for (const layer of requiredLayers) {
|
||||
if (layer.type === 'title' && !content?.title) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: `Campo richiesto: ${layer.type}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const poster = new Poster({
|
||||
templateId,
|
||||
templateSnapshot: template.toObject(), // Snapshot per retrocompatibilità
|
||||
name: name || content?.title || 'Nuova Locandina',
|
||||
description,
|
||||
status: 'draft',
|
||||
content: content || {},
|
||||
assets: assets || {},
|
||||
layerOverrides: layerOverrides || {},
|
||||
renderEngineVersion: '1.0.0',
|
||||
metadata: {
|
||||
userId: req.user._id
|
||||
}
|
||||
});
|
||||
|
||||
await poster.save();
|
||||
|
||||
// Incrementa uso template
|
||||
await template.incrementUsage();
|
||||
|
||||
// Auto-render se richiesto
|
||||
if (autoRender) {
|
||||
await posterController._renderPoster(poster);
|
||||
await poster.save();
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: poster
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Poster create error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET /posters
|
||||
async list(req, res) {
|
||||
try {
|
||||
const {
|
||||
status,
|
||||
templateId,
|
||||
search,
|
||||
page = 1,
|
||||
limit = 20,
|
||||
sortBy = 'createdAt',
|
||||
sortOrder = 'desc'
|
||||
} = req.query;
|
||||
|
||||
const query = { 'metadata.userId': req.user._id };
|
||||
|
||||
if (status) query.status = status;
|
||||
if (templateId) query.templateId = templateId;
|
||||
if (search) query.$text = { $search: search };
|
||||
|
||||
const sort = { [sortBy]: sortOrder === 'asc' ? 1 : -1 };
|
||||
|
||||
const [posters, total] = await Promise.all([
|
||||
Poster.find(query)
|
||||
.populate('templateId', 'name templateType thumbnailUrl')
|
||||
.sort(sort)
|
||||
.skip((page - 1) * limit)
|
||||
.limit(parseInt(limit))
|
||||
.select('-templateSnapshot -history'),
|
||||
Poster.countDocuments(query)
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: posters,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / limit)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET /posters/favorites
|
||||
async listFavorites(req, res) {
|
||||
try {
|
||||
const posters = await Poster.findFavorites(req.user._id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: posters
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET /posters/recent
|
||||
async listRecent(req, res) {
|
||||
try {
|
||||
const { limit = 10 } = req.query;
|
||||
const posters = await Poster.findRecent(req.user._id, parseInt(limit));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: posters
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET /posters/:id
|
||||
async getById(req, res) {
|
||||
try {
|
||||
const poster = await Poster.findById(req.params.id)
|
||||
.populate('templateId')
|
||||
.populate('assets.backgroundImage.assetId')
|
||||
.populate('assets.mainImage.assetId')
|
||||
.populate('assets.logos.assetId');
|
||||
|
||||
if (!poster) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Poster non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
// Check ownership
|
||||
if (poster.metadata.userId.toString() !== req.user._id.toString()) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Accesso negato'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: poster
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// PUT /posters/:id
|
||||
async update(req, res) {
|
||||
try {
|
||||
const poster = await Poster.findById(req.params.id);
|
||||
|
||||
if (!poster) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Poster non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
if (poster.metadata.userId.toString() !== req.user._id.toString()) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Non autorizzato'
|
||||
});
|
||||
}
|
||||
|
||||
const updateFields = [
|
||||
'name', 'description', 'content', 'assets', 'layerOverrides'
|
||||
];
|
||||
|
||||
updateFields.forEach(field => {
|
||||
if (req.body[field] !== undefined) {
|
||||
poster[field] = req.body[field];
|
||||
}
|
||||
});
|
||||
|
||||
// Invalida render precedente se contenuto modificato
|
||||
if (req.body.content || req.body.assets || req.body.layerOverrides) {
|
||||
poster.status = 'draft';
|
||||
poster.addHistory('updated', { fields: Object.keys(req.body) });
|
||||
}
|
||||
|
||||
await poster.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: poster
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// DELETE /posters/:id
|
||||
async delete(req, res) {
|
||||
try {
|
||||
const poster = await Poster.findById(req.params.id);
|
||||
|
||||
if (!poster) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Poster non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
if (poster.metadata.userId.toString() !== req.user._id.toString()) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Non autorizzato'
|
||||
});
|
||||
}
|
||||
|
||||
// Elimina file renderizzati
|
||||
if (poster.renderOutput) {
|
||||
const filesToDelete = [
|
||||
poster.renderOutput.png?.path,
|
||||
poster.renderOutput.jpg?.path,
|
||||
poster.renderOutput.webp?.path
|
||||
].filter(Boolean);
|
||||
|
||||
for (const filePath of filesToDelete) {
|
||||
try {
|
||||
await fs.unlink(filePath);
|
||||
} catch (e) {
|
||||
console.warn('File not found:', filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Poster.deleteOne({ _id: poster._id });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Poster eliminato'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// POST /posters/:id/render
|
||||
async render(req, res) {
|
||||
try {
|
||||
const poster = await Poster.findById(req.params.id)
|
||||
.populate('templateId');
|
||||
|
||||
if (!poster) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Poster non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
if (poster.metadata.userId.toString() !== req.user._id.toString()) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Non autorizzato'
|
||||
});
|
||||
}
|
||||
|
||||
poster.status = 'processing';
|
||||
await poster.save();
|
||||
|
||||
try {
|
||||
await posterController._renderPoster(poster);
|
||||
await poster.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
status: poster.status,
|
||||
renderOutput: poster.renderOutput
|
||||
}
|
||||
});
|
||||
} catch (renderError) {
|
||||
poster.setError(renderError.message);
|
||||
await poster.save();
|
||||
throw renderError;
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// POST /posters/:id/regenerate-ai
|
||||
async regenerateAi(req, res) {
|
||||
try {
|
||||
const { assetType, prompt, provider = 'hf' } = req.body;
|
||||
const poster = await Poster.findById(req.params.id);
|
||||
|
||||
if (!poster) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Poster non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
if (poster.metadata.userId.toString() !== req.user._id.toString()) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Non autorizzato'
|
||||
});
|
||||
}
|
||||
|
||||
// Genera nuova immagine AI
|
||||
const startTime = Date.now();
|
||||
const imageUrl = await imageGenerator.generate(provider, prompt);
|
||||
const generationTime = Date.now() - startTime;
|
||||
|
||||
// Salva su filesystem
|
||||
const fileName = `${poster._id}_${assetType}_${Date.now()}.jpg`;
|
||||
const filePath = path.join(UPLOAD_DIR, 'ai-generated', fileName);
|
||||
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
|
||||
// Se è base64, converti
|
||||
let savedPath;
|
||||
if (imageUrl.startsWith('data:')) {
|
||||
const base64Data = imageUrl.replace(/^data:image\/\w+;base64,/, '');
|
||||
await fs.writeFile(filePath, base64Data, 'base64');
|
||||
savedPath = filePath;
|
||||
} else {
|
||||
// Se è URL, scarica
|
||||
const fetch = require('node-fetch');
|
||||
const response = await fetch(imageUrl);
|
||||
const buffer = await response.buffer();
|
||||
await fs.writeFile(filePath, buffer);
|
||||
savedPath = filePath;
|
||||
}
|
||||
|
||||
// Aggiorna asset nel poster
|
||||
const assetData = {
|
||||
sourceType: 'ai',
|
||||
url: `/uploads/ai-generated/${fileName}`,
|
||||
mimeType: 'image/jpeg',
|
||||
aiParams: {
|
||||
prompt,
|
||||
provider,
|
||||
generatedAt: new Date()
|
||||
}
|
||||
};
|
||||
|
||||
if (assetType === 'backgroundImage') {
|
||||
poster.assets.backgroundImage = assetData;
|
||||
poster.addHistory('ai_background_generated', { provider, duration: generationTime });
|
||||
} else if (assetType === 'mainImage') {
|
||||
poster.assets.mainImage = assetData;
|
||||
poster.addHistory('ai_main_generated', { provider, duration: generationTime });
|
||||
}
|
||||
|
||||
poster.status = 'draft';
|
||||
await poster.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
assetType,
|
||||
asset: assetData
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET /posters/:id/download/:format
|
||||
async download(req, res) {
|
||||
try {
|
||||
const { format } = req.params;
|
||||
const poster = await Poster.findById(req.params.id);
|
||||
|
||||
if (!poster) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Poster non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
const outputFile = poster.renderOutput?.[format];
|
||||
if (!outputFile?.path) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: `Formato ${format} non disponibile`
|
||||
});
|
||||
}
|
||||
|
||||
// Incrementa download count
|
||||
await poster.incrementDownload();
|
||||
|
||||
const fileName = `${poster.name.replace(/[^a-z0-9]/gi, '_')}_poster.${format}`;
|
||||
|
||||
res.download(outputFile.path, fileName);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// POST /posters/:id/favorite
|
||||
async toggleFavorite(req, res) {
|
||||
try {
|
||||
const poster = await Poster.findById(req.params.id);
|
||||
|
||||
if (!poster) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Poster non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
if (poster.metadata.userId.toString() !== req.user._id.toString()) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Non autorizzato'
|
||||
});
|
||||
}
|
||||
|
||||
await poster.toggleFavorite();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
isFavorite: poster.metadata.isFavorite
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// POST /posters/quick-generate (compatibile con la tua bozza)
|
||||
async quickGenerate(req, res) {
|
||||
try {
|
||||
const {
|
||||
templateId,
|
||||
titolo,
|
||||
descrizione,
|
||||
data,
|
||||
ora,
|
||||
luogo,
|
||||
contatti,
|
||||
fotoDescrizione,
|
||||
stile,
|
||||
provider = 'hf',
|
||||
aspectRatio = '9:16'
|
||||
} = req.body;
|
||||
|
||||
// Validazione base
|
||||
if (!titolo || !data || !luogo) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Compila titolo, data e luogo'
|
||||
});
|
||||
}
|
||||
|
||||
// Usa template default o quello specificato
|
||||
let template;
|
||||
if (templateId) {
|
||||
template = await Template.findById(templateId);
|
||||
} else {
|
||||
// Template default per quick-generate
|
||||
template = await Template.findOne({
|
||||
templateType: 'quick-generate',
|
||||
isActive: true
|
||||
});
|
||||
}
|
||||
|
||||
// Genera prompt per AI background
|
||||
const aiPrompt = `Vertical event poster background, ${stile || 'modern style, vivid colors'}. Subject: ${fotoDescrizione || 'abstract artistic shapes'}. Composition: Central empty space suitable for text overlay. NO TEXT, NO LETTERS, clean illustration, high quality, 4k.`;
|
||||
|
||||
// Genera immagine AI
|
||||
const startTime = Date.now();
|
||||
const rawImageUrl = await imageGenerator.generate(provider, aiPrompt);
|
||||
const generationTime = Date.now() - startTime;
|
||||
|
||||
// Salva asset generato
|
||||
const fileName = `quick_${Date.now()}.jpg`;
|
||||
const filePath = path.join(UPLOAD_DIR, 'ai-generated', fileName);
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
|
||||
if (rawImageUrl.startsWith('data:')) {
|
||||
const base64Data = rawImageUrl.replace(/^data:image\/\w+;base64,/, '');
|
||||
await fs.writeFile(filePath, base64Data, 'base64');
|
||||
}
|
||||
|
||||
// Crea poster
|
||||
const poster = new Poster({
|
||||
templateId: template?._id,
|
||||
name: titolo,
|
||||
status: 'processing',
|
||||
content: {
|
||||
title: titolo,
|
||||
subtitle: descrizione,
|
||||
eventDate: data,
|
||||
eventTime: ora,
|
||||
location: luogo,
|
||||
contacts: contatti
|
||||
},
|
||||
assets: {
|
||||
backgroundImage: {
|
||||
sourceType: 'ai',
|
||||
url: `/uploads/ai-generated/${fileName}`,
|
||||
mimeType: 'image/jpeg',
|
||||
aiParams: {
|
||||
prompt: aiPrompt,
|
||||
provider,
|
||||
generatedAt: new Date()
|
||||
}
|
||||
}
|
||||
},
|
||||
originalPrompt: aiPrompt,
|
||||
styleUsed: stile,
|
||||
aspectRatio,
|
||||
provider,
|
||||
metadata: {
|
||||
userId: req.user._id
|
||||
}
|
||||
});
|
||||
|
||||
poster.addHistory('ai_background_generated', { provider, duration: generationTime });
|
||||
|
||||
// Render con testi sovrapposti
|
||||
await posterController._renderPoster(poster, { useQuickRender: true });
|
||||
await poster.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
posterId: poster._id,
|
||||
imageUrl: poster.renderOutput?.png?.url || rawImageUrl,
|
||||
status: poster.status
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Quick generate error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Helper interno: renderizza poster
|
||||
async _renderPoster(poster, options = {}) {
|
||||
const template = poster.templateId || poster.templateSnapshot;
|
||||
|
||||
const result = await posterRenderer.render({
|
||||
template,
|
||||
content: poster.content,
|
||||
assets: poster.assets,
|
||||
layerOverrides: Object.fromEntries(poster.layerOverrides || new Map()),
|
||||
outputDir: path.join(UPLOAD_DIR, 'posters', 'final'),
|
||||
posterId: poster._id.toString()
|
||||
});
|
||||
|
||||
poster.setRenderOutput({
|
||||
png: {
|
||||
path: result.pngPath,
|
||||
url: `/uploads/posters/final/${path.basename(result.pngPath)}`,
|
||||
size: result.pngSize
|
||||
},
|
||||
jpg: {
|
||||
path: result.jpgPath,
|
||||
url: `/uploads/posters/final/${path.basename(result.jpgPath)}`,
|
||||
size: result.jpgSize,
|
||||
quality: 95
|
||||
},
|
||||
dimensions: result.dimensions,
|
||||
duration: result.duration
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = posterController;
|
||||
383
src/controllers/templateController.js
Normal file
383
src/controllers/templateController.js
Normal file
@@ -0,0 +1,383 @@
|
||||
const Template = require('../models/Template');
|
||||
|
||||
// Presets formati standard
|
||||
const FORMAT_PRESETS = {
|
||||
'A4': { width: 2480, height: 3508, dpi: 300 },
|
||||
'A4-landscape': { width: 3508, height: 2480, dpi: 300 },
|
||||
'A3': { width: 3508, height: 4961, dpi: 300 },
|
||||
'A3-landscape': { width: 4961, height: 3508, dpi: 300 },
|
||||
'instagram-post': { width: 1080, height: 1080, dpi: 72 },
|
||||
'instagram-story': { width: 1080, height: 1920, dpi: 72 },
|
||||
'instagram-portrait': { width: 1080, height: 1350, dpi: 72 },
|
||||
'facebook-post': { width: 1200, height: 630, dpi: 72 },
|
||||
'facebook-event': { width: 1920, height: 1080, dpi: 72 },
|
||||
'twitter-post': { width: 1200, height: 675, dpi: 72 },
|
||||
'poster-24x36': { width: 7200, height: 10800, dpi: 300 },
|
||||
'flyer-5x7': { width: 1500, height: 2100, dpi: 300 }
|
||||
};
|
||||
|
||||
const templateController = {
|
||||
// POST /templates
|
||||
async create(req, res) {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
templateType,
|
||||
description,
|
||||
format,
|
||||
safeArea,
|
||||
backgroundColor,
|
||||
layers,
|
||||
logoSlots,
|
||||
palette,
|
||||
typography,
|
||||
defaultAiPromptHints,
|
||||
metadata
|
||||
} = req.body;
|
||||
|
||||
// Applica preset se specificato
|
||||
let finalFormat = format;
|
||||
if (format?.preset && FORMAT_PRESETS[format.preset]) {
|
||||
finalFormat = {
|
||||
...FORMAT_PRESETS[format.preset],
|
||||
preset: format.preset,
|
||||
unit: 'px'
|
||||
};
|
||||
}
|
||||
|
||||
// Valida layers
|
||||
if (!layers || !Array.isArray(layers) || layers.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Almeno un layer è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
// Assicura ID unici per layer
|
||||
const layersWithIds = layers.map((layer, idx) => ({
|
||||
...layer,
|
||||
id: layer.id || `layer_${layer.type}_${idx}`
|
||||
}));
|
||||
|
||||
const template = new Template({
|
||||
name,
|
||||
templateType,
|
||||
description,
|
||||
format: finalFormat,
|
||||
safeArea: safeArea || {},
|
||||
backgroundColor: backgroundColor || '#1a1a2e',
|
||||
layers: layersWithIds,
|
||||
logoSlots: logoSlots || { enabled: false, slots: [] },
|
||||
palette: palette || {},
|
||||
typography: typography || {},
|
||||
defaultAiPromptHints: defaultAiPromptHints || {},
|
||||
metadata: {
|
||||
...metadata,
|
||||
author: req.user?.name || 'System'
|
||||
},
|
||||
userId: req.user?._id
|
||||
});
|
||||
|
||||
await template.save();
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: template
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Template create error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET /templates
|
||||
async list(req, res) {
|
||||
try {
|
||||
const {
|
||||
type,
|
||||
search,
|
||||
tags,
|
||||
page = 1,
|
||||
limit = 20,
|
||||
sortBy = 'createdAt',
|
||||
sortOrder = 'desc'
|
||||
} = req.query;
|
||||
|
||||
const query = { isActive: true };
|
||||
|
||||
if (type) {
|
||||
query.templateType = type;
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
const tagArray = tags.split(',').map(t => t.trim());
|
||||
query['metadata.tags'] = { $in: tagArray };
|
||||
}
|
||||
|
||||
if (search) {
|
||||
query.$text = { $search: search };
|
||||
}
|
||||
|
||||
// Se utente autenticato, mostra anche i suoi privati
|
||||
if (req.user) {
|
||||
query.$or = [
|
||||
{ 'metadata.isPublic': true },
|
||||
{ userId: req.user._id }
|
||||
];
|
||||
} else {
|
||||
query['metadata.isPublic'] = true;
|
||||
}
|
||||
|
||||
const sort = { [sortBy]: sortOrder === 'asc' ? 1 : -1 };
|
||||
|
||||
const [templates, total] = await Promise.all([
|
||||
Template.find(query)
|
||||
.sort(sort)
|
||||
.skip((page - 1) * limit)
|
||||
.limit(parseInt(limit))
|
||||
.select('-layers -logoSlots'), // Escludi dati pesanti per list
|
||||
Template.countDocuments(query)
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: templates,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / limit)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Template list error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET /templates/types
|
||||
async getTypes(req, res) {
|
||||
try {
|
||||
const types = await Template.distinct('templateType', { isActive: true });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: types.sort()
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET /templates/presets
|
||||
async getFormatPresets(req, res) {
|
||||
res.json({
|
||||
success: true,
|
||||
data: FORMAT_PRESETS
|
||||
});
|
||||
},
|
||||
|
||||
// GET /templates/:id
|
||||
async getById(req, res) {
|
||||
try {
|
||||
const template = await Template.findById(req.params.id);
|
||||
|
||||
if (!template) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Template non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
// Check accesso
|
||||
if (!template.metadata.isPublic &&
|
||||
(!req.user || template.userId?.toString() !== req.user._id.toString())) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Accesso negato'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: template
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// PUT /templates/:id
|
||||
async update(req, res) {
|
||||
try {
|
||||
const template = await Template.findById(req.params.id);
|
||||
|
||||
if (!template) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Template non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
// Check ownership
|
||||
if (template.userId?.toString() !== req.user._id.toString()) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Non autorizzato a modificare questo template'
|
||||
});
|
||||
}
|
||||
|
||||
const updateFields = [
|
||||
'name', 'description', 'templateType', 'format', 'safeArea',
|
||||
'backgroundColor', 'layers', 'logoSlots', 'palette',
|
||||
'typography', 'defaultAiPromptHints', 'metadata', 'isActive'
|
||||
];
|
||||
|
||||
updateFields.forEach(field => {
|
||||
if (req.body[field] !== undefined) {
|
||||
template[field] = req.body[field];
|
||||
}
|
||||
});
|
||||
|
||||
// Incrementa versione
|
||||
if (template.metadata) {
|
||||
const version = template.metadata.version || '1.0.0';
|
||||
const parts = version.split('.').map(Number);
|
||||
parts[2]++;
|
||||
template.metadata.version = parts.join('.');
|
||||
}
|
||||
|
||||
await template.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: template
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// DELETE /templates/:id
|
||||
async delete(req, res) {
|
||||
try {
|
||||
const template = await Template.findById(req.params.id);
|
||||
|
||||
if (!template) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Template non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
if (template.userId?.toString() !== req.user._id.toString()) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Non autorizzato'
|
||||
});
|
||||
}
|
||||
|
||||
// Soft delete
|
||||
template.isActive = false;
|
||||
await template.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Template eliminato'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// POST /templates/:id/duplicate
|
||||
async duplicate(req, res) {
|
||||
try {
|
||||
const original = await Template.findById(req.params.id);
|
||||
|
||||
if (!original) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Template non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
const duplicateData = original.toObject();
|
||||
delete duplicateData._id;
|
||||
delete duplicateData.createdAt;
|
||||
delete duplicateData.updatedAt;
|
||||
|
||||
duplicateData.name = `${original.name} (copia)`;
|
||||
duplicateData.userId = req.user._id;
|
||||
duplicateData.metadata = {
|
||||
...duplicateData.metadata,
|
||||
isPublic: false,
|
||||
usageCount: 0,
|
||||
author: req.user?.name || 'System',
|
||||
version: '1.0.0'
|
||||
};
|
||||
|
||||
const duplicate = new Template(duplicateData);
|
||||
await duplicate.save();
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: duplicate
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET /templates/:id/preview
|
||||
async getPreview(req, res) {
|
||||
try {
|
||||
const template = await Template.findById(req.params.id)
|
||||
.select('previewUrl thumbnailUrl name');
|
||||
|
||||
if (!template) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Template non trovato'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
previewUrl: template.previewUrl,
|
||||
thumbnailUrl: template.thumbnailUrl,
|
||||
name: template.name
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = templateController;
|
||||
45
src/data/asset.json
Normal file
45
src/data/asset.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"_id": "asset_bg_001",
|
||||
"type": "image",
|
||||
"category": "background",
|
||||
"sourceType": "ai",
|
||||
|
||||
"file": {
|
||||
"path": "/uploads/assets/backgrounds/forest_autumn_001.jpg",
|
||||
"url": "/api/assets/asset_bg_001/file",
|
||||
"thumbnailPath": "/uploads/assets/backgrounds/thumbs/forest_autumn_001_thumb.jpg",
|
||||
"thumbnailUrl": "/api/assets/asset_bg_001/thumbnail",
|
||||
"originalName": null,
|
||||
"mimeType": "image/jpeg",
|
||||
"size": 2458000,
|
||||
"dimensions": { "width": 2480, "height": 3508 }
|
||||
},
|
||||
|
||||
"aiGeneration": {
|
||||
"prompt": "Mystical autumn forest at golden hour...",
|
||||
"negativePrompt": "text, letters, words...",
|
||||
"provider": "hf",
|
||||
"model": "FLUX.1-dev",
|
||||
"seed": 8847291,
|
||||
"steps": 35,
|
||||
"cfg": 7.5,
|
||||
"requestedSize": "1024x1536",
|
||||
"actualSize": "1024x1536",
|
||||
"generationTime": 12500,
|
||||
"cost": 0
|
||||
},
|
||||
|
||||
"usage": {
|
||||
"usedInPosters": ["poster_sagra_funghi_2025_001"],
|
||||
"usedInTemplates": [],
|
||||
"usageCount": 1
|
||||
},
|
||||
|
||||
"metadata": {
|
||||
"userId": "user_001",
|
||||
"tags": ["forest", "autumn", "background", "nature"],
|
||||
"isReusable": true
|
||||
},
|
||||
|
||||
"createdAt": "2025-01-15T10:25:00.000Z"
|
||||
}
|
||||
150
src/data/poster.json
Normal file
150
src/data/poster.json
Normal file
@@ -0,0 +1,150 @@
|
||||
{
|
||||
"_id": "poster_sagra_funghi_2025_001",
|
||||
"templateId": "template_raccolta_funghi_001",
|
||||
"name": "Sagra del Fungo Porcino 2025",
|
||||
"status": "completed",
|
||||
|
||||
"content": {
|
||||
"title": "SAGRA DEL FUNGO PORCINO",
|
||||
"subtitle": "XXV Edizione - Tradizione e Sapori del Bosco",
|
||||
"eventDate": "15-16-17 Ottobre 2025",
|
||||
"eventTime": "10:00 - 23:00",
|
||||
"location": "Parco delle Querce, Borgo Montano (PG)",
|
||||
"contacts": "Tel: 0742 123456 | info@sagrafungoporcino.it | www.sagrafungoporcino.it",
|
||||
"extraText": [
|
||||
"Ingresso Libero",
|
||||
"Stand Gastronomici • Musica dal Vivo • Mercatino Artigianale"
|
||||
]
|
||||
},
|
||||
|
||||
"assets": {
|
||||
"backgroundImage": {
|
||||
"id": "asset_bg_001",
|
||||
"sourceType": "ai",
|
||||
"url": "/uploads/posters/poster_sagra_2025_bg.jpg",
|
||||
"thumbnailUrl": "/uploads/posters/thumbs/poster_sagra_2025_bg_thumb.jpg",
|
||||
"mimeType": "image/jpeg",
|
||||
"size": 2458000,
|
||||
"dimensions": { "width": 2480, "height": 3508 },
|
||||
"aiParams": {
|
||||
"prompt": "Mystical autumn forest at golden hour, morning mist between ancient oak trees, forest floor covered with porcini mushrooms, warm orange and golden light filtering through leaves, photorealistic, cinematic composition, National Geographic style, 8k quality",
|
||||
"negativePrompt": "text, letters, words, watermark, signature, blurry, low quality, cartoon, anime",
|
||||
"provider": "hf",
|
||||
"model": "FLUX.1-dev",
|
||||
"seed": 8847291,
|
||||
"steps": 35,
|
||||
"cfg": 7.5,
|
||||
"size": "1024x1536",
|
||||
"generatedAt": "2025-01-15T10:25:00.000Z"
|
||||
}
|
||||
},
|
||||
"mainImage": {
|
||||
"id": "asset_main_001",
|
||||
"sourceType": "upload",
|
||||
"url": "/uploads/assets/porcini_basket_hero.jpg",
|
||||
"thumbnailUrl": "/uploads/assets/thumbs/porcini_basket_hero_thumb.jpg",
|
||||
"originalName": "IMG_20241015_porcini.jpg",
|
||||
"mimeType": "image/jpeg",
|
||||
"size": 1845000,
|
||||
"dimensions": { "width": 1920, "height": 1280 },
|
||||
"uploadedAt": "2025-01-15T10:20:00.000Z"
|
||||
},
|
||||
"logos": [
|
||||
{
|
||||
"id": "asset_logo_001",
|
||||
"slotId": "logo_slot_1",
|
||||
"sourceType": "upload",
|
||||
"url": "/uploads/logos/comune_borgomontano.png",
|
||||
"originalName": "logo_comune.png",
|
||||
"mimeType": "image/png",
|
||||
"size": 45000
|
||||
},
|
||||
{
|
||||
"id": "asset_logo_002",
|
||||
"slotId": "logo_slot_2",
|
||||
"sourceType": "upload",
|
||||
"url": "/uploads/logos/proloco_borgomontano.png",
|
||||
"originalName": "logo_proloco.png",
|
||||
"mimeType": "image/png",
|
||||
"size": 38000
|
||||
},
|
||||
{
|
||||
"id": "asset_logo_003",
|
||||
"slotId": "logo_slot_3",
|
||||
"sourceType": "ai",
|
||||
"url": "/uploads/logos/ai_generated_mushroom_logo.png",
|
||||
"mimeType": "image/png",
|
||||
"size": 52000,
|
||||
"aiParams": {
|
||||
"prompt": "Minimal vector logo of a porcini mushroom, flat design, golden brown color, white background, simple elegant icon",
|
||||
"provider": "ideogram",
|
||||
"model": "ideogram-v2"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"layerOverrides": {
|
||||
"layer_title": {
|
||||
"style": {
|
||||
"fontSize": 78,
|
||||
"color": "#fff8e7"
|
||||
}
|
||||
},
|
||||
"layer_event_date": {
|
||||
"style": {
|
||||
"color": "#ffa502"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"renderOutput": {
|
||||
"png": {
|
||||
"path": "/uploads/posters/final/poster_sagra_2025_final.png",
|
||||
"size": 8945000,
|
||||
"url": "/api/posters/poster_sagra_funghi_2025_001/download/png"
|
||||
},
|
||||
"jpg": {
|
||||
"path": "/uploads/posters/final/poster_sagra_2025_final.jpg",
|
||||
"quality": 95,
|
||||
"size": 2145000,
|
||||
"url": "/api/posters/poster_sagra_funghi_2025_001/download/jpg"
|
||||
},
|
||||
"dimensions": {
|
||||
"width": 2480,
|
||||
"height": 3508
|
||||
},
|
||||
"renderedAt": "2025-01-15T10:30:00.000Z"
|
||||
},
|
||||
|
||||
"renderEngineVersion": "1.0.0",
|
||||
|
||||
"history": [
|
||||
{
|
||||
"action": "created",
|
||||
"timestamp": "2025-01-15T10:15:00.000Z",
|
||||
"userId": "user_001"
|
||||
},
|
||||
{
|
||||
"action": "ai_background_generated",
|
||||
"timestamp": "2025-01-15T10:25:00.000Z",
|
||||
"details": { "provider": "hf", "duration": 12500 }
|
||||
},
|
||||
{
|
||||
"action": "rendered",
|
||||
"timestamp": "2025-01-15T10:30:00.000Z",
|
||||
"details": { "duration": 3200 }
|
||||
}
|
||||
],
|
||||
|
||||
"metadata": {
|
||||
"userId": "user_001",
|
||||
"projectId": "project_eventi_2025",
|
||||
"tags": ["sagra", "fungo", "autunno", "2025"],
|
||||
"isPublic": false,
|
||||
"isFavorite": true
|
||||
},
|
||||
|
||||
"createdAt": "2025-01-15T10:15:00.000Z",
|
||||
"updatedAt": "2025-01-15T10:30:00.000Z"
|
||||
}
|
||||
272
src/data/template.json
Normal file
272
src/data/template.json
Normal file
@@ -0,0 +1,272 @@
|
||||
{
|
||||
"_id": "template_raccolta_funghi_001",
|
||||
"name": "Raccolta Funghi Autunnale",
|
||||
"templateType": "outdoor-event",
|
||||
"description": "Template per eventi all'aperto legati alla natura",
|
||||
|
||||
"format": {
|
||||
"preset": "A4",
|
||||
"width": 2480,
|
||||
"height": 3508,
|
||||
"unit": "px",
|
||||
"dpi": 300
|
||||
},
|
||||
|
||||
"safeArea": {
|
||||
"top": 0.04,
|
||||
"right": 0.04,
|
||||
"bottom": 0.04,
|
||||
"left": 0.04
|
||||
},
|
||||
|
||||
"backgroundColor": "#1a1a2e",
|
||||
|
||||
"layers": [
|
||||
{
|
||||
"id": "layer_bg",
|
||||
"type": "backgroundImage",
|
||||
"zIndex": 0,
|
||||
"position": { "x": 0, "y": 0, "w": 1, "h": 1 },
|
||||
"anchor": "top-left",
|
||||
"required": false,
|
||||
"fallback": {
|
||||
"type": "gradient",
|
||||
"direction": "to-bottom",
|
||||
"colors": ["#2d3436", "#636e72"]
|
||||
},
|
||||
"style": {
|
||||
"opacity": 1,
|
||||
"blur": 0,
|
||||
"objectFit": "cover",
|
||||
"overlay": {
|
||||
"enabled": true,
|
||||
"type": "gradient",
|
||||
"direction": "to-bottom",
|
||||
"stops": [
|
||||
{ "position": 0, "color": "rgba(0,0,0,0)" },
|
||||
{ "position": 0.5, "color": "rgba(0,0,0,0.3)" },
|
||||
{ "position": 1, "color": "rgba(0,0,0,0.85)" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "layer_main_image",
|
||||
"type": "mainImage",
|
||||
"zIndex": 1,
|
||||
"position": { "x": 0.5, "y": 0.28, "w": 0.85, "h": 0.38 },
|
||||
"anchor": "center",
|
||||
"required": false,
|
||||
"style": {
|
||||
"borderRadius": 24,
|
||||
"objectFit": "cover",
|
||||
"shadow": {
|
||||
"enabled": true,
|
||||
"blur": 40,
|
||||
"spread": 0,
|
||||
"offsetX": 0,
|
||||
"offsetY": 20,
|
||||
"color": "rgba(0,0,0,0.6)"
|
||||
},
|
||||
"border": {
|
||||
"enabled": false,
|
||||
"width": 4,
|
||||
"color": "#ffffff"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "layer_title",
|
||||
"type": "title",
|
||||
"zIndex": 10,
|
||||
"position": { "x": 0.5, "y": 0.54, "w": 0.92, "h": 0.12 },
|
||||
"anchor": "center",
|
||||
"required": true,
|
||||
"maxLines": 2,
|
||||
"style": {
|
||||
"fontFamily": "Montserrat",
|
||||
"fontWeight": 900,
|
||||
"fontSize": 82,
|
||||
"fontSizeMin": 48,
|
||||
"fontSizeMax": 120,
|
||||
"autoFit": true,
|
||||
"color": "#ffffff",
|
||||
"textAlign": "center",
|
||||
"textTransform": "uppercase",
|
||||
"letterSpacing": 6,
|
||||
"lineHeight": 1.05,
|
||||
"shadow": {
|
||||
"enabled": true,
|
||||
"blur": 15,
|
||||
"offsetX": 3,
|
||||
"offsetY": 3,
|
||||
"color": "rgba(0,0,0,0.9)"
|
||||
},
|
||||
"stroke": {
|
||||
"enabled": true,
|
||||
"width": 3,
|
||||
"color": "rgba(0,0,0,0.5)"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "layer_subtitle",
|
||||
"type": "subtitle",
|
||||
"zIndex": 10,
|
||||
"position": { "x": 0.5, "y": 0.635, "w": 0.85, "h": 0.05 },
|
||||
"anchor": "center",
|
||||
"required": false,
|
||||
"style": {
|
||||
"fontFamily": "Open Sans",
|
||||
"fontWeight": 400,
|
||||
"fontSize": 32,
|
||||
"color": "#f0f0f0",
|
||||
"textAlign": "center",
|
||||
"letterSpacing": 2,
|
||||
"lineHeight": 1.3,
|
||||
"shadow": {
|
||||
"enabled": true,
|
||||
"blur": 8,
|
||||
"offsetX": 1,
|
||||
"offsetY": 1,
|
||||
"color": "rgba(0,0,0,0.7)"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "layer_event_date",
|
||||
"type": "eventDate",
|
||||
"zIndex": 10,
|
||||
"position": { "x": 0.5, "y": 0.72, "w": 0.9, "h": 0.06 },
|
||||
"anchor": "center",
|
||||
"required": true,
|
||||
"style": {
|
||||
"fontFamily": "Bebas Neue",
|
||||
"fontWeight": 400,
|
||||
"fontSize": 56,
|
||||
"color": "#ffd700",
|
||||
"textAlign": "center",
|
||||
"letterSpacing": 4,
|
||||
"textTransform": "uppercase",
|
||||
"shadow": {
|
||||
"enabled": true,
|
||||
"blur": 10,
|
||||
"offsetX": 2,
|
||||
"offsetY": 2,
|
||||
"color": "rgba(0,0,0,0.8)"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "layer_location",
|
||||
"type": "location",
|
||||
"zIndex": 10,
|
||||
"position": { "x": 0.5, "y": 0.79, "w": 0.85, "h": 0.05 },
|
||||
"anchor": "center",
|
||||
"required": true,
|
||||
"icon": {
|
||||
"enabled": true,
|
||||
"name": "location_on",
|
||||
"size": 28,
|
||||
"color": "#e74c3c"
|
||||
},
|
||||
"style": {
|
||||
"fontFamily": "Open Sans",
|
||||
"fontWeight": 600,
|
||||
"fontSize": 28,
|
||||
"color": "#ffffff",
|
||||
"textAlign": "center",
|
||||
"letterSpacing": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "layer_contacts",
|
||||
"type": "contacts",
|
||||
"zIndex": 10,
|
||||
"position": { "x": 0.5, "y": 0.86, "w": 0.9, "h": 0.04 },
|
||||
"anchor": "center",
|
||||
"required": false,
|
||||
"style": {
|
||||
"fontFamily": "Open Sans",
|
||||
"fontWeight": 400,
|
||||
"fontSize": 22,
|
||||
"color": "#cccccc",
|
||||
"textAlign": "center",
|
||||
"letterSpacing": 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "layer_extra_text",
|
||||
"type": "extraText",
|
||||
"zIndex": 10,
|
||||
"position": { "x": 0.5, "y": 0.91, "w": 0.85, "h": 0.03 },
|
||||
"anchor": "center",
|
||||
"required": false,
|
||||
"style": {
|
||||
"fontFamily": "Open Sans",
|
||||
"fontWeight": 300,
|
||||
"fontSize": 18,
|
||||
"fontStyle": "italic",
|
||||
"color": "#aaaaaa",
|
||||
"textAlign": "center"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"logoSlots": {
|
||||
"enabled": true,
|
||||
"maxCount": 3,
|
||||
"collapseIfEmpty": true,
|
||||
"slots": [
|
||||
{
|
||||
"id": "logo_slot_1",
|
||||
"position": { "x": 0.12, "y": 0.96, "w": 0.12, "h": 0.05 },
|
||||
"anchor": "bottom-left",
|
||||
"style": { "objectFit": "contain", "opacity": 0.9 }
|
||||
},
|
||||
{
|
||||
"id": "logo_slot_2",
|
||||
"position": { "x": 0.5, "y": 0.96, "w": 0.12, "h": 0.05 },
|
||||
"anchor": "bottom-center",
|
||||
"style": { "objectFit": "contain", "opacity": 0.9 }
|
||||
},
|
||||
{
|
||||
"id": "logo_slot_3",
|
||||
"position": { "x": 0.88, "y": 0.96, "w": 0.12, "h": 0.05 },
|
||||
"anchor": "bottom-right",
|
||||
"style": { "objectFit": "contain", "opacity": 0.9 }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"palette": {
|
||||
"primary": "#e94560",
|
||||
"secondary": "#0f3460",
|
||||
"accent": "#ffd700",
|
||||
"background": "#1a1a2e",
|
||||
"text": "#ffffff",
|
||||
"textSecondary": "#cccccc",
|
||||
"textMuted": "#888888"
|
||||
},
|
||||
|
||||
"typography": {
|
||||
"titleFont": "Montserrat",
|
||||
"headingFont": "Bebas Neue",
|
||||
"bodyFont": "Open Sans",
|
||||
"accentFont": "Playfair Display"
|
||||
},
|
||||
|
||||
"defaultAiPromptHints": {
|
||||
"backgroundImage": "atmospheric outdoor scene, nature, forest, autumn colors, cinematic lighting, no text, no letters",
|
||||
"mainImage": "detailed illustration, high quality, vibrant colors, no text"
|
||||
},
|
||||
|
||||
"metadata": {
|
||||
"author": "System",
|
||||
"version": "1.0.0",
|
||||
"tags": ["natura", "outdoor", "autunno", "sagra"]
|
||||
},
|
||||
|
||||
"createdAt": "2025-01-15T10:00:00.000Z",
|
||||
"updatedAt": "2025-01-15T10:00:00.000Z"
|
||||
}
|
||||
42
src/middleware/upload.js
Normal file
42
src/middleware/upload.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads';
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
cb(null, UPLOAD_DIR);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const uniqueId = crypto.randomBytes(8).toString('hex');
|
||||
const ext = path.extname(file.originalname);
|
||||
cb(null, `${Date.now()}_${uniqueId}${ext}`);
|
||||
}
|
||||
});
|
||||
|
||||
const fileFilter = (req, file, cb) => {
|
||||
const allowedTypes = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
'image/svg+xml'
|
||||
];
|
||||
|
||||
if (allowedTypes.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Tipo file non supportato'), false);
|
||||
}
|
||||
};
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
fileFilter,
|
||||
limits: {
|
||||
fileSize: 20 * 1024 * 1024 // 20MB max
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = upload;
|
||||
137
src/models/Asset.js
Normal file
137
src/models/Asset.js
Normal file
@@ -0,0 +1,137 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
// Sub-schema: File Info
|
||||
const FileInfoSchema = new Schema({
|
||||
path: { type: String, required: true },
|
||||
url: { type: String },
|
||||
thumbnailPath: { type: String },
|
||||
thumbnailUrl: { type: String },
|
||||
originalName: { type: String },
|
||||
mimeType: { type: String, required: true },
|
||||
size: { type: Number }, // bytes
|
||||
dimensions: {
|
||||
width: { type: Number },
|
||||
height: { type: Number }
|
||||
}
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: AI Generation Params
|
||||
const AiGenerationSchema = new Schema({
|
||||
prompt: { type: String, required: true },
|
||||
negativePrompt: { type: String },
|
||||
provider: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['hf', 'fal', 'ideogram', 'openai', 'stability', 'midjourney']
|
||||
},
|
||||
model: { type: String },
|
||||
seed: { type: Number },
|
||||
steps: { type: Number },
|
||||
cfg: { type: Number },
|
||||
requestedSize: { type: String },
|
||||
actualSize: { type: String },
|
||||
aspectRatio: { type: String },
|
||||
styleType: { type: String },
|
||||
generationTime: { type: Number }, // ms
|
||||
cost: { type: Number, default: 0 },
|
||||
rawResponse: { type: Schema.Types.Mixed }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Usage Tracking
|
||||
const UsageTrackingSchema = new Schema({
|
||||
usedInPosters: [{ type: Schema.Types.ObjectId, ref: 'Poster' }],
|
||||
usedInTemplates: [{ type: Schema.Types.ObjectId, ref: 'Template' }],
|
||||
usageCount: { type: Number, default: 0 }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Asset Metadata
|
||||
const AssetMetadataSchema = new Schema({
|
||||
userId: { type: Schema.Types.ObjectId, ref: 'User', index: true },
|
||||
projectId: { type: Schema.Types.ObjectId, ref: 'Project' },
|
||||
tags: [{ type: String }],
|
||||
description: { type: String },
|
||||
isReusable: { type: Boolean, default: true },
|
||||
isPublic: { type: Boolean, default: false }
|
||||
}, { _id: false });
|
||||
|
||||
// MAIN SCHEMA: Asset
|
||||
const AssetSchema = new Schema({
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['image', 'logo', 'icon', 'font']
|
||||
},
|
||||
category: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['background', 'main', 'logo', 'decoration', 'overlay', 'other'],
|
||||
index: true
|
||||
},
|
||||
sourceType: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['upload', 'ai', 'library', 'url'],
|
||||
index: true
|
||||
},
|
||||
|
||||
file: { type: FileInfoSchema, required: true },
|
||||
aiGeneration: { type: AiGenerationSchema },
|
||||
|
||||
usage: { type: UsageTrackingSchema, default: () => ({}) },
|
||||
metadata: { type: AssetMetadataSchema, default: () => ({}) },
|
||||
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['processing', 'ready', 'error', 'deleted'],
|
||||
default: 'ready'
|
||||
},
|
||||
errorMessage: { type: String }
|
||||
}, {
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true }
|
||||
});
|
||||
|
||||
// Indexes
|
||||
AssetSchema.index({ 'metadata.userId': 1, category: 1 });
|
||||
AssetSchema.index({ 'metadata.tags': 1 });
|
||||
AssetSchema.index({ sourceType: 1, status: 1 });
|
||||
|
||||
// Virtual: isAiGenerated
|
||||
AssetSchema.virtual('isAiGenerated').get(function() {
|
||||
return this.sourceType === 'ai';
|
||||
});
|
||||
|
||||
// Methods
|
||||
AssetSchema.methods.addUsage = async function(posterId, type = 'poster') {
|
||||
if (type === 'poster' && !this.usage.usedInPosters.includes(posterId)) {
|
||||
this.usage.usedInPosters.push(posterId);
|
||||
} else if (type === 'template' && !this.usage.usedInTemplates.includes(posterId)) {
|
||||
this.usage.usedInTemplates.push(posterId);
|
||||
}
|
||||
this.usage.usageCount = this.usage.usedInPosters.length + this.usage.usedInTemplates.length;
|
||||
return this.save();
|
||||
};
|
||||
|
||||
AssetSchema.methods.getPublicUrl = function() {
|
||||
return this.file.url || `/api/assets/${this._id}/file`;
|
||||
};
|
||||
|
||||
// Statics
|
||||
AssetSchema.statics.findByUser = function(userId, category = null) {
|
||||
const query = { 'metadata.userId': userId, status: 'ready' };
|
||||
if (category) query.category = category;
|
||||
return this.find(query).sort({ createdAt: -1 });
|
||||
};
|
||||
|
||||
AssetSchema.statics.findReusable = function(userId, category = null) {
|
||||
const query = {
|
||||
'metadata.userId': userId,
|
||||
'metadata.isReusable': true,
|
||||
status: 'ready'
|
||||
};
|
||||
if (category) query.category = category;
|
||||
return this.find(query).sort({ 'usage.usageCount': -1 });
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('Asset', AssetSchema);
|
||||
262
src/models/Poster.js
Normal file
262
src/models/Poster.js
Normal file
@@ -0,0 +1,262 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
// Sub-schema: Content
|
||||
const PosterContentSchema = new Schema({
|
||||
title: { type: String, maxlength: 500 },
|
||||
subtitle: { type: String, maxlength: 500 },
|
||||
eventDate: { type: String, maxlength: 200 },
|
||||
eventTime: { type: String, maxlength: 100 },
|
||||
location: { type: String, maxlength: 500 },
|
||||
contacts: { type: String, maxlength: 1000 },
|
||||
extraText: [{ type: String }],
|
||||
customFields: { type: Map, of: String }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Asset AI Params (embedded)
|
||||
const EmbeddedAiParamsSchema = new Schema({
|
||||
prompt: { type: String },
|
||||
negativePrompt: { type: String },
|
||||
provider: { type: String },
|
||||
model: { type: String },
|
||||
seed: { type: Number },
|
||||
steps: { type: Number },
|
||||
cfg: { type: Number },
|
||||
size: { type: String },
|
||||
generatedAt: { type: Date }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Poster Asset Reference
|
||||
const PosterAssetSchema = new Schema({
|
||||
id: { type: String },
|
||||
assetId: { type: Schema.Types.ObjectId, ref: 'Asset' },
|
||||
slotId: { type: String }, // per loghi
|
||||
sourceType: { type: String, enum: ['upload', 'ai', 'library', 'url'] },
|
||||
url: { type: String },
|
||||
thumbnailUrl: { type: String },
|
||||
originalName: { type: String },
|
||||
mimeType: { type: String },
|
||||
size: { type: Number },
|
||||
dimensions: {
|
||||
width: { type: Number },
|
||||
height: { type: Number }
|
||||
},
|
||||
aiParams: EmbeddedAiParamsSchema
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Assets Container
|
||||
const PosterAssetsSchema = new Schema({
|
||||
backgroundImage: PosterAssetSchema,
|
||||
mainImage: PosterAssetSchema,
|
||||
logos: [PosterAssetSchema]
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Layer Override Style
|
||||
const LayerOverrideStyleSchema = new Schema({
|
||||
fontSize: { type: Number },
|
||||
color: { type: String },
|
||||
fontWeight: { type: Number },
|
||||
opacity: { type: Number },
|
||||
// altri override possibili
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Layer Override
|
||||
const LayerOverrideSchema = new Schema({
|
||||
position: {
|
||||
x: { type: Number },
|
||||
y: { type: Number },
|
||||
w: { type: Number },
|
||||
h: { type: Number }
|
||||
},
|
||||
visible: { type: Boolean },
|
||||
style: LayerOverrideStyleSchema
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Render Output File
|
||||
const RenderOutputFileSchema = new Schema({
|
||||
path: { type: String, required: true },
|
||||
url: { type: String },
|
||||
size: { type: Number },
|
||||
quality: { type: Number }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Render Output
|
||||
const RenderOutputSchema = new Schema({
|
||||
png: RenderOutputFileSchema,
|
||||
jpg: RenderOutputFileSchema,
|
||||
webp: RenderOutputFileSchema,
|
||||
pdf: RenderOutputFileSchema,
|
||||
dimensions: {
|
||||
width: { type: Number },
|
||||
height: { type: Number }
|
||||
},
|
||||
renderedAt: { type: Date }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: History Entry
|
||||
const HistoryEntrySchema = new Schema({
|
||||
action: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['created', 'updated', 'ai_background_generated', 'ai_main_generated', 'rendered', 'downloaded', 'shared', 'deleted']
|
||||
},
|
||||
timestamp: { type: Date, default: Date.now },
|
||||
userId: { type: Schema.Types.ObjectId, ref: 'User' },
|
||||
details: { type: Schema.Types.Mixed }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Poster Metadata
|
||||
const PosterMetadataSchema = new Schema({
|
||||
userId: { type: Schema.Types.ObjectId, ref: 'User', required: true, index: true },
|
||||
projectId: { type: Schema.Types.ObjectId, ref: 'Project' },
|
||||
tags: [{ type: String }],
|
||||
isPublic: { type: Boolean, default: false },
|
||||
isFavorite: { type: Boolean, default: false },
|
||||
viewCount: { type: Number, default: 0 },
|
||||
downloadCount: { type: Number, default: 0 }
|
||||
}, { _id: false });
|
||||
|
||||
// MAIN SCHEMA: Poster
|
||||
const PosterSchema = new Schema({
|
||||
templateId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Template',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
templateSnapshot: { type: Schema.Types.Mixed }, // copia del template al momento della creazione
|
||||
|
||||
name: { type: String, required: true, trim: true, maxlength: 300 },
|
||||
description: { type: String, maxlength: 1000 },
|
||||
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['draft', 'processing', 'completed', 'error'],
|
||||
default: 'draft',
|
||||
index: true
|
||||
},
|
||||
|
||||
content: { type: PosterContentSchema, required: true },
|
||||
assets: { type: PosterAssetsSchema, default: () => ({}) },
|
||||
layerOverrides: { type: Map, of: LayerOverrideSchema, default: () => new Map() },
|
||||
|
||||
renderOutput: RenderOutputSchema,
|
||||
renderEngineVersion: { type: String, default: '1.0.0' },
|
||||
|
||||
history: [HistoryEntrySchema],
|
||||
metadata: { type: PosterMetadataSchema, required: true },
|
||||
|
||||
errorMessage: { type: String },
|
||||
|
||||
// Campi dalla tua bozza originale
|
||||
originalPrompt: { type: String }, // prompt completo usato
|
||||
styleUsed: { type: String },
|
||||
aspectRatio: { type: String },
|
||||
provider: { type: String }
|
||||
}, {
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true }
|
||||
});
|
||||
|
||||
// Indexes
|
||||
PosterSchema.index({ 'metadata.userId': 1, status: 1 });
|
||||
PosterSchema.index({ 'metadata.tags': 1 });
|
||||
PosterSchema.index({ 'metadata.isFavorite': 1, 'metadata.userId': 1 });
|
||||
PosterSchema.index({ createdAt: -1 });
|
||||
PosterSchema.index({ name: 'text', description: 'text' });
|
||||
|
||||
// Virtual: isCompleted
|
||||
PosterSchema.virtual('isCompleted').get(function() {
|
||||
return this.status === 'completed' && this.renderOutput?.png?.path;
|
||||
});
|
||||
|
||||
// Virtual: downloadUrl
|
||||
PosterSchema.virtual('downloadUrl').get(function() {
|
||||
if (this.renderOutput?.png?.path) {
|
||||
return `/api/posters/${this._id}/download/png`;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// Pre-save: aggiorna history
|
||||
PosterSchema.pre('save', function(next) {
|
||||
if (this.isNew) {
|
||||
this.history = this.history || [];
|
||||
this.history.push({
|
||||
action: 'created',
|
||||
timestamp: new Date(),
|
||||
userId: this.metadata.userId
|
||||
});
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Methods
|
||||
PosterSchema.methods.addHistory = function(action, details = {}) {
|
||||
this.history.push({
|
||||
action,
|
||||
timestamp: new Date(),
|
||||
userId: this.metadata.userId,
|
||||
details
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
PosterSchema.methods.setRenderOutput = function(outputData) {
|
||||
this.renderOutput = {
|
||||
...outputData,
|
||||
renderedAt: new Date()
|
||||
};
|
||||
this.status = 'completed';
|
||||
this.addHistory('rendered', { duration: outputData.duration });
|
||||
return this;
|
||||
};
|
||||
|
||||
PosterSchema.methods.setError = function(errorMessage) {
|
||||
this.status = 'error';
|
||||
this.errorMessage = errorMessage;
|
||||
return this;
|
||||
};
|
||||
|
||||
PosterSchema.methods.incrementDownload = async function() {
|
||||
this.metadata.downloadCount = (this.metadata.downloadCount || 0) + 1;
|
||||
this.addHistory('downloaded');
|
||||
return this.save();
|
||||
};
|
||||
|
||||
PosterSchema.methods.toggleFavorite = async function() {
|
||||
this.metadata.isFavorite = !this.metadata.isFavorite;
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Statics
|
||||
PosterSchema.statics.findByUser = function(userId, options = {}) {
|
||||
const query = { 'metadata.userId': userId };
|
||||
if (options.status) query.status = options.status;
|
||||
if (options.isFavorite) query['metadata.isFavorite'] = true;
|
||||
|
||||
return this.find(query)
|
||||
.populate('templateId', 'name templateType thumbnailUrl')
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(options.limit || 50);
|
||||
};
|
||||
|
||||
PosterSchema.statics.findFavorites = function(userId) {
|
||||
return this.find({
|
||||
'metadata.userId': userId,
|
||||
'metadata.isFavorite': true
|
||||
}).sort({ updatedAt: -1 });
|
||||
};
|
||||
|
||||
PosterSchema.statics.findRecent = function(userId, limit = 10) {
|
||||
return this.find({
|
||||
'metadata.userId': userId,
|
||||
status: 'completed'
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(limit)
|
||||
.select('name renderOutput.png.url thumbnailUrl createdAt');
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('Poster', PosterSchema);
|
||||
253
src/models/Template.js
Normal file
253
src/models/Template.js
Normal file
@@ -0,0 +1,253 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
// Sub-schema: Posizione layer
|
||||
const PositionSchema = new Schema({
|
||||
x: { type: Number, required: true, min: 0, max: 1 },
|
||||
y: { type: Number, required: true, min: 0, max: 1 },
|
||||
w: { type: Number, required: true, min: 0, max: 1 },
|
||||
h: { type: Number, required: true, min: 0, max: 1 }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Ombra
|
||||
const ShadowSchema = new Schema({
|
||||
enabled: { type: Boolean, default: false },
|
||||
blur: { type: Number, default: 10 },
|
||||
spread: { type: Number, default: 0 },
|
||||
offsetX: { type: Number, default: 0 },
|
||||
offsetY: { type: Number, default: 4 },
|
||||
color: { type: String, default: 'rgba(0,0,0,0.5)' }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Stroke
|
||||
const StrokeSchema = new Schema({
|
||||
enabled: { type: Boolean, default: false },
|
||||
width: { type: Number, default: 2 },
|
||||
color: { type: String, default: '#000000' }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Border
|
||||
const BorderSchema = new Schema({
|
||||
enabled: { type: Boolean, default: false },
|
||||
width: { type: Number, default: 2 },
|
||||
color: { type: String, default: '#ffffff' },
|
||||
style: { type: String, enum: ['solid', 'dashed', 'dotted'], default: 'solid' }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Gradient Stop
|
||||
const GradientStopSchema = new Schema({
|
||||
position: { type: Number, required: true, min: 0, max: 1 },
|
||||
color: { type: String, required: true }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Overlay
|
||||
const OverlaySchema = new Schema({
|
||||
enabled: { type: Boolean, default: false },
|
||||
type: { type: String, enum: ['solid', 'gradient'], default: 'gradient' },
|
||||
color: { type: String },
|
||||
direction: { type: String, default: 'to-bottom' },
|
||||
stops: [GradientStopSchema]
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Fallback
|
||||
const FallbackSchema = new Schema({
|
||||
type: { type: String, enum: ['solid', 'gradient'], default: 'solid' },
|
||||
color: { type: String },
|
||||
direction: { type: String },
|
||||
colors: [{ type: String }]
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Icon
|
||||
const IconSchema = new Schema({
|
||||
enabled: { type: Boolean, default: false },
|
||||
name: { type: String },
|
||||
size: { type: Number, default: 24 },
|
||||
color: { type: String, default: '#ffffff' }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Stile Layer (unificato per immagini e testi)
|
||||
const LayerStyleSchema = new Schema({
|
||||
// Comuni
|
||||
opacity: { type: Number, default: 1, min: 0, max: 1 },
|
||||
|
||||
// Per immagini
|
||||
objectFit: { type: String, enum: ['cover', 'contain', 'fill', 'none'], default: 'cover' },
|
||||
blur: { type: Number, default: 0 },
|
||||
borderRadius: { type: Number, default: 0 },
|
||||
overlay: OverlaySchema,
|
||||
border: BorderSchema,
|
||||
|
||||
// Per testi
|
||||
fontFamily: { type: String },
|
||||
fontWeight: { type: Number, default: 400 },
|
||||
fontSize: { type: Number },
|
||||
fontSizeMin: { type: Number },
|
||||
fontSizeMax: { type: Number },
|
||||
autoFit: { type: Boolean, default: false },
|
||||
fontStyle: { type: String, enum: ['normal', 'italic'], default: 'normal' },
|
||||
color: { type: String },
|
||||
textAlign: { type: String, enum: ['left', 'center', 'right'], default: 'center' },
|
||||
textTransform: { type: String, enum: ['none', 'uppercase', 'lowercase', 'capitalize'], default: 'none' },
|
||||
letterSpacing: { type: Number, default: 0 },
|
||||
lineHeight: { type: Number, default: 1.2 },
|
||||
|
||||
// Effetti
|
||||
shadow: ShadowSchema,
|
||||
stroke: StrokeSchema
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Layer
|
||||
const LayerSchema = new Schema({
|
||||
id: { type: String, required: true },
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['backgroundImage', 'mainImage', 'logo', 'title', 'subtitle', 'eventDate', 'eventTime', 'location', 'contacts', 'extraText', 'customText', 'customImage', 'shape', 'divider']
|
||||
},
|
||||
zIndex: { type: Number, default: 0 },
|
||||
position: { type: PositionSchema, required: true },
|
||||
anchor: {
|
||||
type: String,
|
||||
enum: ['top-left', 'top-center', 'top-right', 'center-left', 'center', 'center-right', 'bottom-left', 'bottom-center', 'bottom-right'],
|
||||
default: 'center'
|
||||
},
|
||||
required: { type: Boolean, default: false },
|
||||
visible: { type: Boolean, default: true },
|
||||
locked: { type: Boolean, default: false },
|
||||
maxLines: { type: Number },
|
||||
fallback: FallbackSchema,
|
||||
icon: IconSchema,
|
||||
style: { type: LayerStyleSchema, default: () => ({}) }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Logo Slot
|
||||
const LogoSlotSchema = new Schema({
|
||||
id: { type: String, required: true },
|
||||
position: { type: PositionSchema, required: true },
|
||||
anchor: { type: String, default: 'center' },
|
||||
style: { type: LayerStyleSchema, default: () => ({}) }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Logo Slots Config
|
||||
const LogoSlotsConfigSchema = new Schema({
|
||||
enabled: { type: Boolean, default: true },
|
||||
maxCount: { type: Number, default: 3, min: 1, max: 10 },
|
||||
collapseIfEmpty: { type: Boolean, default: true },
|
||||
slots: [LogoSlotSchema]
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Format
|
||||
const FormatSchema = new Schema({
|
||||
preset: { type: String, default: 'custom' }, // A4, A3, Instagram, Facebook, custom
|
||||
width: { type: Number, required: true },
|
||||
height: { type: Number, required: true },
|
||||
unit: { type: String, enum: ['px', 'mm', 'in'], default: 'px' },
|
||||
dpi: { type: Number, default: 300 }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Safe Area
|
||||
const SafeAreaSchema = new Schema({
|
||||
top: { type: Number, default: 0, min: 0, max: 0.5 },
|
||||
right: { type: Number, default: 0, min: 0, max: 0.5 },
|
||||
bottom: { type: Number, default: 0, min: 0, max: 0.5 },
|
||||
left: { type: Number, default: 0, min: 0, max: 0.5 }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Palette
|
||||
const PaletteSchema = new Schema({
|
||||
primary: { type: String, default: '#e94560' },
|
||||
secondary: { type: String, default: '#0f3460' },
|
||||
accent: { type: String, default: '#ffd700' },
|
||||
background: { type: String, default: '#1a1a2e' },
|
||||
text: { type: String, default: '#ffffff' },
|
||||
textSecondary: { type: String, default: '#cccccc' },
|
||||
textMuted: { type: String, default: '#888888' }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Typography
|
||||
const TypographySchema = new Schema({
|
||||
titleFont: { type: String, default: 'Montserrat' },
|
||||
headingFont: { type: String, default: 'Bebas Neue' },
|
||||
bodyFont: { type: String, default: 'Open Sans' },
|
||||
accentFont: { type: String, default: 'Playfair Display' }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: AI Prompt Hints
|
||||
const AiPromptHintsSchema = new Schema({
|
||||
backgroundImage: { type: String },
|
||||
mainImage: { type: String }
|
||||
}, { _id: false });
|
||||
|
||||
// Sub-schema: Metadata
|
||||
const TemplateMetadataSchema = new Schema({
|
||||
author: { type: String, default: 'System' },
|
||||
version: { type: String, default: '1.0.0' },
|
||||
tags: [{ type: String }],
|
||||
isPublic: { type: Boolean, default: false },
|
||||
usageCount: { type: Number, default: 0 }
|
||||
}, { _id: false });
|
||||
|
||||
// MAIN SCHEMA: Template
|
||||
const TemplateSchema = new Schema({
|
||||
name: { type: String, required: true, trim: true, maxlength: 200 },
|
||||
templateType: { type: String, required: true, trim: true, index: true },
|
||||
description: { type: String, maxlength: 1000 },
|
||||
|
||||
format: { type: FormatSchema, required: true },
|
||||
safeArea: { type: SafeAreaSchema, default: () => ({}) },
|
||||
backgroundColor: { type: String, default: '#1a1a2e' },
|
||||
|
||||
layers: { type: [LayerSchema], required: true, validate: [arr => arr.length > 0, 'Almeno un layer richiesto'] },
|
||||
logoSlots: { type: LogoSlotsConfigSchema, default: () => ({}) },
|
||||
|
||||
palette: { type: PaletteSchema, default: () => ({}) },
|
||||
typography: { type: TypographySchema, default: () => ({}) },
|
||||
defaultAiPromptHints: { type: AiPromptHintsSchema, default: () => ({}) },
|
||||
|
||||
previewUrl: { type: String },
|
||||
thumbnailUrl: { type: String },
|
||||
|
||||
metadata: { type: TemplateMetadataSchema, default: () => ({}) },
|
||||
|
||||
isActive: { type: Boolean, default: true },
|
||||
userId: { type: Schema.Types.ObjectId, ref: 'User', index: true }
|
||||
}, {
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true }
|
||||
});
|
||||
|
||||
// Indexes
|
||||
TemplateSchema.index({ templateType: 1, isActive: 1 });
|
||||
TemplateSchema.index({ 'metadata.tags': 1 });
|
||||
TemplateSchema.index({ name: 'text', description: 'text', templateType: 'text' });
|
||||
|
||||
// Virtual: layer count
|
||||
TemplateSchema.virtual('layerCount').get(function() {
|
||||
return this.layers ? this.layers.length : 0;
|
||||
});
|
||||
|
||||
// Methods
|
||||
TemplateSchema.methods.getLayerById = function(layerId) {
|
||||
return this.layers.find(l => l.id === layerId);
|
||||
};
|
||||
|
||||
TemplateSchema.methods.getLayersByType = function(type) {
|
||||
return this.layers.filter(l => l.type === type);
|
||||
};
|
||||
|
||||
TemplateSchema.methods.incrementUsage = async function() {
|
||||
this.metadata.usageCount = (this.metadata.usageCount || 0) + 1;
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Statics
|
||||
TemplateSchema.statics.findByType = function(templateType) {
|
||||
return this.find({ templateType, isActive: true }).sort({ 'metadata.usageCount': -1 });
|
||||
};
|
||||
|
||||
TemplateSchema.statics.findPublic = function() {
|
||||
return this.find({ 'metadata.isPublic': true, isActive: true });
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('Template', TemplateSchema);
|
||||
@@ -32,6 +32,12 @@ const AccountSchema = new Schema({
|
||||
numtransactions: {
|
||||
type: Number,
|
||||
},
|
||||
sent: {
|
||||
type: Number,
|
||||
},
|
||||
received: {
|
||||
type: Number,
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
},
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -25,6 +25,7 @@ module.exports = {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.log('error insertIntoDb', e);
|
||||
}
|
||||
|
||||
913
src/router/api2_router.js
Normal file
913
src/router/api2_router.js
Normal 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;
|
||||
@@ -2,8 +2,22 @@ const express = require('express');
|
||||
const { authenticate, authenticate_noerror } = require('../middleware/authenticate');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const templatesRouter = require('../routes/templates');
|
||||
const postersRouter = require('../routes/posters');
|
||||
const assetsRouter = require('../routes/assets');
|
||||
|
||||
const PageView = require('../models/PageView');
|
||||
|
||||
// const { Groq } = require('groq-sdk');
|
||||
|
||||
const fal = require('@fal-ai/client');
|
||||
|
||||
|
||||
const imageGenerator = require('../services/imageGenerator'); // Assicurati che il percorso sia corretto
|
||||
|
||||
const posterEditor = require('../services/PosterEditor'); // <--- Importa la nuova classe
|
||||
|
||||
const multer = require('multer');
|
||||
const XLSX = require('xlsx');
|
||||
|
||||
@@ -19,6 +33,10 @@ const { MyElem } = require('../models/myelem');
|
||||
|
||||
const axios = require('axios');
|
||||
|
||||
router.use('/templates', authenticate, templatesRouter);
|
||||
router.use('/posters', authenticate, postersRouter);
|
||||
router.use('/assets', authenticate, assetsRouter);
|
||||
|
||||
router.post('/test-lungo', authenticate, (req, res) => {
|
||||
const timeout = req.body.timeout;
|
||||
|
||||
@@ -389,7 +407,6 @@ router.post('/search-books', authenticate, async (req, res) => {
|
||||
let productfind = null;
|
||||
|
||||
for (let field of book) {
|
||||
|
||||
field = field.trim();
|
||||
let valido = typeof field === 'string' && field.length > 4 && field.length < 50;
|
||||
if (valido) {
|
||||
@@ -494,4 +511,46 @@ router.post('/chatbot', authenticate, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/generateposter', async (req, res) => {
|
||||
const {
|
||||
titolo, data, ora, luogo, descrizione, contatti, fotoDescrizione, stile,
|
||||
provider = 'hf' // Default a HF (Gratis)
|
||||
} = req.body;
|
||||
|
||||
// 1. Prompt per l'AI: Chiediamo SOLO lo sfondo, VIETIAMO il testo.
|
||||
// Questo garantisce che Flux si concentri sulla bellezza dell'immagine.
|
||||
const promptAI = `Vertical event poster background, ${stile || 'modern style, vivid colors'}.
|
||||
Subject: ${fotoDescrizione || 'abstract artistic shapes'}.
|
||||
Composition: Central empty space or clean layout suitable for overlaying text later.
|
||||
NO TEXT, NO LETTERS, clean illustration, high quality, 4k.`;
|
||||
|
||||
try {
|
||||
console.log('1. Generazione Sfondo AI...');
|
||||
// Genera solo l'immagine base
|
||||
const rawImageUrl = await imageGenerator.generate(provider, promptAI);
|
||||
|
||||
console.log('2. Composizione Grafica Testi...');
|
||||
// Sovrapponi i testi con Canvas
|
||||
const finalPosterBase64 = await posterEditor.createPoster(rawImageUrl, {
|
||||
titolo,
|
||||
data,
|
||||
ora,
|
||||
luogo,
|
||||
contatti
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
imageUrl: finalPosterBase64, // Restituisce l'immagine completa in base64
|
||||
step: 'AI + Canvas Composition'
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('Errore:', err.message);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
@@ -1124,6 +1127,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
21
src/routes/assets.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const assetController = require('../controllers/assetController');
|
||||
const { authenticate } = require('../middleware/authenticate');
|
||||
const upload = require('../middleware/upload');
|
||||
|
||||
// Upload
|
||||
router.post('/upload', authenticate, upload.single('file'), assetController.upload);
|
||||
router.post('/upload-multiple', authenticate, upload.array('files', 10), assetController.uploadMultiple);
|
||||
|
||||
// AI Generation
|
||||
router.post('/generate-ai', authenticate, assetController.generateAi);
|
||||
|
||||
// CRUD
|
||||
router.get('/', authenticate, assetController.list);
|
||||
router.get('/:id', assetController.getById);
|
||||
router.get('/:id/file', assetController.getFile);
|
||||
router.get('/:id/thumbnail', assetController.getThumbnail);
|
||||
router.delete('/:id', authenticate, assetController.delete);
|
||||
|
||||
module.exports = router;
|
||||
25
src/routes/posters.js
Normal file
25
src/routes/posters.js
Normal file
@@ -0,0 +1,25 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const posterController = require('../controllers/posterController');
|
||||
const { authenticate } = require('../middleware/authenticate');
|
||||
const upload = require('../middleware/upload');
|
||||
|
||||
// CRUD Posters
|
||||
router.post('/', authenticate, posterController.create);
|
||||
router.get('/', authenticate, posterController.list);
|
||||
router.get('/favorites', authenticate, posterController.listFavorites);
|
||||
router.get('/recent', authenticate, posterController.listRecent);
|
||||
router.get('/:id', authenticate, posterController.getById);
|
||||
router.put('/:id', authenticate, posterController.update);
|
||||
router.delete('/:id', authenticate, posterController.delete);
|
||||
|
||||
// Azioni speciali
|
||||
router.post('/:id/render', authenticate, posterController.render);
|
||||
router.post('/:id/regenerate-ai', authenticate, posterController.regenerateAi);
|
||||
router.get('/:id/download/:format', posterController.download);
|
||||
router.post('/:id/favorite', authenticate, posterController.toggleFavorite);
|
||||
|
||||
// Quick generate (come nella tua bozza)
|
||||
router.post('/quick-generate', authenticate, posterController.quickGenerate);
|
||||
|
||||
module.exports = router;
|
||||
17
src/routes/templates.js
Normal file
17
src/routes/templates.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const templateController = require('../controllers/templateController');
|
||||
const { authenticate } = require('../middleware/authenticate');
|
||||
|
||||
// CRUD Templates
|
||||
router.post('/', authenticate, templateController.create);
|
||||
router.get('/', templateController.list);
|
||||
router.get('/types', templateController.getTypes);
|
||||
router.get('/presets', templateController.getFormatPresets);
|
||||
router.get('/:id', templateController.getById);
|
||||
router.put('/:id', authenticate, templateController.update);
|
||||
router.delete('/:id', authenticate, templateController.delete);
|
||||
router.post('/:id/duplicate', authenticate, templateController.duplicate);
|
||||
router.get('/:id/preview', templateController.getPreview);
|
||||
|
||||
module.exports = router;
|
||||
33
src/scripts/seedTemplates.js
Normal file
33
src/scripts/seedTemplates.js
Normal file
@@ -0,0 +1,33 @@
|
||||
|
||||
const Template = require('../models/Template');
|
||||
const templateSeeds = require('../templates/template-seeds');
|
||||
|
||||
const MONGODB_URI = process.env.MONGODB_URI || '';
|
||||
|
||||
async function seedTemplates() {
|
||||
try {
|
||||
// await mongoose.connect(MONGODB_URI);
|
||||
// Opzionale: rimuovi template esistenti con stessi templateType
|
||||
const existingTypes = templateSeeds.map(t => t.templateType);
|
||||
await Template.deleteMany({ templateType: { $in: existingTypes } });
|
||||
console.log('✓ Template esistenti rimossi');
|
||||
|
||||
// Inserisci nuovi template
|
||||
const result = await Template.insertMany(templateSeeds);
|
||||
console.log(`✓ ${result.length} template inseriti con successo`);
|
||||
|
||||
// Log dei template creati
|
||||
result.forEach(t => {
|
||||
console.log(` - ${t.name} (${t.templateType})`);
|
||||
});
|
||||
|
||||
// await mongoose.disconnect();
|
||||
console.log('✓ Disconnesso da MongoDB');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('✗ Errore seeding:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
seedTemplates();
|
||||
@@ -14,4 +14,3 @@ const seedTemplates = async () => {
|
||||
};
|
||||
|
||||
seedTemplates();
|
||||
s
|
||||
147
src/sendemail.js
147
src/sendemail.js
@@ -368,17 +368,17 @@ function checkifSendEmail() {
|
||||
|
||||
module.exports = {
|
||||
sendEmail_base_e_manager: async function (idapp, template, to, mylocalsconf, replyTo, transport, previewonly) {
|
||||
await this.sendEmail_base(template, to, mylocalsconf, replyTo, transport, previewonly);
|
||||
await this.sendEmail_base(idapp, template, to, mylocalsconf, replyTo, transport, previewonly);
|
||||
|
||||
await this.sendEmail_base(template, tools.getAdminEmailByIdApp(idapp), mylocalsconf, '', transport, previewonly);
|
||||
await this.sendEmail_base(idapp, template, tools.getAdminEmailByIdApp(idapp), mylocalsconf, '', transport, previewonly);
|
||||
|
||||
if (tools.isManagAndAdminDifferent(idapp)) {
|
||||
const email = tools.getManagerEmailByIdApp(idapp);
|
||||
await this.sendEmail_base(template, email, mylocalsconf, '', transport, previewonly);
|
||||
await this.sendEmail_base(idapp, template, email, mylocalsconf, '', transport, previewonly);
|
||||
}
|
||||
},
|
||||
|
||||
sendEmail_base: async function (template, to, mylocalsconf, replyTo, transport, previewonly) {
|
||||
sendEmail_base: async function (idapp, template, to, mylocalsconf, replyTo, transport, previewonly) {
|
||||
if (to === '') return false;
|
||||
|
||||
// console.log('mylocalsconf', mylocalsconf);
|
||||
@@ -389,9 +389,17 @@ module.exports = {
|
||||
|
||||
if (!replyTo) replyTo = '';
|
||||
|
||||
const emailSender = mylocalsconf.dataemail.from;
|
||||
let senderName = '';
|
||||
if (idapp) {
|
||||
senderName = tools.getNomeAppByIdApp(mylocalsconf.idapp);
|
||||
}
|
||||
|
||||
const emailcompleta = senderName ? `"${senderName}" <${emailSender}>` : emailSender;
|
||||
|
||||
const paramemail = {
|
||||
message: {
|
||||
from: mylocalsconf.dataemail.from, // sender address
|
||||
from: emailcompleta,
|
||||
headers: {
|
||||
'Reply-To': replyTo,
|
||||
},
|
||||
@@ -457,9 +465,12 @@ module.exports = {
|
||||
|
||||
sendEmail_Normale: async function (mylocalsconf, to, subject, html, replyTo) {
|
||||
try {
|
||||
const emailSender = tools.getEmailByIdApp(mylocalsconf.idapp);
|
||||
const senderName = tools.getNomeAppByIdApp(mylocalsconf.idapp);
|
||||
|
||||
// setup e-mail data with unicode symbols
|
||||
var mailOptions = {
|
||||
from: tools.getEmailByIdApp(mylocalsconf.idapp), // sender address
|
||||
from: `"${senderName}" <${emailSender}>`,
|
||||
dataemail: await this.getdataemail(mylocalsconf.idapp),
|
||||
to: to,
|
||||
generateTextFromHTML: true,
|
||||
@@ -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,
|
||||
|
||||
@@ -120,6 +120,10 @@ async function runStartupTasks() {
|
||||
|
||||
await inizia();
|
||||
|
||||
if (true) {
|
||||
// const Seed = require('../scripts/seedTemplates');
|
||||
}
|
||||
|
||||
// 4) reset job pendenti
|
||||
await resetProcessingJob();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -146,6 +146,7 @@ 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');
|
||||
|
||||
@@ -252,6 +253,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();
|
||||
});
|
||||
|
||||
151
src/services/PosterEditor.js
Normal file
151
src/services/PosterEditor.js
Normal file
@@ -0,0 +1,151 @@
|
||||
const { createCanvas, loadImage } = require('canvas');
|
||||
|
||||
class PosterEditor {
|
||||
/**
|
||||
* Crea poster con testi sovrapposti (compatibile con tua API originale)
|
||||
*/
|
||||
async createPoster(backgroundImageUrl, data) {
|
||||
const { titolo, data: eventDate, ora, luogo, contatti } = data;
|
||||
|
||||
const width = 1080;
|
||||
const height = 1920;
|
||||
|
||||
const canvas = createCanvas(width, height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Carica e disegna background
|
||||
try {
|
||||
let img;
|
||||
if (backgroundImageUrl.startsWith('data:')) {
|
||||
img = await loadImage(backgroundImageUrl);
|
||||
} else {
|
||||
const fetch = require('node-fetch');
|
||||
const response = await fetch(backgroundImageUrl);
|
||||
const buffer = await response.buffer();
|
||||
img = await loadImage(buffer);
|
||||
}
|
||||
|
||||
// Cover fit
|
||||
const imgRatio = img.width / img.height;
|
||||
const canvasRatio = width / height;
|
||||
|
||||
let dw, dh, dx, dy;
|
||||
if (imgRatio > canvasRatio) {
|
||||
dh = height;
|
||||
dw = height * imgRatio;
|
||||
dx = (width - dw) / 2;
|
||||
dy = 0;
|
||||
} else {
|
||||
dw = width;
|
||||
dh = width / imgRatio;
|
||||
dx = 0;
|
||||
dy = (height - dh) / 2;
|
||||
}
|
||||
|
||||
ctx.drawImage(img, dx, dy, dw, dh);
|
||||
} catch (e) {
|
||||
// Fallback colore solido
|
||||
ctx.fillStyle = '#1a1a2e';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
// Overlay gradient
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, height);
|
||||
gradient.addColorStop(0, 'rgba(0,0,0,0)');
|
||||
gradient.addColorStop(0.5, 'rgba(0,0,0,0.3)');
|
||||
gradient.addColorStop(1, 'rgba(0,0,0,0.85)');
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
// Titolo
|
||||
if (titolo) {
|
||||
ctx.save();
|
||||
ctx.font = 'bold 68px Arial, sans-serif';
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.shadowColor = 'rgba(0,0,0,0.9)';
|
||||
ctx.shadowBlur = 20;
|
||||
ctx.shadowOffsetX = 3;
|
||||
ctx.shadowOffsetY = 3;
|
||||
|
||||
// Word wrap manuale
|
||||
const maxWidth = width * 0.9;
|
||||
const lines = this._wrapText(ctx, titolo.toUpperCase(), maxWidth);
|
||||
const lineHeight = 80;
|
||||
const startY = height * 0.50 - ((lines.length - 1) * lineHeight) / 2;
|
||||
|
||||
lines.forEach((line, i) => {
|
||||
ctx.fillText(line, width / 2, startY + i * lineHeight);
|
||||
});
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Data e ora
|
||||
if (eventDate) {
|
||||
ctx.save();
|
||||
ctx.font = 'bold 44px Arial, sans-serif';
|
||||
ctx.fillStyle = '#ffd700';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.shadowColor = 'rgba(0,0,0,0.8)';
|
||||
ctx.shadowBlur = 10;
|
||||
|
||||
const dateText = ora ? `${eventDate} • ORE ${ora}` : eventDate;
|
||||
ctx.fillText(dateText.toUpperCase(), width / 2, height * 0.68);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Luogo
|
||||
if (luogo) {
|
||||
ctx.save();
|
||||
ctx.font = '600 30px Arial, sans-serif';
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(`📍 ${luogo}`, width / 2, height * 0.76);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Contatti
|
||||
if (contatti) {
|
||||
ctx.save();
|
||||
ctx.font = '400 24px Arial, sans-serif';
|
||||
ctx.fillStyle = '#cccccc';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(contatti, width / 2, height * 0.85);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Ritorna base64
|
||||
return canvas.toDataURL('image/png');
|
||||
}
|
||||
|
||||
/**
|
||||
* Word wrap utility
|
||||
*/
|
||||
_wrapText(ctx, text, maxWidth) {
|
||||
const words = text.split(' ');
|
||||
const lines = [];
|
||||
let currentLine = '';
|
||||
|
||||
words.forEach(word => {
|
||||
const testLine = currentLine ? `${currentLine} ${word}` : word;
|
||||
const metrics = ctx.measureText(testLine);
|
||||
|
||||
if (metrics.width > maxWidth && currentLine) {
|
||||
lines.push(currentLine);
|
||||
currentLine = word;
|
||||
} else {
|
||||
currentLine = testLine;
|
||||
}
|
||||
});
|
||||
|
||||
if (currentLine) {
|
||||
lines.push(currentLine);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new PosterEditor();
|
||||
154
src/services/imageGenerator.js
Normal file
154
src/services/imageGenerator.js
Normal file
@@ -0,0 +1,154 @@
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
class ImageGenerator {
|
||||
constructor() {
|
||||
this.falKey = process.env.FAL_KEY;
|
||||
this.hfToken = process.env.HF_TOKEN;
|
||||
this.ideogramKey = process.env.IDEOGRAM_KEY;
|
||||
}
|
||||
|
||||
async generate(provider, prompt, options = {}) {
|
||||
const {
|
||||
negativePrompt,
|
||||
aspectRatio = '9:16',
|
||||
model,
|
||||
seed,
|
||||
steps,
|
||||
cfg
|
||||
} = options;
|
||||
|
||||
switch (provider) {
|
||||
case 'ideogram':
|
||||
return this._generateIdeogram(prompt, { aspectRatio });
|
||||
case 'fal':
|
||||
return this._generateFal(prompt, { aspectRatio, seed, steps, cfg });
|
||||
case 'hf':
|
||||
default:
|
||||
return this._generateHuggingFace(prompt, { negativePrompt });
|
||||
}
|
||||
}
|
||||
|
||||
// Ideogram V2 (via Fal.ai) - Ottimo per testo
|
||||
async _generateIdeogram(prompt, options = {}) {
|
||||
console.log('--- Generazione Ideogram V2 ---');
|
||||
|
||||
const response = await fetch('https://fal.run/fal-ai/ideogram/v2', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Key ${this.falKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
prompt,
|
||||
aspect_ratio: options.aspectRatio || '9:16',
|
||||
style_type: 'DESIGN',
|
||||
expand_prompt: true
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Ideogram error: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const imageUrl = result.images?.[0]?.url;
|
||||
|
||||
if (!imageUrl) throw new Error('Ideogram: nessun URL restituito');
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// Flux Dev (via Fal.ai)
|
||||
async _generateFal(prompt, options = {}) {
|
||||
console.log('--- Generazione Fal Flux Dev ---');
|
||||
|
||||
const imageSizeMap = {
|
||||
'9:16': 'portrait_16_9',
|
||||
'16:9': 'landscape_16_9',
|
||||
'1:1': 'square'
|
||||
};
|
||||
|
||||
const response = await fetch('https://fal.run/fal-ai/flux/dev', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Key ${this.falKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
prompt,
|
||||
image_size: imageSizeMap[options.aspectRatio] || 'portrait_16_9',
|
||||
num_images: 1,
|
||||
enable_safety_checker: false,
|
||||
seed: options.seed,
|
||||
num_inference_steps: options.steps || 28,
|
||||
guidance_scale: options.cfg || 7.5
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Fal error: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result.images?.[0]?.url;
|
||||
}
|
||||
|
||||
// Flux Dev (via Hugging Face) - GRATIS
|
||||
async _generateHuggingFace(prompt, options = {}) {
|
||||
console.log('--- Generazione HuggingFace (Gratis) ---');
|
||||
|
||||
const response = await fetch(
|
||||
'https://router.huggingface.co/hf-inference/models/black-forest-labs/FLUX.1-dev',
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.hfToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
inputs: prompt,
|
||||
parameters: {
|
||||
negative_prompt: options.negativePrompt
|
||||
}
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await response.text();
|
||||
throw new Error(`HuggingFace error: ${response.status} - ${err}`);
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
return `data:image/jpeg;base64,${buffer.toString('base64')}`;
|
||||
}
|
||||
|
||||
// Utility: verifica disponibilità provider
|
||||
async checkProvider(provider) {
|
||||
const checks = {
|
||||
hf: !!this.hfToken,
|
||||
fal: !!this.falKey,
|
||||
ideogram: !!this.falKey // Ideogram via Fal
|
||||
};
|
||||
|
||||
return checks[provider] || false;
|
||||
}
|
||||
|
||||
// Lista provider disponibili
|
||||
getAvailableProviders() {
|
||||
const providers = [];
|
||||
|
||||
if (this.hfToken) {
|
||||
providers.push({ id: 'hf', name: 'HuggingFace (Gratis)', cost: 'free' });
|
||||
}
|
||||
if (this.falKey) {
|
||||
providers.push({ id: 'fal', name: 'Fal Flux Dev', cost: 'paid' });
|
||||
providers.push({ id: 'ideogram', name: 'Ideogram V2', cost: 'paid' });
|
||||
}
|
||||
|
||||
return providers;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ImageGenerator();
|
||||
870
src/services/posterRenderer.js
Normal file
870
src/services/posterRenderer.js
Normal file
@@ -0,0 +1,870 @@
|
||||
|
||||
const { createCanvas, loadImage, registerFont } = require('canvas');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const sharp = require('sharp');
|
||||
|
||||
// Registra font personalizzati
|
||||
const FONTS_DIR = process.env.FONTS_DIR || './fonts';
|
||||
|
||||
const registerFonts = async () => {
|
||||
const fontMappings = [
|
||||
{ file: 'Montserrat-Black.ttf', family: 'Montserrat', weight: '900' },
|
||||
{ file: 'Montserrat-Bold.ttf', family: 'Montserrat', weight: '700' },
|
||||
{ file: 'Montserrat-Regular.ttf', family: 'Montserrat', weight: '400' },
|
||||
{ file: 'BebasNeue-Regular.ttf', family: 'Bebas Neue', weight: '400' },
|
||||
{ file: 'OpenSans-Bold.ttf', family: 'Open Sans', weight: '700' },
|
||||
{ file: 'OpenSans-SemiBold.ttf', family: 'Open Sans', weight: '600' },
|
||||
{ file: 'OpenSans-Regular.ttf', family: 'Open Sans', weight: '400' },
|
||||
{ file: 'OpenSans-Light.ttf', family: 'Open Sans', weight: '300' },
|
||||
{ file: 'PlayfairDisplay-Bold.ttf', family: 'Playfair Display', weight: '700' },
|
||||
{ file: 'PlayfairDisplay-Regular.ttf', family: 'Playfair Display', weight: '400' }
|
||||
];
|
||||
|
||||
for (const font of fontMappings) {
|
||||
const fontPath = path.join(FONTS_DIR, font.file);
|
||||
try {
|
||||
await fs.access(fontPath);
|
||||
registerFont(fontPath, { family: font.family, weight: font.weight });
|
||||
} catch (e) {
|
||||
// Font non trovato, usa fallback
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Inizializza fonts al caricamento modulo
|
||||
registerFonts().catch(console.warn);
|
||||
|
||||
class PosterRenderer {
|
||||
constructor() {
|
||||
this.version = '1.0.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render principale
|
||||
*/
|
||||
async render(options) {
|
||||
const {
|
||||
template,
|
||||
content,
|
||||
assets,
|
||||
layerOverrides = {},
|
||||
outputDir,
|
||||
posterId
|
||||
} = options;
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
// Dimensioni canvas
|
||||
const width = template.format?.width || 2480;
|
||||
const height = template.format?.height || 3508;
|
||||
|
||||
// Crea canvas
|
||||
const canvas = createCanvas(width, height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// 1. Disegna background
|
||||
await this._drawBackground(ctx, template, assets, width, height);
|
||||
|
||||
// 2. Ordina layer per zIndex
|
||||
const sortedLayers = [...(template.layers || [])]
|
||||
.sort((a, b) => (a.zIndex || 0) - (b.zIndex || 0));
|
||||
|
||||
// 3. Disegna ogni layer
|
||||
for (const layer of sortedLayers) {
|
||||
if (layer.visible === false) continue;
|
||||
|
||||
const override = layerOverrides[layer.id] || {};
|
||||
const mergedLayer = this._mergeLayerOverride(layer, override);
|
||||
|
||||
await this._drawLayer(ctx, mergedLayer, content, assets, width, height, template);
|
||||
}
|
||||
|
||||
// 4. Disegna loghi
|
||||
if (template.logoSlots?.enabled && assets?.logos?.length > 0) {
|
||||
await this._drawLogos(ctx, template.logoSlots, assets.logos, width, height);
|
||||
}
|
||||
|
||||
// 5. Salva output
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
|
||||
const baseName = `poster_${posterId}_${Date.now()}`;
|
||||
const pngPath = path.join(outputDir, `${baseName}.png`);
|
||||
const jpgPath = path.join(outputDir, `${baseName}.jpg`);
|
||||
|
||||
// Salva PNG
|
||||
const pngBuffer = canvas.toBuffer('image/png');
|
||||
await fs.writeFile(pngPath, pngBuffer);
|
||||
|
||||
// Salva JPG con Sharp (migliore qualità)
|
||||
await sharp(pngBuffer)
|
||||
.jpeg({ quality: 95, progressive: true })
|
||||
.toFile(jpgPath);
|
||||
|
||||
const [pngStats, jpgStats] = await Promise.all([
|
||||
fs.stat(pngPath),
|
||||
fs.stat(jpgPath)
|
||||
]);
|
||||
|
||||
return {
|
||||
pngPath,
|
||||
jpgPath,
|
||||
pngSize: pngStats.size,
|
||||
jpgSize: jpgStats.size,
|
||||
dimensions: { width, height },
|
||||
duration: Date.now() - startTime,
|
||||
engineVersion: this.version
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Disegna background
|
||||
*/
|
||||
async _drawBackground(ctx, template, assets, width, height) {
|
||||
// Colore di sfondo base
|
||||
ctx.fillStyle = template.backgroundColor || '#1a1a2e';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
// Background image
|
||||
const bgAsset = assets?.backgroundImage;
|
||||
const bgLayer = template.layers?.find(l => l.type === 'backgroundImage');
|
||||
|
||||
if (bgAsset?.url) {
|
||||
try {
|
||||
const img = await this._loadImageFromUrl(bgAsset.url);
|
||||
|
||||
// Calcola dimensioni per cover
|
||||
const imgRatio = img.width / img.height;
|
||||
const canvasRatio = width / height;
|
||||
|
||||
let drawWidth, drawHeight, drawX, drawY;
|
||||
|
||||
if (imgRatio > canvasRatio) {
|
||||
drawHeight = height;
|
||||
drawWidth = height * imgRatio;
|
||||
drawX = (width - drawWidth) / 2;
|
||||
drawY = 0;
|
||||
} else {
|
||||
drawWidth = width;
|
||||
drawHeight = width / imgRatio;
|
||||
drawX = 0;
|
||||
drawY = (height - drawHeight) / 2;
|
||||
}
|
||||
|
||||
// Applica blur se definito
|
||||
if (bgLayer?.style?.blur > 0) {
|
||||
ctx.filter = `blur(${bgLayer.style.blur}px)`;
|
||||
}
|
||||
|
||||
ctx.drawImage(img, drawX, drawY, drawWidth, drawHeight);
|
||||
ctx.filter = 'none';
|
||||
|
||||
// Applica overlay gradient
|
||||
const overlay = bgLayer?.style?.overlay;
|
||||
if (overlay?.enabled) {
|
||||
this._drawOverlay(ctx, overlay, width, height);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Background image load failed:', e.message);
|
||||
// Usa fallback
|
||||
if (bgLayer?.fallback) {
|
||||
this._drawFallback(ctx, bgLayer.fallback, width, height);
|
||||
}
|
||||
}
|
||||
} else if (bgLayer?.fallback) {
|
||||
this._drawFallback(ctx, bgLayer.fallback, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disegna overlay gradient
|
||||
*/
|
||||
_drawOverlay(ctx, overlay, width, height) {
|
||||
if (overlay.type === 'solid') {
|
||||
ctx.fillStyle = overlay.color || 'rgba(0,0,0,0.5)';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
return;
|
||||
}
|
||||
|
||||
// Gradient
|
||||
let gradient;
|
||||
const dir = overlay.direction || 'to-bottom';
|
||||
|
||||
if (dir === 'to-bottom') {
|
||||
gradient = ctx.createLinearGradient(0, 0, 0, height);
|
||||
} else if (dir === 'to-top') {
|
||||
gradient = ctx.createLinearGradient(0, height, 0, 0);
|
||||
} else if (dir === 'to-right') {
|
||||
gradient = ctx.createLinearGradient(0, 0, width, 0);
|
||||
} else if (dir === 'to-left') {
|
||||
gradient = ctx.createLinearGradient(width, 0, 0, 0);
|
||||
} else if (dir === 'to-bottom-right') {
|
||||
gradient = ctx.createLinearGradient(0, 0, width, height);
|
||||
} else {
|
||||
gradient = ctx.createLinearGradient(0, 0, 0, height);
|
||||
}
|
||||
|
||||
if (overlay.stops) {
|
||||
overlay.stops.forEach(stop => {
|
||||
gradient.addColorStop(stop.position, stop.color);
|
||||
});
|
||||
} else {
|
||||
gradient.addColorStop(0, 'rgba(0,0,0,0)');
|
||||
gradient.addColorStop(1, 'rgba(0,0,0,0.7)');
|
||||
}
|
||||
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disegna fallback
|
||||
*/
|
||||
_drawFallback(ctx, fallback, width, height) {
|
||||
if (fallback.type === 'solid') {
|
||||
ctx.fillStyle = fallback.color || '#333333';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
} else if (fallback.type === 'gradient' && fallback.colors) {
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, height);
|
||||
fallback.colors.forEach((color, i) => {
|
||||
gradient.addColorStop(i / (fallback.colors.length - 1), color);
|
||||
});
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disegna singolo layer
|
||||
*/
|
||||
async _drawLayer(ctx, layer, content, assets, canvasWidth, canvasHeight, template) {
|
||||
const pos = this._calculatePosition(layer.position, layer.anchor, canvasWidth, canvasHeight);
|
||||
|
||||
switch (layer.type) {
|
||||
case 'backgroundImage':
|
||||
// Già gestito in _drawBackground
|
||||
break;
|
||||
|
||||
case 'mainImage':
|
||||
await this._drawMainImage(ctx, assets?.mainImage, pos, layer.style);
|
||||
break;
|
||||
|
||||
case 'title':
|
||||
this._drawText(ctx, content?.title, pos, layer.style, template.palette);
|
||||
break;
|
||||
|
||||
case 'subtitle':
|
||||
this._drawText(ctx, content?.subtitle, pos, layer.style, template.palette);
|
||||
break;
|
||||
|
||||
case 'eventDate':
|
||||
const dateText = content?.eventTime
|
||||
? `${content.eventDate} • ${content.eventTime}`
|
||||
: content?.eventDate;
|
||||
this._drawText(ctx, dateText, pos, layer.style, template.palette);
|
||||
break;
|
||||
|
||||
case 'eventTime':
|
||||
this._drawText(ctx, content?.eventTime, pos, layer.style, template.palette);
|
||||
break;
|
||||
|
||||
case 'location':
|
||||
this._drawTextWithIcon(ctx, content?.location, pos, layer, template.palette);
|
||||
break;
|
||||
|
||||
case 'contacts':
|
||||
this._drawText(ctx, content?.contacts, pos, layer.style, template.palette);
|
||||
break;
|
||||
|
||||
case 'extraText':
|
||||
const extraTexts = Array.isArray(content?.extraText)
|
||||
? content.extraText.join(' • ')
|
||||
: content?.extraText;
|
||||
this._drawText(ctx, extraTexts, pos, layer.style, template.palette);
|
||||
break;
|
||||
|
||||
case 'customText':
|
||||
const customValue = content?.customFields?.get(layer.id);
|
||||
this._drawText(ctx, customValue, pos, layer.style, template.palette);
|
||||
break;
|
||||
|
||||
case 'divider':
|
||||
this._drawDivider(ctx, pos, layer.style);
|
||||
break;
|
||||
|
||||
case 'shape':
|
||||
this._drawShape(ctx, pos, layer.style);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`Layer type non gestito: ${layer.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcola posizione assoluta da coordinate relative
|
||||
*/
|
||||
_calculatePosition(position, anchor, canvasWidth, canvasHeight) {
|
||||
const relX = position.x || 0;
|
||||
const relY = position.y || 0;
|
||||
const relW = position.w || 1;
|
||||
const relH = position.h || 0.1;
|
||||
|
||||
const absW = relW * canvasWidth;
|
||||
const absH = relH * canvasHeight;
|
||||
|
||||
let absX = relX * canvasWidth;
|
||||
let absY = relY * canvasHeight;
|
||||
|
||||
// Aggiusta per anchor
|
||||
switch (anchor) {
|
||||
case 'top-center':
|
||||
absX -= absW / 2;
|
||||
break;
|
||||
case 'top-right':
|
||||
absX -= absW;
|
||||
break;
|
||||
case 'center-left':
|
||||
absY -= absH / 2;
|
||||
break;
|
||||
case 'center':
|
||||
absX -= absW / 2;
|
||||
absY -= absH / 2;
|
||||
break;
|
||||
case 'center-right':
|
||||
absX -= absW;
|
||||
absY -= absH / 2;
|
||||
break;
|
||||
case 'bottom-left':
|
||||
absY -= absH;
|
||||
break;
|
||||
case 'bottom-center':
|
||||
absX -= absW / 2;
|
||||
absY -= absH;
|
||||
break;
|
||||
case 'bottom-right':
|
||||
absX -= absW;
|
||||
absY -= absH;
|
||||
break;
|
||||
// top-left è default, nessun aggiustamento
|
||||
}
|
||||
|
||||
return { x: absX, y: absY, w: absW, h: absH };
|
||||
}
|
||||
|
||||
/**
|
||||
* Disegna main image
|
||||
*/
|
||||
async _drawMainImage(ctx, asset, pos, style = {}) {
|
||||
if (!asset?.url) return;
|
||||
|
||||
try {
|
||||
const img = await this._loadImageFromUrl(asset.url);
|
||||
|
||||
ctx.save();
|
||||
|
||||
// Border radius (clip)
|
||||
const radius = style.borderRadius || 0;
|
||||
if (radius > 0) {
|
||||
this._roundRect(ctx, pos.x, pos.y, pos.w, pos.h, radius);
|
||||
ctx.clip();
|
||||
}
|
||||
|
||||
// Shadow
|
||||
if (style.shadow?.enabled) {
|
||||
ctx.shadowColor = style.shadow.color || 'rgba(0,0,0,0.5)';
|
||||
ctx.shadowBlur = style.shadow.blur || 20;
|
||||
ctx.shadowOffsetX = style.shadow.offsetX || 0;
|
||||
ctx.shadowOffsetY = style.shadow.offsetY || 10;
|
||||
}
|
||||
|
||||
// Calcola dimensioni per object-fit
|
||||
const { sx, sy, sw, sh, dx, dy, dw, dh } = this._calculateObjectFit(
|
||||
img.width, img.height, pos.w, pos.h, style.objectFit || 'cover'
|
||||
);
|
||||
|
||||
ctx.drawImage(img, sx, sy, sw, sh, pos.x + dx, pos.y + dy, dw, dh);
|
||||
|
||||
// Border
|
||||
if (style.border?.enabled) {
|
||||
ctx.strokeStyle = style.border.color || '#ffffff';
|
||||
ctx.lineWidth = style.border.width || 2;
|
||||
if (radius > 0) {
|
||||
this._roundRect(ctx, pos.x, pos.y, pos.w, pos.h, radius);
|
||||
} else {
|
||||
ctx.strokeRect(pos.x, pos.y, pos.w, pos.h);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
} catch (e) {
|
||||
console.warn('Main image load failed:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcola object-fit
|
||||
*/
|
||||
_calculateObjectFit(imgW, imgH, boxW, boxH, fit) {
|
||||
let sx = 0, sy = 0, sw = imgW, sh = imgH;
|
||||
let dx = 0, dy = 0, dw = boxW, dh = boxH;
|
||||
|
||||
const imgRatio = imgW / imgH;
|
||||
const boxRatio = boxW / boxH;
|
||||
|
||||
if (fit === 'cover') {
|
||||
if (imgRatio > boxRatio) {
|
||||
sw = imgH * boxRatio;
|
||||
sx = (imgW - sw) / 2;
|
||||
} else {
|
||||
sh = imgW / boxRatio;
|
||||
sy = (imgH - sh) / 2;
|
||||
}
|
||||
} else if (fit === 'contain') {
|
||||
if (imgRatio > boxRatio) {
|
||||
dh = boxW / imgRatio;
|
||||
dy = (boxH - dh) / 2;
|
||||
} else {
|
||||
dw = boxH * imgRatio;
|
||||
dx = (boxW - dw) / 2;
|
||||
}
|
||||
}
|
||||
// 'fill' usa valori default
|
||||
|
||||
return { sx, sy, sw, sh, dx, dy, dw, dh };
|
||||
}
|
||||
|
||||
/**
|
||||
* Disegna testo
|
||||
*/
|
||||
_drawText(ctx, text, pos, style = {}, palette = {}) {
|
||||
if (!text) return;
|
||||
|
||||
ctx.save();
|
||||
|
||||
// Font
|
||||
const fontWeight = style.fontWeight || 400;
|
||||
const fontSize = style.fontSize || 48;
|
||||
const fontFamily = style.fontFamily || 'Open Sans';
|
||||
ctx.font = `${fontWeight} ${fontSize}px "${fontFamily}"`;
|
||||
|
||||
// Colore
|
||||
ctx.fillStyle = style.color || palette.text || '#ffffff';
|
||||
|
||||
// Allineamento
|
||||
const align = style.textAlign || 'center';
|
||||
ctx.textAlign = align;
|
||||
ctx.textBaseline = 'middle';
|
||||
|
||||
// Transform
|
||||
let displayText = text;
|
||||
if (style.textTransform === 'uppercase') {
|
||||
displayText = text.toUpperCase();
|
||||
} else if (style.textTransform === 'lowercase') {
|
||||
displayText = text.toLowerCase();
|
||||
} else if (style.textTransform === 'capitalize') {
|
||||
displayText = text.replace(/\b\w/g, c => c.toUpperCase());
|
||||
}
|
||||
|
||||
// Calcola X in base ad allineamento
|
||||
let textX;
|
||||
if (align === 'center') {
|
||||
textX = pos.x + pos.w / 2;
|
||||
} else if (align === 'right') {
|
||||
textX = pos.x + pos.w;
|
||||
} else {
|
||||
textX = pos.x;
|
||||
}
|
||||
|
||||
const textY = pos.y + pos.h / 2;
|
||||
|
||||
// Letter spacing (manuale)
|
||||
if (style.letterSpacing && style.letterSpacing > 0) {
|
||||
this._drawTextWithSpacing(ctx, displayText, textX, textY, style, pos);
|
||||
} else {
|
||||
// Shadow
|
||||
if (style.shadow?.enabled) {
|
||||
ctx.shadowColor = style.shadow.color || 'rgba(0,0,0,0.8)';
|
||||
ctx.shadowBlur = style.shadow.blur || 10;
|
||||
ctx.shadowOffsetX = style.shadow.offsetX || 2;
|
||||
ctx.shadowOffsetY = style.shadow.offsetY || 2;
|
||||
}
|
||||
|
||||
// Stroke
|
||||
if (style.stroke?.enabled) {
|
||||
ctx.strokeStyle = style.stroke.color || 'rgba(0,0,0,0.5)';
|
||||
ctx.lineWidth = style.stroke.width || 2;
|
||||
ctx.strokeText(displayText, textX, textY);
|
||||
}
|
||||
|
||||
// Fill
|
||||
ctx.fillText(displayText, textX, textY);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disegna testo con letter-spacing
|
||||
*/
|
||||
_drawTextWithSpacing(ctx, text, x, y, style, pos) {
|
||||
const spacing = style.letterSpacing || 0;
|
||||
const chars = text.split('');
|
||||
|
||||
// Calcola larghezza totale
|
||||
let totalWidth = 0;
|
||||
chars.forEach(char => {
|
||||
totalWidth += ctx.measureText(char).width + spacing;
|
||||
});
|
||||
totalWidth -= spacing; // Rimuovi ultimo spacing
|
||||
|
||||
// Calcola startX in base ad allineamento
|
||||
let startX;
|
||||
if (style.textAlign === 'center') {
|
||||
startX = x - totalWidth / 2;
|
||||
} else if (style.textAlign === 'right') {
|
||||
startX = x - totalWidth;
|
||||
} else {
|
||||
startX = x;
|
||||
}
|
||||
|
||||
// Shadow
|
||||
if (style.shadow?.enabled) {
|
||||
ctx.shadowColor = style.shadow.color || 'rgba(0,0,0,0.8)';
|
||||
ctx.shadowBlur = style.shadow.blur || 10;
|
||||
ctx.shadowOffsetX = style.shadow.offsetX || 2;
|
||||
ctx.shadowOffsetY = style.shadow.offsetY || 2;
|
||||
}
|
||||
|
||||
// Disegna ogni carattere
|
||||
ctx.textAlign = 'left';
|
||||
let currentX = startX;
|
||||
|
||||
chars.forEach(char => {
|
||||
if (style.stroke?.enabled) {
|
||||
ctx.strokeStyle = style.stroke.color || 'rgba(0,0,0,0.5)';
|
||||
ctx.lineWidth = style.stroke.width || 2;
|
||||
ctx.strokeText(char, currentX, y);
|
||||
}
|
||||
ctx.fillText(char, currentX, y);
|
||||
currentX += ctx.measureText(char).width + spacing;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disegna testo con icona
|
||||
*/
|
||||
_drawTextWithIcon(ctx, text, pos, layer, palette) {
|
||||
if (!text) return;
|
||||
|
||||
const icon = layer.icon;
|
||||
const style = layer.style || {};
|
||||
|
||||
// Se icona abilitata, disegna simbolo prima del testo
|
||||
if (icon?.enabled) {
|
||||
ctx.save();
|
||||
|
||||
const iconSize = icon.size || 24;
|
||||
const iconColor = icon.color || palette?.accent || '#e74c3c';
|
||||
|
||||
// Disegna simbolo location semplificato
|
||||
ctx.fillStyle = iconColor;
|
||||
ctx.font = `${iconSize}px Arial`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
|
||||
const iconChar = '📍'; // Emoji o usa font icon
|
||||
const textWithIcon = `${iconChar} ${text}`;
|
||||
|
||||
// Ora disegna testo normale con icona
|
||||
this._drawText(ctx, textWithIcon, pos, style, palette);
|
||||
|
||||
ctx.restore();
|
||||
} else {
|
||||
this._drawText(ctx, text, pos, style, palette);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disegna loghi
|
||||
*/
|
||||
async _drawLogos(ctx, logoSlots, logos, canvasWidth, canvasHeight) {
|
||||
const slots = logoSlots.slots || [];
|
||||
const maxCount = Math.min(logos.length, logoSlots.maxCount || 3, slots.length);
|
||||
|
||||
for (let i = 0; i < maxCount; i++) {
|
||||
const logo = logos[i];
|
||||
const slot = slots[i];
|
||||
|
||||
if (!logo?.url || !slot) continue;
|
||||
|
||||
try {
|
||||
const img = await this._loadImageFromUrl(logo.url);
|
||||
const pos = this._calculatePosition(slot.position, slot.anchor, canvasWidth, canvasHeight);
|
||||
|
||||
ctx.save();
|
||||
|
||||
// Opacity
|
||||
ctx.globalAlpha = slot.style?.opacity ?? 0.9;
|
||||
|
||||
// Object fit contain per loghi
|
||||
const { sx, sy, sw, sh, dx, dy, dw, dh } = this._calculateObjectFit(
|
||||
img.width, img.height, pos.w, pos.h, 'contain'
|
||||
);
|
||||
|
||||
ctx.drawImage(img, sx, sy, sw, sh, pos.x + dx, pos.y + dy, dw, dh);
|
||||
|
||||
ctx.restore();
|
||||
} catch (e) {
|
||||
console.warn(`Logo ${i} load failed:`, e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disegna divider
|
||||
*/
|
||||
_drawDivider(ctx, pos, style = {}) {
|
||||
ctx.save();
|
||||
|
||||
ctx.strokeStyle = style.color || '#ffffff';
|
||||
ctx.lineWidth = style.width || 2;
|
||||
ctx.globalAlpha = style.opacity || 0.5;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(pos.x, pos.y + pos.h / 2);
|
||||
ctx.lineTo(pos.x + pos.w, pos.y + pos.h / 2);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disegna shape
|
||||
*/
|
||||
_drawShape(ctx, pos, style = {}) {
|
||||
ctx.save();
|
||||
|
||||
ctx.fillStyle = style.fill || 'rgba(255,255,255,0.1)';
|
||||
ctx.strokeStyle = style.stroke || 'transparent';
|
||||
ctx.lineWidth = style.strokeWidth || 0;
|
||||
ctx.globalAlpha = style.opacity || 1;
|
||||
|
||||
const shape = style.shape || 'rectangle';
|
||||
const radius = style.borderRadius || 0;
|
||||
|
||||
if (shape === 'rectangle') {
|
||||
if (radius > 0) {
|
||||
this._roundRect(ctx, pos.x, pos.y, pos.w, pos.h, radius);
|
||||
ctx.fill();
|
||||
if (style.strokeWidth) ctx.stroke();
|
||||
} else {
|
||||
ctx.fillRect(pos.x, pos.y, pos.w, pos.h);
|
||||
if (style.strokeWidth) ctx.strokeRect(pos.x, pos.y, pos.w, pos.h);
|
||||
}
|
||||
} else if (shape === 'circle' || shape === 'ellipse') {
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(
|
||||
pos.x + pos.w / 2,
|
||||
pos.y + pos.h / 2,
|
||||
pos.w / 2,
|
||||
pos.h / 2,
|
||||
0, 0, Math.PI * 2
|
||||
);
|
||||
ctx.fill();
|
||||
if (style.strokeWidth) ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility: rounded rectangle
|
||||
*/
|
||||
_roundRect(ctx, x, y, w, h, radius) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + radius, y);
|
||||
ctx.lineTo(x + w - radius, y);
|
||||
ctx.quadraticCurveTo(x + w, y, x + w, y + radius);
|
||||
ctx.lineTo(x + w, y + h - radius);
|
||||
ctx.quadraticCurveTo(x + w, y + h, x + w - radius, y + h);
|
||||
ctx.lineTo(x + radius, y + h);
|
||||
ctx.quadraticCurveTo(x, y + h, x, y + h - radius);
|
||||
ctx.lineTo(x, y + radius);
|
||||
ctx.quadraticCurveTo(x, y, x + radius, y);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility: carica immagine da URL o path locale
|
||||
*/
|
||||
async _loadImageFromUrl(url) {
|
||||
if (!url) throw new Error('URL mancante');
|
||||
|
||||
// Base64
|
||||
if (url.startsWith('data:')) {
|
||||
return loadImage(url);
|
||||
}
|
||||
|
||||
// Path locale
|
||||
if (url.startsWith('/uploads') || url.startsWith('./uploads')) {
|
||||
const localPath = url.startsWith('/')
|
||||
? path.join(process.cwd(), url)
|
||||
: url;
|
||||
return loadImage(localPath);
|
||||
}
|
||||
|
||||
// URL remoto
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
const fetch = require('node-fetch');
|
||||
const response = await fetch(url);
|
||||
const buffer = await response.buffer();
|
||||
return loadImage(buffer);
|
||||
}
|
||||
|
||||
// Assume path locale
|
||||
return loadImage(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge layer con override
|
||||
*/
|
||||
_mergeLayerOverride(layer, override) {
|
||||
if (!override || Object.keys(override).length === 0) {
|
||||
return layer;
|
||||
}
|
||||
|
||||
return {
|
||||
...layer,
|
||||
position: override.position ? { ...layer.position, ...override.position } : layer.position,
|
||||
visible: override.visible !== undefined ? override.visible : layer.visible,
|
||||
style: override.style ? { ...layer.style, ...override.style } : layer.style
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick render (semplificato per quick-generate)
|
||||
*/
|
||||
async quickRender(options) {
|
||||
const {
|
||||
backgroundUrl,
|
||||
content,
|
||||
outputPath,
|
||||
width = 1080,
|
||||
height = 1920
|
||||
} = options;
|
||||
|
||||
const canvas = createCanvas(width, height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Background
|
||||
if (backgroundUrl) {
|
||||
try {
|
||||
const img = await this._loadImageFromUrl(backgroundUrl);
|
||||
const imgRatio = img.width / img.height;
|
||||
const canvasRatio = width / height;
|
||||
|
||||
let dw, dh, dx, dy;
|
||||
if (imgRatio > canvasRatio) {
|
||||
dh = height;
|
||||
dw = height * imgRatio;
|
||||
dx = (width - dw) / 2;
|
||||
dy = 0;
|
||||
} else {
|
||||
dw = width;
|
||||
dh = width / imgRatio;
|
||||
dx = 0;
|
||||
dy = (height - dh) / 2;
|
||||
}
|
||||
|
||||
ctx.drawImage(img, dx, dy, dw, dh);
|
||||
} catch (e) {
|
||||
ctx.fillStyle = '#1a1a2e';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
// Overlay gradient
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, height);
|
||||
gradient.addColorStop(0, 'rgba(0,0,0,0)');
|
||||
gradient.addColorStop(0.4, 'rgba(0,0,0,0.2)');
|
||||
gradient.addColorStop(0.7, 'rgba(0,0,0,0.6)');
|
||||
gradient.addColorStop(1, 'rgba(0,0,0,0.85)');
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
// Title
|
||||
if (content.title) {
|
||||
ctx.save();
|
||||
ctx.font = 'bold 72px "Montserrat", sans-serif';
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.shadowColor = 'rgba(0,0,0,0.8)';
|
||||
ctx.shadowBlur = 15;
|
||||
ctx.shadowOffsetY = 4;
|
||||
ctx.fillText(content.title.toUpperCase(), width / 2, height * 0.52);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Subtitle
|
||||
if (content.subtitle) {
|
||||
ctx.save();
|
||||
ctx.font = '400 32px "Open Sans", sans-serif';
|
||||
ctx.fillStyle = '#f0f0f0';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.shadowColor = 'rgba(0,0,0,0.6)';
|
||||
ctx.shadowBlur = 8;
|
||||
ctx.fillText(content.subtitle, width / 2, height * 0.60);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Date
|
||||
if (content.eventDate) {
|
||||
ctx.save();
|
||||
ctx.font = '400 48px "Bebas Neue", sans-serif';
|
||||
ctx.fillStyle = '#ffd700';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.shadowColor = 'rgba(0,0,0,0.8)';
|
||||
ctx.shadowBlur = 10;
|
||||
const dateText = content.eventTime
|
||||
? `${content.eventDate} • ORE ${content.eventTime}`
|
||||
: content.eventDate;
|
||||
ctx.fillText(dateText.toUpperCase(), width / 2, height * 0.70);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Location
|
||||
if (content.location) {
|
||||
ctx.save();
|
||||
ctx.font = '600 28px "Open Sans", sans-serif';
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(`📍 ${content.location}`, width / 2, height * 0.78);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Contacts
|
||||
if (content.contacts) {
|
||||
ctx.save();
|
||||
ctx.font = '400 22px "Open Sans", sans-serif';
|
||||
ctx.fillStyle = '#cccccc';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(content.contacts, width / 2, height * 0.86);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Salva
|
||||
const buffer = canvas.toBuffer('image/png');
|
||||
await fs.writeFile(outputPath, buffer);
|
||||
|
||||
return {
|
||||
path: outputPath,
|
||||
size: buffer.length,
|
||||
dimensions: { width, height }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new PosterRenderer();
|
||||
@@ -1048,6 +1048,7 @@ const MyTelegramBot = {
|
||||
token_circuito_da_ammettere: token,
|
||||
nomeTerritorio: mycircuit.name,
|
||||
myusername: userDest,
|
||||
circuitId: mycircuit._id,
|
||||
};
|
||||
// if (usersmanagers) {
|
||||
// for (const recadminCirc of usersmanagers) {
|
||||
|
||||
2353
src/templates/template-seeds.js
Normal file
2353
src/templates/template-seeds.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1198,6 +1198,7 @@ module.exports = {
|
||||
let paramsObj = {
|
||||
usernameDest,
|
||||
circuitnameDest: circuitname,
|
||||
circuitId: myreccircuit ? myreccircuit._id : '',
|
||||
path,
|
||||
username_action: username_action,
|
||||
singleadmin_username: usernameDest,
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
@@ -450,6 +450,7 @@ module.exports = {
|
||||
usernameInvitante: paramsObj.extrarec?.username_admin_abilitante,
|
||||
nomeTerritorio: paramsObj.circuitnameDest,
|
||||
link_group: paramsObj.extrarec?.link_group,
|
||||
circuitId: paramsObj.circuitId,
|
||||
};
|
||||
await sendemail.sendEmail_Utente_Abilitato_Circuito_FidoConcesso(usertosend.lang, usertosend.email, usertosend, params.idapp, dati);
|
||||
}
|
||||
|
||||
@@ -1283,7 +1283,6 @@ module.exports = {
|
||||
DASHBOARD: 140,
|
||||
DASHGROUP: 145,
|
||||
MOVEMENTS: 148,
|
||||
CSENDRISTO: 150,
|
||||
STATUSREG: 160,
|
||||
CHECKIFISLOGGED: 170,
|
||||
INFO_VERSION: 180,
|
||||
|
||||
BIN
uploads/ai-generated/ai_1765716970209_55yyoiuf2.jpg
Normal file
BIN
uploads/ai-generated/ai_1765716970209_55yyoiuf2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
Reference in New Issue
Block a user