增加新功能
This commit is contained in:
parent
048d4e20cf
commit
4b5c89786e
351
app.py
351
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/<username>', 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/<username>', 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/<int:user_id>', 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/<int:user_id>', 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__':
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -2,34 +2,48 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Squid代理用户管理系统</title>
|
||||
<title>用户管理 - Squid代理管理系统</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Squid代理用户管理系统</h1>
|
||||
<h1>用户管理</h1>
|
||||
|
||||
<div class="header-actions">
|
||||
<button id="createUserBtn" class="btn-primary">+ 添加新用户</button>
|
||||
<button id="logoutBtn" class="btn-danger">退出系统</button>
|
||||
<a href="{{ url_for('index') }}" class="btn-primary">返回首页</a>
|
||||
<a href="{{ url_for('settings') }}" class="btn-primary">系统设置</a>
|
||||
<button id="createUserBtn" class="btn-success">+ 添加新用户</button>
|
||||
<a href="{{ url_for('logout') }}" class="btn-danger">退出登录</a>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>用户名</th>
|
||||
<th>密码</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>{{ user.name }}</td>
|
||||
<td>{{ user['username'] }}</td>
|
||||
<td class="password-cell">
|
||||
<span class="password-placeholder">••••••••</span>
|
||||
<span class="password-value" style="display:none">{{ user['password'] }}</span>
|
||||
<button class="btn-show-password" data-user-id="{{ user['id'] }}">
|
||||
<i class="eye-icon">👁️</i>
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<label class="switch">
|
||||
<input type="checkbox" {% if user['is_active'] %}checked{% endif %}
|
||||
data-user-id="{{ user['id'] }}">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<div class="actions">
|
||||
<label class="switch">
|
||||
<input type="checkbox" {% if user.is_active %}checked{% endif %} data-username="{{ user.name }}">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<button class="btn-primary edit-btn">编辑</button>
|
||||
<button class="btn-danger delete-btn">删除</button>
|
||||
<button class="btn-primary edit-btn" data-user-id="{{ user['id'] }}">编辑</button>
|
||||
<button class="btn-danger delete-btn" data-user-id="{{ user['id'] }}">删除</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -37,7 +51,7 @@
|
||||
</table>
|
||||
|
||||
<footer>
|
||||
由 Flask 重构 - 基于原项目 <a href="https://github.com/ckazi" target="_blank">ckazi</a>
|
||||
Squid代理管理系统 © 2023
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@ -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 = '/';
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
60
templates/index.html
Normal file
60
templates/index.html
Normal file
@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Squid代理管理系统 - 首页</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Squid代理管理系统</h1>
|
||||
|
||||
<div class="header-actions">
|
||||
<a href="{{ url_for('clients') }}" class="btn-primary">用户管理</a>
|
||||
<a href="{{ url_for('settings') }}" class="btn-primary">系统设置</a>
|
||||
<a href="{{ url_for('logout') }}" class="btn-danger">退出登录</a>
|
||||
</div>
|
||||
|
||||
<div class="dashboard">
|
||||
<div class="dashboard-card">
|
||||
<h3>用户数量</h3>
|
||||
<p class="stat">{{ user_count }}</p>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-card">
|
||||
<h3>代理地址</h3>
|
||||
<p class="stat">{{ settings['proxy_address'] }}:{{ settings['proxy_port'] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="proxy-usage">
|
||||
<h2>代理使用示例</h2>
|
||||
<div class="usage-example">
|
||||
<h3>Linux/Mac终端使用代理</h3>
|
||||
<code>
|
||||
export http_proxy="http://{{ settings['proxy_address'] }}:{{ settings['proxy_port'] }}"<br>
|
||||
export https_proxy="http://{{ settings['proxy_address'] }}:{{ settings['proxy_port'] }}"
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<div class="usage-example">
|
||||
<h3>Windows CMD使用代理</h3>
|
||||
<code>
|
||||
set http_proxy=http://{{ settings['proxy_address'] }}:{{ settings['proxy_port'] }}<br>
|
||||
set https_proxy=http://{{ settings['proxy_address'] }}:{{ settings['proxy_port'] }}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<div class="usage-example">
|
||||
<h3>浏览器配置代理</h3>
|
||||
<p>地址: {{ settings['proxy_address'] }}</p>
|
||||
<p>端口: {{ settings['proxy_port'] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
Squid代理管理系统 © 2023
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
42
templates/login.html
Normal file
42
templates/login.html
Normal file
@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>登录 - Squid代理管理系统</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-box">
|
||||
<h1>Squid代理管理系统</h1>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" action="{{ url_for('login') }}">
|
||||
<div class="form-group">
|
||||
<label for="username">用户名</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary">登录</button>
|
||||
</form>
|
||||
|
||||
<div class="login-footer">
|
||||
<p>初始用户名: admin</p>
|
||||
<p>初始密码: admin123</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
64
templates/settings.html
Normal file
64
templates/settings.html
Normal file
@ -0,0 +1,64 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>系统设置 - Squid代理管理系统</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>系统设置</h1>
|
||||
|
||||
<div class="header-actions">
|
||||
<a href="{{ url_for('index') }}" class="btn-primary">返回首页</a>
|
||||
<a href="{{ url_for('clients') }}" class="btn-primary">用户管理</a>
|
||||
<a href="{{ url_for('logout') }}" class="btn-danger">退出登录</a>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h2>修改管理员密码</h2>
|
||||
<form method="POST" action="{{ url_for('settings') }}">
|
||||
<input type="hidden" name="action" value="change_password">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="current_password">当前密码</label>
|
||||
<input type="password" id="current_password" name="current_password" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="new_password">新密码</label>
|
||||
<input type="password" id="new_password" name="new_password" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="confirm_password">确认新密码</label>
|
||||
<input type="password" id="confirm_password" name="confirm_password" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-success">修改密码</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h2>代理服务器设置</h2>
|
||||
<form method="POST" action="{{ url_for('settings') }}">
|
||||
<input type="hidden" name="action" value="update_proxy">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="proxy_address">代理地址</label>
|
||||
<input type="text" id="proxy_address" name="proxy_address"
|
||||
value="{{ settings['proxy_address'] }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="proxy_port">代理端口</label>
|
||||
<input type="text" id="proxy_port" name="proxy_port"
|
||||
value="{{ settings['proxy_port'] }}" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-success">保存设置</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user