Compare commits

...

28 Commits
v1.0 ... master

Author SHA1 Message Date
wzj
e4c884e79a 修复squid_passwd明文存储问题 2025-07-29 13:37:27 +08:00
wzj
7261b2d2a5 修复squid_passwd明文存储问题 2025-07-29 13:36:03 +08:00
wzj
72d03723cd Merge branch 'master' of https://gitea.liuyan.wang/wzj/squid_ui 2025-07-29 11:12:01 +08:00
wzj
056d91b7ad 增加复制代理按钮 2025-07-29 11:05:22 +08:00
wzj
f7310541e1 增加README.md 2025-06-25 11:59:04 +08:00
wzj
dba95410b4 增加favicon 2025-06-25 11:07:04 +08:00
wzj
42ca12ef11 重构squid 2025-06-24 21:54:16 +08:00
wzj
49116c6d58 重构squid 2025-06-24 21:40:57 +08:00
wzj
cdb2289a64 增加squid 2025-06-24 18:46:09 +08:00
wzj
53d912d632 增加新功能 2025-06-24 16:13:05 +08:00
wzj
73b0d11396 修改配置 2025-06-24 16:10:02 +08:00
wzj
49fa5cdcf1 增加新功能 2025-06-24 16:08:49 +08:00
wzj
6017e93264 增加新功能 2025-06-24 15:09:25 +08:00
wzj
4022dcce1a 增加新功能 2025-06-24 14:30:33 +08:00
wzj
a167c77652 增加新功能 2025-06-24 14:21:58 +08:00
wzj
391aadf5eb 增加新功能 2025-06-24 14:14:03 +08:00
wzj
68efaea7fb 增加新功能 2025-06-24 14:13:57 +08:00
wzj
ea5499af7e 增加新功能 2025-06-24 14:00:26 +08:00
wzj
83fb4e312e 增加新功能 2025-06-24 13:55:33 +08:00
wzj
880c8d79a6 增加新功能 2025-06-24 13:54:02 +08:00
wzj
1a6438bd55 增加新功能 2025-06-24 13:18:50 +08:00
wzj
ca79d4488e 增加新功能 2025-06-24 13:15:30 +08:00
wzj
8c2a0e12a3 增加新功能 2025-06-24 13:13:56 +08:00
wzj
3707bcaf34 增加新功能 2025-06-24 13:09:56 +08:00
wzj
cd7add2377 增加新功能 2025-06-24 13:00:59 +08:00
wzj
8e8b25e0ad 增加新功能 2025-06-24 12:58:42 +08:00
wzj
8300d10074 增加新功能 2025-06-24 12:24:53 +08:00
wzj
84e57bc8db 增加新功能 2025-06-24 12:21:20 +08:00
16 changed files with 1003 additions and 67 deletions

182
README.md Normal file
View File

