572 lines
18 KiB
Python
572 lines
18 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
|
from fastapi.security import HTTPAuthorizationCredentials
|
|
from sqlalchemy.orm import Session
|
|
from datetime import timedelta
|
|
from typing import List
|
|
import json
|
|
|
|
from database import get_db
|
|
from models.user import User, UserRole
|
|
from models.api_key import APIKey, APIKeyScope
|
|
from models.api_key_usage import APIKeyUsage
|
|
from schemas.auth import UserLogin, UserRegister, Token, RefreshToken
|
|
from schemas.api_key import APIKeyCreate, APIKeyResponse, APIKeyWithToken, APIKeyUpdate, APIKeyUsageLog
|
|
from utils.auth import (
|
|
verify_password,
|
|
get_password_hash,
|
|
create_access_token,
|
|
create_refresh_token,
|
|
verify_token,
|
|
security,
|
|
ACCESS_TOKEN_EXPIRE_MINUTES,
|
|
REFRESH_TOKEN_EXPIRE_DAYS,
|
|
generate_api_key,
|
|
hash_api_key,
|
|
get_current_user_flexible,
|
|
require_role
|
|
)
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.post("/register", response_model=dict)
|
|
async def register(user_data: UserRegister, db: Session = Depends(get_db)):
|
|
"""Register a new user account."""
|
|
# Check if user already exists
|
|
existing_user = db.query(User).filter(User.email == user_data.email).first()
|
|
if existing_user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Email already registered"
|
|
)
|
|
|
|
# Create new user
|
|
hashed_password = get_password_hash(user_data.password)
|
|
new_user = User(
|
|
email=user_data.email,
|
|
password_hash=hashed_password,
|
|
first_name=user_data.first_name,
|
|
last_name=user_data.last_name,
|
|
role=UserRole.ARTIST, # Default role
|
|
is_approved=False # Requires admin approval
|
|
)
|
|
|
|
db.add(new_user)
|
|
db.commit()
|
|
db.refresh(new_user)
|
|
|
|
return {
|
|
"message": "User registered successfully. Awaiting admin approval.",
|
|
"user_id": new_user.id
|
|
}
|
|
|
|
|
|
@router.post("/login", response_model=Token)
|
|
async def login(user_credentials: UserLogin, db: Session = Depends(get_db)):
|
|
"""Authenticate user and return JWT tokens."""
|
|
# Find user by email
|
|
print(user_credentials.email)
|
|
user = db.query(User).filter(User.email == user_credentials.email).first()
|
|
if not user or not verify_password(user_credentials.password, user.password_hash):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Incorrect email or password"
|
|
)
|
|
|
|
# Check if user is approved
|
|
if not user.is_approved:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Account not approved by administrator"
|
|
)
|
|
|
|
# Create tokens
|
|
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
|
refresh_token_expires = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
|
|
|
|
token_data = {"sub": str(user.id), "email": user.email, "role": user.role}
|
|
|
|
access_token = create_access_token(
|
|
data=token_data,
|
|
expires_delta=access_token_expires
|
|
)
|
|
refresh_token = create_refresh_token(
|
|
data=token_data,
|
|
expires_delta=refresh_token_expires
|
|
)
|
|
|
|
return Token(
|
|
access_token=access_token,
|
|
refresh_token=refresh_token
|
|
)
|
|
|
|
|
|
@router.post("/refresh", response_model=Token)
|
|
async def refresh_token(refresh_data: RefreshToken, db: Session = Depends(get_db)):
|
|
"""Refresh access token using refresh token."""
|
|
# Verify refresh token
|
|
payload = verify_token(refresh_data.refresh_token, "refresh")
|
|
if payload is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid refresh token"
|
|
)
|
|
|
|
# Get user from database
|
|
user_id = payload.get("sub")
|
|
user = db.query(User).filter(User.id == user_id).first()
|
|
if not user or not user.is_approved:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="User not found or not approved"
|
|
)
|
|
|
|
# Create new tokens
|
|
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
|
refresh_token_expires = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
|
|
|
|
token_data = {"sub": str(user.id), "email": user.email, "role": user.role}
|
|
|
|
new_access_token = create_access_token(
|
|
data=token_data,
|
|
expires_delta=access_token_expires
|
|
)
|
|
new_refresh_token = create_refresh_token(
|
|
data=token_data,
|
|
expires_delta=refresh_token_expires
|
|
)
|
|
|
|
return Token(
|
|
access_token=new_access_token,
|
|
refresh_token=new_refresh_token
|
|
)
|
|
|
|
|
|
@router.post("/logout")
|
|
async def logout():
|
|
"""Logout user (client should discard tokens)."""
|
|
return {"message": "Successfully logged out"}
|
|
|
|
|
|
# API Key Management Endpoints
|
|
|
|
@router.post("/api-keys", response_model=APIKeyWithToken)
|
|
async def create_api_key(
|
|
api_key_data: APIKeyCreate,
|
|
request: Request,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user_flexible)
|
|
):
|
|
"""Create a new API key. Only developers and admins can create API keys."""
|
|
# Check if user has permission to create API keys
|
|
if current_user.role != UserRole.DEVELOPER and not current_user.is_admin:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Only developers and users with admin permission can create API keys"
|
|
)
|
|
|
|
# Determine target user for the API key
|
|
target_user_id = current_user.id # Default to current user
|
|
target_user = current_user
|
|
|
|
# If user_id is specified and current user has admin permission, allow creating for other users
|
|
if api_key_data.user_id is not None:
|
|
if not current_user.is_admin:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Only users with admin permission can create API keys for other users"
|
|
)
|
|
|
|
# Verify target user exists and is approved
|
|
target_user = db.query(User).filter(User.id == api_key_data.user_id).first()
|
|
if not target_user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Target user not found"
|
|
)
|
|
|
|
if not target_user.is_approved:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Cannot create API key for unapproved user"
|
|
)
|
|
|
|
target_user_id = target_user.id
|
|
|
|
# Generate API key
|
|
api_key_token = generate_api_key()
|
|
key_hash = hash_api_key(api_key_token)
|
|
|
|
# Convert scopes to JSON string
|
|
scopes_json = json.dumps([scope.value for scope in api_key_data.scopes])
|
|
|
|
# Create API key record
|
|
new_api_key = APIKey(
|
|
user_id=target_user_id,
|
|
key_hash=key_hash,
|
|
name=api_key_data.name,
|
|
scopes=scopes_json,
|
|
expires_at=api_key_data.expires_at
|
|
)
|
|
|
|
db.add(new_api_key)
|
|
db.commit()
|
|
db.refresh(new_api_key)
|
|
|
|
# Convert scopes back to list for response
|
|
scopes_list = json.loads(new_api_key.scopes)
|
|
|
|
api_key_response = APIKeyResponse(
|
|
id=new_api_key.id,
|
|
user_id=new_api_key.user_id,
|
|
name=new_api_key.name,
|
|
scopes=scopes_list,
|
|
is_active=new_api_key.is_active,
|
|
expires_at=new_api_key.expires_at,
|
|
last_used_at=new_api_key.last_used_at,
|
|
created_at=new_api_key.created_at,
|
|
user_email=target_user.email
|
|
)
|
|
|
|
return APIKeyWithToken(
|
|
api_key=api_key_response,
|
|
token=api_key_token
|
|
)
|
|
|
|
|
|
@router.get("/api-keys", response_model=List[APIKeyResponse])
|
|
async def list_api_keys(
|
|
request: Request,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user_flexible),
|
|
user_id: int = None
|
|
):
|
|
"""List API keys. Developers see their own, admins can see all or filter by user."""
|
|
# Developers and users with admin permission can see API keys
|
|
if current_user.role != UserRole.DEVELOPER and not current_user.is_admin:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Only developers and users with admin permission can manage API keys"
|
|
)
|
|
|
|
# Build query based on user permissions and parameters
|
|
if current_user.is_admin:
|
|
if user_id is not None:
|
|
# Admin requesting specific user's API keys
|
|
api_keys = db.query(APIKey).filter(APIKey.user_id == user_id).all()
|
|
else:
|
|
# Admin requesting all API keys
|
|
api_keys = db.query(APIKey).all()
|
|
else:
|
|
# Developer can only see their own API keys
|
|
if user_id is not None and user_id != current_user.id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Developers can only view their own API keys"
|
|
)
|
|
api_keys = db.query(APIKey).filter(APIKey.user_id == current_user.id).all()
|
|
|
|
result = []
|
|
for api_key in api_keys:
|
|
scopes_list = json.loads(api_key.scopes)
|
|
|
|
# Get user email for admin view
|
|
user_email = None
|
|
if current_user.is_admin:
|
|
user = db.query(User).filter(User.id == api_key.user_id).first()
|
|
if user:
|
|
user_email = user.email
|
|
|
|
result.append(APIKeyResponse(
|
|
id=api_key.id,
|
|
user_id=api_key.user_id,
|
|
name=api_key.name,
|
|
scopes=scopes_list,
|
|
is_active=api_key.is_active,
|
|
expires_at=api_key.expires_at,
|
|
last_used_at=api_key.last_used_at,
|
|
created_at=api_key.created_at,
|
|
user_email=user_email
|
|
))
|
|
|
|
return result
|
|
|
|
|
|
@router.put("/api-keys/{api_key_id}", response_model=APIKeyResponse)
|
|
async def update_api_key(
|
|
api_key_id: int,
|
|
api_key_data: APIKeyUpdate,
|
|
request: Request,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user_flexible)
|
|
):
|
|
"""Update an API key."""
|
|
# Check if user has permission
|
|
if current_user.role != UserRole.DEVELOPER and not current_user.is_admin:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Only developers and users with admin permission can manage API keys"
|
|
)
|
|
|
|
# Get API key with different access rules for admin vs developer
|
|
if current_user.is_admin:
|
|
# Users with admin permission can update any API key
|
|
api_key = db.query(APIKey).filter(APIKey.id == api_key_id).first()
|
|
else:
|
|
# Developers can only update their own API keys
|
|
api_key = db.query(APIKey).filter(
|
|
APIKey.id == api_key_id,
|
|
APIKey.user_id == current_user.id
|
|
).first()
|
|
|
|
if not api_key:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="API key not found"
|
|
)
|
|
|
|
# Update fields
|
|
if api_key_data.name is not None:
|
|
api_key.name = api_key_data.name
|
|
|
|
if api_key_data.scopes is not None:
|
|
scopes_json = json.dumps([scope.value for scope in api_key_data.scopes])
|
|
api_key.scopes = scopes_json
|
|
|
|
if api_key_data.is_active is not None:
|
|
api_key.is_active = api_key_data.is_active
|
|
|
|
if api_key_data.expires_at is not None:
|
|
api_key.expires_at = api_key_data.expires_at
|
|
|
|
db.commit()
|
|
db.refresh(api_key)
|
|
|
|
# Convert scopes back to list for response
|
|
scopes_list = json.loads(api_key.scopes)
|
|
|
|
# Get user email for admin view
|
|
user_email = None
|
|
if current_user.is_admin:
|
|
user = db.query(User).filter(User.id == api_key.user_id).first()
|
|
if user:
|
|
user_email = user.email
|
|
|
|
return APIKeyResponse(
|
|
id=api_key.id,
|
|
user_id=api_key.user_id,
|
|
name=api_key.name,
|
|
scopes=scopes_list,
|
|
is_active=api_key.is_active,
|
|
expires_at=api_key.expires_at,
|
|
last_used_at=api_key.last_used_at,
|
|
created_at=api_key.created_at,
|
|
user_email=user_email
|
|
)
|
|
|
|
|
|
@router.delete("/api-keys/{api_key_id}")
|
|
async def delete_api_key(
|
|
api_key_id: int,
|
|
request: Request,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user_flexible)
|
|
):
|
|
"""Delete (revoke) an API key."""
|
|
# Check if user has permission
|
|
if current_user.role != UserRole.DEVELOPER and not current_user.is_admin:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Only developers and users with admin permission can manage API keys"
|
|
)
|
|
|
|
# Get API key with different access rules for admin vs developer
|
|
if current_user.is_admin:
|
|
# Users with admin permission can delete any API key
|
|
api_key = db.query(APIKey).filter(APIKey.id == api_key_id).first()
|
|
else:
|
|
# Developers can only delete their own API keys
|
|
api_key = db.query(APIKey).filter(
|
|
APIKey.id == api_key_id,
|
|
APIKey.user_id == current_user.id
|
|
).first()
|
|
|
|
if not api_key:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="API key not found"
|
|
)
|
|
|
|
# Delete the API key
|
|
db.delete(api_key)
|
|
db.commit()
|
|
|
|
return {"message": "API key revoked successfully"}
|
|
|
|
|
|
@router.get("/api-keys/{api_key_id}/usage", response_model=List[APIKeyUsageLog])
|
|
async def get_api_key_usage(
|
|
api_key_id: int,
|
|
request: Request,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user_flexible),
|
|
limit: int = 100
|
|
):
|
|
"""Get usage logs for an API key."""
|
|
# Check if user has permission
|
|
if current_user.role != UserRole.DEVELOPER and not current_user.is_admin:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Only developers and users with admin permission can view API key usage"
|
|
)
|
|
|
|
# Verify API key access with different rules for admin vs developer
|
|
if current_user.is_admin:
|
|
# Users with admin permission can view usage for any API key
|
|
api_key = db.query(APIKey).filter(APIKey.id == api_key_id).first()
|
|
else:
|
|
# Developers can only view usage for their own API keys
|
|
api_key = db.query(APIKey).filter(
|
|
APIKey.id == api_key_id,
|
|
APIKey.user_id == current_user.id
|
|
).first()
|
|
|
|
if not api_key:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="API key not found"
|
|
)
|
|
|
|
# Get usage logs
|
|
usage_logs = db.query(APIKeyUsage).filter(
|
|
APIKeyUsage.api_key_id == api_key_id
|
|
).order_by(APIKeyUsage.timestamp.desc()).limit(limit).all()
|
|
|
|
return [
|
|
APIKeyUsageLog(
|
|
api_key_id=log.api_key_id,
|
|
endpoint=log.endpoint,
|
|
method=log.method,
|
|
timestamp=log.timestamp,
|
|
ip_address=log.ip_address,
|
|
user_agent=log.user_agent
|
|
)
|
|
for log in usage_logs
|
|
]
|
|
|
|
|
|
# Admin-only endpoints for API key management
|
|
|
|
@router.get("/admin/users/{user_id}/api-keys", response_model=List[APIKeyResponse])
|
|
async def list_user_api_keys_admin(
|
|
user_id: int,
|
|
request: Request,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user_flexible)
|
|
):
|
|
"""Admin endpoint to list API keys for a specific user."""
|
|
# Only users with admin permission can access this endpoint
|
|
if not current_user.is_admin:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Admin permission required to access this endpoint"
|
|
)
|
|
|
|
# Verify target user exists
|
|
target_user = db.query(User).filter(User.id == user_id).first()
|
|
if not target_user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="User not found"
|
|
)
|
|
|
|
# Get API keys for the specified user
|
|
api_keys = db.query(APIKey).filter(APIKey.user_id == user_id).all()
|
|
|
|
result = []
|
|
for api_key in api_keys:
|
|
scopes_list = json.loads(api_key.scopes)
|
|
result.append(APIKeyResponse(
|
|
id=api_key.id,
|
|
user_id=api_key.user_id,
|
|
name=api_key.name,
|
|
scopes=scopes_list,
|
|
is_active=api_key.is_active,
|
|
expires_at=api_key.expires_at,
|
|
last_used_at=api_key.last_used_at,
|
|
created_at=api_key.created_at,
|
|
user_email=target_user.email
|
|
))
|
|
|
|
return result
|
|
|
|
|
|
@router.post("/admin/users/{user_id}/api-keys", response_model=APIKeyWithToken)
|
|
async def create_api_key_for_user_admin(
|
|
user_id: int,
|
|
api_key_data: APIKeyCreate,
|
|
request: Request,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user_flexible)
|
|
):
|
|
"""Admin endpoint to create an API key for a specific user."""
|
|
# Only users with admin permission can access this endpoint
|
|
if not current_user.is_admin:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Admin permission required to access this endpoint"
|
|
)
|
|
|
|
# Verify target user exists and is approved
|
|
target_user = db.query(User).filter(User.id == user_id).first()
|
|
if not target_user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="User not found"
|
|
)
|
|
|
|
if not target_user.is_approved:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Cannot create API key for unapproved user"
|
|
)
|
|
|
|
# Generate API key
|
|
api_key_token = generate_api_key()
|
|
key_hash = hash_api_key(api_key_token)
|
|
|
|
# Convert scopes to JSON string
|
|
scopes_json = json.dumps([scope.value for scope in api_key_data.scopes])
|
|
|
|
# Create API key record
|
|
new_api_key = APIKey(
|
|
user_id=user_id,
|
|
key_hash=key_hash,
|
|
name=api_key_data.name,
|
|
scopes=scopes_json,
|
|
expires_at=api_key_data.expires_at
|
|
)
|
|
|
|
db.add(new_api_key)
|
|
db.commit()
|
|
db.refresh(new_api_key)
|
|
|
|
# Convert scopes back to list for response
|
|
scopes_list = json.loads(new_api_key.scopes)
|
|
|
|
api_key_response = APIKeyResponse(
|
|
id=new_api_key.id,
|
|
user_id=new_api_key.user_id,
|
|
name=new_api_key.name,
|
|
scopes=scopes_list,
|
|
is_active=new_api_key.is_active,
|
|
expires_at=new_api_key.expires_at,
|
|
last_used_at=new_api_key.last_used_at,
|
|
created_at=new_api_key.created_at,
|
|
user_email=target_user.email
|
|
)
|
|
|
|
return APIKeyWithToken(
|
|
api_key=api_key_response,
|
|
token=api_key_token
|
|
) |