Fix .gitignore and add missing authentication source files

This commit fixes overly broad .gitignore patterns that were excluding
important source code files from version control. Previously, wildcard
patterns like *auth*, *token*, *secret*, *connection*, and *credential*
were excluding ALL files containing these words, including critical
application code.

Changes:
- Updated .gitignore with specific patterns for sensitive config files
  (*.json, *.txt, *.yml, *.yaml extensions only)
- Removed broad wildcards that excluded source code files

Added missing source files:
- shared/auth/ (9 files): Complete authentication system
  - JWT handler, middleware, auth service, models, routes
- reports-app/backend/app/routers/auth.py: Authentication API router
- reports-app/backend/app/auth_middleware_wrapper.py: Middleware wrapper
- reports-app/frontend/src/stores/auth.js: Vue.js auth store
- reports-app/frontend/tests/: E2E tests and fixtures for auth
- reports-app/telegram-bot/app/auth/: Telegram auth linking module
- deployment/windows/scripts/Setup-ClaudeAuth.ps1: Windows deployment script
- security/secrets_scanner.py: Security scanning utility

These files are essential for the application to function and should
have been included in the initial commit.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-25 15:02:28 +03:00
parent 6b13ffa183
commit f42eff71a6
19 changed files with 5035 additions and 21 deletions

View File

@@ -0,0 +1,78 @@
"""
Wrapper pentru AuthenticationMiddleware cu fix pentru endpoint-urile protejate
"""
from fastapi import Request, status
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), '../../../shared'))
from auth.middleware import AuthenticationMiddleware
from auth.models import AuthError
class FixedAuthenticationMiddleware(BaseHTTPMiddleware):
"""
Wrapper pentru AuthenticationMiddleware care aplică fix-ul pentru endpoint-urile protejate
"""
def __init__(self, app, **kwargs):
super().__init__(app)
# Create the original middleware instance without wrapping in BaseHTTPMiddleware
self.auth_middleware = AuthenticationMiddleware(app, **kwargs)
print("[FIXED MIDDLEWARE] FixedAuthenticationMiddleware initialized")
print(f"[FIXED MIDDLEWARE] Original middleware type: {type(self.auth_middleware)}")
async def dispatch(self, request: Request, call_next):
"""
Aplică fix-ul pentru endpoint-urile protejate:
- Returnează 401 pentru căile protejate fără token în loc să seteze request.state
"""
path = request.url.path
print(f"[FIXED MIDDLEWARE] Processing path: {path}")
# Verifică dacă path-ul trebuie exclus
excluded_paths = ["/docs", "/health", "/api/auth/login", "/redoc", "/openapi.json"]
is_excluded = (path == "/" or any(path.startswith(excluded) for excluded in excluded_paths))
print(f"[FIXED MIDDLEWARE] Checking exclusions for {path}")
print(f"[FIXED MIDDLEWARE] Excluded paths: {excluded_paths}")
print(f"[FIXED MIDDLEWARE] Is excluded: {is_excluded}")
if is_excluded:
print(f"[FIXED MIDDLEWARE] Path {path} is excluded, skipping auth")
request.state.user = None
request.state.is_authenticated = False
response = await call_next(request)
return response
# Extrage token-ul
authorization = request.headers.get("Authorization")
print(f"[FIXED MIDDLEWARE] Authorization header: {authorization}")
if not authorization or not authorization.startswith("Bearer "):
print(f"[FIXED MIDDLEWARE] No valid token for protected path {path}, returning 401")
error = AuthError(
error="authentication_required",
error_description="Authentication required",
error_code="AUTH_003"
)
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content=error.dict(),
headers={"WWW-Authenticate": "Bearer"}
)
# Token există, să îl validez prin middleware-ul original
print(f"[FIXED MIDDLEWARE] Token found, delegating to original middleware")
try:
result = await self.auth_middleware.dispatch(request, call_next)
print(f"[FIXED MIDDLEWARE] Original middleware returned: {type(result)}")
print(f"[FIXED MIDDLEWARE] Request state after middleware: user={getattr(request.state, 'user', 'MISSING')}, is_authenticated={getattr(request.state, 'is_authenticated', 'MISSING')}")
return result
except Exception as e:
print(f"[FIXED MIDDLEWARE] Exception in original middleware: {str(e)}")
raise

