Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions backend/app/api/routes/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,17 @@ def get_users(
return db.query(User).all()


@router.get("/users/students")
def get_student_users(
db: Session = Depends(get_db),
current_user: User = Depends(require_roles("admin", "teacher")),
):
return db.query(User).filter(User.role == "student").all()


@router.get("/users/{user_id}")
def get_user_by_id(
user_id: str,
user_id: uuid.UUID,
db: Session = Depends(get_db),
current_user: User = Depends(require_roles("admin")),
):
Expand All @@ -31,17 +39,9 @@ def get_user_by_id(
return user


@router.get("/users/students")
def get_student_users(
db: Session = Depends(get_db),
current_user: User = Depends(require_roles("admin", "teacher")),
):
return db.query(User).filter(User.role == "student").all()


@router.put("/users/{user_id}")
def update_user(
user_id: str,
user_id: uuid.UUID,
user: UserDto,
db: Session = Depends(get_db),
current_user: User = Depends(require_roles("admin")),
Expand Down
88 changes: 88 additions & 0 deletions backend/tests/test_admin_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import uuid

from fastapi.testclient import TestClient

from app.auth import get_current_user
from app.database import get_db
from app.main import app
from app.models import User


def _user(role: str, prefix: str) -> User:
suffix = uuid.uuid4().hex[:8]
return User(
id=uuid.uuid4(),
firstname=prefix.title(),
lastname="User",
username=f"{prefix}_{suffix}",
email=f"{prefix}_{suffix}@example.test",
hashed_password="unchanged-password-hash",
role=role,
)


def _client(db_session, current_user: User) -> TestClient:
app.dependency_overrides[get_db] = lambda: db_session
app.dependency_overrides[get_current_user] = lambda: current_user
return TestClient(app)


def test_admin_can_fetch_user_detail_by_uuid_on_sqlite(db_session):
admin = _user("admin", "admin")
student = _user("student", "student")
db_session.add_all([admin, student])
db_session.commit()

client = _client(db_session, admin)
try:
response = client.get(f"/api/users/{student.id}")
finally:
app.dependency_overrides.clear()

assert response.status_code == 200
assert response.json()["id"] == str(student.id)
assert response.json()["role"] == "student"


def test_admin_can_update_user_role_by_uuid_without_changing_password(db_session):
admin = _user("admin", "admin")
student = _user("student", "student")
db_session.add_all([admin, student])
db_session.commit()

client = _client(db_session, admin)
try:
response = client.put(
f"/api/users/{student.id}",
json={
"firstname": student.firstname,
"lastname": student.lastname,
"username": student.username,
"email": student.email,
"password": "",
"role": "teacher",
},
)
finally:
app.dependency_overrides.clear()

db_session.refresh(student)
assert response.status_code == 200
assert student.role == "teacher"
assert student.hashed_password == "unchanged-password-hash"


def test_static_student_list_route_is_not_shadowed_by_user_detail(db_session):
teacher = _user("teacher", "teacher")
student = _user("student", "student")
db_session.add_all([teacher, student])
db_session.commit()

client = _client(db_session, teacher)
try:
response = client.get("/api/users/students")
finally:
app.dependency_overrides.clear()

assert response.status_code == 200
assert [item["id"] for item in response.json()] == [str(student.id)]
22 changes: 22 additions & 0 deletions docs/ux-overhaul/admin-accessibility-review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Admin Accessibility Review

Date: 2026-07-13

## Implemented Checks

- One named page `h1`, logical section headings, semantic `main`, tables, captions, headers, definition lists, forms, labels, and buttons.
- Desktop semantic tables have equivalent mobile records with the same identity, status, and primary action.
- Search/filter result counts and asynchronous states use live status semantics through shared components.
- Status always includes text; no meaning relies on color.
- Controls have 44px minimum height, visible tokenized focus rings, and no hover-only actions.
- Shared confirmation dialog traps focus, supports Escape, restores focus, names the object and consequence, and exposes action errors.
- Badge images use meaningful alt text; missing images have a named alternative.
- Form validation associates visible labels and `aria-invalid`; badge form errors are announced.
- Long names, usernames, and email addresses wrap in detail definitions.
- Existing shell skip link, mobile drawer focus behavior, and reduced-motion foundation remain unchanged.

## Validation

Karma coverage exercises state semantics, privacy omission, confirmation gating, navigation, deep-link roles, and image naming. Seeded Playwright covers primary Admin navigation, filtering, dialog cancel, unauthorized deep link, and 390px overflow when services are available.

Manual screen-reader and browser zoom execution remains open with seeded browser infrastructure. Sorting and pagination are not present because current APIs return unpaged lists and this phase does not invent server behavior.
14 changes: 14 additions & 0 deletions docs/ux-overhaul/admin-action-risk-matrix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Admin Action Risk Matrix

Date: 2026-07-13

| Action | Classification | Object / backend behavior | Reversible | Confirmation / permission | Recovery and audit gap |
| --- | --- | --- | --- | --- | --- |
| Review user/course/org/badge/report | Informational | GET only | N/A | Guarded route and backend auth | Retryable partial/error states. |
| Create badge | Consequential | Creates badge and optional uploaded image | No delete API | Valid form; `admin` | Failure leaves UI unchanged; uploaded orphan cleanup not supported. |
| Change platform role | Privilege-changing | Rewrites full user DTO including role | Yes by another update | Names user and access impact; `admin`; self-change disabled | Failure retains old role; no audit/final-admin enforcement. |
| Delete user | Destructive | Deletes user plus authored posts/threads, badge awards, and unit links | No | Names user, scope, unaffected objects, irreversibility; `admin`; self-delete disabled in UI | Failure retains record; no restore/audit/final-admin protection. |
| Delete course | Destructive | Deletes course record | No | Names course, learner-access impact, no archive/restore; `admin` | Failure retains record; no restore/audit. |
| Switch organization | Consequential | Reissues token with active organization | Yes | Existing shell interaction | Existing session recovery. Not redesigned here. |

Unsupported disable/restore, invitation resend, organization archive/delete, badge edit/delete, moderation, and platform-setting actions are absent.
20 changes: 20 additions & 0 deletions docs/ux-overhaul/admin-badge-data-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Admin Badge Data Contract

Date: 2026-07-13

| Field/capability | Verified contract | Admin UI behavior |
| --- | --- | --- |
| ID | UUID | Used only for API identity; not displayed. |
| Name | `title`, required | Displayed and searchable. |
| Description | Optional text | Displayed; missing value is stated. |
| Image | Optional `image_url` | Named image alt text; explicit missing-image alternative. |
| Earning criteria | No field/API | Not invented or editable. |
| Course/program relation | No badge relation in response | Not shown. Certifications may reference a badge separately. |
| Active/inactive | No field/API | Not shown. |
| Create | `POST /api/badges`, `admin` | Supported with validation and success/error states. |
| Upload | `POST /api/upload/badge`, `admin` | Optional image upload during creation. |
| Edit | No endpoint | Read-only unavailable. |
| Delete/archive | No endpoint | No control or confirmation shown. |
| Assignment | `POST /api/students/{student_id}/badges/{badge_id}`, `admin` | Verified but not added to broad badge administration because no scoped learner-selection workflow was approved. |

Badge reads require authentication but are not admin-only. `super_admin` therefore receives a read-only catalog; it receives no create/upload control.
13 changes: 13 additions & 0 deletions docs/ux-overhaul/admin-moderation-capability-map.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Admin Moderation Capability Map

Date: 2026-07-13

| Content type | Read API | Action API | Authorization | Reversible/auditable | Admin decision |
| --- | --- | --- | --- | --- | --- |
| Threads | `GET /api/threads*` | Create/update/delete | None | Delete is irreversible; no audit | Not exposed. APIs have no auth, scope, ownership, flag, lock, hide, restore, or moderator contract. |
| Posts | `GET /api/posts*` | Create/update/delete | None | Delete is irreversible; no audit | Not exposed for the same reason. |
| Course discussions | No scoped moderation API | None | None verified | No | Not exposed. |
| Uploaded content | No asset-list API | Upload only | Role-specific upload checks | No delete/audit | No moderation or asset-library UI. Badge upload remains inside badge creation. |
| Flags/reports | No API | None | None | No | No queue, counts, or controls. |

No `/admin/moderation` route is introduced. A platform queue built from unscoped thread/post CRUD would imply authorization, reports, and consequences that do not exist.
18 changes: 18 additions & 0 deletions docs/ux-overhaul/admin-privacy-review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Admin Privacy Review

Date: 2026-07-13

## High-Risk Surfaces And Mitigations

| Surface | Risk | Production mitigation |
| --- | --- | --- |
| User directory | Broad disclosure of contact/security data | Shows display name, role, and creation date only. Email and username appear only after opening one protected record. Raw ID is not visible. |
| Backend user serialization | ORM response may include hashed password/internal fields | Typed frontend projection never renders unknown fields. Dedicated backend response schema remains a gap. |
| User detail | Mixing platform identity with learner progress | Shows identity and platform role only; no learner progress, demographics, tokens, password data, or global activity. |
| Organizations | Cross-organization disclosure | Uses membership-scoped `/api/orgs`; neutral unavailable state for unknown deep links; does not infer members/admins/classes/invites. |
| Courses | Learner and author identity exposure | Shows curriculum/governance metadata, not learner progress or teacher assignment data. Creator/organization UUIDs are not displayed. |
| Reporting | Misleading or identifying analytics | Uses aggregate documented counts only; no learner/organization ranking or individual activity. |
| Errors | Sensitive payload leakage | User-facing messages are neutral and do not echo backend payloads or identifiers. |
| Destructive operations | Hidden scope | Confirmations state known cascades and unaffected organizations/courses. |

Backend authorization remains authoritative for every API call and `RoleGuard` protects deep links after session bootstrap. The frontend does not log administrative payloads. Organization and learner contexts remain separate.
48 changes: 48 additions & 0 deletions docs/ux-overhaul/admin-production-audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Platform Admin Production Audit

Date: 2026-07-13

## Authorization Summary

The platform role stored on `User.role` is the backend authority. `require_roles()` performs exact string matching and does not implement inheritance. Organization roles are separate `OrganizationMembership.role` values. Frontend permissions and hidden navigation are presentation only.

`admin` is the only role authorized for the current user directory, user updates/deletion, Admin overview analytics, course list/detail/write operations, badge creation/assignment, and badge image upload. `super_admin` is not an alias for `admin`: it can bypass organization-name update membership checks and can read unscoped V2 analytics, but it is rejected by the core Admin endpoints above. The current shell previously displayed an Admin area for both roles while legacy Admin routes allowed only `admin`.

## Route And Screen Audit

| Current route or surface | User goal | Authorized role from backend | APIs | Sensitivity | Current actions | Backend enforcement | UX, accessibility, and responsive risks | Recommendation / owner |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `/home` Admin dashboard | Assess platform state | `admin`; `super_admin` cannot call its core APIs | `GET /api/analytics/overview`, `/api/users`, `/api/courses`, course governance endpoints | Platform user counts; learner recommendations | Read summaries; legacy page also deletes users/courses | Exact role checks for lists; governance allows staff roles | 546-line dashboard mixes operational oversight, learner intervention data, and destructive actions; partial failures are difficult to distinguish; dense mobile layout | Canonical `/admin`; Admin. Keep `/home` as a legacy landing until dynamic compatibility routing is retired. |
| `/home/admin/users` | Review and manage accounts | `admin` | `GET /api/users`, `DELETE /api/users/{id}` | Names, email, username, role, created date; API also serializes hashed password unless excluded by ORM serialization | Immediate delete | Exact `admin` check; delete cascades posts, threads, badges, unit links and user | No loading/error/empty state, no confirmation, no search/filter, raw role text, table overflows mobile | Canonical `/admin/users`; Admin. Minimize visible fields and require exact destructive confirmation. |
| No user-detail route | Inspect one account and change role | `admin` | `GET/PUT /api/users/{id}` | Identity, email, role | Full DTO update; role change possible | Exact `admin` check; no role allowlist, final-admin protection, or self-lockout protection | Missing UI; PUT requires complete identity DTO and optional empty password convention | Add `/admin/users/:userId`; Admin. Allow verified role changes except self-change; confirm privilege changes; document backend gaps. |
| `/workspace/learners/users` | Duplicate user list | `admin` | Same as user list | Same | Same | Same | Duplicate namespace suggests organization ownership | Keep as compatibility route; canonical `/admin/users`; Admin. |
| `/home/admin/courses` | Oversee courses | `admin` for list/write | `GET/DELETE /api/courses`, course governance endpoints | Curriculum metadata and learner-facing availability | Legacy create/edit/delete | Exact `admin`/`teacher` on core course CRUD | “Product” language, authoring controls mixed into Admin, immediate delete, no states, table overflows | Canonical `/admin/courses`; Admin oversight only. Link to detail oversight; keep authoring out of Admin. |
| `/workspace/products/manage` | Duplicate course management | `admin` | Same as course list | Same | Same | Same | Duplicates Admin and Studio concepts | Compatibility alias only; canonical `/admin/courses`; Admin. |
| `/home/admin/badges` | Create and inspect badges | Read: any authenticated user; create/upload/assign: `admin` | `GET/POST /api/badges`, `POST /api/upload/badge`, assignment endpoint | Public recognition metadata and images | Create and upload | Explicit `admin` checks on writes | Missing load/error/empty/submission states; inaccessible image; file restrictions not explained; no edit/delete API exists | Canonical `/admin/badges`; Admin. Create supported; existing badges read-only. |
| `/workspace/settings/badges` | Duplicate badge management | Route says `admin` | Same as badge list | Same | Same | Same | Wrong settings ownership | Compatibility alias only; canonical `/admin/badges`; Admin. |
| `/workspace/analytics` | Review workspace/content analytics | Several staff roles including `super_admin` | V2 analytics endpoints | Organization/workspace aggregate operational data | Read only | Exact role list plus workspace scoping; admins/super admins unscoped | Content/Studio reporting is not the same as platform Admin reporting | Keep in Studio. Add `/admin/reports` using only `/analytics/overview` for `admin`; no cross-link for `super_admin` unless definitions match. |
| `/workspace/commercial` | Community project surface | Creator roles | V2/community APIs | Community operational data | Varies | Creator-role route checks | Not moderation and not Admin oversight | Community owner; remove from Admin navigation. Preserve route. |
| `/workspace/settings` | Preferences/settings | Creator roles | Preferences APIs | User preferences | Edit preferences | Existing auth | Exposed as “Platform settings” without a platform-settings API | Keep as user/Studio settings; omit from Admin navigation. |
| `/workspace/review-center` | Content review | Creator roles | V2 review APIs | Content review state | Content workflow actions | Creator-role checks | Belongs to Studio; not platform moderation | Studio owner; do not migrate. |
| `/workspace/access` | Product access grants | Creator roles | V2 access-grant APIs | Learner access | Grant/revoke | V2 role and workspace checks | Organization/content access, not platform account access | Organization/Studio owner; do not migrate. |
| Organization selector/header | See organizations for current account | Any authenticated user | `GET /api/orgs` | Organization identity, current membership role | Switch active organization; create organization elsewhere | List is restricted to current user's memberships; PATCH allows member content/org admin or platform `super_admin` | Cannot provide platform-wide oversight, counts, members, admins, sections, or invitations | Add `/admin/organizations` as explicitly scoped read-only membership visibility. Detail is limited to returned organization metadata. Admin owner for visibility; self-service remains Organization. |
| Threads/posts routes | Discuss or mutate community content | No authentication dependency | CRUD `/api/threads`, `/api/posts` | User-generated content and author identifiers | Create/update/delete | No authentication, scope, ownership, moderation role, report, restore, lock, or audit behavior | Unsafe for platform moderation; deletion irreversible and unscoped | No Admin route. Document as backend security/moderation gap; Community future change. |
| Upload routes | Upload lesson assets | Badge upload: `admin`; coloring/storybook: `admin` or `teacher` | `/api/upload/*` | Uploaded files | Create file | Exact role checks | No asset list, metadata, delete, ownership, or moderation APIs | Badge upload stays inside badge creation. No Admin asset library. |

## Destructive Behavior

- User deletion permanently removes the user plus authored posts/threads, student badges, and unit links. The endpoint has no self-delete protection, final-admin protection, audit record, dry run, or restore.
- Course deletion permanently deletes the course through the current CRUD endpoint and has no archive/restore contract.
- Badge deletion, badge edit, organization archive/delete, account disable/restore, invitation resend, role removal, membership management, and platform settings are not supported by verified APIs.

## Route Migration Decision

The canonical family is `/admin`, `/admin/users`, `/admin/users/:userId`, `/admin/organizations`, `/admin/organizations/:organizationId`, `/admin/courses`, `/admin/courses/:courseId`, `/admin/badges`, and `/admin/reports`. Moderation and platform settings routes are omitted. Legacy routes remain guarded aliases; no broad deletion is part of this phase.

## Primary Risks And Mitigations

- **Authorization mismatch:** exact backend role checks remain authoritative; `super_admin` receives only the supported overview limitation state and organization visibility.
- **Privacy:** broad user lists show name, role, and created date only. Email and username are confined to an individual detail view. Password hashes, tokens, and raw identifiers are never rendered.
- **Responsive tables:** lists switch from semantic desktop tables to equivalent mobile records without hiding primary actions.
- **High-impact actions:** shared confirmation names the object, consequence, scope, and irreversibility; UI updates only after API success.
- **Partial failures:** each overview source reports independently so one failed request does not make successful counts appear absent.
Loading
Loading