Spaces:
Sleeping
Sleeping
blink
Browse files- dependencies.py +51 -1
- routers/blink.py +45 -13
dependencies.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import logging
|
| 2 |
from datetime import datetime, timedelta
|
| 3 |
-
from typing import Optional, Tuple
|
| 4 |
import ipaddress
|
| 5 |
import httpx
|
| 6 |
from fastapi import Request, Depends, HTTPException, status
|
|
@@ -149,6 +149,56 @@ async def get_current_user(
|
|
| 149 |
return user
|
| 150 |
|
| 151 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
async def verify_credits(
|
| 153 |
user: User = Depends(get_current_user),
|
| 154 |
db: AsyncSession = Depends(get_db)
|
|
|
|
| 1 |
import logging
|
| 2 |
from datetime import datetime, timedelta
|
| 3 |
+
from typing import Optional, Tuple, Union
|
| 4 |
import ipaddress
|
| 5 |
import httpx
|
| 6 |
from fastapi import Request, Depends, HTTPException, status
|
|
|
|
| 149 |
return user
|
| 150 |
|
| 151 |
|
| 152 |
+
async def get_optional_user(
|
| 153 |
+
req: Request,
|
| 154 |
+
db: AsyncSession = Depends(get_db)
|
| 155 |
+
) -> Optional[User]:
|
| 156 |
+
"""
|
| 157 |
+
Attempt to extract and verify JWT from Authorization header.
|
| 158 |
+
Returns the authenticated user if valid, or None if not authenticated.
|
| 159 |
+
|
| 160 |
+
Unlike get_current_user, this does NOT raise errors for missing/invalid tokens.
|
| 161 |
+
Useful for endpoints that work for both authenticated and anonymous users.
|
| 162 |
+
|
| 163 |
+
Usage:
|
| 164 |
+
@router.get("/optional-auth")
|
| 165 |
+
async def optional_auth_route(user: Optional[User] = Depends(get_optional_user)):
|
| 166 |
+
if user:
|
| 167 |
+
return {"user_id": user.user_id}
|
| 168 |
+
return {"message": "anonymous"}
|
| 169 |
+
"""
|
| 170 |
+
auth_header = req.headers.get("Authorization")
|
| 171 |
+
|
| 172 |
+
if not auth_header or not auth_header.startswith("Bearer "):
|
| 173 |
+
return None
|
| 174 |
+
|
| 175 |
+
token = auth_header.split(" ", 1)[1]
|
| 176 |
+
|
| 177 |
+
try:
|
| 178 |
+
payload = verify_access_token(token)
|
| 179 |
+
except (TokenExpiredError, InvalidTokenError, JWTError) as e:
|
| 180 |
+
logger.debug(f"Optional auth failed: {e}")
|
| 181 |
+
return None
|
| 182 |
+
|
| 183 |
+
# Get user from DB
|
| 184 |
+
query = select(User).where(
|
| 185 |
+
User.user_id == payload.user_id,
|
| 186 |
+
User.is_active == True
|
| 187 |
+
)
|
| 188 |
+
result = await db.execute(query)
|
| 189 |
+
user = result.scalar_one_or_none()
|
| 190 |
+
|
| 191 |
+
if not user:
|
| 192 |
+
return None
|
| 193 |
+
|
| 194 |
+
# Validate token version
|
| 195 |
+
if payload.token_version < user.token_version:
|
| 196 |
+
logger.debug(f"Token invalidated for user {user.user_id}")
|
| 197 |
+
return None
|
| 198 |
+
|
| 199 |
+
return user
|
| 200 |
+
|
| 201 |
+
|
| 202 |
async def verify_credits(
|
| 203 |
user: User = Depends(get_current_user),
|
| 204 |
db: AsyncSession = Depends(get_db)
|
routers/blink.py
CHANGED
|
@@ -12,7 +12,7 @@ import logging
|
|
| 12 |
from core.database import get_db
|
| 13 |
from core.models import User, AuditLog, GeminiJob, Contact, ClientUser
|
| 14 |
from services.encryption_service import decrypt_multiple_blocks
|
| 15 |
-
from dependencies import get_geolocation
|
| 16 |
|
| 17 |
logger = logging.getLogger(__name__)
|
| 18 |
|
|
@@ -430,11 +430,19 @@ async def get_contacts(
|
|
| 430 |
async def blink(
|
| 431 |
request: Request,
|
| 432 |
userid: str = Query(..., description="User ID (20 chars) + encrypted data"),
|
| 433 |
-
db: AsyncSession = Depends(get_db)
|
|
|
|
| 434 |
):
|
| 435 |
"""
|
| 436 |
Process blink request with encrypted user data.
|
| 437 |
Logs to AuditLog with log_type='client'.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
"""
|
| 439 |
try:
|
| 440 |
# Validate minimum length
|
|
@@ -471,9 +479,37 @@ async def blink(
|
|
| 471 |
else:
|
| 472 |
ip_address = request.client.host if request.client else None
|
| 473 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
# Get geolocation from IP address
|
| 475 |
country, region = await get_geolocation(ip_address)
|
| 476 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 477 |
# Store each decrypted result as separate audit log entries
|
| 478 |
records_created = 0
|
| 479 |
for json_data in decrypted_results:
|
|
@@ -486,7 +522,7 @@ async def blink(
|
|
| 486 |
|
| 487 |
audit_log = AuditLog(
|
| 488 |
log_type="client",
|
| 489 |
-
user_id=
|
| 490 |
client_user_id=client_user_id,
|
| 491 |
action="blink",
|
| 492 |
details=details,
|
|
@@ -502,7 +538,7 @@ async def blink(
|
|
| 502 |
if not decrypted_results and encrypted_data:
|
| 503 |
audit_log = AuditLog(
|
| 504 |
log_type="client",
|
| 505 |
-
user_id=None
|
| 506 |
client_user_id=client_user_id,
|
| 507 |
action="blink",
|
| 508 |
details={"encrypted_length": len(encrypted_data), "country": country, "region": region},
|
|
@@ -516,16 +552,11 @@ async def blink(
|
|
| 516 |
|
| 517 |
await db.commit()
|
| 518 |
|
| 519 |
-
|
|
|
|
| 520 |
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
content={
|
| 524 |
-
"status": "success",
|
| 525 |
-
"client_user_id": client_user_id,
|
| 526 |
-
"records_created": records_created
|
| 527 |
-
}
|
| 528 |
-
)
|
| 529 |
|
| 530 |
except HTTPException:
|
| 531 |
raise
|
|
@@ -536,3 +567,4 @@ async def blink(
|
|
| 536 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 537 |
detail="Internal server error processing request"
|
| 538 |
)
|
|
|
|
|
|
| 12 |
from core.database import get_db
|
| 13 |
from core.models import User, AuditLog, GeminiJob, Contact, ClientUser
|
| 14 |
from services.encryption_service import decrypt_multiple_blocks
|
| 15 |
+
from dependencies import get_geolocation, get_optional_user
|
| 16 |
|
| 17 |
logger = logging.getLogger(__name__)
|
| 18 |
|
|
|
|
| 430 |
async def blink(
|
| 431 |
request: Request,
|
| 432 |
userid: str = Query(..., description="User ID (20 chars) + encrypted data"),
|
| 433 |
+
db: AsyncSession = Depends(get_db),
|
| 434 |
+
current_user: User = Depends(get_optional_user)
|
| 435 |
):
|
| 436 |
"""
|
| 437 |
Process blink request with encrypted user data.
|
| 438 |
Logs to AuditLog with log_type='client'.
|
| 439 |
+
|
| 440 |
+
If authenticated via JWT:
|
| 441 |
+
- Creates a new ClientUser entry linking client_user_id to server user_id
|
| 442 |
+
- Sets user_id in AuditLog entries
|
| 443 |
+
|
| 444 |
+
If not authenticated:
|
| 445 |
+
- Creates AuditLog entries with user_id=None (anonymous)
|
| 446 |
"""
|
| 447 |
try:
|
| 448 |
# Validate minimum length
|
|
|
|
| 479 |
else:
|
| 480 |
ip_address = request.client.host if request.client else None
|
| 481 |
|
| 482 |
+
# Determine IPv4/IPv6
|
| 483 |
+
ipv4_address = None
|
| 484 |
+
ipv6_address = None
|
| 485 |
+
if ip_address:
|
| 486 |
+
try:
|
| 487 |
+
ip_obj = ipaddress.ip_address(ip_address)
|
| 488 |
+
if ip_obj.version == 4:
|
| 489 |
+
ipv4_address = ip_address
|
| 490 |
+
else:
|
| 491 |
+
ipv6_address = ip_address
|
| 492 |
+
except ValueError:
|
| 493 |
+
pass # Invalid IP, leave both as None
|
| 494 |
+
|
| 495 |
# Get geolocation from IP address
|
| 496 |
country, region = await get_geolocation(ip_address)
|
| 497 |
|
| 498 |
+
# Determine server user_id (if authenticated)
|
| 499 |
+
server_user_id = current_user.user_id if current_user else None
|
| 500 |
+
|
| 501 |
+
# If authenticated, always create a new ClientUser entry
|
| 502 |
+
if current_user:
|
| 503 |
+
new_client_user = ClientUser(
|
| 504 |
+
user_id=current_user.user_id,
|
| 505 |
+
client_user_id=client_user_id,
|
| 506 |
+
ipv4_address=ipv4_address,
|
| 507 |
+
ipv6_address=ipv6_address,
|
| 508 |
+
device_info={"user_agent": user_agent} if user_agent else None
|
| 509 |
+
)
|
| 510 |
+
db.add(new_client_user)
|
| 511 |
+
logger.info(f"Created ClientUser entry: user_id={current_user.user_id}, client_user_id={client_user_id}")
|
| 512 |
+
|
| 513 |
# Store each decrypted result as separate audit log entries
|
| 514 |
records_created = 0
|
| 515 |
for json_data in decrypted_results:
|
|
|
|
| 522 |
|
| 523 |
audit_log = AuditLog(
|
| 524 |
log_type="client",
|
| 525 |
+
user_id=server_user_id, # Set if authenticated, None if anonymous
|
| 526 |
client_user_id=client_user_id,
|
| 527 |
action="blink",
|
| 528 |
details=details,
|
|
|
|
| 538 |
if not decrypted_results and encrypted_data:
|
| 539 |
audit_log = AuditLog(
|
| 540 |
log_type="client",
|
| 541 |
+
user_id=server_user_id, # Set if authenticated, None if anonymous
|
| 542 |
client_user_id=client_user_id,
|
| 543 |
action="blink",
|
| 544 |
details={"encrypted_length": len(encrypted_data), "country": country, "region": region},
|
|
|
|
| 552 |
|
| 553 |
await db.commit()
|
| 554 |
|
| 555 |
+
auth_status = "authenticated" if current_user else "anonymous"
|
| 556 |
+
logger.info(f"Successfully processed blink for client: {client_user_id}, records: {records_created}, auth: {auth_status}")
|
| 557 |
|
| 558 |
+
# Return 204 No Content for silent tracking
|
| 559 |
+
return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 560 |
|
| 561 |
except HTTPException:
|
| 562 |
raise
|
|
|
|
| 567 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 568 |
detail="Internal server error processing request"
|
| 569 |
)
|
| 570 |
+
|