View File

@@ -0,0 +1,109 @@
"""
API Router pentru autentificare - Wrapper peste shared auth
"""
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import HTTPBearer
import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared'))
from auth.dependencies import get_current_user
from auth.models import LoginRequest, TokenResponse, CurrentUser
from auth.auth_service import auth_service
from pydantic import BaseModel
router = APIRouter()
security = HTTPBearer()
class LogoutResponse(BaseModel):
"""Răspuns pentru logout"""
message: str
success: bool
@router.post("/login", response_model=TokenResponse)
async def login(login_request: LoginRequest):
"""
Autentificare utilizator cu username și parola
Folosește shared auth service pentru validarea credențialelor
și generarea token-urilor JWT
"""
try:
# Folosește shared auth service pentru autentificare
success, token_response, error_message = await auth_service.authenticate_and_create_tokens(
username=login_request.username,
password=login_request.password
)
if not success:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=error_message or "Authentication failed",
headers={"WWW-Authenticate": "Bearer"},
)
return token_response
except HTTPException:
raise # Re-raise HTTP exceptions as-is
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal authentication error"
)
@router.post("/logout", response_model=LogoutResponse)
async def logout(current_user: CurrentUser = Depends(get_current_user)):
"""
Logout utilizator
Pentru moment doar confirmă logout-ul (token-urile JWT nu sunt invalidate server-side)
În viitor poate fi extins cu blacklist de token-uri
"""
try:
return LogoutResponse(
message=f"Utilizatorul {current_user.username} a fost deconectat cu succes",
success=True
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Eroare la logout: {str(e)}"
)
@router.get("/me", response_model=CurrentUser)
async def get_current_user_info(current_user: CurrentUser = Depends(get_current_user)):
"""
Obține informațiile utilizatorului curent
Returnează datele utilizatorului din token-ul JWT
"""
return current_user
@router.post("/refresh")
async def refresh_token(refresh_token: str):
"""
Reîmprospătează token-ul de acces folosind refresh token-ul
Această funcție va fi implementată în viitor pentru gestionarea
completă a ciclului de viață al token-urilor
"""
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Refresh token nu este încă implementat"
)
@router.get("/validate")
async def validate_token(current_user: CurrentUser = Depends(get_current_user)):
"""
Validează token-ul curent
Endpoint util pentru frontend să verifice dacă token-ul este încă valid
"""
return {
"valid": True,
"user": current_user.username,
"companies": current_user.companies,
"message": "Token valid"
}

View File

@@ -0,0 +1,116 @@
import { defineStore } from "pinia";
import { ref, computed } from "vue";
import { apiService } from "../services/api";
export const useAuthStore = defineStore("auth", () => {
// State
const accessToken = ref(localStorage.getItem("access_token"));
const refreshToken = ref(localStorage.getItem("refresh_token"));
const user = ref(JSON.parse(localStorage.getItem("user") || "null"));
const isLoading = ref(false);
const error = ref(null);
// Getters
const isAuthenticated = computed(() => !!accessToken.value);
const currentUser = computed(() => user.value);
// Actions
const login = async (credentials) => {
isLoading.value = true;
error.value = null;
try {
const loginData = {
username: credentials.username,
password: credentials.password,
};
const response = await apiService.post("/auth/login", loginData);
const { access_token, refresh_token, user: userData } = response.data;
accessToken.value = access_token;
refreshToken.value = refresh_token;
user.value = userData;
localStorage.setItem("access_token", access_token);
localStorage.setItem("refresh_token", refresh_token);
localStorage.setItem("user", JSON.stringify(userData));
apiService.defaults.headers.common["Authorization"] = `Bearer ${access_token}`;
return { success: true };
} catch (err) {
error.value = err.response?.data?.detail || "Login failed";
return { success: false, error: error.value };
} finally {
isLoading.value = false;
}
};
const logout = () => {
accessToken.value = null;
refreshToken.value = null;
user.value = null;
error.value = null;
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
localStorage.removeItem("user");
// Note: selected_company is now per-user and persists across logout/login
// It's stored as 'selected_company_${username}' in localStorage
delete apiService.defaults.headers.common["Authorization"];
};
const refreshAccessToken = async () => {
if (!refreshToken.value) {
logout();
return false;
}
try {
const response = await apiService.post("/auth/refresh", {
refresh_token: refreshToken.value,
});
const { access_token } = response.data;
accessToken.value = access_token;
localStorage.setItem("access_token", access_token);
apiService.defaults.headers.common["Authorization"] = `Bearer ${access_token}`;
return true;
} catch (err) {
console.error("Token refresh failed:", err);
logout();
return false;
}
};
const initializeAuth = () => {
if (accessToken.value) {
apiService.defaults.headers.common["Authorization"] = `Bearer ${accessToken.value}`;
}
};
const clearError = () => {
error.value = null;
};
initializeAuth();
return {
accessToken,
refreshToken,
user,
isLoading,
error,
isAuthenticated,
currentUser,
login,
logout,
refreshAccessToken,
initializeAuth,
clearError,
};
});

