Merges ac-helper (PHP Activation Calendar) and brief-extractor (Python AI) into a single Docker app with React/TypeScript frontend. Features: - Brief upload → AI extraction → review → Activation Calendar import - Handsontable v17 spreadsheet with dependent dropdowns (148 categories) - AI natural language commands via Gemini (YOLO mode, voice input) - Azure AD MSAL SPA PKCE authentication, user roles (user/admin) - CSV Activation Calendar export - Real-time WebSocket job progress - Admin: user management, dropdown Excel upload - Multi-stage Dockerfile, docker-compose, nginx proxy instructions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
83 lines
2.6 KiB
Python
83 lines
2.6 KiB
Python
"""
|
|
Auth API endpoints.
|
|
"""
|
|
|
|
import logging
|
|
from quart import Blueprint, jsonify, request
|
|
|
|
from ..auth.msal_auth import msal_auth
|
|
from ..auth.middleware import auth_required, get_current_user
|
|
from ..auth.user_store import upsert_user
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
auth_bp = Blueprint('auth', __name__, url_prefix='/api/auth')
|
|
|
|
|
|
@auth_bp.route('/config', methods=['GET'])
|
|
async def get_auth_config():
|
|
return jsonify({'config': msal_auth.get_client_config(), 'devMode': msal_auth.is_dev_mode()})
|
|
|
|
|
|
@auth_bp.route('/validate', methods=['POST'])
|
|
async def validate_token():
|
|
try:
|
|
data = await request.get_json()
|
|
token = (data or {}).get('accessToken')
|
|
if not token:
|
|
return jsonify({'error': 'invalid_request', 'message': 'accessToken required'}), 400
|
|
|
|
user_info = await msal_auth.validate_token(token)
|
|
if not user_info:
|
|
return jsonify({'valid': False, 'error': 'invalid_token'}), 401
|
|
|
|
stored = upsert_user(user_info['oid'], user_info.get('preferred_username', ''), user_info.get('name', ''))
|
|
return jsonify({
|
|
'valid': True,
|
|
'user': {
|
|
'id': user_info['oid'],
|
|
'email': user_info.get('preferred_username'),
|
|
'name': user_info.get('name'),
|
|
'role': stored.get('role', 'user'),
|
|
},
|
|
})
|
|
except Exception as e:
|
|
logger.error(f"Token validation error: {e}")
|
|
return jsonify({'error': 'validation_error'}), 500
|
|
|
|
|
|
@auth_bp.route('/me', methods=['GET'])
|
|
@auth_required
|
|
async def me():
|
|
"""Return current user profile including role."""
|
|
from ..auth.user_store import get_user as get_stored_user
|
|
user = await get_current_user()
|
|
stored = get_stored_user(user['oid']) or {}
|
|
return jsonify({
|
|
'id': user['oid'],
|
|
'email': user.get('preferred_username'),
|
|
'name': user.get('name'),
|
|
'role': user.get('role', 'user'),
|
|
'active': stored.get('active', True),
|
|
'created': stored.get('created'),
|
|
'last_seen': stored.get('last_seen'),
|
|
})
|
|
|
|
|
|
@auth_bp.route('/user', methods=['GET'])
|
|
@auth_required
|
|
async def get_current_user_info():
|
|
user = await get_current_user()
|
|
return jsonify({'user': {
|
|
'id': user['oid'],
|
|
'username': user.get('preferred_username'),
|
|
'name': user.get('name'),
|
|
'role': user.get('role', 'user'),
|
|
}})
|
|
|
|
|
|
@auth_bp.route('/logout', methods=['POST'])
|
|
async def logout():
|
|
data = await request.get_json() or {}
|
|
logout_url = await msal_auth.get_logout_url(data.get('redirectUri'))
|
|
return jsonify({'logoutUrl': logout_url})
|