Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ class Settings(BaseSettings):

# Face embedding model
FACE_EMBEDDING_MODEL_NAME: str = "buffalo_l"
FACE_EMBEDDING_PROVIDERS: str = "CPUExecutionProvider"
FACE_EMBEDDING_CTX_ID: int = -1
FACE_EMBEDDING_PROVIDERS: str = "CUDAExecutionProvider,CPUExecutionProvider"
FACE_EMBEDDING_CTX_ID: int = 0
FACE_EMBEDDING_DET_WIDTH: int = 640
FACE_EMBEDDING_DET_HEIGHT: int = 640

Expand Down
25 changes: 25 additions & 0 deletions app/deps/rate_limit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from fastapi import Request, HTTPException
from typing import Callable

from app.infra.redis import RedisClient

def RateLimiter(requests: int, window: int) -> Callable:
async def _rate_limit_dependency(request: Request) -> None:
client_ip = request.client.host if request.client else "127.0.0.1"
# We can also use user_id if we wanted to rate-limit per user, but IP is general.
# For simplicity, IP based rate limit on the endpoint
path = request.url.path
key = f"rate_limit:{path}:{client_ip}"

redis = RedisClient.get_instance()

# Increment request count
current = await redis.incr(key)
if current == 1:
# Set expiry for the window if it's the first request
await redis.expire(key, window)

if current > requests:
raise HTTPException(status_code=429, detail="Too Many Requests")

return _rate_limit_dependency
6 changes: 3 additions & 3 deletions app/infra/redis.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,15 @@ async def incr(self, key: RedisKey | str) -> int:


async def sadd(self, key: RedisKey | str, *values: str) -> int:
result = self._client.sadd(key, *values)
result = await self._client.sadd(key, *values) # type: ignore[misc]
return int(cast(int, result))

async def sismember(self, key: RedisKey | str, value: str) -> bool:
result = self._client.sismember(key, value)
result = await self._client.sismember(key, value) # type: ignore[misc]
return int(cast(int, result)) == 1

async def srem(self, key: RedisKey | str, *values: str) -> int:
result = self._client.srem(key, *values)
result = await self._client.srem(key, *values) # type: ignore[misc]
return int(cast(int, result))


Expand Down
3 changes: 2 additions & 1 deletion app/router/mobile/photos.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@

from app.container import Container, get_container
from app.deps.token_auth import MobileUserSchema, get_current_mobile_user
from app.deps.rate_limit import RateLimiter

router = APIRouter(prefix="/photos")


@router.get("")
@router.get("", dependencies=[Depends(RateLimiter(requests=20, window=60))])
async def list_my_photos(
event_id: UUID | None = Query(default=None),
sort: Literal["asc", "desc"] = Query(default="desc"),
Expand Down
46 changes: 25 additions & 21 deletions app/service/staff_drive.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
import base64
import hashlib
import json
Expand Down Expand Up @@ -238,44 +239,47 @@ async def import_images_from_drive(

access_token = await self.get_access_token_for_staff_user(staff_user.id)
bucket = ImageBucket(f"{DRIVE_BUCKET_PREFIX}/{staff_user.id}")
results: list[DriveImportResult] = []
semaphore = asyncio.Semaphore(10)

for selected in selected_files:
async def process_file(selected: SelectedDriveFile) -> DriveImportResult:
if selected.mime_type and selected.mime_type not in IMAGE_ALLOWED_TYPES:
raise AppException.bad_request(
f"File '{selected.name}' has unsupported type '{selected.mime_type}'. "
f"Allowed: {', '.join(sorted(IMAGE_ALLOWED_TYPES))}"
)

download = await GoogleDriveClient.download_file(
access_token=access_token,
file_id=selected.id,
)

if len(download.content) > MAX_IMPORT_FILE_SIZE_BYTES:
raise AppException.bad_request(
f"File '{selected.name}' exceeds the 20 MB size limit"
async with semaphore:
download = await GoogleDriveClient.download_file(
access_token=access_token,
file_id=selected.id,
)

object_name = self._generate_object_name(selected.name)
content_type = selected.mime_type or download.metadata.mime_type
if len(download.content) > MAX_IMPORT_FILE_SIZE_BYTES:
raise AppException.bad_request(
f"File '{selected.name}' exceeds the 20 MB size limit"
)

await bucket.put_bytes(
data=download.content,
object_name=object_name,
content_type=content_type,
filename=selected.name,
)
object_name = self._generate_object_name(selected.name)
content_type = selected.mime_type or download.metadata.mime_type

results.append(DriveImportResult(
await bucket.put_bytes(
data=download.content,
object_name=object_name,
content_type=content_type,
filename=selected.name,
)

return DriveImportResult(
drive_file_id=selected.id,
original_file_name=selected.name,
minio_bucket=bucket.bucket_name,
minio_object_name=object_name,
minio_object_path=f"{bucket.file_prefix}/{object_name}",
))
)

return results
tasks = [process_file(selected) for selected in selected_files]
results = await asyncio.gather(*tasks)
return list(results)

def _fernet(self) -> Fernet:
digest = hashlib.sha256(settings.encryption_key.encode("utf-8")).digest()
Expand Down
11 changes: 6 additions & 5 deletions app/service/staff_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ async def create_staff_user(
) -> StaffUser:
try:
hashed_password = hash_password(password)
normalized_email = email.strip().lower() if email else None
user = await self.staff_user_querier.create_multi(
email=email,
email=normalized_email,
password=hashed_password,
role=role,
)
Expand Down Expand Up @@ -108,10 +109,10 @@ async def admin_login(
email: str,
password: str,
) -> WebAuthResponse:
print("hello")
staff: StaffUser | None = await self.staff_user_querier.get_staff_user_by_email(email=email)
normalized_email = email.strip().lower()
staff: StaffUser | None = await self.staff_user_querier.get_staff_user_by_email(email=normalized_email)
if staff is None or not verify_password(password, staff.password):
logger.info("admin login failed for email %s", email)
logger.info("admin login failed for email %s", normalized_email)
raise AppException.unauthorized("Invalid email or password")


Expand All @@ -126,7 +127,7 @@ async def admin_login(
role=staff.role,
)

async def Get_stuff_user(
async def get_staff_user(
self,
stuff_id:uuid.UUID
)->StaffUser:
Expand Down
3 changes: 2 additions & 1 deletion app/service/user_notification.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
from typing import Any
import uuid

Expand Down Expand Up @@ -41,7 +42,7 @@ async def create_notification(
notification_record = await self.notification_querier.create_notification(
user_id=user_id,
type=type,
payload=payload,
payload=json.dumps(payload),
)
if notification_record is None:
raise AppException.internal_error("Failed to create user notification")
Expand Down
1 change: 1 addition & 0 deletions db/generated/photo_faces.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ class InsertPhotoFaceWithApprovalParams:
inserted_match AS (
INSERT INTO face_matches (photo_face_id, user_id, confidence)
SELECT upserted_photo_face.id, :p5, :p6
FROM upserted_photo_face
WHERE NOT EXISTS (SELECT 1 FROM existing_match)
RETURNING id
)
Expand Down
1 change: 1 addition & 0 deletions db/queries/photo_faces.sql
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ existing_match AS (
inserted_match AS (
INSERT INTO face_matches (photo_face_id, user_id, confidence)
SELECT upserted_photo_face.id, $5, $6
FROM upserted_photo_face
WHERE NOT EXISTS (SELECT 1 FROM existing_match)
RETURNING id
)
Expand Down
183 changes: 0 additions & 183 deletions tests/e2e/test_mobile_auth_intent_e2e.py

This file was deleted.

Loading