1
0

setup_db.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. import pymysql
  2. import getpass
  3. import os
  4. def run_db_setup():
  5. """
  6. This Python script prompts for passwords securely and executes CREATE USER / GRANT
  7. statements to provision the MySQL server dynamically without config files.
  8. """
  9. print("Welcome to Local Food AI Initial Setup.")
  10. print("WARNING: This will configure your MySQL server. You must know the MySQL root password.\n")
  11. # Automatically fetch passwords for secure CI/CD deployment or fallback for exam/local setup
  12. root_password = os.environ.get("MYSQL_ROOT_PASSWORD", "")
  13. owner_pass = os.environ.get("DB_OWNER_PASS", "BTSai123")
  14. reader_pass = os.environ.get("DB_READER_PASS", "BTSai123")
  15. loader_pass = os.environ.get("DB_LOADER_PASS", "BTSai123")
  16. app_auth_pass = os.environ.get("DB_AUTH_PASS", "BTSai123")
  17. print("\nConnecting as root to configure server...")
  18. try:
  19. # Connect using the local unix socket which allows seamless auth_socket root login on Ubuntu
  20. conn = pymysql.connect(
  21. unix_socket='/var/run/mysqld/mysqld.sock',
  22. user='root',
  23. password=root_password,
  24. autocommit=True
  25. )
  26. cursor = conn.cursor()
  27. except Exception as e:
  28. print(f"Failed to connect: {e}")
  29. return
  30. queries = [
  31. "CREATE DATABASE IF NOT EXISTS food_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;",
  32. # Owner User
  33. f"CREATE USER IF NOT EXISTS 'db_owner'@'%' IDENTIFIED BY '{owner_pass}';",
  34. "GRANT ALL PRIVILEGES ON food_db.* TO 'db_owner'@'%' WITH GRANT OPTION;",
  35. # Reader User
  36. f"CREATE USER IF NOT EXISTS 'db_reader'@'%' IDENTIFIED BY '{reader_pass}';",
  37. "GRANT USAGE ON *.* TO 'db_reader'@'%';",
  38. # Loader User
  39. f"CREATE USER IF NOT EXISTS 'db_loader'@'%' IDENTIFIED BY '{loader_pass}';",
  40. "GRANT USAGE ON *.* TO 'db_loader'@'%';",
  41. "GRANT FILE ON *.* TO 'db_loader'@'%';",
  42. # App Auth User (PoLP)
  43. f"CREATE USER IF NOT EXISTS 'db_app_auth'@'%' IDENTIFIED BY '{app_auth_pass}';",
  44. "GRANT USAGE ON *.* TO 'db_app_auth'@'%';",
  45. "FLUSH PRIVILEGES;"
  46. ]
  47. for q in queries:
  48. print(f"Executing: {q[:60]}...")
  49. cursor.execute(q)
  50. # Now create the table logic safely
  51. print("\nCreating Tables and Granting Table-Specific Access...")
  52. # 1. Users Table
  53. cursor.execute("""
  54. CREATE TABLE IF NOT EXISTS food_db.users (
  55. id INT AUTO_INCREMENT PRIMARY KEY,
  56. username VARCHAR(100) UNIQUE NOT NULL,
  57. password_hash VARCHAR(255) NOT NULL,
  58. email VARCHAR(255) NULL,
  59. search_limit VARCHAR(10) DEFAULT '50',
  60. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  61. ) ENGINE=InnoDB;
  62. """)
  63. # Gracefully add email and search_limit to existing tables if script is re-run
  64. try:
  65. cursor.execute("ALTER TABLE food_db.users ADD COLUMN email VARCHAR(255) NULL;")
  66. except Warning:
  67. pass
  68. except Exception as e:
  69. if 'Duplicate column name' not in str(e):
  70. print(f"Skipped altering users (Email): {e}")
  71. try:
  72. cursor.execute("ALTER TABLE food_db.users ADD COLUMN search_limit VARCHAR(10) DEFAULT '50';")
  73. except Warning:
  74. pass
  75. except Exception as e:
  76. if 'Duplicate column name' not in str(e):
  77. print(f"Skipped altering users (Limit): {e}")
  78. # 1.5 Medical Profiles Table (EAV Migration)
  79. # We drop the old schema to clear constraints, allowing the dynamic structure to take over
  80. cursor.execute("DROP TABLE IF EXISTS food_db.user_profiles;")
  81. cursor.execute("""
  82. CREATE TABLE IF NOT EXISTS food_db.user_health_profiles (
  83. id INT AUTO_INCREMENT PRIMARY KEY,
  84. user_id INT NOT NULL,
  85. illness_health_condition_diet_dislikes_name VARCHAR(100) NOT NULL DEFAULT 'None',
  86. illness_health_condition_diet_dislikes_value VARCHAR(255) NOT NULL DEFAULT 'None',
  87. FOREIGN KEY (user_id) REFERENCES food_db.users(id) ON DELETE CASCADE
  88. ) ENGINE=InnoDB;
  89. """)
  90. # 2. Plates Table (For storing custom combos)
  91. cursor.execute("""
  92. CREATE TABLE IF NOT EXISTS food_db.plates (
  93. id INT AUTO_INCREMENT PRIMARY KEY,
  94. user_id INT NOT NULL,
  95. plate_name VARCHAR(255) NOT NULL,
  96. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  97. FOREIGN KEY (user_id) REFERENCES food_db.users(id) ON DELETE CASCADE
  98. ) ENGINE=InnoDB;
  99. """)
  100. # 3. Plate Items Table (Linking products to a plate natively)
  101. cursor.execute("""
  102. CREATE TABLE IF NOT EXISTS food_db.plate_items (
  103. id INT AUTO_INCREMENT PRIMARY KEY,
  104. plate_id INT NOT NULL,
  105. product_code VARCHAR(50) NOT NULL,
  106. quantity_grams DOUBLE NOT NULL,
  107. FOREIGN KEY (plate_id) REFERENCES food_db.plates(id) ON DELETE CASCADE
  108. ) ENGINE=InnoDB;
  109. """)
  110. # 4. Products Table (Unified)
  111. for i in range(1, 101): # Drop up to 100 partitions just in case
  112. cursor.execute(f"DROP TABLE IF EXISTS food_db.products_{i};")
  113. cursor.execute("DROP VIEW IF EXISTS food_db.products;")
  114. cursor.execute("DROP TABLE IF EXISTS food_db.products;")
  115. cursor.execute("""
  116. CREATE TABLE IF NOT EXISTS food_db.products (
  117. code VARCHAR(50) PRIMARY KEY,
  118. product_name TEXT NULL,
  119. generic_name TEXT NULL,
  120. brands TEXT NULL,
  121. allergens TEXT NULL,
  122. ingredients_text TEXT NULL,
  123. proteins_100g DOUBLE NULL,
  124. fat_100g DOUBLE NULL,
  125. carbohydrates_100g DOUBLE NULL,
  126. sugars_100g DOUBLE NULL,
  127. sodium_100g DOUBLE NULL,
  128. salt_100g DOUBLE NULL,
  129. `energy-kcal_100g` DOUBLE NULL,
  130. `vitamin-a_100g` DOUBLE NULL,
  131. `vitamin-d_100g` DOUBLE NULL,
  132. `vitamin-e_100g` DOUBLE NULL,
  133. `vitamin-k_100g` DOUBLE NULL,
  134. `vitamin-c_100g` DOUBLE NULL,
  135. `vitamin-b1_100g` DOUBLE NULL,
  136. `vitamin-b2_100g` DOUBLE NULL,
  137. `vitamin-pp_100g` DOUBLE NULL,
  138. `vitamin-b6_100g` DOUBLE NULL,
  139. `vitamin-b9_100g` DOUBLE NULL,
  140. `vitamin-b12_100g` DOUBLE NULL,
  141. calcium_100g DOUBLE NULL,
  142. iron_100g DOUBLE NULL,
  143. magnesium_100g DOUBLE NULL,
  144. zinc_100g DOUBLE NULL,
  145. potassium_100g DOUBLE NULL,
  146. cholesterol_100g DOUBLE NULL,
  147. fiber_100g DOUBLE NULL,
  148. FULLTEXT idx_search (product_name, ingredients_text)
  149. ) ENGINE=InnoDB;
  150. """)
  151. # Table Context Grants (PoLP)
  152. # The authenticated app process can handle credentials and now read/write custom plates!
  153. cursor.execute("GRANT SELECT, INSERT, UPDATE, DELETE ON food_db.users TO 'db_app_auth'@'%';")
  154. cursor.execute("GRANT SELECT, INSERT, UPDATE, DELETE ON food_db.user_health_profiles TO 'db_app_auth'@'%';")
  155. cursor.execute("GRANT SELECT, INSERT, UPDATE, DELETE ON food_db.plates TO 'db_app_auth'@'%';")
  156. cursor.execute("GRANT SELECT, INSERT, UPDATE, DELETE ON food_db.plate_items TO 'db_app_auth'@'%';")
  157. # Give the app read privileges on the whole database (including the products view when created)
  158. cursor.execute("GRANT SELECT ON food_db.* TO 'db_app_auth'@'%';")
  159. cursor.execute("GRANT SELECT ON food_db.* TO 'db_reader'@'%';")
  160. cursor.execute("GRANT SELECT, INSERT, UPDATE, DELETE, DROP, CREATE, ALTER, INDEX, CREATE VIEW ON food_db.* TO 'db_loader'@'%';")
  161. cursor.execute("FLUSH PRIVILEGES;")
  162. print("\n✅ Database, Users, and Tables created successfully!")
  163. cursor.close()
  164. conn.close()
  165. print("\n🎉 Setup Complete.")
  166. print("\n!!! IMPORTANT NEXT STEPS !!!")
  167. print("Now, use `mysql_config_editor` to store these passwords locally so the app can use them:")
  168. print(" mysql_config_editor set --login-path=app_reader --host=127.0.0.1 --user=db_reader --password")
  169. print(" mysql_config_editor set --login-path=app_auth --host=127.0.0.1 --user=db_app_auth --password")
  170. if __name__ == "__main__":
  171. run_db_setup()