Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e4c884e79a | |||
| 7261b2d2a5 | |||
| 72d03723cd | |||
| 056d91b7ad | |||
| f7310541e1 | |||
| dba95410b4 | |||
| 42ca12ef11 | |||
| 49116c6d58 | |||
| cdb2289a64 | |||
| 53d912d632 | |||
| 73b0d11396 | |||
| 49fa5cdcf1 | |||
| 6017e93264 | |||
| 4022dcce1a | |||
| a167c77652 | |||
| 391aadf5eb | |||
| 68efaea7fb | |||
| ea5499af7e | |||
| 83fb4e312e | |||
| 880c8d79a6 | |||
| 1a6438bd55 | |||
| ca79d4488e | |||
| 8c2a0e12a3 | |||
| 3707bcaf34 | |||
| cd7add2377 | |||
| 8e8b25e0ad | |||
| 8300d10074 | |||
| 84e57bc8db |
182
README.md
Normal file
182
README.md
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
# Squid 代理管理系统
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
基于 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
197
app.py
@ -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
5
config/config.json
Normal 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
23
config/squid.conf
Normal file → Executable 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
2
config/squid_passwd
Normal file → Executable file
@ -1 +1 @@
|
|||||||
user:$apr1$KhRI8wrZ$SafuEp09ZGnmWYXm4dQvW.
|
user:$apr1$4TQJji47$70jLwdaewHbiP0SRWYBE.1
|
||||||
|
|||||||
7
config/users.json
Normal file
7
config/users.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"username": "user",
|
||||||
|
"password": "112233",
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
|
]
|
||||||
@ -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
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
Flask==2.3.2
|
Flask==2.3.2
|
||||||
|
pyperclip
|
||||||
|
|||||||
24
squid/Dockerfile
Normal file
24
squid/Dockerfile
Normal 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
28
squid/squid.conf
Executable 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
21
squid/start-squid.sh
Normal 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
|
||||||
@ -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
46
templates/base.html
Normal 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>
|
||||||
@ -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) }}">« 上一页</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) }}">下一页 »</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
47
templates/index.html
Normal 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
42
templates/settings.html
Normal 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 %}
|
||||||
Loading…
x
Reference in New Issue
Block a user