package main import ( "context" "errors" "fmt" "net/http" "os" "os/signal" "syscall" "time" "github.com/jackc/pgx/v5/pgxpool" "osaet/backend/internal/admin" ) func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, "error:", err) os.Exit(1) } } func run() error { command := "serve" if len(os.Args) > 1 { command = os.Args[1] } cfg := admin.LoadConfig() if err := cfg.Validate(); err != nil { return err } ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() db, err := admin.OpenDatabase(ctx, cfg.DatabaseURL) if err != nil { return err } defer db.Close() switch command { case "serve": return serve(ctx, cfg, db) case "migrate": return admin.RunMigrations(ctx, db, cfg.MigrationsDir) case "create-user": return createUser(ctx, db) case "import-markdown": return importMarkdown(ctx, cfg, db) default: return fmt.Errorf("unknown command %q", command) } } func createUser(ctx context.Context, db *pgxpool.Pool) error { if len(os.Args) < 3 { return errors.New("usage: osaet-admin create-user ") } password := os.Getenv("OSAET_ADMIN_PASSWORD") if password == "" { return errors.New("OSAET_ADMIN_PASSWORD is required") } user, err := admin.NewStore(db).CreateOrUpdateUser(ctx, os.Args[2], password) if err != nil { return err } fmt.Fprintf(os.Stdout, "admin user %q is ready\n", user.Username) return nil } func importMarkdown(ctx context.Context, cfg admin.Config, db *pgxpool.Pool) error { postsDir := cfg.PostsDir if len(os.Args) >= 3 { postsDir = os.Args[2] } result, err := admin.NewStore(db).ImportMarkdownPosts(ctx, postsDir) if err != nil { return err } fmt.Fprintf(os.Stdout, "imported %d markdown post(s), skipped %d file(s)\n", result.Imported, result.Skipped) return nil } func serve(ctx context.Context, cfg admin.Config, db *pgxpool.Pool) error { server := &http.Server{ Addr: cfg.Addr, Handler: admin.NewServerWithContext(ctx, db, cfg).Router(), ReadHeaderTimeout: 5 * time.Second, } errCh := make(chan error, 1) go func() { errCh <- server.ListenAndServe() }() select { case <-ctx.Done(): shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() return server.Shutdown(shutdownCtx) case err := <-errCh: if errors.Is(err, http.ErrServerClosed) { return nil } return err } }