Skip to content

Conversation

@tankerkiller125
Copy link
Contributor

What type of PR is this?

  • feature

What this PR does / why we need it:

Summary

This migration converts the User-Group relationship from 1:M (one group per user) to M:M (users can belong to multiple groups) while introducing multi-tenant support via the X-Tenant header.

Schema Changes

1. User Entity (backend/internal/data/ent/schema/user.go)

Removed:

  • GroupMixin{ref: "users"} from Mixin slice

Added:

  • default_group_id field (UUID, optional, nullable) - tracks user's primary tenant
  • M:M edge to Group: edge.To("groups", Group.Type)
// Fields added
field.UUID("default_group_id", uuid.UUID{}).
    Optional().
    Nillable(),

// Edges added
edge.To("groups", Group.Type),

2. Group Entity (backend/internal/data/ent/schema/group.go)

Changed:

  • Users edge from owned (1:M) to M:M relationship
  • Changed from: owned("users", User.Type)
  • Changed to: edge.To("users", User.Type)

Database Migrations

SQLite3 Migration (20251226000000_user_group_m2m.sql)

  1. Creates junction table: user_groups with composite primary key
  2. Migrates data: Copies existing group_users values to junction table
  3. Adds column: default_group_id to users table
  4. Sets defaults: Initializes default_group_id from current group
  5. Removes old FK: Drops group_users foreign key column

Rollback:

  • Recreates original schema with group_users foreign key
  • Populates from junction table using default_group_id as fallback

PostgreSQL Migration (20251226000000_user_group_m2m.sql)

Equivalent migration with PostgreSQL-specific syntax:

  • Uses CASCADE and NO ACTION for FK constraints
  • Drops constraints by name: users_groups_users
  • Handles nullable default_group_id with SET NULL

Code Changes

1. Repository Layer (backend/internal/data/repo/repo_users.go)

UserCreate struct:

DefaultGroupID uuid.UUID `json:"defaultGroupID"`  // was: GroupID

UserOut struct:

DefaultGroupID uuid.UUID   `json:"defaultGroupId"`  // was: GroupID
GroupIDs       []uuid.UUID `json:"groupIds"`       // was: GroupName

Query updates:

  • GetOneID(): .WithGroups() instead of .WithGroup()
  • GetOneEmail(): .WithGroups() instead of .WithGroup()
  • GetAll(): .WithGroups() instead of .WithGroup()
  • GetOneOIDC(): .WithGroups() instead of .WithGroup()

Create methods:

  • Use SetDefaultGroupID(usr.DefaultGroupID) instead of SetGroupID()
  • Add user to group via .AddGroupIDs(usr.DefaultGroupID)

2. Services Layer

service_user.go:

  • Updated UserCreate instantiation to use DefaultGroupID instead of GroupID
  • Uses DefaultGroupID for creating default labels/locations

service_group.go:

  • Removed automatic group name defaulting (no longer single group per user)
  • Now requires explicit group name in UpdateGroup

3. Context Management (backend/internal/core/services/contexts.go)

Context struct:

type Context struct {
    UID uuid.UUID  // User ID
    GID uuid.UUID  // Active tenant (from X-Tenant header or user's default)
    // User is the acting user with DefaultGroupID and GroupIDs
}

New functions:

  • UseTenantCtx(ctx) - retrieves active tenant from context
  • SetTenantCtx(ctx, tenantID) - sets active tenant in context
  • ContextTenant - context key for tenant

4. Middleware (backend/app/api/middleware.go)

New mwTenant middleware:

  • Parses X-Tenant header (optional)
  • Validates user has access to requested tenant
  • Falls back to user's DefaultGroupID if header not provided
  • Returns 403 Forbidden if user lacks access
  • Returns 400 Bad Request if header format is invalid
  • Sets tenant in context for use by services

Route integration:

userMW := []errchain.Middleware{
    a.mwAuthToken,
    a.mwTenant,  // NEW
    a.mwRoles(...),
}

5. API Handlers

Updated references from GroupID/GroupName to DefaultGroupID:

  • v1_ctrl_items.go: CSV import
  • v1_ctrl_reporting.go: Bill of materials export
  • demo.go: Demo setup CSV import

Usage

Default Behavior (No X-Tenant Header)

All requests use the user's DefaultGroupID as the active tenant.

curl -H "Authorization: Bearer TOKEN" \
     https://api.example.com/api/v1/groups/statistics

Multi-Tenant Requests (With X-Tenant Header)

Explicitly switch to a different tenant group.

curl -H "Authorization: Bearer TOKEN" \
     -H "X-Tenant: 550e8400-e29b-41d4-a716-446655440000" \
     https://api.example.com/api/v1/groups/statistics

Response codes:

  • 400 - Invalid X-Tenant UUID format
  • 403 - User not member of requested tenant
  • 200 - Success (tenant switched)

Backward Compatibility

✅ Existing clients continue working without modification:

  • Requests without X-Tenant header automatically use user's DefaultGroupID
  • All existing data is preserved during migration
  • User-group relationships migrated to junction table

Testing Recommendations

  1. Migration Safety:

    • Verify data integrity before/after migration
    • Test rollback path
    • Check both SQLite3 and PostgreSQL
  2. Multi-Tenant Features:

    • User with multiple groups can switch between them
    • Access control validates group membership
    • Invalid tenant IDs return 403 Forbidden
  3. Backward Compatibility:

    • Single-group users work without X-Tenant header
    • X-Tenant header is optional
  4. Default Group:

    • First group user creates/joins becomes default
    • Default is used when X-Tenant header absent

Generated Code

After running go generate ./... in backend/internal/data:

  • User entity includes DefaultGroupID field
  • User has M:M relationship methods: AddGroupIDs(), QueryGroups(), etc.
  • Group has M:M relationship methods: AddUserIDs(), QueryUsers(), etc.

Which issue(s) this PR fixes:

Implements part of #760

Special notes for your reviewer:

Still working on this.

Testing

Will need to validate tenant switching works properly, users can't force a tenant they don't belong to, etc.

- 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.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 27, 2025

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch mk/multi-groups

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Dec 28, 2025

Deploying homebox-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 24a49dd
Status: ✅  Deploy successful!
Preview URL: https://6e78f314.homebox-docs.pages.dev
Branch Preview URL: https://mk-multi-groups.homebox-docs.pages.dev

View logs

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure about having this be a store, might be better not to

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure on this one either, I'm no vue expert.

>
<Command :ignore-filter="true">
<CommandGroup>
<CommandItem as-child value="collection-settings">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

settings should prob be below create and join

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants