mirror of
https://github.com/taylanbakircioglu/haproxy-openmanager.git
synced 2026-09-20 01:25:12 +00:00
526 lines
18 KiB
Python
526 lines
18 KiB
Python
from fastapi import APIRouter, HTTPException, Depends, Request, Header
|
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
from typing import Optional
|
|
import logging
|
|
import hashlib
|
|
import time
|
|
from datetime import datetime, timedelta
|
|
|
|
# Import database and models
|
|
from database.connection import get_database_connection, close_database_connection
|
|
from models.user import LoginRequest, User, UserCreate, UserUpdate, UserPasswordUpdate
|
|
from utils.activity_log import log_user_activity
|
|
from auth_middleware import get_current_user_from_token
|
|
|
|
# Rate limiting temporarily disabled
|
|
|
|
router = APIRouter(prefix="/api/auth", tags=["Authentication"])
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Security scheme
|
|
security = HTTPBearer()
|
|
|
|
@router.post("/login", summary="User Login", response_description="JWT access token and user information")
|
|
async def login(login_request: LoginRequest, request: Request):
|
|
"""
|
|
# User Login - Authenticate and Get Access Token
|
|
|
|
Authenticate user with username and password. Returns a JWT access token valid for 24 hours.
|
|
|
|
## Request Body
|
|
- **username**: User's username (required)
|
|
- **password**: User's password (required)
|
|
|
|
## Response
|
|
Returns JWT token, user information, roles, and permissions.
|
|
|
|
## Example Request
|
|
```bash
|
|
curl -X POST "{BASE_URL}/api/auth/login" \\
|
|
-H "Content-Type: application/json" \\
|
|
-d '{
|
|
"username": "admin",
|
|
"password": "admin123"
|
|
}'
|
|
```
|
|
|
|
> Replace `{BASE_URL}` with your deployment URL
|
|
|
|
## Example Response
|
|
```json
|
|
{
|
|
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
|
"token_type": "bearer",
|
|
"expires_in": 86400,
|
|
"user": {
|
|
"id": 1,
|
|
"username": "admin",
|
|
"email": "admin@example.com",
|
|
"role": "admin",
|
|
"is_active": true,
|
|
"created_at": "2024-01-01T00:00:00",
|
|
"last_login_at": "2024-01-15T10:30:00"
|
|
},
|
|
"roles": [
|
|
{
|
|
"id": 1,
|
|
"name": "admin",
|
|
"display_name": "Administrator"
|
|
}
|
|
],
|
|
"permissions": {
|
|
"clusters": {"read": true, "write": true, "delete": true},
|
|
"agents": {"read": true, "write": true, "delete": true}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Using the Token
|
|
Include the access token in subsequent requests:
|
|
```bash
|
|
curl -X GET "{BASE_URL}/api/clusters" \\
|
|
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
|
```
|
|
|
|
## Error Responses
|
|
- **401**: Invalid credentials or inactive account
|
|
- **500**: Server error during authentication
|
|
"""
|
|
|
|
try:
|
|
conn = await get_database_connection()
|
|
|
|
# Find user by username (with safe column handling)
|
|
try:
|
|
user = await conn.fetchrow("""
|
|
SELECT id, username, email, password_hash, is_active, role,
|
|
created_at, updated_at, last_login_at
|
|
FROM users
|
|
WHERE username = $1
|
|
""", login_request.username)
|
|
except Exception as schema_error:
|
|
logger.warning(f"Schema error, trying fallback query: {schema_error}")
|
|
# Fallback query without role column - try different column names
|
|
try:
|
|
user = await conn.fetchrow("""
|
|
SELECT id, username, email, password_hash, is_active,
|
|
created_at, updated_at, last_login_at
|
|
FROM users
|
|
WHERE username = $1
|
|
""", login_request.username)
|
|
except Exception as column_error:
|
|
logger.warning(f"last_login_at column error, trying last_login: {column_error}")
|
|
# Try with last_login instead of last_login_at
|
|
user = await conn.fetchrow("""
|
|
SELECT id, username, email, password_hash, is_active,
|
|
created_at, updated_at, last_login
|
|
FROM users
|
|
WHERE username = $1
|
|
""", login_request.username)
|
|
|
|
if not user:
|
|
await close_database_connection(conn)
|
|
logger.warning(f"Failed login attempt for username: {login_request.username}")
|
|
raise HTTPException(status_code=401, detail="Invalid username or password")
|
|
|
|
if not user['is_active']:
|
|
await close_database_connection(conn)
|
|
logger.warning(f"Login attempt for inactive account: {login_request.username}")
|
|
raise HTTPException(status_code=401, detail="Account is deactivated")
|
|
|
|
# Verify password
|
|
import bcrypt
|
|
if not bcrypt.checkpw(login_request.password.encode('utf-8'), user['password_hash'].encode('utf-8')):
|
|
await close_database_connection(conn)
|
|
logger.warning(f"Wrong password for user: {login_request.username}")
|
|
raise HTTPException(status_code=401, detail="Invalid username or password")
|
|
|
|
# Update last login (try different column names)
|
|
try:
|
|
await conn.execute("""
|
|
UPDATE users SET last_login_at = CURRENT_TIMESTAMP
|
|
WHERE id = $1
|
|
""", user['id'])
|
|
except Exception as update_error:
|
|
logger.warning(f"last_login_at update failed, trying last_login: {update_error}")
|
|
try:
|
|
await conn.execute("""
|
|
UPDATE users SET last_login = CURRENT_TIMESTAMP
|
|
WHERE id = $1
|
|
""", user['id'])
|
|
except Exception as fallback_error:
|
|
logger.warning(f"Could not update last login: {fallback_error}")
|
|
# Continue without updating last_login
|
|
|
|
# Get user roles and permissions before closing connection
|
|
conn_for_roles = conn
|
|
|
|
# Fetch user roles with their permissions
|
|
user_roles = await conn_for_roles.fetch("""
|
|
SELECT r.id, r.name, r.display_name, r.permissions
|
|
FROM user_roles ur
|
|
JOIN roles r ON ur.role_id = r.id
|
|
WHERE ur.user_id = $1 AND ur.is_active = TRUE AND r.is_active = TRUE
|
|
""", user['id'])
|
|
|
|
# Build permissions dictionary
|
|
permissions = {}
|
|
roles_list = []
|
|
|
|
for role_row in user_roles:
|
|
roles_list.append({
|
|
'id': role_row['id'],
|
|
'name': role_row['name'],
|
|
'display_name': role_row['display_name']
|
|
})
|
|
|
|
# Parse permissions from JSON string if needed
|
|
role_permissions = role_row['permissions']
|
|
if isinstance(role_permissions, str):
|
|
import json
|
|
role_permissions = json.loads(role_permissions)
|
|
|
|
# Merge permissions (resource.action format)
|
|
if role_permissions:
|
|
for perm in role_permissions:
|
|
if '.' in perm:
|
|
resource, action = perm.split('.', 1)
|
|
if resource not in permissions:
|
|
permissions[resource] = {}
|
|
permissions[resource][action] = True
|
|
|
|
await close_database_connection(conn)
|
|
|
|
# Create JWT token
|
|
from jose import jwt
|
|
from config import JWT_SECRET_KEY, JWT_ALGORITHM
|
|
|
|
payload = {
|
|
"user_id": user['id'],
|
|
"username": user['username'],
|
|
"email": user.get('email'),
|
|
"role": user.get('role', 'admin'), # Default to admin if role column doesn't exist
|
|
"exp": datetime.utcnow() + timedelta(hours=24) # 24 hour expiry
|
|
}
|
|
|
|
token = jwt.encode(payload, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM)
|
|
|
|
# Log login activity
|
|
await log_user_activity(
|
|
user_id=user['id'],
|
|
action='login',
|
|
resource_type='auth',
|
|
resource_id=str(user['id']),
|
|
details={
|
|
'login_method': 'username_password',
|
|
'success': True
|
|
},
|
|
ip_address=str(request.client.host) if request.client else None,
|
|
user_agent=request.headers.get('user-agent')
|
|
)
|
|
|
|
return {
|
|
"access_token": token,
|
|
"token_type": "bearer",
|
|
"expires_in": 86400, # 24 hours in seconds
|
|
"user": {
|
|
"id": user['id'],
|
|
"username": user['username'],
|
|
"email": user.get('email'),
|
|
"role": user.get('role', 'admin'),
|
|
"is_active": user.get('is_active', True),
|
|
"created_at": user['created_at'].isoformat() if user.get('created_at') else None,
|
|
"last_login_at": datetime.utcnow().isoformat()
|
|
},
|
|
"roles": roles_list,
|
|
"permissions": permissions
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Login error: {e}")
|
|
raise HTTPException(status_code=500, detail="Login failed")
|
|
|
|
@router.post("/logout", summary="User Logout", response_description="Logout confirmation")
|
|
async def logout(request: Request, authorization: str = Header(None)):
|
|
"""
|
|
# User Logout
|
|
|
|
Logout current user and log activity. Requires valid JWT token.
|
|
|
|
## Headers
|
|
- **Authorization**: Bearer {access_token}
|
|
|
|
## Example Request
|
|
```bash
|
|
curl -X POST "{BASE_URL}/api/auth/logout" \\
|
|
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
|
```
|
|
|
|
## Example Response
|
|
```json
|
|
{
|
|
"message": "Logout successful"
|
|
}
|
|
```
|
|
"""
|
|
try:
|
|
# Get current user for activity logging
|
|
current_user = await get_current_user_from_token(authorization)
|
|
|
|
if current_user and current_user.get('id'):
|
|
# Log logout activity
|
|
await log_user_activity(
|
|
user_id=current_user['id'],
|
|
action='logout',
|
|
resource_type='auth',
|
|
resource_id=str(current_user['id']),
|
|
details={
|
|
'logout_method': 'manual'
|
|
},
|
|
ip_address=str(request.client.host) if request.client else None,
|
|
user_agent=request.headers.get('user-agent')
|
|
)
|
|
|
|
return {"message": "Logout successful"}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Logout error: {e}")
|
|
# Still return success even if logging fails
|
|
return {"message": "Logout successful"}
|
|
|
|
@router.get("/me", summary="Get Current User", response_description="Current user information")
|
|
async def get_current_user(authorization: str = Header(None)):
|
|
"""
|
|
# Get Current User Information
|
|
|
|
Get authenticated user's profile information. Requires valid JWT token.
|
|
|
|
## Headers
|
|
- **Authorization**: Bearer {access_token}
|
|
|
|
## Example Request
|
|
```bash
|
|
curl -X GET "{BASE_URL}/api/auth/me" \\
|
|
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
|
```
|
|
|
|
## Example Response
|
|
```json
|
|
{
|
|
"id": 1,
|
|
"username": "admin",
|
|
"email": "admin@example.com",
|
|
"role": "admin",
|
|
"is_active": true,
|
|
"created_at": "2024-01-01T00:00:00",
|
|
"updated_at": "2024-01-15T10:30:00",
|
|
"last_login_at": "2024-01-15T10:30:00"
|
|
}
|
|
```
|
|
"""
|
|
try:
|
|
current_user = await get_current_user_from_token(authorization)
|
|
|
|
if not current_user:
|
|
raise HTTPException(status_code=401, detail="Not authenticated")
|
|
|
|
conn = await get_database_connection()
|
|
|
|
# Get fresh user data (with safe column handling)
|
|
try:
|
|
user = await conn.fetchrow("""
|
|
SELECT id, username, email, role, is_active,
|
|
created_at, updated_at, last_login_at
|
|
FROM users
|
|
WHERE id = $1
|
|
""", current_user['id'])
|
|
except Exception as schema_error:
|
|
logger.warning(f"Schema error in /me endpoint, trying fallback: {schema_error}")
|
|
# Fallback query without role column - try different column names
|
|
try:
|
|
user = await conn.fetchrow("""
|
|
SELECT id, username, email, is_active,
|
|
created_at, updated_at, last_login_at
|
|
FROM users
|
|
WHERE id = $1
|
|
""", current_user['id'])
|
|
except Exception as column_error:
|
|
logger.warning(f"last_login_at column error in /me, trying last_login: {column_error}")
|
|
# Try with last_login instead of last_login_at
|
|
user = await conn.fetchrow("""
|
|
SELECT id, username, email, is_active,
|
|
created_at, updated_at, last_login
|
|
FROM users
|
|
WHERE id = $1
|
|
""", current_user['id'])
|
|
|
|
await close_database_connection(conn)
|
|
|
|
if not user:
|
|
raise HTTPException(status_code=401, detail="User not found")
|
|
|
|
return {
|
|
"id": user['id'],
|
|
"username": user['username'],
|
|
"email": user.get('email'),
|
|
"role": user.get('role', 'admin'),
|
|
"is_active": user.get('is_active', True),
|
|
"created_at": user['created_at'].isoformat() if user.get('created_at') else None,
|
|
"updated_at": user['updated_at'].isoformat() if user.get('updated_at') else None,
|
|
"last_login_at": (user.get('last_login_at') or user.get('last_login', None)).isoformat() if (user.get('last_login_at') or user.get('last_login')) else None
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Get current user error: {e}")
|
|
raise HTTPException(status_code=500, detail="Failed to get user info")
|
|
|
|
@router.post("/change-password", summary="Change Password", response_description="Password change confirmation")
|
|
async def change_password(password_update: UserPasswordUpdate, request: Request, authorization: str = Header(None)):
|
|
"""
|
|
# Change User Password
|
|
|
|
Change authenticated user's password. Requires current password for verification.
|
|
|
|
## Headers
|
|
- **Authorization**: Bearer {access_token}
|
|
|
|
## Request Body
|
|
- **current_password**: Current password for verification
|
|
- **new_password**: New password (min 8 characters recommended)
|
|
|
|
## Example Request
|
|
```bash
|
|
curl -X POST "{BASE_URL}/api/auth/change-password" \\
|
|
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \\
|
|
-H "Content-Type: application/json" \\
|
|
-d '{
|
|
"current_password": "admin123",
|
|
"new_password": "newSecurePassword456"
|
|
}'
|
|
```
|
|
|
|
## Example Response
|
|
```json
|
|
{
|
|
"message": "Password changed successfully"
|
|
}
|
|
```
|
|
|
|
## Error Responses
|
|
- **400**: Current password is incorrect
|
|
- **401**: Not authenticated
|
|
- **404**: User not found
|
|
"""
|
|
try:
|
|
current_user = await get_current_user_from_token(authorization)
|
|
|
|
if not current_user:
|
|
raise HTTPException(status_code=401, detail="Not authenticated")
|
|
|
|
conn = await get_database_connection()
|
|
|
|
# Get current user data
|
|
user = await conn.fetchrow("""
|
|
SELECT id, password_hash FROM users WHERE id = $1
|
|
""", current_user['id'])
|
|
|
|
if not user:
|
|
await close_database_connection(conn)
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
|
|
# Verify current password
|
|
import bcrypt
|
|
if not bcrypt.checkpw(password_update.current_password.encode('utf-8'), user['password_hash'].encode('utf-8')):
|
|
await close_database_connection(conn)
|
|
raise HTTPException(status_code=400, detail="Current password is incorrect")
|
|
|
|
# Hash new password
|
|
new_password_hash = bcrypt.hashpw(password_update.new_password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
|
|
|
# Update password
|
|
await conn.execute("""
|
|
UPDATE users SET password_hash = $1, updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = $2
|
|
""", new_password_hash, current_user['id'])
|
|
|
|
await close_database_connection(conn)
|
|
|
|
# Log password change activity
|
|
await log_user_activity(
|
|
user_id=current_user['id'],
|
|
action='update',
|
|
resource_type='user',
|
|
resource_id=str(current_user['id']),
|
|
details={
|
|
'action': 'password_change',
|
|
'success': True
|
|
},
|
|
ip_address=str(request.client.host) if request.client else None,
|
|
user_agent=request.headers.get('user-agent')
|
|
)
|
|
|
|
return {"message": "Password changed successfully"}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Change password error: {e}")
|
|
raise HTTPException(status_code=500, detail="Failed to change password")
|
|
|
|
@router.get("/validate-token", summary="Validate Token", response_description="Token validation result")
|
|
async def validate_token(authorization: str = Header(None)):
|
|
"""
|
|
# Validate JWT Token
|
|
|
|
Check if a JWT token is valid and not expired.
|
|
|
|
## Headers
|
|
- **Authorization**: Bearer {access_token}
|
|
|
|
## Example Request
|
|
```bash
|
|
curl -X GET "{BASE_URL}/api/auth/validate-token" \\
|
|
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
|
```
|
|
|
|
## Example Response
|
|
```json
|
|
{
|
|
"valid": true,
|
|
"user": {
|
|
"id": 1,
|
|
"username": "admin",
|
|
"email": "admin@example.com",
|
|
"role": "admin"
|
|
}
|
|
}
|
|
```
|
|
|
|
## Error Responses
|
|
- **401**: Invalid or expired token
|
|
"""
|
|
try:
|
|
current_user = await get_current_user_from_token(authorization)
|
|
|
|
if not current_user:
|
|
raise HTTPException(status_code=401, detail="Invalid or expired token")
|
|
|
|
return {
|
|
"valid": True,
|
|
"user": {
|
|
"id": current_user['id'],
|
|
"username": current_user['username'],
|
|
"email": current_user.get('email'),
|
|
"role": current_user.get('role', 'admin')
|
|
}
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Token validation error: {e}")
|
|
raise HTTPException(status_code=401, detail="Token validation failed") |