Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
fa20849
style: line length
michael-pisman Oct 26, 2023
dc5ef91
fix: Corrected resource
michael-pisman Oct 26, 2023
dab954d
feat: Added member routes
michael-pisman Oct 26, 2023
95b89d3
feat: Added routes to get resource permissions
michael-pisman Oct 26, 2023
ec059d9
fix: Fixed Poll Request schema
michael-pisman Oct 26, 2023
74f4cf3
feat: Added policies routes
michael-pisman Oct 26, 2023
cf6a500
feat: Created separate dirrectories for versions
michael-pisman Oct 26, 2023
a82257d
feat: Added fastapi fastapi-versioning
michael-pisman Oct 29, 2023
8f5ec84
revert: Removed fastapi-versioning
michael-pisman Oct 30, 2023
1ff7832
feat: API versioning system
michael-pisman Oct 30, 2023
0d28d1d
fix: Remove unused import statement in app.py
michael-pisman Oct 30, 2023
4ab0ee8
test: Moved old tests to test_v1 folder
michael-pisman Oct 30, 2023
731439d
fix: Disable default redoc in FastAPI application
michael-pisman Oct 30, 2023
b451ef2
refactor: Added ReDoc endpoints to the router
michael-pisman Oct 30, 2023
e17b0e0
refactor: Reordered v2 routes
michael-pisman Oct 30, 2023
cebe5bd
refactor: Reordered v1 api routes
michael-pisman Oct 30, 2023
ae4bf29
style: flake8
michael-pisman Oct 30, 2023
92db5e7
feat: Add unique ID generation for API endpoints
michael-pisman Oct 30, 2023
3912802
refactor: Replaced version specific function with one function for ge…
michael-pisman Oct 30, 2023
0ef9831
style: Removed extra new lines
michael-pisman Nov 1, 2023
886e583
refactor: Updated websocket authentication
michael-pisman Nov 1, 2023
2006124
fix: Updated WebSocketManager
michael-pisman Nov 1, 2023
12f255f
Added token based authentication for WebSockets
michael-pisman Nov 1, 2023
68c9b7f
feat: Added websocket message schema
michael-pisman Nov 1, 2023
6aa0d57
feat: Created websocket message parser
michael-pisman Nov 1, 2023
b498692
feat: Added websocket exceptions
michael-pisman Nov 1, 2023
3126844
refactor: Specified active_user ContextVar type
michael-pisman Nov 1, 2023
8b85dc2
refactor: Updated action parser
michael-pisman Nov 1, 2023
dcd8faf
refactor: Improved error handling
michael-pisman Nov 2, 2023
9788101
style: Added comments
michael-pisman Nov 2, 2023
f59685d
refactor: Changed websocket_auth to return account
michael-pisman Nov 2, 2023
54d3037
style: Added return type to action_parser
michael-pisman Nov 2, 2023
ca63e0a
feat: Added websocket actions
michael-pisman Nov 2, 2023
c66a2e5
refactor: Updated filter_arguments to use websocket actions
michael-pisman Nov 2, 2023
b14d205
feat: Add group actions to websocket.py
michael-pisman Nov 2, 2023
b3ff19d
refactor: Add HTTPException handling to WebSocket route
michael-pisman Nov 2, 2023
b0065cb
style: Remove unused import in websocket_manager.py
michael-pisman Nov 2, 2023
7c4bf21
fix: Fixed websocket_auth to query non-expired tokens
michael-pisman Nov 2, 2023
ccba456
fix: create_workspace requires data of type BaseModel
michael-pisman Nov 3, 2023
63b6f6a
refactor: Added success response for when action does not return a model
michael-pisman Nov 3, 2023
6a81397
feat: Added group and member actions
michael-pisman Nov 5, 2023
6a838a9
refactor: Updated workspace and group actions, removed member actions
michael-pisman Nov 7, 2023
4c40899
feat: Add member and policy actions
michael-pisman Nov 7, 2023
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
2 changes: 1 addition & 1 deletion src/unipoll_api/account_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,4 @@ def get_database_strategy(access_token_db=Depends(get_access_token_db)) -> Datab
fastapi_users = FastAPIUsers[Account, PydanticObjectId](get_user_manager, [jwt_backend, cookie_backend]) # type: ignore

get_current_active_user = fastapi_users.current_user(active=True)
active_user: ContextVar = ContextVar("active_user")
active_user: ContextVar[Account] = ContextVar("active_user")
1 change: 1 addition & 0 deletions src/unipoll_api/actions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
from . import workspace as WorkspaceActions # noqa: F401
from . import permissions as PermissionsActions # noqa: F401
from . import members as MembersActions # noqa: F401
from . import websocket as WebsocketActions # noqa: F401
3 changes: 2 additions & 1 deletion src/unipoll_api/actions/members.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
from unipoll_api.dependencies import get_member


