diff --git a/internal/infrastructure/auth/demo_seed.go b/internal/infrastructure/auth/demo_seed.go index dcade3e..b7670c8 100644 --- a/internal/infrastructure/auth/demo_seed.go +++ b/internal/infrastructure/auth/demo_seed.go @@ -24,6 +24,16 @@ func SeedDemoTenant(userStore *UserStore, tenantStore *TenantStore, socRepo doms // Check if demo user already exists if _, err := userStore.GetByEmail(DemoUserEmail); err == nil { slog.Debug("demo tenant already seeded", "email", DemoUserEmail) + // Force update the plan ID to enforce strict demo limits (1000 events) + if tenantStore.db != nil { + tenantStore.db.Exec("UPDATE tenants SET plan_id = 'demo' WHERE id = $1", DemoTenantID) + } + // Also update in-memory cache + tenantStore.mu.Lock() + if t, ok := tenantStore.tenants[DemoTenantID]; ok { + t.PlanID = "demo" + } + tenantStore.mu.Unlock() return } @@ -50,12 +60,12 @@ func SeedDemoTenant(userStore *UserStore, tenantStore *TenantStore, socRepo doms userStore.persistUser(demoUser) } - // 2. Create demo tenant (starter plan) + // 2. Create demo tenant (demo plan 1000 events max) demoTenant := &Tenant{ ID: DemoTenantID, Name: "SYNTREX Demo", Slug: "demo", - PlanID: "starter", + PlanID: "demo", OwnerUserID: demoUser.ID, Active: true, CreatedAt: time.Now(), diff --git a/internal/infrastructure/auth/handlers.go b/internal/infrastructure/auth/handlers.go index 8e88c4e..f5b6094 100644 --- a/internal/infrastructure/auth/handlers.go +++ b/internal/infrastructure/auth/handlers.go @@ -212,14 +212,14 @@ func HandleMe(store *UserStore) http.HandlerFunc { func HandleListUsers(store *UserStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { claims := GetClaims(r.Context()) - if claims == nil || claims.Role != "admin" { - writeAuthError(w, http.StatusForbidden, "admin role required") + if claims == nil || (claims.Role != "admin" && claims.Role != "superadmin") { + writeAuthError(w, http.StatusForbidden, "admin or superadmin role required") return } // SEC-HIGH1: Block listing when TenantID is empty — prevents // empty-string match showing all users without a tenant. - if claims.TenantID == "" { + if claims.TenantID == "" && claims.Role != "superadmin" { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "users": []*User{}, @@ -232,7 +232,7 @@ func HandleListUsers(store *UserStore) http.HandlerFunc { allUsers := store.ListUsers() var filtered []*User for _, u := range allUsers { - if u.TenantID == claims.TenantID { + if claims.Role == "superadmin" || u.TenantID == claims.TenantID { filtered = append(filtered, u) } } @@ -399,7 +399,7 @@ func HandleCreateAPIKey(store *UserStore) http.HandlerFunc { } } -// HandleListAPIKeys returns API keys for the authenticated user. +// HandleListAPIKeys returns API keys for the authenticated user, or all keys for superadmin. // GET /api/auth/keys func HandleListAPIKeys(store *UserStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -415,7 +415,13 @@ func HandleListAPIKeys(store *UserStore) http.HandlerFunc { return } - keys, err := store.ListAPIKeys(user.ID) + var keys []APIKey + if user.Role == "superadmin" { + keys, err = store.ListAllAPIKeys() + } else { + keys, err = store.ListAPIKeys(user.ID) + } + if err != nil { writeAuthError(w, http.StatusInternalServerError, err.Error()) return diff --git a/internal/infrastructure/auth/tenants.go b/internal/infrastructure/auth/tenants.go index b3c1f9e..040aa82 100644 --- a/internal/infrastructure/auth/tenants.go +++ b/internal/infrastructure/auth/tenants.go @@ -46,6 +46,15 @@ var DefaultPlans = map[string]Plan{ SOCEnabled: false, SLAEnabled: false, SOAREnabled: false, ComplianceEnabled: false, PriceMonthCents: 0, }, + "demo": { + ID: "demo", Name: "Demo Sandbox", + Description: "Общая демо-песочница. Жёсткий лимит.", + MaxUsers: 10, MaxEventsMonth: 1000, MaxIncidents: 100, MaxSensors: 5, + MaxScansMonth: 1000, + RetentionDays: 1, + SOCEnabled: true, SLAEnabled: false, SOAREnabled: false, ComplianceEnabled: false, + PriceMonthCents: 0, + }, "starter": { ID: "starter", Name: "Starter", Description: "AI-мониторинг: до 5 сенсоров, базовая корреляция и алерты", diff --git a/internal/infrastructure/auth/users.go b/internal/infrastructure/auth/users.go index f8d9c99..ca09d76 100644 --- a/internal/infrastructure/auth/users.go +++ b/internal/infrastructure/auth/users.go @@ -489,6 +489,32 @@ func (s *UserStore) ListAPIKeys(userID string) ([]APIKey, error) { return keys, nil } +// ListAllAPIKeys returns all API keys across all users (for superadmin). +func (s *UserStore) ListAllAPIKeys() ([]APIKey, error) { + if s.db == nil { + return nil, nil + } + rows, err := s.db.Query(`SELECT id, user_id, name, role, created_at, last_used FROM api_keys`) + if err != nil { + return nil, err + } + defer rows.Close() + + var keys []APIKey + for rows.Next() { + var ak APIKey + var lastUsed sql.NullTime + if err := rows.Scan(&ak.ID, &ak.UserID, &ak.Name, &ak.Role, &ak.CreatedAt, &lastUsed); err != nil { + continue + } + if lastUsed.Valid { + ak.LastUsed = &lastUsed.Time + } + keys = append(keys, ak) + } + return keys, nil +} + // DeleteAPIKey revokes an API key. func (s *UserStore) DeleteAPIKey(keyID, userID string) error { if s.db == nil {