首次提交

This commit is contained in:
wzj 2025-06-24 10:12:17 +08:00
commit 96ebfc9784
5 changed files with 532 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
venv/
.idea/

142
app.py Normal file
View File

@ -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/<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
write_squid_file(users)
return '', 200
@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
@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)

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
Flask==2.3.2

206
static/styles.css Normal file
View File

@ -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);
}

181
templates/clients.html Normal file
View File

@ -0,0 +1,181 @@
<!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">
<button id="createUserBtn" class="btn-primary">+ 添加新用户</button>
<button id="logoutBtn" class="btn-danger">退出系统</button>
</div>
<table>
<tr>
<th>用户名</th>
<th>操作</th>
</tr>
{% for user in users %}
<tr>
<td>{{ user.name }}</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>
</div>
</td>
</tr>
{% endfor %}
</table>
<footer>
由 Flask 重构 - 基于原项目 <a href="https://github.com/ckazi" target="_blank">ckazi</a>
</footer>
</div>
<!-- 编辑用户模态框 -->
<div id="editModal" class="modal">
<div class="modal-content">
<div class="modal-title">编辑用户信息</div>
<div class="form-group">
<label for="editUsername">用户名</label>
<input type="text" id="editUsername" readonly>
</div>
<div class="form-group">
<label for="editPassword">新密码</label>
<input type="password" id="editPassword" placeholder="请输入新密码">
</div>
<div class="modal-actions">
<button id="closeBtn" class="btn-danger">取消</button>
<button id="saveBtn" class="btn-success">保存更改</button>
</div>
</div>
</div>
<!-- 添加用户模态框 -->
<div id="createModal" class="modal">
<div class="modal-content">
<div class="modal-title">添加新用户</div>
<div class="form-group">
<label for="createUsername">用户名</label>
<input type="text" id="createUsername" placeholder="请输入用户名">
</div>
<div class="form-group">
<label for="createPassword">密码</label>
<input type="password" id="createPassword" placeholder="请输入密码">
</div>
<div class="modal-actions">
<button id="createCloseBtn" class="btn-danger">取消</button>
<button id="createSaveBtn" class="btn-success">确认添加</button>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 切换用户状态
document.querySelectorAll('input[type="checkbox"]').forEach(sw => {
sw.addEventListener('change', function() {
const username = this.dataset.username;
fetch(`/toggle/${username}`, { method: 'POST' });
});
});
// 删除用户
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());
}
});
});
// 编辑用户
document.querySelectorAll('.edit-btn').forEach(btn => {
btn.addEventListener('click', function() {
const username = this.closest('tr').querySelector('td').textContent;
document.getElementById('editUsername').value = username;
document.getElementById('editPassword').value = '';
document.getElementById('editModal').style.display = 'flex';
});
});
// 保存编辑
document.getElementById('saveBtn').addEventListener('click', function() {
const username = document.getElementById('editUsername').value;
const password = document.getElementById('editPassword').value;
if(!password) {
alert('密码不能为空!');
return;
}
fetch('/save_user', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
}).then(response => {
if(response.ok) location.reload();
});
});
// 关闭编辑模态框
document.getElementById('closeBtn').addEventListener('click', function() {
document.getElementById('editModal').style.display = 'none';
});
// 添加用户
document.getElementById('createSaveBtn').addEventListener('click', function() {
const username = document.getElementById('createUsername').value;
const password = document.getElementById('createPassword').value;
if(!username || !password) {
alert('用户名和密码不能为空!');
return;
}
fetch('/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 || '创建用户失败'));
}
});
});
// 打开添加用户模态框
document.getElementById('createUserBtn').addEventListener('click', function() {
document.getElementById('createModal').style.display = 'flex';
});
// 关闭添加用户模态框
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>
</html>