Init Repo
This commit is contained in:
@@ -0,0 +1,572 @@
|
||||
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
|
||||
)
|
||||
Reference in New Issue
Block a user