From 4b5c89786e3513ef036e523ba9512ab3ece2af45 Mon Sep 17 00:00:00 2001 From: wzj <244142824@qq.com> Date: Tue, 24 Jun 2025 11:06:02 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=96=B0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 351 ++++++++++++++++++++++++++------------ static/styles.css | 364 ++++++++++++++++++++++++++-------------- templates/clients.html | 125 +++++++++----- templates/index.html | 60 +++++++ templates/login.html | 42 +++++ templates/settings.html | 64 +++++++ 6 files changed, 736 insertions(+), 270 deletions(-) create mode 100644 templates/index.html create mode 100644 templates/login.html create mode 100644 templates/settings.html diff --git a/app.py b/app.py index d050648..8f00f32 100644 --- a/app.py +++ b/app.py @@ -1,139 +1,278 @@ -from flask import Flask, render_template, request, jsonify, abort, redirect, url_for +from flask import Flask, render_template, request, jsonify, abort, redirect, url_for, session, flash from functools import wraps import os import subprocess import base64 +import sqlite3 +from werkzeug.security import generate_password_hash, check_password_hash app = Flask(__name__) +app.secret_key = os.urandom(24) -# 配置 +# 数据库配置 +DATABASE = 'config/squid_manager.db' SQUID_PASSWD_FILE = 'config/squid_passwd' -ADMIN_PASSWORD = os.getenv('SQUID_PASSWORD', 'admin123') -class User: - def __init__(self, name, password, is_active=True): - self.name = name - self.password = password - self.is_active = is_active +# 初始化数据库 +def init_db(): + with sqlite3.connect(DATABASE) as conn: + cursor = conn.cursor() + cursor.execute(''' + CREATE TABLE IF NOT EXISTS admin_users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL + ) + ''') + cursor.execute(''' + CREATE TABLE IF NOT EXISTS squid_users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + is_active INTEGER DEFAULT 1 + ) + ''') + cursor.execute(''' + CREATE TABLE IF NOT EXISTS settings ( + id INTEGER PRIMARY KEY DEFAULT 1, + proxy_address TEXT DEFAULT 'proxy.example.com', + proxy_port TEXT DEFAULT '3128', + CONSTRAINT singleton CHECK (id = 1) + ) + ''') + + # 检查是否有管理员用户 + cursor.execute("SELECT COUNT(*) FROM admin_users") + if cursor.fetchone()[0] == 0: + cursor.execute( + "INSERT INTO admin_users (username, password) VALUES (?, ?)", + ('admin', generate_password_hash('admin123')) + + # 检查是否有设置 + cursor.execute("SELECT COUNT(*) FROM settings") + if cursor.fetchone()[0] == 0: + cursor.execute( + "INSERT INTO settings (proxy_address, proxy_port) VALUES (?, ?)", + ('proxy.example.com', '3128')) + + conn.commit() + + # 数据库连接 -def basic_auth_required(f): +def get_db(): + db = sqlite3.connect(DATABASE) + db.row_factory = sqlite3.Row + return db + + +# 登录装饰器 +def login_required(f): @wraps(f) - def decorated(*args, **kwargs): - auth = request.authorization - if not auth or not (auth.username == 'admin' and auth.password == ADMIN_PASSWORD): - return ('Unauthorized', 401, - {'WWW-Authenticate': 'Basic realm="Authorization Required"'}) + def decorated_function(*args, **kwargs): + if 'logged_in' not in session: + return redirect(url_for('login')) return f(*args, **kwargs) - return decorated + return decorated_function -def read_squid_file(): - users = [] - try: - with open(SQUID_PASSWD_FILE, 'r') as f: - for line in f: - line = line.strip() - if not line: - continue - - is_active = not line.startswith('#') - if not is_active: - line = line[1:] - - parts = line.split(':', 1) - if len(parts) == 2: - users.append(User(parts[0], parts[1], is_active)) - except FileNotFoundError: - pass - return users - - -def write_squid_file(users): - with open(SQUID_PASSWD_FILE, 'w') as f: - for user in users: - line = f"{'#' if not user.is_active else ''}{user.name}:{user.password}\n" - f.write(line) - - -def create_user(username, password): - users = read_squid_file() - if any(u.name == username for u in users): - return False - - try: - subprocess.run(['htpasswd', '-b', SQUID_PASSWD_FILE, username, password], check=True) - return True - except subprocess.CalledProcessError: - return False +# 初始化应用 +init_db() @app.route('/') -@basic_auth_required +@login_required def index(): - users = read_squid_file() - return render_template('clients.html', users=users) + db = get_db() + squid_users = db.execute("SELECT * FROM squid_users").fetchall() + settings = db.execute("SELECT * FROM settings WHERE id = 1").fetchone() + db.close() + + return render_template('index.html', + user_count=len(squid_users), + settings=settings) -@app.route('/toggle/', methods=['POST']) -@basic_auth_required -def toggle_user(username): - users = read_squid_file() - for user in users: - if user.name == username: - user.is_active = not user.is_active - break +@app.route('/login', methods=['GET', 'POST']) +def login(): + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] - write_squid_file(users) - return '', 200 + db = get_db() + user = db.execute( + "SELECT * FROM admin_users WHERE username = ?", (username,) + ).fetchone() + db.close() + if user and check_password_hash(user['password'], password): + session['logged_in'] = True + session['username'] = username + return redirect(url_for('index')) + else: + flash('用户名或密码错误', 'error') -@app.route('/delete/', methods=['POST']) -@basic_auth_required -def delete_user(username): - users = [u for u in read_squid_file() if u.name != username] - write_squid_file(users) - return '', 200 - - -@app.route('/save_user', methods=['POST']) -@basic_auth_required -def save_user(): - data = request.get_json() - username = data.get('username') - password = data.get('password') - - if not username or not password: - return jsonify({'error': 'Username and password required'}), 400 - - try: - subprocess.run(['htpasswd', '-b', SQUID_PASSWD_FILE, username, password], check=True) - return '', 200 - except subprocess.CalledProcessError: - return jsonify({'error': 'Failed to update password'}), 500 - - -@app.route('/create_user', methods=['POST']) -@basic_auth_required -def handle_create_user(): - data = request.get_json() - username = data.get('username') - password = data.get('password') - - if not username or not password: - return jsonify({'error': 'Username and password required'}), 400 - - if create_user(username, password): - return '', 200 - else: - return jsonify({'error': 'User already exists'}), 409 + return render_template('login.html') @app.route('/logout') def logout(): - return ('', 401, {'WWW-Authenticate': 'Basic realm="Authorization Required"'}) + session.clear() + return redirect(url_for('login')) + + +@app.route('/clients') +@login_required +def clients(): + db = get_db() + users = db.execute("SELECT * FROM squid_users").fetchall() + db.close() + return render_template('clients.html', users=users) + + +@app.route('/settings', methods=['GET', 'POST']) +@login_required +def settings(): + db = get_db() + + if request.method == 'POST': + action = request.form.get('action') + + if action == 'change_password': + current_password = request.form['current_password'] + new_password = request.form['new_password'] + confirm_password = request.form['confirm_password'] + + user = db.execute( + "SELECT * FROM admin_users WHERE username = ?", (session['username'],) + ).fetchone() + + if not check_password_hash(user['password'], current_password): + flash('当前密码不正确', 'error') + elif new_password != confirm_password: + flash('新密码不匹配', 'error') + else: + db.execute( + "UPDATE admin_users SET password = ? WHERE username = ?", + (generate_password_hash(new_password), session['username']) + ) + db.commit() + flash('密码修改成功', 'success') + + elif action == 'update_proxy': + proxy_address = request.form['proxy_address'] + proxy_port = request.form['proxy_port'] + + db.execute( + "UPDATE settings SET proxy_address = ?, proxy_port = ? WHERE id = 1", + (proxy_address, proxy_port) + ) + db.commit() + flash('代理设置更新成功', 'success') + + settings = db.execute("SELECT * FROM settings WHERE id = 1").fetchone() + db.close() + + return render_template('settings.html', settings=settings) + + +# Squid用户管理API +@app.route('/api/toggle_user/', methods=['POST']) +@login_required +def toggle_user(user_id): + db = get_db() + user = db.execute("SELECT is_active FROM squid_users WHERE id = ?", (user_id,)).fetchone() + + if user: + new_status = 0 if user['is_active'] else 1 + db.execute( + "UPDATE squid_users SET is_active = ? WHERE id = ?", + (new_status, user_id) + ) + db.commit() + + # 更新squid_passwd文件 + update_squid_passwd() + + db.close() + return jsonify({'success': True}) + + +@app.route('/api/delete_user/', methods=['POST']) +@login_required +def delete_user(user_id): + db = get_db() + db.execute("DELETE FROM squid_users WHERE id = ?", (user_id,)) + db.commit() + + # 更新squid_passwd文件 + update_squid_passwd() + + db.close() + return jsonify({'success': True}) + + +@app.route('/api/create_user', methods=['POST']) +@login_required +def create_user(): + username = request.json.get('username') + password = request.json.get('password') + + if not username or not password: + return jsonify({'success': False, 'error': '用户名和密码不能为空'}), 400 + + db = get_db() + try: + db.execute( + "INSERT INTO squid_users (username, password) VALUES (?, ?)", + (username, password) + ) + db.commit() + + # 更新squid_passwd文件 + update_squid_passwd() + + return jsonify({'success': True}) + except sqlite3.IntegrityError: + return jsonify({'success': False, 'error': '用户名已存在'}), 400 + finally: + db.close() + + +@app.route('/api/update_user_password', methods=['POST']) +@login_required +def update_user_password(): + user_id = request.json.get('user_id') + password = request.json.get('password') + + if not user_id or not password: + return jsonify({'success': False, 'error': '参数不完整'}), 400 + + db = get_db() + db.execute( + "UPDATE squid_users SET password = ? WHERE id = ?", + (password, user_id) + ) + db.commit() + + # 更新squid_passwd文件 + update_squid_passwd() + + db.close() + return jsonify({'success': True}) + + +def update_squid_passwd(): + db = get_db() + users = db.execute("SELECT * FROM squid_users").fetchall() + db.close() + + with open(SQUID_PASSWD_FILE, 'w') as f: + for user in users: + line = f"{'#' if not user['is_active'] else ''}{user['username']}:{user['password']}\n" + f.write(line) if __name__ == '__main__': diff --git a/static/styles.css b/static/styles.css index b8b250c..fcbb51a 100644 --- a/static/styles.css +++ b/static/styles.css @@ -1,138 +1,50 @@ +/* 基础样式 */ body { font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif; - max-width: 900px; - margin: 0 auto; - padding: 30px; - background-color: #f8f9fa; + margin: 0; + padding: 0; + background-color: #f5f5f5; color: #333; } .container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; background-color: white; - border-radius: 10px; - padding: 30px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); } -h1 { - text-align: center; - color: #2c3e50; - margin-bottom: 30px; - font-weight: 600; - font-size: 28px; -} - -table { - width: 100%; - border-collapse: collapse; - margin: 25px 0; - background-color: white; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); - border-radius: 8px; - overflow: hidden; -} - -th, td { - padding: 15px 20px; - text-align: left; - border-bottom: 1px solid #e9ecef; -} - -th { - background-color: #f8f9fa; - font-weight: 600; - color: #495057; -} - -tr:hover { - background-color: #f8f9fa; -} - -.actions { +.login-container { display: flex; - gap: 10px; - align-items: center; -} - -button { - padding: 8px 16px; - border: none; - border-radius: 6px; - cursor: pointer; - transition: all 0.3s; - font-size: 14px; - font-weight: 500; -} - -.btn-primary { - background-color: #4285f4; - color: white; -} - -.btn-primary:hover { - background-color: #3367d6; -} - -.btn-danger { - background-color: #ea4335; - color: white; -} - -.btn-danger:hover { - background-color: #d33426; -} - -.btn-success { - background-color: #34a853; - color: white; -} - -.btn-success:hover { - background-color: #2d9248; -} - -.header-actions { - display: flex; - justify-content: space-between; - margin-bottom: 20px; -} - -footer { - text-align: center; - margin-top: 40px; - color: #6c757d; - font-size: 14px; -} - -/* 模态框样式 */ -.modal { - display: none; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); - z-index: 1000; justify-content: center; align-items: center; + height: 100vh; + background-color: #f5f5f5; } -.modal-content { +.login-box { background-color: white; + padding: 30px; border-radius: 8px; + box-shadow: 0 0 15px rgba(0, 0, 0, 0.1); width: 400px; - padding: 25px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); } -.modal-title { - font-size: 20px; - font-weight: 600; - margin-bottom: 20px; +.login-box h1 { + text-align: center; + margin-bottom: 30px; color: #2c3e50; } +.login-footer { + margin-top: 20px; + text-align: center; + font-size: 14px; + color: #666; +} + +/* 表单样式 */ .form-group { margin-bottom: 20px; } @@ -141,22 +53,92 @@ footer { display: block; margin-bottom: 8px; font-weight: 500; - color: #495057; } .form-group input { - width: 94%; + width: 100%; padding: 10px; - border: 1px solid #ced4da; + border: 1px solid #ddd; border-radius: 4px; - font-size: 14px; + font-size: 16px; } -.modal-actions { - display: flex; - justify-content: flex-end; - gap: 10px; - margin-top: 20px; +/* 按钮样式 */ +button, .btn { + padding: 10px 15px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: background-color 0.3s; + text-decoration: none; + display: inline-block; +} + +.btn-primary { + background-color: #3498db; + color: white; +} + +.btn-primary:hover { + background-color: #2980b9; +} + +.btn-danger { + background-color: #e74c3c; + color: white; +} + +.btn-danger:hover { + background-color: #c0392b; +} + +.btn-success { + background-color: #2ecc71; + color: white; +} + +.btn-success:hover { + background-color: #27ae60; +} + +/* 表格样式 */ +table { + width: 100%; + border-collapse: collapse; + margin: 20px 0; +} + +th, td { + padding: 12px 15px; + text-align: left; + border-bottom: 1px solid #ddd; +} + +th { + background-color: #f8f9fa; + font-weight: 600; +} + +tr:hover { + background-color: #f5f5f5; +} + +.password-cell { + position: relative; +} + +.password-placeholder { + letter-spacing: 2px; +} + +.btn-show-password { + background: none; + border: none; + cursor: pointer; + padding: 0 5px; + font-size: 16px; } /* 开关样式 */ @@ -198,9 +180,145 @@ footer { } input:checked + .slider { - background-color: #34a853; + background-color: #2ecc71; } input:checked + .slider:before { transform: translateX(24px); +} + +/* 模态框样式 */ +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1000; + justify-content: center; + align-items: center; +} + +.modal-content { + background-color: white; + border-radius: 8px; + width: 400px; + padding: 25px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); +} + +.modal-title { + font-size: 20px; + font-weight: 600; + margin-bottom: 20px; + color: #2c3e50; +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 20px; +} + +/* 仪表盘样式 */ +.dashboard { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; + margin: 30px 0; +} + +.dashboard-card { + background-color: white; + border-radius: 8px; + padding: 20px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + text-align: center; +} + +.dashboard-card h3 { + margin-top: 0; + color: #555; +} + +.stat { + font-size: 36px; + font-weight: bold; + margin: 10px 0; + color: #2c3e50; +} + +/* 代理使用示例 */ +.proxy-usage { + margin-top: 40px; +} + +.proxy-usage h2 { + border-bottom: 1px solid #eee; + padding-bottom: 10px; +} + +.usage-example { + background-color: #f8f9fa; + border-radius: 6px; + padding: 15px; + margin: 15px 0; +} + +.usage-example h3 { + margin-top: 0; +} + +code { + background-color: #eee; + padding: 2px 4px; + border-radius: 3px; + font-family: monospace; +} + +/* 头部操作按钮 */ +.header-actions { + display: flex; + justify-content: space-between; + margin-bottom: 20px; + flex-wrap: wrap; + gap: 10px; +} + +/* 设置部分 */ +.settings-section { + margin-bottom: 40px; + padding-bottom: 20px; + border-bottom: 1px solid #eee; +} + +.settings-section h2 { + margin-top: 0; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .container { + padding: 15px; + } + + .login-box { + width: 90%; + padding: 20px; + } + + table { + font-size: 14px; + } + + th, td { + padding: 8px 10px; + } + + .header-actions { + flex-direction: column; + } } \ No newline at end of file diff --git a/templates/clients.html b/templates/clients.html index 65a9a4c..3763e52 100644 --- a/templates/clients.html +++ b/templates/clients.html @@ -2,34 +2,48 @@ - Squid代理用户管理系统 + 用户管理 - Squid代理管理系统
-

