ac-tool/backend/server/api/auth.py
Vadym Samoilenko 8da149b84e Migrate storage from JSON files to PostgreSQL (asyncpg)
- 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>
2026-03-23 19:51:37 +00:00

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})