Compare commits

...

13 Commits

Author SHA1 Message Date
Matt
6dc801d024 Merge branch 'main' into mk/multi-groups 2025-12-29 15:11:14 -05:00
Matthew Kilgore
2e5a2c9323 Fix front-end tests for groups 2025-12-28 17:26:40 -05:00
Matthew Kilgore
301098bad5 Go lint things 2025-12-28 17:16:39 -05:00
Matthew Kilgore
e6c1a5a47f Fix some tests 2025-12-28 17:11:05 -05:00
Matthew Kilgore
6c4ce49093 Add member management routes 2025-12-28 17:05:33 -05:00
Matthew Kilgore
825eda0f00 Merge remote-tracking branch 'origin/main' into mk/multi-groups
# Conflicts:
#	backend/internal/data/repo/repo_items_test.go
2025-12-28 16:34:00 -05:00
Matthew Kilgore
ab9b821de9 New API endpoints for basic group management 2025-12-28 12:52:05 -05:00
Matthew Kilgore
74bdaa47d2 Fix golang test 2025-12-28 12:18:59 -05:00
Matthew Kilgore
f3b4c0cad3 Forgot to update the API stuff and data-contracts.ts 2025-12-28 12:11:48 -05:00
Matthew Kilgore
9f5095a1d2 The basics of the app are working again now 2025-12-28 12:10:37 -05:00
Matthew Kilgore
3fcb1ccfd0 Fix Sqlite migration (or at least make sure it doesn't wipe things 2025-12-28 11:50:10 -05:00
Matt
70adb00400 Merge branch 'main' into mk/multi-groups 2025-12-28 11:42:55 -05:00
Matthew Kilgore
dd873a95da Add default group handling and user-groups relationship
- Introduced `default_group_id` field in the User model to manage user group defaults.
- Updated user creation and update logic to utilize the new default group ID.
- Implemented a many-to-many relationship between users and groups via a new `user_groups` junction table.
- Refactored relevant queries and middleware to support tenant-based access using the default group.
2025-12-26 20:16:52 -05:00
60 changed files with 3288 additions and 605 deletions

View File

@@ -8,6 +8,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/sysadminsmedia/homebox/backend/internal/core/services" "github.com/sysadminsmedia/homebox/backend/internal/core/services"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent"
) )
func (a *app) SetupDemo() error { func (a *app) SetupDemo() error {
@@ -29,21 +30,26 @@ func (a *app) SetupDemo() error {
Password: "demo", Password: "demo",
} }
// First check if we've already setup a demo user and skip if so // If demo user already exists, skip all demo seeding tasks
log.Debug().Msg("Checking if demo user already exists") if a.services.User.ExistsByEmail(ctx, registration.Email) {
_, err := a.services.User.Login(ctx, registration.Email, registration.Password, false) log.Info().Msg("Demo user already exists; skipping demo seeding")
if err == nil {
log.Info().Msg("Demo user already exists, skipping setup")
return nil return nil
} }
log.Debug().Msg("Demo user does not exist, setting up demo") // Otherwise, register the demo user
_, err = a.services.User.RegisterUser(ctx, registration) log.Debug().Msg("Registering demo user")
_, err := a.services.User.RegisterUser(ctx, registration)
if err != nil { if err != nil {
if ent.IsConstraintError(err) {
// Concurrent creation race: treat as exists and skip
log.Info().Msg("Demo user concurrently created; skipping seeding")
return nil
}
log.Err(err).Msg("Failed to register demo user") log.Err(err).Msg("Failed to register demo user")
return errors.New("failed to setup demo") return errors.New("failed to setup demo")
} }
// Login the demo user to get a token
token, err := a.services.User.Login(ctx, registration.Email, registration.Password, false) token, err := a.services.User.Login(ctx, registration.Email, registration.Password, false)
if err != nil { if err != nil {
log.Err(err).Msg("Failed to login demo user") log.Err(err).Msg("Failed to login demo user")
@@ -55,7 +61,7 @@ func (a *app) SetupDemo() error {
return errors.New("failed to setup demo") return errors.New("failed to setup demo")
} }
_, err = a.services.Items.CsvImport(ctx, self.GroupID, strings.NewReader(csvText)) _, err = a.services.Items.CsvImport(ctx, self.DefaultGroupID, strings.NewReader(csvText))
if err != nil { if err != nil {
log.Err(err).Msg("Failed to import CSV") log.Err(err).Msg("Failed to import CSV")
return errors.New("failed to setup demo") return errors.New("failed to setup demo")

View File

@@ -4,6 +4,7 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/google/uuid"
"github.com/hay-kot/httpkit/errchain" "github.com/hay-kot/httpkit/errchain"
"github.com/sysadminsmedia/homebox/backend/internal/core/services" "github.com/sysadminsmedia/homebox/backend/internal/core/services"
"github.com/sysadminsmedia/homebox/backend/internal/data/repo" "github.com/sysadminsmedia/homebox/backend/internal/data/repo"
@@ -22,6 +23,10 @@ type (
ExpiresAt time.Time `json:"expiresAt"` ExpiresAt time.Time `json:"expiresAt"`
Uses int `json:"uses"` Uses int `json:"uses"`
} }
GroupMemberAdd struct {
UserID uuid.UUID `json:"userId" validate:"required"`
}
) )
// HandleGroupGet godoc // HandleGroupGet godoc
@@ -95,3 +100,132 @@ func (ctrl *V1Controller) HandleGroupInvitationsCreate() errchain.HandlerFunc {
return adapters.Action(fn, http.StatusCreated) return adapters.Action(fn, http.StatusCreated)
} }
// HandleGroupsGetAll godoc
//
// @Summary Get All Groups
// @Tags Group
// @Produce json
// @Success 200 {object} []repo.Group
// @Router /v1/groups [Get]
// @Security Bearer
func (ctrl *V1Controller) HandleGroupsGetAll() errchain.HandlerFunc {
fn := func(r *http.Request) ([]repo.Group, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.Groups.GetAllGroups(auth)
}
return adapters.Command(fn, http.StatusOK)
}
// HandleGroupCreate godoc
//
// @Summary Create Group
// @Tags Group
// @Produce json
// @Param name body string true "Group Name"
// @Success 201 {object} repo.Group
// @Router /v1/groups/{id} [Post]
// @Security Bearer
func (ctrl *V1Controller) HandleGroupCreate() errchain.HandlerFunc {
type CreateRequest struct {
Name string `json:"name" validate:"required"`
}
fn := func(r *http.Request, body CreateRequest) (repo.Group, error) {
auth := services.NewContext(r.Context())
return ctrl.svc.Group.CreateGroup(auth, body.Name)
}
return adapters.Action(fn, http.StatusCreated)
}
// HandleGroupDelete godoc
//
// @Summary Delete Group
// @Tags Group
// @Produce json
// @Success 204
// @Router /v1/groups/{id} [Delete]
// @Security Bearer
func (ctrl *V1Controller) HandleGroupDelete() errchain.HandlerFunc {
fn := func(r *http.Request) (any, error) {
auth := services.NewContext(r.Context())
err := ctrl.svc.Group.DeleteGroup(auth)
return nil, err
}
return adapters.Command(fn, http.StatusNoContent)
}
// HandleGroupInvitationsGetAll godoc
//
// @Summary Get All Group Invitations
// @Tags Group
// @Produce json
// @Success 200 {object} []repo.GroupInvitation
// @Router /v1/groups/invitations [Get]
// @Security Bearer
func (ctrl *V1Controller) HandleGroupInvitationsGetAll() errchain.HandlerFunc {
fn := func(r *http.Request) ([]repo.GroupInvitation, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.Groups.InvitationGetAll(auth, auth.GID)
}
return adapters.Command(fn, http.StatusOK)
}
// HandleGroupMembersGetAll godoc
//
// @Summary Get All Group Members
// @Tags Group
// @Produce json
// @Success 200 {object} []repo.UserOut
// @Router /v1/groups/{id}/members [Get]
// @Security Bearer
func (ctrl *V1Controller) HandleGroupMembersGetAll() errchain.HandlerFunc {
fn := func(r *http.Request) ([]repo.UserOut, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.Users.GetUsersByGroupID(auth, auth.GID)
}
return adapters.Command(fn, http.StatusOK)
}
// HandleGroupMemberAdd godoc
//
// @Summary Add User to Group
// @Tags Group
// @Produce json
// @Param payload body GroupMemberAdd true "User ID"
// @Success 204
// @Router /v1/groups/{id}/members [Post]
// @Security Bearer
func (ctrl *V1Controller) HandleGroupMemberAdd() errchain.HandlerFunc {
fn := func(r *http.Request, body GroupMemberAdd) (any, error) {
auth := services.NewContext(r.Context())
err := ctrl.svc.Group.AddMember(auth, body.UserID)
return nil, err
}
return adapters.Action(fn, http.StatusNoContent)
}
// HandleGroupMemberRemove godoc
//
// @Summary Remove User from Group
// @Tags Group
// @Produce json
// @Param user_id path string true "User ID"
// @Success 204
// @Router /v1/groups/{id}/members/{user_id} [Delete]
// @Security Bearer
func (ctrl *V1Controller) HandleGroupMemberRemove() errchain.HandlerFunc {
fn := func(r *http.Request, userID uuid.UUID) (any, error) {
auth := services.NewContext(r.Context())
err := ctrl.svc.Group.RemoveMember(auth, userID)
return nil, err
}
return adapters.CommandID("user_id", fn, http.StatusNoContent)
}

View File

@@ -337,9 +337,9 @@ func (ctrl *V1Controller) HandleItemsImport() errchain.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
user := services.UseUserCtx(r.Context()) tenant := services.UseTenantCtx(r.Context())
_, err = ctrl.svc.Items.CsvImport(r.Context(), user.GroupID, file) _, err = ctrl.svc.Items.CsvImport(r.Context(), tenant, file)
if err != nil { if err != nil {
log.Err(err).Msg("failed to import items") log.Err(err).Msg("failed to import items")
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)

View File

@@ -1,9 +1,10 @@
package v1 package v1
import ( import (
"net/http"
"github.com/hay-kot/httpkit/errchain" "github.com/hay-kot/httpkit/errchain"
"github.com/sysadminsmedia/homebox/backend/internal/core/services" "github.com/sysadminsmedia/homebox/backend/internal/core/services"
"net/http"
) )
// HandleBillOfMaterialsExport godoc // HandleBillOfMaterialsExport godoc
@@ -16,9 +17,9 @@ import (
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleBillOfMaterialsExport() errchain.HandlerFunc { func (ctrl *V1Controller) HandleBillOfMaterialsExport() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
actor := services.UseUserCtx(r.Context()) tenant := services.UseTenantCtx(r.Context())
csv, err := ctrl.svc.Items.ExportBillOfMaterialsCSV(r.Context(), actor.GroupID) csv, err := ctrl.svc.Items.ExportBillOfMaterialsCSV(r.Context(), tenant)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -7,6 +7,7 @@ import (
"net/url" "net/url"
"strings" "strings"
"github.com/google/uuid"
"github.com/hay-kot/httpkit/errchain" "github.com/hay-kot/httpkit/errchain"
v1 "github.com/sysadminsmedia/homebox/backend/app/api/handlers/v1" v1 "github.com/sysadminsmedia/homebox/backend/app/api/handlers/v1"
"github.com/sysadminsmedia/homebox/backend/internal/core/services" "github.com/sysadminsmedia/homebox/backend/internal/core/services"
@@ -152,3 +153,48 @@ func (a *app) mwAuthToken(next errchain.Handler) errchain.Handler {
return next.ServeHTTP(w, r) return next.ServeHTTP(w, r)
}) })
} }
// mwTenant is a middleware that will parse the X-Tenant header and validate the user has access
// to the requested tenant. If no header is provided, the user's default group is used.
//
// WARNING: This middleware _MUST_ be called after mwAuthToken
func (a *app) mwTenant(next errchain.Handler) errchain.Handler {
return errchain.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
// Get the user from context (set by mwAuthToken)
user := services.UseUserCtx(ctx)
if user == nil {
return validate.NewRequestError(errors.New("user context not found"), http.StatusInternalServerError)
}
tenantID := user.DefaultGroupID
// Check for X-Tenant header
if tenantHeader := r.Header.Get("X-Tenant"); tenantHeader != "" {
parsedTenantID, err := uuid.Parse(tenantHeader)
if err != nil {
return validate.NewRequestError(errors.New("invalid X-Tenant header format"), http.StatusBadRequest)
}
// Validate user has access to the requested tenant
hasAccess := false
for _, gid := range user.GroupIDs {
if gid == parsedTenantID {
hasAccess = true
break
}
}
if !hasAccess {
return validate.NewRequestError(errors.New("user does not have access to the requested tenant"), http.StatusForbidden)
}
tenantID = parsedTenantID
}
// Set the tenant in context
r = r.WithContext(services.SetTenantCtx(ctx, tenantID))
return next.ServeHTTP(w, r)
})
}

View File

@@ -82,10 +82,13 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
userMW := []errchain.Middleware{ userMW := []errchain.Middleware{
a.mwAuthToken, a.mwAuthToken,
a.mwTenant,
a.mwRoles(RoleModeOr, authroles.RoleUser.String()), a.mwRoles(RoleModeOr, authroles.RoleUser.String()),
} }
r.Get("/ws/events", chain.ToHandlerFunc(v1Ctrl.HandleCacheWS(), userMW...)) r.Get("/ws/events", chain.ToHandlerFunc(v1Ctrl.HandleCacheWS(), userMW...))
// User management endpoints
r.Get("/users/self", chain.ToHandlerFunc(v1Ctrl.HandleUserSelf(), userMW...)) r.Get("/users/self", chain.ToHandlerFunc(v1Ctrl.HandleUserSelf(), userMW...))
r.Put("/users/self", chain.ToHandlerFunc(v1Ctrl.HandleUserSelfUpdate(), userMW...)) r.Put("/users/self", chain.ToHandlerFunc(v1Ctrl.HandleUserSelfUpdate(), userMW...))
r.Delete("/users/self", chain.ToHandlerFunc(v1Ctrl.HandleUserSelfDelete(), userMW...)) r.Delete("/users/self", chain.ToHandlerFunc(v1Ctrl.HandleUserSelfDelete(), userMW...))
@@ -93,16 +96,25 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
r.Get("/users/refresh", chain.ToHandlerFunc(v1Ctrl.HandleAuthRefresh(), userMW...)) r.Get("/users/refresh", chain.ToHandlerFunc(v1Ctrl.HandleAuthRefresh(), userMW...))
r.Put("/users/self/change-password", chain.ToHandlerFunc(v1Ctrl.HandleUserSelfChangePassword(), userMW...)) r.Put("/users/self/change-password", chain.ToHandlerFunc(v1Ctrl.HandleUserSelfChangePassword(), userMW...))
// Group management endpoints
r.Get("/groups", chain.ToHandlerFunc(v1Ctrl.HandleGroupsGetAll(), userMW...))
r.Get("/groups/{id}", chain.ToHandlerFunc(v1Ctrl.HandleGroupGet(), userMW...))
r.Post("/groups/{id}", chain.ToHandlerFunc(v1Ctrl.HandleGroupCreate(), userMW...))
r.Put("/groups/{id}", chain.ToHandlerFunc(v1Ctrl.HandleGroupUpdate(), userMW...))
r.Delete("/groups/{id}", chain.ToHandlerFunc(v1Ctrl.HandleGroupDelete(), userMW...))
r.Get("/groups/{id}/members", chain.ToHandlerFunc(v1Ctrl.HandleGroupMembersGetAll(), userMW...))
r.Post("/groups/{id}/members", chain.ToHandlerFunc(v1Ctrl.HandleGroupMemberAdd(), userMW...))
r.Delete("/groups/{id}/members/{user_id}", chain.ToHandlerFunc(v1Ctrl.HandleGroupMemberRemove(), userMW...))
r.Get("/groups/invitations", chain.ToHandlerFunc(v1Ctrl.HandleGroupInvitationsGetAll(), userMW...))
r.Post("/groups/invitations", chain.ToHandlerFunc(v1Ctrl.HandleGroupInvitationsCreate(), userMW...)) r.Post("/groups/invitations", chain.ToHandlerFunc(v1Ctrl.HandleGroupInvitationsCreate(), userMW...))
r.Get("/groups/statistics", chain.ToHandlerFunc(v1Ctrl.HandleGroupStatistics(), userMW...)) r.Get("/groups/statistics", chain.ToHandlerFunc(v1Ctrl.HandleGroupStatistics(), userMW...))
r.Get("/groups/statistics/purchase-price", chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsPriceOverTime(), userMW...)) r.Get("/groups/statistics/purchase-price", chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsPriceOverTime(), userMW...))
r.Get("/groups/statistics/locations", chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsLocations(), userMW...)) r.Get("/groups/statistics/locations", chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsLocations(), userMW...))
r.Get("/groups/statistics/labels", chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsLabels(), userMW...)) r.Get("/groups/statistics/labels", chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsLabels(), userMW...))
// TODO: I don't like /groups being the URL for users // Action endpoints
r.Get("/groups", chain.ToHandlerFunc(v1Ctrl.HandleGroupGet(), userMW...))
r.Put("/groups", chain.ToHandlerFunc(v1Ctrl.HandleGroupUpdate(), userMW...))
r.Post("/actions/ensure-asset-ids", chain.ToHandlerFunc(v1Ctrl.HandleEnsureAssetID(), userMW...)) r.Post("/actions/ensure-asset-ids", chain.ToHandlerFunc(v1Ctrl.HandleEnsureAssetID(), userMW...))
r.Post("/actions/zero-item-time-fields", chain.ToHandlerFunc(v1Ctrl.HandleItemDateZeroOut(), userMW...)) r.Post("/actions/zero-item-time-fields", chain.ToHandlerFunc(v1Ctrl.HandleItemDateZeroOut(), userMW...))
r.Post("/actions/ensure-import-refs", chain.ToHandlerFunc(v1Ctrl.HandleEnsureImportRefs(), userMW...)) r.Post("/actions/ensure-import-refs", chain.ToHandlerFunc(v1Ctrl.HandleEnsureImportRefs(), userMW...))
@@ -110,6 +122,7 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
r.Post("/actions/create-missing-thumbnails", chain.ToHandlerFunc(v1Ctrl.HandleCreateMissingThumbnails(), userMW...)) r.Post("/actions/create-missing-thumbnails", chain.ToHandlerFunc(v1Ctrl.HandleCreateMissingThumbnails(), userMW...))
r.Post("/actions/wipe-inventory", chain.ToHandlerFunc(v1Ctrl.HandleWipeInventory(), userMW...)) r.Post("/actions/wipe-inventory", chain.ToHandlerFunc(v1Ctrl.HandleWipeInventory(), userMW...))
// Location endpoints
r.Get("/locations", chain.ToHandlerFunc(v1Ctrl.HandleLocationGetAll(), userMW...)) r.Get("/locations", chain.ToHandlerFunc(v1Ctrl.HandleLocationGetAll(), userMW...))
r.Post("/locations", chain.ToHandlerFunc(v1Ctrl.HandleLocationCreate(), userMW...)) r.Post("/locations", chain.ToHandlerFunc(v1Ctrl.HandleLocationCreate(), userMW...))
r.Get("/locations/tree", chain.ToHandlerFunc(v1Ctrl.HandleLocationTreeQuery(), userMW...)) r.Get("/locations/tree", chain.ToHandlerFunc(v1Ctrl.HandleLocationTreeQuery(), userMW...))
@@ -117,12 +130,14 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
r.Put("/locations/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLocationUpdate(), userMW...)) r.Put("/locations/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLocationUpdate(), userMW...))
r.Delete("/locations/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLocationDelete(), userMW...)) r.Delete("/locations/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLocationDelete(), userMW...))
// Labels (tags) endpoints
r.Get("/labels", chain.ToHandlerFunc(v1Ctrl.HandleLabelsGetAll(), userMW...)) r.Get("/labels", chain.ToHandlerFunc(v1Ctrl.HandleLabelsGetAll(), userMW...))
r.Post("/labels", chain.ToHandlerFunc(v1Ctrl.HandleLabelsCreate(), userMW...)) r.Post("/labels", chain.ToHandlerFunc(v1Ctrl.HandleLabelsCreate(), userMW...))
r.Get("/labels/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLabelGet(), userMW...)) r.Get("/labels/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLabelGet(), userMW...))
r.Put("/labels/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLabelUpdate(), userMW...)) r.Put("/labels/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLabelUpdate(), userMW...))
r.Delete("/labels/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLabelDelete(), userMW...)) r.Delete("/labels/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLabelDelete(), userMW...))
// Item endpoints
r.Get("/items", chain.ToHandlerFunc(v1Ctrl.HandleItemsGetAll(), userMW...)) r.Get("/items", chain.ToHandlerFunc(v1Ctrl.HandleItemsGetAll(), userMW...))
r.Post("/items", chain.ToHandlerFunc(v1Ctrl.HandleItemsCreate(), userMW...)) r.Post("/items", chain.ToHandlerFunc(v1Ctrl.HandleItemsCreate(), userMW...))
r.Post("/items/import", chain.ToHandlerFunc(v1Ctrl.HandleItemsImport(), userMW...)) r.Post("/items/import", chain.ToHandlerFunc(v1Ctrl.HandleItemsImport(), userMW...))
@@ -137,10 +152,12 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
r.Delete("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemDelete(), userMW...)) r.Delete("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemDelete(), userMW...))
r.Post("/items/{id}/duplicate", chain.ToHandlerFunc(v1Ctrl.HandleItemDuplicate(), userMW...)) r.Post("/items/{id}/duplicate", chain.ToHandlerFunc(v1Ctrl.HandleItemDuplicate(), userMW...))
// Item attachment endpoints
r.Post("/items/{id}/attachments", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentCreate(), userMW...)) r.Post("/items/{id}/attachments", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentCreate(), userMW...))
r.Put("/items/{id}/attachments/{attachment_id}", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentUpdate(), userMW...)) r.Put("/items/{id}/attachments/{attachment_id}", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentUpdate(), userMW...))
r.Delete("/items/{id}/attachments/{attachment_id}", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentDelete(), userMW...)) r.Delete("/items/{id}/attachments/{attachment_id}", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentDelete(), userMW...))
// Item maintenance endpoints
r.Get("/items/{id}/maintenance", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceLogGet(), userMW...)) r.Get("/items/{id}/maintenance", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceLogGet(), userMW...))
r.Post("/items/{id}/maintenance", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryCreate(), userMW...)) r.Post("/items/{id}/maintenance", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryCreate(), userMW...))

View File