View File

@@ -0,0 +1,223 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../../page-objects/LoginPage.js';
import { testCredentials } from '../../fixtures/auth.js';
test.describe('Authentication - Login Flow', () => {
let loginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.navigate();
});
test('should display login page correctly', async ({ page }) => {
// Check page title and main elements
await expect(page).toHaveTitle(/ROA Reports/);
// Check login form elements are visible
await expect(page.locator(loginPage.usernameInput)).toBeVisible();
await expect(page.locator(loginPage.passwordInput)).toBeVisible();
await expect(page.locator(loginPage.loginButton)).toBeVisible();
// Check page title
const title = await loginPage.getPageTitle();
expect(title).toContain('ROA Reports');
});
test('should show validation errors for empty fields', async ({ page }) => {
// Check initial state - button should be disabled
expect(await loginPage.isLoginButtonDisabled()).toBe(true);
// Clear any existing content and verify empty state
await page.fill(loginPage.usernameInput, '');
await page.fill(loginPage.passwordInput, '');
await page.click(loginPage.loginCard); // Click outside to trigger validation
// Wait for Vue reactivity
await page.waitForTimeout(100);
// Button should remain disabled with empty fields
expect(await loginPage.isLoginButtonDisabled()).toBe(true);
// Verify form validation classes are applied
const hasInvalidFields = await loginPage.hasInvalidField();
// Note: validation might not show invalid state until user interaction
});
test('should show validation error for empty username', async ({ page }) => {
// Fill only password
await loginPage.fillCredentials('', 'password123');
// Trigger validation by clicking outside
await page.click(loginPage.loginCard);
// Check that login button is disabled
expect(await loginPage.isLoginButtonDisabled()).toBe(true);
});
test('should show validation error for empty password', async ({ page }) => {
// Fill only username
await loginPage.fillCredentials('username', '');
// Trigger validation by clicking outside
await page.click(loginPage.loginCard);
// Check that login button is disabled
expect(await loginPage.isLoginButtonDisabled()).toBe(true);
});
test('should enable login button with valid input', async ({ page: _page }) => {
// Fill both fields
await loginPage.fillCredentials('testuser', 'testpass');
// Check that login button is enabled
expect(await loginPage.isLoginButtonDisabled()).toBe(false);
});
test('should handle invalid credentials', async ({ page }) => {
// Mock the API response for invalid login
await page.route('**/api/auth/login', async route => {
await route.fulfill({
status: 401,
contentType: 'application/json',
body: JSON.stringify({
detail: 'Invalid credentials'
}),
});
});
// Attempt login with invalid credentials
await loginPage.login(testCredentials.invalid.username, testCredentials.invalid.password);
// Wait for response
await loginPage.waitForLoginResult();
// Check that we're still on login page
expect(await loginPage.isOnLoginPage()).toBe(true);
// Check that error message appears (via toast or error div)
// Note: Error might be shown via PrimeVue Toast, so we need to check for toast messages
const toastError = page.locator('.p-toast-message-error');
if (await toastError.isVisible()) {
const errorText = await toastError.textContent();
expect(errorText).toContain('Eroare');
}
});
test('should handle successful login', async ({ page }) => {
// Mock the API response for successful login
await page.route('**/api/auth/login', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
access_token: 'mock_access_token',
refresh_token: 'mock_refresh_token',
user: {
id: 1,
username: 'testuser',
full_name: 'Test User'
}
}),
});
});
// Mock companies endpoint for dashboard
await page.route('**/api/companies', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ code: 'COMP1', name: 'Company 1' },
{ code: 'COMP2', name: 'Company 2' }
]),
});
});
// Attempt login with valid credentials
await loginPage.login(testCredentials.valid.username, testCredentials.valid.password);
// Wait for redirect to dashboard
await page.waitForURL('/dashboard', { timeout: 10000 });
// Check that we're on dashboard page
expect(page.url()).toContain('/dashboard');
});
test('should show loading state during login', async ({ page }) => {
// Mock slow API response
await page.route('**/api/auth/login', async route => {
// Delay the response to see loading state
await new Promise(resolve => setTimeout(resolve, 1000));
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
access_token: 'mock_access_token',
refresh_token: 'mock_refresh_token',
user: { id: 1, username: 'testuser' }
}),
});
});
// Fill credentials and submit
await loginPage.fillCredentials(testCredentials.valid.username, testCredentials.valid.password);
await loginPage.clickLogin();
// Check loading state appears
await expect(page.locator(loginPage.loadingSpinner)).toBeVisible();
// Wait for loading to finish
await page.waitForLoadState('networkidle');
});
test('should handle network errors gracefully', async ({ page }) => {
// Mock network error
await page.route('**/api/auth/login', async route => {
await route.abort('failed');
});
// Attempt login
await loginPage.login(testCredentials.valid.username, testCredentials.valid.password);
// Wait a bit for error handling
await page.waitForTimeout(2000);
// Should still be on login page
expect(await loginPage.isOnLoginPage()).toBe(true);
// Check for error message in toast summary or detail
const toastSummary = page.locator('.p-toast-summary');
const toastDetail = page.locator('.p-toast-detail');
// Check if either summary or detail contains error text
const summaryVisible = await toastSummary.isVisible();
const detailVisible = await toastDetail.isVisible();
if (summaryVisible || detailVisible) {
let errorFound = false;
if (summaryVisible) {
const summaryText = await toastSummary.textContent();
if (summaryText && summaryText.toLowerCase().includes('eroare')) {
errorFound = true;
}
}
if (detailVisible) {
const detailText = await toastDetail.textContent();
if (detailText && detailText.toLowerCase().includes('eroare')) {
errorFound = true;
}
}
expect(errorFound).toBe(true);
}
});
test('should focus username field on page load', async ({ page }) => {
// Check that username field is focused
const focusedElement = await page.locator(':focus');
await expect(focusedElement).toHaveAttribute('id', 'username');
});
});

View File

@@ -0,0 +1,60 @@
export const testCredentials = {
valid: {
username: 'testuser',
password: 'testpass123'
},
invalid: {
username: 'wronguser',
password: 'wrongpass'
},
empty: {
username: '',
password: ''
},
partialValid: {
username: 'testuser',
password: ''
}
};
export const expectedMessages = {
loginSuccess: 'Conectare reușită',
loginError: 'Eroare de conectare',
usernameRequired: 'Numele de utilizator este obligatoriu',
passwordRequired: 'Parola este obligatorie',
invalidCredentials: 'Date de conectare incorecte'
};
export const apiEndpoints = {
login: '/api/auth/login',
logout: '/api/auth/logout',
refresh: '/api/auth/refresh',
user: '/api/auth/user'
};
export const mockApiResponses = {
loginSuccess: {
status: 200,
body: {
access_token: 'mock_access_token',
refresh_token: 'mock_refresh_token',
user: {
id: 1,
username: 'testuser',
full_name: 'Test User'
}
}
},
loginError: {
status: 401,
body: {
detail: 'Invalid credentials'
}
},
unauthorized: {
status: 401,
body: {
detail: 'Not authenticated'
}
}
};

View File

@@ -0,0 +1,350 @@
"""
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 app.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 app.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 app.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'
]