Initial commit: ROA2WEB - FastAPI + Vue.js + Telegram Bot

Modern ERP Reports Application with microservices architecture

Tech Stack:
- Backend: FastAPI + python-oracledb (Oracle DB integration)
- Frontend: Vue.js 3 + PrimeVue + Vite
- Telegram Bot: python-telegram-bot + SQLite
- Infrastructure: Shared database pool, JWT authentication, SSH tunnel

Features:
- FastAPI backend with async Oracle connection pool
- Vue.js 3 responsive frontend with PrimeVue components
- Telegram bot alternative interface
- Microservices architecture with shared components
- Complete deployment support (Linux Docker + Windows IIS)
- Comprehensive testing (Playwright E2E + pytest)

Repository Structure:
- reports-app/ - Main application (backend, frontend, telegram-bot)
- shared/ - Shared components (database pool, auth, utils)
- deployment/ - Deployment scripts (Linux & Windows)
- docs/ - Project documentation
- security/ - Security scanning and git hooks
This commit is contained in:
2025-10-25 14:55:08 +03:00
commit 6b13ffa183
237 changed files with 70035 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
"""
Database module for Telegram Bot
Provides SQLite database operations for:
- User management and Oracle account linking
- Authentication code management
- Conversation session management
"""
from .database import (
init_database,
get_db_connection,
cleanup_expired_codes,
cleanup_expired_sessions,
get_database_stats,
DB_PATH,
)
from .operations import (
# User operations
create_or_update_user,
get_user,
link_user_to_oracle,
update_user_tokens,
update_user_last_active,
is_user_linked,
# Auth code operations
create_auth_code,
get_auth_code,
verify_and_use_auth_code,
get_pending_codes_for_user,
# Session operations
create_session,
get_session,
get_user_active_session,
update_session_state,
delete_session,
delete_user_sessions,
)
__all__ = [
# Database setup
'init_database',
'get_db_connection',
'cleanup_expired_codes',
'cleanup_expired_sessions',
'get_database_stats',
'DB_PATH',
# User operations
'create_or_update_user',
'get_user',
'link_user_to_oracle',
'update_user_tokens',
'update_user_last_active',
'is_user_linked',
# Auth code operations
'create_auth_code',
'get_auth_code',
'verify_and_use_auth_code',
'get_pending_codes_for_user',
# Session operations
'create_session',
'get_session',
'get_user_active_session',
'update_session_state',
'delete_session',
'delete_user_sessions',
]

View File

