47 Commits

Author SHA1 Message Date
Surya Paolo
3d87c336de - aggiornamento di tante cose...
- generazione Volantini
- pagina RIS
2025-12-17 10:07:51 +01:00
Surya Paolo
037ff6f7f9 - verifica email se non è stata verificata (componente)
- altri aggiornamenti grafica PAGERIS.
- OLLAMA AI
2025-12-12 00:44:12 +01:00
Surya Paolo
b8dcd7f5e0 categorie 2025-12-07 15:17:42 +01:00
Surya Paolo
b35c99c8fb ok 2025-12-07 10:48:08 +01:00
Surya Paolo
086e4ab8ba - aggiornati anche i ContribTypes che appaiono sulla card degli annunci 2025-12-07 10:26:35 +01:00
Surya Paolo
139d3fe241 - Aggiornate tutte le categorie ottimizzandole.
- Migrazione delle vecchie categ. con quelle nuove.
- Create le Categorie e sottocategorie degli Eventi (a parte).
- Aggiornato la card dell'Ospitalità
2025-12-07 02:13:26 +01:00
Surya Paolo
6fe3ed7c8b - aggiornamenti guida RIS, FAQ
- Editor HTML aggiunto CSS e Script
- Statistiche
- CRISBalanceBar
- Inizio Sync... (ma disattivato)
2025-12-02 22:16:29 +01:00
Surya Paolo
81ae2df8ef - aggiornato Card service, e CGridTableRec. 2025-11-28 21:28:38 +01:00
Surya Paolo
997a7b8b98 - email Abilitazione Circuito RISO 2025-11-28 18:53:43 +01:00
Surya Paolo
331a5451b2 - fix zona provinciale
- email abilitazione circuito: invio email ad admin
- admin che abilita la fiducia cliccando sul bottone
2025-11-27 23:51:48 +01:00
Surya Paolo
514c2488cc - comune residenza anche sulla email
- comune non obbligatorio... Skippa
2025-11-27 03:15:01 +01:00
Surya Paolo
33e51bac0e - comune residenza anche sulla email 2025-11-27 01:28:25 +01:00
Surya Paolo
c46d23cb83 - corretto altre cose sulla registrazione 2025-11-27 01:12:05 +01:00
Surya Paolo
acb685f819 - aggiunto il Comune di Residenza alla REgistrazione e al Tutorial 2025-11-27 00:48:58 +01:00
Surya Paolo
18790ee379 - Corretto campo "Comune di Residenza".
- Aggiornato Completamento Profilo: Comune di Residenza.
- Registrazione
2025-11-26 18:32:11 +01:00
Surya Paolo
70698fab44 - primo aggiornamento myreccard
- aggiunta sito germogliamo.app
- aggiornato login con il parametro "browser_random" che serve per fare un login anche su 2 pagine contemporaneamente.
2025-11-25 17:45:24 +01:00
Surya Paolo
c61572a715 - aggiornati form registrazione
- Login
- Password dimenticata
- Aggiorna password.
- Email registrazione
- Ammetti Utente
2025-11-24 17:42:56 +01:00
Surya Paolo
00bdc278d8 - aggiornato la guida per installare la App
- aggiornato la Guida Completa e Breve di RISO.
- pagina per ricevere i RIS.
- sistemato problema creazione nuovi Circuiti (admin non corretti).
- corretto giro delle email, invitante, invitato e ricezione msg su telegram.
2025-11-23 01:13:32 +01:00
Surya Paolo
5b1f3eafbc - sistemazioni Email : registrazione, invio invito, email di benvenuto
- fix circuito
- profilo
2025-11-21 20:47:30 +01:00
Surya Paolo
233c5fa28e - risolto problema sull'attivazione del Circuito ITA. non arrivava il messaggio
- sistemazioni sul profilo
2025-11-19 19:44:27 +01:00
Surya Paolo
aa877dea0a - asggiunto bottone "installa app" sulla email di conferma registrazione e anche la Guida "/guida".
- migliorato InvitaAmico
2025-11-19 11:39:42 +01:00
Surya Paolo
657dc79996 - invita amico 2025-11-19 10:09:51 +01:00
Surya Paolo
294155d5a3 - Sistemato INVITI alla App
- Completamento Profilo
- Registrazione tramite Invito, senza richiedere conferma email.
2025-11-18 23:56:15 +01:00
Surya Paolo
1a342de24a fix: se aggiungi XLS non si aggiornava la lista correttamente, non ricaricava... 2025-11-18 13:08:53 +01:00
Surya Paolo
00ce3bd919 corretto bug GruppoMacro la lista veniva salvata ma anche aggiornata in memoria con solo gli ID... in questo caso TABLES_NON_AGGIORNARE_IN_MEMORIA_PERCHE_DIVERSA_STRUTTURA gli dice che alcune tabelle non devono essere aggiornate in memoria. 2025-11-18 11:19:40 +01:00
Surya Paolo
adf1aac10f - aggiornata la grafica della Home di RISO
- Profilo Completition
- Email Verificata
- Invita un Amico (invio di email)
2025-11-15 19:38:55 +01:00
Surya Paolo
26a42b1f30 - versione: 1.2.77
- aggiustamenti e miglioramenti estetici
2025-11-03 14:24:33 +01:00
Surya Paolo
71fe2ae657 - risolto problema spazi vuoti in cima alla app
- risolto problema sulle card di telegram, immagini non alte uguali e non si vedono bene...
2025-11-02 21:16:48 +01:00
Surya Paolo
8d1dd45648 - check updates
- risolto problema della generazione dei PDF, avevo modificato in CMyPageElem , se si cambia qualcosa occorre stare attenti a mettere !hideHeader
2025-11-01 12:00:57 +01:00
Surya Paolo
38c13eef28 - aggiunto Elena come admin di tutti i circuiti ...
shared_consts.USER_ADMIN_CIRCUITS
2025-10-27 13:34:06 +01:00
Surya Paolo
8f54cd2791 - corretto problema ROGNOSO : Risolvere la questione "Sessioni multiple", se apro 2 browser l'ultimo va a cancellare il precedente, e mi da errore di email non valida !
Il problema era sulla fetch nel service worker, gestita in quel modo personalizzato, andava in conflitto, non tenendo le chiamate bloccanti, ma uscivano prima che arrivasse la risposta del server.
- Per chi è da tanto che non si collega a RISO, compare "Email non verificata"... (si risolve chiudendo su ESCI e riloggandosi)... però andrebbe sistemata.
(stesso problema di prima).
2025-10-26 02:48:07 +02:00
Surya Paolo
610961d22c - corretto problema sulle Organizzazioni, non si correttamente chiedevano
- DataInizio e DataFine: creare un componente che controlli la data inizio e fine...
2025-10-11 19:51:59 +02:00
Surya Paolo
3f7eda05cf - Sistemato problema del Circuito ITALIA, quando veniva fatta la richiesta di entrare, ancora non si era entrati nel circuito territoriale.
- Ora pertanto viene inviata la richiesta agli admin solo dopo che l'utente viene abilitato al Circuito provinciale.
2025-10-11 18:36:11 +02:00
Surya Paolo
ebfd80da76 - corretto componente CDateTime
- aggiunto componente CDateTimeStartEnd
2025-10-05 23:59:03 +02:00
Surya Paolo
ede8a2db3d - aggiornamento 1.2.71 2025-10-05 12:17:45 +02:00
Surya Paolo
4e37475d00 - Gestore Ordini GAS (aggiornato)
- Possibilità di modificare un record, click sulla matita rossa.
- corretto altre sistemazioni sui valori di minimo e massimo quantità.
2025-10-03 16:28:53 +02:00
Surya Paolo
6048cd526b Versione 1.2.71:
- sistemato il carrello su GruppoMacro e su PiuCheBuono.
- Corretto visualizzazione della scontistica.
- Se un prodotto viene cancellato ora lo cancella anche sul carrello.
2025-10-02 16:17:57 +02:00
Surya Paolo
1d52ab1d08 - corretto gruppomacro catalogo, info prodotti, estrazione dati da amazon corretto. 2025-09-27 17:24:46 +02:00
Surya Paolo
08cf4b6d9f - aggiunto componenti per Home Template... ma ancora da provare
- sistemato catprods
- Sistemato menu
2025-09-22 19:09:02 +02:00
Surya Paolo
4a05ddee50 - aggiunto FeaturesSection all'editor HTML 2025-09-17 01:21:46 +02:00
Surya Paolo
c37280fa4a Merge branch 'ChatBox' of http://95.216.147.38:3000/surya/freeplanet_serverside into ChatBox 2025-09-16 17:30:40 +02:00
Surya Paolo
d00e086646 - gestione dell'editor delle pagine (non funzionante!) 2025-09-16 17:30:37 +02:00
Surya Paolo
973875ca66 - migliorata la grafica dell'aggiungi elemento. 2025-09-08 20:42:42 +02:00
Surya Paolo
03c2bcb78e - Editor Pagine Elementi: Sezione, Righe, Colonne, Elementi. (rows, columns, elems) 2025-09-08 01:02:39 +02:00
Surya Paolo
a2dd06bd68 - inizio di modifiche all'editor di Pagine Web 2025-09-05 01:06:46 +02:00
Surya Paolo
7ba408c053 - creato editor di Pagine (iniziato)
- fix: mancano i "t," su alcuni componenti...
2025-09-02 16:22:31 +02:00
Surya Paolo
fcbc64cea8 - Modifiche a ProductInfo... Continua 2025-08-29 23:34:08 +02:00
410 changed files with 32436 additions and 8460 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -30,7 +30,7 @@ PROD=0
PROJECT_DESCR_MAIN='__PROJECTS'
SECRK=Askb38v23jjDFaoskBOWj92axXCQ
TOKEN_LIFE=1m
REFRESH_TOKEN_LIFE=14d
REFRESH_TOKEN_LIFE=30d
FTPSERVER_HOST=139.162.166.31
FTPSERVER_PORT=21
FTPSERVER_USER=ftpusrsrv_

View File

@@ -29,8 +29,8 @@ GCM_API_KEY=""
PROD=0
PROJECT_DESCR_MAIN='__PROJECTS'
SECRK=Askb38v23jjDFaoskBOWj92axXCQ
TOKEN_LIFE=1m
REFRESH_TOKEN_LIFE=14d
TOKEN_LIFE=30d
REFRESH_TOKEN_LIFE=30d
FTPSERVER_HOST=139.162.166.31
FTPSERVER_PORT=21
FTPSERVER_USER=ftpusrsrv_
@@ -39,3 +39,9 @@ AUTH_NEW_SITES=123123123
SCRIPTS_DIR=admin_scripts
CLOUDFLARE_TOKENS=[{"label":"Paolo.arena77@gmail.com","value":"M9EM309v8WFquJKpYgZCw-TViM2wX6vB3wlK6GD0"},{"label":"gruppomacro.com","value":"bqmzGShoX7WqOBzkXocoECyBkPq3GfqcM5t6VFd8"}]
DS_API_KEY="sk-222e3addb3d8455d8b0516d93906eec7"
OLLAMA_URL=http://localhost:11434
OLLAMA_DEFAULT_MODEL=llama3.2:3b
GROK_API="xai-PcNM5obgPaETtmnfDWPZk235D75ZgxENU2QmeqPfMQCHh9dwCDVeRRe0oVVA2YOpiUDh1uJieZsMasja"
REPLICATE_API_TOKEN="r8_AVhM6igwvoOnUA65cHVZdhEDfTqBVk94WTB0u"
FAL_KEY="7d251c88-21b5-4b55-8b3e-4bafd910f99f:b81c0a36a25b052f26eb8ac226c7efff"
HF_TOKEN="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"

View File

@@ -30,11 +30,11 @@ PATH_SERVER_CRT=fullchain.pem
PROD=1
PROJECT_DESCR_MAIN='__PROJECTS'
SECRK=iUUb38v23jjDFaosWj92axkBOXCQ
TOKEN_LIFE=2h
REFRESH_TOKEN_LIFE=14d
TOKEN_LIFE=30d
REFRESH_TOKEN_LIFE=30d
AUTH_NEW_SITES=B234HDSAOJ734ndcsdKWNVZZ
DOMAINS=[{"hostname":"piuchebuono.app","port":"3030"},{"hostname":"gruppomacro.app","port":"3010"}]
DOMAINS_ALLOWED=[]
DOMAINS_ALLOWED=["gruppomacro.app","piuchebuono.app"]
SCRIPTS_DIR=admin_scripts
CLOUDFLARE_TOKENS=[{"label":"Paolo.arena77@gmail.com","value":"M9EM309v8WFquJKpYgZCw-TViM2wX6vB3wlK6GD0"},{"label":"gruppomacro.com","value":"bqmzGShoX7WqOBzkXocoECyBkPq3GfqcM5t6VFd8"}]
MIAB_HOST=box.lamiaposta.org

View File

@@ -27,11 +27,12 @@ PATH_SERVER_CRT=fullchain.pem
PROD=1
PROJECT_DESCR_MAIN='__PROJECTS'
SECRK=iUUb38v23jjDFaosWj92axkBOXCQ
TOKEN_LIFE=2h
REFRESH_TOKEN_LIFE=14d
TOKEN_LIFE=30d
REFRESH_TOKEN_LIFE=30d
AUTH_NEW_SITES=B234HDSAOJ734ndcsdKWNV
DOMAINS=[{"hostname":"riso.app","port":"3006"},{"hostname":"freeplanet.app","port":"3000"}]
DOMAINS_ALLOWED=["comunitanuovomondo.app","kolibrilab.it","artenergetica.org","freeplanet.app","www.freeplanet.app","freeplanet.app:3000","freeplanet.app:3001","www.freeplanet.app:3000","www.freeplanet.app:3001"]
DOMAINS=[{"hostname":"riso.app","port":"3006"},{"hostname":"freeplanet.app","port":"3000"},{"hostname":"nuovomondo.app","port":"3032"}]
DOMAINS_NEW=[{"hostname":"riso.app","port":"3006"},{"hostname":"freeplanet.app","port":"3000"},{"hostname":"nuovomondo.app","port":"3032"},{"hostname":"germogliamo.app","port":"3042"}]
DOMAINS_ALLOWED=["riso.app","germogliamo.app","comunitanuovomondo.app","nuovomondo.app","kolibrilab.it","artenergetica.org","freeplanet.app","www.freeplanet.app","freeplanet.app:3000","freeplanet.app:3001","www.freeplanet.app:3000","www.freeplanet.app:3001"]
#DOMAINS=[{"hostname":"abitaregliiblei.it","port":"3021"},{"hostname":"riso.app","port":"3005"}]
SCRIPTS_DIR=admin_scripts
CLOUDFLARE_TOKENS=[{"label":"Paolo.arena77@gmail.com","value":"M9EM309v8WFquJKpYgZCw-TViM2wX6vB3wlK6GD0"},{"label":"gruppomacro.com","value":"bqmzGShoX7WqOBzkXocoECyBkPq3GfqcM5t6VFd8"}]

View File

@@ -31,8 +31,8 @@ SECRK=iUUb38v23jjDFaosWj92axkBOXCQ
TOKEN_LIFE=2h
REFRESH_TOKEN_LIFE=14d
AUTH_NEW_SITES=B234HDSAOJ734ndcsdKWNV
DOMAINS=[{"hostname":"riso.app","port":"3005"}]
DOMAINS_ALLOWED=["comunitanuovomondo.app","kolibrilab.it","artenergetica.org","freeplanet.app","www.freeplanet.app","freeplanet.app:3000","freeplanet.app:3001","www.freeplanet.app:3000","www.freeplanet.app:3001"]
DOMAINS=[{"hostname":"riso.app","port":"3005"},{"hostname":"nuovomondo.app","port":"3033"}]
DOMAINS_ALLOWED=["riso.app","comunitanuovomondo.app","kolibrilab.it","artenergetica.org","freeplanet.app","www.freeplanet.app","freeplanet.app:3000","freeplanet.app:3001","www.freeplanet.app:3000","www.freeplanet.app:3001"]
SCRIPTS_DIR=admin_scripts
CLOUDFLARE_TOKENS=[{"label":"Paolo.arena77@gmail.com","value":"M9EM309v8WFquJKpYgZCw-TViM2wX6vB3wlK6GD0"},{"label":"gruppomacro.com","value":"bqmzGShoX7WqOBzkXocoECyBkPq3GfqcM5t6VFd8"}]
MIAB_HOST=box.lamiaposta.org

40
.vscode/launch.json vendored
View File

@@ -1,7 +1,7 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
@@ -13,7 +13,7 @@
"--inspect=9229" // Use "--inspect=0.0.0.0:9229" for remote debugging
],
"args": [
"${workspaceFolder}/src/server/server.js"
"${workspaceFolder}/src/server.js"
], // Replace with your entry file
"cwd": "${workspaceFolder}",
"autoAttachChildProcesses": true,
@@ -36,7 +36,7 @@
"--trace-warnings" // Use "--inspect=0.0.0.0:9229" for remote debugging
],
"args": [
"${workspaceFolder}/src/server/server.js"
"${workspaceFolder}/src/server.js"
], // Replace with your entry file
"cwd": "${workspaceFolder}",
"autoAttachChildProcesses": true,
@@ -50,7 +50,7 @@
},
{
"name": "ServerSide",
"program": "${workspaceFolder}/src/server/server.js",
"program": "${workspaceFolder}/src/server.js",
"request": "launch",
"skipFiles": [
"<node_internals>/**"
@@ -61,5 +61,37 @@
"TESTING_ON": "1"
}
},
{
"type": "node",
"request": "launch",
"name": "Debug CheckSmartBot",
"program": "${workspaceFolder}/../checksmartbot/bot.js",
"console": "integratedTerminal",
"outFiles": [
"${workspaceFolder}/../**/*.js"
],
"envFile": "${workspaceFolder}/.env",
"skipFiles": [
"<node_internals>/**"
],
"autoAttachChildProcesses": true,
"smartStep": true
},
{
"type": "node",
"request": "launch",
"name": "Avvia bot con pm2 (attach)",
"runtimeExecutable": "pm2",
"runtimeArgs": [
"start",
"bot.js",
"--name",
"checksmartbot"
],
"console": "integratedTerminal",
"restart": true,
"port": 9229,
"attach": true
}
]
}

