accounts/api/admin_users.go

330 lines
8.9 KiB
Go

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()
}