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

@@ -20,6 +20,7 @@
{% call tabs_nav() %}
{{ tab_button('display', 'Display', icon='view-grid') }}
{{ tab_button('logging', 'Logging', icon='document-text') }}
{{ tab_button('email', 'Email', icon='envelope') }}
{{ tab_button('shipping', 'Shipping', icon='truck') }}
{{ tab_button('system', 'System', icon='cog') }}
{{ tab_button('security', 'Security', icon='shield-check') }}
@@ -218,6 +219,349 @@
</div>
</div>
<!-- Email Settings Tab -->
<div x-show="activeTab === 'email'" x-transition>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
Platform Email Configuration
</h3>
<!-- Edit/Cancel buttons -->
<div class="flex items-center gap-2">
<template x-if="!emailEditMode">
<button
@click="enableEmailEditing()"
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600"
>
<span x-html="$icon('pencil', 'w-4 h-4 inline mr-1')"></span>
Edit Settings
</button>
</template>
<template x-if="emailEditMode">
<button
@click="cancelEmailEditing()"
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600"
>
Cancel
</button>
</template>
</div>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-6">
Configure the platform's email settings for system emails (billing, subscriptions, admin notifications).
Vendor emails use each vendor's own email settings.
</p>
<!-- Current Status -->
<div class="mb-6 p-4 rounded-lg" :class="emailSettings.is_configured ? 'bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800' : 'bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800'">
<div class="flex items-center justify-between">
<div class="flex items-center">
<template x-if="emailSettings.is_configured">
<div class="flex items-center">
<span x-html="$icon('check-circle', 'w-5 h-5 text-green-600 dark:text-green-400 mr-2')"></span>
<span class="text-green-800 dark:text-green-300 font-medium">Email configured</span>
</div>
</template>
<template x-if="!emailSettings.is_configured">
<div class="flex items-center">
<span x-html="$icon('exclamation-triangle', 'w-5 h-5 text-yellow-600 dark:text-yellow-400 mr-2')"></span>
<span class="text-yellow-800 dark:text-yellow-300 font-medium">Email not fully configured</span>
</div>
</template>
</div>
<template x-if="emailSettings.has_db_overrides">
<span class="px-2 py-1 text-xs font-medium bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded">
Database overrides active
</span>
</template>
</div>
</div>
<!-- ===== READ-ONLY VIEW ===== -->
<template x-if="!emailEditMode">
<div>
<!-- Provider Selection (Read-only) -->
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Email Provider
</label>
<div class="grid grid-cols-2 md:grid-cols-5 gap-3">
<template x-for="provider in ['smtp', 'sendgrid', 'mailgun', 'ses', 'debug']" :key="provider">
<div
class="p-3 border-2 rounded-lg text-center"
:class="emailSettings.provider === provider
? 'border-purple-500 bg-purple-50 dark:bg-purple-900/30'
: 'border-gray-200 dark:border-gray-600'"
>
<div class="text-sm font-medium text-gray-700 dark:text-gray-300 capitalize" x-text="provider === 'ses' ? 'Amazon SES' : provider"></div>
<template x-if="emailSettings.provider === provider">
<span x-html="$icon('check-circle', 'w-4 h-4 text-purple-600 dark:text-purple-400 mx-auto mt-1')"></span>
</template>
</div>
</template>
</div>
</div>
<!-- Current Settings (Read-only) -->
<div class="space-y-4">
<h4 class="text-md font-medium text-gray-700 dark:text-gray-300 border-b dark:border-gray-600 pb-2">
Current Configuration
</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">From Email</label>
<div class="px-3 py-2 bg-gray-100 dark:bg-gray-700 rounded-lg text-sm text-gray-700 dark:text-gray-300" x-text="emailSettings.from_email || 'Not configured'"></div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">From Name</label>
<div class="px-3 py-2 bg-gray-100 dark:bg-gray-700 rounded-lg text-sm text-gray-700 dark:text-gray-300" x-text="emailSettings.from_name || 'Not configured'"></div>
</div>
<template x-if="emailSettings.provider === 'smtp'">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">SMTP Host</label>
<div class="px-3 py-2 bg-gray-100 dark:bg-gray-700 rounded-lg text-sm text-gray-700 dark:text-gray-300" x-text="emailSettings.smtp_host || 'Not configured'"></div>
</div>
</template>
<template x-if="emailSettings.provider === 'smtp'">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">SMTP Port</label>
<div class="px-3 py-2 bg-gray-100 dark:bg-gray-700 rounded-lg text-sm text-gray-700 dark:text-gray-300" x-text="emailSettings.smtp_port || 'Not configured'"></div>
</div>
</template>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Debug Mode</label>
<div class="px-3 py-2 bg-gray-100 dark:bg-gray-700 rounded-lg text-sm" :class="emailSettings.debug ? 'text-yellow-600 dark:text-yellow-400' : 'text-gray-700 dark:text-gray-300'" x-text="emailSettings.debug ? 'Enabled (emails logged, not sent)' : 'Disabled'"></div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Email Sending</label>
<div class="px-3 py-2 bg-gray-100 dark:bg-gray-700 rounded-lg text-sm" :class="emailSettings.enabled ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'" x-text="emailSettings.enabled ? 'Enabled' : 'Disabled'"></div>
</div>
</div>
</div>
<!-- Info Box -->
<div class="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<div class="flex items-start">
<span x-html="$icon('information-circle', 'w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 mr-3 flex-shrink-0')"></span>
<div class="text-sm text-blue-800 dark:text-blue-200">
<p class="font-medium mb-1">Configuration Priority</p>
<p>Settings can be configured via environment variables (.env) or overridden in the database using the Edit button above. Database settings take priority over .env values.</p>
</div>
</div>
</div>
<!-- Reset to .env button (only show if DB overrides exist) -->
<template x-if="emailSettings.has_db_overrides">
<div class="mt-4">
<button
@click="resetEmailSettings()"
:disabled="saving"
class="px-4 py-2 text-sm font-medium text-red-600 dark:text-red-400 bg-white dark:bg-gray-700 border border-red-300 dark:border-red-600 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 disabled:opacity-50"
>
<span x-html="$icon('refresh', 'w-4 h-4 inline mr-1')"></span>
Reset to .env Defaults
</button>
</div>
</template>
</div>
</template>
<!-- ===== EDIT MODE ===== -->
<template x-if="emailEditMode">
<div>
<!-- Provider Selection (Editable) -->
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Email Provider
</label>
<select
x-model="emailForm.provider"
class="block w-full md:w-1/2 px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600"
>
<option value="smtp">SMTP</option>
<option value="sendgrid">SendGrid</option>
<option value="mailgun">Mailgun</option>
<option value="ses">Amazon SES</option>
</select>
</div>
<!-- Common Settings -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">From Email *</label>
<input
type="email"
x-model="emailForm.from_email"
placeholder="noreply@yourplatform.com"
class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">From Name *</label>
<input
type="text"
x-model="emailForm.from_name"
placeholder="Your Platform"
class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Reply-To Email</label>
<input
type="email"
x-model="emailForm.reply_to"
placeholder="support@yourplatform.com"
class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600"
/>
</div>
</div>
<!-- SMTP Settings -->
<template x-if="emailForm.provider === 'smtp'">
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg mb-6">
<h4 class="text-md font-medium text-gray-700 dark:text-gray-300 mb-4">SMTP Settings</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">SMTP Host</label>
<input type="text" x-model="emailForm.smtp_host" placeholder="smtp.example.com" class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">SMTP Port</label>
<input type="number" x-model="emailForm.smtp_port" placeholder="587" class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">SMTP Username</label>
<input type="text" x-model="emailForm.smtp_user" placeholder="username" class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">SMTP Password</label>
<input type="password" x-model="emailForm.smtp_password" placeholder="Enter new password" class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600" />
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Leave blank to keep existing</p>
</div>
<div class="flex items-center gap-4">
<label class="flex items-center">
<input type="checkbox" x-model="emailForm.smtp_use_tls" class="rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">Use TLS</span>
</label>
<label class="flex items-center">
<input type="checkbox" x-model="emailForm.smtp_use_ssl" class="rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">Use SSL</span>
</label>
</div>
</div>
</div>
</template>
<!-- SendGrid Settings -->
<template x-if="emailForm.provider === 'sendgrid'">
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg mb-6">
<h4 class="text-md font-medium text-gray-700 dark:text-gray-300 mb-4">SendGrid Settings</h4>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">API Key</label>
<input type="password" x-model="emailForm.sendgrid_api_key" placeholder="Enter API key" class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600" />
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Leave blank to keep existing</p>
</div>
</div>
</template>
<!-- Mailgun Settings -->
<template x-if="emailForm.provider === 'mailgun'">
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg mb-6">
<h4 class="text-md font-medium text-gray-700 dark:text-gray-300 mb-4">Mailgun Settings</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">API Key</label>
<input type="password" x-model="emailForm.mailgun_api_key" placeholder="Enter API key" class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600" />
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Leave blank to keep existing</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Domain</label>
<input type="text" x-model="emailForm.mailgun_domain" placeholder="mg.yourdomain.com" class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600" />
</div>
</div>
</div>
</template>
<!-- SES Settings -->
<template x-if="emailForm.provider === 'ses'">
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg mb-6">
<h4 class="text-md font-medium text-gray-700 dark:text-gray-300 mb-4">Amazon SES Settings</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Access Key ID</label>
<input type="password" x-model="emailForm.aws_access_key_id" placeholder="Enter access key" class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Secret Access Key</label>
<input type="password" x-model="emailForm.aws_secret_access_key" placeholder="Enter secret key" class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Region</label>
<select x-model="emailForm.aws_region" class="block w-full px-3 py-2 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600">
<option value="us-east-1">US East (N. Virginia)</option>
<option value="us-west-2">US West (Oregon)</option>
<option value="eu-west-1">EU (Ireland)</option>
<option value="eu-central-1">EU (Frankfurt)</option>
<option value="ap-southeast-1">Asia Pacific (Singapore)</option>
</select>
</div>
</div>
</div>
</template>
<!-- Behavior Settings -->
<div class="flex items-center gap-6 mb-6">
<label class="flex items-center">
<input type="checkbox" x-model="emailForm.enabled" class="rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">Enable email sending</span>
</label>
<label class="flex items-center">
<input type="checkbox" x-model="emailForm.debug" class="rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">Debug mode (log only, don't send)</span>
</label>
</div>
<!-- Save Button -->
<div class="flex items-center justify-end pt-4 border-t border-gray-200 dark:border-gray-700">
<button
@click="saveEmailSettings()"
:disabled="saving"
class="px-6 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed"
>
<span x-show="!saving">Save Email Settings</span>
<span x-show="saving">Saving...</span>
</button>
</div>
</div>
</template>
<!-- Test Email -->
<div class="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
<h4 class="text-md font-medium text-gray-700 dark:text-gray-300 mb-4">Send Test Email</h4>
<div class="flex items-center gap-3">
<input
type="email"
x-model="testEmailAddress"
placeholder="test@example.com"
class="flex-1 px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600"
/>
<button
@click="sendTestEmail()"
:disabled="!testEmailAddress || sendingTestEmail"
class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span x-show="!sendingTestEmail">Send Test</span>
<span x-show="sendingTestEmail">Sending...</span>
</button>
</div>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
Send a test email to verify the platform email configuration is working.
</p>
</div>
</div>
</div>
<!-- Shipping Settings Tab -->
<div x-show="activeTab === 'shipping'" x-transition>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">