package server import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/hex" "net/http" "strings" "time" "github.com/gin-gonic/gin" "warpbox/lib/userstore" ) const userSessionCookie = "warpbox_user_session" type requestActor struct { User userstore.User FromAPIKey bool KeyID string } func requestBearerToken(ctx *gin.Context) string { auth := strings.TrimSpace(ctx.GetHeader("Authorization")) if !strings.HasPrefix(strings.ToLower(auth), "bearer ") { return "" } return strings.TrimSpace(auth[7:]) } func (app *App) sessionSecret() string { return app.config.AdminUsername + "|" + app.config.AdminPassword + "|warpbox" } func (app *App) signSessionToken(userID string, expiresAt time.Time) string { payload := userID + "|" + expiresAt.UTC().Format(time.RFC3339) mac := hmac.New(sha256.New, []byte(app.sessionSecret())) mac.Write([]byte(payload)) sig := hex.EncodeToString(mac.Sum(nil)) return base64.RawURLEncoding.EncodeToString([]byte(payload)) + "." + sig } func (app *App) parseSessionToken(token string) (string, bool) { parts := strings.Split(token, ".") if len(parts) != 2 { return "", false } payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[0]) if err != nil { return "", false } payload := string(payloadBytes) mac := hmac.New(sha256.New, []byte(app.sessionSecret())) mac.Write([]byte(payload)) expectedSig := hex.EncodeToString(mac.Sum(nil)) if !hmac.Equal([]byte(expectedSig), []byte(parts[1])) { return "", false } items := strings.Split(payload, "|") if len(items) != 2 { return "", false } expiresAt, err := time.Parse(time.RFC3339, items[1]) if err != nil || time.Now().UTC().After(expiresAt) { return "", false } return items[0], true } func (app *App) resolveActor(ctx *gin.Context) (*requestActor, bool) { if app.userStore == nil { return nil, false } if rawKey := requestBearerToken(ctx); rawKey != "" { user, key, ok := app.userStore.FindByAPIKey(rawKey) if ok { app.userStore.TouchAPIKey(user.ID, key.ID) return &requestActor{User: user, FromAPIKey: true, KeyID: key.ID}, true } return nil, false } if token, err := ctx.Cookie(userSessionCookie); err == nil { if userID, ok := app.parseSessionToken(token); ok { if user, found := app.userStore.FindByID(userID); found { app.userStore.TouchUser(user.ID) return &requestActor{User: user}, true } } } return nil, false } func (app *App) denyActor(ctx *gin.Context, status int, message string) bool { ctx.JSON(status, gin.H{"error": message}) return false } func (app *App) authorizeUpload(ctx *gin.Context) (*requestActor, bool) { actor, ok := app.resolveActor(ctx) if !ok { if requestBearerToken(ctx) != "" { return nil, app.denyActor(ctx, http.StatusUnauthorized, "Invalid API key") } return nil, true } if actor.User.Status != userstore.StatusActive { return nil, app.denyActor(ctx, http.StatusForbidden, "User account is disabled") } if !actor.User.Permissions.CanUseAPI { return nil, app.denyActor(ctx, http.StatusForbidden, "API access is not allowed for this user") } if !actor.User.Permissions.CanCreateBox { return nil, app.denyActor(ctx, http.StatusForbidden, "Creating boxes is not allowed for this user") } if !actor.User.Permissions.CanUploadFile { return nil, app.denyActor(ctx, http.StatusForbidden, "Uploading files is not allowed for this user") } return actor, true } func (app *App) handleUserLogin(ctx *gin.Context) { var payload struct { APIKey string `json:"api_key"` } if err := ctx.ShouldBindJSON(&payload); err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid login payload"}) return } if app.userStore == nil { ctx.JSON(http.StatusServiceUnavailable, gin.H{"error": "User store unavailable"}) return } user, key, ok := app.userStore.FindByAPIKey(payload.APIKey) if !ok { ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid API key"}) return } if user.Status != userstore.StatusActive { ctx.JSON(http.StatusForbidden, gin.H{"error": "User account is disabled"}) return } if !user.Permissions.CanUseWeb { ctx.JSON(http.StatusForbidden, gin.H{"error": "Web access is not allowed for this user"}) return } app.userStore.TouchAPIKey(user.ID, key.ID) expiresAt := time.Now().UTC().Add(time.Duration(app.config.SessionTTLSeconds) * time.Second) ctx.SetCookie(userSessionCookie, app.signSessionToken(user.ID, expiresAt), int(app.config.SessionTTLSeconds), "/", "", false, true) ctx.JSON(http.StatusOK, gin.H{"ok": true, "user": gin.H{"id": user.ID, "email": user.Email, "username": user.Username}}) } func (app *App) handleUserLogout(ctx *gin.Context) { ctx.SetCookie(userSessionCookie, "", -1, "/", "", false, true) ctx.JSON(http.StatusOK, gin.H{"ok": true}) } func (app *App) handleUserMe(ctx *gin.Context) { actor, ok := app.resolveActor(ctx) if !ok || actor == nil { ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"}) return } ctx.JSON(http.StatusOK, gin.H{"user": toAdminUserView(actor.User)}) } func (app *App) handleSelfCreateAPIKey(ctx *gin.Context) { actor, ok := app.resolveActor(ctx) if !ok || actor == nil { ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"}) return } if actor.User.Status != userstore.StatusActive { ctx.JSON(http.StatusForbidden, gin.H{"error": "User account is disabled"}) return } var payload struct { Name string `json:"name"` } _ = ctx.ShouldBindJSON(&payload) key, raw, err := app.userStore.CreateAPIKey(actor.User.ID, 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)}) }