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>
96 lines
2.5 KiB
Python
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]
|