This commit is contained in:
Huzaifa Inam 2026-08-07 15:35:45 +05:00
parent ef6f77a126
commit 726e15cffa
8 changed files with 255 additions and 112 deletions

View file

@ -2,9 +2,9 @@
PORT=3000 PORT=3000
NODE_ENV=production NODE_ENV=production
# Host Port Mappings for Docker / Coolify (Prevents conflict if host ports 3000 or 5432 are occupied) # Host Port Mappings for Docker / Coolify (Exposed on host as 7000 and 7001 to prevent conflicts with other applications)
HOST_PORT=8080 HOST_PORT=7000
POSTGRES_HOST_PORT=5433 POSTGRES_HOST_PORT=7001
# PostgreSQL Database Configuration # PostgreSQL Database Configuration
DATABASE_URL=postgresql://postgres:postgres_secure_pass_2026@postgres:5432/balance_sheet_db DATABASE_URL=postgresql://postgres:postgres_secure_pass_2026@postgres:5432/balance_sheet_db
@ -13,9 +13,12 @@ POSTGRES_PASSWORD=postgres_secure_pass_2026
# Fallback File Storage Path (Used if DATABASE_URL is omitted or offline) # Fallback File Storage Path (Used if DATABASE_URL is omitted or offline)
DATA_PATH=/app/data/portal-data.json DATA_PATH=/app/data/portal-data.json
# Optional SMTP Email Configuration (If using real SMTP server) # SMTP Email Configuration (Environment Variable Overrides)
# SMTP_HOST=smtp.gmail.com # SMTP_HOST=smtp.gmail.com
# SMTP_PORT=587 # SMTP_PORT=587
# SMTP_USER=your-email@domain.com # SMTP_USER=alerts@networkbank.com
# SMTP_PASS=your-app-password # SMTP_PASS=your_secret_smtp_password
# SMTP_FROM="Balance Sheet Portal <noreply@domain.com>" # SMTP_FROM="Balance Sheet Portal <noreply@networkbank.com>"
# SMTP_USE_TLS=true
# SMTP_AUTO_REMINDERS_ENABLED=true
# SMTP_REMINDER_FREQUENCY_DAYS=7

View file

@ -1,7 +1,7 @@
# Coolify & PostgreSQL Deployment Guide # Coolify & PostgreSQL Deployment Guide
This application consists of: This application consists of:
1. **Server Side**: Node.js + Express backend (`server.ts`) serving REST APIs, managing authentication, ledger reconciliation, audit logs, FX rates, and financial reports. 1. **Server Side**: Node.js + Express backend (`server.ts`) serving REST APIs, managing authentication, ledger reconciliation, audit logs, FX rates, SMTP dispatching, and financial reports.
2. **Database**: PostgreSQL (via `pg` connection pool) with automatic JSON fallback backup. 2. **Database**: PostgreSQL (via `pg` connection pool) with automatic JSON fallback backup.
3. **Frontend**: React SPA served directly via Express in production. 3. **Frontend**: React SPA served directly via Express in production.
@ -19,12 +19,20 @@ This application consists of:
```env ```env
NODE_ENV=production NODE_ENV=production
PORT=3000 PORT=3000
HOST_PORT=8080 # Customize if host port 3000 is occupied by another app HOST_PORT=7000 # Mapped host port (7000:3000) to avoid conflicts
POSTGRES_HOST_PORT=5433 # Customize if host port 5432 is occupied by another database POSTGRES_HOST_PORT=7001 # Mapped host database port (7001:5432) to avoid conflicts
POSTGRES_PASSWORD=your_custom_secure_password POSTGRES_PASSWORD=your_custom_secure_password
# SMTP Email Dispatch Settings
SMTP_HOST=smtp.your-server.com
SMTP_PORT=587
SMTP_USER=alerts@networkbank.com
SMTP_PASS=your_smtp_password
SMTP_FROM="Balance Sheet Portal <noreply@networkbank.com>"
SMTP_USE_TLS=true
``` ```
3. **Deploy**: 3. **Deploy**:
- Coolify will build the app container and spin up a dedicated `postgres:16-alpine` database container with health checks and persistent volume storage (`balance_sheet_postgres_data`). - Coolify will build the app container and spin up a dedicated `postgres:16-alpine` database container with health checks and persistent volume storage (`balance_sheet_postgres_data`). Access the app on host port `7000`.
--- ---
@ -41,16 +49,23 @@ If you prefer using Coolify's built-in managed PostgreSQL database resource:
```env ```env
DATABASE_URL=postgresql://postgres:pass@coolify-postgres-host:5432/balance_sheet_db DATABASE_URL=postgresql://postgres:pass@coolify-postgres-host:5432/balance_sheet_db
PORT=3000 PORT=3000
# SMTP Email Settings
SMTP_HOST=smtp.your-server.com
SMTP_PORT=587
SMTP_USER=alerts@networkbank.com
SMTP_PASS=your_smtp_password
``` ```
3. **Deploy**: 3. **Deploy**:
- The Express server will automatically connect to the PostgreSQL database, create the necessary table structures on boot, and store all portal state in PostgreSQL. - The Express server will automatically connect to the PostgreSQL database, create the necessary table structures on boot, and store all portal state in PostgreSQL.
--- ---
## 2. Server-Side Architecture & Port Configuration ## 2. Server-Side Architecture & Configuration
- **Internal Container Port**: Application listens on `3000` inside the container. - **Internal Container Port**: Application listens on `3000` inside the container.
- **Conflict Prevention**: Host port mappings in `docker-compose.yml` use `${HOST_PORT:-8080}:3000` and `${POSTGRES_HOST_PORT:-5433}:5432`. If port 3000 or 5432 is already used by another app on your server, simply set `HOST_PORT` or `POSTGRES_HOST_PORT` in your `.env` to any available port (e.g. `8080`, `8081`, `5433`). - **Conflict Prevention**: Host port mappings in `docker-compose.yml` use `${HOST_PORT:-7000}:3000` and `${POSTGRES_HOST_PORT:-7001}:5432` to avoid conflicts on host ports 3000, 8080, and 5432.
- **SMTP Environment Overrides**: SMTP configuration (`SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`, `SMTP_FROM`, `SMTP_USE_TLS`) is fully managed via environment variables.
- **Automatic Table Creation**: On server boot, the backend runs `initPgDatabase()` to verify/create `app_portal_state`. - **Automatic Table Creation**: On server boot, the backend runs `initPgDatabase()` to verify/create `app_portal_state`.
- **Automatic State Recovery**: If PostgreSQL contains existing data, portal state is loaded directly from PostgreSQL. - **Automatic State Recovery**: If PostgreSQL contains existing data, portal state is loaded directly from PostgreSQL.
- **Resilient Fallback**: If `DATABASE_URL` is omitted, the app gracefully operates using persistent JSON storage (`/app/data/portal-data.json`). - **Resilient Fallback**: If `DATABASE_URL` is omitted, the app gracefully operates using persistent JSON storage (`/app/data/portal-data.json`).

View file

