Platform Email Settings (Admin): - Add GET/PUT/DELETE /admin/settings/email/* endpoints - Settings stored in admin_settings table override .env values - Support all providers: SMTP, SendGrid, Mailgun, Amazon SES - Edit mode UI with provider-specific configuration forms - Reset to .env defaults functionality - Test email to verify configuration Vendor Email Settings: - Add VendorEmailSettings model with one-to-one vendor relationship - Migration: v0a1b2c3d4e5_add_vendor_email_settings.py - Service: vendor_email_settings_service.py with tier validation - API endpoints: /vendor/email-settings/* (CRUD, status, verify) - Email tab in vendor settings page with full configuration - Warning banner until email is configured (like billing warnings) - Premium providers (SendGrid, Mailgun, SES) tier-gated to Business+ Email Service Updates: - get_platform_email_config(db) checks DB first, then .env - Configurable provider classes accept config dict - EmailService uses database-aware providers - Vendor emails use vendor's own SMTP (Wizamart doesn't pay) - "Powered by Wizamart" footer for Essential/Professional tiers - White-label (no footer) for Business/Enterprise tiers Other: - Add scripts/install.py for first-time platform setup - Add make install target - Update init-prod to include email template seeding 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
499 lines
19 KiB
JavaScript
499 lines
19 KiB
JavaScript
// static/admin/js/settings.js
|
|
// noqa: JS-003 - Uses ...baseData which is data() with safety check
|
|
|
|
const settingsLog = window.LogConfig?.loggers?.settings || console;
|
|
|
|
function adminSettings() {
|
|
// Get base data with safety check for standalone usage
|
|
const baseData = typeof data === 'function' ? data() : {};
|
|
|
|
return {
|
|
// Inherit base layout functionality from init-alpine.js
|
|
...baseData,
|
|
|
|
// Settings-specific state
|
|
currentPage: 'settings',
|
|
loading: true,
|
|
saving: false,
|
|
error: null,
|
|
successMessage: null,
|
|
activeTab: 'display',
|
|
displaySettings: {
|
|
rows_per_page: 20
|
|
},
|
|
logSettings: {
|
|
log_level: 'INFO',
|
|
log_file_max_size_mb: 10,
|
|
log_file_backup_count: 5,
|
|
db_log_retention_days: 30,
|
|
file_logging_enabled: true,
|
|
db_logging_enabled: true
|
|
},
|
|
notificationSettings: {
|
|
email_enabled: true,
|
|
in_app_enabled: true,
|
|
critical_only: false
|
|
},
|
|
shippingSettings: {
|
|
carrier_greco_label_url: 'https://dispatchweb.fr/Tracky/Home/',
|
|
carrier_colissimo_label_url: '',
|
|
carrier_xpresslogistics_label_url: ''
|
|
},
|
|
emailSettings: {
|
|
provider: 'smtp',
|
|
from_email: '',
|
|
from_name: '',
|
|
reply_to: '',
|
|
smtp_host: '',
|
|
smtp_port: 587,
|
|
smtp_user: '',
|
|
mailgun_domain: '',
|
|
aws_region: '',
|
|
debug: false,
|
|
enabled: true,
|
|
is_configured: false,
|
|
has_db_overrides: false
|
|
},
|
|
// Email editing form (separate from display to track changes)
|
|
emailForm: {
|
|
provider: 'smtp',
|
|
from_email: '',
|
|
from_name: '',
|
|
reply_to: '',
|
|
smtp_host: '',
|
|
smtp_port: 587,
|
|
smtp_user: '',
|
|
smtp_password: '',
|
|
smtp_use_tls: true,
|
|
smtp_use_ssl: false,
|
|
sendgrid_api_key: '',
|
|
mailgun_api_key: '',
|
|
mailgun_domain: '',
|
|
aws_access_key_id: '',
|
|
aws_secret_access_key: '',
|
|
aws_region: 'eu-west-1',
|
|
enabled: true,
|
|
debug: false
|
|
},
|
|
emailEditMode: false,
|
|
testEmailAddress: '',
|
|
sendingTestEmail: false,
|
|
|
|
async init() {
|
|
// Guard against multiple initialization
|
|
if (window._adminSettingsInitialized) return;
|
|
window._adminSettingsInitialized = true;
|
|
|
|
try {
|
|
settingsLog.info('=== SETTINGS PAGE INITIALIZING ===');
|
|
await Promise.all([
|
|
this.loadDisplaySettings(),
|
|
this.loadLogSettings(),
|
|
this.loadShippingSettings(),
|
|
this.loadEmailSettings()
|
|
]);
|
|
} catch (error) {
|
|
settingsLog.error('Init failed:', error);
|
|
this.error = 'Failed to initialize settings page';
|
|
}
|
|
},
|
|
|
|
async refresh() {
|
|
this.error = null;
|
|
this.successMessage = null;
|
|
await Promise.all([
|
|
this.loadDisplaySettings(),
|
|
this.loadLogSettings(),
|
|
this.loadShippingSettings(),
|
|
this.loadEmailSettings()
|
|
]);
|
|
},
|
|
|
|
async loadDisplaySettings() {
|
|
try {
|
|
const data = await apiClient.get('/admin/settings/display/rows-per-page');
|
|
this.displaySettings.rows_per_page = data.rows_per_page || 20;
|
|
settingsLog.info('Display settings loaded:', this.displaySettings);
|
|
} catch (error) {
|
|
settingsLog.error('Failed to load display settings:', error);
|
|
// Use default value on error
|
|
this.displaySettings.rows_per_page = 20;
|
|
}
|
|
},
|
|
|
|
async saveDisplaySettings() {
|
|
this.saving = true;
|
|
this.error = null;
|
|
this.successMessage = null;
|
|
|
|
try {
|
|
const data = await apiClient.put(`/admin/settings/display/rows-per-page?rows=${this.displaySettings.rows_per_page}`);
|
|
this.successMessage = data.message || 'Display settings saved successfully';
|
|
|
|
// Clear the cached platform settings so pages pick up the new value
|
|
if (window.PlatformSettings) {
|
|
window.PlatformSettings.clearCache();
|
|
}
|
|
|
|
// Auto-hide success message after 5 seconds
|
|
setTimeout(() => {
|
|
this.successMessage = null;
|
|
}, 5000);
|
|
|
|
settingsLog.info('Display settings saved successfully');
|
|
} catch (error) {
|
|
settingsLog.error('Failed to save display settings:', error);
|
|
this.error = error.response?.data?.detail || 'Failed to save display settings';
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
|
|
async loadLogSettings() {
|
|
this.loading = true;
|
|
this.error = null;
|
|
|
|
try {
|
|
const data = await apiClient.get('/admin/logs/settings');
|
|
this.logSettings = data;
|
|
settingsLog.info('Log settings loaded:', this.logSettings);
|
|
} catch (error) {
|
|
settingsLog.error('Failed to load log settings:', error);
|
|
this.error = error.response?.data?.detail || 'Failed to load log settings';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
async saveLogSettings() {
|
|
this.saving = true;
|
|
this.error = null;
|
|
this.successMessage = null;
|
|
|
|
try {
|
|
const data = await apiClient.put('/admin/logs/settings', this.logSettings);
|
|
this.successMessage = data.message || 'Log settings saved successfully';
|
|
|
|
// Auto-hide success message after 5 seconds
|
|
setTimeout(() => {
|
|
this.successMessage = null;
|
|
}, 5000);
|
|
|
|
settingsLog.info('Log settings saved successfully');
|
|
} catch (error) {
|
|
settingsLog.error('Failed to save log settings:', error);
|
|
this.error = error.response?.data?.detail || 'Failed to save log settings';
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
|
|
async cleanupOldLogs() {
|
|
if (!confirm(`This will delete all logs older than ${this.logSettings.db_log_retention_days} days. Continue?`)) {
|
|
return;
|
|
}
|
|
|
|
this.error = null;
|
|
this.successMessage = null;
|
|
|
|
try {
|
|
const data = await apiClient.delete(
|
|
`/admin/logs/database/cleanup?retention_days=${this.logSettings.db_log_retention_days}&confirm=true`
|
|
);
|
|
this.successMessage = data.message || 'Old logs cleaned up successfully';
|
|
|
|
// Auto-hide success message after 5 seconds
|
|
setTimeout(() => {
|
|
this.successMessage = null;
|
|
}, 5000);
|
|
|
|
settingsLog.info('Old logs cleaned up successfully');
|
|
} catch (error) {
|
|
settingsLog.error('Failed to cleanup logs:', error);
|
|
this.error = error.response?.data?.detail || 'Failed to cleanup old logs';
|
|
}
|
|
},
|
|
|
|
async saveNotificationSettings() {
|
|
this.saving = true;
|
|
this.error = null;
|
|
this.successMessage = null;
|
|
|
|
try {
|
|
// TODO: Implement API endpoint for notification settings
|
|
// const data = await apiClient.put('/admin/notifications/settings', this.notificationSettings);
|
|
|
|
// For now, just show success (settings are client-side only)
|
|
this.successMessage = 'Notification settings saved successfully';
|
|
|
|
// Auto-hide success message after 5 seconds
|
|
setTimeout(() => {
|
|
this.successMessage = null;
|
|
}, 5000);
|
|
|
|
settingsLog.info('Notification settings saved:', this.notificationSettings);
|
|
} catch (error) {
|
|
settingsLog.error('Failed to save notification settings:', error);
|
|
this.error = error.response?.data?.detail || 'Failed to save notification settings';
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
|
|
async loadShippingSettings() {
|
|
try {
|
|
// Load each carrier setting
|
|
const carriers = ['greco', 'colissimo', 'xpresslogistics'];
|
|
for (const carrier of carriers) {
|
|
try {
|
|
const key = `carrier_${carrier}_label_url`;
|
|
const data = await apiClient.get(`/admin/settings/${key}`);
|
|
if (data && data.value) {
|
|
this.shippingSettings[key] = data.value;
|
|
}
|
|
} catch (error) {
|
|
// Setting doesn't exist yet, use default
|
|
settingsLog.debug(`Setting carrier_${carrier}_label_url not found, using default`);
|
|
}
|
|
}
|
|
settingsLog.info('Shipping settings loaded:', this.shippingSettings);
|
|
} catch (error) {
|
|
settingsLog.error('Failed to load shipping settings:', error);
|
|
// Don't show error for missing settings, just use defaults
|
|
}
|
|
},
|
|
|
|
async saveShippingSettings() {
|
|
this.saving = true;
|
|
this.error = null;
|
|
this.successMessage = null;
|
|
|
|
try {
|
|
// Save each carrier setting using upsert
|
|
const carriers = [
|
|
{ key: 'carrier_greco_label_url', name: 'Greco' },
|
|
{ key: 'carrier_colissimo_label_url', name: 'Colissimo' },
|
|
{ key: 'carrier_xpresslogistics_label_url', name: 'XpressLogistics' }
|
|
];
|
|
|
|
for (const carrier of carriers) {
|
|
await apiClient.post('/admin/settings/upsert', {
|
|
key: carrier.key,
|
|
value: this.shippingSettings[carrier.key] || '',
|
|
category: 'shipping',
|
|
value_type: 'string',
|
|
description: `Label URL prefix for ${carrier.name} carrier`
|
|
});
|
|
}
|
|
|
|
this.successMessage = 'Shipping settings saved successfully';
|
|
|
|
// Auto-hide success message after 5 seconds
|
|
setTimeout(() => {
|
|
this.successMessage = null;
|
|
}, 5000);
|
|
|
|
settingsLog.info('Shipping settings saved:', this.shippingSettings);
|
|
} catch (error) {
|
|
settingsLog.error('Failed to save shipping settings:', error);
|
|
this.error = error.response?.data?.detail || 'Failed to save shipping settings';
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
|
|
getShippingLabelUrl(carrier, shipmentNumber) {
|
|
// Helper to generate full label URL
|
|
const prefix = this.shippingSettings[`carrier_${carrier}_label_url`] || '';
|
|
if (!prefix || !shipmentNumber) return null;
|
|
return prefix + shipmentNumber;
|
|
},
|
|
|
|
// =====================================================================
|
|
// EMAIL SETTINGS
|
|
// =====================================================================
|
|
|
|
async loadEmailSettings() {
|
|
try {
|
|
const data = await apiClient.get('/admin/settings/email/status');
|
|
this.emailSettings = {
|
|
provider: data.provider || 'smtp',
|
|
from_email: data.from_email || '',
|
|
from_name: data.from_name || '',
|
|
reply_to: data.reply_to || '',
|
|
smtp_host: data.smtp_host || '',
|
|
smtp_port: data.smtp_port || 587,
|
|
smtp_user: data.smtp_user || '',
|
|
mailgun_domain: data.mailgun_domain || '',
|
|
aws_region: data.aws_region || '',
|
|
debug: data.debug || false,
|
|
enabled: data.enabled !== false,
|
|
is_configured: data.is_configured || false,
|
|
has_db_overrides: data.has_db_overrides || false
|
|
};
|
|
// Populate edit form with current values
|
|
this.populateEmailForm();
|
|
settingsLog.info('Email settings loaded:', this.emailSettings);
|
|
} catch (error) {
|
|
settingsLog.error('Failed to load email settings:', error);
|
|
// Use defaults on error
|
|
}
|
|
},
|
|
|
|
populateEmailForm() {
|
|
// Copy current settings to form (passwords are not loaded from API)
|
|
this.emailForm = {
|
|
provider: this.emailSettings.provider,
|
|
from_email: this.emailSettings.from_email,
|
|
from_name: this.emailSettings.from_name,
|
|
reply_to: this.emailSettings.reply_to || '',
|
|
smtp_host: this.emailSettings.smtp_host || '',
|
|
smtp_port: this.emailSettings.smtp_port || 587,
|
|
smtp_user: this.emailSettings.smtp_user || '',
|
|
smtp_password: '', // Never populated from API
|
|
smtp_use_tls: true,
|
|
smtp_use_ssl: false,
|
|
sendgrid_api_key: '',
|
|
mailgun_api_key: '',
|
|
mailgun_domain: this.emailSettings.mailgun_domain || '',
|
|
aws_access_key_id: '',
|
|
aws_secret_access_key: '',
|
|
aws_region: this.emailSettings.aws_region || 'eu-west-1',
|
|
enabled: this.emailSettings.enabled,
|
|
debug: this.emailSettings.debug
|
|
};
|
|
},
|
|
|
|
enableEmailEditing() {
|
|
this.emailEditMode = true;
|
|
this.populateEmailForm();
|
|
},
|
|
|
|
cancelEmailEditing() {
|
|
this.emailEditMode = false;
|
|
this.populateEmailForm();
|
|
},
|
|
|
|
async saveEmailSettings() {
|
|
this.saving = true;
|
|
this.error = null;
|
|
this.successMessage = null;
|
|
|
|
try {
|
|
// Only send non-empty values to update
|
|
const payload = {};
|
|
|
|
// Always send these core fields
|
|
if (this.emailForm.provider) payload.provider = this.emailForm.provider;
|
|
if (this.emailForm.from_email) payload.from_email = this.emailForm.from_email;
|
|
if (this.emailForm.from_name) payload.from_name = this.emailForm.from_name;
|
|
if (this.emailForm.reply_to) payload.reply_to = this.emailForm.reply_to;
|
|
payload.enabled = this.emailForm.enabled;
|
|
payload.debug = this.emailForm.debug;
|
|
|
|
// Provider-specific fields
|
|
if (this.emailForm.provider === 'smtp') {
|
|
if (this.emailForm.smtp_host) payload.smtp_host = this.emailForm.smtp_host;
|
|
if (this.emailForm.smtp_port) payload.smtp_port = this.emailForm.smtp_port;
|
|
if (this.emailForm.smtp_user) payload.smtp_user = this.emailForm.smtp_user;
|
|
if (this.emailForm.smtp_password) payload.smtp_password = this.emailForm.smtp_password;
|
|
payload.smtp_use_tls = this.emailForm.smtp_use_tls;
|
|
payload.smtp_use_ssl = this.emailForm.smtp_use_ssl;
|
|
} else if (this.emailForm.provider === 'sendgrid') {
|
|
if (this.emailForm.sendgrid_api_key) payload.sendgrid_api_key = this.emailForm.sendgrid_api_key;
|
|
} else if (this.emailForm.provider === 'mailgun') {
|
|
if (this.emailForm.mailgun_api_key) payload.mailgun_api_key = this.emailForm.mailgun_api_key;
|
|
if (this.emailForm.mailgun_domain) payload.mailgun_domain = this.emailForm.mailgun_domain;
|
|
} else if (this.emailForm.provider === 'ses') {
|
|
if (this.emailForm.aws_access_key_id) payload.aws_access_key_id = this.emailForm.aws_access_key_id;
|
|
if (this.emailForm.aws_secret_access_key) payload.aws_secret_access_key = this.emailForm.aws_secret_access_key;
|
|
if (this.emailForm.aws_region) payload.aws_region = this.emailForm.aws_region;
|
|
}
|
|
|
|
const data = await apiClient.put('/admin/settings/email/settings', payload);
|
|
|
|
this.successMessage = data.message || 'Email settings saved successfully';
|
|
this.emailEditMode = false;
|
|
|
|
// Reload to get updated status
|
|
await this.loadEmailSettings();
|
|
|
|
setTimeout(() => {
|
|
this.successMessage = null;
|
|
}, 5000);
|
|
|
|
settingsLog.info('Email settings saved successfully');
|
|
} catch (error) {
|
|
settingsLog.error('Failed to save email settings:', error);
|
|
this.error = error.message || 'Failed to save email settings';
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
|
|
async resetEmailSettings() {
|
|
if (!confirm('This will reset all email settings to use .env defaults. Continue?')) {
|
|
return;
|
|
}
|
|
|
|
this.saving = true;
|
|
this.error = null;
|
|
this.successMessage = null;
|
|
|
|
try {
|
|
const data = await apiClient.delete('/admin/settings/email/settings');
|
|
|
|
this.successMessage = data.message || 'Email settings reset to defaults';
|
|
this.emailEditMode = false;
|
|
|
|
// Reload to get .env values
|
|
await this.loadEmailSettings();
|
|
|
|
setTimeout(() => {
|
|
this.successMessage = null;
|
|
}, 5000);
|
|
|
|
settingsLog.info('Email settings reset successfully');
|
|
} catch (error) {
|
|
settingsLog.error('Failed to reset email settings:', error);
|
|
this.error = error.message || 'Failed to reset email settings';
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
|
|
async sendTestEmail() {
|
|
if (!this.testEmailAddress) {
|
|
this.error = 'Please enter a test email address';
|
|
return;
|
|
}
|
|
|
|
this.sendingTestEmail = true;
|
|
this.error = null;
|
|
this.successMessage = null;
|
|
|
|
try {
|
|
const data = await apiClient.post('/admin/settings/email/test', {
|
|
to_email: this.testEmailAddress
|
|
});
|
|
|
|
if (data.success) {
|
|
this.successMessage = `Test email sent to ${this.testEmailAddress}`;
|
|
setTimeout(() => {
|
|
this.successMessage = null;
|
|
}, 5000);
|
|
} else {
|
|
this.error = data.message || 'Failed to send test email';
|
|
}
|
|
} catch (error) {
|
|
settingsLog.error('Failed to send test email:', error);
|
|
this.error = error.message || 'Failed to send test email';
|
|
} finally {
|
|
this.sendingTestEmail = false;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
settingsLog.info('Settings module loaded');
|