Files
dev-service/pkg/api/handlers/admin.go
itzaname 4cfa2c7fff
All checks were successful
continuous-integration/drone/push Build is passing
Add user search
2026-02-25 23:36:53 -05:00

421 lines
11 KiB
Go

package handlers
import (
"git.itzana.me/StrafesNET/dev-service/pkg/api/dto"
"git.itzana.me/StrafesNET/dev-service/pkg/datastore"
"git.itzana.me/StrafesNET/dev-service/pkg/model"
"git.itzana.me/StrafesNET/dev-service/pkg/ratelimit"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"net/http"
"strconv"
)
// AdminHandler handles HTTP requests for admin operations.
type AdminHandler struct {
*Handler
}
// NewAdminHandler creates a new AdminHandler with the provided options.
func NewAdminHandler(options ...HandlerOption) (*AdminHandler, error) {
baseHandler, err := NewHandler(options...)
if err != nil {
return nil, err
}
return &AdminHandler{Handler: baseHandler}, nil
}
// --- User management ---
// ListUsers returns a paginated list of all users, or searches by username/ID if ?q= is provided
func (h *AdminHandler) ListUsers(ctx *gin.Context) {
if q := ctx.Query("q"); q != "" {
users, err := h.Store.SearchUsers(ctx, q)
if err != nil {
h.RespondWithError(ctx, http.StatusInternalServerError, err.Error())
return
}
h.RespondWithData(ctx, users, nil)
return
}
pagination := &datastore.CursorPagination{
Cursor: ctx.Query("cursor"),
Limit: DefaultPageLimit,
}
users, err := h.Store.GetAllUsers(ctx, pagination)
if err != nil {
h.RespondWithError(ctx, http.StatusInternalServerError, err.Error())
return
}
h.RespondWithData(ctx, users, pagination)
}
// GetUser returns a single user by ID including their rate limit status
func (h *AdminHandler) GetUser(ctx *gin.Context) {
userID, ok := parseUserID(ctx, h.Handler)
if !ok {
return
}
user, err := h.Store.GetUser(ctx, userID)
if err != nil {
h.RespondWithError(ctx, http.StatusNotFound, "User not found")
return
}
status, err := h.Limiter.GetRateLimitStatus(ctx, user.ID, ratelimit.RateLimitConfig{
BurstLimit: user.RateLimit.BurstLimit,
BurstDurationSeconds: user.RateLimit.BurstDuration,
DailyLimit: user.RateLimit.DailyLimit,
MonthlyLimit: user.RateLimit.MonthlyLimit,
})
if err != nil {
log.WithError(err).Warn("failed to get rate limit status for user")
status = &ratelimit.RateLimitStatus{}
}
h.RespondWithData(ctx, &dto.AdminUserResponse{
ID: user.ID,
Username: user.Username,
Active: user.Active,
RateLimit: user.RateLimit,
RateLimitStatus: dto.UserRateLimitStatus{
RemainingBurst: status.RemainingBurst,
RemainingDaily: status.RemainingDaily,
RemainingMonthly: status.RemainingMonthly,
},
Permissions: user.Permissions,
}, nil)
}
// UpdateUser updates a user's active status and rate limit class
func (h *AdminHandler) UpdateUser(ctx *gin.Context) {
userID, ok := parseUserID(ctx, h.Handler)
if !ok {
return
}
var req dto.AdminUpdateUserRequest
if err := ctx.BindJSON(&req); err != nil {
return
}
user, err := h.Store.GetUser(ctx, userID)
if err != nil {
h.RespondWithError(ctx, http.StatusNotFound, "User not found")
return
}
user.Active = req.Active
user.RateLimitID = req.RateLimitID
if err := h.Store.UpdateUser(ctx, user); err != nil {
log.WithError(err).Error("failed to update user")
h.RespondWithError(ctx, http.StatusInternalServerError, "Failed to update user")
return
}
// Return refreshed user
updated, err := h.Store.GetUser(ctx, userID)
if err != nil {
h.RespondWithError(ctx, http.StatusInternalServerError, "Failed to fetch updated user")
return
}
h.RespondWithData(ctx, updated, nil)
}
// AddUserPermission grants a permission to a user
func (h *AdminHandler) AddUserPermission(ctx *gin.Context) {
userID, ok := parseUserID(ctx, h.Handler)
if !ok {
return
}
permID, ok := parsePermissionID(ctx, h.Handler)
if !ok {
return
}
if err := h.Store.AddPermissionToUser(ctx, userID, permID); err != nil {
log.WithError(err).Error("failed to add permission to user")
h.RespondWithError(ctx, http.StatusInternalServerError, "Failed to add permission")
return
}
h.RespondWithData(ctx, gin.H{"message": "Permission added"}, nil)
}
// RemoveUserPermission revokes a permission from a user
func (h *AdminHandler) RemoveUserPermission(ctx *gin.Context) {
userID, ok := parseUserID(ctx, h.Handler)
if !ok {
return
}
permID, ok := parsePermissionID(ctx, h.Handler)
if !ok {
return
}
if err := h.Store.RemovePermissionFromUser(ctx, userID, permID); err != nil {
log.WithError(err).Error("failed to remove permission from user")
h.RespondWithError(ctx, http.StatusInternalServerError, "Failed to remove permission")
return
}
h.RespondWithData(ctx, gin.H{"message": "Permission removed"}, nil)
}
// GetUserApplications returns all applications for a given user
func (h *AdminHandler) GetUserApplications(ctx *gin.Context) {
userID, ok := parseUserID(ctx, h.Handler)
if !ok {
return
}
apps, err := h.Store.GetApplicationsForUser(ctx, userID)
if err != nil {
h.RespondWithError(ctx, http.StatusInternalServerError, err.Error())
return
}
responses := make([]dto.ApplicationResponse, len(apps))
for i, app := range apps {
responses[i] = dto.ApplicationResponse{
ID: app.ID,
Name: app.Name,
Description: app.Description,
Permissions: app.Permissions,
CreatedAt: app.CreatedAt,
UpdatedAt: app.UpdatedAt,
}
}
h.RespondWithData(ctx, responses, nil)
}
// --- Permission management ---
// ListPermissions returns all permissions
func (h *AdminHandler) ListPermissions(ctx *gin.Context) {
perms, err := h.Store.GetAllPermissions(ctx)
if err != nil {
h.RespondWithError(ctx, http.StatusInternalServerError, err.Error())
return
}
h.RespondWithData(ctx, perms, nil)
}
// CreatePermission creates a new permission
func (h *AdminHandler) CreatePermission(ctx *gin.Context) {
var req dto.CreatePermissionRequest
if err := ctx.BindJSON(&req); err != nil {
return
}
perm := &model.Permission{
Service: req.Service,
PermissionName: req.PermissionName,
Title: req.Title,
Description: req.Description,
IsDefault: req.IsDefault,
}
if err := h.Store.CreatePermission(ctx, perm); err != nil {
log.WithError(err).Error("failed to create permission")
h.RespondWithError(ctx, http.StatusInternalServerError, "Failed to create permission")
return
}
h.RespondWithData(ctx, perm, nil)
}
// UpdatePermission updates an existing permission
func (h *AdminHandler) UpdatePermission(ctx *gin.Context) {
permID, ok := parsePermissionID(ctx, h.Handler)
if !ok {
return
}
var req dto.UpdatePermissionRequest
if err := ctx.BindJSON(&req); err != nil {
return
}
perm, err := h.Store.GetPermission(ctx, permID)
if err != nil {
h.RespondWithError(ctx, http.StatusNotFound, "Permission not found")
return
}
perm.Service = req.Service
perm.PermissionName = req.PermissionName
perm.Title = req.Title
perm.Description = req.Description
perm.IsDefault = req.IsDefault
if err := h.Store.UpdatePermission(ctx, perm); err != nil {
log.WithError(err).Error("failed to update permission")
h.RespondWithError(ctx, http.StatusInternalServerError, "Failed to update permission")
return
}
h.RespondWithData(ctx, perm, nil)
}
// DeletePermission deletes a permission by ID
func (h *AdminHandler) DeletePermission(ctx *gin.Context) {
permID, ok := parsePermissionID(ctx, h.Handler)
if !ok {
return
}
if err := h.Store.DeletePermission(ctx, permID); err != nil {
h.RespondWithError(ctx, http.StatusInternalServerError, "Failed to delete permission")
return
}
h.RespondWithData(ctx, gin.H{"message": "Permission deleted"}, nil)
}
// SetPermissionDefault updates the default status of a permission
func (h *AdminHandler) SetPermissionDefault(ctx *gin.Context) {
permID, ok := parsePermissionID(ctx, h.Handler)
if !ok {
return
}
var req dto.SetPermissionDefaultRequest
if err := ctx.BindJSON(&req); err != nil {
return
}
if err := h.Store.SetPermissionAsDefault(ctx, permID, req.IsDefault); err != nil {
h.RespondWithError(ctx, http.StatusInternalServerError, "Failed to update permission default")
return
}
h.RespondWithData(ctx, gin.H{"message": "Permission default updated"}, nil)
}
// --- Rate limit management ---
// ListRateLimits returns all rate limit classes
func (h *AdminHandler) ListRateLimits(ctx *gin.Context) {
limits, err := h.Store.GetAllRateLimits(ctx)
if err != nil {
h.RespondWithError(ctx, http.StatusInternalServerError, err.Error())
return
}
h.RespondWithData(ctx, limits, nil)
}
// CreateRateLimit creates a new rate limit class
func (h *AdminHandler) CreateRateLimit(ctx *gin.Context) {
var req dto.CreateRateLimitRequest
if err := ctx.BindJSON(&req); err != nil {
return
}
rl := &model.RateLimit{
BurstDuration: req.BurstDuration,
BurstLimit: req.BurstLimit,
DailyLimit: req.DailyLimit,
MonthlyLimit: req.MonthlyLimit,
MaxApplication: req.MaxApplications,
}
if err := h.Store.CreateRateLimit(ctx, rl); err != nil {
log.WithError(err).Error("failed to create rate limit")
h.RespondWithError(ctx, http.StatusInternalServerError, "Failed to create rate limit")
return
}
h.RespondWithData(ctx, rl, nil)
}
// UpdateRateLimit updates an existing rate limit class
func (h *AdminHandler) UpdateRateLimit(ctx *gin.Context) {
rlID, ok := parseRateLimitID(ctx, h.Handler)
if !ok {
return
}
var req dto.UpdateRateLimitRequest
if err := ctx.BindJSON(&req); err != nil {
return
}
rl, err := h.Store.GetRateLimit(ctx, rlID)
if err != nil {
h.RespondWithError(ctx, http.StatusNotFound, "Rate limit not found")
return
}
rl.BurstDuration = req.BurstDuration
rl.BurstLimit = req.BurstLimit
rl.DailyLimit = req.DailyLimit
rl.MonthlyLimit = req.MonthlyLimit
rl.MaxApplication = req.MaxApplications
if err := h.Store.UpdateRateLimit(ctx, rl); err != nil {
log.WithError(err).Error("failed to update rate limit")
h.RespondWithError(ctx, http.StatusInternalServerError, "Failed to update rate limit")
return
}
h.RespondWithData(ctx, rl, nil)
}
// DeleteRateLimit deletes a rate limit class by ID
func (h *AdminHandler) DeleteRateLimit(ctx *gin.Context) {
rlID, ok := parseRateLimitID(ctx, h.Handler)
if !ok {
return
}
if err := h.Store.DeleteRateLimit(ctx, rlID); err != nil {
h.RespondWithError(ctx, http.StatusInternalServerError, "Failed to delete rate limit")
return
}
h.RespondWithData(ctx, gin.H{"message": "Rate limit deleted"}, nil)
}
// --- Helpers ---
func parseUserID(ctx *gin.Context, h *Handler) (uint64, bool) {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
if err != nil {
h.RespondWithError(ctx, http.StatusBadRequest, "Invalid user ID")
return 0, false
}
return id, true
}
func parsePermissionID(ctx *gin.Context, h *Handler) (uint32, bool) {
id, err := strconv.ParseUint(ctx.Param("permId"), 10, 32)
if err != nil {
// Try "id" param for permission-specific routes
id, err = strconv.ParseUint(ctx.Param("id"), 10, 32)
if err != nil {
h.RespondWithError(ctx, http.StatusBadRequest, "Invalid permission ID")
return 0, false
}
}
return uint32(id), true
}
func parseRateLimitID(ctx *gin.Context, h *Handler) (uint32, bool) {
id, err := strconv.ParseUint(ctx.Param("id"), 10, 32)
if err != nil {
h.RespondWithError(ctx, http.StatusBadRequest, "Invalid rate limit ID")
return 0, false
}
return uint32(id), true
}