272 lines
8.7 KiB
Go
272 lines
8.7 KiB
Go
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"})
|
||
|
|
}
|