API credit key rotation

This commit is contained in:
seniorswe
2025-11-22 10:12:50 -05:00
parent fbcccd7aa2
commit ce82dbc91d
5 changed files with 200 additions and 0 deletions

1
.gitignore vendored
View File

@@ -80,3 +80,4 @@ backend-services/routes/proto/*
.production.env
.coverage
coverage_html/
doorman.code-workspace

View File

@@ -19,6 +19,7 @@ from utils.auth_blacklist import TimedHeap, jwt_blacklist, revoke_all_for_user,
from utils.role_util import platform_role_required_bool
from utils.role_util import is_admin_user
from models.update_user_model import UpdateUserModel
from models.create_user_model import CreateUserModel
from utils.limit_throttle_util import limit_by_ip
authorization_router = APIRouter()
@@ -220,6 +221,92 @@ async def authorization(request: Request):
end_time = time.time() * 1000
logger.info(f'{request_id} | Total time: {str(end_time - start_time)}ms')
"""
Register new user
Request:
{}
Response:
{}
"""
@authorization_router.post('/authorization/register',
description='Register new user',
response_model=ResponseModel,
responses={
200: {
'description': 'Successful Response',
'content': {
'application/json': {
'example': {
'message': 'User created successfully'
}
}
}
}
}
)
async def register(request: Request):
request_id = str(uuid.uuid4())
start_time = time.time() * 1000
try:
# Rate limit registration to prevent abuse
reg_limit = int(os.getenv('REGISTER_IP_RATE_LIMIT', '5'))
reg_window = int(os.getenv('REGISTER_IP_RATE_WINDOW', '3600'))
rate_limit_info = await limit_by_ip(request, limit=reg_limit, window=reg_window)
logger.info(f'{request_id} | Register request from: {request.client.host}:{request.client.port}')
try:
data = await request.json()
except Exception:
return respond_rest(ResponseModel(
status_code=400,
response_headers={'request_id': request_id},
error_code='AUTH004',
error_message='Invalid JSON payload'
))
# Validate required fields
if not data.get('email') or not data.get('password'):
return respond_rest(ResponseModel(
status_code=400,
response_headers={'request_id': request_id},
error_code='AUTH001',
error_message='Missing email or password'
))
# Create user model
# Default to 'user' role and active=True
user_data = CreateUserModel(
username=data.get('email').split('@')[0], # Simple username derivation
email=data.get('email'),
password=data.get('password'),
role='user',
active=True
)
# Check if user exists (UserService.create_user handles this but we want clean error)
# Actually UserService.create_user will return error if exists.
result = await UserService.create_user(user_data, request_id)
# If successful, we could auto-login, but for now just return success
return respond_rest(result)
except Exception as e:
logger.critical(f'{request_id} | Unexpected error: {str(e)}', exc_info=True)
return respond_rest(ResponseModel(
status_code=500,
response_headers={'request_id': request_id},
error_code='GTW999',
error_message='An unexpected error occurred'
))
finally:
end_time = time.time() * 1000
logger.info(f'{request_id} | Total time: {str(end_time - start_time)}ms')
"""
Endpoint

View File

@@ -424,3 +424,71 @@ async def get_credits(username: str, request: Request):
finally:
end_time = time.time() * 1000
logger.info(f'{request_id} | Total time: {str(end_time - start_time)}ms')
"""
Rotate API key for a user
Request:
{}
Response:
{}
"""
@credit_router.post('/rotate-key',
description='Rotate API key for the authenticated user',
response_model=ResponseModel,
responses={
200: {
'description': 'Successful Response',
'content': {
'application/json': {
'example': {
'api_key': '******************'
}
}
}
}
}
)
async def rotate_key(request: Request):
request_id = str(uuid.uuid4())
start_time = time.time() * 1000
try:
payload = await auth_required(request)
username = payload.get('sub')
# Get group from body
try:
body = await request.json()
group = body.get('api_credit_group')
except Exception:
group = None
if not group:
return respond_rest(ResponseModel(
status_code=400,
error_code='CRD020',
error_message='api_credit_group is required'
))
logger.info(f'{request_id} | Username: {username} | From: {request.client.host}:{request.client.port}')
logger.info(f'{request_id} | Endpoint: {request.method} {str(request.url.path)}')
# No special role required, just authentication
return respond_rest(await CreditService.rotate_api_key(username, group, request_id))
except Exception as e:
logger.critical(f'{request_id} | Unexpected error: {str(e)}', exc_info=True)
return respond_rest(ResponseModel(
status_code=500,
response_headers={
'request_id': request_id
},
error_code='GTW999',
error_message='An unexpected error occurred'
))
finally:
end_time = time.time() * 1000
logger.info(f'{request_id} | Total time: {str(end_time - start_time)}ms')

View File

@@ -7,6 +7,7 @@ See https://github.com/apidoorman/doorman for more information
from pymongo.errors import PyMongoError
import logging
from typing import Optional
import secrets
from models.response_model import ResponseModel
from models.credit_model import CreditModel
@@ -299,3 +300,43 @@ class CreditService:
except PyMongoError as e:
logger.error(request_id + f' | Get user credits failed with database error: {str(e)}')
return ResponseModel(status_code=500, error_code='CRD018', error_message='Database error occurred while retrieving user credits').dict()
@staticmethod
async def rotate_api_key(username: str, group: str, request_id):
logger.info(request_id + f' | Rotating API key for user: {username}, group: {group}')
try:
doc = await db_find_one(user_credit_collection, {'username': username})
if not doc:
# Create if not exists? Or error?
# For rotation, we expect it to exist, or at least the user to exist.
# But maybe we can create the credit entry if it's missing.
# Let's error if not found for now, or create empty.
doc = {'username': username, 'users_credits': {}}
users_credits = doc.get('users_credits') or {}
group_credits = users_credits.get(group) or {}
# Generate new key
new_key = secrets.token_urlsafe(32)
encrypted_key = encrypt_value(new_key)
# Update group credits
# Preserve other fields in group_credits (like available_credits)
if isinstance(group_credits, dict):
group_credits['user_api_key'] = encrypted_key
else:
# Should be a dict, but if it was somehow not, reset it
group_credits = {'user_api_key': encrypted_key, 'available_credits': 0, 'tier_name': 'default'}
users_credits[group] = group_credits
if doc.get('_id'):
await db_update_one(user_credit_collection, {'username': username}, {'$set': {'users_credits': users_credits}})
else:
await db_insert_one(user_credit_collection, {'username': username, 'users_credits': users_credits})
return ResponseModel(status_code=200, response={'api_key': new_key}).dict()
except PyMongoError as e:
logger.error(request_id + f' | Rotate key failed with database error: {str(e)}')
return ResponseModel(status_code=500, error_code='CRD019', error_message='Database error occurred while rotating API key').dict()

View File

@@ -490,6 +490,9 @@ class GatewayService:
allowed_headers = api.get('api_allowed_headers') or [] if api else []
headers = await get_headers(request, allowed_headers)
headers['X-Request-ID'] = request_id
if username:
headers['X-User-Email'] = str(username)
headers['X-Doorman-User'] = str(username)
if api and api.get('api_credits_enabled'):
ai_token_headers = await credit_util.get_credit_api_header(api.get('api_credit_group'))
if ai_token_headers: