- Add asyncpg connection pool (db/pool.py) with JSONB codec registration - Add schema.sql with users, clients, dropdown_categories, export_templates, sheets tables - Add migrate_json.py one-time migration script for existing JSON data - Rewrite user_store, sheets/manager, api/clients, api/dropdowns, api/export as async DB-backed - Update all callers (auth, sheets, admin, ai_command, export) to await async functions - Add postgres:16-alpine service to docker-compose with named volume and health check - App container depends_on postgres; DATABASE_URL injected via env - Schema applied automatically on startup; global categories seeded if DB is empty Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
82 lines
2.5 KiB
Python
82 lines
2.5 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, get_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 = await 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."""
|
|
user = await get_current_user()
|
|
stored = await get_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})
|