import os import json from flask import Flask, request, g, Response, render_template, redirect, url_for, flash, session # Thêm 'session' import psycopg2 from dotenv import load_dotenv from werkzeug.security import generate_password_hash, check_password_hash from functools import wraps # Tải các biến môi trường từ file .env (chỉ cho môi trường phát triển cục bộ) load_dotenv() app = Flask(__name__) # Cấu hình kết nối cơ sở dữ liệu từ biến môi trường DB_HOST = os.getenv('DB_HOST') DB_NAME = os.getenv('DB_NAME') DB_USER = os.getenv('DB_USER') DB_PASSWORD = os.getenv('DB_PASSWORD') DB_PORT = os.getenv('DB_PORT') # Biến môi trường cho mật khẩu trang PAGE_PASSWORD = os.getenv('PAGE_PASSWORD') # Cấu hình secret key cho Flask (CẦN THIẾT cho flash messages và session) # Trong môi trường production, hãy tạo một chuỗi ngẫu nhiên mạnh và lưu vào biến môi trường FLASK_SECRET_KEY app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'mot_chuoi_bi_mat_rat_ngau_nhien_va_dai') if not PAGE_PASSWORD: print("Cảnh báo: Biến môi trường 'PAGE_PASSWORD' chưa được đặt. Một số chức năng có thể không hoạt động đúng.") # --- Hàm tiện ích để quản lý kết nối cơ sở dữ liệu --- def get_db_connection(): if 'db' not in g: try: g.db = psycopg2.connect( host=DB_HOST, database=DB_NAME, user=DB_USER, password=DB_PASSWORD, port=DB_PORT ) print("Kết nối đến Supabase PostgreSQL thành công!") except Exception as e: print(f"Lỗi khi kết nối đến cơ sở dữ liệu: {e}") g.db = None return g.db @app.teardown_appcontext def close_db_connection(exception): db = g.pop('db', None) if db is not None: db.close() print("Đã đóng kết nối Supabase PostgreSQL.") # --- Hàm tiện ích để trả về JSON với tiếng Việt (giữ cho các API nếu cần) --- def custom_jsonify(data, status_code=200): response = Response( response=json.dumps(data, ensure_ascii=False), status=status_code, mimetype='application/json; charset=utf-8' ) return response # --- Decorator để bảo vệ các trang bằng mật khẩu --- def require_page_password(f): @wraps(f) def decorated_function(*args, **kwargs): # Kiểm tra session trước if session.get('page_logged_in'): return f(*args, **kwargs) # Nếu chưa đăng nhập qua session, kiểm tra mật khẩu từ form/query/header provided_password = request.form.get('page_password') or request.args.get('page_password') if request.method == 'POST' and not provided_password: provided_password = request.headers.get('X-Page-Password') if not PAGE_PASSWORD: flash("Lỗi cấu hình server: Mật khẩu trang chưa được thiết lập.", "error") return redirect(url_for('home')) if provided_password == PAGE_PASSWORD: session['page_logged_in'] = True # Đặt session là đã đăng nhập # Để tránh lỗi chuyển hướng lại chính trang đó với POST, # hãy chuyển hướng đến chính URL hiện tại nhưng là GET request return redirect(request.url) else: flash("Truy cập bị từ chối: Mật khẩu trang không hợp lệ.", "error") return render_template('page_password_form.html', next_url=request.url) # Hiển thị form mật khẩu riêng return decorated_function # --- Tuyến đường mới để hiển thị form mật khẩu trang và xử lý đăng nhập --- @app.route('/page_password', methods=['GET', 'POST']) def page_password_form(): next_url = request.args.get('next') or url_for('home') # Lấy URL gốc muốn truy cập sau khi nhập mật khẩu if request.method == 'POST': provided_password = request.form.get('page_password_input') # Lấy từ form riêng này if not PAGE_PASSWORD: flash("Lỗi cấu hình server: Mật khẩu trang chưa được thiết lập.", "error") return redirect(url_for('home')) if provided_password == PAGE_PASSWORD: session['page_logged_in'] = True flash("Mật khẩu trang đã được xác thực thành công!", "success") return redirect(next_url) else: flash("Mật khẩu trang không hợp lệ. Vui lòng thử lại.", "error") return render_template('page_password_form.html', next_url=next_url) # Nếu là GET request, hiển thị form mật khẩu return render_template('page_password_form.html', next_url=next_url) @app.route('/page_logout') def page_logout(): session.pop('page_logged_in', None) flash("Bạn đã đăng xuất khỏi phiên mật khẩu trang.", "info") return redirect(url_for('home')) # --- Các tuyến đường (Routes) của ứng dụng --- @app.route('/') def home(): """Trang chủ của ứng dụng.""" return render_template('index.html') # Áp dụng decorator cho các trang cần bảo vệ @app.route('/test_db', methods=['GET']) # Đổi thành GET @require_page_password # Áp dụng bảo vệ def test_db(): """Kiểm tra kết nối đến cơ sở dữ liệu và hiển thị kết quả.""" conn = get_db_connection() db_version_info = "Chưa kiểm tra" status_info = "none" if conn is None: flash("Không thể kết nối đến cơ sở dữ liệu.", "error") db_version_info = "Lỗi kết nối" status_info = "error" else: try: cur = conn.cursor() cur.execute("SELECT version();") db_version = cur.fetchone()[0] cur.close() flash("Kết nối cơ sở dữ liệu thành công!", "success") db_version_info = db_version status_info = "success" except Exception as e: flash(f"Lỗi truy vấn cơ sở dữ liệu: {e}", "error") db_version_info = "Lỗi truy vấn" status_info = "error" return render_template('test_db.html', db_version=db_version_info, status=status_info) @app.route('/create_table', methods=['GET']) # Đổi thành GET @require_page_password # Áp dụng bảo vệ def create_table(): """Tạo bảng 'users' nếu chưa tồn tại và hiển thị kết quả.""" conn = get_db_connection() message_info = "Chưa tạo" status_info = "none" if conn is None: flash("Không thể kết nối đến cơ sở dữ liệu.", "error") message_info = "Lỗi kết nối" status_info = "error" else: try: cur = conn.cursor() cur.execute(""" CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY음을 PRIMARY KEY, name VARCHAR(100) NOT NULL, email VARCHAR(100) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL ); """) conn.commit() cur.close() flash("Bảng 'users' đã được tạo (nếu chưa tồn tại) với cột mật khẩu.", "success") message_info = "Tạo bảng thành công" status_info = "success" except Exception as e: conn.rollback() flash(f"Lỗi khi tạo bảng: {e}", "error") message_info = "Lỗi tạo bảng" status_info = "error" return render_template('create_table.html', message=message_info, status=status_info) @app.route('/add_user', methods=['GET', 'POST']) @require_page_password # Áp dụng bảo vệ def add_user(): """Thêm người dùng mới vào cơ sở dữ liệu với mật khẩu đã băm.""" if request.method == 'POST': name = request.form.get('name') email = request.form.get('email') plaintext_password = request.form.get('password') if not name or not email or not plaintext_password: flash("Vui lòng điền đầy đủ Tên, Email và Mật khẩu.", "error") return render_template('add_user.html') conn = get_db_connection() if conn is None: flash("Không thể kết nối đến cơ sở dữ liệu.", "error") return render_template('add_user.html') hashed_password = generate_password_hash(plaintext_password) try: cur = conn.cursor() cur.execute( "INSERT INTO users (name, email, password) VALUES (%s, %s, %s) RETURNING id;", (name, email, hashed_password) ) user_id = cur.fetchone()[0] conn.commit() cur.close() flash(f"Người dùng '{name}' (ID: {user_id}) đã được thêm thành công!", "success") return render_template('add_user.html', added_user_id=user_id) except psycopg2.errors.UniqueViolation: conn.rollback() flash(f"Lỗi: Email '{email}' đã tồn tại. Vui lòng sử dụng email khác.", "error") return render_template('add_user.html') except Exception as e: conn.rollback() flash(f"Lỗi khi thêm người dùng: {e}", "error") return render_template('add_user.html') return render_template('add_user.html') @app.route('/get_users', methods=['GET']) # Đổi thành GET @require_page_password # Áp dụng bảo vệ def get_users(): # Đổi tên hàm thành get_users cho rõ ràng """Lấy danh sách tất cả người dùng từ cơ sở dữ liệu.""" conn = get_db_connection() users_list = [] if conn is None: flash("Không thể kết nối đến cơ sở dữ liệu.", "error") else: try: cur = conn.cursor() cur.execute("SELECT id, name, email FROM users;") users_data = cur.fetchall() cur.close() for user in users_data: users_list.append({"id": user[0], "name": user[1], "email": user[2]}) flash("Lấy danh sách người dùng thành công!", "success") except Exception as e: flash(f"Lỗi khi lấy danh sách người dùng: {e}", "error") return render_template('users.html', users=users_list) @app.route('/login', methods=['GET', 'POST']) #@require_page_password # Vẫn bảo vệ việc truy cập vào trang này bằng PAGE_PASSWORD def login(): """Xác thực người dùng bằng email và mật khẩu.""" if request.method == 'POST': email = request.form.get('email') plaintext_password = request.form.get('password') if not email or not plaintext_password: flash("Vui lòng nhập Email và Mật khẩu.", "error") return render_template('login.html') conn = get_db_connection() if conn is None: flash("Không thể kết nối đến cơ sở dữ liệu.", "error") return render_template('login.html') try: cur = conn.cursor() cur.execute("SELECT id, name, password FROM users WHERE email = %s;", (email,)) user_data = cur.fetchone() cur.close() if user_data: user_id, user_name, stored_hashed_password = user_data if check_password_hash(stored_hashed_password, plaintext_password): flash(f"Đăng nhập thành công! Chào mừng, {user_name}!", "success") # Có thể chuyển hướng đến trang dashboard hoặc lưu thông tin phiên đăng nhập return redirect(url_for('home')) # Ví dụ: chuyển về trang chủ sau đăng nhập else: flash("Mật khẩu không đúng. Vui lòng thử lại.", "error") else: flash("Email không tồn tại. Vui lòng kiểm tra lại email.", "error") except Exception as e: flash(f"Lỗi trong quá trình đăng nhập: {e}", "error") return render_template('login.html') @app.route('/solar_flow') @require_page_password # Áp dụng bảo vệ nếu bạn muốn def solar_flow(): """Hiển thị trang giao diện hệ thống điện mặt trời với hoạt hình.""" return render_template('solar_flow.html') # --- Chạy ứng dụng --- if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=int(os.environ.get('PORT', 7860)))