@ -18,32 +18,32 @@ A full-stack enterprise web application built for multi-branch balance sheet con
| File | Description | | File | Description |
| :--- | :--- | | :--- | :--- |
| `Dockerfile` & `dockerfile` | Multi-stage production container build (Vite client + Express CJS server). | | `Dockerfile` & `dockerfile` | Multi-stage production container build (Vite client + Express CJS server). |
| `docker-compose.yml` | Full-stack orchestration (App container + PostgreSQL 16 container with configurable host port mapping). | | `docker-compose.yml` | Full-stack orchestration (App container mapped to host port 7000 + PostgreSQL container mapped to host port 7001). |
| `.dockerignore` | Defines context exclusions for slim Docker image builds. | | `.dockerignore` | Defines context exclusions for slim Docker image builds. |
| `.env.example` | Template environment variables (`DATABASE_URL`, `PORT`, `HOST_PORT`, `POSTGRES_HOST_PORT`, `DATA_PATH`). | | `.env.example` | Template environment variables (`DATABASE_URL`, `PORT`, `HOST_PORT`, `POSTGRES_HOST_PORT`, `DATA_PATH`, `SMTP_*`). |
| `COOLIFY.md` | Complete deployment guide for Coolify platform & managed databases. | | `COOLIFY.md` | Complete deployment guide for Coolify platform & managed databases. |
| `server.ts` | Express server entry point with PostgreSQL initialization and REST API routes. | | `server.ts` | Express server entry point with PostgreSQL initialization, environment SMTP dispatching, and REST API routes. |
--- ---
## 🛠️ Local Development & Running via Docker ## 🛠️ Local Development & Running via Docker
### 1. Run via Docker Compose (With PostgreSQL & Configurable Host Ports) ### 1. Run via Docker Compose (With PostgreSQL & Non-Conflicting Host Ports)
```bash ```bash
docker-compose up --build -d docker-compose up --build -d
``` ```
By default, the application will be accessible on host port `http://localhost:8080` (mapped to internal container port `3000`), and PostgreSQL on host port `5433` (mapped to internal container port `5432`). By default, the application will be accessible on host port `http://localhost:7000` (mapped to internal container port `3000`), and PostgreSQL on host port `7001` (mapped to internal container port `5432`), preventing conflicts with common host ports like 3000, 8080, or 5432.
If host ports `3000` or `5432` are already used by another application, set custom host ports in `.env`: If you wish to use different custom host ports, set them in `.env`:
```env ```env
HOST_PORT=8080 HOST_PORT=7000
POSTGRES_HOST_PORT=5433 POSTGRES_HOST_PORT=7001
``` ```
### 2. Run via Docker CLI ### 2. Run via Docker CLI
```bash ```bash
docker build -t balance-sheet-portal . docker build -t balance-sheet-portal .
docker run -p 8080:3000 -v portal_data:/app/data balance-sheet-portal docker run -p 7000:3000 -v portal_data:/app/data balance-sheet-portal
``` ```
### 3. Native Node.js Development ### 3. Native Node.js Development

View file

@ -11,8 +11,8 @@ services:
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres_secure_pass_2026} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres_secure_pass_2026}
ports: ports:
# Use POSTGRES_HOST_PORT (default 5433) to prevent conflicts if host port 5432 is already occupied # Use POSTGRES_HOST_PORT (default 7001) to prevent conflicts on standard DB host ports
- "${POSTGRES_HOST_PORT:-5433}:5432" - "${POSTGRES_HOST_PORT:-7001}:5432"
volumes: volumes:
- postgres_db_data:/var/lib/postgresql/data - postgres_db_data:/var/lib/postgresql/data
healthcheck: healthcheck:
@ -33,16 +33,23 @@ services:
postgres: postgres:
condition: service_healthy condition: service_healthy
ports: ports:
- "7000" # Use HOST_PORT (default 7000) to prevent conflicts with other web applications
- "${HOST_PORT:-7000}:3000"
environment: environment:
- NODE_ENV=production - NODE_ENV=production
- PORT=7000 - PORT=3000
- DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD:-postgres_secure_pass_2026}@postgres:5432/balance_sheet_db - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD:-postgres_secure_pass_2026}@postgres:5432/balance_sheet_db
- DATA_PATH=/app/data/portal-data.json - DATA_PATH=/app/data/portal-data.json
- SMTP_HOST=${SMTP_HOST:-}
- SMTP_PORT=${SMTP_PORT:-}
- SMTP_USER=${SMTP_USER:-}
- SMTP_PASS=${SMTP_PASS:-}
- SMTP_FROM=${SMTP_FROM:-}
- SMTP_USE_TLS=${SMTP_USE_TLS:-}
volumes: volumes:
- portal_data:/app/data - portal_data:/app/data
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:7000/api/health"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"]
interval: 30s interval: 30s
timeout: 5s timeout: 5s
retries: 3 retries: 3

116
server.ts
View file

