Consolidate 3 separate applications (reports-app, data-entry-app, telegram-bot) into a unified
architecture with single backend and frontend:
Backend Changes:
- Unified FastAPI backend at backend/ with modular structure
- Modules: reports, data_entry, telegram in backend/modules/
- Centralized config.py and main.py with all routers registered
- Single worker mode (--workers 1) for Telegram bot compatibility
- Shared Oracle connection pool and JWT authentication
- Unified requirements.txt and environment configuration
Frontend Changes:
- Single Vue.js SPA with module-based routing
- Unified frontend at src/ with modules in src/modules/{reports,data-entry}/
- Shared components and stores in src/shared/
- Error boundaries for module isolation
- Dual API proxy in Vite for module communication
Infrastructure:
- New unified startup scripts: start-prod.sh, start-test.sh, start-backend.sh
- Environment templates: .env.dev.example, .env.test.example, .env.prod.example
- Updated deployment scripts for Windows IIS
- Simplified SSH tunnel management
Documentation:
- Comprehensive CLAUDE.md with architecture overview
- Module-specific docs in docs/{data-entry,telegram}/
- Architecture decision records in docs/ARCHITECTURE-DECISIONS.md
- Deployment guides consolidated in deployment/windows/docs/
This migration reduces complexity, improves maintainability, and enables easier
deployment while maintaining all existing functionality.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
351 lines
11 KiB
Python
351 lines
11 KiB
Python
"""
|
|
Authentication and User Linking Logic
|
|
|
|
This module handles the linking process between Telegram users and Oracle ERP accounts.
|
|
It manages authentication codes, verifies users through the backend API, and maintains
|
|
user sessions with JWT tokens.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Optional, Dict, Any
|
|
from datetime import datetime, timedelta
|
|
|
|
from telegram import User as TelegramUser
|
|
|
|
from backend.modules.telegram.db.operations import (
|
|
get_user,
|
|
create_or_update_user,
|
|
link_user_to_oracle,
|
|
update_user_tokens,
|
|
verify_and_use_auth_code,
|
|
is_user_linked
|
|
)
|
|
from backend.modules.telegram.api.client import get_backend_client
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def link_telegram_account(
|
|
telegram_user: TelegramUser,
|
|
auth_code: str
|
|
) -> Optional[Dict[str, Any]]:
|
|
"""
|
|
Link a Telegram account to an Oracle ERP account using an authentication code.
|
|
|
|
Flow:
|
|
1. Verify auth code in database (check exists, not used, not expired)
|
|
2. Extract oracle_username from code
|
|
3. Call backend API to verify user in Oracle and get JWT token
|
|
4. Create/update Telegram user record
|
|
5. Link user to Oracle account with JWT tokens
|
|
6. Return success with user data
|
|
|
|
Args:
|
|
telegram_user: Telegram User object from python-telegram-bot
|
|
auth_code: 8-character authentication code from web frontend
|
|
|
|
Returns:
|
|
Dict with:
|
|
- success: True if linking succeeded
|
|
- username: Oracle username
|
|
- jwt_token: JWT access token
|
|
- companies: List of companies user has access to
|
|
OR None if linking failed
|
|
|
|
Example:
|
|
result = await link_telegram_account(telegram_user, "ABC12345")
|
|
if result:
|
|
print(f"Linked to {result['username']}")
|
|
else:
|
|
print("Linking failed")
|
|
"""
|
|
try:
|
|
telegram_user_id = telegram_user.id
|
|
telegram_username = telegram_user.username
|
|
first_name = telegram_user.first_name
|
|
last_name = telegram_user.last_name
|
|
|
|
logger.info(
|
|
f"Attempting to link Telegram user {telegram_user_id} "
|
|
f"(@{telegram_username}) with code {auth_code}"
|
|
)
|
|
|
|
# Step 1: Verify auth code
|
|
code_data = await verify_and_use_auth_code(auth_code)
|
|
|
|
if not code_data:
|
|
logger.warning(f"Invalid, expired, or already used auth code: {auth_code}")
|
|
return None
|
|
|
|
oracle_username = code_data.get('oracle_username')
|
|
logger.info(f"Auth code valid for Oracle user: {oracle_username}")
|
|
|
|
# Step 2: Create/update Telegram user record (basic info)
|
|
user_created = await create_or_update_user(
|
|
telegram_user_id=telegram_user_id,
|
|
username=telegram_username,
|
|
first_name=first_name,
|
|
last_name=last_name
|
|
)
|
|
|
|
if not user_created:
|
|
logger.error(f"Failed to create/update Telegram user {telegram_user_id}")
|
|
return None
|
|
|
|
# Step 3: Verify user in Oracle and get JWT token via backend API (auto-linking flow)
|
|
backend_client = get_backend_client()
|
|
async with backend_client:
|
|
user_data = await backend_client.verify_user(
|
|
oracle_username=oracle_username,
|
|
linking_code=auth_code
|
|
)
|
|
|
|
if not user_data or not user_data.get('success'):
|
|
logger.error(f"Failed to verify Oracle user {oracle_username} via backend")
|
|
return None
|
|
|
|
# Extract tokens and user info from response
|
|
jwt_token = user_data.get('access_token')
|
|
jwt_refresh_token = user_data.get('refresh_token', jwt_token)
|
|
user_info = user_data.get('user', {})
|
|
companies = user_info.get('companies', [])
|
|
permissions = user_info.get('permissions', [])
|
|
|
|
# Token expiration (typically 30 minutes for access token)
|
|
token_expires_at = datetime.now() + timedelta(minutes=30)
|
|
|
|
# Step 4: Link Telegram user to Oracle account
|
|
linked = await link_user_to_oracle(
|
|
telegram_user_id=telegram_user_id,
|
|
oracle_username=oracle_username,
|
|
jwt_token=jwt_token,
|
|
jwt_refresh_token=jwt_refresh_token,
|
|
token_expires_at=token_expires_at
|
|
)
|
|
|
|
if not linked:
|
|
logger.error(f"Failed to link user {telegram_user_id} to Oracle account")
|
|
return None
|
|
|
|
logger.info(
|
|
f"Successfully linked Telegram user {telegram_user_id} "
|
|
f"to Oracle user {oracle_username}"
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"telegram_user_id": telegram_user_id,
|
|
"username": oracle_username,
|
|
"jwt_token": jwt_token,
|
|
"jwt_refresh_token": jwt_refresh_token,
|
|
"companies": companies,
|
|
"permissions": permissions,
|
|
"linked_at": datetime.now().isoformat()
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error linking Telegram account: {e}", exc_info=True)
|
|
return None
|
|
|
|
|
|
async def get_user_auth_data(telegram_user_id: int) -> Optional[Dict[str, Any]]:
|
|
"""
|
|
Get authentication data for a linked Telegram user.
|
|
|
|
This function retrieves the user's Oracle account information and JWT tokens.
|
|
If the token is expired, it automatically refreshes it.
|
|
|
|
Args:
|
|
telegram_user_id: Telegram user ID
|
|
|
|
Returns:
|
|
Dict with:
|
|
- telegram_user_id: Telegram user ID
|
|
- username: Oracle username
|
|
- jwt_token: Valid JWT access token (refreshed if needed)
|
|
- jwt_refresh_token: JWT refresh token
|
|
- companies: List of companies (fetched if not cached)
|
|
OR None if user is not linked or error occurred
|
|
|
|
Example:
|
|
auth_data = await get_user_auth_data(12345)
|
|
if auth_data:
|
|
jwt = auth_data['jwt_token']
|
|
# Use JWT for API calls
|
|
"""
|
|
try:
|
|
# Get user from database
|
|
user_data = await get_user(telegram_user_id)
|
|
|
|
if not user_data:
|
|
logger.warning(f"User {telegram_user_id} not found in database")
|
|
return None
|
|
|
|
if not user_data.get('oracle_username'):
|
|
logger.warning(f"User {telegram_user_id} is not linked to Oracle account")
|
|
return None
|
|
|
|
oracle_username = user_data['oracle_username']
|
|
jwt_token = user_data['jwt_token']
|
|
jwt_refresh_token = user_data['jwt_refresh_token']
|
|
token_expires_at_str = user_data['token_expires_at']
|
|
|
|
# Parse token expiration
|
|
token_expires_at = datetime.fromisoformat(token_expires_at_str) if token_expires_at_str else None
|
|
|
|
# Check if token is expired or about to expire (< 5 minutes remaining)
|
|
token_expired = (
|
|
token_expires_at is None or
|
|
datetime.now() >= token_expires_at - timedelta(minutes=5)
|
|
)
|
|
|
|
if token_expired:
|
|
logger.info(f"Token expired for user {telegram_user_id}, refreshing...")
|
|
|
|
# Refresh token via backend API
|
|
backend_client = get_backend_client()
|
|
async with backend_client:
|
|
new_token = await backend_client.refresh_token(jwt_refresh_token)
|
|
|
|
if new_token:
|
|
# Update token in database
|
|
new_expires_at = datetime.now() + timedelta(minutes=30)
|
|
await update_user_tokens(
|
|
telegram_user_id=telegram_user_id,
|
|
jwt_token=new_token,
|
|
jwt_refresh_token=jwt_refresh_token, # Keep same refresh token
|
|
token_expires_at=new_expires_at
|
|
)
|
|
|
|
jwt_token = new_token
|
|
logger.info(f"Token refreshed for user {telegram_user_id}")
|
|
else:
|
|
logger.error(f"Failed to refresh token for user {telegram_user_id}")
|
|
return None
|
|
|
|
# Fetch user companies (fresh from backend)
|
|
backend_client = get_backend_client()
|
|
async with backend_client:
|
|
companies = await backend_client.get_user_companies(jwt_token)
|
|
|
|
return {
|
|
"telegram_user_id": telegram_user_id,
|
|
"username": oracle_username,
|
|
"jwt_token": jwt_token,
|
|
"jwt_refresh_token": jwt_refresh_token,
|
|
"companies": companies
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting user auth data: {e}", exc_info=True)
|
|
return None
|
|
|
|
|
|
async def check_user_linked(telegram_user_id: int) -> bool:
|
|
"""
|
|
Check if a Telegram user is linked to an Oracle account.
|
|
|
|
Args:
|
|
telegram_user_id: Telegram user ID
|
|
|
|
Returns:
|
|
bool: True if user is linked, False otherwise
|
|
|
|
Example:
|
|
if await check_user_linked(12345):
|
|
print("User is linked")
|
|
else:
|
|
print("User needs to link account")
|
|
"""
|
|
try:
|
|
return await is_user_linked(telegram_user_id)
|
|
except Exception as e:
|
|
logger.error(f"Error checking if user is linked: {e}")
|
|
return False
|
|
|
|
|
|
async def get_user_companies(telegram_user_id: int) -> Optional[list]:
|
|
"""
|
|
Get list of companies a user has access to.
|
|
|
|
This is a convenience function that fetches user auth data and returns
|
|
just the companies list.
|
|
|
|
Args:
|
|
telegram_user_id: Telegram user ID
|
|
|
|
Returns:
|
|
List of company dicts, or None if user not linked
|
|
|
|
Example:
|
|
companies = await get_user_companies(12345)
|
|
if companies:
|
|
for company in companies:
|
|
print(f"{company['id']}: {company['nume_firma']}")
|
|
"""
|
|
try:
|
|
auth_data = await get_user_auth_data(telegram_user_id)
|
|
|
|
if auth_data:
|
|
return auth_data.get('companies', [])
|
|
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting user companies: {e}")
|
|
return None
|
|
|
|
|
|
async def unlink_user(telegram_user_id: int) -> bool:
|
|
"""
|
|
Unlink a Telegram user from their Oracle account.
|
|
|
|
This removes the linking but keeps the Telegram user record.
|
|
Used for account disconnection or security purposes.
|
|
|
|
Args:
|
|
telegram_user_id: Telegram user ID
|
|
|
|
Returns:
|
|
bool: True if successfully unlinked
|
|
|
|
Example:
|
|
if await unlink_user(12345):
|
|
print("Account unlinked")
|
|
"""
|
|
try:
|
|
# Set Oracle username and tokens to NULL
|
|
from backend.modules.telegram.db.database import DB_PATH
|
|
import aiosqlite
|
|
|
|
async with aiosqlite.connect(DB_PATH) as db:
|
|
db.row_factory = aiosqlite.Row
|
|
await db.execute("""
|
|
UPDATE telegram_users
|
|
SET oracle_username = NULL,
|
|
jwt_token = NULL,
|
|
jwt_refresh_token = NULL,
|
|
token_expires_at = NULL,
|
|
linked_at = NULL
|
|
WHERE telegram_user_id = ?
|
|
""", (telegram_user_id,))
|
|
|
|
await db.commit()
|
|
|
|
logger.info(f"User {telegram_user_id} unlinked from Oracle account")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error unlinking user {telegram_user_id}: {e}")
|
|
return False
|
|
|
|
|
|
# Export main functions
|
|
__all__ = [
|
|
'link_telegram_account',
|
|
'get_user_auth_data',
|
|
'check_user_linked',
|
|
'get_user_companies',
|
|
'unlink_user'
|
|
]
|