From 96ebfc97842804d59622ab1b056465507f1bdc5c Mon Sep 17 00:00:00 2001 From: wzj <244142824@qq.com> Date: Tue, 24 Jun 2025 10:12:17 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A6=96=E6=AC=A1=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + app.py | 142 ++++++++++++++++++++++++++++ requirements.txt | 1 + static/styles.css | 206 +++++++++++++++++++++++++++++++++++++++++ templates/clients.html | 181 ++++++++++++++++++++++++++++++++++++ 5 files changed, 532 insertions(+) create mode 100644 .gitignore create mode 100644 app.py create mode 100644 requirements.txt create mode 100644 static/styles.css create mode 100644 templates/clients.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4e0bbdf --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +venv/ +.idea/ diff --git a/app.py b/app.py new file mode 100644 index 0000000..d050648 --- /dev/null +++ b/app.py @@ -0,0 +1,142 @@ +from flask import Flask, render_template, request, jsonify, abort, redirect, url_for +from functools import wraps +import os +import subprocess +import base64 + +app = Flask(__name__) + +# 配置 +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 basic_auth_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"'}) + return f(*args, **kwargs) + + return decorated + + +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 + + +@app.route('/') +@basic_auth_required +def index(): + users = read_squid_file() + return render_template('clients.html', users=users) + + +@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 + + write_squid_file(users) + return '', 200 + + +@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 + + +@app.route('/logout') +def logout(): + return ('', 401, {'WWW-Authenticate': 'Basic realm="Authorization Required"'}) + + +if __name__ == '__main__': + if not os.path.exists('config'): + os.makedirs('config') + app.run(host='0.0.0.0', port=8080, debug=True) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7883f5b --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +Flask==2.3.2 \ No newline at end of file diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 0000000..b1226e6 --- /dev/null +++ b/static/styles.css @@ -0,0 +1,206 @@ +body { + font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif; + max-width: 900px; + margin: 0 auto; + padding: 30px; + background-color: #f8f9fa; + color: #333; +} + +.container { + background-color: white; + border-radius: 10px; + padding: 30px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); +} + +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 { + 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; +} + +.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; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: 500; + color: #495057; +} + +.form-group input { + width: 100%; + padding: 10px; + border: 1px solid #ced4da; + border-radius: 4px; + font-size: 14px; +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 20px; +} + +/* 开关样式 */ +.switch { + position: relative; + display: inline-block; + width: 50px; + height: 26px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: .4s; + border-radius: 34px; +} + +.slider:before { + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 3px; + bottom: 3px; + background-color: white; + transition: .4s; + border-radius: 50%; +} + +input:checked + .slider { + background-color: #34a853; +} + +input:checked + .slider:before { + transform: translateX(24px); +} \ No newline at end of file diff --git a/templates/clients.html b/templates/clients.html new file mode 100644 index 0000000..65a9a4c --- /dev/null +++ b/templates/clients.html @@ -0,0 +1,181 @@ + + + + + Squid代理用户管理系统 + + + +
+

Squid代理用户管理系统

+ +
+ + +
+ + + + + + + {% for user in users %} + + + + + {% endfor %} +
用户名操作
{{ user.name }} +
+ + + +
+
+ +
+ 由 Flask 重构 - 基于原项目 ckazi +
+
+ + + + + + + + + + \ No newline at end of file