Skip to content

Commit 0d3d771

Browse files
committed
misc stuff
1 parent ecce375 commit 0d3d771

File tree

18 files changed

+973
-89
lines changed

18 files changed

+973
-89
lines changed

.cursorignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Firebase credentials
2+
**/serviceAccountKey.json

.pdm-python

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/Users/mujtaba/Desktop/Blueprint/w25/llsc/.venv/bin/python

backend/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,6 @@ cython_debug/
160160
# and can be added to the global gitignore or merged into this file. For a more nuclear
161161
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
162162
#.idea/
163+
164+
# Firebase
165+
serviceAccountKey.json

backend/app/middleware/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""
2+
Middleware package for LLSC backend.
3+
"""
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
from functools import wraps
2+
from typing import Callable, List, Optional, Set
3+
import logging
4+
5+
from fastapi import Depends, HTTPException, Security
6+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
7+
from sqlalchemy.orm import Session
8+
9+
from app.schemas.user import UserRole
10+
from app.services.implementations.auth_service import AuthService
11+
from app.services.implementations.user_service import UserService
12+
from app.utilities.service_utils import get_auth_service, get_db
13+
14+
security = HTTPBearer()
15+
16+
17+
def get_token_from_header(
18+
credentials: HTTPAuthorizationCredentials = Security(security),
19+
) -> str:
20+
"""Extract token from Authorization header."""
21+
print("\n=== Token Extraction ===")
22+
print(f"Raw credentials type: {type(credentials)}")
23+
print(f"Raw credentials: {credentials}")
24+
print(f"Token from header: {credentials.credentials[:50]}...") # Print first 50 chars
25+
print("=== End Token Extraction ===\n")
26+
return credentials.credentials
27+
28+
29+
def require_auth(
30+
auth_service: AuthService = Depends(get_auth_service),
31+
token: str = Depends(get_token_from_header),
32+
) -> None:
33+
"""Verify that the request has a valid access token."""
34+
try:
35+
# The token validation is done in the role check, so we just need to check
36+
# if the token is valid for any role
37+
if not auth_service.is_authorized_by_role(token, {role.value for role in UserRole}):
38+
raise HTTPException(
39+
status_code=401,
40+
detail="Invalid or expired token",
41+
)
42+
except Exception as e:
43+
raise HTTPException(
44+
status_code=401,
45+
detail="Invalid or expired token",
46+
)
47+
48+
49+
def require_roles(roles: Set[UserRole]):
50+
"""Require specific roles to access the endpoint."""
51+
52+
def decorator(func: Callable) -> Callable:
53+
@wraps(func)
54+
async def wrapper(
55+
*args,
56+
db: Session = Depends(get_db),
57+
token: str = Depends(get_token_from_header),
58+
**kwargs,
59+
):
60+
try:
61+
print("\n=== Role Check ===")
62+
print(f"Checking roles: {roles}")
63+
role_values = {role.value for role in roles}
64+
print(f"Role values to check: {role_values}")
65+
66+
# Create auth service directly
67+
logger = logging.getLogger(__name__)
68+
auth_service = AuthService(logger=logger, user_service=UserService(db))
69+
70+
is_authorized = auth_service.is_authorized_by_role(token, role_values)
71+
print(f"Authorization result: {is_authorized}")
72+
73+
if not is_authorized:
74+
raise HTTPException(
75+
status_code=403,
76+
detail="Insufficient permissions",
77+
)
78+
return await func(*args, **kwargs)
79+
except HTTPException as e:
80+
print(f"HTTP Exception: {str(e)}")
81+
raise e
82+
except Exception as e:
83+
print(f"Unexpected error: {str(e)}")
84+
raise HTTPException(
85+
status_code=401,
86+
detail="Invalid or expired token",
87+
)
88+
89+
return wrapper
90+
91+
return decorator
92+
93+
94+
def require_user_id(user_id_param: str = "user_id"):
95+
"""Require that the token belongs to the requested user."""
96+
97+
def decorator(func: Callable) -> Callable:
98+
@wraps(func)
99+
async def wrapper(
100+
*args,
101+
auth_service: AuthService = Depends(get_auth_service),
102+
token: str = Depends(get_token_from_header),
103+
**kwargs,
104+
):
105+
try:
106+
user_id = kwargs.get(user_id_param)
107+
if not user_id:
108+
raise HTTPException(
109+
status_code=400,
110+
detail=f"Missing {user_id_param} parameter",
111+
)
112+
113+
if not auth_service.is_authorized_by_user_id(token, user_id):
114+
raise HTTPException(
115+
status_code=403,
116+
detail="Not authorized to access this resource",
117+
)
118+
return await func(*args, **kwargs)
119+
except HTTPException as e:
120+
raise e
121+
except Exception as e:
122+
raise HTTPException(
123+
status_code=401,
124+
detail="Invalid or expired token",
125+
)
126+
127+
return wrapper
128+
129+
return decorator

backend/app/routes/auth.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from fastapi import APIRouter, Depends, HTTPException
1+
from fastapi import APIRouter, Depends, HTTPException, Security, Body
2+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
23
from sqlalchemy.orm import Session
3-
from ..schemas.auth import AuthResponse, LoginRequest, Token
4+
from ..schemas.auth import AuthResponse, LoginRequest, Token, RefreshRequest
45
from ..services.implementations.auth_service import AuthService
56
from ..services.implementations.user_service import UserService
67
from ..utilities.db_utils import get_db
@@ -9,6 +10,7 @@
910
import logging
1011

1112
router = APIRouter(prefix="/auth", tags=["auth"])
13+
security = HTTPBearer()
1214

1315
@router.post("/login", response_model=AuthResponse)
1416
async def login(
@@ -24,19 +26,19 @@ async def logout(
2426
auth_service: AuthService = Depends(get_auth_service)
2527
):
2628
try:
27-
auth_service.revoke_tokens(current_user)
29+
auth_service.revoke_tokens(current_user["user"])
2830
return {"message": "Successfully logged out"}
2931
except Exception as e:
3032
raise HTTPException(status_code=500, detail=str(e))
3133

3234

3335
@router.post("/refresh", response_model=Token)
3436
async def refresh(
35-
refresh_token: str,
37+
refresh_data: RefreshRequest,
3638
auth_service: AuthService = Depends(get_auth_service)
3739
):
3840
try:
39-
return auth_service.renew_token(refresh_token)
41+
return auth_service.renew_token(refresh_data.refresh_token)
4042
except Exception as e:
4143
raise HTTPException(status_code=401, detail=str(e))
4244

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from fastapi import APIRouter, Depends
2+
from ..middleware.auth_middleware import require_auth, require_roles, require_user_id
3+
from ..schemas.user import UserRole
4+
5+
router = APIRouter(prefix="/test", tags=["test"])
6+
7+
# # Basic auth test - any valid token
8+
# @router.get("/auth")
9+
# @require_auth
10+
# async def test_auth():
11+
# """Test endpoint requiring just authentication"""
12+
# return {"message": "You are authenticated!"}
13+
14+
# Role-based tests
15+
@router.get("/admin-only")
16+
@require_roles({UserRole.ADMIN})
17+
async def test_admin_only():
18+
"""Test endpoint requiring admin role"""
19+
return {"message": "You are an admin!"}
20+
21+
@router.get("/volunteer-or-admin")
22+
@require_roles({UserRole.VOLUNTEER, UserRole.ADMIN})
23+
async def test_volunteer_or_admin():
24+
"""Test endpoint requiring volunteer or admin role"""
25+
return {"message": "You are a volunteer or admin!"}
26+
27+
@router.get("/participant-only")
28+
@require_roles({UserRole.PARTICIPANT})
29+
async def test_participant_only():
30+
"""Test endpoint requiring participant role"""
31+
return {"message": "You are a participant!"}
32+
33+
# User-specific tests
34+
@router.get("/users/{user_id}/profile")
35+
@require_user_id()
36+
async def test_user_specific(user_id: str):
37+
"""Test endpoint requiring specific user access"""
38+
return {"message": f"You can access user {user_id}'s profile!"}
39+
40+
# Combined tests
41+
@router.get("/users/{user_id}/admin-action")
42+
@require_roles({UserRole.ADMIN})
43+
@require_user_id()
44+
async def test_admin_user_specific(user_id: str):
45+
"""Test endpoint requiring both admin role and specific user access"""
46+
return {"message": f"You are an admin accessing user {user_id}'s data!"}

backend/app/schemas/auth.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ class Token(BaseModel):
1616

1717
model_config = ConfigDict(from_attributes=True)
1818

19+
class RefreshRequest(BaseModel):
20+
"""Request body for token refresh"""
21+
refresh_token: str
22+
1923
class AuthResponse(Token):
2024
"""Authentication response containing tokens and user info"""
2125
user: UserCreateResponse

backend/app/server.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from dotenv import load_dotenv
66
from fastapi import FastAPI
77

8-
from app.routes import auth, email
8+
from app.routes import auth, email, test_endpoints
99

1010
load_dotenv()
1111

@@ -32,7 +32,7 @@ async def lifespan(_: FastAPI):
3232
app.include_router(auth.router)
3333
app.include_router(user.router)
3434
app.include_router(email.router)
35-
35+
app.include_router(test_endpoints.router)
3636

3737
@app.get("/")
3838
def read_root():

backend/app/services/implementations/auth_service.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,20 @@ def send_email_verification_link(self, email: str) -> None:
5555

5656
def is_authorized_by_role(self, access_token: str, roles: set[str]) -> bool:
5757
try:
58+
print("\n=== Auth Service Role Check ===")
59+
# print(f"Verifying token: {access_token[:50]}...")
5860
decoded_token = firebase_admin.auth.verify_id_token(access_token, check_revoked=True)
61+
print(f"Decoded token UID: {decoded_token.get('uid')}")
5962
user_role = self.user_service.get_user_role_by_auth_id(decoded_token["uid"])
63+
print(f"User role from DB: {user_role}")
64+
print(f"Checking against roles: {roles}")
6065
firebase_user = firebase_admin.auth.get_user(decoded_token["uid"])
61-
return firebase_user.email_verified and user_role in roles
62-
except:
66+
print(f"Email verified: {firebase_user.email_verified}")
67+
result = firebase_user.email_verified and user_role in roles
68+
print(f"Final authorization result: {result}")
69+
return result
70+
except Exception as e:
71+
print(f"Authorization error: {str(e)}")
6372
return False
6473

6574
def is_authorized_by_user_id(self, access_token: str, requested_user_id: str) -> bool:

0 commit comments

Comments
 (0)