Skip to content

Commit 2a4668d

Browse files
authored
Merge pull request #600 from lbedner/update-docs-deps
Update Docs Deps
2 parents 694a0d1 + bfd09ba commit 2a4668d

File tree

8 files changed

+197
-13
lines changed

8 files changed

+197
-13
lines changed

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Each generated project includes:
3232

3333
## Installation
3434

35-
**Current Version**: 0.6.8rc2
35+
**Current Version**: 0.6.8rc3
3636

3737
```bash
3838
pip install aegis-stack

aegis/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
Aegis Stack CLI - Component generation and project management tools.
33
"""
44

5-
__version__ = "0.6.8rc2"
5+
__version__ = "0.6.8rc3"

aegis/core/manual_updater.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,21 @@
4242
# Must be regenerated when upgrading auth level.
4343
REGENERATE_ON_AUTH_LEVEL_CHANGE = {
4444
"app/models/user.py",
45+
"app/models/org.py",
4546
"app/core/security.py",
4647
"app/services/auth/auth_service.py",
48+
"app/services/auth/org_service.py",
49+
"app/services/auth/membership_service.py",
50+
"app/services/auth/invite_service.py",
4751
"app/components/backend/api/auth/router.py",
52+
"app/components/backend/api/orgs/router.py",
53+
"app/components/backend/api/orgs/__init__.py",
4854
"app/components/backend/api/deps.py",
4955
"app/components/frontend/dashboard/modals/auth_modal.py",
5056
"app/components/frontend/dashboard/modals/auth_users_tab.py",
57+
"app/components/frontend/dashboard/modals/auth_orgs_tab.py",
58+
"tests/services/test_org_integration.py",
59+
"tests/api/test_org_endpoints.py",
5160
}
5261

5362

aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/orgs/router.py.jinja

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@ from app.models.org import (
2727
BulkAddMembersRequest,
2828
InviteCreate,
2929
InviteResponse,
30+
MemberDetailResponse,
3031
MemberResponse,
3132
OrganizationMember,
3233
OrgCreate,
3334
OrgResponse,
35+
TransferOwnershipRequest,
3436
UserMembershipResponse,
3537
)
3638
from app.services.auth.auth_service import get_current_user_from_token{% if include_auth_rbac %}, require_role{% endif %}
@@ -226,6 +228,28 @@ async def delete_org(
226228
await audit.emit("auth.org_deleted", actor_id=user.id, target_type="org", target_id=org_id)
227229

228230

231+
@router.post("/{org_id}/transfer-ownership")
232+
async def transfer_ownership(
233+
org_id: int,
234+
body: TransferOwnershipRequest,
235+
user=Depends(_get_current_user),
236+
membership_service: MembershipService = Depends(get_membership_service),
237+
audit: AuditEmitter = Depends(get_audit),
238+
) -> dict[str, str]:
239+
"""Transfer org ownership to another member. Requires owner role."""
240+
await _require_org_role(
241+
membership_service, org_id, user.id, {ORG_ROLE_OWNER}
242+
)
243+
if body.user_id == user.id:
244+
raise_bad_request("Cannot transfer ownership to yourself")
245+
try:
246+
await membership_service.transfer_ownership(org_id, user.id, body.user_id)
247+
except ValueError as e:
248+
raise_bad_request(str(e))
249+
await audit.emit("auth.org_ownership_transferred", actor_id=user.id, org_id=org_id, target_type="user", target_id=body.user_id)
250+
return {"detail": "Ownership transferred"}
251+
252+
229253
# =============================================================================
230254
# Membership Management
231255
# =============================================================================
@@ -244,6 +268,19 @@ async def list_members(
244268
return [MemberResponse.model_validate(m) for m in members]
245269

246270

271+
@router.get("/{org_id}/members/details", response_model=list[MemberDetailResponse])
272+
async def list_member_details(
273+
org_id: int,
274+
user=Depends(_get_current_user),
275+
membership_service: MembershipService = Depends(get_membership_service),
276+
) -> list[MemberDetailResponse]:
277+
"""List org members with user details (email, name). Requires membership."""
278+
await _require_member_access(membership_service, org_id, user.id)
279+
280+
rows = await membership_service.list_org_members_with_details(org_id)
281+
return [MemberDetailResponse(**row) for row in rows]
282+
283+
247284
@router.post(
248285
"/{org_id}/members",
249286
response_model=MemberResponse,

aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_org_endpoints.py.jinja

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,144 @@ class TestInviteEndpoints:
390390
)
391391

392392
assert response.status_code == status.HTTP_400_BAD_REQUEST
393+
394+
395+
@pytest.mark.usefixtures("_enable_auth")
396+
class TestMemberDetails:
397+
"""Test GET /orgs/{org_id}/members/details."""
398+
399+
@pytest.mark.asyncio
400+
async def test_org_admin_can_get_member_details(
401+
self, async_client_with_db: TestClient, async_db_session: AsyncSession,
402+
setup_org_with_owner, create_user,
403+
) -> None:
404+
"""Org admin can see member emails and names."""
405+
_, org, token = await setup_org_with_owner(
406+
email="det-owner@test.com", slug="det-org"
407+
)
408+
member = await create_user("det-member@test.com")
409+
membership_service = MembershipService(async_db_session)
410+
await membership_service.add_member(org.id, member.id)
411+
412+
response = async_client_with_db.get(
413+
f"/api/v1/orgs/{org.id}/members/details",
414+
headers={"Authorization": f"Bearer {token}"},
415+
)
416+
417+
assert response.status_code == status.HTTP_200_OK
418+
data = response.json()
419+
assert len(data) >= 2
420+
emails = {m["email"] for m in data}
421+
assert "det-member@test.com" in emails
422+
423+
@pytest.mark.asyncio
424+
async def test_non_member_cannot_get_member_details(
425+
self, async_client_with_db: TestClient, async_db_session: AsyncSession,
426+
setup_org_with_owner, create_user_with_token,
427+
) -> None:
428+
"""Non-member cannot see org member details."""
429+
_, org, _ = await setup_org_with_owner(
430+
email="det2-owner@test.com", slug="det2-org"
431+
)
432+
_, outsider_token = await create_user_with_token("det2-outsider@test.com")
433+
434+
response = async_client_with_db.get(
435+
f"/api/v1/orgs/{org.id}/members/details",
436+
headers={"Authorization": f"Bearer {outsider_token}"},
437+
)
438+
439+
assert response.status_code == status.HTTP_403_FORBIDDEN
440+
441+
442+
@pytest.mark.usefixtures("_enable_auth")
443+
class TestOwnershipTransfer:
444+
"""Test POST /orgs/{org_id}/transfer-ownership."""
445+
446+
@pytest.mark.asyncio
447+
async def test_owner_can_transfer(
448+
self, async_client_with_db: TestClient, async_db_session: AsyncSession,
449+
setup_org_with_owner, create_user,
450+
) -> None:
451+
"""Owner transfers ownership to an admin member."""
452+
owner, org, token = await setup_org_with_owner(
453+
email="xfer-owner@test.com", slug="xfer-org"
454+
)
455+
admin = await create_user("xfer-admin@test.com")
456+
membership_service = MembershipService(async_db_session)
457+
await membership_service.add_member(org.id, admin.id, role="admin")
458+
459+
response = async_client_with_db.post(
460+
f"/api/v1/orgs/{org.id}/transfer-ownership",
461+
json={"user_id": admin.id},
462+
headers={"Authorization": f"Bearer {token}"},
463+
)
464+
465+
assert response.status_code == status.HTTP_200_OK
466+
467+
new_owner = await membership_service.get_member(org.id, admin.id)
468+
assert new_owner.role == "owner"
469+
470+
old_owner = await membership_service.get_member(org.id, owner.id)
471+
assert old_owner.role == "admin"
472+
473+
@pytest.mark.asyncio
474+
async def test_non_owner_cannot_transfer(
475+
self, async_client_with_db: TestClient, async_db_session: AsyncSession,
476+
setup_org_with_owner, create_user_with_token,
477+
) -> None:
478+
"""Admin cannot transfer ownership."""
479+
_, org, _ = await setup_org_with_owner(
480+
email="xfer2-owner@test.com", slug="xfer2-org"
481+
)
482+
admin, admin_token = await create_user_with_token("xfer2-admin@test.com")
483+
membership_service = MembershipService(async_db_session)
484+
await membership_service.add_member(org.id, admin.id, role="admin")
485+
486+
response = async_client_with_db.post(
487+
f"/api/v1/orgs/{org.id}/transfer-ownership",
488+
json={"user_id": admin.id},
489+
headers={"Authorization": f"Bearer {admin_token}"},
490+
)
491+
492+
assert response.status_code == status.HTTP_403_FORBIDDEN
493+
494+
@pytest.mark.asyncio
495+
async def test_transfer_to_non_member_fails(
496+
self, async_client_with_db: TestClient, async_db_session: AsyncSession,
497+
setup_org_with_owner, create_user,
498+
) -> None:
499+
"""Cannot transfer to someone who isn't a member."""
500+
_, org, token = await setup_org_with_owner(
501+
email="xfer3-owner@test.com", slug="xfer3-org"
502+
)
503+
outsider = await create_user("xfer3-outsider@test.com")
504+
505+
response = async_client_with_db.post(
506+
f"/api/v1/orgs/{org.id}/transfer-ownership",
507+
json={"user_id": outsider.id},
508+
headers={"Authorization": f"Bearer {token}"},
509+
)
510+
511+
assert response.status_code == status.HTTP_400_BAD_REQUEST
512+
513+
@pytest.mark.asyncio
514+
async def test_self_transfer_fails(
515+
self, async_client_with_db: TestClient, async_db_session: AsyncSession,
516+
setup_org_with_owner,
517+
) -> None:
518+
"""Cannot transfer ownership to yourself."""
519+
owner, org, token = await setup_org_with_owner(
520+
email="xfer4-owner@test.com", slug="xfer4-org"
521+
)
522+
523+
response = async_client_with_db.post(
524+
f"/api/v1/orgs/{org.id}/transfer-ownership",
525+
json={"user_id": owner.id},
526+
headers={"Authorization": f"Bearer {token}"},
527+
)
528+
529+
assert response.status_code == status.HTTP_400_BAD_REQUEST
530+
assert "yourself" in response.json()["detail"].lower()
393531
{% else %}
394532
# Organization endpoint tests not included (auth_level != org)
395533
{% endif %}

copier.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
# - Update support
77

88
_min_copier_version: "9.0.0"
9-
_version: "0.6.8rc2"
9+
_version: "0.6.8rc3"
1010

1111
# IMPORTANT: Template content is in subdirectory
1212
# This allows the template to be recognized as git-tracked (aegis-stack repo root has .git)

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "aegis-stack"
3-
version = "0.6.8rc2"
3+
version = "0.6.8rc3"
44
description = "A production-ready FastAPI platform with modular components and a built-in control plane. Try: uvx aegis-stack init my-project"
55
readme = "README.md"
66
requires-python = ">=3.11,<3.15"
@@ -46,7 +46,6 @@ dependencies = [
4646
"packaging>=24.0",
4747
"filelock>=3.20.1",
4848
"urllib3>=2.6.3",
49-
"pygments>=2.20.0",
5049
]
5150

5251
[project.urls]
@@ -71,7 +70,8 @@ docs = [
7170
"mkdocs-material>=9.5.0",
7271
"mkdocstrings[python]>=0.24.0",
7372
"mkdocs-gen-files>=0.5.0",
74-
"pymdown-extensions>=10.0.0",
73+
"pygments>=2.20.0",
74+
"pymdown-extensions>=10.21.0",
7575
"mkdocs-mermaid2-plugin>=1.1.0",
7676
"mkdocs-glightbox>=0.3.0",
7777
]

uv.lock

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)