Skip to content

Commit d99d7b7

Browse files
Aurelioloclaude
andauthored
fix: harden setup wizard completion and status checks (#616)
## Summary Three setup wizard hardening fixes from PR #584 review: - **#588**: `needs_admin` now checks for CEO role explicitly via new `count_by_role(HumanRole)` method on `UserRepository` protocol, SQLite implementation, and fake test implementation -- instead of just checking `user_count == 0` - **#587**: `complete_setup` verifies company name and at least one agent exist (via settings) before marking setup complete, in addition to the existing provider check. Check order: company -> agents -> providers - **#589**: `SetupStatusResponse` now includes `min_password_length` from backend settings; `SetupAdmin.vue` reads it from the setup store instead of the hardcoded `MIN_PASSWORD_LENGTH` constant, with fallback to the constant if the server value is unavailable ## Changes ### Backend - `UserRepository` protocol: added `count_by_role(role: HumanRole) -> int` - `SQLiteUserRepository`: parameterized `SELECT COUNT(*) WHERE role = ?` implementation - `setup.py` controller: `get_setup_status` uses `count_by_role(CEO)` for `needs_admin`; `complete_setup` validates company + agents + providers; status response includes `min_password_length` - New observability events: `SETUP_NO_COMPANY`, `SETUP_NO_AGENTS`, `PERSISTENCE_USER_COUNTED_BY_ROLE`, `PERSISTENCE_USER_COUNT_BY_ROLE_FAILED` ### Frontend - `SetupStatusResponse` type: added `min_password_length: number` - `setup.ts` store: added `minPasswordLength` computed with `MIN_PASSWORD_LENGTH` fallback - `SetupAdmin.vue`: reads password length from store instead of hardcoded constant ### Docs - `CLAUDE.md` and `docs/design/operations.md`: clarified completion gate prerequisites ## Test plan - [x] `test_needs_admin_true_when_only_non_admin_exists` -- verifies CEO-specific check - [x] `test_needs_admin_false_when_ceo_exists` -- verifies default fixture passes - [x] `test_status_includes_min_password_length` -- verifies new field in response - [x] `test_complete_validates_all_prerequisites` -- walks through agents -> providers -> success checks - [x] All 9501 existing unit tests pass - [x] mypy strict, ruff lint+format, vue-tsc, ESLint all pass Pre-reviewed by 4 agents (code-reviewer, frontend+API-contract, docs-consistency, issue-resolution-verifier). 2 doc findings addressed. Closes #587 Closes #588 Closes #589 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Setup wizard now requires company info, at least one agent, and a provider before completion. * Setup status now exposes the system minimum password length; the admin setup UI uses this value for validation and messaging. * **Documentation** * User and developer docs updated to reflect the new setup prerequisites and displayed minimum password length. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent db02320 commit d99d7b7

17 files changed

Lines changed: 381 additions & 31 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ curl http://localhost:3000/api/v1/health # backend (via web proxy)
114114

115115
```text
116116
src/synthorg/
117-
api/ # Litestar REST + WebSocket API (controllers, guards, channels, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint, provider management endpoint (CRUD + test + presets), backup endpoint, setup endpoint (first-run wizard: status check, template listing, company/agent creation, completion gate), RFC 9457 structured errors (ErrorCategory, ErrorCode, ErrorDetail, ProblemDetail, CATEGORY_TITLES, category_title, category_type_uri, content negotiation)), AppState hot-reload slots (provider_registry, model_router with swap methods, provider_management), settings dispatcher lifecycle, logging bootstrap (_bootstrap_app_logging, SYNTHORG_LOG_DIR env var override, called before all other setup in create_app), service auto-wiring (auto_wire.py: Phase 1 at construction -- message bus/cost tracker/provider registry/task engine; Phase 2 in on_startup after persistence connects -- settings service + config resolver + provider management), lifecycle helpers (lifecycle.py: _safe_startup, _safe_shutdown, _cleanup_on_failure, _init_persistence, _try_stop)
117+
api/ # Litestar REST + WebSocket API (controllers, guards, channels, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint, provider management endpoint (CRUD + test + presets), backup endpoint, setup endpoint (first-run wizard: status check (CEO-role admin detection, min_password_length from settings), template listing, company/agent creation, completion gate (company + agent + provider verification)), RFC 9457 structured errors (ErrorCategory, ErrorCode, ErrorDetail, ProblemDetail, CATEGORY_TITLES, category_title, category_type_uri, content negotiation)), AppState hot-reload slots (provider_registry, model_router with swap methods, provider_management), settings dispatcher lifecycle, logging bootstrap (_bootstrap_app_logging, SYNTHORG_LOG_DIR env var override, called before all other setup in create_app), service auto-wiring (auto_wire.py: Phase 1 at construction -- message bus/cost tracker/provider registry/task engine; Phase 2 in on_startup after persistence connects -- settings service + config resolver + provider management), lifecycle helpers (lifecycle.py: _safe_startup, _safe_shutdown, _cleanup_on_failure, _init_persistence, _try_stop)
118118
auth/ # Authentication subpackage (controller, service, middleware, JWT + API key + WS ticket store, models, config, secret resolution)
119119
backup/ # Backup and restore -- scheduled/manual/lifecycle backups of persistence DB, agent memory, and company config. BackupService orchestrator, BackupScheduler (periodic asyncio task), RetentionManager (count + age pruning), tar.gz compression, SHA-256 checksums, manifest tracking, validated restore with atomic rollback and safety backup
120120
handlers/ # ComponentHandler protocol + concrete handlers: PersistenceComponentHandler (SQLite VACUUM INTO), MemoryComponentHandler (copytree), ConfigComponentHandler (copy2)
@@ -127,7 +127,7 @@ src/synthorg/
127127
engine/ # Agent orchestration, execution loops, parallel execution, task decomposition, routing, task assignment, centralized single-writer task state engine (TaskEngine), task lifecycle, recovery, shutdown, workspace isolation, coordination (multi-agent pipeline: TopologyDispatcher protocol, 4 dispatchers — SAS/centralized/decentralized/context-dependent, wave execution, workspace lifecycle integration, CoordinationSectionConfig company config bridge, build_coordinator factory), coordination error classification, prompt policy validation, checkpoint recovery (checkpoint/, per-turn persistence, heartbeat detection, CheckpointRecoveryStrategy), approval gate (escalation detection, context parking/resume, EscalationInfo/ResumePayload models), stagnation detection (stagnation/, StagnationDetector protocol, ToolRepetitionDetector, dual-signal analysis, corrective prompt injection), agent runtime state (AgentRuntimeState, lightweight per-agent execution status for dashboard queries and recovery), context budget management (context_budget.py, ContextBudgetIndicator, fill estimation, token estimation protocol in token_estimation.py), conversation compaction (compaction/, CompactionCallback type alias, CompactionConfig, CompressionMetadata, oldest-turns summarizer), execution loop auto-selection (loop_selector.py, AutoLoopConfig, AutoLoopRule, select_loop_type, build_execution_loop -- complexity-based loop routing with budget-aware downgrade, optional hybrid fallback, and configurable default_loop_type), hybrid execution loop (hybrid_loop.py, HybridLoop -- plan + mini-ReAct steps with per-step turn limits, progress-summary checkpoints, LLM-decided replanning; hybrid_models.py, HybridLoopConfig), shared plan helpers (plan_helpers.py, update_step_status, extract_task_summary, assess_step_success)
128128
hr/ # HR engine: hiring, firing, onboarding, offboarding, agent registry, performance tracking (task metrics, collaboration scoring, LLM calibration sampling, collaboration overrides, trend detection), promotion/demotion (criteria evaluation, approval strategies, model mapping)
129129
memory/ # Persistent agent memory (pluggable MemoryBackend protocol), backends/ (Mem0 adapter: backends/mem0/), retrieval pipeline (ranking, RRF fusion, injection, context formatting, non-inferable filtering), shared org memory (org/), consolidation/archival (consolidation/, dual-mode density-aware archival: DensityClassifier, AbstractiveSummarizer, ExtractivePreserver, DualModeConsolidationStrategy)
130-
persistence/ # Operational data persistence pluggable PersistenceBackend protocol, SQLite initial, SettingsRepository (namespaced settings CRUD) (see Memory & Persistence design page)
130+
persistence/ # Operational data persistence -- pluggable PersistenceBackend protocol, SQLite initial, SettingsRepository (namespaced settings CRUD), UserRepository (user CRUD + role-based counting) (see Memory & Persistence design page)
131131
observability/ # Structured logging (8-sink pipeline: console + 7 file sinks with logger-name routing), correlation tracking (request_id/task_id/agent_id via contextvars), sensitive field redaction, SYNTHORG_LOG_LEVEL env var override, critical sink enforcement (audit.log/access.log), log sinks
132132
providers/ # LLM provider abstraction (LiteLLM adapter), auth types (AuthType enum: api_key/oauth/custom_header/none), presets (ProviderPreset, PROVIDER_PRESETS for Ollama/LM Studio/OpenRouter/vLLM), runtime CRUD (management/ -- ProviderManagementService, asyncio.Lock-serialized create/update/delete/test, hot-reload of ProviderRegistry + ModelRouter via AppState swap)
133133
settings/ # Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces, including JSON type for structural data), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic models/collections), ConfigResolver (typed scalar + structural data accessors for controllers — get_agents, get_departments, get_provider_configs with validation fallbacks to YAML), validation, registry, change notifications via message bus, SettingsSubscriber protocol (subscriber.py), SettingsChangeDispatcher (dispatcher.py, polls #settings channel, routes to subscribers, restart_required filtering)

docs/design/operations.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -998,7 +998,7 @@ future CLI tool are thin clients that call the API -- they contain no business l
998998
| `/api/v1/analytics` | Performance metrics, dashboards |
999999
| `/api/v1/settings` | Runtime-editable configuration (9 namespaces), schema discovery |
10001000
| `GET /api/v1/providers`, `POST /api/v1/providers`, `PUT /api/v1/providers/{name}`, `DELETE /api/v1/providers/{name}`, `POST /api/v1/providers/{name}/test`, `GET /api/v1/providers/presets`, `POST /api/v1/providers/from-preset` | Provider CRUD, connection testing, presets, 4 auth types (api_key, oauth, custom_header, none) |
1001-
| `/api/v1/setup` | First-run setup wizard: status check (public), template listing, company/agent creation, completion gate |
1001+
| `/api/v1/setup` | First-run setup wizard: status check (public), template listing, company/agent creation, completion gate (requires company + agent + provider) |
10021002
| `/api/v1/admin/backups` | Manual backup, list, detail, delete |
10031003
| `/api/v1/ws` | WebSocket for real-time updates (ticket auth via `?ticket=`) |
10041004
| `POST /api/v1/auth/ws-ticket` | Exchange JWT for one-time WebSocket connection ticket |

docs/user_guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ After the containers are running, open the web dashboard at [http://localhost:30
6868
3. **Create your company** -- name your synthetic organization and optionally start from a template.
6969
4. **Hire your first agent** -- choose a role, model, and personality for the first AI agent.
7070

71-
After completing the wizard, the dashboard appears and the setup wizard is not shown again.
71+
All four steps must be completed -- the backend validates that a company, at least one agent, and at least one provider exist before allowing setup to finish. After completing the wizard, the dashboard appears and the setup wizard is not shown again.
7272

7373
To re-run the wizard later, use `synthorg setup` (resets the flag and opens the browser) or delete the `api.setup_complete` setting via the settings API.
7474

src/synthorg/api/controllers/setup.py

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99
from litestar.status_codes import HTTP_201_CREATED
1010
from pydantic import BaseModel, ConfigDict, Field, model_validator
1111

12+
from synthorg.api.auth.config import AuthConfig
1213
from synthorg.api.dto import ApiResponse
1314
from synthorg.api.errors import ApiValidationError, ConflictError, NotFoundError
14-
from synthorg.api.guards import require_read_access, require_write_access
15+
from synthorg.api.guards import HumanRole, require_read_access, require_write_access
1516
from synthorg.api.state import AppState # noqa: TC001
1617
from synthorg.core.enums import SeniorityLevel
1718
from synthorg.core.types import NotBlankStr # noqa: TC001
@@ -24,21 +25,30 @@
2425
SETUP_COMPANY_CREATED,
2526
SETUP_COMPLETED,
2627
SETUP_MODEL_NOT_FOUND,
28+
SETUP_NO_AGENTS,
29+
SETUP_NO_COMPANY,
2730
SETUP_NO_PROVIDERS,
2831
SETUP_PROVIDER_NOT_FOUND,
2932
SETUP_STATUS_CHECKED,
33+
SETUP_STATUS_SETTINGS_DEFAULT_USED,
3034
SETUP_STATUS_SETTINGS_UNAVAILABLE,
3135
SETUP_TEMPLATE_INVALID,
3236
SETUP_TEMPLATE_NOT_FOUND,
3337
SETUP_TEMPLATES_LISTED,
3438
)
39+
from synthorg.persistence.errors import QueryError
3540
from synthorg.settings.errors import SettingNotFoundError
3641

3742
if TYPE_CHECKING:
3843
from synthorg.settings.service import SettingsService
3944

4045
logger = get_logger(__name__)
4146

47+
# Derive from AuthConfig default to prevent silent divergence.
48+
_DEFAULT_MIN_PASSWORD_LENGTH: int = AuthConfig.model_fields[
49+
"min_password_length"
50+
].default
51+
4252
# Serializes read-modify-write on the agents settings blob.
4353
_AGENT_LOCK = asyncio.Lock()
4454

@@ -50,16 +60,18 @@ class SetupStatusResponse(BaseModel):
5060
"""First-run setup status.
5161
5262
Attributes:
53-
needs_admin: True if no admin user exists yet.
63+
needs_admin: True if no user with the CEO role exists yet.
5464
needs_setup: True if setup has not been completed.
5565
has_providers: True if at least one provider is configured.
66+
min_password_length: Backend-configured minimum password length.
5667
"""
5768

5869
model_config = ConfigDict(frozen=True)
5970

6071
needs_admin: bool
6172
needs_setup: bool
6273
has_providers: bool
74+
min_password_length: int = Field(ge=8)
6375

6476

6577
class TemplateInfoResponse(BaseModel):
@@ -215,8 +227,15 @@ async def get_status(
215227
app_state: AppState = state.app_state
216228
persistence = app_state.persistence
217229

218-
user_count = await persistence.users.count()
219-
needs_admin = user_count == 0
230+
try:
231+
admin_count = await persistence.users.count_by_role(HumanRole.CEO)
232+
except QueryError:
233+
logger.warning(
234+
SETUP_STATUS_SETTINGS_UNAVAILABLE,
235+
exc_info=True,
236+
)
237+
admin_count = 0
238+
needs_admin = admin_count == 0
220239

221240
settings_svc = app_state.settings_service
222241
try:
@@ -235,6 +254,34 @@ async def get_status(
235254
app_state.has_provider_registry and len(app_state.provider_registry) > 0
236255
)
237256

257+
min_password_length = _DEFAULT_MIN_PASSWORD_LENGTH
258+
raw_pw_value: str | None = None
259+
try:
260+
pw_entry = await settings_svc.get_entry("api", "min_password_length")
261+
raw_pw_value = pw_entry.value
262+
parsed = int(raw_pw_value)
263+
min_password_length = max(parsed, _DEFAULT_MIN_PASSWORD_LENGTH)
264+
except MemoryError, RecursionError:
265+
raise
266+
except SettingNotFoundError:
267+
logger.debug(
268+
SETUP_STATUS_SETTINGS_DEFAULT_USED,
269+
setting="min_password_length",
270+
)
271+
except ValueError:
272+
logger.warning(
273+
SETUP_STATUS_SETTINGS_UNAVAILABLE,
274+
setting="min_password_length",
275+
reason="non_integer_value",
276+
raw=raw_pw_value,
277+
)
278+
except Exception:
279+
logger.warning(
280+
SETUP_STATUS_SETTINGS_UNAVAILABLE,
281+
setting="min_password_length",
282+
exc_info=True,
283+
)
284+
238285
logger.debug(
239286
SETUP_STATUS_CHECKED,
240287
needs_admin=needs_admin,
@@ -247,6 +294,7 @@ async def get_status(
247294
needs_admin=needs_admin,
248295
needs_setup=needs_setup,
249296
has_providers=has_providers,
297+
min_password_length=min_password_length,
250298
),
251299
)
252300

@@ -421,8 +469,8 @@ async def complete_setup(
421469
) -> ApiResponse[SetupCompleteResponse]:
422470
"""Mark first-run setup as complete.
423471
424-
Validates that at least one provider is configured before
425-
allowing completion.
472+
Validates that a company, at least one agent, and at least one
473+
provider are configured before allowing completion.
426474
427475
Args:
428476
state: Application state.
@@ -431,16 +479,40 @@ async def complete_setup(
431479
Success envelope.
432480
433481
Raises:
434-
ApiValidationError: If no providers are configured.
482+
ConflictError: If setup has already been completed.
483+
ApiValidationError: If company, agents, or providers are missing.
435484
"""
436485
app_state: AppState = state.app_state
486+
settings_svc = app_state.settings_service
487+
await _check_setup_not_complete(settings_svc)
437488

489+
# Verify company has been created.
490+
has_company = False
491+
try:
492+
entry = await settings_svc.get_entry("company", "company_name")
493+
has_company = bool(entry.value and entry.value.strip())
494+
except MemoryError, RecursionError:
495+
raise
496+
except SettingNotFoundError:
497+
pass
498+
if not has_company:
499+
msg = "A company must be created before completing setup"
500+
logger.warning(SETUP_NO_COMPANY)
501+
raise ApiValidationError(msg)
502+
503+
# Verify at least one agent has been created.
504+
existing_agents = await _get_existing_agents(settings_svc)
505+
if not existing_agents:
506+
msg = "At least one agent must be created before completing setup"
507+
logger.warning(SETUP_NO_AGENTS)
508+
raise ApiValidationError(msg)
509+
510+
# Verify at least one provider is configured.
438511
if not app_state.has_provider_registry or len(app_state.provider_registry) == 0:
439512
msg = "At least one provider must be configured before completing setup"
440513
logger.warning(SETUP_NO_PROVIDERS)
441514
raise ApiValidationError(msg)
442515

443-
settings_svc = app_state.settings_service
444516
await settings_svc.set("api", "setup_complete", "true")
445517

446518
logger.info(SETUP_COMPLETED)

src/synthorg/observability/events/persistence.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@
132132
PERSISTENCE_USER_LIST_FAILED: Final[str] = "persistence.user.list_failed"
133133
PERSISTENCE_USER_COUNTED: Final[str] = "persistence.user.counted"
134134
PERSISTENCE_USER_COUNT_FAILED: Final[str] = "persistence.user.count_failed"
135+
PERSISTENCE_USER_COUNTED_BY_ROLE: Final[str] = "persistence.user.counted_by_role"
136+
PERSISTENCE_USER_COUNT_BY_ROLE_FAILED: Final[str] = (
137+
"persistence.user.count_by_role_failed"
138+
)
135139
PERSISTENCE_USER_DELETED: Final[str] = "persistence.user.deleted"
136140
PERSISTENCE_USER_DELETE_FAILED: Final[str] = "persistence.user.delete_failed"
137141

src/synthorg/observability/events/setup.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
# Status check fallback (settings service unavailable)
3232
SETUP_STATUS_SETTINGS_UNAVAILABLE: Final[str] = "setup.status.settings_unavailable"
3333

34+
# Status check used a default value for a setting (entry absent or not configured)
35+
SETUP_STATUS_SETTINGS_DEFAULT_USED: Final[str] = "setup.status.settings_default_used"
36+
3437
# Provider not found during agent creation
3538
SETUP_PROVIDER_NOT_FOUND: Final[str] = "setup.agent.provider_not_found"
3639

@@ -40,6 +43,12 @@
4043
# No providers configured when attempting to complete setup
4144
SETUP_NO_PROVIDERS: Final[str] = "setup.flow.no_providers"
4245

46+
# No company created when attempting to complete setup
47+
SETUP_NO_COMPANY: Final[str] = "setup.flow.no_company"
48+
49+
# No agents created when attempting to complete setup
50+
SETUP_NO_AGENTS: Final[str] = "setup.flow.no_agents"
51+
4352
# Template not found during company creation
4453
SETUP_TEMPLATE_NOT_FOUND: Final[str] = "setup.company.template_not_found"
4554

src/synthorg/persistence/repositories.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from pydantic import AwareDatetime # noqa: TC002
1010

1111
from synthorg.api.auth.models import ApiKey, User # noqa: TC001
12+
from synthorg.api.guards import HumanRole # noqa: TC001
1213
from synthorg.budget.cost_record import CostRecord # noqa: TC001
1314
from synthorg.communication.message import Message # noqa: TC001
1415
from synthorg.core.enums import ApprovalRiskLevel, TaskStatus # noqa: TC001
@@ -396,6 +397,20 @@ async def count(self) -> int:
396397
"""
397398
...
398399

400+
async def count_by_role(self, role: HumanRole) -> int:
401+
"""Count users with a specific role.
402+
403+
Args:
404+
role: The role to filter by.
405+
406+
Returns:
407+
Number of users with the given role.
408+
409+
Raises:
410+
PersistenceError: If the operation fails.
411+
"""
412+
...
413+
399414
async def delete(self, user_id: NotBlankStr) -> bool:
400415
"""Delete a user by ID.
401416

src/synthorg/persistence/sqlite/user_repo.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@
2424
PERSISTENCE_API_KEY_LISTED,
2525
PERSISTENCE_API_KEY_SAVE_FAILED,
2626
PERSISTENCE_API_KEY_SAVED,
27+
PERSISTENCE_USER_COUNT_BY_ROLE_FAILED,
2728
PERSISTENCE_USER_COUNT_FAILED,
2829
PERSISTENCE_USER_COUNTED,
30+
PERSISTENCE_USER_COUNTED_BY_ROLE,
2931
PERSISTENCE_USER_DELETE_FAILED,
3032
PERSISTENCE_USER_DELETED,
3133
PERSISTENCE_USER_FETCH_FAILED,
@@ -262,6 +264,40 @@ async def count(self) -> int:
262264
logger.debug(PERSISTENCE_USER_COUNTED, count=result)
263265
return result
264266

267+
async def count_by_role(self, role: HumanRole) -> int:
268+
"""Return the number of users with the given role.
269+
270+
Args:
271+
role: The role to filter by.
272+
273+
Returns:
274+
Non-negative integer count.
275+
276+
Raises:
277+
QueryError: If the database query fails.
278+
"""
279+
try:
280+
cursor = await self._db.execute(
281+
"SELECT COUNT(*) FROM users WHERE role = ?",
282+
(role.value,),
283+
)
284+
row = await cursor.fetchone()
285+
except (sqlite3.Error, aiosqlite.Error) as exc:
286+
msg = "Failed to count users by role"
287+
logger.exception(
288+
PERSISTENCE_USER_COUNT_BY_ROLE_FAILED,
289+
role=role.value,
290+
error=str(exc),
291+
)
292+
raise QueryError(msg) from exc
293+
result = int(row[0]) if row else 0
294+
logger.debug(
295+
PERSISTENCE_USER_COUNTED_BY_ROLE,
296+
role=role.value,
297+
count=result,
298+
)
299+
return result
300+
265301
async def delete(self, user_id: NotBlankStr) -> bool:
266302
"""Delete a user by primary key.
267303

0 commit comments

Comments
 (0)