Files
Nikolay Tatarinov 8f769543f4 Refactor frontend components and enhance API integration
- Updated frontend-nuxt.mdc to specify usage of composables for API calls.
- Added new AuthCard and ConfirmModal components for improved UI consistency.
- Introduced UserAvatar component for user profile display, replacing previous Gravatar implementation.
- Implemented useFormSubmit composable for handling form submissions with loading and error states.
- Enhanced vitest.config.ts to include coverage reporting for composables and components.
- Removed deprecated useAdminApi and useAuth composables to streamline API interactions.
- Updated login and setup pages to utilize new components and composables for better user experience.
2026-03-04 00:14:05 +03:00

243 lines
6.9 KiB
Go

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)
}