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
import os
import subprocess
import base64
import json
import uuid
from math import ceil
from flask import flash
app = Flask(__name__)
app.secret_key = str(uuid.uuid4())
# 配置
# 配置文件路径
CONFIG_FILE = 'config/config.json'
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:
def __init__(self, name, password, is_active=True):
self.name = name
self.password = password
self.password = password # 这里存储明文密码
self.is_active = is_active
def basic_auth_required(f):
@wraps(f)
def decorated(*args, **kwargs):
config = load_config()
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,
{'WWW-Authenticate': 'Basic realm="Authorization Required"'})
return f(*args, **kwargs)
@ -45,17 +75,69 @@ def read_squid_file():
parts = line.split(':', 1)
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:
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)
"""只更新squid_passwd文件的状态是否注释不修改密码内容"""
try:
# 读取现有加密密码
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):
@ -64,6 +146,11 @@ def create_user(username, password):
return False
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)
return True
except subprocess.CalledProcessError:
@ -73,8 +160,62 @@ def create_user(username, password):
@app.route('/')
@basic_auth_required
def index():
config = load_config()
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'])
@ -85,8 +226,8 @@ def toggle_user(username):
if user.name == username:
user.is_active = not user.is_active
break
write_squid_file(users)
save_users_to_json(users) # 更新users.json
return '', 200
@ -95,6 +236,7 @@ def toggle_user(username):
def delete_user(username):
users = [u for u in read_squid_file() if u.name != username]
write_squid_file(users)
save_users_to_json(users) # 更新users.json
return '', 200
@ -109,6 +251,15 @@ def save_user():
return jsonify({'error': 'Username and password required'}), 400
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)
return '', 200
except subprocess.CalledProcessError:
@ -131,6 +282,23 @@ def handle_create_user():
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')
def logout():
return ('', 401, {'WWW-Authenticate': 'Basic realm="Authorization Required"'})
@ -139,4 +307,5 @@ def logout():
if __name__ == '__main__':
if not os.path.exists('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
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 /dev/null
access_log none
cache_store_log none
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

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
ports:
- "51823:8080"
environment:
- SQUID_PASSWORD=Sqd123 # 建议通过环境变量文件或Docker secrets管理密码
volumes:
- ./config/squid_passwd:/app/config/squid_passwd:rw
# 如果模板文件需要动态修改,可以挂载模板目录
# - ./templates:/app/templates
- ./config:/app/config:rw
restart: always
squid:
image: ckazi/squid
image: squid
container_name: squid
ports:
- "51822:3128"
volumes:
- ./config/squid.conf:/etc/squid/squid.conf:ro
- ./config/squid_passwd:/etc/squid/squid_passwd:ro
- ./log:/var/log/squid:rw
depends_on:
- 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 {
font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
max-width: 900px;
@ -203,4 +240,247 @@ input:checked + .slider {
input:checked + .slider:before {
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>
<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>
<!-- templates/clients.html -->
{% extends "base.html" %}
<div class="header-actions">
<button id="createUserBtn" class="btn-primary">+ 添加新用户</button>
<button id="logoutBtn" class="btn-danger">退出系统</button>
</div>
{% block title %}用户管理 - Squid代理用户管理系统{% endblock %}
<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>
{% block content %}
<h1>用户管理</h1>
<footer>
由 Flask 重构 - 基于原项目 <a href="https://github.com/ckazi" target="_blank">ckazi</a>
</footer>
<div class="header-actions">
<button id="createUserBtn" class="btn-primary">+ 添加新用户</button>
</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>
<!-- 编辑用户模态框 -->
@ -78,7 +93,9 @@
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// 切换用户状态
@ -167,6 +184,26 @@
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() {
if(confirm('确定要退出系统吗?')) {
@ -177,5 +214,38 @@
});
});
</script>
</body>
</html>
{% endblock %}
{% 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 %}