async def get_members(resource: Workspace | Group, check_permissions: bool = True) -> MemberSchemas.MemberList:
async def get_members(resource: Workspace | Group,
check_permissions: bool = True) -> MemberSchemas.MemberList:
# Check if the user has permission to add members
await Permissions.check_permissions(resource, "get_members", check_permissions)

Expand Down
2 changes: 1 addition & 1 deletion src/unipoll_api/actions/poll.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ async def create_poll(workspace: Workspace,
input_data: PollSchemas.CreatePollRequest,
check_permissions: bool = True) -> PollSchemas.PollResponse:
# Check if the user has permission to create polls
await Permissions.check_permissions(actions, "create_polls", check_permissions)
await Permissions.check_permissions(workspace, "create_polls", check_permissions)

# Check if poll name is unique
poll: Poll # For type hinting, until Link type is supported
Expand Down
94 changes: 94 additions & 0 deletions src/unipoll_api/actions/websocket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from typing import Literal
from unipoll_api import dependencies
from . import WorkspaceActions, GroupActions, MembersActions, PolicyActions
from unipoll_api.schemas import WorkspaceSchemas, GroupSchemas, MemberSchemas
from unipoll_api.documents import ResourceID
from unipoll_api.exceptions.resource import APIException


# Workspace actions


async def get_workspaces():
return await WorkspaceActions.get_workspaces()


async def create_workspace(name: str, description: str = ""):
data = WorkspaceSchemas.WorkspaceCreateInput(name=name, description=description)
return await WorkspaceActions.create_workspace(data)


async def get_workspace_info(workspace_id: ResourceID):
Copy link

Copilot AI Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable 'args' is referenced but not defined in this function. This will result in a NameError at runtime.

Suggested change
async def get_workspace_info(workspace_id: ResourceID):
async def get_workspace_info(workspace_id: ResourceID, **args):

Copilot uses AI. Check for mistakes.
workspace = await dependencies.get_workspace(workspace_id)
return await WorkspaceActions.get_workspace(workspace, **args)


async def update_workspace_info(workspace_id: ResourceID, name: str, description: str = ""):
workspace = await dependencies.get_workspace(workspace_id)
data = WorkspaceSchemas.WorkspaceUpdateRequest(name=name, description=description)
return await WorkspaceActions.update_workspace(workspace, data)


async def delete_workspace(workspace_id: ResourceID):
workspace = await dependencies.get_workspace(workspace_id)
return await WorkspaceActions.delete_workspace(workspace)


async def get_workspace_members(workspace_id: ResourceID):
workspace = await dependencies.get_workspace(workspace_id)
return await MembersActions.get_members(workspace)


async def add_workspace_members(workspace_id: ResourceID, account_id_list: list[ResourceID]):
workspace = await dependencies.get_workspace(workspace_id)
return await MembersActions.add_members(workspace, account_id_list)


async def get_workspace_policies(workspace_id: ResourceID):
workspace = await dependencies.get_workspace(workspace_id)
return await PolicyActions.get_policies(resource=workspace)


# Group actions


async def get_groups(workspace_id: ResourceID):
workspace = await dependencies.get_workspace(workspace_id)
return await GroupActions.get_groups(workspace)


async def create_group(workspace_id: ResourceID, name: str, description: str = ""):
workspace = await dependencies.get_workspace(workspace_id)
# data = GroupSchemas.GroupCreateInput(name=name, description=description)
return await GroupActions.create_group(workspace, name, description)


async def get_group_info(group_id: ResourceID):
group = await dependencies.get_group(group_id)
return await GroupActions.get_group(group)


async def update_group_info(group_id: ResourceID, name: str, description: str = ""):
group = await dependencies.get_group(group_id)
data = GroupSchemas.GroupUpdateRequest(name=name, description=description)
return await GroupActions.update_group(group, data)


async def delete_group(group_id: ResourceID):
group = await dependencies.get_group(group_id)
return await GroupActions.delete_group(group)


async def get_group_members(group_id: ResourceID):
group = await dependencies.get_group(group_id)
return await MembersActions.get_members(group)


async def add_group_members(group_id: ResourceID, account_id_list: list[ResourceID]):
group = await dependencies.get_group(group_id)
return await MembersActions.add_members(group, account_id_list)


