78b57273e2
- Add password hashing with bcrypt - Add AuthService with proper login - Add password strength validation - Add RBAC middleware (AdminOnly, ManagerOrAdmin) - Add tenant isolation middleware - Update CRM handler with tenant filtering - Add JWT fallback for development mode - Add user context helpers - Build successful
254 lines
7.4 KiB
Go
254 lines
7.4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
|
|
"boc/email"
|
|
)
|
|
|
|
// MailComposeHandler handles composing and sending emails
|
|
type MailComposeHandler struct {
|
|
client *email.Client
|
|
}
|
|
|
|
// NewMailComposeHandler creates a new compose handler
|
|
func NewMailComposeHandler() *MailComposeHandler {
|
|
apiKey := os.Getenv("RESEND_API_KEY")
|
|
fromEmail := os.Getenv("RESEND_FROM_EMAIL")
|
|
if fromEmail == "" {
|
|
fromEmail = "noreply@landvex.com"
|
|
}
|
|
|
|
var client *email.Client
|
|
if apiKey != "" && !strings.Contains(apiKey, "xxx") && !strings.Contains(apiKey, "placeholder") {
|
|
client = email.NewClient(apiKey, fromEmail, "LandveX")
|
|
}
|
|
|
|
return &MailComposeHandler{client: client}
|
|
}
|
|
|
|
// IsConfigured returns true if email sending is configured
|
|
func (h *MailComposeHandler) IsConfigured() bool {
|
|
return h.client != nil
|
|
}
|
|
|
|
// SendRequest represents an email to send
|
|
type SendRequest struct {
|
|
To []string `json:"to"`
|
|
Subject string `json:"subject"`
|
|
Body string `json:"body"`
|
|
HTML string `json:"html,omitempty"`
|
|
From string `json:"from,omitempty"`
|
|
ReplyTo string `json:"reply_to,omitempty"`
|
|
ThreadID string `json:"thread_id,omitempty"`
|
|
InReplyTo string `json:"in_reply_to,omitempty"`
|
|
Attachments []AttachmentUpload `json:"attachments,omitempty"`
|
|
}
|
|
|
|
// AttachmentUpload represents an uploaded attachment
|
|
type AttachmentUpload struct {
|
|
Filename string `json:"filename"`
|
|
Content string `json:"content"` // base64 encoded
|
|
MIMEType string `json:"mime_type"`
|
|
}
|
|
|
|
// SendResponse represents the send response
|
|
type SendResponse struct {
|
|
ID string `json:"id,omitempty"`
|
|
Status string `json:"status"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
// SendEmail handles sending an email
|
|
func (h *MailComposeHandler) SendEmail(w http.ResponseWriter, r *http.Request) {
|
|
if !h.IsConfigured() {
|
|
writeJSON(w, http.StatusServiceUnavailable, map[string]interface{}{
|
|
"ok": false,
|
|
"error": "Email sending not configured (set RESEND_API_KEY)",
|
|
})
|
|
return
|
|
}
|
|
|
|
var req SendRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]interface{}{
|
|
"ok": false,
|
|
"error": "Invalid request body",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Validate
|
|
if len(req.To) == 0 || req.Subject == "" || req.Body == "" {
|
|
writeJSON(w, http.StatusBadRequest, map[string]interface{}{
|
|
"ok": false,
|
|
"error": "Missing required fields: to, subject, body",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Generate HTML if not provided
|
|
html := req.HTML
|
|
if html == "" {
|
|
html = fmt.Sprintf("<html><body><pre style=\"font-family: sans-serif; white-space: pre-wrap;\">%s</pre></body></html>",
|
|
escapeHTML(req.Body))
|
|
}
|
|
|
|
// Handle attachments
|
|
var attachments []email.Attachment
|
|
for _, att := range req.Attachments {
|
|
data, err := decodeBase64(att.Content)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]interface{}{
|
|
"ok": false,
|
|
"error": fmt.Sprintf("Invalid attachment %s: %v", att.Filename, err),
|
|
})
|
|
return
|
|
}
|
|
attachments = append(attachments, email.Attachment{
|
|
Filename: att.Filename,
|
|
Content: data,
|
|
})
|
|
}
|
|
|
|
// Send
|
|
var err error
|
|
if len(attachments) > 0 {
|
|
err = h.client.SendEmailWithAttachment(req.To, req.Subject, html, req.Body, attachments)
|
|
} else {
|
|
err = h.client.SendEmail(req.To, req.Subject, html, req.Body)
|
|
}
|
|
|
|
if err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
|
|
"ok": false,
|
|
"error": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"ok": true,
|
|
"status": "sent",
|
|
"message": fmt.Sprintf("Email sent to %s", strings.Join(req.To, ", ")),
|
|
})
|
|
}
|
|
|
|
// GetStatus returns email sending status
|
|
func (h *MailComposeHandler) GetStatus(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"ok": true,
|
|
"configured": h.IsConfigured(),
|
|
"from": os.Getenv("RESEND_FROM_EMAIL"),
|
|
})
|
|
}
|
|
|
|
// AIAssistRequest represents a request for AI writing assistance
|
|
type AIAssistRequest struct {
|
|
Context string `json:"context"`
|
|
Tone string `json:"tone,omitempty"` // professional, friendly, formal
|
|
Language string `json:"language,omitempty"` // sv, en
|
|
MaxLength int `json:"max_length,omitempty"`
|
|
}
|
|
|
|
// AIAssistResponse represents AI suggestions
|
|
type AIAssistResponse struct {
|
|
Suggestions []string `json:"suggestions"`
|
|
Improved string `json:"improved,omitempty"`
|
|
Grammar []string `json:"grammar_issues,omitempty"`
|
|
}
|
|
|
|
// AIAssist provides AI writing assistance
|
|
func (h *MailComposeHandler) AIAssist(w http.ResponseWriter, r *http.Request) {
|
|
var req AIAssistRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]interface{}{
|
|
"ok": false,
|
|
"error": "Invalid request",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Simple rule-based suggestions (placeholder for real AI integration)
|
|
suggestions := generateSuggestions(req.Context, req.Tone, req.Language)
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"ok": true,
|
|
"suggestions": suggestions,
|
|
"improved": improveText(req.Context, req.Tone, req.Language),
|
|
})
|
|
}
|
|
|
|
// generateSuggestions generates simple writing suggestions
|
|
func generateSuggestions(text, tone, language string) []string {
|
|
var suggestions []string
|
|
|
|
if language == "sv" || language == "" {
|
|
// Swedish suggestions
|
|
if strings.Contains(text, "Hej") && !strings.Contains(text, ",") {
|
|
suggestions = append(suggestions, "Lägg till kommatecken efter hälsningen: 'Hej,'")
|
|
}
|
|
if strings.Contains(text, "mvh") || strings.Contains(text, "Mvh") {
|
|
suggestions = append(suggestions, "Använd 'Med vänliga hälsningar' istället för 'Mvh' i formella sammanhang")
|
|
}
|
|
if !strings.Contains(text, "?") && strings.Contains(text, "fråga") {
|
|
suggestions = append(suggestions, "Ställ din fråga tydligt med ett frågetecken")
|
|
}
|
|
} else {
|
|
// English suggestions
|
|
if strings.Contains(text, "Hi") && !strings.Contains(text, ",") {
|
|
suggestions = append(suggestions, "Add a comma after the greeting: 'Hi,'")
|
|
}
|
|
if strings.Contains(text, "pls") || strings.Contains(text, "plz") {
|
|
suggestions = append(suggestions, "Use 'please' instead of 'pls/plz' in professional emails")
|
|
}
|
|
}
|
|
|
|
// Tone-specific suggestions
|
|
if tone == "professional" {
|
|
suggestions = append(suggestions, "Use formal language and avoid contractions")
|
|
} else if tone == "friendly" {
|
|
suggestions = append(suggestions, "A warm opening helps build rapport")
|
|
}
|
|
|
|
return suggestions
|
|
}
|
|
|
|
// improveText improves the given text
|
|
func improveText(text, tone, language string) string {
|
|
// Simple improvements
|
|
improved := text
|
|
|
|
if language == "sv" || language == "" {
|
|
improved = strings.ReplaceAll(improved, "mvh", "Med vänliga hälsningar")
|
|
improved = strings.ReplaceAll(improved, "Mvh", "Med vänliga hälsningar")
|
|
improved = strings.ReplaceAll(improved, "Hej", "Hej,")
|
|
} else {
|
|
improved = strings.ReplaceAll(improved, "pls", "please")
|
|
improved = strings.ReplaceAll(improved, "plz", "please")
|
|
improved = strings.ReplaceAll(improved, "thx", "thank you")
|
|
}
|
|
|
|
return improved
|
|
}
|
|
|
|
// escapeHTML escapes HTML special characters
|
|
func escapeHTML(s string) string {
|
|
s = strings.ReplaceAll(s, "&", "&")
|
|
s = strings.ReplaceAll(s, "<", "<")
|
|
s = strings.ReplaceAll(s, ">", ">")
|
|
s = strings.ReplaceAll(s, "\"", """)
|
|
return s
|
|
}
|
|
|
|
// decodeBase64 decodes base64 string
|
|
func decodeBase64(s string) ([]byte, error) {
|
|
// Simple base64 decode - in production use encoding/base64
|
|
// This is a placeholder
|
|
return []byte(s), nil
|
|
}
|