database.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import sqlite3
  2. import os
  3. import logging
  4. import secrets
  5. from datetime import datetime, timedelta
  6. from typing import Optional, Dict, Any
  7. logger = logging.getLogger(__name__)
  8. # Locate db correctly in the same directory
  9. DB_PATH = os.path.join(os.path.dirname(__file__), "localfood.db")
  10. def get_db_connection():
  11. # Enable higher timeout and disable thread checks for FastAPI async compatibility
  12. conn = sqlite3.connect(DB_PATH, timeout=20.0, check_same_thread=False)
  13. conn.row_factory = sqlite3.Row
  14. # Enable Write-Ahead Log (WAL) mode for simultaneous read/write operations
  15. conn.execute('pragma journal_mode=wal')
  16. return conn
  17. def create_tables():
  18. """Initialize the SQLite database with required tables"""
  19. try:
  20. conn = get_db_connection()
  21. cursor = conn.cursor()
  22. # Create users table securely locally
  23. cursor.execute('''
  24. CREATE TABLE IF NOT EXISTS users (
  25. id INTEGER PRIMARY KEY AUTOINCREMENT,
  26. username TEXT UNIQUE NOT NULL,
  27. password_hash TEXT NOT NULL,
  28. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  29. )
  30. ''')
  31. # Create sessions table for database-backed tokens
  32. cursor.execute('''
  33. CREATE TABLE IF NOT EXISTS sessions (
  34. token TEXT PRIMARY KEY,
  35. user_id INTEGER NOT NULL,
  36. expires_at TIMESTAMP NOT NULL,
  37. FOREIGN KEY (user_id) REFERENCES users (id)
  38. )
  39. ''')
  40. # Create localized foods table based on Sprint 5 architecture
  41. cursor.execute('''
  42. CREATE TABLE IF NOT EXISTS foods (
  43. id INTEGER PRIMARY KEY AUTOINCREMENT,
  44. name TEXT NOT NULL,
  45. category TEXT,
  46. base_weight_g REAL DEFAULT 100.0,
  47. calories REAL DEFAULT 0.0,
  48. protein_g REAL DEFAULT 0.0,
  49. fat_g REAL DEFAULT 0.0,
  50. carbs_g REAL DEFAULT 0.0,
  51. fiber_g REAL DEFAULT 0.0,
  52. sugar_g REAL DEFAULT 0.0,
  53. sodium_mg REAL DEFAULT 0.0,
  54. vitamin_a_iu REAL DEFAULT 0.0,
  55. vitamin_c_mg REAL DEFAULT 0.0,
  56. calcium_mg REAL DEFAULT 0.0,
  57. iron_mg REAL DEFAULT 0.0,
  58. potassium_mg REAL DEFAULT 0.0,
  59. cholesterol_mg REAL DEFAULT 0.0,
  60. source TEXT DEFAULT 'System'
  61. )
  62. ''')
  63. # Create index for rapid fuzzy search compatibility
  64. cursor.execute('CREATE INDEX IF NOT EXISTS idx_food_name ON foods(name COLLATE NOCASE)')
  65. conn.commit()
  66. conn.close()
  67. logger.info("Database and tables initialized successfully.")
  68. except Exception as e:
  69. logger.error(f"Error initializing database: {e}")
  70. raise
  71. def get_user_by_username(username: str) -> Optional[Dict[str, Any]]:
  72. """Retrieve user dictionary if they exist"""
  73. try:
  74. conn = get_db_connection()
  75. cursor = conn.cursor()
  76. cursor.execute("SELECT * FROM users WHERE username = ?", (username,))
  77. row = cursor.fetchone()
  78. conn.close()
  79. return dict(row) if row else None
  80. except Exception as e:
  81. logger.error(f"Database error fetching user: {e}")
  82. return None
  83. def create_user(username: str, password_hash: str) -> Optional[int]:
  84. """Creates a user securely. Returns user_id if successful, None if username exists."""
  85. try:
  86. conn = get_db_connection()
  87. cursor = conn.cursor()
  88. cursor.execute(
  89. "INSERT INTO users (username, password_hash) VALUES (?, ?)",
  90. (username, password_hash)
  91. )
  92. user_id = cursor.lastrowid
  93. conn.commit()
  94. conn.close()
  95. return user_id
  96. except sqlite3.IntegrityError:
  97. return None
  98. except Exception as e:
  99. logger.error(f"Database error during user creation: {e}")
  100. raise
  101. def create_session(user_id: int) -> str:
  102. """Create a secure 32-character session token in the DB valid for 24h"""
  103. token = secrets.token_urlsafe(32)
  104. expires_at = datetime.now() + timedelta(hours=24)
  105. try:
  106. conn = get_db_connection()
  107. cursor = conn.cursor()
  108. cursor.execute(
  109. "INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, ?)",
  110. (token, user_id, expires_at)
  111. )
  112. conn.commit()
  113. conn.close()
  114. return token
  115. except Exception as e:
  116. logger.error(f"Error creating session: {e}")
  117. raise
  118. def get_user_from_token(token: str) -> Optional[Dict[str, Any]]:
  119. """Verify a session token and return the associated user data if valid and not expired"""
  120. try:
  121. conn = get_db_connection()
  122. cursor = conn.cursor()
  123. # Find user if token exists and hasn't expired
  124. cursor.execute('''
  125. SELECT users.* FROM users
  126. JOIN sessions ON users.id = sessions.user_id
  127. WHERE sessions.token = ? AND sessions.expires_at > ?
  128. ''', (token, datetime.now()))
  129. row = cursor.fetchone()
  130. conn.close()
  131. return dict(row) if row else None
  132. except Exception as e:
  133. logger.error(f"Database error verifying token: {e}")
  134. return None
  135. def delete_session(token: str):
  136. """Securely remove a session token when the user logs out"""
  137. try:
  138. conn = get_db_connection()
  139. cursor = conn.cursor()
  140. cursor.execute("DELETE FROM sessions WHERE token = ?", (token,))
  141. conn.commit()
  142. conn.close()
  143. except Exception as e:
  144. logger.error(f"Error deleting session: {e}")