-
Notifications
You must be signed in to change notification settings - Fork 0
[US-11.10 / OBT-239] Expose the authenticated user's managed project IDs #93
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0f9fc13
15c6b73
6fb1756
17d9664
5caf07b
495b73c
9993213
4992361
fe18be4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,6 @@ | |
| async def assert_project_access(db: AsyncSession, user: User, project_id: str) -> None: | ||
| if user.is_platform_admin: | ||
| return | ||
| allowed = await project_service.can_access_project(db, user.id, project_id) | ||
| if not allowed: | ||
| is_manager = await project_service.is_project_manager(db, user.id, project_id) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| if not is_manager: | ||
| raise AuthorizationError("You do not have access to this project") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,28 +1,36 @@ | ||
| from fastapi import Depends | ||
| from sqlalchemy import select, union | ||
| from sqlalchemy.ext.asyncio import AsyncSession | ||
|
|
||
| from app.core.auth_middleware import get_current_user | ||
| from app.core.database import get_db | ||
| from app.db.models.auth import User | ||
| from app.db.models.org import MemberRole, Organization, OrganizationMember | ||
|
|
||
|
|
||
| async def get_managed_org_ids(db: AsyncSession, user_id: str) -> list[str]: | ||
| from_members = select(OrganizationMember.organization_id.label("org_id")).where( | ||
| OrganizationMember.user_id == user_id, | ||
| OrganizationMember.role == MemberRole.MANAGER, | ||
| ) | ||
|
|
||
| from_orgs = select(Organization.id.label("org_id")).where(Organization.manager_id == user_id) | ||
|
|
||
| combined = union(from_members, from_orgs).subquery() | ||
| result = await db.execute(select(combined.c.org_id)) | ||
| return sorted(result.scalars().all()) | ||
|
|
||
|
|
||
| async def get_current_user_managed_org_ids( | ||
| db: AsyncSession = Depends(get_db), | ||
| user: User = Depends(get_current_user), | ||
| ) -> list[str]: | ||
| return await get_managed_org_ids(db, user.id) | ||
| from fastapi import Depends | ||
| from sqlalchemy import select, union | ||
| from sqlalchemy.ext.asyncio import AsyncSession | ||
|
|
||
| from app.core.auth_middleware import get_current_user | ||
| from app.core.database import get_db | ||
| from app.db.models.auth import User | ||
| from app.db.models.org import MemberRole, Organization, OrganizationMember | ||
|
|
||
|
|
||
| async def get_managed_project_ids(db: AsyncSession, user_id: str) -> list[str]: | ||
| from app.services.project.get_managed_project_ids import ( | ||
| get_managed_project_ids as _get_managed_project_ids, | ||
| ) | ||
|
|
||
| return await _get_managed_project_ids(db, user_id) | ||
|
|
||
|
|
||
| async def get_managed_org_ids(db: AsyncSession, user_id: str) -> list[str]: | ||
| from_members = select(OrganizationMember.organization_id.label("org_id")).where( | ||
| OrganizationMember.user_id == user_id, | ||
| OrganizationMember.role == MemberRole.MANAGER, | ||
| ) | ||
|
|
||
| from_orgs = select(Organization.id.label("org_id")).where(Organization.manager_id == user_id) | ||
|
|
||
| combined = union(from_members, from_orgs).subquery() | ||
| result = await db.execute(select(combined.c.org_id)) | ||
| return sorted(result.scalars().all()) | ||
|
|
||
|
|
||
| async def get_current_user_managed_org_ids( | ||
| db: AsyncSession = Depends(get_db), | ||
| user: User = Depends(get_current_user), | ||
| ) -> list[str]: | ||
| return await get_managed_org_ids(db, user.id) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,7 @@ | |
| from collections.abc import AsyncIterator | ||
| from contextlib import asynccontextmanager | ||
|
|
||
| from fastapi import FastAPI | ||
| from fastapi import Depends, FastAPI, status | ||
| from fastapi.middleware.cors import CORSMiddleware | ||
|
|
||
| from app.api.access_requests import router as access_requests_router | ||
|
|
@@ -41,6 +41,7 @@ | |
| from app.api.translation_helper import router as translation_helper_router | ||
| from app.api.uploads import router as uploads_router | ||
| from app.api.users import router as users_router | ||
| from app.core.auth_middleware import require_admin_or_manager | ||
| from app.core.config import get_settings | ||
| from app.core.database import AsyncSessionLocal, close_db, init_db | ||
| from app.core.exceptions import register_exception_handlers | ||
|
|
@@ -129,11 +130,32 @@ def create_app() -> FastAPI: | |
| app.include_router(platform_router, prefix="/api/platform", tags=["platform"]) | ||
| app.include_router(uploads_router, prefix="/api/uploads", tags=["uploads"]) | ||
| app.include_router(users_router, prefix="/api/users", tags=["users"]) | ||
| app.include_router(languages_router, prefix="/api/languages", tags=["languages"]) | ||
| app.include_router(organizations_router, prefix="/api/organizations", tags=["organizations"]) | ||
| console_guard = [Depends(require_admin_or_manager)] | ||
| app.include_router( | ||
| languages_router, | ||
| prefix="/api/languages", | ||
| tags=["languages"], | ||
| dependencies=console_guard, | ||
| ) | ||
| app.include_router( | ||
| organizations_router, | ||
| prefix="/api/organizations", | ||
| tags=["organizations"], | ||
| dependencies=console_guard, | ||
| ) | ||
| app.include_router(places_router, prefix="/api/places", tags=["places"]) | ||
| app.include_router(projects_router, prefix="/api/projects", tags=["projects"]) | ||
| app.include_router(phases_router, prefix="/api/phases", tags=["phases"]) | ||
| app.include_router( | ||
| projects_router, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| prefix="/api/projects", | ||
| tags=["projects"], | ||
| dependencies=console_guard, | ||
| ) | ||
| app.include_router( | ||
| phases_router, | ||
| prefix="/api/phases", | ||
| tags=["phases"], | ||
| dependencies=console_guard, | ||
| ) | ||
| app.include_router(books_router, prefix="/api/books", tags=["books"]) | ||
| app.include_router(pericopes_router, prefix="/api/pericopes", tags=["pericopes"]) | ||
| app.include_router(meaning_maps_router, prefix="/api/meaning-maps", tags=["meaning-maps"]) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| from sqlalchemy import select | ||
| from sqlalchemy.ext.asyncio import AsyncSession | ||
|
|
||
| from app.db.models.language import Language | ||
| from app.db.models.project import Project | ||
|
|
||
|
|
||
| async def list_languages_by_projects(db: AsyncSession, project_ids: list[str]) -> list[Language]: | ||
| if not project_ids: | ||
| return [] | ||
| language_ids_subq = select(Project.language_id).where(Project.id.in_(project_ids)).distinct() | ||
| stmt = select(Language).where(Language.id.in_(language_ids_subq)).order_by(Language.code) | ||
| result = await db.execute(stmt) | ||
| return list(result.scalars().all()) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| from sqlalchemy import select | ||
| from sqlalchemy.ext.asyncio import AsyncSession | ||
|
|
||
| from app.db.models.org import Organization | ||
| from app.db.models.project import ProjectOrganizationAccess | ||
|
|
||
|
|
||
| async def list_organizations_by_projects( | ||
| db: AsyncSession, project_ids: list[str] | ||
| ) -> list[Organization]: | ||
| if not project_ids: | ||
| return [] | ||
| org_ids_subq = ( | ||
| select(ProjectOrganizationAccess.organization_id) | ||
| .where(ProjectOrganizationAccess.project_id.in_(project_ids)) | ||
| .distinct() | ||
| ) | ||
| stmt = select(Organization).where(Organization.id.in_(org_ids_subq)).order_by(Organization.name) | ||
| result = await db.execute(stmt) | ||
| return list(result.scalars().unique().all()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: shemaobt/tripod-api
Length of output: 13753
Include organizations managed directly by the user.
list_organizations()only scopes non-admins throughget_managed_project_ids(), so users who manage an org directly but no projects passrequire_admin_or_managerand still get an empty list. Merge inget_managed_org_ids()here, or let the service accept both scopes.🤖 Prompt for AI Agents