From 037ff6f7f9e075e882c769ac60fcc12c59c6ff05 Mon Sep 17 00:00:00 2001 From: Surya Paolo Date: Fri, 12 Dec 2025 00:44:12 +0100 Subject: [PATCH] =?UTF-8?q?-=20verifica=20email=20se=20non=20=C3=A8=20stat?= =?UTF-8?q?a=20verificata=20(componente)=20-=20altri=20aggiornamenti=20gra?= =?UTF-8?q?fica=20PAGERIS.=20-=20OLLAMA=20AI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 2 + .../reg_resend_email_to_verifiyng/it/html.pug | 404 ++++++++ .../it/subject.pug | 1 + logtrans.txt | 17 +- package.json | 1 + src/config/config.js | 1 + src/models/movement.js | 12 + src/models/user.js | 23 +- src/populate/populate.js | 1 + src/router/api2_router.js | 913 ++++++++++++++++++ src/router/index_router.js | 38 + src/router/users_router.js | 12 +- src/sendemail.js | 47 + src/server/setupRouters.js | 1 + src/server_old.js | 2 + src/tools/general.js | 25 +- 16 files changed, 1486 insertions(+), 14 deletions(-) create mode 100755 emails/defaultSite/reg_resend_email_to_verifiyng/it/html.pug create mode 100755 emails/defaultSite/reg_resend_email_to_verifiyng/it/subject.pug create mode 100644 src/router/api2_router.js diff --git a/.env.development b/.env.development index 7d4430b..63d130b 100644 --- a/.env.development +++ b/.env.development @@ -39,3 +39,5 @@ 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 \ No newline at end of file diff --git a/emails/defaultSite/reg_resend_email_to_verifiyng/it/html.pug b/emails/defaultSite/reg_resend_email_to_verifiyng/it/html.pug new file mode 100755 index 0000000..e8492a9 --- /dev/null +++ b/emails/defaultSite/reg_resend_email_to_verifiyng/it/html.pug @@ -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} \ No newline at end of file diff --git a/emails/defaultSite/reg_resend_email_to_verifiyng/it/subject.pug b/emails/defaultSite/reg_resend_email_to_verifiyng/it/subject.pug new file mode 100755 index 0000000..6d8ccca --- /dev/null +++ b/emails/defaultSite/reg_resend_email_to_verifiyng/it/subject.pug @@ -0,0 +1 @@ +Verifica la tua Email - ${nomeapp}` diff --git a/logtrans.txt b/logtrans.txt index f8b62ee..0438ab2 100644 --- a/logtrans.txt +++ b/logtrans.txt @@ -519,4 +519,19 @@ Gio 04/12 ORE 18:55: [Circuito RIS Bologna]: Inviate Monete da SurTest a Saldi: SurTest: 0.00 RIS] -ElenaEspx: 38.05 RIS] \ No newline at end of file +ElenaEspx: 38.05 RIS] +Mar 09/12 ORE 21:36: [Circuito RIS Venezia]: 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: [Circuito RIS Venezia]: 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: [Circuito RIS Venezia]: Inviate Monete da surya1977 a amandadi 52 RIS [causale: Ancora test] + +Saldi: +surya1977: -1.00 RIS] +amandadi: 76.00 RIS] \ No newline at end of file diff --git a/package.json b/package.json index a06babe..0f7467b 100755 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "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", diff --git a/src/config/config.js b/src/config/config.js index 7444744..f65d10b 100755 --- a/src/config/config.js +++ b/src/config/config.js @@ -25,6 +25,7 @@ var file = `.env.${node_env}`; // GLOBALI (Uguali per TUTTI) process.env.LINKVERIF_REG = '/vreg'; +process.env.CHECKREVERIF_EMAIL = '/reverif_email'; process.env.LINK_REQUEST_NEWPASSWORD = '/requestnewpwd'; process.env.ADD_NEW_SITE = '/addNewSite'; process.env.LINK_UPDATE_PASSWORD = '/updatepassword'; diff --git a/src/models/movement.js b/src/models/movement.js index 02747a4..c1eadf7 100755 --- a/src/models/movement.js +++ b/src/models/movement.js @@ -342,8 +342,12 @@ MovementSchema.statics.getQueryMovsByCircuitId = async function (idapp, username 'circuitfrom.symbol': 1, 'circuitto.symbol': 1, 'userfrom.verified_by_aportador': 1, + 'userfrom.name': 1, + 'userfrom.surname': 1, 'userfrom.username': 1, 'userfrom.profile.img': 1, + 'userto.name': 1, + 'userto.surname': 1, 'userto.username': 1, 'userto.profile.img': 1, 'userto.verified_by_aportador': 1, @@ -579,7 +583,11 @@ MovementSchema.statics.getQueryAllUsersMovsByCircuitId = async function (idapp, 'circuitfrom.symbol': 1, 'circuitto.symbol': 1, 'userfrom.username': 1, + 'userfrom.name': 1, + 'userfrom.surname': 1, 'userfrom.profile.img': 1, + 'userto.name': 1, + 'userto.surname': 1, 'userto.username': 1, 'userto.profile.img': 1, 'groupfrom.groupname': 1, @@ -1013,7 +1021,11 @@ MovementSchema.statics.getLastN_Transactions = async function ( 'circuitto.name': 1, 'userfrom.verified_by_aportador': 1, 'userfrom.username': 1, + 'userfrom.name': 1, + 'userfrom.surname': 1, 'userfrom.profile.img': 1, + 'userto.name': 1, + 'userto.surname': 1, 'userto.username': 1, 'userto.profile.img': 1, 'userto.verified_by_aportador': 1, diff --git a/src/models/user.js b/src/models/user.js index 580c16f..8d6924e 100755 --- a/src/models/user.js +++ b/src/models/user.js @@ -56,6 +56,9 @@ const UserSchema = new mongoose.Schema( message: '{VALUE} is not a valid email' }*/ }, + link_verif_email: { + type: String, + }, hash: { type: String, }, @@ -2614,6 +2617,12 @@ UserSchema.statics.removeBookmark = async function (idapp, username, id, tab) { UserSchema.statics.addBookmark = async function (idapp, username, id, tab) { return await User.updateOne({ idapp, username }, { $push: { 'profile.bookmark': { id, tab } } }); }; +UserSchema.statics.setLinkToVerifiedEmail = async function (idapp, username, link_verif_email) { + return await User.updateOne({ idapp, username }, { $set: { link_verif_email } }); +}; +UserSchema.statics.findByLinkVerifEmail = async function (idapp, link_verif_email) { + return await User.findOne({ idapp, link_verif_email }); +}; // Rimuovo il Partecipa UserSchema.statics.removeAttend = async function (idapp, username, id, tab) { return await User.updateOne({ idapp, username }, { $pull: { 'profile.attend': { id: { $in: [id] }, tab } } }); @@ -6989,28 +6998,24 @@ UserSchema.statics.getTokenByUsernameAndCircuitName = async function (idapp, use return user?.profile?.mycircuits?.[0]?.token || null; }; -UserSchema.statics.softDelete = async function(id) { +UserSchema.statics.softDelete = async function (id) { return this.findByIdAndUpdate( id, - { + { deleted: true, - deletedAt: new Date() + deletedAt: new Date(), }, { new: true } ); }; -UserSchema.statics.getUsersList = function(idapp) { +UserSchema.statics.getUsersList = function (idapp) { return this.find({ idapp: idapp, - $or: [ - { deleted: { $exists: false } }, - { deleted: false } - ] + $or: [{ deleted: { $exists: false } }, { deleted: false }], }).lean(); }; - const User = mongoose.model('User', UserSchema); class Hero { diff --git a/src/populate/populate.js b/src/populate/populate.js index eccd613..561122f 100644 --- a/src/populate/populate.js +++ b/src/populate/populate.js @@ -25,6 +25,7 @@ module.exports = { }); } } + } catch (e) { console.log('error insertIntoDb', e); } diff --git a/src/router/api2_router.js b/src/router/api2_router.js new file mode 100644 index 0000000..ba02750 --- /dev/null +++ b/src/router/api2_router.js @@ -0,0 +1,913 @@ +/** + * API2 Router - Ollama Integration + */ + +const express = require('express'); + +const router = express.Router(); + +// Configurazione Ollama +const OLLAMA_URL = process.env.OLLAMA_URL || 'http://localhost:11434'; +const DEFAULT_MODEL = process.env.OLLAMA_DEFAULT_MODEL || 'llama3.2'; + +// ============================================ +// ROUTES PRINCIPALI +// ============================================ + +/** + * Health check Ollama + * GET /api2/health + */ +router.get('/health', async (req, res) => { + try { + const response = await fetch(`${OLLAMA_URL}/api/tags`); + if (response.ok) { + const data = await response.json(); + res.json({ + status: 'ok', + ollama: 'connected', + modelsCount: data.models?.length || 0 + }); + } else { + res.status(503).json({ status: 'error', ollama: 'disconnected' }); + } + } catch (error) { + res.status(503).json({ + status: 'error', + ollama: 'unreachable', + message: error.message + }); + } +}); + +/** + * Lista modelli disponibili + * GET /api2/models + */ +router.get('/models', async (req, res) => { + try { + + const response = await fetch(`${OLLAMA_URL}/api/tags`); + if (!response.ok) { + console.error('[api2/models] Error:', response.status); + throw new Error(`Ollama error: ${response.status}`); + } + const data = await response.json(); + res.json({ models: data.models || [] }); + } catch (error) { + console.error('[api2/models] Error:', error); + res.status(500).json({ error: error.message }); + } +}); + +/** + * Info modello specifico + * POST /api2/models/info + */ +router.post('/models/info', async (req, res) => { + try { + const { model } = req.body; + if (!model) { + return res.status(400).json({ error: 'Model name richiesto' }); + } + + const response = await fetch(`${OLLAMA_URL}/api/show`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: model }), + }); + + if (!response.ok) { + throw new Error(`Ollama error: ${response.status}`); + } + + const data = await response.json(); + res.json(data); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// ============================================ +// GENERAZIONE TESTO +// ============================================ + +/** + * Generazione testo (non-streaming) + * POST /api2/generate + */ +router.post('/generate', async (req, res) => { + try { + const { + model = DEFAULT_MODEL, + prompt, + temperature = 0.7, + maxTokens, + system, + topP, + topK, + } = req.body; + + if (!prompt) { + return res.status(400).json({ error: 'Prompt richiesto' }); + } + + const payload = { + model, + prompt, + stream: false, + options: { + temperature, + ...(maxTokens && { num_predict: maxTokens }), + ...(topP && { top_p: topP }), + ...(topK && { top_k: topK }), + }, + }; + + if (system) payload.system = system; + + const response = await fetch(`${OLLAMA_URL}/api/generate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + throw new Error(`Ollama error: ${response.status}`); + } + + const data = await response.json(); + res.json({ + response: data.response, + model: data.model, + totalDuration: data.total_duration, + loadDuration: data.load_duration, + promptEvalCount: data.prompt_eval_count, + evalCount: data.eval_count, + }); + } catch (error) { + console.error('[api2/generate] Error:', error); + res.status(500).json({ error: error.message }); + } +}); + +/** + * Generazione testo con streaming (SSE) + * POST /api2/generate/stream + */ +router.post('/generate/stream', async (req, res) => { + try { + const { + model = DEFAULT_MODEL, + prompt, + temperature = 0.7, + system, + maxTokens, + } = req.body; + + if (!prompt) { + return res.status(400).json({ error: 'Prompt richiesto' }); + } + + const payload = { + model, + prompt, + stream: true, + options: { + temperature, + ...(maxTokens && { num_predict: maxTokens }), + }, + }; + + if (system) payload.system = system; + + const response = await fetch(`${OLLAMA_URL}/api/generate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + throw new Error(`Ollama error: ${response.status}`); + } + + // Setup SSE + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.setHeader('X-Accel-Buffering', 'no'); + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + + const pump = async () => { + try { + while (true) { + const { done, value } = await reader.read(); + if (done) { + res.write('data: [DONE]\n\n'); + res.end(); + break; + } + + const chunk = decoder.decode(value); + const lines = chunk.split('\n').filter(line => line.trim()); + + for (const line of lines) { + try { + const json = JSON.parse(line); + res.write(`data: ${JSON.stringify(json)}\n\n`); + } catch (e) { + // Ignora linee non JSON + } + } + } + } catch (error) { + console.error('[api2/generate/stream] Stream error:', error); + res.end(); + } + }; + + // Handle client disconnect + req.on('close', () => { + reader.cancel(); + }); + + pump(); + + } catch (error) { + console.error('[api2/generate/stream] Error:', error); + res.status(500).json({ error: error.message }); + } +}); + +// ============================================ +// CHAT +// ============================================ + +/** + * Chat (non-streaming) + * POST /api2/chat + */ +router.post('/chat', async (req, res) => { + try { + const { + model = DEFAULT_MODEL, + messages, + temperature = 0.7, + system, + maxTokens, + } = req.body; + + if (!messages || !Array.isArray(messages)) { + return res.status(400).json({ error: 'Messages array richiesto' }); + } + + const payload = { + model, + messages, + stream: false, + options: { + temperature, + ...(maxTokens && { num_predict: maxTokens }), + }, + }; + + if (system) payload.system = system; + + // console.log('payload', payload); + + const response = await fetch(`${OLLAMA_URL}/api/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + throw new Error(`Ollama error: ${response.status} - ${response.statusText}`); + } + + const data = await response.json(); + res.json({ + message: data.message, + model: data.model, + totalDuration: data.total_duration, + }); + } catch (error) { + console.error('[api2/chat] Error:', error); + res.status(500).json({ error: error.message }); + } +}); + +/** + * Chat con streaming (SSE) + * POST /api2/chat/stream + */ +router.post('/chat/stream', async (req, res) => { + try { + const { + model = DEFAULT_MODEL, + messages, + temperature = 0.7, + system, + maxTokens, + } = req.body; + + if (!messages || !Array.isArray(messages)) { + return res.status(400).json({ error: 'Messages array richiesto' }); + } + + const payload = { + model, + messages, + stream: true, + options: { + temperature, + ...(maxTokens && { num_predict: maxTokens }), + }, + }; + + if (system) payload.system = system; + + const response = await fetch(`${OLLAMA_URL}/api/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + throw new Error(`Ollama error: ${response.status}`); + } + + // Setup SSE + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.setHeader('X-Accel-Buffering', 'no'); + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + + const pump = async () => { + try { + while (true) { + const { done, value } = await reader.read(); + if (done) { + res.write('data: [DONE]\n\n'); + res.end(); + break; + } + + const chunk = decoder.decode(value); + const lines = chunk.split('\n').filter(line => line.trim()); + + for (const line of lines) { + try { + const json = JSON.parse(line); + res.write(`data: ${JSON.stringify(json)}\n\n`); + } catch (e) { + // Ignora linee non JSON + } + } + } + } catch (error) { + console.error('[api2/chat/stream] Stream error:', error); + res.end(); + } + }; + + // Handle client disconnect + req.on('close', () => { + reader.cancel(); + }); + + pump(); + + } catch (error) { + console.error('[api2/chat/stream] Error:', error); + res.status(500).json({ error: error.message }); + } +}); + +// ============================================ +// ENDPOINTS HELPER +// ============================================ + +/** + * Genera codice + * POST /api2/code + */ +router.post('/code', async (req, res) => { + try { + const { + prompt, + language = 'javascript', + model = DEFAULT_MODEL, + temperature = 0.3, + } = req.body; + + if (!prompt) { + return res.status(400).json({ error: 'Prompt richiesto' }); + } + + const systemPrompt = `Sei un esperto programmatore. Rispondi SOLO con codice ${language} valido e funzionante, senza spiegazioni prima o dopo. Usa commenti nel codice se necessario per spiegare.`; + + const response = await fetch(`${OLLAMA_URL}/api/generate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model, + prompt: `Scrivi codice ${language} per: ${prompt}`, + system: systemPrompt, + stream: false, + options: { temperature }, + }), + }); + + if (!response.ok) { + throw new Error(`Ollama error: ${response.status}`); + } + + const data = await response.json(); + res.json({ + code: data.response, + language, + model: data.model, + }); + } catch (error) { + console.error('[api2/code] Error:', error); + res.status(500).json({ error: error.message }); + } +}); + +/** + * Traduci testo + * POST /api/translate + */ +router.post('/translate', async (req, res) => { + try { + const { + text, + targetLang = 'english', + sourceLang, + model = DEFAULT_MODEL, + } = req.body; + + if (!text) { + return res.status(400).json({ error: 'Text richiesto' }); + } + + const sourceInfo = sourceLang ? ` da ${sourceLang}` : ''; + const prompt = `Traduci il seguente testo${sourceInfo} in ${targetLang}. Rispondi SOLO con la traduzione, senza spiegazioni:\n\n${text}`; + + const response = await fetch(`${OLLAMA_URL}/api/generate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model, + prompt, + stream: false, + options: { temperature: 0.3 }, + }), + }); + + if (!response.ok) { + throw new Error(`Ollama error: ${response.status}`); + } + + const data = await response.json(); + res.json({ + translation: data.response.trim(), + targetLang, + sourceLang, + }); + } catch (error) { + console.error('[api2/translate] Error:', error); + res.status(500).json({ error: error.message }); + } +}); + +/** + * Riassumi testo + * POST /api/summarize + */ +router.post('/summarize', async (req, res) => { + try { + const { + text, + maxLength, + style = 'conciso', // conciso, dettagliato, bullet + model = DEFAULT_MODEL, + } = req.body; + + if (!text) { + return res.status(400).json({ error: 'Text richiesto' }); + } + + let styleInstruction = ''; + switch (style) { + case 'bullet': + styleInstruction = 'Usa un elenco puntato.'; + break; + case 'dettagliato': + styleInstruction = 'Fornisci un riassunto dettagliato.'; + break; + default: + styleInstruction = 'Sii conciso e chiaro.'; + } + + const lengthInstruction = maxLength ? ` Massimo ${maxLength} parole.` : ''; + + const response = await fetch(`${OLLAMA_URL}/api/generate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model, + prompt: `Riassumi il seguente testo. ${styleInstruction}${lengthInstruction}\n\n${text}`, + stream: false, + options: { temperature: 0.5 }, + }), + }); + + if (!response.ok) { + throw new Error(`Ollama error: ${response.status}`); + } + + const data = await response.json(); + res.json({ + summary: data.response.trim(), + style, + }); + } catch (error) { + console.error('[api2/summarize] Error:', error); + res.status(500).json({ error: error.message }); + } +}); + +/** + * Analisi sentiment + * POST /api/sentiment + */ +router.post('/sentiment', async (req, res) => { + try { + const { text, model = DEFAULT_MODEL } = req.body; + + if (!text) { + return res.status(400).json({ error: 'Text richiesto' }); + } + + const response = await fetch(`${OLLAMA_URL}/api/generate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model, + prompt: `Analizza il sentiment del seguente testo e rispondi SOLO con un JSON valido nel formato: +{"sentiment": "positive" | "negative" | "neutral" | "mixed", "confidence": 0.0-1.0, "emotions": ["emotion1", "emotion2"], "explanation": "breve spiegazione"} + +Testo da analizzare: +${text}`, + stream: false, + options: { temperature: 0.1 }, + }), + }); + + if (!response.ok) { + throw new Error(`Ollama error: ${response.status}`); + } + + const data = await response.json(); + + try { + const jsonMatch = data.response.match(/\{[\s\S]*\}/); + if (jsonMatch) { + const parsed = JSON.parse(jsonMatch[0]); + res.json(parsed); + } else { + res.json({ sentiment: 'unknown', raw: data.response }); + } + } catch (e) { + res.json({ sentiment: 'unknown', raw: data.response }); + } + } catch (error) { + console.error('[api2/sentiment] Error:', error); + res.status(500).json({ error: error.message }); + } +}); + +/** + * Estrai JSON strutturato da testo + * POST /api/extract + */ +router.post('/extract', async (req, res) => { + try { + const { text, schema, model = DEFAULT_MODEL } = req.body; + + if (!text) { + return res.status(400).json({ error: 'Text richiesto' }); + } + if (!schema) { + return res.status(400).json({ error: 'Schema richiesto' }); + } + + const response = await fetch(`${OLLAMA_URL}/api/generate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model, + prompt: `Estrai le informazioni dal seguente testo e restituisci SOLO un JSON valido con questa struttura: +${JSON.stringify(schema, null, 2)} + +Testo da analizzare: +${text} + +Rispondi SOLO con il JSON, senza altro testo.`, + stream: false, + options: { temperature: 0.1 }, + }), + }); + + if (!response.ok) { + throw new Error(`Ollama error: ${response.status}`); + } + + const data = await response.json(); + + try { + const jsonMatch = data.response.match(/\{[\s\S]*\}/); + if (jsonMatch) { + res.json(JSON.parse(jsonMatch[0])); + } else { + res.status(422).json({ error: 'Could not parse JSON', raw: data.response }); + } + } catch (e) { + res.status(422).json({ error: 'Invalid JSON response', raw: data.response }); + } + } catch (error) { + console.error('[api2/extract] Error:', error); + res.status(500).json({ error: error.message }); + } +}); + +/** + * Correggi grammatica + * POST /api/grammar + */ +router.post('/grammar', async (req, res) => { + try { + const { + text, + language = 'italiano', + model = DEFAULT_MODEL, + } = req.body; + + if (!text) { + return res.status(400).json({ error: 'Text richiesto' }); + } + + const response = await fetch(`${OLLAMA_URL}/api/generate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model, + prompt: `Correggi gli errori grammaticali e di ortografia nel seguente testo in ${language}. Rispondi SOLO con il testo corretto, senza spiegazioni:\n\n${text}`, + stream: false, + options: { temperature: 0.2 }, + }), + }); + + if (!response.ok) { + throw new Error(`Ollama error: ${response.status}`); + } + + const data = await response.json(); + res.json({ + corrected: data.response.trim(), + original: text, + language, + }); + } catch (error) { + console.error('[api2/grammar] Error:', error); + res.status(500).json({ error: error.message }); + } +}); + +/** + * Rispondi a domanda con contesto + * POST /api/qa + */ +router.post('/qa', async (req, res) => { + try { + const { + question, + context, + model = DEFAULT_MODEL, + } = req.body; + + if (!question) { + return res.status(400).json({ error: 'Question richiesta' }); + } + if (!context) { + return res.status(400).json({ error: 'Context richiesto' }); + } + + const response = await fetch(`${OLLAMA_URL}/api/generate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model, + prompt: `Basandoti SOLO sul seguente contesto, rispondi alla domanda. Se la risposta non è nel contesto, dì che non hai abbastanza informazioni. + +Contesto: +${context} + +Domanda: ${question} + +Risposta:`, + stream: false, + options: { temperature: 0.3 }, + }), + }); + + if (!response.ok) { + throw new Error(`Ollama error: ${response.status}`); + } + + const data = await response.json(); + res.json({ + answer: data.response.trim(), + question, + }); + } catch (error) { + console.error('[api2/qa] Error:', error); + res.status(500).json({ error: error.message }); + } +}); + +/** + * Genera embeddings + * POST /api/embeddings + */ +router.post('/embeddings', async (req, res) => { + try { + const { + text, + model = 'nomic-embed-text', + } = req.body; + + if (!text) { + return res.status(400).json({ error: 'Text richiesto' }); + } + + const response = await fetch(`${OLLAMA_URL}/api/embeddings`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model, + prompt: text, + }), + }); + + if (!response.ok) { + throw new Error(`Ollama error: ${response.status}`); + } + + const data = await response.json(); + res.json({ + embedding: data.embedding, + dimensions: data.embedding?.length, + model, + }); + } catch (error) { + console.error('[api2/embeddings] Error:', error); + res.status(500).json({ error: error.message }); + } +}); + +/** + * Classifica testo in categorie + * POST /api/classify + */ +router.post('/classify', async (req, res) => { + try { + const { + text, + categories, + model = DEFAULT_MODEL, + } = req.body; + + if (!text) { + return res.status(400).json({ error: 'Text richiesto' }); + } + if (!categories || !Array.isArray(categories)) { + return res.status(400).json({ error: 'Categories array richiesto' }); + } + + const response = await fetch(`${OLLAMA_URL}/api/generate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model, + prompt: `Classifica il seguente testo in UNA delle seguenti categorie: ${categories.join(', ')} + +Rispondi SOLO con un JSON nel formato: +{"category": "categoria_scelta", "confidence": 0.0-1.0, "reason": "breve motivazione"} + +Testo: ${text}`, + stream: false, + options: { temperature: 0.1 }, + }), + }); + + if (!response.ok) { + throw new Error(`Ollama error: ${response.status}`); + } + + const data = await response.json(); + + try { + const jsonMatch = data.response.match(/\{[\s\S]*\}/); + if (jsonMatch) { + res.json(JSON.parse(jsonMatch[0])); + } else { + res.json({ category: 'unknown', raw: data.response }); + } + } catch (e) { + res.json({ category: 'unknown', raw: data.response }); + } + } catch (error) { + console.error('[api2/classify] Error:', error); + res.status(500).json({ error: error.message }); + } +}); + + +/** + * Pull/scarica un nuovo modello + * POST /api2/models/pull + */ +router.post('/models/pull', async (req, res) => { + try { + const { model } = req.body; + if (!model) { + return res.status(400).json({ error: 'Model name richiesto' }); + } + + // Streaming del progresso + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + + const response = await fetch(`${OLLAMA_URL}/api/pull`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: model, stream: true }), + }); + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + + while (true) { + const { done, value } = await reader.read(); + if (done) { + res.write('data: [DONE]\n\n'); + res.end(); + break; + } + + const chunk = decoder.decode(value); + res.write(`data: ${chunk}\n\n`); + } + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +/** + * Elimina un modello + * DELETE /api2/models/:name + */ +router.delete('/models/:name', async (req, res) => { + try { + const { name } = req.params; + + const response = await fetch(`${OLLAMA_URL}/api/delete`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name }), + }); + + if (response.ok) { + res.json({ success: true, message: `Modello ${name} eliminato` }); + } else { + throw new Error(`Errore eliminazione: ${response.status}`); + } + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// ============================================ +// EXPORT +// ============================================ + +module.exports = router; \ No newline at end of file diff --git a/src/router/index_router.js b/src/router/index_router.js index 7aa5047..ae23f80 100755 --- a/src/router/index_router.js +++ b/src/router/index_router.js @@ -300,6 +300,44 @@ router.post(process.env.LINKVERIF_REG, (req, res) => { }); }); +router.post(process.env.CHECKREVERIF_EMAIL, (req, res) => { + const body = _.pick(req.body, ['idapp', 'idlink']); + const idapp = body.idapp; + const idlink = body.idlink; + + // Cerco l'idlink se è ancora da Verificare + + User.findByLinkVerifEmail(idapp, idlink) + .then((user) => { + if (!user) { + //console.log("NON TROVATO!"); + return res.status(404).send({code: RIS_CODE_ERRORE, msg: 'Verifica email non andata a buon fine. Ripetere.'}); + } else { + console.log('user', user); + if (user.verified_email) { + res.send({ + code: server_constants.RIS_CODE_EMAIL_ALREADY_VERIFIED, + msg: tools.getres__("L'Email è già stata Verificata", res), + }); + } else { + user.verified_email = true; + user.lasttimeonline = new Date(); + user.save().then(() => { + //console.log("TROVATOOOOOO!"); + res.send({ + code: server_constants.RIS_CODE_EMAIL_VERIFIED, + msg: tools.getres__('EMAIL', res) + ' ' + tools.getres__('VERIF', res), + }); + }); + } + } + }) + .catch((e) => { + console.log(process.env.LINKVERIF_REG, e.message); + res.status(400).send(); + }); +}); + router.post(process.env.ADD_NEW_SITE, async (req, res) => { try { const body = req.body; diff --git a/src/router/users_router.js b/src/router/users_router.js index 8dc05d8..a86d8ca 100755 --- a/src/router/users_router.js +++ b/src/router/users_router.js @@ -506,13 +506,15 @@ router.post('/profile', authenticate, (req, res) => { const perm = req.user ? req.user.perm : tools.Perm.PERM_NONE; const username = req.body['username']; const idapp = req.body.idapp; + const idnotif = req.body['idnotif'] || ''; //++Todo: controlla che tipo di dati ha il permesso di leggere try { // Check if ìs a Notif to read - const idnotif = req.body['idnotif'] ? req.body['idnotif'] : ''; - SendNotif.setNotifAsRead(idapp, usernameOrig, idnotif); + if (idnotif) { + SendNotif.setNotifAsRead(idapp, usernameOrig, idnotif); + } return User.getUserProfileByUsername(idapp, username, usernameOrig, false, perm) .then((ris) => { @@ -601,6 +603,7 @@ router.post('/panel', authenticate, async (req, res) => { username: 1, name: 1, surname: 1, + verified_email: 1, email: 1, verified_by_aportador: 1, aportador_solidario: 1, @@ -1124,6 +1127,11 @@ async function eseguiDbOpUser(idapp, mydata, locale, req, res) { await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.noCircuit': mydata.value } }); } else if (mydata.dbop === 'noComune') { await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.noComune': mydata.value } }); + } else if (mydata.dbop === 'verifiedemail') { + await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'verified_email': mydata.value } }); + } else if (mydata.dbop === 'resendVerificationEmail') { + // Invia la email di Verifica email + const ris = await sendemail.sendEmail_ReVerifyingEmail(mydata, idapp); } else if (mydata.dbop === 'noCircIta') { await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.noCircIta': mydata.value } }); } else if (mydata.dbop === 'insert_circuito_ita') { diff --git a/src/sendemail.js b/src/sendemail.js index 73107b6..22ef96f 100755 --- a/src/sendemail.js +++ b/src/sendemail.js @@ -494,6 +494,21 @@ module.exports = { const strlinkreg = tools.getHostByIdApp(idapp) + process.env.LINKVERIF_REG + `/?idapp=${idapp}&idlink=${idreg}`; return strlinkreg; }, + getlinkVerifyEmail: async function (idapp, email, username) { + try { + const reg = require('./reg/registration'); + + + const idverif = reg.getlinkregByEmail(idapp, email, username); + + await User.setLinkToVerifiedEmail(idapp, username, idverif); + + const strlinkreg = tools.getHostByIdApp(idapp) + process.env.CHECKREVERIF_EMAIL + `/?idapp=${idapp}&idlink=${idverif}`; + return strlinkreg; + } catch (e) { + console.error('ERROR getlinkVerifyEmail'); + } + }, getlinkInvitoReg: function (idapp, dati) { const strlinkreg = tools.getHostByIdApp(idapp) + `/invitetoreg/${dati.token}`; return strlinkreg; @@ -675,6 +690,38 @@ module.exports = { console.error('Err sendEmail_Utente_Ammesso', e); } }, + sendEmail_ReVerifyingEmail: async function (dati, idapp) { + try { + const user = await User.getUserById(idapp, dati._id); + const lang = user.lang; + const username = user.username; + const email = user.email; + + let mylocalsconf = { + idapp, + dataemail: await this.getdataemail(idapp), + baseurl: tools.getHostByIdApp(idapp), + locale: lang, + nomeapp: tools.getNomeAppByIdApp(idapp), + strlinksito: tools.getHostByIdApp(idapp), + //strlinkreg: this.getlinkReg(idapp, idreg), + emailto: email, + name: user.name, + username: user.username, + verifyLink: await this.getlinkVerifyEmail(idapp, email, username), + user, + supportEmail: tools.getContactEmailSupportBydApp(idapp), + }; + + const quale_email_inviare = this.getPathEmail(idapp, 'reg_resend_email_to_verifiyng') + '/' + lang; + + const ris = await this.sendEmail_base(quale_email_inviare, email, mylocalsconf, ''); + + return ris; + } catch (e) { + console.error('Err sendEmail_ReVerifyingEmail', e); + } + }, sendEmail_Utente_Abilitato_Circuito_FidoConcesso: async function (lang, emailto, user, idapp, dati) { try { let mylocalsconf = { diff --git a/src/server/setupRouters.js b/src/server/setupRouters.js index 7dcb77f..390728d 100644 --- a/src/server/setupRouters.js +++ b/src/server/setupRouters.js @@ -36,6 +36,7 @@ function setupRouters(app) { ['/aitools', 'aitools_router'], ['/apisqlsrv', 'articleRoutes'], ['/api', 'api_router'], + ['/api2', 'api2_router'], ['/api/telegram', 'telegram_router'], ['/inviti', 'invitaAmicoRoutes'], ]; diff --git a/src/server_old.js b/src/server_old.js index d85c2f8..bac849b 100755 --- a/src/server_old.js +++ b/src/server_old.js @@ -146,6 +146,7 @@ connectToDatabase(connectionUrl, options) const aitools_router = require('./router/aitools_router'); const article_router = require('./router/articleRoutes'); const api_router = require('./router/api_router'); + const api2_router = require('./router/api2_router'); const { MyEvent } = require('./models/myevent'); @@ -252,6 +253,7 @@ connectToDatabase(connectionUrl, options) app.use('/aitools', aitools_router); app.use('/apisqlsrv', article_router); app.use('/api', api_router); + app.use('/api2', api2_router); mystart(); }); diff --git a/src/tools/general.js b/src/tools/general.js index 4f9a599..61e12d2 100755 --- a/src/tools/general.js +++ b/src/tools/general.js @@ -2023,6 +2023,19 @@ module.exports = { return false; }, + getContactEmailSupportBydApp: function (idapp, option) { + const myapp = this.MYAPPS.find((item) => item.idapp === idapp); + if (myapp) { + if (myapp.hasOwnProperty('contacts')) { + if (myapp.confsite.hasOwnProperty('email')) { + return myapp.contacts.email; + } + } + } + + return ''; + }, + getEnableTokenExpiredByIdApp: function (idapp) { const myapp = this.MYAPPS.find((item) => item.idapp === idapp); if (myapp && myapp.confpages && myapp.confpages.hasOwnProperty('enableTokenExpired')) { @@ -5849,6 +5862,10 @@ module.exports = { let mystr = ''; let userfrom = ''; let userto = ''; + let namefrom = ''; + let surnamefrom = ''; + let nameto = ''; + let surnameto = ''; let profilefrom = null; let profileto = null; @@ -5866,6 +5883,8 @@ module.exports = { } if (mov.userfrom) { userfrom += mov.userfrom.username; + namefrom = mov.userfrom.name; + surnamefrom = mov.userfrom.surname; profilefrom = mov.userfrom.profile; } @@ -5879,14 +5898,16 @@ module.exports = { } if (mov.userto) { userto += mov.userto.username; + nameto = mov.userto.name; + surnameto = mov.userto.surname; profileto = mov.userto.profile; } // mystr = t('movement.from') + userfrom + ' ' + t('movement.to') + userto return { - userfrom: { profile: profilefrom, username: userfrom }, - userto: { profile: profileto, username: userto }, + userfrom: { profile: profilefrom, username: userfrom, name: namefrom, surname: surnamefrom }, + userto: { profile: profileto, username: userto, name: nameto, surname: surnameto }, tipocontofrom, tipocontoto, };