package main import ( "atmoquest/config" "atmoquest/internal/admincrypto" "atmoquest/internal/db" "atmoquest/internal/oauthclient" "atmoquest/internal/oauthstore" "atmoquest/internal/session" "atmoquest/internal/users" "atmoquest/nats" "atmoquest/router" "context" "errors" "fmt" "log/slog" "net" "net/http" "os" "os/signal" "strings" "time" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/httplog/v3" "golang.org/x/sync/errgroup" ) func main() { ctx := context.Background() if err := run(ctx); err != nil { slog.Error("server failure", "error", err) os.Exit(1) } } func run(ctx context.Context) error { ctx, cancel := signal.NotifyContext(ctx, os.Interrupt) defer cancel() logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: config.Global.LogLevel, })) slog.SetDefault(logger) if err := config.Global.Validate(); err != nil { return fmt.Errorf("invalid config: %w", err) } conn, err := db.Open(config.Global.DatabaseURL) if err != nil { return fmt.Errorf("open db: %w", err) } defer conn.Close() if err := db.Migrate(conn); err != nil { return fmt.Errorf("migrate db: %w", err) } slog.Info("db ready", "dsn", config.Global.DatabaseURL) for _, s := range strings.Fields(os.Getenv("BOOTSTRAP_ADMIN_DIDS")) { did, err := syntax.ParseDID(s) if err != nil { slog.Warn("bootstrap admin: invalid DID, skipping", "value", s, "error", err) continue } if err := users.SetAdmin(ctx, conn, did, true); err != nil { if errors.Is(err, users.ErrNotFound) { slog.Warn("bootstrap admin: DID hasn't signed in yet, skipping", "did", did) continue } return fmt.Errorf("bootstrap admin %s: %w", did, err) } slog.Info("bootstrap admin: granted", "did", did) } r := chi.NewMux() r.Use( httplog.RequestLogger(logger, nil), middleware.Recoverer, ) sessionMgr, err := session.New(config.Global) if err != nil { return fmt.Errorf("session manager: %w", err) } oauthApp, oauthClientID, err := oauthclient.Build(config.Global, oauthstore.New(conn)) if err != nil { return fmt.Errorf("build oauth client: %w", err) } slog.Info("oauth client ready", "client_id", oauthClientID, "localhost", config.Global.IsLocalhost()) // Admin signing key — used to sign event badge designs. In dev we let the // loader generate one and write it to data/admin_signing.key; in prod // the operator must set ADMIN_SIGNING_KEY (base64 ed25519 seed). signer, err := admincrypto.Load(config.Global.Environment == config.Dev) if err != nil { return fmt.Errorf("load admin signer: %w", err) } slog.Info("admin signer ready", "key_id", signer.KeyID) ns, err := nats.SetupNATS(ctx) if err != nil { return err } eg, egctx := errgroup.WithContext(ctx) if err := router.SetupRoutes(egctx, r, sessionMgr, oauthApp, ns, conn, signer); err != nil { return fmt.Errorf("error setting up routes: %w", err) } addr := fmt.Sprintf("%s:%s", config.Global.Host, config.Global.Port) srv := &http.Server{ Addr: addr, Handler: r, BaseContext: func(l net.Listener) context.Context { return ctx }, ErrorLog: slog.NewLogLogger( slog.Default().Handler(), slog.LevelError, ), } eg.Go(func() error { slog.Info("server started", "addr", srv.Addr) err := srv.ListenAndServe() if err == nil || err == http.ErrServerClosed { return nil } return fmt.Errorf("server error: %w", err) }) eg.Go(func() error { <-egctx.Done() slog.Debug("shutdown signal received") shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() slog.Debug("shutting down server...") if err := srv.Shutdown(shutdownCtx); err != nil { return fmt.Errorf("server shutdown error: %w", err) } return nil }) return eg.Wait() }