@@ -0,0 +1,243 @@
"""
SQLite Database Setup for Telegram Bot
This module handles database connection, initialization, and schema creation.
Uses aiosqlite for async SQLite operations.
"""
import aiosqlite
import logging
from pathlib import Path
from datetime import datetime, timedelta
from typing import Optional
logger = logging.getLogger(__name__)
# Database file location
DB_DIR = Path(__file__).parent.parent.parent / "data"
DB_PATH = DB_DIR / "telegram_bot.db"
async def get_db_connection() -> aiosqlite.Connection:
"""
Get a database connection.
Returns:
aiosqlite.Connection: Database connection
"""
conn = await aiosqlite.connect(DB_PATH)
conn.row_factory = aiosqlite.Row # Enable column access by name
return conn
async def init_database() -> None:
"""
Initialize the database and create all tables.
Safe to call multiple times - only creates tables if they don't exist.
"""
try:
# Ensure data directory exists
DB_DIR.mkdir(parents=True, exist_ok=True)
logger.info(f"Database directory: {DB_DIR}")
async with aiosqlite.connect(DB_PATH) as db:
# Enable foreign keys
await db.execute("PRAGMA foreign_keys = ON")
# Create telegram_users table
await db.execute("""
CREATE TABLE IF NOT EXISTS telegram_users (
telegram_user_id INTEGER PRIMARY KEY,
username TEXT,
first_name TEXT NOT NULL,
last_name TEXT,
oracle_username TEXT,
jwt_token TEXT,
jwt_refresh_token TEXT,
token_expires_at TIMESTAMP,
linked_at TIMESTAMP,
last_active_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT 1
)
""")
# Create telegram_auth_codes table
await db.execute("""
CREATE TABLE IF NOT EXISTS telegram_auth_codes (
code TEXT PRIMARY KEY,
telegram_user_id INTEGER,
oracle_username TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
used BOOLEAN DEFAULT 0,
used_at TIMESTAMP,
FOREIGN KEY (telegram_user_id) REFERENCES telegram_users(telegram_user_id)
)
""")
# Create telegram_sessions table
await db.execute("""
CREATE TABLE IF NOT EXISTS telegram_sessions (
session_id TEXT PRIMARY KEY,
telegram_user_id INTEGER NOT NULL,
conversation_state TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
FOREIGN KEY (telegram_user_id) REFERENCES telegram_users(telegram_user_id)
)
""")
# Create indexes for better query performance
await db.execute("""
CREATE INDEX IF NOT EXISTS idx_auth_codes_telegram_user
ON telegram_auth_codes(telegram_user_id)
""")
await db.execute("""
CREATE INDEX IF NOT EXISTS idx_auth_codes_expires
ON telegram_auth_codes(expires_at)
""")
await db.execute("""
CREATE INDEX IF NOT EXISTS idx_sessions_telegram_user
ON telegram_sessions(telegram_user_id)
""")
await db.execute("""
CREATE INDEX IF NOT EXISTS idx_sessions_expires
ON telegram_sessions(expires_at)
""")
await db.commit()
logger.info("Database initialized successfully")
# Log table info
cursor = await db.execute("""
SELECT name FROM sqlite_master
WHERE type='table'
ORDER BY name
""")
tables = await cursor.fetchall()
logger.info(f"Existing tables: {[t[0] for t in tables]}")
except Exception as e:
logger.error(f"Failed to initialize database: {e}")
raise
async def cleanup_expired_codes() -> int:
"""
Delete expired authentication codes from the database.
This should be called periodically (e.g., every hour).
Returns:
int: Number of expired codes deleted
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
DELETE FROM telegram_auth_codes
WHERE expires_at < ?
""", (datetime.now(),))
await db.commit()
deleted = cursor.rowcount
if deleted > 0:
logger.info(f"Cleaned up {deleted} expired auth codes")
return deleted
except Exception as e:
logger.error(f"Failed to cleanup expired codes: {e}")
return 0
async def cleanup_expired_sessions() -> int:
"""
Delete expired sessions from the database.
This should be called periodically (e.g., daily).
Returns:
int: Number of expired sessions deleted
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
DELETE FROM telegram_sessions
WHERE expires_at < ?
""", (datetime.now(),))
await db.commit()
deleted = cursor.rowcount
if deleted > 0:
logger.info(f"Cleaned up {deleted} expired sessions")
return deleted
except Exception as e:
logger.error(f"Failed to cleanup expired sessions: {e}")
return 0
async def get_database_stats() -> dict:
"""
Get database statistics for monitoring.
Returns:
dict: Database statistics
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row
stats = {}
# Count users
cursor = await db.execute("SELECT COUNT(*) FROM telegram_users")
stats['total_users'] = (await cursor.fetchone())[0]
cursor = await db.execute(
"SELECT COUNT(*) FROM telegram_users WHERE is_active = 1"
)
stats['active_users'] = (await cursor.fetchone())[0]
# Count pending codes
cursor = await db.execute("""
SELECT COUNT(*) FROM telegram_auth_codes
WHERE used = 0 AND expires_at > ?
""", (datetime.now(),))
stats['pending_codes'] = (await cursor.fetchone())[0]
# Count active sessions
cursor = await db.execute("""
SELECT COUNT(*) FROM telegram_sessions
WHERE expires_at > ?
""", (datetime.now(),))
stats['active_sessions'] = (await cursor.fetchone())[0]
# Database file size
if DB_PATH.exists():
stats['db_size_mb'] = DB_PATH.stat().st_size / (1024 * 1024)
else:
stats['db_size_mb'] = 0
return stats
except Exception as e:
logger.error(f"Failed to get database stats: {e}")
return {}
# Export main functions
__all__ = [
'get_db_connection',
'init_database',
'cleanup_expired_codes',
'cleanup_expired_sessions',
'get_database_stats',
'DB_PATH',
]

View File

@@ -0,0 +1,591 @@
"""
Database Operations for Telegram Bot
This module provides CRUD operations for:
- telegram_users: Telegram user management and Oracle account linking
- telegram_auth_codes: Authentication code management
- telegram_sessions: Conversation session management
"""
import logging
import uuid
from datetime import datetime, timedelta
from typing import Optional, Dict, Any, List
import aiosqlite
from .database import DB_PATH
logger = logging.getLogger(__name__)
# ============================================================================
# TELEGRAM USERS OPERATIONS
# ============================================================================
async def create_or_update_user(
telegram_user_id: int,
username: Optional[str],
first_name: str,
last_name: Optional[str]
) -> bool:
"""
Create or update a Telegram user record.
Args:
telegram_user_id: Telegram user ID
username: Telegram username (without @)
first_name: User's first name
last_name: User's last name
Returns:
bool: True if successful
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row
await db.execute("""
INSERT INTO telegram_users (
telegram_user_id, username, first_name, last_name, last_active_at
)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(telegram_user_id) DO UPDATE SET
username = excluded.username,
first_name = excluded.first_name,
last_name = excluded.last_name,
last_active_at = excluded.last_active_at
""", (telegram_user_id, username, first_name, last_name, datetime.now()))
await db.commit()
logger.info(f"User {telegram_user_id} created/updated")
return True
except Exception as e:
logger.error(f"Failed to create/update user {telegram_user_id}: {e}")
return False
async def get_user(telegram_user_id: int) -> Optional[Dict[str, Any]]:
"""
Get user information by Telegram user ID.
Args:
telegram_user_id: Telegram user ID
Returns:
Optional[Dict]: User data or None if not found
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
SELECT * FROM telegram_users
WHERE telegram_user_id = ?
""", (telegram_user_id,))
row = await cursor.fetchone()
if row:
return dict(row)
return None
except Exception as e:
logger.error(f"Failed to get user {telegram_user_id}: {e}")
return None
async def link_user_to_oracle(
telegram_user_id: int,
oracle_username: str,
jwt_token: str,
jwt_refresh_token: str,
token_expires_at: datetime
) -> bool:
"""
Link a Telegram user to an Oracle account and save JWT tokens.
Args:
telegram_user_id: Telegram user ID
oracle_username: Oracle username
jwt_token: JWT access token
jwt_refresh_token: JWT refresh token
token_expires_at: Token expiration timestamp
Returns:
bool: True if successful
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row
await db.execute("""
UPDATE telegram_users
SET oracle_username = ?,
jwt_token = ?,
jwt_refresh_token = ?,
token_expires_at = ?,
linked_at = ?,
is_active = 1
WHERE telegram_user_id = ?
""", (
oracle_username,
jwt_token,
jwt_refresh_token,
token_expires_at,
datetime.now(),
telegram_user_id
))
await db.commit()
logger.info(f"User {telegram_user_id} linked to Oracle user {oracle_username}")
return True
except Exception as e:
logger.error(f"Failed to link user {telegram_user_id}: {e}")
return False
async def update_user_tokens(
telegram_user_id: int,
jwt_token: str,
jwt_refresh_token: str,
token_expires_at: datetime
) -> bool:
"""
Update JWT tokens for a user.
Args:
telegram_user_id: Telegram user ID
jwt_token: New JWT access token
jwt_refresh_token: New JWT refresh token
token_expires_at: New token expiration timestamp
Returns:
bool: True if successful
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row
await db.execute("""
UPDATE telegram_users
SET jwt_token = ?,
jwt_refresh_token = ?,
token_expires_at = ?
WHERE telegram_user_id = ?
""", (jwt_token, jwt_refresh_token, token_expires_at, telegram_user_id))
await db.commit()
logger.info(f"Tokens updated for user {telegram_user_id}")
return True
except Exception as e:
logger.error(f"Failed to update tokens for user {telegram_user_id}: {e}")
return False
async def update_user_last_active(telegram_user_id: int) -> bool:
"""
Update the last active timestamp for a user.
Args:
telegram_user_id: Telegram user ID
Returns:
bool: True if successful
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row
await db.execute("""
UPDATE telegram_users
SET last_active_at = ?
WHERE telegram_user_id = ?
""", (datetime.now(), telegram_user_id))
await db.commit()
return True
except Exception as e:
logger.error(f"Failed to update last active for user {telegram_user_id}: {e}")
return False
async def is_user_linked(telegram_user_id: int) -> bool:
"""
Check if a user is linked to an Oracle account.
Args:
telegram_user_id: Telegram user ID
Returns:
bool: True if user is linked
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
SELECT oracle_username FROM telegram_users
WHERE telegram_user_id = ? AND oracle_username IS NOT NULL
""", (telegram_user_id,))
row = await cursor.fetchone()
return row is not None
except Exception as e:
logger.error(f"Failed to check if user {telegram_user_id} is linked: {e}")
return False
# ============================================================================
# AUTHENTICATION CODES OPERATIONS
# ============================================================================
async def create_auth_code(
code: str,
telegram_user_id: int,
oracle_username: str,
expires_in_minutes: int = 5
) -> bool:
"""
Create a new authentication code for linking.
Args:
code: 8-character authentication code
telegram_user_id: Telegram user ID
oracle_username: Oracle username to link
expires_in_minutes: Code expiration time in minutes (default: 5)
Returns:
bool: True if successful
"""
try:
expires_at = datetime.now() + timedelta(minutes=expires_in_minutes)
async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row
await db.execute("""
INSERT INTO telegram_auth_codes (
code, telegram_user_id, oracle_username, expires_at
)
VALUES (?, ?, ?, ?)
""", (code, telegram_user_id, oracle_username, expires_at))
await db.commit()
logger.info(f"Auth code created for user {telegram_user_id}")
return True
except Exception as e:
logger.error(f"Failed to create auth code: {e}")
return False
async def get_auth_code(code: str) -> Optional[Dict[str, Any]]:
"""
Get authentication code information.
Args:
code: 8-character authentication code
Returns:
Optional[Dict]: Code data or None if not found
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
SELECT * FROM telegram_auth_codes
WHERE code = ?
""", (code,))
row = await cursor.fetchone()
if row:
return dict(row)
return None
except Exception as e:
logger.error(f"Failed to get auth code: {e}")
return None
async def verify_and_use_auth_code(code: str) -> Optional[Dict[str, Any]]:
"""
Verify an authentication code and mark it as used.
Args:
code: 8-character authentication code
Returns:
Optional[Dict]: Code data if valid, None if invalid/expired
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row
# Check if code exists, is not used, and not expired
cursor = await db.execute("""
SELECT * FROM telegram_auth_codes
WHERE code = ?
AND used = 0
AND expires_at > ?
""", (code, datetime.now()))
row = await cursor.fetchone()
if not row:
logger.warning(f"Invalid or expired code: {code}")
return None
# Mark code as used
await db.execute("""
UPDATE telegram_auth_codes
SET used = 1, used_at = ?
WHERE code = ?
""", (datetime.now(), code))
await db.commit()
logger.info(f"Auth code {code} verified and used")
return dict(row)
except Exception as e:
logger.error(f"Failed to verify auth code: {e}")
return None
async def get_pending_codes_for_user(telegram_user_id: int) -> List[Dict[str, Any]]:
"""
Get all pending (unused, non-expired) codes for a user.
Args:
telegram_user_id: Telegram user ID
Returns:
List[Dict]: List of pending codes
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
SELECT * FROM telegram_auth_codes
WHERE telegram_user_id = ?
AND used = 0
AND expires_at > ?
ORDER BY created_at DESC
""", (telegram_user_id, datetime.now()))
rows = await cursor.fetchall()
return [dict(row) for row in rows]
except Exception as e:
logger.error(f"Failed to get pending codes for user {telegram_user_id}: {e}")
return []
# ============================================================================
# SESSION OPERATIONS
# ============================================================================
async def create_session(
telegram_user_id: int,
conversation_state: Optional[str] = None,
expires_in_hours: int = 24
) -> Optional[str]:
"""
Create a new conversation session.
Args:
telegram_user_id: Telegram user ID
conversation_state: JSON string of conversation state
expires_in_hours: Session expiration time in hours (default: 24)
Returns:
Optional[str]: Session ID if successful, None otherwise
"""
try:
session_id = str(uuid.uuid4())
expires_at = datetime.now() + timedelta(hours=expires_in_hours)
async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row
await db.execute("""
INSERT INTO telegram_sessions (
session_id, telegram_user_id, conversation_state, expires_at
)
VALUES (?, ?, ?, ?)
""", (session_id, telegram_user_id, conversation_state, expires_at))
await db.commit()
logger.info(f"Session {session_id} created for user {telegram_user_id}")
return session_id
except Exception as e:
logger.error(f"Failed to create session: {e}")
return None
async def get_session(session_id: str) -> Optional[Dict[str, Any]]:
"""
Get session information.
Args:
session_id: Session UUID
Returns:
Optional[Dict]: Session data or None if not found/expired
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
SELECT * FROM telegram_sessions
WHERE session_id = ?
AND expires_at > ?
""", (session_id, datetime.now()))
row = await cursor.fetchone()
if row:
return dict(row)
return None
except Exception as e:
logger.error(f"Failed to get session {session_id}: {e}")
return None
async def get_user_active_session(telegram_user_id: int) -> Optional[Dict[str, Any]]:
"""
Get the most recent active session for a user.
Args:
telegram_user_id: Telegram user ID
Returns:
Optional[Dict]: Session data or None if no active session
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
SELECT * FROM telegram_sessions
WHERE telegram_user_id = ?
AND expires_at > ?
ORDER BY updated_at DESC
LIMIT 1
""", (telegram_user_id, datetime.now()))
row = await cursor.fetchone()
if row:
return dict(row)
return None
except Exception as e:
logger.error(f"Failed to get active session for user {telegram_user_id}: {e}")
return None
async def update_session_state(
session_id: str,
conversation_state: str
) -> bool:
"""
Update the conversation state for a session.
Args:
session_id: Session UUID
conversation_state: JSON string of conversation state
Returns:
bool: True if successful
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row
await db.execute("""
UPDATE telegram_sessions
SET conversation_state = ?,
updated_at = ?
WHERE session_id = ?
""", (conversation_state, datetime.now(), session_id))
await db.commit()
logger.info(f"Session {session_id} state updated")
return True
except Exception as e:
logger.error(f"Failed to update session {session_id}: {e}")
return False
async def delete_session(session_id: str) -> bool:
"""
Delete a session.
Args:
session_id: Session UUID
Returns:
bool: True if successful
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row
await db.execute("""
DELETE FROM telegram_sessions
WHERE session_id = ?
""", (session_id,))
await db.commit()
logger.info(f"Session {session_id} deleted")
return True
except Exception as e:
logger.error(f"Failed to delete session {session_id}: {e}")
return False
async def delete_user_sessions(telegram_user_id: int) -> bool:
"""
Delete all sessions for a user.
Args:
telegram_user_id: Telegram user ID
Returns:
bool: True if successful
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
DELETE FROM telegram_sessions
WHERE telegram_user_id = ?
""", (telegram_user_id,))
await db.commit()
deleted = cursor.rowcount
logger.info(f"Deleted {deleted} sessions for user {telegram_user_id}")
return True
except Exception as e:
logger.error(f"Failed to delete sessions for user {telegram_user_id}: {e}")
return False
# Export all functions
__all__ = [
# User operations
'create_or_update_user',
'get_user',
'link_user_to_oracle',
'update_user_tokens',
'update_user_last_active',
'is_user_linked',
# Auth code operations
'create_auth_code',
'get_auth_code',
'verify_and_use_auth_code',
'get_pending_codes_for_user',
# Session operations
'create_session',
'get_session',
'get_user_active_session',
'update_session_state',
'delete_session',
'delete_user_sessions',
]