package store import ( "context" "fmt" "time" "github.com/jackc/pgx/v5" "github.com/loyaly/behavision-server/internal/api" ) func (s *Store) SetClientActive(ctx context.Context, clientID string, active bool) (api.ClientRow, int, error) { var row api.ClientRow tx, err := s.pool.Begin(ctx) if err != nil { return row, 0, err } defer tx.Rollback(ctx) //nolint:errcheck var at time.Time if err := tx.QueryRow(ctx, ` UPDATE clients SET active = $2 WHERE id = $1::uuid RETURNING id::text, slug, name, active, created_at, (SELECT count(*) FROM sites WHERE client_id = clients.id), (SELECT count(*) FROM app_users WHERE client_id = clients.id)`, clientID, active). Scan(&row.ID, &row.Slug, &row.Name, &row.Active, &at, &row.Sites, &row.Users); err != nil { return row, 0, err } row.CreatedAt = at.UTC().Format(time.RFC3339) revoked := 0 if !active { tag, err := tx.Exec(ctx, ` UPDATE sessions SET revoked_at = now() WHERE revoked_at IS NULL AND user_id IN (SELECT id FROM app_users WHERE client_id = $1::uuid)`, clientID) if err != nil { return row, 0, fmt.Errorf("revoke sessions: %w", err) } revoked = int(tag.RowsAffected()) } return row, revoked, tx.Commit(ctx) } func (s *Store) ClientOwners(ctx context.Context, clientID string) ([]api.TeamMember, error) { rows, err := s.pool.Query(ctx, ` SELECT id::text, email, full_name, role, active FROM app_users WHERE client_id = $1::uuid AND role = 'owner' AND active ORDER BY created_at`, clientID) if err != nil { return nil, err } defer rows.Close() var out []api.TeamMember for rows.Next() { var m api.TeamMember if err := rows.Scan(&m.ID, &m.Email, &m.FullName, &m.Role, &m.Active); err != nil { return nil, err } out = append(out, m) } return out, rows.Err() } func (s *Store) ClientImageKeys(ctx context.Context, clientID string) ([]string, error) { rows, err := s.pool.Query(ctx, ` SELECT image_key FROM visits WHERE client_id = $1::uuid AND image_key <> '' AND image_deleted_at IS NULL`, clientID) if err != nil { return nil, err } defer rows.Close() var keys []string for rows.Next() { var k string if err := rows.Scan(&k); err != nil { return nil, err } keys = append(keys, k) } return keys, rows.Err() } // DeleteClient relies on ON DELETE CASCADE from clients, which every tenant // table declares (001-011). The broker usernames are read before the rows go, // because afterwards nothing remembers them. func (s *Store) DeleteClient(ctx context.Context, clientID string) (api.ClientRow, []string, error) { var row api.ClientRow tx, err := s.pool.Begin(ctx) if err != nil { return row, nil, err } defer tx.Rollback(ctx) //nolint:errcheck if err := tx.QueryRow(ctx, `SELECT id::text, slug, name, active FROM clients WHERE id = $1::uuid FOR UPDATE`, clientID). Scan(&row.ID, &row.Slug, &row.Name, &row.Active); err != nil { return row, nil, err } if row.Active { return row, nil, fmt.Errorf("client %s is active; suspend it first", row.Slug) } rows, err := tx.Query(ctx, `SELECT mqtt_username FROM agents WHERE client_id = $1::uuid`, clientID) if err != nil { return row, nil, err } var users []string for rows.Next() { var u string if err := rows.Scan(&u); err != nil { rows.Close() return row, nil, err } users = append(users, u) } rows.Close() if _, err := tx.Exec(ctx, `DELETE FROM clients WHERE id = $1::uuid`, clientID); err != nil { return row, nil, fmt.Errorf("delete client: %w", err) } return row, users, tx.Commit(ctx) } func (s *Store) DeleteEmptySite(ctx context.Context, clientID, siteID string) (string, error) { tx, err := s.pool.Begin(ctx) if err != nil { return "", err } defer tx.Rollback(ctx) //nolint:errcheck var used bool if err := tx.QueryRow(ctx, ` SELECT EXISTS (SELECT 1 FROM visits WHERE site_id = $1::uuid) OR EXISTS (SELECT 1 FROM site_cameras WHERE site_id = $1::uuid AND deleted_at IS NULL)`, siteID).Scan(&used); err != nil { return "", err } if used { return "", api.ErrSiteInUse } var username string if err := tx.QueryRow(ctx, `SELECT COALESCE((SELECT mqtt_username FROM agents WHERE site_id = $1::uuid LIMIT 1), '')`, siteID).Scan(&username); err != nil { return "", err } tag, err := tx.Exec(ctx, `DELETE FROM sites WHERE id = $1::uuid AND client_id = $2::uuid`, siteID, clientID) if err != nil { return "", err } if tag.RowsAffected() == 0 { return "", pgx.ErrNoRows } return username, tx.Commit(ctx) } func (s *Store) UpdateSite(ctx context.Context, clientID, siteID string, in api.SiteUpdate) (api.SiteHealth, error) { var out api.SiteHealth err := s.pool.QueryRow(ctx, ` UPDATE sites SET name = COALESCE($3, name), timezone = COALESCE($4, timezone) WHERE id = $1::uuid AND client_id = $2::uuid RETURNING id::text, slug, name, timezone`, siteID, clientID, in.Name, in.Timezone). Scan(&out.SiteID, &out.Slug, &out.Name, &out.Timezone) return out, err }