ac-tool/backend/server/auth/user_store.py
Vadym Samoilenko 1b051f4d0d Add per-client category hierarchy, client management, and admin hardcoding
Backend:
- New /api/clients CRUD (create, list, delete, rename)
- dropdowns.py: _load_dropdowns(client_id) — per-client file first, global fallback
- admin.py: per-client dropdown upload/preview/delete endpoints
- ai_command.py: reads sheet's client_id, builds hierarchy from client-specific file
- sheets/manager.py: client_id stored in sheet metadata; get/set_sheet_client_id helpers
- sheets.py: create sheet accepts client_id; PATCH /{id}/client endpoint
- config_runtime.py: CLIENTS_FILE, CLIENTS_DROPDOWNS_DIR, ADMIN_EMAILS list
- user_store.py: bootstrap admin from ADMIN_EMAILS (daveporter + vadymsamoilenko)

Frontend:
- New Client type; SheetMeta gains client_id
- api/clients.ts, stores/useClientStore.ts — client CRUD
- useDropdownStore: re-fetches when client changes (no stale cache)
- SheetPage: client selector in header; fetches per-client categories
- BriefUploadPage: client selector before upload
- AdminClientsPage: create/delete clients, upload per-client .xlsx, preview before apply
- Sidebar: separate admin nav links (Users / Clients / Dropdowns)
- App.tsx: /admin/clients route

Data:
- 4 clients pre-seeded (Adidas, USTUDIO, 3M Colab, Bissell) with custom hierarchy files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 18:56:01 +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 — admin if email is in ADMIN_EMAILS list
default_role = 'admin' if email and email.lower() in server_config.ADMIN_EMAILS 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]