summaryrefslogtreecommitdiff
path: root/main.go
diff options
context:
space:
mode:
authorwikiapiserver2026-06-25 12:22:58 +0200
committerwikiapiserver2026-06-25 12:23:11 +0200
commit6667426e24bba82ade4702cb8f85849bebec6077 (patch)
tree66b3bce3db743d753067b5a5c65479131ae57007 /main.go
downloadwikiapiserver-6667426e24bba82ade4702cb8f85849bebec6077.tar.gz
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)
Diffstat (limited to 'main.go')
-rw-r--r--main.go119
1 files changed, 119 insertions, 0 deletions
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")
+}