feat: admin area with customers, users, settings, audit log, service status (phase 5)
- customer CRUD wizard backend: per-system credential shapes validated via
connector construction, AES-256-GCM at rest, write-only over the API
- ticketing connectors (jira/azure_devops/youtrack/demo) with test-connection
- user management: roles, disable (revokes sessions), force password reset,
delete; self-demotion/disable/delete guards
- admin-editable runtime settings ({_id:app}) incl. atomizer URL override
- audit log written on every privileged mutation + filterable list API
- service status panel: mongo/atomizer/work-performer health + latency with
late-bound breaker, sync and jobs status providers
- tabbed admin UI (customers wizard dialog, users table, settings, status, audit)
- compose: mongo nofile ulimit 64000 (1024 default crashed WiredTiger)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
|
||||
"bountyboard/internal/domain"
|
||||
"bountyboard/internal/ulid"
|
||||
)
|
||||
|
||||
func (s *Store) customers() *mongo.Collection { return s.DB.Collection("customers") }
|
||||
|
||||
func (s *Store) CreateCustomer(ctx context.Context, c *domain.Customer) error {
|
||||
now := time.Now().UTC()
|
||||
if c.ID == "" {
|
||||
c.ID = ulid.New()
|
||||
}
|
||||
c.CreatedAt, c.UpdatedAt, c.Version = now, now, 1
|
||||
if c.ConsultantIDs == nil {
|
||||
c.ConsultantIDs = []string{}
|
||||
}
|
||||
if _, err := s.customers().InsertOne(ctx, c); err != nil {
|
||||
if mongo.IsDuplicateKeyError(err) {
|
||||
return fmt.Errorf("customer %q: %w", c.Name, ErrDuplicate)
|
||||
}
|
||||
return fmt.Errorf("insert customer: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) CustomerByID(ctx context.Context, id string) (*domain.Customer, error) {
|
||||
var c domain.Customer
|
||||
err := s.customers().FindOne(ctx, bson.M{"_id": id}).Decode(&c)
|
||||
if errors.Is(err, mongo.ErrNoDocuments) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("find customer: %w", err)
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
// ListCustomers returns customers, optionally restricted to a consultant and
|
||||
// optionally including archived ones, sorted by name.
|
||||
func (s *Store) ListCustomers(ctx context.Context, consultantID string, includeArchived bool) ([]domain.Customer, error) {
|
||||
filter := bson.M{}
|
||||
if !includeArchived {
|
||||
filter["archived"] = false
|
||||
}
|
||||
if consultantID != "" {
|
||||
filter["consultantIds"] = consultantID
|
||||
}
|
||||
cur, err := s.customers().Find(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list customers: %w", err)
|
||||
}
|
||||
out := []domain.Customer{}
|
||||
if err := cur.All(ctx, &out); err != nil {
|
||||
return nil, fmt.Errorf("decode customers: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// UpdateCustomer applies a version-checked partial update.
|
||||
func (s *Store) UpdateCustomer(ctx context.Context, id string, version int64, set bson.M) error {
|
||||
return UpdateVersioned(ctx, s.customers(), id, version, bson.M{"$set": set})
|
||||
}
|
||||
|
||||
func (s *Store) DeleteCustomer(ctx context.Context, id string) error {
|
||||
res, err := s.customers().DeleteOne(ctx, bson.M{"_id": id})
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete customer: %w", err)
|
||||
}
|
||||
if res.DeletedCount == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetSyncStatus records the outcome of a sync run (not version-checked:
|
||||
// background bookkeeping must not race with admin edits).
|
||||
func (s *Store) SetSyncStatus(ctx context.Context, id string, at time.Time, status, errMsg string) error {
|
||||
_, err := s.customers().UpdateOne(ctx, bson.M{"_id": id}, bson.M{"$set": bson.M{
|
||||
"ticketing.lastSyncAt": at,
|
||||
"ticketing.lastSyncStatus": status,
|
||||
"ticketing.lastSyncError": errMsg,
|
||||
}})
|
||||
if err != nil {
|
||||
return fmt.Errorf("set sync status: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CountTasksForCustomer guards customer deletion.
|
||||
func (s *Store) CountTasksForCustomer(ctx context.Context, customerID string) (int64, error) {
|
||||
n, err := s.DB.Collection("tasks").CountDocuments(ctx, bson.M{"customerId": customerID})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("count tasks: %w", err)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
Reference in New Issue
Block a user