- Added email field to user profile API and frontend components for better user identification. - Implemented PATCH /map/api/me endpoint to update user email, enhancing user experience. - Introduced useGravatarUrl composable for generating Gravatar URLs based on user email. - Updated profile and layout components to display user avatars using Gravatar, improving visual consistency. - Enhanced development documentation to guide testing of navbar and profile features.
257 lines
8.0 KiB
Go
257 lines
8.0 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 req.Method != http.MethodPost {
|
|
JSONError(rw, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED")
|
|
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 req.Method != http.MethodGet {
|
|
JSONError(rw, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED")
|
|
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 req.Method != http.MethodPost {
|
|
JSONError(rw, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED")
|
|
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 req.Method != http.MethodGet {
|
|
JSONError(rw, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED")
|
|
return
|
|
}
|
|
ctx := req.Context()
|
|
s := h.Auth.GetSession(ctx, req)
|
|
if s == nil {
|
|
JSONError(rw, http.StatusUnauthorized, "Unauthorized", "UNAUTHORIZED")
|
|
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 req.Method != http.MethodPatch {
|
|
JSONError(rw, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED")
|
|
return
|
|
}
|
|
ctx := req.Context()
|
|
s := h.Auth.GetSession(ctx, req)
|
|
if s == nil {
|
|
JSONError(rw, http.StatusUnauthorized, "Unauthorized", "UNAUTHORIZED")
|
|
return
|
|
}
|
|
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 req.Method != http.MethodPost {
|
|
JSONError(rw, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED")
|
|
return
|
|
}
|
|
ctx := req.Context()
|
|
s := h.Auth.GetSession(ctx, req)
|
|
if s == nil {
|
|
JSONError(rw, http.StatusUnauthorized, "Unauthorized", "UNAUTHORIZED")
|
|
return
|
|
}
|
|
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 req.Method != http.MethodPost {
|
|
JSONError(rw, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED")
|
|
return
|
|
}
|
|
ctx := req.Context()
|
|
s := h.Auth.GetSession(ctx, req)
|
|
if s == nil {
|
|
JSONError(rw, http.StatusUnauthorized, "Unauthorized", "UNAUTHORIZED")
|
|
return
|
|
}
|
|
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 req.Method != http.MethodGet {
|
|
JSONError(rw, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED")
|
|
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 req.Method != http.MethodGet {
|
|
JSONError(rw, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED")
|
|
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 req.Method != http.MethodGet {
|
|
JSONError(rw, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED")
|
|
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)
|
|
}
|