package admin import ( "encoding/json" "net/http" "strings" "gomail/internal/db" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" ) // ── Users ───────────────────────────────────────────────────────────────────── func (h *Handler) users(w http.ResponseWriter, r *http.Request, user *db.User) { switch r.Method { case http.MethodGet: list, err := h.database.ListUsers(scopeTenant(user)) if err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, list) case http.MethodPost: var req struct { Email, Password, DisplayName, Role, DomainID string } if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Email == "" || req.Password == "" { writeErr(w, http.StatusBadRequest, "email and password are required") return } if len(req.Password) < 8 { writeErr(w, http.StatusBadRequest, "password must be at least 8 characters") return } if req.Role == "" { req.Role = string(db.RoleUser) } tenantID := user.TenantID if user.Role != db.RoleGlobalAdmin && req.Role != string(db.RoleUser) { writeErr(w, http.StatusForbidden, "tenant_admin may only create regular users") return } hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 12) if err != nil { writeErr(w, http.StatusInternalServerError, "password hashing failed") return } newUser := &db.User{ ID: uuid.NewString(), TenantID: tenantID, DomainID: req.DomainID, Email: req.Email, DisplayName: req.DisplayName, Role: db.UserRole(req.Role), } if err := h.database.CreateUser(newUser, string(hash)); err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusCreated, newUser) default: w.WriteHeader(http.StatusMethodNotAllowed) } } func (h *Handler) userByID(w http.ResponseWriter, r *http.Request, user *db.User) { parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/admin/users/"), "/") id := parts[0] action := "" if len(parts) > 1 { action = parts[1] } target, err := h.database.GetUser(id) if err != nil { writeErr(w, http.StatusNotFound, "user not found") return } if user.Role != db.RoleGlobalAdmin && target.TenantID != user.TenantID { writeErr(w, http.StatusForbidden, "not your tenant's user") return } if user.Role != db.RoleGlobalAdmin && (target.Role == db.RoleGlobalAdmin || target.Role == db.RoleTenantAdmin) && target.ID != user.ID { writeErr(w, http.StatusForbidden, "cannot modify an admin account") return } switch { case r.Method == http.MethodPost && action == "suspend": if err := h.database.SetUserActive(id, false); err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"message": "suspended"}) case r.Method == http.MethodPost && action == "activate": if err := h.database.SetUserActive(id, true); err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"message": "activated"}) case r.Method == http.MethodPost && action == "reset-password": var req struct{ NewPassword string } if err := json.NewDecoder(r.Body).Decode(&req); err != nil || len(req.NewPassword) < 8 { writeErr(w, http.StatusBadRequest, "new_password must be at least 8 characters") return } hash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), 12) if err != nil { writeErr(w, http.StatusInternalServerError, "hashing failed") return } if err := h.database.SetUserPassword(id, string(hash)); err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"message": "password reset"}) case r.Method == http.MethodDelete && action == "": if err := h.database.DeleteUser(id); err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"message": "deleted"}) default: w.WriteHeader(http.StatusMethodNotAllowed) } } // ── List rules ──────────────────────────────────────────────────────────────── func (h *Handler) listRules(w http.ResponseWriter, r *http.Request, user *db.User) { tenantID := user.TenantID if user.Role == db.RoleGlobalAdmin { if qt := r.URL.Query().Get("tenant_id"); qt != "" { tenantID = qt } } if tenantID == "" { writeErr(w, http.StatusBadRequest, "tenant_id is required") return } switch r.Method { case http.MethodGet: rules, err := h.database.ListListRules(tenantID) if err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, rules) case http.MethodPost: var req struct{ ListType, MatchType, Value, Note string } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeErr(w, http.StatusBadRequest, "invalid request body") return } if req.ListType != "allow" && req.ListType != "block" { writeErr(w, http.StatusBadRequest, "list_type must be 'allow' or 'block'") return } if req.MatchType != "email" && req.MatchType != "domain" { writeErr(w, http.StatusBadRequest, "match_type must be 'email' or 'domain'") return } if req.Value == "" { writeErr(w, http.StatusBadRequest, "value is required") return } rule := &db.ListRule{ ID: uuid.NewString(), TenantID: tenantID, ListType: db.ListRuleAction(req.ListType), MatchType: req.MatchType, Value: req.Value, Note: req.Note, } if err := h.database.CreateListRule(rule); err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusCreated, rule) default: w.WriteHeader(http.StatusMethodNotAllowed) } } func (h *Handler) listRuleByID(w http.ResponseWriter, r *http.Request, user *db.User) { if r.Method != http.MethodDelete { w.WriteHeader(http.StatusMethodNotAllowed) return } id := strings.TrimPrefix(r.URL.Path, "/api/admin/list-rules/") if err := h.database.DeleteListRule(id); err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"message": "deleted"}) } // ── Outbound queue ──────────────────────────────────────────────────────────── func (h *Handler) queue(w http.ResponseWriter, r *http.Request, user *db.User) { if r.Method != http.MethodGet { w.WriteHeader(http.StatusMethodNotAllowed) return } entries, err := h.database.ListAllOutboundQueue() if err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, entries) } func (h *Handler) queueByID(w http.ResponseWriter, r *http.Request, user *db.User) { parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/admin/queue/"), "/") id := parts[0] action := "" if len(parts) > 1 { action = parts[1] } switch { case r.Method == http.MethodPost && action == "retry": if err := h.database.RetryQueueEntryNow(id); err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"message": "scheduled for immediate retry"}) case r.Method == http.MethodDelete && action == "": if err := h.database.DeleteOutboundEntry(id); err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"message": "cancelled"}) default: w.WriteHeader(http.StatusMethodNotAllowed) } } // ── Global quarantine ───────────────────────────────────────────────────────── func (h *Handler) quarantine(w http.ResponseWriter, r *http.Request, user *db.User) { if r.Method != http.MethodGet { w.WriteHeader(http.StatusMethodNotAllowed) return } entries, err := h.database.ListAllQuarantine() if err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, entries) } func (h *Handler) quarantineByID(w http.ResponseWriter, r *http.Request, user *db.User) { if r.Method != http.MethodDelete { w.WriteHeader(http.StatusMethodNotAllowed) return } id := strings.TrimPrefix(r.URL.Path, "/api/admin/quarantine/") if err := h.database.DeleteQuarantineEntry(id); err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"message": "discarded"}) }