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:
Bernt
2026-08-10 12:52:48 +00:00
parent 8921fd1467
commit 78b57273e2
141 changed files with 29192 additions and 180 deletions
+322
View File
@@ -0,0 +1,322 @@
package email
import (
"crypto/tls"
"fmt"
"net/url"
"strconv"
"strings"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/client"
)
// IMAPClient handles reading emails via IMAP
type IMAPClient struct {
server string
port int
username string
password string
useTLS bool
}
// EmailMessage represents an email in the inbox
type EmailMessage struct {
UID uint32 `json:"uid"`
Subject string `json:"subject"`
From string `json:"from"`
To []string `json:"to"`
Date time.Time `json:"date"`
Body string `json:"body"`
Preview string `json:"preview"`
Read bool `json:"read"`
Attachments int `json:"attachments"`
}
// NewIMAPClient creates a new IMAP client
func NewIMAPClient(server string, port int, username, password string) *IMAPClient {
return &IMAPClient{
server: server,
port: port,
username: username,
password: password,
useTLS: port == 993,
}
}
// Connect establishes connection to IMAP server
func (c *IMAPClient) Connect() (*client.Client, error) {
addr := fmt.Sprintf("%s:%d", c.server, c.port)
var cl *client.Client
var err error
if c.useTLS {
cl, err = client.DialTLS(addr, &tls.Config{InsecureSkipVerify: true})
} else {
cl, err = client.Dial(addr)
}
if err != nil {
return nil, fmt.Errorf("dial imap: %w", err)
}
if err := cl.Login(c.username, c.password); err != nil {
cl.Logout()
return nil, fmt.Errorf("imap login: %w", err)
}
return cl, nil
}
// ListMessages fetches emails from inbox
func (c *IMAPClient) ListMessages(limit int) ([]EmailMessage, error) {
cl, err := c.Connect()
if err != nil {
return nil, err
}
defer cl.Logout()
// Select INBOX
mbox, err := cl.Select("INBOX", false)
if err != nil {
return nil, fmt.Errorf("select inbox: %w", err)
}
if mbox.Messages == 0 {
return []EmailMessage{}, nil
}
// Fetch last N messages
from := uint32(1)
if mbox.Messages > uint32(limit) {
from = mbox.Messages - uint32(limit) + 1
}
seqset := new(imap.SeqSet)
seqset.AddRange(from, mbox.Messages)
messages := make(chan *imap.Message, 10)
done := make(chan error, 1)
go func() {
done <- cl.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope, imap.FetchFlags, imap.FetchRFC822Text}, messages)
}()
var result []EmailMessage
for msg := range messages {
email := EmailMessage{
UID: msg.Uid,
Subject: msg.Envelope.Subject,
Date: msg.Envelope.Date,
Read: !hasFlag(msg.Flags, imap.RecentFlag),
}
if len(msg.Envelope.From) > 0 {
email.From = msg.Envelope.From[0].Address()
}
for _, to := range msg.Envelope.To {
email.To = append(email.To, to.Address())
}
// Extract preview from body
for _, literal := range msg.Body {
if buf := make([]byte, 0); literal != nil {
buf = make([]byte, literal.Len())
n, _ := literal.Read(buf)
if n > 0 {
body := string(buf[:n])
email.Body = body
email.Preview = truncate(stripHTML(body), 200)
break
}
}
}
result = append(result, email)
}
if err := <-done; err != nil {
return nil, fmt.Errorf("fetch messages: %w", err)
}
// Reverse to show newest first
for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
result[i], result[j] = result[j], result[i]
}
return result, nil
}
// GetMessage fetches a single email by UID
func (c *IMAPClient) GetMessage(uid uint32) (*EmailMessage, error) {
cl, err := c.Connect()
if err != nil {
return nil, err
}
defer cl.Logout()
// Select INBOX
if _, err := cl.Select("INBOX", false); err != nil {
return nil, fmt.Errorf("select inbox: %w", err)
}
seqset := new(imap.SeqSet)
seqset.AddNum(uid)
messages := make(chan *imap.Message, 1)
done := make(chan error, 1)
go func() {
done <- cl.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope, imap.FetchFlags, imap.FetchRFC822Text}, messages)
}()
var email *EmailMessage
for msg := range messages {
email = &EmailMessage{
UID: msg.Uid,
Subject: msg.Envelope.Subject,
Date: msg.Envelope.Date,
Read: !hasFlag(msg.Flags, imap.RecentFlag),
}
if len(msg.Envelope.From) > 0 {
email.From = msg.Envelope.From[0].Address()
}
for _, to := range msg.Envelope.To {
email.To = append(email.To, to.Address())
}
for _, literal := range msg.Body {
if literal != nil {
buf := make([]byte, literal.Len())
n, _ := literal.Read(buf)
if n > 0 {
body := string(buf[:n])
email.Body = body
email.Preview = truncate(stripHTML(body), 200)
break
}
}
}
}
if err := <-done; err != nil {
return nil, fmt.Errorf("fetch message: %w", err)
}
return email, nil
}
// MarkAsRead marks an email as read
func (c *IMAPClient) MarkAsRead(uid uint32) error {
cl, err := c.Connect()
if err != nil {
return err
}
defer cl.Logout()
if _, err := cl.Select("INBOX", false); err != nil {
return fmt.Errorf("select inbox: %w", err)
}
seqset := new(imap.SeqSet)
seqset.AddNum(uid)
item := imap.FormatFlagsOp(imap.AddFlags, true)
flags := []interface{}{imap.SeenFlag}
if err := cl.Store(seqset, item, flags, nil); err != nil {
return fmt.Errorf("mark as read: %w", err)
}
return nil
}
// GetUnreadCount returns number of unread messages
func (c *IMAPClient) GetUnreadCount() (int, error) {
cl, err := c.Connect()
if err != nil {
return 0, err
}
defer cl.Logout()
mbox, err := cl.Select("INBOX", false)
if err != nil {
return 0, fmt.Errorf("select inbox: %w", err)
}
return int(mbox.Unseen), nil
}
// Helper functions
func hasFlag(flags []string, flag string) bool {
for _, f := range flags {
if f == flag {
return true
}
}
return false
}
func stripHTML(html string) string {
// Simple HTML stripping
result := html
result = strings.ReplaceAll(result, "<br>", "\n")
result = strings.ReplaceAll(result, "<br/>", "\n")
result = strings.ReplaceAll(result, "<p>", "\n")
result = strings.ReplaceAll(result, "</p>", "")
// Remove tags
for {
start := strings.Index(result, "<")
if start == -1 {
break
}
end := strings.Index(result[start:], ">")
if end == -1 {
break
}
result = result[:start] + result[start+end+1:]
}
// Decode HTML entities
result = strings.ReplaceAll(result, "&nbsp;", " ")
result = strings.ReplaceAll(result, "&lt;", "<")
result = strings.ReplaceAll(result, "&gt;", ">")
result = strings.ReplaceAll(result, "&amp;", "&")
result = strings.ReplaceAll(result, "&quot;", "\"")
return strings.TrimSpace(result)
}
func truncate(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen] + "..."
}
// ParseIMAPURL parses an IMAP URL like imaps://user:pass@server:993
func ParseIMAPURL(imapURL string) (*IMAPClient, error) {
u, err := url.Parse(imapURL)
if err != nil {
return nil, err
}
password, _ := u.User.Password()
port := 993
if u.Port() != "" {
p, err := strconv.Atoi(u.Port())
if err != nil {
return nil, err
}
port = p
}
return NewIMAPClient(u.Hostname(), port, u.User.Username(), password), nil
}