Start creating user resources without ID.

This commit is contained in:
Eli Ribble 2026-04-01 20:22:15 +00:00
parent a656d45a6d
commit 6fbde6389d
No known key found for this signature in database
5 changed files with 159 additions and 33 deletions

View file

@ -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(&params, 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)

View file

@ -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

View file

@ -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() &&

View file

@ -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 {

View file

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