Implement email/password auth with JWT sessions and API key auth for sandbox lifecycle. Users get a default team on signup; sandboxes, snapshots, and API keys are scoped to teams. - Add user, team, users_teams, and team_api_keys tables (goose migrations) - Add JWT middleware (Bearer token) for user management endpoints - Add API key middleware (X-API-Key header, SHA-256 hashed) for sandbox ops - Add signup/login handlers with transactional user+team creation - Add API key CRUD endpoints (create/list/delete) - Replace owner_id with team_id on sandboxes and templates - Update all handlers to use team-scoped queries - Add godotenv for .env file loading - Update OpenAPI spec and test UI with auth flows
94 lines
2.2 KiB
Go
94 lines
2.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"git.omukk.dev/wrenn/sandbox/internal/api"
|
|
"git.omukk.dev/wrenn/sandbox/internal/config"
|
|
"git.omukk.dev/wrenn/sandbox/internal/db"
|
|
"git.omukk.dev/wrenn/sandbox/proto/hostagent/gen/hostagentv1connect"
|
|
)
|
|
|
|
func main() {
|
|
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
|
Level: slog.LevelDebug,
|
|
})))
|
|
|
|
cfg := config.Load()
|
|
|
|
if len(cfg.JWTSecret) < 32 {
|
|
slog.Error("JWT_SECRET must be at least 32 characters")
|
|
os.Exit(1)
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// Database connection pool.
|
|
pool, err := pgxpool.New(ctx, cfg.DatabaseURL)
|
|
if err != nil {
|
|
slog.Error("failed to connect to database", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
defer pool.Close()
|
|
|
|
if err := pool.Ping(ctx); err != nil {
|
|
slog.Error("failed to ping database", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
slog.Info("connected to database")
|
|
|
|
queries := db.New(pool)
|
|
|
|
// Connect RPC client for the host agent.
|
|
agentHTTP := &http.Client{Timeout: 10 * time.Minute}
|
|
agentClient := hostagentv1connect.NewHostAgentServiceClient(
|
|
agentHTTP,
|
|
cfg.HostAgentAddr,
|
|
)
|
|
|
|
// API server.
|
|
srv := api.New(queries, agentClient, pool, []byte(cfg.JWTSecret))
|
|
|
|
// Start reconciler.
|
|
reconciler := api.NewReconciler(queries, agentClient, "default", 30*time.Second)
|
|
reconciler.Start(ctx)
|
|
|
|
httpServer := &http.Server{
|
|
Addr: cfg.ListenAddr,
|
|
Handler: srv.Handler(),
|
|
}
|
|
|
|
// Graceful shutdown on signal.
|
|
sigCh := make(chan os.Signal, 1)
|
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
|
go func() {
|
|
sig := <-sigCh
|
|
slog.Info("received signal, shutting down", "signal", sig)
|
|
cancel()
|
|
|
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer shutdownCancel()
|
|
|
|
if err := httpServer.Shutdown(shutdownCtx); err != nil {
|
|
slog.Error("http server shutdown error", "error", err)
|
|
}
|
|
}()
|
|
|
|
slog.Info("control plane starting", "addr", cfg.ListenAddr, "agent", cfg.HostAgentAddr)
|
|
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
slog.Error("http server error", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
slog.Info("control plane stopped")
|
|
}
|