-
Notifications
You must be signed in to change notification settings - Fork 0
routerの作成 #8
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
routerの作成 #8
Changes from 2 commits
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 |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| from fastapi import Depends | ||
| from sqlalchemy.ext.asyncio import AsyncSession | ||
| from app.infrastructure.database import get_session | ||
| from app.repositories.user_repository import UserRepository | ||
| from app.repositories.trip_repository import TripRepository | ||
| from app.repositories.trip_member_repository import TripMemberRepository | ||
| from app.repositories.spot_repository import SpotRepository | ||
| from app.repositories.candidate_spot_repository import CandidateSpotRepository | ||
| from app.repositories.candidate_reaction_repository import CandidateReactionRepository | ||
| from app.repositories.itinerary_activity_repository import ItineraryActivityRepository | ||
| from app.usecases.user_usecase import UserUsecase | ||
| from app.usecases.trip_usecase import TripUsecase | ||
| from app.usecases.trip_member_usecase import TripMemberUsecase | ||
| from app.usecases.candidate_spot_usecase import CandidateSpotUsecase | ||
| from app.usecases.candidate_reaction_usecase import CandidateReactionUsecase | ||
| from app.usecases.itinerary_usecase import ItineraryUsecase | ||
|
|
||
|
|
||
| def get_user_usecase(db: AsyncSession = Depends(get_session)) -> UserUsecase: | ||
| return UserUsecase(UserRepository(db)) | ||
|
|
||
|
|
||
| def get_trip_usecase(db: AsyncSession = Depends(get_session)) -> TripUsecase: | ||
| return TripUsecase(TripRepository(db), TripMemberRepository(db)) | ||
|
|
||
|
|
||
| def get_trip_member_usecase(db: AsyncSession = Depends(get_session)) -> TripMemberUsecase: | ||
| return TripMemberUsecase(TripRepository(db), TripMemberRepository(db)) | ||
|
|
||
|
|
||
| def get_candidate_spot_usecase(db: AsyncSession = Depends(get_session)) -> CandidateSpotUsecase: | ||
| return CandidateSpotUsecase( | ||
| CandidateSpotRepository(db), | ||
| SpotRepository(db), | ||
| TripMemberRepository(db), | ||
| ) | ||
|
|
||
|
|
||
| def get_candidate_reaction_usecase( | ||
| db: AsyncSession = Depends(get_session), | ||
| ) -> CandidateReactionUsecase: | ||
| return CandidateReactionUsecase( | ||
| CandidateReactionRepository(db), | ||
| CandidateSpotRepository(db), | ||
| TripMemberRepository(db), | ||
| ) | ||
|
|
||
|
|
||
| def get_itinerary_usecase(db: AsyncSession = Depends(get_session)) -> ItineraryUsecase: | ||
| return ItineraryUsecase( | ||
| ItineraryActivityRepository(db), | ||
| TripRepository(db), | ||
| TripMemberRepository(db), | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| from pydantic_settings import BaseSettings, SettingsConfigDict | ||
|
|
||
|
|
||
| class Settings(BaseSettings): | ||
| model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") | ||
|
|
||
| DATABASE_URL: str | ||
| SQL_ECHO: bool = False | ||
| SUPABASE_JWT_SECRET: str | ||
| GOOGLE_PLACES_API_KEY: str = "" | ||
|
|
||
|
|
||
| settings = Settings() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import uuid | ||
| import jwt | ||
| from fastapi import Depends | ||
| from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials | ||
| from app.config.env import settings | ||
| from app.exceptions.app_exceptions import UnauthorizedException | ||
|
|
||
| _bearer = HTTPBearer() | ||
|
|
||
|
|
||
| async def get_current_user_id( | ||
| credentials: HTTPAuthorizationCredentials = Depends(_bearer), | ||
| ) -> uuid.UUID: | ||
| try: | ||
| payload = jwt.decode( | ||
| credentials.credentials, | ||
| settings.SUPABASE_JWT_SECRET, | ||
| algorithms=["HS256"], | ||
| options={"verify_aud": False}, | ||
| ) | ||
| return uuid.UUID(payload["sub"]) | ||
| except (jwt.InvalidTokenError, KeyError, ValueError): | ||
| raise UnauthorizedException() | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,35 +1,28 @@ | ||||||||||||||||||
| from contextlib import asynccontextmanager | ||||||||||||||||||
| from app.infrastructure.database import init_db, engine | ||||||||||||||||||
| import app.models # noqa: F401 — モデルを Base.metadata に登録するために必要 | ||||||||||||||||||
| from fastapi import FastAPI | ||||||||||||||||||
| from fastapi.exceptions import RequestValidationError | ||||||||||||||||||
| from app.infrastructure.database import init_db, engine | ||||||||||||||||||
| import app.models # noqa: F401 | ||||||||||||||||||
| from app.exceptions.app_exceptions import AppException | ||||||||||||||||||
| from app.exceptions.handlers import app_exception_handler, validation_exception_handler | ||||||||||||||||||
| from app.routers import users, trips, trip_members, candidates, reactions, itinerary | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| @asynccontextmanager | ||||||||||||||||||
| async def lifespan(app: FastAPI): | ||||||||||||||||||
| # ① 【起動時】お店を開ける前の準備(yield の前) | ||||||||||||||||||
| print("アプリケーションの起動処理を開始...") | ||||||||||||||||||
| await init_db() # ← DBのテーブルを作成したり、接続の準備をする | ||||||||||||||||||
|
|
||||||||||||||||||
| yield # ② 【営業中】ここでAPIサーバーが立ち上がり、ユーザーからのアクセスを受け付け始める! | ||||||||||||||||||
|
|
||||||||||||||||||
| # ③ 【終了時】サーバーを停止したときの片付け(yield の後) | ||||||||||||||||||
| print("シャットダウンします。DBの接続を閉じます...") | ||||||||||||||||||
| await init_db() | ||||||||||||||||||
| yield | ||||||||||||||||||
| await engine.dispose() | ||||||||||||||||||
|
Comment on lines
+13
to
15
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. DB エンジン破棄を lifespan 中に例外が流れると、 修正案 async def lifespan(app: FastAPI):
await init_db()
- yield
- await engine.dispose()
+ try:
+ yield
+ finally:
+ await engine.dispose()📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| app = FastAPI(lifespan=lifespan) | ||||||||||||||||||
|
|
||||||||||||||||||
| app.add_exception_handler(AppException, app_exception_handler) | ||||||||||||||||||
| app.add_exception_handler(RequestValidationError, validation_exception_handler) | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| @app.get("/") | ||||||||||||||||||
| def read_root(): | ||||||||||||||||||
| return {"Hello": "World"} | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| @app.get("/items/{item_id}") | ||||||||||||||||||
| def read_item(item_id: int, q: str | None = None): | ||||||||||||||||||
| return {"item_id": item_id, "q": q} | ||||||||||||||||||
| app.include_router(users.router, prefix="/api/v1") | ||||||||||||||||||
| app.include_router(trips.router, prefix="/api/v1") | ||||||||||||||||||
| app.include_router(trip_members.router, prefix="/api/v1") | ||||||||||||||||||
| app.include_router(candidates.router, prefix="/api/v1") | ||||||||||||||||||
| app.include_router(reactions.router, prefix="/api/v1") | ||||||||||||||||||
| app.include_router(itinerary.router, prefix="/api/v1") | ||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| import uuid | ||
| from fastapi import APIRouter, Depends, Query, status | ||
| from app.config.jwt import get_current_user_id | ||
| from app.config.dependency import get_candidate_spot_usecase | ||
| from app.usecases.candidate_spot_usecase import CandidateSpotUsecase | ||
| from app.schemas.requests.candidate_spot import ( | ||
| CandidateSpotCreateRequest, | ||
| CandidateSpotStatusUpdateRequest, | ||
| ) | ||
| from app.schemas.responses.candidate_spot import CandidateSpotResponse, CandidateSpotListResponse | ||
|
|
||
| router = APIRouter(prefix="/trips", tags=["candidates"]) | ||
|
|
||
|
|
||
| @router.post( | ||
| "/{trip_id}/candidates", | ||
| response_model=CandidateSpotResponse, | ||
| status_code=status.HTTP_201_CREATED, | ||
| ) | ||
| async def add_candidate( | ||
| trip_id: uuid.UUID, | ||
| request: CandidateSpotCreateRequest, | ||
| current_user_id: uuid.UUID = Depends(get_current_user_id), | ||
| usecase: CandidateSpotUsecase = Depends(get_candidate_spot_usecase), | ||
| ): | ||
| return await usecase.add(trip_id, current_user_id, request) | ||
|
|
||
|
|
||
| @router.get("/{trip_id}/candidates", response_model=CandidateSpotListResponse) | ||
| async def list_candidates( | ||
| trip_id: uuid.UUID, | ||
| candidate_status: str | None = Query(default=None,alias="status"), | ||
| current_user_id: uuid.UUID = Depends(get_current_user_id), | ||
| usecase: CandidateSpotUsecase = Depends(get_candidate_spot_usecase), | ||
| ): | ||
| items = await usecase.get_list(trip_id, current_user_id, candidate_status) | ||
| return {"data": items} | ||
|
|
||
|
|
||
| @router.patch("/{trip_id}/candidates/{candidate_id}", response_model=CandidateSpotResponse) | ||
| async def update_candidate_status( | ||
| trip_id: uuid.UUID, | ||
| candidate_id: uuid.UUID, | ||
| request: CandidateSpotStatusUpdateRequest, | ||
| current_user_id: uuid.UUID = Depends(get_current_user_id), | ||
| usecase: CandidateSpotUsecase = Depends(get_candidate_spot_usecase), | ||
| ): | ||
| return await usecase.update_status(trip_id, candidate_id, current_user_id, request) | ||
|
|
||
|
|
||
| @router.delete("/{trip_id}/candidates/{candidate_id}", status_code=status.HTTP_204_NO_CONTENT) | ||
| async def delete_candidate( | ||
| trip_id: uuid.UUID, | ||
| candidate_id: uuid.UUID, | ||
| current_user_id: uuid.UUID = Depends(get_current_user_id), | ||
| usecase: CandidateSpotUsecase = Depends(get_candidate_spot_usecase), | ||
| ): | ||
| await usecase.delete(trip_id, candidate_id, current_user_id) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| import uuid | ||
| from fastapi import APIRouter, Depends, status | ||
| from app.config.jwt import get_current_user_id | ||
| from app.config.dependency import get_itinerary_usecase | ||
| from app.usecases.itinerary_usecase import ItineraryUsecase | ||
| from app.schemas.requests.itinerary import ( | ||
| ActivityCreateRequest, | ||
| ActivityUpdateRequest, | ||
| ActivityReorderRequest, | ||
| ) | ||
| from app.schemas.responses.itinerary import ActivityResponse, ItineraryResponse, ReorderResponse | ||
|
|
||
| router = APIRouter(prefix="/trips", tags=["itinerary"]) | ||
|
|
||
|
|
||
| @router.get("/{trip_id}/itinerary", response_model=ItineraryResponse) | ||
| async def get_itinerary( | ||
| trip_id: uuid.UUID, | ||
| current_user_id: uuid.UUID = Depends(get_current_user_id), | ||
| usecase: ItineraryUsecase = Depends(get_itinerary_usecase), | ||
| ): | ||
| return await usecase.get_itinerary(trip_id, current_user_id) | ||
|
|
||
|
|
||
| @router.post( | ||
| "/{trip_id}/itinerary", | ||
| response_model=ActivityResponse, | ||
| status_code=status.HTTP_201_CREATED, | ||
| ) | ||
| async def add_activity( | ||
| trip_id: uuid.UUID, | ||
| request: ActivityCreateRequest, | ||
| current_user_id: uuid.UUID = Depends(get_current_user_id), | ||
| usecase: ItineraryUsecase = Depends(get_itinerary_usecase), | ||
| ): | ||
| return await usecase.add_activity(trip_id, current_user_id, request) | ||
|
|
||
|
|
||
| @router.patch("/{trip_id}/itinerary/{activity_id}", response_model=ActivityResponse) | ||
| async def update_activity( | ||
| trip_id: uuid.UUID, | ||
| activity_id: uuid.UUID, | ||
| request: ActivityUpdateRequest, | ||
| current_user_id: uuid.UUID = Depends(get_current_user_id), | ||
| usecase: ItineraryUsecase = Depends(get_itinerary_usecase), | ||
| ): | ||
| return await usecase.update_activity(trip_id, activity_id, current_user_id, request) | ||
|
|
||
|
|
||
| @router.delete("/{trip_id}/itinerary/{activity_id}", status_code=status.HTTP_204_NO_CONTENT) | ||
| async def delete_activity( | ||
| trip_id: uuid.UUID, | ||
| activity_id: uuid.UUID, | ||
| current_user_id: uuid.UUID = Depends(get_current_user_id), | ||
| usecase: ItineraryUsecase = Depends(get_itinerary_usecase), | ||
| ): | ||
| await usecase.delete_activity(trip_id, activity_id, current_user_id) | ||
|
|
||
|
|
||
| @router.patch("/{trip_id}/itinerary/reorder", response_model=ReorderResponse) | ||
| async def reorder_activities( | ||
| trip_id: uuid.UUID, | ||
| request: ActivityReorderRequest, | ||
| current_user_id: uuid.UUID = Depends(get_current_user_id), | ||
| usecase: ItineraryUsecase = Depends(get_itinerary_usecase), | ||
| ): | ||
| updated_count = await usecase.reorder(trip_id, current_user_id, request) | ||
| return {"updated_count": updated_count} | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import uuid | ||
| from fastapi import APIRouter, Depends, Query, status | ||
| from app.config.jwt import get_current_user_id | ||
| from app.config.dependency import get_candidate_reaction_usecase | ||
| from app.usecases.candidate_reaction_usecase import CandidateReactionUsecase | ||
| from app.schemas.requests.candidate_reaction import CandidateReactionUpsertRequest | ||
| from app.schemas.responses.candidate_reaction import CandidateReactionResponse | ||
|
|
||
| router = APIRouter(prefix="/trips", tags=["reactions"]) | ||
|
|
||
|
|
||
| @router.put( | ||
| "/{trip_id}/candidates/{candidate_id}/reactions", | ||
| response_model=CandidateReactionResponse, | ||
| ) | ||
| async def upsert_reaction( | ||
| trip_id: uuid.UUID, | ||
| candidate_id: uuid.UUID, | ||
| request: CandidateReactionUpsertRequest, | ||
| current_user_id: uuid.UUID = Depends(get_current_user_id), | ||
| usecase: CandidateReactionUsecase = Depends(get_candidate_reaction_usecase), | ||
| ): | ||
| return await usecase.upsert(trip_id, candidate_id, current_user_id, request) | ||
|
|
||
|
|
||
| @router.delete( | ||
| "/{trip_id}/candidates/{candidate_id}/reactions", | ||
| status_code=status.HTTP_204_NO_CONTENT, | ||
| ) | ||
| async def delete_reaction( | ||
| trip_id: uuid.UUID, | ||
| candidate_id: uuid.UUID, | ||
| emoji_type: str = Query(), | ||
| current_user_id: uuid.UUID = Depends(get_current_user_id), | ||
| usecase: CandidateReactionUsecase = Depends(get_candidate_reaction_usecase), | ||
| ): | ||
| await usecase.delete(trip_id, candidate_id, current_user_id, emoji_type) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import uuid | ||
| from fastapi import APIRouter, Depends, status | ||
| from app.config.jwt import get_current_user_id | ||
| from app.config.dependency import get_trip_member_usecase | ||
| from app.usecases.trip_member_usecase import TripMemberUsecase | ||
| from app.schemas.requests.trip import TripJoinRequest | ||
| from app.schemas.requests.trip_member import TripMemberRoleUpdateRequest | ||
| from app.schemas.responses.trip_member import TripMemberResponse | ||
|
|
||
| router = APIRouter(prefix="/trips", tags=["trip_members"]) | ||
|
|
||
|
|
||
| @router.post("/join", response_model=TripMemberResponse, status_code=status.HTTP_201_CREATED) | ||
| async def join_trip( | ||
| request: TripJoinRequest, | ||
| current_user_id: uuid.UUID = Depends(get_current_user_id), | ||
| usecase: TripMemberUsecase = Depends(get_trip_member_usecase), | ||
| ): | ||
| return await usecase.join(request.invite_code, current_user_id) | ||
|
|
||
|
|
||
| @router.patch("/{trip_id}/members/{user_id}", response_model=TripMemberResponse) | ||
| async def update_member_role( | ||
| trip_id: uuid.UUID, | ||
| user_id: uuid.UUID, | ||
| request: TripMemberRoleUpdateRequest, | ||
| current_user_id: uuid.UUID = Depends(get_current_user_id), | ||
| usecase: TripMemberUsecase = Depends(get_trip_member_usecase), | ||
| ): | ||
| return await usecase.update_role(trip_id, user_id, current_user_id, request) | ||
|
|
||
|
|
||
| @router.delete("/{trip_id}/members/{user_id}", status_code=status.HTTP_204_NO_CONTENT) | ||
| async def remove_member( | ||
| trip_id: uuid.UUID, | ||
| user_id: uuid.UUID, | ||
| current_user_id: uuid.UUID = Depends(get_current_user_id), | ||
| usecase: TripMemberUsecase = Depends(get_trip_member_usecase), | ||
| ): | ||
| await usecase.remove(trip_id, user_id, current_user_id) |
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.
🧩 Analysis chain
🏁 Script executed:
Repository: Onzoro-rento/Travel-App-Backend
Length of output: 141
🏁 Script executed:
Repository: Onzoro-rento/Travel-App-Backend
Length of output: 366
🏁 Script executed:
Repository: Onzoro-rento/Travel-App-Backend
Length of output: 135
🏁 Script executed:
Repository: Onzoro-rento/Travel-App-Backend
Length of output: 937
🏁 Script executed:
Repository: Onzoro-rento/Travel-App-Backend
Length of output: 481
🏁 Script executed:
Repository: Onzoro-rento/Travel-App-Backend
Length of output: 2629
🏁 Script executed:
Repository: Onzoro-rento/Travel-App-Backend
Length of output: 89
🏁 Script executed:
Repository: Onzoro-rento/Travel-App-Backend
Length of output: 57
🌐 Web query:
FastAPI HTTPBearer auto_error default behavior missing header💡 Result:
By default, FastAPI's HTTPBearer (with auto_error=True, the default) raises an HTTPException when the Authorization header is missing or lacks a valid Bearer token/credentials. Prior to FastAPI 0.122.0 (released 2025-11-24), this was a 403 Forbidden with detail "Not authenticated". Since FastAPI 0.122.0, it raises a 401 Unauthorized with detail "Not authenticated", aligning with HTTP standards (RFC 6750) for missing authentication information. If auto_error=False, it returns None instead of raising an error, allowing manual handling (useful for optional auth). Official docs confirm: "By default, if the HTTP Bearer token is not provided (in an Authorization header), HTTPBearer will automatically cancel the request and send the client an error." Source code (current master): if no authorization/scheme/credentials and auto_error=True, raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Not authenticated").
Citations:
🌐 Web query:
Supabase JWT claims aud iss requirements verification💡 Result:
Supabase JWTs require specific aud (audience) and iss (issuer) claims that must be verified for authentication and authorization, particularly for Row Level Security (RLS) and API access. Required Claims: - iss: string, always present, format "https://.supabase.co/auth/v1". Must match the project's issuer URL. - aud: string or string[] (e.g., "authenticated", "anon", or array), identifies the intended recipient. Verification Requirements: When validating JWTs: 1. Verify iss matches your Supabase project's issuer (e.g., https://project-ref.supabase.co/auth/v1). 2. Validate aud matches expected values like "authenticated" (user tokens), "anon" (public), or "service_role" (admin). 3. Use Supabase's JWKS endpoint (https://.supabase.co/auth/v1/.well-known/jwks.json) for signature verification. 4. Check expiration (exp), issued at (iat), and other required claims. Supabase services automatically verify these claims. For custom validation, use libraries like supabase.auth.getClaims or language-specific JWT verifiers ensuring iss, aud, signature, and expiration. Note: aud can be a string or array; code must handle both.
Citations:
🏁 Script executed:
Repository: Onzoro-rento/Travel-App-Backend
Length of output: 777
Supabase JWT の
aud/issクレーム検証を有効にしてください。verify_aud=Falseにより audience クレーム検証が無効化されており、署名が有効な別用途のトークン(例:anonスコープ)も受け入れる可能性があります。Supabase では JWT に必須のissとaudクレームが含まれ、これらの検証はセキュリティ上必須です。修正が必要な箇所
app/config/jwt.py:
verify_aud=Falseを削除し、audienceとissuerパラメータで Supabase の要件に合わせた検証を追加してください。HTTPBearer(auto_error=False)への変更とcredentials is Noneチェックの追加により、エラーハンドリングを統一できます。app/config/env.py:
SUPABASE_JWT_ISSUERとSUPABASE_JWT_AUDIENCE設定を追加してください。.env.example: 上記設定項目を追加し、値の例を示してください(SUPABASE_JWT_ISSUER=https://your-project-ref.supabase.co/auth/v1、SUPABASE_JWT_AUDIENCE=authenticatedなど)。参考: Supabase JWT Claims Reference, Supabase JWT verification
🧰 Tools
🪛 Ruff (0.15.10)
[warning] 12-12: Do not perform function call
Dependsin argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable(B008)
[warning] 23-23: Within an
exceptclause, raise exceptions withraise ... from errorraise ... from Noneto distinguish them from errors in exception handling(B904)
🤖 Prompt for AI Agents