POST /users/{id}/promote and /demote set a user's role (admin/user), admin-only
(403 otherwise). Mirrors the verify endpoints: SetRole use case + repo method,
validated role. Unit, admin-flow, and integration tests included.
132 lines
3.9 KiB
Go
132 lines
3.9 KiB
Go
package http
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/emil28092005/SciMesh/users/internal/domain"
|
|
"github.com/emil28092005/SciMesh/users/internal/usecase"
|
|
)
|
|
|
|
// Handlers holds the use cases each endpoint drives.
|
|
type Handlers struct {
|
|
register *usecase.Register
|
|
login *usecase.Login
|
|
setVerified *usecase.SetVerified
|
|
setRole *usecase.SetRole
|
|
users usecase.UserRepository
|
|
log *slog.Logger
|
|
}
|
|
|
|
// handleHealth is an unauthenticated liveness probe for the container and load
|
|
// balancer.
|
|
func (h *Handlers) handleHealth(w http.ResponseWriter, _ *http.Request) {
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
// handleRegister creates an account. It returns 201 with the public user view,
|
|
// 409 if the email is taken, or 400 on a malformed body / weak password.
|
|
func (h *Handlers) handleRegister(w http.ResponseWriter, r *http.Request) {
|
|
var req registerRequest
|
|
if !decodeJSON(w, r, &req) {
|
|
return
|
|
}
|
|
u, err := h.register.Execute(r.Context(), req.Email, req.Password)
|
|
if err != nil {
|
|
writeError(w, r, h.log, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, toUserResponse(u))
|
|
}
|
|
|
|
// handleLogin verifies credentials and returns a signed token plus the user.
|
|
func (h *Handlers) handleLogin(w http.ResponseWriter, r *http.Request) {
|
|
var req loginRequest
|
|
if !decodeJSON(w, r, &req) {
|
|
return
|
|
}
|
|
token, u, err := h.login.Execute(r.Context(), req.Email, req.Password)
|
|
if err != nil {
|
|
writeError(w, r, h.log, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, loginResponse{Token: token, User: toUserResponse(u)})
|
|
}
|
|
|
|
// handleMe returns the caller's own account, proving the token works end to end.
|
|
// It reads the user id the JWT middleware stashed in the context.
|
|
func (h *Handlers) handleMe(w http.ResponseWriter, r *http.Request) {
|
|
id, ok := userIDFrom(r.Context())
|
|
if !ok {
|
|
unauthorized(w, r)
|
|
return
|
|
}
|
|
u, err := h.users.GetByID(r.Context(), id)
|
|
if err != nil {
|
|
writeError(w, r, h.log, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, toUserResponse(u))
|
|
}
|
|
|
|
// handleSetVerified grants (verified=true) or revokes (false) the trusted-
|
|
// contributor badge for the user in the path. Admin-only; the withAdmin
|
|
// middleware has already enforced the role by the time this runs.
|
|
func (h *Handlers) handleSetVerified(verified bool) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
id, err := uuid.Parse(r.PathValue("id"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, errorResponse{
|
|
Error: "invalid user id",
|
|
RequestID: requestIDFrom(r.Context()),
|
|
})
|
|
return
|
|
}
|
|
if err := h.setVerified.Execute(r.Context(), id, verified); err != nil {
|
|
writeError(w, r, h.log, err)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
// handleSetRole promotes (admin) or demotes (user) the user in the path. Admin-
|
|
// only; the withAdmin middleware has already enforced the caller's role.
|
|
func (h *Handlers) handleSetRole(role domain.Role) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
id, err := uuid.Parse(r.PathValue("id"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, errorResponse{
|
|
Error: "invalid user id",
|
|
RequestID: requestIDFrom(r.Context()),
|
|
})
|
|
return
|
|
}
|
|
if err := h.setRole.Execute(r.Context(), id, role); err != nil {
|
|
writeError(w, r, h.log, err)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
// decodeJSON reads a size-capped JSON body into dst, rejecting unknown fields.
|
|
// It writes a 400 and returns false on any problem, so callers can `if
|
|
// !decodeJSON(...) { return }`.
|
|
func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) bool {
|
|
r.Body = http.MaxBytesReader(w, r.Body, maxJSONBody)
|
|
dec := json.NewDecoder(r.Body)
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(dst); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, errorResponse{
|
|
Error: "invalid JSON body",
|
|
RequestID: requestIDFrom(r.Context()),
|
|
})
|
|
return false
|
|
}
|
|
return true
|
|
}
|