package api import ( "context" "encoding/json" "log" "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"` } // --- 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.Register(ctx, req.Username, req.Password) if err != nil { if err.Error() == "username already exists" { badRequest(w, "username already exists") return } log.Printf("register error: %v", err) 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 --- // Accepts username and refresh_token. The refresh_token is used to // verify identity; RefreshTokens handles the age-based logic. type refreshReq struct { Username string `json:"username"` RefreshToken string `json:"refresh_token"` } 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.Username == "" || req.RefreshToken == "" { badRequest(w, "username and refresh_token are required") return } acct, err := h.db.RefreshTokens(ctx, req.Username, req.RefreshToken) if err != nil { if err.Error() == "account not found" || err.Error() == "invalid refresh token" { unauthorized(w) return } log.Printf("refresh error: %v", err) 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"}) }