package server import ( "net/http" "strconv" "strings" "time" "github.com/gin-gonic/gin" "warpbox/lib/userstore" ) const bytesPerMegabyte = 1024 * 1024 type adminUserView struct { ID string `json:"id"` Username string `json:"username"` Email string `json:"email"` Status string `json:"status"` Permissions userstore.Permissions `json:"permissions"` Limits userstore.Limits `json:"limits"` APIKeys []adminAPIKeyView `json:"api_keys"` APIKeyCount int `json:"api_key_count"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` LastSeenAt string `json:"last_seen_at"` } type adminAPIKeyView struct { ID string `json:"id"` Name string `json:"name"` Prefix string `json:"prefix"` CreatedAt string `json:"created_at"` LastUsedAt string `json:"last_used_at"` RevokedAt string `json:"revoked_at"` } func formatMaybeTime(value *time.Time) string { if value == nil || value.IsZero() { return "" } return value.UTC().Format(time.RFC3339) } func toAdminAPIKeyView(key userstore.APIKey) adminAPIKeyView { return adminAPIKeyView{ ID: key.ID, Name: key.Name, Prefix: key.Prefix, CreatedAt: key.CreatedAt.UTC().Format(time.RFC3339), LastUsedAt: formatMaybeTime(key.LastUsedAt), RevokedAt: formatMaybeTime(key.RevokedAt), } } func toAdminAPIKeyViews(keys []userstore.APIKey) []adminAPIKeyView { views := make([]adminAPIKeyView, 0, len(keys)) for _, key := range keys { views = append(views, toAdminAPIKeyView(key)) } return views } func toAdminUserView(user userstore.User) adminUserView { return adminUserView{ ID: user.ID, Username: user.Username, Email: user.Email, Status: user.Status, Permissions: user.Permissions, Limits: user.Limits, APIKeys: toAdminAPIKeyViews(user.APIKeys), APIKeyCount: len(user.APIKeys), CreatedAt: user.CreatedAt.UTC().Format(time.RFC3339), UpdatedAt: user.UpdatedAt.UTC().Format(time.RFC3339), LastSeenAt: formatMaybeTime(user.LastSeenAt), } } func (app *App) handleAdminUsers(ctx *gin.Context) { if !app.adminLoginEnabled() { ctx.Redirect(http.StatusSeeOther, "/") return } ctx.HTML(http.StatusOK, "admin/users.html", gin.H{ "AdminUsername": app.config.AdminUsername, "AdminEmail": app.config.AdminEmail, "ActivePage": "users", }) } func (app *App) handleAdminUsersList(ctx *gin.Context) { if app.userStore == nil { ctx.JSON(http.StatusServiceUnavailable, gin.H{"error": "User store unavailable"}) return } users := app.userStore.List() items := make([]adminUserView, 0, len(users)) for _, user := range users { items = append(items, toAdminUserView(user)) } ctx.JSON(http.StatusOK, gin.H{"users": items}) } func parseInt64OrZero(value string) int64 { value = strings.TrimSpace(value) if value == "" { return 0 } parsed, err := strconv.ParseInt(value, 10, 64) if err != nil || parsed < 0 { return 0 } return parsed } func parseMegabytesToBytesOrZero(value string) int64 { megabytes := parseInt64OrZero(value) if megabytes <= 0 { return 0 } return megabytes * bytesPerMegabyte } func (app *App) handleAdminUsersSave(ctx *gin.Context) { if app.userStore == nil { ctx.JSON(http.StatusServiceUnavailable, gin.H{"error": "User store unavailable"}) return } var payload struct { ID string `json:"id"` Username string `json:"username"` Email string `json:"email"` Status string `json:"status"` MaxFileMB string `json:"max_file_size_mb"` MaxBoxMB string `json:"max_box_size_mb"` MaxFileSize string `json:"max_file_size_bytes"` MaxBoxSize string `json:"max_box_size_bytes"` Permissions struct { CanUseWeb bool `json:"can_use_web"` CanUseAPI bool `json:"can_use_api"` CanCreateBox bool `json:"can_create_box"` CanUploadFile bool `json:"can_upload_file"` } `json:"permissions"` } if err := ctx.ShouldBindJSON(&payload); err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user payload"}) return } permissions := userstore.Permissions{ CanUseWeb: payload.Permissions.CanUseWeb, CanUseAPI: payload.Permissions.CanUseAPI, CanCreateBox: payload.Permissions.CanCreateBox, CanUploadFile: payload.Permissions.CanUploadFile, } limits := userstore.Limits{ MaxFileSizeBytes: parseMegabytesToBytesOrZero(payload.MaxFileMB), MaxBoxSizeBytes: parseMegabytesToBytesOrZero(payload.MaxBoxMB), } if limits.MaxFileSizeBytes == 0 && strings.TrimSpace(payload.MaxFileSize) != "" { limits.MaxFileSizeBytes = parseInt64OrZero(payload.MaxFileSize) } if limits.MaxBoxSizeBytes == 0 && strings.TrimSpace(payload.MaxBoxSize) != "" { limits.MaxBoxSizeBytes = parseInt64OrZero(payload.MaxBoxSize) } var ( user userstore.User err error ) if strings.TrimSpace(payload.ID) == "" { user, err = app.userStore.Create(payload.Username, payload.Email, permissions, limits, payload.Status) } else { user, err = app.userStore.Update(payload.ID, payload.Username, payload.Email, permissions, limits, payload.Status) } if err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } ctx.JSON(http.StatusOK, gin.H{"ok": true, "user": toAdminUserView(user)}) } func (app *App) handleAdminUsersDelete(ctx *gin.Context) { if app.userStore == nil { ctx.JSON(http.StatusServiceUnavailable, gin.H{"error": "User store unavailable"}) return } var payload struct { ID string `json:"id"` } if err := ctx.ShouldBindJSON(&payload); err != nil || strings.TrimSpace(payload.ID) == "" { ctx.JSON(http.StatusBadRequest, gin.H{"error": "User id is required"}) return } if err := app.userStore.Delete(payload.ID); err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } ctx.JSON(http.StatusOK, gin.H{"ok": true}) } func (app *App) handleAdminUserAPIKeyCreate(ctx *gin.Context) { if app.userStore == nil { ctx.JSON(http.StatusServiceUnavailable, gin.H{"error": "User store unavailable"}) return } var payload struct { UserID string `json:"user_id"` Name string `json:"name"` } if err := ctx.ShouldBindJSON(&payload); err != nil || strings.TrimSpace(payload.UserID) == "" { ctx.JSON(http.StatusBadRequest, gin.H{"error": "User id is required"}) return } key, raw, err := app.userStore.CreateAPIKey(payload.UserID, payload.Name) if err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } ctx.JSON(http.StatusOK, gin.H{"ok": true, "api_key": raw, "key": toAdminAPIKeyView(key)}) } func (app *App) handleAdminUserAPIKeyRevoke(ctx *gin.Context) { if app.userStore == nil { ctx.JSON(http.StatusServiceUnavailable, gin.H{"error": "User store unavailable"}) return } var payload struct { UserID string `json:"user_id"` KeyID string `json:"key_id"` } if err := ctx.ShouldBindJSON(&payload); err != nil || strings.TrimSpace(payload.UserID) == "" || strings.TrimSpace(payload.KeyID) == "" { ctx.JSON(http.StatusBadRequest, gin.H{"error": "User id and key id are required"}) return } if err := app.userStore.RevokeAPIKey(payload.UserID, payload.KeyID); err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } ctx.JSON(http.StatusOK, gin.H{"ok": true}) }