Squid代理用户管理系统

+

用户管理

- - + 返回首页 + 系统设置 + + 退出登录
+ + {% for user in users %} - + + + @@ -37,7 +51,7 @@
用户名密码状态 操作
{{ user.name }}{{ user['username'] }} + •••••••• + + + + +
- - - + +
- 由 Flask 重构 - 基于原项目 ckazi + Squid代理管理系统 © 2023
@@ -84,8 +98,14 @@ // 切换用户状态 document.querySelectorAll('input[type="checkbox"]').forEach(sw => { sw.addEventListener('change', function() { - const username = this.dataset.username; - fetch(`/toggle/${username}`, { method: 'POST' }); + const userId = this.dataset.userId; + fetch(`/api/toggle_user/${userId}`, { method: 'POST' }) + .then(response => response.json()) + .then(data => { + if (!data.success) { + this.checked = !this.checked; + } + }); }); }); @@ -93,9 +113,31 @@ document.querySelectorAll('.delete-btn').forEach(btn => { btn.addEventListener('click', function() { if(confirm('确定要删除这个用户吗?此操作不可撤销!')) { - const username = this.closest('tr').querySelector('td').textContent; - fetch(`/delete/${username}`, { method: 'POST' }) - .then(() => location.reload()); + const userId = this.dataset.userId; + fetch(`/api/delete_user/${userId}`, { method: 'POST' }) + .then(response => response.json()) + .then(data => { + if (data.success) { + location.reload(); + } + }); + } + }); + }); + + // 显示/隐藏密码 + document.querySelectorAll('.btn-show-password').forEach(btn => { + btn.addEventListener('click', function() { + const row = this.closest('tr'); + const placeholder = row.querySelector('.password-placeholder'); + const password = row.querySelector('.password-value'); + + if (placeholder.style.display === 'none') { + placeholder.style.display = 'inline'; + password.style.display = 'none'; + } else { + placeholder.style.display = 'none'; + password.style.display = 'inline'; } }); }); @@ -103,16 +145,20 @@ // 编辑用户 document.querySelectorAll('.edit-btn').forEach(btn => { btn.addEventListener('click', function() { - const username = this.closest('tr').querySelector('td').textContent; + const row = this.closest('tr'); + const username = row.querySelector('td').textContent; + const userId = this.dataset.userId; + document.getElementById('editUsername').value = username; document.getElementById('editPassword').value = ''; + document.getElementById('editModal').dataset.userId = userId; document.getElementById('editModal').style.display = 'flex'; }); }); // 保存编辑 document.getElementById('saveBtn').addEventListener('click', function() { - const username = document.getElementById('editUsername').value; + const userId = document.getElementById('editModal').dataset.userId; const password = document.getElementById('editPassword').value; if(!password) { @@ -120,13 +166,18 @@ return; } - fetch('/save_user', { + fetch('/api/update_user_password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ username, password }) - }).then(response => { - if(response.ok) location.reload(); - }); + body: JSON.stringify({ user_id: userId, password }) + }).then(response => response.json()) + .then(data => { + if(data.success) { + location.reload(); + } else { + alert(data.error || '更新密码失败'); + } + }); }); // 关闭编辑模态框 @@ -144,17 +195,18 @@ return; } - fetch('/create_user', { + fetch('/api/create_user', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) - }).then(response => { - if(response.ok) { - location.reload(); - } else { - response.json().then(data => alert(data.error || '创建用户失败')); - } - }); + }).then(response => response.json()) + .then(data => { + if(data.success) { + location.reload(); + } else { + alert(data.error || '创建用户失败'); + } + }); }); // 打开添加用户模态框 @@ -166,15 +218,6 @@ document.getElementById('createCloseBtn').addEventListener('click', function() { document.getElementById('createModal').style.display = 'none'; }); - - // 退出系统 - document.getElementById('logoutBtn').addEventListener('click', function() { - if(confirm('确定要退出系统吗?')) { - fetch('/logout').then(() => { - window.location.href = '/'; - }); - } - }); }); diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..44458f3 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,60 @@ + + + + + Squid代理管理系统 - 首页 + + + +
+

