feat: multi-Oracle server support with runtime switching
Complete implementation of multi-server Oracle database support: Backend: - Multi-pool Oracle with lazy loading per server - Email-to-server cache for automatic server discovery - JWT tokens include server_id claim - /auth/check-identity and /auth/check-email endpoints - /auth/my-servers endpoint for listing user's accessible servers - Server switch with password re-authentication Frontend: - New ServerSelector component for header dropdown - Multi-step login flow (identity → server → password) - Server switching from header with password modal - Mobile drawer menu with server selection - Dark mode support for all new components - URL bookmark support with ?server= query param Scripts: - Unified start.sh replacing start-prod.sh/start-test.sh - Unified ssh-tunnel.sh with multi-server support - Updated status.sh for new architecture Tests: - E2E tests for multi-server and single-server login flows - Backend unit tests for all new endpoints - Oracle multi-pool integration tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,7 @@ async def create_or_update_user(
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
await db.execute("PRAGMA busy_timeout=5000")
|
||||
db.row_factory = aiosqlite.Row
|
||||
await db.execute("""
|
||||
INSERT INTO telegram_users (
|
||||
@@ -77,6 +78,7 @@ async def get_user(telegram_user_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
await db.execute("PRAGMA busy_timeout=5000")
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
SELECT * FROM telegram_users
|
||||
@@ -115,6 +117,7 @@ async def link_user_to_oracle(
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
await db.execute("PRAGMA busy_timeout=5000")
|
||||
db.row_factory = aiosqlite.Row
|
||||
await db.execute("""
|
||||
UPDATE telegram_users
|
||||
@@ -163,6 +166,7 @@ async def update_user_tokens(
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
await db.execute("PRAGMA busy_timeout=5000")
|
||||
db.row_factory = aiosqlite.Row
|
||||
await db.execute("""
|
||||
UPDATE telegram_users
|
||||
@@ -193,6 +197,7 @@ async def update_user_last_active(telegram_user_id: int) -> bool:
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
await db.execute("PRAGMA busy_timeout=5000")
|
||||
db.row_factory = aiosqlite.Row
|
||||
await db.execute("""
|
||||
UPDATE telegram_users
|
||||
@@ -220,6 +225,7 @@ async def is_user_linked(telegram_user_id: int) -> bool:
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
await db.execute("PRAGMA busy_timeout=5000")
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
SELECT oracle_username FROM telegram_users
|
||||
@@ -246,6 +252,7 @@ async def is_user_authenticated(telegram_user_id: int) -> bool:
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
await db.execute("PRAGMA busy_timeout=5000")
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
SELECT oracle_username, jwt_token, token_expires_at
|
||||
@@ -299,6 +306,7 @@ async def create_auth_code(
|
||||
expires_at = datetime.now() + timedelta(minutes=expires_in_minutes)
|
||||
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
await db.execute("PRAGMA busy_timeout=5000")
|
||||
db.row_factory = aiosqlite.Row
|
||||
await db.execute("""
|
||||
INSERT INTO telegram_auth_codes (
|
||||
@@ -328,6 +336,7 @@ async def get_auth_code(code: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
await db.execute("PRAGMA busy_timeout=5000")
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
SELECT * FROM telegram_auth_codes
|
||||
@@ -356,6 +365,7 @@ async def verify_and_use_auth_code(code: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
await db.execute("PRAGMA busy_timeout=5000")
|
||||
db.row_factory = aiosqlite.Row
|
||||
# Check if code exists, is not used, and not expired
|
||||
cursor = await db.execute("""
|
||||
@@ -399,6 +409,7 @@ async def get_pending_codes_for_user(telegram_user_id: int) -> List[Dict[str, An
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
await db.execute("PRAGMA busy_timeout=5000")
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
SELECT * FROM telegram_auth_codes
|
||||
@@ -431,6 +442,7 @@ async def get_pending_email_code(
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
await db.execute("PRAGMA busy_timeout=5000")
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
SELECT code, email, oracle_username, expires_at, failed_attempts
|
||||
@@ -476,6 +488,7 @@ async def create_email_auth_code(
|
||||
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
await db.execute("PRAGMA busy_timeout=5000")
|
||||
db.row_factory = aiosqlite.Row
|
||||
await db.execute("""
|
||||
INSERT INTO email_auth_codes
|
||||
@@ -500,6 +513,7 @@ async def get_email_auth_code(code: str) -> Optional[Dict]:
|
||||
"""Get email auth code details"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
await db.execute("PRAGMA busy_timeout=5000")
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
SELECT code, email, oracle_username, telegram_user_id,
|
||||
@@ -534,6 +548,7 @@ async def increment_failed_attempts(code: str) -> bool:
|
||||
"""Increment failed validation attempts for code"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
await db.execute("PRAGMA busy_timeout=5000")
|
||||
db.row_factory = aiosqlite.Row
|
||||
await db.execute("""
|
||||
UPDATE email_auth_codes
|
||||
@@ -553,6 +568,7 @@ async def mark_email_code_used(code: str) -> bool:
|
||||
"""Mark email code as used"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
await db.execute("PRAGMA busy_timeout=5000")
|
||||
db.row_factory = aiosqlite.Row
|
||||
await db.execute("""
|
||||
UPDATE email_auth_codes
|
||||
@@ -574,6 +590,7 @@ async def delete_user_email_codes(telegram_user_id: int) -> int:
|
||||
"""Delete all email codes for user (cleanup)"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
await db.execute("PRAGMA busy_timeout=5000")
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
DELETE FROM email_auth_codes
|
||||
@@ -616,6 +633,7 @@ async def create_session(
|
||||
expires_at = datetime.now() + timedelta(hours=expires_in_hours)
|
||||
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
await db.execute("PRAGMA busy_timeout=5000")
|
||||
db.row_factory = aiosqlite.Row
|
||||
await db.execute("""
|
||||
INSERT INTO telegram_sessions (
|
||||
@@ -645,6 +663,7 @@ async def get_session(session_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
await db.execute("PRAGMA busy_timeout=5000")
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
SELECT * FROM telegram_sessions
|
||||
@@ -674,6 +693,7 @@ async def get_user_active_session(telegram_user_id: int) -> Optional[Dict[str, A
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
await db.execute("PRAGMA busy_timeout=5000")
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
SELECT * FROM telegram_sessions
|
||||
@@ -709,6 +729,7 @@ async def update_session_state(
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
await db.execute("PRAGMA busy_timeout=5000")
|
||||
db.row_factory = aiosqlite.Row
|
||||
await db.execute("""
|
||||
UPDATE telegram_sessions
|
||||
@@ -738,6 +759,7 @@ async def delete_session(session_id: str) -> bool:
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
await db.execute("PRAGMA busy_timeout=5000")
|
||||
db.row_factory = aiosqlite.Row
|
||||
await db.execute("""
|
||||
DELETE FROM telegram_sessions
|
||||
@@ -765,6 +787,7 @@ async def delete_user_sessions(telegram_user_id: int) -> bool:
|
||||
"""
|
||||
try:
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
await db.execute("PRAGMA busy_timeout=5000")
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
DELETE FROM telegram_sessions
|
||||
|
||||
Reference in New Issue
Block a user