Skip to content

Commit ef433bb

Browse files
committed
dev(auth): use EcoTaxa as auth source.
1 parent bf34789 commit ef433bb

4 files changed

Lines changed: 134 additions & 149 deletions

File tree

src/Models.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -351,13 +351,6 @@ class MarkSubsampleReq(BaseModel):
351351
validation_date: Optional[datetime] = None
352352

353353

354-
class ExportSubsampleReq(BaseModel):
355-
"""Request model for exporting a subsample"""
356-
357-
token: str
358-
projid: int
359-
360-
361354
class ExportSubsampleRsp(BaseModel):
362355
"""Response model for exporting a subsample, a task and the subsample"""
363356

src/helpers/auth.py

Lines changed: 117 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
import datetime
2+
import uuid
23
from typing import Dict, Optional
34

45
import jwt
56
from fastapi import HTTPException, status, Depends
67
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
8+
from sqlalchemy.exc import IntegrityError
79
from sqlalchemy.orm import Session
810
from starlette.requests import Request
911

1012
from config_rdr import config
13+
from helpers.logger import logger
1114
from local_DB.db_dependencies import get_db
1215
from local_DB.models import User, BlacklistedToken
16+
from providers.ecotaxa_client import EcoTaxaApiClient
1317

1418

1519
class CustomHTTPBearer(HTTPBearer):
@@ -73,7 +77,7 @@ def get_authorization_scheme_param(authorization_header: Optional[str]):
7377
SESSION_COOKIE_NAME = "zoopp_session"
7478

7579

76-
def decode_jwt_token(token: str, db: Optional[Session] = None) -> Dict:
80+
def decode_jwt_token(token: str, db: Optional[Session] = None) -> Dict[str, str]:
7781
"""
7882
Decode and validate a JWT token.
7983
@@ -143,7 +147,7 @@ def create_jwt_token(data: Dict, expires_delta: Optional[int] = None) -> str:
143147
return encoded_jwt
144148

145149

146-
def get_user_from_token(token: str, db: Optional[Session] = None) -> Dict:
150+
def get_user_from_token(token: str, db: Optional[Session] = None) -> str:
147151
"""
148152
Extract user information from a JWT token.
149153
@@ -152,21 +156,28 @@ def get_user_from_token(token: str, db: Optional[Session] = None) -> Dict:
152156
db: Optional database session for checking token blacklist
153157
154158
Returns:
155-
User information extracted from the token
159+
User email extracted from the token
156160
"""
157161
payload = decode_jwt_token(token, db)
162+
return payload.get("email", "")
158163

159-
# In a real application, you might want to validate the user exists in your database
160-
# or fetch additional user information
161164

162-
return {
163-
"id": payload.get("sub", ""),
164-
"name": payload.get("name", ""),
165-
"email": payload.get("email", ""),
166-
}
165+
def get_ecotaxa_token_from_token(token: str, db: Optional[Session] = None) -> str:
166+
"""
167+
Extract EcoTaxa token in our token
167168
169+
Args:
170+
token: The JWT token
171+
db: Optional database session for checking token blacklist
168172
169-
def get_user_from_db(email: str, db):
173+
Returns:
174+
EcoTaxa token.
175+
"""
176+
payload = decode_jwt_token(token, db)
177+
return payload["token"]
178+
179+
180+
def get_user_from_db(email: str, db) -> User:
170181
"""
171182
Get a user from the database by email.
172183
@@ -178,7 +189,46 @@ def get_user_from_db(email: str, db):
178189
The user if found, None otherwise
179190
"""
180191

181-
return db.query(User).filter(User.email == email).first()
192+
return db.query(User).filter(User.email == email).first() # type:ignore
193+
194+
195+
def user_from_db(name: str, email: str, db) -> User:
196+
"""
197+
Ensure a user with the given email exists; create it if missing, and return its id.
198+
Lookup is performed by email.
199+
200+
Args:
201+
name: The user's name (EcoTaxa conventions)
202+
email: The user's email address used for lookup and creation.
203+
db: The database session.
204+
205+
Returns:
206+
str: The user's id (existing or newly created).
207+
"""
208+
# Try to find existing user by email
209+
user = db.query(User).filter(User.email == email).first()
210+
if user:
211+
return user # type:ignore
212+
213+
# Create a minimal user record if not found
214+
new_user = User(
215+
id=str(uuid.uuid4()),
216+
name=name,
217+
email=email,
218+
password="", # No password stored here (auth handled externally)
219+
)
220+
try:
221+
db.add(new_user)
222+
db.commit()
223+
db.refresh(new_user)
224+
return new_user # type:ignore
225+
except IntegrityError:
226+
# In case of a race condition where the user was created concurrently
227+
db.rollback()
228+
user = db.query(User).filter(User.email == email).first()
229+
if user:
230+
return user.id # type:ignore
231+
raise
182232

183233

184234
def blacklist_token(token: str, db: Session):
@@ -257,23 +307,26 @@ def authenticate_user(email: str, password: str, db) -> str:
257307
Raises:
258308
HTTPException: If authentication fails
259309
"""
260-
# Validate the credentials against the database
261-
user = get_user_from_db(email, db)
262-
263-
if (
264-
not user or user.password != password
265-
): # In a real app, use proper password hashing
310+
# Validate the credentials against EcoTaxa server
311+
client = EcoTaxaApiClient(logger, config.ECOTAXA_SERVER, email, password)
312+
client.token = client.login()
313+
if client.token is None:
266314
raise HTTPException(
267315
status_code=401,
268316
detail="Incorrect email or password",
269317
headers={"WWW-Authenticate": "Bearer"},
270318
)
319+
else:
320+
who = client.whoami()
321+
322+
user = user_from_db(who.name, who.email, db)
271323