@@ -243,15 +243,18 @@ const docTemplate = `{
"tags": [ "tags": [
"Group" "Group"
], ],
"summary": "Get Group", "summary": "Get All Groups",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.Group" "$ref": "#/definitions/repo.Group"
} }
} }
} }
}
}, },
"put": { "put": {
"security": [ "security": [
@@ -288,6 +291,31 @@ const docTemplate = `{
} }
}, },
"/v1/groups/invitations": { "/v1/groups/invitations": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Group"
],
"summary": "Get All Group Invitations",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.GroupInvitation"
}
}
}
}
},
"post": { "post": {
"security": [ "security": [
{ {
@@ -438,6 +466,147 @@ const docTemplate = `{
} }
} }
}, },
"/v1/groups/{id}": {
"post": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Group"
],
"summary": "Create Group",
"parameters": [
{
"description": "Group Name",
"name": "name",
"in": "body",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/repo.Group"
}
}
}
},
"delete": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Group"
],
"summary": "Delete Group",
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/v1/groups/{id}/members": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Group"
],
"summary": "Get All Group Members",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.UserOut"
}
}
}
}
},
"post": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Group"
],
"summary": "Add User to Group",
"parameters": [
{
"description": "User ID",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1.GroupMemberAdd"
}
}
],
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/v1/groups/{id}/members/{user_id}": {
"delete": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Group"
],
"summary": "Remove User from Group",
"parameters": [
{
"type": "string",
"description": "User ID",
"name": "user_id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/v1/items": { "/v1/items": {
"get": { "get": {
"security": [ "security": [
@@ -3529,6 +3698,10 @@ const docTemplate = `{
"description": "CreatedAt holds the value of the \"created_at\" field.", "description": "CreatedAt holds the value of the \"created_at\" field.",
"type": "string" "type": "string"
}, },
"default_group_id": {
"description": "DefaultGroupID holds the value of the \"default_group_id\" field.",
"type": "string"
},
"edges": { "edges": {
"description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the UserQuery when eager-loading is set.", "description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the UserQuery when eager-loading is set.",
"allOf": [ "allOf": [
@@ -3589,13 +3762,12 @@ const docTemplate = `{
"$ref": "#/definitions/ent.AuthTokens" "$ref": "#/definitions/ent.AuthTokens"
} }
}, },
"group": { "groups": {
"description": "Group holds the value of the group edge.", "description": "Groups holds the value of the groups edge.",
"allOf": [ "type": "array",
{ "items": {
"$ref": "#/definitions/ent.Group" "$ref": "#/definitions/ent.Group"
} }
]
}, },
"notifiers": { "notifiers": {
"description": "Notifiers holds the value of the notifiers edge.", "description": "Notifiers holds the value of the notifiers edge.",
@@ -3689,6 +3861,23 @@ const docTemplate = `{
} }
} }
}, },
"repo.GroupInvitation": {
"type": "object",
"properties": {
"expiresAt": {
"type": "string"
},
"group": {
"$ref": "#/definitions/repo.Group"
},
"id": {
"type": "string"
},
"uses": {
"type": "integer"
}
}
},
"repo.GroupStatistics": { "repo.GroupStatistics": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -4906,14 +5095,17 @@ const docTemplate = `{
"repo.UserOut": { "repo.UserOut": {
"type": "object", "type": "object",
"properties": { "properties": {
"defaultGroupId": {
"type": "string"
},
"email": { "email": {
"type": "string" "type": "string"
}, },
"groupId": { "groupIds": {
"type": "string" "type": "array",
}, "items": {
"groupName": {
"type": "string" "type": "string"
}
}, },
"id": { "id": {
"type": "string" "type": "string"
@@ -5134,6 +5326,17 @@ const docTemplate = `{
} }
} }
}, },
"v1.GroupMemberAdd": {
"type": "object",
"required": [
"userId"
],
"properties": {
"userId": {
"type": "string"
}
}
},
"v1.ItemAttachmentToken": { "v1.ItemAttachmentToken": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -242,19 +242,22 @@
"tags": [ "tags": [
"Group" "Group"
], ],
"summary": "Get Group", "summary": "Get All Groups",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/repo.Group" "$ref": "#/components/schemas/repo.Group"
} }
} }
} }
} }
} }
}
}, },
"put": { "put": {
"security": [ "security": [
@@ -292,6 +295,32 @@
} }
}, },
"/v1/groups/invitations": { "/v1/groups/invitations": {
"get": {
"security": [
{
"Bearer": []
}
],
"tags": [
"Group"
],
"summary": "Get All Group Invitations",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/repo.GroupInvitation"
}
}
}
}
}
}
},
"post": { "post": {
"security": [ "security": [
{ {
@@ -451,6 +480,142 @@
} }
} }
}, },
"/v1/groups/{id}": {
"post": {
"security": [
{
"Bearer": []
}
],
"tags": [
"Group"
],
"summary": "Create Group",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "Group Name",
"required": true
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/repo.Group"
}
}
}
}
}
},
"delete": {
"security": [
{
"Bearer": []
}
],
"tags": [
"Group"
],
"summary": "Delete Group",
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/v1/groups/{id}/members": {
"get": {
"security": [
{
"Bearer": []
}
],
"tags": [
"Group"
],
"summary": "Get All Group Members",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/repo.UserOut"
}
}
}
}
}
}
},
"post": {
"security": [
{
"Bearer": []
}
],
"tags": [
"Group"
],
"summary": "Add User to Group",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/v1.GroupMemberAdd"
}
}
},
"description": "User ID",
"required": true
},
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/v1/groups/{id}/members/{user_id}": {
"delete": {
"security": [
{
"Bearer": []
}
],
"tags": [
"Group"
],
"summary": "Remove User from Group",
"parameters": [
{
"description": "User ID",
"name": "user_id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/v1/items": { "/v1/items": {
"get": { "get": {
"security": [ "security": [
@@ -3727,6 +3892,10 @@
"description": "CreatedAt holds the value of the \"created_at\" field.", "description": "CreatedAt holds the value of the \"created_at\" field.",
"type": "string" "type": "string"
}, },
"default_group_id": {
"description": "DefaultGroupID holds the value of the \"default_group_id\" field.",
"type": "string"
},
"edges": { "edges": {
"description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the UserQuery when eager-loading is set.", "description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the UserQuery when eager-loading is set.",
"allOf": [ "allOf": [
@@ -3787,13 +3956,12 @@
"$ref": "#/components/schemas/ent.AuthTokens" "$ref": "#/components/schemas/ent.AuthTokens"
} }
}, },
"group": { "groups": {
"description": "Group holds the value of the group edge.", "description": "Groups holds the value of the groups edge.",
"allOf": [ "type": "array",
{ "items": {
"$ref": "#/components/schemas/ent.Group" "$ref": "#/components/schemas/ent.Group"
} }
]
}, },
"notifiers": { "notifiers": {
"description": "Notifiers holds the value of the notifiers edge.", "description": "Notifiers holds the value of the notifiers edge.",
@@ -3887,6 +4055,23 @@
} }
} }
}, },
"repo.GroupInvitation": {
"type": "object",
"properties": {
"expiresAt": {
"type": "string"
},
"group": {
"$ref": "#/components/schemas/repo.Group"
},
"id": {
"type": "string"
},
"uses": {
"type": "integer"
}
}
},
"repo.GroupStatistics": { "repo.GroupStatistics": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -5104,14 +5289,17 @@
"repo.UserOut": { "repo.UserOut": {
"type": "object", "type": "object",
"properties": { "properties": {
"defaultGroupId": {
"type": "string"
},
"email": { "email": {
"type": "string" "type": "string"
}, },
"groupId": { "groupIds": {
"type": "string" "type": "array",
}, "items": {
"groupName": {
"type": "string" "type": "string"
}
}, },
"id": { "id": {
"type": "string" "type": "string"
@@ -5332,6 +5520,17 @@
} }
} }
}, },
"v1.GroupMemberAdd": {
"type": "object",
"required": [
"userId"
],
"properties": {
"userId": {
"type": "string"
}
}
},
"v1.ItemAttachmentToken": { "v1.ItemAttachmentToken": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -142,13 +142,15 @@ paths:
- Bearer: [] - Bearer: []
tags: tags:
- Group - Group
summary: Get Group summary: Get All Groups
responses: responses:
"200": "200":
description: OK description: OK
content: content:
application/json: application/json:
schema: schema:
type: array
items:
$ref: "#/components/schemas/repo.Group" $ref: "#/components/schemas/repo.Group"
put: put:
security: security:
@@ -171,6 +173,21 @@ paths:
schema: schema:
$ref: "#/components/schemas/repo.Group" $ref: "#/components/schemas/repo.Group"
/v1/groups/invitations: /v1/groups/invitations:
get:
security:
- Bearer: []
tags:
- Group
summary: Get All Group Invitations
responses:
"200":
description: OK
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/repo.GroupInvitation"
post: post:
security: security:
- Bearer: [] - Bearer: []
@@ -262,6 +279,85 @@ paths:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/repo.ValueOverTime" $ref: "#/components/schemas/repo.ValueOverTime"
"/v1/groups/{id}":
post:
security:
- Bearer: []
tags:
- Group
summary: Create Group
requestBody:
content:
application/json:
schema:
type: string
description: Group Name
required: true
responses:
"201":
description: Created
content:
application/json:
schema:
$ref: "#/components/schemas/repo.Group"
delete:
security:
- Bearer: []
tags:
- Group
summary: Delete Group
responses:
"204":
description: No Content
"/v1/groups/{id}/members":
get:
security:
- Bearer: []
tags:
- Group
summary: Get All Group Members
responses:
"200":
description: OK
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/repo.UserOut"
post:
security:
- Bearer: []
tags:
- Group
summary: Add User to Group
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/v1.GroupMemberAdd"
description: User ID
required: true
responses:
"204":
description: No Content
"/v1/groups/{id}/members/{user_id}":
delete:
security:
- Bearer: []
tags:
- Group
summary: Remove User from Group
parameters:
- description: User ID
name: user_id
in: path
required: true
schema:
type: string
responses:
"204":
description: No Content
/v1/items: /v1/items:
get: get:
security: security:
@@ -2322,6 +2418,9 @@ components:
created_at: created_at:
description: CreatedAt holds the value of the "created_at" field. description: CreatedAt holds the value of the "created_at" field.
type: string type: string
default_group_id:
description: DefaultGroupID holds the value of the "default_group_id" field.
type: string
edges: edges:
description: >- description: >-
Edges holds the relations/edges for other nodes in the graph. Edges holds the relations/edges for other nodes in the graph.
@@ -2365,10 +2464,11 @@ components:
type: array type: array
items: items:
$ref: "#/components/schemas/ent.AuthTokens" $ref: "#/components/schemas/ent.AuthTokens"
group: groups:
description: Group holds the value of the group edge. description: Groups holds the value of the groups edge.
allOf: type: array
- $ref: "#/components/schemas/ent.Group" items:
$ref: "#/components/schemas/ent.Group"
notifiers: notifiers:
description: Notifiers holds the value of the notifiers edge. description: Notifiers holds the value of the notifiers edge.
type: array type: array
@@ -2431,6 +2531,17 @@ components:
type: string type: string
updatedAt: updatedAt:
type: string type: string
repo.GroupInvitation:
type: object
properties:
expiresAt:
type: string
group:
$ref: "#/components/schemas/repo.Group"
id:
type: string
uses:
type: integer
repo.GroupStatistics: repo.GroupStatistics:
type: object type: object
properties: properties:
@@ -3264,11 +3375,13 @@ components:
repo.UserOut: repo.UserOut:
type: object type: object
properties: properties:
defaultGroupId:
type: string
email: email:
type: string type: string
groupId: groupIds:
type: string type: array
groupName: items:
type: string type: string
id: id:
type: string type: string
@@ -3413,6 +3526,13 @@ components:
type: integer type: integer
maximum: 100 maximum: 100
minimum: 1 minimum: 1
v1.GroupMemberAdd:
type: object
required:
- userId
properties:
userId:
type: string
v1.ItemAttachmentToken: v1.ItemAttachmentToken:
type: object type: object
properties: properties:

View File

@@ -241,15 +241,18 @@
"tags": [ "tags": [
"Group" "Group"
], ],
"summary": "Get Group", "summary": "Get All Groups",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.Group" "$ref": "#/definitions/repo.Group"
} }
} }
} }
}
}, },
"put": { "put": {
"security": [ "security": [
@@ -286,6 +289,31 @@
} }
}, },
"/v1/groups/invitations": { "/v1/groups/invitations": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Group"
],
"summary": "Get All Group Invitations",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.GroupInvitation"
}
}
}
}
},
"post": { "post": {
"security": [ "security": [
{ {
@@ -436,6 +464,147 @@
} }
} }
}, },
"/v1/groups/{id}": {
"post": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Group"
],
"summary": "Create Group",
"parameters": [
{
"description": "Group Name",
"name": "name",
"in": "body",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/repo.Group"
}
}
}
},
"delete": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Group"
],
"summary": "Delete Group",
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/v1/groups/{id}/members": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Group"
],
"summary": "Get All Group Members",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.UserOut"
}
}
}
}
},
"post": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Group"
],
"summary": "Add User to Group",
"parameters": [
{
"description": "User ID",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1.GroupMemberAdd"
}
}
],
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/v1/groups/{id}/members/{user_id}": {
"delete": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Group"
],
"summary": "Remove User from Group",
"parameters": [
{
"type": "string",
"description": "User ID",
"name": "user_id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/v1/items": { "/v1/items": {
"get": { "get": {
"security": [ "security": [
@@ -3527,6 +3696,10 @@
"description": "CreatedAt holds the value of the \"created_at\" field.", "description": "CreatedAt holds the value of the \"created_at\" field.",
"type": "string" "type": "string"
}, },
"default_group_id": {
"description": "DefaultGroupID holds the value of the \"default_group_id\" field.",
"type": "string"
},
"edges": { "edges": {
"description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the UserQuery when eager-loading is set.", "description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the UserQuery when eager-loading is set.",
"allOf": [ "allOf": [
@@ -3587,13 +3760,12 @@
"$ref": "#/definitions/ent.AuthTokens" "$ref": "#/definitions/ent.AuthTokens"
} }
}, },
"group": { "groups": {
"description": "Group holds the value of the group edge.", "description": "Groups holds the value of the groups edge.",
"allOf": [ "type": "array",
{ "items": {
"$ref": "#/definitions/ent.Group" "$ref": "#/definitions/ent.Group"
} }
]
}, },
"notifiers": { "notifiers": {
"description": "Notifiers holds the value of the notifiers edge.", "description": "Notifiers holds the value of the notifiers edge.",
@@ -3687,6 +3859,23 @@
} }
} }
}, },
"repo.GroupInvitation": {
"type": "object",
"properties": {
"expiresAt": {
"type": "string"
},
"group": {
"$ref": "#/definitions/repo.Group"
},
"id": {
"type": "string"
},
"uses": {
"type": "integer"
}
}
},
"repo.GroupStatistics": { "repo.GroupStatistics": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -4904,14 +5093,17 @@
"repo.UserOut": { "repo.UserOut": {
"type": "object", "type": "object",
"properties": { "properties": {
"defaultGroupId": {
"type": "string"
},
"email": { "email": {
"type": "string" "type": "string"
}, },
"groupId": { "groupIds": {
"type": "string" "type": "array",
}, "items": {
"groupName": {
"type": "string" "type": "string"
}
}, },
"id": { "id": {
"type": "string" "type": "string"
@@ -5132,6 +5324,17 @@
} }
} }
}, },
"v1.GroupMemberAdd": {
"type": "object",
"required": [
"userId"
],
"properties": {
"userId": {
"type": "string"
}
}
},
"v1.ItemAttachmentToken": { "v1.ItemAttachmentToken": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -719,6 +719,9 @@ definitions:
created_at: created_at:
description: CreatedAt holds the value of the "created_at" field. description: CreatedAt holds the value of the "created_at" field.
type: string type: string
default_group_id:
description: DefaultGroupID holds the value of the "default_group_id" field.
type: string
edges: edges:
allOf: allOf:
- $ref: '#/definitions/ent.UserEdges' - $ref: '#/definitions/ent.UserEdges'
@@ -761,10 +764,11 @@ definitions:
items: items:
$ref: '#/definitions/ent.AuthTokens' $ref: '#/definitions/ent.AuthTokens'
type: array type: array
group: groups:
allOf: description: Groups holds the value of the groups edge.
- $ref: '#/definitions/ent.Group' items:
description: Group holds the value of the group edge. $ref: '#/definitions/ent.Group'
type: array
notifiers: notifiers:
description: Notifiers holds the value of the notifiers edge. description: Notifiers holds the value of the notifiers edge.
items: items:
@@ -828,6 +832,17 @@ definitions:
updatedAt: updatedAt:
type: string type: string
type: object type: object
repo.GroupInvitation:
properties:
expiresAt:
type: string
group:
$ref: '#/definitions/repo.Group'
id:
type: string
uses:
type: integer
type: object
repo.GroupStatistics: repo.GroupStatistics:
properties: properties:
totalItemPrice: totalItemPrice:
@@ -1660,12 +1675,14 @@ definitions:
type: object type: object
repo.UserOut: repo.UserOut:
properties: properties:
defaultGroupId:
type: string
email: email:
type: string type: string
groupId: groupIds:
type: string items:
groupName:
type: string type: string
type: array
id: id:
type: string type: string
isOwner: isOwner:
@@ -1810,6 +1827,13 @@ definitions:
required: required:
- uses - uses
type: object type: object
v1.GroupMemberAdd:
properties:
userId:
type: string
required:
- userId
type: object
v1.ItemAttachmentToken: v1.ItemAttachmentToken:
properties: properties:
token: token:
@@ -2032,10 +2056,12 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
items:
$ref: '#/definitions/repo.Group' $ref: '#/definitions/repo.Group'
type: array
security: security:
- Bearer: [] - Bearer: []
summary: Get Group summary: Get All Groups
tags: tags:
- Group - Group
put: put:
@@ -2058,7 +2084,106 @@ paths:
summary: Update Group summary: Update Group
tags: tags:
- Group - Group
/v1/groups/{id}:
delete:
produces:
- application/json
responses:
"204":
description: No Content
security:
- Bearer: []
summary: Delete Group
tags:
- Group
post:
parameters:
- description: Group Name
in: body
name: name
required: true
schema:
type: string
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/repo.Group'
security:
- Bearer: []
summary: Create Group
tags:
- Group
/v1/groups/{id}/members:
get:
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/repo.UserOut'
type: array
security:
- Bearer: []
summary: Get All Group Members
tags:
- Group
post:
parameters:
- description: User ID
in: body
name: payload
required: true
schema:
$ref: '#/definitions/v1.GroupMemberAdd'
produces:
- application/json
responses:
"204":
description: No Content
security:
- Bearer: []
summary: Add User to Group
tags:
- Group
/v1/groups/{id}/members/{user_id}:
delete:
parameters:
- description: User ID
in: path
name: user_id
required: true
type: string
produces:
- application/json
responses:
"204":
description: No Content
security:
- Bearer: []
summary: Remove User from Group
tags:
- Group
/v1/groups/invitations: /v1/groups/invitations:
get:
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/repo.GroupInvitation'
type: array
security:
- Bearer: []
summary: Get All Group Invitations
tags:
- Group
post: post:
parameters: parameters:
- description: User Data - description: User Data

View File

@@ -325,6 +325,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
@@ -347,6 +349,8 @@ github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOF
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/olahol/melody v1.4.0 h1:Pa5SdeZL/zXPi1tJuMAPDbl4n3gQOThSL6G1p4qZ4SI= github.com/olahol/melody v1.4.0 h1:Pa5SdeZL/zXPi1tJuMAPDbl4n3gQOThSL6G1p4qZ4SI=
github.com/olahol/melody v1.4.0/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4= github.com/olahol/melody v1.4.0/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU= github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts= github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
@@ -389,6 +393,10 @@ github.com/shirou/gopsutil/v4 v4.25.11 h1:X53gB7muL9Gnwwo2evPSE+SfOrltMoR6V3xJAX
github.com/shirou/gopsutil/v4 v4.25.11/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU= github.com/shirou/gopsutil/v4 v4.25.11/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

View File

@@ -14,6 +14,7 @@ type contextKeys struct {
var ( var (
ContextUser = &contextKeys{name: "User"} ContextUser = &contextKeys{name: "User"}
ContextUserToken = &contextKeys{name: "UserToken"} ContextUserToken = &contextKeys{name: "UserToken"}
ContextTenant = &contextKeys{name: "Tenant"}
) )
type Context struct { type Context struct {
@@ -33,10 +34,14 @@ type Context struct {
// This extracts the users from the context and embeds it into the ServiceContext struct // This extracts the users from the context and embeds it into the ServiceContext struct
func NewContext(ctx context.Context) Context { func NewContext(ctx context.Context) Context {
user := UseUserCtx(ctx) user := UseUserCtx(ctx)
gid := UseTenantCtx(ctx)
if gid == uuid.Nil && user != nil {
gid = user.DefaultGroupID
}
return Context{ return Context{
Context: ctx, Context: ctx,
UID: user.ID, UID: user.ID,
GID: user.GroupID, GID: gid,
User: user, User: user,
} }
} }
@@ -64,3 +69,17 @@ func UseTokenCtx(ctx context.Context) string {
} }
return "" return ""
} }
// UseTenantCtx is a helper function that returns the tenant group ID from the context.
// Returns uuid.Nil if not set.
func UseTenantCtx(ctx context.Context) uuid.UUID {
if val := ctx.Value(ContextTenant); val != nil {
return val.(uuid.UUID)
}
return uuid.Nil
}
// SetTenantCtx is a helper function that sets the ContextTenant in the context.
func SetTenantCtx(ctx context.Context, tenantID uuid.UUID) context.Context {
return context.WithValue(ctx, ContextTenant, tenantID)
}

View File

@@ -2,11 +2,13 @@ package services
import ( import (
"context" "context"
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
"log" "log"
"os" "os"
"testing" "testing"
"github.com/google/uuid"
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"github.com/sysadminsmedia/homebox/backend/internal/core/currencies" "github.com/sysadminsmedia/homebox/backend/internal/core/currencies"
"github.com/sysadminsmedia/homebox/backend/internal/core/services/reporting/eventbus" "github.com/sysadminsmedia/homebox/backend/internal/core/services/reporting/eventbus"
@@ -33,7 +35,7 @@ func bootstrap() {
ctx = context.Background() ctx = context.Background()
) )
tGroup, err = tRepos.Groups.GroupCreate(ctx, "test-group") tGroup, err = tRepos.Groups.GroupCreate(ctx, "test-group", uuid.Nil)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -44,7 +46,7 @@ func bootstrap() {
Email: fk.Email(), Email: fk.Email(),
Password: &password, Password: &password,
IsSuperuser: fk.Bool(), IsSuperuser: fk.Bool(),
GroupID: tGroup.ID, DefaultGroupID: tGroup.ID,
}) })
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

View File

@@ -4,6 +4,7 @@ import (
"errors" "errors"
"time" "time"
"github.com/google/uuid"
"github.com/sysadminsmedia/homebox/backend/internal/data/repo" "github.com/sysadminsmedia/homebox/backend/internal/data/repo"
"github.com/sysadminsmedia/homebox/backend/pkgs/hasher" "github.com/sysadminsmedia/homebox/backend/pkgs/hasher"
) )
@@ -14,7 +15,7 @@ type GroupService struct {
func (svc *GroupService) UpdateGroup(ctx Context, data repo.GroupUpdate) (repo.Group, error) { func (svc *GroupService) UpdateGroup(ctx Context, data repo.GroupUpdate) (repo.Group, error) {
if data.Name == "" { if data.Name == "" {
data.Name = ctx.User.GroupName return repo.Group{}, errors.New("group name cannot be empty")
} }
if data.Currency == "" { if data.Currency == "" {
@@ -24,6 +25,18 @@ func (svc *GroupService) UpdateGroup(ctx Context, data repo.GroupUpdate) (repo.G
return svc.repos.Groups.GroupUpdate(ctx.Context, ctx.GID, data) return svc.repos.Groups.GroupUpdate(ctx.Context, ctx.GID, data)
} }
func (svc *GroupService) CreateGroup(ctx Context, name string) (repo.Group, error) {
if name == "" {
return repo.Group{}, errors.New("group name cannot be empty")
}
return svc.repos.Groups.GroupCreate(ctx.Context, name, ctx.UID)
}
func (svc *GroupService) DeleteGroup(ctx Context) error {
return svc.repos.Groups.GroupDelete(ctx.Context, ctx.GID)
}
func (svc *GroupService) NewInvitation(ctx Context, uses int, expiresAt time.Time) (string, error) { func (svc *GroupService) NewInvitation(ctx Context, uses int, expiresAt time.Time) (string, error) {
token := hasher.GenerateToken() token := hasher.GenerateToken()
@@ -38,3 +51,19 @@ func (svc *GroupService) NewInvitation(ctx Context, uses int, expiresAt time.Tim
return token.Raw, nil return token.Raw, nil
} }
func (svc *GroupService) AddMember(ctx Context, userID uuid.UUID) error {
if userID == uuid.Nil {
return errors.New("user ID cannot be empty")
}
return svc.repos.Groups.AddMember(ctx.Context, ctx.GID, userID)
}
func (svc *GroupService) RemoveMember(ctx Context, userID uuid.UUID) error {
if userID == uuid.Nil {
return errors.New("user ID cannot be empty")
}
return svc.repos.Groups.RemoveMember(ctx.Context, ctx.GID, userID)
}

View File

@@ -64,7 +64,7 @@ func (svc *UserService) RegisterUser(ctx context.Context, data UserRegistration)
case "": case "":
log.Debug().Msg("creating new group") log.Debug().Msg("creating new group")
creatingGroup = true creatingGroup = true
group, err = svc.repos.Groups.GroupCreate(ctx, "Home") group, err = svc.repos.Groups.GroupCreate(ctx, "Home", uuid.Nil)
if err != nil { if err != nil {
log.Err(err).Msg("Failed to create group") log.Err(err).Msg("Failed to create group")
return repo.UserOut{}, err return repo.UserOut{}, err
@@ -85,7 +85,7 @@ func (svc *UserService) RegisterUser(ctx context.Context, data UserRegistration)
Email: data.Email, Email: data.Email,
Password: &hashed, Password: &hashed,
IsSuperuser: false, IsSuperuser: false,
GroupID: group.ID, DefaultGroupID: group.ID,
IsOwner: creatingGroup, IsOwner: creatingGroup,
} }
@@ -99,7 +99,7 @@ func (svc *UserService) RegisterUser(ctx context.Context, data UserRegistration)
if creatingGroup { if creatingGroup {
log.Debug().Msg("creating default labels") log.Debug().Msg("creating default labels")
for _, label := range defaultLabels() { for _, label := range defaultLabels() {
_, err := svc.repos.Labels.Create(ctx, usr.GroupID, label) _, err := svc.repos.Labels.Create(ctx, usr.DefaultGroupID, label)
if err != nil { if err != nil {
return repo.UserOut{}, err return repo.UserOut{}, err
} }
@@ -107,7 +107,7 @@ func (svc *UserService) RegisterUser(ctx context.Context, data UserRegistration)
log.Debug().Msg("creating default locations") log.Debug().Msg("creating default locations")
for _, location := range defaultLocations() { for _, location := range defaultLocations() {
_, err := svc.repos.Locations.Create(ctx, usr.GroupID, location) _, err := svc.repos.Locations.Create(ctx, usr.DefaultGroupID, location)
if err != nil { if err != nil {
return repo.UserOut{}, err return repo.UserOut{}, err
} }
@@ -280,7 +280,7 @@ func (svc *UserService) LoginOIDC(ctx context.Context, issuer, subject, email, n
// registerOIDCUser creates a new user for OIDC authentication with issuer+subject identity. // registerOIDCUser creates a new user for OIDC authentication with issuer+subject identity.
func (svc *UserService) registerOIDCUser(ctx context.Context, issuer, subject, email, name string) (repo.UserOut, error) { func (svc *UserService) registerOIDCUser(ctx context.Context, issuer, subject, email, name string) (repo.UserOut, error) {
group, err := svc.repos.Groups.GroupCreate(ctx, "Home") group, err := svc.repos.Groups.GroupCreate(ctx, "Home", uuid.Nil)
if err != nil { if err != nil {
log.Err(err).Msg("Failed to create group for OIDC user") log.Err(err).Msg("Failed to create group for OIDC user")
return repo.UserOut{}, err return repo.UserOut{}, err
@@ -291,7 +291,7 @@ func (svc *UserService) registerOIDCUser(ctx context.Context, issuer, subject, e
Email: email, Email: email,
Password: nil, Password: nil,
IsSuperuser: false, IsSuperuser: false,
GroupID: group.ID, DefaultGroupID: group.ID,
IsOwner: true, IsOwner: true,
} }
@@ -369,3 +369,30 @@ func (svc *UserService) ChangePassword(ctx Context, current string, new string)
return true return true
} }
func (svc *UserService) EnsureUserPassword(ctx context.Context, email, password string) error {
usr, err := svc.repos.Users.GetOneEmailNoEdges(ctx, email)
if err != nil {
return err
}
match := false
if usr.PasswordHash != "" {
match, _ = hasher.CheckPasswordHash(password, usr.PasswordHash)
}
if !match {
hash, herr := hasher.HashPassword(password)
if herr != nil {
return herr
}
if cerr := svc.repos.Users.ChangePassword(ctx, usr.ID, hash); cerr != nil {
return cerr
}
}
return nil
}
// ExistsByEmail returns true if a user with the given email exists.
func (svc *UserService) ExistsByEmail(ctx context.Context, email string) bool {
_, err := svc.repos.Users.GetOneEmailNoEdges(ctx, email)
return err == nil
}

View File

@@ -909,7 +909,7 @@ func (c *GroupClient) QueryUsers(_m *Group) *UserQuery {
step := sqlgraph.NewStep( step := sqlgraph.NewStep(
sqlgraph.From(group.Table, group.FieldID, id), sqlgraph.From(group.Table, group.FieldID, id),
sqlgraph.To(user.Table, user.FieldID), sqlgraph.To(user.Table, user.FieldID),
sqlgraph.Edge(sqlgraph.O2M, false, group.UsersTable, group.UsersColumn), sqlgraph.Edge(sqlgraph.M2M, true, group.UsersTable, group.UsersPrimaryKey...),
) )
fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step)
return fromV, nil return fromV, nil
@@ -2711,15 +2711,15 @@ func (c *UserClient) GetX(ctx context.Context, id uuid.UUID) *User {
return obj return obj
} }
// QueryGroup queries the group edge of a User. // QueryGroups queries the groups edge of a User.
func (c *UserClient) QueryGroup(_m *User) *GroupQuery { func (c *UserClient) QueryGroups(_m *User) *GroupQuery {
query := (&GroupClient{config: c.config}).Query() query := (&GroupClient{config: c.config}).Query()
query.path = func(context.Context) (fromV *sql.Selector, _ error) { query.path = func(context.Context) (fromV *sql.Selector, _ error) {
id := _m.ID id := _m.ID
step := sqlgraph.NewStep( step := sqlgraph.NewStep(
sqlgraph.From(user.Table, user.FieldID, id), sqlgraph.From(user.Table, user.FieldID, id),
sqlgraph.To(group.Table, group.FieldID), sqlgraph.To(group.Table, group.FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, user.GroupTable, user.GroupColumn), sqlgraph.Edge(sqlgraph.M2M, false, user.GroupsTable, user.GroupsPrimaryKey...),
) )
fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step)
return fromV, nil return fromV, nil

View File

@@ -39,13 +39,11 @@ const (
EdgeItemTemplates = "item_templates" EdgeItemTemplates = "item_templates"
// Table holds the table name of the group in the database. // Table holds the table name of the group in the database.
Table = "groups" Table = "groups"
// UsersTable is the table that holds the users relation/edge. // UsersTable is the table that holds the users relation/edge. The primary key declared below.
UsersTable = "users" UsersTable = "user_groups"
// UsersInverseTable is the table name for the User entity. // UsersInverseTable is the table name for the User entity.
// It exists in this package in order to avoid circular dependency with the "user" package. // It exists in this package in order to avoid circular dependency with the "user" package.
UsersInverseTable = "users" UsersInverseTable = "users"
// UsersColumn is the table column denoting the users relation/edge.
UsersColumn = "group_users"
// LocationsTable is the table that holds the locations relation/edge. // LocationsTable is the table that holds the locations relation/edge.
LocationsTable = "locations" LocationsTable = "locations"
// LocationsInverseTable is the table name for the Location entity. // LocationsInverseTable is the table name for the Location entity.
@@ -99,6 +97,12 @@ var Columns = []string{
FieldCurrency, FieldCurrency,
} }
var (
// UsersPrimaryKey and UsersColumn2 are the table columns denoting the
// primary key for the users relation (M2M).
UsersPrimaryKey = []string{"user_id", "group_id"}
)
// ValidColumn reports if the column name is valid (part of the table columns). // ValidColumn reports if the column name is valid (part of the table columns).
func ValidColumn(column string) bool { func ValidColumn(column string) bool {
for i := range Columns { for i := range Columns {
@@ -253,7 +257,7 @@ func newUsersStep() *sqlgraph.Step {
return sqlgraph.NewStep( return sqlgraph.NewStep(
sqlgraph.From(Table, FieldID), sqlgraph.From(Table, FieldID),
sqlgraph.To(UsersInverseTable, FieldID), sqlgraph.To(UsersInverseTable, FieldID),
sqlgraph.Edge(sqlgraph.O2M, false, UsersTable, UsersColumn), sqlgraph.Edge(sqlgraph.M2M, true, UsersTable, UsersPrimaryKey...),
) )
} }
func newLocationsStep() *sqlgraph.Step { func newLocationsStep() *sqlgraph.Step {

View File

@@ -291,7 +291,7 @@ func HasUsers() predicate.Group {
return predicate.Group(func(s *sql.Selector) { return predicate.Group(func(s *sql.Selector) {
step := sqlgraph.NewStep( step := sqlgraph.NewStep(
sqlgraph.From(Table, FieldID), sqlgraph.From(Table, FieldID),
sqlgraph.Edge(sqlgraph.O2M, false, UsersTable, UsersColumn), sqlgraph.Edge(sqlgraph.M2M, true, UsersTable, UsersPrimaryKey...),
) )
sqlgraph.HasNeighbors(s, step) sqlgraph.HasNeighbors(s, step)
}) })

View File

@@ -320,10 +320,10 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) {
} }
if nodes := _c.mutation.UsersIDs(); len(nodes) > 0 { if nodes := _c.mutation.UsersIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{ edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M, Rel: sqlgraph.M2M,
Inverse: false, Inverse: true,
Table: group.UsersTable, Table: group.UsersTable,
Columns: []string{group.UsersColumn}, Columns: group.UsersPrimaryKey,
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID),

View File

@@ -88,7 +88,7 @@ func (_q *GroupQuery) QueryUsers() *UserQuery {
step := sqlgraph.NewStep( step := sqlgraph.NewStep(
sqlgraph.From(group.Table, group.FieldID, selector), sqlgraph.From(group.Table, group.FieldID, selector),
sqlgraph.To(user.Table, user.FieldID), sqlgraph.To(user.Table, user.FieldID),
sqlgraph.Edge(sqlgraph.O2M, false, group.UsersTable, group.UsersColumn), sqlgraph.Edge(sqlgraph.M2M, true, group.UsersTable, group.UsersPrimaryKey...),
) )
fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step) fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step)
return fromU, nil return fromU, nil
@@ -671,33 +671,63 @@ func (_q *GroupQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Group,
} }
func (_q *GroupQuery) loadUsers(ctx context.Context, query *UserQuery, nodes []*Group, init func(*Group), assign func(*Group, *User)) error { func (_q *GroupQuery) loadUsers(ctx context.Context, query *UserQuery, nodes []*Group, init func(*Group), assign func(*Group, *User)) error {
fks := make([]driver.Value, 0, len(nodes)) edgeIDs := make([]driver.Value, len(nodes))
nodeids := make(map[uuid.UUID]*Group) byID := make(map[uuid.UUID]*Group)
for i := range nodes { nids := make(map[uuid.UUID]map[*Group]struct{})
fks = append(fks, nodes[i].ID) for i, node := range nodes {
nodeids[nodes[i].ID] = nodes[i] edgeIDs[i] = node.ID
byID[node.ID] = node
if init != nil { if init != nil {
init(nodes[i]) init(node)
} }
} }
query.withFKs = true query.Where(func(s *sql.Selector) {
query.Where(predicate.User(func(s *sql.Selector) { joinT := sql.Table(group.UsersTable)
s.Where(sql.InValues(s.C(group.UsersColumn), fks...)) s.Join(joinT).On(s.C(user.FieldID), joinT.C(group.UsersPrimaryKey[0]))
})) s.Where(sql.InValues(joinT.C(group.UsersPrimaryKey[1]), edgeIDs...))
neighbors, err := query.All(ctx) columns := s.SelectedColumns()
s.Select(joinT.C(group.UsersPrimaryKey[1]))
s.AppendSelect(columns...)
s.SetDistinct(false)
})
if err := query.prepareQuery(ctx); err != nil {
return err
}
qr := QuerierFunc(func(ctx context.Context, q Query) (Value, error) {
return query.sqlAll(ctx, func(_ context.Context, spec *sqlgraph.QuerySpec) {
assign := spec.Assign
values := spec.ScanValues
spec.ScanValues = func(columns []string) ([]any, error) {
values, err := values(columns[1:])
if err != nil {
return nil, err
}
return append([]any{new(uuid.UUID)}, values...), nil
}
spec.Assign = func(columns []string, values []any) error {
outValue := *values[0].(*uuid.UUID)
inValue := *values[1].(*uuid.UUID)
if nids[inValue] == nil {
nids[inValue] = map[*Group]struct{}{byID[outValue]: {}}
return assign(columns[1:], values[1:])
}
nids[inValue][byID[outValue]] = struct{}{}
return nil
}
})
})
neighbors, err := withInterceptors[[]*User](ctx, query, qr, query.inters)
if err != nil { if err != nil {
return err return err
} }
for _, n := range neighbors { for _, n := range neighbors {
fk := n.group_users nodes, ok := nids[n.ID]
if fk == nil {
return fmt.Errorf(`foreign-key "group_users" is nil for node %v`, n.ID)
}
node, ok := nodeids[*fk]
if !ok { if !ok {
return fmt.Errorf(`unexpected referenced foreign-key "group_users" returned %v for node %v`, *fk, n.ID) return fmt.Errorf(`unexpected "users" node returned %v`, n.ID)
}
for kn := range nodes {
assign(kn, n)
} }
assign(node, n)
} }
return nil return nil
} }

View File

@@ -396,10 +396,10 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) {
} }
if _u.mutation.UsersCleared() { if _u.mutation.UsersCleared() {
edge := &sqlgraph.EdgeSpec{ edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M, Rel: sqlgraph.M2M,
Inverse: false, Inverse: true,
Table: group.UsersTable, Table: group.UsersTable,
Columns: []string{group.UsersColumn}, Columns: group.UsersPrimaryKey,
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID),
@@ -409,10 +409,10 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) {
} }
if nodes := _u.mutation.RemovedUsersIDs(); len(nodes) > 0 && !_u.mutation.UsersCleared() { if nodes := _u.mutation.RemovedUsersIDs(); len(nodes) > 0 && !_u.mutation.UsersCleared() {
edge := &sqlgraph.EdgeSpec{ edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M, Rel: sqlgraph.M2M,
Inverse: false, Inverse: true,
Table: group.UsersTable, Table: group.UsersTable,
Columns: []string{group.UsersColumn}, Columns: group.UsersPrimaryKey,
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID),
@@ -425,10 +425,10 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) {
} }
if nodes := _u.mutation.UsersIDs(); len(nodes) > 0 { if nodes := _u.mutation.UsersIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{ edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M, Rel: sqlgraph.M2M,
Inverse: false, Inverse: true,
Table: group.UsersTable, Table: group.UsersTable,
Columns: []string{group.UsersColumn}, Columns: group.UsersPrimaryKey,
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID),
@@ -1119,10 +1119,10 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error)
} }
if _u.mutation.UsersCleared() { if _u.mutation.UsersCleared() {
edge := &sqlgraph.EdgeSpec{ edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M, Rel: sqlgraph.M2M,
Inverse: false, Inverse: true,
Table: group.UsersTable, Table: group.UsersTable,
Columns: []string{group.UsersColumn}, Columns: group.UsersPrimaryKey,
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID),
@@ -1132,10 +1132,10 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error)
} }
if nodes := _u.mutation.RemovedUsersIDs(); len(nodes) > 0 && !_u.mutation.UsersCleared() { if nodes := _u.mutation.RemovedUsersIDs(); len(nodes) > 0 && !_u.mutation.UsersCleared() {
edge := &sqlgraph.EdgeSpec{ edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M, Rel: sqlgraph.M2M,
Inverse: false, Inverse: true,
Table: group.UsersTable, Table: group.UsersTable,
Columns: []string{group.UsersColumn}, Columns: group.UsersPrimaryKey,
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID),
@@ -1148,10 +1148,10 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error)
} }
if nodes := _u.mutation.UsersIDs(); len(nodes) > 0 { if nodes := _u.mutation.UsersIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{ edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M, Rel: sqlgraph.M2M,
Inverse: false, Inverse: true,
Table: group.UsersTable, Table: group.UsersTable,
Columns: []string{group.UsersColumn}, Columns: group.UsersPrimaryKey,
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID),

View File

@@ -468,21 +468,13 @@ var (
{Name: "activated_on", Type: field.TypeTime, Nullable: true}, {Name: "activated_on", Type: field.TypeTime, Nullable: true},
{Name: "oidc_issuer", Type: field.TypeString, Nullable: true}, {Name: "oidc_issuer", Type: field.TypeString, Nullable: true},
{Name: "oidc_subject", Type: field.TypeString, Nullable: true}, {Name: "oidc_subject", Type: field.TypeString, Nullable: true},
{Name: "group_users", Type: field.TypeUUID}, {Name: "default_group_id", Type: field.TypeUUID, Nullable: true},
} }
// UsersTable holds the schema information for the "users" table. // UsersTable holds the schema information for the "users" table.
UsersTable = &schema.Table{ UsersTable = &schema.Table{
Name: "users", Name: "users",
Columns: UsersColumns, Columns: UsersColumns,
PrimaryKey: []*schema.Column{UsersColumns[0]}, PrimaryKey: []*schema.Column{UsersColumns[0]},
ForeignKeys: []*schema.ForeignKey{
{
Symbol: "users_groups_users",
Columns: []*schema.Column{UsersColumns[12]},
RefColumns: []*schema.Column{GroupsColumns[0]},
OnDelete: schema.Cascade,
},
},
Indexes: []*schema.Index{ Indexes: []*schema.Index{
{ {
Name: "user_oidc_issuer_oidc_subject", Name: "user_oidc_issuer_oidc_subject",
@@ -516,6 +508,31 @@ var (
}, },
}, },
} }
// UserGroupsColumns holds the columns for the "user_groups" table.
UserGroupsColumns = []*schema.Column{
{Name: "user_id", Type: field.TypeUUID},
{Name: "group_id", Type: field.TypeUUID},
}
// UserGroupsTable holds the schema information for the "user_groups" table.
UserGroupsTable = &schema.Table{
Name: "user_groups",
Columns: UserGroupsColumns,
PrimaryKey: []*schema.Column{UserGroupsColumns[0], UserGroupsColumns[1]},
ForeignKeys: []*schema.ForeignKey{
{
Symbol: "user_groups_user_id",
Columns: []*schema.Column{UserGroupsColumns[0]},
RefColumns: []*schema.Column{UsersColumns[0]},
OnDelete: schema.Cascade,
},
{
Symbol: "user_groups_group_id",
Columns: []*schema.Column{UserGroupsColumns[1]},
RefColumns: []*schema.Column{GroupsColumns[0]},
OnDelete: schema.Cascade,
},
},
}
// Tables holds all the tables in the schema. // Tables holds all the tables in the schema.
Tables = []*schema.Table{ Tables = []*schema.Table{
AttachmentsTable, AttachmentsTable,
@@ -533,6 +550,7 @@ var (
TemplateFieldsTable, TemplateFieldsTable,
UsersTable, UsersTable,
LabelItemsTable, LabelItemsTable,
UserGroupsTable,
} }
) )
@@ -555,7 +573,8 @@ func init() {
NotifiersTable.ForeignKeys[0].RefTable = GroupsTable NotifiersTable.ForeignKeys[0].RefTable = GroupsTable
NotifiersTable.ForeignKeys[1].RefTable = UsersTable NotifiersTable.ForeignKeys[1].RefTable = UsersTable
TemplateFieldsTable.ForeignKeys[0].RefTable = ItemTemplatesTable TemplateFieldsTable.ForeignKeys[0].RefTable = ItemTemplatesTable
UsersTable.ForeignKeys[0].RefTable = GroupsTable
LabelItemsTable.ForeignKeys[0].RefTable = LabelsTable LabelItemsTable.ForeignKeys[0].RefTable = LabelsTable
LabelItemsTable.ForeignKeys[1].RefTable = ItemsTable LabelItemsTable.ForeignKeys[1].RefTable = ItemsTable
UserGroupsTable.ForeignKeys[0].RefTable = UsersTable
UserGroupsTable.ForeignKeys[1].RefTable = GroupsTable
} }

View File

@@ -12583,9 +12583,11 @@ type UserMutation struct {
activated_on *time.Time activated_on *time.Time
oidc_issuer *string oidc_issuer *string
oidc_subject *string oidc_subject *string
default_group_id *uuid.UUID
clearedFields map[string]struct{} clearedFields map[string]struct{}
group *uuid.UUID groups map[uuid.UUID]struct{}
clearedgroup bool removedgroups map[uuid.UUID]struct{}
clearedgroups bool
auth_tokens map[uuid.UUID]struct{} auth_tokens map[uuid.UUID]struct{}
removedauth_tokens map[uuid.UUID]struct{} removedauth_tokens map[uuid.UUID]struct{}
clearedauth_tokens bool clearedauth_tokens bool
@@ -13149,43 +13151,107 @@ func (m *UserMutation) ResetOidcSubject() {
delete(m.clearedFields, user.FieldOidcSubject) delete(m.clearedFields, user.FieldOidcSubject)
} }
// SetGroupID sets the "group" edge to the Group entity by id. // SetDefaultGroupID sets the "default_group_id" field.
func (m *UserMutation) SetGroupID(id uuid.UUID) { func (m *UserMutation) SetDefaultGroupID(u uuid.UUID) {
m.group = &id m.default_group_id = &u
} }
// ClearGroup clears the "group" edge to the Group entity. // DefaultGroupID returns the value of the "default_group_id" field in the mutation.
func (m *UserMutation) ClearGroup() { func (m *UserMutation) DefaultGroupID() (r uuid.UUID, exists bool) {
m.clearedgroup = true v := m.default_group_id
if v == nil {
return
}
return *v, true
} }
// GroupCleared reports if the "group" edge to the Group entity was cleared. // OldDefaultGroupID returns the old "default_group_id" field's value of the User entity.
func (m *UserMutation) GroupCleared() bool { // If the User object wasn't provided to the builder, the object is fetched from the database.
return m.clearedgroup // An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *UserMutation) OldDefaultGroupID(ctx context.Context) (v *uuid.UUID, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldDefaultGroupID is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldDefaultGroupID requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldDefaultGroupID: %w", err)
}
return oldValue.DefaultGroupID, nil
} }
// GroupID returns the "group" edge ID in the mutation. // ClearDefaultGroupID clears the value of the "default_group_id" field.
func (m *UserMutation) GroupID() (id uuid.UUID, exists bool) { func (m *UserMutation) ClearDefaultGroupID() {
if m.group != nil { m.default_group_id = nil
return *m.group, true m.clearedFields[user.FieldDefaultGroupID] = struct{}{}
}
// DefaultGroupIDCleared returns if the "default_group_id" field was cleared in this mutation.
func (m *UserMutation) DefaultGroupIDCleared() bool {
_, ok := m.clearedFields[user.FieldDefaultGroupID]
return ok
}
// ResetDefaultGroupID resets all changes to the "default_group_id" field.
func (m *UserMutation) ResetDefaultGroupID() {
m.default_group_id = nil
delete(m.clearedFields, user.FieldDefaultGroupID)
}
// AddGroupIDs adds the "groups" edge to the Group entity by ids.
func (m *UserMutation) AddGroupIDs(ids ...uuid.UUID) {
if m.groups == nil {
m.groups = make(map[uuid.UUID]struct{})
}
for i := range ids {
m.groups[ids[i]] = struct{}{}
}
}
// ClearGroups clears the "groups" edge to the Group entity.
func (m *UserMutation) ClearGroups() {
m.clearedgroups = true
}
// GroupsCleared reports if the "groups" edge to the Group entity was cleared.
func (m *UserMutation) GroupsCleared() bool {
return m.clearedgroups
}
// RemoveGroupIDs removes the "groups" edge to the Group entity by IDs.
func (m *UserMutation) RemoveGroupIDs(ids ...uuid.UUID) {
if m.removedgroups == nil {
m.removedgroups = make(map[uuid.UUID]struct{})
}
for i := range ids {
delete(m.groups, ids[i])
m.removedgroups[ids[i]] = struct{}{}
}
}
// RemovedGroups returns the removed IDs of the "groups" edge to the Group entity.
func (m *UserMutation) RemovedGroupsIDs() (ids []uuid.UUID) {
for id := range m.removedgroups {
ids = append(ids, id)
} }
return return
} }
// GroupIDs returns the "group" edge IDs in the mutation. // GroupsIDs returns the "groups" edge IDs in the mutation.
// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use func (m *UserMutation) GroupsIDs() (ids []uuid.UUID) {
// GroupID instead. It exists only for internal usage by the builders. for id := range m.groups {
func (m *UserMutation) GroupIDs() (ids []uuid.UUID) { ids = append(ids, id)
if id := m.group; id != nil {
ids = append(ids, *id)
} }
return return
} }
// ResetGroup resets all changes to the "group" edge. // ResetGroups resets all changes to the "groups" edge.
func (m *UserMutation) ResetGroup() { func (m *UserMutation) ResetGroups() {
m.group = nil m.groups = nil
m.clearedgroup = false m.clearedgroups = false
m.removedgroups = nil
} }
// AddAuthTokenIDs adds the "auth_tokens" edge to the AuthTokens entity by ids. // AddAuthTokenIDs adds the "auth_tokens" edge to the AuthTokens entity by ids.
@@ -13330,7 +13396,7 @@ func (m *UserMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call // order to get all numeric fields that were incremented/decremented, call
// AddedFields(). // AddedFields().
func (m *UserMutation) Fields() []string { func (m *UserMutation) Fields() []string {
fields := make([]string, 0, 11) fields := make([]string, 0, 12)
if m.created_at != nil { if m.created_at != nil {
fields = append(fields, user.FieldCreatedAt) fields = append(fields, user.FieldCreatedAt)
} }
@@ -13364,6 +13430,9 @@ func (m *UserMutation) Fields() []string {
if m.oidc_subject != nil { if m.oidc_subject != nil {
fields = append(fields, user.FieldOidcSubject) fields = append(fields, user.FieldOidcSubject)
} }
if m.default_group_id != nil {
fields = append(fields, user.FieldDefaultGroupID)
}
return fields return fields
} }
@@ -13394,6 +13463,8 @@ func (m *UserMutation) Field(name string) (ent.Value, bool) {
return m.OidcIssuer() return m.OidcIssuer()
case user.FieldOidcSubject: case user.FieldOidcSubject:
return m.OidcSubject() return m.OidcSubject()
case user.FieldDefaultGroupID:
return m.DefaultGroupID()
} }
return nil, false return nil, false
} }
@@ -13425,6 +13496,8 @@ func (m *UserMutation) OldField(ctx context.Context, name string) (ent.Value, er
return m.OldOidcIssuer(ctx) return m.OldOidcIssuer(ctx)
case user.FieldOidcSubject: case user.FieldOidcSubject:
return m.OldOidcSubject(ctx) return m.OldOidcSubject(ctx)
case user.FieldDefaultGroupID:
return m.OldDefaultGroupID(ctx)
} }
return nil, fmt.Errorf("unknown User field %s", name) return nil, fmt.Errorf("unknown User field %s", name)
} }
@@ -13511,6 +13584,13 @@ func (m *UserMutation) SetField(name string, value ent.Value) error {
} }
m.SetOidcSubject(v) m.SetOidcSubject(v)
return nil return nil
case user.FieldDefaultGroupID:
v, ok := value.(uuid.UUID)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetDefaultGroupID(v)
return nil
} }
return fmt.Errorf("unknown User field %s", name) return fmt.Errorf("unknown User field %s", name)
} }
@@ -13553,6 +13633,9 @@ func (m *UserMutation) ClearedFields() []string {
if m.FieldCleared(user.FieldOidcSubject) { if m.FieldCleared(user.FieldOidcSubject) {
fields = append(fields, user.FieldOidcSubject) fields = append(fields, user.FieldOidcSubject)
} }
if m.FieldCleared(user.FieldDefaultGroupID) {
fields = append(fields, user.FieldDefaultGroupID)
}
return fields return fields
} }
@@ -13579,6 +13662,9 @@ func (m *UserMutation) ClearField(name string) error {
case user.FieldOidcSubject: case user.FieldOidcSubject:
m.ClearOidcSubject() m.ClearOidcSubject()
return nil return nil
case user.FieldDefaultGroupID:
m.ClearDefaultGroupID()
return nil
} }
return fmt.Errorf("unknown User nullable field %s", name) return fmt.Errorf("unknown User nullable field %s", name)
} }
@@ -13620,6 +13706,9 @@ func (m *UserMutation) ResetField(name string) error {
case user.FieldOidcSubject: case user.FieldOidcSubject:
m.ResetOidcSubject() m.ResetOidcSubject()
return nil return nil
case user.FieldDefaultGroupID:
m.ResetDefaultGroupID()
return nil
} }
return fmt.Errorf("unknown User field %s", name) return fmt.Errorf("unknown User field %s", name)
} }
@@ -13627,8 +13716,8 @@ func (m *UserMutation) ResetField(name string) error {
// AddedEdges returns all edge names that were set/added in this mutation. // AddedEdges returns all edge names that were set/added in this mutation.
func (m *UserMutation) AddedEdges() []string { func (m *UserMutation) AddedEdges() []string {
edges := make([]string, 0, 3) edges := make([]string, 0, 3)
if m.group != nil { if m.groups != nil {
edges = append(edges, user.EdgeGroup) edges = append(edges, user.EdgeGroups)
} }
if m.auth_tokens != nil { if m.auth_tokens != nil {
edges = append(edges, user.EdgeAuthTokens) edges = append(edges, user.EdgeAuthTokens)
@@ -13643,10 +13732,12 @@ func (m *UserMutation) AddedEdges() []string {
// name in this mutation. // name in this mutation.
func (m *UserMutation) AddedIDs(name string) []ent.Value { func (m *UserMutation) AddedIDs(name string) []ent.Value {
switch name { switch name {
case user.EdgeGroup: case user.EdgeGroups:
if id := m.group; id != nil { ids := make([]ent.Value, 0, len(m.groups))
return []ent.Value{*id} for id := range m.groups {
ids = append(ids, id)
} }
return ids
case user.EdgeAuthTokens: case user.EdgeAuthTokens:
ids := make([]ent.Value, 0, len(m.auth_tokens)) ids := make([]ent.Value, 0, len(m.auth_tokens))
for id := range m.auth_tokens { for id := range m.auth_tokens {
@@ -13666,6 +13757,9 @@ func (m *UserMutation) AddedIDs(name string) []ent.Value {
// RemovedEdges returns all edge names that were removed in this mutation. // RemovedEdges returns all edge names that were removed in this mutation.
func (m *UserMutation) RemovedEdges() []string { func (m *UserMutation) RemovedEdges() []string {
edges := make([]string, 0, 3) edges := make([]string, 0, 3)
if m.removedgroups != nil {
edges = append(edges, user.EdgeGroups)
}
if m.removedauth_tokens != nil { if m.removedauth_tokens != nil {
edges = append(edges, user.EdgeAuthTokens) edges = append(edges, user.EdgeAuthTokens)
} }
@@ -13679,6 +13773,12 @@ func (m *UserMutation) RemovedEdges() []string {
// the given name in this mutation. // the given name in this mutation.
func (m *UserMutation) RemovedIDs(name string) []ent.Value { func (m *UserMutation) RemovedIDs(name string) []ent.Value {
switch name { switch name {
case user.EdgeGroups:
ids := make([]ent.Value, 0, len(m.removedgroups))
for id := range m.removedgroups {
ids = append(ids, id)
}
return ids
case user.EdgeAuthTokens: case user.EdgeAuthTokens:
ids := make([]ent.Value, 0, len(m.removedauth_tokens)) ids := make([]ent.Value, 0, len(m.removedauth_tokens))
for id := range m.removedauth_tokens { for id := range m.removedauth_tokens {
@@ -13698,8 +13798,8 @@ func (m *UserMutation) RemovedIDs(name string) []ent.Value {
// ClearedEdges returns all edge names that were cleared in this mutation. // ClearedEdges returns all edge names that were cleared in this mutation.
func (m *UserMutation) ClearedEdges() []string { func (m *UserMutation) ClearedEdges() []string {
edges := make([]string, 0, 3) edges := make([]string, 0, 3)
if m.clearedgroup { if m.clearedgroups {
edges = append(edges, user.EdgeGroup) edges = append(edges, user.EdgeGroups)
} }
if m.clearedauth_tokens { if m.clearedauth_tokens {
edges = append(edges, user.EdgeAuthTokens) edges = append(edges, user.EdgeAuthTokens)
@@ -13714,8 +13814,8 @@ func (m *UserMutation) ClearedEdges() []string {
// was cleared in this mutation. // was cleared in this mutation.
func (m *UserMutation) EdgeCleared(name string) bool { func (m *UserMutation) EdgeCleared(name string) bool {
switch name { switch name {
case user.EdgeGroup: case user.EdgeGroups:
return m.clearedgroup return m.clearedgroups
case user.EdgeAuthTokens: case user.EdgeAuthTokens:
return m.clearedauth_tokens return m.clearedauth_tokens
case user.EdgeNotifiers: case user.EdgeNotifiers:
@@ -13728,9 +13828,6 @@ func (m *UserMutation) EdgeCleared(name string) bool {
// if that edge is not defined in the schema. // if that edge is not defined in the schema.
func (m *UserMutation) ClearEdge(name string) error { func (m *UserMutation) ClearEdge(name string) error {
switch name { switch name {
case user.EdgeGroup:
m.ClearGroup()
return nil
} }
return fmt.Errorf("unknown User unique edge %s", name) return fmt.Errorf("unknown User unique edge %s", name)
} }
@@ -13739,8 +13836,8 @@ func (m *UserMutation) ClearEdge(name string) error {
// It returns an error if the edge is not defined in the schema. // It returns an error if the edge is not defined in the schema.
func (m *UserMutation) ResetEdge(name string) error { func (m *UserMutation) ResetEdge(name string) error {
switch name { switch name {
case user.EdgeGroup: case user.EdgeGroups:
m.ResetGroup() m.ResetGroups()
return nil return nil
case user.EdgeAuthTokens: case user.EdgeAuthTokens:
m.ResetAuthTokens() m.ResetAuthTokens()

View File

@@ -42,7 +42,8 @@ func (Group) Edges() []ent.Edge {
} }
return []ent.Edge{ return []ent.Edge{
owned("users", User.Type), // Use edge.From + Ref("groups") to model M:M between users and groups via junction table
edge.From("users", User.Type).Ref("groups"),
owned("locations", Location.Type), owned("locations", Location.Type),
owned("items", Item.Type), owned("items", Item.Type),
owned("labels", Label.Type), owned("labels", Label.Type),
@@ -72,14 +73,14 @@ func (g GroupMixin) Fields() []ent.Field {
} }
func (g GroupMixin) Edges() []ent.Edge { func (g GroupMixin) Edges() []ent.Edge {
edge := edge.From("group", Group.Type). e := edge.From("group", Group.Type).
Ref(g.ref). Ref(g.ref).
Unique(). Unique().
Required() Required()
if g.field != "" { if g.field != "" {
edge = edge.Field(g.field) e = e.Field(g.field)
} }
return []ent.Edge{edge} return []ent.Edge{e}
} }

View File

@@ -19,7 +19,6 @@ type User struct {
func (User) Mixin() []ent.Mixin { func (User) Mixin() []ent.Mixin {
return []ent.Mixin{ return []ent.Mixin{
mixins.BaseMixin{}, mixins.BaseMixin{},
GroupMixin{ref: "users"},
} }
} }
@@ -54,6 +53,10 @@ func (User) Fields() []ent.Field {
field.String("oidc_subject"). field.String("oidc_subject").
Optional(). Optional().
Nillable(), Nillable(),
// default_group_id is the user's primary tenant/group
field.UUID("default_group_id", uuid.UUID{}).
Optional().
Nillable(),
} }
} }
@@ -66,6 +69,7 @@ func (User) Indexes() []ent.Index {
// Edges of the User. // Edges of the User.
func (User) Edges() []ent.Edge { func (User) Edges() []ent.Edge {
return []ent.Edge{ return []ent.Edge{
edge.To("groups", Group.Type),
edge.To("auth_tokens", AuthTokens.Type). edge.To("auth_tokens", AuthTokens.Type).
Annotations(entsql.Annotation{ Annotations(entsql.Annotation{
OnDelete: entsql.Cascade, OnDelete: entsql.Cascade,

View File

@@ -10,7 +10,6 @@ import (
"entgo.io/ent" "entgo.io/ent"
"entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/group"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/user" "github.com/sysadminsmedia/homebox/backend/internal/data/ent/user"
) )
@@ -41,17 +40,18 @@ type User struct {
OidcIssuer *string `json:"oidc_issuer,omitempty"` OidcIssuer *string `json:"oidc_issuer,omitempty"`
// OidcSubject holds the value of the "oidc_subject" field. // OidcSubject holds the value of the "oidc_subject" field.
OidcSubject *string `json:"oidc_subject,omitempty"` OidcSubject *string `json:"oidc_subject,omitempty"`
// DefaultGroupID holds the value of the "default_group_id" field.
DefaultGroupID *uuid.UUID `json:"default_group_id,omitempty"`
// Edges holds the relations/edges for other nodes in the graph. // Edges holds the relations/edges for other nodes in the graph.
// The values are being populated by the UserQuery when eager-loading is set. // The values are being populated by the UserQuery when eager-loading is set.
Edges UserEdges `json:"edges"` Edges UserEdges `json:"edges"`
group_users *uuid.UUID
selectValues sql.SelectValues selectValues sql.SelectValues
} }
// UserEdges holds the relations/edges for other nodes in the graph. // UserEdges holds the relations/edges for other nodes in the graph.
type UserEdges struct { type UserEdges struct {
// Group holds the value of the group edge. // Groups holds the value of the groups edge.
Group *Group `json:"group,omitempty"` Groups []*Group `json:"groups,omitempty"`
// AuthTokens holds the value of the auth_tokens edge. // AuthTokens holds the value of the auth_tokens edge.
AuthTokens []*AuthTokens `json:"auth_tokens,omitempty"` AuthTokens []*AuthTokens `json:"auth_tokens,omitempty"`
// Notifiers holds the value of the notifiers edge. // Notifiers holds the value of the notifiers edge.
@@ -61,15 +61,13 @@ type UserEdges struct {
loadedTypes [3]bool loadedTypes [3]bool
} }
// GroupOrErr returns the Group value or an error if the edge // GroupsOrErr returns the Groups value or an error if the edge
// was not loaded in eager-loading, or loaded but was not found. // was not loaded in eager-loading.
func (e UserEdges) GroupOrErr() (*Group, error) { func (e UserEdges) GroupsOrErr() ([]*Group, error) {
if e.Group != nil { if e.loadedTypes[0] {
return e.Group, nil return e.Groups, nil
} else if e.loadedTypes[0] {
return nil, &NotFoundError{label: group.Label}
} }
return nil, &NotLoadedError{edge: "group"} return nil, &NotLoadedError{edge: "groups"}
} }
// AuthTokensOrErr returns the AuthTokens value or an error if the edge // AuthTokensOrErr returns the AuthTokens value or an error if the edge
@@ -95,6 +93,8 @@ func (*User) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns)) values := make([]any, len(columns))
for i := range columns { for i := range columns {
switch columns[i] { switch columns[i] {
case user.FieldDefaultGroupID:
values[i] = &sql.NullScanner{S: new(uuid.UUID)}
case user.FieldIsSuperuser, user.FieldSuperuser: case user.FieldIsSuperuser, user.FieldSuperuser:
values[i] = new(sql.NullBool) values[i] = new(sql.NullBool)
case user.FieldName, user.FieldEmail, user.FieldPassword, user.FieldRole, user.FieldOidcIssuer, user.FieldOidcSubject: case user.FieldName, user.FieldEmail, user.FieldPassword, user.FieldRole, user.FieldOidcIssuer, user.FieldOidcSubject:
@@ -103,8 +103,6 @@ func (*User) scanValues(columns []string) ([]any, error) {
values[i] = new(sql.NullTime) values[i] = new(sql.NullTime)
case user.FieldID: case user.FieldID:
values[i] = new(uuid.UUID) values[i] = new(uuid.UUID)
case user.ForeignKeys[0]: // group_users
values[i] = &sql.NullScanner{S: new(uuid.UUID)}
default: default:
values[i] = new(sql.UnknownType) values[i] = new(sql.UnknownType)
} }
@@ -195,12 +193,12 @@ func (_m *User) assignValues(columns []string, values []any) error {
_m.OidcSubject = new(string) _m.OidcSubject = new(string)
*_m.OidcSubject = value.String *_m.OidcSubject = value.String
} }
case user.ForeignKeys[0]: case user.FieldDefaultGroupID:
if value, ok := values[i].(*sql.NullScanner); !ok { if value, ok := values[i].(*sql.NullScanner); !ok {
return fmt.Errorf("unexpected type %T for field group_users", values[i]) return fmt.Errorf("unexpected type %T for field default_group_id", values[i])
} else if value.Valid { } else if value.Valid {
_m.group_users = new(uuid.UUID) _m.DefaultGroupID = new(uuid.UUID)
*_m.group_users = *value.S.(*uuid.UUID) *_m.DefaultGroupID = *value.S.(*uuid.UUID)
} }
default: default:
_m.selectValues.Set(columns[i], values[i]) _m.selectValues.Set(columns[i], values[i])
@@ -215,9 +213,9 @@ func (_m *User) Value(name string) (ent.Value, error) {
return _m.selectValues.Get(name) return _m.selectValues.Get(name)
} }
// QueryGroup queries the "group" edge of the User entity. // QueryGroups queries the "groups" edge of the User entity.
func (_m *User) QueryGroup() *GroupQuery { func (_m *User) QueryGroups() *GroupQuery {
return NewUserClient(_m.config).QueryGroup(_m) return NewUserClient(_m.config).QueryGroups(_m)
} }
// QueryAuthTokens queries the "auth_tokens" edge of the User entity. // QueryAuthTokens queries the "auth_tokens" edge of the User entity.
@@ -288,6 +286,11 @@ func (_m *User) String() string {
builder.WriteString("oidc_subject=") builder.WriteString("oidc_subject=")
builder.WriteString(*v) builder.WriteString(*v)
} }
builder.WriteString(", ")
if v := _m.DefaultGroupID; v != nil {
builder.WriteString("default_group_id=")
builder.WriteString(fmt.Sprintf("%v", *v))
}
builder.WriteByte(')') builder.WriteByte(')')
return builder.String() return builder.String()
} }

View File

@@ -38,21 +38,21 @@ const (
FieldOidcIssuer = "oidc_issuer" FieldOidcIssuer = "oidc_issuer"
// FieldOidcSubject holds the string denoting the oidc_subject field in the database. // FieldOidcSubject holds the string denoting the oidc_subject field in the database.
FieldOidcSubject = "oidc_subject" FieldOidcSubject = "oidc_subject"
// EdgeGroup holds the string denoting the group edge name in mutations. // FieldDefaultGroupID holds the string denoting the default_group_id field in the database.
EdgeGroup = "group" FieldDefaultGroupID = "default_group_id"
// EdgeGroups holds the string denoting the groups edge name in mutations.
EdgeGroups = "groups"
// EdgeAuthTokens holds the string denoting the auth_tokens edge name in mutations. // EdgeAuthTokens holds the string denoting the auth_tokens edge name in mutations.
EdgeAuthTokens = "auth_tokens" EdgeAuthTokens = "auth_tokens"
// EdgeNotifiers holds the string denoting the notifiers edge name in mutations. // EdgeNotifiers holds the string denoting the notifiers edge name in mutations.
EdgeNotifiers = "notifiers" EdgeNotifiers = "notifiers"
// Table holds the table name of the user in the database. // Table holds the table name of the user in the database.
Table = "users" Table = "users"
// GroupTable is the table that holds the group relation/edge. // GroupsTable is the table that holds the groups relation/edge. The primary key declared below.
GroupTable = "users" GroupsTable = "user_groups"
// GroupInverseTable is the table name for the Group entity. // GroupsInverseTable is the table name for the Group entity.
// It exists in this package in order to avoid circular dependency with the "group" package. // It exists in this package in order to avoid circular dependency with the "group" package.
GroupInverseTable = "groups" GroupsInverseTable = "groups"
// GroupColumn is the table column denoting the group relation/edge.
GroupColumn = "group_users"
// AuthTokensTable is the table that holds the auth_tokens relation/edge. // AuthTokensTable is the table that holds the auth_tokens relation/edge.
AuthTokensTable = "auth_tokens" AuthTokensTable = "auth_tokens"
// AuthTokensInverseTable is the table name for the AuthTokens entity. // AuthTokensInverseTable is the table name for the AuthTokens entity.
@@ -83,13 +83,14 @@ var Columns = []string{
FieldActivatedOn, FieldActivatedOn,
FieldOidcIssuer, FieldOidcIssuer,
FieldOidcSubject, FieldOidcSubject,
FieldDefaultGroupID,
} }
// ForeignKeys holds the SQL foreign-keys that are owned by the "users" var (
// table and are not defined as standalone fields in the schema. // GroupsPrimaryKey and GroupsColumn2 are the table columns denoting the
var ForeignKeys = []string{ // primary key for the groups relation (M2M).
"group_users", GroupsPrimaryKey = []string{"user_id", "group_id"}
} )
// ValidColumn reports if the column name is valid (part of the table columns). // ValidColumn reports if the column name is valid (part of the table columns).
func ValidColumn(column string) bool { func ValidColumn(column string) bool {
@@ -98,11 +99,6 @@ func ValidColumn(column string) bool {
return true return true
} }
} }
for i := range ForeignKeys {
if column == ForeignKeys[i] {
return true
}
}
return false return false
} }
@@ -216,10 +212,22 @@ func ByOidcSubject(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldOidcSubject, opts...).ToFunc() return sql.OrderByField(FieldOidcSubject, opts...).ToFunc()
} }
// ByGroupField orders the results by group field. // ByDefaultGroupID orders the results by the default_group_id field.
func ByGroupField(field string, opts ...sql.OrderTermOption) OrderOption { func ByDefaultGroupID(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldDefaultGroupID, opts...).ToFunc()
}
// ByGroupsCount orders the results by groups count.
func ByGroupsCount(opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) { return func(s *sql.Selector) {
sqlgraph.OrderByNeighborTerms(s, newGroupStep(), sql.OrderByField(field, opts...)) sqlgraph.OrderByNeighborsCount(s, newGroupsStep(), opts...)
}
}
// ByGroups orders the results by groups terms.
func ByGroups(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborTerms(s, newGroupsStep(), append([]sql.OrderTerm{term}, terms...)...)
} }
} }
@@ -250,11 +258,11 @@ func ByNotifiers(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption {
sqlgraph.OrderByNeighborTerms(s, newNotifiersStep(), append([]sql.OrderTerm{term}, terms...)...) sqlgraph.OrderByNeighborTerms(s, newNotifiersStep(), append([]sql.OrderTerm{term}, terms...)...)
} }
} }
func newGroupStep() *sqlgraph.Step { func newGroupsStep() *sqlgraph.Step {
return sqlgraph.NewStep( return sqlgraph.NewStep(
sqlgraph.From(Table, FieldID), sqlgraph.From(Table, FieldID),
sqlgraph.To(GroupInverseTable, FieldID), sqlgraph.To(GroupsInverseTable, FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, GroupTable, GroupColumn), sqlgraph.Edge(sqlgraph.M2M, false, GroupsTable, GroupsPrimaryKey...),
) )
} }
func newAuthTokensStep() *sqlgraph.Step { func newAuthTokensStep() *sqlgraph.Step {

View File

@@ -106,6 +106,11 @@ func OidcSubject(v string) predicate.User {
return predicate.User(sql.FieldEQ(FieldOidcSubject, v)) return predicate.User(sql.FieldEQ(FieldOidcSubject, v))
} }
// DefaultGroupID applies equality check predicate on the "default_group_id" field. It's identical to DefaultGroupIDEQ.
func DefaultGroupID(v uuid.UUID) predicate.User {
return predicate.User(sql.FieldEQ(FieldDefaultGroupID, v))
}
// CreatedAtEQ applies the EQ predicate on the "created_at" field. // CreatedAtEQ applies the EQ predicate on the "created_at" field.
func CreatedAtEQ(v time.Time) predicate.User { func CreatedAtEQ(v time.Time) predicate.User {
return predicate.User(sql.FieldEQ(FieldCreatedAt, v)) return predicate.User(sql.FieldEQ(FieldCreatedAt, v))
@@ -631,21 +636,71 @@ func OidcSubjectContainsFold(v string) predicate.User {
return predicate.User(sql.FieldContainsFold(FieldOidcSubject, v)) return predicate.User(sql.FieldContainsFold(FieldOidcSubject, v))
} }
// HasGroup applies the HasEdge predicate on the "group" edge. // DefaultGroupIDEQ applies the EQ predicate on the "default_group_id" field.
func HasGroup() predicate.User { func DefaultGroupIDEQ(v uuid.UUID) predicate.User {
return predicate.User(sql.FieldEQ(FieldDefaultGroupID, v))
}
// DefaultGroupIDNEQ applies the NEQ predicate on the "default_group_id" field.
func DefaultGroupIDNEQ(v uuid.UUID) predicate.User {
return predicate.User(sql.FieldNEQ(FieldDefaultGroupID, v))
}
// DefaultGroupIDIn applies the In predicate on the "default_group_id" field.
func DefaultGroupIDIn(vs ...uuid.UUID) predicate.User {
return predicate.User(sql.FieldIn(FieldDefaultGroupID, vs...))
}
// DefaultGroupIDNotIn applies the NotIn predicate on the "default_group_id" field.
func DefaultGroupIDNotIn(vs ...uuid.UUID) predicate.User {
return predicate.User(sql.FieldNotIn(FieldDefaultGroupID, vs...))
}
// DefaultGroupIDGT applies the GT predicate on the "default_group_id" field.
func DefaultGroupIDGT(v uuid.UUID) predicate.User {
return predicate.User(sql.FieldGT(FieldDefaultGroupID, v))
}
// DefaultGroupIDGTE applies the GTE predicate on the "default_group_id" field.
func DefaultGroupIDGTE(v uuid.UUID) predicate.User {
return predicate.User(sql.FieldGTE(FieldDefaultGroupID, v))
}
// DefaultGroupIDLT applies the LT predicate on the "default_group_id" field.
func DefaultGroupIDLT(v uuid.UUID) predicate.User {
return predicate.User(sql.FieldLT(FieldDefaultGroupID, v))
}
// DefaultGroupIDLTE applies the LTE predicate on the "default_group_id" field.
func DefaultGroupIDLTE(v uuid.UUID) predicate.User {
return predicate.User(sql.FieldLTE(FieldDefaultGroupID, v))
}
// DefaultGroupIDIsNil applies the IsNil predicate on the "default_group_id" field.
func DefaultGroupIDIsNil() predicate.User {
return predicate.User(sql.FieldIsNull(FieldDefaultGroupID))
}
// DefaultGroupIDNotNil applies the NotNil predicate on the "default_group_id" field.
func DefaultGroupIDNotNil() predicate.User {
return predicate.User(sql.FieldNotNull(FieldDefaultGroupID))
}
// HasGroups applies the HasEdge predicate on the "groups" edge.
func HasGroups() predicate.User {
return predicate.User(func(s *sql.Selector) { return predicate.User(func(s *sql.Selector) {
step := sqlgraph.NewStep( step := sqlgraph.NewStep(
sqlgraph.From(Table, FieldID), sqlgraph.From(Table, FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, GroupTable, GroupColumn), sqlgraph.Edge(sqlgraph.M2M, false, GroupsTable, GroupsPrimaryKey...),
) )
sqlgraph.HasNeighbors(s, step) sqlgraph.HasNeighbors(s, step)
}) })
} }
// HasGroupWith applies the HasEdge predicate on the "group" edge with a given conditions (other predicates). // HasGroupsWith applies the HasEdge predicate on the "groups" edge with a given conditions (other predicates).
func HasGroupWith(preds ...predicate.Group) predicate.User { func HasGroupsWith(preds ...predicate.Group) predicate.User {
return predicate.User(func(s *sql.Selector) { return predicate.User(func(s *sql.Selector) {
step := newGroupStep() step := newGroupsStep()
sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
for _, p := range preds { for _, p := range preds {
p(s) p(s)

View File

@@ -162,6 +162,20 @@ func (_c *UserCreate) SetNillableOidcSubject(v *string) *UserCreate {
return _c return _c
} }
// SetDefaultGroupID sets the "default_group_id" field.
func (_c *UserCreate) SetDefaultGroupID(v uuid.UUID) *UserCreate {
_c.mutation.SetDefaultGroupID(v)
return _c
}
// SetNillableDefaultGroupID sets the "default_group_id" field if the given value is not nil.
func (_c *UserCreate) SetNillableDefaultGroupID(v *uuid.UUID) *UserCreate {
if v != nil {
_c.SetDefaultGroupID(*v)
}
return _c
}
// SetID sets the "id" field. // SetID sets the "id" field.
func (_c *UserCreate) SetID(v uuid.UUID) *UserCreate { func (_c *UserCreate) SetID(v uuid.UUID) *UserCreate {
_c.mutation.SetID(v) _c.mutation.SetID(v)
@@ -176,15 +190,19 @@ func (_c *UserCreate) SetNillableID(v *uuid.UUID) *UserCreate {
return _c return _c
} }
// SetGroupID sets the "group" edge to the Group entity by ID. // AddGroupIDs adds the "groups" edge to the Group entity by IDs.
func (_c *UserCreate) SetGroupID(id uuid.UUID) *UserCreate { func (_c *UserCreate) AddGroupIDs(ids ...uuid.UUID) *UserCreate {
_c.mutation.SetGroupID(id) _c.mutation.AddGroupIDs(ids...)
return _c return _c
} }
// SetGroup sets the "group" edge to the Group entity. // AddGroups adds the "groups" edges to the Group entity.
func (_c *UserCreate) SetGroup(v *Group) *UserCreate { func (_c *UserCreate) AddGroups(v ...*Group) *UserCreate {
return _c.SetGroupID(v.ID) ids := make([]uuid.UUID, len(v))
for i := range v {
ids[i] = v[i].ID
}
return _c.AddGroupIDs(ids...)
} }
// AddAuthTokenIDs adds the "auth_tokens" edge to the AuthTokens entity by IDs. // AddAuthTokenIDs adds the "auth_tokens" edge to the AuthTokens entity by IDs.
@@ -321,9 +339,6 @@ func (_c *UserCreate) check() error {
return &ValidationError{Name: "role", err: fmt.Errorf(`ent: validator failed for field "User.role": %w`, err)} return &ValidationError{Name: "role", err: fmt.Errorf(`ent: validator failed for field "User.role": %w`, err)}
} }
} }
if len(_c.mutation.GroupIDs()) == 0 {
return &ValidationError{Name: "group", err: errors.New(`ent: missing required edge "User.group"`)}
}
return nil return nil
} }
@@ -403,12 +418,16 @@ func (_c *UserCreate) createSpec() (*User, *sqlgraph.CreateSpec) {
_spec.SetField(user.FieldOidcSubject, field.TypeString, value) _spec.SetField(user.FieldOidcSubject, field.TypeString, value)
_node.OidcSubject = &value _node.OidcSubject = &value
} }
if nodes := _c.mutation.GroupIDs(); len(nodes) > 0 { if value, ok := _c.mutation.DefaultGroupID(); ok {
_spec.SetField(user.FieldDefaultGroupID, field.TypeUUID, value)
_node.DefaultGroupID = &value
}
if nodes := _c.mutation.GroupsIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{ edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O, Rel: sqlgraph.M2M,
Inverse: true, Inverse: false,
Table: user.GroupTable, Table: user.GroupsTable,
Columns: []string{user.GroupColumn}, Columns: user.GroupsPrimaryKey,
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeUUID), IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeUUID),
@@ -417,7 +436,6 @@ func (_c *UserCreate) createSpec() (*User, *sqlgraph.CreateSpec) {
for _, k := range nodes { for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k) edge.Target.Nodes = append(edge.Target.Nodes, k)
} }
_node.group_users = &nodes[0]
_spec.Edges = append(_spec.Edges, edge) _spec.Edges = append(_spec.Edges, edge)
} }
if nodes := _c.mutation.AuthTokensIDs(); len(nodes) > 0 { if nodes := _c.mutation.AuthTokensIDs(); len(nodes) > 0 {

View File

@@ -27,10 +27,9 @@ type UserQuery struct {
order []user.OrderOption order []user.OrderOption
inters []Interceptor inters []Interceptor
predicates []predicate.User predicates []predicate.User
withGroup *GroupQuery withGroups *GroupQuery
withAuthTokens *AuthTokensQuery withAuthTokens *AuthTokensQuery
withNotifiers *NotifierQuery withNotifiers *NotifierQuery
withFKs bool
// intermediate query (i.e. traversal path). // intermediate query (i.e. traversal path).
sql *sql.Selector sql *sql.Selector
path func(context.Context) (*sql.Selector, error) path func(context.Context) (*sql.Selector, error)
@@ -67,8 +66,8 @@ func (_q *UserQuery) Order(o ...user.OrderOption) *UserQuery {
return _q return _q
} }
// QueryGroup chains the current query on the "group" edge. // QueryGroups chains the current query on the "groups" edge.
func (_q *UserQuery) QueryGroup() *GroupQuery { func (_q *UserQuery) QueryGroups() *GroupQuery {
query := (&GroupClient{config: _q.config}).Query() query := (&GroupClient{config: _q.config}).Query()
query.path = func(ctx context.Context) (fromU *sql.Selector, err error) { query.path = func(ctx context.Context) (fromU *sql.Selector, err error) {
if err := _q.prepareQuery(ctx); err != nil { if err := _q.prepareQuery(ctx); err != nil {
@@ -81,7 +80,7 @@ func (_q *UserQuery) QueryGroup() *GroupQuery {
step := sqlgraph.NewStep( step := sqlgraph.NewStep(
sqlgraph.From(user.Table, user.FieldID, selector), sqlgraph.From(user.Table, user.FieldID, selector),
sqlgraph.To(group.Table, group.FieldID), sqlgraph.To(group.Table, group.FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, user.GroupTable, user.GroupColumn), sqlgraph.Edge(sqlgraph.M2M, false, user.GroupsTable, user.GroupsPrimaryKey...),
) )
fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step) fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step)
return fromU, nil return fromU, nil
@@ -325,7 +324,7 @@ func (_q *UserQuery) Clone() *UserQuery {
order: append([]user.OrderOption{}, _q.order...), order: append([]user.OrderOption{}, _q.order...),
inters: append([]Interceptor{}, _q.inters...), inters: append([]Interceptor{}, _q.inters...),
predicates: append([]predicate.User{}, _q.predicates...), predicates: append([]predicate.User{}, _q.predicates...),
withGroup: _q.withGroup.Clone(), withGroups: _q.withGroups.Clone(),
withAuthTokens: _q.withAuthTokens.Clone(), withAuthTokens: _q.withAuthTokens.Clone(),
withNotifiers: _q.withNotifiers.Clone(), withNotifiers: _q.withNotifiers.Clone(),
// clone intermediate query. // clone intermediate query.
@@ -334,14 +333,14 @@ func (_q *UserQuery) Clone() *UserQuery {
} }
} }
// WithGroup tells the query-builder to eager-load the nodes that are connected to // WithGroups tells the query-builder to eager-load the nodes that are connected to
// the "group" edge. The optional arguments are used to configure the query builder of the edge. // the "groups" edge. The optional arguments are used to configure the query builder of the edge.
func (_q *UserQuery) WithGroup(opts ...func(*GroupQuery)) *UserQuery { func (_q *UserQuery) WithGroups(opts ...func(*GroupQuery)) *UserQuery {
query := (&GroupClient{config: _q.config}).Query() query := (&GroupClient{config: _q.config}).Query()
for _, opt := range opts { for _, opt := range opts {
opt(query) opt(query)
} }
_q.withGroup = query _q.withGroups = query
return _q return _q
} }
@@ -444,20 +443,13 @@ func (_q *UserQuery) prepareQuery(ctx context.Context) error {
func (_q *UserQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*User, error) { func (_q *UserQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*User, error) {
var ( var (
nodes = []*User{} nodes = []*User{}
withFKs = _q.withFKs
_spec = _q.querySpec() _spec = _q.querySpec()
loadedTypes = [3]bool{ loadedTypes = [3]bool{
_q.withGroup != nil, _q.withGroups != nil,
_q.withAuthTokens != nil, _q.withAuthTokens != nil,
_q.withNotifiers != nil, _q.withNotifiers != nil,
} }
) )
if _q.withGroup != nil {
withFKs = true
}
if withFKs {
_spec.Node.Columns = append(_spec.Node.Columns, user.ForeignKeys...)
}
_spec.ScanValues = func(columns []string) ([]any, error) { _spec.ScanValues = func(columns []string) ([]any, error) {
return (*User).scanValues(nil, columns) return (*User).scanValues(nil, columns)
} }
@@ -476,9 +468,10 @@ func (_q *UserQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*User, e
if len(nodes) == 0 { if len(nodes) == 0 {
return nodes, nil return nodes, nil
} }
if query := _q.withGroup; query != nil { if query := _q.withGroups; query != nil {
if err := _q.loadGroup(ctx, query, nodes, nil, if err := _q.loadGroups(ctx, query, nodes,
func(n *User, e *Group) { n.Edges.Group = e }); err != nil { func(n *User) { n.Edges.Groups = []*Group{} },
func(n *User, e *Group) { n.Edges.Groups = append(n.Edges.Groups, e) }); err != nil {
return nil, err return nil, err
} }
} }
@@ -499,34 +492,63 @@ func (_q *UserQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*User, e
return nodes, nil return nodes, nil
} }
func (_q *UserQuery) loadGroup(ctx context.Context, query *GroupQuery, nodes []*User, init func(*User), assign func(*User, *Group)) error { func (_q *UserQuery) loadGroups(ctx context.Context, query *GroupQuery, nodes []*User, init func(*User), assign func(*User, *Group)) error {
ids := make([]uuid.UUID, 0, len(nodes)) edgeIDs := make([]driver.Value, len(nodes))
nodeids := make(map[uuid.UUID][]*User) byID := make(map[uuid.UUID]*User)
for i := range nodes { nids := make(map[uuid.UUID]map[*User]struct{})
if nodes[i].group_users == nil { for i, node := range nodes {
continue edgeIDs[i] = node.ID
byID[node.ID] = node
if init != nil {
init(node)
} }
fk := *nodes[i].group_users
if _, ok := nodeids[fk]; !ok {
ids = append(ids, fk)
} }
nodeids[fk] = append(nodeids[fk], nodes[i]) query.Where(func(s *sql.Selector) {
joinT := sql.Table(user.GroupsTable)
s.Join(joinT).On(s.C(group.FieldID), joinT.C(user.GroupsPrimaryKey[1]))
s.Where(sql.InValues(joinT.C(user.GroupsPrimaryKey[0]), edgeIDs...))
columns := s.SelectedColumns()
s.Select(joinT.C(user.GroupsPrimaryKey[0]))
s.AppendSelect(columns...)
s.SetDistinct(false)
})
if err := query.prepareQuery(ctx); err != nil {
return err
} }
if len(ids) == 0 { qr := QuerierFunc(func(ctx context.Context, q Query) (Value, error) {
return query.sqlAll(ctx, func(_ context.Context, spec *sqlgraph.QuerySpec) {
assign := spec.Assign
values := spec.ScanValues
spec.ScanValues = func(columns []string) ([]any, error) {
values, err := values(columns[1:])
if err != nil {
return nil, err
}
return append([]any{new(uuid.UUID)}, values...), nil
}
spec.Assign = func(columns []string, values []any) error {
outValue := *values[0].(*uuid.UUID)
inValue := *values[1].(*uuid.UUID)
if nids[inValue] == nil {
nids[inValue] = map[*User]struct{}{byID[outValue]: {}}
return assign(columns[1:], values[1:])
}
nids[inValue][byID[outValue]] = struct{}{}
return nil return nil
} }
query.Where(group.IDIn(ids...)) })
neighbors, err := query.All(ctx) })
neighbors, err := withInterceptors[[]*Group](ctx, query, qr, query.inters)
if err != nil { if err != nil {
return err return err
} }
for _, n := range neighbors { for _, n := range neighbors {
nodes, ok := nodeids[n.ID] nodes, ok := nids[n.ID]
if !ok { if !ok {
return fmt.Errorf(`unexpected foreign-key "group_users" returned %v`, n.ID) return fmt.Errorf(`unexpected "groups" node returned %v`, n.ID)
} }
for i := range nodes { for kn := range nodes {
assign(nodes[i], n) assign(kn, n)
} }
} }
return nil return nil

View File

@@ -188,15 +188,39 @@ func (_u *UserUpdate) ClearOidcSubject() *UserUpdate {
return _u return _u
} }
// SetGroupID sets the "group" edge to the Group entity by ID. // SetDefaultGroupID sets the "default_group_id" field.
func (_u *UserUpdate) SetGroupID(id uuid.UUID) *UserUpdate { func (_u *UserUpdate) SetDefaultGroupID(v uuid.UUID) *UserUpdate {
_u.mutation.SetGroupID(id) _u.mutation.SetDefaultGroupID(v)
return _u return _u
} }
// SetGroup sets the "group" edge to the Group entity. // SetNillableDefaultGroupID sets the "default_group_id" field if the given value is not nil.
func (_u *UserUpdate) SetGroup(v *Group) *UserUpdate { func (_u *UserUpdate) SetNillableDefaultGroupID(v *uuid.UUID) *UserUpdate {
return _u.SetGroupID(v.ID) if v != nil {
_u.SetDefaultGroupID(*v)
}
return _u
}
// ClearDefaultGroupID clears the value of the "default_group_id" field.
func (_u *UserUpdate) ClearDefaultGroupID() *UserUpdate {
_u.mutation.ClearDefaultGroupID()
return _u
}
// AddGroupIDs adds the "groups" edge to the Group entity by IDs.
func (_u *UserUpdate) AddGroupIDs(ids ...uuid.UUID) *UserUpdate {
_u.mutation.AddGroupIDs(ids...)
return _u
}
// AddGroups adds the "groups" edges to the Group entity.
func (_u *UserUpdate) AddGroups(v ...*Group) *UserUpdate {
ids := make([]uuid.UUID, len(v))
for i := range v {
ids[i] = v[i].ID
}
return _u.AddGroupIDs(ids...)
} }
// AddAuthTokenIDs adds the "auth_tokens" edge to the AuthTokens entity by IDs. // AddAuthTokenIDs adds the "auth_tokens" edge to the AuthTokens entity by IDs.
@@ -234,12 +258,27 @@ func (_u *UserUpdate) Mutation() *UserMutation {
return _u.mutation return _u.mutation
} }
// ClearGroup clears the "group" edge to the Group entity. // ClearGroups clears all "groups" edges to the Group entity.
func (_u *UserUpdate) ClearGroup() *UserUpdate { func (_u *UserUpdate) ClearGroups() *UserUpdate {
_u.mutation.ClearGroup() _u.mutation.ClearGroups()
return _u return _u
} }
// RemoveGroupIDs removes the "groups" edge to Group entities by IDs.
func (_u *UserUpdate) RemoveGroupIDs(ids ...uuid.UUID) *UserUpdate {
_u.mutation.RemoveGroupIDs(ids...)
return _u
}
// RemoveGroups removes "groups" edges to Group entities.
func (_u *UserUpdate) RemoveGroups(v ...*Group) *UserUpdate {
ids := make([]uuid.UUID, len(v))
for i := range v {
ids[i] = v[i].ID
}
return _u.RemoveGroupIDs(ids...)
}
// ClearAuthTokens clears all "auth_tokens" edges to the AuthTokens entity. // ClearAuthTokens clears all "auth_tokens" edges to the AuthTokens entity.
func (_u *UserUpdate) ClearAuthTokens() *UserUpdate { func (_u *UserUpdate) ClearAuthTokens() *UserUpdate {
_u.mutation.ClearAuthTokens() _u.mutation.ClearAuthTokens()
@@ -340,9 +379,6 @@ func (_u *UserUpdate) check() error {
return &ValidationError{Name: "role", err: fmt.Errorf(`ent: validator failed for field "User.role": %w`, err)} return &ValidationError{Name: "role", err: fmt.Errorf(`ent: validator failed for field "User.role": %w`, err)}
} }
} }
if _u.mutation.GroupCleared() && len(_u.mutation.GroupIDs()) > 0 {
return errors.New(`ent: clearing a required unique edge "User.group"`)
}
return nil return nil
} }
@@ -400,12 +436,18 @@ func (_u *UserUpdate) sqlSave(ctx context.Context) (_node int, err error) {
if _u.mutation.OidcSubjectCleared() { if _u.mutation.OidcSubjectCleared() {
_spec.ClearField(user.FieldOidcSubject, field.TypeString) _spec.ClearField(user.FieldOidcSubject, field.TypeString)
} }
if _u.mutation.GroupCleared() { if value, ok := _u.mutation.DefaultGroupID(); ok {
_spec.SetField(user.FieldDefaultGroupID, field.TypeUUID, value)
}
if _u.mutation.DefaultGroupIDCleared() {
_spec.ClearField(user.FieldDefaultGroupID, field.TypeUUID)
}
if _u.mutation.GroupsCleared() {
edge := &sqlgraph.EdgeSpec{ edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O, Rel: sqlgraph.M2M,
Inverse: true, Inverse: false,
Table: user.GroupTable, Table: user.GroupsTable,
Columns: []string{user.GroupColumn}, Columns: user.GroupsPrimaryKey,
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeUUID), IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeUUID),
@@ -413,12 +455,28 @@ func (_u *UserUpdate) sqlSave(ctx context.Context) (_node int, err error) {
} }
_spec.Edges.Clear = append(_spec.Edges.Clear, edge) _spec.Edges.Clear = append(_spec.Edges.Clear, edge)
} }
if nodes := _u.mutation.GroupIDs(); len(nodes) > 0 { if nodes := _u.mutation.RemovedGroupsIDs(); len(nodes) > 0 && !_u.mutation.GroupsCleared() {
edge := &sqlgraph.EdgeSpec{ edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O, Rel: sqlgraph.M2M,
Inverse: true, Inverse: false,
Table: user.GroupTable, Table: user.GroupsTable,
Columns: []string{user.GroupColumn}, Columns: user.GroupsPrimaryKey,
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeUUID),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := _u.mutation.GroupsIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2M,
Inverse: false,
Table: user.GroupsTable,
Columns: user.GroupsPrimaryKey,
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeUUID), IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeUUID),
@@ -695,15 +753,39 @@ func (_u *UserUpdateOne) ClearOidcSubject() *UserUpdateOne {
return _u return _u
} }
// SetGroupID sets the "group" edge to the Group entity by ID. // SetDefaultGroupID sets the "default_group_id" field.
func (_u *UserUpdateOne) SetGroupID(id uuid.UUID) *UserUpdateOne { func (_u *UserUpdateOne) SetDefaultGroupID(v uuid.UUID) *UserUpdateOne {
_u.mutation.SetGroupID(id) _u.mutation.SetDefaultGroupID(v)
return _u return _u
} }
// SetGroup sets the "group" edge to the Group entity. // SetNillableDefaultGroupID sets the "default_group_id" field if the given value is not nil.
func (_u *UserUpdateOne) SetGroup(v *Group) *UserUpdateOne { func (_u *UserUpdateOne) SetNillableDefaultGroupID(v *uuid.UUID) *UserUpdateOne {
return _u.SetGroupID(v.ID) if v != nil {
_u.SetDefaultGroupID(*v)
}
return _u
}
// ClearDefaultGroupID clears the value of the "default_group_id" field.
func (_u *UserUpdateOne) ClearDefaultGroupID() *UserUpdateOne {
_u.mutation.ClearDefaultGroupID()
return _u
}
// AddGroupIDs adds the "groups" edge to the Group entity by IDs.
func (_u *UserUpdateOne) AddGroupIDs(ids ...uuid.UUID) *UserUpdateOne {
_u.mutation.AddGroupIDs(ids...)
return _u
}
// AddGroups adds the "groups" edges to the Group entity.
func (_u *UserUpdateOne) AddGroups(v ...*Group) *UserUpdateOne {
ids := make([]uuid.UUID, len(v))
for i := range v {
ids[i] = v[i].ID
}
return _u.AddGroupIDs(ids...)
} }
// AddAuthTokenIDs adds the "auth_tokens" edge to the AuthTokens entity by IDs. // AddAuthTokenIDs adds the "auth_tokens" edge to the AuthTokens entity by IDs.
@@ -741,12 +823,27 @@ func (_u *UserUpdateOne) Mutation() *UserMutation {
return _u.mutation return _u.mutation
} }
// ClearGroup clears the "group" edge to the Group entity. // ClearGroups clears all "groups" edges to the Group entity.
func (_u *UserUpdateOne) ClearGroup() *UserUpdateOne { func (_u *UserUpdateOne) ClearGroups() *UserUpdateOne {
_u.mutation.ClearGroup() _u.mutation.ClearGroups()
return _u return _u
} }
// RemoveGroupIDs removes the "groups" edge to Group entities by IDs.
func (_u *UserUpdateOne) RemoveGroupIDs(ids ...uuid.UUID) *UserUpdateOne {
_u.mutation.RemoveGroupIDs(ids...)
return _u
}
// RemoveGroups removes "groups" edges to Group entities.
func (_u *UserUpdateOne) RemoveGroups(v ...*Group) *UserUpdateOne {
ids := make([]uuid.UUID, len(v))
for i := range v {
ids[i] = v[i].ID
}
return _u.RemoveGroupIDs(ids...)
}
// ClearAuthTokens clears all "auth_tokens" edges to the AuthTokens entity. // ClearAuthTokens clears all "auth_tokens" edges to the AuthTokens entity.
func (_u *UserUpdateOne) ClearAuthTokens() *UserUpdateOne { func (_u *UserUpdateOne) ClearAuthTokens() *UserUpdateOne {
_u.mutation.ClearAuthTokens() _u.mutation.ClearAuthTokens()
@@ -860,9 +957,6 @@ func (_u *UserUpdateOne) check() error {
return &ValidationError{Name: "role", err: fmt.Errorf(`ent: validator failed for field "User.role": %w`, err)} return &ValidationError{Name: "role", err: fmt.Errorf(`ent: validator failed for field "User.role": %w`, err)}
} }
} }
if _u.mutation.GroupCleared() && len(_u.mutation.GroupIDs()) > 0 {
return errors.New(`ent: clearing a required unique edge "User.group"`)
}
return nil return nil
} }
@@ -937,12 +1031,18 @@ func (_u *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error) {
if _u.mutation.OidcSubjectCleared() { if _u.mutation.OidcSubjectCleared() {
_spec.ClearField(user.FieldOidcSubject, field.TypeString) _spec.ClearField(user.FieldOidcSubject, field.TypeString)
} }
if _u.mutation.GroupCleared() { if value, ok := _u.mutation.DefaultGroupID(); ok {
_spec.SetField(user.FieldDefaultGroupID, field.TypeUUID, value)
}
if _u.mutation.DefaultGroupIDCleared() {
_spec.ClearField(user.FieldDefaultGroupID, field.TypeUUID)
}
if _u.mutation.GroupsCleared() {
edge := &sqlgraph.EdgeSpec{ edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O, Rel: sqlgraph.M2M,
Inverse: true, Inverse: false,
Table: user.GroupTable, Table: user.GroupsTable,
Columns: []string{user.GroupColumn}, Columns: user.GroupsPrimaryKey,
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeUUID), IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeUUID),
@@ -950,12 +1050,28 @@ func (_u *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error) {
} }
_spec.Edges.Clear = append(_spec.Edges.Clear, edge) _spec.Edges.Clear = append(_spec.Edges.Clear, edge)
} }
if nodes := _u.mutation.GroupIDs(); len(nodes) > 0 { if nodes := _u.mutation.RemovedGroupsIDs(); len(nodes) > 0 && !_u.mutation.GroupsCleared() {
edge := &sqlgraph.EdgeSpec{ edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O, Rel: sqlgraph.M2M,
Inverse: true, Inverse: false,
Table: user.GroupTable, Table: user.GroupsTable,
Columns: []string{user.GroupColumn}, Columns: user.GroupsPrimaryKey,
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeUUID),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := _u.mutation.GroupsIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2M,
Inverse: false,
Table: user.GroupsTable,
Columns: user.GroupsPrimaryKey,
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeUUID), IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeUUID),

View File

@@ -31,5 +31,4 @@ func Migrations(dialect string) (embed.FS, error) {
return embed.FS{}, fmt.Errorf("unknown sql dialect: %s", dialect) return embed.FS{}, fmt.Errorf("unknown sql dialect: %s", dialect)
} }
// This should never get hit, but just in case // This should never get hit, but just in case
return sqliteFiles, nil
} }

View File

@@ -0,0 +1,47 @@
-- +goose Up
-- Create user_groups junction table for M:M relationship
CREATE TABLE IF NOT EXISTS "user_groups" (
"user_id" uuid NOT NULL,
"group_id" uuid NOT NULL,
PRIMARY KEY ("user_id", "group_id"),
CONSTRAINT "user_groups_user_id" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON UPDATE NO ACTION ON DELETE CASCADE,
CONSTRAINT "user_groups_group_id" FOREIGN KEY ("group_id") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE
);
-- Migrate existing user->group relationships to the junction table
INSERT INTO "user_groups" ("user_id", "group_id")
SELECT "id", "group_users" FROM "users" WHERE "group_users" IS NOT NULL;
-- Add default_group_id column to users table
ALTER TABLE "users" ADD COLUMN "default_group_id" uuid;
-- Set default_group_id to the user's current group
UPDATE "users" SET "default_group_id" = "group_users" WHERE "group_users" IS NOT NULL;
-- Drop the old group_users foreign key constraint and column
ALTER TABLE "users" DROP CONSTRAINT "users_groups_users";
ALTER TABLE "users" DROP COLUMN "group_users";
-- Add foreign key constraint for default_group_id
ALTER TABLE "users" ADD CONSTRAINT "users_groups_users_default" FOREIGN KEY ("default_group_id") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE SET NULL;
-- +goose Down
-- Recreate group_users column with foreign key
ALTER TABLE "users" ADD COLUMN "group_users" uuid;
-- Restore the group_users values from user_groups (using the default_group_id or first entry)
UPDATE "users"
SET "group_users" = COALESCE("default_group_id", (
SELECT "group_id" FROM "user_groups" WHERE "user_id" = "users"."id" LIMIT 1
));
-- Drop the default_group_id foreign key and column
ALTER TABLE "users" DROP CONSTRAINT "users_groups_users_default";
ALTER TABLE "users" DROP COLUMN "default_group_id";
-- Add back the original foreign key constraint
ALTER TABLE "users" ADD CONSTRAINT "users_groups_users" FOREIGN KEY ("group_users") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;
-- Drop the junction table
DROP TABLE IF EXISTS "user_groups";

View File

@@ -0,0 +1,68 @@
-- +goose Up
-- +goose no transaction
-- Turn off foreign key constraints because otherwise we'll wipe notifiers out of the database when dropping the older users table
PRAGMA foreign_keys=OFF;
-- Create user_groups junction table for M:M relationship
CREATE TABLE IF NOT EXISTS user_groups (
user_id UUID NOT NULL,
group_id UUID NOT NULL,
PRIMARY KEY (user_id, group_id),
CONSTRAINT user_groups_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT user_groups_group_id FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE
);
-- Migrate existing user->group relationships to the junction table
INSERT INTO user_groups (user_id, group_id)
SELECT id, group_users FROM users WHERE group_users IS NOT NULL;
-- Add default_group_id column to users table
ALTER TABLE users ADD COLUMN default_group_id UUID;
-- Set default_group_id to the user's current group
UPDATE users SET default_group_id = group_users WHERE group_users IS NOT NULL;
-- Add foreign key constraint for default_group_id
CREATE TABLE users_new (
id UUID NOT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
password TEXT,
is_superuser BOOLEAN NOT NULL DEFAULT false,
superuser BOOLEAN NOT NULL DEFAULT false,
role TEXT NOT NULL DEFAULT 'user',
activated_on DATETIME,
oidc_issuer TEXT,
oidc_subject TEXT,
default_group_id UUID,
PRIMARY KEY (id),
CONSTRAINT users_groups_users_default FOREIGN KEY (default_group_id) REFERENCES groups(id) ON DELETE SET NULL,
UNIQUE (oidc_issuer, oidc_subject)
);
-- Copy data from old table to new table
INSERT INTO users_new (
id, created_at, updated_at, name, email, password, is_superuser, superuser, role,
activated_on, oidc_issuer, oidc_subject, default_group_id
)
SELECT
id, created_at, updated_at, name, email, password, is_superuser, superuser, role,
activated_on, oidc_issuer, oidc_subject, default_group_id
FROM users;
-- Drop old indexes
DROP INDEX IF EXISTS users_email_key;
DROP INDEX IF EXISTS users_oidc_issuer_subject_key;
-- Drop old table
DROP TABLE users;
-- Rename new table to users
ALTER TABLE users_new RENAME TO users;
-- Recreate indexes
CREATE UNIQUE INDEX IF NOT EXISTS users_email_key ON users(email);
CREATE UNIQUE INDEX IF NOT EXISTS users_oidc_issuer_subject_key ON users(oidc_issuer, oidc_subject);
PRAGMA foreign_keys=ON;

View File

@@ -2,6 +2,7 @@ package repo
import ( import (
"context" "context"
"github.com/google/uuid"
"github.com/sysadminsmedia/homebox/backend/internal/sys/config" "github.com/sysadminsmedia/homebox/backend/internal/sys/config"
"log" "log"
"os" "os"
@@ -29,7 +30,7 @@ func bootstrap() {
ctx = context.Background() ctx = context.Background()
) )
tGroup, err = tRepos.Groups.GroupCreate(ctx, "test-group") tGroup, err = tRepos.Groups.GroupCreate(ctx, "test-group", uuid.Nil)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@@ -223,7 +223,7 @@ func (r *GroupRepository) StatsPurchasePrice(ctx context.Context, gid uuid.UUID,
func (r *GroupRepository) StatsGroup(ctx context.Context, gid uuid.UUID) (GroupStatistics, error) { func (r *GroupRepository) StatsGroup(ctx context.Context, gid uuid.UUID) (GroupStatistics, error) {
q := ` q := `
SELECT SELECT
(SELECT COUNT(*) FROM users WHERE group_users = $2) AS total_users, (SELECT COUNT(*) FROM user_groups WHERE group_id = $2) AS total_users,
(SELECT COUNT(*) FROM items WHERE group_items = $2 AND items.archived = false) AS total_items, (SELECT COUNT(*) FROM items WHERE group_items = $2 AND items.archived = false) AS total_items,
(SELECT COUNT(*) FROM locations WHERE group_locations = $2) AS total_locations, (SELECT COUNT(*) FROM locations WHERE group_locations = $2) AS total_locations,
(SELECT COUNT(*) FROM labels WHERE group_labels = $2) AS total_labels, (SELECT COUNT(*) FROM labels WHERE group_labels = $2) AS total_labels,
@@ -252,10 +252,15 @@ func (r *GroupRepository) StatsGroup(ctx context.Context, gid uuid.UUID) (GroupS
return stats, nil return stats, nil
} }
func (r *GroupRepository) GroupCreate(ctx context.Context, name string) (Group, error) { func (r *GroupRepository) GroupCreate(ctx context.Context, name string, userID uuid.UUID) (Group, error) {
return r.groupMapper.MapErr(r.db.Group.Create(). createQuery := r.db.Group.Create().SetName(name)
SetName(name).
Save(ctx)) // Only link user if a valid user ID is provided
if userID != uuid.Nil {
createQuery = createQuery.AddUserIDs(userID)
}
return r.groupMapper.MapErr(createQuery.Save(ctx))
} }
func (r *GroupRepository) GroupUpdate(ctx context.Context, id uuid.UUID, data GroupUpdate) (Group, error) { func (r *GroupRepository) GroupUpdate(ctx context.Context, id uuid.UUID, data GroupUpdate) (Group, error) {
@@ -271,6 +276,10 @@ func (r *GroupRepository) GroupByID(ctx context.Context, id uuid.UUID) (Group, e
return r.groupMapper.MapErr(r.db.Group.Get(ctx, id)) return r.groupMapper.MapErr(r.db.Group.Get(ctx, id))
} }
func (r *GroupRepository) GroupDelete(ctx context.Context, id uuid.UUID) error {
return r.db.Group.DeleteOneID(id).Exec(ctx)
}
func (r *GroupRepository) InvitationGet(ctx context.Context, token []byte) (GroupInvitation, error) { func (r *GroupRepository) InvitationGet(ctx context.Context, token []byte) (GroupInvitation, error) {
return r.invitationMapper.MapErr(r.db.GroupInvitationToken.Query(). return r.invitationMapper.MapErr(r.db.GroupInvitationToken.Query().
Where(groupinvitationtoken.Token(token)). Where(groupinvitationtoken.Token(token)).
@@ -278,6 +287,18 @@ func (r *GroupRepository) InvitationGet(ctx context.Context, token []byte) (Grou
Only(ctx)) Only(ctx))
} }
func (r *GroupRepository) InvitationGetAll(ctx context.Context, groupID uuid.UUID) ([]GroupInvitation, error) {
invitations, err := r.db.GroupInvitationToken.Query().
Where(groupinvitationtoken.HasGroupWith(group.ID(groupID))).
WithGroup().
All(ctx)
if err != nil {
return nil, err
}
return r.invitationMapper.MapEach(invitations), nil
}
func (r *GroupRepository) InvitationCreate(ctx context.Context, groupID uuid.UUID, invite GroupInvitationCreate) (GroupInvitation, error) { func (r *GroupRepository) InvitationCreate(ctx context.Context, groupID uuid.UUID, invite GroupInvitationCreate) (GroupInvitation, error) {
entity, err := r.db.GroupInvitationToken.Create(). entity, err := r.db.GroupInvitationToken.Create().
SetGroupID(groupID). SetGroupID(groupID).
@@ -308,3 +329,11 @@ func (r *GroupRepository) InvitationPurge(ctx context.Context) (amount int, err
return q.Exec(ctx) return q.Exec(ctx)
} }
func (r *GroupRepository) AddMember(ctx context.Context, groupID, userID uuid.UUID) error {
return r.db.Group.UpdateOneID(groupID).AddUserIDs(userID).Exec(ctx)
}
func (r *GroupRepository) RemoveMember(ctx context.Context, groupID, userID uuid.UUID) error {
return r.db.Group.UpdateOneID(groupID).RemoveUserIDs(userID).Exec(ctx)
}

View File

@@ -4,12 +4,13 @@ import (
"context" "context"
"testing" "testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func Test_Group_Create(t *testing.T) { func Test_Group_Create(t *testing.T) {
g, err := tRepos.Groups.GroupCreate(context.Background(), "test") g, err := tRepos.Groups.GroupCreate(context.Background(), "test", uuid.Nil)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "test", g.Name) assert.Equal(t, "test", g.Name)
@@ -21,7 +22,7 @@ func Test_Group_Create(t *testing.T) {
} }
func Test_Group_Update(t *testing.T) { func Test_Group_Update(t *testing.T) {
g, err := tRepos.Groups.GroupCreate(context.Background(), "test") g, err := tRepos.Groups.GroupCreate(context.Background(), "test", uuid.Nil)
require.NoError(t, err) require.NoError(t, err)
g, err = tRepos.Groups.GroupUpdate(context.Background(), g.ID, GroupUpdate{ g, err = tRepos.Groups.GroupUpdate(context.Background(), g.ID, GroupUpdate{

View File

@@ -45,12 +45,12 @@ type (
// Default values for items // Default values for items
DefaultQuantity *int `json:"defaultQuantity,omitempty" extensions:"x-nullable"` DefaultQuantity *int `json:"defaultQuantity,omitempty" extensions:"x-nullable"`
DefaultInsured bool `json:"defaultInsured"` DefaultInsured bool `json:"defaultInsured"`
DefaultName *string `json:"defaultName,omitempty" extensions:"x-nullable" validate:"omitempty,max=255"` DefaultName *string `json:"defaultName,omitempty" validate:"omitempty,max=255" extensions:"x-nullable"`
DefaultDescription *string `json:"defaultDescription,omitempty" extensions:"x-nullable" validate:"omitempty,max=1000"` DefaultDescription *string `json:"defaultDescription,omitempty" validate:"omitempty,max=1000" extensions:"x-nullable"`
DefaultManufacturer *string `json:"defaultManufacturer,omitempty" extensions:"x-nullable" validate:"omitempty,max=255"` DefaultManufacturer *string `json:"defaultManufacturer,omitempty" validate:"omitempty,max=255" extensions:"x-nullable"`
DefaultModelNumber *string `json:"defaultModelNumber,omitempty" extensions:"x-nullable" validate:"omitempty,max=255"` DefaultModelNumber *string `json:"defaultModelNumber,omitempty" validate:"omitempty,max=255" extensions:"x-nullable"`
DefaultLifetimeWarranty bool `json:"defaultLifetimeWarranty"` DefaultLifetimeWarranty bool `json:"defaultLifetimeWarranty"`
DefaultWarrantyDetails *string `json:"defaultWarrantyDetails,omitempty" extensions:"x-nullable" validate:"omitempty,max=1000"` DefaultWarrantyDetails *string `json:"defaultWarrantyDetails,omitempty" validate:"omitempty,max=1000" extensions:"x-nullable"`
// Default location and labels // Default location and labels
DefaultLocationID uuid.UUID `json:"defaultLocationId,omitempty" extensions:"x-nullable"` DefaultLocationID uuid.UUID `json:"defaultLocationId,omitempty" extensions:"x-nullable"`
@@ -74,12 +74,12 @@ type (
// Default values for items // Default values for items
DefaultQuantity *int `json:"defaultQuantity,omitempty" extensions:"x-nullable"` DefaultQuantity *int `json:"defaultQuantity,omitempty" extensions:"x-nullable"`
DefaultInsured bool `json:"defaultInsured"` DefaultInsured bool `json:"defaultInsured"`
DefaultName *string `json:"defaultName,omitempty" extensions:"x-nullable" validate:"omitempty,max=255"` DefaultName *string `json:"defaultName,omitempty" validate:"omitempty,max=255" extensions:"x-nullable"`
DefaultDescription *string `json:"defaultDescription,omitempty" extensions:"x-nullable" validate:"omitempty,max=1000"` DefaultDescription *string `json:"defaultDescription,omitempty" validate:"omitempty,max=1000" extensions:"x-nullable"`
DefaultManufacturer *string `json:"defaultManufacturer,omitempty" extensions:"x-nullable" validate:"omitempty,max=255"` DefaultManufacturer *string `json:"defaultManufacturer,omitempty" validate:"omitempty,max=255" extensions:"x-nullable"`
DefaultModelNumber *string `json:"defaultModelNumber,omitempty" extensions:"x-nullable" validate:"omitempty,max=255"` DefaultModelNumber *string `json:"defaultModelNumber,omitempty" validate:"omitempty,max=255" extensions:"x-nullable"`
DefaultLifetimeWarranty bool `json:"defaultLifetimeWarranty"` DefaultLifetimeWarranty bool `json:"defaultLifetimeWarranty"`
DefaultWarrantyDetails *string `json:"defaultWarrantyDetails,omitempty" extensions:"x-nullable" validate:"omitempty,max=1000"` DefaultWarrantyDetails *string `json:"defaultWarrantyDetails,omitempty" validate:"omitempty,max=1000" extensions:"x-nullable"`
// Default location and labels // Default location and labels
DefaultLocationID uuid.UUID `json:"defaultLocationId,omitempty" extensions:"x-nullable"` DefaultLocationID uuid.UUID `json:"defaultLocationId,omitempty" extensions:"x-nullable"`

View File

@@ -164,7 +164,7 @@ func TestItemsRepository_AccentInsensitiveSearch(t *testing.T) {
if tc.shouldMatch { if tc.shouldMatch {
// If it should match, then either the original query should match // If it should match, then either the original query should match
// or the normalized query should match when applied to the stored data // or the normalized query should match when applied to the stored data
assert.NotEqual(t, "", normalizedSearch, "Normalized search should not be empty") assert.NotEmpty(t, normalizedSearch, "Normalized search should not be empty")
// The key insight is that we're searching with both the original and normalized queries // The key insight is that we're searching with both the original and normalized queries
// So "electrónica" will be found when searching for "electronica" because: // So "electrónica" will be found when searching for "electronica" because:

View File

@@ -313,7 +313,7 @@ func TestItemRepository_GetAllCustomFields(t *testing.T) {
// Test getting all values from field // Test getting all values from field
{ {
results, err := tRepos.Items.GetAllCustomFieldValues(context.Background(), tUser.GroupID, names[0]) results, err := tRepos.Items.GetAllCustomFieldValues(context.Background(), tUser.DefaultGroupID, names[0])
require.NoError(t, err) require.NoError(t, err)
assert.ElementsMatch(t, values[:1], results) assert.ElementsMatch(t, values[:1], results)
@@ -465,7 +465,7 @@ func TestItemsRepository_WipeInventory(t *testing.T) {
t.Run("wipe all including labels, locations, and maintenance", func(t *testing.T) { t.Run("wipe all including labels, locations, and maintenance", func(t *testing.T) {
deleted, err := tRepos.Items.WipeInventory(context.Background(), tGroup.ID, true, true, true) deleted, err := tRepos.Items.WipeInventory(context.Background(), tGroup.ID, true, true, true)
require.NoError(t, err) require.NoError(t, err)
assert.Greater(t, deleted, 0, "Should have deleted at least some entities") assert.Positive(t, deleted, "Should have deleted at least some entities")
// Verify items are deleted // Verify items are deleted
_, err = tRepos.Items.GetOneByGroup(context.Background(), tGroup.ID, item1.ID) _, err = tRepos.Items.GetOneByGroup(context.Background(), tGroup.ID, item1.ID)
@@ -532,7 +532,7 @@ func TestItemsRepository_WipeInventory_OnlyItems(t *testing.T) {
// Test: Wipe inventory with only items (no labels, locations, or maintenance) // Test: Wipe inventory with only items (no labels, locations, or maintenance)
deleted, err := tRepos.Items.WipeInventory(context.Background(), tGroup.ID, false, false, false) deleted, err := tRepos.Items.WipeInventory(context.Background(), tGroup.ID, false, false, false)
require.NoError(t, err) require.NoError(t, err)
assert.Greater(t, deleted, 0, "Should have deleted at least the item") assert.Positive(t, deleted, "Should have deleted at least the item")
// Verify item is deleted // Verify item is deleted
_, err = tRepos.Items.GetOneByGroup(context.Background(), tGroup.ID, item.ID) _, err = tRepos.Items.GetOneByGroup(context.Background(), tGroup.ID, item.ID)
@@ -555,4 +555,3 @@ func TestItemsRepository_WipeInventory_OnlyItems(t *testing.T) {
_ = tRepos.Labels.DeleteByGroup(context.Background(), tGroup.ID, label.ID) _ = tRepos.Labels.DeleteByGroup(context.Background(), tGroup.ID, label.ID)
_ = tRepos.Locations.delete(context.Background(), loc.ID) _ = tRepos.Locations.delete(context.Background(), loc.ID)
} }

View File

@@ -40,7 +40,7 @@ func (r *TokenRepository) GetUserFromToken(ctx context.Context, token []byte) (U
Where(authtokens.ExpiresAtGTE(time.Now())). Where(authtokens.ExpiresAtGTE(time.Now())).
WithUser(). WithUser().
QueryUser(). QueryUser().
WithGroup(). WithGroups().
Only(ctx) Only(ctx)
if err != nil { if err != nil {
return UserOut{}, err return UserOut{}, err

View File

@@ -5,6 +5,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent" "github.com/sysadminsmedia/homebox/backend/internal/data/ent"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/group"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/user" "github.com/sysadminsmedia/homebox/backend/internal/data/ent/user"
) )
@@ -20,8 +21,8 @@ type (
Name string `json:"name"` Name string `json:"name"`
Email string `json:"email"` Email string `json:"email"`
Password *string `json:"password"` Password *string `json:"password"`
IsSuperuser bool `json:"isSuperuser"` IsSuperuser bool `json:"isSuperUser"`
GroupID uuid.UUID `json:"groupID"` DefaultGroupID uuid.UUID `json:"defaultGroupID"`
IsOwner bool `json:"isOwner"` IsOwner bool `json:"isOwner"`
} }
@@ -35,8 +36,8 @@ type (
Name string `json:"name"` Name string `json:"name"`
Email string `json:"email"` Email string `json:"email"`
IsSuperuser bool `json:"isSuperuser"` IsSuperuser bool `json:"isSuperuser"`
GroupID uuid.UUID `json:"groupId"` DefaultGroupID uuid.UUID `json:"defaultGroupId"`
GroupName string `json:"groupName"` GroupIDs []uuid.UUID `json:"groupIds"`
PasswordHash string `json:"-"` PasswordHash string `json:"-"`
IsOwner bool `json:"isOwner"` IsOwner bool `json:"isOwner"`
OidcIssuer *string `json:"oidcIssuer"` OidcIssuer *string `json:"oidcIssuer"`
@@ -55,13 +56,24 @@ func mapUserOut(user *ent.User) UserOut {
passwordHash = *user.Password passwordHash = *user.Password
} }
groupIDs := make([]uuid.UUID, len(user.Edges.Groups))
for i, g := range user.Edges.Groups {
groupIDs[i] = g.ID
}
// Get the default group ID, handling the optional pointer
defaultGroupID := uuid.Nil
if user.DefaultGroupID != nil {
defaultGroupID = *user.DefaultGroupID
}
return UserOut{ return UserOut{
ID: user.ID, ID: user.ID,
Name: user.Name, Name: user.Name,
Email: user.Email, Email: user.Email,
IsSuperuser: user.IsSuperuser, IsSuperuser: user.IsSuperuser,
GroupID: user.Edges.Group.ID, DefaultGroupID: defaultGroupID,
GroupName: user.Edges.Group.Name, GroupIDs: groupIDs,
PasswordHash: passwordHash, PasswordHash: passwordHash,
IsOwner: user.Role == "owner", IsOwner: user.Role == "owner",
OidcIssuer: user.OidcIssuer, OidcIssuer: user.OidcIssuer,
@@ -72,20 +84,27 @@ func mapUserOut(user *ent.User) UserOut {
func (r *UserRepository) GetOneID(ctx context.Context, id uuid.UUID) (UserOut, error) { func (r *UserRepository) GetOneID(ctx context.Context, id uuid.UUID) (UserOut, error) {
return mapUserOutErr(r.db.User.Query(). return mapUserOutErr(r.db.User.Query().
Where(user.ID(id)). Where(user.ID(id)).
WithGroup(). WithGroups().
Only(ctx)) Only(ctx))
} }
func (r *UserRepository) GetOneEmail(ctx context.Context, email string) (UserOut, error) { func (r *UserRepository) GetOneEmail(ctx context.Context, email string) (UserOut, error) {
return mapUserOutErr(r.db.User.Query(). return mapUserOutErr(r.db.User.Query().
Where(user.EmailEqualFold(email)). Where(user.EmailEqualFold(email)).
WithGroup(). WithGroups().
Only(ctx),
)
}
func (r *UserRepository) GetOneEmailNoEdges(ctx context.Context, email string) (UserOut, error) {
return mapUserOutErr(r.db.User.Query().
Where(user.EmailEqualFold(email)).
Only(ctx), Only(ctx),
) )
} }
func (r *UserRepository) GetAll(ctx context.Context) ([]UserOut, error) { func (r *UserRepository) GetAll(ctx context.Context) ([]UserOut, error) {
return mapUsersOutErr(r.db.User.Query().WithGroup().All(ctx)) return mapUsersOutErr(r.db.User.Query().WithGroups().All(ctx))
} }
func (r *UserRepository) Create(ctx context.Context, usr UserCreate) (UserOut, error) { func (r *UserRepository) Create(ctx context.Context, usr UserCreate) (UserOut, error) {
@@ -99,8 +118,9 @@ func (r *UserRepository) Create(ctx context.Context, usr UserCreate) (UserOut, e
SetName(usr.Name). SetName(usr.Name).
SetEmail(usr.Email). SetEmail(usr.Email).
SetIsSuperuser(usr.IsSuperuser). SetIsSuperuser(usr.IsSuperuser).
SetGroupID(usr.GroupID). SetDefaultGroupID(usr.DefaultGroupID).
SetRole(role) SetRole(role).
AddGroupIDs(usr.DefaultGroupID)
// Only set password if provided (non-nil) // Only set password if provided (non-nil)
if usr.Password != nil { if usr.Password != nil {
@@ -126,10 +146,11 @@ func (r *UserRepository) CreateWithOIDC(ctx context.Context, usr UserCreate, iss
SetName(usr.Name). SetName(usr.Name).
SetEmail(usr.Email). SetEmail(usr.Email).
SetIsSuperuser(usr.IsSuperuser). SetIsSuperuser(usr.IsSuperuser).
SetGroupID(usr.GroupID). SetDefaultGroupID(usr.DefaultGroupID).
SetRole(role). SetRole(role).
SetOidcIssuer(issuer). SetOidcIssuer(issuer).
SetOidcSubject(subject) SetOidcSubject(subject).
AddGroupIDs(usr.DefaultGroupID)
if usr.Password != nil { if usr.Password != nil {
createQuery = createQuery.SetPassword(*usr.Password) createQuery = createQuery.SetPassword(*usr.Password)
@@ -183,6 +204,13 @@ func (r *UserRepository) SetOIDCIdentity(ctx context.Context, uid uuid.UUID, iss
func (r *UserRepository) GetOneOIDC(ctx context.Context, issuer, subject string) (UserOut, error) { func (r *UserRepository) GetOneOIDC(ctx context.Context, issuer, subject string) (UserOut, error) {
return mapUserOutErr(r.db.User.Query(). return mapUserOutErr(r.db.User.Query().
Where(user.OidcIssuerEQ(issuer), user.OidcSubjectEQ(subject)). Where(user.OidcIssuerEQ(issuer), user.OidcSubjectEQ(subject)).
WithGroup(). WithGroups().
Only(ctx)) Only(ctx))
} }
func (r *UserRepository) GetUsersByGroupID(ctx context.Context, gid uuid.UUID) ([]UserOut, error) {
return mapUsersOutErr(r.db.User.Query().
WithGroups().
Where(user.HasGroupsWith(group.ID(gid))).
All(ctx))
}

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"testing" "testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -15,7 +16,7 @@ func userFactory() UserCreate {
Email: fk.Email(), Email: fk.Email(),
Password: &password, Password: &password,
IsSuperuser: fk.Bool(), IsSuperuser: fk.Bool(),
GroupID: tGroup.ID, DefaultGroupID: tGroup.ID,
} }
} }
@@ -87,7 +88,8 @@ func TestUserRepo_GetAll(t *testing.T) {
assert.Equal(t, usr.Email, usr2.Email) assert.Equal(t, usr.Email, usr2.Email)
// Check groups are loaded // Check groups are loaded
assert.NotNil(t, usr2.GroupID) assert.NotEqual(t, uuid.Nil, usr2.DefaultGroupID)
assert.NotEmpty(t, usr2.GroupIDs)
} }
} }
} }

View File

@@ -108,12 +108,12 @@ func TestWipeInventory_Integration(t *testing.T) {
// 7. Test wipe inventory with all options enabled // 7. Test wipe inventory with all options enabled
deleted, err := tRepos.Items.WipeInventory(context.Background(), tGroup.ID, true, true, true) deleted, err := tRepos.Items.WipeInventory(context.Background(), tGroup.ID, true, true, true)
require.NoError(t, err) require.NoError(t, err)
assert.Greater(t, deleted, 0, "Should have deleted entities") assert.Positive(t, deleted, "Should have deleted entities")
// 8. Verify all items are deleted // 8. Verify all items are deleted
allItemsAfter, err := tRepos.Items.GetAll(context.Background(), tGroup.ID) allItemsAfter, err := tRepos.Items.GetAll(context.Background(), tGroup.ID)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 0, len(allItemsAfter), "All items should be deleted") assert.Empty(t, allItemsAfter, "All items should be deleted")
// 9. Verify maintenance entries are deleted // 9. Verify maintenance entries are deleted
maint1After, err := tRepos.MaintEntry.GetMaintenanceByItemID(context.Background(), tGroup.ID, item1.ID, MaintenanceFilters{}) maint1After, err := tRepos.MaintEntry.GetMaintenanceByItemID(context.Background(), tGroup.ID, item1.ID, MaintenanceFilters{})
@@ -169,7 +169,7 @@ func TestWipeInventory_SelectiveWipe(t *testing.T) {
// Test: Wipe only items (keep labels and locations) // Test: Wipe only items (keep labels and locations)
deleted, err := tRepos.Items.WipeInventory(context.Background(), tGroup.ID, false, false, false) deleted, err := tRepos.Items.WipeInventory(context.Background(), tGroup.ID, false, false, false)
require.NoError(t, err) require.NoError(t, err)
assert.Greater(t, deleted, 0, "Should have deleted at least items") assert.Positive(t, deleted, "Should have deleted at least items")
// Verify item is deleted // Verify item is deleted
_, err = tRepos.Items.GetOneByGroup(context.Background(), tGroup.ID, item.ID) _, err = tRepos.Items.GetOneByGroup(context.Background(), tGroup.ID, item.ID)

View File

@@ -241,15 +241,18 @@
"tags": [ "tags": [
"Group" "Group"
], ],
"summary": "Get Group", "summary": "Get All Groups",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.Group" "$ref": "#/definitions/repo.Group"
} }
} }
} }
}
}, },
"put": { "put": {
"security": [ "security": [
@@ -286,6 +289,31 @@
} }
}, },
"/v1/groups/invitations": { "/v1/groups/invitations": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Group"
],
"summary": "Get All Group Invitations",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.GroupInvitation"
}
}
}
}
},
"post": { "post": {
"security": [ "security": [
{ {
@@ -436,6 +464,147 @@
} }
} }
}, },
"/v1/groups/{id}": {
"post": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Group"
],
"summary": "Create Group",
"parameters": [
{
"description": "Group Name",
"name": "name",
"in": "body",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/repo.Group"
}
}
}
},
"delete": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Group"
],
"summary": "Delete Group",
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/v1/groups/{id}/members": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Group"
],
"summary": "Get All Group Members",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.UserOut"
}
}
}
}
},
"post": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Group"
],
"summary": "Add User to Group",
"parameters": [
{
"description": "User ID",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1.GroupMemberAdd"
}
}
],
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/v1/groups/{id}/members/{user_id}": {
"delete": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Group"
],
"summary": "Remove User from Group",
"parameters": [
{
"type": "string",
"description": "User ID",
"name": "user_id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/v1/items": { "/v1/items": {
"get": { "get": {
"security": [ "security": [
@@ -3527,6 +3696,10 @@
"description": "CreatedAt holds the value of the \"created_at\" field.", "description": "CreatedAt holds the value of the \"created_at\" field.",
"type": "string" "type": "string"
}, },
"default_group_id": {
"description": "DefaultGroupID holds the value of the \"default_group_id\" field.",
"type": "string"
},
"edges": { "edges": {
"description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the UserQuery when eager-loading is set.", "description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the UserQuery when eager-loading is set.",
"allOf": [ "allOf": [
@@ -3587,13 +3760,12 @@
"$ref": "#/definitions/ent.AuthTokens" "$ref": "#/definitions/ent.AuthTokens"
} }
}, },
"group": { "groups": {
"description": "Group holds the value of the group edge.", "description": "Groups holds the value of the groups edge.",
"allOf": [ "type": "array",
{ "items": {
"$ref": "#/definitions/ent.Group" "$ref": "#/definitions/ent.Group"
} }
]
}, },
"notifiers": { "notifiers": {
"description": "Notifiers holds the value of the notifiers edge.", "description": "Notifiers holds the value of the notifiers edge.",
@@ -3687,6 +3859,23 @@
} }
} }
}, },
"repo.GroupInvitation": {
"type": "object",
"properties": {
"expiresAt": {
"type": "string"
},
"group": {
"$ref": "#/definitions/repo.Group"
},
"id": {
"type": "string"
},
"uses": {
"type": "integer"
}
}
},
"repo.GroupStatistics": { "repo.GroupStatistics": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -4904,14 +5093,17 @@
"repo.UserOut": { "repo.UserOut": {
"type": "object", "type": "object",
"properties": { "properties": {
"defaultGroupId": {
"type": "string"
},
"email": { "email": {
"type": "string" "type": "string"
}, },
"groupId": { "groupIds": {
"type": "string" "type": "array",
}, "items": {
"groupName": {
"type": "string" "type": "string"
}
}, },
"id": { "id": {
"type": "string" "type": "string"
@@ -5132,6 +5324,17 @@
} }
} }
}, },
"v1.GroupMemberAdd": {
"type": "object",
"required": [
"userId"
],
"properties": {
"userId": {
"type": "string"
}
}
},
"v1.ItemAttachmentToken": { "v1.ItemAttachmentToken": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -719,6 +719,9 @@ definitions:
created_at: created_at:
description: CreatedAt holds the value of the "created_at" field. description: CreatedAt holds the value of the "created_at" field.
type: string type: string
default_group_id:
description: DefaultGroupID holds the value of the "default_group_id" field.
type: string
edges: edges:
allOf: allOf:
- $ref: '#/definitions/ent.UserEdges' - $ref: '#/definitions/ent.UserEdges'
@@ -761,10 +764,11 @@ definitions:
items: items:
$ref: '#/definitions/ent.AuthTokens' $ref: '#/definitions/ent.AuthTokens'
type: array type: array
group: groups:
allOf: description: Groups holds the value of the groups edge.
- $ref: '#/definitions/ent.Group' items:
description: Group holds the value of the group edge. $ref: '#/definitions/ent.Group'
type: array
notifiers: notifiers:
description: Notifiers holds the value of the notifiers edge. description: Notifiers holds the value of the notifiers edge.
items: items:
@@ -828,6 +832,17 @@ definitions:
updatedAt: updatedAt:
type: string type: string
type: object type: object
repo.GroupInvitation:
properties:
expiresAt:
type: string
group:
$ref: '#/definitions/repo.Group'
id:
type: string
uses:
type: integer
type: object
repo.GroupStatistics: repo.GroupStatistics:
properties: properties:
totalItemPrice: totalItemPrice:
@@ -1660,12 +1675,14 @@ definitions:
type: object type: object
repo.UserOut: repo.UserOut:
properties: properties:
defaultGroupId:
type: string
email: email:
type: string type: string
groupId: groupIds:
type: string items:
groupName:
type: string type: string
type: array
id: id:
type: string type: string
isOwner: isOwner:
@@ -1810,6 +1827,13 @@ definitions:
required: required:
- uses - uses
type: object type: object
v1.GroupMemberAdd:
properties:
userId:
type: string
required:
- userId
type: object
v1.ItemAttachmentToken: v1.ItemAttachmentToken:
properties: properties:
token: token:
@@ -2032,10 +2056,12 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
items:
$ref: '#/definitions/repo.Group' $ref: '#/definitions/repo.Group'
type: array
security: security:
- Bearer: [] - Bearer: []
summary: Get Group summary: Get All Groups
tags: tags:
- Group - Group
put: put:
@@ -2058,7 +2084,106 @@ paths:
summary: Update Group summary: Update Group
tags: tags:
- Group - Group
/v1/groups/{id}:
delete:
produces:
- application/json
responses:
"204":
description: No Content
security:
- Bearer: []
summary: Delete Group
tags:
- Group
post:
parameters:
- description: Group Name
in: body
name: name
required: true
schema:
type: string
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/repo.Group'
security:
- Bearer: []
summary: Create Group
tags:
- Group
/v1/groups/{id}/members:
get:
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/repo.UserOut'
type: array
security:
- Bearer: []
summary: Get All Group Members
tags:
- Group
post:
parameters:
- description: User ID
in: body
name: payload
required: true
schema:
$ref: '#/definitions/v1.GroupMemberAdd'
produces:
- application/json
responses:
"204":
description: No Content
security:
- Bearer: []
summary: Add User to Group
tags:
- Group
/v1/groups/{id}/members/{user_id}:
delete:
parameters:
- description: User ID
in: path
name: user_id
required: true
type: string
produces:
- application/json
responses:
"204":
description: No Content
security:
- Bearer: []
summary: Remove User from Group
tags:
- Group
/v1/groups/invitations: /v1/groups/invitations:
get:
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/repo.GroupInvitation'
type: array
security:
- Bearer: []
summary: Get All Group Invitations
tags:
- Group
post: post:
parameters: parameters:
- description: User Data - description: User Data

View File

@@ -242,19 +242,22 @@
"tags": [ "tags": [
"Group" "Group"
], ],
"summary": "Get Group", "summary": "Get All Groups",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/repo.Group" "$ref": "#/components/schemas/repo.Group"
} }
} }
} }
} }
} }
}
}, },
"put": { "put": {
"security": [ "security": [
@@ -292,6 +295,32 @@
} }
}, },
"/v1/groups/invitations": { "/v1/groups/invitations": {
"get": {
"security": [
{
"Bearer": []
}
],
"tags": [
"Group"
],
"summary": "Get All Group Invitations",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/repo.GroupInvitation"
}
}
}
}
}
}
},
"post": { "post": {
"security": [ "security": [
{ {
@@ -451,6 +480,142 @@
} }
} }
}, },
"/v1/groups/{id}": {
"post": {
"security": [
{
"Bearer": []
}
],
"tags": [
"Group"
],
"summary": "Create Group",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "Group Name",
"required": true
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/repo.Group"
}
}
}
}
}
},
"delete": {
"security": [
{
"Bearer": []
}
],
"tags": [
"Group"
],
"summary": "Delete Group",
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/v1/groups/{id}/members": {
"get": {
"security": [
{
"Bearer": []
}
],
"tags": [
"Group"
],
"summary": "Get All Group Members",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/repo.UserOut"
}
}
}
}
}
}
},
"post": {
"security": [
{
"Bearer": []
}
],
"tags": [
"Group"
],
"summary": "Add User to Group",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/v1.GroupMemberAdd"
}
}
},
"description": "User ID",
"required": true
},
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/v1/groups/{id}/members/{user_id}": {
"delete": {
"security": [
{
"Bearer": []
}
],
"tags": [
"Group"
],
"summary": "Remove User from Group",
"parameters": [
{
"description": "User ID",
"name": "user_id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/v1/items": { "/v1/items": {
"get": { "get": {
"security": [ "security": [
@@ -3727,6 +3892,10 @@
"description": "CreatedAt holds the value of the \"created_at\" field.", "description": "CreatedAt holds the value of the \"created_at\" field.",
"type": "string" "type": "string"
}, },
"default_group_id": {
"description": "DefaultGroupID holds the value of the \"default_group_id\" field.",
"type": "string"
},
"edges": { "edges": {
"description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the UserQuery when eager-loading is set.", "description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the UserQuery when eager-loading is set.",
"allOf": [ "allOf": [
@@ -3787,13 +3956,12 @@
"$ref": "#/components/schemas/ent.AuthTokens" "$ref": "#/components/schemas/ent.AuthTokens"
} }
}, },
"group": { "groups": {
"description": "Group holds the value of the group edge.", "description": "Groups holds the value of the groups edge.",
"allOf": [ "type": "array",
{ "items": {
"$ref": "#/components/schemas/ent.Group" "$ref": "#/components/schemas/ent.Group"
} }
]
}, },
"notifiers": { "notifiers": {
"description": "Notifiers holds the value of the notifiers edge.", "description": "Notifiers holds the value of the notifiers edge.",
@@ -3887,6 +4055,23 @@
} }
} }
}, },
"repo.GroupInvitation": {
"type": "object",
"properties": {
"expiresAt": {
"type": "string"
},
"group": {
"$ref": "#/components/schemas/repo.Group"
},
"id": {
"type": "string"
},
"uses": {
"type": "integer"
}
}
},
"repo.GroupStatistics": { "repo.GroupStatistics": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -5104,14 +5289,17 @@
"repo.UserOut": { "repo.UserOut": {
"type": "object", "type": "object",
"properties": { "properties": {
"defaultGroupId": {
"type": "string"
},
"email": { "email": {
"type": "string" "type": "string"
}, },
"groupId": { "groupIds": {
"type": "string" "type": "array",
}, "items": {
"groupName": {
"type": "string" "type": "string"
}
}, },
"id": { "id": {
"type": "string" "type": "string"
@@ -5332,6 +5520,17 @@
} }
} }
}, },
"v1.GroupMemberAdd": {
"type": "object",
"required": [
"userId"
],
"properties": {
"userId": {
"type": "string"
}
}
},
"v1.ItemAttachmentToken": { "v1.ItemAttachmentToken": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -142,13 +142,15 @@ paths:
- Bearer: [] - Bearer: []
tags: tags:
- Group - Group
summary: Get Group summary: Get All Groups
responses: responses:
"200": "200":
description: OK description: OK
content: content:
application/json: application/json:
schema: schema:
type: array
items:
$ref: "#/components/schemas/repo.Group" $ref: "#/components/schemas/repo.Group"
put: put:
security: security:
@@ -171,6 +173,21 @@ paths:
schema: schema:
$ref: "#/components/schemas/repo.Group" $ref: "#/components/schemas/repo.Group"
/v1/groups/invitations: /v1/groups/invitations:
get:
security:
- Bearer: []
tags:
- Group
summary: Get All Group Invitations
responses:
"200":
description: OK
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/repo.GroupInvitation"
post: post:
security: security:
- Bearer: [] - Bearer: []
@@ -262,6 +279,85 @@ paths:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/repo.ValueOverTime" $ref: "#/components/schemas/repo.ValueOverTime"
"/v1/groups/{id}":
post:
security:
- Bearer: []
tags:
- Group
summary: Create Group
requestBody:
content:
application/json:
schema:
type: string
description: Group Name
required: true
responses:
"201":
description: Created
content:
application/json:
schema:
$ref: "#/components/schemas/repo.Group"
delete:
security:
- Bearer: []
tags:
- Group
summary: Delete Group
responses:
"204":
description: No Content
"/v1/groups/{id}/members":
get:
security:
- Bearer: []
tags:
- Group
summary: Get All Group Members
responses:
"200":
description: OK
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/repo.UserOut"
post:
security:
- Bearer: []
tags:
- Group
summary: Add User to Group
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/v1.GroupMemberAdd"
description: User ID
required: true
responses:
"204":
description: No Content
"/v1/groups/{id}/members/{user_id}":
delete:
security:
- Bearer: []
tags:
- Group
summary: Remove User from Group
parameters:
- description: User ID
name: user_id
in: path
required: true
schema:
type: string
responses:
"204":
description: No Content
/v1/items: /v1/items:
get: get:
security: security:
@@ -2322,6 +2418,9 @@ components:
created_at: created_at:
description: CreatedAt holds the value of the "created_at" field. description: CreatedAt holds the value of the "created_at" field.
type: string type: string
default_group_id:
description: DefaultGroupID holds the value of the "default_group_id" field.
type: string
edges: edges:
description: >- description: >-
Edges holds the relations/edges for other nodes in the graph. Edges holds the relations/edges for other nodes in the graph.
@@ -2365,10 +2464,11 @@ components:
type: array type: array
items: items:
$ref: "#/components/schemas/ent.AuthTokens" $ref: "#/components/schemas/ent.AuthTokens"
group: groups:
description: Group holds the value of the group edge. description: Groups holds the value of the groups edge.
allOf: type: array
- $ref: "#/components/schemas/ent.Group" items:
$ref: "#/components/schemas/ent.Group"
notifiers: notifiers:
description: Notifiers holds the value of the notifiers edge. description: Notifiers holds the value of the notifiers edge.
type: array type: array
@@ -2431,6 +2531,17 @@ components:
type: string type: string
updatedAt: updatedAt:
type: string type: string
repo.GroupInvitation:
type: object
properties:
expiresAt:
type: string
group:
$ref: "#/components/schemas/repo.Group"
id:
type: string
uses:
type: integer
repo.GroupStatistics: repo.GroupStatistics:
type: object type: object
properties: properties:
@@ -3264,11 +3375,13 @@ components:
repo.UserOut: repo.UserOut:
type: object type: object
properties: properties:
defaultGroupId:
type: string
email: email:
type: string type: string
groupId: groupIds:
type: string type: array
groupName: items:
type: string type: string
id: id:
type: string type: string
@@ -3413,6 +3526,13 @@ components:
type: integer type: integer
maximum: 100 maximum: 100
minimum: 1 minimum: 1
v1.GroupMemberAdd:
type: object
required:
- userId
properties:
userId:
type: string
v1.ItemAttachmentToken: v1.ItemAttachmentToken:
type: object type: object
properties: properties:

View File

@@ -241,15 +241,18 @@
"tags": [ "tags": [
"Group" "Group"
], ],
"summary": "Get Group", "summary": "Get All Groups",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.Group" "$ref": "#/definitions/repo.Group"
} }
} }
} }
}
}, },
"put": { "put": {
"security": [ "security": [
@@ -286,6 +289,31 @@
} }
}, },
"/v1/groups/invitations": { "/v1/groups/invitations": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Group"
],
"summary": "Get All Group Invitations",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.GroupInvitation"
}
}
}
}
},
"post": { "post": {
"security": [ "security": [
{ {
@@ -436,6 +464,147 @@
} }
} }
}, },
"/v1/groups/{id}": {
"post": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Group"
],
"summary": "Create Group",
"parameters": [
{
"description": "Group Name",
"name": "name",
"in": "body",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/repo.Group"
}
}
}
},
"delete": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Group"
],
"summary": "Delete Group",
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/v1/groups/{id}/members": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Group"
],
"summary": "Get All Group Members",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.UserOut"
}
}
}
}
},
"post": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Group"
],
"summary": "Add User to Group",
"parameters": [
{
"description": "User ID",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1.GroupMemberAdd"
}
}
],
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/v1/groups/{id}/members/{user_id}": {
"delete": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Group"
],
"summary": "Remove User from Group",
"parameters": [
{
"type": "string",
"description": "User ID",
"name": "user_id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/v1/items": { "/v1/items": {
"get": { "get": {
"security": [ "security": [
@@ -3527,6 +3696,10 @@
"description": "CreatedAt holds the value of the \"created_at\" field.", "description": "CreatedAt holds the value of the \"created_at\" field.",
"type": "string" "type": "string"
}, },
"default_group_id": {
"description": "DefaultGroupID holds the value of the \"default_group_id\" field.",
"type": "string"
},
"edges": { "edges": {
"description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the UserQuery when eager-loading is set.", "description": "Edges holds the relations/edges for other nodes in the graph.\nThe values are being populated by the UserQuery when eager-loading is set.",
"allOf": [ "allOf": [
@@ -3587,13 +3760,12 @@
"$ref": "#/definitions/ent.AuthTokens" "$ref": "#/definitions/ent.AuthTokens"
} }
}, },
"group": { "groups": {
"description": "Group holds the value of the group edge.", "description": "Groups holds the value of the groups edge.",
"allOf": [ "type": "array",
{ "items": {
"$ref": "#/definitions/ent.Group" "$ref": "#/definitions/ent.Group"
} }
]
}, },
"notifiers": { "notifiers": {
"description": "Notifiers holds the value of the notifiers edge.", "description": "Notifiers holds the value of the notifiers edge.",
@@ -3687,6 +3859,23 @@
} }
} }
}, },
"repo.GroupInvitation": {
"type": "object",
"properties": {
"expiresAt": {
"type": "string"
},
"group": {
"$ref": "#/definitions/repo.Group"
},
"id": {
"type": "string"
},
"uses": {
"type": "integer"
}
}
},
"repo.GroupStatistics": { "repo.GroupStatistics": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -4904,14 +5093,17 @@
"repo.UserOut": { "repo.UserOut": {
"type": "object", "type": "object",
"properties": { "properties": {
"defaultGroupId": {
"type": "string"
},
"email": { "email": {
"type": "string" "type": "string"
}, },
"groupId": { "groupIds": {
"type": "string" "type": "array",
}, "items": {
"groupName": {
"type": "string" "type": "string"
}
}, },
"id": { "id": {
"type": "string" "type": "string"
@@ -5132,6 +5324,17 @@
} }
} }
}, },
"v1.GroupMemberAdd": {
"type": "object",
"required": [
"userId"
],
"properties": {
"userId": {
"type": "string"
}
}
},
"v1.ItemAttachmentToken": { "v1.ItemAttachmentToken": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -719,6 +719,9 @@ definitions:
created_at: created_at:
description: CreatedAt holds the value of the "created_at" field. description: CreatedAt holds the value of the "created_at" field.
type: string type: string
default_group_id:
description: DefaultGroupID holds the value of the "default_group_id" field.
type: string
edges: edges:
allOf: allOf:
- $ref: '#/definitions/ent.UserEdges' - $ref: '#/definitions/ent.UserEdges'
@@ -761,10 +764,11 @@ definitions:
items: items:
$ref: '#/definitions/ent.AuthTokens' $ref: '#/definitions/ent.AuthTokens'
type: array type: array
group: groups:
allOf: description: Groups holds the value of the groups edge.
- $ref: '#/definitions/ent.Group' items:
description: Group holds the value of the group edge. $ref: '#/definitions/ent.Group'
type: array
notifiers: notifiers:
description: Notifiers holds the value of the notifiers edge. description: Notifiers holds the value of the notifiers edge.
items: items:
@@ -828,6 +832,17 @@ definitions:
updatedAt: updatedAt:
type: string type: string
type: object type: object
repo.GroupInvitation:
properties:
expiresAt:
type: string
group:
$ref: '#/definitions/repo.Group'
id:
type: string
uses:
type: integer
type: object
repo.GroupStatistics: repo.GroupStatistics:
properties: properties:
totalItemPrice: totalItemPrice:
@@ -1660,12 +1675,14 @@ definitions:
type: object type: object
repo.UserOut: repo.UserOut:
properties: properties:
defaultGroupId:
type: string
email: email:
type: string type: string
groupId: groupIds:
type: string items:
groupName:
type: string type: string
type: array
id: id:
type: string type: string
isOwner: isOwner:
@@ -1810,6 +1827,13 @@ definitions:
required: required:
- uses - uses
type: object type: object
v1.GroupMemberAdd:
properties:
userId:
type: string
required:
- userId
type: object
v1.ItemAttachmentToken: v1.ItemAttachmentToken:
properties: properties:
token: token:
@@ -2032,10 +2056,12 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
items:
$ref: '#/definitions/repo.Group' $ref: '#/definitions/repo.Group'
type: array
security: security:
- Bearer: [] - Bearer: []
summary: Get Group summary: Get All Groups
tags: tags:
- Group - Group
put: put:
@@ -2058,7 +2084,106 @@ paths:
summary: Update Group summary: Update Group
tags: tags:
- Group - Group
/v1/groups/{id}:
delete:
produces:
- application/json
responses:
"204":
description: No Content
security:
- Bearer: []
summary: Delete Group
tags:
- Group
post:
parameters:
- description: Group Name
in: body
name: name
required: true
schema:
type: string
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/repo.Group'
security:
- Bearer: []
summary: Create Group
tags:
- Group
/v1/groups/{id}/members:
get:
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/repo.UserOut'
type: array
security:
- Bearer: []
summary: Get All Group Members
tags:
- Group
post:
parameters:
- description: User ID
in: body
name: payload
required: true
schema:
$ref: '#/definitions/v1.GroupMemberAdd'
produces:
- application/json
responses:
"204":
description: No Content
security:
- Bearer: []
summary: Add User to Group
tags:
- Group
/v1/groups/{id}/members/{user_id}:
delete:
parameters:
- description: User ID
in: path
name: user_id
required: true
type: string
produces:
- application/json
responses:
"204":
description: No Content
security:
- Bearer: []
summary: Remove User from Group
tags:
- Group
/v1/groups/invitations: /v1/groups/invitations:
get:
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/repo.GroupInvitation'
type: array
security:
- Bearer: []
summary: Get All Group Invitations
tags:
- Group
post: post:
parameters: parameters:
- description: User Data - description: User Data

View File

@@ -7,12 +7,16 @@ describe("first time user workflow (register, login, join group)", () => {
test("user should be able to update group", async () => { test("user should be able to update group", async () => {
const { client } = await factories.client.singleUse(); const { client } = await factories.client.singleUse();
const { data: user } = await client.user.self();
const name = faker.person.firstName(); const name = faker.person.firstName();
const { response, data: group } = await client.group.update({ const { response, data: group } = await client.group.update(
{
name, name,
currency: "eur", currency: "eur",
}); },
user.item.defaultGroupId
);
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(group.name).toBe(name); expect(group.name).toBe(name);
@@ -21,7 +25,8 @@ describe("first time user workflow (register, login, join group)", () => {
test("user should be able to get own group", async () => { test("user should be able to get own group", async () => {
const { client } = await factories.client.singleUse(); const { client } = await factories.client.singleUse();
const { response, data: group } = await client.group.get(); const { data: user } = await client.user.self();
const { response, data: group } = await client.group.get(user.item.defaultGroupId);
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(group.name).toBeTruthy(); expect(group.name).toBeTruthy();
@@ -57,7 +62,7 @@ describe("first time user workflow (register, login, join group)", () => {
const client2 = factories.client.user(loginData.token); const client2 = factories.client.user(loginData.token);
const { data: user2 } = await client2.user.self(); const { data: user2 } = await client2.user.self();
user2.item.groupName = user1.item.groupName; expect(user2.item.defaultGroupId).toBe(user1.item.defaultGroupId);
// Cleanup User 2 // Cleanup User 2
const { response: deleteResp } = await client2.user.delete(); const { response: deleteResp } = await client2.user.delete();

View File

@@ -15,16 +15,16 @@ export class GroupApi extends BaseAPI {
}); });
} }
update(data: GroupUpdate) { update(data: GroupUpdate, groupId?: string) {
return this.http.put<GroupUpdate, Group>({ return this.http.put<GroupUpdate, Group>({
url: route("/groups"), url: route(`/groups/${groupId || ""}`),
body: data, body: data,
}); });
} }
get() { get(groupId?: string) {
return this.http.get<Group>({ return this.http.get<Group>({
url: route("/groups"), url: route(`/groups/${groupId || ""}`),
}); });
} }

View File

@@ -508,6 +508,8 @@ export interface EntUser {
activated_on: string; activated_on: string;
/** CreatedAt holds the value of the "created_at" field. */ /** CreatedAt holds the value of the "created_at" field. */
created_at: string; created_at: string;
/** DefaultGroupID holds the value of the "default_group_id" field. */
default_group_id: string;
/** /**
* Edges holds the relations/edges for other nodes in the graph. * Edges holds the relations/edges for other nodes in the graph.
* The values are being populated by the UserQuery when eager-loading is set. * The values are being populated by the UserQuery when eager-loading is set.
@@ -536,8 +538,8 @@ export interface EntUser {
export interface EntUserEdges { export interface EntUserEdges {
/** AuthTokens holds the value of the auth_tokens edge. */ /** AuthTokens holds the value of the auth_tokens edge. */
auth_tokens: EntAuthTokens[]; auth_tokens: EntAuthTokens[];
/** Group holds the value of the group edge. */ /** Groups holds the value of the groups edge. */
group: EntGroup; groups: EntGroup[];
/** Notifiers holds the value of the notifiers edge. */ /** Notifiers holds the value of the notifiers edge. */
notifiers: EntNotifier[]; notifiers: EntNotifier[];
} }
@@ -570,6 +572,13 @@ export interface Group {
updatedAt: Date | string; updatedAt: Date | string;
} }
export interface GroupInvitation {
expiresAt: Date | string;
group: Group;
id: string;
uses: number;
}
export interface GroupStatistics { export interface GroupStatistics {
totalItemPrice: number; totalItemPrice: number;
totalItems: number; totalItems: number;
@@ -1027,9 +1036,9 @@ export interface TreeItem {
} }
export interface UserOut { export interface UserOut {
defaultGroupId: string;
email: string; email: string;
groupId: string; groupIds: string[];
groupName: string;
id: string; id: string;
isOwner: boolean; isOwner: boolean;
isSuperuser: boolean; isSuperuser: boolean;
@@ -1112,6 +1121,10 @@ export interface GroupInvitationCreate {
uses: number; uses: number;
} }
export interface GroupMemberAdd {
userId: string;
}
export interface ItemAttachmentToken { export interface ItemAttachmentToken {
token: string; token: string;
} }