http: GET /api/users (list)
This commit is contained in:
@@ -0,0 +1,52 @@
|
|||||||
|
// api_users.go — JSON handlers for the user-management surface.
|
||||||
|
//
|
||||||
|
// All endpoints in this file are admin-only; gating happens at the
|
||||||
|
// route-mount site (server.go's admin band).
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
|
stdhttp "net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type listUsersResponse struct {
|
||||||
|
Users []apiUser `json:"users"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type apiUser struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
Email *string `json:"email,omitempty"`
|
||||||
|
Disabled bool `json:"disabled"`
|
||||||
|
MustChangePassword bool `json:"must_change_password"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
LastLoginAt *string `json:"last_login_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleAPIUsersList(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
||||||
|
users, err := s.deps.Store.ListUsers(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("api users: list", "err", err)
|
||||||
|
writeJSONError(w, stdhttp.StatusInternalServerError, "internal", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out := make([]apiUser, len(users))
|
||||||
|
for i, u := range users {
|
||||||
|
var lastLogin *string
|
||||||
|
if u.LastLoginAt != nil {
|
||||||
|
s := u.LastLoginAt.UTC().Format("2006-01-02T15:04:05Z")
|
||||||
|
lastLogin = &s
|
||||||
|
}
|
||||||
|
out[i] = apiUser{
|
||||||
|
ID: u.ID, Username: u.Username, Role: string(u.Role),
|
||||||
|
Email: u.Email, Disabled: u.DisabledAt != nil,
|
||||||
|
MustChangePassword: u.MustChangePassword,
|
||||||
|
CreatedAt: u.CreatedAt.UTC().Format("2006-01-02T15:04:05Z"),
|
||||||
|
LastLoginAt: lastLogin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
_ = json.NewEncoder(w).Encode(listUsersResponse{Users: out})
|
||||||
|
}
|
||||||
@@ -144,10 +144,6 @@ func TestLoginRejectsDisabledUser(t *testing.T) {
|
|||||||
|
|
||||||
func TestAdminBandRejectsOperator(t *testing.T) {
|
func TestAdminBandRejectsOperator(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
// This test will start asserting 403 once Task B4 mounts /api/users
|
|
||||||
// inside the admin band and Task E1 lands the handler. Until then,
|
|
||||||
// the route 404s — we skip rather than red-flag the suite.
|
|
||||||
t.Skip("re-enable after B4 route grouping + E1 /api/users handler land")
|
|
||||||
srv, urlBase := newTestServer(t, false)
|
srv, urlBase := newTestServer(t, false)
|
||||||
makeUser(t, srv, "admin1", store.RoleAdmin)
|
makeUser(t, srv, "admin1", store.RoleAdmin)
|
||||||
opID := makeUser(t, srv, "op1", store.RoleOperator)
|
opID := makeUser(t, srv, "op1", store.RoleOperator)
|
||||||
|
|||||||
@@ -251,6 +251,7 @@ func (s *Server) routes(r chi.Router) {
|
|||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(s.requireRole(store.RoleAdmin))
|
r.Use(s.requireRole(store.RoleAdmin))
|
||||||
|
|
||||||
|
r.Get("/api/users", s.handleAPIUsersList)
|
||||||
r.Post("/api/notifications/{id}/test", s.handleAPINotificationTest)
|
r.Post("/api/notifications/{id}/test", s.handleAPINotificationTest)
|
||||||
|
|
||||||
if s.deps.UI != nil {
|
if s.deps.UI != nil {
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
stdhttp "net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dcglab.co.uk/steve/restic-manager/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAPIUsersList(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
srv, ts, _ := rawTestServerWithUI(t)
|
||||||
|
adminID := makeUser(t, srv, "admin1", store.RoleAdmin)
|
||||||
|
makeUser(t, srv, "op1", store.RoleOperator)
|
||||||
|
cookie := loginAs(t, srv, adminID)
|
||||||
|
|
||||||
|
req, _ := stdhttp.NewRequest("GET", ts.URL+"/api/users", nil)
|
||||||
|
req.AddCookie(cookie)
|
||||||
|
res, err := stdhttp.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GET: %v", err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != stdhttp.StatusOK {
|
||||||
|
body, _ := io.ReadAll(res.Body)
|
||||||
|
t.Fatalf("status: got %d body=%s", res.StatusCode, body)
|
||||||
|
}
|
||||||
|
var got listUsersResponse
|
||||||
|
_ = json.NewDecoder(res.Body).Decode(&got)
|
||||||
|
if len(got.Users) != 2 {
|
||||||
|
t.Errorf("count: got %d want 2", len(got.Users))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user