package handlers import ( "log" "net/http" "regexp" "strconv" "strings" "git.workaround.org/chaas/imc/backend/internal/auth" "git.workaround.org/chaas/imc/backend/internal/db" "git.workaround.org/chaas/imc/backend/internal/mail" "github.com/gin-gonic/gin" ) type UserHandler struct { db *db.DB } func NewUserHandler(database *db.DB) *UserHandler { return &UserHandler{db: database} } type CreateUserRequest struct { Email string `json:"email" binding:"required"` Password string `json:"password"` Quota int64 `json:"quota"` } type UpdateUserRequest struct { Password string `json:"password,omitempty"` Quota int64 `json:"quota,omitempty"` } type UserWithQuota struct { ID uint32 `json:"id"` Email string `json:"email"` Quota int64 `json:"quota"` UsedQuota *int64 `json:"usedQuota"` } func (h *UserHandler) List(c *gin.Context) { domainName := c.Param("name") if domainName == "" { Error(c, http.StatusBadRequest, "domain name required") return } authCtx := GetAuthContext(c) if authCtx == nil { Error(c, http.StatusUnauthorized, "authentication required") return } ctx := c.Request.Context() canAccess, _ := h.db.CanAccessDomain(ctx, uint32(authCtx.UserID), domainName, authCtx.IsAdmin()) if !canAccess { Error(c, http.StatusForbidden, "access denied") return } domain, err := h.db.GetDomainByName(ctx, domainName) if err != nil { Error(c, http.StatusNotFound, "domain not found") return } users, err := h.db.GetUsersByDomain(ctx, domain.ID) if err != nil { Error(c, http.StatusInternalServerError, "database error") return } result := make([]UserWithQuota, len(users)) for i, user := range users { quota := user.Quota.Int64 result[i] = UserWithQuota{ ID: user.ID, Email: user.Email, Quota: quota, } mailQuota, err := mail.GetQuota(user.Email) if err == nil && mailQuota != nil { result[i].Quota = mailQuota.Limit result[i].UsedQuota = &mailQuota.Used } } Success(c, result) } func (h *UserHandler) Get(c *gin.Context) { domainName := c.Param("name") idStr := c.Param("id") if domainName == "" || idStr == "" { Error(c, http.StatusBadRequest, "domain name and user id required") return } authCtx := GetAuthContext(c) if authCtx == nil { Error(c, http.StatusUnauthorized, "authentication required") return } ctx := c.Request.Context() canAccess, _ := h.db.CanAccessDomain(ctx, uint32(authCtx.UserID), domainName, authCtx.IsAdmin()) if !canAccess { Error(c, http.StatusForbidden, "access denied") return } id, err := strconv.ParseUint(idStr, 10, 64) if err != nil { Error(c, http.StatusBadRequest, "invalid user id") return } user, err := h.db.GetUserByID(ctx, uint32(id)) if err != nil { Error(c, http.StatusNotFound, "user not found") return } Success(c, user) } func (h *UserHandler) Create(c *gin.Context) { domainName := c.Param("name") if domainName == "" { Error(c, http.StatusBadRequest, "domain name required") return } authCtx := GetAuthContext(c) if authCtx == nil { Error(c, http.StatusUnauthorized, "authentication required") return } ctx := c.Request.Context() canAccess, _ := h.db.CanAccessDomain(ctx, uint32(authCtx.UserID), domainName, authCtx.IsAdmin()) if !canAccess { Error(c, http.StatusForbidden, "access denied") return } domain, err := h.db.GetDomainByName(ctx, domainName) if err != nil { Error(c, http.StatusNotFound, "domain not found") return } var req CreateUserRequest if err := c.ShouldBindJSON(&req); err != nil { Error(c, http.StatusBadRequest, "invalid request body") return } if err := validateEmailLocalPart(req.Email); err != nil { Error(c, http.StatusBadRequest, err.Error()) return } _, err = h.db.GetUserByEmail(ctx, req.Email) if err == nil { Error(c, http.StatusConflict, "user already exists") return } password := req.Password if password == "" { password = auth.GenerateRandomPassword(16) } passwordHash := "{BLF-CRYPT}" + password err = h.db.CreateUser(ctx, domain.ID, req.Email, passwordHash, req.Quota) if err != nil { log.Printf("CreateUser error: email=%s, domain=%s, err=%v", req.Email, domainName, err) Error(c, http.StatusInternalServerError, "failed to create user") return } Created(c, map[string]interface{}{"message": "user created", "password": password}) } func (h *UserHandler) Update(c *gin.Context) { domainName := c.Param("name") idStr := c.Param("id") if domainName == "" || idStr == "" { Error(c, http.StatusBadRequest, "domain name and user id required") return } authCtx := GetAuthContext(c) if authCtx == nil { Error(c, http.StatusUnauthorized, "authentication required") return } ctx := c.Request.Context() canAccess, _ := h.db.CanAccessDomain(ctx, uint32(authCtx.UserID), domainName, authCtx.IsAdmin()) if !canAccess { Error(c, http.StatusForbidden, "access denied") return } id, err := strconv.ParseUint(idStr, 10, 64) if err != nil { Error(c, http.StatusBadRequest, "invalid user id") return } user, err := h.db.GetUserByID(ctx, uint32(id)) if err != nil { Error(c, http.StatusNotFound, "user not found") return } var req UpdateUserRequest if err := c.ShouldBindJSON(&req); err != nil { Error(c, http.StatusBadRequest, "invalid request") return } if req.Password != "" { passwordHash := "{BLF-CRYPT}" + req.Password if err := h.db.UpdateUserPassword(ctx, user.ID, passwordHash); err != nil { Error(c, http.StatusInternalServerError, "failed to update password") return } } if req.Quota >= 0 { if err := h.db.UpdateUserQuota(ctx, user.ID, req.Quota); err != nil { Error(c, http.StatusInternalServerError, "failed to update quota") return } } Success(c, map[string]string{"message": "user updated"}) } func (h *UserHandler) Delete(c *gin.Context) { domainName := c.Param("name") idStr := c.Param("id") if domainName == "" || idStr == "" { Error(c, http.StatusBadRequest, "domain name and user id required") return } authCtx := GetAuthContext(c) if authCtx == nil { Error(c, http.StatusUnauthorized, "authentication required") return } ctx := c.Request.Context() canAccess, _ := h.db.CanAccessDomain(ctx, uint32(authCtx.UserID), domainName, authCtx.IsAdmin()) if !canAccess { Error(c, http.StatusForbidden, "access denied") return } id, err := strconv.ParseUint(idStr, 10, 64) if err != nil { Error(c, http.StatusBadRequest, "invalid user id") return } err = h.db.DeleteUser(ctx, uint32(id)) if err != nil { Error(c, http.StatusInternalServerError, "failed to delete user") return } NoContent(c) } var emailLocalPartRegex = regexp.MustCompile("^[a-zA-Z0-9!#$%&'*+\\-=?^_~-]+$") func validateEmailLocalPart(email string) error { parts := strings.Split(email, "@") if len(parts) != 2 { return &ValidationError{Message: "invalid email format"} } localPart := parts[0] if len(localPart) < 1 || len(localPart) > 64 { return &ValidationError{Message: "username must be between 1 and 64 characters"} } if strings.HasPrefix(localPart, ".") || strings.HasSuffix(localPart, ".") { return &ValidationError{Message: "username cannot start or end with a dot"} } if strings.Contains(localPart, "..") { return &ValidationError{Message: "username cannot contain consecutive dots"} } if !emailLocalPartRegex.MatchString(localPart) { return &ValidationError{Message: "username contains invalid characters"} } return nil }