package api import ( "crypto/rand" "encoding/hex" "errors" "net/http" "strings" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" "account/internal/store" ) type createCustomUserRequest struct { Email string `json:"email"` UUID string `json:"uuid"` Groups []string `json:"groups"` } func normalizeGroups(values []string) []string { if len(values) == 0 { return nil } seen := make(map[string]struct{}, len(values)) groups := make([]string, 0, len(values)) for _, value := range values { normalized := strings.TrimSpace(value) if normalized == "" { continue } if _, ok := seen[normalized]; ok { continue } seen[normalized] = struct{}{} groups = append(groups, normalized) } if len(groups) == 0 { return nil } return groups } func generatePasswordHash() (string, error) { b := make([]byte, 24) if _, err := rand.Read(b); err != nil { return "", err } hash, err := bcrypt.GenerateFromPassword([]byte(hex.EncodeToString(b)), bcrypt.DefaultCost) if err != nil { return "", err } return string(hash), nil } func (h *handler) createCustomUser(c *gin.Context) { requestUser, ok := h.requireAdminPermission(c, permissionAdminUsersRoleWrite) if !ok { return } if !h.isRootAccount(requestUser) { respondError(c, http.StatusForbidden, "root_only", "only root account can create custom uuid users") return } var req createCustomUserRequest if err := c.ShouldBindJSON(&req); err != nil { respondError(c, http.StatusBadRequest, "invalid_request", "invalid request payload") return } email := strings.ToLower(strings.TrimSpace(req.Email)) if email == "" || !strings.Contains(email, "@") { respondError(c, http.StatusBadRequest, "invalid_email", "email must be a valid address") return } proxyUUID := strings.TrimSpace(req.UUID) if _, err := uuid.Parse(proxyUUID); err != nil { respondError(c, http.StatusBadRequest, "invalid_uuid", "uuid must be a valid UUID") return } groups := normalizeGroups(req.Groups) if len(groups) == 0 { respondError(c, http.StatusBadRequest, "invalid_groups", "at least one group is required") return } blacklisted, err := h.store.IsBlacklisted(c.Request.Context(), email) if err != nil { respondError(c, http.StatusInternalServerError, "blacklist_check_failed", "failed to verify email status") return } if blacklisted { respondError(c, http.StatusForbidden, "email_blacklisted", "this email address is blocked") return } passwordHash, err := generatePasswordHash() if err != nil { respondError(c, http.StatusInternalServerError, "password_generation_failed", "failed to prepare account credentials") return } user := &store.User{ Name: email, Email: email, PasswordHash: passwordHash, EmailVerified: true, Level: store.LevelUser, Role: store.RoleUser, Groups: groups, Active: true, ProxyUUID: proxyUUID, } if err := h.store.CreateUser(c.Request.Context(), user); err != nil { switch { case errors.Is(err, store.ErrEmailExists): respondError(c, http.StatusConflict, "email_exists", "user with this email already exists") return case errors.Is(err, store.ErrNameExists): respondError(c, http.StatusConflict, "name_exists", "user with this name already exists") return case errors.Is(err, store.ErrInvalidName): respondError(c, http.StatusBadRequest, "invalid_name", "name is invalid") return default: respondError(c, http.StatusInternalServerError, "user_creation_failed", "failed to create user") return } } createdUser, err := h.store.GetUserByID(c.Request.Context(), user.ID) if err != nil { createdUser = user } c.JSON(http.StatusCreated, gin.H{ "message": "user_created", "user": sanitizeUser(createdUser, nil), }) } func (h *handler) pauseUser(c *gin.Context) { if _, ok := h.requireAdminPermission(c, permissionAdminUsersPause); !ok { return } userID := c.Param("userId") user, err := h.store.GetUserByID(c.Request.Context(), userID) if err != nil { respondError(c, http.StatusInternalServerError, "user_lookup_failed", "failed to find user") return } if h.isRootAccount(user) { respondError(c, http.StatusForbidden, "root_protected", "root account cannot be paused") return } user.Active = false if err := h.store.UpdateUser(c.Request.Context(), user); err != nil { respondError(c, http.StatusInternalServerError, "update_failed", "failed to pause user") return } c.JSON(http.StatusOK, gin.H{"message": "user paused"}) } func (h *handler) resumeUser(c *gin.Context) { if _, ok := h.requireAdminPermission(c, permissionAdminUsersResume); !ok { return } userID := c.Param("userId") user, err := h.store.GetUserByID(c.Request.Context(), userID) if err != nil { respondError(c, http.StatusInternalServerError, "user_lookup_failed", "failed to find user") return } if h.isRootAccount(user) { respondError(c, http.StatusForbidden, "root_protected", "root account is always active") return } user.Active = true if err := h.store.UpdateUser(c.Request.Context(), user); err != nil { respondError(c, http.StatusInternalServerError, "update_failed", "failed to resume user") return } c.JSON(http.StatusOK, gin.H{"message": "user resumed"}) } func (h *handler) deleteUser(c *gin.Context) { if _, ok := h.requireAdminPermission(c, permissionAdminUsersDelete); !ok { return } userID := c.Param("userId") user, err := h.store.GetUserByID(c.Request.Context(), userID) if err != nil { respondError(c, http.StatusInternalServerError, "user_lookup_failed", "failed to find user") return } if h.isRootAccount(user) { respondError(c, http.StatusForbidden, "root_protected", "root account cannot be deleted") return } if err := h.store.DeleteUser(c.Request.Context(), userID); err != nil { respondError(c, http.StatusInternalServerError, "delete_failed", "failed to delete user") return } c.JSON(http.StatusOK, gin.H{"message": "user deleted"}) } func (h *handler) renewProxyUUID(c *gin.Context) { if _, ok := h.requireAdminPermission(c, permissionAdminUsersRenewUUID); !ok { return } userID := c.Param("userId") var req struct { ExpiresInDays int `json:"expires_in_days"` ExpiresAt string `json:"expires_at"` } if err := c.ShouldBindJSON(&req); err != nil { respondError(c, http.StatusBadRequest, "invalid_request", "invalid request payload") return } user, err := h.store.GetUserByID(c.Request.Context(), userID) if err != nil { respondError(c, http.StatusInternalServerError, "user_lookup_failed", "failed to find user") return } if h.isRootAccount(user) { respondError(c, http.StatusForbidden, "root_protected", "root account UUID cannot be renewed") return } if req.ExpiresAt != "" { t, err := time.Parse("2006-01-02", req.ExpiresAt) if err != nil { respondError(c, http.StatusBadRequest, "invalid_date", "invalid date format, use YYYY-MM-DD") return } expiration := t.UTC() user.ProxyUUIDExpiresAt = &expiration } else if req.ExpiresInDays > 0 { expiration := time.Now().UTC().AddDate(0, 0, req.ExpiresInDays) user.ProxyUUIDExpiresAt = &expiration } else { user.ProxyUUIDExpiresAt = nil } user.ProxyUUID = generateRandomUUID() if err := h.store.UpdateUser(c.Request.Context(), user); err != nil { respondError(c, http.StatusInternalServerError, "update_failed", "failed to renew proxy UUID") return } c.JSON(http.StatusOK, gin.H{ "message": "proxy UUID renewed", "proxy_uuid": user.ProxyUUID, "expires_at": user.ProxyUUIDExpiresAt, }) } func (h *handler) listBlacklist(c *gin.Context) { if _, ok := h.requireAdminPermission(c, permissionAdminBlacklistRead); !ok { return } list, err := h.store.ListBlacklist(c.Request.Context()) if err != nil { respondError(c, http.StatusInternalServerError, "list_failed", "failed to list blacklist") return } c.JSON(http.StatusOK, gin.H{"blacklist": list}) } func (h *handler) addToBlacklist(c *gin.Context) { if _, ok := h.requireAdminPermission(c, permissionAdminBlacklistWrite); !ok { return } var req struct { Email string `json:"email"` } if err := c.ShouldBindJSON(&req); err != nil { respondError(c, http.StatusBadRequest, "invalid_request", "invalid request payload") return } if err := h.store.AddToBlacklist(c.Request.Context(), req.Email); err != nil { respondError(c, http.StatusInternalServerError, "add_failed", "failed to add to blacklist") return } c.JSON(http.StatusOK, gin.H{"message": "email added to blacklist"}) } func (h *handler) removeFromBlacklist(c *gin.Context) { if _, ok := h.requireAdminPermission(c, permissionAdminBlacklistWrite); !ok { return } email := c.Param("email") if err := h.store.RemoveFromBlacklist(c.Request.Context(), email); err != nil { respondError(c, http.StatusInternalServerError, "remove_failed", "failed to remove from blacklist") return } c.JSON(http.StatusOK, gin.H{"message": "email removed from blacklist"}) } func generateRandomUUID() string { return uuid.New().String() }