diff --git a/src/core/admin/handlers.go b/src/core/admin/handlers.go index 9562e1a..efcf049 100644 --- a/src/core/admin/handlers.go +++ b/src/core/admin/handlers.go @@ -5,6 +5,8 @@ import ( "log/slog" "net/http" "strings" + + "git.lohmar.co.uk/lexton-it/NextWks/core/email" ) // Handler bundles admin HTTP handlers and their dependencies. @@ -12,15 +14,17 @@ type Handler struct { store *UserStore groupStore *GroupStore syncWriter *SyncWriter + emailer *email.Sender logger *slog.Logger configPath string } -func NewHandler(store *UserStore, groupStore *GroupStore, syncWriter *SyncWriter, logger *slog.Logger, configPath string) *Handler { +func NewHandler(store *UserStore, groupStore *GroupStore, syncWriter *SyncWriter, emailer *email.Sender, logger *slog.Logger, configPath string) *Handler { return &Handler{ store: store, groupStore: groupStore, syncWriter: syncWriter, + emailer: emailer, logger: logger, configPath: configPath, } diff --git a/src/core/admin/ui.go b/src/core/admin/ui.go index 590311f..873c846 100644 --- a/src/core/admin/ui.go +++ b/src/core/admin/ui.go @@ -283,6 +283,21 @@ func (h *Handler) createUsersHandler(w http.ResponseWriter, r *http.Request) { // Sync to Authelia YAML h.syncWriter.Sync() + // Send welcome emails + for _, r := range results { + if r.Error == "" && r.GeneratedPassword != "" { + email := "" + for _, input := range req.Users { + if input.Username == r.Username { + email = input.Email + } + } + if email != "" { + go h.emailer.SendWelcome(email, r.Username, r.GeneratedPassword, "") // workspace URL from config + } + } + } + // Convert to template results resultRows := make([]templates.CreateUserResultRow, 0, len(results)) for _, r := range results { diff --git a/src/core/email/email.go b/src/core/email/email.go new file mode 100644 index 0000000..d48abed --- /dev/null +++ b/src/core/email/email.go @@ -0,0 +1,76 @@ +package email + +import ( + "fmt" + "log/slog" + "net/smtp" + "strings" +) + +// Sender handles SMTP email delivery. +type Sender struct { + host string + port string + username string + password string + from string + logger *slog.Logger +} + +// NewSender creates an email sender from SMTP config. +func NewSender(host string, port int, username, password, from string, logger *slog.Logger) *Sender { + return &Sender{ + host: host, + port: fmt.Sprintf("%d", port), + username: username, + password: password, + from: from, + logger: logger, + } +} + +// SendWelcome sends a welcome email with the generated password. +func (s *Sender) SendWelcome(to, username, password, workspaceURL string) error { + subject := "Welcome to Next Workspace" + body := fmt.Sprintf(`Hello %s, + +Your Next Workspace account has been created. + + Workspace: %s + Username: %s + Password: %s + +Please log in and change your password. + +Best regards, +Next Workspace`, username, workspaceURL, username, password) + + return s.send(to, subject, body) +} + +// SendAlert sends a generic alert email. +func (s *Sender) SendAlert(to, subject, message string) error { + return s.send(to, subject, message) +} + +func (s *Sender) send(to, subject, body string) error { + if s.host == "" || s.username == "" { + s.logger.Warn("email not configured, skipping send", "to", to) + return nil + } + + msg := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n%s", + s.from, to, subject, body) + + addr := fmt.Sprintf("%s:%s", s.host, s.port) + auth := smtp.PlainAuth("", s.username, s.password, s.host) + + err := smtp.SendMail(addr, auth, s.from, strings.Split(to, ","), []byte(msg)) + if err != nil { + s.logger.Error("send email failed", "to", to, "error", err) + return fmt.Errorf("send email: %w", err) + } + + s.logger.Info("email sent", "to", to, "subject", subject) + return nil +} diff --git a/src/main.go b/src/main.go index 076ba9d..ad4c9b3 100644 --- a/src/main.go +++ b/src/main.go @@ -16,6 +16,7 @@ import ( "git.lohmar.co.uk/lexton-it/NextWks/core/auth" "git.lohmar.co.uk/lexton-it/NextWks/core/config" "git.lohmar.co.uk/lexton-it/NextWks/core/db" + "git.lohmar.co.uk/lexton-it/NextWks/core/email" "git.lohmar.co.uk/lexton-it/NextWks/core/ui" "git.lohmar.co.uk/lexton-it/NextWks/core/version" ) @@ -70,8 +71,11 @@ func main() { logger.Info("fixed user roles", "count", fixed) } + // Create email sender + emailSender := email.NewSender(cfg.SMTP.Host, cfg.SMTP.Port, cfg.SMTP.Username, cfg.SMTP.Password, cfg.SMTP.From, logger) + // Create admin handler - adminHandler := admin.NewHandler(userStore, admin.NewGroupStore(database.DB), syncWriter, logger, *configPath) + adminHandler := admin.NewHandler(userStore, admin.NewGroupStore(database.DB), syncWriter, emailSender, logger, *configPath) // Initialize session store and OIDC auth sessionStore := auth.NewSessionStore(database.DB)