feat: add email settings with database overrides for admin and vendor

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>
This commit is contained in:
2026-01-05 22:23:47 +01:00
parent ad28a8a9a3
commit 36603178c3
51 changed files with 4959 additions and 1141 deletions

View File

@@ -39,6 +39,45 @@ function adminSettings() {
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
@@ -50,7 +89,8 @@ function adminSettings() {
await Promise.all([
this.loadDisplaySettings(),
this.loadLogSettings(),
this.loadShippingSettings()
this.loadShippingSettings(),
this.loadEmailSettings()
]);
} catch (error) {
settingsLog.error('Init failed:', error);
@@ -64,7 +104,8 @@ function adminSettings() {
await Promise.all([
this.loadDisplaySettings(),
this.loadLogSettings(),
this.loadShippingSettings()
this.loadShippingSettings(),
this.loadEmailSettings()
]);
},
@@ -266,6 +307,190 @@ function adminSettings() {
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;
}
}
};
}