package handlers import ( "encoding/json" "net/http" "github.com/andyleap/hnh-map/internal/app" "github.com/andyleap/hnh-map/internal/app/services" ) type loginRequest struct { User string `json:"user"` Pass string `json:"pass"` } type meResponse struct { Username string `json:"username"` Auths []string `json:"auths"` Tokens []string `json:"tokens,omitempty"` Prefix string `json:"prefix,omitempty"` Email string `json:"email,omitempty"` } type passwordRequest struct { Pass string `json:"pass"` } type meUpdateRequest struct { Email string `json:"email"` } // APILogin handles POST /map/api/login. func (h *Handlers) APILogin(rw http.ResponseWriter, req *http.Request) { if !h.requireMethod(rw, req, http.MethodPost) { return } ctx := req.Context() var body loginRequest if err := json.NewDecoder(req.Body).Decode(&body); err != nil { JSONError(rw, http.StatusBadRequest, "bad request", "BAD_REQUEST") return } if u := h.Auth.GetUserByUsername(ctx, body.User); u != nil && u.Pass == nil { JSONError(rw, http.StatusUnauthorized, "Use OAuth to sign in", "OAUTH_ONLY") return } u := h.Auth.GetUser(ctx, body.User, body.Pass) if u == nil { if boot := h.Auth.BootstrapAdmin(ctx, body.User, body.Pass, services.GetBootstrapPassword()); boot != nil { u = boot } else { JSONError(rw, http.StatusUnauthorized, "Unauthorized", "UNAUTHORIZED") return } } sessionID := h.Auth.CreateSession(ctx, body.User, u.Auths.Has("tempadmin")) if sessionID == "" { JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR") return } http.SetCookie(rw, &http.Cookie{ Name: "session", Value: sessionID, Path: "/", MaxAge: app.SessionMaxAge, HttpOnly: true, Secure: req.TLS != nil, SameSite: http.SameSiteLaxMode, }) JSON(rw, http.StatusOK, meResponse{Username: body.User, Auths: u.Auths}) } // APISetup handles GET /map/api/setup. func (h *Handlers) APISetup(rw http.ResponseWriter, req *http.Request) { if !h.requireMethod(rw, req, http.MethodGet) { return } JSON(rw, http.StatusOK, struct { SetupRequired bool `json:"setupRequired"` }{SetupRequired: h.Auth.SetupRequired(req.Context())}) } // APILogout handles POST /map/api/logout. func (h *Handlers) APILogout(rw http.ResponseWriter, req *http.Request) { if !h.requireMethod(rw, req, http.MethodPost) { return } ctx := req.Context() s := h.Auth.GetSession(ctx, req) if s != nil { h.Auth.DeleteSession(ctx, s) } rw.WriteHeader(http.StatusOK) } // APIMe handles GET /map/api/me. func (h *Handlers) APIMe(rw http.ResponseWriter, req *http.Request) { if !h.requireMethod(rw, req, http.MethodGet) { return } ctx := req.Context() s := h.requireSession(rw, req) if s == nil { return } out := meResponse{Username: s.Username, Auths: s.Auths} out.Tokens, out.Prefix = h.Auth.GetUserTokensAndPrefix(ctx, s.Username) if u := h.Auth.GetUserByUsername(ctx, s.Username); u != nil { out.Email = u.Email } JSON(rw, http.StatusOK, out) } // APIMeUpdate handles PATCH /map/api/me (update current user email). func (h *Handlers) APIMeUpdate(rw http.ResponseWriter, req *http.Request) { if !h.requireMethod(rw, req, http.MethodPatch) { return } s := h.requireSession(rw, req) if s == nil { return } ctx := req.Context() var body meUpdateRequest if err := json.NewDecoder(req.Body).Decode(&body); err != nil { JSONError(rw, http.StatusBadRequest, "bad request", "BAD_REQUEST") return } if err := h.Auth.SetUserEmail(ctx, s.Username, body.Email); err != nil { JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR") return } rw.WriteHeader(http.StatusOK) } // APIMeTokens handles POST /map/api/me/tokens. func (h *Handlers) APIMeTokens(rw http.ResponseWriter, req *http.Request) { if !h.requireMethod(rw, req, http.MethodPost) { return } s := h.requireSession(rw, req) if s == nil { return } ctx := req.Context() if !s.Auths.Has(app.AUTH_UPLOAD) { JSONError(rw, http.StatusForbidden, "Forbidden", "FORBIDDEN") return } tokens := h.Auth.GenerateTokenForUser(ctx, s.Username) if tokens == nil { JSONError(rw, http.StatusInternalServerError, "internal error", "INTERNAL_ERROR") return } JSON(rw, http.StatusOK, map[string][]string{"tokens": tokens}) } // APIMePassword handles POST /map/api/me/password. func (h *Handlers) APIMePassword(rw http.ResponseWriter, req *http.Request) { if !h.requireMethod(rw, req, http.MethodPost) { return } s := h.requireSession(rw, req) if s == nil { return } ctx := req.Context() var body passwordRequest if err := json.NewDecoder(req.Body).Decode(&body); err != nil { JSONError(rw, http.StatusBadRequest, "bad request", "BAD_REQUEST") return } if err := h.Auth.SetUserPassword(ctx, s.Username, body.Pass); err != nil { JSONError(rw, http.StatusBadRequest, "bad request", "BAD_REQUEST") return } rw.WriteHeader(http.StatusOK) } // APIOAuthProviders handles GET /map/api/oauth/providers. func (h *Handlers) APIOAuthProviders(rw http.ResponseWriter, req *http.Request) { if !h.requireMethod(rw, req, http.MethodGet) { return } JSON(rw, http.StatusOK, services.OAuthProviders()) } // APIOAuthLogin handles GET /map/api/oauth/:provider/login. func (h *Handlers) APIOAuthLogin(rw http.ResponseWriter, req *http.Request, provider string) { if !h.requireMethod(rw, req, http.MethodGet) { return } redirect := req.URL.Query().Get("redirect") authURL, err := h.Auth.OAuthInitLogin(req.Context(), provider, redirect, req) if err != nil { HandleServiceError(rw, err) return } http.Redirect(rw, req, authURL, http.StatusFound) } // APIOAuthCallback handles GET /map/api/oauth/:provider/callback. func (h *Handlers) APIOAuthCallback(rw http.ResponseWriter, req *http.Request, provider string) { if !h.requireMethod(rw, req, http.MethodGet) { return } code := req.URL.Query().Get("code") state := req.URL.Query().Get("state") if code == "" || state == "" { JSONError(rw, http.StatusBadRequest, "missing code or state", "BAD_REQUEST") return } sessionID, redirectTo, err := h.Auth.OAuthHandleCallback(req.Context(), provider, code, state, req) if err != nil { HandleServiceError(rw, err) return } http.SetCookie(rw, &http.Cookie{ Name: "session", Value: sessionID, Path: "/", MaxAge: app.SessionMaxAge, HttpOnly: true, Secure: req.TLS != nil, SameSite: http.SameSiteLaxMode, }) http.Redirect(rw, req, redirectTo, http.StatusFound) } // RedirectLogout handles GET /logout. func (h *Handlers) RedirectLogout(rw http.ResponseWriter, req *http.Request) { if req.URL.Path != "/logout" { http.NotFound(rw, req) return } ctx := req.Context() s := h.Auth.GetSession(ctx, req) if s != nil { h.Auth.DeleteSession(ctx, s) } http.Redirect(rw, req, "/login", http.StatusFound) }