feat: add groups management with CRUD operations and policy target assignment

Add Group type with id, name, description, members array and optional user_ids field. Add name field to policy targets for display. Add groups API client methods for list, create, update and delete operations. Add GroupsPage component with create form, edit modal, member selection and table view. Add groups route and navigation item to Layout. Add reusable Modal component with title, subtitle and close handler. Update
This commit is contained in:
2026-03-17 21:42:46 +01:00
parent 0986a36aca
commit a8fbe725a2
17 changed files with 900 additions and 103 deletions

View File

@@ -52,6 +52,11 @@ func (r *PGRepository) List(ctx context.Context) ([]Policy, error) {
if err := rows.Scan(&item.ID, &item.Name, &item.Description, &item.Priority, &item.Effect, &item.FullTunnel, &item.IsActive, &item.Destinations); err != nil {
return nil, err
}
targets, err := r.listTargets(ctx, item.ID)
if err != nil {
return nil, err
}
item.Targets = targets
items = append(items, item)
}
return items, rows.Err()
@@ -95,18 +100,7 @@ func (r *PGRepository) Create(ctx context.Context, input CreateRequest, createdB
return Policy{}, err
}
inputPolicy := Policy{
ID: policyID,
Name: input.Name,
Description: input.Description,
Priority: input.Priority,
Effect: input.Effect,
FullTunnel: input.FullTunnel,
IsActive: true,
Destinations: input.Destinations,
Targets: input.Targets,
}
return inputPolicy, nil
return r.getByID(ctx, policyID)
}
func (r *PGRepository) Update(ctx context.Context, policyID uuid.UUID, input UpdateRequest) (Policy, error) {
@@ -164,19 +158,7 @@ func (r *PGRepository) Update(ctx context.Context, policyID uuid.UUID, input Upd
return Policy{}, err
}
items, err := r.List(ctx)
if err != nil {
return Policy{}, err
}
for _, item := range items {
if item.ID == policyID {
if input.Targets != nil {
item.Targets = input.Targets
}
return item, nil
}
}
return Policy{}, errors.New("policy not found after update")
return r.getByID(ctx, policyID)
}
func (r *PGRepository) Delete(ctx context.Context, policyID uuid.UUID) error {
@@ -195,6 +177,10 @@ func (r *PGRepository) ResolveDestinations(ctx context.Context, userID uuid.UUID
and p.effect = 'allow'
and (
(pt.target_type = 'user' and pt.target_id = $1)
or (pt.target_type = 'group' and exists (
select 1 from group_memberships gm
where gm.group_id = pt.target_id and gm.user_id = $1
))
`
args := []any{userID}
if deviceID != nil {
@@ -219,3 +205,46 @@ func (r *PGRepository) ResolveDestinations(ctx context.Context, userID uuid.UUID
}
return destinations, rows.Err()
}
func (r *PGRepository) getByID(ctx context.Context, policyID uuid.UUID) (Policy, error) {
items, err := r.List(ctx)
if err != nil {
return Policy{}, err
}
for _, item := range items {
if item.ID == policyID {
return item, nil
}
}
return Policy{}, errors.New("policy not found")
}
func (r *PGRepository) listTargets(ctx context.Context, policyID uuid.UUID) ([]Target, error) {
rows, err := r.db.Query(ctx, `
select
pt.target_type,
pt.target_id,
coalesce(u.username, g.name, d.name, '')
from policy_targets pt
left join users u on pt.target_type = 'user' and u.id = pt.target_id and u.deleted_at is null
left join groups g on pt.target_type = 'group' and g.id = pt.target_id and g.deleted_at is null
left join devices d on pt.target_type = 'device' and d.id = pt.target_id and d.deleted_at is null
where pt.policy_id = $1
order by pt.created_at asc
`, policyID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Target
for rows.Next() {
var item Target
if err := rows.Scan(&item.Type, &item.ID, &item.Name); err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}

View File

@@ -5,6 +5,7 @@ import "github.com/google/uuid"
type Target struct {
Type string `json:"type"`
ID uuid.UUID `json:"id"`
Name string `json:"name,omitempty"`
}
type Policy struct {