Server Deploy / deploy (push) Successful in 5m22s
Implements spec 7 tasks 2-10 on top of the six-plan payload from task 1. Admin: plans re-keyed on (deployment, tier); new catalogue collection holds every Paddle price ID (one row per priceable component); new entitlements collection holds desired beside granted. admin/internal/catalogue owns both folds — entitlement to licence limits, and entitlement to Paddle line items — so the base allowance is subtracted in exactly one place. licensing.Issue now snapshots the instance's granted entitlement, never desired. Free is enforced per account AND deployment. Staff endpoints for plans, catalogue and entitlements; Free self-hosted can be claimed and renewed on its annual term; the reaper stays cloud-only. Server: enforces the monitor cap, audit-log retention (daily sweep, skips Unlimited and lapsed instances), and gates the OIDC callback. Unset limits are filled from the seed plan at the single decode site so old blobs never read as zero. Frontends: adminsite gains a catalogue price-ID editor, six-plan allowance screen, and a catalogue-driven PlanConfigurator mounted on the staff instance page. web shows monitors, audit retention and support level on the licence page. Docs: CLAUDE.md, spec index and plan 5 preamble updated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
150 lines
4.5 KiB
Go
150 lines
4.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/mrhid6/vantage/server/internal/api"
|
|
"github.com/mrhid6/vantage/server/internal/auth"
|
|
"github.com/mrhid6/vantage/server/internal/db"
|
|
grpcserver "github.com/mrhid6/vantage/server/internal/grpc"
|
|
"github.com/mrhid6/vantage/server/internal/monitorsched"
|
|
"github.com/mrhid6/vantage/server/internal/services"
|
|
)
|
|
|
|
func main() {
|
|
mongoURI := getEnv("MONGO_URI", "mongodb://localhost:27017")
|
|
dbName := getEnv("MONGO_DB", "vantage")
|
|
|
|
if os.Getenv("GRPC_HOST") == "" {
|
|
log.Fatal("GRPC_HOST is required (host:port agents dial for gRPC)")
|
|
}
|
|
|
|
if err := db.Connect(mongoURI, dbName); err != nil {
|
|
log.Fatalf("failed to connect to MongoDB: %v", err)
|
|
}
|
|
log.Println("connected to MongoDB")
|
|
|
|
// Migrations 0001 to 0003 still speak the pre-rename shape (orgs, org_id),
|
|
// so they must run before 0004 renames everything underneath them.
|
|
if err := services.RunMigrations(); err != nil {
|
|
log.Fatalf("migration failed: %v", err)
|
|
}
|
|
// 0002 must precede 0003: 0003 can create a "default" org, which pushes
|
|
// 0002 into its ambiguous multi-org branch.
|
|
if err := services.MigrateSettingsOrg(); err != nil {
|
|
log.Fatalf("settings org migration failed: %v", err)
|
|
}
|
|
if err := services.MigrateMissedOrgScopes(); err != nil {
|
|
log.Fatalf("missed org scope migration failed: %v", err)
|
|
}
|
|
|
|
// 0004 renames orgs to instances. It must run BEFORE the index builders:
|
|
// EnsureAuthIndexes creates instances.slug, which would create an empty
|
|
// instances collection and make 0004 refuse to rename onto it.
|
|
migCtx, migCancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
|
migErr := services.MigrateOrgToInstance(migCtx, db.Database)
|
|
migCancel()
|
|
if migErr != nil {
|
|
log.Fatalf("instance rename migration failed: %v", migErr)
|
|
}
|
|
|
|
assertCtx, assertCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
assertErr := services.AssertNoScopedCollectionMissed(assertCtx, db.Database)
|
|
assertCancel()
|
|
if assertErr != nil {
|
|
log.Fatalf("scoped collection check failed: %v", assertErr)
|
|
}
|
|
|
|
if err := services.EnsureAuthIndexes(); err != nil {
|
|
log.Fatalf("failed to ensure auth indexes: %v", err)
|
|
}
|
|
|
|
if err := services.EnsureSecretIndexes(); err != nil {
|
|
log.Printf("warning: failed to ensure secret indexes: %v", err)
|
|
}
|
|
|
|
if err := services.EnsureSettingsIndexes(); err != nil {
|
|
log.Fatalf("failed to ensure settings indexes: %v", err)
|
|
}
|
|
|
|
if err := services.EnsureWorkflowIndexes(); err != nil {
|
|
log.Printf("warning: failed to ensure workflow indexes: %v", err)
|
|
}
|
|
|
|
if instanceIDs, err := services.ListInstanceIDs(); err != nil {
|
|
log.Printf("warning: failed to list instances for default step seeding: %v", err)
|
|
} else {
|
|
for _, instanceID := range instanceIDs {
|
|
if created, updated, err := services.SeedDefaultSteps(instanceID); err != nil {
|
|
log.Printf("warning: failed to seed default steps for instance %s: %v", instanceID, err)
|
|
} else {
|
|
log.Printf("default steps seeded for instance %s: %d created, %d updated", instanceID, created, updated)
|
|
}
|
|
}
|
|
}
|
|
|
|
services.StartLogSweeper()
|
|
services.StartAuditSweeper()
|
|
|
|
redisAddr := getEnv("REDIS_ADDR", "localhost:6379")
|
|
if err := auth.InitRedis(redisAddr); err != nil {
|
|
log.Fatalf("failed to connect to Redis: %v", err)
|
|
}
|
|
log.Println("connected to Redis")
|
|
|
|
go func() {
|
|
ticker := time.NewTicker(2 * time.Minute)
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
if err := services.MarkOfflineServers(); err != nil {
|
|
log.Printf("mark offline error: %v", err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
if err := grpcserver.StartGRPC(9090); err != nil {
|
|
log.Fatalf("gRPC server error: %v", err)
|
|
}
|
|
}()
|
|
|
|
monitorsched.Start(context.Background())
|
|
|
|
services.StartReaper(context.Background())
|
|
|
|
r := gin.New()
|
|
r.Use(gin.Recovery())
|
|
r.Use(gin.LoggerWithConfig(gin.LoggerConfig{SkipPaths: []string{"/api/console/tunnel"}}))
|
|
r.Use(corsMiddleware())
|
|
api.RegisterRoutes(r)
|
|
|
|
log.Println("REST server listening on :8080")
|
|
if err := r.Run(":8080"); err != nil {
|
|
log.Fatalf("REST server error: %v", err)
|
|
}
|
|
}
|
|
|
|
func corsMiddleware() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
c.Header("Access-Control-Allow-Origin", "*")
|
|
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
|
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
if c.Request.Method == "OPTIONS" {
|
|
c.AbortWithStatus(204)
|
|
return
|
|
}
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
func getEnv(key, fallback string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|