View File

@@ -9,7 +9,7 @@ yarn
### Start the app in development mode (hot-code reloading, error reporting, etc.)
```bash
node src/server/server.js
node src/server.js
```
### Creating the ambient test

View File

@@ -66,7 +66,7 @@ db.myelems.insertMany([
"listcards": [],
"list": [],
"__v": 0,
"containerHtml": "<style>\nbody {\n    font-family: Arial, sans-serif;\n    margin: 0;\n    padding: 20px;\n    background-color: #f0f0f0;\n    color: #333;\n}\n\nh1 {\n    color: #0056b3;\n    text-align: center;\n}\n\n\np, li {\n    line-height: 1.6;\n}\n\n\nul {\n    list-style-type: none;\n    padding: 0;\n}\n\n\nli {\n    background-color: #fff !important;\n    margin-bottom: 10px !important;\n    padding: 10px !important;\n    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important; \n}\n\n\na {\n    color: #007bff;\n    text-decoration: none;\n}\n\n\na:hover {\n    text-decoration: underline;\n}\n\n\n.container {\n    max-width: 800px;\n    margin: 0 auto;\n    background-color: #fff;\n    padding: 20px;\n    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);\n}\n\n\n.strong {\n    font-weight: bold;\n}\n</style>\n\n\n<p><strong>“Abitare gli Iblei”</strong> è una rete aperta che ha lo scopo di riunire tutte quelle persone che vogliono valorizzare e qualificare la vita nel territorio degli Iblei.&nbsp;</p>\n&nbsp; &nbsp;&nbsp;\n&nbsp; &nbsp; <p>Chi aderisce alla rete si riconosce in una <strong>Carta dei valori comuni</strong> e usa la rete per scambiare conoscenze, esperienze, risorse e prodotti sviluppati nellambito delle proprie iniziative (profit e non profit) individuali o collettive.</p>\n\n\n&nbsp; &nbsp; <p>Larea territoriale di questa rete è quella dei <strong>Monti Iblei orientali e occidentali</strong> (Noto, Avola, Canicattini, Siracusa, Palazzolo, Buccheri, Ferla, Modica, …).</p>\n\n\n&nbsp; &nbsp; <p>La rete <strong>“Abitare gli Iblei”</strong> offre i seguenti servizi utili per il territorio ed i suoi abitanti, frutto di una costruzione collettiva:</p>\n&nbsp; &nbsp;&nbsp;\n&nbsp; &nbsp; <ul>\n&nbsp; &nbsp; &nbsp; &nbsp; <li><strong>1. Mappa delle attività virtuose:</strong> permette di identificare attività pubbliche e private nel territorio che possono essere utili nella vita quotidiana. Queste attività possono riguardare artigiani, produttori o fornitori di servizi di cui almeno un membro della rete conosca la qualità e laffidabilità (agricoltori, falegnami, fabbri, idraulici, imprese edili, strutture ricettive, …). Altre informazioni utili possono riguardare associazioni/istituzioni operanti in vari settori. <strong>Accesso pubblico</strong></li>\n&nbsp; &nbsp; &nbsp; &nbsp; <li><strong>2. Calendario:</strong> permette di accedere ad annunci di eventi utili alla crescita culturale del territorio. La pubblicazione di eventi è riservata ai soli membri della rete che possono presentare iniziative anche di altri organizzatori. <strong>Accesso pubblico</strong></li>\n&nbsp; &nbsp; &nbsp; &nbsp; <li><strong>3. Scambi di servizi, prodotti e ospitalità:</strong> questa funzione è riservata ai soli membri della rete e si realizza attraverso la Rete italiana di scambi orizzontali (RISO). <strong>Accesso riservato</strong></li>\n&nbsp; &nbsp; &nbsp; &nbsp; <li><strong>4. Segnalazione di pericoli per il territorio:</strong> attraverso questa mappa è possibile segnalare incendi, immondizia abbandonata, discariche abusive, fonti di inquinamento per corsi dacqua e spiagge, presenza di inquinamento nellaria, … <strong>Accesso riservato</strong></li>\n&nbsp; &nbsp; </ul>\n&nbsp; &nbsp;&nbsp;\n&nbsp; &nbsp; <p>Se vuoi aderire alla rete puoi richiederne la registrazione utilizzando questo Link <a href=\"#\"><strong>(Pagina in Costruzione)</strong></a>.</p>\n\n",
"containerHtml": "<style>\nbody {\n    font-family: Arial, sans-serif;\n    margin: 0;\n    padding: 20px;\n    background-color: #f0f0f0;\n    color: #333;\n}\n\nh1 {\n    color: #0056b3;\n    text-align: center;\n}\n\n\np, li {\n    line-height: 1.6;\n}\n\n\nul {\n    list-style-type: none;\n    padding: 0;\n}\n\n\nli {\n    background-color: #fff !important;\n    margin-bottom: 10px !important;\n    padding: 10px !important;\n    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important; \n}\n\n\na {\n    color: #007bff;\n    text-decoration: none;\n}\n\n\na:hover {\n    text-decoration: underline;\n}\n\n\n.container {\n    max-width: 800px;\n    margin: 0 auto;\n    background-color: #fff;\n    padding: 20px;\n    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);\n}\n\n\n.strong {\n    font-weight: bold;\n}\n</style>\n\n\n<p><strong>“Abitare gli Iblei”</strong> è una rete aperta che ha lo scopo di riunire tutte quelle persone che vogliono valorizzare e qualificare la vita nel territorio degli Iblei.&nbsp;</p>\n&nbsp; &nbsp;&nbsp;\n&nbsp; &nbsp; <p>Chi aderisce alla rete si riconosce in una <strong>Carta dei valori comuni</strong> e usa la rete per scambiare conoscenze, esperienze, risorse e prodotti sviluppati nellambito delle proprie iniziative (profit e non profit) individuali o collettive.</p>\n\n\n&nbsp; &nbsp; <p>Larea territoriale di questa rete è quella dei <strong>Monti Iblei orientali e occidentali</strong> (Noto, Avola, Canicattini, Siracusa, Palazzolo, Buccheri, Ferla, Modica, …).</p>\n\n\n&nbsp; &nbsp; <p>La rete <strong>“Abitare gli Iblei”</strong> offre i seguenti servizi utili per il territorio ed i suoi abitanti, frutto di una costruzione collettiva:</p>\n&nbsp; &nbsp;&nbsp;\n&nbsp; &nbsp; <ul>\n&nbsp; &nbsp; &nbsp; &nbsp; <li><strong>1. Mappa delle attività virtuose:</strong> permette di identificare attività pubbliche e private nel territorio che possono essere utili nella vita quotidiana. Queste attività possono riguardare artigiani, produttori o fornitori di servizi di cui almeno un membro della rete conosca la qualità e laffidabilità (agricoltori, falegnami, fabbri, idraulici, imprese edili, strutture ricettive, …). Altre informazioni utili possono riguardare associazioni/istituzioni operanti in vari settori. <strong>Accesso pubblico</strong></li>\n&nbsp; &nbsp; &nbsp; &nbsp; <li><strong>2. Calendario:</strong> permette di accedere ad annunci di eventi utili alla crescita culturale del territorio. La pubblicazione di eventi è riservata ai soli membri della rete che possono presentare iniziative anche di altri organizzatori. <strong>Accesso pubblico</strong></li>\n&nbsp; &nbsp; &nbsp; &nbsp; <li><strong>3. Scambi di servizi, prodotti e ospitalità:</strong> questa funzione è riservata ai soli membri della rete e si realizza attraverso la Rete Italiana scambio orizzontale (RISO). <strong>Accesso riservato</strong></li>\n&nbsp; &nbsp; &nbsp; &nbsp; <li><strong>4. Segnalazione di pericoli per il territorio:</strong> attraverso questa mappa è possibile segnalare incendi, immondizia abbandonata, discariche abusive, fonti di inquinamento per corsi dacqua e spiagge, presenza di inquinamento nellaria, … <strong>Accesso riservato</strong></li>\n&nbsp; &nbsp; </ul>\n&nbsp; &nbsp;&nbsp;\n&nbsp; &nbsp; <p>Se vuoi aderire alla rete puoi richiederne la registrazione utilizzando questo Link <a href=\"#\"><strong>(Pagina in Costruzione)</strong></a>.</p>\n\n",
"anim": {
"_id": new ObjectId("66db393e3b885ccdfaed28d6"),
"name": "",

View File

@@ -11,9 +11,9 @@ db.mypages.insertMany([
"_id": new ObjectId("66db21118009ea4503bb6a03"),
"order": 10,
"idapp": "19",
"path": "home_logout",
"path": "presentazione",
"active": true,
"title": "Home NoLoggato",
"title": "Presentazione",
},
{
"_id": new ObjectId("66e322dd5a6360e3b3c71c5a"),

View File

@@ -164,7 +164,9 @@ db.users.insertMany([
"stepTutorial": 0,
"noNameSurname": false,
"noCircuit": false,
"noComune": false,
"noCircIta": false,
"insert_circuito_ita": false,
"noFoto": false,
"resid_province": "",
"resid_card": "",
@@ -281,7 +283,9 @@ db.users.insertMany([
"stepTutorial": 0,
"noNameSurname": false,
"noCircuit": false,
"noComune": false,
"noCircIta": false,
"insert_circuito_ita": false,
"noFoto": false,
"resid_province": "",
"resid_card": "",

1
aggiornaserver.txt Executable file
View File

@@ -0,0 +1 @@
Aggiornamento Server...

View File

@@ -72,6 +72,11 @@ if [[ $risposta == "Y" || $risposta == "y" ]]; then
rsync -avz -e "ssh $SSH_OPTIONS" \
$CONFIG_JS "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/ecosystem.config.js"
echo ""
echo "*** Copia del file aggiornaserver.txt ... "
rsync -avz -e "ssh $SSH_OPTIONS" \
aggiornaserver.txt "$REMOTE_USER@$REMOTE_HOST:/opt/scripts/aggiornaserver_pcb_prod.txt"
# Verifica il risultato
if [ $? -eq 0 ]; then
echo "Sincronizzazione completata con successo. SERVER PCB PRODUZIONE! "

View File

@@ -72,6 +72,11 @@ if [[ $risposta == "Y" || $risposta == "y" ]]; then
rsync -avz -e "ssh $SSH_OPTIONS" \
$CONFIG_JS "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/ecosystem.config.js"
echo ""
echo "*** Copia del file aggiornaserver.txt ... "
rsync -avz -e "ssh $SSH_OPTIONS" \
aggiornaserver.txt "$REMOTE_USER@$REMOTE_HOST:/opt/scripts/aggiornaserver_pcb_test.txt"
# Verifica il risultato
if [ $? -eq 0 ]; then
echo "Sincronizzazione completata con successo. SERVER PCB TEST! "

View File

@@ -67,6 +67,12 @@ if [[ $risposta == "Y" || $risposta == "y" ]]; then
rsync -avz -e "ssh $SSH_OPTIONS" \
"$CONFIG_JS" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/ecosystem.config.js"
echo ""
echo "*** Copia del file aggiornaserver.txt ... "
rsync -avz -e "ssh $SSH_OPTIONS" \
aggiornaserver.txt "$REMOTE_USER@$REMOTE_HOST:/opt/scripts/aggiornaserver_riso_prod.txt"
# Verifica il risultato
if [ $? -eq 0 ]; then
echo "✅ Sincronizzazione completata con successo. SERVER $REMOTE_DIR! "

View File

@@ -75,6 +75,11 @@ if [[ $risposta == "Y" || $risposta == "y" ]]; then
rsync -avz -e "ssh $SSH_OPTIONS" \
$CONFIG_JS "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/ecosystem.config.js"
echo ""
echo "*** Copia del file aggiornaserver.txt ... "
rsync -avz -e "ssh $SSH_OPTIONS" \
aggiornaserver.txt "$REMOTE_USER@$REMOTE_HOST:/opt/scripts/aggiornaserver_riso_test.txt"
# Verifica il risultato
if [ $? -eq 0 ]; then

View File

@@ -0,0 +1,4 @@
Define MIODOMINIO riso.app
Define MIODOMINIO_COMPLETO test.riso.app
Define MIOURL_API testapi.riso.app
Define PORTA 3005

View File

@@ -0,0 +1,30 @@
<VirtualHost 65.108.222.97:80>
ServerName ${MIODOMINIO_COMPLETO}
RewriteEngine On
# Forza la connessione HTTPS
RewriteCond %{HTTPS} off
RewriteRule ^ https://${MIODOMINIO_COMPLETO}%{REQUEST_URI} [L,R=301]
</VirtualHost>
<VirtualHost 65.108.222.97:443>
ServerName ${MIODOMINIO_COMPLETO}
DocumentRoot /var/www/${MIODOMINIO_COMPLETO}
ServerAdmin surya@riso.app
SSLEngine On
SSLProtocol -ALL +TLSv1.2
SSLCompression Off
SSLHonorCipherOrder off
SSLCipherSuite ECDH+AESGCM:ECDH+AES256:!aNULL:!MD5:!DSS:!DH:!AES128
SSLVerifyDepth 10
SSLCertificateFile /etc/letsencrypt/live/${MIODOMINIO}/cert.pem
SSLCertificateKeyFile /etc/letsencrypt/live/${MIODOMINIO}/privkey.pem
<Directory /var/www/${MIODOMINIO_COMPLETO}/>
CGIPassAuth On
Require all granted
AllowOverride All
</Directory>
LogLevel warn
ErrorLog "/var/www/${MIODOMINIO_COMPLETO}/logs/error.log"
CustomLog "/var/www/${MIODOMINIO_COMPLETO}/logs/access.log" combined
</VirtualHost>

View File

@@ -0,0 +1,11 @@
<VirtualHost *:443>
ServerName _default_
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/riso.app/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/riso.app/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
Protocols h2 http/1.1
# Puoi anche fare un 403:
Redirect 403 /
</VirtualHost>

View File

@@ -0,0 +1,4 @@
Define MIODOMINIO riso.app
Define MIODOMINIO_COMPLETO riso.app
Define MIOURL_API api.riso.app
Define PORTA 3006

View File

@@ -0,0 +1,3 @@
Include /etc/apache2/sites-available/dominioinclude.riso.app.conf
Include /etc/apache2/sites-available/websites_ssl_virtualhost.conf

View File

@@ -0,0 +1,3 @@
Include /etc/apache2/sites-available/dominioinclude_test.riso.app.conf
Include /etc/apache2/sites-available/websites_ssl_virtualhost_sottodomini.conf

View File

@@ -1,30 +1,94 @@
<VirtualHost 65.108.222.97:80>
ServerName ${MIODOMINIO_COMPLETO}
RewriteEngine On
# ==================== APEX (SITO) ====================
# HTTP -> HTTPS
<VirtualHost *:80>
ServerName ${MIODOMINIO}
ServerAlias www.${MIODOMINIO}
# Forza la connessione HTTPS
RewriteCond %{HTTPS} off
RewriteRule ^ https://${MIODOMINIO_COMPLETO}%{REQUEST_URI} [L,R=301]
RewriteEngine On
RewriteRule ^ https://${MIODOMINIO}%{REQUEST_URI} [R=301,L]
ErrorLog /var/log/apache2/${MIODOMINIO}-error.log
CustomLog /var/log/apache2/${MIODOMINIO}-access.log combined
</VirtualHost>
<VirtualHost 65.108.222.97:443>
ServerName ${MIODOMINIO_COMPLETO}
DocumentRoot /var/www/${MIODOMINIO_COMPLETO}
ServerAdmin surya@riso.app
SSLEngine On
SSLProtocol -ALL +TLSv1.2
SSLCompression Off
SSLHonorCipherOrder off
SSLCipherSuite ECDH+AESGCM:ECDH+AES256:!aNULL:!MD5:!DSS:!DH:!AES128
SSLVerifyDepth 10
SSLCertificateFile /etc/letsencrypt/live/${MIODOMINIO}/cert.pem
SSLCertificateKeyFile /etc/letsencrypt/live/${MIODOMINIO}/privkey.pem
<Directory /var/www/${MIODOMINIO_COMPLETO}/>
CGIPassAuth On
Require all granted
AllowOverride All
</Directory>
LogLevel warn
ErrorLog "/var/www/${MIODOMINIO_COMPLETO}/logs/error.log"
CustomLog "/var/www/${MIODOMINIO_COMPLETO}/logs/access.log" combined
# HTTPS (servizio sito - NO proxy qui)
<VirtualHost *:443>
ServerName ${MIODOMINIO}
ServerAlias www.${MIODOMINIO}
DocumentRoot /var/www/${MIODOMINIO_COMPLETO}
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/${MIODOMINIO}/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/${MIODOMINIO}/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
Protocols h2 http/1.1
<Directory "/var/www/${MIODOMINIO_COMPLETO}">
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
DirectoryIndex index.html
# (Opzionale per SPA Quasar/Vue)
# RewriteEngine On
# RewriteCond %{REQUEST_FILENAME} !-f
# RewriteCond %{REQUEST_FILENAME} !-d
# RewriteRule ^ /index.html [L]
ErrorLog /var/log/apache2/${MIODOMINIO}-error.log
CustomLog /var/log/apache2/${MIODOMINIO}-access.log combined
</VirtualHost>
# ==================== API ====================
# HTTP -> HTTPS
<VirtualHost *:80>
ServerName ${MIOURL_API}
RewriteEngine On
RewriteRule ^ https://${MIOURL_API}%{REQUEST_URI} [R=301,L]
ErrorLog /var/log/apache2/${MIOURL_API}-error.log
CustomLog /var/log/apache2/${MIOURL_API}-access.log combined
</VirtualHost>
# HTTPS (proxy verso backend)
<VirtualHost *:443>
ServerName ${MIOURL_API}
ServerAdmin surya@riso.app
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/${MIODOMINIO}/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/${MIODOMINIO}/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
Protocols h2 http/1.1
ProxyPreserveHost On
# ===== SCEGLI UNO DEI DUE BLOCCHI, in base al TUO backend su ${PORTA} =====
# --- Backend HTTP (più comune) ---
# ProxyPass / http://127.0.0.1:${PORTA}/ retry=0 timeout=310 connectiontimeout=30
# ProxyPassReverse / http://127.0.0.1:${PORTA}/
# --- Backend HTTPS (nel tuo caso probabile, visto l“Empty reply” via HTTP) ---
SSLProxyEngine On
SSLProxyVerify none
SSLProxyCheckPeerName off
ProxyPass / https://127.0.0.1:${PORTA}/ retry=0 timeout=310 connectiontimeout=30
ProxyPassReverse / https://127.0.0.1:${PORTA}/
# ==========================================================================
<Proxy *>
Require all granted
</Proxy>
ProxyTimeout 310
Timeout 310
ProxyBadHeader Ignore
ErrorLog /var/log/apache2/${MIOURL_API}-error.log
CustomLog /var/log/apache2/${MIOURL_API}-access.log combined
</VirtualHost>

View File

@@ -0,0 +1,92 @@
# ==================== APEX (SITO) ====================
# HTTP -> HTTPS
<VirtualHost *:80>
ServerName ${MIODOMINIO_COMPLETO}
RewriteEngine On
RewriteRule ^ https://${MIODOMINIO_COMPLETO}%{REQUEST_URI} [R=301,L]
ErrorLog /var/log/apache2/${MIODOMINIO_COMPLETO}-error.log
CustomLog /var/log/apache2/${MIODOMINIO_COMPLETO}-access.log combined
</VirtualHost>
# HTTPS (servizio sito - NO proxy qui)
<VirtualHost *:443>
ServerName ${MIODOMINIO_COMPLETO}
DocumentRoot /var/www/${MIODOMINIO_COMPLETO}
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/${MIODOMINIO}/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/${MIODOMINIO}/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
Protocols h2 http/1.1
<Directory "/var/www/${MIODOMINIO_COMPLETO}">
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
DirectoryIndex index.html
# (Opzionale per SPA Quasar/Vue)
# RewriteEngine On
# RewriteCond %{REQUEST_FILENAME} !-f
# RewriteCond %{REQUEST_FILENAME} !-d
# RewriteRule ^ /index.html [L]
ErrorLog /var/log/apache2/${MIODOMINIO_COMPLETO}-error.log
CustomLog /var/log/apache2/${MIODOMINIO_COMPLETO}-access.log combined
</VirtualHost>
# ==================== API ====================
# HTTP -> HTTPS
<VirtualHost *:80>
ServerName ${MIOURL_API}
RewriteEngine On
RewriteRule ^ https://${MIOURL_API}%{REQUEST_URI} [R=301,L]
ErrorLog /var/log/apache2/${MIOURL_API}-error.log
CustomLog /var/log/apache2/${MIOURL_API}-access.log combined
</VirtualHost>
# HTTPS (proxy verso backend)
<VirtualHost *:443>
ServerName ${MIOURL_API}
ServerAdmin surya@riso.app
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/${MIODOMINIO}/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/${MIODOMINIO}/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
Protocols h2 http/1.1
ProxyPreserveHost On
# ===== SCEGLI UNO DEI DUE BLOCCHI, in base al TUO backend su ${PORTA} =====
# --- Backend HTTP (più comune) ---
# ProxyPass / http://127.0.0.1:${PORTA}/ retry=0 timeout=310 connectiontimeout=30
# ProxyPassReverse / http://127.0.0.1:${PORTA}/
# --- Backend HTTPS (nel tuo caso probabile, visto l“Empty reply” via HTTP) ---
SSLProxyEngine On
SSLProxyVerify none
SSLProxyCheckPeerName off
ProxyPass / https://127.0.0.1:${PORTA}/ retry=0 timeout=310 connectiontimeout=30
ProxyPassReverse / https://127.0.0.1:${PORTA}/
# ==========================================================================
<Proxy *>
Require all granted
</Proxy>
ProxyTimeout 310
Timeout 310
ProxyBadHeader Ignore
ErrorLog /var/log/apache2/${MIOURL_API}-error.log
CustomLog /var/log/apache2/${MIOURL_API}-access.log combined
</VirtualHost>

View File

@@ -2,7 +2,7 @@ module.exports = {
apps : [
{
name: "FreePlanetServerSide",
script: "./src/server/server.js",
script: "./src/server.js",
ignore_watch : ["node_modules"],
watch: false,
//autorestart: true,

View File

@@ -1,10 +1,10 @@
module.exports = {
apps: [
{
name: "PiuCheBuono (Prod)",
script: "/var/www/nodejs_piuchebuono_server/src/server/server.js",
name: "PCB-PROD",
script: "/var/www/nodejs_piuchebuono_server/src/server.js",
ignore_watch: ["node_modules", "logs"],
interpreter: "/root/.nvm/versions/node/v22.13.1/bin/node",
// interpreter: "/root/.nvm/versions/node/v22.13.1/bin/node",
//autorestart: true,
instances: 1,
watch: false,

View File

@@ -1,10 +1,10 @@
module.exports = {
apps: [
{
name: "PiuCheBuono (TEST)",
script: "/var/www/nodejs_test.piuchebuono_server/src/server/server.js",
name: "PCB-TEST",
script: "/var/www/nodejs_test.piuchebuono_server/src/server.js",
ignore_watch: ["node_modules", "logs"],
interpreter: "/root/.nvm/versions/node/v22.13.1/bin/node",
// interpreter: "/root/.nvm/versions/node/v22.13.1/bin/node",
instances: 1,
watch: false,
env: {

View File

@@ -2,9 +2,9 @@ module.exports = {
apps: [
{
name: "PRODUZIONE - FREEPLANET",
script: "/var/www/www.freeplanet_server/src/server/server.js",
script: "/var/www/www.freeplanet_server/src/server.js",
ignore_watch: ["node_modules", "logs"],
interpreter: "/root/.nvm/versions/node/v16.19.0/bin/node",
// interpreter: "/root/.nvm/versions/node/v16.19.0/bin/node",
//autorestart: true,
instances: 1,
watch: false,

View File

@@ -1,10 +1,10 @@
module.exports = {
apps: [
{
name: "RISO (Prod)",
script: "/var/www/nodejs_riso_server/src/server/server.js",
name: "RISO-PROD",
script: "/var/www/nodejs_riso_server/src/server.js",
ignore_watch: ["node_modules", "logs"],
interpreter: "/root/.nvm/versions/node/v22.13.1/bin/node",
// interpreter: "/root/.nvm/versions/node/v22.13.1/bin/node",
//autorestart: true,
instances: 1,
watch: false,

View File

@@ -1,10 +1,10 @@
module.exports = {
apps: [
{
name: "RISO (TEST)",
script: "./src/server/server.js",
name: "RISO-TEST",
script: "./src/server.js",
ignore_watch: ["node_modules", "logs"],
interpreter: "/root/.nvm/versions/node/v22.13.1/bin/node",
// interpreter: "/root/.nvm/versions/node/v22.13.1/bin/node",
watch: false,
instances: 1,
env_test: {

BIN
emails/.DS_Store vendored

Binary file not shown.

View File

@@ -0,0 +1,492 @@
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;
}
.email-body {
padding: 32px 24px;
}
.intro-text {
font-size: 16px;
color: #333;
margin-bottom: 20px;
text-align: center;
line-height: 1.7;
}
.congrats-card {
background: linear-gradient(135deg, #e8f5e9 0%, #f1f8f4 100%);
border: 2px solid #7cb342;
border-radius: 12px;
padding: 24px;
margin: 20px 0;
text-align: center;
}
.congrats-card .congrats-icon {
font-size: 48px;
margin-bottom: 12px;
}
.congrats-card h3 {
font-size: 22px;
color: #558b2f;
margin-bottom: 12px;
font-weight: 700;
}
.congrats-card .territory-name {
font-size: 20px;
color: #7cb342;
font-weight: 600;
margin-top: 8px;
}
.info-section {
background: #ffffff;
border-radius: 8px;
padding: 20px;
margin: 24px 0;
}
.info-section h3 {
font-size: 18px;
color: #1a1a1a;
margin-bottom: 16px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.info-section p {
font-size: 15px;
color: #555;
line-height: 1.7;
margin-bottom: 12px;
}
.info-section ul {
margin: 12px 0;
padding-left: 24px;
}
.info-section li {
font-size: 15px;
color: #555;
line-height: 1.7;
margin-bottom: 8px;
}
.highlight-box {
background: linear-gradient(135deg, #fff8dc 0%, #fef9f3 100%);
border-left: 4px solid #f0ad4e;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
.highlight-box h4 {
font-size: 17px;
color: #f0ad4e;
margin-bottom: 12px;
font-weight: 600;
}
.highlight-box p {
font-size: 15px;
color: #555;
line-height: 1.7;
margin-bottom: 8px;
}
.example-box {
background: #e3f2fd;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
.example-box h4 {
font-size: 16px;
color: #1976d2;
margin-bottom: 12px;
font-weight: 600;
}
.example-box .transaction {
background: white;
border-radius: 6px;
padding: 12px;
margin: 8px 0;
font-size: 14px;
}
.example-box .benefit {
background: #c8e6c9;
border-radius: 6px;
padding: 12px;
margin-top: 12px;
font-size: 14px;
color: #2e7d32;
}
.steps-box {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
.steps-box h4 {
font-size: 17px;
color: #1a1a1a;
margin-bottom: 16px;
font-weight: 600;
text-align: center;
}
.step-item {
display: flex;
align-items: flex-start;
margin-bottom: 16px;
padding: 12px;
background: white;
border-radius: 6px;
}
.step-number {
font-size: 24px;
font-weight: 700;
color: #7cb342;
min-width: 40px;
margin-right: 12px;
}
.step-content h5 {
font-size: 16px;
color: #1a1a1a;
margin-bottom: 6px;
font-weight: 600;
}
.step-content p {
font-size: 14px;
color: #555;
line-height: 1.6;
margin: 0;
}
.cta-section {
text-align: center;
margin: 32px 0;
padding: 24px 0;
border-top: 2px solid #e0e0e0;
border-bottom: 2px solid #e0e0e0;
}
.cta-title {
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 20px;
}
.cta-button {
display: inline-block;
padding: 18px 56px;
font-size: 20px;
font-weight: 700;
color: white;
background: linear-gradient(135deg, #7cb342 0%, #558b2f 100%);
border-radius: 50px;
text-decoration: none;
box-shadow: 0 6px 20px rgba(124, 179, 66, 0.4);
transition: all 0.3s ease;
}
.button-icon {
font-size: 20px;
margin-right: 10px;
vertical-align: middle;
}
.community-box {
background: linear-gradient(135deg, #e8f5e9 0%, #f1f8f4 100%);
border-radius: 8px;
padding: 20px;
margin: 20px 0;
text-align: center;
}
.community-box h4 {
font-size: 17px;
color: #558b2f;
margin-bottom: 12px;
font-weight: 600;
}
.community-box p {
font-size: 15px;
color: #555;
line-height: 1.7;
margin-bottom: 12px;
}
.community-box a {
color: #2196f3;
text-decoration: none;
font-weight: 600;
}
.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;
}
.email-body {
padding: 20px 16px;
}
.congrats-card .congrats-icon {
font-size: 40px;
}
.congrats-card h3 {
font-size: 20px;
}
.cta-button {
padding: 16px 40px;
font-size: 18px;
width: 100%;
max-width: 300px;
}
.step-item {
flex-direction: column;
}
.step-number {
margin-bottom: 8px;
}
}
body
.email-container
//- Header
.email-header
img.header-logo(src=baseurl+'/images/logo.png' alt=nomeapp)
h1 🎉 Benvenuto nel #{nomeTerritorio}!
p.subtitle Sei stato abilitato con successo
//- Body
.email-body
//- Intro
.intro-text
| Ciao <strong>#{usernameMembro}</strong>,<br>
| complimenti! Sei stato abilitato #{nomeTerritorio} da #{usernameInvitante}.
if linkProfiloAdmin
.divider(style="margin: 16px 0;")
p(style="text-align: center; margin: 16px 0;")
a.profile-button(href=linkProfiloAdmin target="_blank" style="display: inline-block; padding: 10px 24px; font-size: 15px; font-weight: 600; color: #7cb342; background: white; border: 2px solid #7cb342; border-radius: 20px; text-decoration: none; transition: all 0.3s ease;")
span(style="margin-right: 6px;") 👤
| Profilo #{usernameInvitante}
//- Congratulazioni
.congrats-card
.congrats-icon ✅
h3 Abilitazione Completata
p(style="font-size: 15px; color: #555; margin-top: 8px;")
| Ora puoi utilizzare i #{symbol} per i tuoi scambi nella comunità
.territory-name 📍 #{nomeTerritorio}
//- Info comunità
.community-box
h4 💬 Unisciti alla Comunità Territoriale
p
| Entra nel gruppo Telegram di <strong>#{nomeTerritorio}</strong> per interagire con i partecipanti, rimanere aggiornato su eventi, mercatini e opportunità di scambio nella tua zona, e per poter inserire, anche tu, annunci di offro/cerco.
if linkTelegramTerritorio
a.telegram-button(href=linkTelegramTerritorio target="_blank" style="display: inline-block; margin-top: 16px; padding: 14px 32px; font-size: 17px; font-weight: 600; color: white; background: linear-gradient(135deg, #0088cc 0%, #006699 100%); border-radius: 25px; text-decoration: none; box-shadow: 0 4px 12px rgba(0, 136, 204, 0.3); transition: all 0.3s ease;")
span(style="font-size: 20px; margin-right: 8px; vertical-align: middle;") ✈️
| Unisciti al gruppo Telegram
//- Cos'è RIS
.info-section
h3
span 💰
| Cosa sono i RIS?
p
| <strong>RIS</strong> (Rete Italiana Scambio) è un sistema di <strong>credito comunitario</strong> basato sulla fiducia reciproca. Non sono soldi tradizionali, ma un'unità di misura che rappresenta il valore degli scambi all'interno della comunità RISO.
p
| <strong>Parità con l'Euro:</strong> 1 RIS = 1 Euro (solo come riferimento di valore, non come convertibilità)
//- Come funziona la fiducia
.highlight-box
h4 🤝 Come Funziona la "Fiducia Concessa"
p
| <strong>Parti da 0 RIS</strong> - Non devi avere un "saldo positivo" per iniziare a scambiare!
p
| <strong>Quando ricevi</strong> un bene o servizio pagando in RIS → il tuo saldo <strong>diventa positivo</strong>
p
| <strong>Quando offri</strong> un bene o servizio ricevendo RIS → il tuo saldo <strong>diventa negativo</strong>
p(style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #f0ad4e;")
| 💡 <strong>Il saldo negativo non è un debito!</strong> È la fiducia che la comunità ti concede. Significa che hai ricevuto prima di aver dato, e la comunità si fida che restituirai nel tempo.
//- Esempio pratico
.example-box
h4 📖 Esempio Pratico
.transaction
| <strong>Situazione:</strong> Sei un grafico e vuoi comprare 100€ di verdure da un produttore locale
.transaction
| <strong>Transazione mista:</strong>
| <br>• Paghi <strong>80€ in Euro</strong>
| <br>• Paghi <strong>20 RIS</strong> (20% in RIS)
| <br><br>🔻 Il tuo saldo RIS passa da 0 a <strong>-20 RIS</strong>
.benefit
| <strong>✓ Beneficio:</strong> Hai ridotto del 20% l'uso degli Euro, sostenendo il produttore locale e rafforzando la comunità! Puoi iniziare con percentuali basse (5-10%) e aumentare man mano che acquisisci fiducia.
//- Come riequilibrare
.info-section
h3
span ⚖️
| Come Riequilibrare il Saldo
p
| Per riportare il tuo saldo verso lo zero (o in positivo), puoi:
ul
li <strong>Offrire beni o servizi</strong> ricevendo RIS in cambio
li <strong>Vendere prodotti</strong> accettando pagamenti parziali o totali in RIS
li <strong>Mettere annunci</strong> sulla piattaforma specificando che accetti RIS
li <strong>Partecipare ai mercatini</strong> locali della comunità RISO
//- Primi passi
.steps-box
h4 🚀 I Tuoi Primi Passi
.step-item
.step-number 1
.step-content
h5 Esplora la Piattaforma
p Familiarizza con gli annunci, i membri e le funzionalità del #{nomeTerritorio}
.step-item
.step-number 2
.step-content
h5 Crea il Tuo Primo Annuncio
p Pubblica cosa offri o cosa cerchi, specificando se accetti pagamenti in RIS
.step-item
.step-number 3
.step-content
h5 Inizia con Piccole Transazioni
p Comincia con percentuali basse di RIS (5-10%) per prendere confidenza e vedi cosa succede. Più siamo aperti noi e più l'Universo ci aiuta e sostiene e ci dona quello di cui abbiamo bisogno.
.step-item
.step-number 4
.step-content
h5 Partecipa alla Comunità
p Unisciti agli incontri locali e ai mercatini per conoscere altri membri. Se non ci sono membri che propongono incontri, puoi proporti anche tu!
//- CTA
.cta-section
.cta-title Inizia Subito a Usare i RIS!
a.cta-button(href=strlinksito target="_blank")
span.button-icon 🌾
| Vai alla Piattaforma
//- Supporto
.info-section
h3
span ❓
| Hai Domande?
p
| Se hai dubbi sul funzionamento dei RIS o sulla piattaforma, non esitare a:
ul
li Contattare il facilitatore del tuo territorio
li Chiedere nel gruppo Telegram locale
li Partecipare agli incontri di comunità
//- Footer
.email-footer
.divider
p Benvenuto nella Rete Italiana Scambio orizzontale - #{nomeTerritorio}
p(style="margin-top: 12px; font-size: 12px;")
| #{new Date().getFullYear()} #{nomeapp}

View File

@@ -0,0 +1 @@
=`Abilitazione avvenuta su ${nomeTerritorio} in ${nomeapp} - (${usernameMembro})`

View File

@@ -0,0 +1,401 @@
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;
}
.alert-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;
}
.request-card {
background: linear-gradient(135deg, #e3f2fd 0%, #f0f7ff 100%);
border: 2px solid #2196f3;
border-radius: 8px;
padding: 24px;
margin: 20px 0;
text-align: center;
}
.request-card h3 {
font-size: 14px;
text-transform: uppercase;
color: #2196f3;
margin-bottom: 12px;
letter-spacing: 0.5px;
font-weight: 600;
}
.request-card .member-name {
font-size: 28px;
color: #1a1a1a;
font-weight: 700;
margin-bottom: 8px;
}
.request-card .member-detail {
font-size: 15px;
color: #555;
margin: 6px 0;
}
.request-card .member-detail strong {
color: #2196f3;
}
.territory-badge {
background: linear-gradient(135deg, #4caf50 0%, #388e3c 100%);
color: white;
display: inline-block;
padding: 8px 20px;
border-radius: 20px;
font-size: 16px;
font-weight: 600;
margin-top: 12px;
}
.question-box {
background: #e1f5fe;
border-left: 4px solid #2196f3;
border-radius: 8px;
padding: 20px;
margin: 24px 0;
text-align: center;
}
.question-box p {
font-size: 20px;
color: #1a1a1a;
font-weight: 600;
margin: 0;
line-height: 1.4;
}
.cta-section {
text-align: center;
margin: 32px 0;
padding: 24px 0;
border-top: 2px solid #e0e0e0;
border-bottom: 2px solid #e0e0e0;
}
.cta-title {
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 20px;
}
.cta-button {
display: inline-block;
padding: 18px 56px;
font-size: 20px;
font-weight: 700;
color: white;
background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%);
border-radius: 50px;
text-decoration: none;
box-shadow: 0 6px 20px rgba(33, 150, 243, 0.4);
transition: all 0.3s ease;
}
.button-icon {
font-size: 20px;
margin-right: 10px;
vertical-align: middle;
}
.info-box {
background: #e8f5e9;
border-radius: 8px;
padding: 16px;
margin: 20px 0;
}
.info-box p {
margin: 0 0 8px 0;
color: #2e7d32;
font-size: 15px;
line-height: 1.6;
}
.info-box p:last-child {
margin-bottom: 0;
}
.responsibility-box {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
.responsibility-box h3 {
font-size: 17px;
color: #1a1a1a;
margin-bottom: 12px;
text-align: center;
font-weight: 600;
}
.responsibility-item {
display: flex;
align-items: flex-start;
margin-bottom: 10px;
padding: 6px 0;
}
.responsibility-icon {
font-size: 20px;
margin-right: 10px;
min-width: 24px;
flex-shrink: 0;
}
.responsibility-text {
font-size: 15px;
color: #555;
line-height: 1.5;
}
.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;
}
.alert-icon {
font-size: 48px;
}
.email-body {
padding: 20px 16px;
}
.request-card .member-name {
font-size: 24px;
}
.territory-badge {
font-size: 14px;
padding: 6px 16px;
}
.question-box p {
font-size: 18px;
}
.cta-button {
padding: 16px 40px;
font-size: 18px;
width: 100%;
max-width: 300px;
}
.responsibility-item {
font-size: 14px;
}
}
body
.email-container
//- Header
.email-header
img.header-logo(src=baseurl+'/images/logo.png' alt=nomeapp)
h1 🎯 Richiesta Abilitazione #{nomeTerritorio}
p.subtitle Nuovo membro in attesa di attivazione
//- Body
.email-body
//- Intro
.intro-text
| Ciao <strong>#{nomeFacilitatore}</strong>,<br>
| un nuovo membro richiede l'abilitazione alla fiducia al Circuito del tuo territorio!
//- Card richiesta
.request-card
h3 👤 Richiesta Ingresso Circuito
.member-name #{usernameMembro}
if nomeMembro
if cognomeMembro
.member-detail
strong Nome:
| #{nomeMembro} #{cognomeMembro}
else
.member-detail
strong Nome:
| #{nomeMembro}
if emailMembro
.member-detail
strong Email:
| #{emailMembro}
if telegramMembro
.member-detail
strong Telegram:
a(href=`https://t.me/${telegramMembro}` target="_blank" style="color: #2196f3; text-decoration: none;") @#{telegramMembro}
if comuneResidenza
.member-detail
strong Comune:
| #{comuneResidenza} (#{provinciaResidenza})
.territory-badge 📍 #{nomeTerritorio}
if usernameInvitante
.divider(style="margin: 20px auto; width: 80%;")
h3(style="font-size: 14px; text-transform: uppercase; color: #2196f3; margin-bottom: 12px;") 👥 Invitato da
.member-detail
strong Invitante:
| #{usernameInvitante}
if nomeInvitante
if cognomeInvitante
.member-detail
strong Nome:
| #{nomeInvitante} #{cognomeInvitante}
else
.member-detail
strong Nome:
| #{nomeInvitante}
if telegramInvitante
.member-detail
strong Telegram:
a(href=`https://t.me/${telegramInvitante}` target="_blank" style="color: #2196f3; text-decoration: none;") @#{telegramInvitante}
//- CTA principale
.cta-section
a.cta-button(href=linkAbilitazione target="_blank")
span.button-icon ✓
| Abilita fiducia
.divider(style="margin: 24px 0;")
p(style="font-size: 16px; color: #666; margin-bottom: 16px;") Visualizza i profili
.button-group(style="display: flex; gap: 12px; justify-content: center; flex-wrap: wrap;")
a.secondary-button(href=linkProfiloMembro target="_blank" style="display: inline-block; padding: 12px 24px; font-size: 16px; font-weight: 600; color: #2196f3; background: white; border: 2px solid #2196f3; border-radius: 25px; text-decoration: none; transition: all 0.3s ease;")
span(style="margin-right: 6px;") 👤
| Profilo Membro
if linkProfiloInvitante
a.secondary-button(href=linkProfiloInvitante target="_blank" style="display: inline-block; padding: 12px 24px; font-size: 16px; font-weight: 600; color: #7cb342; background: white; border: 2px solid #7cb342; border-radius: 25px; text-decoration: none; transition: all 0.3s ease;")
span(style="margin-right: 6px;") 👥
| Profilo Invitante
//- Responsabilità
.responsibility-box
h3 📋 Compiti del Facilitatore RISO
.responsibility-item
span.responsibility-icon 🔍
span.responsibility-text
strong Verifica:
| Contatta il membro, se non lo conosci, oppure il suo invitante: #{usernameInvitante}
.responsibility-item
span.responsibility-icon 🌍
span.responsibility-text
strong Territorio:
| Assicurati che il membro appartenga effettivamente al territorio di competenza
.responsibility-item
span.responsibility-icon 👥
span.responsibility-text
strong Integrazione:
| Supporta il nuovo membro nell'attivazione e utilizzo del Circuito locale
//- Info box
.info-box
p
| ✓ Dopo l'abilitazione, #{usernameMembro} potrà accedere al #{nomeTerritorio}
p
| ✓ Il membro riceverà una notifica automatica dell'avvenuta attivazione
//- Footer
.email-footer
.divider
p Hai ricevuto questa email in qualità di Facilitatore RISO per #{nomeTerritorio}
p(style="margin-top: 12px; font-size: 12px;")
| #{new Date().getFullYear()} #{nomeapp}

View File

@@ -0,0 +1 @@
=`Richiesta ingresso di ${usernameMembro} - ${nomeMembro} ${cognomeMembro} su ${nomeTerritorio} in ${nomeapp}`

View File

@@ -0,0 +1,370 @@
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;
}
.alert-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;
}
.request-card {
background: linear-gradient(135deg, #fff8dc 0%, #fef9f3 100%);
border: 2px solid #f0ad4e;
border-radius: 8px;
padding: 24px;
margin: 20px 0;
text-align: center;
}
.request-card h3 {
font-size: 14px;
text-transform: uppercase;
color: #f0ad4e;
margin-bottom: 12px;
letter-spacing: 0.5px;
font-weight: 600;
}
.request-card .member-name {
font-size: 28px;
color: #1a1a1a;
font-weight: 700;
margin-bottom: 8px;
}
.request-card .member-detail {
font-size: 15px;
color: #555;
margin: 6px 0;
}
.request-card .member-detail strong {
color: #f0ad4e;
}
.question-box {
background: #fff3cd;
border-left: 4px solid #f0ad4e;
border-radius: 8px;
padding: 20px;
margin: 24px 0;
text-align: center;
}
.question-box p {
font-size: 20px;
color: #1a1a1a;
font-weight: 600;
margin: 0;
line-height: 1.4;
}
.cta-section {
text-align: center;
margin: 32px 0;
padding: 24px 0;
border-top: 2px solid #e0e0e0;
border-bottom: 2px solid #e0e0e0;
}
.cta-title {
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 20px;
}
.cta-button {
display: inline-block;
padding: 18px 56px;
font-size: 20px;
font-weight: 700;
color: white;
background: linear-gradient(135deg, #7cb342 0%, #558b2f 100%);
border-radius: 50px;
text-decoration: none;
box-shadow: 0 6px 20px rgba(124, 179, 66, 0.4);
transition: all 0.3s ease;
}
.button-icon {
font-size: 20px;
margin-right: 10px;
vertical-align: middle;
}
.info-box {
background: #e8f5e9;
border-radius: 8px;
padding: 16px;
margin: 20px 0;
}
.info-box p {
margin: 0 0 8px 0;
color: #2e7d32;
font-size: 15px;
line-height: 1.6;
}
.info-box p:last-child {
margin-bottom: 0;
}
.warning-box {
background: #f8d7da;
border-left: 4px solid #dc3545;
border-radius: 8px;
padding: 16px;
margin: 20px 0;
}
.warning-box p {
margin: 0;
color: #721c24;
font-size: 15px;
line-height: 1.6;
}
.warning-box strong {
color: #dc3545;
}
.responsibility-box {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
.responsibility-box h3 {
font-size: 17px;
color: #1a1a1a;
margin-bottom: 12px;
text-align: center;
font-weight: 600;
}
.responsibility-item {
display: flex;
align-items: flex-start;
margin-bottom: 10px;
padding: 6px 0;
}
.responsibility-icon {
font-size: 20px;
margin-right: 10px;
min-width: 24px;
flex-shrink: 0;
}
.responsibility-text {
font-size: 15px;
color: #555;
line-height: 1.5;
}
.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;
}
.alert-icon {
font-size: 48px;
}
.email-body {
padding: 20px 16px;
}
.request-card .member-name {
font-size: 24px;
}
.question-box p {
font-size: 18px;
}
.cta-button {
padding: 16px 40px;
font-size: 18px;
width: 100%;
max-width: 300px;
}
.responsibility-item {
font-size: 14px;
}
}
body
.email-container
//- Header
.email-header
img.header-logo(src=baseurl+'/images/logo.png' alt=nomeapp)
h1 🔔 Richiesta di Ammissione
p.subtitle Nuovo membro in attesa
//- Body
.email-body
//- Intro
.intro-text
| Ciao <strong>#{nomeInvitante}</strong>,<br>
| hai una nuova richiesta di ammissione da confermare!
//- Card richiesta
.request-card
h3 👤 Richiesta di Ingresso
.member-name #{usernameInvitato}
if nomeInvitato
.member-detail
strong Nome:
| #{nomeInvitato}
if emailInvitato
.member-detail
strong Email:
| #{emailInvitato}
if userprofile && userprofile.profile.resid_str_comune && userprofile.profile.resid_province
.member-detail
strong Comune Residenza:
| #{userprofile.profile.resid_str_comune} (#{userprofile.profile.resid_province})
//- Domanda di conferma
.question-box
p 🤔 Confermi di conoscere #{nomeInvitato}?
//- CTA principale
.cta-section
a.cta-button(href=linkAmmissione target="_blank")
span.button-icon ✓
| Sì, Confermo
//- Responsabilità
.responsibility-box
h3 📋 La tua responsabilità come invitante
.responsibility-item
span.responsibility-icon 🤝
span.responsibility-text
strong Conoscenza diretta:
| Dovresti ammettere solo persone che conosci personalmente e di cui ti fidi
.responsibility-item
span.responsibility-icon 🛡️
span.responsibility-text
strong Fiducia:
| L'ammissione si basa sulla fiducia reciproca nella comunità #{nomeapp}
.responsibility-item
span.responsibility-icon 👥
span.responsibility-text
strong Supporto:
| Come invitante, aiuterai il nuovo membro a integrarsi nella comunità
//- Info box
.info-box
p
| ✓ Dopo l'ammissione, #{usernameInvitato} potrà completare il profilo
//- Warning box
.warning-box
p
strong ⚠️ Importante:
| Se non conosci questa persona o hai dubbi sull'ammissione, ti consigliamo di non procedere. Puoi contattare il facilitatore locale per ulteriori informazioni o semplicemente non cliccare sul pulsante di conferma.
//- Footer
.email-footer
.divider
p Hai ricevuto questa email perché #{usernameInvitato} ha indicato te come invitante su #{nomeapp}
p(style="margin-top: 12px; font-size: 12px;")
| #{new Date().getFullYear()} #{nomeapp}

View File

@@ -0,0 +1 @@
=`🎉 ${name ? ', ' + name : username} chiede di essere ammesso su ${nomeapp} !`

View File

@@ -0,0 +1,534 @@
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')
h1 Benvenuto/a #{name || username} nella community RISO! 💚
.email-body
.welcome-message
if ammessoUtente
h2 🎉 Il tuo invitante #{nomeInvitante} ti ha ammesso!
p Sei ora un membro 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
| .
.next-steps
.next-steps-title 🎯 I tuoi primi passi nella community RISO
.step-item
.step-number 1
.step-content
h3 ✅ Completa il tuo profilo
p Collega il tuo profilo a Telegram ed accedi ai Circuiti Territoriali per poter iniziare ad usare i RIS. Un profilo completo aiuta gli altri membri a conoscerti meglio!
.step-item
.step-number 2
.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 3
.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 e sperimentalo da subito!
.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
.cta-section
.cta-title 🚀 Accedi e inizia il tuo viaggio RISO
.cta-subtitle Scegli come vuoi accedere alla piattaforma
.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?
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
.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

View File

@@ -0,0 +1 @@
=`Benvenuto in ${nomeapp}`

View File

@@ -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 Scambio orizzontale')
h1 🎉 Il tuo invito è stato accettato!
p.subtitle Un nuovo membro si è unito a #{nomeapp}
//- Body
.email-body
//- Intro
.intro-text
| Ciao <strong>#{nomeInvitante}</strong>,<br>
| 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:
a(href=`mailto:${emailInvitato}` style="color: #667eea; text-decoration: none;") #{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
| 🙏 <strong>Grazie per aver contribuito alla crescita di #{nomeapp}!</strong>
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
| 🌱 <strong>Costruiamo insieme un'economia più solidale!</strong><br>
| 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 Scambio orizzontale
p(style="margin-top: 12px; font-size: 12px;")
| 🍚 Comunità · Fiducia · Scambi Solidali · Sostenibilità

View File

@@ -0,0 +1 @@
=`🎉 Il tuo invito è stato accettato su RISO da ${name ? name : username} !`

View File

@@ -1,71 +0,0 @@
p #{username} (#{name} #{surname}) si è appena Iscritto ad Arcadei Foundation su #{nomeapp}
p Con i seguenti dati di accesso:
span Nome:&nbsp;
strong #{iscritto.name}<br>
span Cognome:&nbsp;
strong #{iscritto.surname}<br>
span Email:&nbsp;
strong #{iscritto.email}<br>
span Email Secondaria:&nbsp;
strong #{iscritto.email2}<br>
span Indirizzo di Residenza:&nbsp;
strong #{iscritto.residency_address}<br>
span Città di Residenza:&nbsp;
strong #{iscritto.residency_city}<br>
span Provincia:&nbsp;
strong #{iscritto.residency_province}<br>
span CAP:&nbsp;
strong #{iscritto.residency_zipcode}<br>
span Nazione:&nbsp;
strong #{iscritto.residency_country}<br>
span Data di Nascita:&nbsp;
strong #{data_nascita}<br>
span Città di Nascita:&nbsp;
strong #{iscritto.born_city}<br>
span Provincia di Nascita:&nbsp;
strong #{iscritto.born_province}<br>
span Paese di Nascita:&nbsp;
strong #{iscritto.born_country}<br>
span Telefono:&nbsp;
strong #{iscritto.cell_phone}<br>
span Telefono2:&nbsp;
strong #{iscritto.cell_phone2}<br>
span Tipo di Documento :&nbsp;
strong #{iscritto.doctype}<br>
span Numero Documento :&nbsp;
strong #{iscritto.documentnumber}<br>
span Metodo di Pagamento :&nbsp;
strong #{iscritto.metodo_pagamento}<br>
span Quota scelta da versare :&nbsp;
strong #{iscritto.quota_versata}<br>
span Scrivi altre eventuali informazioni o comunicazioni:&nbsp;
strong #{iscritto.altre_comunicazioni}<br>
span Categorie d'Interesse :&nbsp;
strong #{iscritto.categorie_interesse}<br>
p <br>Saluti
style(type="text/css").
html, body {
padding: 0;
margin: 0;
}
p {
font-size: 1rem;
}
.divbtn {
display: flex;
align-items: center;
justify-content: center;
}
.btn-lg {
padding: 5px;
margin: 5px;
font-size: 26px;
cursor: pointer;
color: white;
background: #027be3 !important;
border-radius: 28px;
}

View File

@@ -1 +0,0 @@
=`Nuova Iscrizione Arcadei di ${name} ${surname} (${emailto}) su ${nomeapp}`

View File

@@ -1,42 +1,185 @@
p #{username} (#{name} #{surname}) si è appena Registrato su #{nomeapp}
p Con i seguenti dati di accesso:
span Username:&nbsp;
strong #{username}<br>
span Email:&nbsp;
strong #{emailto}<br>
span Invitante:&nbsp;
strong #{aportador_solidario}<br>
span Nome:&nbsp;
strong #{user.name}<br>
span Cognome:&nbsp;
strong #{user.surname}<br>
span Cellulare:&nbsp;
strong #{user.profile.intcode_cell} #{user.profile.cell}<br>
span Nazionalità:&nbsp;
strong #{user.profile.nationality}<br>
span Gruppo:&nbsp;
strong #{idMyGroup}<br>
p <br>Saluti
style(type="text/css").
html, body {
padding: 0;
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;
}
.divbtn {
display: flex;
align-items: center;
justify-content: center;
}
.btn-lg {
padding: 5px;
margin: 5px;
font-size: 26px;
cursor: pointer;
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;
}
.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, #027be3 0%, #0056b3 100%);
color: white;
background: #027be3 !important;
border-radius: 28px;
padding: 32px 24px;
text-align: center;
}
.email-header h1 {
margin: 0;
font-size: 24px;
font-weight: 600;
}
.email-header p {
margin: 8px 0 0 0;
font-size: 14px;
opacity: 0.9;
}
.email-body {
padding: 32px 24px;
}
.user-info {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin-bottom: 24px;
}
.info-row {
display: flex;
padding: 10px 0;
border-bottom: 1px solid #e0e0e0;
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
font-weight: 600;
color: #555;
min-width: 140px;
font-size: 14px;
}
.info-value {
color: #1a1a1a;
font-size: 14px;
flex: 1;
word-break: break-word;
}
.email-footer {
padding: 24px;
text-align: center;
background: #f8f9fa;
color: #777;
font-size: 13px;
}
.highlight {
display: inline-block;
background: #e3f2fd;
color: #027be3;
padding: 2px 8px;
border-radius: 4px;
font-weight: 600;
}
@media only screen and (max-width: 600px) {
body {
padding: 10px;
}
.email-header {
padding: 24px 16px;
}
.email-body {
padding: 24px 16px;
}
.info-row {
flex-direction: column;
}
.info-label {
margin-bottom: 4px;
}
}
}
body
.email-container
.email-header
h1 ✓ Nuova Registrazione su #{nomeapp}
p #{nomeapp || 'Piattaforma'}
.email-body
p(style="margin-bottom: 20px; font-size: 15px; color: #333;")
| L'utente
span.highlight #{username || 'N/D'}
| si è appena registrato sulla piattaforma #{nomeapp}.
.user-info
if username
.info-row
.info-label Username:
.info-value #{username}
if emailto
.info-row
.info-label Email:
.info-value #{emailto}
if user && user.name
.info-row
.info-label Nome:
.info-value #{user.name}
if user && user.surname
.info-row
.info-label Cognome:
.info-value #{user.surname}
if user && user.profile && (user.profile.cell || user.profile.intcode_cell)
.info-row
.info-label Cellulare:
.info-value
| #{user.profile.intcode_cell || ''} #{user.profile.cell || ''}
if user && user.profile && (user.profile.resid_str_comune && user.profile.resid_province)
.info-row
.info-label Comune di Residenza:
.info-value
| #{user.profile.resid_str_comune || ''} (#{user.profile.resid_province || ''})
if user && user.profile && user.profile.nationality
.info-row
.info-label Nazionalità:
.info-value #{user.profile.nationality}
if aportador_solidario
.info-row
.info-label Invitante:
.info-value #{aportador_solidario}
if idMyGroup
.info-row
.info-label Gruppo:
.info-value #{idMyGroup}
.email-footer
p Questa è una email automatica di notifica
p(style="margin-top: 8px; font-size: 12px;")
| © #{new Date().getFullYear()} #{nomeapp || ''}.

View File

@@ -0,0 +1,375 @@
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;
}
.alert-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;
}
.request-card {
background: linear-gradient(135deg, #fff8dc 0%, #fef9f3 100%);
border: 2px solid #f0ad4e;
border-radius: 8px;
padding: 24px;
margin: 20px 0;
text-align: center;
}
.request-card h3 {
font-size: 14px;
text-transform: uppercase;
color: #f0ad4e;
margin-bottom: 12px;
letter-spacing: 0.5px;
font-weight: 600;
}
.request-card .member-name {
font-size: 28px;
color: #1a1a1a;
font-weight: 700;
margin-bottom: 8px;
}
.request-card .member-detail {
font-size: 15px;
color: #555;
margin: 6px 0;
}
.request-card .member-detail strong {
color: #f0ad4e;
}
.question-box {
background: #fff3cd;
border-left: 4px solid #f0ad4e;
border-radius: 8px;
padding: 20px;
margin: 24px 0;
text-align: center;
}
.question-box p {
font-size: 20px;
color: #1a1a1a;
font-weight: 600;
margin: 0;
line-height: 1.4;
}
.cta-section {
text-align: center;
margin: 32px 0;
padding: 24px 0;
border-top: 2px solid #e0e0e0;
border-bottom: 2px solid #e0e0e0;
}
.cta-title {
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 20px;
}
.cta-button {
display: inline-block;
padding: 18px 56px;
font-size: 20px;
font-weight: 700;
color: white;
background: linear-gradient(135deg, #7cb342 0%, #558b2f 100%);
border-radius: 50px;
text-decoration: none;
box-shadow: 0 6px 20px rgba(124, 179, 66, 0.4);
transition: all 0.3s ease;
}
.button-icon {
font-size: 20px;
margin-right: 10px;
vertical-align: middle;
}
.info-box {
background: #e8f5e9;
border-radius: 8px;
padding: 16px;
margin: 20px 0;
}
.info-box p {
margin: 0 0 8px 0;
color: #2e7d32;
font-size: 15px;
line-height: 1.6;
}
.info-box p:last-child {
margin-bottom: 0;
}
.warning-box {
background: #f8d7da;
border-left: 4px solid #dc3545;
border-radius: 8px;
padding: 16px;
margin: 20px 0;
}
.warning-box p {
margin: 0;
color: #721c24;
font-size: 15px;
line-height: 1.6;
}
.warning-box strong {
color: #dc3545;
}
.responsibility-box {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
.responsibility-box h3 {
font-size: 17px;
color: #1a1a1a;
margin-bottom: 12px;
text-align: center;
font-weight: 600;
}
.responsibility-item {
display: flex;
align-items: flex-start;
margin-bottom: 10px;
padding: 6px 0;
}
.responsibility-icon {
font-size: 20px;
margin-right: 10px;
min-width: 24px;
flex-shrink: 0;
}
.responsibility-text {
font-size: 15px;
color: #555;
line-height: 1.5;
}
.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;
}
.alert-icon {
font-size: 48px;
}
.email-body {
padding: 20px 16px;
}
.request-card .member-name {
font-size: 24px;
}
.question-box p {
font-size: 18px;
}
.cta-button {
padding: 16px 40px;
font-size: 18px;
width: 100%;
max-width: 300px;
}
.responsibility-item {
font-size: 14px;
}
}
body
.email-container
//- Header
.email-header
img.header-logo(src=baseurl+'/images/logo.png' alt=nomeapp)
h1 🔔 Richiesta di Ammissione
p.subtitle Nuovo membro in attesa
//- Body
.email-body
//- Intro
.intro-text
| Ciao <strong>#{nomeInvitante}</strong>,<br>
| hai una nuova richiesta di ammissione da confermare!
//- Card richiesta
.request-card
h3 👤 Richiesta di Ingresso
.member-name #{usernameInvitato}
if nomeInvitato
.member-detail
strong Nome:
| #{nomeInvitato}
if emailInvitato
.member-detail
strong Email:
| #{emailInvitato}
if userprofile && userprofile.profile.resid_str_comune && userprofile.profile.resid_province
.member-detail
strong Comune Residenza:
| #{userprofile.profile.resid_str_comune} (#{userprofile.profile.resid_province})
//- Domanda di conferma
.question-box
p 🤔 Confermi di conoscere #{nomeInvitato}?
//- CTA principale
.cta-section
.cta-title Ammetti #{usernameInvitato} su #{nomeapp}
a.cta-button(href=linkAmmissione target="_blank")
span.button-icon ✓
| Sì, Confermo
//- Responsabilità
.responsibility-box
h3 📋 La tua responsabilità come invitante
.responsibility-item
span.responsibility-icon 🤝
span.responsibility-text
strong Conoscenza diretta:
| Dovresti ammettere solo persone che conosci personalmente e di cui ti fidi
.responsibility-item
span.responsibility-icon 🛡️
span.responsibility-text
strong Fiducia:
| L'ammissione si basa sulla fiducia reciproca nella piattaforma su #{nomeapp}
.responsibility-item
span.responsibility-icon 👥
span.responsibility-text
strong Supporto:
| Come invitante, aiuterai il nuovo membro a integrarsi nella comunità
//- Info box
.info-box
p
| ✓ Dopo l'ammissione, #{usernameInvitato} potrà completare il profilo
p
| ✓ Il facilitatore locale valuterà l'abilitazione all'uso dei RIS
//- Warning box
.warning-box
p
strong ⚠️ Importante:
| Se non conosci questa persona o hai dubbi sull'ammissione, ti consigliamo di non procedere. Puoi contattare il facilitatore locale per ulteriori informazioni o semplicemente non cliccare sul pulsante di conferma.
//- Footer
.email-footer
.divider
p Hai ricevuto questa email perché #{usernameInvitato} ha indicato te come invitante su #{nomeapp}
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à

View File

@@ -0,0 +1 @@
=`🎉 ${name ? ', ' + name : username} chiede di essere ammesso su ${nomeapp} !`

View File

@@ -0,0 +1,471 @@
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')
h1 💚 #{name || username ? ',' : ''} Benvenuto/a nella piattaforma #{nomeapp} 💚 !
.email-body
.welcome-message
if ammessoUtente
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
.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
.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
.email-footer
.divider
p 💚 Benvenuto/a nella famiglia #{nomeapp}!
p(style="margin-top: 8px;") Hai ricevuto questa email perché sei stato/a ammesso/a nella community di #{nomeapp}
p(style="margin-top: 12px; font-size: 12px;")
| © #{new Date().getFullYear()} #{nomeapp}

View File

@@ -0,0 +1 @@
=`Benvenuto in ${nomeapp}`

View File

@@ -0,0 +1,342 @@
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 Scambio orizzontale')
h1 🎉 Il tuo invito è stato accettato!
p.subtitle Un nuovo membro si è unito a #{nomeapp}
//- Body
.email-body
//- Intro
.intro-text
| Ciao <strong>#{nomeInvitante}</strong>,<br>
| 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:
a(href=`mailto:${emailInvitato}` style="color: #667eea; text-decoration: none;") #{emailInvitato}
if usernameInvitato
.member-detail
strong Username:
| #{usernameInvitato}
//- Ringraziamento
.thank-you-box
p
| 🙏 <strong>Grazie per aver contribuito alla crescita di #{nomeapp}!</strong>
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
| 🌱 <strong>Costruiamo insieme un'economia più solidale!</strong><br>
| 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à

View File

@@ -0,0 +1 @@
=`🎉 Il tuo invito è stato accettato su ${nomeapp} da ${name ? name : username} !`

View File

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

View File

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

View File

@@ -19,7 +19,6 @@ html
}
body(yahoofix, style="background: #ffffff")
- var baseimg = baseurl + '/statics/'
span(id='body_style', style='display:block')
table(cellpadding="10", cellspacing="0", width="600", align="center")
tr

View File

@@ -0,0 +1,422 @@
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;
}
.welcome-icon {
font-size: 48px;
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;
}
.riso-info {
background: #fef9f3;
border-radius: 8px;
padding: 16px;
margin: 20px 0;
border: 1px solid #7cb342;
}
.riso-info h3 {
font-size: 16px;
color: #558b2f;
margin-bottom: 10px;
text-align: center;
}
.riso-info p {
font-size: 16px;
color: #555;
line-height: 1.6;
margin-bottom: 8px;
}
.benefits-list {
background: #f8f9fa;
border-radius: 8px;
padding: 16px;
margin: 20px 0;
}
.benefits-list h3 {
font-size: 17px;
color: #1a1a1a;
margin-bottom: 12px;
text-align: center;
font-weight: 600;
}
.benefit-item {
display: flex;
align-items: flex-start;
margin-bottom: 8px;
padding: 6px 0;
}
.benefit-icon {
font-size: 20px;
margin-right: 10px;
min-width: 24px;
flex-shrink: 0;
}
.benefit-text {
font-size: 16px;
color: #555;
line-height: 1.5;
}
.values-section {
background: linear-gradient(135deg, #fff8dc 0%, #fef9f3 100%);
border-radius: 8px;
padding: 16px;
margin: 20px 0;
}
.values-section h3 {
font-size: 16px;
color: #558b2f;
margin-bottom: 12px;
text-align: center;
font-weight: 600;
}
.value-item {
display: flex;
align-items: center;
margin-bottom: 8px;
font-size: 16px;
color: #555;
}
.value-item strong {
color: #558b2f;
margin-right: 6px;
}
.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(210, 105, 30, 0.3);
transition: all 0.3s ease;
}
.info-box {
background: #e8f5e9;
border-radius: 8px;
padding: 14px;
margin-top: 20px;
text-align: center;
}
.info-box p {
margin: 0;
color: #2e7d32;
font-size: 16px;
line-height: 1.6;
}
.telegram-box {
background: #e3f2fd;
border-radius: 8px;
padding: 14px;
margin: 16px 0;
text-align: center;
border: 1px solid #64b5f6;
}
.telegram-box p {
margin: 0;
color: #1976d2;
font-size: 16px;
line-height: 1.6;
}
.telegram-box strong {
color: #0d47a1;
}
.link-box {
background: #fff3e0;
border-radius: 8px;
padding: 12px;
margin: 16px 0;
text-align: center;
}
.link-box a {
color: #558b2f;
text-decoration: none;
font-weight: 600;
font-size: 16px;
}
.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;
}
.email-body {
padding: 20px 16px;
}
.cta-button {
padding: 14px 32px;
font-size: 16px;
width: 100%;
max-width: 300px;
}
.benefit-item, .value-item {
font-size: 15px;
}
}
body
.email-container
//- Header
.email-header
img.header-logo(src=baseurl+'/images/logo.png' alt='RISO - Rete Italiana Scambio orizzontale')
h1 Sei stato invitato ad unirti a #{nomeapp}!
p.subtitle Rete Italiana Scambio orizzontale
//- Body
.email-body
//- Intro
.intro-text
if usernameInvitante
| <strong>#{usernameInvitante}</strong> ti ha invitato a far parte della comunità RISO, una rete di persone che credono in un'economia basata su fiducia, comunità e scambi solidali.
else
| Hai ricevuto un invito speciale per unirti alla comunità RISO, una rete di persone che credono in un'economia basata su fiducia, comunità e scambi solidali.
.cta-section
.cta-title Unisciti alla Comunità RISO 🍚💚☀️
a.cta-button(href=linkRegistrazione target="_blank") Registrati Ora
//- Messaggio personalizzato
if messaggioPersonalizzato
.highlight-box
p
| 💌 <strong>Messaggio da #{usernameInvitante}:</strong><br>
| "#{messaggioPersonalizzato}"
//- Cos'è RISO
.riso-info
h3 🍚 Cos'è il Progetto RISO?
p
| RISO è una <strong>rete di comunità territoriali</strong> in tutta Italia dove puoi scambiare beni, servizi e ospitalità con altre persone, usando il <strong>baratto, il dono</strong>, oppure in <strong>RIS</strong>.
p
| Non è un marketplace commerciale: è una <strong>comunità basata su fiducia e relazioni</strong> reali tra persone che si conoscono e si incontrano.
//- Benefici
.benefits-list
h3 ✨ Cosa puoi fare con RISO
.benefit-item
span.benefit-icon 🔄
span.benefit-text Scambiare beni, servizi e ospitalità con baratto, dono, RIS o Euro
.benefit-item
span.benefit-icon 🤝
span.benefit-text Connetterti con la tua comunità territoriale locale (circuito provinciale)
.benefit-item
span.benefit-icon 💰
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
.benefit-item
span.benefit-icon 📱
span.benefit-text Creare annunci di ciò che offri o cerchi direttamente dall'app
.benefit-item
span.benefit-icon 🎯
span.benefit-text Partecipare a mercatini ed eventi locali organizzati dalla tua comunità
//- Valori
.values-section
h3 ⭐ I Valori della Comunità RISO
.value-item
span.benefit-icon 🤝
| <strong>Comunità:</strong> Creiamo legami autentici tra persone
.value-item
span.benefit-icon ✅
| <strong>Fiducia:</strong> Base del nostro sistema di scambio
.value-item
span.benefit-icon 🚫
| <strong>Non speculativo:</strong> il RIS non è una moneta complementare, è un Sistema di Credito Naturale. Non serve a 'comprare' cose, ma a <strong>misurare e scambiare l'energia umana</strong>.
.value-item
span.benefit-icon 🌍
| <strong>Sostenibilità:</strong> Promuoviamo stili di vita sani e naturali
.value-item
span.benefit-icon 👁️
| <strong>Trasparenza:</strong> Tutte le transazioni in RIS sono visibili alla comunità
//- Info RIS
.riso-info
h3 💡 Come funzionano i RIS?
p
| • Non esiste una banca centrale che li crea<br>
| • Ogni utente parte da <strong>0 RIS</strong><br>
| • Quando ricevi vai in positivo (+), quando spendi vai in negativo (-)<br>
| • I facilitatori locali ti abilitano all'uso in base a conoscenza e fiducia<br>
| • I circuiti provinciali hanno bilancio sempre a zero (sostenibilità garantita!)
//- CTA
.cta-section
.cta-title Unisciti alla Comunità RISO 🍚💚☀️
a.cta-button(href=linkRegistrazione target="_blank") Registrati Ora
//- Info pratica
.info-box
p
| ✓ <strong>Piattaforma Libera e Gratuita</strong><br>
| ✓ <strong>Registrazione ad invito</strong> · Necessario username Telegram<br>
| ✓ <strong>App web installabile</strong> · Non serve store, installa da browser
//- Telegram
.telegram-box
p
| 📲 <strong>Unisciti al gruppo Telegram della tua provincia!</strong><br>
| Cerca "<strong>Progetto RISO</strong>" su Telegram e trova il gruppo nel messaggio fissato.<br>
| Qui organizziamo incontri reali e mercatini. 🍚
//- Link utile
.link-box
p 📚 Vuoi saperne di più sul sistema RIS?
a(href="https://sicrenacc.info/" target="_blank") Leggi il libro Si.cre.na.C.C →
//- Footer
.email-footer
.divider
p Hai ricevuto questa email perché #{usernameInvitante || 'un membro della comunità'} ti ha invitato su #{nomeapp}
p(style="margin-top: 12px; font-size: 12px;")
| #{new Date().getFullYear()} #{nomeapp} - Rete Italiana Scambio orizzontale
p(style="margin-top: 8px; font-size: 11px; color: #999;")
| Se non sei interessato, puoi semplicemente ignorare questa email.
p(style="margin-top: 12px; font-size: 12px;")
| 🍚 Comunità · Fiducia · Scambi Solidali · Sostenibilità

View File

@@ -0,0 +1 @@
=`${usernameInvitante} ti ha invitato su ${nomeapp}`

View File

@@ -1,29 +0,0 @@
p Ciao #{name},<br>Grazie per aver inviato la tua richiesta di adesione come Sostenitore di Arcadei !
p Cordiali Saluti<br>Arcadei
style(type="text/css").
html, body {
padding: 0;
margin: 0;
}
p {
font-size: 1rem;
}
.divbtn {
display: flex;
align-items: center;
justify-content: center;
}
.btn-lg {
padding: 5px;
margin: 5px;
font-size: 26px;
cursor: pointer;
color: white;
background: #027be3 !important;
border-radius: 28px;
}

View File

@@ -1 +0,0 @@
=`Richiesta di Adesione ad Arcadei (${name} ${surname})`

View File

@@ -176,7 +176,7 @@ html
tr
td.logoContainer
a(href=baseurl)
img.logo(src=baseurl+"/public/images/logo.png", alt="Logo")
img.logo(src=baseurl+"/images/logo.png", alt="Logo")
if dataemail.templ.testoheadermail_out
tr

View File

@@ -1,37 +1,363 @@
p Benvenuto #{name} nel portale di #{nomeapp}!
p I tuoi dati di accesso da ricordare sono:
span Username:&nbsp;
strong #{username}<br>
span hai dimenticato la Password? :&nbsp;
strong <a href=#{forgetpwd} target="_blank">Trovala qui</a><br>
span Email:&nbsp;
strong #{emailto}<br>
p Per confermare la registrazione clicca sul bottone, oppure su questo link: #{strlinkreg}
div.divbtn <a href=#{strlinkreg} target="_blank">
button.btn.btn-lg Verifica Registrazione</a>
p Potrai cosi' accedere al sito digitando i tuoi dati di accesso.
style(type="text/css").
html, body {
padding: 0;
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;
}
.divbtn {
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;
}
.welcome-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;
}
.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;
}
.button-icon {
font-size: 18px;
margin-right: 8px;
vertical-align: middle;
}
.alternative-link {
margin-top: 16px;
font-size: 13px;
color: #777;
line-height: 1.6;
}
.alternative-link a {
color: #558b2f;
text-decoration: none;
word-break: break-all;
}
.verification-process {
background: #f8f9fa;
border-radius: 8px;
padding: 24px 20px;
margin: 24px 0;
}
.process-title {
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
text-align: center;
margin-bottom: 20px;
}
.process-title .icon {
font-size: 24px;
margin-right: 8px;
vertical-align: middle;
}
.process-steps {
margin: 20px 0;
}
.process-step {
display: flex;
align-items: flex-start;
margin-bottom: 20px;
padding: 16px;
background: white;
border-radius: 8px;
border-left: 4px solid #e0e0e0;
}
.process-step.active {
border-left-color: #7cb342;
background: linear-gradient(135deg, #f8fdf8 0%, #e8f5e9 100%);
}
.step-badge {
min-width: 40px;
height: 40px;
border-radius: 50%;
background: #e0e0e0;
color: #666;
display: flex;
align-items: center;
justify-content: center;
}
.btn-lg {
padding: 5px;
margin: 5px;
font-size: 26px;
cursor: pointer;
font-weight: 700;
font-size: 18px;
margin-right: 16px;
flex-shrink: 0;
}
.process-step.active .step-badge {
background: linear-gradient(135deg, #7cb342 0%, #558b2f 100%);
color: white;
background: #027be3 !important;
border-radius: 28px;
}
.step-content {
flex: 1;
}
.step-content h4 {
font-size: 16px;
color: #1a1a1a;
margin-bottom: 6px;
font-weight: 600;
}
.step-content p {
font-size: 15px;
color: #555;
line-height: 1.5;
margin: 0;
}
.info-note {
background: #fff8dc;
border-left: 4px solid #7cb342;
border-radius: 8px;
padding: 14px;
margin-top: 20px;
}
.info-note p {
margin: 0;
font-size: 15px;
color: #1a1a1a;
line-height: 1.6;
}
.info-note 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;
}
.welcome-icon {
font-size: 48px;
}
.email-body {
padding: 20px 16px;
}
.cta-button {
padding: 14px 32px;
font-size: 16px;
width: 100%;
max-width: 300px;
}
.verification-process {
padding: 20px 16px;
}
.process-step {
padding: 12px;
}
.step-badge {
min-width: 36px;
height: 36px;
font-size: 16px;
margin-right: 12px;
}
.step-content h4 {
font-size: 15px;
}
.step-content p {
font-size: 14px;
}
}
}
body
.email-container
//- Header
.email-header
- var baseimg = baseurl + '/';
img.header-logo(src=baseimg+"images/logo.png" alt=nomeapp || 'RISO')
h1 Benvenuto/a #{name ? name : username} su #{nomeapp}!
p.subtitle Rete Italiana Scambio Orizzontale
//- Body
.email-body
if !verified_email
.intro-text
| Per iniziare il tuo percorso in RISO, verifica il tuo indirizzo email cliccando sul pulsante qui sotto.
.cta-section
.cta-title 🔐 Verifica il tuo account
if strlinkreg
a.cta-button(href=strlinkreg target="_blank")
span.button-icon ✓
| Verifica Email
.alternative-link
| Oppure copia e incolla questo link nel tuo browser:
br
a(href=strlinkreg target="_blank") #{strlinkreg}
//- Processo di verifica
.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 Clicca sul pulsante sopra per confermare il tuo indirizzo email.
.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 🎯 Completa il tuo profilo
p Una volta ammesso, potrai completare il tuo profilo e iniziare a fare parte della comunità RISO.
.process-step
.step-badge 4
.step-content
h4 🍚 Inizia a scambiare
p Crea annunci, scambia beni e servizi, e partecipa agli eventi della tua comunità locale!
.info-note
p
strong 💡 Suggerimento:
| Se dovessero passare più di 24 ore senza ammissione, contatta il tuo invitante per ricordargli di approvarti.
//- Footer
.email-footer
.divider
p Hai ricevuto questa email perché ti sei registrato su #{nomeapp || 'RISO'}
p(style="margin-top: 12px; font-size: 12px;")
| #{new Date().getFullYear()} #{nomeapp || 'RISO'} - Rete Italiana Scambi Orizzontali
p(style="margin-top: 8px; font-size: 11px; color: #999;")
| Se non hai richiesto questa registrazione, puoi semplicemente ignorare questa email.
p(style="margin-top: 12px; font-size: 12px;")
| 🍚 Comunità · Fiducia · Scambi Solidali · Sostenibilità

View File

@@ -1,39 +0,0 @@
p Welcome #{name} to the portal #{nomeapp}!
p Your access data to remember are:
span Username:&nbsp;
strong #{username}<br>
span Forgot your Password? :&nbsp;
strong <a href=#{forgetpwd} target="_blank">Find it here</a><br>
span Email:&nbsp;
strong #{emailto}<br>
p To confirm the registration click on the button:
div.divbtn <a href=#{strlinkreg} target="_blank">
button.btn.btn-lg Verify Registration</a>
p You can then access your personal area by clicking on LOGIN and filling in all the STEPS required by :&nbsp;
strong "Step by Step Guide" !<br>
style(type="text/css").
html, body {
padding: 0;
margin: 0;
}
.divbtn {
display: flex;
align-items: center;
justify-content: center;
}
.btn-lg {
padding: 5px;
margin: 5px;
font-size: 26px;
cursor: pointer;
color: white;
background: #027be3 !important;
border-radius: 28px;
}

View File

@@ -1 +0,0 @@
=`Confirm Registration to ${nomeapp}`

View File

@@ -1,38 +0,0 @@
p ¡Bienvenido #{name} al portal #{nomeapp}!
p Sus datos de acceso a recordar son:
Nombre Usuario:&nbsp;
strong #{username}<br>
span ¿Olvidaste tu contraseña? :&nbsp;
strong <a href=#{forgetpwd} target="_blank">Encuéntrelo aquí</a><br>
span Email:&nbsp;
strong #{emailto}<br>
p Para confirmar el registro haz clic en el botón, o aquì #{strlinkreg}:
div.divbtn <a href=#{strlinkreg} target="_blank">
button.btn.btn-lg Verificar registro</a>
p A continuación, puede acceder a su área personal haciendo clic en INICIAR SESIÓN y rellenando todos los PASOS requeridos por :&nbsp;
strong "Guía paso a paso"!<br>
style(type="text/css").
html, body {
padding: 0;
margin: 0;
}
.divbtn {
display: flex;
align-items: center;
justify-content: center;
}
.btn-lg {
padding: 5px;
margin: 5px;
font-size: 26px;
cursor: pointer;
color: white;
background: #027be3 !important;
border-radius: 28px;
}

View File

@@ -1 +0,0 @@
=`Confirmar registro a ${nomeapp}`

View File

@@ -1,43 +0,0 @@
p Bienvenue #{name} sur le portail de #{nomeapp} !
p Vos identifiants de connexion à retenir sont :
span Username:&nbsp;
strong #{username}<br>
span avez-vous oublié votre mot de passe ? :&nbsp;
strong <a href=#{forgetpwd} target="_blank">Essayez ici</a><br>
span Email:&nbsp;
strong #{emailto}<br>
p Cliquez sur le bouton pour confirmer votre inscription ou cliquez ici #{strlinkreg}:
div.divbtn <a href=#{strlinkreg} target="_blank">
button.btn.btn-lg Vérifier l'enregistrement</a>
p Vous pouvez ensuite accéder à votre espace personnel en entrant votre nom d'utilisateur et votre mot de passe et en cliquant sur &nbsp;
strong ENTREZ<br>
p Donc, une fois que vous avez vérifié votre inscription, connectez-vous au site et &nbsp;
strong COMPLÉTER TOUTES LES ÉTAPES &nbsp;
de la portée du "Guide pas-à-pas" via ce lien:<br>
span <a href=#{strlinksito} target="_blank">#{strlinksito}</a>
style(type="text/css").
html, body {
padding: 0;
margin: 0;
}
.divbtn {
display: flex;
align-items: center;
justify-content: center;
}
.btn-lg {
padding: 5px;
margin: 5px;
font-size: 26px;
cursor: pointer;
color: white;
background: #027be3 !important;
border-radius: 28px;
}

View File

@@ -1 +0,0 @@
=`Confirmer l'inscription à ${nomeapp}`

View File

@@ -1,43 +0,0 @@
p Benvenuto #{name} nel portale di #{nomeapp}!
p I tuoi dati di accesso da ricordare sono:
span Username:&nbsp;
strong #{username}<br>
span hai dimenticato la Password? :&nbsp;
strong <a href=#{forgetpwd} target="_blank">Trovala qui</a><br>
span Email:&nbsp;
strong #{emailto}<br>
p Per confermare la registrazione clicca sul bottone, oppure <a href=#{strlinkreg} target="_blank">CLICCA QUI</a>
div.divbtn <a href=#{strlinkreg} target="_blank">
button.btn.btn-lg Verifica Registrazione</a>
p Potrai cosi' accedere alla tua area personale inserendo lo username e la password e cliccando su &nbsp;
strong ACCEDI <br>
p Pertanto una volta Verificata la Registrazione, accedi al sito e &nbsp;
strong COMPLETA TUTTI I PASSI &nbsp;
span della "Guida Passo Passo" tramite questo Link:<br>
span <a href=#{strlinksito} target="_blank">#{strlinksito}</a>
style(type="text/css").
html, body {
padding: 0;
margin: 0;
}
.divbtn {
display: flex;
align-items: center;
justify-content: center;
}
.btn-lg {
padding: 5px;
margin: 5px;
font-size: 26px;
cursor: pointer;
color: white;
background: #027be3 !important;
border-radius: 28px;
}

View File

@@ -1 +0,0 @@
=`Confermare la Registrazione a ${nomeapp}`

View File

@@ -1,43 +0,0 @@
p Bem-vindo #{name} ao portal de #{nomeapp}!
p Os seus dados de login a lembrar são:
span Username:&nbsp;
strong #{username}<br>
span você esqueceu sua senha? :&nbsp;
strong <a href=##{forgetpwd} target="_blank">Encontre-o aqui</a>>br>
span Email:&nbsp;
strong #{emailto}<br>
p Clique no botão para confirmar o seu registo ou clique aqui #{strlinkreg}:
div.divbtn <a href=#{strlinkreg} target="_blank">
button.btn.btn-lg Verificação de registro</a>
p Você pode então acessar sua área pessoal digitando seu nome de usuário e senha e clicando em &nbsp;
strong ENTRAR <br>
p Então, uma vez verificada a sua inscrição, inicie a sessão no site e &nbsp;
strong COMPLETA TODOS OS PASSOS &nbsp;
span do "Guia Passo a Passo" através deste link:<br>
span <a href=#{strlinksito} target="_blank">#{strlinksito}</a>
style(type="text/css").
html, body {
padding: 0;
margin: 0;
}
.divbtn {
display: flex;
align-items: center;
justify-content: center;
}
.btn-lg {
padding: 5px;
margin: 5px;
font-size: 26px;
cursor: pointer;
color: white;
background: #027be3 !important;
border-radius: 28px;
}

View File

@@ -1 +0,0 @@
=`Confirme o registo em ${nomeapp}`

View File

@@ -1,44 +0,0 @@
p Dobrodošli #{name} na portalu #{nomeapp}!
p Vaši podatki za dostop so:
span Username:&nbsp;
strong #{username}<br>
span ste pozabili geslo? :&nbsp;
strong <a href=#{forgetpwd} target="_blank">Poiščite tukaj</a><br>
span Email:&nbsp;
strong #{emailto}<br>
p Za potrditev registracije kliknite na gumb ali kliknite tukaj: #{strlinkreg}
div.divbtn <a href=#{strlinkreg} target="_blank">
button.btn.btn-lg Preveri registracijo</a>
p Tako boste lahko dostopali do svojega osebnega območja, tako da vnesete uporabniško ime in geslo in kliknete &nbsp;
strong PRIJAVA <br>
p Potem ko je registracija preverjena, obiščite spletno mesto in &nbsp;
strong IZPOLNITE VSE KORAKE &nbsp;
span "Vodnika po korakih" prek te povezave:<br>
span <a href=#{strlinksito} target="_blank">#{strlinksito}</a>
style(type="text/css").
html, body {
padding: 0;
margin: 0;
}
.divbtn {
display: flex;
align-items: center;
justify-content: center;
}
.btn-lg {
padding: 5px;
margin: 5px;
font-size: 26px;
cursor: pointer;
color: white;
background: #027be3 !important;
border-radius: 28px;
}

View File

@@ -1 +0,0 @@
=`Potrdi registracijo za ${nomeapp}`

View File

@@ -1,26 +1,128 @@
p Ciao #{name},
p #{nomeapp} recentemente ha ricevuto una richiesta per una password dimenticata.
p Per cambiare la tua password di #{nomeapp}
p <a href=#{strlinksetpassword} target="_blank">Clicca QUI</a>
span Oppure inserisci il codice
span.grande #{tokenforgot_code}
span sulla APP
p
p P.S: Se non sei stato tu a richiedere questo cambiamento, non hai bisogno di fare niente.
p Questo link scadrà tra 4 ore.<br>
p Cordiali Saluti
p Supporto #{nomeapp}
style(type="text/css").
html, body {
padding: 0;
margin: 0;
}
.grande {
font-size: 1.25rem;
font-weight: bold;
}
doctype html
html(lang="it")
head
meta(charset="UTF-8")
meta(name="viewport" content="width=device-width, initial-scale=1.0")
title Recupera Password - #{nomeapp}
body(style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background-color: #f5f7fa;")
// Container Principale
table(role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px 20px;")
tr
td(align="center")
// Card Email
table(role="presentation" cellspacing="0" cellpadding="0" border="0" width="600" style="max-width: 600px; background: #ffffff; border-radius: 24px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.16); overflow: hidden;")
// Header con Icona
tr
td(style="background: linear-gradient(to bottom, rgba(103, 126, 234, 0.08), transparent); padding: 40px 32px 32px; text-align: center; border-bottom: 1px solid rgba(0, 0, 0, 0.06);")
table(role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%")
tr
td(align="center")
// Icona
div(style="width: 100px; height: 100px; margin: 0 auto 20px; background: linear-gradient(135deg, rgba(25, 118, 210, 0.15), rgba(38, 166, 154, 0.15)); border-radius: 50%; display: flex; align-items: center; justify-content: center;")
span(style="font-size: 48px; color: #1976d2;") 🔐
// Titolo
h1(style="margin: 0 0 12px; font-size: 28px; font-weight: 600; color: #1a1a1a; line-height: 1.3;") Recupera la tua Password
p(style="margin: 0; font-size: 16px; color: #666; line-height: 1.5;")
| Ciao
strong(style="color: #333;") #{name}
| , abbiamo ricevuto una richiesta per reimpostare la tua password
// Contenuto Principale
tr
td(style="padding: 32px;")
// Messaggio
p(style="margin: 0 0 24px; font-size: 16px; color: #333; line-height: 1.6;")
| Per cambiare la tua password di
strong #{nomeapp}
| , puoi utilizzare uno dei seguenti metodi:
// Metodo 1: Bottone Link
table(role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin-bottom: 32px;")
tr
td(style="background: linear-gradient(to right, rgba(25, 118, 210, 0.08), transparent); padding: 24px; border-radius: 16px; border: 1px solid rgba(25, 118, 210, 0.15);")
p(style="margin: 0 0 16px; font-size: 14px; font-weight: 600; color: #1976d2; text-transform: uppercase; letter-spacing: 0.5px;") ✨ Metodo 1: Link Diretto
// Bottone CTA
table(role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%")
tr
td(align="center" style="padding: 8px 0;")
a(href=strlinksetpassword target="_blank" style="display: inline-block; padding: 16px 48px; background: linear-gradient(135deg, #1976d2, #42a5f5); color: #ffffff; text-decoration: none; border-radius: 12px; font-size: 16px; font-weight: 600; box-shadow: 0 4px 16px rgba(25, 118, 210, 0.3);")
| 🔓 Reimposta Password
p(style="margin: 16px 0 0; font-size: 13px; color: #666; text-align: center;")
| Il link è valido per
strong(style="color: #f2c037;") 4 ore
// Divisore
table(role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin: 24px 0;")
tr
td(style="position: relative; text-align: center;")
div(style="position: relative; height: 1px; background: linear-gradient(to right, transparent, rgba(0, 0, 0, 0.12), transparent);")
span(style="position: relative; top: -12px; display: inline-block; padding: 4px 16px; background: #ffffff; color: #666; font-size: 14px; font-weight: 500; border-radius: 12px; border: 1px solid rgba(0, 0, 0, 0.08);") oppure
// Metodo 2: Codice
table(role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin-bottom: 24px;")
tr
td(style="background: linear-gradient(to right, rgba(33, 186, 69, 0.08), transparent); padding: 24px; border-radius: 16px; border: 1px solid rgba(33, 186, 69, 0.15);")
p(style="margin: 0 0 16px; font-size: 14px; font-weight: 600; color: #21ba45; text-transform: uppercase; letter-spacing: 0.5px;") 🔢 Metodo 2: Codice di Verifica
p(style="margin: 0 0 12px; font-size: 15px; color: #333;") Inserisci questo codice nell'app:
// Codice Box
div(style="background: #ffffff; border: 2px dashed #21ba45; border-radius: 12px; padding: 20px; text-align: center; margin: 16px 0;")
div(style="font-size: 36px; font-weight: 700; color: #1976d2; letter-spacing: 8px; font-family: 'Courier New', monospace;") #{tokenforgot_code}
p(style="margin: 12px 0 0; font-size: 13px; color: #666; text-align: center;")
| Copia e incolla il codice nell'app per confermare
// Warning Box
table(role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%")
tr
td(style="background: rgba(242, 192, 55, 0.1); padding: 16px; border-radius: 12px; border-left: 4px solid #f2c037;")
p(style="margin: 0; font-size: 14px; color: #666; line-height: 1.6;")
strong(style="color: #333;") ⚠️ Importante:
br
| Se non sei stato tu a richiedere questo cambiamento, ignora questa email. La tua password rimarrà invariata.
// Footer
tr
td(style="background: linear-gradient(to top, rgba(0, 0, 0, 0.02), transparent); padding: 32px; text-align: center; border-top: 1px solid rgba(0, 0, 0, 0.06);")
p(style="margin: 0 0 8px; font-size: 15px; color: #333; font-weight: 600;") Cordiali Saluti
p(style="margin: 0 0 20px; font-size: 15px; color: #1976d2; font-weight: 600;")
| Supporto #{nomeapp}
// Info Footer
p(style="margin: 0; font-size: 13px; color: #999; line-height: 1.6;")
| Questa è una email automatica, per favore non rispondere.
br
| © #{new Date().getFullYear()} #{nomeapp}. Tutti i diritti riservati.
// Versione Mobile (per client email che non supportano media queries)
table(role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="max-width: 600px; margin: 0 auto; display: none;")
tr
td
style(type="text/css").
@media only screen and (max-width: 600px) {
table[class="mobile-responsive"] {
width: 100% !important;
}
td[class="mobile-padding"] {
padding: 20px 16px !important;
}
h1 {
font-size: 24px !important;
}
.code-box {
font-size: 28px !important;
letter-spacing: 4px !important;
}
}

View File

@@ -1,37 +1,307 @@
p Ciao #{name},
p Hai ricevuto
strong #{qty} #{symbol}
if groupDestoContoCom
| sul conto
strong #{groupDestoContoCom}
span da parte di #{mittente} in data #{transactionDate} sul
strong #{nomecircuito} !
if causalDest
p <br>
p Descrizione: #{causalDest}
if causale
p <br>
p Commento di #{mittente}: #{causale}
p <br>
p Apri
strong <a href=#{strlinksito} target="_blank">#{nomeapp}</a>&nbsp;
span per vedere il tuo nuovo saldo.
p <br>
p Cordiali Saluti
p Supporto #{nomeapp}
style(type="text/css").
html, body {
padding: 0;
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;
}
p {
margin: 4px; /* Imposta il margine a 0 per i paragrafi */
}
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;
}
.money-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;
}
.transaction-card {
background: linear-gradient(135deg, #f8fdf8 0%, #e8f5e9 100%);
border: 2px solid #7cb342;
border-radius: 8px;
padding: 24px;
margin: 20px 0;
text-align: center;
}
.transaction-amount {
font-size: 42px;
color: #558b2f;
font-weight: 700;
margin-bottom: 12px;
line-height: 1.2;
}
.transaction-label {
font-size: 14px;
text-transform: uppercase;
color: #558b2f;
font-weight: 600;
letter-spacing: 0.5px;
margin-bottom: 16px;
}
.transaction-detail {
font-size: 15px;
color: #555;
margin: 8px 0;
line-height: 1.5;
}
.transaction-detail strong {
color: #558b2f;
}
.divider-line {
height: 1px;
background: #c8e6c9;
margin: 16px 0;
}
.highlight-box {
background: #fff8dc;
border-left: 4px solid #7cb342;
border-radius: 8px;
padding: 16px;
margin: 20px 0;
}
.highlight-box p {
margin: 0;
font-size: 16px;
color: #1a1a1a;
line-height: 1.6;
}
.highlight-box .label {
font-size: 13px;
text-transform: uppercase;
color: #558b2f;
font-weight: 600;
letter-spacing: 0.5px;
margin-bottom: 6px;
}
.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;
}
.info-box {
background: #e8f5e9;
border-radius: 8px;
padding: 16px;
margin: 20px 0;
text-align: center;
}
.info-box p {
margin: 0;
color: #2e7d32;
font-size: 16px;
line-height: 1.6;
}
.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;
}
.money-icon {
font-size: 48px;
}
.email-body {
padding: 20px 16px;
}
.transaction-amount {
font-size: 36px;
}
.cta-button {
padding: 14px 32px;
font-size: 16px;
width: 100%;
max-width: 300px;
}
}
.grande {
font-size: 1.25rem;
font-weight: bold;
}
body
.email-container
//- Header
.email-header
img.header-logo(src=baseurl+'/images/logo.png' alt='RISO - Rete Italiana Scambio Orizzontale')
h1 Ciao <strong>#{username}</strong>, Hai ricevuto dei #{symbol}!
p.subtitle Nuova transazione sul #{nomecircuito}
//- Body
.email-body
//- Intro
//- Transaction card
.transaction-card
.transaction-label Importo Ricevuto
.transaction-amount +#{qty} #{symbol}
.divider-line
.transaction-detail(style="font-size: 18px; margin-top: 12px;")
strong Nuovo Saldo:
span(style="color: #558b2f; font-weight: 700; font-size: 20px;") #{saldoAttuale} #{symbol}
.transaction-detail
strong Da:
| #{mittente}
.transaction-detail
strong Data:
| #{transactionDate}
if groupDestoContoCom
.transaction-detail
strong Conto:
| #{groupDestoContoCom}
.transaction-detail
strong Circuito:
| #{nomecircuito}
//- Descrizione
if causalDest
.highlight-box
.label 📝 Descrizione
p #{causalDest}
//- Commento mittente
if causale
.highlight-box
.label 💬 Commento di #{mittente}
p "#{causale}"
//- Info box
.info-box
p
| ✓ La transazione è stata registrata con successo<br>
| ✓ Il tuo saldo è stato aggiornato
//- CTA
.cta-section
.cta-title Accedi alla App
a.cta-button(href=strlinksito target="_blank") Apri #{nomeapp}
//- Footer
.email-footer
.divider
p Hai ricevuto questa email perché hai ricevuto una transazione su #{nomeapp}
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à

View File

@@ -6,3 +6,79 @@ Lun 10/03 ORE 15:52: USER [surya1977]: ciao
Lun 10/03 ORE 15:56: USER [surya1977]: ciao
Lun 07/07 ORE 10:45: USER [surya1977]: ciao
Gio 09/10 ORE 20:42: USER [surya1977]: ciao
Sab 11/10 ORE 16:11: USER [surya1977]: ciao
Sab 11/10 ORE 16:32: USER [surya1977]: ciao
Sab 11/10 ORE 16:36: USER [surya1977]: ciao
Sab 11/10 ORE 16:43: USER [surya1977]: ciao
Sab 11/10 ORE 16:53: USER [surya1977]: ciao
Sab 11/10 ORE 17:29: USER [surya1977]: ciao
Sab 11/10 ORE 17:30: USER [surya1977]: ciao
Sab 11/10 ORE 17:30: USER [surya1977]: ciao
Sab 11/10 ORE 18:01: USER [surya1977]: ciao
Sab 11/10 ORE 18:15: USER [surya1977]: ciao
Sab 11/10 ORE 19:05: USER [surya1977]: ciao
Sab 11/10 ORE 19:36: USER [surya1977]: ciao
Sab 11/10 ORE 19:38: USER [surya1977]: ciao
Lun 27/10 ORE 13:35: USER [surya1977]: ciao
Gio 06/11 ORE 11:46: USER [SuryaSecondo]: ciao
Gio 06/11 ORE 11:46: USER [SuryaSecondo]: chi sono
Gio 06/11 ORE 11:47: USER [SuryaSecondo]: exitontelegram
Sab 08/11 ORE 20:24: USER [SuryaSecondo]: ciao
Sab 08/11 ORE 20:24: USER [surya1977]: /start 4b989bfb3d9af38551a8459ddf4a902c82e12017600c29bc050cc56fb835a881
Sab 08/11 ORE 20:24: USER [SuryaSecondo]: /start 4b989bfb3d9af38551a8459ddf4a902c82e12017600c29bc050cc56fb835a881
Sab 08/11 ORE 20:24: USER [SuryaSecondo]: /start 4b989bfb3d9af38551a8459ddf4a902c82e12017600c29bc050cc56fb835a881
Sab 08/11 ORE 21:42: USER [surya4]: ciao
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
Sab 22/11 ORE 23:30: USER [surya1977]: ciao
Dom 23/11 ORE 00:02: USER [perseo5]: ciao
Dom 23/11 ORE 00:22: USER [perseo7]: ciao
Lun 24/11 ORE 13:56: USER [perseo7]: ciao
Lun 24/11 ORE 13:57: USER [perseo7]: chi sono

View File

@@ -837,4 +837,28 @@ Il gruppo dei Facilitatori Territoriali RISO
Mer 24/04 ORE 22:02: 🤖: Da Sùrya (Paolo) (paoloar77):
✅ la regolarizzazione può avv
Lun 07/07 ORE 10:50: 🤖: Da Sùrya undefined (surya1977):
✅ provatest7 è stato Ammesso correttamente (da surya1977)!
✅ provatest7 è stato Ammesso correttamente (da surya1977)!
Gio 09/10 ORE 20:45: 🤖: Da Sùrya undefined (surya1977):
✅ prova123 è stato Ammesso correttamente (da surya1977)!
Gio 06/11 ORE 11:47: 🤖: Da Surya Ar (SuryaSecondo):
exitontelegram
Gio 06/11 ORE 11:51: 🤖: Da Sùrya undefined (surya1977):
✅ SuryaArena è stato Ammesso correttamente (da surya1977)!
Gio 06/11 ORE 20:38: 🤖: Da Sùrya undefined (surya1977):
✅ surya3 è stato Ammesso correttamente (da surya1977)!
Gio 06/11 ORE 22:50: 🤖: Da Sùrya undefined (surya1977):
✅ surya4 è stato Ammesso correttamente (da surya1977)!
Sab 08/11 ORE 20:24: 🤖: Da Sùrya (surya1977):
/start 4b989bfb3d9af38551a8459ddf4a902c82e12017600c29bc050cc56fb835a881
Sab 08/11 ORE 20:24: 🤖: Da Surya Ar (SuryaSecondo):
/start 4b989bfb3d9af38551a8459ddf4a902c82e12017600c29bc050cc56fb835a881
Sab 08/11 ORE 20:24: 🤖: Da Surya Ar (SuryaSecondo):
/start 4b989bfb3d9af38551a8459ddf4a902c82e12017600c29bc050cc56fb835a881
Dom 09/11 ORE 18:36: 🤖: Da Sùrya undefined (surya1977):
✅ surya4 è stato Ammesso correttamente (da surya1977)!
Mar 18/11 ORE 22:31: 🤖: Da Sùrya undefined (surya1977):
✅ surya8 è stato Ammesso correttamente (da surya1977)!
Gio 27/11 ORE 00:43: 🤖: Da Sùrya undefined (surya1977):
✅ test123 è stato Ammesso correttamente (da surya1977)!
Gio 27/11 ORE 00:53: 🤖: Da Sùrya undefined (surya1977):
✅ test123 è stato Ammesso correttamente (da surya1977)!

View File

@@ -483,4 +483,55 @@ Dom 23/03 ORE 22:24: [<b>Circuito RIS Italia</b>]: Inviate Monete da surya1977 a
Saldi:
surya1977: 88.20 RIS]
GruppoYurta: 6.00 RIS]
GruppoYurta: 6.00 RIS]
Ven 03/10 ORE 15:03: [<b>Euro</b>]: Inviate Monete da paoloar77 a piuchebuono 6 € [causale: Pagato Ordine n.442]
Saldi:
paoloar77: -312.80 €]
piuchebuono: 10552.82 €]
Dom 23/11 ORE 00:52: [<b>Circuito RIS Italia</b>]: Inviate Monete da perseo9 a surya1977 1 RIS [causale: prova 1 Ciaoooooooo]
Saldi:
perseo9: -1.00 RIS]
surya1977: 123.95 RIS]
Dom 23/11 ORE 00:56: [<b>Circuito RIS Italia</b>]: Inviate Monete da perseo9 a surya1977 2 RIS [causale: Eccolo!
]
Saldi:
perseo9: -3.00 RIS]
surya1977: 125.95 RIS]
Dom 23/11 ORE 01:00: [<b>Circuito RIS Italia</b>]: Inviate Monete da perseo9 a surya1977 3 RIS [causale: aaaaa]
Saldi:
perseo9: -6.00 RIS]
surya1977: 128.95 RIS]
Dom 23/11 ORE 01:03: [<b>Circuito RIS Italia</b>]: Inviate Monete da perseo9 a surya1977 4 RIS [causale: BBB]
Saldi:
perseo9: -14.00 RIS]
surya1977: 136.95 RIS]
Dom 23/11 ORE 01:10: [<b>Circuito RIS Italia</b>]: Inviate Monete da perseo9 a surya1977 5 RIS [causale: sdasdas]
Saldi:
perseo9: -19.00 RIS]
surya1977: 141.95 RIS]
Gio 04/12 ORE 18:55: [<b>Circuito RIS Bologna</b>]: Inviate Monete da SurTest a ElenaEspx 0.1 RIS [causale: ]
Saldi:
SurTest: 0.00 RIS]
ElenaEspx: 38.05 RIS]
Mar 09/12 ORE 21:36: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 11 RIS [causale: ssss]
Saldi:
surya1977: 63.00 RIS]
amandadi: 12.00 RIS]
Mer 10/12 ORE 16:18: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 12 RIS [causale: Ciaoo]
Saldi:
surya1977: 51.00 RIS]
amandadi: 24.00 RIS]
Mer 10/12 ORE 17:02: [<b>Circuito RIS Venezia</b>]: Inviate Monete da surya1977 a amandadi 52 RIS [causale: Ancora test]
Saldi:
surya1977: -1.00 RIS]
amandadi: 76.00 RIS]

View File

@@ -2,24 +2,27 @@
"name": "freeplanet-serverside",
"version": "1.0.2",
"description": "freeplanet serverside",
"main": "server/server.js",
"main": "server.js",
"scripts": {
"start": "node src/server/server.js",
"dev": "NODE_ENV=development nodemon src/server/server.js",
"start": "node src/server.js",
"dev": "NODE_ENV=development nodemon src/server.js",
"build": "gulp build",
"watch": "gulp watch",
"test": "export NODE_ENV=development || SET NODE_ENV=development && mocha src/server/**/*.test.js",
"start:prod": "NODE_ENV=production node src/server/server.js",
"starttest": "NODE_ENV=test node src/server/server.js",
"start:prod": "NODE_ENV=production node src/server.js",
"starttest": "NODE_ENV=test node src/server.js",
"test-watch": "nodemon --exec 'npm test'"
},
"author": "Surya",
"license": "MIT",
"dependencies": {
"@fal-ai/client": "^1.7.2",
"axios": "^1.13.0",
"basic-ftp": "^5.0.5",
"bcryptjs": "^3.0.2",
"bluebird": "^3.7.2",
"body-parser": "^1.20.3",
"canvas": "^3.2.0",
"cheerio": "^1.0.0",
"compress-pdf": "^0.5.3",
"cookie-parser": "^1.4.7",
@@ -30,9 +33,15 @@
"dotenv": "^16.4.7",
"ejs": "^3.1.10",
"email-templates": "^12.0.2",
"entities": "^7.0.0",
"express": "^4.21.2",
"express-rate-limit": "^7.1.5",
"fast-csv": "^5.0.5",
"formidable": "^3.5.2",
"fs-extra": "^11.3.2",
"ghostscript4js": "^3.2.3",
"groq-sdk": "^0.37.0",
"helmet": "^8.1.0",
"i18n": "^0.15.1",
"image-downloader": "^4.3.0",
"internet-available": "^1.0.0",
@@ -42,6 +51,7 @@
"lodash": "^4.17.21",
"mongodb": "^6.14.2",
"mongoose": "^8.12.1",
"morgan": "^1.10.1",
"multer": "^1.4.5-lts.2",
"mysql": "^2.18.1",
"node-cron": "^3.0.3",
@@ -52,7 +62,7 @@
"node-telegram-bot-api": "^0.66.0",
"nodemailer": "^6.10.0",
"npm-check-updates": "^17.1.15",
"openai": "^4.86.2",
"openai": "^4.104.0",
"pdf-lib": "^1.17.1",
"pdf-parse": "^1.1.1",
"pem": "^1.14.8",
@@ -60,6 +70,7 @@
"pug": "^3.0.3",
"puppeteer": "^24.9.0",
"rate-limiter-flexible": "^5.0.5",
"replicate": "^1.4.0",
"request": "^2.88",
"sanitize-html": "^2.14.0",
"save": "^2.9.0",
@@ -70,6 +81,7 @@
"validator": "^13.12.0",
"vhost": "^3.0.2",
"web-push": "^3.6.7",
"ws": "^8.18.3",
"xlsx": "^0.18.5",
"xml2js": "^0.6.2",
"xoauth2": "^1.2.0"

View File

@@ -25,11 +25,11 @@ var file = `.env.${node_env}`;
// GLOBALI (Uguali per TUTTI)
process.env.LINKVERIF_REG = '/vreg';
process.env.CHECKREVERIF_EMAIL = '/reverif_email';
process.env.LINK_REQUEST_NEWPASSWORD = '/requestnewpwd';
process.env.ADD_NEW_SITE = '/addNewSite';
process.env.LINK_UPDATE_PASSWORD = '/updatepassword';
process.env.LINK_UPDATE_PWD = '/updatepwd';
process.env.LINK_CHECK_UPDATES = '/checkupdates';
process.env.KEY_APP_ID = 'KKPPAA5KJK435J3KSS9F9D8S9F8SD98F9SDF';
console.log("Starting Node with: " + file);

View File

@@ -0,0 +1,492 @@
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).header('x-browser-random', result.browser_random).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 {
console.log('LOGIN');
const { username, password, idapp, keyappid, br } = req.body;
const browser_random = br;
// 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, browser_random);
console.log('attempt...', result);
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).header('x-browser-random', result.browser_random).send({
usertosend: result.user,
code: server_constants.RIS_CODE_OK,
subsExistonDb: result.subsExistonDb,
});
} catch (error) {
console.error('Error in login:', error.message);
res.status(400).send({
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, br } = req.body;
const browser_random = br;
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,
browser_random,
});
} 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;

View File

@@ -237,7 +237,7 @@ exports.getTableContentBase = async (options) => {
GROUP BY IdArticolo
) b ON T.IdArticolo = b.IdArticolo AND T.DataOra = b.data ` +
(options.campispeciali
? `
? `
RIGHT JOIN
( SELECT
e.IdStatoProdotto,
@@ -302,10 +302,9 @@ ON
GROUP BY IdArgomento
) h ON g2.IdArgomento = h.IdArgomento AND g2.DataOra = h.data12
) i2 ON T.ListaArgomenti = i2.IdArgomento
`+
(options.showQtaDisponibile
? ` RIGHT JOIN(
` +
(options.showQtaDisponibile
? ` RIGHT JOIN(
SELECT o.Codice, o.QtaDisponibile
FROM T_WEB_Disponibile o
JOIN(
@@ -314,8 +313,8 @@ ON
GROUP BY Codice
) p ON o.Codice = p.Codice AND o.DataOra = p.data1
) q ON T.IdArticolo = q.Codice`
: ``) +
` RIGHT JOIN(
: ``) +
` RIGHT JOIN(
SELECT a3.IdMarchioEditoriale, a3.Descrizione as CasaEditrice
FROM T_WEB_MarchiEditoriali a3
JOIN(
@@ -816,17 +815,17 @@ exports.viewTable = async (req, res) => {
exports.updateLocalDb = async (tableContent, options) => {
try {
const ProductInfo = require('../models/productInfo');
const Product = require('../models/product');
const CatProd = require('../models/catprod');
let recproductInfo = {
code: tableContent.Ean13.trim(),
};
const code = tableContent.Ean13.trim();
let risrecUpdated = null;
const recfound = await ProductInfo.findOne({ code: recproductInfo.code }).lean();
const recfound = await Product.findOne({ 'productInfo.code': code }).lean();
if (recfound) {
const recproductInfo = recfound.productInfo;
ListaArgomenti = tableContent.ListaArgomenti;
let arrayPulito = ListaArgomenti.trim() // Rimuove gli spazi all'inizio e alla fine
@@ -835,7 +834,7 @@ exports.updateLocalDb = async (tableContent, options) => {
if (arrayPulito && arrayPulito.length > 0) {
let aggiornacat = false;
const precCatProds = recfound.idCatProds;
const precCatProds = recfound.productInfo.idCatProds;
let reccatprods = [];
for (let i = 0; i < arrayPulito.length; i++) {
const idArgomento = parseInt(arrayPulito[i]);
@@ -882,9 +881,9 @@ exports.updateLocalDb = async (tableContent, options) => {
}
if (aggiorna) {
risrecUpdated = await ProductInfo.findOneAndUpdate(
{ code: recproductInfo.code },
{ $set: recproductInfo },
risrecUpdated = await Product.findOneAndUpdate(
{ _id: recfound._id },
{ $set: { productInfo: recproductInfo } },
{ new: true, upsert: true }
);
}

View File

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

View File

@@ -0,0 +1,46 @@
// @ts-check
const EventModel = require('../models/Event');
const { pick } = require('../utils/pick');
async function listEvents(req, res) {
const { limit = '6', page = '1', from, to, sort = 'start' } = /** @type {any} */ (req.query);
const q = {};
if (from) q.start = { ...(q.start || {}), $gte: new Date(from) };
if (to) q.start = { ...(q.start || {}), $lte: new Date(to) };
const lim = Math.min(Number(limit) || 6, 50);
const skip = (Math.max(Number(page) || 1, 1) - 1) * lim;
const [items, total] = await Promise.all([
EventModel.find(q)
.sort({ [sort]: 1 })
.skip(skip)
.limit(lim)
.lean(),
EventModel.countDocuments(q),
]);
res.set('Cache-Control', 'public, max-age=30, stale-while-revalidate=120');
return res.json({ items, total, page: Number(page), limit: lim });
}
async function createEvent(req, res) {
const data = pick(req.body, ['title', 'start', 'end', 'place', 'teaser', 'cover', 'to']);
const doc = await EventModel.create(data);
return res.status(201).json(doc);
}
async function updateEvent(req, res) {
const data = pick(req.body, ['title', 'start', 'end', 'place', 'teaser', 'cover', 'to']);
const doc = await EventModel.findByIdAndUpdate(req.params.id, data, { new: true });
if (!doc) return res.status(404).json({ message: 'Evento non trovato' });
return res.json(doc);
}
async function deleteEvent(req, res) {
const r = await EventModel.findByIdAndDelete(req.params.id);
if (!r) return res.status(404).json({ message: 'Evento non trovato' });
return res.status(204).send();
}
module.exports = { listEvents, createEvent, updateEvent, deleteEvent };

View File

@@ -0,0 +1,18 @@
// @ts-check
const { HomeModel } = require('../models/Home');
async function getHome(req, res) {
const doc = await HomeModel.findOne({});
if (!doc) return res.status(404).json({ message: 'Home non configurata' });
res.set('Cache-Control', 'public, max-age=60, stale-while-revalidate=300');
res.set('ETag', `"${doc.updatedAt?.getTime?.() || Date.now()}"`);
return res.json(doc);
}
async function upsertHome(req, res) {
const payload = req.body || {};
const doc = await HomeModel.findOneAndUpdate({}, payload, { upsert: true, new: true, setDefaultsOnInsert: true });
return res.json(doc);
}
module.exports = { getHome, upsertHome };

View File

@@ -0,0 +1,20 @@
// @ts-check
async function subscribe(req, res) {
const { email } = req.body || {};
if (!email || !/.+@.+\..+/.test(email)) {
return res.status(400).json({ message: 'Email non valida' });
}
// Integra qui Mailchimp/Sendinblue se necessario
return res.status(200).json({ ok: true });
}
async function unsubscribe(req, res) {
const { email } = req.body || {};
if (!email || !/.+@.+\..+/.test(email)) {
return res.status(400).json({ message: 'Email non valida' });
}
// Integra qui Mailchimp/Sendinblue se necessario
return res.status(200).json({ ok: true });
}
module.exports = { subscribe, unsubscribe };

View File

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

View File

@@ -0,0 +1,41 @@
// @ts-check
const { PostModel } = require('../models/Post');
const { pick } = require('../utils/pick');
async function listPosts(req, res) {
const { limit = '3', page = '1', sort = '-date', category } = /** @type {any} */ (req.query);
const q = {};
if (category) q.category = category;
const lim = Math.min(Number(limit) || 3, 50);
const skip = (Math.max(Number(page) || 1, 1) - 1) * lim;
const [items, total] = await Promise.all([
PostModel.find(q).sort(sort).skip(skip).limit(lim).lean(),
PostModel.countDocuments(q)
]);
res.set('Cache-Control', 'public, max-age=30, stale-while-revalidate=120');
return res.json({ items, total, page: Number(page), limit: lim });
}
async function createPost(req, res) {
const data = pick(req.body, ['title','date','category','teaser','cover','to','bodyMd']);
const doc = await PostModel.create(data);
return res.status(201).json(doc);
}
async function updatePost(req, res) {
const data = pick(req.body, ['title','date','category','teaser','cover','to','bodyMd']);
const doc = await PostModel.findByIdAndUpdate(req.params.id, data, { new: true });
if (!doc) return res.status(404).json({ message: 'Post non trovato' });
return res.json(doc);
}
async function deletePost(req, res) {
const r = await PostModel.findByIdAndDelete(req.params.id);
if (!r) return res.status(404).json({ message: 'Post non trovato' });
return res.status(204).send();
}
module.exports = { listPosts, createPost, updatePost, deletePost };

View File

@@ -0,0 +1,294 @@
// telegram.controller.js
const crypto = require('crypto');
const axios = require('axios');
const { User } = require('../models/user');
// Configurazione
const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
const TELEGRAM_API_URL = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}`;
/**
* Genera un token di verifica univoco
*/
exports.generateVerificationToken = async (req, res) => {
try {
const { username, idapp } = req.body;
if (!username) {
return res.status(400).json({ error: 'Username richiesto' });
}
// Verifica che l'utente esista
const user = await User.findOne({ idapp, username });
if (!user) {
return res.status(404).json({ error: 'Utente non trovato' });
}
// Verifica se già verificato
if (user.profile?.username_telegram && user.profile?.teleg_id) {
return res.status(400).json({ error: 'Account Telegram già collegato' });
}
// Genera token univoco
const token = crypto.randomBytes(32).toString('hex');
// Salva SOLO sul database (non serve la Map in-memory!)
await User.updateOne(
{ _id: user._id },
{
$set: {
'profile.telegram_verification_token': token,
'profile.telegram_verification_expires': new Date(Date.now() + 20 * 60 * 1000),
'profile.telegram_verified': false,
},
}
);
res.json({ token });
} catch (error) {
console.error('Errore generazione token:', error);
return res.status(500).json({ error: 'Errore del server' });
}
};
/**
* Controlla se la verifica è stata completata
*/
exports.checkVerification = async (req, res) => {
try {
const { token } = req.query;
if (!token) {
return res.status(400).json({ error: 'Token richiesto' });
}
// Cerca l'utente con questo token DIRETTAMENTE sul DB
const user = await User.findOne({
'profile.telegram_verification_token': token,
});
if (!user) {
return res.status(404).json({ error: 'Token non valido', verified: false });
}
// Verifica se è scaduto
const now = new Date();
if (now > user.profile.telegram_verification_expires) {
// Pulisci il token scaduto
await User.updateOne(
{ _id: user._id },
{
$unset: {
'profile.telegram_verification_token': '',
'profile.telegram_verification_expires': '',
},
}
);
return res.status(410).json({ error: 'Token scaduto', verified: false });
}
// Controlla se è stato verificato
const verified = !!(user.profile?.teleg_id && user.profile?.username_telegram);
return res.json({
verified: verified,
username_telegram: user.profile?.username_telegram || null,
teleg_id: user.profile?.teleg_id || null,
});
} catch (error) {
console.error('Errore verifica token:', error);
return res.status(500).json({ error: 'Errore del server' });
}
};
/**
* Webhook del bot Telegram
* Gestisce i messaggi /start con il token
*/
exports.telegramWebhook = async (req, res) => {
try {
const update = req.body;
// Gestisci solo messaggi con /start
if (update.message && update.message.text && update.message.text.startsWith('/start')) {
const chatId = update.message.chat.id;
const userId = update.message.from.id;
const username = update.message.from.username;
const firstName = update.message.from.first_name;
// Estrai il token dal comando /start
const parts = update.message.text.split(' ');
const token = parts[1];
if (!token) {
await sendTelegramMessage(
chatId,
"⚠️ Link di verifica non valido. Richiedi un nuovo link dall'applicazione."
);
return res.sendStatus(200);
}
// Cerca l'utente con questo token
const user = await User.findOne({
'profile.telegram_verification_token': token,
});
if (!user) {
await sendTelegramMessage(
chatId,
"❌ Token non valido o scaduto. Richiedi un nuovo link dall'applicazione."
);
return res.sendStatus(200);
}
// Verifica scadenza
const now = new Date();
if (now > user.profile.telegram_verification_expires) {
await User.updateOne(
{ _id: user._id },
{
$unset: {
'profile.telegram_verification_token': '',
'profile.telegram_verification_expires': '',
},
}
);
await sendTelegramMessage(
chatId,
"⏰ Il token è scaduto. Richiedi un nuovo link dall'applicazione."
);
return res.sendStatus(200);
}
// Verifica se già collegato
if (user.profile?.teleg_id && user.profile?.username_telegram) {
await sendTelegramMessage(chatId, '✅ Questo account è già stato verificato!');
return res.sendStatus(200);
}
// Salva i dati Telegram e rimuovi il token
await User.updateOne(
{ _id: user._id },
{
$set: {
'profile.teleg_id': userId.toString(),
'profile.username_telegram': username || firstName,
},
$unset: {
'profile.telegram_verification_token': '',
'profile.telegram_verification_expires': '',
},
}
);
// Invia messaggio di conferma
await sendTelegramMessage(
chatId,
`✅ Account verificato con successo!\n\n` +
`Il tuo Telegram è stato collegato a: ${user.username}\n\n` +
`Puoi ora tornare all'applicazione.`
);
}
res.sendStatus(200);
} catch (error) {
console.error('Errore webhook Telegram:', error);
res.sendStatus(500);
}
};
/**
* Funzione helper per inviare messaggi Telegram
*/
async function sendTelegramMessage(chatId, text) {
try {
await axios.post(`${TELEGRAM_API_URL}/sendMessage`, {
chat_id: chatId,
text: text,
parse_mode: 'HTML',
});
} catch (error) {
console.error('Errore invio messaggio Telegram:', error);
}
}
/**
* Configura il webhook di Telegram (da eseguire una volta)
*/
exports.setupWebhook = async (req, res) => {
try {
/*const webhookUrl = `${process.env.APP_URL}/api/telegram/webhook`;
const response = await axios.post(`${TELEGRAM_API_URL}/setWebhook`, {
url: webhookUrl,
allowed_updates: ['message'],
});
res.json({
success: true,
message: 'Webhook configurato',
data: response.data,
});*/
} catch (error) {
console.error('Errore setup webhook:', error);
return res.status(500).json({ error: 'Errore configurazione webhook' });
}
};
/**
* Rimuovi collegamento Telegram
*/
exports.unlinkTelegram = async (req, res) => {
try {
const userId = req.user.id; // Assumi autenticazione middleware
await User.updateOne(
{ _id: userId },
{
$unset: {
'profile.teleg_id': '',
'profile.username_telegram': '',
'profile.telegram_verification_token': '',
'profile.telegram_verification_expires': '',
},
}
);
res.json({ success: true, message: 'Telegram scollegato' });
} catch (error) {
console.error('Errore unlink Telegram:', error);
return res.status(500).json({ error: 'Errore del server' });
}
};
/**
* Salta la verifica Telegram
*/
exports.skipTelegramVerification = async (req, res) => {
try {
const { idapp } = req.body;
const user = await User.findOne({ idapp });
if (!user) {
return res.status(404).json({ error: 'Utente non trovato' });
}
await User.updateOne(
{ _id: user._id },
{
$set: {
'profile.telegram_verification_skipped': true,
},
$unset: {
'profile.telegram_verification_token': '',
'profile.telegram_verification_expires': '',
},
}
);
res.json({ success: true, message: 'Verifica Telegram saltata' });
} catch (error) {
console.error('Errore skip verifica:', error);
return res.status(500).json({ error: 'Errore del server' });
}
};

View File

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

45
src/data/asset.json Normal file
View File

@@ -0,0 +1,45 @@
{
"_id": "asset_bg_001",
"type": "image",
"category": "background",
"sourceType": "ai",
"file": {
"path": "/uploads/assets/backgrounds/forest_autumn_001.jpg",
"url": "/api/assets/asset_bg_001/file",
"thumbnailPath": "/uploads/assets/backgrounds/thumbs/forest_autumn_001_thumb.jpg",
"thumbnailUrl": "/api/assets/asset_bg_001/thumbnail",
"originalName": null,
"mimeType": "image/jpeg",
"size": 2458000,
"dimensions": { "width": 2480, "height": 3508 }
},
"aiGeneration": {
"prompt": "Mystical autumn forest at golden hour...",
"negativePrompt": "text, letters, words...",
"provider": "hf",
"model": "FLUX.1-dev",
"seed": 8847291,
"steps": 35,
"cfg": 7.5,
"requestedSize": "1024x1536",
"actualSize": "1024x1536",
"generationTime": 12500,
"cost": 0
},
"usage": {
"usedInPosters": ["poster_sagra_funghi_2025_001"],
"usedInTemplates": [],
"usageCount": 1
},
"metadata": {
"userId": "user_001",
"tags": ["forest", "autumn", "background", "nature"],
"isReusable": true
},
"createdAt": "2025-01-15T10:25:00.000Z"
}

View File

Can't render this file because it is too large.

Some files were not shown because too many files have changed in this diff Show More