osaet/backend/cmd/osaet-admin/main.go
yarnom f0b50d13ea feat: add admin publishing workflow and yar theme
Add Go/Postgres admin APIs, Angular admin UI, manual build flow, asset uploads, markdown import/export, configurable slug generation, and the Yar reading theme. Exclude local docs and generated development artifacts from version control.
2026-06-01 15:48:04 +08:00

116 lines
2.4 KiB
Go

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 <username>")
}
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
}
}