diff --git a/backend/auth.py b/backend/auth.py index 2765c35..22fbaf8 100644 --- a/backend/auth.py +++ b/backend/auth.py @@ -15,6 +15,7 @@ SECRET_KEY = os.environ["SECRET_KEY"] # Required - see .env.example ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days COOKIE_NAME = "auth_token" +COOKIE_SECURE = os.environ.get("COOKIE_SECURE", "false").lower() == "true" def verify_password(plain_password: str, hashed_password: str) -> bool: diff --git a/backend/env.example b/backend/env.example index 452a2bd..c90a230 100644 --- a/backend/env.example +++ b/backend/env.example @@ -9,6 +9,9 @@ SECRET_KEY= # Database URL DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/arbret +# Cookie security: set to "true" in production with HTTPS +COOKIE_SECURE=false + # Dev user credentials (regular user) DEV_USER_EMAIL= DEV_USER_PASSWORD= diff --git a/backend/main.py b/backend/main.py index 6002765..1149f14 100644 --- a/backend/main.py +++ b/backend/main.py @@ -38,3 +38,4 @@ app.include_router(counter_routes.router) app.include_router(audit_routes.router) app.include_router(profile_routes.router) app.include_router(invites_routes.router) +app.include_router(invites_routes.admin_router) diff --git a/backend/routes/audit.py b/backend/routes/audit.py index 94388bd..8144497 100644 --- a/backend/routes/audit.py +++ b/backend/routes/audit.py @@ -114,4 +114,3 @@ async def get_sum_records( per_page=per_page, total_pages=total_pages, ) - diff --git a/backend/routes/auth.py b/backend/routes/auth.py index 46e5068..5763cb2 100644 --- a/backend/routes/auth.py +++ b/backend/routes/auth.py @@ -8,6 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from auth import ( ACCESS_TOKEN_EXPIRE_MINUTES, COOKIE_NAME, + COOKIE_SECURE, get_password_hash, get_user_by_email, authenticate_user, @@ -30,7 +31,7 @@ def set_auth_cookie(response: Response, token: str) -> None: key=COOKIE_NAME, value=token, httponly=True, - secure=False, # Set to True in production with HTTPS + secure=COOKIE_SECURE, samesite="lax", max_age=ACCESS_TOKEN_EXPIRE_MINUTES * 60, ) @@ -132,4 +133,3 @@ async def get_me( ) -> UserResponse: """Get the current authenticated user's info.""" return await build_user_response(current_user, db) - diff --git a/backend/routes/counter.py b/backend/routes/counter.py index f53fcff..034766e 100644 --- a/backend/routes/counter.py +++ b/backend/routes/counter.py @@ -51,4 +51,3 @@ async def increment_counter( db.add(record) await db.commit() return {"value": counter.value} - diff --git a/backend/routes/invites.py b/backend/routes/invites.py index 58c4c29..46f9948 100644 --- a/backend/routes/invites.py +++ b/backend/routes/invites.py @@ -20,7 +20,8 @@ from schemas import ( ) -router = APIRouter(tags=["invites"]) +router = APIRouter(prefix="/api/invites", tags=["invites"]) +admin_router = APIRouter(prefix="/api/admin", tags=["admin"]) MAX_INVITE_COLLISION_RETRIES = 3 @@ -41,11 +42,7 @@ def build_invite_response(invite: Invite) -> InviteResponse: ) -# ============================================================================= -# Public Endpoints -# ============================================================================= - -@router.get("/api/invites/{identifier}/check", response_model=InviteCheckResponse) +@router.get("/{identifier}/check", response_model=InviteCheckResponse) async def check_invite( identifier: str, db: AsyncSession = Depends(get_db), @@ -69,11 +66,7 @@ async def check_invite( return InviteCheckResponse(valid=True, status=invite.status.value) -# ============================================================================= -# User Endpoints (requires VIEW_OWN_INVITES permission) -# ============================================================================= - -@router.get("/api/invites", response_model=list[UserInviteResponse]) +@router.get("", response_model=list[UserInviteResponse]) async def get_my_invites( db: AsyncSession = Depends(get_db), current_user: User = Depends(require_permission(Permission.VIEW_OWN_INVITES)), @@ -100,11 +93,7 @@ async def get_my_invites( ] -# ============================================================================= -# Admin Endpoints (requires MANAGE_INVITES permission) -# ============================================================================= - -@router.get("/api/admin/users", response_model=list[AdminUserResponse]) +@admin_router.get("/users", response_model=list[AdminUserResponse]) async def list_users_for_admin( db: AsyncSession = Depends(get_db), _current_user: User = Depends(require_permission(Permission.MANAGE_INVITES)), @@ -115,7 +104,7 @@ async def list_users_for_admin( return [AdminUserResponse(id=u.id, email=u.email) for u in users] -@router.post("/api/admin/invites", response_model=InviteResponse) +@admin_router.post("/invites", response_model=InviteResponse) async def create_invite( data: InviteCreate, db: AsyncSession = Depends(get_db), @@ -163,7 +152,7 @@ async def create_invite( return build_invite_response(invite) -@router.get("/api/admin/invites", response_model=PaginatedInviteRecords) +@admin_router.get("/invites", response_model=PaginatedInviteRecords) async def list_all_invites( page: int = Query(1, ge=1), per_page: int = Query(10, ge=1, le=100), @@ -216,7 +205,7 @@ async def list_all_invites( ) -@router.post("/api/admin/invites/{invite_id}/revoke", response_model=InviteResponse) +@admin_router.post("/invites/{invite_id}/revoke", response_model=InviteResponse) async def revoke_invite( invite_id: int, db: AsyncSession = Depends(get_db), @@ -244,4 +233,3 @@ async def revoke_invite( await db.refresh(invite) return build_invite_response(invite) - diff --git a/backend/routes/profile.py b/backend/routes/profile.py index d8679dc..aba4cc4 100644 --- a/backend/routes/profile.py +++ b/backend/routes/profile.py @@ -91,4 +91,3 @@ async def update_profile( nostr_npub=current_user.nostr_npub, godfather_email=godfather_email, ) - diff --git a/backend/routes/sum.py b/backend/routes/sum.py index 8bae906..ab0bbfa 100644 --- a/backend/routes/sum.py +++ b/backend/routes/sum.py @@ -28,4 +28,3 @@ async def calculate_sum( db.add(record) await db.commit() return SumResponse(a=data.a, b=data.b, result=result) - diff --git a/backend/schemas.py b/backend/schemas.py index b033d97..a8e7f38 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -5,10 +5,6 @@ from typing import Generic, TypeVar from pydantic import BaseModel, EmailStr -# ============================================================================= -# Auth Schemas -# ============================================================================= - class UserCredentials(BaseModel): """Base model for user email/password.""" email: EmailStr @@ -27,13 +23,6 @@ class UserResponse(BaseModel): permissions: list[str] -class TokenResponse(BaseModel): - """Response model for token-based auth (unused but kept for API completeness).""" - access_token: str - token_type: str - user: UserResponse - - class RegisterWithInvite(BaseModel): """Request model for registration with invite.""" email: EmailStr @@ -41,19 +30,6 @@ class RegisterWithInvite(BaseModel): invite_identifier: str -# ============================================================================= -# Counter Schemas -# ============================================================================= - -class CounterValue(BaseModel): - """Response model for counter value.""" - value: int - - -# ============================================================================= -# Sum Schemas -# ============================================================================= - class SumRequest(BaseModel): """Request model for sum calculation.""" a: float @@ -67,10 +43,6 @@ class SumResponse(BaseModel): result: float -# ============================================================================= -# Audit Schemas -# ============================================================================= - class CounterRecordResponse(BaseModel): """Response model for a counter audit record.""" id: int @@ -90,10 +62,6 @@ class SumRecordResponse(BaseModel): created_at: datetime -# ============================================================================= -# Pagination (Generic) -# ============================================================================= - RecordT = TypeVar("RecordT", bound=BaseModel) @@ -110,10 +78,6 @@ PaginatedCounterRecords = PaginatedResponse[CounterRecordResponse] PaginatedSumRecords = PaginatedResponse[SumRecordResponse] -# ============================================================================= -# Profile Schemas -# ============================================================================= - class ProfileResponse(BaseModel): """Response model for profile data.""" contact_email: str | None @@ -131,10 +95,6 @@ class ProfileUpdate(BaseModel): nostr_npub: str | None = None -# ============================================================================= -# Invite Schemas -# ============================================================================= - class InviteCheckResponse(BaseModel): """Response for invite check endpoint.""" valid: bool @@ -174,12 +134,7 @@ class UserInviteResponse(BaseModel): PaginatedInviteRecords = PaginatedResponse[InviteResponse] -# ============================================================================= -# Admin Schemas -# ============================================================================= - class AdminUserResponse(BaseModel): """Minimal user info for admin dropdowns.""" id: int email: str -