diff --git a/.DS_Store b/.DS_Store index 3bf134a..205ff75 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/admin_scripts/3_DATABASE/dati.txt.js b/admin_scripts/3_DATABASE/dati.txt.js index 5195096..1ca133d 100644 --- a/admin_scripts/3_DATABASE/dati.txt.js +++ b/admin_scripts/3_DATABASE/dati.txt.js @@ -66,7 +66,7 @@ db.myelems.insertMany([ "listcards": [], "list": [], "__v": 0, - "containerHtml": "\n\n\n

“Abitare gli Iblei” è una rete aperta che ha lo scopo di riunire tutte quelle persone che vogliono valorizzare e qualificare la vita nel territorio degli Iblei. 

\n    \n   

Chi aderisce alla rete si riconosce in una Carta dei valori comuni e usa la rete per scambiare conoscenze, esperienze, risorse e prodotti sviluppati nell’ambito delle proprie iniziative (profit e non profit) individuali o collettive.

\n\n\n   

L’area territoriale di questa rete è quella dei Monti Iblei orientali e occidentali (Noto, Avola, Canicattini, Siracusa, Palazzolo, Buccheri, Ferla, Modica, …).

\n\n\n   

La rete “Abitare gli Iblei” offre i seguenti servizi utili per il territorio ed i suoi abitanti, frutto di una costruzione collettiva:

\n    \n    \n    \n   

Se vuoi aderire alla rete puoi richiederne la registrazione utilizzando questo Link (Pagina in Costruzione).

\n\n", + "containerHtml": "\n\n\n

“Abitare gli Iblei” è una rete aperta che ha lo scopo di riunire tutte quelle persone che vogliono valorizzare e qualificare la vita nel territorio degli Iblei. 

\n    \n   

Chi aderisce alla rete si riconosce in una Carta dei valori comuni e usa la rete per scambiare conoscenze, esperienze, risorse e prodotti sviluppati nell’ambito delle proprie iniziative (profit e non profit) individuali o collettive.

\n\n\n   

L’area territoriale di questa rete è quella dei Monti Iblei orientali e occidentali (Noto, Avola, Canicattini, Siracusa, Palazzolo, Buccheri, Ferla, Modica, …).

\n\n\n   

La rete “Abitare gli Iblei” offre i seguenti servizi utili per il territorio ed i suoi abitanti, frutto di una costruzione collettiva:

\n    \n    \n    \n   

Se vuoi aderire alla rete puoi richiederne la registrazione utilizzando questo Link (Pagina in Costruzione).

\n\n", "anim": { "_id": new ObjectId("66db393e3b885ccdfaed28d6"), "name": "", diff --git a/emails/RISO/reg_notifica_all_invitante/it/html.pug b/emails/RISO/reg_notifica_all_invitante/it/html.pug new file mode 100755 index 0000000..3856c1a --- /dev/null +++ b/emails/RISO/reg_notifica_all_invitante/it/html.pug @@ -0,0 +1,394 @@ +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: 120px; + 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, #7cb342 0%, #558b2f 100%); + color: white; + padding: 40px 24px; + text-align: center; + } + + .email-header h1 { + margin: 0 0 8px 0; + font-size: 26px; + font-weight: 600; + line-height: 1.3; + } + + .email-header .subtitle { + margin: 8px 0 0 0; + font-size: 17px; + opacity: 0.95; + font-style: italic; + } + + .success-icon { + font-size: 56px; + margin-bottom: 12px; + } + + .email-body { + padding: 32px 24px; + } + + .intro-text { + font-size: 16px; + color: #333; + margin-bottom: 20px; + text-align: center; + line-height: 1.7; + } + + .highlight-box { + background: #fff8dc; + border-left: 4px solid #7cb342; + border-radius: 8px; + padding: 16px; + margin: 20px 0; + } + + .highlight-box p { + margin: 0; + font-size: 17px; + color: #1a1a1a; + line-height: 1.6; + } + + .member-card { + background: linear-gradient(135deg, #f8fdf8 0%, #e8f5e9 100%); + border: 2px solid #7cb342; + border-radius: 8px; + padding: 20px; + margin: 20px 0; + text-align: center; + } + + .member-card h3 { + font-size: 14px; + text-transform: uppercase; + color: #558b2f; + margin-bottom: 12px; + letter-spacing: 0.5px; + font-weight: 600; + } + + .member-card .member-name { + font-size: 24px; + color: #1a1a1a; + font-weight: 600; + margin-bottom: 8px; + } + + .member-card .member-detail { + font-size: 15px; + color: #555; + margin: 6px 0; + } + + .member-card .member-detail strong { + color: #558b2f; + } + + .info-box { + background: #e8f5e9; + border-radius: 8px; + padding: 16px; + margin: 20px 0; + } + + .info-box p { + margin: 0 0 8px 0; + color: #2e7d32; + font-size: 16px; + line-height: 1.6; + } + + .info-box p:last-child { + margin-bottom: 0; + } + + .tips-section { + background: #f8f9fa; + border-radius: 8px; + padding: 16px; + margin: 20px 0; + } + + .tips-section h3 { + font-size: 17px; + color: #1a1a1a; + margin-bottom: 12px; + text-align: center; + font-weight: 600; + } + + .tip-item { + display: flex; + align-items: flex-start; + margin-bottom: 10px; + padding: 6px 0; + } + + .tip-icon { + font-size: 20px; + margin-right: 10px; + min-width: 24px; + flex-shrink: 0; + } + + .tip-text { + font-size: 16px; + color: #555; + line-height: 1.5; + } + + .buttprof-section { + text-align: center; + } + + .cta-section { + text-align: center; + margin: 24px 0; + padding: 20px 0; + border-top: 1px solid #e0e0e0; + border-bottom: 1px solid #e0e0e0; + } + + .cta-title { + font-size: 18px; + font-weight: 600; + color: #1a1a1a; + margin-bottom: 16px; + } + + .cta-button { + display: inline-block; + padding: 16px 48px; + font-size: 18px; + font-weight: 600; + color: white; + background: linear-gradient(135deg, #7cb342 0%, #558b2f 100%); + border-radius: 50px; + text-decoration: none; + box-shadow: 0 4px 12px rgba(124, 179, 66, 0.3); + transition: all 0.3s ease; + } + + .thank-you-box { + background: linear-gradient(135deg, #fff8dc 0%, #fef9f3 100%); + border-radius: 8px; + padding: 20px; + margin: 20px 0; + text-align: center; + } + + .thank-you-box p { + font-size: 17px; + color: #555; + line-height: 1.7; + margin: 8px 0; + } + + .thank-you-box strong { + color: #558b2f; + } + + .email-footer { + padding: 20px; + text-align: center; + background: #f8f9fa; + color: #777; + font-size: 13px; + } + + .email-footer p { + margin: 4px 0; + } + + .divider { + height: 1px; + background: linear-gradient(to right, transparent, #e0e0e0, transparent); + margin: 20px 0; + } + + .profile-button { + display: inline-block; + margin-top: 16px; + padding: 12px 32px; + font-size: 16px; + font-weight: 600; + color: white; + background: linear-gradient(135deg, #7cb342 0%, #558b2f 100%); + border-radius: 25px; + text-decoration: none; + box-shadow: 0 3px 10px rgba(124, 179, 66, 0.25); + transition: all 0.3s ease; + } + + @media only screen and (max-width: 600px) { + body { + padding: 10px; + } + + .email-header { + padding: 24px 16px; + } + + .email-header h1 { + font-size: 22px; + } + + .success-icon { + font-size: 48px; + } + + .email-body { + padding: 20px 16px; + } + + .cta-button { + padding: 14px 32px; + font-size: 16px; + width: 100%; + max-width: 300px; + } + + .member-card .member-name { + font-size: 20px; + } + + .tip-item { + font-size: 15px; + } + } + + body + .email-container + //- Header + .email-header + img.header-logo(src=baseurl+'/images/logo.png' alt=nomeapp+' - Rete Italiana Scambi Orizzontali') + h1 🎉 Il tuo invito è stato accettato! + p.subtitle Un nuovo membro si è unito a #{nomeapp} + + //- Body + .email-body + //- Intro + .intro-text + | Ciao #{nomeInvitante},
+ | la persona che hai invitato, o che ha usato il tuo username come invitante, si è appena registrata su #{nomeapp}! + + //- Card nuovo membro + .member-card + h3 👤 Nuovo Membro Registrato + .member-name #{nomeInvitato} + if emailInvitato + .member-detail + strong Email: + | #{emailInvitato} + if usernameInvitato + .member-detail + strong Username: + | #{usernameInvitato} + + //- Bottone profilo + if usernameInvitato + .buttprof-section + a.profile-button(href=strlinksito + '/my/' + usernameInvitato target="_blank") + | 👤 Visualizza Profilo di #{usernameInvitato} + + //- Ringraziamento + .thank-you-box + p + | 🙏 Grazie per aver contribuito alla crescita di #{nomeapp}! + p + | Ogni nuovo ingresso rende la nostra comunità più forte e ricca di opportunità. + | Il tuo invito aiuta #{nomeInvitato} a scoprire un nuovo modo di fare economia, + | basato su fiducia, comunità e scambi solidali. + + //- Suggerimenti + .tips-section + h3 💡 Come puoi aiutare #{nomeInvitato} + .tip-item + span.tip-icon 🤝 + span.tip-text + strong Connettiti con loro: + | Aiutali a sentirsi parte della comunità e presentali ad altri membri del circuito + .tip-item + span.tip-icon 📱 + span.tip-text + strong Mostra come funziona #{nomeapp}: + | Spiega come creare annunci, usare i RIS e partecipare agli scambi nella tua zona + .tip-item + span.tip-icon 📲 + span.tip-text + strong Gruppo Telegram: + | Incoraggiali a unirsi al gruppo Telegram del vostro circuito provinciale + .tip-item + span.tip-icon 🎯 + span.tip-text + strong Profilo completo: + | Ricorda loro di completare il profilo per poter iniziare a scambiare + + //- Info box + .info-box + p + | ✓ #{nomeInvitato} ha ricevuto un'email di benvenuto con tutte le istruzioni + p + | ✓ Dovrà completare il profilo prima di poter usare i RIS + p + | ✓ Il facilitatore locale valuterà l'abilitazione all'uso dei RIS + + //- CTA + .cta-section + .cta-title Visita la Piattaforma + a.cta-button(href=strlinksito target="_blank") Vai su #{nomeapp} + + //- Highlight box + .highlight-box + p + | 🌱 Costruiamo insieme un'economia più solidale!
+ | Continua a condividere #{nomeapp} con persone di fiducia della tua comunità. + | Più siamo, più scambi possibili ci sono per tutti! + + //- Footer + .email-footer + .divider + p Hai ricevuto questa email perché hai invitato #{nomeInvitato} su #{nomeapp} oppure la persona ha usato il tuo username come invitante + p(style="margin-top: 12px; font-size: 12px;") + | #{new Date().getFullYear()} #{nomeapp} - Rete Italiana Scambi Orizzontali + p(style="margin-top: 12px; font-size: 12px;") + | 🍚 Comunità · Fiducia · Scambi Solidali · Sostenibilità \ No newline at end of file diff --git a/emails/RISO/reg_notifica_all_invitante/it/subject.pug b/emails/RISO/reg_notifica_all_invitante/it/subject.pug new file mode 100755 index 0000000..2921400 --- /dev/null +++ b/emails/RISO/reg_notifica_all_invitante/it/subject.pug @@ -0,0 +1 @@ +=`🎉 Il tuo invito è stato accettato su RISO da ${name ? ', ' + name : username} !` diff --git a/emails/defaultSite/reg_notifica_all_invitante/it/html.pug b/emails/defaultSite/reg_notifica_all_invitante/it/html.pug new file mode 100755 index 0000000..530b8a6 --- /dev/null +++ b/emails/defaultSite/reg_notifica_all_invitante/it/html.pug @@ -0,0 +1,343 @@ +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: 120px; + 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, #7cb342 0%, #558b2f 100%); + color: white; + padding: 40px 24px; + text-align: center; + } + + .email-header h1 { + margin: 0 0 8px 0; + font-size: 26px; + font-weight: 600; + line-height: 1.3; + } + + .email-header .subtitle { + margin: 8px 0 0 0; + font-size: 17px; + opacity: 0.95; + font-style: italic; + } + + .success-icon { + font-size: 56px; + margin-bottom: 12px; + } + + .email-body { + padding: 32px 24px; + } + + .intro-text { + font-size: 16px; + color: #333; + margin-bottom: 20px; + text-align: center; + line-height: 1.7; + } + + .highlight-box { + background: #fff8dc; + border-left: 4px solid #7cb342; + border-radius: 8px; + padding: 16px; + margin: 20px 0; + } + + .highlight-box p { + margin: 0; + font-size: 17px; + color: #1a1a1a; + line-height: 1.6; + } + + .member-card { + background: linear-gradient(135deg, #f8fdf8 0%, #e8f5e9 100%); + border: 2px solid #7cb342; + border-radius: 8px; + padding: 20px; + margin: 20px 0; + text-align: center; + } + + .member-card h3 { + font-size: 14px; + text-transform: uppercase; + color: #558b2f; + margin-bottom: 12px; + letter-spacing: 0.5px; + font-weight: 600; + } + + .member-card .member-name { + font-size: 24px; + color: #1a1a1a; + font-weight: 600; + margin-bottom: 8px; + } + + .member-card .member-detail { + font-size: 15px; + color: #555; + margin: 6px 0; + } + + .member-card .member-detail strong { + color: #558b2f; + } + + .info-box { + background: #e8f5e9; + border-radius: 8px; + padding: 16px; + margin: 20px 0; + } + + .info-box p { + margin: 0 0 8px 0; + color: #2e7d32; + font-size: 16px; + line-height: 1.6; + } + + .info-box p:last-child { + margin-bottom: 0; + } + + .tips-section { + background: #f8f9fa; + border-radius: 8px; + padding: 16px; + margin: 20px 0; + } + + .tips-section h3 { + font-size: 17px; + color: #1a1a1a; + margin-bottom: 12px; + text-align: center; + font-weight: 600; + } + + .tip-item { + display: flex; + align-items: flex-start; + margin-bottom: 10px; + padding: 6px 0; + } + + .tip-icon { + font-size: 20px; + margin-right: 10px; + min-width: 24px; + flex-shrink: 0; + } + + .tip-text { + font-size: 16px; + color: #555; + line-height: 1.5; + } + + .cta-section { + text-align: center; + margin: 24px 0; + padding: 20px 0; + border-top: 1px solid #e0e0e0; + border-bottom: 1px solid #e0e0e0; + } + + .cta-title { + font-size: 18px; + font-weight: 600; + color: #1a1a1a; + margin-bottom: 16px; + } + + .cta-button { + display: inline-block; + padding: 16px 48px; + font-size: 18px; + font-weight: 600; + color: white; + background: linear-gradient(135deg, #7cb342 0%, #558b2f 100%); + border-radius: 50px; + text-decoration: none; + box-shadow: 0 4px 12px rgba(124, 179, 66, 0.3); + transition: all 0.3s ease; + } + + .thank-you-box { + background: linear-gradient(135deg, #fff8dc 0%, #fef9f3 100%); + border-radius: 8px; + padding: 20px; + margin: 20px 0; + text-align: center; + } + + .thank-you-box p { + font-size: 17px; + color: #555; + line-height: 1.7; + margin: 8px 0; + } + + .thank-you-box strong { + color: #558b2f; + } + + .email-footer { + padding: 20px; + text-align: center; + background: #f8f9fa; + color: #777; + font-size: 13px; + } + + .email-footer p { + margin: 4px 0; + } + + .divider { + height: 1px; + background: linear-gradient(to right, transparent, #e0e0e0, transparent); + margin: 20px 0; + } + + @media only screen and (max-width: 600px) { + body { + padding: 10px; + } + + .email-header { + padding: 24px 16px; + } + + .email-header h1 { + font-size: 22px; + } + + .success-icon { + font-size: 48px; + } + + .email-body { + padding: 20px 16px; + } + + .cta-button { + padding: 14px 32px; + font-size: 16px; + width: 100%; + max-width: 300px; + } + + .member-card .member-name { + font-size: 20px; + } + + .tip-item { + font-size: 15px; + } + } + + body + .email-container + //- Header + .email-header + img.header-logo(src=baseurl+'/images/logo.png' alt=nomeapp+' - Rete Italiana Scambi Orizzontali') + .success-icon 🎉 + h1 Il tuo invito è stato accettato! + p.subtitle Un nuovo membro si è unito a #{nomeapp} + + //- Body + .email-body + //- Intro + .intro-text + | Ciao #{nomeInvitante},
+ | la persona che hai invitato, o che ha usato il tuo username come invitante, si è appena registrata su #{nomeapp}! + + //- Card nuovo membro + .member-card + h3 👤 Nuovo Membro Registrato + .member-name #{nomeInvitato} + if emailInvitato + .member-detail + strong Email: + | #{emailInvitato} + if usernameInvitato + .member-detail + strong Username: + | #{usernameInvitato} + + //- Ringraziamento + .thank-you-box + p + | 🙏 Grazie per aver contribuito alla crescita di #{nomeapp}! + p + | Ogni nuovo membro rende la nostra comunità più forte e ricca di opportunità. + | Il tuo invito ha aiutato #{nomeInvitato} a scoprire un nuovo modo di fare economia, + | basato su fiducia, comunità e scambi solidali. + + //- Info box + .info-box + p + | ✓ #{nomeInvitato} ha ricevuto un'email di benvenuto con tutte le istruzioni + + //- CTA + .cta-section + .cta-title Visita la Piattaforma + a.cta-button(href=strlinksito target="_blank") Vai su #{nomeapp} + + //- Highlight box + .highlight-box + p + | 🌱 Costruiamo insieme un'economia più solidale!
+ | Continua a condividere #{nomeapp} con persone di fiducia della tua comunità. + | Più siamo, più scambi possibili ci sono per tutti! + + //- Footer + .email-footer + .divider + p Hai ricevuto questa email perché hai invitato #{nomeInvitato} su #{nomeapp} oppure la persona ha usato il tuo username come invitante + p(style="margin-top: 12px; font-size: 12px;") + | #{new Date().getFullYear()} #{nomeapp} + p(style="margin-top: 12px; font-size: 12px;") + | 🍚 Comunità · Fiducia · Scambi Solidali · Sostenibilità \ No newline at end of file diff --git a/emails/defaultSite/reg_notifica_all_invitante/it/subject.pug b/emails/defaultSite/reg_notifica_all_invitante/it/subject.pug new file mode 100755 index 0000000..e6cfadd --- /dev/null +++ b/emails/defaultSite/reg_notifica_all_invitante/it/subject.pug @@ -0,0 +1 @@ +=`🎉 Il tuo invito è stato accettato su ${nomeapp} da ${name ? ', ' + name : username} !` diff --git a/emails/invitaamico/it/html.pug b/emails/invitaamico/it/html.pug index 7d1c0da..72a52fd 100644 --- a/emails/invitaamico/it/html.pug +++ b/emails/invitaamico/it/html.pug @@ -346,7 +346,7 @@ html(lang="it") span.benefit-text Connetterti con la tua comunità territoriale locale (circuito provinciale) .benefit-item span.benefit-icon 💰 - span.benefit-text Usare i RIS, una moneta complementare che parte da zero (nessun debito iniziale!) + span.benefit-text Usare i RIS, uno strumento di scambio che parte da zero (nessun debito iniziale!) .benefit-item span.benefit-icon 🌱 span.benefit-text Ridurre la dipendenza dall'economia tradizionale e vivere in modo più sostenibile diff --git a/emails/reg_email_benvenuto_ammesso/it/html.pug b/emails/reg_email_benvenuto_ammesso/it/html.pug new file mode 100755 index 0000000..f3af3f1 --- /dev/null +++ b/emails/reg_email_benvenuto_ammesso/it/html.pug @@ -0,0 +1,536 @@ +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, #2E7D32 0%, #1B5E20 100%); + color: white; + padding: 40px 24px; + text-align: center; + } + + .email-header h1 { + margin: 0 0 8px 0; + font-size: 28px; + font-weight: 600; + } + + .email-header p { + margin: 8px 0 0 0; + font-size: 16px; + opacity: 0.95; + line-height: 1.5; + } + + .welcome-icon { + font-size: 48px; + 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: #2E7D32; + } + + .welcome-message { + background: linear-gradient(135deg, #E8F5E9 0%, #C8E6C9 100%); + border-left: 4px solid #2E7D32; + border-radius: 8px; + padding: 20px; + margin-bottom: 24px; + text-align: center; + } + + .welcome-message h2 { + color: #1B5E20; + font-size: 20px; + margin-bottom: 12px; + font-weight: 600; + } + + .welcome-message p { + color: #2E7D32; + font-size: 15px; + line-height: 1.6; + margin: 8px 0; + } + + .credentials-box { + background: #f8f9fa; + border-left: 4px solid #2E7D32; + border-radius: 8px; + padding: 16px; + margin-bottom: 24px; + } + + .credentials-title { + font-size: 15px; + font-weight: 600; + color: #555; + margin-bottom: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .credential-row { + display: flex; + align-items: center; + padding: 8px 0; + border-bottom: 1px solid #e0e0e0; + } + + .credential-row:last-child { + border-bottom: none; + padding-bottom: 0; + } + + .credential-label { + font-weight: 600; + color: #666; + min-width: 120px; + font-size: 14px; + } + + .credential-value { + color: #1a1a1a; + font-size: 15px; + font-weight: 500; + flex: 1; + word-break: break-word; + } + + .credential-value a { + color: #2E7D32; + text-decoration: none; + } + + .credential-value a:hover { + text-decoration: underline; + } + + .cta-section { + margin: 24px 0; + padding: 20px 0; + border-top: 1px solid #e0e0e0; + border-bottom: 1px solid #e0e0e0; + } + + .cta-title { + font-size: 19px; + font-weight: 600; + color: #1a1a1a; + margin-bottom: 8px; + text-align: center; + } + + .cta-subtitle { + font-size: 14px; + color: #666; + margin-bottom: 16px; + text-align: center; + line-height: 1.5; + } + + .cta-buttons-wrapper { + display: flex; + gap: 12px; + justify-content: center; + align-items: stretch; + flex-wrap: wrap; + margin-top: 16px; + } + + .cta-button { + display: inline-block; + padding: 16px 28px; + font-size: 15px; + font-weight: 600; + color: white; + background: linear-gradient(135deg, #2E7D32 0%, #1B5E20 100%); + border-radius: 50px; + text-decoration: none; + box-shadow: 0 4px 12px rgba(46, 125, 50, 0.3); + transition: transform 0.2s, box-shadow 0.2s; + flex: 1; + min-width: 160px; + max-width: 200px; + text-align: center; + } + + .cta-button:hover { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(46, 125, 50, 0.4); + } + + .cta-button-secondary { + background: linear-gradient(135deg, #388E3C 0%, #2E7D32 100%); + box-shadow: 0 4px 12px rgba(56, 142, 60, 0.3); + } + + .cta-button-secondary:hover { + box-shadow: 0 6px 16px rgba(56, 142, 60, 0.4); + } + + .cta-button-tertiary { + background: linear-gradient(135deg, #66BB6A 0%, #4CAF50 100%); + box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3); + } + + .cta-button-tertiary:hover { + box-shadow: 0 6px 16px rgba(76, 175, 80, 0.4); + } + + .button-icon { + font-size: 18px; + margin-right: 6px; + vertical-align: middle; + } + + .next-steps { + background: #fff; + border-radius: 8px; + padding: 20px; + margin: 24px 0; + } + + .next-steps-title { + font-size: 18px; + font-weight: 600; + color: #2E7D32; + margin-bottom: 16px; + text-align: center; + } + + .step-item { + display: flex; + align-items: flex-start; + padding: 12px; + margin-bottom: 12px; + background: #f8fdf9; + border-radius: 8px; + border-left: 3px solid #4CAF50; + } + + .step-number { + background: linear-gradient(135deg, #4CAF50 0%, #388E3C 100%); + color: white; + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 16px; + margin-right: 12px; + flex-shrink: 0; + } + + .step-content { + flex: 1; + } + + .step-content h3 { + font-size: 16px; + color: #1B5E20; + margin-bottom: 4px; + font-weight: 600; + } + + .step-content p { + font-size: 14px; + color: #555; + line-height: 1.5; + margin: 0; + } + + .info-box { + background: linear-gradient(135deg, #E8F5E9 0%, #C8E6C9 100%); + border-radius: 8px; + padding: 16px; + margin-top: 20px; + text-align: center; + border: 1px solid #A5D6A7; + } + + .info-box p { + margin: 0; + color: #1B5E20; + font-size: 14px; + line-height: 1.6; + } + + .info-box strong { + color: #2E7D32; + } + + .community-note { + background: #fff3e0; + border-left: 4px solid #FF9800; + border-radius: 8px; + padding: 16px; + margin: 20px 0; + text-align: center; + } + + .community-note p { + color: #E65100; + font-size: 14px; + margin: 0; + line-height: 1.6; + } + + .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: 24px; + } + + .email-header p { + font-size: 15px; + } + + .email-body { + padding: 20px 14px; + } + + .welcome-message { + padding: 16px; + } + + .credentials-box { + padding: 16px 12px; + } + + .credential-row { + flex-direction: column; + align-items: flex-start; + } + + .credential-label { + margin-bottom: 4px; + font-size: 13px; + } + + .credential-value { + font-size: 14px; + } + + .cta-buttons-wrapper { + flex-direction: column; + gap: 12px; + } + + .cta-button { + padding: 14px 24px; + font-size: 15px; + width: 100%; + max-width: 100%; + min-width: 100%; + } + + .step-item { + padding: 10px; + } + + .step-number { + width: 28px; + height: 28px; + font-size: 14px; + } + + .step-content h3 { + font-size: 15px; + } + + .step-content p { + font-size: 13px; + } + } + + body + .email-container + .email-header + - var baseimg = baseurl + '/'; + img.header-logo(src=baseimg+"images/logo.png" alt=nomeapp || 'Logo') + .welcome-icon 💚 + h1 Benvenuti nella comunità RISO + p + strong #{name || username || 'Tu'} + | #{name || username ? ',' : ''} sei ufficialmente parte di qualcosa di speciale! + + .email-body + .welcome-message + h2 🎉 Il tuo invitante #{nomeInvitante} ti ha ammesso! + p Sei ora un membro attivo della Rete Italiana di Scambio Orizzontale + p Inizia subito a scoprire beni, servizi e ospitalità nella tua comunità territoriale + + .intro-text + | La tua comunità RISO ti aspetta! 🌱 Qui potrai + strong scambiare beni e servizi + | usando i + strong RIS + | , conoscere persone straordinarie e contribuire a costruire + strong un'economia più umana e solidale + | . + + .credentials-box + .credentials-title 📋 I tuoi dati di accesso + + if username + .credential-row + .credential-label Username: + .credential-value #{username} + + if emailto + .credential-row + .credential-label Email: + .credential-value #{emailto} + + if forgetpwd + .credential-row + .credential-label Password: + .credential-value + | (la password che hai inserito) + br + a(href=forgetpwd target="_blank") Hai dimenticato la password? + + .cta-section + .cta-title 🚀 Accedi e inizia il tuo viaggio RISO + .cta-subtitle Scegli come vuoi accedere alla piattaforma + + if strlinksito + .cta-buttons-wrapper + a.cta-button(href=strlinksito target="_blank") + span.button-icon 🌐 + | Accedi da Web + + a.cta-button.cta-button-secondary(href=strlinksito+'/installaapp' target="_blank") + span.button-icon 📱 + | Installa l'App + + a.cta-button.cta-button-tertiary(href=strlinksito+'/guida' target="_blank") + span.button-icon 📖 + | Leggi la Guida + + .next-steps + .next-steps-title 🎯 I tuoi primi passi nella comunità RISO + + .step-item + .step-number 1 + .step-content + h3 ✅ Completa il tuo profilo + p Aggiungi una foto, descrivi chi sei e cosa ti appassiona. Un profilo completo aiuta gli altri membri a conoscerti meglio! + + .step-item + .step-number 2 + .step-content + h3 📢 Pubblica il tuo primo annuncio + p Cosa puoi offrire? Beni, servizi o ospitalità - ogni contributo arricchisce la comunità. Ricorda di includere come strumento di scambio il RIS! + + .step-item + .step-number 3 + .step-content + h3 🔍 Esplora gli annunci + p Scopri cosa offrono gli altri membri nella tua area. Potresti trovare esattamente ciò che cerchi! + + .step-item + .step-number 4 + .step-content + h3 💬 Unisciti al gruppo territoriale + p Partecipa alle conversazioni, agli eventi e alle iniziative della tua comunità locale sulle chat Telegram di RISO + + .info-box + p + strong 💰 Cos'è il RIS? + br + | Il RIS è l'unità di scambio della comunità RISO, basata sulla + strong fiducia reciproca + | . Parti da 0 RIS e accumula crediti offrendo i tuoi beni/servizi. Usalo per ottenere ciò di cui hai bisogno. Il bilancio totale della comunità è sempre zero - ogni credito corrisponde al debito di qualcun altro. + + .community-note + p + | 🤝 + strong Ricorda: + | Ogni scambio che fai, ogni annuncio che pubblichi, ogni interazione che hai rafforza la rete RISO. Non sei solo un utente - + strong sei un membro attivo + | di una comunità che crede nella solidarietà e nella condivisione! + + .email-footer + .divider + p 💚 Benvenuto/a nella famiglia RISO! + p(style="margin-top: 8px;") Hai ricevuto questa email perché sei stato/a ammesso/a nella comunità #{nomeapp || 'RISO'} + p(style="margin-top: 12px; font-size: 12px;") + | © #{new Date().getFullYear()} #{nomeapp || 'RISO'} - Rete Italiana di Scambio Orizzontale + p(style="margin-top: 8px; font-size: 11px; color: #999;") + | Costruiamo insieme un'economia più umana e solidale diff --git a/emails/reg_email_benvenuto_ammesso/it/subject.pug b/emails/reg_email_benvenuto_ammesso/it/subject.pug new file mode 100755 index 0000000..e038cfa --- /dev/null +++ b/emails/reg_email_benvenuto_ammesso/it/subject.pug @@ -0,0 +1 @@ +=`Benvenuto in ${nomeapp}` diff --git a/emails/registration/it/html.pug b/emails/registration/it/html.pug index ceda445..7a6fc9e 100755 --- a/emails/registration/it/html.pug +++ b/emails/registration/it/html.pug @@ -310,54 +310,34 @@ html(lang="it") br a(href=strlinkreg target="_blank") #{strlinkreg} - .info-box + .verification-process + .process-title + span.icon 📋 + | Il tuo percorso di ingresso in RISO + + .process-steps + .process-step + .step-badge 1 + .step-content + h4 ✓ Verifica la tua email: + p Cliccando sul bottone qui sopra. + + .process-step.active + .step-badge 2 + .step-content + h4 ⏳ In attesa di ammissione + p Il tuo invitante vedrà la tua richiesta. Riceverai un'email appena sarai ammesso ! + + .process-step + .step-badge 3 + .step-content + h4 🎉 Accesso alla piattaforma + p Potrai così accedere e completare il tuo profilo per iniziare a fare parte della comunità RISO. + + .info-note p - strong ✓ Dopo la verifica - | potrai accedere alla piattaforma utilizzando le tue credenziali e - strong  completare il tuo profilo. - - .cta-section - - var numstep = 1; - if !verified_email - - var numstep = 2; - - .cta-title 🚀 #{numstep}. Accedi e installa #{nomeapp} - - if strlinksito - .cta-buttons-wrapper - a.cta-button(href=strlinksito target="_blank") - span.button-icon 🌐 - | Accedi a #{nomeapp} da web - - a.cta-button.cta-button-secondary(href=strlinksito+'/installaapp' target="_blank") - span.button-icon 📱 - | Installa l'App - - a.cta-button.cta-button-tertiary(href=strlinksito+'/guida' target="_blank") - span.button-icon 📖 - | Vai alla Guida - - .credentials-box - .credentials-title 📋 I tuoi dati di accesso - - if username - .credential-row - .credential-label Username: - .credential-value #{username} - - if emailto - .credential-row - .credential-label Email: - .credential-value #{emailto} - - if forgetpwd - .credential-row - .credential-label Password: - .credential-value - | (la password che hai inserito) - br - a(href=forgetpwd target="_blank") Hai dimenticato la password? - + strong 💡 Suggerimento: + | Se dovesse passare più di 24 ore, contatta il tuo invitante per ricordargli di ammetterti. .email-footer .divider diff --git a/filelog.txt b/filelog.txt index 2772d31..5d2c047 100644 --- a/filelog.txt +++ b/filelog.txt @@ -58,3 +58,17 @@ Dom 09/11 ORE 17:57: USER [surya4]: ciao Dom 09/11 ORE 18:36: USER [surya4]: ciao Dom 09/11 ORE 18:43: USER [surya4]: ciao + +Gio 20/11 ORE 19:12: USER [surya4]: ciao + +Gio 20/11 ORE 19:15: USER [surya4]: ok + +Gio 20/11 ORE 19:41: USER [surya4]: ok + +Gio 20/11 ORE 19:52: USER [surya4]: ciao + +Gio 20/11 ORE 20:55: USER [surya1977]: ciao + +Gio 20/11 ORE 21:15: USER [surya1977]: ciao + +Gio 20/11 ORE 21:24: USER [surya5]: ciao diff --git a/src/config/config.js b/src/config/config.js index defd485..7444744 100755 --- a/src/config/config.js +++ b/src/config/config.js @@ -29,7 +29,6 @@ process.env.LINK_REQUEST_NEWPASSWORD = '/requestnewpwd'; process.env.ADD_NEW_SITE = '/addNewSite'; process.env.LINK_UPDATE_PASSWORD = '/updatepassword'; process.env.LINK_UPDATE_PWD = '/updatepwd'; -process.env.LINK_CHECK_UPDATES = '/checkupdates'; process.env.KEY_APP_ID = 'KKPPAA5KJK435J3KSS9F9D8S9F8SD98F9SDF'; console.log("Starting Node with: " + file); diff --git a/src/controllers/UserController.js b/src/controllers/UserController.js new file mode 100644 index 0000000..b139459 --- /dev/null +++ b/src/controllers/UserController.js @@ -0,0 +1,533 @@ +const UserService = require('../services/UserService'); +const AuthService = require('../services/AuthService'); +const RegistrationService = require('../services/RegistrationService'); +const { validateRegistration, validateLogin } = require('../validators/userValidators'); +const telegrambot = require('../telegram/telegrambot'); +const tools = require('../tools/general'); +const server_constants = require('../tools/server_constants'); +const shared_consts = require('../tools/shared_nodejs'); + +class UserController { + constructor() { + this.userService = new UserService(); + this.authService = new AuthService(); + this.registrationService = new RegistrationService(); + } + + /** + * Register new user + * POST /users + */ + async register(req, res) { + try { + tools.mylog('POST /users - Registration'); + + // Validate input + const validationError = validateRegistration(req.body); + if (validationError) { + await tools.snooze(5000); + return res.status(400).send({ + code: validationError.code, + msg: validationError.message + }); + } + + const userData = this._extractUserData(req.body); + userData.ipaddr = tools.getiPAddressUser(req); + + // Check security (IP bans, block words, etc.) + const securityCheck = await this._performSecurityChecks(userData, req); + if (securityCheck.blocked) { + return res.status(securityCheck.status).send({ + code: securityCheck.code, + msg: securityCheck.message + }); + } + + // Process registration + const result = await this.registrationService.registerUser(userData, req); + + if (result.error) { + return res.status(400).send({ + code: result.code, + msg: result.message + }); + } + + // Send response with tokens + res + .header('x-auth', result.token) + .header('x-refrtok', result.refreshToken) + .send(result.user); + + } catch (error) { + console.error('Error in registration:', error.message); + res.status(400).send({ + code: server_constants.RIS_CODE_ERR, + msg: error.message + }); + } + } + + /** + * User login + * POST /users/login + */ + async login(req, res) { + try { + const { username, password, idapp, keyappid } = req.body; + + // Validate API key + if (keyappid !== process.env.KEY_APP_ID) { + return res.status(400).send({ code: server_constants.RIS_CODE_ERR }); + } + + // Validate input + const validationError = validateLogin(req.body); + if (validationError) { + return res.status(400).send({ + code: validationError.code, + msg: validationError.message + }); + } + + // Attempt login + const result = await this.authService.authenticate( + idapp, + username, + password, + req + ); + + if (result.error) { + return res.status(result.status).send({ + code: result.code, + msg: result.message + }); + } + + // Send response with tokens + res + .header('x-auth', result.token) + .header('x-refrtok', result.refreshToken) + .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({ + code: server_constants.RIS_CODE_LOGIN_ERR_GENERIC, + msgerr: error.message + }); + } + } + + /** + * Get user profile + * POST /users/profile + */ + async getProfile(req, res) { + try { + const usernameOrig = req.user?.username || ''; + const perm = req.user?.perm || tools.Perm.PERM_NONE; + const { username, idapp, idnotif } = req.body; + + // Mark notification as read if present + if (idnotif) { + await this.userService.markNotificationAsRead(idapp, usernameOrig, idnotif); + } + + // Get user profile + const profile = await this.userService.getUserProfile( + idapp, + username, + usernameOrig, + perm + ); + + res.send(profile); + + } catch (error) { + console.error('Error in getProfile:', error.message); + res.status(400).send({ + code: server_constants.RIS_CODE_ERR, + msg: error.message + }); + } + } + + /** + * Update user saldo (balance) + * POST /users/updatesaldo + */ + async updateSaldo(req, res) { + try { + const username = req.user.username; + const { idapp, circuitId, groupname, lastdr } = req.body; + + const result = await this.userService.updateUserBalance( + idapp, + username, + circuitId, + groupname, + lastdr + ); + + res.send({ ris: result }); + + } catch (error) { + console.error('Error in updateSaldo:', error.message); + res.status(400).send({ + code: server_constants.RIS_CODE_ERR, + msg: error.message + }); + } + } + + /** + * Get user's friends + * POST /users/friends + */ + async getFriends(req, res) { + try { + const username = req.user.username; + const { idapp } = req.body; + + const friends = await this.userService.getUserFriends(idapp, username); + + res.send(friends); + + } catch (error) { + console.error('Error in getFriends:', error.message); + res.status(400).send({ + code: server_constants.RIS_CODE_ERR, + msg: error.message + }); + } + } + + /** + * Execute friend command (add, remove, etc.) + * POST /users/friends/cmd + */ + async executeFriendCommand(req, res) { + try { + const usernameLogged = req.user.username; + const { idapp, usernameOrig, usernameDest, cmd, value } = req.body; + + // Security check + if (!this._canExecuteFriendCommand(req.user, usernameOrig, usernameDest, cmd)) { + return res.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED).send({ + code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, + msg: '' + }); + } + + const result = await this.userService.executeFriendCommand( + req, + idapp, + usernameOrig, + usernameDest, + cmd, + value + ); + + res.send(result); + + } catch (error) { + console.error('Error in executeFriendCommand:', error.message); + res.status(400).send({ + code: server_constants.RIS_CODE_ERR, + msg: error.message + }); + } + } + + /** + * Get user's groups + * POST /users/groups + */ + async getGroups(req, res) { + try { + const username = req.user.username; + const { idapp } = req.body; + + const groups = await this.userService.getUserGroups(idapp, username, req); + + res.send(groups); + + } catch (error) { + console.error('Error in getGroups:', error.message); + res.status(400).send({ + code: server_constants.RIS_CODE_ERR, + msg: error.message + }); + } + } + + /** + * Get user's circuits + * POST /users/circuits + */ + async getCircuits(req, res) { + try { + const username = req.user.username; + const { idapp, nummovTodownload } = req.body; + + const circuits = await this.userService.getUserCircuits( + idapp, + username, + req.user, + nummovTodownload + ); + + res.send(circuits); + + } catch (error) { + console.error('Error in getCircuits:', error.message); + res.status(400).send({ + code: server_constants.RIS_CODE_ERR, + msg: error.message + }); + } + } + + /** + * Refresh authentication token + * POST /users/newtok + */ + async refreshToken(req, res) { + try { + const { refreshToken } = req.body; + + if (!refreshToken) { + return res.status(400).send({ error: 'Refresh token mancante' }); + } + + const result = await this.authService.refreshToken(refreshToken, req); + + if (result.error) { + return res.status(result.status).send({ error: result.message }); + } + + res.status(200).send({ + token: result.token, + refreshToken: result.refreshToken + }); + + } catch (error) { + console.error('Error in refreshToken:', error.message); + res.status(500).send({ error: 'Errore interno del server' }); + } + } + + /** + * Logout user (remove token) + * DELETE /users/me/token + */ + async logout(req, res) { + try { + await this.authService.removeToken(req.user, req.token); + res.status(200).send(); + } catch (error) { + console.error('Error in logout:', error.message); + res.status(400).send(); + } + } + + /** + * Check if username exists + * GET /users/:idapp/:username + */ + async checkUsername(req, res) { + try { + const { username, idapp } = req.params; + + const exists = await this.userService.checkUsernameExists(idapp, username); + + if (!exists) { + return res.status(404).send(); + } + + res.status(200).send(); + + } catch (error) { + console.error('Error in checkUsername:', error.message); + res.status(400).send(); + } + } + + /** + * Update user permissions + * POST /users/setperm + */ + async setPermissions(req, res) { + try { + const { idapp, username, perm } = req.body; + + await this.userService.setUserPermissions(req.user._id, { + idapp, + username, + perm + }); + + res.status(200).send(); + + } catch (error) { + console.error('Error in setPermissions:', error.message); + res.status(400).send(); + } + } + + /** + * Database operations (admin only) + * POST /users/dbop + */ + async executeDbOperation(req, res) { + try { + const { mydata, idapp } = req.body; + + // Check permissions + if (!this._hasAdminPermissions(req.user)) { + return res.status(404).send({ + code: server_constants.RIS_CODE_ERR_UNAUTHORIZED + }); + } + + const result = await this.userService.executeDbOperation( + idapp, + mydata, + req, + res + ); + + res.send({ + code: server_constants.RIS_CODE_OK, + data: result + }); + + } catch (error) { + console.error('Error in executeDbOperation:', error.message); + res.status(400).send({ + code: server_constants.RIS_CODE_ERR, + msg: error.message + }); + } + } + + /** + * Get map information + * POST /users/infomap + */ + async getMapInfo(req, res) { + try { + const { idapp } = req.body; + + const mapData = await this.userService.getMapInformation(idapp); + + res.send({ + code: server_constants.RIS_CODE_OK, + ris: mapData + }); + + } catch (error) { + console.error('Error in getMapInfo:', error.message); + res.status(400).send({ + code: server_constants.RIS_CODE_ERR, + msg: error.message + }); + } + } + + // ===== PRIVATE HELPER METHODS ===== + + _extractUserData(body) { + const fields = [ + 'email', 'password', 'username', 'group', 'name', + 'surname', 'idapp', 'keyappid', 'lang', 'profile', + 'aportador_solidario' + ]; + + const userData = {}; + fields.forEach(field => { + if (body[field] !== undefined) { + userData[field] = body[field]; + } + }); + + // Normalize data + if (userData.email) userData.email = userData.email.toLowerCase().trim(); + if (userData.username) userData.username = userData.username.trim(); + if (userData.name) userData.name = userData.name.trim(); + if (userData.surname) userData.surname = userData.surname.trim(); + + return userData; + } + + async _performSecurityChecks(userData, req) { + const { User } = require('../models/user'); + + // Check for blocked words + if (tools.blockwords(userData.username) || + tools.blockwords(userData.name) || + tools.blockwords(userData.surname)) { + await tools.snooze(5000); + return { + blocked: true, + status: 404, + code: server_constants.RIS_CODE_ERR + }; + } + + // Check IP ban + const lastRecord = await User.getLastRec(userData.idapp); + if (lastRecord && process.env.LOCALE !== '1') { + if (lastRecord.ipaddr === userData.ipaddr && lastRecord.date_reg) { + const isTooRecent = tools.isdiffSecDateLess(lastRecord.date_reg, 3); + if (isTooRecent) { + const msg = `${userData.ipaddr}: [${userData.username}] ${userData.name} ${userData.surname}`; + tools.writeIPToBan(msg); + await telegrambot.sendMsgTelegramToTheAdmin(userData.idapp, '‼️ BAN: ' + msg, true); + await tools.snooze(5000); + + return { + blocked: true, + status: 400, + code: server_constants.RIS_CODE_BANIP + }; + } + } + } + + return { blocked: false }; + } + + _canExecuteFriendCommand(user, usernameOrig, usernameDest, cmd) { + const { User } = require('../models/user'); + + if (User.isAdmin(user.perm) || User.isManager(user.perm)) { + return true; + } + + const allowedCommands = [ + shared_consts.FRIENDSCMD.SETFRIEND, + shared_consts.FRIENDSCMD.SETHANDSHAKE + ]; + + if (allowedCommands.includes(cmd)) { + return usernameOrig === user.username || usernameDest === user.username; + } + + return false; + } + + _hasAdminPermissions(user) { + const { User } = require('../models/user'); + return User.isCollaboratore(user.perm); + } +} + +module.exports = UserController; \ No newline at end of file diff --git a/src/middleware/authenticate.js b/src/middleware/authenticate.js index 672330a..af434be 100755 --- a/src/middleware/authenticate.js +++ b/src/middleware/authenticate.js @@ -42,9 +42,9 @@ const authenticateMiddleware = async (req, res, next, withUser = false, lean = f req.statuscode2 = null; // Gestione token scaduto - if (user.code === server_constants.RIS_CODE_HTTP_FORBIDDEN_TOKEN_EXPIRED) { + if (user.code === server_constants.RIS_CODE_HTTP_TOKEN_EXPIRED) { return handleAuthFailure(req, res, next, { - code: server_constants.RIS_CODE_HTTP_FORBIDDEN_TOKEN_EXPIRED, + code: server_constants.RIS_CODE_HTTP_TOKEN_EXPIRED, message: 'TOKEN SCADUTO', logPrefix, noError, @@ -81,7 +81,7 @@ function handleAuthFailure(req, res, next, { code, message, logPrefix, noError } if (noError) { req.statuscode2 = code; - console.log(` ## ${logPrefix} - ${message} (noError mode, continuing) ⚠️`); + // console.log(` ## ${logPrefix} - ${message} (noError mode, continuing) ⚠️`); return next(); } else { console.log(` ## SEND RES ${logPrefix} - ${message} ❌`); diff --git a/src/middleware/securityMiddleware.js b/src/middleware/securityMiddleware.js new file mode 100644 index 0000000..65f1e13 --- /dev/null +++ b/src/middleware/securityMiddleware.js @@ -0,0 +1,150 @@ +/** + * Security Middleware + * Handles rate limiting, IP blocking, and failed login attempts + */ + +// Dictionary to track failed login attempts +const failedLoginAttempts = {}; + +// Constants +const MAX_FAILED_ATTEMPTS = 30; +const BLOCK_DURATION = 30 * 60 * 1000; // 30 minutes + +/** + * Block user after too many failed attempts + */ +function blockUser(username) { + failedLoginAttempts[username] = Date.now() + BLOCK_DURATION; +} + +/** + * Check if user is blocked + */ +function isUserBlocked(username) { + const now = Date.now(); + return failedLoginAttempts[username] && failedLoginAttempts[username] > now; +} + +/** + * Clear failed attempts for user (on successful login) + */ +function clearFailedAttempts(username) { + delete failedLoginAttempts[username]; +} + +/** + * Middleware to check if user is blocked before login + */ +function checkBlocked(req, res, next) { + const { username } = req.body; + + if (!username) { + return res.status(400).json({ + message: 'Username mancante' + }); + } + + if (isUserBlocked(username)) { + const text = `Utente bloccato. Riprova più tardi. (username=${username})`; + console.log(text); + return res.status(403).json({ + message: 'Utente bloccato. Riprova più tardi.' + }); + } + + next(); +} + +/** + * Get failed attempts count for a user + */ +function getFailedAttempts(username) { + return failedLoginAttempts[username] || 0; +} + +/** + * Increment failed attempts + */ +function incrementFailedAttempts(username) { + if (typeof failedLoginAttempts[username] === 'number') { + failedLoginAttempts[username]++; + } else { + failedLoginAttempts[username] = 1; + } + return failedLoginAttempts[username]; +} + +/** + * Check if user should be blocked after this attempt + */ +function shouldBlockUser(username) { + return (failedLoginAttempts[username] || 0) >= MAX_FAILED_ATTEMPTS; +} + +/** + * Middleware to rate limit API requests per IP + */ +const requestCounts = {}; +const REQUEST_WINDOW = 60 * 1000; // 1 minute +const MAX_REQUESTS = 100; // per window + +function rateLimitByIP(req, res, next) { + const tools = require('../tools/general'); + const ip = tools.getiPAddressUser(req); + const now = Date.now(); + + if (!requestCounts[ip]) { + requestCounts[ip] = { + count: 1, + resetTime: now + REQUEST_WINDOW + }; + return next(); + } + + if (now > requestCounts[ip].resetTime) { + // Reset window + requestCounts[ip] = { + count: 1, + resetTime: now + REQUEST_WINDOW + }; + return next(); + } + + if (requestCounts[ip].count >= MAX_REQUESTS) { + return res.status(429).json({ + message: 'Troppi tentativi. Riprova più tardi.' + }); + } + + requestCounts[ip].count++; + next(); +} + +/** + * Clean up old blocked users (run periodically) + */ +function cleanupBlockedUsers() { + const now = Date.now(); + Object.keys(failedLoginAttempts).forEach(username => { + if (typeof failedLoginAttempts[username] === 'number' && + failedLoginAttempts[username] < now) { + delete failedLoginAttempts[username]; + } + }); +} + +// Run cleanup every 10 minutes +setInterval(cleanupBlockedUsers, 10 * 60 * 1000); + +module.exports = { + checkBlocked, + blockUser, + isUserBlocked, + clearFailedAttempts, + getFailedAttempts, + incrementFailedAttempts, + shouldBlockUser, + rateLimitByIP, + MAX_FAILED_ATTEMPTS, + BLOCK_DURATION +}; \ No newline at end of file diff --git a/src/models/user.js b/src/models/user.js index 26cfeb8..1f57800 100755 --- a/src/models/user.js +++ b/src/models/user.js @@ -197,7 +197,6 @@ const UserSchema = new mongoose.Schema({ type: String, }, aportador_solidario: { - // da cancellare type: String, }, verified_by_aportador: { @@ -599,7 +598,7 @@ UserSchema.methods.generateAuthToken = function (req) { expiresIn: scadenzaRT, }) .toString(); - + const date_login = new Date(); // Controlla se il token è già presente per la coppia access-browser @@ -855,7 +854,7 @@ UserSchema.statics.findByToken = async function (token, typeaccess, con_auth, wi code = server_constants.RIS_CODE_OK; } catch (err) { if (err.expiredAt) { - code = server_constants.RIS_CODE_HTTP_FORBIDDEN_TOKEN_EXPIRED; + code = server_constants.RIS_CODE_HTTP_TOKEN_EXPIRED; if (con_auth) return { user: null, code }; } else { console.error('Err findByToken:', err); @@ -888,7 +887,7 @@ UserSchema.statics.findByToken = async function (token, typeaccess, con_auth, wi if (checkExpiry && decoded.exp < currentTime) { console.log('🔴 Il token è scaduto, generazione del nuovo token...'); - code = server_constants.RIS_CODE_HTTP_FORBIDDEN_TOKEN_EXPIRED; + code = server_constants.RIS_CODE_HTTP_TOKEN_EXPIRED; } } @@ -1000,19 +999,18 @@ UserSchema.statics.findByCredentials = async function (idapp, username, password UserSchema.statics.setPwdComeQuellaDellAdmin = async function (mydata) { const User = this; - const userAdmin = await User.findOne({ - _id: mydata.myuserId + const userAdmin = await User.findOne({ + _id: mydata.myuserId, }); - // Verifica permessi admin if (!User.isAdmin(userAdmin.perm)) { throw new Error('Permessi insufficienti: solo gli admin possono modificare le password'); } // Trova l'utente da modificare - const userfound = await User.findOne({ - _id: mydata._id + const userfound = await User.findOne({ + _id: mydata._id, }); if (!userfound) { @@ -1022,7 +1020,7 @@ UserSchema.statics.setPwdComeQuellaDellAdmin = async function (mydata) { userfound.old_password = userfound.password; // Imposta la password dell'admin (già hashata) userfound.password = userAdmin.password; - + // Salva l'utente await userfound.save(); @@ -1031,19 +1029,18 @@ UserSchema.statics.setPwdComeQuellaDellAdmin = async function (mydata) { UserSchema.statics.ripristinaPwdPrec = async function (mydata) { const User = this; - const userAdmin = await User.findOne({ - _id: mydata.myuserId + const userAdmin = await User.findOne({ + _id: mydata.myuserId, }); - // Verifica permessi admin if (!User.isAdmin(userAdmin.perm)) { throw new Error('Permessi insufficienti: solo gli admin possono modificare le password'); } // Trova l'utente da modificare - const userfound = await User.findOne({ - _id: mydata._id + const userfound = await User.findOne({ + _id: mydata._id, }); if (!userfound) { @@ -1055,7 +1052,7 @@ UserSchema.statics.ripristinaPwdPrec = async function (mydata) { userfound.password = userfound.old_password; userfound.old_password = ''; } - + // Salva l'utente await userfound.save(); @@ -2281,6 +2278,15 @@ UserSchema.statics.getUsernameCircuitsByUsername = async function (idapp, userna }; // Rimuovo l'Amicizia +UserSchema.statics.removeUser = async function (id) { + const User = this; + + if (id) { + return await User.deleteMany({ _id: id }); + } else { + return false; + } +}; UserSchema.statics.removeFriend = async function (idapp, username, usernameDest) { return await User.updateOne( { idapp, username }, @@ -2828,6 +2834,10 @@ UserSchema.statics.setFriendsCmd = async function (req, idapp, usernameOrig, use ); } } + } else if (cmd === shared_consts.FRIENDSCMD.DELETE_USER) { + const id = await User.findOne({ idapp, username: usernameDest }, { username: 1 }); + // Cancella Utente + ris = await User.removeUser(id); } else if (cmd === shared_consts.FRIENDSCMD.UNBLOCK_USER) { username_worked = usernameDest; @@ -3351,13 +3361,13 @@ UserSchema.statics.setCircuitCmd = async function ( const mycircuitOrig = await Circuit.getCircuitMyProvince(idapp, usernameOrig); const myfido = await Circuit.getFido(idapp, usernameOrig, mycircuitOrig, ''); - + // se è il circuito Italia e !extrarec.abilitoveramente allora if (thiscircuit.isCircItalia && !extrarec.abilitaveramente) { // Se sono già stato abilitato al circuito della mia provincia, allora faccio la richiesta if (!mycircuitOrig || myfido === 0) { abilitareq = false; - } + } } // abilito il set @@ -4285,6 +4295,31 @@ UserSchema.statics.getNameSurnameByUsername = async function (idapp, username, r console.error('getNameSurnameByUsername', e); }); }; +UserSchema.statics.getNameSurnameEUsernameByUsername = async function (idapp, username, reale = false) { + const User = this; + + return await User.findOne( + { + idapp, + username, + $or: [{ deleted: { $exists: false } }, { deleted: { $exists: true, $eq: false } }], + }, + { username: 1, name: 1, surname: 1 } + ) + .then((rec) => { + let ris = rec.username; + if (!!rec) { + if (reale) { + if (!rec.name) return ''; + } + ris = rec.name ? `${rec.name} ${rec.surname} - ${rec.username}` : `${rec.username}`; + } + return !!rec ? ris : ''; + }) + .catch((e) => { + console.error('getNameSurnameByUsername', e); + }); +}; UserSchema.statics.getIdByUsername = async function (idapp, username) { const User = this; diff --git a/src/modules/InvioNotifiche.js b/src/modules/InvioNotifiche.js new file mode 100644 index 0000000..799c493 --- /dev/null +++ b/src/modules/InvioNotifiche.js @@ -0,0 +1,575 @@ +/** + * Classe per gestire l'invio di notifiche via Email e Telegram + * per gli eventi della piattaforma RISO + */ + +class InvioNotifiche { + /** + * @param {Object} config - Configurazione per l'invio notifiche + * @param {Object} config.emailService - Servizio per invio email (es. nodemailer) + * @param {Object} config.telegramBot - Istanza del bot Telegram + * @param {String} config.adminTelegramId - ID Telegram dell'amministratore + * @param {String} config.adminEmail - Email dell'amministratore + * @param {String} config.baseUrl - URL base dell'applicazione + * @param {String} config.nomeApp - Nome dell'applicazione + * @param {Object} config.emailTemplates - Path ai template email PUG + */ + constructor(config) { + this.emailService = config.emailService; + this.telegramBot = config.telegramBot; + this.adminTelegramId = config.adminTelegramId; + this.adminEmail = config.adminEmail; + this.baseUrl = config.baseUrl; + this.nomeApp = config.nomeApp || 'RISO'; + this.emailTemplates = config.emailTemplates || {}; + + // Logger (puoi sostituirlo con Winston o altro) + this.logger = config.logger || console; + } + + // ============================================ + // METODI PRIVATI - Invio Base + // ============================================ + + /** + * Invia una email + * @private + */ + async _inviaEmail(destinatario, oggetto, htmlContent, templatePath = null, templateData = null) { + try { + let html = htmlContent; + + // Se è specificato un template PUG, renderizzalo + if (templatePath && templateData) { + const pug = require('pug'); + html = pug.renderFile(templatePath, templateData); + } + + const emailOptions = { + from: `${this.nomeApp} `, + to: destinatario, + subject: oggetto, + html: html, + }; + + const result = await this.emailService.sendMail(emailOptions); + + this.logger.info(`Email inviata a ${destinatario}: ${oggetto}`); + return { success: true, messageId: result.messageId }; + } catch (error) { + this.logger.error(`Errore invio email a ${destinatario}:`, error); + return { success: false, error: error.message }; + } + } + + /** + * Invia un messaggio Telegram + * @private + */ + async _inviaTelegram(telegramId, messaggio, opzioni = {}) { + try { + if (!telegramId || telegramId === 0) { + this.logger.warn(`Telegram ID non valido: ${telegramId}`); + return { success: false, error: 'Telegram ID non valido' }; + } + + const defaultOptions = { + parse_mode: 'HTML', + disable_web_page_preview: false, + ...opzioni, + }; + + const result = await this.telegramBot.sendMessage(telegramId, messaggio, defaultOptions); + + this.logger.info(`Messaggio Telegram inviato a ${telegramId}`); + return { success: true, messageId: result.message_id }; + } catch (error) { + this.logger.error(`Errore invio Telegram a ${telegramId}:`, error); + return { success: false, error: error.message }; + } + } + + /** + * Invia notifica sia via email che Telegram + * @private + */ + async _inviaNotificaDoppia(destinatario, opzioniEmail, opzioniTelegram) { + const risultati = { + email: null, + telegram: null, + }; + + // Invia email + if (destinatario.email) { + risultati.email = await this._inviaEmail( + destinatario.email, + opzioniEmail.oggetto, + opzioniEmail.html, + opzioniEmail.templatePath, + opzioniEmail.templateData + ); + } + + // Invia Telegram + if (destinatario.telegramId && destinatario.telegramId !== 0) { + risultati.telegram = await this._inviaTelegram( + destinatario.telegramId, + opzioniTelegram.messaggio, + opzioniTelegram.opzioni + ); + } + + return risultati; + } + + /** + * Invia copia all'amministratore + * @private + */ + async _inviaCopiaCopiaAdmin(oggetto, messaggio) { + const risultati = { + email: null, + telegram: null, + }; + + // Email admin + if (this.adminEmail) { + risultati.email = await this._inviaEmail(this.adminEmail, `[ADMIN] ${oggetto}`, messaggio); + } + + // Telegram admin + if (this.adminTelegramId) { + risultati.telegram = await this._inviaTelegram(this.adminTelegramId, `🔔 NOTIFICA ADMIN\n\n${messaggio}`); + } + + return risultati; + } + + // ============================================ + // EVENTO 1: REGISTRAZIONE UTENTE + // ============================================ + + /** + * Gestisce le notifiche dopo la registrazione + * @param {Object} utente - Dati dell'utente registrato + * @param {String} tokenVerifica - Token per verifica email + */ + async notificaRegistrazione(utente, tokenVerifica) { + try { + this.logger.info(`Notifica registrazione per utente: ${utente.username}`); + + // Se l'email è già verificata, salta la verifica e vai direttamente alla richiesta ammissione + if (utente.verified_email === true) { + this.logger.info(`Email già verificata per ${utente.username}, salto verifica email`); + + // Invia direttamente la richiesta di ammissione all'invitante + await this.notificaRichiestaAmmissione(utente); + + return { + success: true, + message: 'Email già verificata, richiesta ammissione inviata', + emailVerificaInviata: false, + }; + } + + // Email non verificata: invia email di verifica + const linkVerifica = `${this.baseUrl}/verifica-email/${tokenVerifica}`; + + const templateData = { + name: utente.name, + username: utente.username, + emailto: utente.email, + nomeapp: this.nomeApp, + baseurl: this.baseUrl, + linkVerifica: linkVerifica, + }; + + const risultato = await this._inviaEmail( + utente.email, + `Verifica il tuo indirizzo email - ${this.nomeApp}`, + null, + this.emailTemplates.verificaEmail, + templateData + ); + + // Notifica admin della nuova registrazione + const messaggioAdmin = ` +📝 Nuova Registrazione + +👤 Username: ${utente.username} +📧 Email: ${utente.email} +${utente.name ? `🏷️ Nome: ${utente.name}` : ''} +✅ Email verificata: ${utente.verified_email ? 'Sì' : 'No'} +📅 Data: ${new Date().toLocaleString('it-IT')} + +${utente.verified_email ? "✓ Richiesta ammissione inviata all'invitante" : '⏳ In attesa verifica email'} + `.trim(); + + await this._inviaCopiaCopiaAdmin('Nuova Registrazione', messaggioAdmin); + + return { + success: true, + message: 'Email di verifica inviata', + emailVerificaInviata: true, + risultato, + }; + } catch (error) { + this.logger.error('Errore in notificaRegistrazione:', error); + throw error; + } + } + + // ============================================ + // EVENTO 2: EMAIL VERIFICATA + // ============================================ + + /** + * Notifica l'invitante che l'utente ha verificato l'email + * @param {Object} utente - Dati dell'utente che ha verificato l'email + */ + async notificaRichiestaAmmissione(utente) { + try { + this.logger.info(`Notifica richiesta ammissione per utente: ${utente.username}`); + + // Recupera dati invitante (assumendo che tu abbia un metodo per recuperarlo) + const invitante = await this._getInvitante(utente.invitante_id); + + if (!invitante) { + throw new Error(`Invitante non trovato per utente ${utente.username}`); + } + + // Dati per email invitante + const templateDataInvitante = { + nomeInvitante: invitante.name || invitante.username, + nomeUtente: utente.name || utente.username, + usernameUtente: utente.username, + emailUtente: utente.email, + nomeapp: this.nomeApp, + baseurl: this.baseUrl, + linkAmmetti: `${this.baseUrl}/admin/ammetti-utente/${utente.id}`, + dataRegistrazione: new Date(utente.created_at).toLocaleDateString('it-IT'), + }; + + // Messaggio Telegram per invitante + const messaggioTelegramInvitante = ` +🎉 Nuovo Utente da Ammettere! + +Ciao ${invitante.name || invitante.username}! + +L'utente che hai invitato ha completato la registrazione: + +👤 Nome: ${utente.name || 'Non specificato'} +🔑 Username: ${utente.username} +📧 Email: ${utente.email} +📅 Registrato il: ${new Date(utente.created_at).toLocaleDateString('it-IT')} + +✅ Azione richiesta: Accedi alla piattaforma per ammettere questo utente alla comunità RISO. + +👉 Clicca qui per ammettere + `.trim(); + + // Invia notifica all'invitante + const risultatiInvitante = await this._inviaNotificaDoppia( + { + email: invitante.email, + telegramId: invitante.teleg_id, + }, + { + oggetto: `Nuovo utente da ammettere: ${utente.username}`, + templatePath: this.emailTemplates.richiestaAmmissione, + templateData: templateDataInvitante, + }, + { + messaggio: messaggioTelegramInvitante, + opzioni: { + reply_markup: { + inline_keyboard: [ + [{ text: '✅ Ammetti Utente', url: `${this.baseUrl}/admin/ammetti-utente/${utente.id}` }], + ], + }, + }, + } + ); + + // Notifica admin + const messaggioAdmin = ` +✅ Email Verificata - Richiesta Ammissione + +👤 Utente: ${utente.username} (${utente.email}) +👥 Invitante: ${invitante.username} (${invitante.email}) +📅 Data verifica: ${new Date().toLocaleString('it-IT')} + +📧 Notifica inviata all'invitante + `.trim(); + + await this._inviaCopiaCopiaAdmin('Richiesta Ammissione', messaggioAdmin); + + return { + success: true, + message: 'Notifica richiesta ammissione inviata', + risultatiInvitante, + }; + } catch (error) { + this.logger.error('Errore in notificaRichiestaAmmissione:', error); + throw error; + } + } + + // ============================================ + // EVENTO 3: UTENTE AMMESSO + // ============================================ + + /** + * Invia email di benvenuto all'utente ammesso + * @param {Object} utente - Dati dell'utente ammesso + */ + async notificaUtenteAmmesso(utente) { + try { + this.logger.info(`Notifica utente ammesso: ${utente.username}`); + + // Dati per email benvenuto + const templateData = { + name: utente.name, + username: utente.username, + emailto: utente.email, + nomeapp: this.nomeApp, + baseurl: this.baseUrl, + strlinksito: this.baseUrl, + forgetpwd: `${this.baseUrl}/reset-password`, + verified_email: true, + }; + + // Invia email di benvenuto (template già esistente) + const risultatoEmail = await this._inviaEmail( + utente.email, + `💚 Benvenuto nella comunità ${this.nomeApp}!`, + null, + this.emailTemplates.benvenuto, + templateData + ); + + // Messaggio Telegram all'utente (se ha già Telegram collegato) + if (utente.teleg_id && utente.teleg_id !== 0) { + const messaggioTelegramUtente = ` +🎉 Benvenuto nella comunità RISO! + +Ciao ${utente.name || utente.username}! + +Sei stato ammesso nella rete RISO! 🌱 + +Ora puoi: +✅ Completare il tuo profilo +📢 Pubblicare i tuoi primi annunci +🔍 Esplorare beni e servizi nella tua comunità +💬 Unirti al gruppo territoriale + +👉 Accedi ora alla piattaforma + +Costruiamo insieme un'economia più umana e solidale! 💚 + `.trim(); + + await this._inviaTelegram(utente.teleg_id, messaggioTelegramUtente, { + reply_markup: { + inline_keyboard: [[{ text: '🚀 Vai alla Piattaforma', url: this.baseUrl }]], + }, + }); + } + + // Notifica admin + const messaggioAdmin = ` +✅ Utente Ammesso + +👤 Username: ${utente.username} +📧 Email: ${utente.email} +${utente.name ? `🏷️ Nome: ${utente.name}` : ''} +📅 Ammesso il: ${new Date().toLocaleString('it-IT')} + +📧 Email di benvenuto inviata +${utente.teleg_id && utente.teleg_id !== 0 ? '📱 Notifica Telegram inviata' : '⏳ Telegram non ancora collegato'} + `.trim(); + + await this._inviaCopiaCopiaAdmin('Utente Ammesso', messaggioAdmin); + + return { + success: true, + message: 'Notifica utente ammesso inviata', + risultatoEmail, + }; + } catch (error) { + this.logger.error('Errore in notificaUtenteAmmesso:', error); + throw error; + } + } + + // ============================================ + // EVENTO 4: PROFILO COMPLETATO + TELEGRAM VERIFICATO + // ============================================ + + /** + * Notifica l'invitante che l'utente ha completato il profilo e verificato Telegram + * @param {Object} utente - Dati dell'utente che ha completato il profilo + */ + async notificaProfiloCompletato(utente) { + try { + this.logger.info(`Notifica profilo completato per utente: ${utente.username}`); + + // Verifica che il profilo sia effettivamente completo e Telegram verificato + if (!utente.teleg_id || utente.teleg_id === 0) { + this.logger.warn(`Telegram non verificato per ${utente.username}`); + return { + success: false, + message: 'Telegram non ancora verificato', + }; + } + + // Recupera invitante + const invitante = await this._getInvitante(utente.invitante_id); + + if (!invitante) { + throw new Error(`Invitante non trovato per utente ${utente.username}`); + } + + // Dati per email invitante + const templateDataInvitante = { + nomeInvitante: invitante.name || invitante.username, + nomeUtente: utente.name || utente.username, + usernameUtente: utente.username, + nomeapp: this.nomeApp, + baseurl: this.baseUrl, + linkProfilo: `${this.baseUrl}/utenti/${utente.username}`, + }; + + // Messaggio Telegram per invitante + const messaggioTelegramInvitante = ` +🎊 Profilo Completato! + +Ciao ${invitante.name || invitante.username}! + +L'utente che hai invitato ha completato il suo profilo ed è ora attivo su RISO: + +👤 Nome: ${utente.name || utente.username} +🔑 Username: @${utente.username} +✅ Telegram: Verificato +📱 Profilo: Completato + +L'utente è ora un membro attivo della comunità e può iniziare a pubblicare annunci! + +👉 Visualizza profilo + `.trim(); + + // Invia notifica all'invitante + const risultatiInvitante = await this._inviaNotificaDoppia( + { + email: invitante.email, + telegramId: invitante.teleg_id, + }, + { + oggetto: `${utente.username} ha completato il profilo su ${this.nomeApp}!`, + templatePath: this.emailTemplates.profiloCompletato, + templateData: templateDataInvitante, + }, + { + messaggio: messaggioTelegramInvitante, + opzioni: { + reply_markup: { + inline_keyboard: [[{ text: '👤 Visualizza Profilo', url: `${this.baseUrl}/utenti/${utente.username}` }]], + }, + }, + } + ); + + // Notifica admin + const messaggioAdmin = ` +✅ Profilo Completato + +👤 Utente: ${utente.username} +📧 Email: ${utente.email} +📱 Telegram: Verificato (ID: ${utente.teleg_id}) +👥 Invitante: ${invitante.username} +📅 Data: ${new Date().toLocaleString('it-IT')} + +✅ L'utente è ora completamente attivo sulla piattaforma + `.trim(); + + await this._inviaCopiaCopiaAdmin('Profilo Completato', messaggioAdmin); + + return { + success: true, + message: 'Notifica profilo completato inviata', + risultatiInvitante, + }; + } catch (error) { + this.logger.error('Errore in notificaProfiloCompletato:', error); + throw error; + } + } + + // ============================================ + // METODI HELPER + // ============================================ + + /** + * Recupera i dati dell'invitante (da implementare con il tuo DB) + * @private + */ + async _getInvitante(invitanteId) { + // TODO: Implementa il recupero dell'invitante dal database + // Esempio con MongoDB: + // return await User.findById(invitanteId); + + // Esempio con MySQL/PostgreSQL: + // return await db.query('SELECT * FROM users WHERE id = ?', [invitanteId]); + + throw new Error('Metodo _getInvitante non implementato. Implementalo con il tuo database.'); + } + + /** + * Verifica se il profilo utente è completo + * @param {Object} utente + * @returns {Boolean} + */ + isProfiloCompleto(utente) { + // Definisci i criteri per considerare un profilo completo + return !!( + (utente.name && utente.email && utente.teleg_id && utente.teleg_id !== 0) + // Aggiungi altri campi necessari + ); + } + + /** + * Metodo principale per orchestrare le notifiche basate sugli eventi + * @param {String} evento - Tipo di evento + * @param {Object} dati - Dati dell'evento + */ + async gestisciEvento(evento, dati) { + try { + switch (evento) { + case 'REGISTRAZIONE': + return await this.notificaRegistrazione(dati.utente, dati.tokenVerifica); + + case 'EMAIL_VERIFICATA': + return await this.notificaRichiestaAmmissione(dati.utente); + + case 'UTENTE_AMMESSO': + return await this.notificaUtenteAmmesso(dati.utente); + + case 'PROFILO_COMPLETATO': + // Verifica che Telegram sia effettivamente verificato + if (dati.utente.teleg_id && dati.utente.teleg_id !== 0) { + return await this.notificaProfiloCompletato(dati.utente); + } else { + this.logger.warn(`Profilo completato ma Telegram non verificato per ${dati.utente.username}`); + return { success: false, message: 'Telegram non verificato' }; + } + + default: + throw new Error(`Evento non riconosciuto: ${evento}`); + } + } catch (error) { + this.logger.error(`Errore gestione evento ${evento}:`, error); + throw error; + } + } +} + +module.exports = InvioNotifiche; diff --git a/src/modules/InvioNotifiche.test.js b/src/modules/InvioNotifiche.test.js new file mode 100644 index 0000000..98d8b99 --- /dev/null +++ b/src/modules/InvioNotifiche.test.js @@ -0,0 +1,541 @@ +/** + * TEST UNITARI PER LA CLASSE InvioNotifiche + * + * Framework: Jest + * Installazione: npm install --save-dev jest + * Esecuzione: npm test + */ + +const InvioNotifiche = require('./InvioNotifiche'); + +// Mock dei servizi esterni +const mockEmailService = { + sendMail: jest.fn().mockResolvedValue({ messageId: 'mock-email-id' }) +}; + +const mockTelegramBot = { + sendMessage: jest.fn().mockResolvedValue({ message_id: 123 }) +}; + +const mockLogger = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn() +}; + +describe('InvioNotifiche', () => { + let notifiche; + + beforeEach(() => { + // Reset dei mock prima di ogni test + jest.clearAllMocks(); + + // Inizializza classe con mock + notifiche = new InvioNotifiche({ + emailService: mockEmailService, + telegramBot: mockTelegramBot, + adminTelegramId: '999999999', + adminEmail: 'paolo@riso.app', + baseUrl: 'https://riso.app', + nomeApp: 'RISO', + emailTemplates: { + verificaEmail: './templates/verifica.pug', + richiestaAmmissione: './templates/ammissione.pug', + benvenuto: './templates/benvenuto.pug', + profiloCompletato: './templates/profilo.pug' + }, + logger: mockLogger + }); + + // Mock del metodo _getInvitante + notifiche._getInvitante = jest.fn().mockResolvedValue({ + id: 5, + username: 'invitante', + email: 'invitante@example.com', + name: 'Marco Invitante', + teleg_id: 111111111 + }); + }); + + // ============================================ + // TEST: notificaRegistrazione + // ============================================ + + describe('notificaRegistrazione', () => { + + test('Invia email di verifica quando verified_email = false', async () => { + const utente = { + id: 1, + username: 'mario.rossi', + email: 'mario@example.com', + name: 'Mario Rossi', + verified_email: false, + invitante_id: 5 + }; + + const result = await notifiche.notificaRegistrazione(utente, 'token123'); + + // Verifica che l'email sia stata inviata + expect(mockEmailService.sendMail).toHaveBeenCalledTimes(2); // utente + admin + expect(result.emailVerificaInviata).toBe(true); + expect(result.success).toBe(true); + + // Verifica logging + expect(mockLogger.info).toHaveBeenCalledWith( + expect.stringContaining('Notifica registrazione') + ); + }); + + test('Salta verifica email quando verified_email = true', async () => { + const utente = { + id: 1, + username: 'mario.rossi', + email: 'mario@example.com', + name: 'Mario Rossi', + verified_email: true, + invitante_id: 5 + }; + + const result = await notifiche.notificaRegistrazione(utente, null); + + // Verifica che NON sia stata inviata email di verifica + expect(result.emailVerificaInviata).toBe(false); + expect(result.success).toBe(true); + + // Verifica che sia stata chiamata notificaRichiestaAmmissione + expect(notifiche._getInvitante).toHaveBeenCalledWith(5); + }); + + test('Gestisce errori correttamente', async () => { + mockEmailService.sendMail.mockRejectedValueOnce(new Error('SMTP Error')); + + const utente = { + id: 1, + username: 'mario.rossi', + email: 'mario@example.com', + verified_email: false, + invitante_id: 5 + }; + + await expect( + notifiche.notificaRegistrazione(utente, 'token123') + ).rejects.toThrow(); + + expect(mockLogger.error).toHaveBeenCalled(); + }); + }); + + // ============================================ + // TEST: notificaRichiestaAmmissione + // ============================================ + + describe('notificaRichiestaAmmissione', () => { + + test('Invia notifica email e telegram all\'invitante', async () => { + const utente = { + id: 1, + username: 'mario.rossi', + email: 'mario@example.com', + name: 'Mario Rossi', + invitante_id: 5, + created_at: new Date() + }; + + const result = await notifiche.notificaRichiestaAmmissione(utente); + + // Verifica chiamate + expect(notifiche._getInvitante).toHaveBeenCalledWith(5); + expect(mockEmailService.sendMail).toHaveBeenCalled(); + expect(mockTelegramBot.sendMessage).toHaveBeenCalled(); + expect(result.success).toBe(true); + }); + + test('Gestisce invitante non trovato', async () => { + notifiche._getInvitante.mockResolvedValueOnce(null); + + const utente = { + id: 1, + username: 'mario.rossi', + invitante_id: 999 + }; + + await expect( + notifiche.notificaRichiestaAmmissione(utente) + ).rejects.toThrow('Invitante non trovato'); + }); + + test('Invia notifica anche se Telegram fallisce', async () => { + mockTelegramBot.sendMessage.mockRejectedValueOnce(new Error('Telegram Error')); + + const utente = { + id: 1, + username: 'mario.rossi', + email: 'mario@example.com', + invitante_id: 5, + created_at: new Date() + }; + + const result = await notifiche.notificaRichiestaAmmissione(utente); + + // Email dovrebbe essere comunque inviata + expect(mockEmailService.sendMail).toHaveBeenCalled(); + expect(mockLogger.error).toHaveBeenCalledWith( + expect.stringContaining('Errore invio Telegram'), + expect.any(Error) + ); + }); + }); + + // ============================================ + // TEST: notificaUtenteAmmesso + // ============================================ + + describe('notificaUtenteAmmesso', () => { + + test('Invia email di benvenuto all\'utente', async () => { + const utente = { + id: 1, + username: 'mario.rossi', + email: 'mario@example.com', + name: 'Mario Rossi', + teleg_id: 0 + }; + + const result = await notifiche.notificaUtenteAmmesso(utente); + + expect(mockEmailService.sendMail).toHaveBeenCalled(); + expect(result.success).toBe(true); + + // Verifica oggetto email + const emailCall = mockEmailService.sendMail.mock.calls[0][0]; + expect(emailCall.subject).toContain('Benvenuto'); + }); + + test('Invia anche Telegram se utente ha teleg_id', async () => { + const utente = { + id: 1, + username: 'mario.rossi', + email: 'mario@example.com', + name: 'Mario Rossi', + teleg_id: 123456789 + }; + + await notifiche.notificaUtenteAmmesso(utente); + + expect(mockTelegramBot.sendMessage).toHaveBeenCalledWith( + 123456789, + expect.stringContaining('Benvenuto'), + expect.any(Object) + ); + }); + + test('Salta Telegram se teleg_id = 0', async () => { + const utente = { + id: 1, + username: 'mario.rossi', + email: 'mario@example.com', + teleg_id: 0 + }; + + await notifiche.notificaUtenteAmmesso(utente); + + // Solo email, no Telegram + expect(mockTelegramBot.sendMessage).not.toHaveBeenCalled(); + }); + }); + + // ============================================ + // TEST: notificaProfiloCompletato + // ============================================ + + describe('notificaProfiloCompletato', () => { + + test('Invia notifica se Telegram è verificato', async () => { + const utente = { + id: 1, + username: 'mario.rossi', + email: 'mario@example.com', + name: 'Mario Rossi', + teleg_id: 123456789, + invitante_id: 5 + }; + + const result = await notifiche.notificaProfiloCompletato(utente); + + expect(notifiche._getInvitante).toHaveBeenCalledWith(5); + expect(mockEmailService.sendMail).toHaveBeenCalled(); + expect(mockTelegramBot.sendMessage).toHaveBeenCalled(); + expect(result.success).toBe(true); + }); + + test('Non invia se Telegram non verificato', async () => { + const utente = { + id: 1, + username: 'mario.rossi', + email: 'mario@example.com', + teleg_id: 0, + invitante_id: 5 + }; + + const result = await notifiche.notificaProfiloCompletato(utente); + + expect(result.success).toBe(false); + expect(result.message).toContain('Telegram non ancora verificato'); + expect(mockEmailService.sendMail).not.toHaveBeenCalled(); + }); + }); + + // ============================================ + // TEST: gestisciEvento + // ============================================ + + describe('gestisciEvento', () => { + + test('Gestisce evento REGISTRAZIONE', async () => { + const spy = jest.spyOn(notifiche, 'notificaRegistrazione'); + + await notifiche.gestisciEvento('REGISTRAZIONE', { + utente: { username: 'test' }, + tokenVerifica: 'token123' + }); + + expect(spy).toHaveBeenCalled(); + }); + + test('Gestisce evento EMAIL_VERIFICATA', async () => { + const spy = jest.spyOn(notifiche, 'notificaRichiestaAmmissione'); + + await notifiche.gestisciEvento('EMAIL_VERIFICATA', { + utente: { username: 'test', invitante_id: 5 } + }); + + expect(spy).toHaveBeenCalled(); + }); + + test('Gestisce evento UTENTE_AMMESSO', async () => { + const spy = jest.spyOn(notifiche, 'notificaUtenteAmmesso'); + + await notifiche.gestisciEvento('UTENTE_AMMESSO', { + utente: { username: 'test' } + }); + + expect(spy).toHaveBeenCalled(); + }); + + test('Gestisce evento PROFILO_COMPLETATO solo se Telegram verificato', async () => { + const spy = jest.spyOn(notifiche, 'notificaProfiloCompletato'); + + // Con Telegram verificato + await notifiche.gestisciEvento('PROFILO_COMPLETATO', { + utente: { username: 'test', teleg_id: 123, invitante_id: 5 } + }); + + expect(spy).toHaveBeenCalled(); + + spy.mockClear(); + + // Senza Telegram + const result = await notifiche.gestisciEvento('PROFILO_COMPLETATO', { + utente: { username: 'test', teleg_id: 0 } + }); + + expect(spy).not.toHaveBeenCalled(); + expect(result.success).toBe(false); + }); + + test('Lancia errore per evento non riconosciuto', async () => { + await expect( + notifiche.gestisciEvento('EVENTO_INESISTENTE', {}) + ).rejects.toThrow('Evento non riconosciuto'); + }); + }); + + // ============================================ + // TEST: isProfiloCompleto + // ============================================ + + describe('isProfiloCompleto', () => { + + test('Ritorna true se profilo completo', () => { + const utente = { + name: 'Mario Rossi', + email: 'mario@example.com', + teleg_id: 123456789 + }; + + expect(notifiche.isProfiloCompleto(utente)).toBe(true); + }); + + test('Ritorna false se manca name', () => { + const utente = { + email: 'mario@example.com', + teleg_id: 123456789 + }; + + expect(notifiche.isProfiloCompleto(utente)).toBe(false); + }); + + test('Ritorna false se manca email', () => { + const utente = { + name: 'Mario Rossi', + teleg_id: 123456789 + }; + + expect(notifiche.isProfiloCompleto(utente)).toBe(false); + }); + + test('Ritorna false se Telegram non verificato', () => { + const utente = { + name: 'Mario Rossi', + email: 'mario@example.com', + teleg_id: 0 + }; + + expect(notifiche.isProfiloCompleto(utente)).toBe(false); + }); + }); + + // ============================================ + // TEST: Metodi Privati + // ============================================ + + describe('Metodi privati', () => { + + test('_inviaEmail invia email correttamente', async () => { + const result = await notifiche._inviaEmail( + 'test@example.com', + 'Test Subject', + '

Test HTML

' + ); + + expect(result.success).toBe(true); + expect(mockEmailService.sendMail).toHaveBeenCalledWith( + expect.objectContaining({ + to: 'test@example.com', + subject: 'Test Subject', + html: '

Test HTML

' + }) + ); + }); + + test('_inviaTelegram invia messaggio correttamente', async () => { + const result = await notifiche._inviaTelegram( + 123456789, + 'Test message' + ); + + expect(result.success).toBe(true); + expect(mockTelegramBot.sendMessage).toHaveBeenCalledWith( + 123456789, + 'Test message', + expect.any(Object) + ); + }); + + test('_inviaTelegram gestisce ID non valido', async () => { + const result = await notifiche._inviaTelegram(0, 'Test'); + + expect(result.success).toBe(false); + expect(result.error).toContain('non valido'); + expect(mockLogger.warn).toHaveBeenCalled(); + }); + + test('_inviaCopiaCopiaAdmin invia a email e telegram admin', async () => { + await notifiche._inviaCopiaCopiaAdmin('Test', 'Message'); + + expect(mockEmailService.sendMail).toHaveBeenCalledWith( + expect.objectContaining({ + to: 'admin@riso.app', + subject: '[ADMIN] Test' + }) + ); + + expect(mockTelegramBot.sendMessage).toHaveBeenCalledWith( + '999999999', + expect.stringContaining('NOTIFICA ADMIN'), + expect.any(Object) + ); + }); + }); +}); + +// ============================================ +// TEST: Integrazione +// ============================================ + +describe('Test di Integrazione', () => { + let notifiche; + + beforeEach(() => { + notifiche = new InvioNotifiche({ + emailService: mockEmailService, + telegramBot: mockTelegramBot, + adminTelegramId: '999999999', + adminEmail: 'admin@riso.app', + baseUrl: 'https://riso.app', + nomeApp: 'RISO', + emailTemplates: {}, + logger: mockLogger + }); + + notifiche._getInvitante = jest.fn().mockResolvedValue({ + id: 5, + username: 'invitante', + email: 'invitante@example.com', + teleg_id: 111111111 + }); + }); + + test('Flusso completo: Registrazione → Verifica → Ammissione → Profilo', async () => { + const utente = { + id: 1, + username: 'mario.rossi', + email: 'mario@example.com', + name: 'Mario Rossi', + verified_email: false, + invitante_id: 5, + teleg_id: 0, + created_at: new Date() + }; + + // 1. Registrazione + await notifiche.notificaRegistrazione(utente, 'token123'); + expect(mockEmailService.sendMail).toHaveBeenCalled(); + + // 2. Verifica email + utente.verified_email = true; + await notifiche.notificaRichiestaAmmissione(utente); + expect(notifiche._getInvitante).toHaveBeenCalled(); + + // 3. Ammissione + await notifiche.notificaUtenteAmmesso(utente); + expect(mockEmailService.sendMail).toHaveBeenCalled(); + + // 4. Profilo completato + utente.teleg_id = 123456789; + await notifiche.notificaProfiloCompletato(utente); + expect(mockTelegramBot.sendMessage).toHaveBeenCalled(); + + // Verifica che tutte le notifiche admin siano state inviate + const adminCalls = mockEmailService.sendMail.mock.calls.filter( + call => call[0].subject.includes('[ADMIN]') + ); + expect(adminCalls.length).toBeGreaterThan(0); + }); +}); + +// Configurazione package.json per Jest +/* +{ + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage" + }, + "jest": { + "testEnvironment": "node", + "coveragePathIgnorePatterns": ["/node_modules/"], + "testMatch": ["**/__tests__/**/*.js", "**/?(*.)+(spec|test).js"] + } +} +*/ diff --git a/src/router/admin_router.js b/src/router/admin_router.js index b49a39d..e999fcb 100755 --- a/src/router/admin_router.js +++ b/src/router/admin_router.js @@ -2303,7 +2303,7 @@ router.post('/exec', authenticate, async (req, res) => { if (!User.isAdmin(req.user.perm) || tokcheck !== TOKCHECK) { // If without permissions, exit - return res.status(404).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: '' }); } let result = ''; @@ -2335,7 +2335,7 @@ router.post('/cloudflare', authenticate, async (req, res) => { if (!User.isAdmin(req.user.perm) || tokcheck !== TOKCHECK) { // If without permissions, exit - return res.status(404).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: '' }); } let result = ''; @@ -2403,7 +2403,7 @@ router.post('/miab', authenticate, async (req, res) => { if (!User.isAdmin(req.user.perm) || tokcheck !== TOKCHECK) { // If without permissions, exit - return res.status(404).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: '' }); } let result = ''; diff --git a/src/router/dashboard_router.js b/src/router/dashboard_router.js index bead09b..977c188 100755 --- a/src/router/dashboard_router.js +++ b/src/router/dashboard_router.js @@ -24,7 +24,7 @@ router.post('/', authenticate, async (req, res) => { if ((!User.isAdmin(req.user.perm) && !User.isManager(req.user.perm)) && (username) !== req.user.username) { // If without permissions, exit - return res.status(404).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: '' }); } let aportador_solidario = req.user.aportador_solidario; let aportador_solidario_nome_completo = req.user.aportador_solidario_nome_completo; @@ -55,7 +55,7 @@ router.post('/downline', authenticate, async (req, res) => { if ((!User.isAdmin(req.user.perm) && !User.isManager(req.user.perm)) && (username) !== req.user.username) { // If without permissions, exit - return res.status(404).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: '' }); } let aportador_solidario = req.user.aportador_solidario; let aportador_solidario_nome_completo = req.user.aportador_solidario_nome_completo; diff --git a/src/router/index_router.js b/src/router/index_router.js index 13392fa..fe12bdf 100755 --- a/src/router/index_router.js +++ b/src/router/index_router.js @@ -334,7 +334,7 @@ router.post('/sendmailreg', authenticate, async (req, res) => { const diffTime = Math.abs(now - user.lasttime_email_sent_verify); const diffMinutes = Math.ceil(diffTime / (1000 * 60)); - if (diffMinutes < 1) { + if (diffMinutes < 5) { return res.status(200).send({ email_inviata: false, error: 'Attendi qualche minuto prima di reinviare nuovamente.' }); } ris = await sendemail.sendEmail_Registration(user.lang, user.email, user, user.idapp, user.linkreg); @@ -396,7 +396,7 @@ router.post('/settable', authenticate, async (req, res) => { (await !tools.ModificheConsentite(req, params.table, fieldsvalue, mydata ? mydata._id : '')) ) { // If without permissions, exit - return res.status(404).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: '' }); } if (shared_consts.TABLES_USER_ID.includes(params.table)) { @@ -827,7 +827,7 @@ router.post('/getexp', authenticate, (req, res) => { if (!User.isAdmin(req.user.perm) && !User.isManager(req.user.perm) && !User.isFacilitatore(req.user.perm)) { // If without permissions, exit - return res.status(404).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 { @@ -1334,7 +1334,7 @@ router.patch('/chval', authenticate, async (req, res) => { !(mydata.table === 'accounts' && (await Account.canEditAccountAdmins(req.user.username, mydata.id))) ) { // If without permissions, exit - return res.status(404).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: '' }); } const camporequisiti = UserCost.FIELDS_REQUISITI.includes(Object.keys(fieldsvalue)[0]); @@ -1617,7 +1617,7 @@ router.patch('/askfunz', authenticate, async (req, res) => { req.user._id.toString() !== id ) { // If without permissions, exit - return res.status(404).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: '' }); } } @@ -1676,7 +1676,7 @@ router.patch('/callfunz', authenticate, async (req, res) => { req.user._id.toString() !== id ) { // If without permissions, exit - return res.status(404).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: '' }); } } @@ -1755,7 +1755,7 @@ router.delete('/delrec/:table/:id', authenticate, async (req, res) => { (await !tools.ModificheConsentite(req, tablename, fields, id, req.user)) ) { // If without permissions, exit - return res.status(404).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: '' }); } let cancellato = false; @@ -1863,7 +1863,7 @@ router.post('/duprec/:table/:id', authenticate, async (req, res) => { const mytable = globalTables.getTableByTableName(tablename); if (!req.user) { - return res.status(404).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: '' }); } /* if (!User.isAdmin(req.user.perm) && !User.isManager(req.user.perm)) { @@ -1902,7 +1902,7 @@ router.post('/duprec/:table/:id', authenticate, async (req, res) => { router.get('/loadsite/:userId/:idapp', authenticate_noerror_WithUserLean, async (req, res) => { try { - // if ((req.statuscode2 = server_constants.RIS_CODE_HTTP_FORBIDDEN_TOKEN_EXPIRED)) { + // if ((req.statuscode2 = server_constants.RIS_CODE_HTTP_TOKEN_EXPIRED)) { // return; // } @@ -2031,8 +2031,8 @@ async function load(req, res, version = '0') { // Estrazione e validazione degli input const userId = req.user ? req.user._id.toString() : req.params.userId || '0'; const idapp = req.params.idapp; - /*const status = req.code === server_constants.RIS_CODE_HTTP_FORBIDDEN_TOKEN_EXPIRED - ? server_constants.RIS_CODE_HTTP_FORBIDDEN_TOKEN_EXPIRED + /*const status = req.code === server_constants.RIS_CODE_HTTP_TOKEN_EXPIRED + ? server_constants.RIS_CODE_HTTP_TOKEN_EXPIRED : 200;*/ let status = req.code; @@ -2251,11 +2251,10 @@ async function load(req, res, version = '0') { } } -router.get(process.env.LINK_CHECK_UPDATES, authenticate_noerror, async (req, res) => { +router.get('/checkupdates', authenticate_noerror, async (req, res) => { try { const idapp = req.query.idapp; - // console.log("POST " + process.env.LINK_CHECK_UPDATES + " userId=" + userId); if (!req.user) { if (req.code === 1) return res.status(200).send(); else return res.status(req.code).send(); diff --git a/src/router/newsletter_router.js b/src/router/newsletter_router.js index cf40070..a1e815b 100755 --- a/src/router/newsletter_router.js +++ b/src/router/newsletter_router.js @@ -391,7 +391,7 @@ router.post('/testemail', authenticate, async (req, res) => { if (!User.isAdmin(req.user.perm)) { // If without permissions, exit - return res.status(404).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: '' }); } idapp = req.body.idapp; diff --git a/src/router/users_router.js b/src/router/users_router.js index 442fbf2..3e78cb7 100755 --- a/src/router/users_router.js +++ b/src/router/users_router.js @@ -1,455 +1,196 @@ const express = require('express'); const router = express.Router(); -const { User } = require('../models/user'); - -const ListaInvitiEmail = require('../models/listainvitiemail'); - -// const { Nave } = require('../models/nave'); -const Hours = require('../models/hours'); -//const { NavePersistente } = require('../models/navepersistente'); -//const { ListaIngresso } = require('../models/listaingresso'); -//const { Graduatoria } = require('../models/graduatoria'); -// const { ExtraList } = require('../models/extralist'); -const { ObjectId } = require('mongodb'); - -const sendemail = require('../sendemail'); - -const { Settings } = require('../models/settings'); -const CronMod = require('../modules/CronMod'); - -const { SendNotif } = require('../models/sendnotif'); -const { MyElem } = require('../models/myelem'); - -const { MyBot } = require('../models/bot'); - -const tools = require('../tools/general'); -const shared_consts = require('../tools/shared_nodejs'); - -const server_constants = require('../tools/server_constants'); - -const telegrambot = require('../telegram/telegrambot'); - -const _ = require('lodash'); - -const reg = require('../reg/registration'); - +const UserController = require('../controllers/UserController'); const { authenticate, authenticate_noerror, authenticate_withUser } = require('../middleware/authenticate'); +const { checkBlocked } = require('../middleware/securityMiddleware'); -const Cart = require('../models/cart'); -const CartClass = require('../modules/Cart'); -const Product = require('../models/product'); -const ProductInfo = require('../models/productInfo'); -const CatProd = require('../models/catprod'); -const SubCatProd = require('../models/subcatprod'); -const Order = require('../models/order'); -const OrdersCart = require('../models/orderscart'); -const Variant = require('../models/variant'); -const TypedError = require('../modules/ErrorHandler'); +// Initialize controller +const userController = new UserController(); -const { MyGroup } = require('../models/mygroup'); -const { Circuit } = require('../models/circuit'); -const { Province } = require('../models/province'); -const { City } = require('../models/city'); -const { Account } = require('../models/account'); +// ===== PUBLIC ROUTES ===== -const mongoose = require('mongoose').set('debug', false); +/** + * Register new user + * POST /users + */ +router.post('/', (req, res) => userController.register(req, res)); -const Subscription = require('../models/subscribers'); -const Macro = require('../modules/Macro'); +/** + * Check if username exists + * GET /users/:idapp/:username + */ +router.get('/:idapp/:username', (req, res) => userController.checkUsername(req, res)); -async function existSubScribe(userId, access, browser) { +/** + * User login + * POST /users/login + */ +router.post('/login', checkBlocked, (req, res) => userController.login(req, res)); + +/** + * Refresh authentication token + * POST /users/newtok + */ +router.post('/newtok', (req, res) => userController.refreshToken(req, res)); + +/** + * Get user activities (public profile) + * POST /users/activities + */ +router.post('/activities', authenticate_noerror, (req, res) => + userController.getProfile(req, res) +); + +// ===== AUTHENTICATED ROUTES ===== + +/** + * Get user profile + * POST /users/profile + */ +router.post('/profile', authenticate, (req, res) => + userController.getProfile(req, res) +); + +/** + * Get user panel info (admin/manager only) + * POST /users/panel + */ +router.post('/panel', authenticate, (req, res) => { + const { User } = require('../models/user'); + const server_constants = require('../tools/server_constants'); + + if (!req.user || (!User.isAdmin(req.user.perm) && + !User.isManager(req.user.perm) && + !User.isFacilitatore(req.user.perm))) { + return res.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED).send({ + code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, + msg: '' + }); + } + + userController.getProfile(req, res); +}); + +/** + * Update user balance + * POST /users/updatesaldo + */ +router.post('/updatesaldo', authenticate, (req, res) => + userController.updateSaldo(req, res) +); + +/** + * Get user's friends + * POST /users/friends + */ +router.post('/friends', authenticate, (req, res) => + userController.getFriends(req, res) +); + +/** + * Execute friend command + * POST /users/friends/cmd + */ +router.post('/friends/cmd', authenticate, (req, res) => + userController.executeFriendCommand(req, res) +); + +/** + * Send command to user + * POST /users/sendcmd + */ +router.post('/sendcmd', authenticate, (req, res) => { + const usernameLogged = req.user.username; + const { idapp, usernameOrig, usernameDest, cmd, value } = req.body; + + userController.userService.sendCommand( + req, idapp, usernameOrig, usernameDest, cmd, value + ).then(result => res.send(result)) + .catch(error => res.status(400).send({ error: error.message })); +}); + +/** + * Get user's groups + * POST /users/groups + */ +router.post('/groups', authenticate, (req, res) => + userController.getGroups(req, res) +); + +/** + * Execute group command + * POST /users/groups/cmd + */ +router.post('/groups/cmd', authenticate, (req, res) => { + const usernameLogged = req.user.username; + const { idapp, usernameOrig, groupnameDest, cmd, value } = req.body; + + userController.userService.executeGroupCommand( + idapp, usernameOrig, groupnameDest, cmd, value, usernameLogged + ).then(result => res.send(result)) + .catch(error => res.status(400).send({ error: error.message })); +}); + +/** + * Get user's circuits + * POST /users/circuits + */ +router.post('/circuits', authenticate_withUser, (req, res) => + userController.getCircuits(req, res) +); + +/** + * Execute circuit command + * POST /users/circuits/cmd + */ +router.post('/circuits/cmd', authenticate, async (req, res) => { + const usernameLogged = req.user.username; + const { idapp, usernameOrig, circuitname, cmd, value, extrarec } = req.body; + try { - const itemsub = await Subscription.findOne({ userId, access, browser }).lean(); - return itemsub; - } catch (err) { - return null; - } -} - -function getMobileComplete(user) { - let str = user.profile.intcode_cell + user.profile.cell; - str = str.replace(/\s+/g, ''); - // str = str.replace(/.+/g, ''); - // str = str.replace(/-+/g, ''); - - return str; -} - -router.post('/test1', async (req, res) => { - const user = await User.findOne({ - idapp: 1, - username: 'paoloar77', - }); - - await sendemail.sendEmail_Registration(user.lang, user.email, user, user.idapp, user.linkreg); -}); - -// POST /users -router.post('/', async (req, res) => { - try { - tools.mylog('POST /users'); - const body = _.pick(req.body, [ - 'email', - 'password', - 'username', - 'group', - 'name', - 'surname', - 'idapp', - 'keyappid', - 'lang', - 'profile', - 'aportador_solidario', - ]); - body.email = body.email.toLowerCase(); - - const user = new User(body); - user.ipaddr = tools.getiPAddressUser(req); - - user.email = user.email.trim(); - user.username = user.username.trim(); - user.name = user.name.trim(); - user.surname = user.surname.trim(); - - if (user.aportador_solidario === 'tuo_username' || user.aportador_solidario === '{username}') { - user.aportador_solidario = 'surya1977'; - } - - // tools.mylog("LANG PASSATO = " + user.lang, "IDAPP", user.idapp); - - if ( - !tools.isAlphaNumericAndSpecialCharacter(body.username) || - body.email.length < 6 || - body.username.length < 4 || - body.password.length < 5 - ) { - await tools.snooze(5000); - console.log('Username non valido in Registrazione: ' + body.username); - res.status(400).send({ code: server_constants.RIS_CODE_USERNAME_NOT_VALID, msg: '' }); - return 1; - } - - if (tools.blockwords(body.username) || tools.blockwords(body.name) || tools.blockwords(body.surname)) { - // tools.writeIPToBan(user.ipaddr + ': [' + user.username + '] ' + user.name + ' ' + user.surname); - await tools.snooze(5000); - return res.status(404).send(); - } - - user.linkreg = reg.getlinkregByEmail(body.idapp, body.email, body.username); - user.verified_email = false; - - // Se è parte di un invito allora verified_email = true - const recinvito = await ListaInvitiEmail.findOne({ email: body.email }); - if (recinvito) { - user.verified_email = true; - - recinvito.registered = true; - recinvito.userIdRegistered = user._id; - await recinvito.save(); - } - - user.lasttimeonline = new Date(); - user.date_reg = new Date(); - user.aportador_iniziale = user.aportador_solidario; - - let regexpire = req.body['regexpire']; - let nonchiedereverifica = false; - if (regexpire) { - nonchiedereverifica = await User.getifRegTokenIsValid(body.idapp, regexpire); - } - - if (!nonchiedereverifica) regexpire = ''; - - if (!tools.getAskToVerifyReg(body.idapp) || nonchiedereverifica) { - // Se non devo chiedere di verificare all'Invitato, allora lo verifico direttamente - user.verified_by_aportador = true; - } - - /* if (user.idapp === tools.AYNI) { - user.profile.paymenttypes = ['paypal']; - } */ - - // Controlla se anche l'ultimo record era dallo stesso IP: - const lastrec = await User.getLastRec(body.idapp); - if (!!lastrec) { - if (process.env.LOCALE !== '1') { - if (lastrec.ipaddr === user.ipaddr) { - // Se l'ha fatto troppo ravvicinato - if (lastrec.date_reg) { - let ris = tools.isdiffSecDateLess(lastrec.date_reg, 3); - if (ris) { - const msg = user.ipaddr + ': [' + user.username + '] ' + user.name + ' ' + user.surname; - tools.writeIPToBan(msg); - - await User.findOneAndUpdate({ _id: user._id }, { $set: { banIp: true } }); - - await telegrambot.sendMsgTelegramToTheAdmin(body.idapp, '‼️ BAN: ' + msg, true); - - await tools.snooze(5000); - res.status(400).send({ code: server_constants.RIS_CODE_BANIP, msg: '' }); - return 1; - } - } - } - } - } - - // user.perm = 3; - // if (tools.testing()) { - // user.verified_email = true; - // } - - // if (user.profile.intcode_cell) { - // if (user.profile.cell.substring(0, user.profile.intcode_cell.length) === user.profile.intcode_cell) { - // user.profile.cell = user.profile.cell.substring(user.profile.intcode_cell.length) - // } - // } - let exit; - - let utentenonancoraVerificato = false; - - const trovarec = await User.findByCredentials(user.idapp, user.username, user.password, true); - - // Check if already esist email or username - exit = await User.findByUsername(user.idapp, user.username).then((useralreadyexist) => { - if (useralreadyexist) { - if (tools.getAskToVerifyReg(useralreadyexist.idapp)) { - if (!useralreadyexist.verified_by_aportador && useralreadyexist.profile.teleg_id > 0) { - if (trovarec) { - utentenonancoraVerificato = true; - } - } - } - - if (!utentenonancoraVerificato) { - res.status(400).send({ - code: server_constants.RIS_CODE_USERNAME_ALREADY_EXIST, - msg: '', - }); - return 1; - } - } - }); - - if (!utentenonancoraVerificato) { - if (exit === 1) return; - - exit = await User.findByEmail(user.idapp, user.email).then((useralreadyexist) => { - if (useralreadyexist) { - res.status(400).send({ - code: server_constants.RIS_CODE_EMAIL_ALREADY_EXIST, - msg: '', - }); - return 1; - } - }); - - if (exit === 1) return; - - let recuser = null; - - recuser = await User.findByCellAndNameSurname(user.idapp, user.profile.cell, user.name, user.surname); - if (recuser && user.name !== '' && user.surname !== '' && user.profile.cell !== '') { - console.log('UTENTE GIA ESISTENTE:\n'); - console.log(user); - // User already registered! - res.status(400).send({ code: server_constants.RIS_CODE_USER_ALREADY_EXIST, msg: '' }); - return 1; - } - } - - let recextra = null; - - user.aportador_solidario = user.aportador_solidario.trim(); - - user.aportador_solidario = user.aportador_solidario.replace('@', ''); - - let id_aportador = await User.getIdByUsername(user.idapp, user.aportador_solidario); - if (!id_aportador) { - // Cerca se esiste l'aportador solidario con l'username Telegram - const useraportador = await User.getUserByUsernameTelegram(user.idapp, user.aportador_solidario); - if (useraportador) { - id_aportador = useraportador._id; - user.aportador_solidario = useraportador.username; - } - } - - let idMyGroupSite = tools.getidMyGroupBySite(body.idapp); - user.idMyGroup = idMyGroupSite ? idMyGroupSite : ''; - - if (id_aportador) { - // Ottiene l'username "corretto" (senza maiuscole o minuscole) - user.aportador_solidario = await User.getRealUsernameByUsername(user.idapp, user.aportador_solidario); - } - - if (!id_aportador && tools.getAskToVerifyReg(body.idapp)) { - // Si sta tentando di registrare una persona sotto che non corrisponde! - let msg = - 'Il link di registrazione non sembra risultare valido.
invitante: ' + - user.aportador_solidario + - '
username: ' + - user.username; - - await telegrambot.sendMsgTelegramToTheManagers(user.idapp, msg); - res.status(400).send({ - code: server_constants.RIS_CODE_USER_APORTADOR_NOT_VALID, - msg: '', - }); - return 1; - } - - if (utentenonancoraVerificato) { - if (id_aportador) { - // Se mi sono registrato ma l'invitante non mi abilita, allora il posso registrarmi nuovamente, con lo stesso username e password, - // con un'altro link di un'altro invitante ! - await User.setaportador_solidario(user.idapp, user.username, user.aportador_solidario); - - const myuser = await User.findOne({ _id: trovarec._id }); - if (myuser) { - await telegrambot.askConfirmationUser(myuser.idapp, shared_consts.CallFunz.REGISTRATION, myuser); - - const { token, refreshToken } = await myuser.generateAuthToken(req); - res.header('x-auth', token).header('x-refrtok', refreshToken).send(myuser); - return true; - } - } - } - - // let already_registered = (recextra || user.aportador_solidario === tools.APORTADOR_NONE) && (user.idapp === tools.AYNI); - - // Check if is an other people aportador_solidario - - /*if (already_registered) { - // Check in the extraList if is present! - const msg = 'Utente non trovato: ' + user.name + ' ' + user.surname + ' ' + user.profile.nationality + ' ' + user.profile.cell + ' email: ' + user.email + ' username: ' + user.username; - console.log('Utente non trovato; ', msg); - await telegrambot.sendMsgTelegramToTheManagers(user.idapp, msg); - res.status(400).send({ - code: server_constants.RIS_CODE_USER_EXTRALIST_NOTFOUND, - msg: 'Controlla se il numero ' + user.profile.cell + ' è corretto.' - }); - return 1; - } */ - - return user - .save() - .then(async () => { - return User.findByUsername(user.idapp, user.username, false) - .then((usertrovato) => { - // tools.mylog("TROVATO USERNAME ? ", user.username, usertrovato); - if (usertrovato !== null) { - return user.generateAuthToken(req); - } else { - res.status(400).send(); - return 0; - } - }) - .then(async (ris) => { - // tools.mylog("passo il TOKEN: ", token); - - if (recextra) { - recextra.registered = true; - recextra.username = user.username; - await recextra.save(); - - // await User.fixUsername(user.idapp, user.ind_order, user.username); - } - return ris; - }) - .then(async (ris) => { - // tools.mylog("LINKREG = " + user.linkreg); - // Invia un'email all'utente - // tools.mylog('process.env.TESTING_ON', process.env.TESTING_ON); - console.log('res.locale', res.locale); - - await telegrambot.askConfirmationUser( - user.idapp, - shared_consts.CallFunz.REGISTRATION, - user, - '', - '', - '', - '', - regexpire - ); - - // 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).send(user); - return true; - }); - }) - .catch((e) => { - console.error(e.message); - res.status(400).send(e); - }); - } catch (e) { - console.error('Error: /users REG: ' + e.message); + const result = await userController.userService.executeCircuitCommand( + idapp, usernameOrig, circuitname, cmd, value, usernameLogged, extrarec + ); + res.send(result); + } catch (error) { + res.status(400).send({ error: error.message }); } }); -router.get('/:idapp/:username', async (req, res) => { - var username = req.params.username; - const idapp = req.params.idapp; +/** + * Logout user + * DELETE /users/me/token + */ +router.delete('/me/token', authenticate_withUser, (req, res) => + userController.logout(req, res) +); - // if (username === 'pippo') { - // return res.status(200).send(); - // } - - await User.findByUsername(idapp, username, false, true) - .then(async (user) => { - if (!user) { - user = await User.findByUsernameTelegram(idapp, username, false, true); - if (!user) return res.status(404).send(); - } - // console.log('TROVATO!') - return res.status(200).send(); - }) - .catch((e) => { - return res.status(400).send(); - }); -}); - -router.patch('/:id', authenticate, (req, res) => { - const id = req.params.id; - const body = _.pick(req.body.user, shared_consts.fieldsUserToChange()); - - tools.mylogshow('PATCH USER: ', id); - - if (!User.isAdmin(req.user.perm)) { - // If without permissions, exit - return res.status(404).send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' }); - } - - User.findByIdAndUpdate(id, { $set: body }) - .then((user) => { - tools.mylogshow(' USER TO MODIFY: ', user); - if (!user) { - return res.status(404).send(); - } else { - res.send({ code: server_constants.RIS_CODE_OK, msg: '' }); - } - }) - .catch((e) => { - tools.mylogserr('Error patch USER: ', e); - res.status(400).send(); - }); -}); +/** + * Set user permissions + * POST /users/setperm + */ +router.post('/setperm', authenticate, (req, res) => + userController.setPermissions(req, res) +); +/** + * Get last movements/transactions + * POST /users/lastmovs + */ router.post('/lastmovs', authenticate, async (req, res) => { - const nummov = req.body.nummov; - const nummov_uscita = req.body.nummov_uscita; - const idapp = req.body.idapp; - + const { nummov, nummov_uscita, idapp } = req.body; + const server_constants = require('../tools/server_constants'); + const tools = require('../tools/general'); + try { const { Movement } = require('../models/movement'); - + + let last_transactions = []; if (nummov) { last_transactions = await Movement.getLastN_Transactions(idapp, nummov, nummov_uscita); } - + res.send({ code: server_constants.RIS_CODE_OK, last_transactions }); } catch (e) { tools.mylogserr('Error lastmovs: ', e); @@ -457,866 +198,178 @@ router.post('/lastmovs', authenticate, async (req, res) => { } }); -router.post('/receiveris', authenticate, (req, res) => { - const username = req.user ? req.user.username : ''; - const groupname = req.body.groupname; - const idapp = req.body.idapp; - +/** + * Set receive RIS flag + * POST /users/receiveris + */ +router.post('/receiveris', authenticate, async (req, res) => { + const username = req.user?.username || ''; + const { groupname, idapp } = req.body; + const { User } = require('../models/user'); + const { MyGroup } = require('../models/mygroup'); + const server_constants = require('../tools/server_constants'); + const tools = require('../tools/general'); + try { - if (!username) return res.send({ code: server_constants.RIS_CODE_ERR }); - - if (groupname) { - return MyGroup.setReceiveRisGroup(idapp, groupname) - .then((risult) => { - res.send({ code: server_constants.RIS_CODE_OK }); - }) - .catch((err) => { - tools.mylog('ERRORE IN receiveris: ' + err.message); - res.status(400).send(); - }); - } else if (username) { - return User.setReceiveRis(idapp, username) - .then((risult) => { - res.send({ code: server_constants.RIS_CODE_OK }); - }) - .catch((err) => { - tools.mylog('ERRORE IN receiveris: ' + err.message); - res.status(400).send(); - }); + if (!username) { + return res.send({ code: server_constants.RIS_CODE_ERR }); } - } catch (e) { - res.status(400).send(); - } -}); - -router.post('/listlinkreg', authenticate, (req, res) => { - const username = req.user ? req.user.username : ''; - const groupname = req.body.groupname; - const idapp = req.body.idapp; - - try { - if (!username) return res.send({ code: server_constants.RIS_CODE_ERR }); - - return User.setLinkReg(idapp, username) - .then((risult) => { - res.send({ code: server_constants.RIS_CODE_OK }); - }) - .catch((err) => { - tools.mylog('ERRORE IN listlinkreg: ' + err.message); - res.status(400).send(); - }); - } catch (e) { - res.status(400).send(); - } -}); - -router.post('/profile', authenticate, (req, res) => { - const usernameOrig = req.user ? req.user.username : ''; - const perm = req.user ? req.user.perm : tools.Perm.PERM_NONE; - const username = req.body['username']; - const idapp = req.body.idapp; - - //++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); - - return User.getUserProfileByUsername(idapp, username, usernameOrig, false, perm) - .then((ris) => { - return User.getFriendsByUsername(idapp, usernameOrig) - .then(async (friends) => { - if (username === usernameOrig) { - const userprofile = await User.getExtraInfoByUsername(idapp, ris.username); - ris.profile = userprofile; - } - - return { ris, friends }; - }) - .then((tot) => { - return res.send({ user: tot.ris, friends: tot.friends }); - }); - }) - .catch((e) => { - tools.mylog('ERRORE IN Profile: ' + e.message); - res.status(400).send(); - }); - } catch (e) { - tools.mylogserr('Error profile: ', e); - res.status(400).send(); - } -}); - -router.post('/activities', authenticate_noerror, (req, res) => { - const usernameOrig = req.user ? req.user.username : ''; - const perm = req.user ? req.user.perm : tools.Perm.PERM_NONE; - const username = req.body['username']; - const idapp = req.body.idapp; - const locale = req.body.locale; - - //++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); - - return User.getUserProfileByUsername(idapp, username, usernameOrig, false, perm) - .then((ris) => { - return User.getFriendsByUsername(idapp, usernameOrig) - .then(async (friends) => { - let userprofile = null; - if (req.user) { - userprofile = await User.getExtraInfoByUsername(idapp, ris.username); - } else { - userprofile = await User.getProfilePerActivitiesByUsername(idapp, ris.username); - ris.aportador_solidario = ''; - ris.date_reg = ''; - ris.email = ''; - } - ris.profile = userprofile; - - return { ris, friends }; - }) - .then((tot) => { - return res.send({ user: tot.ris, friends: tot.friends }); - }); - }) - .catch((e) => { - tools.mylog('ERRORE IN Profile: ' + e.message); - res.status(400).send(); - }); - } catch (e) { - tools.mylogserr('Error profile: ', e); - res.status(400).send(); - } -}); - -router.post('/panel', authenticate, async (req, res) => { - const username = req.body['username']; - 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 without permissions, exit - return res.status(404).send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' }); - } - - try { - const myuser = await User.findOne( - { idapp, username }, - { - username: 1, - name: 1, - surname: 1, - email: 1, - verified_by_aportador: 1, - aportador_solidario: 1, - lasttimeonline: 1, - deleted: 1, - sospeso: 1, - blocked: 1, - reported: 1, - username_who_report: 1, - date_report: 1, - profile: 1, - } - ).lean(); - if (!!myuser) { - res.send(myuser); - } else { - tools.mylog('ERRORE IN panel: '); - res.status(400).send(); - } - } catch (e) { - tools.mylogserr('Error profile: ', e); - res.status(400).send(); - } -}); - -router.post('/notifs', authenticate, async (req, res) => { - /* const notifs = req.body['notifs']; - idapp = req.body.idapp; - locale = req.body.locale; - - const myuser = req.user; - if (!myuser) { - return res.status(404). - send({code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: ''}); - } - - try { - if (!!myuser) { - if (tools.isArray(notifs) && notifs.length >= 0) { - myuser.profile.notifs = notifs; - myuser.save(); - return res.send({code: server_constants.RIS_CODE_OK, msg: ''}); - } - } - return res.send({code: server_constants.RIS_CODE_OK, msg: ''}); - } catch (e) { - tools.mylogserr('Error profile: ', e); - res.status(400).send(); - } - - */ -}); - -router.post('/newtok', async (req, res) => { - try { - const refreshToken = req.body.refreshToken; - - // return res.status(403).send({ error: 'Refresh token non valido' }); - - if (!refreshToken) { - return res.status(400).send({ error: 'Refresh token mancante' }); - } - - const recFound = await User.findByRefreshTokenAnyAccess(refreshToken); - if (!recFound) { - return res.status(403).send({ error: 'Refresh token non valido' }); - } - - const { token, refreshToken: newRefreshToken } = await recFound.generateAuthToken(req); - - return res.status(200).send({ - token, - refreshToken: newRefreshToken, - }); - } catch (e) { - console.error('Errore durante il refresh token:', e); - return res.status(500).send({ error: 'Errore interno del server' }); - } -}); - -// Dizionario per tenere traccia dei tentativi di accesso falliti per ogni utente -const failedLoginAttempts = {}; - -// Costante per il numero massimo di tentativi di accesso falliti prima del blocco -const MAX_FAILED_ATTEMPTS = 30; - -// Costante per la durata del blocco in millisecondi (ad esempio 30 minuti) -const BLOCK_DURATION = 30 * 60 * 1000; // 30 minuti - -// Funzione per bloccare un utente per un periodo di tempo dopo un numero specificato di tentativi falliti -function blockUser(username) { - failedLoginAttempts[username] = Date.now() + BLOCK_DURATION; -} - -// Middleware per controllare se l'utente è bloccato -function checkBlocked(req, res, next) { - const { username } = req.body; - const now = Date.now(); - - if (failedLoginAttempts[username] && failedLoginAttempts[username] > now) { - text = 'Utente bloccato. Riprova più tardi. (username=' + username + ')'; - console.log(text); - return res.status(403).json({ message: 'Utente bloccato. Riprova più tardi.' }); - } - - next(); -} - -router.post('/login', checkBlocked, async (req, res) => { - const body = _.pick(req.body, ['username', 'password', 'idapp', 'keyappid', 'lang']); - const userpass = new User(body); - // const subs = _.pick(req.body, ['subs']); - - // tools.mylog("LOG: u: " + user.username + " p:" + user.password); - - // tools.mylog("user REC:", user); - - if (body.keyappid !== process.env.KEY_APP_ID) return res.status(400).send(); - - let resalreadysent = false; - - try { - const user = await User.findByCredentials(userpass.idapp, userpass.username, userpass.password); - - if (!user) { - const rislogin = await User.tooManyLoginWrong(body.idapp, body.username, true); - - if (rislogin.troppilogin) { - let text = - 'Troppe richieste di Login ERRATE: ' + - body.username + - ' [IP: ' + - tools.getiPAddressUser(req) + - '] Tentativi: ' + - rislogin.retry_pwd; - telegrambot.sendMsgTelegramToTheManagers(body.idapp, text); - console.log('/login', text); - res.status(400).send({ code: server_constants.RIS_CODE_ERR, msg: text }); - return; - } - - await tools.snooze(2000); - - if (!failedLoginAttempts[body.username]) { - failedLoginAttempts[body.username] = 1; - } else { - failedLoginAttempts[body.username]++; - } - - let numvolteerrati = failedLoginAttempts[body.username]; - - if (numvolteerrati > 2) { - const msg = - 'Tentativo (' + - numvolteerrati + - ') di Login ERRATO [' + - body.username + - ' , ' + - ']\n' + - '[IP: ' + - tools.getiPAddressUser(req) + - ']'; - tools.mylogshow(msg); - await telegrambot.sendMsgTelegramToTheAdmin(req.body.idapp, msg, true); - tools.writeErrorLog(msg); - } - - if (failedLoginAttempts[body.username] >= MAX_FAILED_ATTEMPTS) { - blockUser(body.username); - text = - 'Troppi tentativi di accesso falliti. Utente bloccato (' + - body.username + - ')' + - ' [IP: ' + - tools.getiPAddressUser(req) + - ']'; - tools.mylogshow(text); - telegrambot.sendMsgTelegramToTheManagers(req.body.idapp, text); - res.status(403).json({ message: text }); - resalreadysent = true; - } - - return res.status(401).send({ code: server_constants.RIS_CODE_LOGIN_ERR }); + if (groupname) { + await MyGroup.setReceiveRisGroup(idapp, groupname); } else { - const myris = await user.generateAuthToken(req); - - const usertosend = new User(); - - shared_consts.fieldsUserToChange().forEach((field) => { - usertosend[field] = user[field]; - }); - - const subsExistonDb = await existSubScribe(usertosend._id, 'auth', req.get('User-Agent')); - - res.header('x-auth', myris.token).header('x-refrtok', myris.refreshToken).send({ - usertosend, - code: server_constants.RIS_CODE_OK, - subsExistonDb, - }); + await User.setReceiveRis(idapp, username); } - } catch (e) { - console.error('ERRORE IN LOGIN: ' + e.message); - if (!resalreadysent) res.status(400).send({ code: server_constants.RIS_CODE_LOGIN_ERR_GENERIC, msgerr: e.message }); - } -}); - -router.delete('/me/token', authenticate_withUser, (req, res) => { - // tools.mylog("TOKENREM = " + req.token); - try { - req.user.removeToken(req.token).then( - () => { - res.status(200).send(); - }, - () => { - res.status(400).send(); - } - ); - } catch (e) { - console.log('delete(/me/token', e.message); - } -}); - -router.post('/setperm', authenticate, (req, res) => { - const body = _.pick(req.body, ['idapp', 'username', 'perm']); - tools.mylog('SETPERM = ' + req.token); - - User.setPermissionsById(req.user._id, body).then( - () => { - res.status(200).send(); - }, - () => { - res.status(400).send(); - } - ); -}); - -router.post('/import_extralist', async (req, res) => { - const strdata = req.body.strdata; - idapp = req.body.idapp; - locale = req.body.locale; - - // const ris = await ExtraList.ImportData(locale, idapp, strdata); - console.log('ris', ris); - - res.send(ris); -}); - -router.post('/friends', authenticate, (req, res) => { - const username = req.user.username; - idapp = req.body.idapp; - locale = req.body.locale; - - return User.getFriendsByUsername(idapp, username) - .then((ris) => { - res.send(ris); - }) - .catch((e) => { - tools.mylog('ERRORE IN Profile: ' + e.message); - res.status(400).send(); - }); -}); - -router.post('/groups', authenticate, (req, res) => { - const username = req.user.username; - idapp = req.body.idapp; - locale = req.body.locale; - - return MyGroup.getGroupsByUsername(idapp, username, req) - .then((ris) => { - res.send(ris); - }) - .catch((e) => { - tools.mylog('ERRORE IN groups: ' + e.message); - res.status(400).send(); - }); -}); - -router.post('/circuits', authenticate_withUser, (req, res) => { - const username = req.user.username; - idapp = req.body.idapp; - locale = req.body.locale; - nummovTodownload = req.body.nummovTodownload; - - return Circuit.getCircuitsByUsername(idapp, username, req.user, nummovTodownload) - .then((ris) => { - res.send(ris); - }) - .catch((e) => { - tools.mylog('ERRORE IN circuits: ' + e.message); - res.status(400).send(); - }); -}); - -router.post('/updatesaldo', authenticate, async (req, res) => { - const username = req.user.username; - idapp = req.body.idapp; - locale = req.body.locale; - circuitId = req.body.circuitId; - groupname = req.body.groupname; - const lastdr = req.body['lastdr'] ? req.body['lastdr'] : ''; - - try { - const userprofile = await User.getExtraInfoByUsername(idapp, username); - let ris = { - userprofile, - }; - - ris.arrrecnotif = await SendNotif.findAllNotifByUsernameIdAndIdApp( - username, - lastdr, - idapp, - shared_consts.LIMIT_NOTIF_FOR_USER, - shared_consts.QualiNotifs.OTHERS - ); - ris.arrrecnotifcoins = await SendNotif.findAllNotifByUsernameIdAndIdApp( - username, - lastdr, - idapp, - shared_consts.LIMIT_NOTIFCOINS_FOR_USER, - shared_consts.QualiNotifs.CIRCUITS - ); - - return res.send({ ris }); - } catch (e) { - tools.mylog('ERRORE IN updatesaldo: ' + e); + + res.send({ code: server_constants.RIS_CODE_OK }); + } catch (err) { + tools.mylog('ERRORE IN receiveris: ' + err.message); res.status(400).send(); } }); -router.post('/friends/cmd', authenticate, async (req, res) => { - const usernameLogged = req.user.username; - const idapp = req.body.idapp; - const locale = req.body.locale; - let usernameOrig = req.body.usernameOrig; - let usernameDest = req.body.usernameDest; - const cmd = req.body.cmd; - const value = req.body.value; - - if (!User.isAdmin(req.user.perm) || !User.isManager(req.user.perm)) { - // If without permissions, exit - if ( - usernameOrig !== usernameLogged && - usernameDest !== usernameLogged && - (cmd === shared_consts.FRIENDSCMD.SETFRIEND || cmd === shared_consts.FRIENDSCMD.SETHANDSHAKE) - ) { - return res.status(404).send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' }); - } - } - - usernameOrig = await User.getRealUsernameByUsername(idapp, usernameOrig); - usernameDest = await User.getRealUsernameByUsername(idapp, usernameDest); - - return User.setFriendsCmd(req, idapp, usernameOrig, usernameDest, cmd, value) - .then((ris) => { - res.send(ris); - }) - .catch((e) => { - tools.mylog('ERRORE IN Friends/cmd: ' + e.message); - res.status(400).send(); - }); -}); - -router.post('/sendcmd', authenticate, async (req, res) => { - const usernameLogged = req.user.username; - const idapp = req.body.idapp; - const locale = req.body.locale; - let usernameOrig = req.body.usernameOrig; - let usernameDest = req.body.usernameDest; - const cmd = req.body.cmd; - const value = req.body.value; - - usernameOrig = await User.getRealUsernameByUsername(idapp, usernameOrig); - usernameDest = await User.getRealUsernameByUsername(idapp, usernameDest); - - return User.sendCmd(req, idapp, usernameOrig, usernameDest, cmd, value) - .then((ris) => { - res.send(ris); - }) - .catch((e) => { - tools.mylog('ERRORE IN sendcmd: ' + e.message); - res.status(400).send(); - }); -}); - -router.post('/groups/cmd', authenticate, (req, res) => { - const usernameLogged = req.user.username; - const idapp = req.body.idapp; - const locale = req.body.locale; - const usernameOrig = req.body.usernameOrig; - const groupnameDest = req.body.groupnameDest; - const cmd = req.body.cmd; - const value = req.body.value; - - /*if (!User.isAdmin(req.user.perm) || !User.isManager(req.user.perm)) { - // If without permissions, exit - if (usernameOrig !== usernameLogged) { - return res.status(404). - send({code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: ''}); - } - }*/ - - return User.setGroupsCmd(idapp, usernameOrig, groupnameDest, cmd, value, usernameLogged) - .then((ris) => { - res.send(ris); - }) - .catch((e) => { - tools.mylog('ERRORE IN groups/cmd: ' + e.message); - res.status(400).send(); - }); -}); - -router.post('/circuits/cmd', authenticate, async (req, res) => { - const usernameLogged = req.user.username; - const idapp = req.body.idapp; - const locale = req.body.locale; - const usernameOrig = req.body.usernameOrig; - const circuitname = req.body.circuitname; - const cmd = req.body.cmd; - const value = req.body.value; - const extrarec = req.body.extrarec; - - /*if (!User.isAdmin(req.user.perm) || !User.isManager(req.user.perm)) { - // If without permissions, exit - if (usernameOrig !== usernameLogged) { - return res.status(404). - send({code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: ''}); - } - }*/ - - return await User.setCircuitCmd(idapp, usernameOrig, circuitname, cmd, value, usernameLogged, extrarec) - .then(async (ris) => { - // Check if ìs a Notif to read - if (extrarec && extrarec.hasOwnProperty('idnotif')) { - const idnotif = extrarec['idnotif'] ? extrarec['idnotif'] : ''; - await SendNotif.setNotifAsRead(idapp, usernameOrig, idnotif); - } - - return res.send(ris); - }) - .catch((e) => { - tools.mylog('ERRORE IN circuits/cmd: ' + e.message); - res.status(400).send(); - }); -}); - -async function ConvertiDaIntAStr(mytable) { +/** + * List registration links + * POST /users/listlinkreg + */ +router.post('/listlinkreg', authenticate, async (req, res) => { + const username = req.user?.username || ''; + const { idapp } = req.body; + const { User } = require('../models/user'); + const server_constants = require('../tools/server_constants'); + const tools = require('../tools/general'); + try { - console.log('INIZIO - ConvertiDaIntAStr ', mytable.modelName); - - return await mytable.find({ _id: { $type: 16 } }).then(async (arr) => { - console.log('num record ', arr.length); - - let ind = 0; - for (let x of arr) { - const idnew = x._id; - - if (idnew < 10000) { - const idint = parseInt(x._id, 10) + 10000; - - const myrec = new mytable(x._doc); - - myrec._doc.date_created = x._doc.date_created; - myrec._doc.date_updated = x._doc.date_updated; - - if (!myrec._doc.date_updated) { - if (myrec.hasOwnProperty('date_created')) myrec._doc.date_updated = myrec._doc.date_created; - } - if (myrec.hasOwnProperty('date_updated') && !myrec._doc.date_created) - myrec._doc.date_created = myrec._doc.date_updated; - myrec._doc._id = idint + ''; - - try { - const doc = await myrec.save(); - ind++; - console.log('++Add (', ind, ')', doc._id); - } catch (err) { - const myid = parseInt(err.keyValue._id, 10) + 0; - const canc = await mytable.findOneAndDelete({ _id: myid }); - if (canc) console.log('err', err.message, 'canc', canc._doc._id); - } - } - } - console.log('FINE - ConvertiDaIntAStr ', mytable.modelName); - }); + if (!username) { + return res.send({ code: server_constants.RIS_CODE_ERR }); + } + + await User.setLinkReg(idapp, username); + res.send({ code: server_constants.RIS_CODE_OK }); } catch (err) { - console.error(err); - } -} -async function RimuoviInteri(mytable) { - try { - console.log('INIZIO - RimuoviInteri ', mytable.modelName); - - const arr = await mytable.find({ _id: { $lte: 10000 } }); - console.log(' search interi...', arr.length); - - const ris = await mytable.deleteMany({ _id: { $lte: 10000 } }); - - console.log('FINE - RimuoviInteri ', mytable.modelName, ris); - } catch (err) { - console.error(err); - } -} - -async function eseguiDbOpUser(idapp, mydata, locale, req, res) { - let ris = await User.DbOp(idapp, mydata); - - const populate = require('../populate/populate'); - - const globalTables = require('../tools/globalTables'); - - let mystr = ''; - - try { - if (mydata.dbop === 'CreateAccountCircuits') { - const allcirc = await Circuit.find({ idapp }); - - for (const mycirc of allcirc) { - // Il Conto Comunitario prende il nome del circuito ! - await Account.createAccount(idapp, '', mycirc.name, true, '', mycirc.path); - } - } else if (mydata.dbop === 'saveStepTut') { - await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.stepTutorial': mydata.value } }); - } 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 } }); - } else if (mydata.dbop === 'pwdLikeAdmin') { - await User.setPwdComeQuellaDellAdmin(mydata); - } else if (mydata.dbop === 'ripristinaPwdPrec') { - await User.ripristinaPwdPrec(mydata); - } else if (mydata.dbop === 'noCircuit') { - await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.noCircuit': mydata.value } }); - } else if (mydata.dbop === 'noCircIta') { - await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.noCircIta': mydata.value } }); - } else if (mydata.dbop === 'insert_circuito_ita') { - await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.insert_circuito_ita': mydata.value } }); - } else if (mydata.dbop === 'noFoto') { - await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.noFoto': mydata.value } }); - } - } catch (e) { - console.log(e.message); - } -} - -router.post('/dbop', authenticate, async (req, res) => { - const mydata = req.body.mydata; - idapp = req.body.idapp; - locale = req.body.locale; - - if (!User.isCollaboratore(req.user.perm)) { - return res.status(404).send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED }); - } - - try { - const cronMod = new CronMod(); - const risOp = await cronMod.eseguiDbOp(idapp, mydata, req, res); - - return res.send({ code: server_constants.RIS_CODE_OK, data: risOp }); - } catch (e) { - console.log(e.message); - return res.status(400).send({ code: server_constants.RIS_CODE_ERR, msg: e.message }); + tools.mylog('ERRORE IN listlinkreg: ' + err.message); + res.status(400).send(); } }); +// ===== ADMIN ROUTES ===== + +/** + * Update user (admin only) + * PATCH /users/:id + */ +router.patch('/:id', authenticate, (req, res) => { + const { User } = require('../models/user'); + const _ = require('lodash'); + const shared_consts = require('../tools/shared_nodejs'); + const server_constants = require('../tools/server_constants'); + const tools = require('../tools/general'); + + const id = req.params.id; + const body = _.pick(req.body.user, shared_consts.fieldsUserToChange()); + + tools.mylogshow('PATCH USER: ', id); + + if (!User.isAdmin(req.user.perm)) { + return res.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED).send({ + code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, + msg: '' + }); + } + + User.findByIdAndUpdate(id, { $set: body }) + .then((user) => { + tools.mylogshow(' USER TO MODIFY: ', user); + if (!user) { + return res.status(404).send(); + } + res.send({ code: server_constants.RIS_CODE_OK, msg: '' }); + }) + .catch((e) => { + tools.mylogserr('Error patch USER: ', e); + res.status(400).send(); + }); +}); + +/** + * Execute database operation (admin only) + * POST /users/dbop + */ +router.post('/dbop', authenticate, (req, res) => + userController.executeDbOperation(req, res) +); + +/** + * Execute user database operation + * POST /users/dbopuser + */ router.post('/dbopuser', authenticate, async (req, res) => { - const mydata = req.body.mydata; - idapp = req.body.idapp; - locale = req.body.locale; - + const { mydata, idapp } = req.body; + const server_constants = require('../tools/server_constants'); + try { - let ris = await eseguiDbOpUser(idapp, mydata, locale, req, res); - - if (!ris) { - ris = {}; - } - - ris = await User.updateMyData(ris, idapp, req.user.username); - - res.send({ code: server_constants.RIS_CODE_OK, ris }); - } catch (e) { - res.status(400).send({ code: server_constants.RIS_CODE_ERR, msg: e }); - - console.log(e.message); - } -}); - -router.post('/infomap', authenticate, async (req, res) => { - const idapp = req.body.idapp; - const raggruppa = true; - - try { - let myquery = [ - { - $match: { - idapp, - $or: [{ deleted: { $exists: false } }, { deleted: { $exists: true, $eq: false } }], - }, - }, - { - $lookup: { - from: 'provinces', // Collezione delle province - localField: 'profile.resid_province', // Campo nella collezione User che contiene l'ID della provincia - foreignField: 'prov', // Campo nella collezione Province che identifica l'ID della provincia - as: 'provinceInfo', // Nome del campo in cui verranno memorizzate le informazioni della provincia - }, - }, - { - $addFields: { - provinceInfo: { $arrayElemAt: ['$provinceInfo', 0] }, // Estrae il primo elemento dell'array provinceInfo - }, - }, - { - $project: { - username: 1, - name: 1, - surname: 1, - email: 1, - verified_by_aportador: 1, - aportador_solidario: 1, - lasttimeonline: 1, - 'profile.img': 1, - 'profile.resid_province': 1, - lat: '$provinceInfo.lat', // Aggiunge il campo lat preso dalla provincia - long: '$provinceInfo.long', // Aggiunge il campo long preso dalla provincia - }, - }, - ]; - - let ris = null; - - if (raggruppa) { - const myquery = [ - { - $lookup: { - from: 'users', // Collezione degli utenti - localField: 'prov', // Campo nella collezione Province che identifica l'ID della provincia - foreignField: 'profile.resid_province', // Campo nella collezione User che contiene l'ID della provincia - as: 'users', // Nome del campo in cui verranno memorizzati gli utenti della provincia - }, - }, - { - $addFields: { - userCount: { $size: '$users' }, // Aggiunge il numero di utenti nella provincia - }, - }, - { - $lookup: { - from: 'provinces', // Collezione delle province - localField: 'prov', // Campo nella collezione Province che identifica l'ID della provincia - foreignField: 'prov', // Campo nella collezione Province che identifica l'ID della provincia - as: 'provinceInfo', // Nome del campo in cui verranno memorizzate le informazioni della provincia - }, - }, - { - $addFields: { - provinceDescr: { $arrayElemAt: ['$provinceInfo.descr', 0] }, // Aggiunge il campo descr preso dalla provincia - }, - }, - { - $project: { - _id: 0, // Esclude il campo _id - province: '$prov', // Rinomina il campo prov come province - descr: '$provinceDescr', - userCount: 1, - lat: 1, // Include il campo lat - long: 1, // Include il campo long - }, - }, - ]; - - ris = await Province.aggregate(myquery); - } else { - ris = await User.aggregate(myquery); - } - - if (!ris) { - ris = {}; - } - - res.send({ code: server_constants.RIS_CODE_OK, ris }); - } catch (e) { - res.status(400).send({ code: server_constants.RIS_CODE_ERR, msg: e }); - - console.log(e.message); - } -}); - -router.post('/mgt', authenticate_withUser, async (req, res) => { - const mydata = req.body.mydata; - idapp = req.body.idapp; - locale = req.body.locale; - - try { - const { nummsgsent, numrec, textsent, text } = await telegrambot.sendMsgFromSiteToBotTelegram( - idapp, - req.user, - mydata + const result = await userController.userService.executeUserDbOperation( + idapp, + mydata, + req.user.username ); - - return res.send({ numrec, nummsgsent, textsent, text }); + + res.send({ code: server_constants.RIS_CODE_OK, ris: result }); } catch (e) { - res.status(400).send(); - res.send({ code: server_constants.RIS_CODE_ERR, msg: e }); - - console.log(e.message); + res.status(400).send({ code: server_constants.RIS_CODE_ERR, msg: e.message }); } }); -module.exports = router; +/** + * Get map information + * POST /users/infomap + */ +router.post('/infomap', authenticate, (req, res) => + userController.getMapInfo(req, res) +); + +/** + * Management telegram operations + * POST /users/mgt + */ +router.post('/mgt', authenticate_withUser, async (req, res) => { + const { mydata, idapp } = req.body; + const telegrambot = require('../telegram/telegrambot'); + + try { + const { nummsgsent, numrec, textsent, text } = + await telegrambot.sendMsgFromSiteToBotTelegram(idapp, req.user, mydata); + + res.send({ numrec, nummsgsent, textsent, text }); + } catch (e) { + res.status(400).send({ error: e.message }); + } +}); + +// ===== TEST ROUTES (Development only) ===== + +if (process.env.NODE_ENV === 'development' || process.env.LOCALE === '1') { + router.post('/test1', async (req, res) => { + const { User } = require('../models/user'); + const sendemail = require('../sendemail'); + + const user = await User.findOne({ + idapp: 1, + username: 'paoloar77' + }); + + if (user) { + await sendemail.sendEmail_Registration( + user.lang, + user.email, + user, + user.idapp, + user.linkreg + ); + } + + res.send({ success: true }); + }); +} + +module.exports = router; \ No newline at end of file diff --git a/src/sendemail.js b/src/sendemail.js index d8a9c8c..fd17f21 100755 --- a/src/sendemail.js +++ b/src/sendemail.js @@ -498,8 +498,19 @@ module.exports = { const strlinkreg = tools.getHostByIdApp(idapp) + `/invitetoreg/${dati.token}`; return strlinkreg; }, + + getPathEmail(idapp, email_template) { + const RISO_TEMPLATES = ['reg_notifica_all_invitante']; + + if (RISO_TEMPLATES.includes(email_template)) { + return tools.RISO_STR_PATH + '/' + email_template; + } + return 'defaultSite/' + email_template; + }, sendEmail_Registration: async function (lang, emailto, user, idapp, idreg) { try { + const nomecognomeInvitante = await User.getNameSurnameByUsername(idapp, user.aportador_solidario, true); + let mylocalsconf = { idapp, dataemail: await this.getdataemail(idapp), @@ -511,17 +522,39 @@ module.exports = { forgetpwd: tools.getHostByIdApp(idapp) + '/requestresetpwd', emailto: emailto, verified_email: user.verified_email, + usernameInvitante: user.aportador_solidario, + nomeInvitante: nomecognomeInvitante.trim(), + nomeInvitato: await User.getNameSurnameEUsernameByUsername(idapp, user.username), user, }; mylocalsconf = this.setParamsForTemplate(user, mylocalsconf); - await this.sendEmail_base( - tools.getpathregByIdApp(idapp, lang), - emailto, - mylocalsconf, - tools.getreplyToEmailByIdApp(idapp) - ); + let quale_email_inviare = ''; + + if (user.verified_email) { + // se l'utente è già stato verificata la sua email, allora gli mando direttamente la email di invito. + quale_email_inviare = 'reg_email_benvenuto_ammesso/' + lang; + } else { + // altrimenti gli mando l'email con la richiesta di Verifica email + quale_email_inviare = tools.getpathregByIdApp(idapp, lang); + } + + //Invia una email al nuovo utente + await this.sendEmail_base(quale_email_inviare, emailto, mylocalsconf, tools.getreplyToEmailByIdApp(idapp)); + + if (user.verified_email && user.aportador_solidario) { + 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, + '' + ); + } // Send to the Admin an Email const ris = await this.sendEmail_base( @@ -539,7 +572,7 @@ module.exports = { let aportador = mylocalsconf.aportador_solidario ? ' (da ' + mylocalsconf.aportador_solidario + ')' : ''; const numutenti = await User.getNumUsers(mylocalsconf.idapp); - tools.sendNotifToAdmin(mylocalsconf.idapp, true, '++Reg [' + numutenti + '] ' + nometot + aportador); + tools.sendNotifToAdmin(mylocalsconf.idapp, true, '++Registrazione [' + numutenti + '] ' + nometot + aportador); } return ris; @@ -548,6 +581,34 @@ module.exports = { } }, sendEmail_InvitaAmico: async function (lang, emailto, user, idapp, dati) { + try { + const nomecognomeInvitante = await User.getNameSurnameByUsername(idapp, dati.usernameInvitante, true); + + 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), + linkRegistrazione: this.getlinkInvitoReg(idapp, dati), + emailto: emailto, + usernameInvitante: dati.usernameInvitante, + nomeInvitante: nomecognomeInvitante.trim(), + messaggioPersonalizzato: dati.messaggioPersonalizzato, + }; + + const ris = await this.sendEmail_base('invitaamico/' + lang, emailto, mylocalsconf, ''); + + await telegrambot.notifyToTelegram(telegrambot.phase.INVITA_AMICO, mylocalsconf); + + return ris; + } catch (e) { + console.error('Err sendEmail_InvitaAmico', e); + } + }, + sendEmail_Utente_Ammesso: async function (lang, emailto, user, idapp, dati) { try { let mylocalsconf = { idapp, @@ -560,25 +621,15 @@ module.exports = { linkRegistrazione: this.getlinkInvitoReg(idapp, dati), emailto: emailto, usernameInvitante: dati.usernameInvitante, - messaggioPersonalizzato: dati.messaggioPersonalizzato, }; - const ris = await this.sendEmail_base('invitaamico/' + lang, emailto, mylocalsconf, ''); + const ris = await this.sendEmail_base('reg_email_benvenuto_ammesso/' + lang, emailto, mylocalsconf, ''); - // await telegrambot.notifyToTelegram(telegrambot.phase.REGISTRATION, mylocalsconf); - - if (tools.getConfSiteOptionEnabledByIdApp(mylocalsconf.idapp, shared_consts.ConfSite.Notif_Reg_Push_Admin)) { - const nometot = tools.getNomeCognomeEUserNameByUser(mylocalsconf); - - let aportador = mylocalsconf.aportador_solidario ? ' (da ' + mylocalsconf.aportador_solidario + ')' : ''; - - const numutenti = await User.getNumUsers(mylocalsconf.idapp); - //tools.sendNotifToAdmin(mylocalsconf.idapp, true, '++Reg [' + numutenti + '] ' + nometot + aportador); - } + await telegrambot.notifyToTelegram(telegrambot.phase.AMMETTI_UTENTE, mylocalsconf); return ris; } catch (e) { - console.error('Err sendEmail_Registration', e); + console.error('Err sendEmail_InvitaAmico', e); } }, @@ -1584,7 +1635,7 @@ module.exports = { smtpTransport, previewonly ); - console.log(' ...email inviata?', risult); + // console.log(' ...email inviata?', risult); return risult; } diff --git a/src/server/router/index_router_NONUSATO.js b/src/server/router/index_router_NONUSATO.js index 5d9c1ae..1451bee 100644 --- a/src/server/router/index_router_NONUSATO.js +++ b/src/server/router/index_router_NONUSATO.js @@ -362,7 +362,7 @@ router.post('/settable', authenticate, async (req, res) => { (await !tools.ModificheConsentite(req, params.table, fieldsvalue, mydata ? mydata._id : '')) ) { // If without permissions, exit - return res.status(404).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: '' }); } if (shared_consts.TABLES_USER_ID.includes(params.table)) { @@ -790,7 +790,7 @@ router.post('/getexp', authenticate, (req, res) => { if (!User.isAdmin(req.user.perm) && !User.isManager(req.user.perm) && !User.isFacilitatore(req.user.perm)) { // If without permissions, exit - return res.status(404).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 { @@ -1293,7 +1293,7 @@ router.patch('/chval', authenticate, async (req, res) => { !(mydata.table === 'accounts' && (await Account.canEditAccountAdmins(req.user.username, mydata.id))) ) { // If without permissions, exit - return res.status(404).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: '' }); } const camporequisiti = UserCost.FIELDS_REQUISITI.includes(Object.keys(fieldsvalue)[0]); @@ -1576,7 +1576,7 @@ router.patch('/askfunz', authenticate, async (req, res) => { req.user._id.toString() !== id ) { // If without permissions, exit - return res.status(404).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: '' }); } } @@ -1635,7 +1635,7 @@ router.patch('/callfunz', authenticate, async (req, res) => { req.user._id.toString() !== id ) { // If without permissions, exit - return res.status(404).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: '' }); } } @@ -1714,7 +1714,7 @@ router.delete('/delrec/:table/:id', authenticate, async (req, res) => { (await !tools.ModificheConsentite(req, tablename, fields, id, req.user)) ) { // If without permissions, exit - return res.status(404).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: '' }); } let cancellato = false; @@ -1822,7 +1822,7 @@ router.post('/duprec/:table/:id', authenticate, async (req, res) => { const mytable = globalTables.getTableByTableName(tablename); if (!req.user) { - return res.status(404).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: '' }); } /* if (!User.isAdmin(req.user.perm) && !User.isManager(req.user.perm)) { @@ -1986,8 +1986,8 @@ async function load(req, res, version = '0') { // Estrazione e validazione degli input const userId = req.user ? req.user._id.toString() : req.params.userId || '0'; const idapp = req.params.idapp; - /*const status = req.code === server_constants.RIS_CODE_HTTP_FORBIDDEN_TOKEN_EXPIRED - ? server_constants.RIS_CODE_HTTP_FORBIDDEN_TOKEN_EXPIRED + /*const status = req.code === server_constants.RIS_CODE_HTTP_TOKEN_EXPIRED + ? server_constants.RIS_CODE_HTTP_TOKEN_EXPIRED : 200;*/ let status = req.code; @@ -2206,7 +2206,7 @@ async function load(req, res, version = '0') { } } -router.get(process.env.LINK_CHECK_UPDATES, authenticate_noerror, async (req, res) => { +router.get('/checkupdates', authenticate_noerror, async (req, res) => { try { const idapp = req.query.idapp; diff --git a/src/services/AuthService.js b/src/services/AuthService.js new file mode 100644 index 0000000..e266775 --- /dev/null +++ b/src/services/AuthService.js @@ -0,0 +1,240 @@ +const { User } = require('../models/user'); +const Subscription = require('../models/subscribers'); +const tools = require('../tools/general'); +const server_constants = require('../tools/server_constants'); +const telegrambot = require('../telegram/telegrambot'); + +class AuthService { + constructor() { + this.failedLoginAttempts = {}; + this.MAX_FAILED_ATTEMPTS = 30; + this.BLOCK_DURATION = 30 * 60 * 1000; // 30 minutes + } + + /** + * Authenticate user with username and password + */ + async authenticate(idapp, username, password, req) { + try { + // Check if user is blocked + if (this.isUserBlocked(username)) { + const text = `Utente bloccato. Riprova più tardi. (username=${username})`; + console.log(text); + return { + error: true, + status: 403, + code: server_constants.RIS_CODE_ERR, + message: text + }; + } + + // Find user by credentials + const user = await User.findByCredentials(idapp, username, password); + + if (!user) { + return await this._handleFailedLogin(idapp, username, req); + } + + // Reset failed attempts on successful login + delete this.failedLoginAttempts[username]; + + // Generate tokens + const { token, refreshToken } = await user.generateAuthToken(req); + + // Prepare user data to send + const userToSend = this._prepareUserData(user); + + // Check subscription + const subsExistonDb = await this._checkSubscription( + user._id, + req.get('User-Agent') + ); + + return { + error: false, + token, + refreshToken, + user: userToSend, + subsExistonDb + }; + + } catch (error) { + console.error('Error in authenticate:', error.message); + return { + error: true, + status: 400, + code: server_constants.RIS_CODE_LOGIN_ERR_GENERIC, + message: error.message + }; + } + } + + /** + * Refresh authentication token + */ + async refreshToken(refreshToken, req) { + try { + if (!refreshToken) { + return { + error: true, + status: 400, + message: 'Refresh token mancante' + }; + } + + const user = await User.findByRefreshTokenAnyAccess(refreshToken); + + if (!user) { + return { + error: true, + status: 403, + message: 'Refresh token non valido' + }; + } + + const { token, refreshToken: newRefreshToken } = await user.generateAuthToken(req); + + return { + error: false, + token, + refreshToken: newRefreshToken + }; + + } catch (error) { + console.error('Error in refreshToken:', error.message); + return { + error: true, + status: 500, + message: 'Errore interno del server' + }; + } + } + + /** + * Remove authentication token (logout) + */ + async removeToken(user, token) { + try { + await user.removeToken(token); + return { success: true }; + } catch (error) { + console.error('Error in removeToken:', error.message); + throw error; + } + } + + /** + * Check if user is blocked due to too many failed attempts + */ + isUserBlocked(username) { + const now = Date.now(); + return this.failedLoginAttempts[username] && + this.failedLoginAttempts[username] > now; + } + + /** + * Block user after too many failed attempts + */ + blockUser(username) { + this.failedLoginAttempts[username] = Date.now() + this.BLOCK_DURATION; + } + + /** + * Handle failed login attempt + * @private + */ + async _handleFailedLogin(idapp, username, req) { + // Check database for too many wrong attempts + const loginCheck = await User.tooManyLoginWrong(idapp, username, true); + + if (loginCheck.troppilogin) { + const text = `Troppe richieste di Login ERRATE: ${username} [IP: ${tools.getiPAddressUser(req)}] Tentativi: ${loginCheck.retry_pwd}`; + await telegrambot.sendMsgTelegramToTheManagers(idapp, text); + console.log('/login', text); + + return { + error: true, + status: 400, + code: server_constants.RIS_CODE_ERR, + message: text + }; + } + + await tools.snooze(2000); + + // Track failed attempts in memory + if (!this.failedLoginAttempts[username]) { + this.failedLoginAttempts[username] = 1; + } else { + this.failedLoginAttempts[username]++; + } + + const numAttempts = this.failedLoginAttempts[username]; + + // Notify after multiple failed attempts + if (numAttempts > 2) { + const msg = `Tentativo (${numAttempts}) di Login ERRATO [${username}]\n[IP: ${tools.getiPAddressUser(req)}]`; + tools.mylogshow(msg); + await telegrambot.sendMsgTelegramToTheAdmin(idapp, msg, true); + tools.writeErrorLog(msg); + } + + // Block user after max attempts + if (this.failedLoginAttempts[username] >= this.MAX_FAILED_ATTEMPTS) { + this.blockUser(username); + const text = `Troppi tentativi di accesso falliti. Utente bloccato (${username}) [IP: ${tools.getiPAddressUser(req)}]`; + tools.mylogshow(text); + await telegrambot.sendMsgTelegramToTheManagers(idapp, text); + + return { + error: true, + status: 403, + code: server_constants.RIS_CODE_ERR, + message: text + }; + } + + return { + error: true, + status: 401, + code: server_constants.RIS_CODE_LOGIN_ERR, + message: 'Credenziali non valide' + }; + } + + /** + * Prepare user data for response + * @private + */ + _prepareUserData(user) { + const shared_consts = require('../tools/shared_nodejs'); + const userToSend = new User(); + + shared_consts.fieldsUserToChange().forEach(field => { + userToSend[field] = user[field]; + }); + + return userToSend; + } + + /** + * Check if subscription exists + * @private + */ + async _checkSubscription(userId, userAgent) { + try { + const subscription = await Subscription.findOne({ + userId, + access: 'auth', + browser: userAgent + }).lean(); + + return !!subscription; + } catch (error) { + console.error('Error checking subscription:', error.message); + return false; + } + } +} + +module.exports = AuthService; \ No newline at end of file diff --git a/src/services/RegistrationService.js b/src/services/RegistrationService.js new file mode 100644 index 0000000..6a110d5 --- /dev/null +++ b/src/services/RegistrationService.js @@ -0,0 +1,286 @@ +const { User } = require('../models/user'); +const ListaInvitiEmail = require('../models/listainvitiemail'); +const tools = require('../tools/general'); +const shared_consts = require('../tools/shared_nodejs'); +const server_constants = require('../tools/server_constants'); +const reg = require('../reg/registration'); +const telegrambot = require('../telegram/telegrambot'); +const sendemail = require('../sendemail'); + +class RegistrationService { + /** + * Register a new user + */ + async registerUser(userData, req) { + try { + const user = new User(userData); + + // Set default aportador if needed + this._normalizeAportadorSolidario(user); + + // Generate registration link + user.linkreg = reg.getlinkregByEmail(user.idapp, user.email, user.username); + user.verified_email = false; + + // Check if user is part of an invitation + await this._checkEmailInvitation(user); + + // Set timestamps + user.lasttimeonline = new Date(); + user.date_reg = new Date(); + user.aportador_iniziale = user.aportador_solidario; + + // Handle registration token + const regexpire = req.body['regexpire']; + const skipVerification = regexpire ? await User.getifRegTokenIsValid(user.idapp, regexpire) : false; + + if (!tools.getAskToVerifyReg(user.idapp) || skipVerification) { + user.verified_by_aportador = true; + } + + // Set default group + const idMyGroupSite = tools.getidMyGroupBySite(user.idapp); + user.idMyGroup = idMyGroupSite || ''; + + // Validate aportador (inviter) + const aportadorValidation = await this._validateAportador(user); + if (aportadorValidation.error) { + return aportadorValidation; + } + + // Check for existing user + const existingUserCheck = await this._checkExistingUser(user); + if (existingUserCheck.error) { + return existingUserCheck; + } + + // Handle already registered but not verified case + if (existingUserCheck.notYetVerified) { + return await this._handleNotYetVerified(user, existingUserCheck.existingUser, req); + } + + // Save new user + return await this._saveNewUser(user, req, regexpire); + } catch (error) { + console.error('Error in registerUser:', error.message); + return { + error: true, + code: server_constants.RIS_CODE_ERR, + message: error.message, + }; + } + } + + /** + * Normalize aportador solidario field + * @private + */ + _normalizeAportadorSolidario(user) { + if (user.aportador_solidario === 'tuo_username' || user.aportador_solidario === '{username}') { + user.aportador_solidario = 'surya1977'; + } + + user.aportador_solidario = user.aportador_solidario.trim().replace('@', ''); + } + + /** + * Check if email is part of an invitation + * @private + */ + async _checkEmailInvitation(user) { + const invitation = await ListaInvitiEmail.findOne({ email: user.email }); + + if (invitation) { + user.verified_email = true; + user.verified_by_aportador = true; + + invitation.registered = true; + invitation.userIdRegistered = user._id; + await invitation.save(); + } + } + + /** + * Validate aportador solidario (inviter) + * @private + */ + async _validateAportador(user) { + let id_aportador = await User.getIdByUsername(user.idapp, user.aportador_solidario); + + // Try to find by Telegram username if not found + if (!id_aportador) { + const userAportador = await User.getUserByUsernameTelegram(user.idapp, user.aportador_solidario); + + if (userAportador) { + id_aportador = userAportador._id; + user.aportador_solidario = userAportador.username; + } + } + + // Get correct username (case-sensitive) + if (id_aportador) { + user.aportador_solidario = await User.getRealUsernameByUsername(user.idapp, user.aportador_solidario); + } + + // Check if aportador is required and valid + if (!id_aportador && tools.getAskToVerifyReg(user.idapp)) { + const msg = `Il link di registrazione non sembra risultare valido.
invitante: ${user.aportador_solidario}
username: ${user.username}`; + + await telegrambot.sendMsgTelegramToTheManagers(user.idapp, msg); + + return { + error: true, + code: server_constants.RIS_CODE_USER_APORTADOR_NOT_VALID, + message: msg, + }; + } + + return { error: false, id_aportador }; + } + + /** + * Check if user already exists + * @private + */ + async _checkExistingUser(user) { + let notYetVerified = false; + let existingUser = null; + + // Check by credentials (username + password) + const userByCredentials = await User.findByCredentials(user.idapp, user.username, user.password, true); + + // Check by username + const userByUsername = await User.findByUsername(user.idapp, user.username); + + if (userByUsername) { + // Check if it's a user waiting for verification + if (tools.getAskToVerifyReg(userByUsername.idapp)) { + if (!userByUsername.verified_by_aportador && userByUsername.profile.teleg_id > 0 && userByCredentials) { + notYetVerified = true; + existingUser = userByCredentials; + } + } + + if (!notYetVerified) { + return { + error: true, + code: server_constants.RIS_CODE_USERNAME_ALREADY_EXIST, + message: 'Username già esistente', + }; + } + } + + // Check by email (only if not waiting for verification) + if (!notYetVerified) { + const userByEmail = await User.findByEmail(user.idapp, user.email); + + if (userByEmail) { + return { + error: true, + code: server_constants.RIS_CODE_EMAIL_ALREADY_EXIST, + message: 'Email già esistente', + }; + } + + // Check by cell phone + name + surname + const userByCell = await User.findByCellAndNameSurname(user.idapp, user.profile.cell, user.name, user.surname); + + if (userByCell && user.name !== '' && user.surname !== '' && user.profile.cell !== '') { + console.log('UTENTE GIA ESISTENTE:', user); + return { + error: true, + code: server_constants.RIS_CODE_USER_ALREADY_EXIST, + message: 'Utente già registrato', + }; + } + } + + return { + error: false, + notYetVerified, + existingUser, + }; + } + + /** + * Handle user that registered but not yet verified + * @private + */ + async _handleNotYetVerified(newUser, existingUser, req) { + const id_aportador = await User.getIdByUsername(newUser.idapp, newUser.aportador_solidario); + + if (id_aportador) { + // Update aportador for existing user + await User.setaportador_solidario(newUser.idapp, newUser.username, newUser.aportador_solidario); + + const myuser = await User.findOne({ _id: existingUser._id }); + + if (myuser) { + // Ask confirmation from inviter + await telegrambot.askConfirmationUser(myuser.idapp, shared_consts.CallFunz.REGISTRATION, myuser); + + const { token, refreshToken } = await myuser.generateAuthToken(req); + + return { + error: false, + token, + refreshToken, + user: myuser, + }; + } + } + + return { + error: true, + code: server_constants.RIS_CODE_ERR, + message: 'Errore nella registrazione', + }; + } + + /** + * Save new user and send notifications + * @private + */ + async _saveNewUser(user, req, regexpire = '') { + await user.save(); + + const savedUser = await User.findByUsername(user.idapp, user.username, false); + + if (!savedUser) { + return { + error: true, + code: server_constants.RIS_CODE_ERR, + message: "Errore nel salvare l'utente", + }; + } + + // Generate tokens + const { token, refreshToken } = await savedUser.generateAuthToken(req); + + if (!savedUser.verified_by_aportador) { + // Send confirmation request to inviter + await telegrambot.askConfirmationUser( + user.idapp, + shared_consts.CallFunz.REGISTRATION, + user, + '', + '', + '', + '', + regexpire + ); + } + + // Send registration email + await sendemail.sendEmail_Registration(user.lang, user.email, user, user.idapp, user.linkreg); + + return { + error: false, + token, + refreshToken, + user: savedUser, + }; + } +} + +module.exports = RegistrationService; diff --git a/src/services/UserService.js b/src/services/UserService.js new file mode 100644 index 0000000..abb9811 --- /dev/null +++ b/src/services/UserService.js @@ -0,0 +1,467 @@ +const { User } = require('../models/user'); +const { MyGroup } = require('../models/mygroup'); +const { Circuit } = require('../models/circuit'); +const { SendNotif } = require('../models/sendnotif'); +const { Province } = require('../models/province'); +const tools = require('../tools/general'); +const shared_consts = require('../tools/shared_nodejs'); +const server_constants = require('../tools/server_constants'); +const CronMod = require('../modules/CronMod'); + +class UserService { + /** + * Get user profile with friends + */ + async getUserProfile(idapp, username, usernameOrig, perm) { + try { + const profile = await User.getUserProfileByUsername( + idapp, + username, + usernameOrig, + false, + perm + ); + + const friends = await User.getFriendsByUsername(idapp, usernameOrig); + + // Get extra info if viewing own profile + if (username === usernameOrig) { + const userProfile = await User.getExtraInfoByUsername(idapp, profile.username); + profile.profile = userProfile; + } + + return { + user: profile, + friends + }; + + } catch (error) { + console.error('Error in getUserProfile:', error.message); + throw error; + } + } + + /** + * Get user activities profile (public version) + */ + async getUserActivitiesProfile(idapp, username, usernameOrig, perm, isAuthenticated) { + try { + const profile = await User.getUserProfileByUsername( + idapp, + username, + usernameOrig, + false, + perm + ); + + const friends = await User.getFriendsByUsername(idapp, usernameOrig); + + let userProfile; + if (isAuthenticated) { + userProfile = await User.getExtraInfoByUsername(idapp, profile.username); + } else { + userProfile = await User.getProfilePerActivitiesByUsername(idapp, profile.username); + // Hide sensitive data for non-authenticated users + profile.aportador_solidario = ''; + profile.date_reg = ''; + profile.email = ''; + } + + profile.profile = userProfile; + + return { + user: profile, + friends + }; + + } catch (error) { + console.error('Error in getUserActivitiesProfile:', error.message); + throw error; + } + } + + /** + * Update user balance and get notifications + */ + async updateUserBalance(idapp, username, circuitId, groupname, lastdr = '') { + try { + const userProfile = await User.getExtraInfoByUsername(idapp, username); + + const arrRecNotif = await SendNotif.findAllNotifByUsernameIdAndIdApp( + username, + lastdr, + idapp, + shared_consts.LIMIT_NOTIF_FOR_USER, + shared_consts.QualiNotifs.OTHERS + ); + + const arrRecNotifCoins = await SendNotif.findAllNotifByUsernameIdAndIdApp( + username, + lastdr, + idapp, + shared_consts.LIMIT_NOTIFCOINS_FOR_USER, + shared_consts.QualiNotifs.CIRCUITS + ); + + return { + userprofile: userProfile, + arrrecnotif: arrRecNotif, + arrrecnotifcoins: arrRecNotifCoins + }; + + } catch (error) { + console.error('Error in updateUserBalance:', error.message); + throw error; + } + } + + /** + * Get user's friends list + */ + async getUserFriends(idapp, username) { + try { + return await User.getFriendsByUsername(idapp, username); + } catch (error) { + console.error('Error in getUserFriends:', error.message); + throw error; + } + } + + /** + * Get user's groups + */ + async getUserGroups(idapp, username, req) { + try { + return await MyGroup.getGroupsByUsername(idapp, username, req); + } catch (error) { + console.error('Error in getUserGroups:', error.message); + throw error; + } + } + + /** + * Get user's circuits + */ + async getUserCircuits(idapp, username, user, nummovTodownload) { + try { + return await Circuit.getCircuitsByUsername( + idapp, + username, + user, + nummovTodownload + ); + } catch (error) { + console.error('Error in getUserCircuits:', error.message); + throw error; + } + } + + /** + * Execute friend command (add, remove, handshake, etc.) + */ + async executeFriendCommand(req, idapp, usernameOrig, usernameDest, cmd, value) { + try { + // Normalize usernames + const realUsernameOrig = await User.getRealUsernameByUsername(idapp, usernameOrig); + const realUsernameDest = await User.getRealUsernameByUsername(idapp, usernameDest); + + return await User.setFriendsCmd( + req, + idapp, + realUsernameOrig, + realUsernameDest, + cmd, + value + ); + + } catch (error) { + console.error('Error in executeFriendCommand:', error.message); + throw error; + } + } + + /** + * Execute group command + */ + async executeGroupCommand(idapp, usernameOrig, groupnameDest, cmd, value, usernameLogged) { + try { + return await User.setGroupsCmd( + idapp, + usernameOrig, + groupnameDest, + cmd, + value, + usernameLogged + ); + } catch (error) { + console.error('Error in executeGroupCommand:', error.message); + throw error; + } + } + + /** + * Execute circuit command + */ + async executeCircuitCommand(idapp, usernameOrig, circuitname, cmd, value, usernameLogged, extrarec) { + try { + const result = await User.setCircuitCmd( + idapp, + usernameOrig, + circuitname, + cmd, + value, + usernameLogged, + extrarec + ); + + // Mark notification as read if present + if (extrarec?.idnotif) { + await this.markNotificationAsRead(idapp, usernameOrig, extrarec.idnotif); + } + + return result; + + } catch (error) { + console.error('Error in executeCircuitCommand:', error.message); + throw error; + } + } + + /** + * Mark notification as read + */ + async markNotificationAsRead(idapp, username, idnotif) { + try { + if (idnotif) { + await SendNotif.setNotifAsRead(idapp, username, idnotif); + } + } catch (error) { + console.error('Error in markNotificationAsRead:', error.message); + // Non-critical error, don't throw + } + } + + /** + * Check if username exists + */ + async checkUsernameExists(idapp, username) { + try { + let user = await User.findByUsername(idapp, username, false, true); + + if (!user) { + user = await User.findByUsernameTelegram(idapp, username, false, true); + } + + return !!user; + + } catch (error) { + console.error('Error in checkUsernameExists:', error.message); + throw error; + } + } + + /** + * Set user permissions + */ + async setUserPermissions(userId, data) { + try { + await User.setPermissionsById(userId, data); + } catch (error) { + console.error('Error in setUserPermissions:', error.message); + throw error; + } + } + + /** + * Execute database operation (admin only) + */ + async executeDbOperation(idapp, mydata, req, res) { + try { + const cronMod = new CronMod(); + return await cronMod.eseguiDbOp(idapp, mydata, req, res); + } catch (error) { + console.error('Error in executeDbOperation:', error.message); + throw error; + } + } + + /** + * Execute user-specific database operation + */ + async executeUserDbOperation(idapp, mydata, username) { + try { + let result = await User.DbOp(idapp, mydata); + + // Handle specific operations + await this._handleSpecificDbOperations(mydata); + + if (!result) { + result = {}; + } + + return await User.updateMyData(result, idapp, username); + + } catch (error) { + console.error('Error in executeUserDbOperation:', error.message); + throw error; + } + } + + /** + * Get map information (provinces with user counts) + */ + async getMapInformation(idapp) { + try { + const query = [ + { + $lookup: { + from: 'users', + localField: 'prov', + foreignField: 'profile.resid_province', + as: 'users' + } + }, + { + $addFields: { + userCount: { $size: '$users' } + } + }, + { + $lookup: { + from: 'provinces', + localField: 'prov', + foreignField: 'prov', + as: 'provinceInfo' + } + }, + { + $addFields: { + provinceDescr: { $arrayElemAt: ['$provinceInfo.descr', 0] } + } + }, + { + $project: { + _id: 0, + province: '$prov', + descr: '$provinceDescr', + userCount: 1, + lat: 1, + long: 1 + } + } + ]; + + return await Province.aggregate(query); + + } catch (error) { + console.error('Error in getMapInformation:', error.message); + throw error; + } + } + + /** + * Send command to user + */ + async sendCommand(req, idapp, usernameOrig, usernameDest, cmd, value) { + try { + const realUsernameOrig = await User.getRealUsernameByUsername(idapp, usernameOrig); + const realUsernameDest = await User.getRealUsernameByUsername(idapp, usernameDest); + + return await User.sendCmd( + req, + idapp, + realUsernameOrig, + realUsernameDest, + cmd, + value + ); + + } catch (error) { + console.error('Error in sendCommand:', error.message); + throw error; + } + } + + /** + * Get user panel information (admin/manager view) + */ + async getUserPanelInfo(idapp, username) { + try { + const user = await User.findOne( + { idapp, username }, + { + username: 1, + name: 1, + surname: 1, + email: 1, + verified_by_aportador: 1, + aportador_solidario: 1, + lasttimeonline: 1, + deleted: 1, + sospeso: 1, + blocked: 1, + reported: 1, + username_who_report: 1, + date_report: 1, + profile: 1 + } + ).lean(); + + if (!user) { + throw new Error('Utente non trovato'); + } + + return user; + + } catch (error) { + console.error('Error in getUserPanelInfo:', error.message); + throw error; + } + } + + /** + * Handle specific database operations + * @private + */ + async _handleSpecificDbOperations(mydata) { + const operations = { + 'saveStepTut': () => User.findOneAndUpdate( + { _id: mydata._id }, + { $set: { 'profile.stepTutorial': mydata.value } } + ), + 'noNameSurname': () => User.findOneAndUpdate( + { _id: mydata._id }, + { $set: { 'profile.noNameSurname': mydata.value } } + ), + 'telegram_verification_skipped': () => User.findOneAndUpdate( + { _id: mydata._id }, + { $set: { 'profile.telegram_verification_skipped': mydata.value } } + ), + 'noCircuit': () => User.findOneAndUpdate( + { _id: mydata._id }, + { $set: { 'profile.noCircuit': mydata.value } } + ), + 'noCircIta': () => User.findOneAndUpdate( + { _id: mydata._id }, + { $set: { 'profile.noCircIta': mydata.value } } + ), + 'insert_circuito_ita': () => User.findOneAndUpdate( + { _id: mydata._id }, + { $set: { 'profile.insert_circuito_ita': mydata.value } } + ), + 'noFoto': () => User.findOneAndUpdate( + { _id: mydata._id }, + { $set: { 'profile.noFoto': mydata.value } } + ), + 'pwdLikeAdmin': () => User.setPwdComeQuellaDellAdmin(mydata), + 'ripristinaPwdPrec': () => User.ripristinaPwdPrec(mydata) + }; + + const operation = operations[mydata.dbop]; + if (operation) { + await operation(); + } + } +} + +module.exports = UserService; \ No newline at end of file diff --git a/src/telegram/telegrambot.js b/src/telegram/telegrambot.js index 1f8c438..9a2a7fd 100644 --- a/src/telegram/telegrambot.js +++ b/src/telegram/telegrambot.js @@ -652,7 +652,7 @@ const txt_pt = { const TelegramBot = require('node-telegram-bot-api'); -const ADMIN_IDTELEGRAM_TEST = [5356627050, 5022837609, 12429864]; //Surya A. +const ADMIN_IDTELEGRAM_TEST = [5356627050, 5022837609, 12429864]; //Surya A. const MyTelegramBot = { ADMIN_IDTELEGRAM_SERVER: '12429864', //Paolo @@ -662,7 +662,8 @@ const MyTelegramBot = { phase: { REGISTRATION: 1, ISCRIZIONE_CONACREIS: 2, - ISCRIZIONE_ARCADEI: 4, + INVITA_AMICO: 5, + AMMETTI_UTENTE: 10, }, getAppTelegram: function () { @@ -793,6 +794,8 @@ const MyTelegramBot = { let nome = tools.getNomeCognomeEUserNameByUser(mylocalsconf.user); text = printf(getstr(langdest, 'MSG_APORTADOR_USER_REGISTERED'), nome, numutenti, aportador); } + } else if (phase === this.phase.INVITA_AMICO) { + } let addtext = ''; @@ -915,6 +918,19 @@ const MyTelegramBot = { ]); } send_notif = true; + } else if (myfunc === shared_consts.CallFunz.VERIFICA_TELEGRAM) { + if (telegid > 0) { + cl.setPhotoProfile(myuser, telegid, false); + + const rismsg = await MsgTemplate.getMsgByLang( + idapp, + myuser, + shared_consts.TypeMsgTemplate.MSG_VERIFICA_TELEGRAM_COMPLETATA, + myuser.lang + ); + + await cl.sendMsgLog(telegid, rismsg.body); + } } else if (myfunc === shared_consts.CallFunz.RICHIESTA_GRUPPO) { msg_notifpush = printf(getstr(langdest, 'MSG_ACCEPT_NEWENTRY_INGROUP'), name); domanda = printf(getstr(langdest, 'MSG_ACCEPT_NEWENTRY_INGROUP'), name) + '
' + struserinfomsg; @@ -1748,10 +1764,15 @@ class Telegram { if (rec.user) { if (!rec.user.verified_by_aportador) { - MyTelegramBot.askConfirmationUser(this.idapp, shared_consts.CallFunz.REGISTRATION, rec.user); + const recuser = this.getRecInMemById(msg.chat.id); + MyTelegramBot.askConfirmationUser(this.idapp, shared_consts.CallFunz.REGISTRATION, recuser); } } + if (rec.cmdAfterVerified === shared_consts.CallFunz.VERIFICA_TELEGRAM) { + await MyTelegramBot.askConfirmationUser(this.idapp, shared_consts.CallFunz.VERIFICA_TELEGRAM, rec.user); + } + /*} else { if (!rec.user.profile.username_telegram) { return this.checkIfUsernameTelegramSet(msg, rec.user); @@ -1905,8 +1926,8 @@ class Telegram { risp = 'Siiiii ! Davvero! ' + emo.DREAM; } else if (MsgBot.PAROLACCE.find((rec) => testo.indexOf(rec) > -1)) { risp = "Da te non me l'aspettavo proprio !! " + emo.INNOCENT + emo.CROSS_ROSSA; - } else if (MsgBot.OK.find((rec) => testo.indexOf(rec) > -1)) { - risp = '👍🏻'; + // } else if (MsgBot.OK.find((rec) => testo.indexOf(rec) > -1)) { + // risp = '👍🏻'; } else if (MsgBot.CUORE.find((rec) => testo.indexOf(rec) > -1)) { risp = '❤️💚💜'; } else if (MsgBot.HAHA.find((rec) => testo.indexOf(rec) > -1) && testo.length < 8) { @@ -3308,9 +3329,25 @@ class Telegram { console.log(recuser.username, " SI E' VERIFICATO CON TELEGRAM !"); let username = recuser.name; + if (!msg.from.username) { + rec.cmdAfterVerified === shared_consts.CallFunz.VERIFICA_TELEGRAM; + } else { + await this.verificaTelegramCompleted(msg); + } + } + } + } + + async verificaTelegramCompleted(msg) { + try { + const rec = this.getRecInMem(msg); + const id = msg.chat.id; + const recuser = this.getRecInMemById(id); + + if (recuser) { await User.setUsernameTelegram( this.idapp, - recuser._id, + recuser.user._id, msg.from.username || '', msg.from.first_name || '', msg.from.last_name || '' @@ -3320,12 +3357,12 @@ class Telegram { rec.datemenu_updated = null; rec.menuDb = null; - if (!!msg.from.username) { - await MyTelegramBot.askConfirmationUser(this.idapp, shared_consts.CallFunz.REGISTRATION, recuser); - } else { - console.log(" ... MA GLI MANCA L'USERNAME TELEGRAM !! "); - } + const user = await User.UserByIdTelegram(this.idapp, msg.chat.id); + + await MyTelegramBot.askConfirmationUser(this.idapp, shared_consts.CallFunz.VERIFICA_TELEGRAM, user); } + } catch (e) { + console.error(e); } } @@ -4557,6 +4594,9 @@ if (true) { `${userDest.username}` ); + // Invia una email alla persona che è stata ammessa + + await local_sendMsgTelegram(user.idapp, data.username, msgOrig); await local_sendMsgTelegram(user.idapp, data.userDest, msgDest); // Invia questo msg anche all'Admin diff --git a/src/tools/general.js b/src/tools/general.js index 2a34c15..c4972d2 100755 --- a/src/tools/general.js +++ b/src/tools/general.js @@ -467,6 +467,7 @@ module.exports = { AYNI: '7', CNM: '10', RISO: '13', + RISO_STR_PATH: 'RISO', FIOREDELLAVITA: '15', PIUCHEBUONO: '17', MACRO: '18', diff --git a/src/tools/server_constants.js b/src/tools/server_constants.js index abc9060..d7a5f14 100755 --- a/src/tools/server_constants.js +++ b/src/tools/server_constants.js @@ -30,7 +30,7 @@ module.exports = Object.freeze({ RIS_CODE_HTTP_INVALID_TOKEN: 401, RIS_CODE_HTTP_FORBIDDEN_PERMESSI: 403, - RIS_CODE_HTTP_FORBIDDEN_TOKEN_EXPIRED: 408, + RIS_CODE_HTTP_TOKEN_EXPIRED: 408, RIS_CODE_TOKEN_RESETPASSWORD_NOT_FOUND: -23, diff --git a/src/tools/shared_nodejs.js b/src/tools/shared_nodejs.js index 9cfa9d6..de83c6c 100755 --- a/src/tools/shared_nodejs.js +++ b/src/tools/shared_nodejs.js @@ -80,6 +80,7 @@ module.exports = { UNBLOCK_USER: 156, REPORT_USER: 158, FIND_PEOPLE: 166, + DELETE_USER: 170, }, GROUPSCMD: { @@ -595,6 +596,7 @@ module.exports = { MS_SHARE_LINK: 2000, MSG_BENV_REGISTRATO: 2020, MSG_INVITE_WHATSAPP: 2040, + MSG_VERIFICA_TELEGRAM_COMPLETATA: 2050, }, TypeSend: { diff --git a/src/validators/UserValidators.js b/src/validators/UserValidators.js new file mode 100644 index 0000000..c64d6ba --- /dev/null +++ b/src/validators/UserValidators.js @@ -0,0 +1,186 @@ + +const tools = require('../tools/general'); +const server_constants = require('../tools/server_constants'); + +/** + * Validate registration data + */ +function validateRegistration(body) { + const { username, email, password, name, surname } = body; + + // Check required fields + if (!username || !email || !password) { + return { + code: server_constants.RIS_CODE_ERR, + message: 'Campi obbligatori mancanti' + }; + } + + // Check minimum lengths + if (email.length < 6) { + return { + code: server_constants.RIS_CODE_ERR, + message: 'Email troppo corta' + }; + } + + if (username.length < 4) { + return { + code: server_constants.RIS_CODE_USERNAME_NOT_VALID, + message: 'Username deve essere almeno 4 caratteri' + }; + } + + if (password.length < 5) { + return { + code: server_constants.RIS_CODE_ERR, + message: 'Password deve essere almeno 5 caratteri' + }; + } + + // Check username format + if (!tools.isAlphaNumericAndSpecialCharacter(username)) { + return { + code: server_constants.RIS_CODE_USERNAME_NOT_VALID, + message: 'Username contiene caratteri non validi' + }; + } + + // Check for blocked words + if (tools.blockwords(username) || tools.blockwords(name) || tools.blockwords(surname)) { + return { + code: server_constants.RIS_CODE_ERR, + message: 'Contenuto non valido' + }; + } + + return null; +} + +/** + * Validate login data + */ +function validateLogin(body) { + const { username, password, idapp } = body; + + // Check required fields + if (!username || !password || !idapp) { + return { + code: server_constants.RIS_CODE_ERR, + message: 'Campi obbligatori mancanti' + }; + } + + // Check minimum lengths + if (username.length < 3) { + return { + code: server_constants.RIS_CODE_LOGIN_ERR, + message: 'Username troppo corto' + }; + } + + if (password.length < 4) { + return { + code: server_constants.RIS_CODE_LOGIN_ERR, + message: 'Password troppo corta' + }; + } + + return null; +} + +/** + * Validate email format + */ +function validateEmail(email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +} + +/** + * Validate phone number format + */ +function validatePhoneNumber(phone) { + if (!phone) return true; // Optional field + + // Remove spaces and special characters + const cleanPhone = phone.replace(/[\s\-\(\)]/g, ''); + + // Check if it's numeric and reasonable length + return /^\+?\d{8,15}$/.test(cleanPhone); +} + +/** + * Validate user profile data + */ +function validateProfileData(profileData) { + const errors = []; + + if (profileData.email && !validateEmail(profileData.email)) { + errors.push('Email non valida'); + } + + if (profileData.cell && !validatePhoneNumber(profileData.cell)) { + errors.push('Numero di telefono non valido'); + } + + if (profileData.username) { + if (profileData.username.length < 4) { + errors.push('Username deve essere almeno 4 caratteri'); + } + if (!tools.isAlphaNumericAndSpecialCharacter(profileData.username)) { + errors.push('Username contiene caratteri non validi'); + } + } + + return errors.length > 0 ? { + code: server_constants.RIS_CODE_ERR, + message: errors.join(', ') + } : null; +} + +/** + * Validate permissions value + */ +function validatePermissions(perm) { + const validPermissions = [0, 1, 2, 3, 4, 5]; // Based on your Perm enum + return validPermissions.includes(perm); +} + +/** + * Validate circuit/group command + */ +function validateCommand(cmd, validCommands) { + return validCommands.includes(cmd); +} + +/** + * Sanitize user input (remove dangerous characters) + */ +function sanitizeInput(input) { + if (typeof input !== 'string') return input; + + return input + .trim() + .replace(/[<>]/g, '') // Remove potential XSS + .replace(/['";]/g, ''); // Remove SQL injection chars +} + +/** + * Validate MongoDB ObjectId + */ +function validateObjectId(id) { + return /^[0-9a-fA-F]{24}$/.test(id); +} + +module.exports = { + validateRegistration, + validateLogin, + validateEmail, + validatePhoneNumber, + validateProfileData, + validatePermissions, + validateCommand, + sanitizeInput, + validateObjectId +}; \ No newline at end of file diff --git a/src/version.txt b/src/version.txt index 3c9b023..8acd4cd 100644 --- a/src/version.txt +++ b/src/version.txt @@ -1 +1 @@ -1.2.79 \ No newline at end of file +1.2.84 \ No newline at end of file