async def get_group_policies(group_id: ResourceID):
group = await dependencies.get_group(group_id)
return await PolicyActions.get_policies(resource=group)
8 changes: 6 additions & 2 deletions src/unipoll_api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
from fastapi.routing import APIRoute
from fastapi.middleware.cors import CORSMiddleware
from beanie import init_beanie
from unipoll_api.routes import router
# from unipoll_api.routes import router, websocket
from unipoll_api.routes import create_router
from unipoll_api.mongo_db import mainDB, documentModels
from unipoll_api.config import get_settings
from unipoll_api.__version__ import version
Expand All @@ -18,13 +19,16 @@

# Create FastAPI application
app = FastAPI(
docs_url=None, # Disable default docs
redoc_url=None, # Disable default redoc
title=settings.app_name, # Title of the application
description=settings.app_description, # Description of the application
version=settings.app_version, # Version of the application
)


# Add endpoints defined in the routes directory
app.include_router(router)
app.include_router(create_router(app, 2)) # Set default API version to 2

# Add CORS middleware to allow cross-origin requests
origins = settings.origins.split(",")
Expand Down
20 changes: 15 additions & 5 deletions src/unipoll_api/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from typing import Annotated
from functools import wraps
# from bson import DBRef
from fastapi import Cookie, Depends, Query, HTTPException, WebSocket
from fastapi import Cookie, Depends, Query, HTTPException
from unipoll_api.account_manager import active_user, get_current_active_user
from unipoll_api.documents import ResourceID, Workspace, Group, Account, Poll, Policy, Member
from unipoll_api import exceptions as Exceptions
from unipoll_api.account_manager import get_access_token_db, get_database_strategy
from datetime import timedelta, timezone, datetime


# Wrapper to handle exceptions and raise HTTPException
Expand Down Expand Up @@ -41,10 +43,18 @@ async def get_member(account: Account, resource: Workspace | Group) -> Member:
raise Exceptions.ResourceExceptions.ResourceNotFound("member", account.id)


async def websocket_auth(websocket: WebSocket,
session: Annotated[str | None, Cookie()] = None,
token: Annotated[str | None, Query()] = None) -> dict:
return {"cookie": session, "token": token}
async def websocket_auth(session: Annotated[str | None, Cookie()] = None,
token: Annotated[str | None, Query()] = None,
token_db=Depends(get_access_token_db),
strategy=Depends(get_database_strategy)
) -> Account:
user = None
if token:
max_age = datetime.now(timezone.utc) - timedelta(seconds=strategy.lifetime_seconds)
token_data = await token_db.get_by_token(token, max_age)
if token_data:
user = await Account.get(token_data.user_id)
return user


# Dependency for getting a workspace with the given id
Expand Down
1 change: 1 addition & 0 deletions src/unipoll_api/exceptions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
from . import poll as PollExceptions # noqa: F401
from . import resource as ResourceExceptions # noqa: F401
from . import workspace as WorkspaceExceptions # noqa: F401
from . import websocket as WebSocketExceptions # noqa: F401
29 changes: 29 additions & 0 deletions src/unipoll_api/exceptions/websocket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from fastapi import status
from unipoll_api.exceptions.resource import APIException


class AuthenticationError(APIException):
def __init__(self):
super().__init__(code=status.WS_1008_POLICY_VIOLATION,
detail="Invalid access token or authorization header")


class InvalidAction(APIException):
def __init__(self, action: str):
super().__init__(code=status.WS_1003_UNSUPPORTED_DATA,
detail=f'Action "{action}" not found')


class InvalidMessageData(APIException):
def __init__(self, action: str, valid_args: list[str], data: list[str]):
super().__init__(code=status.WS_1003_UNSUPPORTED_DATA,
detail=f'Invalid data for action "{action}".' +
f'Valid arguments are: {valid_args}.' +
f'You provided: {data}')


class ActionMissingRequiredArgs(APIException):
def __init__(self, action: str, required_args: list[str], provided_args: list[str]):
super().__init__(code=status.WS_1003_UNSUPPORTED_DATA,
detail=f"The action '{action}' requires arguments: {required_args}. " +
f"You provided: {provided_args}")
98 changes: 50 additions & 48 deletions src/unipoll_api/routes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,50 @@
from fastapi import APIRouter, Depends
from unipoll_api.dependencies import set_active_user

# Impport endpoints defined in the routes directory
from . import account as AccountRoutes
from . import authentication as AuthenticationRoutes
from . import group as GroupRoutes
from . import poll as PollRoutes
from . import websocket as WebSocketRoutes
from . import workspace as WorkspaceRoutes

# Create main router
router: APIRouter = APIRouter()

