v0.2
This commit is contained in:
parent
ef6f77a126
commit
726e15cffa
8 changed files with 255 additions and 112 deletions
17
.env.example
17
.env.example
|
|
@ -2,9 +2,9 @@
|
|||
PORT=3000
|
||||
NODE_ENV=production
|
||||
|
||||
# Host Port Mappings for Docker / Coolify (Prevents conflict if host ports 3000 or 5432 are occupied)
|
||||
HOST_PORT=8080
|
||||
POSTGRES_HOST_PORT=5433
|
||||
# Host Port Mappings for Docker / Coolify (Exposed on host as 7000 and 7001 to prevent conflicts with other applications)
|
||||
HOST_PORT=7000
|
||||
POSTGRES_HOST_PORT=7001
|
||||
|
||||
# PostgreSQL Database Configuration
|
||||
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)
|
||||
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_PORT=587
|
||||
# SMTP_USER=your-email@domain.com
|
||||
# SMTP_PASS=your-app-password
|
||||
# SMTP_FROM="Balance Sheet Portal <noreply@domain.com>"
|
||||
# SMTP_USER=alerts@networkbank.com
|
||||
# SMTP_PASS=your_secret_smtp_password
|
||||
# SMTP_FROM="Balance Sheet Portal <noreply@networkbank.com>"
|
||||
# SMTP_USE_TLS=true
|
||||
# SMTP_AUTO_REMINDERS_ENABLED=true
|
||||
# SMTP_REMINDER_FREQUENCY_DAYS=7
|
||||
|
|
|
|||
27
COOLIFY.md
27
COOLIFY.md
|
|
@ -1,7 +1,7 @@
|
|||
# Coolify & PostgreSQL Deployment Guide
|
||||
|
||||
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.
|
||||
3. **Frontend**: React SPA served directly via Express in production.
|
||||
|
||||
|
|
@ -19,12 +19,20 @@ This application consists of:
|
|||
```env
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
HOST_PORT=8080 # Customize if host port 3000 is occupied by another app
|
||||
POSTGRES_HOST_PORT=5433 # Customize if host port 5432 is occupied by another database
|
||||
HOST_PORT=7000 # Mapped host port (7000:3000) to avoid conflicts
|
||||
POSTGRES_HOST_PORT=7001 # Mapped host database port (7001:5432) to avoid conflicts
|
||||
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**:
|
||||
- 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
|
||||
DATABASE_URL=postgresql://postgres:pass@coolify-postgres-host:5432/balance_sheet_db
|
||||
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**:
|
||||
- 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.
|
||||
- **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 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`).
|
||||
|
|
|
|||
18
README.md
18
README.md
|
|
@ -18,32 +18,32 @@ A full-stack enterprise web application built for multi-branch balance sheet con
|
|||
| File | Description |
|
||||
| :--- | :--- |
|
||||
| `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. |
|
||||
| `.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. |
|
||||
| `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
|
||||
|
||||
### 1. Run via Docker Compose (With PostgreSQL & Configurable Host Ports)
|
||||
### 1. Run via Docker Compose (With PostgreSQL & Non-Conflicting Host Ports)
|
||||
```bash
|
||||
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
|
||||
HOST_PORT=8080
|
||||
POSTGRES_HOST_PORT=5433
|
||||
HOST_PORT=7000
|
||||
POSTGRES_HOST_PORT=7001
|
||||
```
|
||||
|
||||
### 2. Run via Docker CLI
|
||||
```bash
|
||||
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
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ services:
|
|||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres_secure_pass_2026}
|
||||
ports:
|
||||
# Use POSTGRES_HOST_PORT (default 5433) to prevent conflicts if host port 5432 is already occupied
|
||||
- "${POSTGRES_HOST_PORT:-5433}:5432"
|
||||
# Use POSTGRES_HOST_PORT (default 7001) to prevent conflicts on standard DB host ports
|
||||
- "${POSTGRES_HOST_PORT:-7001}:5432"
|
||||
volumes:
|
||||
- postgres_db_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
|
|
@ -33,16 +33,23 @@ services:
|
|||
postgres:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "7000"
|
||||
# Use HOST_PORT (default 7000) to prevent conflicts with other web applications
|
||||
- "${HOST_PORT:-7000}:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=7000
|
||||
- PORT=3000
|
||||
- DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD:-postgres_secure_pass_2026}@postgres:5432/balance_sheet_db
|
||||
- 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:
|
||||
- portal_data:/app/data
|
||||
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
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
|
|
|||
116
server.ts
116
server.ts
|
|
@ -6,6 +6,7 @@ import nodemailer from 'nodemailer';
|
|||
import {
|
||||
INITIAL_USERS,
|
||||
INITIAL_SMTP_CONFIG,
|
||||
getSmtpConfigFromEnv,
|
||||
RAW_15_MAY_26,
|
||||
RAW_22_MAY_26,
|
||||
INITIAL_VARIANCE_COMMENTS_22_MAY,
|
||||
|
|
@ -79,6 +80,59 @@ const DEFAULT_PKR_RATES: Record<string, number> = {
|
|||
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 {
|
||||
const defaultFx: ExchangeRateRecord[] = Object.entries(DEFAULT_FX_RATES).map(([curr, rate], idx) => ({
|
||||
id: `fx-${idx + 1}`,
|
||||
|
|
@ -93,6 +147,7 @@ function loadDB(): DB {
|
|||
try {
|
||||
const raw = fs.readFileSync(dbPath, 'utf-8');
|
||||
const parsed = JSON.parse(raw);
|
||||
parsed.smtpConfig = getEffectiveSmtpConfig(parsed.smtpConfig);
|
||||
if (!parsed.pkrRates) {
|
||||
parsed.pkrRates = { ...DEFAULT_PKR_RATES };
|
||||
}
|
||||
|
|
@ -140,7 +195,7 @@ function loadDB(): DB {
|
|||
const db: DB = {
|
||||
users: [...INITIAL_USERS],
|
||||
submissions: initialSubmissions,
|
||||
smtpConfig: { ...INITIAL_SMTP_CONFIG },
|
||||
smtpConfig: getEffectiveSmtpConfig(),
|
||||
emailLogs: [
|
||||
{
|
||||
id: 'log-1',
|
||||
|
|
@ -1496,29 +1551,60 @@ app.post('/api/v1/submissions/status', (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) => {
|
||||
const { host, port, username, fromEmail, useTls, autoRemindersEnabled } = req.body;
|
||||
db.smtpConfig = {
|
||||
host: host || db.smtpConfig.host,
|
||||
port: Number(port) || db.smtpConfig.port,
|
||||
username: username || db.smtpConfig.username,
|
||||
fromEmail: fromEmail || db.smtpConfig.fromEmail,
|
||||
useTls: useTls !== undefined ? useTls : db.smtpConfig.useTls,
|
||||
autoRemindersEnabled: autoRemindersEnabled !== undefined ? autoRemindersEnabled : db.smtpConfig.autoRemindersEnabled,
|
||||
reminderFrequencyDays: 7,
|
||||
};
|
||||
const { autoRemindersEnabled } = req.body;
|
||||
|
||||
if (!db.smtpConfig) {
|
||||
db.smtpConfig = getEffectiveSmtpConfig();
|
||||
}
|
||||
|
||||
if (autoRemindersEnabled !== undefined) {
|
||||
db.smtpConfig.autoRemindersEnabled = Boolean(autoRemindersEnabled);
|
||||
}
|
||||
|
||||
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
|
||||
app.post('/api/smtp/send-reminders', async (req, res) => {
|
||||
const period = db.activePeriod;
|
||||
const submissions = db.submissions[period] || {};
|
||||
const currentSmtp = getEffectiveSmtpConfig(db.smtpConfig);
|
||||
|
||||
const submittedBranchIds = Object.keys(submissions);
|
||||
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, 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 = {
|
||||
id: `log-${Date.now()}-${Math.random().toString(36).substring(2, 6)}`,
|
||||
recipientEmail: branch.contactEmail,
|
||||
|
|
@ -1542,7 +1630,7 @@ app.post('/api/smtp/send-reminders', async (req, res) => {
|
|||
subject,
|
||||
body,
|
||||
sentAt: new Date().toISOString(),
|
||||
status: 'sent',
|
||||
status: sentOk ? 'sent' : 'failed',
|
||||
triggerType: 'manual_reminder',
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ export const AdminUserManagement: React.FC<AdminUserManagementProps> = ({ onUser
|
|||
const [emailLogs, setEmailLogs] = useState<EmailLog[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [reminderSending, setReminderSending] = useState<boolean>(false);
|
||||
const [testingSmtp, setTestingSmtp] = useState<boolean>(false);
|
||||
const [testEmailAddress, setTestEmailAddress] = useState<string>('');
|
||||
const [statusMsg, setStatusMsg] = useState<string | null>(null);
|
||||
|
||||
// FX Rates State (Head Office PKR Rates)
|
||||
|
|
@ -153,20 +155,43 @@ export const AdminUserManagement: React.FC<AdminUserManagementProps> = ({ onUser
|
|||
}
|
||||
};
|
||||
|
||||
// Save SMTP Settings
|
||||
const handleSaveSmtp = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
// Toggle Automated Notifications / Alerts
|
||||
const handleToggleAutoReminders = async (enabled: boolean) => {
|
||||
try {
|
||||
const updated = { ...smtpConfig, autoRemindersEnabled: enabled };
|
||||
setSmtpConfig(updated);
|
||||
const res = await fetch('/api/smtp/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(smtpConfig),
|
||||
body: JSON.stringify({ autoRemindersEnabled: enabled }),
|
||||
});
|
||||
if (res.ok) {
|
||||
setStatusMsg('SMTP Server Configuration saved successfully.');
|
||||
setStatusMsg(`Automated notifications & submission alerts ${enabled ? 'ENABLED' : 'DISABLED'}.`);
|
||||
}
|
||||
} 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">
|
||||
<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" />
|
||||
SMTP Server & Automated Notifications
|
||||
Automated Notifications & SMTP Verification
|
||||
</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>
|
||||
|
||||
<form onSubmit={handleSaveSmtp} className="space-y-3 text-xs">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{/* Environmental Variable Info Banner */}
|
||||
<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>
|
||||
<label className="text-slate-400 block mb-1 font-semibold">SMTP Host Server</label>
|
||||
<input
|
||||
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 className="text-xs font-bold text-slate-200">Automated Submission Notifications & Alerts</div>
|
||||
<div className="text-[11px] text-slate-400">Automatically dispatch email alerts & reminders for pending balance sheet submissions.</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<label className="relative inline-flex items-center cursor-pointer shrink-0 ml-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={smtpConfig.useTls}
|
||||
onChange={(e) => setSmtpConfig({ ...smtpConfig, useTls: e.target.checked })}
|
||||
className="rounded text-amber-500 focus:ring-0"
|
||||
checked={smtpConfig.autoRemindersEnabled}
|
||||
onChange={(e) => handleToggleAutoReminders(e.target.checked)}
|
||||
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>
|
||||
</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
|
||||
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>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -135,15 +135,18 @@ export const INITIAL_USERS: User[] = [
|
|||
}
|
||||
];
|
||||
|
||||
export const INITIAL_SMTP_CONFIG: SmtpConfig = {
|
||||
host: 'smtp.networkbank.com',
|
||||
port: 587,
|
||||
username: 'alerts@networkbank.com',
|
||||
fromEmail: 'noreply-portal@networkbank.com',
|
||||
useTls: true,
|
||||
autoRemindersEnabled: true,
|
||||
reminderFrequencyDays: 7,
|
||||
};
|
||||
export const getSmtpConfigFromEnv = (): SmtpConfig => ({
|
||||
host: process.env.SMTP_HOST || 'smtp.networkbank.com',
|
||||
port: process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : 587,
|
||||
username: process.env.SMTP_USER || process.env.SMTP_USERNAME || 'alerts@networkbank.com',
|
||||
password: process.env.SMTP_PASS || process.env.SMTP_PASSWORD || '',
|
||||
fromEmail: process.env.SMTP_FROM || process.env.SMTP_FROM_EMAIL || 'noreply-portal@networkbank.com',
|
||||
useTls: process.env.SMTP_USE_TLS !== undefined ? process.env.SMTP_USE_TLS === 'true' : true,
|
||||
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
|
||||
export const RAW_15_MAY_26: Record<BranchId, BalanceSheetItems> = {
|
||||
|
|
|
|||
|
|
@ -292,6 +292,7 @@ export interface SmtpConfig {
|
|||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password?: string;
|
||||
fromEmail: string;
|
||||
useTls: boolean;
|
||||
autoRemindersEnabled: boolean;
|
||||
|
|
|
|||
Loading…
Reference in a new issue