Add frontend access control enhancements and JWT support

- Hide New Project button for unauthenticated users, show login link
- Add lock icon for private projects on home page
- Show access level badges on project cards (Owner, Admin, Write, Read)
- Add permission expiration date field to AccessManagement component
- Add query timeout configuration for database (ORCHARD_DATABASE_QUERY_TIMEOUT)
- Add JWT token validation support for external identity providers
  - Configurable via ORCHARD_JWT_* environment variables
  - Supports HS256 with secret or RS256 with JWKS
  - Auto-provisions users from JWT claims
This commit is contained in:
Mondo Diaz
2026-01-08 18:52:57 -06:00
parent f7c91e94f6
commit 6b9f63a30e
10 changed files with 373 additions and 21 deletions

View File

@@ -45,11 +45,13 @@ from .models import (
Consumer,
AuditLog,
User,
AccessPermission,
)
from .schemas import (
ProjectCreate,
ProjectUpdate,
ProjectResponse,
ProjectWithAccessResponse,
PackageCreate,
PackageUpdate,
PackageResponse,
@@ -947,7 +949,7 @@ def global_search(
# Project routes
@router.get("/api/v1/projects", response_model=PaginatedResponse[ProjectResponse])
@router.get("/api/v1/projects", response_model=PaginatedResponse[ProjectWithAccessResponse])
def list_projects(
request: Request,
page: int = Query(default=1, ge=1, description="Page number"),
@@ -963,8 +965,9 @@ def list_projects(
),
order: str = Query(default="asc", description="Sort order (asc, desc)"),
db: Session = Depends(get_db),
current_user: Optional[User] = Depends(get_current_user_optional),
):
user_id = get_user_id(request)
user_id = current_user.username if current_user else get_user_id(request)
# Validate sort field
valid_sort_fields = {
@@ -1022,8 +1025,51 @@ def list_projects(
# Calculate total pages
total_pages = math.ceil(total / limit) if total > 0 else 1
# Build access level info for each project
project_ids = [p.id for p in projects]
access_map = {}
if current_user and project_ids:
# Get access permissions for this user across these projects
permissions = (
db.query(AccessPermission)
.filter(
AccessPermission.project_id.in_(project_ids),
AccessPermission.user_id == current_user.username,
)
.all()
)
access_map = {p.project_id: p.level for p in permissions}
# Build response with access levels
items = []
for p in projects:
is_owner = p.created_by == user_id
access_level = None
if is_owner:
access_level = "admin"
elif p.id in access_map:
access_level = access_map[p.id]
elif p.is_public:
access_level = "read"
items.append(
ProjectWithAccessResponse(
id=p.id,
name=p.name,
description=p.description,
is_public=p.is_public,
created_at=p.created_at,
updated_at=p.updated_at,
created_by=p.created_by,
access_level=access_level,
is_owner=is_owner,
)
)
return PaginatedResponse(
items=projects,
items=items,
pagination=PaginationMeta(
page=page,
limit=limit,