Squid代理管理系统

+ + + +
+
+

用户数量

+

{{ user_count }}

+
+ +
+

代理地址

+

{{ settings['proxy_address'] }}:{{ settings['proxy_port'] }}

+
+
+ +
+

代理使用示例

+
+

Linux/Mac终端使用代理

+ + export http_proxy="http://{{ settings['proxy_address'] }}:{{ settings['proxy_port'] }}"
+ export https_proxy="http://{{ settings['proxy_address'] }}:{{ settings['proxy_port'] }}" +
+
+ +
+

Windows CMD使用代理

+ + set http_proxy=http://{{ settings['proxy_address'] }}:{{ settings['proxy_port'] }}
+ set https_proxy=http://{{ settings['proxy_address'] }}:{{ settings['proxy_port'] }} +
+
+ +
+

浏览器配置代理

+

地址: {{ settings['proxy_address'] }}

+

端口: {{ settings['proxy_port'] }}

+
+
+ +
+ Squid代理管理系统 © 2023 +
+
+ + \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..ab943c2 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,42 @@ + + + + + 登录 - Squid代理管理系统 + + + + + + \ No newline at end of file diff --git a/templates/settings.html b/templates/settings.html new file mode 100644 index 0000000..1805ac4 --- /dev/null +++ b/templates/settings.html @@ -0,0 +1,64 @@ + + + + + 系统设置 - Squid代理管理系统 + + + +
+

系统设置

+ + + +
+

修改管理员密码

+
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+ +
+

代理服务器设置

+
+ + +
+ + +
+ +
+ + +
+ + +
+
+
+ + \ No newline at end of file