security: Add proper authentication, RBAC, and tenant isolation
- 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
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user