Skip to content

feat(users): quality pass + admin UX (sub-projects 1 & 2) - #43

Merged
antosubash merged 31 commits into
mainfrom
feature/goofy-ride-57cdce
Apr 20, 2026
Merged

feat(users): quality pass + admin UX (sub-projects 1 & 2)#43
antosubash merged 31 commits into
mainfrom
feature/goofy-ride-57cdce

Conversation

@antosubash

Copy link
Copy Markdown
Owner

Summary

Two sub-projects against the users module, branched before origin/main picked up the BackgroundTasks / Settings / Permissions / FileStorage modules and merged forward since:

Sub-project 1 — Quality pass

  • UserService raises UserNotFoundError and endpoints translate to HTTP 404; service uses flush() only (framework auto-commits).
  • Extracted _roles_payload helper in views; promoted PasswordHelper to module-top.
  • Renamed services.pystate.py, UsersServicesUsersState.
  • Module-owned Inertia auth.user projection via PrincipalSerializer registered on app.state.
  • Split test_models.py by model.

Sub-project 2 — Admin UX

  • UserListItem gains created_at (other fields preserved).
  • UserService.list_users accepts status / role_name / verified filters and sort / order (email, last_login_at, created_at; last_login_at always NULLS LAST).
  • /api/users/admin and /users/admin (view) accept the same params and echo sanitized values in Inertia filters props.
  • New PATCH /api/users/admin/{id}/verify (admin mark-verified, idempotent).
  • Admin detail page: Metadata card, Mark-verified action, AlertDialog confirmations for Disable and Copy-reset-link, cross-link into Permissions module gated on a new has_permissions_module prop.
  • Admin index page: filter controls (status/role/verified), clickable sort headers, URL-synced state.
  • Helpers IndexFilters.tsx and RolesTab.tsx live under components/ (outside pages/) to avoid SM003 orphan-page warnings.
  • Shared sanitize_list_filters helper in users/constants.py replaces duplicated filter-validation blocks; no private imports across endpoint modules.

Post-review simplifications

  • Dropped unused user_count field from RolesTab (never populated by server).
  • Made UserService.to_list_item synchronous (pure mapper, no I/O).
  • Removed WHAT-comments and trimmed redundant docstring lines.

Merge from origin/main

  • Aligned with #41 (Drop CSRF token middleware): replaced fetchWithCsrf(...) with plain fetch(...) in Edit.tsx across five call sites.
  • Merged middleware.py: retained this branch's PrincipalSerializer block, dropped the removed CSRF-token minting.

Public-contract invariants

  • UserListItem gained only created_at; no field removed.
  • GET /api/users/admin still accepts page, per_page, q; shape unchanged (filters/sort are additive kwargs).
  • PATCH /admin/{id}/{disable,enable}, PUT /admin/{id}/roles, POST /admin/{id}/reset-password-link unchanged.
  • Inertia page names Users/Users/Index and Users/Users/Edit preserved.
  • Event publication timing unchanged (UserInvited, UserDisabled, RoleAssigned).

Test plan

  • uv run pytest modules/users/tests/ -q — 191 passed
  • make lint — ruff / ty / biome / tsc / file-size / hardcoded-strings all green
  • make doctor — 0 errors, 0 warnings from users
  • Manual smoke: load /users/admin, apply each filter, toggle sort, open a user, mark verified, disable with confirmation, copy reset link, click Permissions cross-link

Module view routes mount at prefix + "/" (e.g. /dashboard/), so links
pointing to /dashboard (no slash) triggered FastAPI's redirect_slashes
307. Update menu items, Inertia links/visits, and server-side
RedirectResponses to the canonical trailing-slash form so the frontend
requests the route directly.
Hosting middleware no longer hardcodes the {id, name, email, roles}
shape. Modules register a principal_serializer on app.state during
register_settings; the users module owns the UserContext→dict projection.
Removes the last known module-schema leak from framework/hosting.
…slate to 404

Moves HTTP coupling out of UserService. Service raises domain exception;
api_admin.py and views.py catch-and-translate at the boundary. Preserves
404 status codes and 'User not found' detail strings.
Complies with CLAUDE.md convention that service code must not call
session.commit() — the per-request session auto-commits via after_flush.
…State

Disambiguates from service.py (holds UserService). The dataclass at
app.state.users is module state, not a 'services' collection.
Current file sat at 299 LOC (one under the cap). Split by responsibility:
user/role/user_role/access_token/constants. Every test keeps its original
name and body; only the containing file changes.
Filtering + sorting, confirmation flows, expanded detail page. Bulk actions deferred per scope call.
…cdce

# Conflicts:
#	modules/dashboard/dashboard/module.py
#	modules/products/products/endpoints/views.py
#	modules/products/products/module.py
#	modules/users/users/endpoints/api.py
- Note Permissions module (#37) as separate concern; add cross-link from detail page
- Reference PERM_USERS_MANAGE constant convention (#34)
- Confirm UserService stays concrete (Protocol drop #39 didn't touch it)
Wire status/role/verified/sort/order query params onto the Inertia
admin_index view, reusing _ALLOWED_* sets from api_admin.py, and
echo cleaned values into props as a filters dict.
Add status/role/verified filter selects, sortable Email/Last login/Created
column headers with URL-sync, and a Created column to the admin users page.
Extract filter selects to IndexFilters.tsx and roles tab to RolesTab.tsx
to stay under the 300-line cap.
…e case

When the Permissions module is installed via uv sync --all-packages,
it joins app.state.sm.modules and the flag flips to True.
Mirror the "true" case's dataclass.replace pattern to pin the expected state.
Extract filter + sort + mark_verified tests from test_service.py into
test_service_admin.py; extract TestAdminListFilters + TestAdminVerify
from test_api_admin.py into test_api_admin_filters.py; extract
TestAdminIndexFilters + TestHasPermissionsModuleFlag from test_views.py
into test_views_admin.py. 191 tests still pass.
- Wrap filters in useMemo in Index.tsx to avoid biome react-hooks violation
- Inline router.reload() to trim Edit.tsx back under 300-line cap
- Move IndexFilters and RolesTab helpers to components/ (prevents SM003 orphan page warning)
- Apply ruff/biome auto-formatting drift across touched files
- Drop unused user_count from RolesTab (never populated by server)
- Move admin-list filter allowed sets to constants.py and introduce
  sanitize_list_filters helper; views.py no longer imports private names
  from api_admin.py
- Make UserService.to_list_item synchronous (no I/O) and update callers
- Drop WHAT-comments in Edit.tsx and redundant docstring lines
…cdce

# Conflicts:
#	framework/hosting/simple_module_hosting/middleware.py
#	modules/users/users/pages/Users/Edit.tsx
These files are imported by already-committed modules but were never added
to git on origin/main, causing install-py (hatch build) to fail in CI with
a "Forced include not found" error. Adding them resolves:

- framework/core/simple_module_core/diagnostics/_module.py imports from
  _coupling.py and _js_workspace.py
- modules/settings/settings/endpoints/views.py imports from _module_settings.py
- scripts/new_module.py imports from _templates_js
- modules/file_storage and modules/permissions pyproject.toml force-include
  package.json / tsconfig.json
- Settings module registers a Modules.tsx page
…cdce

# Conflicts:
#	modules/permissions/permissions/pages/Browse.tsx
#	modules/users/users/endpoints/views.py
#	modules/users/users/pages/Users/Index.tsx
#	modules/users/users/service.py
@antosubash
antosubash merged commit a2394ac into main Apr 20, 2026
10 checks passed
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.

1 participant