272324
# Create user data for the token
273325
user_data = {
274326
"sub": user.id,
275327
"name": user.name,
276328
"email": user.email,
329+
"token": client.token,
277330
}
278331

279332
# Create a JWT token with 30-day expiration
@@ -301,6 +354,51 @@ async def get_current_user_from_credentials(
301354
Raises:
302355
HTTPException: If authentication fails
303356
"""
357+
token = await get_token_from_credentials(request, credentials)
358+
359+
# Validate the JWT token and extract user information
360+
user_mail = get_user_from_token(token, db)
361+
362+
# Get the user from the database to ensure they exist
363+
user = get_user_from_db(user_mail, db)
364+
365+
if not user:
366+
raise HTTPException(
367+
status_code=401,
368+
detail="User not found",
369+
headers={"WWW-Authenticate": "Bearer"},
370+
)
371+
372+
return user
373+
374+
375+
async def get_ecotaxa_token_from_credentials(
376+
request: Request,
377+
credentials: HTTPAuthorizationCredentials = Depends(security),
378+
db: Session = Depends(get_db),
379+
) -> str:
380+
"""
381+
FastAPI dependency that extracts EcoTaxa token from request.
382+
383+
Args:
384+
request: The request object to access cookies
385+
credentials: The HTTP authorization credentials
386+
db: The database session
387+
388+
Returns:
389+
The EcoTaxa token.
390+
391+
Raises:
392+
HTTPException: If authentication problem
393+
"""
394+
token = await get_token_from_credentials(request, credentials)
395+
396+
return get_ecotaxa_token_from_token(token, db)
397+
398+
399+
async def get_token_from_credentials(
400+
request: Request, credentials: HTTPAuthorizationCredentials
401+
) -> str:
304402
token = None
305403

306404
# Try to extract token from the authorization header
@@ -318,18 +416,4 @@ async def get_current_user_from_credentials(
318416
detail="Not authenticated",
319417
headers={"WWW-Authenticate": "Bearer"},
320418
)
321-
322-
# Validate the JWT token and extract user information
323-
user_data = get_user_from_token(token, db)
324-
325-
# Get the user from the database to ensure they exist
326-
user = get_user_from_db(user_data["email"], db)
327-
328-
if not user:
329-
raise HTTPException(
330-
status_code=401,
331-
detail="User not found",
332-
headers={"WWW-Authenticate": "Bearer"},
333-
)
334-
335-
return user
419+
return token

src/routers/ecotaxa.py

Lines changed: 11 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1-
from typing import List, Dict, Optional
1+
from typing import List
22

33
from fastapi import APIRouter, Depends
4+
from fastapi.security import HTTPAuthorizationCredentials
45
from sqlalchemy.orm import Session
56

67
from Models import Project
78
from config_rdr import config
8-
from helpers.auth import get_current_user_from_credentials, create_jwt_token
9+
from helpers.auth import (
10+
get_current_user_from_credentials,
11+
get_ecotaxa_token_from_credentials,
12+
)
913
from helpers.logger import logger
1014
from local_DB.db_dependencies import get_db
11-
from providers.EcoTaxa.ecotaxa_model import ProjectModel, LoginReq
15+
from providers.EcoTaxa.ecotaxa_model import ProjectModel
1216
from providers.ecotaxa_client import EcoTaxaApiClient
13-
from helpers.auth import decode_jwt_token
1417

1518
# Create a routers instance
1619
router = APIRouter(
@@ -21,63 +24,24 @@
2124

2225
@router.get("/projects", response_model=List[ProjectModel])
2326
def get_projects(
24-
token: str,
2527
_user=Depends(get_current_user_from_credentials),
28+
ecotaxa_token: HTTPAuthorizationCredentials = Depends(
29+
get_ecotaxa_token_from_credentials
30+
),
2631
db: Session = Depends(get_db),
2732
) -> List[ProjectModel]:
2833
"""
2934
Get a list of ZooScan projects from EcoTaxa.
3035
3136
Args:
32-
token: EcoTaxa API token (JWT-encoded).
3337
_user: The authenticated user
38+
ecotaxa_token: embedded EcoTaxa token
3439
db: The database session
3540
3641
Returns:
3742
List[Project]: A list of ZooScan projects
3843
"""
39-
# Decode the JWT token to get the actual EcoTaxa token
40-
decoded_token = decode_jwt_token(token, db)
41-
ecotaxa_token = decoded_token.get("ecotaxa_token")
4244
assert isinstance(ecotaxa_token, str), "Failed to decode JWT-encoded EcoTaxa token"
4345

4446
client = EcoTaxaApiClient.from_token(logger, config.ECOTAXA_SERVER, ecotaxa_token)
4547
return client.list_zooscan_projects()
46-
47-
48-
@router.post("/login")
49-
def ecotaxa_login(
50-
login_req: LoginReq,
51-
_user=Depends(get_current_user_from_credentials),
52-
_db: Session = Depends(get_db),
53-
):
54-
"""
55-
Authenticates with EcoTaxa using the provided credentials.
56-
57-
Args:
58-
login_req (LoginReq): The login request containing username (email) and password.
59-
_user: The authenticated user
60-
_db: The database session
61-
62-
Returns:
63-
Dict: A dictionary containing the JWT-encrypted EcoTaxa token.
64-
"""
65-
client = EcoTaxaApiClient(
66-
logger, config.ECOTAXA_SERVER, login_req.username, login_req.password
67-
)
68-
ecotaxa_token: Optional[str] = None
69-
try:
70-
ecotaxa_token = client.login()
71-
except Exception as err:
72-
pass
73-
if ecotaxa_token is None:
74-
return {"token": None}
75-
76-
# Create a data dictionary for the JWT token
77-
token_data = {
78-
"ecotaxa_token": ecotaxa_token,
79-
}
80-
# Create a JWT token with 30-day expiration (similar to user sessions)
81-
jwt_token = create_jwt_token(token_data, expires_delta=30 * 24 * 60 * 60)
82-
83-
return {"token": jwt_token}

0 commit comments

Comments
 (0)