Skip to content

Commit e952373

Browse files
author
Aegis Stack
committed
FIXUP
1 parent b154fd6 commit e952373

8 files changed

Lines changed: 543 additions & 2 deletions

File tree

aegis/core/copier_manager.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ def generate_with_copier(
116116
AnswerKeys.OBSERVABILITY: template_context.get(AnswerKeys.OBSERVABILITY, "no")
117117
== "yes",
118118
AnswerKeys.AUTH: template_context.get(AnswerKeys.AUTH, "no") == "yes",
119+
AnswerKeys.AUTH_LEVEL: template_context.get(AnswerKeys.AUTH_LEVEL, "basic"),
120+
AnswerKeys.AUTH_RBAC: template_context.get(AnswerKeys.AUTH_RBAC, "no") == "yes",
121+
AnswerKeys.AUTH_ORG: template_context.get(AnswerKeys.AUTH_ORG, "no") == "yes",
119122
AnswerKeys.AI: template_context.get(AnswerKeys.AI, "no") == "yes",
120123
AnswerKeys.COMMS: template_context.get(AnswerKeys.COMMS, "no") == "yes",
121124
AnswerKeys.AI_FRAMEWORK: template_context.get(
@@ -249,6 +252,8 @@ def generate_with_copier(
249252
ai_voice_enabled: bool = copier_data.get(AnswerKeys.AI_VOICE, False) is True
250253
context = {
251254
"include_auth": is_auth_included,
255+
"include_auth_org": copier_data.get(AnswerKeys.AUTH_ORG, False) is True,
256+
"auth_level": copier_data.get(AnswerKeys.AUTH_LEVEL, "basic"),
252257
"include_ai": is_ai_included,
253258
"ai_backend": ai_backend_str,
254259
"ai_voice": ai_voice_enabled,
@@ -356,6 +361,19 @@ def generate_with_copier(
356361
# Production mode: use GitHub URL
357362
answers["_src_path"] = GITHUB_TEMPLATE_URL
358363

364+
# Persist conditional auth fields (Copier may omit conditional
365+
# questions from answers file when values are provided via data)
366+
if copier_data.get(AnswerKeys.AUTH):
367+
answers[AnswerKeys.AUTH_LEVEL] = copier_data.get(
368+
AnswerKeys.AUTH_LEVEL, "basic"
369+
)
370+
answers[AnswerKeys.AUTH_RBAC] = copier_data.get(
371+
AnswerKeys.AUTH_RBAC, False
372+
)
373+
answers[AnswerKeys.AUTH_ORG] = copier_data.get(
374+
AnswerKeys.AUTH_ORG, False
375+
)
376+
359377
with open(answers_file, "w") as f:
360378
yaml.safe_dump(answers, f, default_flow_style=False, sort_keys=False)
361379

aegis/templates/copier-aegis-project/{{ project_slug }}/.copier-answers.yml.jinja

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ include_database: {{ include_database }}
1818
database_engine: {{ database_engine }}
1919
include_cache: {{ include_cache }}
2020
include_auth: {{ include_auth }}
21+
auth_level: {{ auth_level }}
22+
include_auth_rbac: {{ include_auth_rbac }}
23+
include_auth_org: {{ include_auth_org }}
2124
include_ai: {{ include_ai }}
2225
include_comms: {{ include_comms }}
2326
ai_providers: {{ ai_providers }}

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

Whitespace-only changes.
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
"""Organization API routes."""
2+
{% if include_auth_org %}
3+
4+
from app.components.backend.api.deps import get_async_db
5+
from app.models.org import (
6+
MemberResponse,
7+
OrgCreate,
8+
OrgResponse,
9+
OrganizationMember,
10+
ORG_ROLE_OWNER,
11+
ORG_ROLE_ADMIN,
12+
)
13+
from app.services.auth.auth_service import get_current_user_from_token
14+
from app.services.auth.membership_service import MembershipService
15+
from app.services.auth.org_service import OrgService
16+
from fastapi import APIRouter, Depends, HTTPException, status
17+
from fastapi.security import OAuth2PasswordBearer
18+
from sqlmodel.ext.asyncio.session import AsyncSession
19+
20+
router = APIRouter(prefix="/orgs", tags=["organizations"])
21+
22+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/token")
23+
24+
25+
async def _get_current_user(
26+
token: str = Depends(oauth2_scheme),
27+
db: AsyncSession = Depends(get_async_db),
28+
):
29+
"""Get authenticated user."""
30+
return await get_current_user_from_token(token, db)
31+
32+
33+
async def _require_org_role(
34+
org_id: int,
35+
db: AsyncSession,
36+
user_id: int,
37+
allowed_roles: set[str],
38+
) -> OrganizationMember:
39+
"""Check user has required org role. Raises 403 if not."""
40+
membership_service = MembershipService(db)
41+
member = await membership_service.get_member(org_id, user_id)
42+
if not member or member.role not in allowed_roles:
43+
raise HTTPException(
44+
status_code=status.HTTP_403_FORBIDDEN,
45+
detail="Insufficient organization permissions",
46+
)
47+
return member
48+
49+
50+
# =============================================================================
51+
# Organization CRUD
52+
# =============================================================================
53+
54+
55+
@router.post("", response_model=OrgResponse, status_code=status.HTTP_201_CREATED)
56+
async def create_org(
57+
org_data: OrgCreate,
58+
user=Depends(_get_current_user),
59+
db: AsyncSession = Depends(get_async_db),
60+
) -> OrgResponse:
61+
"""Create a new organization. Creator becomes owner."""
62+
org_service = OrgService(db)
63+
64+
# Check slug uniqueness
65+
existing = await org_service.get_org_by_slug(org_data.slug)
66+
if existing:
67+
raise HTTPException(
68+
status_code=status.HTTP_400_BAD_REQUEST,
69+
detail="Organization slug already taken",
70+
)
71+
72+
org = await org_service.create_org(org_data)
73+
74+
# Add creator as owner
75+
membership_service = MembershipService(db)
76+
await membership_service.add_member(org.id, user.id, role=ORG_ROLE_OWNER)
77+
78+
return OrgResponse.model_validate(org)
79+
80+
81+
@router.get("", response_model=list[OrgResponse])
82+
async def list_my_orgs(
83+
user=Depends(_get_current_user),
84+
db: AsyncSession = Depends(get_async_db),
85+
) -> list[OrgResponse]:
86+
"""List organizations the current user belongs to."""
87+
membership_service = MembershipService(db)
88+
orgs = await membership_service.list_user_orgs(user.id)
89+
return [OrgResponse.model_validate(org) for org in orgs]
90+
91+
92+
@router.get("/{org_id}", response_model=OrgResponse)
93+
async def get_org(
94+
org_id: int,
95+
user=Depends(_get_current_user),
96+
db: AsyncSession = Depends(get_async_db),
97+
) -> OrgResponse:
98+
"""Get organization details."""
99+
org_service = OrgService(db)
100+
org = await org_service.get_org_by_id(org_id)
101+
if not org:
102+
raise HTTPException(
103+
status_code=status.HTTP_404_NOT_FOUND,
104+
detail="Organization not found",
105+
)
106+
return OrgResponse.model_validate(org)
107+
108+
109+
@router.patch("/{org_id}", response_model=OrgResponse)
110+
async def update_org(
111+
org_id: int,
112+
name: str | None = None,
113+
description: str | None = None,
114+
user=Depends(_get_current_user),
115+
db: AsyncSession = Depends(get_async_db),
116+
) -> OrgResponse:
117+
"""Update organization. Requires admin or owner role."""
118+
await _require_org_role(org_id, db, user.id, {ORG_ROLE_ADMIN, ORG_ROLE_OWNER})
119+
120+
updates: dict[str, str] = {}
121+
if name is not None:
122+
updates["name"] = name
123+
if description is not None:
124+
updates["description"] = description
125+
126+
if not updates:
127+
raise HTTPException(
128+
status_code=status.HTTP_400_BAD_REQUEST,
129+
detail="No fields to update",
130+
)
131+
132+
org_service = OrgService(db)
133+
org = await org_service.update_org(org_id, **updates)
134+
if not org:
135+
raise HTTPException(
136+
status_code=status.HTTP_404_NOT_FOUND,
137+
detail="Organization not found",
138+
)
139+
return OrgResponse.model_validate(org)
140+
141+
142+
@router.delete("/{org_id}", status_code=status.HTTP_204_NO_CONTENT)
143+
async def delete_org(
144+
org_id: int,
145+
user=Depends(_get_current_user),
146+
db: AsyncSession = Depends(get_async_db),
147+
) -> None:
148+
"""Delete organization. Requires owner role."""
149+
await _require_org_role(org_id, db, user.id, {ORG_ROLE_OWNER})
150+
151+
org_service = OrgService(db)
152+
success = await org_service.delete_org(org_id)
153+
if not success:
154+
raise HTTPException(
155+
status_code=status.HTTP_404_NOT_FOUND,
156+
detail="Organization not found",
157+
)
158+
159+
160+
# =============================================================================
161+
# Membership Management
162+
# =============================================================================
163+
164+
165+
@router.get("/{org_id}/members", response_model=list[MemberResponse])
166+
async def list_members(
167+
org_id: int,
168+
user=Depends(_get_current_user),
169+
db: AsyncSession = Depends(get_async_db),
170+
) -> list[MemberResponse]:
171+
"""List organization members. Requires membership."""
172+
await _require_org_role(
173+
org_id, db, user.id, {ORG_ROLE_OWNER, ORG_ROLE_ADMIN, "member"}
174+
)
175+
176+
membership_service = MembershipService(db)
177+
members = await membership_service.list_org_members(org_id)
178+
return [MemberResponse.model_validate(m) for m in members]
179+
180+
181+
@router.post(
182+
"/{org_id}/members",
183+
response_model=MemberResponse,
184+
status_code=status.HTTP_201_CREATED,
185+
)
186+
async def add_member(
187+
org_id: int,
188+
user_id: int,
189+
role: str = "member",
190+
user=Depends(_get_current_user),
191+
db: AsyncSession = Depends(get_async_db),
192+
) -> MemberResponse:
193+
"""Add a member to organization. Requires admin or owner role."""
194+
await _require_org_role(org_id, db, user.id, {ORG_ROLE_ADMIN, ORG_ROLE_OWNER})
195+
196+
membership_service = MembershipService(db)
197+
198+
# Check if already a member
199+
existing = await membership_service.get_member(org_id, user_id)
200+
if existing:
201+
raise HTTPException(
202+
status_code=status.HTTP_400_BAD_REQUEST,
203+
detail="User is already a member of this organization",
204+
)
205+
206+
member = await membership_service.add_member(org_id, user_id, role)
207+
return MemberResponse.model_validate(member)
208+
209+
210+
@router.patch("/{org_id}/members/{user_id}", response_model=MemberResponse)
211+
async def update_member_role(
212+
org_id: int,
213+
user_id: int,
214+
role: str,
215+
user=Depends(_get_current_user),
216+
db: AsyncSession = Depends(get_async_db),
217+
) -> MemberResponse:
218+
"""Update a member's role. Requires admin or owner role."""
219+
await _require_org_role(org_id, db, user.id, {ORG_ROLE_ADMIN, ORG_ROLE_OWNER})
220+
221+
membership_service = MembershipService(db)
222+
member = await membership_service.update_member_role(org_id, user_id, role)
223+
if not member:
224+
raise HTTPException(
225+
status_code=status.HTTP_404_NOT_FOUND,
226+
detail="Member not found",
227+
)
228+
return MemberResponse.model_validate(member)
229+
230+
231+
@router.delete(
232+
"/{org_id}/members/{user_id}", status_code=status.HTTP_204_NO_CONTENT
233+
)
234+
async def remove_member(
235+
org_id: int,
236+
user_id: int,
237+
user=Depends(_get_current_user),
238+
db: AsyncSession = Depends(get_async_db),
239+
) -> None:
240+
"""Remove a member from organization. Owner cannot be removed."""
241+
await _require_org_role(org_id, db, user.id, {ORG_ROLE_ADMIN, ORG_ROLE_OWNER})
242+
243+
# Prevent removing the owner
244+
membership_service = MembershipService(db)
245+
target = await membership_service.get_member(org_id, user_id)
246+
if not target:
247+
raise HTTPException(
248+
status_code=status.HTTP_404_NOT_FOUND,
249+
detail="Member not found",
250+
)
251+
if target.role == ORG_ROLE_OWNER:
252+
raise HTTPException(
253+
status_code=status.HTTP_403_FORBIDDEN,
254+
detail="Cannot remove the organization owner",
255+
)
256+
257+
await membership_service.remove_member(org_id, user_id)
258+
{% else %}
259+
# Organization endpoints not included (auth_level != org)
260+
{% endif %}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ from app.components.backend.api import scheduler
1212
{%- if include_auth %}
1313
from app.components.backend.api.auth.router import router as auth_router
1414
{%- endif %}
15+
{%- if include_auth_org %}
16+
from app.components.backend.api.orgs.router import router as org_router
17+
{%- endif %}
1518
{%- if include_ai %}
1619
from app.components.backend.api.ai.router import router as ai_router
1720
{%- if ai_voice %}
@@ -43,6 +46,9 @@ def include_routers(app: FastAPI) -> None:
4346
{%- if include_auth %}
4447
app.include_router(auth_router, prefix="/api/v1")
4548
{%- endif %}
49+
{%- if include_auth_org %}
50+
app.include_router(org_router, prefix="/api/v1")
51+
{%- endif %}
4652
{%- if include_ai %}
4753
app.include_router(ai_router)
4854
{%- if ai_voice %}

0 commit comments

Comments
 (0)