import os import psycopg2 from psycopg2.extras import RealDictCursor from werkzeug.security import generate_password_hash, check_password_hash from dotenv import load_dotenv # Load environment variables dari .env load_dotenv() DATABASE_URL = os.getenv("DATABASE_URL") def get_db_connection(): """ Mengambil koneksi database PostgreSQL (Supabase). """ if not DATABASE_URL or "YOUR_PASSWORD_HERE" in DATABASE_URL or "YOUR_HOST_HERE" in DATABASE_URL: raise ValueError("DATABASE_URL belum dikonfigurasi dengan benar di file .env") conn = psycopg2.connect(DATABASE_URL) return conn def init_db(): """ Menginisialisasi tabel database PostgreSQL jika belum ada. """ conn = None try: conn = get_db_connection() with conn.cursor() as cur: # 1. Buat tabel users cur.execute(""" CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, username VARCHAR(100) UNIQUE NOT NULL, password_hash TEXT NOT NULL ); """) # 2. Buat tabel analyses cur.execute(""" CREATE TABLE IF NOT EXISTS analyses ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, filename VARCHAR(255) NOT NULL, text_column VARCHAR(255) NOT NULL, timestamp TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, positive_count INTEGER DEFAULT 0, neutral_count INTEGER DEFAULT 0, negative_count INTEGER DEFAULT 0, wordcloud_base64 TEXT, results_table_html TEXT, results_csv TEXT ); """) # Jalankan migrasi kolom jika tabel sudah ada sebelumnya cur.execute(""" ALTER TABLE analyses ADD COLUMN IF NOT EXISTS results_csv TEXT; """) conn.commit() print("Database initialized successfully.") except Exception as e: print(f"Error initializing database: {e}") if conn: conn.rollback() raise e finally: if conn: conn.close() # --- Manajemen Pengguna --- def create_user(username, password): """ Membuat pengguna baru dengan password yang di-hash. Mengembalikan ID pengguna jika sukses, atau None jika username sudah ada. """ conn = None try: conn = get_db_connection() hashed_password = generate_password_hash(password) with conn.cursor() as cur: cur.execute( "INSERT INTO users (username, password_hash) VALUES (%s, %s) RETURNING id;", (username, hashed_password) ) user_id = cur.fetchone()[0] conn.commit() return user_id except psycopg2.errors.UniqueViolation: if conn: conn.rollback() return None except Exception as e: if conn: conn.rollback() print(f"Error creating user: {e}") raise e finally: if conn: conn.close() def check_user_credentials(username, password): """ Memeriksa kredensial pengguna. Mengembalikan dict user jika valid, else None. """ conn = None try: conn = get_db_connection() with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute("SELECT * FROM users WHERE username = %s;", (username,)) user = cur.fetchone() if user and check_password_hash(user['password_hash'], password): return user return None except Exception as e: print(f"Error checking user credentials: {e}") return None finally: if conn: conn.close() def get_user_by_id(user_id): """ Mengambil data pengguna berdasarkan ID. """ conn = None try: conn = get_db_connection() with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute("SELECT id, username FROM users WHERE id = %s;", (user_id,)) return cur.fetchone() except Exception as e: print(f"Error getting user by ID: {e}") return None finally: if conn: conn.close() # --- Manajemen Riwayat Analisis --- def save_analysis(user_id, filename, text_column, positive_count, neutral_count, negative_count, wordcloud_base64, results_table_html, results_csv=None): """ Menyimpan riwayat analisis file ke database Supabase. """ conn = None try: conn = get_db_connection() with conn.cursor() as cur: cur.execute( """ INSERT INTO analyses ( user_id, filename, text_column, positive_count, neutral_count, negative_count, wordcloud_base64, results_table_html, results_csv ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id; """, (user_id, filename, text_column, positive_count, neutral_count, negative_count, wordcloud_base64, results_table_html, results_csv) ) analysis_id = cur.fetchone()[0] conn.commit() return analysis_id except Exception as e: if conn: conn.rollback() print(f"Error saving analysis: {e}") raise e finally: if conn: conn.close() def get_user_analyses(user_id): """ Mengambil seluruh daftar riwayat analisis milik user (tanpa text besar). """ conn = None try: conn = get_db_connection() with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute( """ SELECT id, filename, text_column, timestamp, positive_count, neutral_count, negative_count FROM analyses WHERE user_id = %s ORDER BY timestamp DESC; """, (user_id,) ) return cur.fetchall() except Exception as e: print(f"Error getting user analyses: {e}") return [] finally: if conn: conn.close() def get_analysis_detail(analysis_id, user_id): """ Mengambil detail lengkap analisis (termasuk wordcloud & tabel html). Memastikan data hanya bisa diakses oleh pemiliknya. """ conn = None try: conn = get_db_connection() with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute( """ SELECT * FROM analyses WHERE id = %s AND user_id = %s; """, (analysis_id, user_id) ) return cur.fetchone() except Exception as e: print(f"Error getting analysis detail: {e}") return None finally: if conn: conn.close() def delete_analysis(analysis_id, user_id): """ Menghapus riwayat analisis tertentu milik user. """ conn = None try: conn = get_db_connection() with conn.cursor() as cur: cur.execute( "DELETE FROM analyses WHERE id = %s AND user_id = %s;", (analysis_id, user_id) ) conn.commit() return True except Exception as e: if conn: conn.rollback() print(f"Error deleting analysis: {e}") return False finally: if conn: conn.close()