mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-10-23 22:40:25 +02:00
Add Email Testing Functionality to Admin Tools
- Introduced a new section in the admin tools for testing email configurations. - Added a form to send test emails, including recipient, subject, and message fields. - Implemented backend logic to handle test email requests and send emails using the configured SMTP server. - Created a toast notification component to display success or failure messages for email tests. - Updated routes to include a new endpoint for handling test email submissions.
This commit is contained in:
1 parent
1a1df435de
commit
d6bd471eb0
4 files changed
+407
-5
No files matched your search
@@ -36,6 +36,9 @@ type AdminToolsData struct {
|
|||||||
LogFiles []LogFile
|
LogFiles []LogFile
|
||||||
LogContent string
|
LogContent string
|
||||||
CurrentLogFile string
|
CurrentLogFile string
|
||||||
|
EmailTestSuccess *bool
|
||||||
|
EmailTestMessage string
|
||||||
|
SmtpServer string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dialog component for confirmation dialogs
|
// Dialog component for confirmation dialogs
|
||||||
@@ -463,6 +466,101 @@ templ AdminTools(ctx context.Context, data AdminToolsData) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Email Testing Tools -->
|
||||||
|
<div class="mt-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="text-lg font-medium text-secondary-900 dark:text-secondary-100">
|
||||||
|
<i class="fas fa-envelope mr-2 text-primary-500"></i>
|
||||||
|
Email Testing
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="text-sm text-secondary-600 dark:text-secondary-400 mb-4">
|
||||||
|
Test your email configuration by sending a test email to verify the server can send emails properly.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form id="test-email-form" hx-post="/admin/test-email" hx-target="#email-test-result" hx-swap="outerHTML" hx-indicator="#email-test-indicator">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="test-email-recipient" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
||||||
|
Recipient Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="test-email-recipient"
|
||||||
|
name="recipient"
|
||||||
|
class="form-input w-full"
|
||||||
|
placeholder="recipient@example.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="test-email-subject" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
||||||
|
Subject (Optional)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="test-email-subject"
|
||||||
|
name="subject"
|
||||||
|
class="form-input w-full"
|
||||||
|
placeholder="Test Email from GoMFT"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="test-email-message" class="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
||||||
|
Message (Optional)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="test-email-message"
|
||||||
|
name="message"
|
||||||
|
rows="3"
|
||||||
|
class="form-textarea w-full"
|
||||||
|
placeholder="This is a test email from GoMFT."
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button type="submit" class="btn-primary flex items-center justify-center" onclick="fadeInAnimation()">
|
||||||
|
<i class="fas fa-paper-plane mr-2"></i>
|
||||||
|
<span>Send Test Email</span>
|
||||||
|
<div id="email-test-indicator" class="htmx-indicator ml-2">
|
||||||
|
<i class="fas fa-circle-notch fa-spin"></i>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Toast container for email test results -->
|
||||||
|
<div id="email-test-result" class="mt-4 hidden">
|
||||||
|
if data.EmailTestSuccess != nil {
|
||||||
|
@EmailTestToast(*data.EmailTestSuccess, data.EmailTestMessage)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email settings reminder -->
|
||||||
|
<div class="mt-6 bg-secondary-50 dark:bg-secondary-800/50 p-4 rounded-lg">
|
||||||
|
<h4 class="text-sm font-medium text-secondary-900 dark:text-secondary-100 flex items-center">
|
||||||
|
<i class="fas fa-info-circle mr-2 text-blue-500"></i>
|
||||||
|
Email Configuration
|
||||||
|
</h4>
|
||||||
|
if data.SmtpServer != "" {
|
||||||
|
<p class="mt-2 text-xs text-secondary-600 dark:text-secondary-400">
|
||||||
|
Current SMTP server: <span class="font-mono">{ data.SmtpServer }</span>
|
||||||
|
</p>
|
||||||
|
} else {
|
||||||
|
<p class="mt-2 text-xs text-secondary-600 dark:text-secondary-400">
|
||||||
|
Email settings are configured in your application configuration file. Make sure SMTP settings are properly configured before testing.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Available Backups -->
|
<!-- Available Backups -->
|
||||||
<div id="backups-container" class="mt-8">
|
<div id="backups-container" class="mt-8">
|
||||||
@BackupsList(data)
|
@BackupsList(data)
|
||||||
@@ -1000,3 +1098,74 @@ var Commit = "unknown"
|
|||||||
func getCommit() string {
|
func getCommit() string {
|
||||||
return Commit
|
return Commit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EmailTestToast is a component for showing email test results
|
||||||
|
templ EmailTestToast(success bool, message string) {
|
||||||
|
<div id="email-test-result" class="mt-4 animate-fade-in">
|
||||||
|
<div class={
|
||||||
|
"rounded-lg p-4 flex items-start",
|
||||||
|
templ.KV("bg-green-50 dark:bg-green-900/20 border-l-4 border-green-400", success),
|
||||||
|
templ.KV("bg-red-50 dark:bg-red-900/20 border-l-4 border-red-400", !success),
|
||||||
|
}>
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
if success {
|
||||||
|
<i class="fas fa-check-circle text-green-400"></i>
|
||||||
|
} else {
|
||||||
|
<i class="fas fa-exclamation-circle text-red-400"></i>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<h3 class={
|
||||||
|
"text-sm font-medium",
|
||||||
|
templ.KV("text-green-800 dark:text-green-300", success),
|
||||||
|
templ.KV("text-red-800 dark:text-red-300", !success),
|
||||||
|
}>
|
||||||
|
if success {
|
||||||
|
Email Sent Successfully
|
||||||
|
} else {
|
||||||
|
Email Sending Failed
|
||||||
|
}
|
||||||
|
</h3>
|
||||||
|
<div class={
|
||||||
|
"mt-1 text-sm",
|
||||||
|
templ.KV("text-green-700 dark:text-green-400", success),
|
||||||
|
templ.KV("text-red-700 dark:text-red-400", !success),
|
||||||
|
}>
|
||||||
|
{ message }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ml-auto pl-3 flex-shrink-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="document.getElementById('email-test-result').classList.add('hidden');"
|
||||||
|
class={
|
||||||
|
"inline-flex rounded-md p-1.5 focus:outline-none focus:ring-2 focus:ring-offset-2",
|
||||||
|
templ.KV("text-green-500 hover:bg-green-100 dark:hover:bg-green-900/30 focus:ring-green-500", success),
|
||||||
|
templ.KV("text-red-500 hover:bg-red-100 dark:hover:bg-red-900/30 focus:ring-red-500", !success),
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a style for the animate-fade-in animation
|
||||||
|
script fadeInAnimation() {
|
||||||
|
// Add CSS animation if it doesn't exist
|
||||||
|
if (!document.getElementById('fade-in-animation')) {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = 'fade-in-animation';
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(-10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fadeIn 0.3s ease-out forwards;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -270,3 +270,182 @@ func (s *Service) sendEmail(toEmail, subject, htmlContent string) error {
|
|||||||
return client.Quit()
|
return client.Quit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendTestEmail sends a test email to verify email configuration
|
||||||
|
func (s *Service) SendTestEmail(toEmail, subject, message string) error {
|
||||||
|
if !s.Config.Email.Enabled {
|
||||||
|
return fmt.Errorf("email service is disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use default subject if not provided
|
||||||
|
if subject == "" {
|
||||||
|
subject = "Test Email from GoMFT"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use default message if not provided
|
||||||
|
if message == "" {
|
||||||
|
message = "This is a test email from GoMFT to verify the email configuration is working correctly."
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create email data for template
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"Subject": subject,
|
||||||
|
"Message": message,
|
||||||
|
"AppName": "GoMFT",
|
||||||
|
"Year": time.Now().Year(),
|
||||||
|
"SMTPServer": s.Config.Email.Host,
|
||||||
|
"SMTPPort": s.Config.Email.Port,
|
||||||
|
"FromEmail": s.Config.Email.FromEmail,
|
||||||
|
"CurrentTime": time.Now().Format(time.RFC1123Z),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate email content
|
||||||
|
htmlContent, err := s.generateTestEmailHTML(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the email
|
||||||
|
return s.sendEmail(toEmail, subject, htmlContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateTestEmailHTML generates the HTML content for test emails
|
||||||
|
func (s *Service) generateTestEmailHTML(data map[string]interface{}) (string, error) {
|
||||||
|
// HTML template for test email
|
||||||
|
tmpl, err := template.New("testEmail").Parse(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{.Subject}}</title>
|
||||||
|
<style>
|
||||||
|
/* Base styles */
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
color: #374151;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
margin: 0 auto 15px;
|
||||||
|
background-color: #2563eb;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.logo-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #111827;
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0 0 15px;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
.info-box {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.info-label {
|
||||||
|
font-weight: bold;
|
||||||
|
width: 140px;
|
||||||
|
}
|
||||||
|
.note {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
padding: 20px 0;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<div class="logo">
|
||||||
|
<div class="logo-icon">G</div>
|
||||||
|
</div>
|
||||||
|
<h1>{{.Subject}}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{.Message}}</p>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">SMTP Server:</div>
|
||||||
|
<div>{{.SMTPServer}}:{{.SMTPPort}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">From:</div>
|
||||||
|
<div>{{.FromEmail}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">Sent:</div>
|
||||||
|
<div>{{.CurrentTime}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="note">
|
||||||
|
<p>This is a test email sent from the GoMFT admin interface. If you've received this email, your email configuration is working correctly.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>© {{.Year}} {{.AppName}}. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result bytes.Buffer
|
||||||
|
if err := tmpl.Execute(&result, data); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.String(), nil
|
||||||
|
}
|
||||||
@@ -63,17 +63,28 @@ func (h *Handlers) HandleAdminTools(c *gin.Context) {
|
|||||||
data.TotalUsers = int(totalUsers)
|
data.TotalUsers = int(totalUsers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get last backup time and backup count
|
// Get backup info (last backup time and count)
|
||||||
data.LastBackupTime, data.BackupCount = h.getBackupInfo()
|
lastBackup, backupCount := h.getBackupInfo()
|
||||||
|
data.LastBackupTime = lastBackup
|
||||||
|
data.BackupCount = backupCount
|
||||||
|
|
||||||
// Get list of backup files
|
// Get list of backup files
|
||||||
data.BackupFiles = h.getBackupFiles()
|
data.BackupFiles = h.getBackupFiles()
|
||||||
|
|
||||||
// Check for maintenance issues
|
// Get maintenance message if any
|
||||||
data.MaintenanceMessage = h.checkMaintenanceIssues()
|
data.MaintenanceMessage = h.checkMaintenanceIssues()
|
||||||
|
|
||||||
// Render the admin tools page
|
// Add SMTP server info if available
|
||||||
components.AdminTools(components.CreateTemplateContext(c), data).Render(c, c.Writer)
|
if h.Email != nil && h.Email.Config != nil && h.Email.Config.Email.Host != "" {
|
||||||
|
smtpServer := h.Email.Config.Email.Host
|
||||||
|
if h.Email.Config.Email.Port != 0 {
|
||||||
|
data.SmtpServer = fmt.Sprintf("%s:%d", smtpServer, h.Email.Config.Email.Port)
|
||||||
|
} else {
|
||||||
|
data.SmtpServer = smtpServer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
components.AdminTools(c.Request.Context(), data).Render(c.Request.Context(), c.Writer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleBackupDatabase handles the backup database request
|
// HandleBackupDatabase handles the backup database request
|
||||||
@@ -1368,3 +1379,43 @@ func (h *Handlers) HandleImportConfigsFromFile(c *gin.Context) {
|
|||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("%d configs imported successfully", imported)})
|
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("%d configs imported successfully", imported)})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleTestEmail handles the POST /admin/test-email route
|
||||||
|
func (h *Handlers) HandleTestEmail(c *gin.Context) {
|
||||||
|
// Parse the form
|
||||||
|
recipient := c.PostForm("recipient")
|
||||||
|
subject := c.PostForm("subject")
|
||||||
|
message := c.PostForm("message")
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if recipient == "" {
|
||||||
|
components.EmailTestToast(false, "Recipient email is required").Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get SMTP server info for display
|
||||||
|
smtpServer := ""
|
||||||
|
if h.Email != nil && h.Email.Config != nil && h.Email.Config.Email.Host != "" {
|
||||||
|
smtpServer = h.Email.Config.Email.Host
|
||||||
|
if h.Email.Config.Email.Port != 0 {
|
||||||
|
smtpServer = fmt.Sprintf("%s:%d", smtpServer, h.Email.Config.Email.Port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the test email
|
||||||
|
if h.Email == nil {
|
||||||
|
components.EmailTestToast(false, "Email service is not configured").Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.Email.SendTestEmail(recipient, subject, message)
|
||||||
|
if err != nil {
|
||||||
|
// Failed to send email
|
||||||
|
components.EmailTestToast(false, fmt.Sprintf("Failed to send email: %v", err)).Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email sent successfully
|
||||||
|
successMsg := fmt.Sprintf("Test email sent successfully to %s", recipient)
|
||||||
|
components.EmailTestToast(true, successMsg).Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
@@ -110,6 +110,9 @@ func (h *Handlers) RegisterRoutes(router *gin.Engine) {
|
|||||||
admin.GET("/logs/refresh", h.HandleRefreshLogs)
|
admin.GET("/logs/refresh", h.HandleRefreshLogs)
|
||||||
admin.GET("/logs/view/:fileName", h.HandleViewLog)
|
admin.GET("/logs/view/:fileName", h.HandleViewLog)
|
||||||
admin.GET("/logs/download/:fileName", h.HandleDownloadLog)
|
admin.GET("/logs/download/:fileName", h.HandleDownloadLog)
|
||||||
|
|
||||||
|
// Email test route
|
||||||
|
admin.POST("/test-email", h.HandleTestEmail)
|
||||||
}
|
}
|
||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
|
|||||||
Reference in new issue
Block a user