From 6fbde6389dee2bba115155b8539d810df5ce56d9 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 1 Apr 2026 20:22:15 +0000 Subject: [PATCH] Start creating user resources without ID. --- api/handler.go | 37 +++++++++++++++ api/routes.go | 6 +-- platform/organization.go | 15 ++++++ platform/user.go | 35 +++++++++++++- resource/user.go | 99 ++++++++++++++++++++++++++++------------ 5 files changed, 159 insertions(+), 33 deletions(-) diff --git a/api/handler.go b/api/handler.go index fb6c899a..a102f8c4 100644 --- a/api/handler.go +++ b/api/handler.go @@ -66,6 +66,43 @@ func authenticatedHandlerJSON[T any](f handlerFunctionGet[T]) http.Handler { }) } +type handlerFunctionGetSlice[T any] func(context.Context, *http.Request, platform.User, resource.QueryParams) ([]*T, *nhttp.ErrorWithStatus) + +func authenticatedHandlerJSONSlice[T any](f handlerFunctionGetSlice[T]) http.Handler { + return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) { + ctx := r.Context() + var body []byte + var params resource.QueryParams + err := decoder.Decode(¶ms, r.URL.Query()) + if err != nil { + log.Error().Err(err).Msg("decode query failure") + http.Error(w, "failed to decode query", http.StatusInternalServerError) + return + } + resp, e := f(ctx, r, u, params) + w.Header().Set("Content-Type", "application/json") + //log.Info().Str("template", template).Err(e).Msg("handler done") + if e != nil { + log.Warn().Int("status", e.Status).Err(e).Str("user message", e.Message).Msg("Responding with an error from api") + body, err = json.Marshal(ErrorAPI{Message: e.Error()}) + if err != nil { + log.Error().Err(err).Msg("failed to marshal error") + http.Error(w, "{\"message\": \"boom. I can't even tell you what went wrong\"}", http.StatusInternalServerError) + return + } + http.Error(w, string(body), e.Status) + return + } + body, err = json.Marshal(resp) + if err != nil { + log.Error().Err(err).Msg("failed to marshal json") + http.Error(w, "{\"message\": \"failed to marshal json\"}", http.StatusInternalServerError) + return + } + w.Write(body) + }) +} + type handlerFunctionPost[ReqType any] func(context.Context, *http.Request, ReqType) (string, *nhttp.ErrorWithStatus) type handlerFunctionPostAuthenticated[ReqType any] func(context.Context, *http.Request, platform.User, ReqType) (string, *nhttp.ErrorWithStatus) diff --git a/api/routes.go b/api/routes.go index 8fb699e3..c6cd7e18 100644 --- a/api/routes.go +++ b/api/routes.go @@ -50,11 +50,11 @@ func AddRoutes(r *mux.Router) { r.Handle("/upload/{id}/commit", authenticatedHandlerJSONPost(upload.Commit)).Methods("POST") r.Handle("/upload/{id}/discard", authenticatedHandlerJSONPost(upload.Discard)).Methods("POST") - user := resource.NewUser(r) + user := resource.User(r) r.Handle("/user/self", authenticatedHandlerJSON(user.SelfGet)).Methods("GET") r.Handle("/user/suggestion", authenticatedHandlerJSON(user.SuggestionGet)).Methods("GET") - r.Handle("/user", authenticatedHandlerJSON(user.List)).Methods("GET") - r.Handle("/user/{id}", authenticatedHandlerJSON(user.ByIDGet)).Methods("GET") + r.Handle("/user", authenticatedHandlerJSONSlice(user.List)).Methods("GET") + r.Handle("/user/{id}", authenticatedHandlerJSON(user.ByIDGet)).Methods("GET").Name("user.ByIDGet") r.Handle("/user/{id}", authenticatedHandlerJSONPut(user.ByIDPut)).Methods("PUT") // Unauthenticated endpoints diff --git a/platform/organization.go b/platform/organization.go index bac16295..21fa2262 100644 --- a/platform/organization.go +++ b/platform/organization.go @@ -98,6 +98,21 @@ func OrganizationByID(ctx context.Context, id int) (*Organization, error) { o := newOrganization(org) return &o, nil } +func OrganizationList(ctx context.Context, user User) ([]*Organization, error) { + if !user.HasRoot() { + return []*Organization{&user.Organization}, nil + } + rows, err := models.Organizations.Query().All(ctx, db.PGInstance.BobDB) + if err != nil { + return nil, fmt.Errorf("query orgs: %w", err) + } + results := make([]*Organization, len(rows)) + for i, row := range rows { + o := newOrganization(row) + results[i] = &o + } + return results, err +} func newOrganization(org *models.Organization) Organization { var sa *ServiceArea if org.ServiceAreaXmax.IsValue() && diff --git a/platform/user.go b/platform/user.go index abe5741e..7ca09a02 100644 --- a/platform/user.go +++ b/platform/user.go @@ -37,7 +37,6 @@ type User struct { PasswordHashType string `json:"-"` Role string `json:"role"` Tags []string `json:"tags"` - URI string `json:"uri"` Username string `json:"username"` model *models.User @@ -67,7 +66,6 @@ func newUser(ctx context.Context, org Organization, user *models.User) User { PasswordHashType: string(user.PasswordHashType), Role: user.Role.String(), Tags: []string{}, - URI: fmt.Sprintf("/user/%d", user.ID), Username: user.Username, model: user, @@ -113,6 +111,39 @@ func UserByID(ctx context.Context, user_id int32) (*User, error) { func UserByUsername(ctx context.Context, username string) (*User, error) { return getUser(ctx, models.SelectWhere.Users.Username.EQ(username)) } +func UserList(ctx context.Context, user User) ([]*User, error) { + var query models.UsersQuery + var orgByID map[int32]*Organization + if user.HasRoot() { + query = models.Users.Query() + orgs, err := OrganizationList(ctx, user) + if err != nil { + return nil, fmt.Errorf("org list: %w", err) + } + orgByID = make(map[int32]*Organization, len(orgs)) + for _, org := range orgs { + orgByID[org.ID] = org + } + } else { + query = user.Organization.model.User() + orgByID = make(map[int32]*Organization, 1) + orgByID[user.model.OrganizationID] = &user.Organization + } + rows, err := query.All(ctx, db.PGInstance.BobDB) + results := make([]*User, len(rows)) + if err != nil { + return nil, fmt.Errorf("query users: %w", err) + } + for i, row := range rows { + org, ok := orgByID[row.OrganizationID] + if !ok { + return nil, fmt.Errorf("get org %d", row.OrganizationID) + } + new_user := newUser(ctx, *org, row) + results[i] = &new_user + } + return results, nil +} func UsersByOrg(ctx context.Context, org Organization) (map[int32]*User, error) { users, err := org.model.User().All(ctx, db.PGInstance.BobDB) if err != nil { diff --git a/resource/user.go b/resource/user.go index 6e95f34b..40881d21 100644 --- a/resource/user.go +++ b/resource/user.go @@ -2,6 +2,7 @@ package resource import ( "context" + "fmt" "net/http" "strconv" @@ -13,16 +14,56 @@ import ( "github.com/rs/zerolog/log" ) -type userR struct { - router *mux.Router +type userResponse struct { + Avatar string `json:"avatar"` + DisplayName string `json:"display_name"` + Initials string `json:"initials"` + IsActive bool `json:"is_active"` + //Notifications []Notification `json:"notifications"` + //NotificationCounts UserNotificationCounts `json:"notification_counts"` + //Organization Organization `json:"organization"` + PasswordHash string `json:"-"` + PasswordHashType string `json:"-"` + Role string `json:"role"` + Tags []string `json:"tags"` + URI string `json:"uri"` + Username string `json:"username"` } -func NewUser(r *mux.Router) *userR { +func User(r *mux.Router) *userR { return &userR{ router: r, } } +func (res *userR) response(u *platform.User) (*userResponse, error) { + if u == nil { + return nil, fmt.Errorf("nil user") + } + log.Info().Int("id", u.ID).Msg("making response from user") + i := strconv.FormatInt(int64(u.ID), 10) + handler := res.router.Get("user.ByIDGet") + if handler == nil { + return nil, fmt.Errorf("nil handler") + } + uri, err := handler.URL("id", i) + if err != nil { + return nil, fmt.Errorf("build uri: %w", err) + } + return &userResponse{ + Avatar: u.Avatar, + DisplayName: u.DisplayName, + Initials: u.Initials, + IsActive: u.Active, + Role: u.Role, + Tags: u.Tags, + URI: uri.String(), + Username: u.Username, + }, nil +} +type userR struct { + router *mux.Router +} type responseListUser struct { Users []*platform.User `json:"users"` } @@ -56,6 +97,21 @@ func (res *userR) ByIDGet(ctx context.Context, r *http.Request, user platform.Us return u, nil } +func (res *userR) ByIDPut(ctx context.Context, r *http.Request, user platform.User, updates platform.UserChangeRequest) (string, *nhttp.ErrorWithStatus) { + log.Info().Str("avatar", updates.Avatar).Msg("doing updates") + vars := mux.Vars(r) + user_id_str := vars["id"] + user_id, err := strconv.Atoi(user_id_str) + if err != nil { + return "", nhttp.NewErrorStatus(http.StatusBadRequest, "user update: %w", err) + } + err = platform.UserUpdate(ctx, user, user_id, updates) + if err != nil { + return "", nhttp.NewError("user update: %w", err) + } + return "", nil +} + func (res *userR) SelfGet(ctx context.Context, r *http.Request, user platform.User, query QueryParams) (*contentUserSelf, *nhttp.ErrorWithStatus) { counts, err := platform.NotificationCountsForUser(ctx, user) if err != nil { @@ -86,20 +142,22 @@ func (res *userR) SelfGet(ctx context.Context, r *http.Request, user platform.Us }, nil } -func (res *userR) List(ctx context.Context, r *http.Request, user platform.User, query QueryParams) (*responseListUser, *nhttp.ErrorWithStatus) { - users, err := platform.UsersByOrg(ctx, user.Organization) +func (res *userR) List(ctx context.Context, r *http.Request, user platform.User, query QueryParams) ([]*userResponse, *nhttp.ErrorWithStatus) { + users, err := platform.UserList(ctx, user) if err != nil { return nil, nhttp.NewError("list users: %w", err) } - results := make([]*platform.User, len(users)) - i := 0 - for _, v := range users { - results[i] = v - i++ + results := make([]*userResponse, len(users)) + log.Debug().Int("len", len(users)).Msg("building response") + for i, v := range users { + log.Debug().Int("i", i).Msg("making results") + resp, err := res.response(v) + if err != nil { + return nil, nhttp.NewError("create response: %w", err) + } + results[i] = resp } - return &responseListUser{ - Users: results, - }, nil + return results, nil } type responseListUserSuggestion struct { @@ -118,18 +176,3 @@ func (res *userR) SuggestionGet(ctx context.Context, r *http.Request, user platf Users: users, }, nil } - -func (res *userR) ByIDPut(ctx context.Context, r *http.Request, user platform.User, updates platform.UserChangeRequest) (string, *nhttp.ErrorWithStatus) { - log.Info().Str("avatar", updates.Avatar).Msg("doing updates") - vars := mux.Vars(r) - user_id_str := vars["id"] - user_id, err := strconv.Atoi(user_id_str) - if err != nil { - return "", nhttp.NewErrorStatus(http.StatusBadRequest, "user update: %w", err) - } - err = platform.UserUpdate(ctx, user, user_id, updates) - if err != nil { - return "", nhttp.NewError("user update: %w", err) - } - return "", nil -}