File size: 12,800 Bytes
46612cf
3227549
5d0ba21
46612cf
 
3227549
62c419a
76fd41a
46612cf
 
76fd41a
f9bb959
76fd41a
46612cf
 
 
 
 
 
 
62c419a
 
 
5d0ba21
 
62c419a
 
 
 
 
3227549
46612cf
 
 
 
 
 
 
 
 
 
 
 
 
62c419a
46612cf
 
 
 
 
 
 
 
 
5d0ba21
3227549
 
 
 
 
 
 
 
62c419a
 
 
 
5d0ba21
 
 
62c419a
5d0ba21
 
62c419a
 
 
 
 
5d0ba21
62c419a
 
5d0ba21
 
 
 
62c419a
 
5d0ba21
62c419a
 
5d0ba21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3227549
 
 
 
 
62c419a
3227549
5d0ba21
 
 
46612cf
62c419a
5d0ba21
 
 
 
 
 
 
 
62c419a
 
 
 
 
 
5d0ba21
 
62c419a
 
5d0ba21
 
 
 
62c419a
5d0ba21
 
46612cf
62c419a
5d0ba21
 
 
 
 
 
 
 
62c419a
 
 
 
5d0ba21
62c419a
 
 
 
 
 
 
 
5d0ba21
 
62c419a
 
 
5d0ba21
 
 
 
62c419a
 
5d0ba21
46612cf
3227549
62c419a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5d0ba21
 
 
3227549
46612cf
62c419a
46612cf
62c419a
 
 
 
 
 
 
 
 
 
 
 
 
 
46612cf
62c419a
b8cf926
62c419a
 
 
 
 
46612cf
62c419a
 
 
46612cf
62c419a
 
 
 
46612cf
62c419a
 
 
 
 
 
 
 
 
 
 
 
 
 
3227549
62c419a
3227549
62c419a
 
 
76fd41a
ecf93ea
 
 
 
 
 
3227549
f9bb959
3227549
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
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)))