首次提交
This commit is contained in:
commit
96ebfc9784
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
venv/
|
||||
.idea/
|
||||
142
app.py
Normal file
142
app.py
Normal 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
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
Flask==2.3.2
|
||||
206
static/styles.css
Normal file
206
static/styles.css
Normal 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
181
templates/clients.html
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user