feat: Add SMTP testing and improve Docker setup
Introduce a new feature to test SMTP configuration directly from the settings page. This involves adding a new API endpoint and corresponding UI elements to trigger and display the results of an SMTP test. Additionally, this commit refactors the Docker setup by consolidating Dockerfiles and removing unnecessary configuration files. The goal is to streamline the build process and reduce image size and complexity.
This commit is contained in:
BIN
.dockerignore
BIN
.dockerignore
Binary file not shown.
15
Dockerfile
15
Dockerfile
@@ -1,15 +0,0 @@
|
|||||||
# Stage 1: Build Frontend
|
|
||||||
FROM node:18-alpine as build
|
|
||||||
WORKDIR /app
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm install
|
|
||||||
COPY . .
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Stage 2: Serve with Nginx
|
|
||||||
FROM nginx:alpine
|
|
||||||
COPY --from=build /app/dist /usr/share/nginx/html
|
|
||||||
# Copy the nginx configuration file (using the .txt extension as provided in source)
|
|
||||||
COPY nginx.txt /etc/nginx/nginx.conf
|
|
||||||
EXPOSE 80
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
|
||||||
|
|||||||
38
nginx.conf
38
nginx.conf
@@ -1,38 +0,0 @@
|
|||||||
worker_processes 1;
|
|
||||||
|
|
||||||
events { worker_connections 1024; }
|
|
||||||
|
|
||||||
http {
|
|
||||||
include mime.types;
|
|
||||||
default_type application/octet-stream;
|
|
||||||
sendfile on;
|
|
||||||
keepalive_timeout 65;
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
# Limite upload per allegati (es. foto/video ticket) - Allineato con il backend
|
|
||||||
client_max_body_size 50M;
|
|
||||||
|
|
||||||
# Compressione Gzip
|
|
||||||
gzip on;
|
|
||||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
|
||||||
|
|
||||||
# Gestione SPA (React Router)
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Proxy API verso il backend
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://backend:3001;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { CondoService } from '../services/mockDb';
|
import { CondoService } from '../services/mockDb';
|
||||||
import { AppSettings, Family, User, AlertDefinition, Condo, Notice, NoticeIconType, NoticeRead } from '../types';
|
import { AppSettings, Family, User, AlertDefinition, Condo, Notice, NoticeIconType, NoticeRead } from '../types';
|
||||||
import { Save, Building, Coins, Plus, Pencil, Trash2, X, CalendarCheck, AlertTriangle, User as UserIcon, Server, Bell, Clock, FileText, Lock, Megaphone, CheckCircle2, Info, Hammer, Link as LinkIcon, Eye, Calendar, List, UserCog, Mail, Power, MapPin, CreditCard, ToggleLeft, ToggleRight, LayoutGrid, PieChart, Users } from 'lucide-react';
|
import { Save, Building, Coins, Plus, Pencil, Trash2, X, CalendarCheck, AlertTriangle, User as UserIcon, Server, Bell, Clock, FileText, Lock, Megaphone, CheckCircle2, Info, Hammer, Link as LinkIcon, Eye, Calendar, List, UserCog, Mail, Power, MapPin, CreditCard, ToggleLeft, ToggleRight, LayoutGrid, PieChart, Users, Send } from 'lucide-react';
|
||||||
|
|
||||||
export const SettingsPage: React.FC = () => {
|
export const SettingsPage: React.FC = () => {
|
||||||
const currentUser = CondoService.getCurrentUser();
|
const currentUser = CondoService.getCurrentUser();
|
||||||
@@ -91,6 +91,8 @@ export const SettingsPage: React.FC = () => {
|
|||||||
|
|
||||||
// SMTP Modal State
|
// SMTP Modal State
|
||||||
const [showSmtpModal, setShowSmtpModal] = useState(false);
|
const [showSmtpModal, setShowSmtpModal] = useState(false);
|
||||||
|
const [testingSmtp, setTestingSmtp] = useState(false);
|
||||||
|
const [testSmtpMsg, setTestSmtpMsg] = useState('');
|
||||||
|
|
||||||
// Notices (Bacheca) State
|
// Notices (Bacheca) State
|
||||||
const [notices, setNotices] = useState<Notice[]>([]);
|
const [notices, setNotices] = useState<Notice[]>([]);
|
||||||
@@ -238,6 +240,20 @@ export const SettingsPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSmtpTest = async () => {
|
||||||
|
if (!globalSettings?.smtpConfig) return;
|
||||||
|
setTestingSmtp(true);
|
||||||
|
setTestSmtpMsg('');
|
||||||
|
try {
|
||||||
|
await CondoService.testSmtpConfig(globalSettings.smtpConfig);
|
||||||
|
setTestSmtpMsg('Successo! Email di prova inviata.');
|
||||||
|
} catch(e: any) {
|
||||||
|
setTestSmtpMsg('Errore: ' + (e.message || "Impossibile connettersi"));
|
||||||
|
} finally {
|
||||||
|
setTestingSmtp(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleNewYear = async () => {
|
const handleNewYear = async () => {
|
||||||
if (!globalSettings) return;
|
if (!globalSettings) return;
|
||||||
const nextYear = globalSettings.currentYear + 1;
|
const nextYear = globalSettings.currentYear + 1;
|
||||||
@@ -1041,10 +1057,21 @@ export const SettingsPage: React.FC = () => {
|
|||||||
<label className="text-xs font-bold text-slate-500 uppercase mb-1 block">Email Mittente</label>
|
<label className="text-xs font-bold text-slate-500 uppercase mb-1 block">Email Mittente</label>
|
||||||
<input type="email" value={globalSettings?.smtpConfig?.fromEmail || ''} onChange={(e) => setGlobalSettings(prev => prev ? {...prev, smtpConfig: {...(prev.smtpConfig || {}), fromEmail: e.target.value} as any} : null)} className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="no-reply@condominio.it"/>
|
<input type="email" value={globalSettings?.smtpConfig?.fromEmail || ''} onChange={(e) => setGlobalSettings(prev => prev ? {...prev, smtpConfig: {...(prev.smtpConfig || {}), fromEmail: e.target.value} as any} : null)} className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="no-reply@condominio.it"/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input type="checkbox" checked={globalSettings?.smtpConfig?.secure || false} onChange={(e) => setGlobalSettings(prev => prev ? {...prev, smtpConfig: {...(prev.smtpConfig || {}), secure: e.target.checked} as any} : null)} className="w-4 h-4 text-blue-600"/>
|
<input type="checkbox" checked={globalSettings?.smtpConfig?.secure || false} onChange={(e) => setGlobalSettings(prev => prev ? {...prev, smtpConfig: {...(prev.smtpConfig || {}), secure: e.target.checked} as any} : null)} className="w-4 h-4 text-blue-600"/>
|
||||||
<label className="text-sm text-slate-700 font-medium">Usa SSL/TLS (Secure)</label>
|
<label className="text-sm text-slate-700 font-medium">Usa SSL/TLS (Secure)</label>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSmtpTest}
|
||||||
|
disabled={testingSmtp}
|
||||||
|
className="text-xs font-bold bg-amber-100 text-amber-700 px-3 py-1.5 rounded hover:bg-amber-200 transition-colors flex items-center gap-1 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{testingSmtp ? <span className="animate-pulse">Test...</span> : <><Send className="w-3 h-3"/> Test Configurazione</>}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{testSmtpMsg && <p className={`text-xs font-medium text-center ${testSmtpMsg.startsWith('Errore') ? 'text-red-500' : 'text-green-600'}`}>{testSmtpMsg}</p>}
|
||||||
|
|
||||||
<div className="pt-2 flex justify-between items-center border-t border-slate-100 mt-2">
|
<div className="pt-2 flex justify-between items-center border-t border-slate-100 mt-2">
|
||||||
<span className="text-green-600 text-sm font-medium">{successMsg}</span>
|
<span className="text-green-600 text-sm font-medium">{successMsg}</span>
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
FROM node:18-alpine
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Set production environment
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm install --production
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
EXPOSE 3001
|
|
||||||
CMD ["node", "server.js"]
|
|
||||||
|
|||||||
@@ -163,6 +163,42 @@ app.put('/api/settings', authenticateToken, requireAdmin, async (req, res) => {
|
|||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// SMTP TEST
|
||||||
|
app.post('/api/settings/smtp-test', authenticateToken, requireAdmin, async (req, res) => {
|
||||||
|
const config = req.body; // Expects SmtpConfig object
|
||||||
|
const userEmail = req.user.email;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!config.host || !config.user || !config.pass) {
|
||||||
|
return res.status(400).json({ message: 'Parametri SMTP incompleti' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: config.host,
|
||||||
|
port: config.port,
|
||||||
|
secure: config.secure,
|
||||||
|
auth: { user: config.user, pass: config.pass },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify connection
|
||||||
|
await transporter.verify();
|
||||||
|
|
||||||
|
// Send Test Email
|
||||||
|
await transporter.sendMail({
|
||||||
|
from: config.fromEmail || config.user,
|
||||||
|
to: userEmail,
|
||||||
|
subject: 'CondoPay - Test Configurazione SMTP',
|
||||||
|
text: 'Se leggi questo messaggio, la configurazione SMTP è corretta.',
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (e) {
|
||||||
|
console.error("SMTP Test Error", e);
|
||||||
|
res.status(400).json({ message: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/api/years', authenticateToken, async (req, res) => {
|
app.get('/api/years', authenticateToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [rows] = await pool.query('SELECT DISTINCT for_year FROM payments ORDER BY for_year DESC');
|
const [rows] = await pool.query('SELECT DISTINCT for_year FROM payments ORDER BY for_year DESC');
|
||||||
@@ -199,7 +235,7 @@ app.post('/api/condos', authenticateToken, requireAdmin, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
await pool.query(
|
await pool.query(
|
||||||
'INSERT INTO condos (id, name, address, street_number, city, province, zip_code, notes, default_monthly_quota, paypal_client_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
'INSERT INTO condos (id, name, address, street_number, city, province, zip_code, notes, default_monthly_quota, paypal_client_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||||
[id, name, address, streetNumber, city, province, zipCode, notes, defaultMonthlyQuota, paypalClientId]
|
[id, name, address, streetNumber, city, province, zip_code, notes, defaultMonthlyQuota, paypalClientId]
|
||||||
);
|
);
|
||||||
res.json({ id, name, address, streetNumber, city, province, zipCode, notes, defaultMonthlyQuota, paypalClientId });
|
res.json({ id, name, address, streetNumber, city, province, zipCode, notes, defaultMonthlyQuota, paypalClientId });
|
||||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import { Family, Payment, AppSettings, User, AlertDefinition, Condo, Notice, NoticeRead, Ticket, TicketAttachment, TicketComment } from '../types';
|
import { Family, Payment, AppSettings, User, AlertDefinition, Condo, Notice, NoticeRead, Ticket, TicketAttachment, TicketComment, SmtpConfig } from '../types';
|
||||||
|
|
||||||
// --- CONFIGURATION TOGGLE ---
|
// --- CONFIGURATION TOGGLE ---
|
||||||
const FORCE_LOCAL_DB = false;
|
const FORCE_LOCAL_DB = false;
|
||||||
@@ -186,6 +186,13 @@ export const CondoService = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
testSmtpConfig: async (config: SmtpConfig): Promise<void> => {
|
||||||
|
await request('/settings/smtp-test', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(config)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
getAvailableYears: async (): Promise<number[]> => {
|
getAvailableYears: async (): Promise<number[]> => {
|
||||||
return request<number[]>('/years');
|
return request<number[]>('/years');
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user