@ -0,0 +1,182 @@
# Squid 代理管理系统
![徽章](https://img.shields.io/badge/proxy-squid-4caf50?logo=nginxproxymanager)
![徽章](https://img.shields.io/badge/Python-3.8%2B-blue?labelColor=yellow&style=plastic&logo=python&logoColor=blue)
![徽章](https://img.shields.io/badge/Flask-2-blue?labelColor=yellow&style=plastic&logo=flask&logoColor=blue)
![徽章](https://img.shields.io/badge/github-jeazw-blue?labelColor=yellow&style=plastic&logo=github&logoColor=blue)
基于 Flask 的 Squid 代理用户管理界面,提供 Web 界面管理 Squid 代理用户和配置。
## 功能特性
- 🛡️ 基于 Basic Auth 的管理员认证
- 👥 代理用户管理(增删改查、启用/禁用)
- ⚙️ 代理服务器配置管理
- 📊 用户统计仪表盘
- 🐳 Docker 容器化部署
- 📝 配置文件持久化存储
## 快速部署
### 前提条件
- Docker 20.10+
- Docker Compose 2.0+
- 开放端口 51822 (Squid) 和 51823 (Web UI)
### 部署步骤
1. **准备持久化目录**
在项目根目录执行:
```bash
mkdir -p config log
chown 31:31 log # Squid 默认使用 squid 用户(UID 31)
```
2. **构建镜像**
分别构建两个服务的镜像:
```bash
# 构建 Web UI 镜像
docker build -t squid-ui:latest .
# 构建 Squid 镜像
cd squid && docker build -t squid:latest .
cd ..
```
3. **初始化配置文件**
首次运行前需要准备基础配置:
```bash
touch config/squid_passwd
cat > config/config.json <<EOF
{
"admin_password": "admin123",
"proxy_address": "0.0.0.0",
"proxy_port": "3128"
}
EOF
```
4. **启动服务**
通过 compose 启动服务:
```bash
docker-compose up -d
```
5. **验证服务状态**
```bash
docker-compose ps
```
应该看到两个服务状态均为 `running`
### 访问管理界面
```
http://your-server-ip:51823
```
默认管理员凭证:
- 用户名: `admin`
- 密码: `admin123`
### 配置代理客户端
```
代理地址: your-server-ip
端口: 51822
认证方式: 用户名/密码
```
## 持久化存储说明
| 目录/文件 | 用途 | 权限要求 |
|-----------|------|----------|
| `./config` | 存储所有配置文件 | 默认 |
| `./config/squid_passwd` | 用户认证文件 | 容器内可读写 |
| `./log` | Squid 日志目录 | 必须设置为 31:31 (squid用户) |
## 常见问题
### 权限问题排查
如果 Squid 容器启动失败,检查日志目录权限:
```bash
ls -ld log
```
正确输出应该类似:
```
drwxr-xr-x 2 31 31 4096 Jan 1 00:00 log
```
修复命令:
```bash
chown -R 31:31 log
```
### 重新构建镜像
当代码更新后需要重新构建:
```bash
docker-compose down
docker build -t squid-ui:latest .
cd squid && docker build -t squid:latest .
cd ..
docker-compose up -d
```
## 升级指南
1. 停止服务:
```bash
docker-compose down
```
2. 备份配置:
```bash
cp -r config config_backup_$(date +%Y%m%d)
```
3. 按上述步骤重新构建和启动
## 管理指南
### 添加新用户
1. 登录管理界面
2. 导航到 "客户端管理"
3. 点击 "添加用户"
4. 输入用户名和密码
5. 点击保存
### 修改代理设置
1. 登录管理界面
2. 导航到 "系统设置"
3. 修改以下参数:
- 管理员密码
- 代理服务器地址
- 代理服务器端口
4. 点击 "保存设置"
### 查看日志
Squid 日志位于主机上的 `./log` 目录:
```bash
tail -f ./log/access.log
```
## 常见问题
**Q: 为什么我的代理连接被拒绝?**
A: 检查:
- Squid 容器是否正常运行 (`docker ps`)
- 用户是否已激活
- 防火墙是否允许 51822 端口
**Q: 如何重置管理员密码?**
A: 编辑 `config/config.json` 文件中的 `admin_password` 字段
## 许可证
MIT License
Copyright (c) 2025 Jeazw

197
app.py
View File

@ -1,28 +1,58 @@
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
from functools import wraps from functools import wraps
import os import os
import subprocess import subprocess
import base64 import json
import uuid
from math import ceil
from flask import flash
app = Flask(__name__) app = Flask(__name__)
app.secret_key = str(uuid.uuid4())
# 配置 # 配置文件路径
CONFIG_FILE = 'config/config.json'
SQUID_PASSWD_FILE = 'config/squid_passwd' SQUID_PASSWD_FILE = 'config/squid_passwd'
ADMIN_PASSWORD = os.getenv('SQUID_PASSWORD', 'admin123') USERS_FILE = 'config/users.json' # 新增用户信息存储文件
# 加载配置
def load_config():
try:
with open(CONFIG_FILE, 'r') as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
# 默认配置
default_config = {
"admin_password": "admin123",
"proxy_address": "127.0.0.1",
"proxy_port": "3128"
}
os.makedirs('config', exist_ok=True)
with open(CONFIG_FILE, 'w') as f:
json.dump(default_config, f, indent=4)
return default_config
# 保存配置
def save_config(config):
with open(CONFIG_FILE, 'w') as f:
json.dump(config, f, indent=4)
class User: class User:
def __init__(self, name, password, is_active=True): def __init__(self, name, password, is_active=True):
self.name = name self.name = name
self.password = password self.password = password # 这里存储明文密码
self.is_active = is_active self.is_active = is_active
def basic_auth_required(f): def basic_auth_required(f):
@wraps(f) @wraps(f)
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
config = load_config()
auth = request.authorization auth = request.authorization
if not auth or not (auth.username == 'admin' and auth.password == ADMIN_PASSWORD): if not auth or not (auth.username == 'admin' and auth.password == config['admin_password']):
return ('Unauthorized', 401, return ('Unauthorized', 401,
{'WWW-Authenticate': 'Basic realm="Authorization Required"'}) {'WWW-Authenticate': 'Basic realm="Authorization Required"'})
return f(*args, **kwargs) return f(*args, **kwargs)
@ -45,17 +75,69 @@ def read_squid_file():
parts = line.split(':', 1) parts = line.split(':', 1)
if len(parts) == 2: if len(parts) == 2:
users.append(User(parts[0], parts[1], is_active)) # 从users.json中获取明文密码
users_data = load_users_data()
plain_password = next((u['password'] for u in users_data if u['username'] == parts[0]), None)
users.append(User(parts[0], plain_password, is_active))
except FileNotFoundError: except FileNotFoundError:
pass pass
return users return users
def write_squid_file(users): def write_squid_file(users):
with open(SQUID_PASSWD_FILE, 'w') as f: """只更新squid_passwd文件的状态是否注释不修改密码内容"""
for user in users: try:
line = f"{'#' if not user.is_active else ''}{user.name}:{user.password}\n" # 读取现有加密密码
f.write(line) with open(SQUID_PASSWD_FILE, 'r') as f:
existing_lines = f.readlines()
# 创建用户名到密码的映射
existing_passwords = {}
for line in existing_lines:
line = line.strip()
if not line:
continue
active = not line.startswith('#')
if not active:
line = line[1:]
parts = line.split(':', 1)
if len(parts) == 2:
existing_passwords[parts[0]] = parts[1]
# 写入新文件,保持加密密码不变
with open(SQUID_PASSWD_FILE, 'w') as f:
for user in users:
encrypted_password = existing_passwords.get(user.name, user.password)
line = f"{'#' if not user.is_active else ''}{user.name}:{encrypted_password}\n"
f.write(line)
except FileNotFoundError:
# 如果文件不存在,创建新文件
with open(SQUID_PASSWD_FILE, 'w') as f:
for user in users:
# 首次创建时需要使用htpasswd命令生成加密密码
subprocess.run(['htpasswd', '-b', SQUID_PASSWD_FILE, user.name, user.password], check=True)
# 如果不是活跃用户,需要添加注释
if not user.is_active:
with open(SQUID_PASSWD_FILE, 'r') as f_read:
content = f_read.read()
with open(SQUID_PASSWD_FILE, 'w') as f_write:
f_write.write(f"#{content}")
def load_users_data():
"""加载用户明文数据"""
try:
with open(USERS_FILE, 'r') as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return []
def save_users_to_json(users):
"""保存用户明文信息到JSON文件"""
users_data = [{'username': u.name, 'password': u.password, 'active': u.is_active} for u in users]
with open(USERS_FILE, 'w') as f:
json.dump(users_data, f, indent=4)
def create_user(username, password): def create_user(username, password):
@ -64,6 +146,11 @@ def create_user(username, password):
return False return False
try: try:
# 先保存明文密码到users.json
users.append(User(username, password, True))
save_users_to_json(users)
# 然后创建加密密码
subprocess.run(['htpasswd', '-b', SQUID_PASSWD_FILE, username, password], check=True) subprocess.run(['htpasswd', '-b', SQUID_PASSWD_FILE, username, password], check=True)
return True return True
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
@ -73,8 +160,62 @@ def create_user(username, password):
@app.route('/') @app.route('/')
@basic_auth_required @basic_auth_required
def index(): def index():
config = load_config()
users = read_squid_file() users = read_squid_file()
return render_template('clients.html', users=users) return render_template('index.html',
user_count=len(users),
proxy_address=config['proxy_address'],
proxy_port=config['proxy_port'])
@app.route('/clients')
@basic_auth_required
def clients():
page = request.args.get('page', 1, type=int)
per_page = 5
users = read_squid_file()
total_pages = ceil(len(users) / per_page)
start = (page - 1) * per_page
end = start + per_page
paginated_users = users[start:end]
config = load_config()
return render_template('clients.html',
users=paginated_users,
page=page,
total_pages=total_pages,
proxy_address=config['proxy_address'],
proxy_port=config['proxy_port'])
@app.route('/settings')
@basic_auth_required
def settings():
config = load_config()
return render_template('settings.html', config=config)
@app.route('/update_settings', methods=['POST'])
@basic_auth_required
def update_settings():
config = load_config()
new_password = request.form.get('admin_password')
proxy_address = request.form.get('proxy_address')
proxy_port = request.form.get('proxy_port')
if new_password:
config['admin_password'] = new_password
if proxy_address:
config['proxy_address'] = proxy_address
if proxy_port:
config['proxy_port'] = proxy_port
save_config(config)
flash('设置已成功保存!', 'success')
return redirect(url_for('settings'))
@app.route('/toggle/<username>', methods=['POST']) @app.route('/toggle/<username>', methods=['POST'])
@ -85,8 +226,8 @@ def toggle_user(username):
if user.name == username: if user.name == username:
user.is_active = not user.is_active user.is_active = not user.is_active
break break
write_squid_file(users) write_squid_file(users)
save_users_to_json(users) # 更新users.json
return '', 200 return '', 200
@ -95,6 +236,7 @@ def toggle_user(username):
def delete_user(username): def delete_user(username):
users = [u for u in read_squid_file() if u.name != username] users = [u for u in read_squid_file() if u.name != username]
write_squid_file(users) write_squid_file(users)
save_users_to_json(users) # 更新users.json
return '', 200 return '', 200
@ -109,6 +251,15 @@ def save_user():
return jsonify({'error': 'Username and password required'}), 400 return jsonify({'error': 'Username and password required'}), 400
try: try:
# 更新明文密码
users = read_squid_file()
for user in users:
if user.name == username:
user.password = password
break
save_users_to_json(users)
# 更新加密密码
subprocess.run(['htpasswd', '-b', SQUID_PASSWD_FILE, username, password], check=True) subprocess.run(['htpasswd', '-b', SQUID_PASSWD_FILE, username, password], check=True)
return '', 200 return '', 200
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
@ -131,6 +282,23 @@ def handle_create_user():
return jsonify({'error': 'User already exists'}), 409 return jsonify({'error': 'User already exists'}), 409
@app.route('/copy_proxy/<username>', methods=['POST'])
@basic_auth_required
def copy_proxy(username):
config = load_config()
users = read_squid_file()
user = next((u for u in users if u.name == username), None)
if not user:
return jsonify({'error': 'User not found'}), 404
proxy_url = f"http://{user.name}:{user.password}@{config['proxy_address']}:{config['proxy_port']}"
return jsonify({
'message': 'Proxy URL copied to clipboard (simulated)',
'proxy_url': proxy_url
})
@app.route('/logout') @app.route('/logout')
def logout(): def logout():
return ('', 401, {'WWW-Authenticate': 'Basic realm="Authorization Required"'}) return ('', 401, {'WWW-Authenticate': 'Basic realm="Authorization Required"'})
@ -139,4 +307,5 @@ def logout():
if __name__ == '__main__': if __name__ == '__main__':
if not os.path.exists('config'): if not os.path.exists('config'):
os.makedirs('config') os.makedirs('config')
app.run(host='0.0.0.0', port=8080, debug=True) load_config() # 初始化配置文件
app.run(host='0.0.0.0', port=8080, debug=True)

5
config/config.json Normal file
View File

@ -0,0 +1,5 @@
{
"admin_password": "admin123",
"proxy_address": "127.0.0.1",
"proxy_port": "3128"
}

23
config/squid.conf Normal file → Executable file
View File

@ -5,7 +5,24 @@ auth_param basic realm Squid proxy-caching web server
auth_param basic credentialsttl 2 hours auth_param basic credentialsttl 2 hours
acl auth_users proxy_auth REQUIRED acl auth_users proxy_auth REQUIRED
http_access allow auth_users http_access allow auth_users
http_access deny all
# 缓存配置
cache_dir ufs /var/cache/squid 100 16 256
cache_mem 256 MB
maximum_object_size 50 MB
# 日志与PID
pid_filename /tmp/squid.pid pid_filename /tmp/squid.pid
cache_log /dev/null cache_log /var/log/squid/cache.log
access_log none cache_store_log /var/log/squid/store.log
cache_store_log none
# 日志轮转设置
logfile_rotate 10
# 自定义日志格式(年月日时分秒)
logformat custom_format %{%Y/%m/%d %H:%M:%S}tl.%03tu %6tr %>a %Ss/%03Hs %<st %rm %ru %[un] %Sh/%<A %mt
access_log /var/log/squid/access.log custom_format
# 强制使用英语错误页面(可选)
error_directory /usr/share/squid/errors/en

2
config/squid_passwd Normal file → Executable file
View File

@ -1 +1 @@
user:$apr1$KhRI8wrZ$SafuEp09ZGnmWYXm4dQvW. user:$apr1$4TQJji47$70jLwdaewHbiP0SRWYBE.1

7
config/users.json Normal file
View File

@ -0,0 +1,7 @@
[
{
"username": "user",
"password": "112233",
"active": true
}
]

View File

@ -6,22 +6,19 @@ services:
container_name: squid-ui container_name: squid-ui
ports: ports:
- "51823:8080" - "51823:8080"
environment:
- SQUID_PASSWORD=Sqd123 # 建议通过环境变量文件或Docker secrets管理密码
volumes: volumes:
- ./config/squid_passwd:/app/config/squid_passwd:rw - ./config:/app/config:rw
# 如果模板文件需要动态修改,可以挂载模板目录
# - ./templates:/app/templates
restart: always restart: always
squid: squid:
image: ckazi/squid image: squid
container_name: squid container_name: squid
ports: ports:
- "51822:3128" - "51822:3128"
volumes: volumes:
- ./config/squid.conf:/etc/squid/squid.conf:ro - ./config/squid.conf:/etc/squid/squid.conf:ro
- ./config/squid_passwd:/etc/squid/squid_passwd:ro - ./config/squid_passwd:/etc/squid/squid_passwd:ro
- ./log:/var/log/squid:rw
depends_on: depends_on:
- squid-ui - squid-ui
restart: always restart: always

View File

@ -1 +1,2 @@
Flask==2.3.2 Flask==2.3.2
pyperclip

24
squid/Dockerfile Normal file
View File

@ -0,0 +1,24 @@
FROM alpine:latest
# 安装 Squid
RUN apk add --no-cache squid
# 创建必要的目录并设置权限
RUN mkdir -p \
/var/cache/squid \
/var/log/squid \
/var/run \
&& chown -R squid:squid /var/cache/squid \
&& chown -R squid:squid /var/log/squid
# 复制配置文件
COPY squid.conf /etc/squid/squid.conf
COPY start-squid.sh /usr/local/bin/start-squid.sh
RUN chmod +x /usr/local/bin/start-squid.sh
USER squid
EXPOSE 3128
ENTRYPOINT ["/usr/local/bin/start-squid.sh"]

28
squid/squid.conf Executable file
View File

@ -0,0 +1,28 @@
http_port 3128
auth_param basic program /usr/lib/squid/basic_ncsa_auth /etc/squid/squid_passwd
auth_param basic children 5
auth_param basic realm Squid proxy-caching web server
auth_param basic credentialsttl 2 hours
acl auth_users proxy_auth REQUIRED
http_access allow auth_users
http_access deny all
# 缓存配置
cache_dir ufs /var/cache/squid 100 16 256
cache_mem 256 MB
maximum_object_size 50 MB
# 日志与PID
pid_filename /tmp/squid.pid
cache_log /var/log/squid/cache.log
cache_store_log /var/log/squid/store.log
# 日志轮转设置
logfile_rotate 10
# 自定义日志格式(年月日时分秒)
logformat custom_format %{%Y/%m/%d %H:%M:%S}tl.%03tu %6tr %>a %Ss/%03Hs %<st %rm %ru %[un] %Sh/%<A %mt
access_log /var/log/squid/access.log custom_format
# 强制使用英语错误页面(可选)
error_directory /usr/share/squid/errors/en

21
squid/start-squid.sh Normal file
View File

@ -0,0 +1,21 @@
#!/bin/sh
set -e
SQUID=$(/usr/bin/which squid)
initialize_cache() {
echo "Initializing cache..."
if [ ! -d "/var/cache/squid/00" ]; then
"$SQUID" -z
sleep 5
fi
}
run() {
echo "Starting squid..."
initialize_cache
exec "$SQUID" -NYCd 1 -f /etc/squid/squid.conf
}
run

View File

@ -1,3 +1,40 @@
/*s tyle.css */
/* 导航栏样式 */
.main-nav {
display: flex;
background-color: #2c3e50;
border-radius: 8px;
padding: 0 20px;
margin-bottom: 30px;
}
.nav-link {
color: white;
text-decoration: none;
padding: 15px 20px;
transition: background-color 0.3s;
}
.nav-link:hover {
background-color: #34495e;
}
.nav-link.active {
background-color: #4285f4;
font-weight: bold;
}
.nav-logout {
margin-left: auto;
align-self: center;
padding: 8px 16px;
}
/* 内容区域 */
.content {
min-height: 60vh;
}
body { body {
font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif; font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
max-width: 900px; max-width: 900px;
@ -203,4 +240,247 @@ input:checked + .slider {
input:checked + .slider:before { input:checked + .slider:before {
transform: translateX(24px); transform: translateX(24px);
} }
/* 在现有CSS文件末尾添加以下内容 */
/* 首页专用样式 */
.dashboard {
display: flex;
justify-content: space-around;
gap: 20px;
margin: 20px 0;
}
.home-header h1 {
font-size: 24px;
margin-bottom: 15px;
}
.dashboard-card {
flex: 1;
background: linear-gradient(135deg, #4285f4, #34a853);
color: white;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
text-align: center;
transition: transform 0.3s, box-shadow 0.3s;
padding: 5px;
}
.dashboard-card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
}
.dashboard-card h3 {
margin-top: 0;
font-size: 18px;
font-weight: 500;
}
.stat {
font-size: 24px;
margin: 5px 0 0;
font-weight: 600;
}
.proxy-info {
background-color: #f8f9fa;
border-radius: 10px;
padding: 10px;
margin: 20px 0;
border-left: 4px solid #4285f4;
}
.proxy-info h2 {
margin-top: 0;
color: #2c3e50;
font-size: 18px;
}
.proxy-info p {
margin: 15px 0;
line-height: 1.6;
}
.proxy-info code {
background-color: #e9ecef;
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
color: #d63384;
}
/* 响应式设计 */
@media (max-width: 768px) {
.dashboard {
flex-direction: column;
}
.dashboard-card {
margin-bottom: 15px;
}
}
/* 添加以下新样式 */
.home-header {
text-align: center;
margin-bottom: 30px;
}
.home-header .subtitle {
color: #6c757d;
font-size: 18px;
margin-top: 10px;
}
.card-icon {
font-size: 40px;
margin-bottom: 15px;
}
.card-desc {
font-size: 14px;
opacity: 0.9;
margin-top: 5px;
}
.info-steps {
display: flex;
flex-direction: column;
gap: 5px;
}
.step {
display: flex;
align-items: flex-start;
gap: 15px;
margin-bottom: 10px;
}
.step-number {
background-color: #4285f4;
color: white;
width: 25px;
height: 25px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
flex-shrink: 0;
}
.quick-actions {
display: flex;
gap: 20px;
margin-top: 30px;
}
.action-card {
flex: 1;
background-color: white;
border-radius: 10px;
padding: 25px;
text-align: center;
text-decoration: none;
color: #333;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: transform 0.3s, box-shadow 0.3s;
}
.action-card:hover {
transform: translateY(-3px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.action-icon {
font-size: 36px;
margin-bottom: 15px;
}
.action-card h3 {
margin: 0 0 10px;
color: #2c3e50;
}
.action-card p {
margin: 0;
color: #6c757d;
font-size: 14px;
}
/* 设置页按钮样式调整 */
.settings-form .btn-danger {
padding: 8px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
font-size: 14px;
font-weight: 500;
}
.settings-form .btn-danger:hover {
background-color: #d33426;
}
/* 添加消息提示样式 */
.alert {
padding: 12px 20px;
border-radius: 6px;
margin-bottom: 20px;
font-size: 14px;
}
.alert-success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
/* 分页样式 */
.pagination {
display: flex;
justify-content: center;
margin: 20px 0;
gap: 5px;
position: relative; /* 改为相对定位 */
bottom: auto;
}
/* 固定分页区域位置 */
.table-container {
min-height: calc(100vh - 400px); /* 动态计算最小高度 */
margin-bottom: 60px; /* 为分页留出空间 */
}
.pagination a, .pagination span {
padding: 8px 12px;
border: 1px solid #ddd;
text-decoration: none;
color: #4285f4;
border-radius: 4px;
}
.pagination a:hover {
background-color: #f1f1f1;
}
.pagination .current {
background-color: #4285f4;
color: white;
border-color: #4285f4;
}
/* 响应式调整 */
@media (max-width: 768px) {
.quick-actions {
flex-direction: column;
}
.step {
flex-direction: column;
gap: 5px;
}
}

46
templates/base.html Normal file
View File

@ -0,0 +1,46 @@
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Squid代理用户管理系统{% endblock %}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='black' d='M12.008 0A822.933 822.933 0 0 0 1.59 6.043V18c3.578 2.087 7.238 4.274 10.418 6 3.928-2.267 6.71-3.868 10.402-6v-3.043l-1.045.6v1.8l-1.545.9-1.56-.9v-1.8l1.56-.885v-.002l.268.156a8.15 8.15 0 0 0 .404-1.754v-.002a8.72 8.72 0 0 0 .072-1.072 9.885 9.885 0 0 0-.072-1.127 8.873 8.873 0 0 0-.515-1.97v-.003a8.137 8.137 0 0 0-1.301-2.242 7.113 7.113 0 0 0-.615-.699 10.271 10.271 0 0 0-.846-.728 7.91 7.91 0 0 0-1.902-1.116 4.776 4.776 0 0 0-.586-.213v-.957c.41.118.812.265 1.2.442a9.2 9.2 0 0 1 1.618.943 9.4 9.4 0 0 1 1.158.986c.273.277.53.568.774.872.532.686.97 1.44 1.302 2.244h-.002a9.45 9.45 0 0 1 .645 2.613c.04.317.06.637.056.957 0 .314-.014.614-.043.914-.082.838-.37 1.786-.542 2.373l.472.27 1.045-.602V8.986l-1.303-.742v-.002l1.303.744V6c-3.56-2.057-7.212-4.154-10.402-6Zm8.08 14.826c-.02.052.003.002.004.002zM12.035 1.213l1.56.9v1.801l-1.56.885-1.545-.885h-.002v-.328a8.458 8.458 0 0 0-1.744.516 8.178 8.178 0 0 0-1.889 1.07l-.001.002a6.77 6.77 0 0 0-.9.783 9.171 9.171 0 0 0-.616.672 8.84 8.84 0 0 0-1.3 2.228l.228.127 1.287-.742 1.203.686c1.929-1.112 3.397-1.961 5.252-3.014l.027.014c1.926 1.114 3.398 1.955 5.238 3.029.028 1.997.014 4.064.014 6.086-1.874 1.084-3.753 2.16-5.28 3.043a859.719 859.719 0 0 1-5.294-3.043V8.957l.043-.027-1.203-.688-1.287.744-.229-.129h-.002a8.376 8.376 0 0 0-.53 2.057c-.044.36-.068.723-.07 1.086.002.344.026.687.07 1.027v.002c.015.215.06.429.102.643l-.83.484a7.017 7.017 0 0 1-.2-1.199A7.065 7.065 0 0 1 2.52 12c0-.329.028-.672.056-1a9.77 9.77 0 0 1 .658-2.6 9.438 9.438 0 0 1 1.303-2.244c.243-.3.5-.57.758-.842.37-.372.773-.71 1.203-1.013-1.215-.084-1.215-.084 0-.002a9.394 9.394 0 0 1 1.645-.942 9.866 9.866 0 0 1 2.347-.7l-.002-.542-1.043-.601 1.045.6zm0 .773-.887.514v1.027l.887.516.887-.516V2.5Zm-.03 6.928c-.935.532-1.888 1.084-2.689 1.543v3.086c.933.535 1.892 1.095 2.692 1.557.926-.565 1.865-1.093 2.676-1.557v-3.086c-.945-.542-1.857-1.074-2.678-1.543Zm-7.74 5.758 1.546.885v1.8l-.329.186c.146.177.303.344.471.5.277.288.58.55.902.785a8.07 8.07 0 0 0 1.83 1.059h.002a8.14 8.14 0 0 0 2.061.57c.417.058.837.087 1.258.086a8.37 8.37 0 0 0 1.332-.1 8.64 8.64 0 0 0 2.017-.572 8.076 8.076 0 0 0 1.86-1.1c.172-.114.315-.242.472-.37l.83.47a9.79 9.79 0 0 1-.945.787l.946.541 1.302-.756-1.302.758-.946-.543c-.516.37-1.067.69-1.644.955l-.002.002a9.502 9.502 0 0 1-2.588.756c-.441.057-.885.086-1.33.086a12.048 12.048 0 0 1-1.26-.072v.002a9.38 9.38 0 0 1-2.605-.744 9.044 9.044 0 0 1-1.688-.971 9.625 9.625 0 0 1-1.775-1.658h-.002l-.412.244-1.56-.9v-1.801zm0 .756-.886.515v1.028l.887.515.886-.515v-1.028zm15.555 0-.902.515v1.028l.902.515.887-.515v-1.028z'/></svg>">
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
<div class="container">
<!-- 统一的导航栏 -->
<nav class="main-nav">
<a href="{{ url_for('index') }}" class="nav-link {% if request.endpoint == 'index' %}active{% endif %}">首页</a>
<a href="{{ url_for('clients') }}" class="nav-link {% if request.endpoint == 'clients' %}active{% endif %}">用户管理</a>
<a href="{{ url_for('settings') }}" class="nav-link {% if request.endpoint == 'settings' %}active{% endif %}">系统设置</a>
<button id="logoutBtn" class="btn-danger nav-logout">退出系统</button>
</nav>
<!-- 内容区块 -->
<div class="content">
{% block content %}{% endblock %}
</div>
</div>
<!-- 统一的页脚 -->
<footer>
由 Flask 重构 - 基于原项目 <a href="https://github.com/ckazi" target="_blank">ckazi</a>
</footer>
<!-- 统一的JavaScript -->
<script>
// 退出系统功能
document.getElementById('logoutBtn')?.addEventListener('click', function() {
if(confirm('确定要退出系统吗?')) {
fetch('/logout').then(() => {
window.location.href = '/';
});
}
});
</script>
<!-- 子模板可以添加自己的JavaScript -->
{% block scripts %}{% endblock %}
</body>
</html>

View File

@ -1,44 +1,59 @@
<!DOCTYPE html> <!-- templates/clients.html -->
<html> {% extends "base.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"> {% block title %}用户管理 - Squid代理用户管理系统{% endblock %}
<button id="createUserBtn" class="btn-primary">+ 添加新用户</button>
<button id="logoutBtn" class="btn-danger">退出系统</button>
</div>
<table> {% block content %}
<tr> <h1>用户管理</h1>
<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> <div class="header-actions">
由 Flask 重构 - 基于原项目 <a href="https://github.com/ckazi" target="_blank">ckazi</a> <button id="createUserBtn" class="btn-primary">+ 添加新用户</button>
</footer> </div>
<table>
<tr>
<th>用户名</th>
<th>状态</th>
<th>操作</th>
</tr>
{% for user in users %}
<tr>
<td>{{ user.name }}</td>
<td>{{ "启用" if user.is_active else "禁用" }}</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-success copy-btn" data-username="{{ user.name }}">
<i class="icon-copy"></i> 复制代理
</button>
<button class="btn-danger delete-btn">删除</button>
</div>
</td>
</tr>
{% endfor %}
</table>
<!-- 分页导航 -->
<div class="pagination">
{% if page > 1 %}
<a href="{{ url_for('clients', page=page-1) }}">&laquo; 上一页</a>
{% endif %}
{% for p in range(1, total_pages + 1) %}
{% if p == page %}
<span class="current">{{ p }}</span>
{% else %}
<a href="{{ url_for('clients', page=p) }}">{{ p }}</a>
{% endif %}
{% endfor %}
{% if page < total_pages %}
<a href="{{ url_for('clients', page=page+1) }}">下一页 &raquo;</a>
{% endif %}
</div> </div>
<!-- 编辑用户模态框 --> <!-- 编辑用户模态框 -->
@ -78,7 +93,9 @@
</div> </div>
</div> </div>
</div> </div>
{% endblock %}
{% block scripts %}
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// 切换用户状态 // 切换用户状态
@ -167,6 +184,26 @@
document.getElementById('createModal').style.display = 'none'; document.getElementById('createModal').style.display = 'none';
}); });
// 复制代理地址
document.querySelectorAll('.copy-btn').forEach(btn => {
btn.addEventListener('click', function() {
const username = this.dataset.username;
fetch(`/copy_proxy/${username}`, { method: 'POST' })
.then(response => response.json())
.then(data => {
// 创建一个临时的textarea元素来复制文本
const textarea = document.createElement('textarea');
textarea.value = data.proxy_url;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
alert('代理地址已复制到剪贴板: ' + data.proxy_url);
});
});
});
// 退出系统 // 退出系统
document.getElementById('logoutBtn').addEventListener('click', function() { document.getElementById('logoutBtn').addEventListener('click', function() {
if(confirm('确定要退出系统吗?')) { if(confirm('确定要退出系统吗?')) {
@ -177,5 +214,38 @@
}); });
}); });
</script> </script>
</body> {% endblock %}
</html>
{% block styles %}
<style>
/* 添加复制按钮的绿色样式 */
.btn-success {
background-color: #28a745;
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.btn-success:hover {
background-color: #218838;
}
.btn-success:active {
background-color: #1e7e34;
}
.icon-copy {
display: inline-block;
width: 14px;
height: 14px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/20000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center;
margin-right: 5px;
vertical-align: middle;
}
</style>
{% endblock %}

47
templates/index.html Normal file
View File

@ -0,0 +1,47 @@
{% extends "base.html" %}
{% block title %}Squid代理用户管理系统{% endblock %}
{% block content %}
<div class="home-header">
<h1>Squid代理用户管理系统</h1>
</div>
<div class="dashboard">
<div class="dashboard-card">
<div class="card-icon">👥</div>
<h3>用户数量</h3>
<p class="stat">{{ user_count }}</p>
<p class="card-desc">当前活跃用户总数</p>
</div>
<div class="dashboard-card">
<div class="card-icon">🔌</div>
<h3>代理地址</h3>
<p class="stat">{{ proxy_address }}:{{ proxy_port }}</p>
<p class="card-desc">服务器连接信息</p>
</div>
</div>
<div class="proxy-info">
<h2><i class="icon">📋</i> 代理使用说明</h2>
<div class="info-steps">
<div class="step">
<span class="step-number">1</span>
<p>在浏览器或系统设置中配置代理服务器</p>
</div>
<div class="step">
<span class="step-number">2</span>
<p>地址: <code>{{ proxy_address }}</code> 端口: <code>{{ proxy_port }}</code></p>
</div>
<div class="step">
<span class="step-number">3</span>
<p>使用格式: <code>http://用户名:密码@{{ proxy_address }}:{{ proxy_port }}</code></p>
</div>
<div class="step">
<span class="step-number">4</span>
<p>或者在PAC文件中配置: <code>PROXY {{ proxy_address }}:{{ proxy_port }}</code></p>
</div>
</div>
</div>
{% endblock %}

42
templates/settings.html Normal file
View File

@ -0,0 +1,42 @@
<!-- templates/settings.html -->
{% extends "base.html" %}
{% block title %}系统设置 - Squid代理用户管理系统{% endblock %}
{% block content %}
<h1>系统设置</h1>
<!-- 添加消息显示区域 -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="alert alert-{{ messages[0][0] }}">
{{ messages[0][1] }}
</div>
{% endif %}
{% endwith %}
<form method="post" action="{{ url_for('update_settings') }}" class="settings-form">
<div class="form-group">
<label for="admin_password">管理员密码</label>
<input type="password" id="admin_password" name="admin_password"
placeholder="留空则不修改" value="{{ config.admin_password }}">
</div>
<div class="form-group">
<label for="proxy_address">代理服务器地址</label>
<input type="text" id="proxy_address" name="proxy_address"
value="{{ config.proxy_address }}" required>
</div>
<div class="form-group">
<label for="proxy_port">代理服务器端口</label>
<input type="text" id="proxy_port" name="proxy_port"
value="{{ config.proxy_port }}" required>
</div>
<div class="form-actions">
<button type="submit" class="btn-success">保存设置</button>
<a href="{{ url_for('index') }}" class="btn-danger">取消</a>
</div>
</form>
{% endblock %}