From 6667426e24bba82ade4702cb8f85849bebec6077 Mon Sep 17 00:00:00 2001 From: wikiapiserver Date: Thu, 25 Jun 2026 12:22:58 +0200 Subject: feat: initial wiki API server with account management - HTTP API with JSON over configurable port (default 8080) - Endpoints: POST /register, POST /login, POST /refresh, GET /health - MariaDB storage with SHA-256 hashed credentials and tokens - Token rotation on login and refresh - Config loaded from config.json (not tracked in git) - Graceful shutdown on SIGINT/SIGTERM - Connection pool (25 max open, 10 idle, 5min max lifetime) --- main.go | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 main.go (limited to 'main.go') diff --git a/main.go b/main.go new file mode 100644 index 0000000..813ac41 --- /dev/null +++ b/main.go @@ -0,0 +1,119 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "path/filepath" + "syscall" + "time" + + "wikiapiserver/api" + "wikiapiserver/db" +) + +// Config is loaded from config.json at startup. +type Config struct { + Database struct { + Host string `json:"host"` + Username string `json:"username"` + Password string `json:"password"` + Name string `json:"name"` + } `json:"database"` + Server struct { + Port int `json:"port"` + } `json:"server"` +} + +func loadConfig() (*Config, error) { + // Try CWD first (works for go run), then executable dir (works for built binary) + for _, dir := range []string{".", ""} { + if dir == "" { + if exe, err := os.Executable(); err == nil { + dir = filepath.Dir(exe) + } else { + continue + } + } + + path := filepath.Join(dir, "config.json") + f, err := os.Open(path) + if err != nil { + continue + } + + var cfg Config + if err := json.NewDecoder(f).Decode(&cfg); err != nil { + f.Close() + return nil, fmt.Errorf("decode %s: %w", path, err) + } + f.Close() + return &cfg, nil + } + + return nil, fmt.Errorf("config.json not found in . or next to executable") +} + +func buildDSN(cfg *Config) string { + return fmt.Sprintf("%s:%s@tcp(%s)/%s", + cfg.Database.Username, + cfg.Database.Password, + cfg.Database.Host, + cfg.Database.Name, + ) +} + +func main() { + cfg, err := loadConfig() + if err != nil { + log.Fatalf("config: %v", err) + } + + database, err := db.Connect(buildDSN(cfg)) + if err != nil { + log.Fatalf("db: %v", err) + } + defer database.Close() + + handler := api.NewHandler(database) + + mux := http.NewServeMux() + mux.HandleFunc("POST /register", handler.Register) + mux.HandleFunc("POST /login", handler.Login) + mux.HandleFunc("POST /refresh", handler.Refresh) + mux.HandleFunc("GET /health", handler.Health) + + addr := fmt.Sprintf(":%d", cfg.Server.Port) + srv := &http.Server{ + Addr: addr, + Handler: mux, + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 120 * time.Second, + } + + // Graceful shutdown on SIGINT / SIGTERM + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + + go func() { + <-ctx.Done() + log.Println("shutting down...") + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := srv.Shutdown(shutdownCtx); err != nil { + log.Printf("shutdown error: %v", err) + } + }() + + log.Printf("listening on %s", addr) + if err := srv.ListenAndServe(); err != http.ErrServerClosed { + log.Fatalf("server: %v", err) + } + + log.Println("stopped") +} -- cgit v1.2.3