@ -6,6 +6,7 @@ import nodemailer from 'nodemailer';
import { import {
INITIAL_USERS, INITIAL_USERS,
INITIAL_SMTP_CONFIG, INITIAL_SMTP_CONFIG,
getSmtpConfigFromEnv,
RAW_15_MAY_26, RAW_15_MAY_26,
RAW_22_MAY_26, RAW_22_MAY_26,
INITIAL_VARIANCE_COMMENTS_22_MAY, INITIAL_VARIANCE_COMMENTS_22_MAY,
@ -79,6 +80,59 @@ const DEFAULT_PKR_RATES: Record<string, number> = {
CADPKR: 204.53, CADPKR: 204.53,
}; };
function getEffectiveSmtpConfig(storedConfig?: SmtpConfig): SmtpConfig {
const envConfig = getSmtpConfigFromEnv();
const base = storedConfig || INITIAL_SMTP_CONFIG;
return {
host: process.env.SMTP_HOST || base.host || envConfig.host,
port: process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : (base.port || envConfig.port),
username: process.env.SMTP_USER || process.env.SMTP_USERNAME || base.username || envConfig.username,
password: process.env.SMTP_PASS || process.env.SMTP_PASSWORD || base.password || envConfig.password || '',
fromEmail: process.env.SMTP_FROM || process.env.SMTP_FROM_EMAIL || base.fromEmail || envConfig.fromEmail,
useTls: process.env.SMTP_USE_TLS !== undefined ? process.env.SMTP_USE_TLS === 'true' : (base.useTls ?? envConfig.useTls),
autoRemindersEnabled: process.env.SMTP_AUTO_REMINDERS_ENABLED !== undefined ? process.env.SMTP_AUTO_REMINDERS_ENABLED === 'true' : (base.autoRemindersEnabled ?? envConfig.autoRemindersEnabled),
reminderFrequencyDays: process.env.SMTP_REMINDER_FREQUENCY_DAYS ? parseInt(process.env.SMTP_REMINDER_FREQUENCY_DAYS, 10) : (base.reminderFrequencyDays || envConfig.reminderFrequencyDays),
};
}
async function sendEmailNotification(to: string, subject: string, text: string, config: SmtpConfig): Promise<boolean> {
const host = process.env.SMTP_HOST || config.host;
const port = process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : config.port;
const user = process.env.SMTP_USER || process.env.SMTP_USERNAME || config.username;
const pass = process.env.SMTP_PASS || process.env.SMTP_PASSWORD || config.password;
const from = process.env.SMTP_FROM || process.env.SMTP_FROM_EMAIL || config.fromEmail;
const secure = process.env.SMTP_USE_TLS !== undefined ? process.env.SMTP_USE_TLS === 'true' : config.useTls;
if (host && user && pass && host !== 'smtp.networkbank.com') {
try {
const transporter = nodemailer.createTransport({
host,
port,
secure: port === 465 || secure,
auth: {
user,
pass,
},
tls: {
rejectUnauthorized: false
}
});
await transporter.sendMail({
from: from || user,
to,
subject,
text,
});
console.log(`[SMTP] Dispatched email to ${to} via ${host}:${port}`);
return true;
} catch (err) {
console.error(`[SMTP] Failed to send email to ${to} via ${host}:${port}:`, err);
return false;
}
}
return true;
}
function loadDB(): DB { function loadDB(): DB {
const defaultFx: ExchangeRateRecord[] = Object.entries(DEFAULT_FX_RATES).map(([curr, rate], idx) => ({ const defaultFx: ExchangeRateRecord[] = Object.entries(DEFAULT_FX_RATES).map(([curr, rate], idx) => ({
id: `fx-${idx + 1}`, id: `fx-${idx + 1}`,
@ -93,6 +147,7 @@ function loadDB(): DB {
try { try {
const raw = fs.readFileSync(dbPath, 'utf-8'); const raw = fs.readFileSync(dbPath, 'utf-8');
const parsed = JSON.parse(raw); const parsed = JSON.parse(raw);
parsed.smtpConfig = getEffectiveSmtpConfig(parsed.smtpConfig);
if (!parsed.pkrRates) { if (!parsed.pkrRates) {
parsed.pkrRates = { ...DEFAULT_PKR_RATES }; parsed.pkrRates = { ...DEFAULT_PKR_RATES };
} }
@ -140,7 +195,7 @@ function loadDB(): DB {
const db: DB = { const db: DB = {
users: [...INITIAL_USERS], users: [...INITIAL_USERS],
submissions: initialSubmissions, submissions: initialSubmissions,
smtpConfig: { ...INITIAL_SMTP_CONFIG }, smtpConfig: getEffectiveSmtpConfig(),
emailLogs: [ emailLogs: [
{ {
id: 'log-1', id: 'log-1',
@ -1496,29 +1551,60 @@ app.post('/api/v1/submissions/status', (req, res) => {
// ------------------------------------------------------------- // -------------------------------------------------------------
app.get('/api/smtp/config', (req, res) => { app.get('/api/smtp/config', (req, res) => {
res.json({ config: db.smtpConfig, logs: db.emailLogs }); res.json({ config: getEffectiveSmtpConfig(db.smtpConfig), logs: db.emailLogs });
}); });
app.post('/api/smtp/config', (req, res) => { app.post('/api/smtp/config', (req, res) => {
const { host, port, username, fromEmail, useTls, autoRemindersEnabled } = req.body; const { autoRemindersEnabled } = req.body;
db.smtpConfig = {
host: host || db.smtpConfig.host, if (!db.smtpConfig) {
port: Number(port) || db.smtpConfig.port, db.smtpConfig = getEffectiveSmtpConfig();
username: username || db.smtpConfig.username, }
fromEmail: fromEmail || db.smtpConfig.fromEmail,
useTls: useTls !== undefined ? useTls : db.smtpConfig.useTls, if (autoRemindersEnabled !== undefined) {
autoRemindersEnabled: autoRemindersEnabled !== undefined ? autoRemindersEnabled : db.smtpConfig.autoRemindersEnabled, db.smtpConfig.autoRemindersEnabled = Boolean(autoRemindersEnabled);
reminderFrequencyDays: 7, }
};
saveDB(db); saveDB(db);
res.json({ config: db.smtpConfig }); res.json({ config: getEffectiveSmtpConfig(db.smtpConfig) });
});
// Test SMTP connection by sending a test message
app.post('/api/smtp/test', async (req, res) => {
const { testEmail } = req.body;
const currentSmtp = getEffectiveSmtpConfig(db.smtpConfig);
const targetEmail = testEmail || currentSmtp.fromEmail || 'admin@networkbank.com';
const subject = `[SMTP TEST] Network Bank Balance Sheet Portal Connection Test`;
const body = `This is a test notification dispatched from the Head Office Balance Sheet Portal to verify SMTP server connectivity.\n\nServer Host: ${currentSmtp.host}:${currentSmtp.port}\nSender: ${currentSmtp.fromEmail}\nTimestamp: ${new Date().toISOString()}`;
const sentOk = await sendEmailNotification(targetEmail, subject, body, currentSmtp);
const emailLog: EmailLog = {
id: `log-${Date.now()}-${Math.random().toString(36).substring(2, 6)}`,
recipientEmail: targetEmail,
subject,
body,
sentAt: new Date().toISOString(),
status: sentOk ? 'sent' : 'failed',
triggerType: 'manual_reminder',
};
db.emailLogs.unshift(emailLog);
saveDB(db);
if (sentOk) {
res.json({ success: true, message: `SMTP test notification dispatched successfully to ${targetEmail}.`, logs: db.emailLogs });
} else {
res.status(500).json({ success: false, message: `Failed to send test email to ${targetEmail}. Please verify SMTP environment configuration on the host server.`, logs: db.emailLogs });
}
}); });
// Send reminders to branches // Send reminders to branches
app.post('/api/smtp/send-reminders', async (req, res) => { app.post('/api/smtp/send-reminders', async (req, res) => {
const period = db.activePeriod; const period = db.activePeriod;
const submissions = db.submissions[period] || {}; const submissions = db.submissions[period] || {};
const currentSmtp = getEffectiveSmtpConfig(db.smtpConfig);
const submittedBranchIds = Object.keys(submissions); const submittedBranchIds = Object.keys(submissions);
const pendingBranches = BRANCHES_LIST.filter((b) => !submittedBranchIds.includes(b.id)); const pendingBranches = BRANCHES_LIST.filter((b) => !submittedBranchIds.includes(b.id));
@ -1535,6 +1621,8 @@ app.post('/api/smtp/send-reminders', async (req, res) => {
? `Dear ${branch.name} Team, Thank you. Your Balance Sheet submission for period ${period} has been received and verified by Head Office.` ? `Dear ${branch.name} Team, Thank you. Your Balance Sheet submission for period ${period} has been received and verified by Head Office.`
: `Dear ${branch.name} Team, This is an automated reminder from Head Office Balance Sheet Portal. Please complete and submit your fixed format balance sheet for period ${period}. Ensure all variances > USD 2 Mn carry explanatory notes.`; : `Dear ${branch.name} Team, This is an automated reminder from Head Office Balance Sheet Portal. Please complete and submit your fixed format balance sheet for period ${period}. Ensure all variances > USD 2 Mn carry explanatory notes.`;
const sentOk = await sendEmailNotification(branch.contactEmail, subject, body, currentSmtp);
const emailLog: EmailLog = { const emailLog: EmailLog = {
id: `log-${Date.now()}-${Math.random().toString(36).substring(2, 6)}`, id: `log-${Date.now()}-${Math.random().toString(36).substring(2, 6)}`,
recipientEmail: branch.contactEmail, recipientEmail: branch.contactEmail,
@ -1542,7 +1630,7 @@ app.post('/api/smtp/send-reminders', async (req, res) => {
subject, subject,
body, body,
sentAt: new Date().toISOString(), sentAt: new Date().toISOString(),
status: 'sent', status: sentOk ? 'sent' : 'failed',
triggerType: 'manual_reminder', triggerType: 'manual_reminder',
}; };

View file

@ -34,6 +34,8 @@ export const AdminUserManagement: React.FC<AdminUserManagementProps> = ({ onUser
const [emailLogs, setEmailLogs] = useState<EmailLog[]>([]); const [emailLogs, setEmailLogs] = useState<EmailLog[]>([]);
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [reminderSending, setReminderSending] = useState<boolean>(false); const [reminderSending, setReminderSending] = useState<boolean>(false);
const [testingSmtp, setTestingSmtp] = useState<boolean>(false);
const [testEmailAddress, setTestEmailAddress] = useState<string>('');
const [statusMsg, setStatusMsg] = useState<string | null>(null); const [statusMsg, setStatusMsg] = useState<string | null>(null);
// FX Rates State (Head Office PKR Rates) // FX Rates State (Head Office PKR Rates)
@ -153,20 +155,43 @@ export const AdminUserManagement: React.FC<AdminUserManagementProps> = ({ onUser
} }
}; };
// Save SMTP Settings // Toggle Automated Notifications / Alerts
const handleSaveSmtp = async (e: React.FormEvent) => { const handleToggleAutoReminders = async (enabled: boolean) => {
e.preventDefault();
try { try {
const updated = { ...smtpConfig, autoRemindersEnabled: enabled };
setSmtpConfig(updated);
const res = await fetch('/api/smtp/config', { const res = await fetch('/api/smtp/config', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(smtpConfig), body: JSON.stringify({ autoRemindersEnabled: enabled }),
}); });
if (res.ok) { if (res.ok) {
setStatusMsg('SMTP Server Configuration saved successfully.'); setStatusMsg(`Automated notifications & submission alerts ${enabled ? 'ENABLED' : 'DISABLED'}.`);
} }
} catch (err) { } catch (err) {
console.error('Error saving SMTP config:', err); console.error('Error toggling auto reminders:', err);
}
};
// Test SMTP Connection
const handleTestSmtp = async (e: React.FormEvent) => {
e.preventDefault();
setTestingSmtp(true);
setStatusMsg(null);
try {
const res = await fetch('/api/smtp/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ testEmail: testEmailAddress || undefined }),
});
const data = await res.json();
if (data.logs) setEmailLogs(data.logs);
setStatusMsg(data.message || (res.ok ? 'Test email dispatched successfully.' : 'SMTP test failed.'));
} catch (err) {
console.error('Error testing SMTP connection:', err);
setStatusMsg('Failed to test SMTP connection.');
} finally {
setTestingSmtp(false);
} }
}; };
@ -407,72 +432,73 @@ export const AdminUserManagement: React.FC<AdminUserManagementProps> = ({ onUser
<div className="flex justify-between items-center border-b border-slate-800 pb-3"> <div className="flex justify-between items-center border-b border-slate-800 pb-3">
<h3 className="text-sm font-bold text-white uppercase tracking-wider flex items-center gap-2"> <h3 className="text-sm font-bold text-white uppercase tracking-wider flex items-center gap-2">
<Mail className="w-4 h-4 text-amber-400" /> <Mail className="w-4 h-4 text-amber-400" />
SMTP Server & Automated Notifications Automated Notifications & SMTP Verification
</h3> </h3>
<span className="text-xs font-mono text-emerald-400">SMTP Ready</span> <span className="text-xs font-mono text-emerald-400 flex items-center gap-1">
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span>
Env Config Active
</span>
</div> </div>
<form onSubmit={handleSaveSmtp} className="space-y-3 text-xs"> {/* Environmental Variable Info Banner */}
<div className="grid grid-cols-2 gap-3"> <div className="bg-slate-950/80 border border-slate-800 rounded-lg p-3 text-xs space-y-1">
<div className="text-slate-300 font-semibold flex items-center justify-between">
<span>Server SMTP Host:</span>
<span className="font-mono text-amber-300">{smtpConfig.host}:{smtpConfig.port}</span>
</div>
<div className="text-slate-400 text-[11px] font-mono">
Sender: {smtpConfig.fromEmail} ({smtpConfig.useTls ? 'TLS Enabled' : 'Standard'})
</div>
<p className="text-[10px] text-slate-500 pt-1 border-t border-slate-800/80">
Note: SMTP server credentials, port, and host are securely configured via server environment variables (<code className="text-slate-400">SMTP_HOST</code>, <code className="text-slate-400">SMTP_PORT</code>, <code className="text-slate-400">SMTP_USER</code>, <code className="text-slate-400">SMTP_PASS</code>).
</p>
</div>
{/* Toggle Controls for Automated Alerts & Notifications */}
<div className="bg-slate-950 p-3.5 rounded-lg border border-slate-800 flex items-center justify-between">
<div> <div>
<label className="text-slate-400 block mb-1 font-semibold">SMTP Host Server</label> <div className="text-xs font-bold text-slate-200">Automated Submission Notifications & Alerts</div>
<input <div className="text-[11px] text-slate-400">Automatically dispatch email alerts & reminders for pending balance sheet submissions.</div>
type="text"
value={smtpConfig.host}
onChange={(e) => setSmtpConfig({ ...smtpConfig, host: e.target.value })}
className="w-full bg-slate-950 border border-slate-700 rounded px-2.5 py-1.5 text-white focus:outline-none focus:ring-1 focus:ring-amber-500 font-mono"
/>
</div> </div>
<label className="relative inline-flex items-center cursor-pointer shrink-0 ml-3">
<div>
<label className="text-slate-400 block mb-1 font-semibold">Port</label>
<input
type="number"
value={smtpConfig.port}
onChange={(e) => setSmtpConfig({ ...smtpConfig, port: Number(e.target.value) })}
className="w-full bg-slate-950 border border-slate-700 rounded px-2.5 py-1.5 text-white focus:outline-none focus:ring-1 focus:ring-amber-500 font-mono"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-slate-400 block mb-1 font-semibold">From Sender Email</label>
<input
type="email"
value={smtpConfig.fromEmail}
onChange={(e) => setSmtpConfig({ ...smtpConfig, fromEmail: e.target.value })}
className="w-full bg-slate-950 border border-slate-700 rounded px-2.5 py-1.5 text-white focus:outline-none focus:ring-1 focus:ring-amber-500 font-mono"
/>
</div>
<div>
<label className="text-slate-400 block mb-1 font-semibold">Username / Service Account</label>
<input
type="text"
value={smtpConfig.username}
onChange={(e) => setSmtpConfig({ ...smtpConfig, username: e.target.value })}
className="w-full bg-slate-950 border border-slate-700 rounded px-2.5 py-1.5 text-white focus:outline-none focus:ring-1 focus:ring-amber-500 font-mono"
/>
</div>
</div>
<div className="flex items-center justify-between pt-2 border-t border-slate-800">
<label className="flex items-center space-x-2 cursor-pointer text-slate-300">
<input <input
type="checkbox" type="checkbox"
checked={smtpConfig.useTls} checked={smtpConfig.autoRemindersEnabled}
onChange={(e) => setSmtpConfig({ ...smtpConfig, useTls: e.target.checked })} onChange={(e) => handleToggleAutoReminders(e.target.checked)}
className="rounded text-amber-500 focus:ring-0" className="sr-only peer"
/> />
<span>Enable TLS / SSL Secure Enclosure</span> <div className="w-9 h-5 bg-slate-800 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-emerald-600"></div>
</label> </label>
</div>
{/* Test SMTP Connection Form */}
<form onSubmit={handleTestSmtp} className="bg-slate-950 p-3.5 rounded-lg border border-slate-800 space-y-2.5">
<h4 className="text-xs font-bold text-slate-300 flex items-center gap-1.5">
<Send className="w-3.5 h-3.5 text-amber-400" />
Test SMTP Connection
</h4>
<p className="text-[11px] text-slate-400">
Dispatch a test email message to verify host connectivity and credential authentication.
</p>
<div className="flex items-center gap-2">
<input
type="email"
placeholder={smtpConfig.fromEmail || "recipient@networkbank.com"}
value={testEmailAddress}
onChange={(e) => setTestEmailAddress(e.target.value)}
className="flex-1 bg-slate-900 border border-slate-700 rounded px-2.5 py-1.5 text-xs text-white placeholder-slate-500 font-mono focus:outline-none focus:ring-1 focus:ring-amber-500"
/>
<button <button
type="submit" type="submit"
className="px-3 py-1.5 bg-amber-600 hover:bg-amber-500 text-white font-bold rounded text-xs transition-colors" disabled={testingSmtp}
className="px-3.5 py-1.5 bg-amber-600 hover:bg-amber-500 text-white font-bold rounded text-xs flex items-center gap-1.5 transition-colors cursor-pointer shrink-0"
> >
Save SMTP Config {testingSmtp ? (
<RefreshCw className="w-3.5 h-3.5 animate-spin" />
) : (
<Send className="w-3.5 h-3.5" />
)}
<span>Test SMTP</span>
</button> </button>
</div> </div>
</form> </form>

View file

@ -135,15 +135,18 @@ export const INITIAL_USERS: User[] = [
} }
]; ];
export const INITIAL_SMTP_CONFIG: SmtpConfig = { export const getSmtpConfigFromEnv = (): SmtpConfig => ({
host: 'smtp.networkbank.com', host: process.env.SMTP_HOST || 'smtp.networkbank.com',
port: 587, port: process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : 587,
username: 'alerts@networkbank.com', username: process.env.SMTP_USER || process.env.SMTP_USERNAME || 'alerts@networkbank.com',
fromEmail: 'noreply-portal@networkbank.com', password: process.env.SMTP_PASS || process.env.SMTP_PASSWORD || '',
useTls: true, fromEmail: process.env.SMTP_FROM || process.env.SMTP_FROM_EMAIL || 'noreply-portal@networkbank.com',
autoRemindersEnabled: true, useTls: process.env.SMTP_USE_TLS !== undefined ? process.env.SMTP_USE_TLS === 'true' : true,
reminderFrequencyDays: 7, autoRemindersEnabled: process.env.SMTP_AUTO_REMINDERS_ENABLED !== undefined ? process.env.SMTP_AUTO_REMINDERS_ENABLED === 'true' : true,
}; reminderFrequencyDays: process.env.SMTP_REMINDER_FREQUENCY_DAYS ? parseInt(process.env.SMTP_REMINDER_FREQUENCY_DAYS, 10) : 7,
});
export const INITIAL_SMTP_CONFIG: SmtpConfig = getSmtpConfigFromEnv();
// Raw baseline submissions for 15-May-26 // Raw baseline submissions for 15-May-26
export const RAW_15_MAY_26: Record<BranchId, BalanceSheetItems> = { export const RAW_15_MAY_26: Record<BranchId, BalanceSheetItems> = {

View file

@ -292,6 +292,7 @@ export interface SmtpConfig {
host: string; host: string;
port: number; port: number;
username: string; username: string;
password?: string;
fromEmail: string; fromEmail: string;
useTls: boolean; useTls: boolean;
autoRemindersEnabled: boolean; autoRemindersEnabled: boolean;