# Add endpoints defined in the routes directory
router.include_router(WorkspaceRoutes.open_router,
prefix="/workspaces",
tags=["Workspaces"],
dependencies=[Depends(set_active_user)])
router.include_router(WorkspaceRoutes.router,
prefix="/workspaces",
tags=["Workspaces"],
dependencies=[Depends(set_active_user)])
router.include_router(GroupRoutes.open_router,
prefix="/groups",
tags=["Groups"],
dependencies=[Depends(set_active_user)])
router.include_router(GroupRoutes.router,
prefix="/groups",
tags=["Groups"],
dependencies=[Depends(set_active_user)])
router.include_router(PollRoutes.router,
prefix="/polls",
tags=["Polls"],
dependencies=[Depends(set_active_user)])
router.include_router(WebSocketRoutes.router,
prefix="/ws",
tags=["WebSocket"])
router.include_router(AccountRoutes.router,
prefix="/accounts",
tags=["Accounts"],
dependencies=[Depends(set_active_user)])
router.include_router(AuthenticationRoutes.router,
prefix="/auth",
tags=["Authentication"])
router.include_router(WebSocketRoutes.router,
prefix="/ws",
tags=["WebSocket"])
from fastapi import APIRouter
from fastapi.routing import APIRoute
from unipoll_api.utils import api_versioning
from .v1 import router as v1_router
from .v2 import router as v2_router
from .swagger_docs import create_doc_router
from .websocket import router as websocket_router


# Function to generate unique operation IDs for different API versions
def generate_unique_id(version: int):
def func(route: APIRoute):
return f"api-v{version}-{route.tags[0]}-{route.name}"
return func


# Function to create API Router
def create_router(app, default_version):
# API Router that contains all of the endpoints
router = APIRouter()

# Dictionary of endpoints for each version of the API
endpoints = {
1: v1_router,
2: v2_router,
}

# Default API version
router.include_router(endpoints[default_version])
router.include_router(websocket_router, prefix="/ws", tags=["WebSocket"])

# Add API v1 endpoints to the main router
router.include_router(v1_router,
prefix="/v1",
generate_unique_id_function=generate_unique_id(1))
# Add API v1 OpenAPI schemas for documentation
api_versioning.add_api(v1_router, version=1)

# Add API v2 endpoints to the main router
router.include_router(v2_router,
prefix="/v2",
generate_unique_id_function=generate_unique_id(2))
# Add API v2 OpenAPI schemas for documentation
api_versioning.add_api(v2_router, version=2)

# Swagger Documentation Endpoints
router.include_router(create_doc_router(app, default_version))

# Return the main router
return router
63 changes: 63 additions & 0 deletions src/unipoll_api/routes/swagger_docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from fastapi import APIRouter
from fastapi.openapi.docs import (
get_redoc_html,
get_swagger_ui_html,
get_swagger_ui_oauth2_redirect_html,
)
from unipoll_api.utils import api_versioning


# Function to create router for documentation endpoints
def create_doc_router(app, default_version):
# Router for documentation endpoints
doc_router = APIRouter()

@doc_router.get("/v{version}/openapi.json", include_in_schema=False)
async def get_openapi_schema(version: int):
return api_versioning.openapi_schemas[version]

# Default Docs
@doc_router.get("/docs", include_in_schema=False)
async def default_swagger_ui_html():
return get_swagger_ui_html(
openapi_url=f"/v{default_version}/openapi.json",
title=app.title + " - Swagger UI",
oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
swagger_js_url="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js",
swagger_css_url="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css",
)

# Versioned Docs
@doc_router.get("/v{version}/docs", include_in_schema=False)
async def versioned_swagger_ui_html(version: int):
return get_swagger_ui_html(
openapi_url=f"/v{version}/openapi.json",
title=app.title + " - Swagger UI",
oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
swagger_js_url="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js",
swagger_css_url="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css",
)

@doc_router.get(app.swagger_ui_oauth2_redirect_url, include_in_schema=False)
async def swagger_ui_redirect():
return get_swagger_ui_oauth2_redirect_html()

# Default ReDoc
@doc_router.get("/redoc", include_in_schema=False)
async def default_redoc_html():
return get_redoc_html(
openapi_url=f"/v{default_version}/openapi.json",
title=app.title + " - ReDoc",
redoc_js_url="https://unpkg.com/redoc@next/bundles/redoc.standalone.js",
)

# Versioned ReDoc
@doc_router.get("/v{version}/redoc", include_in_schema=False)
async def versioned_redoc_html(version: int):
return get_redoc_html(
openapi_url=f"/v{version}/openapi.json",
title=app.title + " - ReDoc",
redoc_js_url="https://unpkg.com/redoc@next/bundles/redoc.standalone.js",
)

return doc_router
Loading
Loading