317 lines
8.8 KiB
Python
317 lines
8.8 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
認證 API
|
||
|
||
Author: PANJIT IT Team
|
||
Created: 2024-01-28
|
||
Modified: 2024-01-28
|
||
"""
|
||
|
||
from flask import Blueprint, request, jsonify, current_app
|
||
from flask_jwt_extended import create_access_token, create_refresh_token, jwt_required, get_jwt_identity
|
||
from app.utils.ldap_auth import LDAPAuthService
|
||
from app.utils.decorators import login_required, validate_json, rate_limit
|
||
from app.utils.exceptions import AuthenticationError
|
||
from app.utils.logger import get_logger
|
||
from app.models.user import User
|
||
from app.models.log import SystemLog
|
||
|
||
auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
|
||
logger = get_logger(__name__)
|
||
|
||
|
||
@auth_bp.route('/login', methods=['POST'])
|
||
@rate_limit(max_requests=10, per_seconds=300) # 5分鐘內最多10次嘗試
|
||
@validate_json(['username', 'password'])
|
||
def login():
|
||
"""使用者登入"""
|
||
try:
|
||
data = request.get_json()
|
||
username = data['username'].strip()
|
||
password = data['password']
|
||
|
||
if not username or not password:
|
||
return jsonify({
|
||
'success': False,
|
||
'error': 'INVALID_INPUT',
|
||
'message': '帳號和密碼不能為空'
|
||
}), 400
|
||
|
||
# LDAP 認證
|
||
ldap_service = LDAPAuthService()
|
||
user_info = ldap_service.authenticate_user(username, password)
|
||
|
||
# 取得或建立使用者
|
||
user = User.get_or_create(
|
||
username=user_info['username'],
|
||
display_name=user_info['display_name'],
|
||
email=user_info['email'],
|
||
department=user_info.get('department')
|
||
)
|
||
|
||
# 更新登入時間
|
||
user.update_last_login()
|
||
|
||
# 創建 JWT tokens
|
||
access_token = create_access_token(
|
||
identity=user.username,
|
||
additional_claims={
|
||
'user_id': user.id,
|
||
'is_admin': user.is_admin,
|
||
'display_name': user.display_name,
|
||
'email': user.email
|
||
}
|
||
)
|
||
refresh_token = create_refresh_token(identity=user.username)
|
||
|
||
# 記錄登入日誌
|
||
SystemLog.info(
|
||
'auth.login',
|
||
f'User {username} logged in successfully',
|
||
user_id=user.id,
|
||
extra_data={
|
||
'ip_address': request.remote_addr,
|
||
'user_agent': request.headers.get('User-Agent')
|
||
}
|
||
)
|
||
|
||
logger.info(f"🔑 [JWT Created] User: {username}, UserID: {user.id}")
|
||
logger.info(f"User {username} logged in successfully")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'data': {
|
||
'access_token': access_token,
|
||
'refresh_token': refresh_token,
|
||
'user': user.to_dict()
|
||
},
|
||
'message': '登入成功'
|
||
})
|
||
|
||
except AuthenticationError as e:
|
||
# 記錄認證失敗
|
||
SystemLog.warning(
|
||
'auth.login_failed',
|
||
f'Authentication failed for user {username}: {str(e)}',
|
||
extra_data={
|
||
'username': username,
|
||
'ip_address': request.remote_addr,
|
||
'error': str(e)
|
||
}
|
||
)
|
||
|
||
logger.warning(f"Authentication failed for user {username}: {str(e)}")
|
||
|
||
return jsonify({
|
||
'success': False,
|
||
'error': 'INVALID_CREDENTIALS',
|
||
'message': str(e)
|
||
}), 401
|
||
|
||
except Exception as e:
|
||
logger.error(f"Login error: {str(e)}")
|
||
|
||
SystemLog.error(
|
||
'auth.login_error',
|
||
f'Login system error: {str(e)}',
|
||
extra_data={
|
||
'username': username,
|
||
'error': str(e)
|
||
}
|
||
)
|
||
|
||
return jsonify({
|
||
'success': False,
|
||
'error': 'SYSTEM_ERROR',
|
||
'message': '系統錯誤,請稍後再試'
|
||
}), 500
|
||
|
||
|
||
@auth_bp.route('/logout', methods=['POST'])
|
||
@jwt_required()
|
||
def logout():
|
||
"""使用者登出"""
|
||
try:
|
||
username = get_jwt_identity()
|
||
|
||
# 記錄登出日誌
|
||
SystemLog.info(
|
||
'auth.logout',
|
||
f'User {username} logged out'
|
||
)
|
||
|
||
logger.info(f"🚪 [JWT Logout] User: {username}")
|
||
logger.info(f"User {username} logged out")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': '登出成功'
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"Logout error: {str(e)}")
|
||
|
||
return jsonify({
|
||
'success': False,
|
||
'error': 'SYSTEM_ERROR',
|
||
'message': '登出時發生錯誤'
|
||
}), 500
|
||
|
||
|
||
@auth_bp.route('/me', methods=['GET'])
|
||
@jwt_required()
|
||
def get_current_user():
|
||
"""取得當前使用者資訊"""
|
||
try:
|
||
from flask_jwt_extended import get_jwt
|
||
|
||
username = get_jwt_identity()
|
||
claims = get_jwt()
|
||
|
||
user_data = {
|
||
'username': username,
|
||
'user_id': claims.get('user_id'),
|
||
'is_admin': claims.get('is_admin'),
|
||
'display_name': claims.get('display_name'),
|
||
'email': claims.get('email')
|
||
}
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'data': {
|
||
'user': user_data
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"Get current user error: {str(e)}")
|
||
|
||
return jsonify({
|
||
'success': False,
|
||
'error': 'SYSTEM_ERROR',
|
||
'message': '取得使用者資訊時發生錯誤'
|
||
}), 500
|
||
|
||
|
||
@auth_bp.route('/refresh', methods=['POST'])
|
||
@jwt_required(refresh=True)
|
||
def refresh_token():
|
||
"""刷新 Session"""
|
||
try:
|
||
from flask import g
|
||
user = g.current_user
|
||
|
||
# 更新 Session 資訊
|
||
session['user_id'] = user.id
|
||
session['username'] = user.username
|
||
session['is_admin'] = user.is_admin
|
||
session.permanent = True
|
||
|
||
logger.info(f"Session refreshed for user {user.username}")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'data': {
|
||
'user': user.to_dict(),
|
||
'session_refreshed': True
|
||
},
|
||
'message': 'Session 已刷新'
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"Session refresh error: {str(e)}")
|
||
|
||
return jsonify({
|
||
'success': False,
|
||
'error': 'SYSTEM_ERROR',
|
||
'message': '刷新 Session 時發生錯誤'
|
||
}), 500
|
||
|
||
|
||
@auth_bp.route('/check', methods=['GET'])
|
||
def check_auth():
|
||
"""檢查認證狀態"""
|
||
try:
|
||
user_id = session.get('user_id')
|
||
|
||
if not user_id:
|
||
return jsonify({
|
||
'success': False,
|
||
'authenticated': False,
|
||
'message': '未登入'
|
||
}), 401
|
||
|
||
# 驗證使用者是否仍然存在
|
||
user = User.query.get(user_id)
|
||
if not user:
|
||
session.clear()
|
||
return jsonify({
|
||
'success': False,
|
||
'authenticated': False,
|
||
'message': '使用者不存在'
|
||
}), 401
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'authenticated': True,
|
||
'data': {
|
||
'user': user.to_dict()
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"Auth check error: {str(e)}")
|
||
|
||
return jsonify({
|
||
'success': False,
|
||
'error': 'SYSTEM_ERROR',
|
||
'message': '檢查認證狀態時發生錯誤'
|
||
}), 500
|
||
|
||
|
||
@auth_bp.route('/search-users', methods=['GET'])
|
||
@login_required
|
||
def search_users():
|
||
"""搜尋使用者(LDAP)"""
|
||
try:
|
||
search_term = request.args.get('q', '').strip()
|
||
limit = min(int(request.args.get('limit', 20)), 50)
|
||
|
||
if len(search_term) < 2:
|
||
return jsonify({
|
||
'success': False,
|
||
'error': 'INVALID_SEARCH_TERM',
|
||
'message': '搜尋關鍵字至少需要2個字元'
|
||
}), 400
|
||
|
||
ldap_service = LDAPAuthService()
|
||
users = ldap_service.search_users(search_term, limit)
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'data': {
|
||
'users': users,
|
||
'count': len(users)
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"User search error: {str(e)}")
|
||
|
||
return jsonify({
|
||
'success': False,
|
||
'error': 'SYSTEM_ERROR',
|
||
'message': '搜尋使用者時發生錯誤'
|
||
}), 500
|
||
|
||
|
||
# 錯誤處理器
|
||
@auth_bp.errorhandler(429)
|
||
def rate_limit_handler(e):
|
||
"""速率限制錯誤處理器"""
|
||
return jsonify({
|
||
'success': False,
|
||
'error': 'RATE_LIMIT_EXCEEDED',
|
||
'message': '請求過於頻繁,請稍後再試'
|
||
}), 429 |