Compare commits
10 Commits
b8dcd7f5e0
...
feat/recur
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb40743694 | ||
|
|
85141df8a4 | ||
|
|
cb965eaa27 | ||
|
|
b78e3ce544 | ||
|
|
2e7801b4ba | ||
|
|
afeedf27a5 | ||
|
|
80c929436c | ||
|
|
9a0cdec7bd | ||
|
|
3d87c336de | ||
|
|
037ff6f7f9 |
@@ -43,4 +43,5 @@ MIAB_ADMIN_EMAIL=admin@lamiaposta.org
|
||||
MIAB_ADMIN_PASSWORD=passpao1pabox@1A
|
||||
DS_API_KEY="sk-222e3addb3d8455d8b0516d93906eec7"
|
||||
SERVER_A_URL="http://51.77.156.69:3000"
|
||||
API_KEY_MSSQL="m68yADSr123MIVIDA@154$DSAGVOK"
|
||||
API_KEY_MSSQL="m68yADSr123MIVIDA@154$DSAGVOK"
|
||||
ORS_API_KEY="eyJvcmciOiI1YjNjZTM1OTc4NTExMTAwMDFjZjYyNDgiLCJpZCI6IjNjNTllZmY1ZTM1ZDQ5ODI5NThhOTIzYTQ5MDkxOWIwIiwiaCI6Im11cm11cjY0In0="
|
||||
@@ -39,3 +39,4 @@ 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"
|
||||
ORS_API_KEY="eyJvcmciOiI1YjNjZTM1OTc4NTExMTAwMDFjZjYyNDgiLCJpZCI6IjNjNTllZmY1ZTM1ZDQ5ODI5NThhOTIzYTQ5MDkxOWIwIiwiaCI6Im11cm11cjY0In0="
|
||||
@@ -39,3 +39,10 @@ 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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"
|
||||
ORS_API_KEY="eyJvcmciOiI1YjNjZTM1OTc4NTExMTAwMDFjZjYyNDgiLCJpZCI6IjNjNTllZmY1ZTM1ZDQ5ODI5NThhOTIzYTQ5MDkxOWIwIiwiaCI6Im11cm11cjY0In0="
|
||||
@@ -41,4 +41,5 @@ MIAB_HOST=box.lamiaposta.org
|
||||
MIAB_ADMIN_EMAIL=admin@lamiaposta.org
|
||||
MIAB_ADMIN_PASSWORD=passpao1pabox@1A
|
||||
SERVER_A_URL="http://51.77.156.69:3000"
|
||||
API_KEY_MSSQL="m68yADSr123MIVIDA@154$DSAGVOK"
|
||||
API_KEY_MSSQL="m68yADSr123MIVIDA@154$DSAGVOK"
|
||||
ORS_API_KEY="eyJvcmciOiI1YjNjZTM1OTc4NTExMTAwMDFjZjYyNDgiLCJpZCI6IjNjNTllZmY1ZTM1ZDQ5ODI5NThhOTIzYTQ5MDkxOWIwIiwiaCI6Im11cm11cjY0In0="
|
||||
@@ -38,4 +38,5 @@ SCRIPTS_DIR=admin_scripts
|
||||
CLOUDFLARE_TOKENS=[{"label":"Paolo.arena77@gmail.com","value":"M9EM309v8WFquJKpYgZCw-TViM2wX6vB3wlK6GD0"},{"label":"gruppomacro.com","value":"bqmzGShoX7WqOBzkXocoECyBkPq3GfqcM5t6VFd8"}]
|
||||
MIAB_HOST=box.lamiaposta.org
|
||||
MIAB_ADMIN_EMAIL=admin@lamiaposta.org
|
||||
MIAB_ADMIN_PASSWORD=passpao1pabox@1A
|
||||
MIAB_ADMIN_PASSWORD=passpao1pabox@1A
|
||||
ORS_API_KEY="eyJvcmciOiI1YjNjZTM1OTc4NTExMTAwMDFjZjYyNDgiLCJpZCI6IjNjNTllZmY1ZTM1ZDQ5ODI5NThhOTIzYTQ5MDkxOWIwIiwiaCI6Im11cm11cjY0In0="
|
||||
@@ -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}`
|
||||
87
logtrans.txt
87
logtrans.txt
@@ -519,4 +519,89 @@ Gio 04/12 ORE 18:55: [<b>Circuito RIS Bologna</b>]: Inviate Monete da SurTest a
|
||||
|
||||
Saldi:
|
||||
SurTest: 0.00 RIS]
|
||||
ElenaEspx: 38.05 RIS]
|
||||
ElenaEspx: 38.05 RIS]
|
||||
Mar 09/12 ORE 21:36: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 11 RIS [causale: ssss]
|
||||
|
||||
Saldi:
|
||||
surya1977: 63.00 RIS]
|
||||
amandadi: 12.00 RIS]
|
||||
Mer 10/12 ORE 16:18: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 12 RIS [causale: Ciaoo]
|
||||
|
||||
Saldi:
|
||||
surya1977: 51.00 RIS]
|
||||
amandadi: 24.00 RIS]
|
||||
Mer 10/12 ORE 17:02: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 52 RIS [causale: Ancora test]
|
||||
|
||||
Saldi:
|
||||
surya1977: -1.00 RIS]
|
||||
amandadi: 76.00 RIS]
|
||||
Gio 18/12 ORE 15:41: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 22 RIS [causale: Prova]
|
||||
|
||||
Saldi:
|
||||
surya1977: -23.00 RIS]
|
||||
amandadi: 98.00 RIS]
|
||||
Gio 18/12 ORE 15:59: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 2 RIS [causale: aaaa]
|
||||
|
||||
Saldi:
|
||||
surya1977: -3.00 RIS]
|
||||
amandadi: 78.00 RIS]
|
||||
Gio 18/12 ORE 16:00: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 2 RIS [causale: aaaa]
|
||||
|
||||
Saldi:
|
||||
surya1977: -5.00 RIS]
|
||||
amandadi: 80.00 RIS]
|
||||
Gio 18/12 ORE 16:07: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 1 RIS [causale: aaa]
|
||||
|
||||
Saldi:
|
||||
surya1977: -6.00 RIS]
|
||||
amandadi: 81.00 RIS]
|
||||
Gio 18/12 ORE 16:07: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 2 RIS [causale: asdasdas]
|
||||
|
||||
Saldi:
|
||||
surya1977: -8.00 RIS]
|
||||
amandadi: 83.00 RIS]
|
||||
Gio 18/12 ORE 16:11: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 3 RIS [causale: Provaa]
|
||||
|
||||
Saldi:
|
||||
surya1977: -11.00 RIS]
|
||||
amandadi: 86.00 RIS]
|
||||
Gio 18/12 ORE 16:35: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 2 RIS [causale: aaaa]
|
||||
|
||||
Saldi:
|
||||
surya1977: -13.00 RIS]
|
||||
amandadi: 88.00 RIS]
|
||||
Gio 18/12 ORE 16:49: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 5 RIS [causale: Prova da 3. Eccolo 2!]
|
||||
|
||||
Saldi:
|
||||
surya1977: -18.00 RIS]
|
||||
amandadi: 96.00 RIS]
|
||||
Gio 18/12 ORE 16:50: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 4 RIS [causale: dasdasdasd]
|
||||
|
||||
Saldi:
|
||||
surya1977: -22.00 RIS]
|
||||
amandadi: 100.00 RIS]
|
||||
Gio 18/12 ORE 16:50: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 3 RIS [causale: -25]
|
||||
|
||||
Saldi:
|
||||
surya1977: -25.00 RIS]
|
||||
amandadi: 103.00 RIS]
|
||||
Gio 18/12 ORE 16:52: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 1 RIS [causale: asdasdsadas]
|
||||
|
||||
Saldi:
|
||||
surya1977: -27.50 RIS]
|
||||
amandadi: 105.50 RIS]
|
||||
Gio 18/12 ORE 18:30: [<b>Circuito RIS Bologna</b>]: Inviate Monete da surya1977 a ElenaEspx 1 RIS [causale: prova 1]
|
||||
|
||||
Saldi:
|
||||
surya1977: 34.90 RIS]
|
||||
ElenaEspx: 39.05 RIS]
|
||||
Gio 18/12 ORE 18:42: [<b>Circuito RIS Pordenone</b>]: Inviate Monete da surya1977 a GruppoYurta 3 RIS [causale: ECCOLO]
|
||||
|
||||
Saldi:
|
||||
surya1977: -3.00 RIS]
|
||||
GruppoYurta: -1.00 RIS]
|
||||
Gio 18/12 ORE 18:53: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 50 RIS [causale: asdasdasda]
|
||||
|
||||
Saldi:
|
||||
surya1977: -77.50 RIS]
|
||||
amandadi: 155.50 RIS]
|
||||
@@ -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';
|
||||
|
||||
@@ -55,7 +55,11 @@ class UserController {
|
||||
}
|
||||
|
||||
// Send response with tokens
|
||||
res.header('x-auth', result.token).header('x-refrtok', result.refreshToken).header('x-browser-random', result.browser_random).send(result.user);
|
||||
res
|
||||
.header('x-auth', result.token)
|
||||
.header('x-refrtok', result.refreshToken)
|
||||
.header('x-browser-random', result.browser_random)
|
||||
.send(result.user);
|
||||
} catch (error) {
|
||||
console.error('Error in registration:', error.message);
|
||||
res.status(400).send({
|
||||
@@ -103,11 +107,15 @@ class UserController {
|
||||
}
|
||||
|
||||
// Send response with tokens
|
||||
res.header('x-auth', result.token).header('x-refrtok', result.refreshToken).header('x-browser-random', result.browser_random).send({
|
||||
usertosend: result.user,
|
||||
code: server_constants.RIS_CODE_OK,
|
||||
subsExistonDb: result.subsExistonDb,
|
||||
});
|
||||
res
|
||||
.header('x-auth', result.token)
|
||||
.header('x-refrtok', result.refreshToken)
|
||||
.header('x-browser-random', result.browser_random)
|
||||
.send({
|
||||
usertosend: result.user,
|
||||
code: server_constants.RIS_CODE_OK,
|
||||
subsExistonDb: result.subsExistonDb,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in login:', error.message);
|
||||
res.status(400).send({
|
||||
@@ -487,6 +495,7 @@ class UserController {
|
||||
const { User } = require('../models/user');
|
||||
return User.isCollaboratore(user.perm);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = UserController;
|
||||
|
||||
588
src/controllers/VideoController.js
Normal file
588
src/controllers/VideoController.js
Normal file
@@ -0,0 +1,588 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
class VideoController {
|
||||
constructor(baseUploadPath = 'uploads/videos') {
|
||||
this.basePath = path.resolve(baseUploadPath);
|
||||
this._ensureDirectory(this.basePath);
|
||||
}
|
||||
|
||||
// ============ PRIVATE METHODS ============
|
||||
|
||||
_ensureDirectory(dir) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
_isVideoFile(filename) {
|
||||
return /\.(mp4|webm|ogg|mov|avi|mkv)$/i.test(filename);
|
||||
}
|
||||
|
||||
_getFileInfo(filePath, relativePath = '') {
|
||||
const stat = fs.statSync(filePath);
|
||||
const filename = path.basename(filePath);
|
||||
|
||||
return {
|
||||
id: uuidv4(),
|
||||
filename,
|
||||
folder: relativePath,
|
||||
path: `/videos/${relativePath ? relativePath + '/' : ''}${filename}`,
|
||||
size: stat.size,
|
||||
createdAt: stat.birthtime.toISOString(),
|
||||
modifiedAt: stat.mtime.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
_scanFolders(dir, relativePath = '') {
|
||||
const folders = [];
|
||||
|
||||
if (!fs.existsSync(dir)) return folders;
|
||||
|
||||
const items = fs.readdirSync(dir);
|
||||
|
||||
items.forEach((item) => {
|
||||
const fullPath = path.join(dir, item);
|
||||
const relPath = relativePath ? `${relativePath}/${item}` : item;
|
||||
|
||||
if (fs.statSync(fullPath).isDirectory()) {
|
||||
folders.push({
|
||||
name: item,
|
||||
path: relPath,
|
||||
level: relPath.split('/').length,
|
||||
});
|
||||
// Ricorsione per sottocartelle
|
||||
folders.push(...this._scanFolders(fullPath, relPath));
|
||||
}
|
||||
});
|
||||
|
||||
return folders;
|
||||
}
|
||||
|
||||
// ============ FOLDER METHODS ============
|
||||
|
||||
/**
|
||||
* Ottiene tutte le cartelle
|
||||
*/
|
||||
getFolders = async (req, res) => {
|
||||
try {
|
||||
const folders = this._scanFolders(this.basePath);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { folders },
|
||||
message: 'Cartelle recuperate con successo',
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Crea una nuova cartella
|
||||
*/
|
||||
createFolder = async (req, res) => {
|
||||
try {
|
||||
const { folderName, parentPath = '' } = req.body;
|
||||
|
||||
if (!folderName || !folderName.trim()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Nome cartella richiesto',
|
||||
});
|
||||
}
|
||||
|
||||
// Sanitizza il nome cartella
|
||||
const sanitizedName = folderName.replace(/[<>:"/\\|?*]/g, '_').trim();
|
||||
|
||||
const basePath = parentPath ? path.join(this.basePath, parentPath) : this.basePath;
|
||||
|
||||
const newFolderPath = path.join(basePath, sanitizedName);
|
||||
|
||||
if (fs.existsSync(newFolderPath)) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
error: 'La cartella esiste già',
|
||||
});
|
||||
}
|
||||
|
||||
fs.mkdirSync(newFolderPath, { recursive: true });
|
||||
|
||||
const folderData = {
|
||||
name: sanitizedName,
|
||||
path: parentPath ? `${parentPath}/${sanitizedName}` : sanitizedName,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: { folder: folderData },
|
||||
message: 'Cartella creata con successo',
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Rinomina una cartella
|
||||
*/
|
||||
renameFolder = async (req, res) => {
|
||||
try {
|
||||
const { folderPath } = req.params;
|
||||
const { newName } = req.body;
|
||||
|
||||
if (!newName || !newName.trim()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Nuovo nome richiesto',
|
||||
});
|
||||
}
|
||||
|
||||
const sanitizedName = newName.replace(/[<>:"/\\|?*]/g, '_').trim();
|
||||
const oldPath = path.join(this.basePath, folderPath);
|
||||
const parentDir = path.dirname(oldPath);
|
||||
const newPath = path.join(parentDir, sanitizedName);
|
||||
|
||||
if (!fs.existsSync(oldPath)) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Cartella non trovata',
|
||||
});
|
||||
}
|
||||
|
||||
if (fs.existsSync(newPath)) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
error: 'Una cartella con questo nome esiste già',
|
||||
});
|
||||
}
|
||||
|
||||
fs.renameSync(oldPath, newPath);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Cartella rinominata con successo',
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Elimina una cartella
|
||||
*/
|
||||
deleteFolder = async (req, res) => {
|
||||
try {
|
||||
const { folderPath } = req.params;
|
||||
const fullPath = path.join(this.basePath, folderPath);
|
||||
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Cartella non trovata',
|
||||
});
|
||||
}
|
||||
|
||||
// Verifica che sia una directory
|
||||
if (!fs.statSync(fullPath).isDirectory()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Il percorso non è una cartella',
|
||||
});
|
||||
}
|
||||
|
||||
fs.rmSync(fullPath, { recursive: true, force: true });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Cartella eliminata con successo',
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ============ VIDEO METHODS ============
|
||||
|
||||
/**
|
||||
* Ottiene i video di una cartella
|
||||
*/
|
||||
getVideos = async (req, res) => {
|
||||
try {
|
||||
const folder = req.query.folder || '';
|
||||
const targetPath = folder ? path.join(this.basePath, folder) : this.basePath;
|
||||
|
||||
if (!fs.existsSync(targetPath)) {
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
videos: [],
|
||||
folders: [],
|
||||
currentPath: folder,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const items = fs.readdirSync(targetPath);
|
||||
const videos = [];
|
||||
const subfolders = [];
|
||||
|
||||
items.forEach((item) => {
|
||||
const itemPath = path.join(targetPath, item);
|
||||
const stat = fs.statSync(itemPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
subfolders.push({
|
||||
name: item,
|
||||
path: folder ? `${folder}/${item}` : item,
|
||||
});
|
||||
} else if (stat.isFile() && this._isVideoFile(item)) {
|
||||
videos.push(this._getFileInfo(itemPath, folder));
|
||||
}
|
||||
});
|
||||
|
||||
// Ordina per data di creazione (più recenti prima)
|
||||
videos.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
videos,
|
||||
folders: subfolders,
|
||||
currentPath: folder,
|
||||
totalVideos: videos.length,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload singolo video
|
||||
*/
|
||||
uploadVideo = async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Nessun file caricato',
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Legge da query parameter
|
||||
const folder = req.query.folder || 'default';
|
||||
|
||||
const videoInfo = {
|
||||
id: uuidv4(),
|
||||
originalName: req.file.originalname,
|
||||
filename: req.file.filename,
|
||||
folder: folder,
|
||||
path: `/videos/${folder}/${req.file.filename}`,
|
||||
size: req.file.size,
|
||||
mimetype: req.file.mimetype,
|
||||
uploadedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: { video: videoInfo },
|
||||
message: 'Video caricato con successo',
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload multiplo video
|
||||
*/
|
||||
uploadVideos = async (req, res) => {
|
||||
try {
|
||||
if (!req.files || req.files.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Nessun file caricato',
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Legge da query parameter
|
||||
const folder = req.query.folder || 'default';
|
||||
|
||||
const videos = req.files.map((file) => ({
|
||||
id: uuidv4(),
|
||||
originalName: file.originalname,
|
||||
filename: file.filename,
|
||||
folder: folder,
|
||||
path: `/videos/${folder}/${file.filename}`,
|
||||
size: file.size,
|
||||
mimetype: file.mimetype,
|
||||
uploadedAt: new Date().toISOString(),
|
||||
}));
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: { videos },
|
||||
message: `${videos.length} video caricati con successo`,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Ottiene info di un singolo video
|
||||
*/
|
||||
getVideo = async (req, res) => {
|
||||
try {
|
||||
const { folder, filename } = req.params;
|
||||
const videoPath = path.join(this.basePath, folder, filename);
|
||||
|
||||
if (!fs.existsSync(videoPath)) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Video non trovato',
|
||||
});
|
||||
}
|
||||
|
||||
const videoInfo = this._getFileInfo(videoPath, folder);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { video: videoInfo },
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Rinomina un video
|
||||
*/
|
||||
renameVideo = async (req, res) => {
|
||||
try {
|
||||
const { folder, filename } = req.params;
|
||||
const { newFilename } = req.body;
|
||||
|
||||
if (!newFilename || !newFilename.trim()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Nuovo nome file richiesto',
|
||||
});
|
||||
}
|
||||
|
||||
const oldPath = path.join(this.basePath, folder, filename);
|
||||
const newPath = path.join(this.basePath, folder, newFilename);
|
||||
|
||||
if (!fs.existsSync(oldPath)) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Video non trovato',
|
||||
});
|
||||
}
|
||||
|
||||
if (fs.existsSync(newPath)) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
error: 'Un file con questo nome esiste già',
|
||||
});
|
||||
}
|
||||
|
||||
fs.renameSync(oldPath, newPath);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Video rinominato con successo',
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sposta un video in un'altra cartella
|
||||
*/
|
||||
moveVideo = async (req, res) => {
|
||||
try {
|
||||
const { folder, filename } = req.params;
|
||||
const { destinationFolder } = req.body;
|
||||
|
||||
const sourcePath = path.join(this.basePath, folder, filename);
|
||||
const destDir = path.join(this.basePath, destinationFolder);
|
||||
const destPath = path.join(destDir, filename);
|
||||
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Video non trovato',
|
||||
});
|
||||
}
|
||||
|
||||
this._ensureDirectory(destDir);
|
||||
|
||||
if (fs.existsSync(destPath)) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
error: 'Un file con questo nome esiste già nella destinazione',
|
||||
});
|
||||
}
|
||||
|
||||
fs.renameSync(sourcePath, destPath);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Video spostato con successo',
|
||||
data: {
|
||||
newPath: `/videos/${destinationFolder}/${filename}`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Elimina un video
|
||||
*/
|
||||
deleteVideo = async (req, res) => {
|
||||
try {
|
||||
const { folder, filename } = req.params;
|
||||
const videoPath = path.join(this.basePath, folder, filename);
|
||||
|
||||
if (!fs.existsSync(videoPath)) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Video non trovato',
|
||||
});
|
||||
}
|
||||
|
||||
fs.unlinkSync(videoPath);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Video eliminato con successo',
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Stream video (per player)
|
||||
*/
|
||||
streamVideo = async (req, res) => {
|
||||
try {
|
||||
const { folder, filename } = req.params;
|
||||
const videoPath = path.join(this.basePath, folder, filename);
|
||||
|
||||
if (!fs.existsSync(videoPath)) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Video non trovato',
|
||||
});
|
||||
}
|
||||
|
||||
const stat = fs.statSync(videoPath);
|
||||
const fileSize = stat.size;
|
||||
const range = req.headers.range;
|
||||
|
||||
if (range) {
|
||||
const parts = range.replace(/bytes=/, '').split('-');
|
||||
const start = parseInt(parts[0], 10);
|
||||
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
|
||||
const chunkSize = end - start + 1;
|
||||
|
||||
const file = fs.createReadStream(videoPath, { start, end });
|
||||
const headers = {
|
||||
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': chunkSize,
|
||||
'Content-Type': 'video/mp4',
|
||||
};
|
||||
|
||||
res.writeHead(206, headers);
|
||||
file.pipe(res);
|
||||
} else {
|
||||
const headers = {
|
||||
'Content-Length': fileSize,
|
||||
'Content-Type': 'video/mp4',
|
||||
};
|
||||
|
||||
res.writeHead(200, headers);
|
||||
fs.createReadStream(videoPath).pipe(res);
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ============ ERROR HANDLER MIDDLEWARE ============
|
||||
|
||||
static errorHandler = (error, req, res, next) => {
|
||||
if (error.code === 'LIMIT_FILE_SIZE') {
|
||||
return res.status(413).json({
|
||||
success: false,
|
||||
error: 'File troppo grande. Dimensione massima: 500MB',
|
||||
});
|
||||
}
|
||||
|
||||
if (error.code === 'LIMIT_FILE_COUNT') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Troppi file. Massimo 10 file per upload',
|
||||
});
|
||||
}
|
||||
|
||||
if (error.message.includes('Tipo file non supportato')) {
|
||||
return res.status(415).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || 'Errore interno del server',
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = VideoController;
|
||||
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 || './upload';
|
||||
|
||||
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: `/upload/${file.filename}`,
|
||||
thumbnailPath: thumbPath,
|
||||
thumbnailUrl: `/upload/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: `/upload/${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: `/upload/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;
|
||||
789
src/controllers/chatController.js
Normal file
789
src/controllers/chatController.js
Normal file
@@ -0,0 +1,789 @@
|
||||
const Chat = require('../models/viaggi/Chat');
|
||||
const Message = require('../models/Message');
|
||||
const { User } = require('../models/user');
|
||||
|
||||
// ===== GET USER CHATS =====
|
||||
exports.getUserChats = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const idapp = req.user.idapp;
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 20;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// ✅ Trova chat dove l'utente è partecipante E non l'ha cancellata
|
||||
const chats = await Chat.find({
|
||||
idapp,
|
||||
participants: userId,
|
||||
isActive: true,
|
||||
deletedBy: { $ne: userId }, // ✅ Escludi chat cancellate
|
||||
})
|
||||
.populate('participants', 'username name surname profile')
|
||||
.populate({
|
||||
path: 'rideId',
|
||||
select: 'departure destination departureDate departureTime status',
|
||||
})
|
||||
.sort({ updatedAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.lean();
|
||||
|
||||
const enrichedChats = chats.map((chat) => {
|
||||
let unreadCount = 0;
|
||||
|
||||
if (chat.unreadCount) {
|
||||
if (chat.unreadCount instanceof Map) {
|
||||
unreadCount = chat.unreadCount.get(userId.toString()) || 0;
|
||||
} else if (typeof chat.unreadCount === 'object') {
|
||||
// Dopo .lean(), la Map diventa un oggetto plain
|
||||
unreadCount = chat.unreadCount[userId.toString()] || 0;
|
||||
}
|
||||
}
|
||||
return {
|
||||
...chat,
|
||||
unreadCount,
|
||||
};
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: enrichedChats,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
hasMore: chats.length === limit,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching chats:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nel recupero delle chat',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ===== GET OR CREATE DIRECT CHAT =====
|
||||
exports.getOrCreateDirectChat = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const { otherUserId, rideId } = req.body;
|
||||
const idapp = req.user.idapp;
|
||||
|
||||
if (!otherUserId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'otherUserId è richiesto',
|
||||
});
|
||||
}
|
||||
|
||||
// Verifica che l'altro utente esista
|
||||
const otherUser = await User.findById(otherUserId);
|
||||
if (!otherUser) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Utente non trovato',
|
||||
});
|
||||
}
|
||||
|
||||
// Cerca chat esistente
|
||||
let chat = await Chat.findOne({
|
||||
idapp,
|
||||
type: 'direct',
|
||||
participants: { $all: [userId, otherUserId], $size: 2 },
|
||||
});
|
||||
|
||||
if (!chat) {
|
||||
// Crea nuova chat
|
||||
chat = new Chat({
|
||||
idapp,
|
||||
type: 'direct',
|
||||
participants: [userId, otherUserId],
|
||||
rideId: rideId || null,
|
||||
unreadCount: new Map(),
|
||||
});
|
||||
await chat.save();
|
||||
} else if (rideId && !chat.rideId) {
|
||||
// Aggiungi rideId se non presente
|
||||
chat.rideId = rideId;
|
||||
await chat.save();
|
||||
}
|
||||
|
||||
// ✅ Se la chat era stata cancellata da uno dei due, rimuovilo da deletedBy
|
||||
if (chat.deletedBy && chat.deletedBy.length > 0) {
|
||||
const wasDeleted = chat.deletedBy.some((id) => id.toString() === userId.toString());
|
||||
|
||||
if (wasDeleted) {
|
||||
chat.deletedBy = chat.deletedBy.filter((id) => id.toString() !== userId.toString());
|
||||
await chat.save();
|
||||
}
|
||||
}
|
||||
|
||||
// Popola i partecipanti
|
||||
await chat.populate('participants', 'username name surname profile');
|
||||
if (chat.rideId) {
|
||||
await chat.populate('rideId', 'departure destination departureDate');
|
||||
}
|
||||
|
||||
// Aggiungi unread count
|
||||
const chatObj = chat.toObject();
|
||||
chatObj.unreadCount = chat.getUnreadForUser(userId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: chatObj,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting/creating direct chat:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nella creazione della chat',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ===== GET CHAT BY ID =====
|
||||
exports.getChatById = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const { chatId } = req.params;
|
||||
|
||||
const chat = await Chat.findById(chatId)
|
||||
.populate('participants', 'username name surname profile')
|
||||
.populate({
|
||||
path: 'rideId',
|
||||
select: 'departure destination departureDate departureTime status',
|
||||
})
|
||||
.lean();
|
||||
|
||||
if (!chat) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Chat non trovata',
|
||||
});
|
||||
}
|
||||
|
||||
// Verifica che l'utente sia partecipante
|
||||
const isParticipant = chat.participants.some((p) => {
|
||||
const pId = p._id ? p._id.toString() : p.toString();
|
||||
return pId === userId.toString();
|
||||
});
|
||||
|
||||
if (!isParticipant) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Non autorizzato',
|
||||
});
|
||||
}
|
||||
|
||||
// Verifica se l'utente ha cancellato questa chat
|
||||
const wasDeleted = chat.deletedBy?.some((id) => id.toString() === userId.toString());
|
||||
|
||||
if (wasDeleted) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Chat non trovata',
|
||||
});
|
||||
}
|
||||
|
||||
// Aggiungi unread count per l'utente corrente
|
||||
let unreadCount = 0;
|
||||
if (chat.unreadCount) {
|
||||
if (chat.unreadCount instanceof Map) {
|
||||
unreadCount = chat.unreadCount.get(userId.toString()) || 0;
|
||||
} else if (typeof chat.unreadCount === 'object') {
|
||||
unreadCount = chat.unreadCount[userId.toString()] || 0;
|
||||
}
|
||||
}
|
||||
|
||||
const chatObj = {
|
||||
...chat,
|
||||
unreadCount,
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: chatObj,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting chat by ID:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nel recupero della chat',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ===== GET CHAT MESSAGES =====
|
||||
exports.getChatMessages = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const { chatId } = req.params;
|
||||
const idapp = req.user.idapp;
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const { before, after, limit = 50 } = req.query;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Verifica chat e partecipazione
|
||||
const chat = await Chat.findById(chatId);
|
||||
if (!chat) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Chat non trovata',
|
||||
});
|
||||
}
|
||||
|
||||
if (!chat.hasParticipant(userId)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Non autorizzato',
|
||||
});
|
||||
}
|
||||
|
||||
const query = {
|
||||
chatId,
|
||||
idapp,
|
||||
isDeleted: { $ne: true },
|
||||
};
|
||||
|
||||
// clearedBefore
|
||||
let clearedDate = null;
|
||||
if (chat.clearedBefore) {
|
||||
if (chat.clearedBefore instanceof Map) {
|
||||
clearedDate = chat.clearedBefore.get(userId.toString());
|
||||
} else if (typeof chat.clearedBefore === 'object') {
|
||||
clearedDate = chat.clearedBefore[userId.toString()];
|
||||
}
|
||||
}
|
||||
|
||||
if (clearedDate) {
|
||||
query.createdAt = { $gt: new Date(clearedDate) };
|
||||
}
|
||||
|
||||
// ✅ Paginazione: before (messaggi più vecchi)
|
||||
if (before) {
|
||||
query.createdAt = {
|
||||
...query.createdAt,
|
||||
$lt: new Date(before),
|
||||
};
|
||||
}
|
||||
|
||||
// ✅ Polling: after (messaggi più nuovi)
|
||||
if (after) {
|
||||
query.createdAt = {
|
||||
...query.createdAt,
|
||||
$gt: new Date(after), // Messaggi DOPO questo timestamp
|
||||
};
|
||||
}
|
||||
|
||||
// ✅ Ordina in base alla direzione
|
||||
const sortOrder = after ? 1 : -1; // after: asc, before: desc
|
||||
|
||||
const messages = await Message.find(query)
|
||||
.sort({ createdAt: sortOrder })
|
||||
.limit(parseInt(limit))
|
||||
.populate('senderId', 'username name surname profile.img profile.avatar')
|
||||
.populate({
|
||||
path: 'replyTo',
|
||||
select: 'text senderId',
|
||||
populate: {
|
||||
path: 'senderId',
|
||||
select: 'username name',
|
||||
},
|
||||
})
|
||||
.lean();
|
||||
|
||||
// ✅ Se usato after, i messaggi sono già in ordine cronologico
|
||||
// Se usato before, invertili
|
||||
if (!after) {
|
||||
messages.reverse();
|
||||
}
|
||||
|
||||
// Marca i messaggi come letti
|
||||
await chat.markAsRead(userId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: messages.reverse(),
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
hasMore: messages.length === limit,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching messages:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nel recupero dei messaggi',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ===== SEND MESSAGE =====
|
||||
exports.sendMessage = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const idapp = req.user.idapp;
|
||||
const { chatId } = req.params;
|
||||
const { text, type = 'text', metadata } = req.body;
|
||||
|
||||
// Verifica chat
|
||||
const chat = await Chat.findById(chatId);
|
||||
if (!chat) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Chat non trovata',
|
||||
});
|
||||
}
|
||||
|
||||
if (!chat.hasParticipant(userId)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Non autorizzato',
|
||||
});
|
||||
}
|
||||
|
||||
if (chat.isBlockedFor(userId)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Non puoi inviare messaggi in questa chat',
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ IMPORTANTE: Se qualcuno aveva cancellato la chat, rimuovilo da deletedBy
|
||||
// così la chat riappare nella sua lista
|
||||
if (chat.deletedBy && chat.deletedBy.length > 0) {
|
||||
const otherParticipants = chat.participants.filter((p) => p.toString() !== userId.toString());
|
||||
|
||||
let needsSave = false;
|
||||
otherParticipants.forEach((participantId) => {
|
||||
const wasDeleted = chat.deletedBy.some((id) => id.toString() === participantId.toString());
|
||||
if (wasDeleted) {
|
||||
chat.deletedBy = chat.deletedBy.filter((id) => id.toString() !== participantId.toString());
|
||||
needsSave = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (needsSave) {
|
||||
await chat.save();
|
||||
}
|
||||
}
|
||||
|
||||
// Crea messaggio
|
||||
const message = new Message({
|
||||
idapp,
|
||||
chatId: chat._id,
|
||||
senderId: userId,
|
||||
text,
|
||||
type,
|
||||
metadata,
|
||||
readBy: [userId],
|
||||
});
|
||||
|
||||
await message.save();
|
||||
await message.populate('senderId', 'username name surname profile');
|
||||
|
||||
// Aggiorna chat
|
||||
await chat.updateLastMessage(message);
|
||||
await chat.incrementUnread(userId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: message,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Errore nell'invio del messaggio",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ===== MARK MESSAGES AS READ =====
|
||||
exports.markAsRead = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const { chatId } = req.params;
|
||||
|
||||
const chat = await Chat.findById(chatId);
|
||||
if (!chat) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Chat non trovata',
|
||||
});
|
||||
}
|
||||
|
||||
if (!chat.hasParticipant(userId)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Non autorizzato',
|
||||
});
|
||||
}
|
||||
|
||||
// Marca come letti
|
||||
await chat.markAsRead(userId);
|
||||
|
||||
// Aggiorna anche i singoli messaggi
|
||||
await Message.updateMany(
|
||||
{
|
||||
chatId: chat._id,
|
||||
senderId: { $ne: userId },
|
||||
readBy: { $ne: userId },
|
||||
},
|
||||
{
|
||||
$addToSet: { readBy: userId },
|
||||
}
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Messaggi marcati come letti',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error marking as read:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nella marcatura dei messaggi',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ===== DELETE CHAT (SOFT DELETE) =====
|
||||
exports.deleteChat = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const { chatId } = req.params;
|
||||
|
||||
const chat = await Chat.findById(chatId);
|
||||
if (!chat) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Chat non trovata',
|
||||
});
|
||||
}
|
||||
|
||||
if (!chat.hasParticipant(userId)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Non autorizzato',
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Soft delete: aggiungi userId a deletedBy
|
||||
if (!chat.deletedBy) {
|
||||
chat.deletedBy = [];
|
||||
}
|
||||
|
||||
const alreadyDeleted = chat.deletedBy.some((id) => id.toString() === userId.toString());
|
||||
|
||||
if (!alreadyDeleted) {
|
||||
chat.deletedBy.push(userId);
|
||||
}
|
||||
|
||||
// ✅ Salva il timestamp di quando l'utente ha cancellato
|
||||
// così quando riappare la chat, vedrà solo messaggi nuovi
|
||||
if (!chat.clearedBefore) {
|
||||
chat.clearedBefore = new Map();
|
||||
}
|
||||
chat.clearedBefore.set(userId.toString(), new Date());
|
||||
|
||||
await chat.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Chat eliminata',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error deleting chat:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Errore nell'eliminazione della chat",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ===== TOGGLE MUTE CHAT =====
|
||||
exports.toggleMuteChat = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const { chatId } = req.params;
|
||||
const { mute } = req.body;
|
||||
|
||||
const chat = await Chat.findById(chatId);
|
||||
if (!chat) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Chat non trovata',
|
||||
});
|
||||
}
|
||||
|
||||
if (!chat.hasParticipant(userId)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Non autorizzato',
|
||||
});
|
||||
}
|
||||
|
||||
if (!chat.mutedBy) {
|
||||
chat.mutedBy = [];
|
||||
}
|
||||
|
||||
if (mute) {
|
||||
// Aggiungi a mutedBy se non presente
|
||||
const alreadyMuted = chat.mutedBy.some((id) => id.toString() === userId.toString());
|
||||
if (!alreadyMuted) {
|
||||
chat.mutedBy.push(userId);
|
||||
}
|
||||
} else {
|
||||
// Rimuovi da mutedBy
|
||||
chat.mutedBy = chat.mutedBy.filter((id) => id.toString() !== userId.toString());
|
||||
}
|
||||
|
||||
await chat.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: mute ? 'Chat silenziata' : 'Notifiche attivate',
|
||||
data: { muted: mute },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error toggling mute:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Errore nell'aggiornamento",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ===== GET UNREAD COUNT =====
|
||||
exports.getUnreadCount = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const idapp = req.user.idapp;
|
||||
|
||||
const chats = await Chat.find({
|
||||
idapp,
|
||||
participants: userId,
|
||||
isActive: true,
|
||||
deletedBy: { $ne: userId },
|
||||
}).lean();
|
||||
|
||||
let totalUnread = 0;
|
||||
chats.forEach((chat) => {
|
||||
const unread = chat.unreadCount?.get(userId.toString()) || 0;
|
||||
totalUnread += unread;
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
totalUnread,
|
||||
chatCount: chats.length,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting unread count:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nel conteggio messaggi non letti',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Marca una chat come letta
|
||||
* @route PUT /api/viaggi/chats/:chatId/read
|
||||
* @access Private
|
||||
*/
|
||||
exports.markChatAsRead = async (req, res) => {
|
||||
try {
|
||||
const { chatId } = req.params;
|
||||
const userId = req.user._id;
|
||||
|
||||
const chat = await Chat.findById(chatId);
|
||||
if (!chat) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Chat non trovata',
|
||||
});
|
||||
}
|
||||
|
||||
if (!chat.hasParticipant(userId)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Non sei autorizzato',
|
||||
});
|
||||
}
|
||||
|
||||
// Reset unread count
|
||||
if (!chat.unreadCount) {
|
||||
chat.unreadCount = new Map();
|
||||
}
|
||||
chat.unreadCount.set(userId.toString(), 0);
|
||||
chat.markModified('unreadCount');
|
||||
await chat.save();
|
||||
|
||||
// Marca messaggi come letti
|
||||
await Message.updateMany(
|
||||
{
|
||||
chatId,
|
||||
senderId: { $ne: userId },
|
||||
readBy: { $ne: userId },
|
||||
},
|
||||
{
|
||||
$addToSet: { readBy: userId },
|
||||
}
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Chat marcata come letta',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore marca come letto:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Muta/smuta notifiche
|
||||
* @route PUT /api/viaggi/chats/:chatId/mute
|
||||
* @access Private
|
||||
*/
|
||||
exports.toggleMuteChat = async (req, res) => {
|
||||
try {
|
||||
const { chatId } = req.params;
|
||||
const { mute } = req.body;
|
||||
const userId = req.user._id;
|
||||
|
||||
const chat = await Chat.findById(chatId);
|
||||
if (!chat) {
|
||||
return res.status(404).json({ success: false, message: 'Chat non trovata' });
|
||||
}
|
||||
|
||||
if (!chat.hasParticipant(userId)) {
|
||||
return res.status(403).json({ success: false, message: 'Non sei autorizzato' });
|
||||
}
|
||||
|
||||
if (!chat.mutedBy) {
|
||||
chat.mutedBy = [];
|
||||
}
|
||||
|
||||
const isMuted = chat.mutedBy.some((mid) => mid.toString() === userId.toString());
|
||||
|
||||
if (mute && !isMuted) {
|
||||
chat.mutedBy.push(userId);
|
||||
} else if (!mute && isMuted) {
|
||||
chat.mutedBy = chat.mutedBy.filter((mid) => mid.toString() !== userId.toString());
|
||||
}
|
||||
|
||||
await chat.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: mute ? 'Notifiche disattivate' : 'Notifiche attivate',
|
||||
data: { muted: mute },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore mute chat:', error);
|
||||
res.status(500).json({ success: false, message: 'Errore', error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Elimina un messaggio (soft delete)
|
||||
* @route DELETE /api/viaggi/chats/:chatId/messages/:messageId
|
||||
* @access Private
|
||||
*/
|
||||
exports.deleteMessage = async (req, res) => {
|
||||
try {
|
||||
const { chatId, messageId } = req.params;
|
||||
const userId = req.user._id;
|
||||
|
||||
const message = await Message.findById(messageId);
|
||||
|
||||
if (!message) {
|
||||
return res.status(404).json({ success: false, message: 'Messaggio non trovato' });
|
||||
}
|
||||
|
||||
if (message.chatId.toString() !== chatId) {
|
||||
return res.status(400).json({ success: false, message: 'Messaggio non appartiene a questa chat' });
|
||||
}
|
||||
|
||||
if (message.senderId.toString() !== userId.toString()) {
|
||||
return res.status(403).json({ success: false, message: 'Non sei autorizzato' });
|
||||
}
|
||||
|
||||
message.isDeleted = true;
|
||||
message.deletedAt = new Date();
|
||||
message.text = 'Messaggio eliminato';
|
||||
await message.save();
|
||||
|
||||
res.json({ success: true, message: 'Messaggio eliminato' });
|
||||
} catch (error) {
|
||||
console.error('Errore eliminazione messaggio:', error);
|
||||
res.status(500).json({ success: false, message: 'Errore', error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Blocca/sblocca una chat
|
||||
* @route PUT /api/viaggi/chats/:chatId/block
|
||||
* @access Private
|
||||
*/
|
||||
exports.toggleBlockChat = async (req, res) => {
|
||||
try {
|
||||
const { chatId } = req.params;
|
||||
const { block } = req.body;
|
||||
const userId = req.user._id;
|
||||
|
||||
const chat = await Chat.findById(chatId);
|
||||
if (!chat) {
|
||||
return res.status(404).json({ success: false, message: 'Chat non trovata' });
|
||||
}
|
||||
|
||||
if (!chat.hasParticipant(userId)) {
|
||||
return res.status(403).json({ success: false, message: 'Non sei autorizzato' });
|
||||
}
|
||||
|
||||
if (!chat.blockedBy) {
|
||||
chat.blockedBy = [];
|
||||
}
|
||||
|
||||
const isBlocked = chat.blockedBy.some((bid) => bid.toString() === userId.toString());
|
||||
|
||||
if (block && !isBlocked) {
|
||||
chat.blockedBy.push(userId);
|
||||
} else if (!block && isBlocked) {
|
||||
chat.blockedBy = chat.blockedBy.filter((bid) => bid.toString() !== userId.toString());
|
||||
}
|
||||
|
||||
await chat.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: block ? 'Chat bloccata' : 'Chat sbloccata',
|
||||
data: { blocked: block },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore blocco chat:', error);
|
||||
res.status(500).json({ success: false, message: 'Errore', error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = exports;
|
||||
931
src/controllers/feedbackController.js
Normal file
931
src/controllers/feedbackController.js
Normal file
@@ -0,0 +1,931 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Feedback = require('../models/viaggi/Feedback');
|
||||
const Ride = require('../models/viaggi/Ride');
|
||||
const RideRequest = require('../models/viaggi/RideRequest');
|
||||
const { User } = require('../models/user');
|
||||
|
||||
// ============================================================
|
||||
// 🔧 HELPER FUNCTIONS (definite prima per essere disponibili)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Converti userId in ObjectId in modo sicuro
|
||||
*/
|
||||
const toObjectId = (id) => {
|
||||
if (!id) return null;
|
||||
|
||||
if (id instanceof mongoose.Types.ObjectId) {
|
||||
return id;
|
||||
}
|
||||
|
||||
if (typeof id === 'object' && id._id) {
|
||||
return new mongoose.Types.ObjectId(id._id.toString());
|
||||
}
|
||||
|
||||
return new mongoose.Types.ObjectId(id.toString());
|
||||
};
|
||||
|
||||
/**
|
||||
* Ottieni statistiche feedback per un utente
|
||||
*/
|
||||
const getStatsForUser = async (idapp, userId) => {
|
||||
try {
|
||||
const userObjectId = toObjectId(userId);
|
||||
|
||||
if (!userObjectId) {
|
||||
return {
|
||||
averageRating: 0,
|
||||
totalFeedback: 0,
|
||||
asDriver: 0,
|
||||
asPassenger: 0,
|
||||
distribution: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
const result = await Feedback.aggregate([
|
||||
{
|
||||
$match: {
|
||||
idapp,
|
||||
toUserId: userObjectId,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
averageRating: { $avg: '$rating' },
|
||||
totalFeedback: { $sum: 1 },
|
||||
asDriver: {
|
||||
$sum: { $cond: [{ $eq: ['$role', 'driver'] }, 1, 0] },
|
||||
},
|
||||
asPassenger: {
|
||||
$sum: { $cond: [{ $eq: ['$role', 'passenger'] }, 1, 0] },
|
||||
},
|
||||
rating5: { $sum: { $cond: [{ $eq: ['$rating', 5] }, 1, 0] } },
|
||||
rating4: { $sum: { $cond: [{ $eq: ['$rating', 4] }, 1, 0] } },
|
||||
rating3: { $sum: { $cond: [{ $eq: ['$rating', 3] }, 1, 0] } },
|
||||
rating2: { $sum: { $cond: [{ $eq: ['$rating', 2] }, 1, 0] } },
|
||||
rating1: { $sum: { $cond: [{ $eq: ['$rating', 1] }, 1, 0] } },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
if (!result || result.length === 0) {
|
||||
return {
|
||||
averageRating: 0,
|
||||
totalFeedback: 0,
|
||||
asDriver: 0,
|
||||
asPassenger: 0,
|
||||
distribution: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
const stats = result[0];
|
||||
|
||||
return {
|
||||
averageRating: stats.averageRating
|
||||
? Math.round(stats.averageRating * 10) / 10
|
||||
: 0,
|
||||
totalFeedback: stats.totalFeedback || 0,
|
||||
asDriver: stats.asDriver || 0,
|
||||
asPassenger: stats.asPassenger || 0,
|
||||
distribution: {
|
||||
1: stats.rating1 || 0,
|
||||
2: stats.rating2 || 0,
|
||||
3: stats.rating3 || 0,
|
||||
4: stats.rating4 || 0,
|
||||
5: stats.rating5 || 0,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Errore calcolo stats feedback:', error);
|
||||
return {
|
||||
averageRating: 0,
|
||||
totalFeedback: 0,
|
||||
asDriver: 0,
|
||||
asPassenger: 0,
|
||||
distribution: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 },
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Calcola la distribuzione dei rating per un utente
|
||||
*/
|
||||
const getRatingDistribution = async (idapp, userId) => {
|
||||
try {
|
||||
const userObjectId = toObjectId(userId);
|
||||
|
||||
if (!userObjectId) {
|
||||
return { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
|
||||
}
|
||||
|
||||
const result = await Feedback.aggregate([
|
||||
{
|
||||
$match: {
|
||||
idapp,
|
||||
toUserId: userObjectId,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$rating',
|
||||
count: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
{ $sort: { _id: -1 } },
|
||||
]);
|
||||
|
||||
const distribution = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
|
||||
|
||||
result.forEach((r) => {
|
||||
if (r._id >= 1 && r._id <= 5) {
|
||||
distribution[r._id] = r.count;
|
||||
}
|
||||
});
|
||||
|
||||
return distribution;
|
||||
} catch (error) {
|
||||
console.error('Errore calcolo distribuzione:', error);
|
||||
return { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Aggiorna la media rating nel profilo utente
|
||||
*/
|
||||
const updateUserRating = async (idapp, userId) => {
|
||||
try {
|
||||
const stats = await getStatsForUser(idapp, userId);
|
||||
|
||||
await User.findByIdAndUpdate(userId, {
|
||||
$set: {
|
||||
'profile.driverProfile.averageRating': stats.averageRating,
|
||||
'profile.driverProfile.totalFeedback': stats.totalFeedback,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore aggiornamento rating utente:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 📝 CONTROLLER FUNCTIONS
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* @desc Crea un feedback per un viaggio
|
||||
* @route POST /api/viaggi/feedback
|
||||
* @access Private
|
||||
*/
|
||||
const createFeedback = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
idapp,
|
||||
rideId,
|
||||
rideRequestId,
|
||||
toUserId,
|
||||
role,
|
||||
rating,
|
||||
categories,
|
||||
comment,
|
||||
pros,
|
||||
cons,
|
||||
tags,
|
||||
isPublic,
|
||||
} = req.body;
|
||||
|
||||
const fromUserId = req.user._id;
|
||||
|
||||
// Validazione base
|
||||
if (!idapp || !rideId || !toUserId || !role || !rating) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Campi obbligatori: idapp, rideId, toUserId, role, rating',
|
||||
});
|
||||
}
|
||||
|
||||
// Verifica rating valido
|
||||
if (rating < 1 || rating > 5) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Il rating deve essere tra 1 e 5',
|
||||
});
|
||||
}
|
||||
|
||||
// Verifica che il ride esista
|
||||
const ride = await Ride.findById(rideId);
|
||||
if (!ride) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Viaggio non trovato',
|
||||
});
|
||||
}
|
||||
|
||||
// Verifica che l'utente abbia partecipato al viaggio
|
||||
const wasDriver = ride.userId.toString() === fromUserId.toString();
|
||||
const wasPassenger = ride.confirmedPassengers?.some(
|
||||
(p) => p.userId.toString() === fromUserId.toString()
|
||||
);
|
||||
|
||||
if (!wasDriver && !wasPassenger) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Non hai partecipato a questo viaggio',
|
||||
});
|
||||
}
|
||||
|
||||
// Verifica che non stia valutando se stesso
|
||||
if (fromUserId.toString() === toUserId.toString()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Non puoi valutare te stesso',
|
||||
});
|
||||
}
|
||||
|
||||
// Verifica che non esista già un feedback
|
||||
const existingFeedback = await Feedback.findOne({
|
||||
rideId,
|
||||
fromUserId,
|
||||
toUserId,
|
||||
});
|
||||
|
||||
if (existingFeedback) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Hai già lasciato un feedback per questo utente in questo viaggio',
|
||||
});
|
||||
}
|
||||
|
||||
// Crea il feedback
|
||||
const feedbackData = {
|
||||
idapp,
|
||||
rideId,
|
||||
fromUserId,
|
||||
toUserId,
|
||||
role,
|
||||
rating,
|
||||
isVerified: ride.status === 'completed',
|
||||
};
|
||||
|
||||
if (rideRequestId) feedbackData.rideRequestId = rideRequestId;
|
||||
if (categories) feedbackData.categories = categories;
|
||||
if (comment) feedbackData.comment = comment;
|
||||
if (pros) feedbackData.pros = pros;
|
||||
if (cons) feedbackData.cons = cons;
|
||||
if (tags) feedbackData.tags = tags;
|
||||
if (isPublic !== undefined) feedbackData.isPublic = isPublic;
|
||||
|
||||
const feedback = new Feedback(feedbackData);
|
||||
await feedback.save();
|
||||
|
||||
// Aggiorna la media rating dell'utente destinatario
|
||||
await updateUserRating(idapp, toUserId);
|
||||
|
||||
// Aggiorna flag nella richiesta se presente
|
||||
if (rideRequestId) {
|
||||
await RideRequest.findByIdAndUpdate(rideRequestId, {
|
||||
$set: { feedbackGiven: true },
|
||||
});
|
||||
}
|
||||
|
||||
await feedback.populate('fromUserId', 'username name surname profile.img');
|
||||
await feedback.populate('toUserId', 'username name surname');
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Feedback inviato con successo!',
|
||||
data: feedback,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore creazione feedback:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nella creazione del feedback',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Ottieni i feedback ricevuti da un utente
|
||||
* @route GET /api/viaggi/feedback/user/:userId
|
||||
* @access Public
|
||||
*/
|
||||
const getUserFeedback = async (req, res) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
const { idapp, role, page = 1, limit = 10 } = req.query;
|
||||
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è obbligatorio',
|
||||
});
|
||||
}
|
||||
|
||||
const query = {
|
||||
idapp,
|
||||
toUserId: userId,
|
||||
isPublic: true,
|
||||
};
|
||||
|
||||
if (role) {
|
||||
query.role = role;
|
||||
}
|
||||
|
||||
const skip = (parseInt(page) - 1) * parseInt(limit);
|
||||
|
||||
const [feedbacks, total, stats] = await Promise.all([
|
||||
Feedback.find(query)
|
||||
.populate('fromUserId', 'username name surname profile.img')
|
||||
.populate('rideId', 'departure destination departureDate')
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(parseInt(limit)),
|
||||
Feedback.countDocuments(query),
|
||||
getStatsForUser(idapp, userId),
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: feedbacks,
|
||||
stats,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / parseInt(limit)),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore recupero feedbacks:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nel recupero dei feedback',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Ottieni statistiche feedback per un utente
|
||||
* @route GET /api/viaggi/feedback/user/:userId/stats
|
||||
* @access Public
|
||||
*/
|
||||
const getUserFeedbackStats = async (req, res) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
const { idapp } = req.query;
|
||||
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è obbligatorio',
|
||||
});
|
||||
}
|
||||
|
||||
const stats = await getStatsForUser(idapp, userId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: stats,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore recupero stats:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nel recupero delle statistiche',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Ottieni i feedback per un viaggio
|
||||
* @route GET /api/viaggi/feedback/ride/:rideId
|
||||
* @access Public/Private
|
||||
*/
|
||||
const getRideFeedback = async (req, res) => {
|
||||
try {
|
||||
const { rideId } = req.params;
|
||||
const { idapp } = req.query;
|
||||
const userId = req.user?._id;
|
||||
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è obbligatorio',
|
||||
});
|
||||
}
|
||||
|
||||
const ride = await Ride.findById(rideId);
|
||||
if (!ride) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Viaggio non trovato',
|
||||
});
|
||||
}
|
||||
|
||||
const query = {
|
||||
idapp,
|
||||
rideId,
|
||||
isPublic: true,
|
||||
};
|
||||
|
||||
const feedbacks = await Feedback.find(query)
|
||||
.populate('fromUserId', 'username name surname profile.img')
|
||||
.populate('toUserId', 'username name surname profile.img')
|
||||
.sort({ createdAt: -1 });
|
||||
|
||||
let pendingFeedbacks = [];
|
||||
let myFeedbacks = [];
|
||||
|
||||
if (userId) {
|
||||
const wasDriver = ride.userId.toString() === userId.toString();
|
||||
const wasPassenger = ride.confirmedPassengers?.some(
|
||||
(p) => p.userId.toString() === userId.toString()
|
||||
);
|
||||
|
||||
if (wasDriver || wasPassenger) {
|
||||
myFeedbacks = feedbacks.filter(
|
||||
(f) => f.fromUserId._id.toString() === userId.toString()
|
||||
);
|
||||
|
||||
if (wasDriver) {
|
||||
const feedbackGivenTo = myFeedbacks.map((f) => f.toUserId._id.toString());
|
||||
pendingFeedbacks = (ride.confirmedPassengers || [])
|
||||
.filter((p) => !feedbackGivenTo.includes(p.userId.toString()))
|
||||
.map((p) => ({ userId: p.userId, role: 'passenger' }));
|
||||
} else {
|
||||
const hasGivenToDriver = myFeedbacks.some(
|
||||
(f) => f.toUserId._id.toString() === ride.userId.toString()
|
||||
);
|
||||
if (!hasGivenToDriver) {
|
||||
pendingFeedbacks.push({ userId: ride.userId, role: 'driver' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
feedbacks,
|
||||
pendingFeedbacks,
|
||||
myFeedbacks,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore recupero feedbacks viaggio:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Verifica se l'utente può lasciare un feedback
|
||||
* @route GET /api/viaggi/feedback/can-leave/:rideId/:toUserId
|
||||
* @access Private
|
||||
*/
|
||||
const canLeaveFeedback = async (req, res) => {
|
||||
try {
|
||||
const { rideId, toUserId } = req.params;
|
||||
const { idapp } = req.query;
|
||||
const fromUserId = req.user._id;
|
||||
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è obbligatorio',
|
||||
});
|
||||
}
|
||||
|
||||
const ride = await Ride.findById(rideId);
|
||||
if (!ride) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Viaggio non trovato',
|
||||
});
|
||||
}
|
||||
|
||||
const wasDriver = ride.userId.toString() === fromUserId.toString();
|
||||
const wasPassenger = ride.confirmedPassengers?.some(
|
||||
(p) => p.userId.toString() === fromUserId.toString()
|
||||
);
|
||||
|
||||
if (!wasDriver && !wasPassenger) {
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
canLeave: false,
|
||||
reason: 'Non hai partecipato a questo viaggio',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (fromUserId.toString() === toUserId.toString()) {
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
canLeave: false,
|
||||
reason: 'Non puoi valutare te stesso',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const toUserWasDriver = ride.userId.toString() === toUserId.toString();
|
||||
const toUserWasPassenger = ride.confirmedPassengers?.some(
|
||||
(p) => p.userId.toString() === toUserId.toString()
|
||||
);
|
||||
|
||||
if (!toUserWasDriver && !toUserWasPassenger) {
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
canLeave: false,
|
||||
reason: "L'utente destinatario non ha partecipato a questo viaggio",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (ride.status !== 'completed') {
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
canLeave: false,
|
||||
reason: 'Il viaggio non è ancora stato completato',
|
||||
rideStatus: ride.status,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const existingFeedback = await Feedback.findOne({
|
||||
rideId,
|
||||
fromUserId,
|
||||
toUserId,
|
||||
});
|
||||
|
||||
if (existingFeedback) {
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
canLeave: false,
|
||||
reason: 'Hai già lasciato un feedback per questo utente in questo viaggio',
|
||||
existingFeedbackId: existingFeedback._id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const toUserRole = toUserWasDriver ? 'driver' : 'passenger';
|
||||
const toUser = await User.findById(toUserId).select('username name surname profile.img');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
canLeave: true,
|
||||
toUser: toUser ? {
|
||||
_id: toUser._id,
|
||||
username: toUser.username,
|
||||
name: toUser.name,
|
||||
surname: toUser.surname,
|
||||
img: toUser.profile?.img,
|
||||
} : null,
|
||||
toUserRole,
|
||||
ride: {
|
||||
_id: ride._id,
|
||||
departure: ride.departure,
|
||||
destination: ride.destination,
|
||||
departureDate: ride.departureDate,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore verifica canLeaveFeedback:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nella verifica',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Rispondi a un feedback ricevuto
|
||||
* @route POST /api/viaggi/feedback/:id/response
|
||||
* @access Private
|
||||
*/
|
||||
const respondToFeedback = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { text } = req.body;
|
||||
const userId = req.user._id;
|
||||
|
||||
if (!text || !text.trim()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Il testo della risposta è obbligatorio',
|
||||
});
|
||||
}
|
||||
|
||||
const feedback = await Feedback.findById(id);
|
||||
|
||||
if (!feedback) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Feedback non trovato',
|
||||
});
|
||||
}
|
||||
|
||||
if (feedback.toUserId.toString() !== userId.toString()) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Non sei autorizzato a rispondere a questo feedback',
|
||||
});
|
||||
}
|
||||
|
||||
if (feedback.response?.text) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Hai già risposto a questo feedback',
|
||||
});
|
||||
}
|
||||
|
||||
feedback.response = {
|
||||
text: text.trim(),
|
||||
createdAt: new Date(),
|
||||
};
|
||||
await feedback.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Risposta aggiunta',
|
||||
data: feedback,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore risposta feedback:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Segna un feedback come utile
|
||||
* @route POST /api/viaggi/feedback/:id/helpful
|
||||
* @access Private
|
||||
*/
|
||||
const markAsHelpful = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const userId = req.user._id;
|
||||
|
||||
const feedback = await Feedback.findById(id);
|
||||
|
||||
if (!feedback) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Feedback non trovato',
|
||||
});
|
||||
}
|
||||
|
||||
if (!feedback.helpful) {
|
||||
feedback.helpful = { count: 0, users: [] };
|
||||
}
|
||||
|
||||
const userIdStr = userId.toString();
|
||||
const alreadyMarked = feedback.helpful.users.some(
|
||||
(u) => u.toString() === userIdStr
|
||||
);
|
||||
|
||||
if (alreadyMarked) {
|
||||
feedback.helpful.users = feedback.helpful.users.filter(
|
||||
(u) => u.toString() !== userIdStr
|
||||
);
|
||||
feedback.helpful.count = Math.max(0, feedback.helpful.count - 1);
|
||||
} else {
|
||||
feedback.helpful.users.push(userId);
|
||||
feedback.helpful.count += 1;
|
||||
}
|
||||
|
||||
await feedback.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: alreadyMarked ? 'Voto rimosso' : 'Feedback segnato come utile',
|
||||
data: {
|
||||
helpfulCount: feedback.helpful.count,
|
||||
isHelpful: !alreadyMarked,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore mark helpful:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Segnala un feedback inappropriato
|
||||
* @route POST /api/viaggi/feedback/:id/report
|
||||
* @access Private
|
||||
*/
|
||||
const reportFeedback = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { reason } = req.body;
|
||||
const userId = req.user._id;
|
||||
|
||||
if (!reason || !reason.trim()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'La motivazione è obbligatoria',
|
||||
});
|
||||
}
|
||||
|
||||
const feedback = await Feedback.findById(id);
|
||||
|
||||
if (!feedback) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Feedback non trovato',
|
||||
});
|
||||
}
|
||||
|
||||
if (!feedback.reports) {
|
||||
feedback.reports = [];
|
||||
}
|
||||
|
||||
const alreadyReported = feedback.reports.some(
|
||||
(r) => r.userId.toString() === userId.toString()
|
||||
);
|
||||
|
||||
if (alreadyReported) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Hai già segnalato questo feedback',
|
||||
});
|
||||
}
|
||||
|
||||
feedback.reports.push({
|
||||
userId,
|
||||
reason: reason.trim(),
|
||||
createdAt: new Date(),
|
||||
});
|
||||
|
||||
if (feedback.reports.length >= 3) {
|
||||
feedback.isPublic = false;
|
||||
feedback.hiddenReason = 'Nascosto automaticamente per multiple segnalazioni';
|
||||
}
|
||||
|
||||
await feedback.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Feedback segnalato. Lo esamineremo al più presto.',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore segnalazione:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Ottieni i miei feedback dati
|
||||
* @route GET /api/viaggi/feedback/my/given
|
||||
* @access Private
|
||||
*/
|
||||
const getMyGivenFeedback = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const { idapp, page = 1, limit = 20 } = req.query;
|
||||
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è obbligatorio',
|
||||
});
|
||||
}
|
||||
|
||||
const skip = (parseInt(page) - 1) * parseInt(limit);
|
||||
|
||||
const [feedbacks, total] = await Promise.all([
|
||||
Feedback.find({ idapp, fromUserId: userId })
|
||||
.populate('toUserId', 'username name surname profile.img')
|
||||
.populate('rideId', 'departure destination departureDate')
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(parseInt(limit)),
|
||||
Feedback.countDocuments({ idapp, fromUserId: userId }),
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: feedbacks,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / parseInt(limit)),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore recupero feedback dati:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Ottieni i miei feedback ricevuti
|
||||
* @route GET /api/viaggi/feedback/my/received
|
||||
* @access Private
|
||||
*/
|
||||
const getMyReceivedFeedback = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const { idapp, page = 1, limit = 20 } = req.query;
|
||||
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è obbligatorio',
|
||||
});
|
||||
}
|
||||
|
||||
const skip = (parseInt(page) - 1) * parseInt(limit);
|
||||
|
||||
const [feedbacks, total, stats] = await Promise.all([
|
||||
Feedback.find({ idapp, toUserId: userId })
|
||||
.populate('fromUserId', 'username name surname profile.img')
|
||||
.populate('rideId', 'departure destination departureDate')
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(parseInt(limit)),
|
||||
Feedback.countDocuments({ idapp, toUserId: userId }),
|
||||
getStatsForUser(idapp, userId),
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: feedbacks,
|
||||
stats,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / parseInt(limit)),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore recupero feedback ricevuti:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 📤 EXPORTS
|
||||
// ============================================================
|
||||
|
||||
module.exports = {
|
||||
// Controller functions
|
||||
createFeedback,
|
||||
getUserFeedback,
|
||||
getUserFeedbackStats,
|
||||
getRideFeedback,
|
||||
canLeaveFeedback,
|
||||
respondToFeedback,
|
||||
reportFeedback,
|
||||
markAsHelpful,
|
||||
getMyGivenFeedback,
|
||||
getMyReceivedFeedback,
|
||||
|
||||
// Alias per compatibilità
|
||||
getFeedbacksForUser: getUserFeedback,
|
||||
getFeedbacksForRide: getRideFeedback,
|
||||
getMyGivenFeedbacks: getMyGivenFeedback,
|
||||
getMyReceivedFeedbacks: getMyReceivedFeedback,
|
||||
|
||||
// Helper functions
|
||||
getStatsForUser,
|
||||
getRatingDistribution,
|
||||
updateUserRating,
|
||||
};
|
||||
735
src/controllers/geocodingController.js
Normal file
735
src/controllers/geocodingController.js
Normal file
@@ -0,0 +1,735 @@
|
||||
/**
|
||||
* Controller per Geocoding usando OpenRouteService
|
||||
* Documentazione: https://openrouteservice.org/dev/#/api-docs
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
|
||||
// Configurazione OpenRouteService
|
||||
const ORS_BASE = 'https://api.openrouteservice.org';
|
||||
const ORS_API_KEY = process.env.ORS_API_KEY || 'YOUR_API_KEY_HERE';
|
||||
|
||||
/**
|
||||
* Helper per fare richieste HTTPS a OpenRouteService
|
||||
*/
|
||||
const makeRequest = (url, method = 'GET', body = null) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const urlObj = new URL(url);
|
||||
|
||||
const options = {
|
||||
hostname: urlObj.hostname,
|
||||
path: urlObj.pathname + urlObj.search,
|
||||
method,
|
||||
headers: {
|
||||
Authorization: ORS_API_KEY,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => (data += chunk));
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
if (res.statusCode >= 400) {
|
||||
reject(new Error(parsed.error?.message || `HTTP ${res.statusCode}`));
|
||||
} else {
|
||||
resolve(parsed);
|
||||
}
|
||||
} catch (e) {
|
||||
reject(new Error('Errore parsing risposta'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.setTimeout(15000, () => {
|
||||
req.destroy();
|
||||
reject(new Error('Timeout richiesta'));
|
||||
});
|
||||
|
||||
if (body) {
|
||||
req.write(JSON.stringify(body));
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Autocomplete città (ORS Geocode Autocomplete)
|
||||
* @route GET /api/geo/autocomplete
|
||||
*/
|
||||
const autocomplete = async (req, res) => {
|
||||
try {
|
||||
const { q, limit = 5, lang = 'it', country = 'IT' } = req.query;
|
||||
|
||||
if (!q || q.length < 2) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Query deve essere almeno 2 caratteri',
|
||||
});
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
text: q,
|
||||
size: limit,
|
||||
lang,
|
||||
'boundary.country': country,
|
||||
layers: 'locality,county,region', // Solo città/comuni
|
||||
});
|
||||
|
||||
const url = `${ORS_BASE}/geocode/autocomplete?${params}`;
|
||||
const data = await makeRequest(url);
|
||||
|
||||
const results = data.features.map((feature) => ({
|
||||
id: feature.properties.id,
|
||||
city: feature.properties.name,
|
||||
locality: feature.properties.locality,
|
||||
county: feature.properties.county,
|
||||
region: feature.properties.region,
|
||||
country: feature.properties.country,
|
||||
postalCode: feature.properties.postalcode,
|
||||
coordinates: {
|
||||
lat: feature.geometry.coordinates[1],
|
||||
lng: feature.geometry.coordinates[0],
|
||||
},
|
||||
displayName: feature.properties.label,
|
||||
type: feature.properties.layer,
|
||||
confidence: feature.properties.confidence,
|
||||
}));
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
count: results.length,
|
||||
data: results,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore autocomplete:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante la ricerca',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Geocoding - indirizzo a coordinate (ORS Geocode Search)
|
||||
* @route GET /api/geo/geocode
|
||||
*/
|
||||
const geocode = async (req, res) => {
|
||||
try {
|
||||
const { address, city, country = 'IT', limit = 5, lang = 'it' } = req.query;
|
||||
|
||||
const searchQuery = [address, city].filter(Boolean).join(', ');
|
||||
|
||||
if (!searchQuery) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Fornisci un indirizzo o città da cercare',
|
||||
});
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
text: searchQuery,
|
||||
size: limit,
|
||||
lang,
|
||||
'boundary.country': country,
|
||||
});
|
||||
|
||||
const url = `${ORS_BASE}/geocode/search?${params}`;
|
||||
const data = await makeRequest(url);
|
||||
|
||||
if (!data.features || data.features.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Nessun risultato trovato',
|
||||
});
|
||||
}
|
||||
|
||||
const results = data.features.map((feature) => ({
|
||||
id: feature.properties.id,
|
||||
displayName: feature.properties.label,
|
||||
name: feature.properties.name,
|
||||
street: feature.properties.street,
|
||||
houseNumber: feature.properties.housenumber,
|
||||
city: feature.properties.locality || feature.properties.county,
|
||||
county: feature.properties.county,
|
||||
region: feature.properties.region,
|
||||
country: feature.properties.country,
|
||||
postalCode: feature.properties.postalcode,
|
||||
coordinates: {
|
||||
lat: feature.geometry.coordinates[1],
|
||||
lng: feature.geometry.coordinates[0],
|
||||
},
|
||||
type: feature.properties.layer,
|
||||
confidence: feature.properties.confidence,
|
||||
}));
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
count: results.length,
|
||||
data: results,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore geocoding:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante il geocoding',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Reverse geocoding - coordinate a indirizzo (ORS Reverse)
|
||||
* @route GET /api/geo/reverse
|
||||
*/
|
||||
const reverseGeocode = async (req, res) => {
|
||||
try {
|
||||
const { lat, lng, lang = 'it' } = req.query;
|
||||
|
||||
if (!lat || !lng) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Coordinate lat e lng richieste',
|
||||
});
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
'point.lat': lat,
|
||||
'point.lon': lng,
|
||||
lang,
|
||||
size: '1',
|
||||
layers: 'address,street,locality',
|
||||
});
|
||||
|
||||
const url = `${ORS_BASE}/geocode/reverse?${params}`;
|
||||
const data = await makeRequest(url);
|
||||
|
||||
if (!data.features || data.features.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Nessun risultato trovato',
|
||||
});
|
||||
}
|
||||
|
||||
const feature = data.features[0];
|
||||
|
||||
const result = {
|
||||
displayName: feature.properties.label,
|
||||
name: feature.properties.name,
|
||||
street: feature.properties.street,
|
||||
houseNumber: feature.properties.housenumber,
|
||||
city: feature.properties.locality || feature.properties.county,
|
||||
county: feature.properties.county,
|
||||
region: feature.properties.region,
|
||||
country: feature.properties.country,
|
||||
postalCode: feature.properties.postalcode,
|
||||
coordinates: {
|
||||
lat: parseFloat(lat),
|
||||
lng: parseFloat(lng),
|
||||
},
|
||||
distance: feature.properties.distance, // distanza dal punto esatto
|
||||
};
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore reverse geocoding:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante il reverse geocoding',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Calcola percorso tra due o più punti (ORS Directions)
|
||||
* @route POST /api/geo/route
|
||||
* @body { coordinates: [[lng,lat], [lng,lat], ...], profile: 'driving-car' }
|
||||
*/
|
||||
const getRoute = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
startLat,
|
||||
startLng,
|
||||
endLat,
|
||||
endLng,
|
||||
waypoints, // formato: "lat1,lng1;lat2,lng2;..."
|
||||
profile = 'driving-car', // driving-car, driving-hgv, cycling-regular, foot-walking
|
||||
language = 'it',
|
||||
units = 'km',
|
||||
} = req.query;
|
||||
|
||||
if (!startLat || !startLng || !endLat || !endLng) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Coordinate di partenza e arrivo richieste',
|
||||
});
|
||||
}
|
||||
|
||||
// Costruisci array coordinate [lng, lat] (formato GeoJSON)
|
||||
const coordinates = [[parseFloat(startLng), parseFloat(startLat)]];
|
||||
|
||||
if (waypoints) {
|
||||
const waypointsList = waypoints.split(';');
|
||||
waypointsList.forEach((wp) => {
|
||||
const [lat, lng] = wp.split(',').map(parseFloat);
|
||||
coordinates.push([lng, lat]);
|
||||
});
|
||||
}
|
||||
|
||||
coordinates.push([parseFloat(endLng), parseFloat(endLat)]);
|
||||
|
||||
// Richiesta POST a ORS Directions
|
||||
const url = `${ORS_BASE}/v2/directions/${profile}`;
|
||||
|
||||
const body = {
|
||||
coordinates,
|
||||
language,
|
||||
units,
|
||||
geometry: true,
|
||||
instructions: true,
|
||||
maneuvers: true,
|
||||
};
|
||||
|
||||
const data = await makeRequest(url, 'POST', body);
|
||||
|
||||
if (!data.routes || data.routes.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Impossibile calcolare il percorso',
|
||||
});
|
||||
}
|
||||
|
||||
const route = data.routes[0];
|
||||
const summary = route.summary;
|
||||
|
||||
const result = {
|
||||
distance: Math.round(summary.distance * 10) / 10, // km
|
||||
duration: Math.round(summary.duration / 60), // minuti
|
||||
durationFormatted: formatDuration(summary.duration),
|
||||
bbox: data.bbox, // Bounding box
|
||||
geometry: route.geometry, // Polyline encoded
|
||||
segments: route.segments.map((segment) => ({
|
||||
distance: Math.round(segment.distance * 10) / 10,
|
||||
duration: Math.round(segment.duration / 60),
|
||||
steps: segment.steps.map((step) => ({
|
||||
instruction: step.instruction,
|
||||
name: step.name,
|
||||
distance: Math.round(step.distance * 100) / 100,
|
||||
duration: Math.round(step.duration / 60),
|
||||
type: step.type,
|
||||
maneuver: step.maneuver,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore calcolo percorso:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante il calcolo del percorso',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Calcola matrice distanze tra più punti (ORS Matrix)
|
||||
* @route POST /api/geo/matrix
|
||||
*/
|
||||
const getMatrix = async (req, res) => {
|
||||
try {
|
||||
const { locations, profile = 'driving-car' } = req.body;
|
||||
|
||||
if (!locations || locations.length < 2) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Almeno 2 location richieste',
|
||||
});
|
||||
}
|
||||
|
||||
// Formato locations: [[lng, lat], [lng, lat], ...]
|
||||
const url = `${ORS_BASE}/v2/matrix/${profile}`;
|
||||
|
||||
const body = {
|
||||
locations,
|
||||
metrics: ['distance', 'duration'],
|
||||
units: 'km',
|
||||
};
|
||||
|
||||
const data = await makeRequest(url, 'POST', body);
|
||||
|
||||
const result = {
|
||||
distances: data.distances, // Matrice distanze in km
|
||||
durations: data.durations, // Matrice durate in secondi
|
||||
sources: data.sources,
|
||||
destinations: data.destinations,
|
||||
};
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore calcolo matrice:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante il calcolo della matrice',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Suggerisci città intermedie su un percorso
|
||||
* @route GET /api/geo/suggest-waypoints
|
||||
*/
|
||||
const suggestWaypoints = async (req, res) => {
|
||||
try {
|
||||
const { startLat, startLng, endLat, endLng, count = 3 } = req.query;
|
||||
|
||||
if (!startLat || !startLng || !endLat || !endLng) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Coordinate di partenza e arrivo richieste',
|
||||
});
|
||||
}
|
||||
|
||||
// Prima ottieni il percorso
|
||||
const routeUrl = `${ORS_BASE}/v2/directions/driving-car`;
|
||||
const routeBody = {
|
||||
coordinates: [
|
||||
[parseFloat(startLng), parseFloat(startLat)],
|
||||
[parseFloat(endLng), parseFloat(endLat)],
|
||||
],
|
||||
geometry: true,
|
||||
};
|
||||
|
||||
const routeData = await makeRequest(routeUrl, 'POST', routeBody);
|
||||
|
||||
if (!routeData.routes || routeData.routes.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Impossibile calcolare il percorso',
|
||||
});
|
||||
}
|
||||
|
||||
// Decodifica polyline per ottenere punti
|
||||
const geometry = routeData.routes[0].geometry;
|
||||
const decodedPoints = decodePolyline(geometry);
|
||||
|
||||
// Seleziona punti equidistanti lungo il percorso
|
||||
const totalPoints = decodedPoints.length;
|
||||
const step = Math.floor(totalPoints / (parseInt(count) + 1));
|
||||
|
||||
const sampledPoints = [];
|
||||
for (let i = 1; i <= count; i++) {
|
||||
const index = Math.min(step * i, totalPoints - 1);
|
||||
sampledPoints.push(decodedPoints[index]);
|
||||
}
|
||||
|
||||
// Fai reverse geocoding per ogni punto
|
||||
const cities = [];
|
||||
const seenCities = new Set();
|
||||
|
||||
for (const point of sampledPoints) {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
'point.lat': point[1],
|
||||
'point.lon': point[0],
|
||||
lang: 'it',
|
||||
size: '1',
|
||||
layers: 'locality,county',
|
||||
});
|
||||
|
||||
const reverseUrl = `${ORS_BASE}/geocode/reverse?${params}`;
|
||||
const data = await makeRequest(reverseUrl);
|
||||
|
||||
if (data.features && data.features.length > 0) {
|
||||
const feature = data.features[0];
|
||||
const cityName = feature.properties.locality || feature.properties.county;
|
||||
|
||||
if (cityName && !seenCities.has(cityName.toLowerCase())) {
|
||||
seenCities.add(cityName.toLowerCase());
|
||||
cities.push({
|
||||
city: cityName,
|
||||
county: feature.properties.county,
|
||||
region: feature.properties.region,
|
||||
coordinates: {
|
||||
lat: point[1],
|
||||
lng: point[0],
|
||||
},
|
||||
displayName: feature.properties.label,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Errore reverse per punto:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
count: cities.length,
|
||||
data: cities,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore suggerimento waypoints:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante il suggerimento delle tappe',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Cerca città italiane (ottimizzato)
|
||||
* @route GET /api/geo/cities/it
|
||||
*/
|
||||
const searchItalianCities = async (req, res) => {
|
||||
try {
|
||||
const { q, limit = 10, region } = req.query;
|
||||
|
||||
if (!q || q.length < 2) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Query deve essere almeno 2 caratteri',
|
||||
});
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
text: q,
|
||||
size: limit,
|
||||
lang: 'it',
|
||||
'boundary.country': 'IT',
|
||||
layers: 'locality,county',
|
||||
});
|
||||
|
||||
// Filtro opzionale per regione
|
||||
if (region) {
|
||||
params.append('region', region);
|
||||
}
|
||||
|
||||
const url = `${ORS_BASE}/geocode/search?${params}`;
|
||||
const data = await makeRequest(url);
|
||||
|
||||
const results = data.features
|
||||
.filter((f) => f.properties.locality || f.properties.county)
|
||||
.map((feature) => ({
|
||||
city: feature.properties.locality || feature.properties.name,
|
||||
county: feature.properties.county,
|
||||
region: feature.properties.region,
|
||||
postalCode: feature.properties.postalcode,
|
||||
coordinates: {
|
||||
lat: feature.geometry.coordinates[1],
|
||||
lng: feature.geometry.coordinates[0],
|
||||
},
|
||||
displayName: `${feature.properties.locality || feature.properties.name}, ${feature.properties.region}`,
|
||||
confidence: feature.properties.confidence,
|
||||
}));
|
||||
|
||||
// Rimuovi duplicati
|
||||
const unique = results.filter(
|
||||
(v, i, a) => a.findIndex((t) => t.city?.toLowerCase() === v.city?.toLowerCase()) === i
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
count: unique.length,
|
||||
data: unique,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore ricerca città italiane:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante la ricerca',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Calcola distanza e durata tra due punti (semplificato)
|
||||
* @route GET /api/geo/distance
|
||||
*/
|
||||
const getDistance = async (req, res) => {
|
||||
try {
|
||||
const { startLat, startLng, endLat, endLng, profile = 'driving-car' } = req.query;
|
||||
|
||||
if (!startLat || !startLng || !endLat || !endLng) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Tutte le coordinate sono richieste',
|
||||
});
|
||||
}
|
||||
|
||||
const url = `${ORS_BASE}/v2/directions/${profile}`;
|
||||
|
||||
const body = {
|
||||
coordinates: [
|
||||
[parseFloat(startLng), parseFloat(startLat)],
|
||||
[parseFloat(endLng), parseFloat(endLat)],
|
||||
],
|
||||
geometry: false,
|
||||
instructions: false,
|
||||
};
|
||||
|
||||
const data = await makeRequest(url, 'POST', body);
|
||||
|
||||
if (!data.routes || data.routes.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Impossibile calcolare la distanza',
|
||||
});
|
||||
}
|
||||
|
||||
const summary = data.routes[0].summary;
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
distance: Math.round(summary.distance * 10) / 10, // km
|
||||
duration: Math.round(summary.duration / 60), // minuti
|
||||
durationFormatted: formatDuration(summary.duration),
|
||||
profile,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore calcolo distanza:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante il calcolo della distanza',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Ottieni isocrone (aree raggiungibili in X minuti)
|
||||
* @route GET /api/geo/isochrone
|
||||
*/
|
||||
const getIsochrone = async (req, res) => {
|
||||
try {
|
||||
const { lat, lng, minutes = 30, profile = 'driving-car' } = req.query;
|
||||
|
||||
if (!lat || !lng) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Coordinate richieste',
|
||||
});
|
||||
}
|
||||
|
||||
const url = `${ORS_BASE}/v2/isochrones/${profile}`;
|
||||
|
||||
const body = {
|
||||
locations: [[parseFloat(lng), parseFloat(lat)]],
|
||||
range: [parseInt(minutes) * 60], // secondi
|
||||
range_type: 'time',
|
||||
};
|
||||
|
||||
const data = await makeRequest(url, 'POST', body);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
type: 'FeatureCollection',
|
||||
features: data.features,
|
||||
center: { lat: parseFloat(lat), lng: parseFloat(lng) },
|
||||
minutes: parseInt(minutes),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore calcolo isocrone:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante il calcolo isocrone',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// HELPER FUNCTIONS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Formatta durata in formato leggibile
|
||||
*/
|
||||
const formatDuration = (seconds) => {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.round((seconds % 3600) / 60);
|
||||
|
||||
if (hours === 0) {
|
||||
return `${minutes} min`;
|
||||
} else if (minutes === 0) {
|
||||
return `${hours} h`;
|
||||
} else {
|
||||
return `${hours} h ${minutes} min`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Decodifica polyline encoded (formato Google/ORS)
|
||||
*/
|
||||
const decodePolyline = (encoded) => {
|
||||
const points = [];
|
||||
let index = 0;
|
||||
let lat = 0;
|
||||
let lng = 0;
|
||||
|
||||
while (index < encoded.length) {
|
||||
let b;
|
||||
let shift = 0;
|
||||
let result = 0;
|
||||
|
||||
do {
|
||||
b = encoded.charCodeAt(index++) - 63;
|
||||
result |= (b & 0x1f) << shift;
|
||||
shift += 5;
|
||||
} while (b >= 0x20);
|
||||
|
||||
const dlat = result & 1 ? ~(result >> 1) : result >> 1;
|
||||
lat += dlat;
|
||||
|
||||
shift = 0;
|
||||
result = 0;
|
||||
|
||||
do {
|
||||
b = encoded.charCodeAt(index++) - 63;
|
||||
result |= (b & 0x1f) << shift;
|
||||
shift += 5;
|
||||
} while (b >= 0x20);
|
||||
|
||||
const dlng = result & 1 ? ~(result >> 1) : result >> 1;
|
||||
lng += dlng;
|
||||
|
||||
points.push([lng / 1e5, lat / 1e5]); // [lng, lat] formato GeoJSON
|
||||
}
|
||||
|
||||
return points;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
autocomplete,
|
||||
geocode,
|
||||
reverseGeocode,
|
||||
getRoute,
|
||||
getMatrix,
|
||||
suggestWaypoints,
|
||||
searchItalianCities,
|
||||
getDistance,
|
||||
getIsochrone,
|
||||
};
|
||||
522
src/controllers/geocodingController_OLD.js
Normal file
522
src/controllers/geocodingController_OLD.js
Normal file
@@ -0,0 +1,522 @@
|
||||
/**
|
||||
* Controller per Geocoding usando servizi Open Source
|
||||
* - Nominatim (OpenStreetMap) per geocoding/reverse
|
||||
* - OSRM per routing
|
||||
* - Photon per autocomplete
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
|
||||
// Configurazione servizi
|
||||
const NOMINATIM_BASE = 'https://nominatim.openstreetmap.org';
|
||||
const PHOTON_BASE = 'https://photon.komoot.io';
|
||||
const OSRM_BASE = 'https://router.project-osrm.org';
|
||||
|
||||
// User-Agent richiesto da Nominatim
|
||||
const USER_AGENT = 'FreePlanetApp/1.0';
|
||||
|
||||
/**
|
||||
* Helper per fare richieste HTTP/HTTPS
|
||||
*/
|
||||
const makeRequest = (url) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = url.startsWith('https') ? https : http;
|
||||
|
||||
const req = client.get(url, {
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
}, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
resolve(JSON.parse(data));
|
||||
} catch (e) {
|
||||
reject(new Error('Errore parsing risposta'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.setTimeout(10000, () => {
|
||||
req.destroy();
|
||||
reject(new Error('Timeout richiesta'));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Autocomplete città (Photon API)
|
||||
* @route GET /api/viaggi/geo/autocomplete
|
||||
*/
|
||||
const autocomplete = async (req, res) => {
|
||||
try {
|
||||
const { q, limit = 5, lang = 'it' } = req.query;
|
||||
|
||||
if (!q || q.length < 2) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Query deve essere almeno 2 caratteri'
|
||||
});
|
||||
}
|
||||
|
||||
// Photon API - gratuito e veloce
|
||||
const url = `${PHOTON_BASE}/api/?q=${encodeURIComponent(q)}&limit=${limit}&lang=${lang}&osm_tag=place:city&osm_tag=place:town&osm_tag=place:village`;
|
||||
|
||||
const data = await makeRequest(url);
|
||||
|
||||
// Formatta risultati
|
||||
const results = data.features.map(feature => ({
|
||||
city: feature.properties.name,
|
||||
province: feature.properties.county || feature.properties.state,
|
||||
region: feature.properties.state,
|
||||
country: feature.properties.country,
|
||||
postalCode: feature.properties.postcode,
|
||||
coordinates: {
|
||||
lat: feature.geometry.coordinates[1],
|
||||
lng: feature.geometry.coordinates[0]
|
||||
},
|
||||
displayName: [
|
||||
feature.properties.name,
|
||||
feature.properties.county,
|
||||
feature.properties.state,
|
||||
feature.properties.country
|
||||
].filter(Boolean).join(', '),
|
||||
type: feature.properties.osm_value || 'place'
|
||||
}));
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Errore autocomplete:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante la ricerca',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Geocoding - indirizzo a coordinate (Nominatim)
|
||||
* @route GET /api/viaggi/geo/geocode
|
||||
*/
|
||||
const geocode = async (req, res) => {
|
||||
try {
|
||||
const { address, city, country = 'Italy' } = req.query;
|
||||
|
||||
const searchQuery = [address, city, country].filter(Boolean).join(', ');
|
||||
|
||||
if (!searchQuery) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Fornisci un indirizzo o città da cercare'
|
||||
});
|
||||
}
|
||||
|
||||
const url = `${NOMINATIM_BASE}/search?format=json&q=${encodeURIComponent(searchQuery)}&limit=5&addressdetails=1`;
|
||||
|
||||
const data = await makeRequest(url);
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Nessun risultato trovato'
|
||||
});
|
||||
}
|
||||
|
||||
const results = data.map(item => ({
|
||||
displayName: item.display_name,
|
||||
city: item.address.city || item.address.town || item.address.village || item.address.municipality,
|
||||
address: item.address.road ? `${item.address.road}${item.address.house_number ? ' ' + item.address.house_number : ''}` : null,
|
||||
province: item.address.county || item.address.province,
|
||||
region: item.address.state,
|
||||
country: item.address.country,
|
||||
postalCode: item.address.postcode,
|
||||
coordinates: {
|
||||
lat: parseFloat(item.lat),
|
||||
lng: parseFloat(item.lon)
|
||||
},
|
||||
type: item.type,
|
||||
importance: item.importance
|
||||
}));
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Errore geocoding:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante il geocoding',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Reverse geocoding - coordinate a indirizzo (Nominatim)
|
||||
* @route GET /api/viaggi/geo/reverse
|
||||
*/
|
||||
const reverseGeocode = async (req, res) => {
|
||||
try {
|
||||
const { lat, lng } = req.query;
|
||||
|
||||
if (!lat || !lng) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Coordinate lat e lng richieste'
|
||||
});
|
||||
}
|
||||
|
||||
const url = `${NOMINATIM_BASE}/reverse?format=json&lat=${lat}&lon=${lng}&addressdetails=1`;
|
||||
|
||||
const data = await makeRequest(url);
|
||||
|
||||
if (!data || data.error) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Nessun risultato trovato'
|
||||
});
|
||||
}
|
||||
|
||||
const result = {
|
||||
displayName: data.display_name,
|
||||
city: data.address.city || data.address.town || data.address.village || data.address.municipality,
|
||||
address: data.address.road ? `${data.address.road}${data.address.house_number ? ' ' + data.address.house_number : ''}` : null,
|
||||
province: data.address.county || data.address.province,
|
||||
region: data.address.state,
|
||||
country: data.address.country,
|
||||
postalCode: data.address.postcode,
|
||||
coordinates: {
|
||||
lat: parseFloat(lat),
|
||||
lng: parseFloat(lng)
|
||||
}
|
||||
};
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Errore reverse geocoding:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante il reverse geocoding',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Calcola percorso tra due punti (OSRM)
|
||||
* @route GET /api/viaggi/geo/route
|
||||
*/
|
||||
const getRoute = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
startLat, startLng,
|
||||
endLat, endLng,
|
||||
waypoints // formato: "lat1,lng1;lat2,lng2;..."
|
||||
} = req.query;
|
||||
|
||||
if (!startLat || !startLng || !endLat || !endLng) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Coordinate di partenza e arrivo richieste'
|
||||
});
|
||||
}
|
||||
|
||||
// Costruisci stringa coordinate
|
||||
let coordinates = `${startLng},${startLat}`;
|
||||
|
||||
if (waypoints) {
|
||||
const waypointsList = waypoints.split(';');
|
||||
waypointsList.forEach(wp => {
|
||||
const [lat, lng] = wp.split(',');
|
||||
coordinates += `;${lng},${lat}`;
|
||||
});
|
||||
}
|
||||
|
||||
coordinates += `;${endLng},${endLat}`;
|
||||
|
||||
const url = `${OSRM_BASE}/route/v1/driving/${coordinates}?overview=full&geometries=polyline&steps=true`;
|
||||
|
||||
const data = await makeRequest(url);
|
||||
|
||||
if (!data || data.code !== 'Ok' || !data.routes || data.routes.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Impossibile calcolare il percorso'
|
||||
});
|
||||
}
|
||||
|
||||
const route = data.routes[0];
|
||||
|
||||
// Estrai città attraversate (dalle istruzioni)
|
||||
const citiesAlongRoute = [];
|
||||
if (route.legs) {
|
||||
route.legs.forEach(leg => {
|
||||
if (leg.steps) {
|
||||
leg.steps.forEach(step => {
|
||||
if (step.name && step.name.length > 0) {
|
||||
// Qui potresti fare reverse geocoding per ottenere città
|
||||
// Per ora usiamo i nomi delle strade principali
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const result = {
|
||||
distance: Math.round(route.distance / 1000 * 10) / 10, // km
|
||||
duration: Math.round(route.duration / 60), // minuti
|
||||
polyline: route.geometry, // Polyline encoded
|
||||
legs: route.legs.map(leg => ({
|
||||
distance: Math.round(leg.distance / 1000 * 10) / 10,
|
||||
duration: Math.round(leg.duration / 60),
|
||||
summary: leg.summary,
|
||||
steps: leg.steps ? leg.steps.slice(0, 10).map(s => ({ // Limita step
|
||||
instruction: s.maneuver ? s.maneuver.instruction : '',
|
||||
name: s.name,
|
||||
distance: Math.round(s.distance),
|
||||
duration: Math.round(s.duration / 60)
|
||||
})) : []
|
||||
}))
|
||||
};
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Errore calcolo percorso:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante il calcolo del percorso',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Suggerisci città intermedie su un percorso
|
||||
* @route GET /api/viaggi/geo/suggest-waypoints
|
||||
*/
|
||||
const suggestWaypoints = async (req, res) => {
|
||||
try {
|
||||
const { startLat, startLng, endLat, endLng } = req.query;
|
||||
|
||||
if (!startLat || !startLng || !endLat || !endLng) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Coordinate di partenza e arrivo richieste'
|
||||
});
|
||||
}
|
||||
|
||||
// Prima ottieni il percorso
|
||||
const routeUrl = `${OSRM_BASE}/route/v1/driving/${startLng},${startLat};${endLng},${endLat}?overview=full&geometries=geojson`;
|
||||
|
||||
const routeData = await makeRequest(routeUrl);
|
||||
|
||||
if (!routeData || routeData.code !== 'Ok') {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Impossibile calcolare il percorso'
|
||||
});
|
||||
}
|
||||
|
||||
// Prendi punti lungo il percorso (ogni ~50km circa)
|
||||
const coordinates = routeData.routes[0].geometry.coordinates;
|
||||
const totalPoints = coordinates.length;
|
||||
const step = Math.max(1, Math.floor(totalPoints / 6)); // ~5 punti intermedi
|
||||
|
||||
const sampledPoints = [];
|
||||
for (let i = step; i < totalPoints - step; i += step) {
|
||||
sampledPoints.push(coordinates[i]);
|
||||
}
|
||||
|
||||
// Fai reverse geocoding per ogni punto
|
||||
const cities = [];
|
||||
const seenCities = new Set();
|
||||
|
||||
for (const point of sampledPoints.slice(0, 5)) { // Limita a 5 richieste
|
||||
try {
|
||||
const reverseUrl = `${NOMINATIM_BASE}/reverse?format=json&lat=${point[1]}&lon=${point[0]}&addressdetails=1&zoom=10`;
|
||||
const data = await makeRequest(reverseUrl);
|
||||
|
||||
if (data && data.address) {
|
||||
const cityName = data.address.city || data.address.town || data.address.village;
|
||||
if (cityName && !seenCities.has(cityName.toLowerCase())) {
|
||||
seenCities.add(cityName.toLowerCase());
|
||||
cities.push({
|
||||
city: cityName,
|
||||
province: data.address.county || data.address.province,
|
||||
region: data.address.state,
|
||||
coordinates: {
|
||||
lat: point[1],
|
||||
lng: point[0]
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Rate limiting - aspetta 1 secondo tra le richieste (requisito Nominatim)
|
||||
await new Promise(resolve => setTimeout(resolve, 1100));
|
||||
} catch (e) {
|
||||
console.log('Errore reverse per punto:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: cities
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Errore suggerimento waypoints:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante il suggerimento delle tappe',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Cerca città italiane (ottimizzato per Italia)
|
||||
* @route GET /api/viaggi/geo/cities/it
|
||||
*/
|
||||
const searchItalianCities = async (req, res) => {
|
||||
try {
|
||||
const { q, limit = 10 } = req.query;
|
||||
|
||||
if (!q || q.length < 2) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Query deve essere almeno 2 caratteri'
|
||||
});
|
||||
}
|
||||
|
||||
// Usa Nominatim con filtro Italia
|
||||
const url = `${NOMINATIM_BASE}/search?format=json&q=${encodeURIComponent(q)}&countrycodes=it&limit=${limit}&addressdetails=1&featuretype=city`;
|
||||
|
||||
const data = await makeRequest(url);
|
||||
|
||||
const results = data
|
||||
.filter(item =>
|
||||
item.address &&
|
||||
(item.address.city || item.address.town || item.address.village)
|
||||
)
|
||||
.map(item => ({
|
||||
city: item.address.city || item.address.town || item.address.village,
|
||||
province: item.address.county || item.address.province,
|
||||
region: item.address.state,
|
||||
postalCode: item.address.postcode,
|
||||
coordinates: {
|
||||
lat: parseFloat(item.lat),
|
||||
lng: parseFloat(item.lon)
|
||||
},
|
||||
displayName: `${item.address.city || item.address.town || item.address.village}, ${item.address.county || item.address.state}`
|
||||
}));
|
||||
|
||||
// Rimuovi duplicati
|
||||
const unique = results.filter((v, i, a) =>
|
||||
a.findIndex(t => t.city.toLowerCase() === v.city.toLowerCase()) === i
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: unique
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Errore ricerca città italiane:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante la ricerca',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Calcola distanza e durata tra due punti
|
||||
* @route GET /api/viaggi/geo/distance
|
||||
*/
|
||||
const getDistance = async (req, res) => {
|
||||
try {
|
||||
const { startLat, startLng, endLat, endLng } = req.query;
|
||||
|
||||
if (!startLat || !startLng || !endLat || !endLng) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Tutte le coordinate sono richieste'
|
||||
});
|
||||
}
|
||||
|
||||
const url = `${OSRM_BASE}/route/v1/driving/${startLng},${startLat};${endLng},${endLat}?overview=false`;
|
||||
|
||||
const data = await makeRequest(url);
|
||||
|
||||
if (!data || data.code !== 'Ok' || !data.routes || data.routes.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Impossibile calcolare la distanza'
|
||||
});
|
||||
}
|
||||
|
||||
const route = data.routes[0];
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
distance: Math.round(route.distance / 1000 * 10) / 10, // km
|
||||
duration: Math.round(route.duration / 60), // minuti
|
||||
durationFormatted: formatDuration(route.duration)
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Errore calcolo distanza:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante il calcolo della distanza',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Helper per formattare durata
|
||||
const formatDuration = (seconds) => {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.round((seconds % 3600) / 60);
|
||||
|
||||
if (hours === 0) {
|
||||
return `${minutes} min`;
|
||||
} else if (minutes === 0) {
|
||||
return `${hours} h`;
|
||||
} else {
|
||||
return `${hours} h ${minutes} min`;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
autocomplete,
|
||||
geocode,
|
||||
reverseGeocode,
|
||||
getRoute,
|
||||
suggestWaypoints,
|
||||
searchItalianCities,
|
||||
getDistance
|
||||
};
|
||||
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 || './upload';
|
||||
|
||||
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: `/upload/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: `/upload/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: `/upload/posters/final/${path.basename(result.pngPath)}`,
|
||||
size: result.pngSize
|
||||
},
|
||||
jpg: {
|
||||
path: result.jpgPath,
|
||||
url: `/upload/posters/final/${path.basename(result.jpgPath)}`,
|
||||
size: result.jpgSize,
|
||||
quality: 95
|
||||
},
|
||||
dimensions: result.dimensions,
|
||||
duration: result.duration
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = posterController;
|
||||
2069
src/controllers/rideController.js
Normal file
2069
src/controllers/rideController.js
Normal file
File diff suppressed because it is too large
Load Diff
1059
src/controllers/rideRequestController.js
Normal file
1059
src/controllers/rideRequestController.js
Normal file
File diff suppressed because it is too large
Load Diff
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;
|
||||
847
src/controllers/viaggi/TrasportiNotifications.js
Normal file
847
src/controllers/viaggi/TrasportiNotifications.js
Normal file
@@ -0,0 +1,847 @@
|
||||
/**
|
||||
* TrasportiNotifications.js
|
||||
*
|
||||
* Servizio notifiche centralizzato per Trasporti Solidali.
|
||||
* USA il telegrambot.js esistente per Telegram, AGGIUNGE Email e Push.
|
||||
*
|
||||
* NON MODIFICA telegrambot.js - lo importa e usa i suoi metodi.
|
||||
*/
|
||||
|
||||
const nodemailer = require('nodemailer');
|
||||
const webpush = require('web-push');
|
||||
|
||||
// Importa il tuo telegrambot esistente
|
||||
const MyTelegramBot = require('../../telegram/telegrambot');
|
||||
|
||||
// =============================================================================
|
||||
// CONFIGURAZIONE
|
||||
// =============================================================================
|
||||
|
||||
const config = {
|
||||
// Email SMTP
|
||||
smtp: {
|
||||
host: process.env.SMTP_HOST || 'smtp.gmail.com',
|
||||
port: parseInt(process.env.SMTP_PORT) || 465,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS
|
||||
}
|
||||
},
|
||||
emailFrom: process.env.SMTP_FROM || 'noreply@trasporti.app',
|
||||
|
||||
// Push VAPID
|
||||
vapidPublicKey: process.env.VAPID_PUBLIC_KEY,
|
||||
vapidPrivateKey: process.env.VAPID_PRIVATE_KEY,
|
||||
vapidEmail: process.env.VAPID_EMAIL || 'admin@trasporti.app',
|
||||
|
||||
// App
|
||||
appName: process.env.APP_NAME || 'Trasporti Solidali',
|
||||
appUrl: process.env.APP_URL || 'https://trasporti.app'
|
||||
};
|
||||
|
||||
// Configura web-push se le chiavi sono presenti
|
||||
if (config.vapidPublicKey && config.vapidPrivateKey) {
|
||||
webpush.setVapidDetails(
|
||||
`mailto:${config.vapidEmail}`,
|
||||
config.vapidPublicKey,
|
||||
config.vapidPrivateKey
|
||||
);
|
||||
}
|
||||
|
||||
// Crea transporter email
|
||||
let emailTransporter = null;
|
||||
if (config.smtp.auth.user && config.smtp.auth.pass) {
|
||||
emailTransporter = nodemailer.createTransport(config.smtp);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TIPI DI NOTIFICA
|
||||
// =============================================================================
|
||||
|
||||
const NotificationType = {
|
||||
// Viaggi
|
||||
NEW_RIDE_REQUEST: 'new_ride_request',
|
||||
REQUEST_ACCEPTED: 'request_accepted',
|
||||
REQUEST_REJECTED: 'request_rejected',
|
||||
RIDE_REMINDER_24H: 'ride_reminder_24h',
|
||||
RIDE_REMINDER_2H: 'ride_reminder_2h',
|
||||
RIDE_CANCELLED: 'ride_cancelled',
|
||||
RIDE_MODIFIED: 'ride_modified',
|
||||
|
||||
// Messaggi
|
||||
NEW_MESSAGE: 'new_message',
|
||||
|
||||
// Community
|
||||
NEW_COMMUNITY_RIDE: 'new_community_ride',
|
||||
|
||||
// Sistema
|
||||
WEEKLY_DIGEST: 'weekly_digest',
|
||||
TEST: 'test',
|
||||
WELCOME: 'welcome'
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// EMOJI PER NOTIFICHE
|
||||
// =============================================================================
|
||||
|
||||
const emo = {
|
||||
CAR: '🚗',
|
||||
PASSENGER: '🧑🤝🧑',
|
||||
CHECK: '✅',
|
||||
CROSS: '❌',
|
||||
BELL: '🔔',
|
||||
CLOCK: '⏰',
|
||||
CALENDAR: '📅',
|
||||
PIN: '📍',
|
||||
ARROW: '➡️',
|
||||
MESSAGE: '💬',
|
||||
STAR: '⭐',
|
||||
WARNING: '⚠️',
|
||||
INFO: 'ℹ️',
|
||||
WAVE: '👋',
|
||||
HEART: '❤️'
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// TRADUZIONI NOTIFICHE
|
||||
// =============================================================================
|
||||
|
||||
const translations = {
|
||||
it: {
|
||||
// Richieste
|
||||
NEW_RIDE_REQUEST_TITLE: 'Nuova richiesta di passaggio',
|
||||
NEW_RIDE_REQUEST_BODY: '{{passengerName}} chiede un passaggio per il viaggio {{departure}} → {{destination}} del {{date}}',
|
||||
NEW_RIDE_REQUEST_ACTION: 'Visualizza richiesta',
|
||||
|
||||
// Accettazione
|
||||
REQUEST_ACCEPTED_TITLE: 'Richiesta accettata!',
|
||||
REQUEST_ACCEPTED_BODY: '{{driverName}} ha accettato la tua richiesta per {{departure}} → {{destination}} del {{date}}',
|
||||
REQUEST_ACCEPTED_ACTION: 'Visualizza viaggio',
|
||||
|
||||
// Rifiuto
|
||||
REQUEST_REJECTED_TITLE: 'Richiesta non accettata',
|
||||
REQUEST_REJECTED_BODY: '{{driverName}} non ha potuto accettare la tua richiesta per {{departure}} → {{destination}}',
|
||||
|
||||
// Promemoria
|
||||
RIDE_REMINDER_24H_TITLE: 'Viaggio domani!',
|
||||
RIDE_REMINDER_24H_BODY: 'Promemoria: domani hai un viaggio {{departure}} → {{destination}} alle {{time}}',
|
||||
RIDE_REMINDER_2H_TITLE: 'Viaggio tra 2 ore!',
|
||||
RIDE_REMINDER_2H_BODY: 'Il tuo viaggio {{departure}} → {{destination}} parte tra 2 ore alle {{time}}',
|
||||
|
||||
// Cancellazione
|
||||
RIDE_CANCELLED_TITLE: 'Viaggio cancellato',
|
||||
RIDE_CANCELLED_BODY: 'Il viaggio {{departure}} → {{destination}} del {{date}} è stato cancellato',
|
||||
RIDE_CANCELLED_REASON: 'Motivo: {{reason}}',
|
||||
|
||||
// Modifica
|
||||
RIDE_MODIFIED_TITLE: 'Viaggio modificato',
|
||||
RIDE_MODIFIED_BODY: 'Il viaggio {{departure}} → {{destination}} è stato modificato. Verifica i nuovi dettagli.',
|
||||
|
||||
// Messaggi
|
||||
NEW_MESSAGE_TITLE: 'Nuovo messaggio',
|
||||
NEW_MESSAGE_BODY: '{{senderName}}: {{preview}}',
|
||||
|
||||
// Community
|
||||
NEW_COMMUNITY_RIDE_TITLE: 'Nuovo viaggio nella tua zona',
|
||||
NEW_COMMUNITY_RIDE_BODY: 'Nuovo viaggio disponibile: {{departure}} → {{destination}} il {{date}}',
|
||||
|
||||
// Test
|
||||
TEST_TITLE: 'Notifica di test',
|
||||
TEST_BODY: 'Questa è una notifica di test da Trasporti Solidali. Se la vedi, tutto funziona!',
|
||||
|
||||
// Welcome
|
||||
WELCOME_TITLE: 'Benvenuto su Trasporti Solidali!',
|
||||
WELCOME_BODY: 'Le notifiche sono state attivate correttamente. Riceverai aggiornamenti sui tuoi viaggi.',
|
||||
|
||||
// Common
|
||||
VIEW_DETAILS: 'Visualizza dettagli',
|
||||
REPLY: 'Rispondi'
|
||||
},
|
||||
|
||||
en: {
|
||||
NEW_RIDE_REQUEST_TITLE: 'New ride request',
|
||||
NEW_RIDE_REQUEST_BODY: '{{passengerName}} requests a ride for {{departure}} → {{destination}} on {{date}}',
|
||||
NEW_RIDE_REQUEST_ACTION: 'View request',
|
||||
|
||||
REQUEST_ACCEPTED_TITLE: 'Request accepted!',
|
||||
REQUEST_ACCEPTED_BODY: '{{driverName}} accepted your request for {{departure}} → {{destination}} on {{date}}',
|
||||
REQUEST_ACCEPTED_ACTION: 'View ride',
|
||||
|
||||
REQUEST_REJECTED_TITLE: 'Request not accepted',
|
||||
REQUEST_REJECTED_BODY: '{{driverName}} could not accept your request for {{departure}} → {{destination}}',
|
||||
|
||||
RIDE_REMINDER_24H_TITLE: 'Ride tomorrow!',
|
||||
RIDE_REMINDER_24H_BODY: 'Reminder: tomorrow you have a ride {{departure}} → {{destination}} at {{time}}',
|
||||
RIDE_REMINDER_2H_TITLE: 'Ride in 2 hours!',
|
||||
RIDE_REMINDER_2H_BODY: 'Your ride {{departure}} → {{destination}} leaves in 2 hours at {{time}}',
|
||||
|
||||
RIDE_CANCELLED_TITLE: 'Ride cancelled',
|
||||
RIDE_CANCELLED_BODY: 'The ride {{departure}} → {{destination}} on {{date}} has been cancelled',
|
||||
RIDE_CANCELLED_REASON: 'Reason: {{reason}}',
|
||||
|
||||
RIDE_MODIFIED_TITLE: 'Ride modified',
|
||||
RIDE_MODIFIED_BODY: 'The ride {{departure}} → {{destination}} has been modified. Check the new details.',
|
||||
|
||||
NEW_MESSAGE_TITLE: 'New message',
|
||||
NEW_MESSAGE_BODY: '{{senderName}}: {{preview}}',
|
||||
|
||||
NEW_COMMUNITY_RIDE_TITLE: 'New ride in your area',
|
||||
NEW_COMMUNITY_RIDE_BODY: 'New ride available: {{departure}} → {{destination}} on {{date}}',
|
||||
|
||||
TEST_TITLE: 'Test notification',
|
||||
TEST_BODY: 'This is a test notification from Trasporti Solidali. If you see this, everything works!',
|
||||
|
||||
WELCOME_TITLE: 'Welcome to Trasporti Solidali!',
|
||||
WELCOME_BODY: 'Notifications have been enabled successfully. You will receive updates about your rides.',
|
||||
|
||||
VIEW_DETAILS: 'View details',
|
||||
REPLY: 'Reply'
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Ottiene traduzione con sostituzione variabili
|
||||
*/
|
||||
function getTranslation(lang, key, data = {}) {
|
||||
const langTranslations = translations[lang] || translations['it'];
|
||||
let text = langTranslations[key] || translations['it'][key] || key;
|
||||
|
||||
// Sostituisci {{variabile}}
|
||||
Object.keys(data).forEach(varName => {
|
||||
const regex = new RegExp(`\\{\\{${varName}\\}\\}`, 'g');
|
||||
text = text.replace(regex, data[varName] || '');
|
||||
});
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappa tipo notifica a chiave preferenze
|
||||
*/
|
||||
function getPreferenceKey(type) {
|
||||
const map = {
|
||||
[NotificationType.NEW_RIDE_REQUEST]: 'newRideRequest',
|
||||
[NotificationType.REQUEST_ACCEPTED]: 'requestAccepted',
|
||||
[NotificationType.REQUEST_REJECTED]: 'requestRejected',
|
||||
[NotificationType.RIDE_REMINDER_24H]: 'rideReminder24h',
|
||||
[NotificationType.RIDE_REMINDER_2H]: 'rideReminder2h',
|
||||
[NotificationType.RIDE_CANCELLED]: 'rideCancelled',
|
||||
[NotificationType.RIDE_MODIFIED]: 'rideCancelled',
|
||||
[NotificationType.NEW_MESSAGE]: 'newMessage',
|
||||
[NotificationType.NEW_COMMUNITY_RIDE]: 'newCommunityRide',
|
||||
[NotificationType.WEEKLY_DIGEST]: 'weeklyDigest',
|
||||
[NotificationType.TEST]: null, // Sempre inviato
|
||||
[NotificationType.WELCOME]: null // Sempre inviato
|
||||
};
|
||||
return map[type];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se inviare notifica su un canale
|
||||
*/
|
||||
function shouldSend(prefs, channel, type) {
|
||||
if (!prefs) return false;
|
||||
|
||||
const channelPrefs = prefs[channel];
|
||||
if (!channelPrefs || !channelPrefs.enabled) return false;
|
||||
|
||||
// Test e Welcome sempre inviati se canale abilitato
|
||||
if (type === NotificationType.TEST || type === NotificationType.WELCOME) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const prefKey = getPreferenceKey(type);
|
||||
if (!prefKey) return true; // Se non mappato, invia
|
||||
|
||||
return channelPrefs[prefKey] !== false; // Default true
|
||||
}
|
||||
|
||||
/**
|
||||
* Tronca testo
|
||||
*/
|
||||
function truncate(text, maxLength = 100) {
|
||||
if (!text || text.length <= maxLength) return text;
|
||||
return text.substring(0, maxLength - 3) + '...';
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EMAIL TEMPLATES
|
||||
// =============================================================================
|
||||
|
||||
function buildEmailHtml(type, data, lang = 'it') {
|
||||
const t = (key) => getTranslation(lang, key, data);
|
||||
|
||||
const title = t(`${type.toUpperCase()}_TITLE`);
|
||||
const body = t(`${type.toUpperCase()}_BODY`);
|
||||
|
||||
// Colori per tipo
|
||||
const colors = {
|
||||
[NotificationType.NEW_RIDE_REQUEST]: '#667eea',
|
||||
[NotificationType.REQUEST_ACCEPTED]: '#21ba45',
|
||||
[NotificationType.REQUEST_REJECTED]: '#c10015',
|
||||
[NotificationType.RIDE_REMINDER_24H]: '#f2711c',
|
||||
[NotificationType.RIDE_REMINDER_2H]: '#db2828',
|
||||
[NotificationType.RIDE_CANCELLED]: '#c10015',
|
||||
[NotificationType.NEW_MESSAGE]: '#2185d0',
|
||||
[NotificationType.NEW_COMMUNITY_RIDE]: '#a333c8',
|
||||
[NotificationType.TEST]: '#667eea',
|
||||
[NotificationType.WELCOME]: '#21ba45'
|
||||
};
|
||||
|
||||
const color = colors[type] || '#667eea';
|
||||
|
||||
// Emoji per tipo
|
||||
const emojis = {
|
||||
[NotificationType.NEW_RIDE_REQUEST]: emo.PASSENGER,
|
||||
[NotificationType.REQUEST_ACCEPTED]: emo.CHECK,
|
||||
[NotificationType.REQUEST_REJECTED]: emo.CROSS,
|
||||
[NotificationType.RIDE_REMINDER_24H]: emo.CALENDAR,
|
||||
[NotificationType.RIDE_REMINDER_2H]: emo.CLOCK,
|
||||
[NotificationType.RIDE_CANCELLED]: emo.WARNING,
|
||||
[NotificationType.NEW_MESSAGE]: emo.MESSAGE,
|
||||
[NotificationType.NEW_COMMUNITY_RIDE]: emo.CAR,
|
||||
[NotificationType.TEST]: emo.BELL,
|
||||
[NotificationType.WELCOME]: emo.WAVE
|
||||
};
|
||||
|
||||
const emoji = emojis[type] || emo.BELL;
|
||||
|
||||
// CTA button
|
||||
let ctaHtml = '';
|
||||
if (data.actionUrl) {
|
||||
const actionText = data.actionText || t('VIEW_DETAILS');
|
||||
ctaHtml = `
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${data.actionUrl}"
|
||||
style="display: inline-block; padding: 14px 32px; background: ${color};
|
||||
color: white; text-decoration: none; border-radius: 8px;
|
||||
font-weight: 600; font-size: 16px;">
|
||||
${actionText}
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Info viaggio
|
||||
let rideInfoHtml = '';
|
||||
if (data.departure && data.destination) {
|
||||
rideInfoHtml = `
|
||||
<div style="background: #f8f9fa; border-radius: 12px; padding: 20px; margin: 20px 0;">
|
||||
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;">
|
||||
<span style="font-size: 24px;">${emo.PIN}</span>
|
||||
<div>
|
||||
<div style="color: #666; font-size: 12px;">Partenza</div>
|
||||
<div style="font-weight: 600; font-size: 16px;">${data.departure}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: center; color: #999; margin: 10px 0;">${emo.ARROW}</div>
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<span style="font-size: 24px;">${emo.PIN}</span>
|
||||
<div>
|
||||
<div style="color: #666; font-size: 12px;">Destinazione</div>
|
||||
<div style="font-weight: 600; font-size: 16px;">${data.destination}</div>
|
||||
</div>
|
||||
</div>
|
||||
${data.date ? `
|
||||
<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #e0e0e0;">
|
||||
<span style="color: #666;">${emo.CALENDAR} ${data.date}</span>
|
||||
${data.time ? `<span style="margin-left: 15px; color: #666;">${emo.CLOCK} ${data.time}</span>` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5;">
|
||||
<div style="max-width: 600px; margin: 0 auto; background: white;">
|
||||
<!-- Header -->
|
||||
<div style="background: linear-gradient(135deg, ${color} 0%, ${color}dd 100%); padding: 30px 20px; text-align: center;">
|
||||
<div style="font-size: 48px; margin-bottom: 10px;">${emoji}</div>
|
||||
<h1 style="margin: 0; color: white; font-size: 24px; font-weight: 600;">${title}</h1>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div style="padding: 30px 20px;">
|
||||
<p style="font-size: 16px; line-height: 1.6; color: #333; margin: 0 0 20px;">
|
||||
${body}
|
||||
</p>
|
||||
|
||||
${rideInfoHtml}
|
||||
|
||||
${data.reason ? `<p style="color: #666; font-style: italic;">${t('RIDE_CANCELLED_REASON')}</p>` : ''}
|
||||
|
||||
${ctaHtml}
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div style="background: #f8f9fa; padding: 20px; text-align: center; border-top: 1px solid #e0e0e0;">
|
||||
<p style="margin: 0 0 10px; color: #666; font-size: 14px;">
|
||||
${config.appName}
|
||||
</p>
|
||||
<p style="margin: 0; color: #999; font-size: 12px;">
|
||||
Ricevi questa email perché hai attivato le notifiche.
|
||||
<a href="${config.appUrl}/impostazioni" style="color: ${color};">Gestisci preferenze</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TELEGRAM MESSAGE BUILDER
|
||||
// =============================================================================
|
||||
|
||||
function buildTelegramMessage(type, data, lang = 'it') {
|
||||
const t = (key) => getTranslation(lang, key, data);
|
||||
|
||||
const title = t(`${type.toUpperCase()}_TITLE`);
|
||||
const body = t(`${type.toUpperCase()}_BODY`);
|
||||
|
||||
// Emoji per tipo
|
||||
const emojis = {
|
||||
[NotificationType.NEW_RIDE_REQUEST]: emo.PASSENGER,
|
||||
[NotificationType.REQUEST_ACCEPTED]: emo.CHECK,
|
||||
[NotificationType.REQUEST_REJECTED]: emo.CROSS,
|
||||
[NotificationType.RIDE_REMINDER_24H]: emo.CALENDAR,
|
||||
[NotificationType.RIDE_REMINDER_2H]: emo.CLOCK,
|
||||
[NotificationType.RIDE_CANCELLED]: emo.WARNING,
|
||||
[NotificationType.NEW_MESSAGE]: emo.MESSAGE,
|
||||
[NotificationType.NEW_COMMUNITY_RIDE]: emo.CAR,
|
||||
[NotificationType.TEST]: emo.BELL,
|
||||
[NotificationType.WELCOME]: emo.WAVE
|
||||
};
|
||||
|
||||
const emoji = emojis[type] || emo.BELL;
|
||||
|
||||
let message = `${emoji} <b>${title}</b>\n\n${body}`;
|
||||
|
||||
// Aggiungi info viaggio
|
||||
if (data.departure && data.destination) {
|
||||
message += `\n\n${emo.PIN} <b>Percorso:</b>\n${data.departure} ${emo.ARROW} ${data.destination}`;
|
||||
if (data.date) {
|
||||
message += `\n${emo.CALENDAR} ${data.date}`;
|
||||
}
|
||||
if (data.time) {
|
||||
message += ` ${emo.CLOCK} ${data.time}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Motivo cancellazione
|
||||
if (data.reason) {
|
||||
message += `\n\n<i>${t('RIDE_CANCELLED_REASON')}</i>`;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PUSH NOTIFICATION BUILDER
|
||||
// =============================================================================
|
||||
|
||||
function buildPushPayload(type, data, lang = 'it') {
|
||||
const t = (key) => getTranslation(lang, key, data);
|
||||
|
||||
const title = t(`${type.toUpperCase()}_TITLE`);
|
||||
let body = t(`${type.toUpperCase()}_BODY`);
|
||||
|
||||
// Tronca body per push
|
||||
body = truncate(body, 150);
|
||||
|
||||
// Icone per tipo
|
||||
const icons = {
|
||||
[NotificationType.NEW_RIDE_REQUEST]: '/icons/request.png',
|
||||
[NotificationType.REQUEST_ACCEPTED]: '/icons/accepted.png',
|
||||
[NotificationType.REQUEST_REJECTED]: '/icons/rejected.png',
|
||||
[NotificationType.RIDE_REMINDER_24H]: '/icons/reminder.png',
|
||||
[NotificationType.RIDE_REMINDER_2H]: '/icons/urgent.png',
|
||||
[NotificationType.RIDE_CANCELLED]: '/icons/cancelled.png',
|
||||
[NotificationType.NEW_MESSAGE]: '/icons/message.png',
|
||||
[NotificationType.NEW_COMMUNITY_RIDE]: '/icons/community.png',
|
||||
[NotificationType.TEST]: '/icons/notification.png',
|
||||
[NotificationType.WELCOME]: '/icons/welcome.png'
|
||||
};
|
||||
|
||||
return {
|
||||
title,
|
||||
body,
|
||||
icon: icons[type] || '/icons/notification.png',
|
||||
badge: '/icons/badge.png',
|
||||
tag: type,
|
||||
data: {
|
||||
type,
|
||||
url: data.actionUrl || config.appUrl,
|
||||
...data
|
||||
},
|
||||
actions: data.actionUrl ? [
|
||||
{ action: 'open', title: t('VIEW_DETAILS') }
|
||||
] : []
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN SERVICE
|
||||
// =============================================================================
|
||||
|
||||
const TrasportiNotifications = {
|
||||
|
||||
// Esponi tipi e emoji
|
||||
NotificationType,
|
||||
emo,
|
||||
|
||||
// Esponi config
|
||||
config,
|
||||
|
||||
/**
|
||||
* Invia notifica su tutti i canali abilitati
|
||||
*
|
||||
* @param {Object} user - Utente destinatario (con notificationPreferences)
|
||||
* @param {string} type - Tipo notifica (da NotificationType)
|
||||
* @param {Object} data - Dati per template
|
||||
* @param {string} idapp - ID app (per Telegram)
|
||||
* @returns {Object} { success, results: { email, telegram, push } }
|
||||
*/
|
||||
async sendNotification(user, type, data, idapp) {
|
||||
const results = {
|
||||
email: null,
|
||||
telegram: null,
|
||||
push: null
|
||||
};
|
||||
|
||||
const prefs = user.notificationPreferences || {};
|
||||
const lang = user.lang || 'it';
|
||||
|
||||
// Aggiungi URL azione se non presente
|
||||
if (!data.actionUrl && data.rideId) {
|
||||
data.actionUrl = `${config.appUrl}/trasporti/viaggio/${data.rideId}`;
|
||||
}
|
||||
if (!data.actionUrl && data.requestId) {
|
||||
data.actionUrl = `${config.appUrl}/trasporti/richieste/${data.requestId}`;
|
||||
}
|
||||
if (!data.actionUrl && data.chatId) {
|
||||
data.actionUrl = `${config.appUrl}/trasporti/chat/${data.chatId}`;
|
||||
}
|
||||
|
||||
// EMAIL
|
||||
if (shouldSend(prefs, 'email', type) && user.email) {
|
||||
results.email = await this.sendEmail(user.email, type, data, lang);
|
||||
}
|
||||
|
||||
// TELEGRAM (usa il tuo telegrambot.js esistente!)
|
||||
const telegId = user.profile?.teleg_id || prefs.telegram?.chatId;
|
||||
if (shouldSend(prefs, 'telegram', type) && telegId) {
|
||||
results.telegram = await this.sendTelegram(idapp, telegId, type, data, lang);
|
||||
}
|
||||
|
||||
// PUSH
|
||||
const pushSub = prefs.push?.subscription;
|
||||
if (shouldSend(prefs, 'push', type) && pushSub) {
|
||||
results.push = await this.sendPush(pushSub, type, data, lang);
|
||||
}
|
||||
|
||||
return {
|
||||
success: Object.values(results).some(r => r?.success),
|
||||
results
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Invia notifica a multipli utenti
|
||||
*/
|
||||
async sendNotificationToMany(users, type, data, idapp) {
|
||||
const results = [];
|
||||
|
||||
for (const user of users) {
|
||||
try {
|
||||
const result = await this.sendNotification(user, type, data, idapp);
|
||||
results.push({ userId: user._id, ...result });
|
||||
|
||||
// Delay per evitare rate limiting
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
} catch (error) {
|
||||
results.push({ userId: user._id, success: false, error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
},
|
||||
|
||||
// ===========================================================================
|
||||
// EMAIL
|
||||
// ===========================================================================
|
||||
|
||||
async sendEmail(to, type, data, lang = 'it') {
|
||||
if (!emailTransporter) {
|
||||
return { success: false, error: 'Email not configured' };
|
||||
}
|
||||
|
||||
try {
|
||||
const t = (key) => getTranslation(lang, key, data);
|
||||
const subject = `${config.appName} - ${t(`${type.toUpperCase()}_TITLE`)}`;
|
||||
const html = buildEmailHtml(type, data, lang);
|
||||
|
||||
const info = await emailTransporter.sendMail({
|
||||
from: `"${config.appName}" <${config.emailFrom}>`,
|
||||
to,
|
||||
subject,
|
||||
html
|
||||
});
|
||||
|
||||
return { success: true, messageId: info.messageId };
|
||||
} catch (error) {
|
||||
console.error('Email send error:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
// ===========================================================================
|
||||
// TELEGRAM (usa il tuo MyTelegramBot!)
|
||||
// ===========================================================================
|
||||
|
||||
async sendTelegram(idapp, chatId, type, data, lang = 'it') {
|
||||
try {
|
||||
const message = buildTelegramMessage(type, data, lang);
|
||||
|
||||
// USA IL TUO METODO ESISTENTE!
|
||||
const result = await MyTelegramBot.local_sendMsgTelegramByIdTelegram(
|
||||
idapp,
|
||||
chatId,
|
||||
message,
|
||||
null, // message_id
|
||||
null, // chat_id reply
|
||||
false, // ripr_menuPrec
|
||||
null, // MyForm (bottoni)
|
||||
'' // img
|
||||
);
|
||||
|
||||
return { success: true, messageId: result?.message_id };
|
||||
} catch (error) {
|
||||
console.error('Telegram send error:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Invia notifica Telegram con bottoni inline
|
||||
*/
|
||||
async sendTelegramWithButtons(idapp, chatId, type, data, buttons, lang = 'it') {
|
||||
try {
|
||||
const message = buildTelegramMessage(type, data, lang);
|
||||
|
||||
// Crea inline keyboard
|
||||
const cl = MyTelegramBot.getclTelegByidapp(idapp);
|
||||
if (!cl) {
|
||||
return { success: false, error: 'Telegram client not found' };
|
||||
}
|
||||
|
||||
const keyboard = cl.getInlineKeyboard(lang, buttons);
|
||||
|
||||
const result = await MyTelegramBot.local_sendMsgTelegramByIdTelegram(
|
||||
idapp,
|
||||
chatId,
|
||||
message,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
keyboard,
|
||||
''
|
||||
);
|
||||
|
||||
return { success: true, messageId: result?.message_id };
|
||||
} catch (error) {
|
||||
console.error('Telegram send error:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
// ===========================================================================
|
||||
// PUSH
|
||||
// ===========================================================================
|
||||
|
||||
async sendPush(subscription, type, data, lang = 'it') {
|
||||
if (!config.vapidPublicKey || !config.vapidPrivateKey) {
|
||||
return { success: false, error: 'Push not configured' };
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = JSON.stringify(buildPushPayload(type, data, lang));
|
||||
|
||||
await webpush.sendNotification(subscription, payload);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Push send error:', error);
|
||||
|
||||
// Subscription scaduta
|
||||
if (error.statusCode === 410 || error.statusCode === 404) {
|
||||
return { success: false, error: 'Subscription expired', expired: true };
|
||||
}
|
||||
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
// ===========================================================================
|
||||
// METODI SPECIFICI PER TRASPORTI
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* Notifica nuova richiesta passaggio al conducente
|
||||
*/
|
||||
async notifyNewRideRequest(driver, passenger, ride, request, idapp) {
|
||||
return this.sendNotification(driver, NotificationType.NEW_RIDE_REQUEST, {
|
||||
passengerName: `${passenger.name} ${passenger.surname}`,
|
||||
departure: ride.departure?.city || ride.departure?.address,
|
||||
destination: ride.destination?.city || ride.destination?.address,
|
||||
date: formatDate(ride.departureTime),
|
||||
time: formatTime(ride.departureTime),
|
||||
seats: request.seats || 1,
|
||||
rideId: ride._id,
|
||||
requestId: request._id,
|
||||
actionUrl: `${config.appUrl}/trasporti/richieste/${request._id}`
|
||||
}, idapp);
|
||||
},
|
||||
|
||||
/**
|
||||
* Notifica richiesta accettata al passeggero
|
||||
*/
|
||||
async notifyRequestAccepted(passenger, driver, ride, idapp) {
|
||||
return this.sendNotification(passenger, NotificationType.REQUEST_ACCEPTED, {
|
||||
driverName: `${driver.name} ${driver.surname}`,
|
||||
departure: ride.departure?.city || ride.departure?.address,
|
||||
destination: ride.destination?.city || ride.destination?.address,
|
||||
date: formatDate(ride.departureTime),
|
||||
time: formatTime(ride.departureTime),
|
||||
rideId: ride._id,
|
||||
actionUrl: `${config.appUrl}/trasporti/viaggio/${ride._id}`
|
||||
}, idapp);
|
||||
},
|
||||
|
||||
/**
|
||||
* Notifica richiesta rifiutata al passeggero
|
||||
*/
|
||||
async notifyRequestRejected(passenger, driver, ride, reason, idapp) {
|
||||
return this.sendNotification(passenger, NotificationType.REQUEST_REJECTED, {
|
||||
driverName: `${driver.name} ${driver.surname}`,
|
||||
departure: ride.departure?.city || ride.departure?.address,
|
||||
destination: ride.destination?.city || ride.destination?.address,
|
||||
reason
|
||||
}, idapp);
|
||||
},
|
||||
|
||||
/**
|
||||
* Notifica promemoria viaggio
|
||||
*/
|
||||
async notifyRideReminder(user, ride, hoursBefor, idapp) {
|
||||
const type = hoursBefor === 24
|
||||
? NotificationType.RIDE_REMINDER_24H
|
||||
: NotificationType.RIDE_REMINDER_2H;
|
||||
|
||||
return this.sendNotification(user, type, {
|
||||
departure: ride.departure?.city || ride.departure?.address,
|
||||
destination: ride.destination?.city || ride.destination?.address,
|
||||
date: formatDate(ride.departureTime),
|
||||
time: formatTime(ride.departureTime),
|
||||
rideId: ride._id,
|
||||
actionUrl: `${config.appUrl}/trasporti/viaggio/${ride._id}`
|
||||
}, idapp);
|
||||
},
|
||||
|
||||
/**
|
||||
* Notifica viaggio cancellato
|
||||
*/
|
||||
async notifyRideCancelled(user, ride, reason, idapp) {
|
||||
return this.sendNotification(user, NotificationType.RIDE_CANCELLED, {
|
||||
departure: ride.departure?.city || ride.departure?.address,
|
||||
destination: ride.destination?.city || ride.destination?.address,
|
||||
date: formatDate(ride.departureTime),
|
||||
reason,
|
||||
rideId: ride._id
|
||||
}, idapp);
|
||||
},
|
||||
|
||||
/**
|
||||
* Notifica nuovo messaggio
|
||||
*/
|
||||
async notifyNewMessage(recipient, sender, message, chatId, idapp) {
|
||||
return this.sendNotification(recipient, NotificationType.NEW_MESSAGE, {
|
||||
senderName: `${sender.name} ${sender.surname}`,
|
||||
preview: truncate(message.text, 100),
|
||||
chatId,
|
||||
actionUrl: `${config.appUrl}/trasporti/chat/${chatId}`
|
||||
}, idapp);
|
||||
},
|
||||
|
||||
/**
|
||||
* Invia notifica di test
|
||||
*/
|
||||
async sendTestNotification(user, channel, idapp) {
|
||||
const type = NotificationType.TEST;
|
||||
const data = {
|
||||
actionUrl: `${config.appUrl}/trasporti/impostazioni`
|
||||
};
|
||||
const lang = user.lang || 'it';
|
||||
|
||||
if (channel === 'email' && user.email) {
|
||||
return this.sendEmail(user.email, type, data, lang);
|
||||
}
|
||||
|
||||
const telegId = user.profile?.teleg_id || user.notificationPreferences?.telegram?.chatId;
|
||||
if (channel === 'telegram' && telegId) {
|
||||
return this.sendTelegram(idapp, telegId, type, data, lang);
|
||||
}
|
||||
|
||||
const pushSub = user.notificationPreferences?.push?.subscription;
|
||||
if (channel === 'push' && pushSub) {
|
||||
return this.sendPush(pushSub, type, data, lang);
|
||||
}
|
||||
|
||||
if (channel === 'all') {
|
||||
return this.sendNotification(user, type, data, idapp);
|
||||
}
|
||||
|
||||
return { success: false, error: 'Invalid channel or not configured' };
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// HELPER DATE
|
||||
// =============================================================================
|
||||
|
||||
function formatDate(date) {
|
||||
if (!date) return '';
|
||||
const d = new Date(date);
|
||||
return d.toLocaleDateString('it-IT', {
|
||||
weekday: 'short',
|
||||
day: 'numeric',
|
||||
month: 'short'
|
||||
});
|
||||
}
|
||||
|
||||
function formatTime(date) {
|
||||
if (!date) return '';
|
||||
const d = new Date(date);
|
||||
return d.toLocaleTimeString('it-IT', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORT
|
||||
// =============================================================================
|
||||
|
||||
module.exports = TrasportiNotifications;
|
||||
422
src/controllers/viaggi/settingsController.js
Normal file
422
src/controllers/viaggi/settingsController.js
Normal file
@@ -0,0 +1,422 @@
|
||||
// ============================================================
|
||||
// 🔧 SETTINGS CONTROLLER - Trasporti Solidali
|
||||
// ============================================================
|
||||
// File: server/controllers/viaggi/settingsController.js
|
||||
|
||||
const UserSettings = require('../../models/viaggi/UserSettings');
|
||||
|
||||
/**
|
||||
* 📄 GET /api/viaggi/settings
|
||||
* Ottieni le impostazioni dell'utente
|
||||
*/
|
||||
exports.getSettings = async (req, res) => {
|
||||
try {
|
||||
const { idapp } = req.query;
|
||||
const userId = req.user._id;
|
||||
|
||||
// Validazione
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
// Ottieni o crea impostazioni
|
||||
const settings = await UserSettings.getOrCreateSettings(idapp, userId);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
data: settings.toClientJSON()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Errore getSettings:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nel caricamento delle impostazioni',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📝 PUT /api/viaggi/settings
|
||||
* Aggiorna le impostazioni dell'utente
|
||||
*/
|
||||
exports.updateSettings = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const idapp = req.user.idapp;
|
||||
const updates = req.body;
|
||||
|
||||
// Validazione
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
if (!updates || Object.keys(updates).length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Nessuna modifica specificata'
|
||||
});
|
||||
}
|
||||
|
||||
// Aggiorna impostazioni
|
||||
const settings = await UserSettings.updateSettings(idapp, userId, updates);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Impostazioni aggiornate con successo',
|
||||
data: settings.toClientJSON()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Errore updateSettings:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nell\'aggiornamento delle impostazioni',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📝 PATCH /api/viaggi/settings/notifications
|
||||
* Aggiorna solo le impostazioni notifiche
|
||||
*/
|
||||
exports.updateNotifications = async (req, res) => {
|
||||
try {
|
||||
const idapp = req.user.idapp;
|
||||
const userId = req.user._id;
|
||||
const { notifications } = req.body;
|
||||
|
||||
// Validazione
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
if (!notifications) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'notifications è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
// Aggiorna solo notifiche
|
||||
const settings = await UserSettings.updateSettings(idapp, userId, { notifications });
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Notifiche aggiornate',
|
||||
data: settings.notifications
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Errore updateNotifications:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nell\'aggiornamento delle notifiche',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📝 PATCH /api/viaggi/settings/privacy
|
||||
* Aggiorna solo le impostazioni privacy
|
||||
*/
|
||||
exports.updatePrivacy = async (req, res) => {
|
||||
try {
|
||||
const idapp = req.user.idapp;
|
||||
const userId = req.user._id;
|
||||
const { privacy } = req.body;
|
||||
|
||||
// Validazione
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
if (!privacy) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'privacy è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
// Aggiorna solo privacy
|
||||
const settings = await UserSettings.updateSettings(idapp, userId, { privacy });
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Privacy aggiornata',
|
||||
data: settings.privacy
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Errore updatePrivacy:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nell\'aggiornamento della privacy',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📝 PATCH /api/viaggi/settings/ride-preferences
|
||||
* Aggiorna solo le preferenze viaggi
|
||||
*/
|
||||
exports.updateRidePreferences = async (req, res) => {
|
||||
try {
|
||||
const idapp = req.user.idapp;
|
||||
const userId = req.user._id;
|
||||
const { ridePreferences } = req.body;
|
||||
|
||||
// Validazione
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
if (!ridePreferences) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'ridePreferences è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
// Aggiorna preferenze viaggi
|
||||
const settings = await UserSettings.updateSettings(idapp, userId, { ridePreferences });
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Preferenze viaggi aggiornate',
|
||||
data: settings.ridePreferences
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Errore updateRidePreferences:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nell\'aggiornamento delle preferenze',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📝 PATCH /api/viaggi/settings/interface
|
||||
* Aggiorna solo le impostazioni interfaccia
|
||||
*/
|
||||
exports.updateInterface = async (req, res) => {
|
||||
try {
|
||||
const idapp = req.user.idapp;
|
||||
const userId = req.user._id;
|
||||
const { interface: interfaceSettings } = req.body;
|
||||
|
||||
// Validazione
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
if (!interfaceSettings) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'interface è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
// Aggiorna interfaccia
|
||||
const settings = await UserSettings.updateSettings(idapp, userId, {
|
||||
interface: interfaceSettings
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Interfaccia aggiornata',
|
||||
data: settings.interface
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Errore updateInterface:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nell\'aggiornamento dell\'interfaccia',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 🔄 POST /api/viaggi/settings/reset
|
||||
* Reset impostazioni ai valori predefiniti
|
||||
*/
|
||||
exports.resetSettings = async (req, res) => {
|
||||
try {
|
||||
const idapp = req.user.idapp;
|
||||
const userId = req.user._id;
|
||||
const { section } = req.body; // Opzionale: resetta solo una sezione
|
||||
|
||||
// Validazione
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
// Trova impostazioni esistenti
|
||||
let settings = await UserSettings.findOne({ idapp, userId });
|
||||
|
||||
if (!settings) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Impostazioni non trovate'
|
||||
});
|
||||
}
|
||||
|
||||
if (section) {
|
||||
// Reset solo di una sezione specifica
|
||||
const schema = UserSettings.schema.paths[section];
|
||||
if (!schema) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Sezione non valida'
|
||||
});
|
||||
}
|
||||
|
||||
// Ottieni valori predefiniti dalla schema
|
||||
settings[section] = schema.defaultValue || {};
|
||||
} else {
|
||||
// Reset completo - cancella e ricrea
|
||||
await UserSettings.deleteOne({ idapp, userId });
|
||||
settings = await UserSettings.getOrCreateSettings(idapp, userId);
|
||||
}
|
||||
|
||||
await settings.save();
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: section
|
||||
? `Sezione ${section} resettata`
|
||||
: 'Impostazioni resettate ai valori predefiniti',
|
||||
data: settings.toClientJSON()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Errore resetSettings:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nel reset delle impostazioni',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 GET /api/viaggi/settings/export
|
||||
* Esporta tutte le impostazioni (per backup o trasferimento)
|
||||
*/
|
||||
exports.exportSettings = async (req, res) => {
|
||||
try {
|
||||
const { idapp } = req.query;
|
||||
const userId = req.user._id;
|
||||
|
||||
// Validazione
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
const settings = await UserSettings.findOne({ idapp, userId });
|
||||
|
||||
if (!settings) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Impostazioni non trovate'
|
||||
});
|
||||
}
|
||||
|
||||
// Esporta in formato JSON pulito
|
||||
const exportData = {
|
||||
exportDate: new Date().toISOString(),
|
||||
userId: userId.toString(),
|
||||
idapp,
|
||||
settings: settings.toClientJSON()
|
||||
};
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
data: exportData
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Errore exportSettings:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nell\'esportazione delle impostazioni',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📥 POST /api/viaggi/settings/import
|
||||
* Importa impostazioni da backup
|
||||
*/
|
||||
exports.importSettings = async (req, res) => {
|
||||
try {
|
||||
const idapp = req.user.idapp;
|
||||
const userId = req.user._id;
|
||||
const { settings: importedSettings } = req.body;
|
||||
|
||||
// Validazione
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
if (!importedSettings) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'settings è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
// Aggiorna con le impostazioni importate
|
||||
const settings = await UserSettings.updateSettings(idapp, userId, importedSettings);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Impostazioni importate con successo',
|
||||
data: settings.toClientJSON()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Errore importSettings:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nell\'importazione delle impostazioni',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
506
src/controllers/viaggi/trasportiNotificationsController.js
Normal file
506
src/controllers/viaggi/trasportiNotificationsController.js
Normal file
@@ -0,0 +1,506 @@
|
||||
/**
|
||||
* trasportiNotificationsController.js
|
||||
*
|
||||
* Controller API per gestire le preferenze di notifica utente.
|
||||
* Funziona insieme a TrasportiNotifications.js
|
||||
*/
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
const TrasportiNotifications = require('./TrasportiNotifications');
|
||||
|
||||
// =============================================================================
|
||||
// SCHEMA PREFERENZE (da aggiungere al model User)
|
||||
// =============================================================================
|
||||
|
||||
const notificationPreferencesSchema = new mongoose.Schema({
|
||||
email: {
|
||||
enabled: { type: Boolean, default: true },
|
||||
newRideRequest: { type: Boolean, default: true },
|
||||
requestAccepted: { type: Boolean, default: true },
|
||||
requestRejected: { type: Boolean, default: true },
|
||||
rideReminder24h: { type: Boolean, default: true },
|
||||
rideReminder2h: { type: Boolean, default: true },
|
||||
rideCancelled: { type: Boolean, default: true },
|
||||
newMessage: { type: Boolean, default: true },
|
||||
newCommunityRide: { type: Boolean, default: false },
|
||||
weeklyDigest: { type: Boolean, default: false }
|
||||
},
|
||||
telegram: {
|
||||
enabled: { type: Boolean, default: false },
|
||||
chatId: { type: Number, default: 0 },
|
||||
username: { type: String, default: '' },
|
||||
connectedAt: { type: Date },
|
||||
newRideRequest: { type: Boolean, default: true },
|
||||
requestAccepted: { type: Boolean, default: true },
|
||||
requestRejected: { type: Boolean, default: true },
|
||||
rideReminder24h: { type: Boolean, default: true },
|
||||
rideReminder2h: { type: Boolean, default: true },
|
||||
rideCancelled: { type: Boolean, default: true },
|
||||
newMessage: { type: Boolean, default: true }
|
||||
},
|
||||
push: {
|
||||
enabled: { type: Boolean, default: false },
|
||||
subscription: { type: mongoose.Schema.Types.Mixed },
|
||||
subscribedAt: { type: Date },
|
||||
newRideRequest: { type: Boolean, default: true },
|
||||
requestAccepted: { type: Boolean, default: true },
|
||||
requestRejected: { type: Boolean, default: true },
|
||||
rideReminder24h: { type: Boolean, default: true },
|
||||
rideReminder2h: { type: Boolean, default: true },
|
||||
rideCancelled: { type: Boolean, default: true },
|
||||
newMessage: { type: Boolean, default: true }
|
||||
}
|
||||
}, { _id: false });
|
||||
|
||||
// =============================================================================
|
||||
// STORAGE CODICI TELEGRAM (in-memory, usa Redis in produzione)
|
||||
// =============================================================================
|
||||
|
||||
const telegramConnectCodes = new Map();
|
||||
|
||||
// Pulizia codici scaduti ogni 5 minuti
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [code, data] of telegramConnectCodes) {
|
||||
if (now - data.createdAt > 10 * 60 * 1000) { // 10 minuti
|
||||
telegramConnectCodes.delete(code);
|
||||
}
|
||||
}
|
||||
}, 5 * 60 * 1000);
|
||||
|
||||
/**
|
||||
* Genera codice random 6 caratteri
|
||||
*/
|
||||
function generateCode() {
|
||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // Escludo caratteri ambigui
|
||||
let code = '';
|
||||
for (let i = 0; i < 6; i++) {
|
||||
code += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTROLLER
|
||||
// =============================================================================
|
||||
|
||||
const trasportiNotificationsController = {
|
||||
|
||||
// Esponi schema per User model
|
||||
notificationPreferencesSchema,
|
||||
|
||||
/**
|
||||
* GET /api/trasporti/notifications/preferences
|
||||
* Ottiene preferenze notifiche utente
|
||||
*/
|
||||
async getNotificationPreferences(req, res) {
|
||||
try {
|
||||
const user = req.user;
|
||||
|
||||
// Default preferences se non esistono
|
||||
const defaultPrefs = {
|
||||
email: {
|
||||
enabled: true,
|
||||
newRideRequest: true,
|
||||
requestAccepted: true,
|
||||
requestRejected: true,
|
||||
rideReminder24h: true,
|
||||
rideReminder2h: true,
|
||||
rideCancelled: true,
|
||||
newMessage: true,
|
||||
newCommunityRide: false,
|
||||
weeklyDigest: false
|
||||
},
|
||||
telegram: {
|
||||
enabled: false,
|
||||
chatId: user.profile?.teleg_id || 0,
|
||||
username: user.profile?.teleg_username || '',
|
||||
newRideRequest: true,
|
||||
requestAccepted: true,
|
||||
requestRejected: true,
|
||||
rideReminder24h: true,
|
||||
rideReminder2h: true,
|
||||
rideCancelled: true,
|
||||
newMessage: true
|
||||
},
|
||||
push: {
|
||||
enabled: false,
|
||||
newRideRequest: true,
|
||||
requestAccepted: true,
|
||||
requestRejected: true,
|
||||
rideReminder24h: true,
|
||||
rideReminder2h: true,
|
||||
rideCancelled: true,
|
||||
newMessage: true
|
||||
}
|
||||
};
|
||||
|
||||
// Merge con preferenze salvate
|
||||
const prefs = user.notificationPreferences || {};
|
||||
const mergedPrefs = {
|
||||
email: { ...defaultPrefs.email, ...prefs.email },
|
||||
telegram: { ...defaultPrefs.telegram, ...prefs.telegram },
|
||||
push: { ...defaultPrefs.push, ...prefs.push }
|
||||
};
|
||||
|
||||
// Sync chatId da profile se presente
|
||||
if (user.profile?.teleg_id && !mergedPrefs.telegram.chatId) {
|
||||
mergedPrefs.telegram.chatId = user.profile.teleg_id;
|
||||
mergedPrefs.telegram.enabled = true;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
email: user.email,
|
||||
preferences: mergedPrefs,
|
||||
vapidPublicKey: TrasportiNotifications.config.vapidPublicKey,
|
||||
telegramBotUsername: process.env.TELEGRAM_BOT_USERNAME || 'TrasportiSolidaliBot'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('getNotificationPreferences error:', error);
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* PUT /api/trasporti/notifications/preferences
|
||||
* Aggiorna preferenze notifiche
|
||||
*/
|
||||
async updateNotificationPreferences(req, res) {
|
||||
try {
|
||||
const { User } = require('../../models/user');
|
||||
const { email, telegram, push } = req.body;
|
||||
|
||||
const updateData = {};
|
||||
|
||||
// Email preferences
|
||||
if (email) {
|
||||
Object.keys(email).forEach(key => {
|
||||
if (key !== 'enabled' || typeof email[key] === 'boolean') {
|
||||
updateData[`notificationPreferences.email.${key}`] = email[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Telegram preferences (escludi chatId, username - gestiti via connect)
|
||||
if (telegram) {
|
||||
const allowedKeys = ['enabled', 'newRideRequest', 'requestAccepted', 'requestRejected',
|
||||
'rideReminder24h', 'rideReminder2h', 'rideCancelled', 'newMessage'];
|
||||
Object.keys(telegram).forEach(key => {
|
||||
if (allowedKeys.includes(key)) {
|
||||
updateData[`notificationPreferences.telegram.${key}`] = telegram[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Push preferences (escludi subscription - gestito via subscribe)
|
||||
if (push) {
|
||||
const allowedKeys = ['enabled', 'newRideRequest', 'requestAccepted', 'requestRejected',
|
||||
'rideReminder24h', 'rideReminder2h', 'rideCancelled', 'newMessage'];
|
||||
Object.keys(push).forEach(key => {
|
||||
if (allowedKeys.includes(key)) {
|
||||
updateData[`notificationPreferences.push.${key}`] = push[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await User.updateOne(
|
||||
{ _id: req.user._id },
|
||||
{ $set: updateData }
|
||||
);
|
||||
|
||||
res.json({ success: true, message: 'Preferenze aggiornate' });
|
||||
} catch (error) {
|
||||
console.error('updateNotificationPreferences error:', error);
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* POST /api/trasporti/notifications/telegram/code
|
||||
* Genera codice per connessione Telegram
|
||||
*/
|
||||
async generateTelegramCode(req, res) {
|
||||
try {
|
||||
const userId = req.user._id.toString();
|
||||
|
||||
// Rimuovi codici esistenti per questo utente
|
||||
for (const [code, data] of telegramConnectCodes) {
|
||||
if (data.userId === userId) {
|
||||
telegramConnectCodes.delete(code);
|
||||
}
|
||||
}
|
||||
|
||||
// Genera nuovo codice
|
||||
let code;
|
||||
do {
|
||||
code = generateCode();
|
||||
} while (telegramConnectCodes.has(code));
|
||||
|
||||
// Salva
|
||||
telegramConnectCodes.set(code, {
|
||||
userId,
|
||||
createdAt: Date.now(),
|
||||
chatId: null,
|
||||
username: null
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
code,
|
||||
expiresIn: 600, // 10 minuti
|
||||
botUsername: process.env.TELEGRAM_BOT_USERNAME || 'TrasportiSolidaliBot',
|
||||
instructions: `Invia "${code}" al bot @${process.env.TELEGRAM_BOT_USERNAME || 'TrasportiSolidaliBot'} su Telegram`
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('generateTelegramCode error:', error);
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* POST /api/trasporti/notifications/telegram/connect
|
||||
* Completa connessione Telegram dopo validazione codice dal bot
|
||||
*/
|
||||
async connectTelegram(req, res) {
|
||||
try {
|
||||
const { User } = require('../../models/user');
|
||||
const { code } = req.body;
|
||||
|
||||
if (!code) {
|
||||
return res.status(400).json({ success: false, message: 'Codice richiesto' });
|
||||
}
|
||||
|
||||
const codeData = telegramConnectCodes.get(code.toUpperCase());
|
||||
|
||||
if (!codeData) {
|
||||
return res.status(400).json({ success: false, message: 'Codice non valido o scaduto' });
|
||||
}
|
||||
|
||||
if (codeData.userId !== req.user._id.toString()) {
|
||||
return res.status(400).json({ success: false, message: 'Codice non valido' });
|
||||
}
|
||||
|
||||
// Verifica che il bot abbia validato il codice (impostando chatId)
|
||||
if (!codeData.chatId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Invia prima il codice al bot su Telegram',
|
||||
needsBotInteraction: true
|
||||
});
|
||||
}
|
||||
|
||||
// Aggiorna utente
|
||||
await User.updateOne(
|
||||
{ _id: req.user._id },
|
||||
{
|
||||
$set: {
|
||||
'notificationPreferences.telegram.enabled': true,
|
||||
'notificationPreferences.telegram.chatId': codeData.chatId,
|
||||
'notificationPreferences.telegram.username': codeData.username || '',
|
||||
'notificationPreferences.telegram.connectedAt': new Date(),
|
||||
// Retrocompatibilità con profile.teleg_id
|
||||
'profile.teleg_id': codeData.chatId,
|
||||
'profile.teleg_username': codeData.username || ''
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Rimuovi codice usato
|
||||
telegramConnectCodes.delete(code.toUpperCase());
|
||||
|
||||
// Invia messaggio benvenuto
|
||||
const idapp = req.user.idapp;
|
||||
await TrasportiNotifications.sendTelegram(
|
||||
idapp,
|
||||
codeData.chatId,
|
||||
TrasportiNotifications.NotificationType.WELCOME,
|
||||
{},
|
||||
req.user.lang || 'it'
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Telegram connesso!',
|
||||
data: {
|
||||
chatId: codeData.chatId,
|
||||
username: codeData.username
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('connectTelegram error:', error);
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* DELETE /api/trasporti/notifications/telegram/disconnect
|
||||
* Disconnette Telegram
|
||||
*/
|
||||
async disconnectTelegram(req, res) {
|
||||
try {
|
||||
const { User } = require('../../models/user');
|
||||
|
||||
// Ottieni chatId prima di disconnettere per inviare messaggio
|
||||
const chatId = req.user.notificationPreferences?.telegram?.chatId || req.user.profile?.teleg_id;
|
||||
const idapp = req.user.idapp;
|
||||
|
||||
// Aggiorna utente
|
||||
await User.updateOne(
|
||||
{ _id: req.user._id },
|
||||
{
|
||||
$set: {
|
||||
'notificationPreferences.telegram.enabled': false,
|
||||
'notificationPreferences.telegram.chatId': 0,
|
||||
'notificationPreferences.telegram.username': '',
|
||||
'notificationPreferences.telegram.connectedAt': null,
|
||||
'profile.teleg_id': 0,
|
||||
'profile.teleg_username': ''
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Invia messaggio di disconnessione
|
||||
if (chatId && idapp) {
|
||||
const MyTelegramBot = require('./telegram/telegrambot');
|
||||
await MyTelegramBot.local_sendMsgTelegramByIdTelegram(
|
||||
idapp,
|
||||
chatId,
|
||||
'👋 Telegram disconnesso da Trasporti Solidali.\n\nPuoi riconnettere in qualsiasi momento dalla pagina impostazioni.',
|
||||
null, null, false, null, ''
|
||||
);
|
||||
}
|
||||
|
||||
res.json({ success: true, message: 'Telegram disconnesso' });
|
||||
} catch (error) {
|
||||
console.error('disconnectTelegram error:', error);
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* POST /api/trasporti/notifications/push/subscribe
|
||||
* Registra subscription push
|
||||
*/
|
||||
async subscribePushNotifications(req, res) {
|
||||
try {
|
||||
const { User } = require('../../models/user');
|
||||
const { subscription } = req.body;
|
||||
|
||||
if (!subscription || !subscription.endpoint) {
|
||||
return res.status(400).json({ success: false, message: 'Subscription non valida' });
|
||||
}
|
||||
|
||||
await User.updateOne(
|
||||
{ _id: req.user._id },
|
||||
{
|
||||
$set: {
|
||||
'notificationPreferences.push.enabled': true,
|
||||
'notificationPreferences.push.subscription': subscription,
|
||||
'notificationPreferences.push.subscribedAt': new Date()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
res.json({ success: true, message: 'Push notifications attivate' });
|
||||
} catch (error) {
|
||||
console.error('subscribePushNotifications error:', error);
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* DELETE /api/trasporti/notifications/push/unsubscribe
|
||||
* Rimuove subscription push
|
||||
*/
|
||||
async unsubscribePushNotifications(req, res) {
|
||||
try {
|
||||
const { User } = require('../../models/user');
|
||||
|
||||
await User.updateOne(
|
||||
{ _id: req.user._id },
|
||||
{
|
||||
$set: {
|
||||
'notificationPreferences.push.enabled': false,
|
||||
'notificationPreferences.push.subscription': null
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
res.json({ success: true, message: 'Push notifications disattivate' });
|
||||
} catch (error) {
|
||||
console.error('unsubscribePushNotifications error:', error);
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* POST /api/trasporti/notifications/test
|
||||
* Invia notifica di test
|
||||
*/
|
||||
async sendTestNotification(req, res) {
|
||||
try {
|
||||
const { channel } = req.body; // 'email', 'telegram', 'push', 'all'
|
||||
const idapp = req.user.idapp;
|
||||
|
||||
const result = await TrasportiNotifications.sendTestNotification(req.user, channel, idapp);
|
||||
|
||||
if (result.success) {
|
||||
res.json({ success: true, message: `Notifica di test inviata su ${channel}` });
|
||||
} else {
|
||||
res.status(400).json({ success: false, message: result.error });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('sendTestNotification error:', error);
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handler per il bot Telegram quando riceve un codice
|
||||
* Chiamare questa funzione dal tuo telegrambot.js
|
||||
*/
|
||||
handleTelegramCodeFromBot(code, chatId, username) {
|
||||
const codeUpper = code.toUpperCase();
|
||||
const codeData = telegramConnectCodes.get(codeUpper);
|
||||
|
||||
if (!codeData) {
|
||||
return { success: false, error: 'Codice non valido o scaduto' };
|
||||
}
|
||||
|
||||
// Aggiorna con chatId e username
|
||||
codeData.chatId = chatId;
|
||||
codeData.username = username;
|
||||
telegramConnectCodes.set(codeUpper, codeData);
|
||||
|
||||
return { success: true, userId: codeData.userId };
|
||||
},
|
||||
|
||||
/**
|
||||
* Rimuovi subscription push scadute
|
||||
* Chiamare quando si riceve errore 410/404
|
||||
*/
|
||||
async removePushSubscription(userId) {
|
||||
try {
|
||||
const { User } = require('../../models/user');
|
||||
await User.updateOne(
|
||||
{ _id: userId },
|
||||
{
|
||||
$set: {
|
||||
'notificationPreferences.push.subscription': null,
|
||||
'notificationPreferences.push.enabled': false
|
||||
}
|
||||
}
|
||||
);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = trasportiNotificationsController;
|
||||
219
src/controllers/viaggi/widgetController.js
Normal file
219
src/controllers/viaggi/widgetController.js
Normal file
@@ -0,0 +1,219 @@
|
||||
// ============================================================
|
||||
// 📊 WIDGET & STATS CONTROLLER - Trasporti Solidali
|
||||
// ============================================================
|
||||
// File: server/controllers/viaggi/widgetController.js
|
||||
|
||||
const Ride = require('../../models/viaggi/Ride');
|
||||
const RideRequest = require('../../models/viaggi/RideRequest');
|
||||
const Feedback = require('../../models/viaggi/Feedback');
|
||||
const Chat = require('../../models/viaggi/Chat');
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
/**
|
||||
* 📊 GET /api/viaggi/widget/data
|
||||
* Ottieni dati per il widget dashboard
|
||||
*/
|
||||
exports.getWidgetData = async (req, res) => {
|
||||
try {
|
||||
const { idapp } = req.query;
|
||||
const userId = req.user._id;
|
||||
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
|
||||
// Query parallele per ottimizzare
|
||||
const [
|
||||
offersCount,
|
||||
requestsCount,
|
||||
recentRides,
|
||||
myActiveRides,
|
||||
pendingRequestsCount,
|
||||
unreadMessagesCount
|
||||
] = await Promise.all([
|
||||
// Conta offerte attive
|
||||
Ride.countDocuments({
|
||||
idapp,
|
||||
type: 'offer',
|
||||
status: 'active',
|
||||
departureDate: { $gte: now }
|
||||
}),
|
||||
|
||||
// Conta richieste attive
|
||||
Ride.countDocuments({
|
||||
idapp,
|
||||
type: 'request',
|
||||
status: 'active',
|
||||
departureDate: { $gte: now }
|
||||
}),
|
||||
|
||||
// Ultimi viaggi pubblicati (non propri)
|
||||
Ride.find({
|
||||
idapp,
|
||||
userId: { $ne: userId },
|
||||
status: 'active',
|
||||
departureDate: { $gte: now }
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(5)
|
||||
.populate('userId', 'name surname profile')
|
||||
.lean(),
|
||||
|
||||
// I miei viaggi attivi
|
||||
Ride.find({
|
||||
idapp,
|
||||
userId: userId,
|
||||
status: 'active',
|
||||
departureDate: { $gte: now }
|
||||
})
|
||||
.sort({ departureDate: 1 })
|
||||
.limit(3)
|
||||
.lean(),
|
||||
|
||||
// Richieste pendenti ricevute (per i miei viaggi)
|
||||
RideRequest.countDocuments({
|
||||
idapp,
|
||||
driverUserId: userId,
|
||||
status: 'pending'
|
||||
}),
|
||||
|
||||
// Messaggi non letti
|
||||
Chat.countDocuments({
|
||||
idapp,
|
||||
participants: userId,
|
||||
isDeleted: false,
|
||||
[`deletedBy.${userId}`]: { $ne: true },
|
||||
'messages': {
|
||||
$elemMatch: {
|
||||
senderId: { $ne: userId },
|
||||
readBy: { $ne: userId }
|
||||
}
|
||||
}
|
||||
})
|
||||
]);
|
||||
|
||||
// Calcola "matches" - viaggi compatibili con le mie richieste
|
||||
let matchesCount = 0;
|
||||
const myRequests = await Ride.find({
|
||||
idapp,
|
||||
userId: userId,
|
||||
type: 'request',
|
||||
status: 'active',
|
||||
departureDate: { $gte: now }
|
||||
}).select('departure destination departureDate').lean();
|
||||
|
||||
if (myRequests.length > 0) {
|
||||
// Per ogni mia richiesta, cerca offerte compatibili
|
||||
for (const request of myRequests) {
|
||||
const compatibleOffers = await Ride.countDocuments({
|
||||
idapp,
|
||||
userId: { $ne: userId },
|
||||
type: 'offer',
|
||||
status: 'active',
|
||||
departureDate: {
|
||||
$gte: new Date(request.departureDate.getTime() - 2 * 60 * 60 * 1000), // -2h
|
||||
$lte: new Date(request.departureDate.getTime() + 2 * 60 * 60 * 1000) // +2h
|
||||
},
|
||||
// Potresti aggiungere filtri geografici qui
|
||||
'departure.city': request.departure?.city,
|
||||
'destination.city': request.destination?.city
|
||||
});
|
||||
matchesCount += compatibleOffers;
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
stats: {
|
||||
offers: offersCount,
|
||||
requests: requestsCount,
|
||||
matches: matchesCount
|
||||
},
|
||||
recentRides: recentRides,
|
||||
myActiveRides: myActiveRides,
|
||||
pendingRequests: pendingRequestsCount,
|
||||
unreadMessages: unreadMessagesCount
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Errore getWidgetData:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nel caricamento dei dati widget',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 GET /api/viaggi/stats/quick
|
||||
* Statistiche rapide per badge/notifiche
|
||||
*/
|
||||
exports.getQuickStats = async (req, res) => {
|
||||
try {
|
||||
const { idapp } = req.query;
|
||||
const userId = req.user._id;
|
||||
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
const [pendingRequests, unreadMessages, activeRides] = await Promise.all([
|
||||
RideRequest.countDocuments({
|
||||
idapp,
|
||||
driverUserId: userId,
|
||||
status: 'pending'
|
||||
}),
|
||||
|
||||
Chat.countDocuments({
|
||||
idapp,
|
||||
participants: userId,
|
||||
isDeleted: false,
|
||||
[`deletedBy.${userId}`]: { $ne: true },
|
||||
'messages': {
|
||||
$elemMatch: {
|
||||
senderId: { $ne: userId },
|
||||
readBy: { $ne: userId }
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
Ride.countDocuments({
|
||||
idapp,
|
||||
userId: userId,
|
||||
status: 'active',
|
||||
departureDate: { $gte: new Date() }
|
||||
})
|
||||
]);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
pendingRequests,
|
||||
unreadMessages,
|
||||
activeRides,
|
||||
totalNotifications: pendingRequests + unreadMessages
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Errore getQuickStats:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nel caricamento delle statistiche rapide',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = exports;
|
||||
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": "/upload/assets/backgrounds/forest_autumn_001.jpg",
|
||||
"url": "/api/assets/asset_bg_001/file",
|
||||
"thumbnailPath": "/upload/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": "/upload/posters/poster_sagra_2025_bg.jpg",
|
||||
"thumbnailUrl": "/upload/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": "/upload/assets/porcini_basket_hero.jpg",
|
||||
"thumbnailUrl": "/upload/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": "/upload/logos/comune_borgomontano.png",
|
||||
"originalName": "logo_comune.png",
|
||||
"mimeType": "image/png",
|
||||
"size": 45000
|
||||
},
|
||||
{
|
||||
"id": "asset_logo_002",
|
||||
"slotId": "logo_slot_2",
|
||||
"sourceType": "upload",
|
||||
"url": "/upload/logos/proloco_borgomontano.png",
|
||||
"originalName": "logo_proloco.png",
|
||||
"mimeType": "image/png",
|
||||
"size": 38000
|
||||
},
|
||||
{
|
||||
"id": "asset_logo_003",
|
||||
"slotId": "logo_slot_3",
|
||||
"sourceType": "ai",
|
||||
"url": "/upload/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": "/upload/posters/final/poster_sagra_2025_final.png",
|
||||
"size": 8945000,
|
||||
"url": "/api/posters/poster_sagra_funghi_2025_001/download/png"
|
||||
},
|
||||
"jpg": {
|
||||
"path": "/upload/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"
|
||||
}
|
||||
77
src/helpers/recurrenceHelper.js
Normal file
77
src/helpers/recurrenceHelper.js
Normal file
@@ -0,0 +1,77 @@
|
||||
// Helper per calcolare le date dalle ricorrenze
|
||||
const getRecurrenceDates = (ride, startRange, endRange) => {
|
||||
const { recurrence, departureDate } = ride;
|
||||
|
||||
if (!recurrence || recurrence.type === 'once') {
|
||||
return [new Date(departureDate)];
|
||||
}
|
||||
|
||||
const dates = [];
|
||||
const start = new Date(startRange || recurrence.startDate || departureDate);
|
||||
const end = new Date(endRange || recurrence.endDate || new Date(start.getTime() + 365 * 24 * 60 * 60 * 1000)); // Default 1 anno
|
||||
|
||||
const excludedDatesSet = new Set(
|
||||
(recurrence.excludedDates || []).map(d => new Date(d).toISOString().split('T')[0])
|
||||
);
|
||||
|
||||
switch (recurrence.type) {
|
||||
case 'weekly':
|
||||
if (!recurrence.daysOfWeek || recurrence.daysOfWeek.length === 0) break;
|
||||
|
||||
let current = new Date(start);
|
||||
while (current <= end) {
|
||||
const dayOfWeek = current.getDay();
|
||||
if (recurrence.daysOfWeek.includes(dayOfWeek)) {
|
||||
const dateStr = current.toISOString().split('T')[0];
|
||||
if (!excludedDatesSet.has(dateStr)) {
|
||||
dates.push(new Date(current));
|
||||
}
|
||||
}
|
||||
current.setDate(current.getDate() + 1);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'custom_days':
|
||||
if (!recurrence.daysOfWeek || recurrence.daysOfWeek.length === 0) break;
|
||||
|
||||
let curr = new Date(start);
|
||||
while (curr <= end) {
|
||||
const dayOfWeek = curr.getDay();
|
||||
if (recurrence.daysOfWeek.includes(dayOfWeek)) {
|
||||
const dateStr = curr.toISOString().split('T')[0];
|
||||
if (!excludedDatesSet.has(dateStr)) {
|
||||
dates.push(new Date(curr));
|
||||
}
|
||||
}
|
||||
curr.setDate(curr.getDate() + 1);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'custom_dates':
|
||||
if (!recurrence.customDates || recurrence.customDates.length === 0) break;
|
||||
|
||||
recurrence.customDates.forEach(date => {
|
||||
const d = new Date(date);
|
||||
if (d >= start && d <= end) {
|
||||
const dateStr = d.toISOString().split('T')[0];
|
||||
if (!excludedDatesSet.has(dateStr)) {
|
||||
dates.push(d);
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
return dates.length > 0 ? dates : [new Date(departureDate)];
|
||||
};
|
||||
|
||||
const isRideActiveOnDate = (ride, targetDate) => {
|
||||
const dates = getRecurrenceDates(ride, targetDate, targetDate);
|
||||
const targetStr = new Date(targetDate).toISOString().split('T')[0];
|
||||
return dates.some(d => d.toISOString().split('T')[0] === targetStr);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getRecurrenceDates,
|
||||
isRideActiveOnDate
|
||||
};
|
||||
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 || './upload';
|
||||
|
||||
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;
|
||||
81
src/middleware/uploadMiddleware.js
Normal file
81
src/middleware/uploadMiddleware.js
Normal file
@@ -0,0 +1,81 @@
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
class UploadMiddleware {
|
||||
constructor(baseUploadPath = 'uploads/videos') {
|
||||
this.baseUploadPath = path.resolve(baseUploadPath);
|
||||
this._ensureDirectory(this.baseUploadPath);
|
||||
}
|
||||
|
||||
_ensureDirectory(dir) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
_createStorage() {
|
||||
return multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
// ✅ Legge SOLO da req.query (affidabile con multer)
|
||||
const folder = req.query.folder || 'default';
|
||||
|
||||
console.log('📁 Upload folder:', folder); // Debug
|
||||
|
||||
const uploadPath = path.join(this.baseUploadPath, folder);
|
||||
this._ensureDirectory(uploadPath);
|
||||
cb(null, uploadPath);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const ext = path.extname(file.originalname);
|
||||
const uniqueName = `${uuidv4()}-${Date.now()}${ext}`;
|
||||
cb(null, uniqueName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_fileFilter(req, file, cb) {
|
||||
const allowedMimes = [
|
||||
'video/mp4',
|
||||
'video/webm',
|
||||
'video/ogg',
|
||||
'video/quicktime',
|
||||
'video/x-msvideo',
|
||||
'video/x-matroska'
|
||||
];
|
||||
|
||||
if (allowedMimes.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error(`Tipo file non supportato: ${file.mimetype}`), false);
|
||||
}
|
||||
}
|
||||
|
||||
getUploader(options = {}) {
|
||||
const config = {
|
||||
storage: this._createStorage(),
|
||||
fileFilter: this._fileFilter.bind(this),
|
||||
limits: {
|
||||
fileSize: options.maxSize || 500 * 1024 * 1024,
|
||||
files: options.maxFiles || 10
|
||||
}
|
||||
};
|
||||
|
||||
return multer(config);
|
||||
}
|
||||
|
||||
single(fieldName = 'video') {
|
||||
return this.getUploader().single(fieldName);
|
||||
}
|
||||
|
||||
multiple(fieldName = 'videos', maxCount = 10) {
|
||||
return this.getUploader({ maxFiles: maxCount }).array(fieldName, maxCount);
|
||||
}
|
||||
|
||||
getBasePath() {
|
||||
return this.baseUploadPath;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UploadMiddleware;
|
||||
137
src/models/Asset.js
Normal file
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);
|
||||
241
src/models/Message.js
Normal file
241
src/models/Message.js
Normal file
@@ -0,0 +1,241 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const MessageSchema = new Schema({
|
||||
idapp: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
chatId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Chat',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
senderId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 2000
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['text', 'ride_share', 'location', 'image', 'voice', 'system', 'ride_request', 'ride_accepted', 'ride_rejected'],
|
||||
default: 'text'
|
||||
},
|
||||
metadata: {
|
||||
// Per messaggi speciali (condivisione viaggio, posizione, ecc.)
|
||||
rideId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Ride'
|
||||
},
|
||||
rideRequestId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'RideRequest'
|
||||
},
|
||||
location: {
|
||||
lat: Number,
|
||||
lng: Number,
|
||||
address: String
|
||||
},
|
||||
imageUrl: String,
|
||||
voiceUrl: String,
|
||||
voiceDuration: Number,
|
||||
systemAction: String
|
||||
},
|
||||
readBy: [{
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
},
|
||||
readAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}],
|
||||
replyTo: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Message'
|
||||
},
|
||||
isEdited: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
editedAt: {
|
||||
type: Date
|
||||
},
|
||||
isDeleted: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
deletedAt: {
|
||||
type: Date
|
||||
},
|
||||
reactions: [{
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
},
|
||||
emoji: String,
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true }
|
||||
});
|
||||
|
||||
// Indici per query efficienti
|
||||
MessageSchema.index({ chatId: 1, createdAt: -1 });
|
||||
MessageSchema.index({ senderId: 1, createdAt: -1 });
|
||||
MessageSchema.index({ idapp: 1, chatId: 1 });
|
||||
|
||||
// Virtual per verificare se il messaggio è stato letto da tutti
|
||||
MessageSchema.virtual('isReadByAll').get(function() {
|
||||
// Logica da implementare confrontando con partecipanti chat
|
||||
return false;
|
||||
});
|
||||
|
||||
// Metodo per marcare come letto da un utente
|
||||
MessageSchema.methods.markAsReadBy = function(userId) {
|
||||
const alreadyRead = this.readBy.some(
|
||||
r => r.userId.toString() === userId.toString()
|
||||
);
|
||||
|
||||
if (!alreadyRead) {
|
||||
this.readBy.push({
|
||||
userId,
|
||||
readAt: new Date()
|
||||
});
|
||||
return this.save();
|
||||
}
|
||||
return Promise.resolve(this);
|
||||
};
|
||||
|
||||
// Metodo per verificare se è stato letto da un utente
|
||||
MessageSchema.methods.isReadBy = function(userId) {
|
||||
return this.readBy.some(
|
||||
r => r.userId.toString() === userId.toString()
|
||||
);
|
||||
};
|
||||
|
||||
// Metodo per aggiungere reazione
|
||||
MessageSchema.methods.addReaction = function(userId, emoji) {
|
||||
// Rimuovi eventuale reazione precedente dello stesso utente
|
||||
this.reactions = this.reactions.filter(
|
||||
r => r.userId.toString() !== userId.toString()
|
||||
);
|
||||
|
||||
this.reactions.push({
|
||||
userId,
|
||||
emoji,
|
||||
createdAt: new Date()
|
||||
});
|
||||
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo per rimuovere reazione
|
||||
MessageSchema.methods.removeReaction = function(userId) {
|
||||
this.reactions = this.reactions.filter(
|
||||
r => r.userId.toString() !== userId.toString()
|
||||
);
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo per soft delete
|
||||
MessageSchema.methods.softDelete = function() {
|
||||
this.isDeleted = true;
|
||||
this.deletedAt = new Date();
|
||||
this.text = '[Messaggio eliminato]';
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo per modificare testo
|
||||
MessageSchema.methods.editText = function(newText) {
|
||||
this.text = newText;
|
||||
this.isEdited = true;
|
||||
this.editedAt = new Date();
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo statico per ottenere messaggi di una chat con paginazione
|
||||
// Message.js (model)
|
||||
|
||||
MessageSchema.statics.getByChat = async function(idapp, chatId, options = {}) {
|
||||
const { limit = 50, before, after } = options;
|
||||
|
||||
const query = {
|
||||
idapp,
|
||||
chatId,
|
||||
isDeleted: false
|
||||
};
|
||||
|
||||
// Filtra per timestamp
|
||||
if (before) {
|
||||
query.createdAt = { $lt: new Date(before) };
|
||||
}
|
||||
if (after) {
|
||||
query.createdAt = { $gt: new Date(after) };
|
||||
}
|
||||
|
||||
// ✅ Sempre in ordine decrescente (dal più recente al più vecchio)
|
||||
return this.find(query)
|
||||
.populate('senderId', 'username name surname profile.img')
|
||||
.populate('replyTo', 'text senderId')
|
||||
.sort({ createdAt: -1 }) // -1 = più recente prima
|
||||
.limit(limit)
|
||||
};
|
||||
|
||||
// Metodo statico per creare messaggio di sistema
|
||||
MessageSchema.statics.createSystemMessage = async function(idapp, chatId, text, action = null) {
|
||||
const message = new this({
|
||||
idapp,
|
||||
chatId,
|
||||
senderId: null, // Sistema
|
||||
text,
|
||||
type: 'system',
|
||||
metadata: {
|
||||
systemAction: action
|
||||
}
|
||||
});
|
||||
return message.save();
|
||||
};
|
||||
|
||||
// Metodo statico per contare messaggi non letti
|
||||
MessageSchema.statics.countUnreadForUser = async function(idapp, chatId, userId) {
|
||||
return this.countDocuments({
|
||||
idapp,
|
||||
chatId,
|
||||
isDeleted: false,
|
||||
senderId: { $ne: userId },
|
||||
'readBy.userId': { $ne: userId }
|
||||
});
|
||||
};
|
||||
|
||||
// Hook post-save per aggiornare la chat
|
||||
MessageSchema.post('save', async function(doc) {
|
||||
try {
|
||||
const Chat = mongoose.model('Chat');
|
||||
const chat = await Chat.findById(doc.chatId);
|
||||
|
||||
if (chat) {
|
||||
await chat.updateLastMessage(doc);
|
||||
await chat.incrementUnread(doc.senderId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Errore aggiornamento chat dopo messaggio:', error);
|
||||
}
|
||||
});
|
||||
|
||||
const Message = mongoose.model('Message', MessageSchema);
|
||||
|
||||
module.exports = Message;
|
||||
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') {
|
||||
|
||||
@@ -56,11 +56,11 @@ ContribtypeSchema.statics.findAllIdApp = async function (idapp) {
|
||||
return await Contribtype.find(myfind).lean();
|
||||
};
|
||||
|
||||
const Contribtype = mongoose.model('Contribtype', ContribtypeSchema);
|
||||
const Contribtype = mongoose.models.Contribtype || mongoose.model('Contribtype', ContribtypeSchema);
|
||||
|
||||
Contribtype.createIndexes()
|
||||
.then(() => { })
|
||||
.catch((err) => { throw err; });
|
||||
|
||||
|
||||
module.exports = { Contribtype };
|
||||
module.exports = { Contribtype };
|
||||
@@ -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,8 @@ const SiteSchema = new Schema({
|
||||
bookingEvents: { type: Boolean, default: false },
|
||||
enableEcommerce: { type: Boolean, default: false },
|
||||
enableAI: { type: Boolean, default: false },
|
||||
enablePoster: { type: Boolean, default: false },
|
||||
enableTrasporti: { type: Boolean, default: false },
|
||||
enableGroups: { type: Boolean, default: false },
|
||||
enableCircuits: { type: Boolean, default: false },
|
||||
enableGoods: { type: Boolean, default: false },
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const bcrypt = require('bcryptjs');
|
||||
const mongoose = require('mongoose').set('debug', false);
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const validator = require('validator');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const _ = require('lodash');
|
||||
@@ -30,6 +32,8 @@ const i18n = require('i18n');
|
||||
|
||||
const shared_consts = require('../tools/shared_nodejs');
|
||||
|
||||
const { notificationPreferencesSchema } = require('../controllers/viaggi/trasportiNotificationsController');
|
||||
|
||||
mongoose.Promise = global.Promise;
|
||||
|
||||
mongoose.level = 'F';
|
||||
@@ -56,6 +60,9 @@ const UserSchema = new mongoose.Schema(
|
||||
message: '{VALUE} is not a valid email'
|
||||
}*/
|
||||
},
|
||||
link_verif_email: {
|
||||
type: String,
|
||||
},
|
||||
hash: {
|
||||
type: String,
|
||||
},
|
||||
@@ -282,6 +289,10 @@ const UserSchema = new mongoose.Schema(
|
||||
cell: {
|
||||
type: String,
|
||||
},
|
||||
cellVerified: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
country_pay: {
|
||||
type: String,
|
||||
},
|
||||
@@ -581,6 +592,266 @@ const UserSchema = new mongoose.Schema(
|
||||
],
|
||||
version: { type: Number },
|
||||
insert_circuito_ita: { type: Boolean },
|
||||
|
||||
// ============ DRIVER PROFILE ============
|
||||
driverProfile: {
|
||||
isDriver: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
bio: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 500,
|
||||
},
|
||||
vehicles: [
|
||||
{
|
||||
type: {
|
||||
type: String,
|
||||
default: 'auto',
|
||||
},
|
||||
brand: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
model: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
colorHex: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
year: {
|
||||
type: Number,
|
||||
},
|
||||
seats: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
max: 50,
|
||||
},
|
||||
licensePlate: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
features: [
|
||||
{
|
||||
type: String,
|
||||
},
|
||||
],
|
||||
photos: [
|
||||
{
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
],
|
||||
isDefault: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isVerified: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
ridesCompletedAsDriver: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
ridesCompletedAsPassenger: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
averageRating: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 5,
|
||||
},
|
||||
totalRatings: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
verifiedDriver: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
licenseVerified: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
licenseNumber: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
licenseExpiry: {
|
||||
type: Date,
|
||||
},
|
||||
memberSince: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
responseRate: {
|
||||
type: Number,
|
||||
default: 100,
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
responseTime: {
|
||||
type: String,
|
||||
default: 'within_day',
|
||||
},
|
||||
totalKmShared: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
co2Saved: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
// kg di CO2 risparmiati
|
||||
},
|
||||
badges: [
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
},
|
||||
earnedAt: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
},
|
||||
],
|
||||
level: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
min: 1,
|
||||
},
|
||||
points: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
|
||||
// ============ PREFERENCES ============
|
||||
preferences: {
|
||||
// Preferenze di viaggio
|
||||
smoking: {
|
||||
type: String,
|
||||
default: 'no',
|
||||
},
|
||||
pets: {
|
||||
type: String,
|
||||
default: 'small',
|
||||
},
|
||||
music: {
|
||||
type: String,
|
||||
default: 'moderate',
|
||||
},
|
||||
conversation: {
|
||||
type: String,
|
||||
default: 'moderate',
|
||||
},
|
||||
|
||||
// Notifiche
|
||||
notifications: {
|
||||
rideRequests: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
rideAccepted: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
rideReminders: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
messages: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
marketing: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
pushEnabled: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
emailEnabled: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
// Privacy
|
||||
privacy: {
|
||||
showEmail: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showPhone: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showLastName: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showRides: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
// Località preferite
|
||||
favoriteLocations: [
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
city: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
address: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
coordinates: {
|
||||
lat: Number,
|
||||
lng: Number,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'other',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
// Lingue parlate
|
||||
languages: [
|
||||
{
|
||||
type: String,
|
||||
},
|
||||
],
|
||||
|
||||
// Metodo di pagamento preferito
|
||||
preferredContribType: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Contribtype',
|
||||
},
|
||||
},
|
||||
},
|
||||
notificationPreferences: {
|
||||
type: notificationPreferencesSchema,
|
||||
default: () => ({}),
|
||||
},
|
||||
updatedAt: { type: Date, default: Date.now },
|
||||
},
|
||||
@@ -2614,6 +2885,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 } } });
|
||||
@@ -6801,7 +7078,9 @@ UserSchema.statics.addNewSite = async function (idappPass, body) {
|
||||
}
|
||||
|
||||
if (arrSite && arrSite.length === 1 && numutenti < 2) {
|
||||
const MyTelegramBot = require('../telegram/telegrambot');
|
||||
//const MyTelegramBot = require('../telegram/telegrambot');
|
||||
const MyTelegramBot = require('../telegram');
|
||||
|
||||
|
||||
// Nessun Sito Installato e Nessun Utente installato !
|
||||
let myuser = new User();
|
||||
@@ -6989,30 +7268,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 {
|
||||
constructor(name, level) {
|
||||
this.name = name;
|
||||
@@ -7069,6 +7342,8 @@ const FuncUsers = {
|
||||
|
||||
UserSchema.index({ 'tokens.token': 1, 'tokens.access': 1, idapp: 1, deleted: 1, updatedAt: 1 });
|
||||
|
||||
const User = mongoose.models.User || mongoose.model('User', UserSchema);
|
||||
|
||||
module.exports = {
|
||||
User,
|
||||
Hero,
|
||||
|
||||
237
src/models/viaggi/Chat.js
Normal file
237
src/models/viaggi/Chat.js
Normal file
@@ -0,0 +1,237 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const LastMessageSchema = new Schema(
|
||||
{
|
||||
text: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
senderId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
},
|
||||
timestamp: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
type: String,
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const ChatSchema = new Schema(
|
||||
{
|
||||
idapp: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
participants: [
|
||||
{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
rideId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Ride',
|
||||
index: true,
|
||||
// Opzionale: chat collegata a un viaggio specifico
|
||||
},
|
||||
rideRequestId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'RideRequest',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['direct', 'ride', 'group'],
|
||||
default: 'direct',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
trim: true,
|
||||
// Solo per chat di gruppo
|
||||
},
|
||||
lastMessage: {
|
||||
type: LastMessageSchema,
|
||||
},
|
||||
unreadCount: {
|
||||
type: Map,
|
||||
of: Number,
|
||||
default: new Map(),
|
||||
// { odIdUtente: numeroMessaggiNonLetti }
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
mutedBy: [
|
||||
{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
},
|
||||
],
|
||||
blockedBy: [
|
||||
{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
},
|
||||
],
|
||||
deletedBy: [
|
||||
{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
},
|
||||
],
|
||||
clearedBefore: {
|
||||
type: Map,
|
||||
of: Date,
|
||||
},
|
||||
metadata: {
|
||||
type: Schema.Types.Mixed,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true },
|
||||
}
|
||||
);
|
||||
|
||||
// Indici
|
||||
ChatSchema.index({ participants: 1 });
|
||||
ChatSchema.index({ idapp: 1, participants: 1 });
|
||||
ChatSchema.index({ idapp: 1, updatedAt: -1 });
|
||||
|
||||
// Virtual per contare messaggi non letti totali
|
||||
ChatSchema.virtual('totalUnread').get(function () {
|
||||
if (!this.unreadCount) return 0;
|
||||
let total = 0;
|
||||
this.unreadCount.forEach((count) => {
|
||||
total += count;
|
||||
});
|
||||
return total;
|
||||
});
|
||||
|
||||
// Metodo per ottenere unread count per un utente specifico
|
||||
ChatSchema.methods.getUnreadForUser = function (userId) {
|
||||
if (!this.unreadCount) return 0;
|
||||
return this.unreadCount.get(userId.toString()) || 0;
|
||||
};
|
||||
|
||||
// ✅ FIX: incrementUnread (assicura conversione corretta)
|
||||
ChatSchema.methods.incrementUnread = function (excludeUserId) {
|
||||
const excludeIdStr = excludeUserId.toString();
|
||||
|
||||
this.participants.forEach((participantId) => {
|
||||
// Gestisci sia ObjectId che oggetti popolati
|
||||
const id = participantId._id ? participantId._id.toString() : participantId.toString();
|
||||
|
||||
if (id !== excludeIdStr) {
|
||||
const current = this.unreadCount.get(id) || 0;
|
||||
this.unreadCount.set(id, current + 1);
|
||||
}
|
||||
});
|
||||
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo per resettare unread count per un utente
|
||||
ChatSchema.methods.markAsRead = function (userId) {
|
||||
this.unreadCount.set(userId.toString(), 0);
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo per aggiornare ultimo messaggio
|
||||
ChatSchema.methods.updateLastMessage = function (message) {
|
||||
this.lastMessage = {
|
||||
text: message.text,
|
||||
senderId: message.senderId,
|
||||
timestamp: message.createdAt || new Date(),
|
||||
type: message.type || 'text',
|
||||
};
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo per verificare se un utente è partecipante
|
||||
// ✅ FIX: Gestisce sia ObjectId che oggetti User popolati
|
||||
ChatSchema.methods.hasParticipant = function (userId) {
|
||||
const userIdStr = userId.toString();
|
||||
|
||||
return this.participants.some((p) => {
|
||||
// Se p è un oggetto popolato (ha _id), usa p._id
|
||||
// Altrimenti p è già un ObjectId
|
||||
const participantId = p._id ? p._id.toString() : p.toString();
|
||||
return participantId === userIdStr;
|
||||
});
|
||||
};
|
||||
|
||||
// Metodo per verificare se la chat è bloccata per un utente
|
||||
// ✅ FIX: Metodo isBlockedFor (stesso problema)
|
||||
ChatSchema.methods.isBlockedFor = function (userId) {
|
||||
const userIdStr = userId.toString();
|
||||
|
||||
return this.blockedBy.some((id) => {
|
||||
const blockedId = id._id ? id._id.toString() : id.toString();
|
||||
return blockedId === userIdStr;
|
||||
});
|
||||
};
|
||||
|
||||
// Metodo statico per trovare o creare una chat diretta
|
||||
ChatSchema.statics.findOrCreateDirect = async function (idapp, userId1, userId2, rideId = null) {
|
||||
// Cerca chat esistente tra i due utenti
|
||||
let chat = await this.findOne({
|
||||
idapp,
|
||||
type: 'direct',
|
||||
participants: { $all: [userId1, userId2], $size: 2 },
|
||||
});
|
||||
|
||||
if (!chat) {
|
||||
chat = new this({
|
||||
idapp,
|
||||
type: 'direct',
|
||||
participants: [userId1, userId2],
|
||||
rideId,
|
||||
unreadCount: new Map(),
|
||||
});
|
||||
await chat.save();
|
||||
} else if (rideId && !chat.rideId) {
|
||||
// Aggiorna con rideId se fornito
|
||||
chat.rideId = rideId;
|
||||
await chat.save();
|
||||
}
|
||||
|
||||
return chat;
|
||||
};
|
||||
|
||||
// Metodo statico per ottenere tutte le chat di un utente
|
||||
ChatSchema.statics.getChatsForUser = function (idapp, userId) {
|
||||
return this.find({
|
||||
idapp,
|
||||
participants: userId,
|
||||
isActive: true,
|
||||
blockedBy: { $ne: userId },
|
||||
})
|
||||
.populate('participants', 'username name surname profile.avatar')
|
||||
.populate('rideId', 'departure destination dateTime')
|
||||
.sort({ updatedAt: -1 });
|
||||
};
|
||||
|
||||
// Metodo statico per creare chat di gruppo per un viaggio
|
||||
ChatSchema.statics.createRideGroupChat = async function (idapp, rideId, title, participantIds) {
|
||||
const chat = new this({
|
||||
idapp,
|
||||
type: 'group',
|
||||
rideId,
|
||||
title,
|
||||
participants: participantIds,
|
||||
unreadCount: new Map(),
|
||||
});
|
||||
return chat.save();
|
||||
};
|
||||
|
||||
const Chat = mongoose.model('Chat', ChatSchema);
|
||||
|
||||
module.exports = Chat;
|
||||
357
src/models/viaggi/Feedback.js
Normal file
357
src/models/viaggi/Feedback.js
Normal file
@@ -0,0 +1,357 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const FeedbackCategoriesSchema = new Schema(
|
||||
{
|
||||
punctuality: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
max: 5,
|
||||
},
|
||||
cleanliness: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
max: 5,
|
||||
},
|
||||
communication: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
max: 5,
|
||||
},
|
||||
driving: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
max: 5,
|
||||
// Solo per feedback a conducenti
|
||||
},
|
||||
respect: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
max: 5,
|
||||
},
|
||||
reliability: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
max: 5,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const FeedbackSchema = new Schema(
|
||||
{
|
||||
idapp: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
rideId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Ride',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
rideRequestId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'RideRequest',
|
||||
},
|
||||
fromUserId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
toUserId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
enum: ['driver', 'passenger'],
|
||||
required: true,
|
||||
// Il ruolo dell'utente che RICEVE il feedback
|
||||
// 'driver' = sto valutando il conducente
|
||||
// 'passenger' = sto valutando il passeggero
|
||||
},
|
||||
rating: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 1,
|
||||
max: 5,
|
||||
},
|
||||
categories: {
|
||||
type: FeedbackCategoriesSchema,
|
||||
},
|
||||
comment: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 1000,
|
||||
},
|
||||
pros: [
|
||||
{
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
],
|
||||
cons: [
|
||||
{
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
],
|
||||
tags: [
|
||||
{
|
||||
type: String,
|
||||
enum: [
|
||||
'puntuale',
|
||||
'gentile',
|
||||
'auto_pulita',
|
||||
'guida_sicura',
|
||||
'buona_conversazione',
|
||||
'silenzioso',
|
||||
'flessibile',
|
||||
'rispettoso',
|
||||
'affidabile',
|
||||
'consigliato',
|
||||
// Tag negativi
|
||||
'in_ritardo',
|
||||
'scortese',
|
||||
'guida_pericolosa',
|
||||
'auto_sporca',
|
||||
'non_rispettoso',
|
||||
],
|
||||
},
|
||||
],
|
||||
isPublic: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
isVerified: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
// Feedback verificato (viaggio effettivamente completato)
|
||||
},
|
||||
response: {
|
||||
text: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 500,
|
||||
},
|
||||
respondedAt: {
|
||||
type: Date,
|
||||
},
|
||||
},
|
||||
helpful: {
|
||||
count: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
users: [
|
||||
{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
},
|
||||
],
|
||||
},
|
||||
reported: {
|
||||
isReported: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
reason: String,
|
||||
reportedBy: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
},
|
||||
reportedAt: Date,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true },
|
||||
}
|
||||
);
|
||||
|
||||
// Indici composti
|
||||
FeedbackSchema.index({ toUserId: 1, role: 1 });
|
||||
FeedbackSchema.index({ rideId: 1, fromUserId: 1 });
|
||||
FeedbackSchema.index({ idapp: 1, toUserId: 1 });
|
||||
|
||||
// Vincolo: un utente può lasciare un solo feedback per viaggio verso un altro utente
|
||||
FeedbackSchema.index({ rideId: 1, fromUserId: 1, toUserId: 1 }, { unique: true });
|
||||
|
||||
// Virtual per calcolare media categorie
|
||||
FeedbackSchema.virtual('categoryAverage').get(function () {
|
||||
if (!this.categories) return null;
|
||||
const cats = this.categories.toObject ? this.categories.toObject() : this.categories;
|
||||
const values = Object.values(cats).filter((v) => typeof v === 'number');
|
||||
if (values.length === 0) return null;
|
||||
return values.reduce((a, b) => a + b, 0) / values.length;
|
||||
});
|
||||
|
||||
// Metodo per aggiungere risposta
|
||||
FeedbackSchema.methods.addResponse = function (text) {
|
||||
this.response = {
|
||||
text,
|
||||
respondedAt: new Date(),
|
||||
};
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo per segnare come utile
|
||||
FeedbackSchema.methods.markAsHelpful = function (userId) {
|
||||
if (!this.helpful.users.includes(userId)) {
|
||||
this.helpful.users.push(userId);
|
||||
this.helpful.count = this.helpful.users.length;
|
||||
return this.save();
|
||||
}
|
||||
return Promise.resolve(this);
|
||||
};
|
||||
|
||||
// Metodo per segnalare feedback
|
||||
FeedbackSchema.methods.report = function (userId, reason) {
|
||||
this.reported = {
|
||||
isReported: true,
|
||||
reason,
|
||||
reportedBy: userId,
|
||||
reportedAt: new Date(),
|
||||
};
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo statico per ottenere feedback di un utente
|
||||
FeedbackSchema.statics.getForUser = function (idapp, userId, options = {}) {
|
||||
const query = {
|
||||
idapp,
|
||||
toUserId: userId,
|
||||
isPublic: true,
|
||||
};
|
||||
|
||||
if (options.role) {
|
||||
query.role = options.role;
|
||||
}
|
||||
|
||||
return this.find(query)
|
||||
.populate('fromUserId', 'username name surname profile.avatar')
|
||||
.populate('rideId', 'departure destination dateTime')
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(options.limit || 20);
|
||||
};
|
||||
|
||||
// Metodo statico per calcolare statistiche
|
||||
FeedbackSchema.statics.getStatsForUser = async function (idapp, userId) {
|
||||
const stats = await this.aggregate([
|
||||
{
|
||||
$match: {
|
||||
idapp,
|
||||
toUserId: new mongoose.Types.ObjectId(userId),
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$role',
|
||||
averageRating: { $avg: '$rating' },
|
||||
totalFeedbacks: { $sum: 1 },
|
||||
avgPunctuality: { $avg: '$categories.punctuality' },
|
||||
avgCleanliness: { $avg: '$categories.cleanliness' },
|
||||
avgCommunication: { $avg: '$categories.communication' },
|
||||
avgDriving: { $avg: '$categories.driving' },
|
||||
avgRespect: { $avg: '$categories.respect' },
|
||||
avgReliability: { $avg: '$categories.reliability' },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// Trasforma in oggetto più leggibile
|
||||
const result = {
|
||||
asDriver: null,
|
||||
asPassenger: null,
|
||||
overall: {
|
||||
averageRating: 0,
|
||||
totalFeedbacks: 0,
|
||||
},
|
||||
};
|
||||
|
||||
stats.forEach((stat) => {
|
||||
const data = {
|
||||
averageRating: Math.round(stat.averageRating * 10) / 10,
|
||||
totalFeedbacks: stat.totalFeedbacks,
|
||||
categories: {
|
||||
punctuality: stat.avgPunctuality ? Math.round(stat.avgPunctuality * 10) / 10 : null,
|
||||
cleanliness: stat.avgCleanliness ? Math.round(stat.avgCleanliness * 10) / 10 : null,
|
||||
communication: stat.avgCommunication ? Math.round(stat.avgCommunication * 10) / 10 : null,
|
||||
driving: stat.avgDriving ? Math.round(stat.avgDriving * 10) / 10 : null,
|
||||
respect: stat.avgRespect ? Math.round(stat.avgRespect * 10) / 10 : null,
|
||||
reliability: stat.avgReliability ? Math.round(stat.avgReliability * 10) / 10 : null,
|
||||
},
|
||||
};
|
||||
|
||||
if (stat._id === 'driver') {
|
||||
result.asDriver = data;
|
||||
} else if (stat._id === 'passenger') {
|
||||
result.asPassenger = data;
|
||||
}
|
||||
});
|
||||
|
||||
// Calcola overall
|
||||
const allStats = stats.reduce(
|
||||
(acc, s) => {
|
||||
acc.total += s.totalFeedbacks;
|
||||
acc.sum += s.averageRating * s.totalFeedbacks;
|
||||
return acc;
|
||||
},
|
||||
{ total: 0, sum: 0 }
|
||||
);
|
||||
|
||||
if (allStats.total > 0) {
|
||||
result.overall = {
|
||||
averageRating: Math.round((allStats.sum / allStats.total) * 10) / 10,
|
||||
totalFeedbacks: allStats.total,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Metodo statico per contare distribuzioni rating
|
||||
FeedbackSchema.statics.getRatingDistribution = async function (idapp, userId, role = null) {
|
||||
const match = {
|
||||
idapp,
|
||||
toUserId: new mongoose.Types.ObjectId(userId),
|
||||
};
|
||||
if (role) match.role = role;
|
||||
|
||||
return this.aggregate([
|
||||
{ $match: match },
|
||||
{
|
||||
$group: {
|
||||
_id: '$rating',
|
||||
count: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
{ $sort: { _id: -1 } },
|
||||
]);
|
||||
};
|
||||
|
||||
// Hook post-save per aggiornare rating utente
|
||||
FeedbackSchema.post('save', async function (doc) {
|
||||
try {
|
||||
const { User } = require('../User');
|
||||
|
||||
const stats = await mongoose.model('Feedback').getStatsForUser(doc.idapp, doc.toUserId);
|
||||
|
||||
await User.findByIdAndUpdate(doc.toUserId, {
|
||||
'profile.driverProfile.averageRating': stats.overall.averageRating,
|
||||
'profile.driverProfile.totalRatings': stats.overall.totalFeedbacks,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore aggiornamento rating utente:', error);
|
||||
}
|
||||
});
|
||||
|
||||
const Feedback = mongoose.model('Feedback', FeedbackSchema);
|
||||
|
||||
module.exports = Feedback;
|
||||
538
src/models/viaggi/Ride.js
Normal file
538
src/models/viaggi/Ride.js
Normal file
@@ -0,0 +1,538 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
// Schema per le coordinate geografiche
|
||||
const CoordinatesSchema = new Schema(
|
||||
{
|
||||
lat: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
lng: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema per una località (partenza, destinazione, waypoint)
|
||||
const LocationSchema = new Schema(
|
||||
{
|
||||
city: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
address: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
province: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
region: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
country: {
|
||||
type: String,
|
||||
default: 'Italia',
|
||||
trim: true,
|
||||
},
|
||||
postalCode: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
coordinates: {
|
||||
type: CoordinatesSchema,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema per i waypoint (tappe intermedie)
|
||||
const WaypointSchema = new Schema({
|
||||
/*_id: {
|
||||
type: String,
|
||||
required: false
|
||||
},*/
|
||||
location: {
|
||||
type: LocationSchema,
|
||||
required: true,
|
||||
},
|
||||
order: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
estimatedArrival: {
|
||||
type: Date,
|
||||
},
|
||||
stopDuration: {
|
||||
type: Number, // minuti di sosta
|
||||
default: 0,
|
||||
},
|
||||
}, { _id: false }); // 👈 AGGIUNGI QUESTO
|
||||
|
||||
// Schema per la ricorrenza del viaggio
|
||||
const RecurrenceSchema = new Schema(
|
||||
{
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['once', 'weekly', 'custom_days', 'custom_dates'],
|
||||
default: 'once',
|
||||
},
|
||||
daysOfWeek: [
|
||||
{
|
||||
type: Number,
|
||||
min: 0,
|
||||
max: 6,
|
||||
// 0 = Domenica, 1 = Lunedì, ..., 6 = Sabato
|
||||
},
|
||||
],
|
||||
customDates: [
|
||||
{
|
||||
type: Date,
|
||||
},
|
||||
],
|
||||
startDate: {
|
||||
type: Date,
|
||||
},
|
||||
endDate: {
|
||||
type: Date,
|
||||
},
|
||||
excludedDates: [
|
||||
{
|
||||
type: Date,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema per i passeggeri
|
||||
const PassengersSchema = new Schema(
|
||||
{
|
||||
available: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 1,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema per il veicolo
|
||||
const VehicleSchema = new Schema(
|
||||
{
|
||||
type: {
|
||||
type: String,
|
||||
default: 'auto',
|
||||
},
|
||||
brand: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
model: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
colorHex: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
year: {
|
||||
type: Number,
|
||||
},
|
||||
licensePlate: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
seats: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
},
|
||||
photos: [
|
||||
{
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
],
|
||||
features: [
|
||||
{
|
||||
type: String,
|
||||
enum: ['aria_condizionata', 'wifi', 'presa_usb', 'bluetooth', 'bagagliaio_grande', 'seggiolino_bimbi'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema per le preferenze di viaggio
|
||||
const RidePreferencesSchema = new Schema(
|
||||
{
|
||||
smoking: {
|
||||
type: String,
|
||||
enum: ['yes', 'no', 'outside_only'],
|
||||
default: 'no',
|
||||
},
|
||||
pets: {
|
||||
type: String,
|
||||
enum: ['no', 'small', 'medium', 'large', 'all'],
|
||||
default: 'no',
|
||||
},
|
||||
luggage: {
|
||||
type: String,
|
||||
enum: ['none', 'small', 'medium', 'large'],
|
||||
default: 'medium',
|
||||
},
|
||||
packages: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
maxPackageSize: {
|
||||
type: String,
|
||||
enum: ['small', 'medium', 'large', 'xlarge'],
|
||||
default: 'medium',
|
||||
},
|
||||
music: {
|
||||
type: String,
|
||||
enum: ['no_music', 'quiet', 'moderate', 'loud', 'passenger_choice'],
|
||||
default: 'moderate',
|
||||
},
|
||||
conversation: {
|
||||
type: String,
|
||||
enum: ['quiet', 'moderate', 'chatty'],
|
||||
default: 'moderate',
|
||||
},
|
||||
foodAllowed: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
childrenFriendly: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
wheelchairAccessible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
otherPreferences: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 500,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema per il contributo/pagamento
|
||||
const ContributionItemSchema = new Schema({
|
||||
contribTypeId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Contribtype',
|
||||
required: true,
|
||||
},
|
||||
price: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
},
|
||||
pricePerKm: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
},
|
||||
notes: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
});
|
||||
|
||||
const ContributionSchema = new Schema(
|
||||
{
|
||||
contribTypes: [ContributionItemSchema],
|
||||
negotiable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
freeForStudents: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
freeForElders: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema principale del Ride
|
||||
const RideSchema = new Schema(
|
||||
{
|
||||
idapp: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['offer', 'request'],
|
||||
required: true,
|
||||
index: true,
|
||||
// offer = 🟢 Offerta passaggio (sono conducente)
|
||||
// request = 🔴 Richiesta passaggio (cerco passaggio)
|
||||
},
|
||||
departure: {
|
||||
type: LocationSchema,
|
||||
required: true,
|
||||
},
|
||||
destination: {
|
||||
type: LocationSchema,
|
||||
required: true,
|
||||
},
|
||||
waypoints: [WaypointSchema],
|
||||
departureDate: {
|
||||
type: Date,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
flexibleTime: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
flexibleMinutes: {
|
||||
type: Number,
|
||||
default: 30,
|
||||
min: 0,
|
||||
max: 180,
|
||||
},
|
||||
recurrence: {
|
||||
type: RecurrenceSchema,
|
||||
default: () => ({ type: 'once' }),
|
||||
},
|
||||
passengers: {
|
||||
type: PassengersSchema,
|
||||
required: function () {
|
||||
return this.type === 'offer';
|
||||
},
|
||||
},
|
||||
seatsNeeded: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
default: 1,
|
||||
// Solo per type = 'request'
|
||||
},
|
||||
vehicle: {
|
||||
type: VehicleSchema,
|
||||
required: function () {
|
||||
return this.type === 'offer';
|
||||
},
|
||||
},
|
||||
preferences: {
|
||||
type: RidePreferencesSchema,
|
||||
default: () => ({}),
|
||||
},
|
||||
contribution: {
|
||||
type: ContributionSchema,
|
||||
default: () => ({ contribTypes: [] }),
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['draft', 'active', 'full', 'in_progress', 'completed', 'cancelled', 'expired'],
|
||||
default: 'active',
|
||||
index: true,
|
||||
},
|
||||
estimatedDistance: {
|
||||
type: Number, // in km
|
||||
min: 0,
|
||||
},
|
||||
estimatedDuration: {
|
||||
type: Number, // in minuti
|
||||
min: 0,
|
||||
},
|
||||
routePolyline: {
|
||||
type: String, // Polyline encoded per visualizzare il percorso
|
||||
},
|
||||
confirmedPassengers: [
|
||||
{
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
},
|
||||
seats: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
pickupPoint: LocationSchema,
|
||||
dropoffPoint: LocationSchema,
|
||||
confirmedAt: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
},
|
||||
],
|
||||
views: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
isFeatured: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
notes: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 1000,
|
||||
},
|
||||
cancellationReason: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
cancelledAt: {
|
||||
type: Date,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true },
|
||||
}
|
||||
);
|
||||
|
||||
// Indici per ricerche ottimizzate
|
||||
RideSchema.index({ 'departure.city': 1, 'destination.city': 1 });
|
||||
RideSchema.index({ 'departure.coordinates': '2dsphere' });
|
||||
RideSchema.index({ 'destination.coordinates': '2dsphere' });
|
||||
RideSchema.index({ 'waypoints.location.city': 1 });
|
||||
RideSchema.index({ departureDate: 1, status: 1 });
|
||||
RideSchema.index({ idapp: 1, status: 1, departureDate: 1 });
|
||||
|
||||
// Virtual per verificare se il viaggio è pieno
|
||||
RideSchema.virtual('isFull').get(function () {
|
||||
if (this.type === 'request') return false;
|
||||
// ⚠️ CONTROLLO: verifica che passengers esista
|
||||
if (!this.passengers || typeof this.passengers.available === 'undefined') return false;
|
||||
return this.passengers.available <= 0;
|
||||
});
|
||||
|
||||
// Virtual per calcolare posti occupati
|
||||
RideSchema.virtual('bookedSeats').get(function () {
|
||||
if (!this.confirmedPassengers) return 0;
|
||||
return this.confirmedPassengers.reduce((sum, p) => sum + (p.seats || 1), 0);
|
||||
});
|
||||
|
||||
// Virtual per ottenere tutte le città del percorso
|
||||
RideSchema.virtual('allCities').get(function () {
|
||||
const cities = [this.departure.city];
|
||||
if (this.waypoints && this.waypoints.length > 0) {
|
||||
this.waypoints.sort((a, b) => a.order - b.order).forEach((wp) => cities.push(wp.location.city));
|
||||
}
|
||||
cities.push(this.destination.city);
|
||||
return cities;
|
||||
});
|
||||
|
||||
// Metodo per verificare se passa per una città
|
||||
RideSchema.methods.passesThrough = function (cityName) {
|
||||
const normalizedCity = cityName.toLowerCase().trim();
|
||||
return this.allCities.some(
|
||||
(city) => city.toLowerCase().trim().includes(normalizedCity) || normalizedCity.includes(city.toLowerCase().trim())
|
||||
);
|
||||
};
|
||||
|
||||
// Metodo per aggiornare posti disponibili
|
||||
RideSchema.methods.updateAvailableSeats = function () {
|
||||
// ⚠️ CONTROLLO: verifica che sia un'offerta e che passengers esista
|
||||
if (this.type === 'offer' && this.passengers) {
|
||||
const booked = this.bookedSeats;
|
||||
this.passengers.available = this.passengers.max - booked;
|
||||
if (this.passengers.available <= 0) {
|
||||
this.status = 'full';
|
||||
} else if (this.status === 'full') {
|
||||
this.status = 'active';
|
||||
}
|
||||
}
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Pre-save hook
|
||||
RideSchema.pre('save', function (next) {
|
||||
// ⚠️ CONTROLLO: Aggiorna posti disponibili solo se è un'offerta e passengers esiste
|
||||
if (this.type === 'offer' && this.passengers && this.isModified('confirmedPassengers')) {
|
||||
const booked = this.confirmedPassengers.reduce((sum, p) => sum + (p.seats || 1), 0);
|
||||
this.passengers.available = this.passengers.max - booked;
|
||||
if (this.passengers.available <= 0) {
|
||||
this.status = 'full';
|
||||
}
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Metodi statici per ricerche comuni
|
||||
RideSchema.statics.findActiveByCity = function (idapp, departureCity, destinationCity, options = {}) {
|
||||
const query = {
|
||||
idapp,
|
||||
status: { $in: ['active', 'full'] },
|
||||
departureDate: { $gte: new Date() },
|
||||
};
|
||||
|
||||
if (departureCity) {
|
||||
query['departure.city'] = new RegExp(departureCity, 'i');
|
||||
}
|
||||
if (destinationCity) {
|
||||
query['destination.city'] = new RegExp(destinationCity, 'i');
|
||||
}
|
||||
if (options.type) {
|
||||
query.type = options.type;
|
||||
}
|
||||
if (options.date) {
|
||||
const startOfDay = new Date(options.date);
|
||||
startOfDay.setHours(0, 0, 0, 0);
|
||||
const endOfDay = new Date(options.date);
|
||||
endOfDay.setHours(23, 59, 59, 999);
|
||||
query.departureDate = { $gte: startOfDay, $lte: endOfDay };
|
||||
}
|
||||
|
||||
return this.find(query)
|
||||
.populate('userId', 'username name surname profile.driverProfile.averageRating')
|
||||
.sort({ departureDate: 1 });
|
||||
};
|
||||
|
||||
// Ricerca viaggi che passano per una città intermedia
|
||||
RideSchema.statics.findPassingThrough = function (idapp, cityName, options = {}) {
|
||||
const cityRegex = new RegExp(cityName, 'i');
|
||||
const query = {
|
||||
idapp,
|
||||
status: { $in: ['active'] },
|
||||
departureDate: { $gte: new Date() },
|
||||
$or: [{ 'departure.city': cityRegex }, { 'destination.city': cityRegex }, { 'waypoints.location.city': cityRegex }],
|
||||
};
|
||||
|
||||
if (options.type) {
|
||||
query.type = options.type;
|
||||
}
|
||||
|
||||
return this.find(query)
|
||||
.populate('userId', 'username name surname profile.driverProfile.averageRating')
|
||||
.sort({ departureDate: 1 });
|
||||
};
|
||||
|
||||
const Ride = mongoose.model('Ride', RideSchema);
|
||||
|
||||
module.exports = Ride;
|
||||
296
src/models/viaggi/RideRequest.js
Normal file
296
src/models/viaggi/RideRequest.js
Normal file
@@ -0,0 +1,296 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
// Schema per le coordinate
|
||||
const CoordinatesSchema = new Schema({
|
||||
lat: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
lng: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
}, { _id: false });
|
||||
|
||||
// Schema per località
|
||||
const LocationSchema = new Schema({
|
||||
city: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
address: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
province: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
coordinates: {
|
||||
type: CoordinatesSchema,
|
||||
required: true
|
||||
}
|
||||
}, { _id: false });
|
||||
|
||||
const RideRequestSchema = new Schema({
|
||||
idapp: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
rideId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Ride',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
passengerId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
driverId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 500
|
||||
},
|
||||
pickupPoint: {
|
||||
type: LocationSchema
|
||||
},
|
||||
dropoffPoint: {
|
||||
type: LocationSchema
|
||||
},
|
||||
useOriginalRoute: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
// true = usa partenza/destinazione originali del ride
|
||||
},
|
||||
seatsRequested: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 1,
|
||||
default: 1
|
||||
},
|
||||
hasLuggage: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
luggageSize: {
|
||||
type: String,
|
||||
enum: ['small', 'medium', 'large'],
|
||||
default: 'small'
|
||||
},
|
||||
hasPackages: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
packageDescription: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 200
|
||||
},
|
||||
hasPets: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
petType: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
petSize: {
|
||||
type: String,
|
||||
enum: ['small', 'medium', 'large']
|
||||
},
|
||||
specialNeeds: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 300
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['pending', 'accepted', 'rejected', 'cancelled', 'expired', 'completed'],
|
||||
default: 'pending',
|
||||
index: true
|
||||
},
|
||||
responseMessage: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 500
|
||||
},
|
||||
respondedAt: {
|
||||
type: Date
|
||||
},
|
||||
contribution: {
|
||||
agreed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
contribTypeId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Contribtype'
|
||||
},
|
||||
amount: {
|
||||
type: Number,
|
||||
min: 0
|
||||
},
|
||||
notes: {
|
||||
type: String,
|
||||
trim: true
|
||||
}
|
||||
},
|
||||
cancelledBy: {
|
||||
type: String,
|
||||
enum: ['passenger', 'driver']
|
||||
},
|
||||
cancellationReason: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
cancelledAt: {
|
||||
type: Date
|
||||
},
|
||||
completedAt: {
|
||||
type: Date
|
||||
},
|
||||
feedbackGiven: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}, {
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true }
|
||||
});
|
||||
|
||||
// Indici composti per ricerche ottimizzate
|
||||
RideRequestSchema.index({ rideId: 1, status: 1 });
|
||||
RideRequestSchema.index({ passengerId: 1, status: 1 });
|
||||
RideRequestSchema.index({ driverId: 1, status: 1 });
|
||||
RideRequestSchema.index({ idapp: 1, createdAt: -1 });
|
||||
|
||||
// Virtual per verificare se la richiesta può essere cancellata
|
||||
RideRequestSchema.virtual('canCancel').get(function() {
|
||||
return ['pending', 'accepted'].includes(this.status);
|
||||
});
|
||||
|
||||
// Virtual per verificare se è in attesa
|
||||
RideRequestSchema.virtual('isPending').get(function() {
|
||||
return this.status === 'pending';
|
||||
});
|
||||
|
||||
// Metodo per accettare la richiesta
|
||||
RideRequestSchema.methods.accept = async function(responseMessage = '') {
|
||||
this.status = 'accepted';
|
||||
this.responseMessage = responseMessage;
|
||||
this.respondedAt = new Date();
|
||||
|
||||
// Aggiorna il ride con il passeggero confermato
|
||||
const Ride = mongoose.model('Ride');
|
||||
const ride = await Ride.findById(this.rideId);
|
||||
|
||||
if (ride) {
|
||||
ride.confirmedPassengers.push({
|
||||
userId: this.passengerId,
|
||||
seats: this.seatsRequested,
|
||||
pickupPoint: this.pickupPoint || ride.departure,
|
||||
dropoffPoint: this.dropoffPoint || ride.destination,
|
||||
confirmedAt: new Date()
|
||||
});
|
||||
await ride.updateAvailableSeats();
|
||||
}
|
||||
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo per rifiutare la richiesta
|
||||
RideRequestSchema.methods.reject = function(responseMessage = '') {
|
||||
this.status = 'rejected';
|
||||
this.responseMessage = responseMessage;
|
||||
this.respondedAt = new Date();
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo per cancellare la richiesta
|
||||
RideRequestSchema.methods.cancel = async function(cancelledBy, reason = '') {
|
||||
this.status = 'cancelled';
|
||||
this.cancelledBy = cancelledBy;
|
||||
this.cancellationReason = reason;
|
||||
this.cancelledAt = new Date();
|
||||
|
||||
// Se era accettata, rimuovi il passeggero dal ride
|
||||
if (this.status === 'accepted') {
|
||||
const Ride = mongoose.model('Ride');
|
||||
const ride = await Ride.findById(this.rideId);
|
||||
|
||||
if (ride) {
|
||||
ride.confirmedPassengers = ride.confirmedPassengers.filter(
|
||||
p => p.userId.toString() !== this.passengerId.toString()
|
||||
);
|
||||
await ride.updateAvailableSeats();
|
||||
}
|
||||
}
|
||||
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo statico per ottenere richieste pendenti di un conducente
|
||||
RideRequestSchema.statics.getPendingForDriver = function(idapp, driverId) {
|
||||
return this.find({
|
||||
idapp,
|
||||
driverId,
|
||||
status: 'pending'
|
||||
})
|
||||
.populate('passengerId', 'username name surname email')
|
||||
.populate('rideId', 'departure destination departureDate')
|
||||
.sort({ createdAt: -1 });
|
||||
};
|
||||
|
||||
// Metodo statico per ottenere richieste di un passeggero
|
||||
RideRequestSchema.statics.getByPassenger = function(idapp, passengerId, status = null) {
|
||||
const query = { idapp, passengerId };
|
||||
if (status) {
|
||||
query.status = status;
|
||||
}
|
||||
return this.find(query)
|
||||
.populate('rideId')
|
||||
.populate('driverId', 'username name surname')
|
||||
.sort({ createdAt: -1 });
|
||||
};
|
||||
|
||||
// Pre-save hook per validazioni
|
||||
RideRequestSchema.pre('save', async function(next) {
|
||||
if (this.isNew) {
|
||||
// Verifica che il ride esista e abbia posti disponibili
|
||||
const Ride = mongoose.model('Ride');
|
||||
const ride = await Ride.findById(this.rideId);
|
||||
|
||||
if (!ride) {
|
||||
throw new Error('Viaggio non trovato');
|
||||
}
|
||||
|
||||
if (ride.type === 'offer' && ride.passengers.available < this.seatsRequested) {
|
||||
throw new Error('Posti non sufficienti per questo viaggio');
|
||||
}
|
||||
|
||||
if (ride.userId.toString() === this.passengerId.toString()) {
|
||||
throw new Error('Non puoi richiedere un passaggio per il tuo stesso viaggio');
|
||||
}
|
||||
|
||||
// Imposta il driverId dal ride
|
||||
this.driverId = ride.userId;
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
const RideRequest = mongoose.model('RideRequest', RideRequestSchema);
|
||||
|
||||
module.exports = RideRequest;
|
||||
310
src/models/viaggi/UserSettings.js
Normal file
310
src/models/viaggi/UserSettings.js
Normal file
@@ -0,0 +1,310 @@
|
||||
// ============================================================
|
||||
// 🔧 USER SETTINGS MODEL - Trasporti Solidali
|
||||
// ============================================================
|
||||
// File: server/models/viaggi/UserSettings.js
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const userSettingsSchema = new mongoose.Schema({
|
||||
// ID App e Utente
|
||||
idapp: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
userId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
required: true,
|
||||
ref: 'User',
|
||||
index: true
|
||||
},
|
||||
|
||||
// ============================================================
|
||||
// 🔔 NOTIFICHE
|
||||
// ============================================================
|
||||
notifications: {
|
||||
// Notifiche Email
|
||||
email: {
|
||||
newMessage: { type: Boolean, default: true },
|
||||
rideRequest: { type: Boolean, default: true },
|
||||
rideConfirmation: { type: Boolean, default: true },
|
||||
rideCancellation: { type: Boolean, default: true },
|
||||
rideReminder: { type: Boolean, default: true },
|
||||
feedbackReceived: { type: Boolean, default: true },
|
||||
newsletter: { type: Boolean, default: false }
|
||||
},
|
||||
// Notifiche Push
|
||||
push: {
|
||||
newMessage: { type: Boolean, default: true },
|
||||
rideRequest: { type: Boolean, default: true },
|
||||
rideConfirmation: { type: Boolean, default: true },
|
||||
rideCancellation: { type: Boolean, default: true },
|
||||
rideReminder: { type: Boolean, default: true },
|
||||
feedbackReceived: { type: Boolean, default: true }
|
||||
},
|
||||
// Notifiche In-App
|
||||
inApp: {
|
||||
newMessage: { type: Boolean, default: true },
|
||||
rideRequest: { type: Boolean, default: true },
|
||||
rideConfirmation: { type: Boolean, default: true },
|
||||
rideCancellation: { type: Boolean, default: true }
|
||||
}
|
||||
},
|
||||
|
||||
// ============================================================
|
||||
// 🔒 PRIVACY
|
||||
// ============================================================
|
||||
privacy: {
|
||||
// Visibilità profilo
|
||||
profileVisibility: {
|
||||
type: String,
|
||||
enum: ['public', 'members', 'private'],
|
||||
default: 'members'
|
||||
},
|
||||
// Mostra informazioni di contatto
|
||||
showPhone: { type: Boolean, default: false },
|
||||
showEmail: { type: Boolean, default: false },
|
||||
// Mostra statistiche profilo
|
||||
showStats: { type: Boolean, default: true },
|
||||
// Mostra feedback ricevuti
|
||||
showFeedbacks: { type: Boolean, default: true },
|
||||
// Condividi posizione durante viaggio
|
||||
shareLocation: { type: Boolean, default: true },
|
||||
// Chi può contattarmi
|
||||
whoCanContact: {
|
||||
type: String,
|
||||
enum: ['everyone', 'verified', 'afterBooking'],
|
||||
default: 'verified'
|
||||
}
|
||||
},
|
||||
|
||||
// ============================================================
|
||||
// 🚗 PREFERENZE VIAGGI
|
||||
// ============================================================
|
||||
ridePreferences: {
|
||||
// Preferenze come conducente
|
||||
driver: {
|
||||
// Accetta prenotazioni istantanee
|
||||
instantBooking: { type: Boolean, default: false },
|
||||
// Richiede verifica documento passeggeri
|
||||
requireVerification: { type: Boolean, default: false },
|
||||
// Conversazione durante il viaggio
|
||||
chattiness: {
|
||||
type: String,
|
||||
enum: ['silent', 'moderate', 'chatty', 'any'],
|
||||
default: 'any'
|
||||
},
|
||||
// Musica
|
||||
music: {
|
||||
type: String,
|
||||
enum: ['no', 'soft', 'any'],
|
||||
default: 'any'
|
||||
},
|
||||
// Fumatori
|
||||
smoking: {
|
||||
type: String,
|
||||
enum: ['no', 'outside', 'yes'],
|
||||
default: 'no'
|
||||
},
|
||||
// Animali
|
||||
pets: {
|
||||
type: String,
|
||||
enum: ['no', 'small', 'yes'],
|
||||
default: 'no'
|
||||
},
|
||||
// Bagagli extra
|
||||
luggage: {
|
||||
type: String,
|
||||
enum: ['small', 'medium', 'large'],
|
||||
default: 'medium'
|
||||
}
|
||||
},
|
||||
// Preferenze come passeggero
|
||||
passenger: {
|
||||
// Conversazione
|
||||
chattiness: {
|
||||
type: String,
|
||||
enum: ['silent', 'moderate', 'chatty', 'any'],
|
||||
default: 'any'
|
||||
},
|
||||
// Musica
|
||||
music: {
|
||||
type: String,
|
||||
enum: ['no', 'soft', 'any'],
|
||||
default: 'any'
|
||||
},
|
||||
// Fumatori
|
||||
smokingTolerance: {
|
||||
type: String,
|
||||
enum: ['no', 'outside', 'yes'],
|
||||
default: 'no'
|
||||
},
|
||||
// Viaggio con animali
|
||||
comfortableWithPets: { type: Boolean, default: true }
|
||||
}
|
||||
},
|
||||
|
||||
// ============================================================
|
||||
// 🔍 RICERCA & FILTRI PREDEFINITI
|
||||
// ============================================================
|
||||
searchPreferences: {
|
||||
// Raggio di ricerca predefinito (km)
|
||||
defaultRadius: { type: Number, default: 50 },
|
||||
// Ordine risultati
|
||||
defaultSortBy: {
|
||||
type: String,
|
||||
enum: ['date', 'price', 'distance', 'rating'],
|
||||
default: 'date'
|
||||
},
|
||||
// Solo viaggi verificati
|
||||
verifiedOnly: { type: Boolean, default: false },
|
||||
// Solo con recensioni positive
|
||||
minRating: { type: Number, min: 0, max: 5, default: 0 }
|
||||
},
|
||||
|
||||
// ============================================================
|
||||
// 💳 PAGAMENTI & DONAZIONI
|
||||
// ============================================================
|
||||
payment: {
|
||||
// Metodo di pagamento predefinito
|
||||
defaultMethod: {
|
||||
type: String,
|
||||
enum: ['cash', 'card', 'app', 'none'],
|
||||
default: 'cash'
|
||||
},
|
||||
// Contributo suggerito automatico
|
||||
autoSuggestContribution: { type: Boolean, default: true },
|
||||
// Accetta pagamenti anticipati
|
||||
acceptAdvancePayment: { type: Boolean, default: false }
|
||||
},
|
||||
|
||||
// ============================================================
|
||||
// 📱 INTERFACCIA
|
||||
// ============================================================
|
||||
interface: {
|
||||
// Tema
|
||||
theme: {
|
||||
type: String,
|
||||
enum: ['light', 'dark', 'auto'],
|
||||
default: 'auto'
|
||||
},
|
||||
// Lingua
|
||||
language: {
|
||||
type: String,
|
||||
enum: ['it', 'en', 'de', 'fr', 'es'],
|
||||
default: 'it'
|
||||
},
|
||||
// Mostra tutorial
|
||||
showTutorials: { type: Boolean, default: true },
|
||||
// Vista mappa predefinita
|
||||
defaultMapView: { type: Boolean, default: false }
|
||||
},
|
||||
|
||||
// ============================================================
|
||||
// 🔐 SICUREZZA
|
||||
// ============================================================
|
||||
security: {
|
||||
// Richiedi verifica telefono per prenotazioni
|
||||
requirePhoneVerification: { type: Boolean, default: true },
|
||||
// Autenticazione a due fattori
|
||||
twoFactorAuth: { type: Boolean, default: false },
|
||||
// Logout automatico dopo inattività (minuti)
|
||||
autoLogout: { type: Number, default: 30 },
|
||||
// Richiedi conferma prima di cancellare viaggio
|
||||
confirmBeforeCancel: { type: Boolean, default: true }
|
||||
}
|
||||
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 📊 INDICI
|
||||
// ============================================================
|
||||
userSettingsSchema.index({ idapp: 1, userId: 1 }, { unique: true });
|
||||
|
||||
// ============================================================
|
||||
// 🎯 METODI STATICI
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Ottieni o crea impostazioni utente con valori predefiniti
|
||||
*/
|
||||
userSettingsSchema.statics.getOrCreateSettings = async function(idapp, userId) {
|
||||
let settings = await this.findOne({ idapp, userId });
|
||||
|
||||
if (!settings) {
|
||||
settings = await this.create({
|
||||
idapp,
|
||||
userId,
|
||||
// I valori predefiniti sono già definiti nello schema
|
||||
});
|
||||
}
|
||||
|
||||
return settings;
|
||||
};
|
||||
|
||||
/**
|
||||
* Aggiorna impostazioni parziali
|
||||
*/
|
||||
userSettingsSchema.statics.updateSettings = async function(idapp, userId, updates) {
|
||||
const settings = await this.getOrCreateSettings(idapp, userId);
|
||||
|
||||
// Merge delle impostazioni
|
||||
Object.keys(updates).forEach(section => {
|
||||
if (settings[section] && typeof updates[section] === 'object') {
|
||||
settings[section] = {
|
||||
...settings[section],
|
||||
...updates[section]
|
||||
};
|
||||
} else {
|
||||
settings[section] = updates[section];
|
||||
}
|
||||
});
|
||||
|
||||
await settings.save();
|
||||
return settings;
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 🎯 METODI ISTANZA
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Verifica se una notifica è abilitata
|
||||
*/
|
||||
userSettingsSchema.methods.isNotificationEnabled = function(type, channel) {
|
||||
if (!this.notifications[channel]) return false;
|
||||
return this.notifications[channel][type] !== false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Ottieni preferenze compatibilità viaggio
|
||||
*/
|
||||
userSettingsSchema.methods.getCompatibilityPreferences = function(asRole = 'passenger') {
|
||||
if (asRole === 'driver') {
|
||||
return this.ridePreferences.driver;
|
||||
}
|
||||
return this.ridePreferences.passenger;
|
||||
};
|
||||
|
||||
/**
|
||||
* Esporta impostazioni per frontend
|
||||
*/
|
||||
userSettingsSchema.methods.toClientJSON = function() {
|
||||
return {
|
||||
notifications: this.notifications,
|
||||
privacy: this.privacy,
|
||||
ridePreferences: this.ridePreferences,
|
||||
searchPreferences: this.searchPreferences,
|
||||
payment: this.payment,
|
||||
interface: this.interface,
|
||||
security: {
|
||||
requirePhoneVerification: this.security.requirePhoneVerification,
|
||||
twoFactorAuth: this.security.twoFactorAuth,
|
||||
confirmBeforeCancel: this.security.confirmBeforeCancel
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('TrasportiUserSettings', userSettingsSchema);
|
||||
@@ -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') {
|
||||
|
||||
@@ -463,6 +463,8 @@ async function aggiornaCategorieESottoCategorie() {
|
||||
|
||||
async function runMigration() {
|
||||
try {
|
||||
const { User } = require('../models/user');
|
||||
|
||||
const idapp = 0; // TUTTI
|
||||
|
||||
console.log('🚀 Controllo Versioni Tabelle (runMigration)');
|
||||
@@ -471,6 +473,10 @@ async function runMigration() {
|
||||
idapp,
|
||||
shared_consts.JOB_TO_EXECUTE.MIGRATION_SECTORS_DIC25
|
||||
);
|
||||
const isMigratione30Dic2025Telegram = await Version.isJobExecuted(
|
||||
idapp,
|
||||
shared_consts.JOB_TO_EXECUTE.MIGRATION_TELEGRAM_30DIC25
|
||||
);
|
||||
|
||||
const vers_server_str = await tools.getVersServer();
|
||||
|
||||
@@ -522,6 +528,18 @@ async function runMigration() {
|
||||
console.log('\n✅ Migrazione DIC 2025 completata con successo!');
|
||||
}
|
||||
|
||||
if (isMigratione30Dic2025Telegram) {
|
||||
await User.updateMany({ 'profile.teleg_id': { $exists: true, $ne: 0 } }, [
|
||||
{
|
||||
$set: {
|
||||
'notificationPreferences.telegram.enabled': true,
|
||||
'notificationPreferences.telegram.chatId': '$profile.teleg_id',
|
||||
},
|
||||
},
|
||||
]);
|
||||
await Version.setJobExecuted(idapp, shared_consts.JOB_TO_EXECUTE.MIGRATION_TELEGRAM_30DIC25);
|
||||
}
|
||||
|
||||
await Version.setLastVersionRun(idapp, version_server);
|
||||
} catch (error) {
|
||||
console.error('❌ Errore durante la migrazione:', error);
|
||||
@@ -535,5 +553,5 @@ module.exports = {
|
||||
subSkillMapping,
|
||||
sectorGoodMapping,
|
||||
sectorBachecaMapping,
|
||||
aggiornaCategorieESottoCategorie,
|
||||
aggiornaCategorieESottoCategorie,
|
||||
};
|
||||
|
||||
@@ -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,21 @@ 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 +32,26 @@ const { MyElem } = require('../models/myelem');
|
||||
|
||||
const axios = require('axios');
|
||||
|
||||
const settingsRoutes = require('../routes/viaggi/settingsRoutes');
|
||||
router.use('/viaggi/settings', settingsRoutes);
|
||||
|
||||
const widgetRoutes = require('../routes/viaggi/widgetRoutes');
|
||||
router.use('/viaggi/widget', widgetRoutes);
|
||||
|
||||
const viaggiRoutes = require('../routes/viaggiRoutes');
|
||||
router.use('/viaggi', viaggiRoutes);
|
||||
|
||||
|
||||
// Importa le routes video
|
||||
const videoRoutes = require('../routes/videoRoutes');
|
||||
|
||||
// Monta le routes video
|
||||
router.use('/video', videoRoutes);
|
||||
|
||||
router.use('/templates', authenticate, templatesRouter);
|
||||
router.use('/posters', authenticate, postersRouter);
|
||||
router.use('/assets', authenticate, assetsRouter);
|
||||
|
||||
router.post('/test-lungo', authenticate, (req, res) => {
|
||||
const timeout = req.body.timeout;
|
||||
|
||||
@@ -389,7 +422,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 +526,82 @@ 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 });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/users/search', authenticate, async (req, res) => {
|
||||
try {
|
||||
const { User } = require('../models/user');
|
||||
|
||||
const { q, idapp } = req.query;
|
||||
|
||||
if (!q || q.length < 2) {
|
||||
return res.status(400).json({ success: false, message: 'Query too short' });
|
||||
}
|
||||
|
||||
const query = q.trim();
|
||||
|
||||
const users = await User.find({
|
||||
idapp,
|
||||
$or: [
|
||||
{ name: { $regex: query, $options: 'i' } },
|
||||
{ surname: { $regex: query, $options: 'i' } },
|
||||
{ username: { $regex: query, $options: 'i' } },
|
||||
],
|
||||
_id: { $ne: req.user?._id }, // escludi l'utente corrente se autenticato
|
||||
})
|
||||
.select('_id name surname username profile') // solo campi necessari
|
||||
.limit(10); // evita overload
|
||||
|
||||
res.json({ success: true, data: users });
|
||||
} catch (error) {
|
||||
console.error('User search error:', error);
|
||||
res.status(500).json({ success: false, message: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
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;
|
||||
|
||||
@@ -317,7 +317,11 @@ router.post('/', async (req, res) => {
|
||||
await telegrambot.askConfirmationUser(myuser.idapp, shared_consts.CallFunz.REGISTRATION, myuser);
|
||||
|
||||
const { token, refreshToken, browser_random } = await myuser.generateAuthToken(req, browser_random);
|
||||
res.header('x-auth', token).header('x-refrtok', refreshToken).header('x-browser-random', browser_random).send(myuser);
|
||||
res
|
||||
.header('x-auth', token)
|
||||
.header('x-refrtok', refreshToken)
|
||||
.header('x-browser-random', browser_random)
|
||||
.send(myuser);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -368,7 +372,11 @@ router.post('/', async (req, res) => {
|
||||
// if (!tools.testing()) {
|
||||
await sendemail.sendEmail_Registration(user.lang, user.email, user, user.idapp, user.linkreg);
|
||||
// }
|
||||
res.header('x-auth', ris.token).header('x-refrtok', ris.refreshToken).header('x-browser-random', ris.browser_random).send(user);
|
||||
res
|
||||
.header('x-auth', ris.token)
|
||||
.header('x-refrtok', ris.refreshToken)
|
||||
.header('x-browser-random', ris.browser_random)
|
||||
.send(user);
|
||||
return true;
|
||||
});
|
||||
})
|
||||
@@ -411,7 +419,9 @@ router.patch('/:id', authenticate, (req, res) => {
|
||||
|
||||
if (!User.isAdmin(req.user.perm)) {
|
||||
// If without permissions, exit
|
||||
return res.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED).send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' });
|
||||
return res
|
||||
.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED)
|
||||
.send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' });
|
||||
}
|
||||
|
||||
User.findByIdAndUpdate(id, { $set: body })
|
||||
@@ -506,13 +516,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) => {
|
||||
@@ -589,9 +601,14 @@ router.post('/panel', authenticate, async (req, res) => {
|
||||
idapp = req.body.idapp;
|
||||
locale = req.body.locale;
|
||||
|
||||
if (!req.user || !User.isAdmin(req.user.perm) && !User.isManager(req.user.perm) && !User.isFacilitatore(req.user.perm)) {
|
||||
if (
|
||||
!req.user ||
|
||||
(!User.isAdmin(req.user.perm) && !User.isManager(req.user.perm) && !User.isFacilitatore(req.user.perm))
|
||||
) {
|
||||
// If without permissions, exit
|
||||
return res.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED).send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' });
|
||||
return res
|
||||
.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED)
|
||||
.send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -601,6 +618,7 @@ router.post('/panel', authenticate, async (req, res) => {
|
||||
username: 1,
|
||||
name: 1,
|
||||
surname: 1,
|
||||
verified_email: 1,
|
||||
email: 1,
|
||||
verified_by_aportador: 1,
|
||||
aportador_solidario: 1,
|
||||
@@ -657,6 +675,7 @@ router.post('/notifs', authenticate, async (req, res) => {
|
||||
router.post('/newtok', async (req, res) => {
|
||||
try {
|
||||
const refreshToken = req.body.refreshToken;
|
||||
const browser_random = req.body.br;
|
||||
|
||||
// return res.status(403).send({ error: 'Refresh token non valido' });
|
||||
|
||||
@@ -665,7 +684,7 @@ router.post('/newtok', async (req, res) => {
|
||||
}
|
||||
|
||||
const recFound = await User.findByRefreshTokenAnyAccess(refreshToken);
|
||||
|
||||
|
||||
if (!recFound) {
|
||||
return res.status(403).send({ error: 'Refresh token non valido' });
|
||||
}
|
||||
@@ -949,7 +968,9 @@ router.post('/friends/cmd', authenticate, async (req, res) => {
|
||||
usernameDest !== usernameLogged &&
|
||||
(cmd === shared_consts.FRIENDSCMD.SETFRIEND || cmd === shared_consts.FRIENDSCMD.SETHANDSHAKE)
|
||||
) {
|
||||
return res.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED).send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' });
|
||||
return res
|
||||
.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED)
|
||||
.send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1115,7 +1136,10 @@ async function eseguiDbOpUser(idapp, mydata, locale, req, res) {
|
||||
} else if (mydata.dbop === 'noNameSurname') {
|
||||
await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.noNameSurname': mydata.value } });
|
||||
} else if (mydata.dbop === 'telegram_verification_skipped') {
|
||||
await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.telegram_verification_skipped': mydata.value } });
|
||||
await User.findOneAndUpdate(
|
||||
{ _id: mydata._id },
|
||||
{ $set: { 'profile.telegram_verification_skipped': mydata.value } }
|
||||
);
|
||||
} else if (mydata.dbop === 'pwdLikeAdmin') {
|
||||
await User.setPwdComeQuellaDellAdmin(mydata);
|
||||
} else if (mydata.dbop === 'ripristinaPwdPrec') {
|
||||
@@ -1124,6 +1148,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;
|
||||
37
src/routes/geoRoutes.js
Normal file
37
src/routes/geoRoutes.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const {
|
||||
autocomplete,
|
||||
geocode,
|
||||
reverseGeocode,
|
||||
getRoute,
|
||||
getMatrix,
|
||||
suggestWaypoints,
|
||||
searchItalianCities,
|
||||
getDistance,
|
||||
getIsochrone
|
||||
} = require('../controllers/geocodingController');
|
||||
|
||||
// Rate limiting opzionale
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
const geoLimiter = rateLimit({
|
||||
windowMs: 60 * 1000, // 1 minuto
|
||||
max: 60, // 60 richieste per minuto
|
||||
message: { success: false, message: 'Troppe richieste, riprova tra poco' }
|
||||
});
|
||||
|
||||
router.use(geoLimiter);
|
||||
|
||||
// Routes
|
||||
router.get('/autocomplete', autocomplete);
|
||||
router.get('/geocode', geocode);
|
||||
router.get('/reverse', reverseGeocode);
|
||||
router.get('/route', getRoute);
|
||||
router.post('/matrix', getMatrix);
|
||||
router.get('/suggest-waypoints', suggestWaypoints);
|
||||
router.get('/cities/it', searchItalianCities);
|
||||
router.get('/distance', getDistance);
|
||||
router.get('/isochrone', getIsochrone);
|
||||
|
||||
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;
|
||||
100
src/routes/viaggi/settingsRoutes.js
Normal file
100
src/routes/viaggi/settingsRoutes.js
Normal file
@@ -0,0 +1,100 @@
|
||||
// ============================================================
|
||||
// 🔧 SETTINGS ROUTES - Trasporti Solidali
|
||||
// ============================================================
|
||||
// File: server/routes/viaggi/settingsRoutes.js
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const settingsController = require('../../controllers/viaggi/settingsController');
|
||||
const { authenticate } = require('../../middleware/authenticate');
|
||||
|
||||
// ============================================================
|
||||
// 🔐 TUTTE LE ROUTES RICHIEDONO AUTENTICAZIONE
|
||||
// ============================================================
|
||||
router.use(authenticate);
|
||||
|
||||
// ============================================================
|
||||
// 📄 IMPOSTAZIONI GENERALI
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* GET /api/viaggi/settings
|
||||
* Ottieni tutte le impostazioni dell'utente
|
||||
*/
|
||||
router.get('/', settingsController.getSettings);
|
||||
|
||||
/**
|
||||
* PUT /api/viaggi/settings
|
||||
* Aggiorna le impostazioni (completo)
|
||||
*/
|
||||
router.put('/', settingsController.updateSettings);
|
||||
|
||||
/**
|
||||
* POST /api/viaggi/settings/reset
|
||||
* Reset impostazioni ai valori predefiniti
|
||||
*/
|
||||
router.post('/reset', settingsController.resetSettings);
|
||||
|
||||
// ============================================================
|
||||
// 📝 AGGIORNAMENTI PARZIALI (per sezione)
|
||||
// ============================================================
|
||||
|
||||
const notifController = require('../../controllers/viaggi/trasportiNotificationsController');
|
||||
|
||||
// Preferenze
|
||||
router.get('/notifications/preferences', authenticate, notifController.getNotificationPreferences);
|
||||
router.put('/notifications/preferences', authenticate, notifController.updateNotificationPreferences);
|
||||
|
||||
// Telegram
|
||||
router.post('/notifications/telegram/code', authenticate, notifController.generateTelegramCode);
|
||||
router.post('/notifications/telegram/connect', authenticate, notifController.connectTelegram);
|
||||
router.delete('/notifications/telegram/disconnect', authenticate, notifController.disconnectTelegram);
|
||||
|
||||
// Push
|
||||
router.post('/notifications/push/subscribe', authenticate, notifController.subscribePushNotifications);
|
||||
router.delete('/notifications/push/unsubscribe', authenticate, notifController.unsubscribePushNotifications);
|
||||
|
||||
// Test
|
||||
router.post('/notifications/test', authenticate, notifController.sendTestNotification);
|
||||
|
||||
/**
|
||||
* PATCH /api/viaggi/settings/notifications
|
||||
* Aggiorna solo le notifiche
|
||||
*/
|
||||
router.patch('/notifications', settingsController.updateNotifications);
|
||||
|
||||
/**
|
||||
* PATCH /api/viaggi/settings/privacy
|
||||
* Aggiorna solo la privacy
|
||||
*/
|
||||
router.patch('/privacy', settingsController.updatePrivacy);
|
||||
|
||||
/**
|
||||
* PATCH /api/viaggi/settings/ride-preferences
|
||||
* Aggiorna solo le preferenze viaggi
|
||||
*/
|
||||
router.patch('/ride-preferences', settingsController.updateRidePreferences);
|
||||
|
||||
/**
|
||||
* PATCH /api/viaggi/settings/interface
|
||||
* Aggiorna solo l'interfaccia
|
||||
*/
|
||||
router.patch('/interface', settingsController.updateInterface);
|
||||
|
||||
// ============================================================
|
||||
// 📊 EXPORT / IMPORT
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* GET /api/viaggi/settings/export
|
||||
* Esporta tutte le impostazioni
|
||||
*/
|
||||
router.get('/export', settingsController.exportSettings);
|
||||
|
||||
/**
|
||||
* POST /api/viaggi/settings/import
|
||||
* Importa impostazioni da backup
|
||||
*/
|
||||
router.post('/import', settingsController.importSettings);
|
||||
|
||||
module.exports = router;
|
||||
32
src/routes/viaggi/widgetRoutes.js
Normal file
32
src/routes/viaggi/widgetRoutes.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// ============================================================
|
||||
// 📊 WIDGET & STATS ROUTES - Trasporti Solidali
|
||||
// ============================================================
|
||||
// File: server/routes/viaggi/widgetRoutes.js
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const widgetController = require('../../controllers/viaggi/widgetController');
|
||||
const { authenticate } = require('../../middleware/authenticate');
|
||||
|
||||
// ============================================================
|
||||
// 🔐 TUTTE LE ROUTES RICHIEDONO AUTENTICAZIONE
|
||||
// ============================================================
|
||||
router.use(authenticate);
|
||||
|
||||
// ============================================================
|
||||
// 📊 WIDGET DATA
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* GET /api/viaggi/widget/data
|
||||
* Ottieni tutti i dati per il widget dashboard
|
||||
*/
|
||||
router.get('/data', widgetController.getWidgetData);
|
||||
|
||||
/**
|
||||
* GET /api/viaggi/widget/stats
|
||||
* Statistiche rapide per badge/notifiche
|
||||
*/
|
||||
router.get('/stats', widgetController.getQuickStats);
|
||||
|
||||
module.exports = router;
|
||||
1052
src/routes/viaggiRoutes.js
Normal file
1052
src/routes/viaggiRoutes.js
Normal file
File diff suppressed because it is too large
Load Diff
58
src/routes/videoRoutes.js
Normal file
58
src/routes/videoRoutes.js
Normal file
@@ -0,0 +1,58 @@
|
||||
const express = require('express');
|
||||
const VideoController = require('../controllers/VideoController');
|
||||
const UploadMiddleware = require('../middleware/uploadMiddleware');
|
||||
|
||||
const {
|
||||
authenticate,
|
||||
authenticate_noerror,
|
||||
authenticate_noerror_WithUser,
|
||||
authenticate_noerror_WithUserLean,
|
||||
} = require('../middleware/authenticate');
|
||||
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Configurazione
|
||||
const UPLOAD_PATH = process.env.VIDEO_UPLOAD_PATH || 'uploads/videos';
|
||||
|
||||
// Istanze
|
||||
const videoController = new VideoController(UPLOAD_PATH);
|
||||
const uploadMiddleware = new UploadMiddleware(UPLOAD_PATH);
|
||||
|
||||
// ============ FOLDER ROUTES ============
|
||||
router.get('/folders', authenticate, videoController.getFolders);
|
||||
router.post('/folders', authenticate, videoController.createFolder);
|
||||
router.put('/folders/:folderPath(*)', authenticate, videoController.renameFolder);
|
||||
router.delete('/folders/:folderPath(*)', authenticate, videoController.deleteFolder);
|
||||
|
||||
// ============ VIDEO ROUTES ============
|
||||
router.get('/videos', authenticate, videoController.getVideos);
|
||||
router.get('/videos/:folder/:filename', authenticate, videoController.getVideo);
|
||||
|
||||
// Upload
|
||||
router.post(
|
||||
'/videos/upload',
|
||||
uploadMiddleware.single('video'),
|
||||
videoController.uploadVideo
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/videos/upload-multiple',
|
||||
uploadMiddleware.multiple('videos', 10),
|
||||
videoController.uploadVideos
|
||||
);
|
||||
|
||||
// Modifica
|
||||
router.put('/videos/:folder/:filename/rename', authenticate, videoController.renameVideo);
|
||||
router.put('/videos/:folder/:filename/move', authenticate, videoController.moveVideo);
|
||||
|
||||
// Elimina
|
||||
router.delete('/videos/:folder/:filename', authenticate, videoController.deleteVideo);
|
||||
|
||||
// Stream
|
||||
router.get('/stream/:folder/:filename', authenticate, videoController.streamVideo);
|
||||
|
||||
// Error Handler
|
||||
router.use(VideoController.errorHandler);
|
||||
|
||||
module.exports = router;
|
||||
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();
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ function setupExpress(app, corsOptions) {
|
||||
app.use(helmet());
|
||||
app.use(morgan('dev'));
|
||||
app.use(cors(corsOptions));
|
||||
app.set('trust proxy', true);
|
||||
app.set('trust proxy', (process.env.NODE_ENV === 'development') ? false : true);
|
||||
|
||||
// parser
|
||||
app.use(express.json({ limit: '100mb' }));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const pty = require('node-pty');
|
||||
const { User } = require('../models/user');
|
||||
const { sendMessage } = require('../telegram/api');
|
||||
|
||||
function setupShellWebSocket(ws) {
|
||||
console.log('🔌 Client WebSocket Shell connesso');
|
||||
let scriptProcess = null;
|
||||
let buffer = '';
|
||||
|
||||
ws.on('message', async (message) => {
|
||||
try {
|
||||
const parsed = JSON.parse(message);
|
||||
const { type, user_id, scriptName, data } = parsed;
|
||||
|
||||
if (type === 'start_script' && (await User.isAdminById(user_id))) {
|
||||
if (scriptProcess) scriptProcess.kill();
|
||||
|
||||
const scriptPath = path.join(__dirname, '..', '..', scriptName);
|
||||
if (!fs.existsSync(scriptPath)) {
|
||||
return ws.send(JSON.stringify({ type: 'error', data: 'Script non trovato' }));
|
||||
}
|
||||
|
||||
scriptProcess = pty.spawn('bash', [scriptPath], {
|
||||
name: 'xterm-color',
|
||||
cols: 80,
|
||||
rows: 40,
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
scriptProcess.on('data', (chunk) => {
|
||||
buffer += chunk;
|
||||
ws.send(JSON.stringify({ type: 'output', data: chunk }));
|
||||
if (buffer.length > 4096) buffer = buffer.slice(-2048);
|
||||
});
|
||||
|
||||
scriptProcess.on('exit', (code) => {
|
||||
const msg = code === 0 ? '✅ Script completato' : `❌ Uscito con codice ${code}`;
|
||||
ws.send(JSON.stringify({ type: 'close', data: msg }));
|
||||
});
|
||||
} else if (type === 'input' && scriptProcess) {
|
||||
scriptProcess.write(data + '\n');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ Errore WS Shell:', err.message);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
if (scriptProcess) scriptProcess.kill();
|
||||
console.log('🔌 WS Shell chiuso');
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { setupShellWebSocket };
|
||||
@@ -146,9 +146,12 @@ connectToDatabase(connectionUrl, options)
|
||||
const aitools_router = require('./router/aitools_router');
|
||||
const article_router = require('./router/articleRoutes');
|
||||
const api_router = require('./router/api_router');
|
||||
const api2_router = require('./router/api2_router');
|
||||
|
||||
const { MyEvent } = require('./models/myevent');
|
||||
|
||||
app.use('/videos', express.static(path.join(__dirname, 'uploads/videos')));
|
||||
|
||||
app.use(bodyParser.json({ limit: '50mb' }));
|
||||
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
||||
|
||||
@@ -252,6 +255,7 @@ connectToDatabase(connectionUrl, options)
|
||||
app.use('/aitools', aitools_router);
|
||||
app.use('/apisqlsrv', article_router);
|
||||
app.use('/api', api_router);
|
||||
app.use('/api2', api2_router);
|
||||
|
||||
mystart();
|
||||
});
|
||||
@@ -1060,6 +1064,7 @@ connectToDatabase(connectionUrl, options)
|
||||
const NOCORS = false;
|
||||
|
||||
const { domains, domainsAllowed } = parseDomains();
|
||||
|
||||
|
||||
|
||||
console.log('domains:', domains);
|
||||
|
||||
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();
|
||||
871
src/services/posterRenderer.js
Normal file
871
src/services/posterRenderer.js
Normal file
@@ -0,0 +1,871 @@
|
||||
|
||||
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('/upload') || url.startsWith('./upload')) {
|
||||
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();
|
||||
@@ -1,33 +0,0 @@
|
||||
const axios = require('axios');
|
||||
const { API_URL, TIMEOUT } = require('./config');
|
||||
|
||||
async function callTelegram(method, params) {
|
||||
try {
|
||||
const { data } = await axios.post(`${API_URL}/${method}`, params, { timeout: TIMEOUT });
|
||||
if (!data.ok) throw new Error(`Telegram error: ${data.description}`);
|
||||
return data.result;
|
||||
} catch (err) {
|
||||
console.error('❌ Telegram API error:', err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage(chatId, text, options = {}) {
|
||||
return callTelegram('sendMessage', {
|
||||
chat_id: chatId,
|
||||
text,
|
||||
parse_mode: options.parse_mode || 'HTML',
|
||||
disable_web_page_preview: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function sendPhoto(chatId, photo, caption = '', options = {}) {
|
||||
return callTelegram('sendPhoto', {
|
||||
chat_id: chatId,
|
||||
photo,
|
||||
caption,
|
||||
parse_mode: 'HTML',
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { sendMessage, sendPhoto };
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
TOKEN: process.env.TELEGRAM_BOT_TOKEN,
|
||||
API_URL: `https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}`,
|
||||
ADMIN_GROUP_IDS: process.env.TELEGRAM_ADMIN_GROUPS
|
||||
? process.env.TELEGRAM_ADMIN_GROUPS.split(',')
|
||||
: [],
|
||||
TIMEOUT: 5000,
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
// Ruoli, fasi logiche e costanti admin (adatta gli ID ai tuoi reali)
|
||||
module.exports = {
|
||||
ADMIN_USER_SERVER: process.env.ADMIN_USER_SERVER || 'server_admin',
|
||||
ADMIN_IDTELEGRAM_SERVER: process.env.ADMIN_IDTELEGRAM_SERVER || '',
|
||||
phase: {
|
||||
REGISTRATION: 'REGISTRATION',
|
||||
REGISTRATION_CONFIRMED: 'REGISTRATION_CONFIRMED',
|
||||
RESET_PWD: 'RESET_PWD',
|
||||
NOTIFICATION: 'NOTIFICATION',
|
||||
GENERIC: 'GENERIC',
|
||||
},
|
||||
roles: {
|
||||
ADMIN: 'ADMIN',
|
||||
MANAGER: 'MANAGER',
|
||||
FACILITATORE: 'FACILITATORE',
|
||||
EDITOR: 'EDITOR',
|
||||
},
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
const { sendMessage } = require('../api');
|
||||
const { ADMIN_GROUP_IDS } = require('../config');
|
||||
const { safeExec } = require('../helpers');
|
||||
|
||||
const sendToAdmins = safeExec(async (message) => {
|
||||
for (const id of ADMIN_GROUP_IDS) {
|
||||
await sendMessage(id, message);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = { sendToAdmins };
|
||||
@@ -1,101 +0,0 @@
|
||||
// telegram/handlers/callbackHandler.js
|
||||
const tools = require('../../tools/general');
|
||||
const shared_consts = require('../../tools/shared_nodejs');
|
||||
const { User } = require('../../models/user');
|
||||
const { Circuit } = require('../../models/circuit');
|
||||
const { handleRegistration } = require('./registrationHandler');
|
||||
const { handleFriends } = require('./friendsHandler');
|
||||
const { handleCircuit } = require('./circuitHandler');
|
||||
const { handleZoom } = require('./zoomHandler');
|
||||
const { handlePassword } = require('./passwordHandler');
|
||||
|
||||
async function handleCallback(bot, cl, callbackQuery) {
|
||||
const idapp = cl.idapp;
|
||||
let notifyText = ''; // testo di notifica Telegram (answerCallbackQuery)
|
||||
try {
|
||||
// parsing payload dal tuo formato originale (action|username|userDest|groupId|circuitId|groupname)
|
||||
let data = {
|
||||
action: '',
|
||||
username: '',
|
||||
userDest: '',
|
||||
groupId: '',
|
||||
circuitId: '',
|
||||
groupname: '',
|
||||
};
|
||||
|
||||
const raw = callbackQuery?.data || '';
|
||||
if (raw) {
|
||||
const arr = raw.split(tools.SEP);
|
||||
data = {
|
||||
action: arr[0] || '',
|
||||
username: arr[1] || '',
|
||||
userDest: arr[2] || '',
|
||||
groupId: arr[3] || '',
|
||||
circuitId: arr[4] || '',
|
||||
groupname: arr[5] || '',
|
||||
};
|
||||
}
|
||||
|
||||
// normalizza username reali (come nel sorgente)
|
||||
data.username = await User.getRealUsernameByUsername(idapp, data.username);
|
||||
data.userDest = data.userDest ? await User.getRealUsernameByUsername(idapp, data.userDest) : '';
|
||||
|
||||
const msg = callbackQuery.message;
|
||||
const opts = { chat_id: msg.chat.id, message_id: msg.message_id };
|
||||
|
||||
// contest utente corrente
|
||||
await cl.setInit?.(msg); // se presente nel tuo codice
|
||||
const rec = cl.getRecInMem?.(msg);
|
||||
const username_action = rec?.user ? rec.user.username : '';
|
||||
|
||||
// carica user e userDest compatti (come nel tuo codice)
|
||||
const user = data.username ? await User.getUserShortDataByUsername(idapp, data.username) : null;
|
||||
const userDest = data.userDest ? await User.getUserShortDataByUsername(idapp, data.userDest) : null;
|
||||
|
||||
// routing per ambito
|
||||
const act = data.action || '';
|
||||
|
||||
// 1) REGISTRAZIONE e varianti
|
||||
if (act.includes(shared_consts.CallFunz.REGISTRATION)) {
|
||||
notifyText = await handleRegistration({ bot, cl, idapp, msg, data, user, userDest, username_action });
|
||||
}
|
||||
// 2) AMICIZIA / HANDSHAKE
|
||||
else if (
|
||||
act.includes(shared_consts.CallFunz.RICHIESTA_AMICIZIA) ||
|
||||
act.includes(shared_consts.CallFunz.RICHIESTA_HANDSHAKE)
|
||||
) {
|
||||
notifyText = await handleFriends({ bot, cl, idapp, msg, data, user, userDest, username_action });
|
||||
}
|
||||
// 3) CIRCUITI (aggiunta/rimozione)
|
||||
else if (
|
||||
act.includes(shared_consts.CallFunz.ADDUSERTOCIRCUIT) ||
|
||||
act.includes(shared_consts.CallFunz.REMUSERFROMCIRCUIT)
|
||||
) {
|
||||
notifyText = await handleCircuit({ bot, cl, idapp, msg, data, user, userDest, username_action });
|
||||
}
|
||||
// 4) ZOOM (registrazione/presenze)
|
||||
else if (act.includes(shared_consts.CallFunz.REGISTRATION_TOZOOM) || act.includes('ZOOM')) {
|
||||
notifyText = await handleZoom({ bot, cl, idapp, msg, data, user, userDest, username_action });
|
||||
}
|
||||
// 5) RESET PASSWORD
|
||||
else if (act.includes(shared_consts.CallFunz.RESET_PWD)) {
|
||||
notifyText = await handlePassword({ bot, cl, idapp, msg, data, user, userDest, username_action });
|
||||
} else if (act.includes(shared_consts.CallFunz.RICHIESTA_GRUPPO)) {
|
||||
notifyText = await handleGroup({ bot, cl, idapp, msg, data, user, userDest, username_action });
|
||||
}
|
||||
// default
|
||||
else {
|
||||
notifyText = 'Operazione completata';
|
||||
await cl.sendMsg(msg.chat.id, `⚙️ Azione non riconosciuta: ${act}`);
|
||||
}
|
||||
|
||||
await bot.answerCallbackQuery(callbackQuery.id, { text: notifyText || 'OK' });
|
||||
} catch (err) {
|
||||
console.error('❌ callbackHandler error:', err.message);
|
||||
try {
|
||||
await bot.answerCallbackQuery(callbackQuery.id, { text: 'Errore', show_alert: true });
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { handleCallback };
|
||||
@@ -1,54 +0,0 @@
|
||||
// telegram/handlers/circuitHandler.js
|
||||
const shared_consts = require('../../tools/shared_nodejs');
|
||||
const tools = require('../../tools/general');
|
||||
const { User } = require('../../models/user');
|
||||
|
||||
const InlineConferma = { RISPOSTA_SI: 'SI_', RISPOSTA_NO: 'NO_' };
|
||||
|
||||
async function handleCircuit({ bot, cl, idapp, msg, data, user, userDest, username_action }) {
|
||||
let notifyText = '';
|
||||
|
||||
// Aggiunta al circuito
|
||||
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.ADDUSERTOCIRCUIT) {
|
||||
const cmd = shared_consts.CIRCUITCMD.ADDUSERTOCIRCUIT;
|
||||
const req = tools.getReqByPar(idapp, username_action);
|
||||
|
||||
// se viene da gruppo usa ifCircuitAlreadyInGroup, altrimenti ifAlreadyInCircuit (come nel tuo codice)
|
||||
const already = data.groupname
|
||||
? await User.ifCircuitAlreadyInGroup(idapp, data.groupname, data.circuitId)
|
||||
: await User.ifAlreadyInCircuit(idapp, data.username, data.circuitId);
|
||||
|
||||
if (!already) {
|
||||
await User.setCircuitCmd(idapp, data.username, data.circuitId, cmd, 1, username_action, { groupname: data.groupname });
|
||||
await cl.sendMsg(msg.chat.id, `✅ ${data.username} aggiunto al circuito ${data.circuitId}`);
|
||||
notifyText = 'Circuito OK';
|
||||
} else {
|
||||
await cl.sendMsg(msg.chat.id, `ℹ️ ${data.username} è già nel circuito ${data.circuitId}`);
|
||||
notifyText = 'Già presente';
|
||||
}
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
// Rimozione dal circuito
|
||||
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.REMUSERFROMCIRCUIT) {
|
||||
const cmd = shared_consts.CIRCUITCMD.REMOVEUSERFROMCIRCUIT;
|
||||
await User.setCircuitCmd(idapp, data.username, data.circuitId, cmd, 0, username_action, { groupname: data.groupname });
|
||||
await cl.sendMsg(msg.chat.id, `🗑️ ${data.username} rimosso dal circuito ${data.circuitId}`);
|
||||
notifyText = 'Rimosso';
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
// NO / annulla
|
||||
if (
|
||||
data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.ADDUSERTOCIRCUIT ||
|
||||
data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.REMUSERFROMCIRCUIT
|
||||
) {
|
||||
await cl.sendMsg(msg.chat.id, '❌ Operazione circuito annullata.');
|
||||
notifyText = 'Annullata';
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
module.exports = { handleCircuit };
|
||||
@@ -1,24 +0,0 @@
|
||||
const { sendMessage, sendPhoto } = require('../api');
|
||||
const { safeExec } = require('../helpers');
|
||||
|
||||
const sendMsgTelegram = safeExec(async (user, text) => {
|
||||
if (!user || !user.telegram_id) return null;
|
||||
return sendMessage(user.telegram_id, text);
|
||||
});
|
||||
|
||||
const sendMsgTelegramByIdTelegram = safeExec(async (telegramId, text) => {
|
||||
if (!telegramId) return null;
|
||||
return sendMessage(telegramId, text);
|
||||
});
|
||||
|
||||
const sendPhotoTelegram = safeExec(async (chatIdOrUser, photoUrl, caption = '') => {
|
||||
const chatId = typeof chatIdOrUser === 'object' ? chatIdOrUser?.telegram_id : chatIdOrUser;
|
||||
if (!chatId || !photoUrl) return null;
|
||||
return sendPhoto(chatId, photoUrl, caption);
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
sendMsgTelegram,
|
||||
sendMsgTelegramByIdTelegram,
|
||||
sendPhotoTelegram,
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
const { sendMessage } = require('../api');
|
||||
const { ADMIN_GROUP_IDS } = require('../config');
|
||||
const { safeExec } = require('../helpers');
|
||||
|
||||
const reportError = safeExec(async (context, err) => {
|
||||
const msg = `🚨 Errore in <b>${context}</b>\n<pre>${err.stack || err.message}</pre>`;
|
||||
for (const id of ADMIN_GROUP_IDS) {
|
||||
await sendMessage(id, msg);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = { reportError };
|
||||
@@ -1,61 +0,0 @@
|
||||
// telegram/handlers/friendsHandler.js
|
||||
const shared_consts = require('../../tools/shared_nodejs');
|
||||
const tools = require('../../tools/general');
|
||||
const { User } = require('../../models/user');
|
||||
const printf = require('util').format;
|
||||
const { handleRegistration, InlineConferma } = require('./registrationHandler');
|
||||
|
||||
async function handleFriends({ bot, cl, idapp, msg, data, user, userDest, username_action }) {
|
||||
let notifyText = '';
|
||||
|
||||
// SI -> amicizia
|
||||
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.RICHIESTA_AMICIZIA) {
|
||||
if (userDest) {
|
||||
const req = tools.getReqByPar(idapp, username_action);
|
||||
const already = await User.isMyFriend(idapp, data.username, data.userDest);
|
||||
if (!already) await User.setFriendsCmd(req, idapp, data.username, data.userDest, shared_consts.FRIENDSCMD.SETFRIEND);
|
||||
await cl.sendMsg(msg.chat.id, '🤝 Amicizia confermata!');
|
||||
notifyText = 'Amicizia OK';
|
||||
}
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
// NO -> amicizia (rimuovi/nega)
|
||||
if (data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.RICHIESTA_AMICIZIA) {
|
||||
if (userDest) {
|
||||
const req = tools.getReqByPar(idapp, username_action);
|
||||
const ris = await User.setFriendsCmd(req, idapp, data.username, data.userDest, shared_consts.FRIENDSCMD.REMOVE_FROM_MYFRIENDS);
|
||||
if (ris) {
|
||||
const msgDest = printf(tools.gettranslate('MSG_FRIENDS_NOT_ACCEPTED_CONFIRMED', user.lang), data.username);
|
||||
await localSendMsgByUsername(idapp, data.userDest, msgDest);
|
||||
}
|
||||
await cl.sendMsg(msg.chat.id, '🚫 Amicizia rifiutata.');
|
||||
notifyText = 'Rifiutata';
|
||||
}
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
// SI -> handshake
|
||||
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.RICHIESTA_HANDSHAKE) {
|
||||
if (userDest) {
|
||||
const req = tools.getReqByPar(idapp, username_action);
|
||||
const already = await User.isMyHandShake(idapp, data.userDest, data.username);
|
||||
if (!already) await User.setFriendsCmd(req, idapp, data.userDest, data.username, shared_consts.FRIENDSCMD.SETHANDSHAKE);
|
||||
await cl.sendMsg(msg.chat.id, '🤝 Handshake confermato!');
|
||||
notifyText = 'Handshake OK';
|
||||
}
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
// helper locale (equivalente del tuo local_sendMsgTelegram)
|
||||
async function localSendMsgByUsername(idapp, username, text) {
|
||||
const teleg_id = await User.TelegIdByUsername(idapp, username);
|
||||
const cl = require('../telegram.bot.init').getclTelegByidapp(idapp);
|
||||
if (cl && teleg_id) return await cl.sendMsg(teleg_id, text);
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = { handleFriends };
|
||||
@@ -1,70 +0,0 @@
|
||||
// telegram/handlers/groupHandler.js
|
||||
const shared_consts = require('../../tools/shared_nodejs');
|
||||
const tools = require('../../tools/general');
|
||||
const { User } = require('../../models/user');
|
||||
|
||||
const InlineConferma = { RISPOSTA_SI: 'SI_', RISPOSTA_NO: 'NO_' };
|
||||
|
||||
/**
|
||||
* Gestisce conferma/rifiuto a richieste di GRUPPO
|
||||
* Payload data:
|
||||
* - action
|
||||
* - username (mittente originale)
|
||||
* - userDest (destinatario/utente da aggiungere)
|
||||
* - groupId (id o path del gruppo)
|
||||
* - groupname (nome del gruppo)
|
||||
*/
|
||||
async function handleGroup({ bot, cl, idapp, msg, data, user, userDest, username_action }) {
|
||||
let notifyText = '';
|
||||
|
||||
// SI → accetta richiesta d'ingresso nel gruppo
|
||||
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.RICHIESTA_GRUPPO) {
|
||||
// Se l’app ha funzioni di persistenza specifiche, usale se esistono
|
||||
// (non assumo nomi rigidi per non rompere il deploy)
|
||||
if (typeof User.setGroupCmd === 'function') {
|
||||
try {
|
||||
await User.setGroupCmd(idapp, data.userDest || data.username, data.groupId, shared_consts.GROUPCMD.ADDUSERTOGROUP, 1, username_action, { groupname: data.groupname });
|
||||
} catch (e) {
|
||||
console.warn('handleGroup:setGroupCmd non eseguito:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Messaggi di conferma
|
||||
await cl.sendMsg(msg.chat.id, `✅ ${data.userDest || data.username} è stato aggiunto al gruppo ${data.groupname || data.groupId}.`);
|
||||
// Notifica anche l’utente interessato
|
||||
const targetUsername = data.userDest || data.username;
|
||||
const teleg_id = await User.TelegIdByUsername(idapp, targetUsername);
|
||||
if (teleg_id) {
|
||||
await cl.sendMsg(teleg_id, `👥 Sei stato aggiunto al gruppo: ${data.groupname || data.groupId}`);
|
||||
}
|
||||
|
||||
notifyText = 'Gruppo: aggiunta OK';
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
// NO → rifiuta/annulla richiesta
|
||||
if (data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.RICHIESTA_GRUPPO) {
|
||||
if (typeof User.setGroupCmd === 'function') {
|
||||
try {
|
||||
await User.setGroupCmd(idapp, data.userDest || data.username, data.groupId, shared_consts.GROUPCMD.REMOVEUSERFROMGROUP, 0, username_action, { groupname: data.groupname });
|
||||
} catch (e) {
|
||||
console.warn('handleGroup:setGroupCmd non eseguito:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
await cl.sendMsg(msg.chat.id, '🚫 Richiesta gruppo rifiutata.');
|
||||
// Avvisa il richiedente
|
||||
const targetUsername = data.userDest || data.username;
|
||||
const teleg_id = await User.TelegIdByUsername(idapp, targetUsername);
|
||||
if (teleg_id) {
|
||||
await cl.sendMsg(teleg_id, `❌ La tua richiesta per il gruppo ${data.groupname || data.groupId} è stata rifiutata.`);
|
||||
}
|
||||
|
||||
notifyText = 'Gruppo: rifiutata';
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
module.exports = { handleGroup };
|
||||
@@ -1,47 +0,0 @@
|
||||
const { sendMessage } = require('../api');
|
||||
const { safeExec, eachSeries } = require('../helpers');
|
||||
const tools = require('../../tools/general');
|
||||
const {
|
||||
getAdminTelegramUsers,
|
||||
getManagersTelegramUsers,
|
||||
getAllTelegramUsersByApp,
|
||||
} = require('./userQuery');
|
||||
|
||||
const sendMsgTelegramToTheAdminAllSites = safeExec(async (text, alsoGroups = false) => {
|
||||
const apps = await tools.getApps(); // deve restituire {idapp,...}
|
||||
await eachSeries(apps, async (app) => {
|
||||
const admins = await getAdminTelegramUsers(app.idapp);
|
||||
await eachSeries(admins, async (u) => sendMessage(u.telegram_id, text));
|
||||
if (alsoGroups && app?.telegram_admin_group_id) {
|
||||
await sendMessage(app.telegram_admin_group_id, text);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const sendMsgTelegramByIdApp = safeExec(async (idapp, text) => {
|
||||
const users = await getAllTelegramUsersByApp(idapp);
|
||||
await eachSeries(users, async (u) => sendMessage(u.telegram_id, text));
|
||||
});
|
||||
|
||||
const sendMsgTelegramToTheManagers = safeExec(async (idapp, text) => {
|
||||
const managers = await getManagersTelegramUsers(idapp);
|
||||
await eachSeries(managers, async (u) => sendMessage(u.telegram_id, text));
|
||||
});
|
||||
|
||||
const sendMsgTelegramToTheAdmin = safeExec(async (idapp, text) => {
|
||||
const admins = await getAdminTelegramUsers(idapp);
|
||||
await eachSeries(admins, async (u) => sendMessage(u.telegram_id, text));
|
||||
});
|
||||
|
||||
const sendMsgTelegramToTheGroup = safeExec(async (chatId, text) => {
|
||||
if (!chatId) return null;
|
||||
return sendMessage(chatId, text);
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
sendMsgTelegramToTheAdminAllSites,
|
||||
sendMsgTelegramByIdApp,
|
||||
sendMsgTelegramToTheManagers,
|
||||
sendMsgTelegramToTheAdmin,
|
||||
sendMsgTelegramToTheGroup,
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
const { sendMessage } = require('../api');
|
||||
const { safeExec } = require('../helpers');
|
||||
|
||||
const sendNotification = safeExec(async (chatId, title, body) => {
|
||||
const msg = `🔔 <b>${title}</b>\n${body}`;
|
||||
await sendMessage(chatId, msg);
|
||||
});
|
||||
|
||||
module.exports = { sendNotification };
|
||||
@@ -1,35 +0,0 @@
|
||||
// telegram/handlers/passwordHandler.js
|
||||
const shared_consts = require('../../tools/shared_nodejs');
|
||||
const tools = require('../../tools/general');
|
||||
|
||||
const InlineConferma = { RISPOSTA_SI: 'SI_', RISPOSTA_NO: 'NO_' };
|
||||
|
||||
async function handlePassword({ bot, cl, idapp, msg, data, user, userDest, username_action }) {
|
||||
let notifyText = '';
|
||||
|
||||
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.RESET_PWD) {
|
||||
// Nel tuo codice usavi anche tools.sendNotificationToUser ecc.
|
||||
await tools.sendNotificationToUser(
|
||||
user?._id || msg.chat.id,
|
||||
'🔑 Reset Password',
|
||||
`La password di ${data.username} è stata resettata.`,
|
||||
'/',
|
||||
'',
|
||||
'server',
|
||||
[]
|
||||
);
|
||||
await cl.sendMsg(msg.chat.id, '✅ Password resettata.');
|
||||
notifyText = 'Reset OK';
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
if (data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.RESET_PWD) {
|
||||
await cl.sendMsg(msg.chat.id, '❌ Reset password annullato.');
|
||||
notifyText = 'Annullato';
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
module.exports = { handlePassword };
|
||||
@@ -1,65 +0,0 @@
|
||||
const messages = require('../messages');
|
||||
const { phase } = require('../constants');
|
||||
const { safeExec } = require('../helpers');
|
||||
const { sendMessage } = require('../api');
|
||||
const {
|
||||
getAdminTelegramUsers,
|
||||
getManagersTelegramUsers,
|
||||
} = require('./userQuery');
|
||||
|
||||
// locals: { idapp, username, nomeapp, text, ... }
|
||||
const notifyToTelegram = safeExec(async (ph, locals = {}) => {
|
||||
const idapp = String(locals.idapp || '');
|
||||
let text = '';
|
||||
|
||||
const templ = messages.byPhase[ph] || messages.byPhase.GENERIC;
|
||||
text = templ(locals);
|
||||
|
||||
// router di default: manda agli admin dell'app
|
||||
const admins = await getAdminTelegramUsers(idapp);
|
||||
for (const a of admins) {
|
||||
if (a.telegram_id) await sendMessage(a.telegram_id, text);
|
||||
}
|
||||
});
|
||||
|
||||
const askConfirmationUser = safeExec(async (idapp, phaseCode, user) => {
|
||||
const txt = messages.askConfirmationUser({
|
||||
idapp,
|
||||
username: user?.username,
|
||||
nomeapp: user?.nomeapp,
|
||||
});
|
||||
if (user?.telegram_id) await sendMessage(user.telegram_id, txt);
|
||||
});
|
||||
|
||||
// helper semplici
|
||||
const sendNotifToAdmin = safeExec(async (idapp, title, body = '') => {
|
||||
const admins = await getAdminTelegramUsers(String(idapp));
|
||||
const txt = `📣 <b>${title}</b>\n${body}`;
|
||||
for (const a of admins) {
|
||||
if (a.telegram_id) await sendMessage(a.telegram_id, txt);
|
||||
}
|
||||
});
|
||||
|
||||
const sendNotifToManager = safeExec(async (idapp, title, body = '') => {
|
||||
const managers = await getManagersTelegramUsers(String(idapp));
|
||||
const txt = `📣 <b>${title}</b>\n${body}`;
|
||||
for (const m of managers) {
|
||||
if (m.telegram_id) await sendMessage(m.telegram_id, txt);
|
||||
}
|
||||
});
|
||||
|
||||
const sendNotifToAdminOrManager = safeExec(async (idapp, title, body = '', preferManagers = false) => {
|
||||
if (preferManagers) {
|
||||
return sendNotifToManager(idapp, title, body);
|
||||
}
|
||||
return sendNotifToAdmin(idapp, title, body);
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
notifyToTelegram,
|
||||
askConfirmationUser,
|
||||
sendNotifToAdmin,
|
||||
sendNotifToManager,
|
||||
sendNotifToAdminOrManager,
|
||||
phase, // re-export utile
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
// telegram/handlers/registrationHandler.js
|
||||
const shared_consts = require('../../tools/shared_nodejs');
|
||||
const { User } = require('../../models/user');
|
||||
const telegrambot = require('../telegram.bot.init'); // per sendMsgTelegramToTheAdminAllSites
|
||||
const printf = require('util').format;
|
||||
|
||||
const InlineConferma = {
|
||||
RISPOSTA_SI: 'SI_',
|
||||
RISPOSTA_NO: 'NO_',
|
||||
};
|
||||
|
||||
async function handleRegistration({ bot, cl, idapp, msg, data, user, userDest, username_action }) {
|
||||
let notifyText = '';
|
||||
|
||||
// NO alla registrazione
|
||||
if (data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.REGISTRATION) {
|
||||
await cl.sendMsg(msg.chat.id, '❌ Registrazione annullata.');
|
||||
notifyText = 'Annullata';
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
// SI alla registrazione standard
|
||||
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.REGISTRATION) {
|
||||
// set verified (come da tuo codice)
|
||||
await User.setVerifiedReg(idapp, data.username, data.userDest);
|
||||
await cl.sendMsg(msg.chat.id, '✅ Registrazione confermata.');
|
||||
await telegrambot.sendMsgTelegramToTheAdminAllSites(`🆕 Nuova registrazione confermata: ${data.userDest}`);
|
||||
notifyText = 'Registrazione OK';
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
// SI/NO alla REGISTRATION_FRIEND
|
||||
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.REGISTRATION_FRIEND) {
|
||||
await User.setVerifiedReg(idapp, data.username, data.userDest);
|
||||
await cl.sendMsg(msg.chat.id, '🤝 Conferma amicizia completata!');
|
||||
notifyText = 'Amicizia OK';
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
if (data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.REGISTRATION_FRIEND) {
|
||||
await cl.sendMsg(msg.chat.id, '🚫 Invito amicizia rifiutato.');
|
||||
notifyText = 'Rifiutata';
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
// deleghe future (es. REGISTRATION_TOZOOM gestita in zoomHandler)
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
module.exports = { handleRegistration, InlineConferma };
|
||||
@@ -1,15 +0,0 @@
|
||||
const { sendMessage, sendPhoto } = require('../api');
|
||||
const { formatUser, safeExec } = require('../helpers');
|
||||
|
||||
const notifyUser = safeExec(async (user, text) => {
|
||||
if (!user?.telegram_id) return;
|
||||
const msg = `👋 Ciao ${formatUser(user)}\n${text}`;
|
||||
await sendMessage(user.telegram_id, msg);
|
||||
});
|
||||
|
||||
const sendUserPhoto = safeExec(async (user, photoUrl, caption) => {
|
||||
if (!user?.telegram_id) return;
|
||||
await sendPhoto(user.telegram_id, photoUrl, caption);
|
||||
});
|
||||
|
||||
module.exports = { notifyUser, sendUserPhoto };
|
||||
@@ -1,32 +0,0 @@
|
||||
const { User } = require('../../models/user');
|
||||
|
||||
async function getTelegramUsersByQuery(query = {}) {
|
||||
return User.find({
|
||||
...query,
|
||||
telegram_id: { $exists: true, $ne: null },
|
||||
}).lean();
|
||||
}
|
||||
|
||||
async function getAdminTelegramUsers(idapp) {
|
||||
return getTelegramUsersByQuery({ idapp, isAdmin: true });
|
||||
}
|
||||
|
||||
async function getManagersTelegramUsers(idapp) {
|
||||
return getTelegramUsersByQuery({ idapp, isManager: true });
|
||||
}
|
||||
|
||||
async function getFacilitatoriTelegramUsers(idapp) {
|
||||
return getTelegramUsersByQuery({ idapp, isFacilitatore: true });
|
||||
}
|
||||
|
||||
async function getAllTelegramUsersByApp(idapp) {
|
||||
return getTelegramUsersByQuery({ idapp });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getTelegramUsersByQuery,
|
||||
getAdminTelegramUsers,
|
||||
getManagersTelegramUsers,
|
||||
getFacilitatoriTelegramUsers,
|
||||
getAllTelegramUsersByApp,
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
// telegram/handlers/zoomHandler.js
|
||||
const shared_consts = require('../../tools/shared_nodejs');
|
||||
const { User } = require('../../models/user');
|
||||
|
||||
const InlineConferma = { RISPOSTA_SI: 'SI_', RISPOSTA_NO: 'NO_' };
|
||||
|
||||
async function handleZoom({ bot, cl, idapp, msg, data, user, userDest, username_action }) {
|
||||
let notifyText = '';
|
||||
|
||||
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.REGISTRATION_TOZOOM) {
|
||||
// nelle tue callback originale: conferma registrazione + messaggio
|
||||
await User.setVerifiedReg(idapp, data.username, data.userDest);
|
||||
await cl.sendMsg(msg.chat.id, '🟢 Accesso Zoom confermato!');
|
||||
notifyText = 'Zoom OK';
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
if (data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.REGISTRATION_TOZOOM) {
|
||||
await cl.sendMsg(msg.chat.id, '🚫 Accesso Zoom rifiutato.');
|
||||
notifyText = 'Rifiutato';
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
module.exports = { handleZoom };
|
||||
@@ -1,31 +0,0 @@
|
||||
function formatUser(user) {
|
||||
const u = user || {};
|
||||
const username = u.username || (u.profile && u.profile.username_telegram) || 'no_username';
|
||||
return `${u.name || ''} ${u.surname || ''} (@${username})`.trim();
|
||||
}
|
||||
|
||||
function safeExec(fn) {
|
||||
return async (...args) => {
|
||||
try {
|
||||
return await fn(...args);
|
||||
} catch (e) {
|
||||
console.error('Telegram helper error:', e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function ensureArray(x) {
|
||||
if (!x) return [];
|
||||
return Array.isArray(x) ? x : [x];
|
||||
}
|
||||
|
||||
// utility semplice per evitare flood (se ti serve rate-limit: usa bottleneck)
|
||||
async function eachSeries(arr, fn) {
|
||||
for (const item of arr) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await fn(item);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { formatUser, safeExec, ensureArray, eachSeries };
|
||||
@@ -1,66 +0,0 @@
|
||||
const messages = require('./messages');
|
||||
|
||||
// base api/handlers già creati in precedenza
|
||||
const { sendMessage, sendPhoto } = require('./api');
|
||||
const { sendToAdmins } = require('./handlers/adminHandler');
|
||||
const { notifyUser, sendUserPhoto } = require('./handlers/userHandler');
|
||||
const { sendNotification } = require('./handlers/notificationHandler');
|
||||
const { reportError } = require('./handlers/errorHandler');
|
||||
|
||||
// NUOVI HANDLER aggiunti ora
|
||||
const {
|
||||
sendMsgTelegram,
|
||||
sendMsgTelegramByIdTelegram,
|
||||
sendPhotoTelegram,
|
||||
} = require('./handlers/directHandler');
|
||||
|
||||
const {
|
||||
sendMsgTelegramToTheAdminAllSites,
|
||||
sendMsgTelegramByIdApp,
|
||||
sendMsgTelegramToTheManagers,
|
||||
sendMsgTelegramToTheAdmin,
|
||||
sendMsgTelegramToTheGroup,
|
||||
} = require('./handlers/multiAppHandler');
|
||||
|
||||
const {
|
||||
notifyToTelegram,
|
||||
askConfirmationUser,
|
||||
sendNotifToAdmin,
|
||||
sendNotifToManager,
|
||||
sendNotifToAdminOrManager,
|
||||
phase,
|
||||
} = require('./handlers/phaseHandler');
|
||||
|
||||
module.exports = {
|
||||
// messaggi/template
|
||||
messages,
|
||||
phase,
|
||||
|
||||
// API raw
|
||||
sendMessage,
|
||||
sendPhoto,
|
||||
|
||||
// generico
|
||||
sendToAdmins,
|
||||
notifyUser,
|
||||
sendUserPhoto,
|
||||
sendNotification,
|
||||
reportError,
|
||||
|
||||
// EQUIVALENTI del vecchio file
|
||||
sendMsgTelegram, // (user, text)
|
||||
sendMsgTelegramByIdTelegram, // (telegramId, text)
|
||||
sendPhotoTelegram, // (chatIdOrUser, photoUrl, caption)
|
||||
|
||||
sendMsgTelegramToTheAdminAllSites, // (text, alsoGroups?)
|
||||
sendMsgTelegramByIdApp, // (idapp, text)
|
||||
sendMsgTelegramToTheManagers, // (idapp, text)
|
||||
sendMsgTelegramToTheAdmin, // (idapp, text)
|
||||
sendMsgTelegramToTheGroup, // (chatId, text)
|
||||
|
||||
notifyToTelegram, // (phase, locals)
|
||||
askConfirmationUser, // (idapp, phase, user)
|
||||
sendNotifToAdmin, // (idapp, title, body)
|
||||
sendNotifToManager, // (idapp, title, body)
|
||||
sendNotifToAdminOrManager, // (idapp, title, body, preferManagers?)
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
module.exports = {
|
||||
// messaggi generici
|
||||
serverStarted: (dbName) => `🚀 Il server <b>${dbName}</b> è stato avviato con successo.`,
|
||||
userUnlocked: (user) => `⚠️ L'utente <b>${user.username}</b> (${user.name} ${user.surname}) è stato sbloccato.`,
|
||||
errorOccurred: (context, err) =>
|
||||
`❌ Errore in <b>${context}</b>\n<code>${(err && err.message) || err}</code>`,
|
||||
notifyAdmin: (msg) => `📢 Notifica Admin:\n${msg}`,
|
||||
|
||||
// fasi logiche
|
||||
byPhase: {
|
||||
REGISTRATION: (locals = {}) =>
|
||||
`🆕 Nuova registrazione su <b>${locals.nomeapp || 'App'}</b>\nUtente: <b>${locals.username}</b>`,
|
||||
REGISTRATION_CONFIRMED: (locals = {}) =>
|
||||
`✅ Registrazione confermata su <b>${locals.nomeapp || 'App'}</b> da <b>${locals.username}</b>`,
|
||||
RESET_PWD: (locals = {}) =>
|
||||
`🔁 Reset password richiesto per <b>${locals.username}</b>`,
|
||||
NOTIFICATION: (locals = {}) =>
|
||||
`🔔 Notifica: ${locals.text || ''}`,
|
||||
GENERIC: (locals = {}) =>
|
||||
`${locals.text || ''}`,
|
||||
},
|
||||
|
||||
askConfirmationUser: (locals = {}) =>
|
||||
`👋 Ciao <b>${locals.username}</b>!\nConfermi l'operazione su <b>${locals.nomeapp || 'App'}</b>?`,
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
http://localhost:8084/signup/paoloar77/SuryaArena/5356627050
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user