diff options
| author | wikiapiserver | 2026-06-25 12:22:58 +0200 |
|---|---|---|
| committer | wikiapiserver | 2026-06-25 12:23:11 +0200 |
| commit | 6667426e24bba82ade4702cb8f85849bebec6077 (patch) | |
| tree | 66b3bce3db743d753067b5a5c65479131ae57007 /api | |
| download | wikiapiserver-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 'api')
| -rw-r--r-- | api/handlers.go | 166 |
1 files changed, 166 insertions, 0 deletions
diff --git a/api/handlers.go b/api/handlers.go new file mode 100644 index 0000000..0d23bdb --- /dev/null +++ b/api/handlers.go @@ -0,0 +1,166 @@ +package api + +import ( + "context" + "encoding/json" + "net/http" + "time" + + "wikiapiserver/db" +) + +const defaultTimeout = 5 * time.Second + +// Handler holds the DB dependency for all HTTP handlers. +type Handler struct { + db *db.DB +} + +// NewHandler creates a Handler backed by the given DB. +func NewHandler(database *db.DB) *Handler { + return &Handler{db: database} +} + +// --- request/response types --- + +type errResp struct { + Error string `json:"error"` +} + +type registerReq struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type loginReq struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type refreshReq struct { + RefreshToken string `json:"refresh_token"` +} + +// --- helper writers --- + +func writeJSON(w http.ResponseWriter, code int, v any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + json.NewEncoder(w).Encode(v) //nolint:errcheck +} + +func badRequest(w http.ResponseWriter, msg string) { + writeJSON(w, http.StatusBadRequest, errResp{Error: msg}) +} + +func unauthorized(w http.ResponseWriter) { + writeJSON(w, http.StatusUnauthorized, errResp{Error: "unauthorized"}) +} + +func serverError(w http.ResponseWriter, msg string) { + writeJSON(w, http.StatusInternalServerError, errResp{Error: msg}) +} + +// --- Register: POST /register --- + +func (h *Handler) Register(w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), defaultTimeout) + defer cancel() + + var req registerReq + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + badRequest(w, "invalid JSON") + return + } + + if req.Username == "" || req.Password == "" { + badRequest(w, "username and password are required") + return + } + + acct, err := h.db.CreateAccount(ctx, req.Username, req.Password) + if err != nil { + if err.Error() == "username already exists" { + badRequest(w, "username already exists") + return + } + serverError(w, "could not create account") + return + } + + writeJSON(w, http.StatusCreated, acct) +} + +// --- Login: POST /login --- + +func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), defaultTimeout) + defer cancel() + + var req loginReq + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + badRequest(w, "invalid JSON") + return + } + + if req.Username == "" || req.Password == "" { + badRequest(w, "username and password are required") + return + } + + acct, err := h.db.Authenticate(ctx, req.Username, req.Password) + if err != nil { + if err.Error() == "invalid credentials" { + unauthorized(w) + return + } + serverError(w, "authentication failed") + return + } + + writeJSON(w, http.StatusOK, acct) +} + +// --- Refresh: POST /refresh --- + +func (h *Handler) Refresh(w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), defaultTimeout) + defer cancel() + + var req refreshReq + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + badRequest(w, "invalid JSON") + return + } + + if req.RefreshToken == "" { + badRequest(w, "refresh_token is required") + return + } + + acct, err := h.db.RefreshByToken(ctx, req.RefreshToken) + if err != nil { + if err.Error() == "invalid refresh token" { + unauthorized(w) + return + } + serverError(w, "could not refresh token") + return + } + + writeJSON(w, http.StatusOK, acct) +} + +// --- Health: GET /health --- + +func (h *Handler) Health(w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), defaultTimeout) + defer cancel() + + if err := h.db.Ping(ctx); err != nil { + writeJSON(w, http.StatusServiceUnavailable, errResp{Error: "database unavailable"}) + return + } + + writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) +} |
