feat: add share calendar API

This commit is contained in:
qzl
2026-02-28 12:15:59 +08:00
parent 709ae5ab73
commit 7a49783156
5 changed files with 204 additions and 1 deletions
@@ -10,6 +10,7 @@ from sqlalchemy.exc import SQLAlchemyError
from core.db.base_repository import BaseRepository
from core.logging import get_logger
from models.schedule_items import ScheduleItem
from models.schedule_subscriptions import ScheduleSubscription
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession
@@ -18,6 +19,7 @@ logger = get_logger("v1.schedule_items.repository")
class ScheduleItemRepository(Protocol):
async def get_by_id(self, entity_id: UUID) -> ScheduleItem | None: ...
async def get_by_item_id(
self, item_id: UUID, owner_id: UUID
) -> ScheduleItem | None: ...
@@ -31,6 +33,7 @@ class ScheduleItemRepository(Protocol):
async def list_by_date_range(
self, owner_id: UUID, start_at: datetime, end_at: datetime
) -> list[ScheduleItem]: ...
async def create_subscription(self, data: dict) -> ScheduleSubscription: ...
class SQLAlchemyScheduleItemRepository(BaseRepository[ScheduleItem]):
@@ -127,3 +130,12 @@ class SQLAlchemyScheduleItemRepository(BaseRepository[ScheduleItem]):
except SQLAlchemyError:
logger.exception("Schedule item list failed", owner_id=str(owner_id))
raise
async def create_subscription(self, data: dict) -> ScheduleSubscription:
sub = ScheduleSubscription(**data)
self._session.add(sub)
await self._session.flush()
return sub
async def get_by_id(self, entity_id: UUID) -> ScheduleItem | None:
return await super().get_by_id(entity_id)
+11
View File
@@ -12,6 +12,8 @@ from v1.schedule_items.schemas import (
ScheduleItemListItem,
ScheduleItemListRequest,
ScheduleItemResponse,
ScheduleItemShareRequest,
ScheduleItemShareResponse,
ScheduleItemUpdateRequest,
)
from v1.schedule_items.service import ScheduleItemService
@@ -72,3 +74,12 @@ async def delete_schedule_item(
service: Annotated[ScheduleItemService, Depends(get_schedule_item_service)],
) -> None:
await service.delete(item_id)
@router.post("/{item_id}/share", response_model=ScheduleItemShareResponse)
async def share_schedule_item(
item_id: UUID,
request: ScheduleItemShareRequest,
service: Annotated[ScheduleItemService, Depends(get_schedule_item_service)],
) -> ScheduleItemShareResponse:
return await service.share(item_id, request)
+21
View File
@@ -96,3 +96,24 @@ class ScheduleItemListItem(BaseModel):
class ScheduleItemListRequest(BaseModel):
start_at: datetime
end_at: datetime
class ScheduleItemShareRequest(BaseModel):
email: str = Field(..., description="Email of user to share with")
permission_view: bool = Field(True, description="Grant view permission")
permission_edit: bool = Field(False, description="Grant edit permission")
permission_invite: bool = Field(False, description="Grant invite permission")
def _permission_value(self) -> int:
value = 0
if self.permission_view:
value |= 1
if self.permission_edit:
value |= 4
if self.permission_invite:
value |= 2
return value
class ScheduleItemShareResponse(BaseModel):
message: str
+59 -1
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import json
from typing import TYPE_CHECKING, Protocol
from uuid import UUID
from fastapi import HTTPException
@@ -9,13 +10,17 @@ from sqlalchemy.exc import SQLAlchemyError
from core.auth.models import CurrentUser
from core.db.base_service import BaseService
from core.logging import get_logger
from models.inbox_messages import InboxMessage, InboxMessageType
from models.schedule_items import ScheduleItem
from v1.auth.gateway import SupabaseAuthGateway
from v1.schedule_items.repository import ScheduleItemRepository
from v1.schedule_items.schemas import (
ScheduleItemCreateRequest,
ScheduleItemListRequest,
ScheduleItemMetadata,
ScheduleItemResponse,
ScheduleItemShareRequest,
ScheduleItemShareResponse,
ScheduleItemUpdateRequest,
ScheduleItemSourceType,
ScheduleItemStatus,
@@ -24,22 +29,31 @@ from v1.schedule_items.schemas import (
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession
from v1.auth.schemas import UserByEmailResponse
logger = get_logger("v1.schedule_items.service")
class AuthByEmailGateway(Protocol):
async def get_user_by_email(self, email: str) -> "UserByEmailResponse": ...
class ScheduleItemService(BaseService):
_repository: ScheduleItemRepository
_session: AsyncSession
_auth_gateway: AuthByEmailGateway
def __init__(
self,
repository: ScheduleItemRepository,
session: AsyncSession,
current_user: CurrentUser | None,
auth_gateway: AuthByEmailGateway | None = None,
) -> None:
super().__init__(current_user=current_user)
self._repository = repository
self._session = session
self._auth_gateway = auth_gateway or SupabaseAuthGateway()
async def create(self, request: ScheduleItemCreateRequest) -> ScheduleItemResponse:
user_id = self.require_user_id()
@@ -179,6 +193,50 @@ class ScheduleItemService(BaseService):
return [self._to_response(item) for item in items]
async def share(
self, item_id: UUID, request: ScheduleItemShareRequest
) -> ScheduleItemShareResponse:
user_id = self.require_user_id()
try:
item = await self._repository.get_by_id(item_id)
if item is None:
raise HTTPException(status_code=404, detail="Schedule item not found")
if item.owner_id != user_id:
raise HTTPException(
status_code=403,
detail="Only owner can share this schedule item",
)
target_user = await self._auth_gateway.get_user_by_email(request.email)
recipient_id = UUID(target_user.id)
message = InboxMessage(
recipient_id=recipient_id,
sender_id=user_id,
message_type=InboxMessageType.CALENDAR,
schedule_item_id=item.id,
content=json.dumps({"permission": request._permission_value()}),
created_by=user_id,
)
self._session.add(message)
await self._session.commit()
except HTTPException:
raise
except SQLAlchemyError:
await self._session.rollback()
logger.exception("Failed to share schedule item", item_id=str(item_id))
raise HTTPException(
status_code=503, detail="Schedule item store unavailable"
)
except ValueError:
await self._session.rollback()
logger.exception(
"Auth lookup returned invalid user id", email=request.email
)
raise HTTPException(status_code=503, detail="Auth lookup unavailable")
return ScheduleItemShareResponse(message="Calendar invitation sent")
def _to_response(self, item: ScheduleItem) -> ScheduleItemResponse:
return ScheduleItemResponse(
id=item.id,