ac-tool/backend/server/auth/user_store.py
Vadym Samoilenko 72c50b2c92 Initial commit — AC Tool unified application
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>
2026-03-23 13:24:46 +00:00

96 lines
2.5 KiB
Python

"""
User store — manages users.json (roles, active status).
Keyed by Azure AD oid (object ID).
"""
import json
import logging
import os
from datetime import datetime, timezone
from typing import Dict, Optional
from ..config_runtime import server_config
logger = logging.getLogger(__name__)
_LOCK_FILE = server_config.USERS_FILE + '.lock'
def _load() -> Dict:
path = server_config.USERS_FILE
if not os.path.exists(path):
return {}
try:
with open(path, 'r') as f:
return json.load(f)
except Exception:
return {}
def _save(data: Dict):
path = server_config.USERS_FILE
with open(path, 'w') as f:
json.dump(data, f, indent=2)
def get_user(user_id: str) -> Optional[Dict]:
users = _load()
return users.get(user_id)
def upsert_user(user_id: str, email: str, name: str, role: Optional[str] = None) -> Dict:
"""
Create or update user. On creation defaults to 'user' role,
unless the email matches ADMIN_EMAIL env var (gets 'admin').
"""
users = _load()
existing = users.get(user_id)
if existing is None:
# First login — determine default role
default_role = 'admin' if email and email.lower() == server_config.ADMIN_EMAIL.lower() else 'user'
user = {
'id': user_id,
'email': email,
'name': name,
'role': role or default_role,
'active': True,
'created': datetime.now(timezone.utc).isoformat(),
'last_seen': datetime.now(timezone.utc).isoformat(),
}
else:
user = {**existing}
user['email'] = email or existing.get('email', '')
user['name'] = name or existing.get('name', '')
user['last_seen'] = datetime.now(timezone.utc).isoformat()
if role is not None:
user['role'] = role
users[user_id] = user
_save(users)
return user
def list_users() -> list:
users = _load()
return sorted(users.values(), key=lambda u: u.get('last_seen', ''), reverse=True)
def set_role(user_id: str, role: str) -> Optional[Dict]:
if role not in ('user', 'admin'):
return None
users = _load()
if user_id not in users:
return None
users[user_id]['role'] = role
_save(users)
return users[user_id]
def set_active(user_id: str, active: bool) -> Optional[Dict]:
users = _load()
if user_id not in users:
return None
users[user_id]['active'] = active
_save(users)
return users[user_id]