Upload 26 files
Browse files- Dockerfile +11 -0
- README.md +6 -5
- app.py +18 -0
- gateway/__init__.py +0 -0
- gateway/admin.py +50 -0
- gateway/chatgpt.py +134 -0
- gateway/claude.py +80 -0
- gateway/geteway.py +20 -0
- gateway/index.py +15 -0
- gateway/login.py +56 -0
- gateway/timing.py +249 -0
- gateway/user.py +171 -0
- requirements.txt +5 -0
- static/claude.png +0 -0
- static/favicon.png +0 -0
- static/gpt.png +0 -0
- static/login.css +64 -0
- templates/GPT.html +532 -0
- templates/base.html +156 -0
- templates/claude.html +350 -0
- templates/login.html +194 -0
- templates/user_management.html +467 -0
- utils/Logger.py +24 -0
- utils/configs.py +23 -0
- utils/globals.py +141 -0
- utils/tools.py +171 -0
Dockerfile
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY . /app
|
| 6 |
+
|
| 7 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
+
|
| 9 |
+
EXPOSE 5100
|
| 10 |
+
|
| 11 |
+
CMD ["python", "app.py"]
|
README.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
+
title: cs
|
| 3 |
+
emoji: 👀
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: yellow
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
+
license: gpl-3.0
|
| 9 |
+
app_port: 5100
|
| 10 |
---
|
| 11 |
|
|
|
app.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask
|
| 2 |
+
import utils.configs as configs
|
| 3 |
+
|
| 4 |
+
app = Flask(__name__)
|
| 5 |
+
app.secret_key = configs.authorization # 用于加密 session
|
| 6 |
+
|
| 7 |
+
from gateway.index import *
|
| 8 |
+
from gateway.geteway import *
|
| 9 |
+
from gateway.chatgpt import *
|
| 10 |
+
from gateway.login import *
|
| 11 |
+
from gateway.timing import *
|
| 12 |
+
from gateway.user import *
|
| 13 |
+
from gateway.admin import *
|
| 14 |
+
from gateway.claude import *
|
| 15 |
+
|
| 16 |
+
# 启动 Flask 应用
|
| 17 |
+
if __name__ == '__main__':
|
| 18 |
+
app.run(host='0.0.0.0', port=5100)
|
gateway/__init__.py
ADDED
|
File without changes
|
gateway/admin.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app import app
|
| 2 |
+
from flask import render_template, request, jsonify
|
| 3 |
+
import utils.globals as globals
|
| 4 |
+
from utils.globals import *
|
| 5 |
+
from utils.tools import *
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# GPT 主页路由
|
| 13 |
+
@app.route('/chatgpt', methods=['GET', 'POST'])
|
| 14 |
+
@admin_required
|
| 15 |
+
def chatgpt():
|
| 16 |
+
|
| 17 |
+
if request.method == 'GET':
|
| 18 |
+
# 加载并显示 chatToken.json 文件中的内容
|
| 19 |
+
return render_template('GPT.html', retokens=globals.chatToken)
|
| 20 |
+
|
| 21 |
+
if request.method == 'POST':
|
| 22 |
+
# 获取更新后的 retoken 数据
|
| 23 |
+
globals.chatToken = request.json.get('retokens')
|
| 24 |
+
|
| 25 |
+
# 如果数据格式有效,保存到文件
|
| 26 |
+
if globals.chatToken:
|
| 27 |
+
save_retoken(globals.chatToken)
|
| 28 |
+
return jsonify({"status": "success", "message": "chatToken.json 已更新!"}), 200
|
| 29 |
+
else:
|
| 30 |
+
return jsonify({"status": "error", "message": "无效的数据格式!"}), 400
|
| 31 |
+
|
| 32 |
+
# Claude 主页路由
|
| 33 |
+
@app.route('/claude', methods=['GET', 'POST'])
|
| 34 |
+
@admin_required
|
| 35 |
+
def claude():
|
| 36 |
+
|
| 37 |
+
if request.method == 'GET':
|
| 38 |
+
# 加载并显示 chatToken.json 文件中的内容
|
| 39 |
+
return render_template('claude.html', retokens=globals.cluadeToken)
|
| 40 |
+
|
| 41 |
+
if request.method == 'POST':
|
| 42 |
+
# 获取更新后的 cltoken 数据
|
| 43 |
+
globals.cluadeToken = request.json.get('cltokens')
|
| 44 |
+
|
| 45 |
+
# 如果数据格式有效,保存到文件
|
| 46 |
+
if globals.cluadeToken:
|
| 47 |
+
save_cltoken(globals.cluadeToken)
|
| 48 |
+
return jsonify({"status": "success", "message": "claudeToken.json 已更新!"}), 200
|
| 49 |
+
else:
|
| 50 |
+
return jsonify({"status": "error", "message": "无效的数据格式!"}), 400
|
gateway/chatgpt.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app import app
|
| 2 |
+
import json
|
| 3 |
+
from flask import request, jsonify
|
| 4 |
+
import utils.globals as globals
|
| 5 |
+
from utils.globals import *
|
| 6 |
+
from utils.tools import *
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
# 加载刷新历史
|
| 11 |
+
@app.route('/refresh_history', methods=['GET'])
|
| 12 |
+
@admin_required
|
| 13 |
+
def get_refresh_history():
|
| 14 |
+
return jsonify({
|
| 15 |
+
"status": "success",
|
| 16 |
+
"history": globals.refresh_history
|
| 17 |
+
}), 200
|
| 18 |
+
|
| 19 |
+
# 加载失败Refresh Token
|
| 20 |
+
@app.route('/get_failed_tokens')
|
| 21 |
+
@admin_required
|
| 22 |
+
def get_failed_tokens():
|
| 23 |
+
try:
|
| 24 |
+
return jsonify(globals.failed_tokens), 200
|
| 25 |
+
except json.JSONDecodeError:
|
| 26 |
+
return jsonify({"error": "Invalid JSON in failed_tokens.json"}), 500
|
| 27 |
+
except Exception as e:
|
| 28 |
+
return jsonify({"error": str(e)}), 500
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# 加载Refresh Token
|
| 32 |
+
@app.route('/get_tokens')
|
| 33 |
+
@admin_required
|
| 34 |
+
def get_tokens():
|
| 35 |
+
try:
|
| 36 |
+
return jsonify(globals.chatToken), 200
|
| 37 |
+
except json.JSONDecodeError:
|
| 38 |
+
return jsonify({"error": "Invalid JSON in tokens.json"}), 500
|
| 39 |
+
except Exception as e:
|
| 40 |
+
return jsonify({"error": str(e)}), 500
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
# 添加新账号
|
| 44 |
+
@app.route('/api/tokens', methods=['POST'])
|
| 45 |
+
@admin_required
|
| 46 |
+
def create_tokens():
|
| 47 |
+
data = request.get_json()
|
| 48 |
+
|
| 49 |
+
# 检查账号是否已存在
|
| 50 |
+
if any(token['email'] == data['email'] for token in globals.chatToken):
|
| 51 |
+
return jsonify({'success': False, 'message': '该账号已存在'}), 400
|
| 52 |
+
|
| 53 |
+
new_token = {
|
| 54 |
+
'email': data['email'],
|
| 55 |
+
'refresh_token': data['ReToken'],
|
| 56 |
+
'access_token': data['AcToken'],
|
| 57 |
+
'status': True,
|
| 58 |
+
'type':"/static/gpt.png",
|
| 59 |
+
'PLUS': data['PLUS']
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
globals.chatToken.append(new_token)
|
| 63 |
+
save_retoken(globals.chatToken)
|
| 64 |
+
|
| 65 |
+
return jsonify({'success': True, 'message': '用户创建成功'})
|
| 66 |
+
|
| 67 |
+
# 更新账号信息
|
| 68 |
+
@app.route('/api/tokens/<email>', methods=['PUT'])
|
| 69 |
+
@admin_required
|
| 70 |
+
def update_token(email):
|
| 71 |
+
data = request.get_json()
|
| 72 |
+
|
| 73 |
+
token_index = next((i for i, token in enumerate(globals.chatToken) if token['email'] == email), None)
|
| 74 |
+
if token_index is None:
|
| 75 |
+
return jsonify({'success': False, 'message': '账号不存在'}), 400
|
| 76 |
+
|
| 77 |
+
# 如果提供了邮箱,则更新邮箱
|
| 78 |
+
new_email = data.get('email')
|
| 79 |
+
if new_email:
|
| 80 |
+
# 检查是否已有重复的邮箱
|
| 81 |
+
if any(token['email'] == new_email for i, token in enumerate(globals.chatToken) if i != token_index):
|
| 82 |
+
return jsonify({'success': False, 'message': '邮箱已存在'}), 400
|
| 83 |
+
globals.chatToken[token_index]['email'] = new_email
|
| 84 |
+
|
| 85 |
+
# 如果提供了ReToken,则更新ReToken
|
| 86 |
+
if data.get('ReToken'):
|
| 87 |
+
globals.chatToken[token_index]['refresh_token'] = data['ReToken']
|
| 88 |
+
else:
|
| 89 |
+
globals.chatToken[token_index]['refresh_token'] = ''
|
| 90 |
+
|
| 91 |
+
# 如果提供了AcToken,则更新AcToken
|
| 92 |
+
if data.get('AcToken'):
|
| 93 |
+
globals.chatToken[token_index]['access_token'] = data['AcToken']
|
| 94 |
+
globals.chatToken[token_index]['status'] = True
|
| 95 |
+
for i, user in enumerate(globals.users):
|
| 96 |
+
if user['bind_email'] == email:
|
| 97 |
+
globals.users[i]['bind_token'] = data['AcToken']
|
| 98 |
+
set_seedmap(globals.users[i]['id'],data['AcToken'])
|
| 99 |
+
save_users(globals.users)
|
| 100 |
+
else:
|
| 101 |
+
globals.chatToken[token_index]['access_token'] = ''
|
| 102 |
+
for i, user in enumerate(globals.users):
|
| 103 |
+
if user['bind_email'] == email:
|
| 104 |
+
globals.users[i]['bind_token'] = ''
|
| 105 |
+
del_seedmap(globals.users[i]['id'])
|
| 106 |
+
save_users(globals.users)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
if data.get('PLUS'):
|
| 110 |
+
globals.chatToken[token_index]['PLUS'] = data['PLUS']
|
| 111 |
+
|
| 112 |
+
save_retoken(globals.chatToken)
|
| 113 |
+
return jsonify({'success': True, 'message': '账号更新成功'})
|
| 114 |
+
|
| 115 |
+
# 删除账号
|
| 116 |
+
@app.route('/api/tokens/<email>', methods=['DELETE'])
|
| 117 |
+
@admin_required
|
| 118 |
+
def delete_token(email):
|
| 119 |
+
|
| 120 |
+
# 过滤掉要删除的账号
|
| 121 |
+
updated_email = [token for token in globals.chatToken if token['email'] != email]
|
| 122 |
+
|
| 123 |
+
if len(updated_email) == len(globals.chatToken):
|
| 124 |
+
return jsonify({'success': False, 'message': '账号不存在'}), 404
|
| 125 |
+
|
| 126 |
+
for i, user in enumerate(globals.users):
|
| 127 |
+
if user['bind_email'] == email:
|
| 128 |
+
globals.users[i]['bind_email'] = ''
|
| 129 |
+
globals.users[i]['bind_token'] = ''
|
| 130 |
+
del_seedmap(globals.users[i]['id'])
|
| 131 |
+
save_users(globals.users)
|
| 132 |
+
globals.chatToken=updated_email
|
| 133 |
+
save_retoken(globals.chatToken)
|
| 134 |
+
return jsonify({'success': True, 'message': '账号删除成功'})
|
gateway/claude.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app import app
|
| 2 |
+
import json
|
| 3 |
+
from flask import request, jsonify
|
| 4 |
+
import utils.globals as globals
|
| 5 |
+
from utils.globals import *
|
| 6 |
+
from utils.tools import *
|
| 7 |
+
|
| 8 |
+
# 加载Claude Token
|
| 9 |
+
@app.route('/get_Claude')
|
| 10 |
+
@admin_required
|
| 11 |
+
def get_Claude():
|
| 12 |
+
try:
|
| 13 |
+
return jsonify(globals.cluadeToken), 200
|
| 14 |
+
except json.JSONDecodeError:
|
| 15 |
+
return jsonify({"error": "Invalid JSON in tokens.json"}), 500
|
| 16 |
+
except Exception as e:
|
| 17 |
+
return jsonify({"error": str(e)}), 500
|
| 18 |
+
|
| 19 |
+
# 添加新账号
|
| 20 |
+
@app.route('/api/Claude', methods=['POST'])
|
| 21 |
+
@admin_required
|
| 22 |
+
def create_Claude():
|
| 23 |
+
data = request.get_json()
|
| 24 |
+
|
| 25 |
+
# 检查账号是否已存在
|
| 26 |
+
if any(token['email'] == data['email'] for token in globals.cluadeToken):
|
| 27 |
+
return jsonify({'success': False, 'message': '该账号已存在'}), 400
|
| 28 |
+
|
| 29 |
+
new_token = {
|
| 30 |
+
'email': data['email'],
|
| 31 |
+
'skToken': data['SkToken'],
|
| 32 |
+
'status': True,
|
| 33 |
+
'type':"/static/claude.png",
|
| 34 |
+
'PLUS': data['PLUS']
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
globals.cluadeToken.append(new_token)
|
| 38 |
+
save_cltoken(globals.cluadeToken)
|
| 39 |
+
|
| 40 |
+
return jsonify({'success': True, 'message': '用户创建成功'})
|
| 41 |
+
|
| 42 |
+
# 更新账号信息
|
| 43 |
+
@app.route('/api/Claude/<email>', methods=['PUT'])
|
| 44 |
+
@admin_required
|
| 45 |
+
def update_Claude(email):
|
| 46 |
+
data = request.get_json()
|
| 47 |
+
|
| 48 |
+
token_index = next((i for i, token in enumerate(globals.cluadeToken) if token['email'] == email), None)
|
| 49 |
+
if token_index is None:
|
| 50 |
+
return jsonify({'success': False, 'message': '账号不存在'}), 400
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
# 如果提供了邮箱,则更新邮箱
|
| 54 |
+
if data.get('email'):
|
| 55 |
+
globals.cluadeToken[token_index]['email'] = data['email']
|
| 56 |
+
|
| 57 |
+
# 如果提供了ReToken,则更新ReToken
|
| 58 |
+
if data.get('SkToken'):
|
| 59 |
+
globals.cluadeToken[token_index]['skToken'] = data['SkToken']
|
| 60 |
+
globals.cluadeToken[token_index]['status'] = True
|
| 61 |
+
|
| 62 |
+
if data.get('PLUS'):
|
| 63 |
+
globals.cluadeToken[token_index]['PLUS'] = data['PLUS']
|
| 64 |
+
|
| 65 |
+
save_cltoken(globals.cluadeToken)
|
| 66 |
+
return jsonify({'success': True, 'message': '账号更新成功'})
|
| 67 |
+
|
| 68 |
+
# 删除用户
|
| 69 |
+
@app.route('/api/Claude/<email>', methods=['DELETE'])
|
| 70 |
+
@admin_required
|
| 71 |
+
def delete_Claude(email):
|
| 72 |
+
|
| 73 |
+
# 过滤掉要删除的用户
|
| 74 |
+
updated_email = [token for token in globals.cluadeToken if token['email'] != email]
|
| 75 |
+
|
| 76 |
+
if len(updated_email) == len(globals.cluadeToken):
|
| 77 |
+
return jsonify({'success': False, 'message': '账号不存在'}), 404
|
| 78 |
+
globals.cluadeToken = updated_email
|
| 79 |
+
save_cltoken(globals.cluadeToken)
|
| 80 |
+
return jsonify({'success': True, 'message': '账号删除成功'})
|
gateway/geteway.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app import app
|
| 2 |
+
from flask import jsonify
|
| 3 |
+
import utils.globals as globals
|
| 4 |
+
from utils.globals import *
|
| 5 |
+
from utils.tools import *
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
# 一键同步网关seedmap
|
| 12 |
+
@app.route('/api/syc', methods=['GET'])
|
| 13 |
+
@admin_required
|
| 14 |
+
def syc_seedmap():
|
| 15 |
+
del_seedmap("clear")
|
| 16 |
+
|
| 17 |
+
for user in globals.users:
|
| 18 |
+
if user['bind_email'] != '' and user['bind_token'] != '':
|
| 19 |
+
set_seedmap(user['id'],user['bind_token'])
|
| 20 |
+
return jsonify({'success': True, 'message': '同步成功'})
|
gateway/index.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app import app
|
| 2 |
+
from flask import redirect, url_for, session
|
| 3 |
+
from utils.globals import *
|
| 4 |
+
from utils.tools import *
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
# 主页路由
|
| 9 |
+
@app.route('/')
|
| 10 |
+
@login_required
|
| 11 |
+
def index():
|
| 12 |
+
|
| 13 |
+
if session.get('logged_in'):
|
| 14 |
+
session.clear()
|
| 15 |
+
return redirect(url_for('login'))
|
gateway/login.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app import app
|
| 2 |
+
from flask import render_template, request, redirect, url_for, session, flash
|
| 3 |
+
import utils.globals as globals
|
| 4 |
+
from utils.globals import *
|
| 5 |
+
from utils.tools import *
|
| 6 |
+
from werkzeug.security import check_password_hash
|
| 7 |
+
from gateway.index import *
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
# 登录页面
|
| 11 |
+
@app.route('/login', methods=['GET', 'POST'])
|
| 12 |
+
def login():
|
| 13 |
+
if request.method == 'POST':
|
| 14 |
+
username = request.form['username']
|
| 15 |
+
password = request.form['password']
|
| 16 |
+
loginTarget = request.form['login_target']
|
| 17 |
+
user = next((user for user in globals.users if user['username'] == username), None)
|
| 18 |
+
|
| 19 |
+
if user and check_password_hash(user['password'], password):
|
| 20 |
+
# 登录成功,存储用户信息到session
|
| 21 |
+
session['logged_in'] = True
|
| 22 |
+
session['user_id'] = user['id']
|
| 23 |
+
session['username'] = user['username']
|
| 24 |
+
session['role'] = user['role']
|
| 25 |
+
|
| 26 |
+
flash('登录成功!', 'success')
|
| 27 |
+
|
| 28 |
+
# 如果是管理员,跳转到管理页面,否则跳转到ChatGPT共享页面
|
| 29 |
+
if user['role'] == 'admin' and loginTarget == 'manage':
|
| 30 |
+
return redirect(url_for('chatgpt'))
|
| 31 |
+
elif user['role'] == 'user' and loginTarget == 'manage':
|
| 32 |
+
flash('你没有管理员权限。', 'danger')
|
| 33 |
+
elif loginTarget == 'gpt':
|
| 34 |
+
logurl = getoauth(session.get('user_id'))
|
| 35 |
+
session.clear()
|
| 36 |
+
return redirect(logurl)
|
| 37 |
+
elif loginTarget == 'claude':
|
| 38 |
+
logurl = get_claude_login_url(user['bind_claude_token'],session.get('user_id'))
|
| 39 |
+
if logurl == None:
|
| 40 |
+
flash('你没有此权限。', 'danger')
|
| 41 |
+
else:
|
| 42 |
+
session.clear()
|
| 43 |
+
return redirect(logurl)
|
| 44 |
+
else:
|
| 45 |
+
flash('用户名或密码错误,请重试。', 'danger')
|
| 46 |
+
|
| 47 |
+
return render_template('login.html')
|
| 48 |
+
|
| 49 |
+
# 登出路由
|
| 50 |
+
@app.route('/logout')
|
| 51 |
+
def logout():
|
| 52 |
+
session.clear()
|
| 53 |
+
flash('已成功登出。', 'success')
|
| 54 |
+
return redirect(url_for('login'))
|
| 55 |
+
|
| 56 |
+
|
gateway/timing.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app import app
|
| 2 |
+
from datetime import datetime, timedelta
|
| 3 |
+
import time
|
| 4 |
+
import threading
|
| 5 |
+
from flask import request, jsonify
|
| 6 |
+
import utils.globals as globals
|
| 7 |
+
from utils.globals import *
|
| 8 |
+
from utils.tools import *
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def is_main_process():
|
| 13 |
+
import os
|
| 14 |
+
return os.environ.get('WERKZEUG_RUN_MAIN') != 'true'
|
| 15 |
+
|
| 16 |
+
current_timer = None
|
| 17 |
+
timer_lock = threading.Lock()
|
| 18 |
+
|
| 19 |
+
def schedule_next_refresh():
|
| 20 |
+
if not is_main_process():
|
| 21 |
+
print("在 reloader 进程中,跳过定时器设置")
|
| 22 |
+
return
|
| 23 |
+
|
| 24 |
+
global current_timer
|
| 25 |
+
|
| 26 |
+
with timer_lock:
|
| 27 |
+
if globals.auto_refresh_config['auto_refresh_enabled']:
|
| 28 |
+
if current_timer:
|
| 29 |
+
current_timer.cancel()
|
| 30 |
+
|
| 31 |
+
next_refresh = datetime.now() + timedelta(days=globals.auto_refresh_config['refresh_interval_days'])
|
| 32 |
+
globals.auto_refresh_config['next_refresh_time'] = next_refresh.isoformat()
|
| 33 |
+
save_auto_refresh_config(globals.auto_refresh_config)
|
| 34 |
+
|
| 35 |
+
current_timer = threading.Timer(
|
| 36 |
+
(next_refresh - datetime.now()).total_seconds(),
|
| 37 |
+
auto_refresh_tokens
|
| 38 |
+
)
|
| 39 |
+
current_timer.start()
|
| 40 |
+
|
| 41 |
+
def auto_refresh_tokens():
|
| 42 |
+
|
| 43 |
+
print('开始自动刷新')
|
| 44 |
+
new_access_tokens = refresh_access_tokens()
|
| 45 |
+
|
| 46 |
+
# 更新刷新历史
|
| 47 |
+
update_refresh_history(len(new_access_tokens))
|
| 48 |
+
|
| 49 |
+
# 添加延时,确保两次刷新之间有足够间隔
|
| 50 |
+
time.sleep(2) # 等待1秒
|
| 51 |
+
|
| 52 |
+
# 刷新完成后,调度下一次刷新
|
| 53 |
+
schedule_next_refresh()
|
| 54 |
+
|
| 55 |
+
# 更新刷新历史
|
| 56 |
+
def update_refresh_history(token_count):
|
| 57 |
+
|
| 58 |
+
globals.refresh_history.append({
|
| 59 |
+
"timestamp": datetime.now().isoformat(),
|
| 60 |
+
"token_count": token_count
|
| 61 |
+
})
|
| 62 |
+
|
| 63 |
+
# 保留最近的 5 条记录
|
| 64 |
+
globals.refresh_history = globals.refresh_history[-5:]
|
| 65 |
+
|
| 66 |
+
save_refresh_history(globals.refresh_history)
|
| 67 |
+
|
| 68 |
+
# 设定定时任务
|
| 69 |
+
@app.route('/set_auto_refresh', methods=['POST'])
|
| 70 |
+
@admin_required
|
| 71 |
+
def set_auto_refresh():
|
| 72 |
+
data = request.json
|
| 73 |
+
|
| 74 |
+
# 取消现有的定时任务
|
| 75 |
+
globals.auto_refresh_config['auto_refresh_enabled'] = data['enabled']
|
| 76 |
+
globals.auto_refresh_config['refresh_interval_days'] = data['interval']
|
| 77 |
+
save_auto_refresh_config(globals.auto_refresh_config)
|
| 78 |
+
|
| 79 |
+
if globals.auto_refresh_config['auto_refresh_enabled']:
|
| 80 |
+
schedule_next_refresh()
|
| 81 |
+
|
| 82 |
+
return jsonify({"status": "success", "message": "自动刷新设置已更新"})
|
| 83 |
+
|
| 84 |
+
# 加载定时任务配置信息
|
| 85 |
+
@app.route('/get_auto_refresh_config', methods=['GET'])
|
| 86 |
+
def get_auto_refresh_config():
|
| 87 |
+
return jsonify(globals.auto_refresh_config)
|
| 88 |
+
|
| 89 |
+
# 在应用启动时调用这个函数
|
| 90 |
+
def init_auto_refresh():
|
| 91 |
+
if not is_main_process():
|
| 92 |
+
print("在 reloader 进程中,跳过定时器初始化")
|
| 93 |
+
return
|
| 94 |
+
|
| 95 |
+
print(f"在主进程中初始化自动刷新, 当前时间: {datetime.now()}")
|
| 96 |
+
|
| 97 |
+
if globals.auto_refresh_config['auto_refresh_enabled'] and globals.auto_refresh_config['next_refresh_time']:
|
| 98 |
+
next_refresh = datetime.fromisoformat(globals.auto_refresh_config['next_refresh_time'])
|
| 99 |
+
|
| 100 |
+
if next_refresh > datetime.now():
|
| 101 |
+
delay_seconds = (next_refresh - datetime.now()).total_seconds()
|
| 102 |
+
print(f"设置初始定时器, 延迟秒数: {delay_seconds}")
|
| 103 |
+
|
| 104 |
+
global current_timer
|
| 105 |
+
with timer_lock:
|
| 106 |
+
current_timer = threading.Timer(delay_seconds, auto_refresh_tokens)
|
| 107 |
+
current_timer.start()
|
| 108 |
+
else:
|
| 109 |
+
schedule_next_refresh()
|
| 110 |
+
|
| 111 |
+
# 在应用启动时调用
|
| 112 |
+
init_auto_refresh()
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
# 删除gpt过期用户
|
| 116 |
+
def delete_expired_users():
|
| 117 |
+
print('开始检查并清理过期用户bind_token')
|
| 118 |
+
|
| 119 |
+
# 获取当前时间
|
| 120 |
+
now = datetime.now()
|
| 121 |
+
active_users = []
|
| 122 |
+
# 遍历 globals.users 来检查用户的过期时间
|
| 123 |
+
for user in globals.users:
|
| 124 |
+
# 如果 expiration_time 字段为空,跳过此用户
|
| 125 |
+
if user.get('expiration_time')=="":
|
| 126 |
+
active_users.append(user)
|
| 127 |
+
continue
|
| 128 |
+
|
| 129 |
+
try:
|
| 130 |
+
# 转换 expiration_time 为 datetime 对象
|
| 131 |
+
expiration_time = datetime.fromisoformat(user['expiration_time'])
|
| 132 |
+
except ValueError:
|
| 133 |
+
print(f"无效的过期时间格式,跳过用户 {user['username']}")
|
| 134 |
+
active_users.append(user)
|
| 135 |
+
continue
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
# 如果过期时间小于当前时间,设置 expiration_time 为空
|
| 139 |
+
if expiration_time < now:
|
| 140 |
+
user['expiration_time'] = ""
|
| 141 |
+
user['bind_email'] = ''
|
| 142 |
+
user['bind_token'] = ''
|
| 143 |
+
del_seedmap(user['id'])
|
| 144 |
+
active_users.append(user)
|
| 145 |
+
print(f"用户 {user['username']} 的过期时间已到,清除该用户绑定的token")
|
| 146 |
+
else:
|
| 147 |
+
active_users.append(user)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
globals.users = active_users
|
| 151 |
+
save_users(globals.users)
|
| 152 |
+
# 打印处理结果
|
| 153 |
+
print(f"过期用户的bind_token已设置为空")
|
| 154 |
+
|
| 155 |
+
# 调度下一次检查
|
| 156 |
+
schedule_next_user_cleanup()
|
| 157 |
+
|
| 158 |
+
# 删除claude过期用户
|
| 159 |
+
def delete_expired_users_claude():
|
| 160 |
+
print('开始检查并清理过期用户bind_token')
|
| 161 |
+
|
| 162 |
+
# 获取当前时间
|
| 163 |
+
now = datetime.now()
|
| 164 |
+
active_users = []
|
| 165 |
+
# 遍历 globals.users 来检查用户的过期时间
|
| 166 |
+
for user in globals.users:
|
| 167 |
+
# 如果 claude_expiration_time 字段为空,跳过此用户
|
| 168 |
+
if user.get('claude_expiration_time')=="":
|
| 169 |
+
active_users.append(user)
|
| 170 |
+
continue
|
| 171 |
+
|
| 172 |
+
try:
|
| 173 |
+
# 转换 claude_expiration_time 为 datetime 对象
|
| 174 |
+
claude_expiration_time = datetime.fromisoformat(user['claude_expiration_time'])
|
| 175 |
+
except ValueError:
|
| 176 |
+
print(f"无效的过期时间格式,跳过用户 {user['username']}")
|
| 177 |
+
active_users.append(user)
|
| 178 |
+
continue
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
# 如果过期时间小于当前时间,设置 claude_expiration_time 为空
|
| 182 |
+
if claude_expiration_time < now:
|
| 183 |
+
user['claude_expiration_time'] = ""
|
| 184 |
+
user['bind_claude_email'] = ''
|
| 185 |
+
user['bind_claude_token'] = ''
|
| 186 |
+
active_users.append(user)
|
| 187 |
+
print(f"用户 {user['username']} 的过期时间已到,清除该用户绑定的token")
|
| 188 |
+
else:
|
| 189 |
+
active_users.append(user)
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
globals.users = active_users
|
| 193 |
+
save_users(globals.users)
|
| 194 |
+
# 打印处理结果
|
| 195 |
+
print(f"过期用户的bind_token已设置为空")
|
| 196 |
+
|
| 197 |
+
# 调度下一次检查
|
| 198 |
+
schedule_next_user_cleanup_claude()
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
# 设定gpt定时任务
|
| 202 |
+
def schedule_next_user_cleanup():
|
| 203 |
+
# 设定检查过期用户的时间间隔,例如每天检查一次
|
| 204 |
+
next_check = datetime.now() + timedelta(days=1)
|
| 205 |
+
delay_seconds = (next_check - datetime.now()).total_seconds()
|
| 206 |
+
|
| 207 |
+
# 使用 threading.Timer 启动定时器
|
| 208 |
+
threading.Timer(delay_seconds, delete_expired_users).start()
|
| 209 |
+
|
| 210 |
+
# 设定claude定时任务
|
| 211 |
+
def schedule_next_user_cleanup_claude():
|
| 212 |
+
# 设定检查过期用户的时间间隔,例如每23小时检查一次
|
| 213 |
+
next_check = datetime.now() + timedelta(minutes=1380)
|
| 214 |
+
delay_seconds = (next_check - datetime.now()).total_seconds()
|
| 215 |
+
|
| 216 |
+
# 使用 threading.Timer 启动定时器
|
| 217 |
+
threading.Timer(delay_seconds, delete_expired_users_claude).start()
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
# 在应用启动时调用
|
| 221 |
+
def init_user_cleanup():
|
| 222 |
+
print(f"在应用启动时初始化用户清理任务, 当前时间: {datetime.now()}")
|
| 223 |
+
# 启动定时器,开始检查过期用户
|
| 224 |
+
schedule_next_user_cleanup()
|
| 225 |
+
schedule_next_user_cleanup_claude()
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
# 初始化用户清理任务
|
| 229 |
+
init_user_cleanup()
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
# 手动刷新access token
|
| 233 |
+
@app.route('/refresh_tokens', methods=['POST'])
|
| 234 |
+
@admin_required
|
| 235 |
+
def refresh_tokens():
|
| 236 |
+
try:
|
| 237 |
+
# 调用刷新 access_token 的函数
|
| 238 |
+
new_access_tokens = refresh_access_tokens()
|
| 239 |
+
update_refresh_history(len(new_access_tokens))
|
| 240 |
+
|
| 241 |
+
return jsonify({
|
| 242 |
+
"status": "success",
|
| 243 |
+
"access_tokens": new_access_tokens
|
| 244 |
+
}), 200
|
| 245 |
+
except Exception as e:
|
| 246 |
+
return jsonify({
|
| 247 |
+
"status": "error",
|
| 248 |
+
"message": str(e)
|
| 249 |
+
}), 500
|
gateway/user.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app import app
|
| 2 |
+
from flask import render_template, request, jsonify
|
| 3 |
+
import uuid
|
| 4 |
+
import utils.globals as globals
|
| 5 |
+
from utils.globals import *
|
| 6 |
+
from utils.tools import *
|
| 7 |
+
from werkzeug.security import generate_password_hash
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@app.route('/user')
|
| 16 |
+
@admin_required
|
| 17 |
+
def user_management():
|
| 18 |
+
return render_template('user_management.html')
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# 获取所有用户
|
| 23 |
+
@app.route('/api/users', methods=['GET'])
|
| 24 |
+
@admin_required
|
| 25 |
+
def get_users():
|
| 26 |
+
# 返回用户列表时不包含密码信息
|
| 27 |
+
return jsonify([{k: v for k, v in user.items() if k != 'password' and k != 'bind_token'} for user in globals.users])
|
| 28 |
+
|
| 29 |
+
# 创建新用户
|
| 30 |
+
@app.route('/api/users', methods=['POST'])
|
| 31 |
+
@admin_required
|
| 32 |
+
def create_user():
|
| 33 |
+
data = request.get_json()
|
| 34 |
+
|
| 35 |
+
# 检查用户名是否已存在
|
| 36 |
+
if any(user['username'] == data['username'] for user in globals.users):
|
| 37 |
+
return jsonify({'success': False, 'message': '用户名已存在'}), 400
|
| 38 |
+
|
| 39 |
+
new_user = {
|
| 40 |
+
'id': str(uuid.uuid4()),
|
| 41 |
+
'username': data['username'],
|
| 42 |
+
'password': generate_password_hash(data['password']),
|
| 43 |
+
'role': data['role'],
|
| 44 |
+
'bind_token': '',
|
| 45 |
+
'bind_email': '',
|
| 46 |
+
'expiration_time': data['expiration_time'],
|
| 47 |
+
'bind_claude_token': '',
|
| 48 |
+
'bind_claude_email': '',
|
| 49 |
+
'claude_expiration_time': data['claude_expiration_time']
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
globals.users.append(new_user)
|
| 53 |
+
save_users(globals.users)
|
| 54 |
+
|
| 55 |
+
return jsonify({'success': True, 'message': '用户创建成功'})
|
| 56 |
+
|
| 57 |
+
# 更新用户信息
|
| 58 |
+
@app.route('/api/users/<user_id>', methods=['PUT'])
|
| 59 |
+
@admin_required
|
| 60 |
+
def update_user(user_id):
|
| 61 |
+
data = request.get_json()
|
| 62 |
+
|
| 63 |
+
user_index = next((i for i, user in enumerate(globals.users) if user['id'] == user_id), None)
|
| 64 |
+
if user_index is None:
|
| 65 |
+
return jsonify({'success': False, 'message': '用户不存在'}), 404
|
| 66 |
+
|
| 67 |
+
# 检查用户名是否与其他用户冲突
|
| 68 |
+
if any(user['username'] == data['username'] and user['id'] != user_id for user in globals.users):
|
| 69 |
+
return jsonify({'success': False, 'message': '用户名已存在'}), 400
|
| 70 |
+
|
| 71 |
+
# 更新用户信息
|
| 72 |
+
globals.users[user_index]['username'] = data['username']
|
| 73 |
+
globals.users[user_index]['role'] = data['role']
|
| 74 |
+
|
| 75 |
+
# 如果提供了新密码,则更新密码
|
| 76 |
+
if data.get('password'):
|
| 77 |
+
globals.users[user_index]['password'] = generate_password_hash(data['password'])
|
| 78 |
+
|
| 79 |
+
globals.users[user_index]['expiration_time'] = data['expiration_time']
|
| 80 |
+
globals.users[user_index]['claude_expiration_time'] = data['claude_expiration_time']
|
| 81 |
+
|
| 82 |
+
save_users(globals.users)
|
| 83 |
+
return jsonify({'success': True, 'message': '用户更新成功'})
|
| 84 |
+
|
| 85 |
+
# 绑定ChatGPT账号
|
| 86 |
+
@app.route('/api/bind/<user_id>', methods=['PUT'])
|
| 87 |
+
@admin_required
|
| 88 |
+
def bind_account(user_id):
|
| 89 |
+
data = request.get_json()
|
| 90 |
+
user_index = next((i for i, user in enumerate(globals.users) if user['id'] == user_id), None)
|
| 91 |
+
token_index = next((i for i, token in enumerate(globals.chatToken) if token['email'] == data['email']), None)
|
| 92 |
+
res = set_seedmap(user_id,globals.chatToken[token_index]['access_token'])
|
| 93 |
+
if res == 200:
|
| 94 |
+
globals.users[user_index]['bind_email'] = data['email']
|
| 95 |
+
globals.users[user_index]['bind_token'] = globals.chatToken[token_index]['access_token']
|
| 96 |
+
save_users(globals.users)
|
| 97 |
+
return jsonify({'success': True, 'message': '账号绑定成功'})
|
| 98 |
+
else:
|
| 99 |
+
return jsonify({'success': False, 'message': '账号绑定失败'})
|
| 100 |
+
|
| 101 |
+
# 解绑ChatGPT账号
|
| 102 |
+
@app.route('/api/del_bind/<user_id>', methods=['DELETE'])
|
| 103 |
+
@admin_required
|
| 104 |
+
def del_bind_account(user_id):
|
| 105 |
+
res = del_seedmap(user_id)
|
| 106 |
+
if res == 200:
|
| 107 |
+
user_index = next((i for i, user in enumerate(globals.users) if user['id'] == user_id), None)
|
| 108 |
+
globals.users[user_index]['bind_email'] = ''
|
| 109 |
+
globals.users[user_index]['bind_token'] = ''
|
| 110 |
+
save_users(globals.users)
|
| 111 |
+
return jsonify({'success': True, 'message': '账号解绑成功'})
|
| 112 |
+
else:
|
| 113 |
+
return jsonify({'success': False, 'message': '账号解绑失败'})
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
# 获取全部ChatGPT账号的email
|
| 117 |
+
@app.route('/api/all_email', methods=['GET'])
|
| 118 |
+
@admin_required
|
| 119 |
+
def all_email():
|
| 120 |
+
# 返回账号的全部email
|
| 121 |
+
return jsonify([token['email'] for token in globals.chatToken if 'email' in token])
|
| 122 |
+
|
| 123 |
+
# 绑定Claude账号
|
| 124 |
+
@app.route('/api/bindClaude/<user_id>', methods=['PUT'])
|
| 125 |
+
@admin_required
|
| 126 |
+
def bind_claude_account(user_id):
|
| 127 |
+
data = request.get_json()
|
| 128 |
+
user_index = next((i for i, user in enumerate(globals.users) if user['id'] == user_id), None)
|
| 129 |
+
token_index = next((i for i, token in enumerate(globals.cluadeToken) if token['email'] == data['email']), None)
|
| 130 |
+
globals.users[user_index]['bind_claude_email'] = data['email']
|
| 131 |
+
globals.users[user_index]['bind_claude_token'] = globals.cluadeToken[token_index]['skToken']
|
| 132 |
+
save_users(globals.users)
|
| 133 |
+
return jsonify({'success': True, 'message': '账号绑定成功'})
|
| 134 |
+
|
| 135 |
+
# 解绑Claude账号
|
| 136 |
+
@app.route('/api/del_bindClaude/<user_id>', methods=['DELETE'])
|
| 137 |
+
@admin_required
|
| 138 |
+
def del_bind_claude_account(user_id):
|
| 139 |
+
|
| 140 |
+
user_index = next((i for i, user in enumerate(globals.users) if user['id'] == user_id), None)
|
| 141 |
+
globals.users[user_index]['bind_claude_email'] = ''
|
| 142 |
+
globals.users[user_index]['bind_claude_token'] = ''
|
| 143 |
+
save_users(globals.users)
|
| 144 |
+
return jsonify({'success': True, 'message': '账号解绑成功'})
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
# 获取全部Claude账号的email
|
| 148 |
+
@app.route('/api/all_claude_email', methods=['GET'])
|
| 149 |
+
@admin_required
|
| 150 |
+
def all_claude_email():
|
| 151 |
+
# 返回账号的全部email
|
| 152 |
+
return jsonify([token['email'] for token in globals.cluadeToken if 'email' in token])
|
| 153 |
+
|
| 154 |
+
# 删除用户
|
| 155 |
+
@app.route('/api/users/<user_id>', methods=['DELETE'])
|
| 156 |
+
@admin_required
|
| 157 |
+
def delete_user(user_id):
|
| 158 |
+
|
| 159 |
+
# 过滤掉要删除的用户
|
| 160 |
+
updated_users = [user for user in globals.users if user['id'] != user_id]
|
| 161 |
+
|
| 162 |
+
user = next((user for user in globals.users if user['id'] == user_id), None)
|
| 163 |
+
|
| 164 |
+
if len(updated_users) == len(globals.users):
|
| 165 |
+
return jsonify({'success': False, 'message': '用户不存在'}), 404
|
| 166 |
+
|
| 167 |
+
globals.users = updated_users
|
| 168 |
+
save_users(globals.users)
|
| 169 |
+
if user['bind_token'] != '':
|
| 170 |
+
del_seedmap(user_id)
|
| 171 |
+
return jsonify({'success': True, 'message': '用户删除成功'})
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask
|
| 2 |
+
requests
|
| 3 |
+
werkzeug
|
| 4 |
+
gunicorn
|
| 5 |
+
python-dotenv
|
static/claude.png
ADDED
|
static/favicon.png
ADDED
|
|
static/gpt.png
ADDED
|
static/login.css
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
body {
|
| 2 |
+
font-family: Arial, sans-serif;
|
| 3 |
+
background-color: #f0f2f5;
|
| 4 |
+
display: flex;
|
| 5 |
+
justify-content: center;
|
| 6 |
+
align-items: center;
|
| 7 |
+
height: 100vh;
|
| 8 |
+
margin: 0;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
.login-container {
|
| 12 |
+
background-color: white;
|
| 13 |
+
padding: 40px;
|
| 14 |
+
border-radius: 10px;
|
| 15 |
+
box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
|
| 16 |
+
width: 300px;
|
| 17 |
+
text-align: center;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
.login-container h1 {
|
| 21 |
+
margin-bottom: 20px;
|
| 22 |
+
font-size: 24px;
|
| 23 |
+
color: #333;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
.login-container input[type="text"],
|
| 27 |
+
.login-container input[type="password"] {
|
| 28 |
+
width: 300px;
|
| 29 |
+
padding: 10px;
|
| 30 |
+
margin: 10px 0;
|
| 31 |
+
border: 1px solid #ddd;
|
| 32 |
+
border-radius: 5px;
|
| 33 |
+
font-size: 16px;
|
| 34 |
+
box-sizing: border-box;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
.login-container button {
|
| 38 |
+
width: 300px;
|
| 39 |
+
padding: 10px;
|
| 40 |
+
background-color: #007bff;
|
| 41 |
+
border: none;
|
| 42 |
+
border-radius: 5px;
|
| 43 |
+
color: white;
|
| 44 |
+
font-size: 16px;
|
| 45 |
+
cursor: pointer;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
.login-container button:hover {
|
| 49 |
+
background-color: #0056b3;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
.message {
|
| 53 |
+
margin-bottom: 15px;
|
| 54 |
+
color: red;
|
| 55 |
+
font-size: 14px;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.success {
|
| 59 |
+
color: green;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.danger {
|
| 63 |
+
color: red;
|
| 64 |
+
}
|
templates/GPT.html
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Token管理{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block extra_css %}
|
| 6 |
+
<style>
|
| 7 |
+
.token-list {
|
| 8 |
+
width: 100%;
|
| 9 |
+
border-collapse: collapse;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
.token-list th,
|
| 13 |
+
.token-list td {
|
| 14 |
+
border: 1px solid #e2e8f0;
|
| 15 |
+
padding: 8px;
|
| 16 |
+
text-align: left;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
.token-list th {
|
| 20 |
+
background-color: #f8fafc;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.token-list tr:nth-child(even) {
|
| 24 |
+
background-color: #f1f5f9;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.truncate {
|
| 28 |
+
max-width: 200px;
|
| 29 |
+
white-space: nowrap;
|
| 30 |
+
overflow: hidden;
|
| 31 |
+
text-overflow: ellipsis;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
h2 {
|
| 35 |
+
position: relative;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
#addTokenBtn {
|
| 39 |
+
position: absolute;
|
| 40 |
+
font-size: 1rem;
|
| 41 |
+
right: 0;
|
| 42 |
+
top: 50%;
|
| 43 |
+
transform: translateY(-50%);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
button:disabled {
|
| 47 |
+
color: #ccc;
|
| 48 |
+
background-color: #eee;
|
| 49 |
+
border: none;
|
| 50 |
+
cursor: not-allowed;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
button:disabled:hover {
|
| 54 |
+
color: #ccc;
|
| 55 |
+
background-color: #eee;
|
| 56 |
+
border: none;
|
| 57 |
+
cursor: not-allowed;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
@media (max-width: 770px) {
|
| 61 |
+
body {
|
| 62 |
+
font-size: 2.3vw;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
h2 {
|
| 66 |
+
font-size: 3.5vw !important;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
#addTokenBtn {
|
| 70 |
+
font-size: 2vw;
|
| 71 |
+
line-height: 2vw;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.token-list {
|
| 75 |
+
flex: 1;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.token-list thead {
|
| 79 |
+
display: flex;
|
| 80 |
+
flex-wrap: wrap;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.token-list #tokenTableBody {
|
| 84 |
+
display: flex;
|
| 85 |
+
flex-wrap: wrap;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.token-list tr {
|
| 89 |
+
display: flex;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.token-list tr,
|
| 93 |
+
.token-list th,
|
| 94 |
+
.token-list td {
|
| 95 |
+
flex: 1;
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
</style>
|
| 99 |
+
{% endblock %}
|
| 100 |
+
|
| 101 |
+
{% block content %}
|
| 102 |
+
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
| 103 |
+
<h2 class="text-2xl font-bold text-gray-800 mb-4">账号列表
|
| 104 |
+
<button id="addTokenBtn"
|
| 105 |
+
class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline transition duration-300 ease-in-out mr-2">
|
| 106 |
+
添加账号
|
| 107 |
+
</button>
|
| 108 |
+
</h2>
|
| 109 |
+
<table class="token-list">
|
| 110 |
+
<thead>
|
| 111 |
+
<tr>
|
| 112 |
+
<th>Email</th>
|
| 113 |
+
<th>Re Token</th>
|
| 114 |
+
<th>状态</th>
|
| 115 |
+
<th>操作</th>
|
| 116 |
+
</tr>
|
| 117 |
+
</thead>
|
| 118 |
+
<tbody id="tokenTableBody">
|
| 119 |
+
<!-- Token 数据将在这里动态添加 -->
|
| 120 |
+
</tbody>
|
| 121 |
+
</table>
|
| 122 |
+
<div class="flex items-center justify-between mt-4">
|
| 123 |
+
<div>
|
| 124 |
+
<button id="refreshBtn"
|
| 125 |
+
class="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline transition duration-300 ease-in-out mr-2">
|
| 126 |
+
立即刷新 Tokens
|
| 127 |
+
</button>
|
| 128 |
+
</div>
|
| 129 |
+
<p id="statusMessage" class="italic"></p>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
|
| 133 |
+
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
| 134 |
+
<h2 class="text-2xl font-bold text-gray-800 mb-4">Access Token刷新设置</h2>
|
| 135 |
+
<div class="flex items-center mb-4">
|
| 136 |
+
<input type="checkbox" id="autoRefreshToggle" class="mr-2">
|
| 137 |
+
<label for="autoRefreshToggle" class="mr-4">启用自动刷新</label>
|
| 138 |
+
<input type="number" id="refreshInterval" min="1" value="1" class="border rounded px-2 py-1 w-20 mr-2">
|
| 139 |
+
<span>天</span>
|
| 140 |
+
</div>
|
| 141 |
+
<p id="nextRefreshTime" class="text-gray-600"></p>
|
| 142 |
+
</div>
|
| 143 |
+
|
| 144 |
+
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
| 145 |
+
<h2 class="text-2xl font-bold text-gray-800 mb-4">刷新Refresh Token失败的账号</h2>
|
| 146 |
+
<details class="mb-4">
|
| 147 |
+
<summary class="cursor-pointer text-blue-600 hover:text-blue-800">点击查看</summary>
|
| 148 |
+
<div id="failedTokens" class="mt-2 space-y-1">
|
| 149 |
+
<!-- 失败的 tokens 将在这里动态添加 -->
|
| 150 |
+
</div>
|
| 151 |
+
</details>
|
| 152 |
+
</div>
|
| 153 |
+
|
| 154 |
+
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
| 155 |
+
<h2 class="text-2xl font-bold text-gray-800 mb-4">刷新历史</h2>
|
| 156 |
+
<details class="mb-4">
|
| 157 |
+
<summary class="cursor-pointer text-blue-600 hover:text-blue-800">点击查看</summary>
|
| 158 |
+
<div id="refreshHistory" class="space-y-2">
|
| 159 |
+
<!-- 刷新历史将在这里动态添加 -->
|
| 160 |
+
</div>
|
| 161 |
+
</details>
|
| 162 |
+
|
| 163 |
+
</div>
|
| 164 |
+
|
| 165 |
+
<div id="tokenModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden">
|
| 166 |
+
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
| 167 |
+
<div class="mt-3">
|
| 168 |
+
<h3 class="text-lg font-medium text-gray-900 mb-4">账号信息</h3>
|
| 169 |
+
<form id="tokenForm">
|
| 170 |
+
<input type="hidden" id="id">
|
| 171 |
+
<div class="mb-4">
|
| 172 |
+
<label class="block text-gray-700 text-sm font-bold mb-2" for="email">
|
| 173 |
+
邮箱
|
| 174 |
+
</label>
|
| 175 |
+
<input
|
| 176 |
+
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
| 177 |
+
id="email" type="text" required placeholder="仅用于标识 之后不得修改">
|
| 178 |
+
</div>
|
| 179 |
+
<div class="mb-4">
|
| 180 |
+
<label class="block text-gray-700 text-sm font-bold mb-2" for="ReToken">
|
| 181 |
+
Refresh Token
|
| 182 |
+
</label>
|
| 183 |
+
<input
|
| 184 |
+
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
| 185 |
+
id="ReToken" type="text" placeholder="可为空">
|
| 186 |
+
</div>
|
| 187 |
+
<div class="mb-4">
|
| 188 |
+
<label class="block text-gray-700 text-sm font-bold mb-2" for="acToken">
|
| 189 |
+
Access Token
|
| 190 |
+
</label>
|
| 191 |
+
<input
|
| 192 |
+
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
| 193 |
+
id="AcToken" type="text" placeholder="无Refresh Token时必填">
|
| 194 |
+
</div>
|
| 195 |
+
<div class="mb-4">
|
| 196 |
+
<label class="block text-gray-700 text-sm font-bold mb-2" for="PLUS">
|
| 197 |
+
PLUS
|
| 198 |
+
</label>
|
| 199 |
+
<select
|
| 200 |
+
class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
| 201 |
+
id="PLUS" required>
|
| 202 |
+
<option value='false'>false</option>
|
| 203 |
+
<option value='true'>true</option>
|
| 204 |
+
</select>
|
| 205 |
+
</div>
|
| 206 |
+
<div class="flex items-center justify-end">
|
| 207 |
+
<button type="button" onclick="closeModal()"
|
| 208 |
+
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline mr-2">
|
| 209 |
+
取消
|
| 210 |
+
</button>
|
| 211 |
+
<button type="submit"
|
| 212 |
+
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
| 213 |
+
保存
|
| 214 |
+
</button>
|
| 215 |
+
</div>
|
| 216 |
+
</form>
|
| 217 |
+
</div>
|
| 218 |
+
</div>
|
| 219 |
+
</div>
|
| 220 |
+
|
| 221 |
+
</div>
|
| 222 |
+
{% endblock %}
|
| 223 |
+
{% block extra_js %}
|
| 224 |
+
|
| 225 |
+
<script>
|
| 226 |
+
|
| 227 |
+
function loadTokens() {
|
| 228 |
+
fetch('/get_tokens')
|
| 229 |
+
.then(response => response.json())
|
| 230 |
+
.then(data => {
|
| 231 |
+
tokens = data
|
| 232 |
+
tokenTableBody.innerHTML = '';
|
| 233 |
+
data.forEach(token => {
|
| 234 |
+
let rt = '有'
|
| 235 |
+
if (!token.refresh_token) {
|
| 236 |
+
rt = '无'
|
| 237 |
+
}
|
| 238 |
+
let recolor = token.refresh_token ? ' style="color: rgb(6, 161, 6);"' : ' style="color: rgb(204, 28, 28);"'
|
| 239 |
+
let color = token.status ? ' style="color: rgb(6, 161, 6);"' : ' style="color: rgb(204, 28, 28);"'
|
| 240 |
+
const row = document.createElement('tr');
|
| 241 |
+
row.innerHTML = `
|
| 242 |
+
<td class="truncate">${token.email}</td>
|
| 243 |
+
<td class="truncate" ${recolor}>${rt}</td>
|
| 244 |
+
<td class="truncate" ${color}>${token.status ? '有效' : '失效'}</td>
|
| 245 |
+
<td>
|
| 246 |
+
<button onclick="editUser('${token.email}')" class="text-blue-600 hover:text-blue-900 mr-4">编辑</button>
|
| 247 |
+
<button onclick="deleteUser('${token.email}')" class="text-red-600 hover:text-red-900">删除</button>
|
| 248 |
+
</td>
|
| 249 |
+
`;
|
| 250 |
+
tokenTableBody.appendChild(row);
|
| 251 |
+
});
|
| 252 |
+
})
|
| 253 |
+
.catch(error => {
|
| 254 |
+
console.error('加载 Tokens 失败:', error);
|
| 255 |
+
showStatus('加载 Tokens 失败', 'error');
|
| 256 |
+
});
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
let tokens = []
|
| 260 |
+
const modal = document.getElementById('tokenModal');
|
| 261 |
+
const tokenForm = document.getElementById('tokenForm');
|
| 262 |
+
const addTokenBtn = document.getElementById('addTokenBtn');
|
| 263 |
+
|
| 264 |
+
// 打开模态框
|
| 265 |
+
function openModal(email = null) {
|
| 266 |
+
const form = document.getElementById('tokenForm');
|
| 267 |
+
if (email) {
|
| 268 |
+
document.getElementById('id').value = 'id'
|
| 269 |
+
document.getElementById('email').value = email.email;
|
| 270 |
+
document.getElementById('ReToken').value = email.refresh_token;
|
| 271 |
+
document.getElementById('AcToken').value = email.access_token;
|
| 272 |
+
document.getElementById('PLUS').value = email.PLUS;
|
| 273 |
+
} else {
|
| 274 |
+
form.reset();
|
| 275 |
+
document.getElementById('id').value = ''
|
| 276 |
+
}
|
| 277 |
+
modal.classList.remove('hidden');
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
// 关闭模态框
|
| 281 |
+
function closeModal() {
|
| 282 |
+
modal.classList.add('hidden');
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
// 编辑用户
|
| 286 |
+
function editUser(email) {
|
| 287 |
+
const tkemail = tokens.find(u => u.email === email);
|
| 288 |
+
if (tkemail) {
|
| 289 |
+
openModal(tkemail);
|
| 290 |
+
}
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
// 删除用户
|
| 294 |
+
function deleteUser(email) {
|
| 295 |
+
if (confirm('确定要删除这个账户吗?')) {
|
| 296 |
+
fetch(`/api/tokens/${email}`, {
|
| 297 |
+
method: 'DELETE'
|
| 298 |
+
})
|
| 299 |
+
.then(response => response.json())
|
| 300 |
+
.then(data => {
|
| 301 |
+
if (data.success) {
|
| 302 |
+
loadTokens();
|
| 303 |
+
}
|
| 304 |
+
})
|
| 305 |
+
.catch(error => console.error('Error:', error));
|
| 306 |
+
}
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
// 表单提交处理
|
| 310 |
+
tokenForm.addEventListener('submit', function (e) {
|
| 311 |
+
e.preventDefault();
|
| 312 |
+
const Id = document.getElementById('id').value;
|
| 313 |
+
const tkemail = document.getElementById('email').value
|
| 314 |
+
const retoken = document.getElementById('ReToken').value
|
| 315 |
+
const actoken = document.getElementById('AcToken').value
|
| 316 |
+
const plus = document.getElementById('PLUS').value
|
| 317 |
+
if(!(retoken || actoken)){
|
| 318 |
+
alert('必须有一个Refresh Token或Access Token')
|
| 319 |
+
return
|
| 320 |
+
}
|
| 321 |
+
const tkData = {
|
| 322 |
+
email: document.getElementById('email').value,
|
| 323 |
+
ReToken: retoken,
|
| 324 |
+
AcToken: actoken,
|
| 325 |
+
PLUS: plus
|
| 326 |
+
};
|
| 327 |
+
|
| 328 |
+
const url = Id ? `/api/tokens/${tkemail}` : '/api/tokens';
|
| 329 |
+
const method = Id ? 'PUT' : 'POST';
|
| 330 |
+
|
| 331 |
+
fetch(url, {
|
| 332 |
+
method: method,
|
| 333 |
+
headers: {
|
| 334 |
+
'Content-Type': 'application/json'
|
| 335 |
+
},
|
| 336 |
+
body: JSON.stringify(tkData)
|
| 337 |
+
})
|
| 338 |
+
.then(response => response.json())
|
| 339 |
+
.then(data => {
|
| 340 |
+
if (data.success) {
|
| 341 |
+
closeModal();
|
| 342 |
+
loadTokens();
|
| 343 |
+
}
|
| 344 |
+
})
|
| 345 |
+
.catch(error => {
|
| 346 |
+
console.error('Error:', error);
|
| 347 |
+
});
|
| 348 |
+
|
| 349 |
+
});
|
| 350 |
+
|
| 351 |
+
// 添加用户按钮点击事件
|
| 352 |
+
addTokenBtn.addEventListener('click', () => openModal());
|
| 353 |
+
</script>
|
| 354 |
+
<script>
|
| 355 |
+
document.addEventListener('DOMContentLoaded', function () {
|
| 356 |
+
const tokenTableBody = document.getElementById('tokenTableBody');
|
| 357 |
+
const refreshBtn = document.getElementById('refreshBtn');
|
| 358 |
+
const statusMessage = document.getElementById('statusMessage');
|
| 359 |
+
const autoRefreshToggle = document.getElementById('autoRefreshToggle');
|
| 360 |
+
const refreshInterval = document.getElementById('refreshInterval');
|
| 361 |
+
const nextRefreshTime = document.getElementById('nextRefreshTime');
|
| 362 |
+
const refreshHistory = document.getElementById('refreshHistory');
|
| 363 |
+
|
| 364 |
+
function loadTokens() {
|
| 365 |
+
fetch('/get_tokens')
|
| 366 |
+
.then(response => response.json())
|
| 367 |
+
.then(data => {
|
| 368 |
+
tokens = data
|
| 369 |
+
tokenTableBody.innerHTML = '';
|
| 370 |
+
data.forEach(token => {
|
| 371 |
+
let rt = '有'
|
| 372 |
+
if (!token.refresh_token) {
|
| 373 |
+
rt = '无'
|
| 374 |
+
}
|
| 375 |
+
let recolor = token.refresh_token ? ' style="color: rgb(6, 161, 6);"' : ' style="color: rgb(204, 28, 28);"'
|
| 376 |
+
let color = token.status ? ' style="color: rgb(6, 161, 6);"' : ' style="color: rgb(204, 28, 28);"'
|
| 377 |
+
const row = document.createElement('tr');
|
| 378 |
+
row.innerHTML = `
|
| 379 |
+
<td class="truncate">${token.email}</td>
|
| 380 |
+
<td class="truncate" ${recolor}>${rt}</td>
|
| 381 |
+
<td class="truncate" ${color}>${token.status ? '有效' : '失效'}</td>
|
| 382 |
+
<td>
|
| 383 |
+
<button onclick="editUser('${token.email}')" class="text-blue-600 hover:text-blue-900 mr-4">编辑</button>
|
| 384 |
+
<button onclick="deleteUser('${token.email}')" class="text-red-600 hover:text-red-900">删除</button>
|
| 385 |
+
</td>
|
| 386 |
+
`;
|
| 387 |
+
tokenTableBody.appendChild(row);
|
| 388 |
+
});
|
| 389 |
+
})
|
| 390 |
+
.catch(error => {
|
| 391 |
+
console.error('加载 Tokens 失败:', error);
|
| 392 |
+
showStatus('加载 Tokens 失败', 'error');
|
| 393 |
+
});
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
function loadFailedTokens() {
|
| 397 |
+
fetch('/get_failed_tokens')
|
| 398 |
+
.then(response => response.json())
|
| 399 |
+
.then(data => {
|
| 400 |
+
const failedTokensDiv = document.getElementById('failedTokens');
|
| 401 |
+
failedTokensDiv.innerHTML = '';
|
| 402 |
+
data.forEach(item => {
|
| 403 |
+
const email = item.email;
|
| 404 |
+
const tokenItem = document.createElement('div');
|
| 405 |
+
tokenItem.className = 'bg-red-100 p-2 rounded';
|
| 406 |
+
tokenItem.textContent = email;
|
| 407 |
+
failedTokensDiv.appendChild(tokenItem);
|
| 408 |
+
});
|
| 409 |
+
})
|
| 410 |
+
.catch(error => {
|
| 411 |
+
console.error('加载失败Refresh Token 失败:', error);
|
| 412 |
+
showStatus('加载失败的 Tokens 失败', 'error');
|
| 413 |
+
});
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
function updateAutoRefreshUI(config) {
|
| 417 |
+
autoRefreshToggle.checked = config.auto_refresh_enabled;
|
| 418 |
+
refreshInterval.value = config.refresh_interval_days;
|
| 419 |
+
if (config.next_refresh_time) {
|
| 420 |
+
nextRefreshTime.textContent = `下次刷新时间: ${new Date(config.next_refresh_time).toLocaleString()}`;
|
| 421 |
+
} else {
|
| 422 |
+
nextRefreshTime.textContent = '自动刷新已关闭';
|
| 423 |
+
}
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
function loadAutoRefreshConfig() {
|
| 427 |
+
fetch('/get_auto_refresh_config')
|
| 428 |
+
.then(response => response.json())
|
| 429 |
+
.then(config => {
|
| 430 |
+
updateAutoRefreshUI(config);
|
| 431 |
+
})
|
| 432 |
+
.catch(error => {
|
| 433 |
+
console.error('加载自动刷新配置失败:', error);
|
| 434 |
+
showStatus('加载自动刷新配置失败', 'error');
|
| 435 |
+
});
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
function saveAutoRefreshConfig() {
|
| 439 |
+
fetch('/set_auto_refresh', {
|
| 440 |
+
method: 'POST',
|
| 441 |
+
headers: {
|
| 442 |
+
'Content-Type': 'application/json'
|
| 443 |
+
},
|
| 444 |
+
body: JSON.stringify({
|
| 445 |
+
enabled: autoRefreshToggle.checked,
|
| 446 |
+
interval: parseInt(refreshInterval.value)
|
| 447 |
+
})
|
| 448 |
+
})
|
| 449 |
+
.then(response => response.json())
|
| 450 |
+
.then(data => {
|
| 451 |
+
if (data.status === 'success') {
|
| 452 |
+
showStatus('自动刷新设置已更新', 'success');
|
| 453 |
+
loadAutoRefreshConfig();
|
| 454 |
+
} else {
|
| 455 |
+
showStatus('更新自动刷新设置失败', 'error');
|
| 456 |
+
}
|
| 457 |
+
})
|
| 458 |
+
.catch(error => {
|
| 459 |
+
showStatus('更新自动刷新设置失败', 'error');
|
| 460 |
+
});
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
function refreshTokens() {
|
| 464 |
+
refreshBtn.disabled = true;
|
| 465 |
+
fetch('/refresh_tokens', {
|
| 466 |
+
method: 'POST'
|
| 467 |
+
})
|
| 468 |
+
.then(response => response.json())
|
| 469 |
+
.then(data => {
|
| 470 |
+
if (data.status === 'success') {
|
| 471 |
+
refreshBtn.disabled = false;
|
| 472 |
+
showStatus('Tokens 刷新成功', 'success');
|
| 473 |
+
loadTokens();
|
| 474 |
+
loadRefreshHistory();
|
| 475 |
+
loadAutoRefreshConfig();
|
| 476 |
+
loadFailedTokens();
|
| 477 |
+
} else {
|
| 478 |
+
refreshBtn.disabled = false;
|
| 479 |
+
showStatus('Tokens 刷新失败: ' + data.message, 'error');
|
| 480 |
+
}
|
| 481 |
+
})
|
| 482 |
+
.catch(error => {
|
| 483 |
+
refreshBtn.disabled = false;
|
| 484 |
+
showStatus('Tokens 刷新失败', 'error');
|
| 485 |
+
});
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
function loadRefreshHistory() {
|
| 489 |
+
fetch('/refresh_history')
|
| 490 |
+
.then(response => response.json())
|
| 491 |
+
.then(data => {
|
| 492 |
+
if (data.status === 'success') {
|
| 493 |
+
refreshHistory.innerHTML = '';
|
| 494 |
+
data.history.forEach(item => {
|
| 495 |
+
const historyItem = document.createElement('div');
|
| 496 |
+
historyItem.className = 'bg-gray-100 p-2 rounded';
|
| 497 |
+
historyItem.innerHTML = `
|
| 498 |
+
<p><strong>刷新时间:</strong> ${new Date(item.timestamp).toLocaleString()}</p>
|
| 499 |
+
<p><strong>Token 数量:</strong> ${item.token_count}</p>
|
| 500 |
+
`;
|
| 501 |
+
refreshHistory.appendChild(historyItem);
|
| 502 |
+
});
|
| 503 |
+
}
|
| 504 |
+
})
|
| 505 |
+
.catch(error => {
|
| 506 |
+
console.error('加载刷新历史失败:', error);
|
| 507 |
+
showStatus('加载刷新历史失败', 'error');
|
| 508 |
+
});
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
function showStatus(message, status) {
|
| 512 |
+
statusMessage.textContent = message;
|
| 513 |
+
statusMessage.className = status === 'success' ? 'text-green-600' : 'text-red-600';
|
| 514 |
+
setTimeout(() => {
|
| 515 |
+
statusMessage.textContent = '';
|
| 516 |
+
}, 3000);
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
|
| 520 |
+
refreshBtn.addEventListener('click', refreshTokens);
|
| 521 |
+
autoRefreshToggle.addEventListener('change', saveAutoRefreshConfig);
|
| 522 |
+
refreshInterval.addEventListener('change', saveAutoRefreshConfig);
|
| 523 |
+
|
| 524 |
+
|
| 525 |
+
// 初始化
|
| 526 |
+
loadTokens();
|
| 527 |
+
loadAutoRefreshConfig();
|
| 528 |
+
loadRefreshHistory();
|
| 529 |
+
loadFailedTokens();
|
| 530 |
+
});
|
| 531 |
+
</script>
|
| 532 |
+
{% endblock %}
|
templates/base.html
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>{% block title %}管理页面{% endblock %}</title>
|
| 8 |
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
|
| 9 |
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.css" rel="stylesheet">
|
| 10 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.js"></script>
|
| 11 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/javascript/javascript.min.js"></script>
|
| 12 |
+
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.png') }}">
|
| 13 |
+
{% block extra_css %}{% endblock %}
|
| 14 |
+
</head>
|
| 15 |
+
|
| 16 |
+
<body class="bg-gray-100 min-h-screen flex flex-col">
|
| 17 |
+
<!-- 顶部导航栏 -->
|
| 18 |
+
<nav class="bg-white shadow-lg">
|
| 19 |
+
<div class="max-w-7xl mx-auto px-4">
|
| 20 |
+
<div class="flex justify-between h-16">
|
| 21 |
+
<!-- 左侧 Logo 和导航链接 -->
|
| 22 |
+
<div class="flex">
|
| 23 |
+
<div class="flex-shrink-0 flex items-center">
|
| 24 |
+
<span class="text-xl font-bold text-gray-800">后台管理</span>
|
| 25 |
+
</div>
|
| 26 |
+
|
| 27 |
+
<!-- 导航链接 -->
|
| 28 |
+
<div class="hidden md:ml-6 md:flex md:space-x-4">
|
| 29 |
+
{% if session.get('role') == 'admin' %}
|
| 30 |
+
<a href="{{ url_for('chatgpt') }}"
|
| 31 |
+
class="{% if request.endpoint == 'chatgpt' %}bg-gray-900 text-white{% else %}text-gray-900 hover:bg-gray-700 hover:text-white{% endif %} px-3 py-2 rounded-md text-sm font-medium flex items-center">
|
| 32 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" viewBox="0 0 20 20"
|
| 33 |
+
fill="currentColor">
|
| 34 |
+
<path
|
| 35 |
+
d="M10.394 2.08a1 1 0 00-.788 0l-7 3a1 1 0 000 1.84L5.25 8.051a.999.999 0 01.356-.257l4-1.714a1 1 0 11.788 1.838L7.667 9.088l1.94.831a1 1 0 00.787 0l7-3a1 1 0 000-1.838l-7-3zM3.31 9.397L5 10.12v4.102a8.969 8.969 0 00-1.05-.174 1 1 0 01-.89-.89 11.115 11.115 0 01.25-3.762zM9.3 16.573A9.026 9.026 0 007 14.935v-3.957l1.818.78a3 3 0 002.364 0l5.508-2.361a11.026 11.026 0 01.25 3.762 1 1 0 01-.89.89 8.968 8.968 0 00-5.35 2.524 1 1 0 01-1.4 0zM6 18a1 1 0 001-1v-2.065a8.935 8.935 0 00-2-.712V17a1 1 0 001 1z" />
|
| 36 |
+
</svg>
|
| 37 |
+
ChatGPT
|
| 38 |
+
</a>
|
| 39 |
+
<a href="{{ url_for('claude') }}"
|
| 40 |
+
class="{% if request.endpoint == 'claude' %}bg-gray-900 text-white{% else %}text-gray-900 hover:bg-gray-700 hover:text-white{% endif %} px-3 py-2 rounded-md text-sm font-medium flex items-center">
|
| 41 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" viewBox="0 0 20 20"
|
| 42 |
+
fill="currentColor">
|
| 43 |
+
<path
|
| 44 |
+
d="M10.394 2.08a1 1 0 00-.788 0l-7 3a1 1 0 000 1.84L5.25 8.051a.999.999 0 01.356-.257l4-1.714a1 1 0 11.788 1.838L7.667 9.088l1.94.831a1 1 0 00.787 0l7-3a1 1 0 000-1.838l-7-3zM3.31 9.397L5 10.12v4.102a8.969 8.969 0 00-1.05-.174 1 1 0 01-.89-.89 11.115 11.115 0 01.25-3.762zM9.3 16.573A9.026 9.026 0 007 14.935v-3.957l1.818.78a3 3 0 002.364 0l5.508-2.361a11.026 11.026 0 01.25 3.762 1 1 0 01-.89.89 8.968 8.968 0 00-5.35 2.524 1 1 0 01-1.4 0zM6 18a1 1 0 001-1v-2.065a8.935 8.935 0 00-2-.712V17a1 1 0 001 1z" />
|
| 45 |
+
</svg>
|
| 46 |
+
Claude
|
| 47 |
+
</a>
|
| 48 |
+
<a href="{{ url_for('user_management') }}"
|
| 49 |
+
class="{% if request.endpoint == 'user_management' %}bg-gray-900 text-white{% else %}text-gray-900 hover:bg-gray-700 hover:text-white{% endif %} px-3 py-2 rounded-md text-sm font-medium flex items-center">
|
| 50 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" viewBox="0 0 20 20"
|
| 51 |
+
fill="currentColor">
|
| 52 |
+
<path
|
| 53 |
+
d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
|
| 54 |
+
</svg>
|
| 55 |
+
用户管理
|
| 56 |
+
</a>
|
| 57 |
+
|
| 58 |
+
{% endif %}
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
|
| 62 |
+
<!-- 右侧用户信息和退出按钮 -->
|
| 63 |
+
<div class="flex items-center">
|
| 64 |
+
{% if session.get('logged_in') %}
|
| 65 |
+
<div class="flex items-center">
|
| 66 |
+
<span class="text-gray-700 mr-4">
|
| 67 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline-block mr-1"
|
| 68 |
+
viewBox="0 0 20 20" fill="currentColor">
|
| 69 |
+
<path fill-rule="evenodd"
|
| 70 |
+
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-6-3a2 2 0 11-4 0 2 2 0 014 0zm-2 4a5 5 0 00-4.546 2.916A5.986 5.986 0 0010 16a5.986 5.986 0 004.546-2.084A5 5 0 0010 11z"
|
| 71 |
+
clip-rule="evenodd" />
|
| 72 |
+
</svg>
|
| 73 |
+
{{ session.get('username') }}
|
| 74 |
+
{% if session.get('role') == 'admin' %}
|
| 75 |
+
<span
|
| 76 |
+
class="bg-blue-100 text-blue-800 text-xs font-medium mr-2 px-2 py-0.5 rounded">管理员</span>
|
| 77 |
+
{% endif %}
|
| 78 |
+
</span>
|
| 79 |
+
<a href="{{ url_for('logout') }}"
|
| 80 |
+
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded flex items-center">
|
| 81 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" viewBox="0 0 20 20"
|
| 82 |
+
fill="currentColor">
|
| 83 |
+
<path fill-rule="evenodd"
|
| 84 |
+
d="M3 3a1 1 0 011 1v12a1 1 0 11-2 0V4a1 1 0 011-1zm7.707 3.293a1 1 0 010 1.414L9.414 9H17a1 1 0 110 2H9.414l1.293 1.293a1 1 0 01-1.414 1.414l-3-3a1 1 0 010-1.414l3-3a1 1 0 011.414 0z"
|
| 85 |
+
clip-rule="evenodd" />
|
| 86 |
+
</svg>
|
| 87 |
+
退出
|
| 88 |
+
</a>
|
| 89 |
+
</div>
|
| 90 |
+
{% endif %}
|
| 91 |
+
</div>
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
|
| 95 |
+
<!-- 移动端菜单按钮 -->
|
| 96 |
+
<div class="md:hidden">
|
| 97 |
+
<button type="button"
|
| 98 |
+
class="mobile-menu-button inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
|
| 99 |
+
aria-expanded="false">
|
| 100 |
+
<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
| 101 |
+
stroke="currentColor">
|
| 102 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
| 103 |
+
</svg>
|
| 104 |
+
</button>
|
| 105 |
+
</div>
|
| 106 |
+
</nav>
|
| 107 |
+
|
| 108 |
+
<!-- 移动端导航菜单 -->
|
| 109 |
+
<div class="mobile-menu hidden md:hidden">
|
| 110 |
+
<div class="px-2 pt-2 pb-3 space-y-1">
|
| 111 |
+
{% if session.get('role') == 'admin' %}
|
| 112 |
+
<a href="{{ url_for('chatgpt') }}"
|
| 113 |
+
class="{% if request.endpoint == 'chatgpt' %}bg-gray-900 text-white{% else %}text-gray-900 hover:bg-gray-700 hover:text-white{% endif %} block px-3 py-2 rounded-md text-base font-medium">
|
| 114 |
+
ChatGPT
|
| 115 |
+
</a>
|
| 116 |
+
<a href="{{ url_for('claude') }}"
|
| 117 |
+
class="{% if request.endpoint == 'claude' %}bg-gray-900 text-white{% else %}text-gray-900 hover:bg-gray-700 hover:text-white{% endif %} block px-3 py-2 rounded-md text-base font-medium">
|
| 118 |
+
Claude
|
| 119 |
+
</a>
|
| 120 |
+
<a href="{{ url_for('user_management') }}"
|
| 121 |
+
class="{% if request.endpoint == 'user_management' %}bg-gray-900 text-white{% else %}text-gray-900 hover:bg-gray-700 hover:text-white{% endif %} block px-3 py-2 rounded-md text-base font-medium">
|
| 122 |
+
用户管理
|
| 123 |
+
</a>
|
| 124 |
+
{% endif %}
|
| 125 |
+
</div>
|
| 126 |
+
</div>
|
| 127 |
+
|
| 128 |
+
<!-- 主要内容区域 -->
|
| 129 |
+
<main class="flex-grow container mx-auto px-4 py-8">
|
| 130 |
+
{% with messages = get_flashed_messages(with_categories=true) %}
|
| 131 |
+
{% if messages %}
|
| 132 |
+
{% for category, message in messages %}
|
| 133 |
+
<div class="mb-4 p-4 rounded
|
| 134 |
+
{% if category == 'success' %}bg-green-100 text-green-700
|
| 135 |
+
{% elif category == 'danger' %}bg-red-100 text-red-700
|
| 136 |
+
{% else %}bg-blue-100 text-blue-700{% endif %}">
|
| 137 |
+
{{ message }}
|
| 138 |
+
</div>
|
| 139 |
+
{% endfor %}
|
| 140 |
+
{% endif %}
|
| 141 |
+
{% endwith %}
|
| 142 |
+
|
| 143 |
+
{% block content %}{% endblock %}
|
| 144 |
+
</main>
|
| 145 |
+
|
| 146 |
+
{% block extra_js %}{% endblock %}
|
| 147 |
+
|
| 148 |
+
<script>
|
| 149 |
+
// 移动端菜单切换
|
| 150 |
+
document.querySelector('.mobile-menu-button').addEventListener('click', function () {
|
| 151 |
+
document.querySelector('.mobile-menu').classList.toggle('hidden');
|
| 152 |
+
});
|
| 153 |
+
</script>
|
| 154 |
+
</body>
|
| 155 |
+
|
| 156 |
+
</html>
|
templates/claude.html
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Token管理{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block extra_css %}
|
| 6 |
+
<style>
|
| 7 |
+
.token-list {
|
| 8 |
+
width: 100%;
|
| 9 |
+
border-collapse: collapse;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
.token-list th,
|
| 13 |
+
.token-list td {
|
| 14 |
+
border: 1px solid #e2e8f0;
|
| 15 |
+
padding: 8px;
|
| 16 |
+
text-align: left;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
.token-list th {
|
| 20 |
+
background-color: #f8fafc;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.token-list tr:nth-child(even) {
|
| 24 |
+
background-color: #f1f5f9;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.truncate {
|
| 28 |
+
max-width: 200px;
|
| 29 |
+
white-space: nowrap;
|
| 30 |
+
overflow: hidden;
|
| 31 |
+
text-overflow: ellipsis;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
h2 {
|
| 35 |
+
position: relative;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
#addTokenBtn {
|
| 39 |
+
position: absolute;
|
| 40 |
+
font-size: 1rem;
|
| 41 |
+
right: 0;
|
| 42 |
+
top: 50%;
|
| 43 |
+
transform: translateY(-50%);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
button:disabled {
|
| 47 |
+
color: #ccc;
|
| 48 |
+
background-color: #eee;
|
| 49 |
+
border: none;
|
| 50 |
+
cursor: not-allowed;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
button:disabled:hover {
|
| 54 |
+
color: #ccc;
|
| 55 |
+
background-color: #eee;
|
| 56 |
+
border: none;
|
| 57 |
+
cursor: not-allowed;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
@media (max-width: 770px) {
|
| 61 |
+
body {
|
| 62 |
+
font-size: 2.3vw;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
h2 {
|
| 66 |
+
font-size: 3.5vw !important;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
#addTokenBtn {
|
| 70 |
+
font-size: 2vw;
|
| 71 |
+
line-height: 2vw;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.token-list {
|
| 75 |
+
flex: 1;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.token-list thead {
|
| 79 |
+
display: flex;
|
| 80 |
+
flex-wrap: wrap;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.token-list #tokenTableBody {
|
| 84 |
+
display: flex;
|
| 85 |
+
flex-wrap: wrap;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.token-list tr {
|
| 89 |
+
display: flex;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.token-list tr,
|
| 93 |
+
.token-list th,
|
| 94 |
+
.token-list td {
|
| 95 |
+
flex: 1;
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
</style>
|
| 99 |
+
{% endblock %}
|
| 100 |
+
|
| 101 |
+
{% block content %}
|
| 102 |
+
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
| 103 |
+
<h2 class="text-2xl font-bold text-gray-800 mb-4">账号列表
|
| 104 |
+
<button id="addTokenBtn"
|
| 105 |
+
class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline transition duration-300 ease-in-out mr-2">
|
| 106 |
+
添加账号
|
| 107 |
+
</button>
|
| 108 |
+
</h2>
|
| 109 |
+
<table class="token-list">
|
| 110 |
+
<thead>
|
| 111 |
+
<tr>
|
| 112 |
+
<th>Email</th>
|
| 113 |
+
<th>Sk Token</th>
|
| 114 |
+
<th>状态</th>
|
| 115 |
+
<th>操作</th>
|
| 116 |
+
</tr>
|
| 117 |
+
</thead>
|
| 118 |
+
<tbody id="tokenTableBody">
|
| 119 |
+
<!-- Token 数据将在这里动态添加 -->
|
| 120 |
+
</tbody>
|
| 121 |
+
</table>
|
| 122 |
+
<!-- <div class="flex items-center justify-between mt-4">
|
| 123 |
+
<div>
|
| 124 |
+
<button id="refreshBtn"
|
| 125 |
+
class="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline transition duration-300 ease-in-out mr-2">
|
| 126 |
+
立即刷新 Tokens
|
| 127 |
+
</button>
|
| 128 |
+
</div>
|
| 129 |
+
<p id="statusMessage" class="italic"></p>
|
| 130 |
+
</div> -->
|
| 131 |
+
</div>
|
| 132 |
+
|
| 133 |
+
<div id="tokenModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden">
|
| 134 |
+
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
| 135 |
+
<div class="mt-3">
|
| 136 |
+
<h3 class="text-lg font-medium text-gray-900 mb-4">账号信息</h3>
|
| 137 |
+
<form id="tokenForm">
|
| 138 |
+
<input type="hidden" id="id">
|
| 139 |
+
<div class="mb-4">
|
| 140 |
+
<label class="block text-gray-700 text-sm font-bold mb-2" for="email">
|
| 141 |
+
邮箱
|
| 142 |
+
</label>
|
| 143 |
+
<input
|
| 144 |
+
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
| 145 |
+
id="email" type="text" required placeholder="仅用于标识">
|
| 146 |
+
</div>
|
| 147 |
+
<div class="mb-4">
|
| 148 |
+
<label class="block text-gray-700 text-sm font-bold mb-2" for="skToken">
|
| 149 |
+
skToken
|
| 150 |
+
</label>
|
| 151 |
+
<input
|
| 152 |
+
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
| 153 |
+
id="skToken" type="text" placeholder="必填" required>
|
| 154 |
+
</div>
|
| 155 |
+
<div class="mb-4">
|
| 156 |
+
<label class="block text-gray-700 text-sm font-bold mb-2" for="PLUS">
|
| 157 |
+
PLUS
|
| 158 |
+
</label>
|
| 159 |
+
<select
|
| 160 |
+
class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
| 161 |
+
id="PLUS" required>
|
| 162 |
+
<option value='false'>false</option>
|
| 163 |
+
<option value='true'>true</option>
|
| 164 |
+
</select>
|
| 165 |
+
</div>
|
| 166 |
+
<div class="flex items-center justify-end">
|
| 167 |
+
<button type="button" onclick="closeModal()"
|
| 168 |
+
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline mr-2">
|
| 169 |
+
取消
|
| 170 |
+
</button>
|
| 171 |
+
<button type="submit"
|
| 172 |
+
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
| 173 |
+
保存
|
| 174 |
+
</button>
|
| 175 |
+
</div>
|
| 176 |
+
</form>
|
| 177 |
+
</div>
|
| 178 |
+
</div>
|
| 179 |
+
</div>
|
| 180 |
+
|
| 181 |
+
</div>
|
| 182 |
+
{% endblock %}
|
| 183 |
+
{% block extra_js %}
|
| 184 |
+
|
| 185 |
+
<script>
|
| 186 |
+
|
| 187 |
+
function loadTokens() {
|
| 188 |
+
fetch('/get_Claude')
|
| 189 |
+
.then(response => response.json())
|
| 190 |
+
.then(data => {
|
| 191 |
+
tokens = data
|
| 192 |
+
tokenTableBody.innerHTML = '';
|
| 193 |
+
data.forEach(token => {
|
| 194 |
+
let rt = '有'
|
| 195 |
+
if (!token.skToken) {
|
| 196 |
+
rt = '无'
|
| 197 |
+
}
|
| 198 |
+
let recolor = token.skToken ? ' style="color: rgb(6, 161, 6);"' : ' style="color: rgb(204, 28, 28);"'
|
| 199 |
+
let color = token.status ? ' style="color: rgb(6, 161, 6);"' : ' style="color: rgb(204, 28, 28);"'
|
| 200 |
+
const row = document.createElement('tr');
|
| 201 |
+
row.innerHTML = `
|
| 202 |
+
<td class="truncate">${token.email}</td>
|
| 203 |
+
<td class="truncate" ${recolor}>${rt}</td>
|
| 204 |
+
<td class="truncate" ${color}>${token.status ? '有效' : '失效'}</td>
|
| 205 |
+
<td>
|
| 206 |
+
<button onclick="editUser('${token.email}')" class="text-blue-600 hover:text-blue-900 mr-4">编辑</button>
|
| 207 |
+
<button onclick="deleteUser('${token.email}')" class="text-red-600 hover:text-red-900">删除</button>
|
| 208 |
+
</td>
|
| 209 |
+
`;
|
| 210 |
+
tokenTableBody.appendChild(row);
|
| 211 |
+
});
|
| 212 |
+
})
|
| 213 |
+
.catch(error => {
|
| 214 |
+
console.error('加载 Tokens 失败:', error);
|
| 215 |
+
showStatus('加载 Tokens 失败', 'error');
|
| 216 |
+
});
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
let tokens = []
|
| 220 |
+
const modal = document.getElementById('tokenModal');
|
| 221 |
+
const tokenForm = document.getElementById('tokenForm');
|
| 222 |
+
const addTokenBtn = document.getElementById('addTokenBtn');
|
| 223 |
+
|
| 224 |
+
// 打开模态框
|
| 225 |
+
function openModal(email = null) {
|
| 226 |
+
const form = document.getElementById('tokenForm');
|
| 227 |
+
if (email) {
|
| 228 |
+
document.getElementById('id').value = 'id'
|
| 229 |
+
document.getElementById('email').value = email.email;
|
| 230 |
+
document.getElementById('skToken').value = email.skToken;
|
| 231 |
+
document.getElementById('PLUS').value = email.PLUS;
|
| 232 |
+
} else {
|
| 233 |
+
form.reset();
|
| 234 |
+
document.getElementById('id').value = ''
|
| 235 |
+
}
|
| 236 |
+
modal.classList.remove('hidden');
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
// 关闭模态框
|
| 240 |
+
function closeModal() {
|
| 241 |
+
modal.classList.add('hidden');
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
// 编辑用户
|
| 245 |
+
function editUser(email) {
|
| 246 |
+
const tkemail = tokens.find(u => u.email === email);
|
| 247 |
+
if (tkemail) {
|
| 248 |
+
openModal(tkemail);
|
| 249 |
+
}
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
// 删除用户
|
| 253 |
+
function deleteUser(email) {
|
| 254 |
+
if (confirm('确定要删除这个账户吗?')) {
|
| 255 |
+
fetch(`/api/Claude/${email}`, {
|
| 256 |
+
method: 'DELETE'
|
| 257 |
+
})
|
| 258 |
+
.then(response => response.json())
|
| 259 |
+
.then(data => {
|
| 260 |
+
if (data.success) {
|
| 261 |
+
loadTokens();
|
| 262 |
+
}
|
| 263 |
+
})
|
| 264 |
+
.catch(error => console.error('Error:', error));
|
| 265 |
+
}
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
// 表单提交处理
|
| 269 |
+
tokenForm.addEventListener('submit', function (e) {
|
| 270 |
+
e.preventDefault();
|
| 271 |
+
const Id = document.getElementById('id').value;
|
| 272 |
+
const tkemail = document.getElementById('email').value
|
| 273 |
+
const skToken = document.getElementById('skToken').value
|
| 274 |
+
const plus = document.getElementById('PLUS').value
|
| 275 |
+
const tkData = {
|
| 276 |
+
email: document.getElementById('email').value,
|
| 277 |
+
SkToken: skToken,
|
| 278 |
+
PLUS: plus
|
| 279 |
+
};
|
| 280 |
+
|
| 281 |
+
const url = Id ? `/api/Claude/${tkemail}` : '/api/Claude';
|
| 282 |
+
const method = Id ? 'PUT' : 'POST';
|
| 283 |
+
|
| 284 |
+
fetch(url, {
|
| 285 |
+
method: method,
|
| 286 |
+
headers: {
|
| 287 |
+
'Content-Type': 'application/json'
|
| 288 |
+
},
|
| 289 |
+
body: JSON.stringify(tkData)
|
| 290 |
+
})
|
| 291 |
+
.then(response => response.json())
|
| 292 |
+
.then(data => {
|
| 293 |
+
if (data.success) {
|
| 294 |
+
closeModal();
|
| 295 |
+
loadTokens();
|
| 296 |
+
}
|
| 297 |
+
})
|
| 298 |
+
.catch(error => console.error('Error:', error));
|
| 299 |
+
});
|
| 300 |
+
|
| 301 |
+
// 添加用户按钮点击事件
|
| 302 |
+
addTokenBtn.addEventListener('click', () => openModal());
|
| 303 |
+
</script>
|
| 304 |
+
<script>
|
| 305 |
+
document.addEventListener('DOMContentLoaded', function () {
|
| 306 |
+
const tokenTableBody = document.getElementById('tokenTableBody');
|
| 307 |
+
const refreshBtn = document.getElementById('refreshBtn');
|
| 308 |
+
const statusMessage = document.getElementById('statusMessage');
|
| 309 |
+
const autoRefreshToggle = document.getElementById('autoRefreshToggle');
|
| 310 |
+
const refreshInterval = document.getElementById('refreshInterval');
|
| 311 |
+
const nextRefreshTime = document.getElementById('nextRefreshTime');
|
| 312 |
+
const refreshHistory = document.getElementById('refreshHistory');
|
| 313 |
+
|
| 314 |
+
function loadTokens() {
|
| 315 |
+
fetch('/get_Claude')
|
| 316 |
+
.then(response => response.json())
|
| 317 |
+
.then(data => {
|
| 318 |
+
tokens = data
|
| 319 |
+
tokenTableBody.innerHTML = '';
|
| 320 |
+
data.forEach(token => {
|
| 321 |
+
let rt = '有'
|
| 322 |
+
if (!token.skToken) {
|
| 323 |
+
rt = '无'
|
| 324 |
+
}
|
| 325 |
+
let recolor = token.skToken ? ' style="color: rgb(6, 161, 6);"' : ' style="color: rgb(204, 28, 28);"'
|
| 326 |
+
let color = token.status ? ' style="color: rgb(6, 161, 6);"' : ' style="color: rgb(204, 28, 28);"'
|
| 327 |
+
const row = document.createElement('tr');
|
| 328 |
+
row.innerHTML = `
|
| 329 |
+
<td class="truncate">${token.email}</td>
|
| 330 |
+
<td class="truncate" ${recolor}>${rt}</td>
|
| 331 |
+
<td class="truncate" ${color}>${token.status ? '有效' : '失效'}</td>
|
| 332 |
+
<td>
|
| 333 |
+
<button onclick="editUser('${token.email}')" class="text-blue-600 hover:text-blue-900 mr-4">编辑</button>
|
| 334 |
+
<button onclick="deleteUser('${token.email}')" class="text-red-600 hover:text-red-900">删除</button>
|
| 335 |
+
</td>
|
| 336 |
+
`;
|
| 337 |
+
tokenTableBody.appendChild(row);
|
| 338 |
+
});
|
| 339 |
+
})
|
| 340 |
+
.catch(error => {
|
| 341 |
+
console.error('加载 Tokens 失败:', error);
|
| 342 |
+
showStatus('加载 Tokens 失败', 'error');
|
| 343 |
+
});
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
// 初始化
|
| 347 |
+
loadTokens();
|
| 348 |
+
});
|
| 349 |
+
</script>
|
| 350 |
+
{% endblock %}
|
templates/login.html
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>登录 | GPT共享</title>
|
| 8 |
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
|
| 9 |
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
| 10 |
+
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.png') }}">
|
| 11 |
+
<style>
|
| 12 |
+
@keyframes fadeIn {
|
| 13 |
+
from { opacity: 0; transform: translateY(-20px); }
|
| 14 |
+
to { opacity: 1; transform: translateY(0); }
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
.animate-fade-in {
|
| 18 |
+
animation: fadeIn 0.5s ease-out forwards;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
.form-input {
|
| 22 |
+
transition: all 0.3s ease;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
.form-input:focus {
|
| 26 |
+
transform: translateY(-2px);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
.login-button {
|
| 30 |
+
transition: all 0.3s ease;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
.login-button:hover {
|
| 34 |
+
transform: translateY(-2px);
|
| 35 |
+
box-shadow: 0 4px 12px rgba(66, 153, 225, 0.3);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.alert {
|
| 39 |
+
animation: slideIn 0.5s ease-out forwards;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
@keyframes slideIn {
|
| 43 |
+
from { transform: translateX(-100%); opacity: 0; }
|
| 44 |
+
to { transform: translateX(0); opacity: 1; }
|
| 45 |
+
}
|
| 46 |
+
</style>
|
| 47 |
+
</head>
|
| 48 |
+
<body class="bg-gradient-to-br from-blue-50 to-indigo-100 min-h-screen flex items-center justify-center p-4">
|
| 49 |
+
<!-- 登录卡片 -->
|
| 50 |
+
<div class="bg-white rounded-2xl shadow-xl w-full max-w-md p-8 animate-fade-in">
|
| 51 |
+
<!-- Logo和标题 -->
|
| 52 |
+
<div class="text-center mb-8">
|
| 53 |
+
<div class="inline-block p-3 rounded-full bg-blue-50 mb-4">
|
| 54 |
+
<i class="fas fa-user-circle text-4xl text-blue-500"></i>
|
| 55 |
+
</div>
|
| 56 |
+
<h1 class="text-2xl font-bold text-gray-800">欢迎回来</h1>
|
| 57 |
+
<p class="text-gray-500 mt-2">请登录您的账号</p>
|
| 58 |
+
</div>
|
| 59 |
+
|
| 60 |
+
<!-- Flash messages -->
|
| 61 |
+
{% with messages = get_flashed_messages(with_categories=true) %}
|
| 62 |
+
{% if messages %}
|
| 63 |
+
{% for category, message in messages %}
|
| 64 |
+
<div class="alert mb-4 p-4 rounded-lg {% if category == 'error' %}bg-red-100 text-red-700 border-l-4 border-red-500{% else %}bg-green-100 text-green-700 border-l-4 border-green-500{% endif %}">
|
| 65 |
+
<div class="flex items-center">
|
| 66 |
+
<div class="flex-shrink-0">
|
| 67 |
+
{% if category == 'error' %}
|
| 68 |
+
<i class="fas fa-exclamation-circle"></i>
|
| 69 |
+
{% else %}
|
| 70 |
+
<i class="fas fa-check-circle"></i>
|
| 71 |
+
{% endif %}
|
| 72 |
+
</div>
|
| 73 |
+
<div class="ml-3">
|
| 74 |
+
<p class="text-sm">{{ message }}</p>
|
| 75 |
+
</div>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
{% endfor %}
|
| 79 |
+
{% endif %}
|
| 80 |
+
{% endwith %}
|
| 81 |
+
|
| 82 |
+
<!-- 登录表单 -->
|
| 83 |
+
<form action="{{ url_for('login') }}" method="post" class="space-y-6">
|
| 84 |
+
<div>
|
| 85 |
+
<div class="relative">
|
| 86 |
+
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
| 87 |
+
<i class="fas fa-user text-gray-400"></i>
|
| 88 |
+
</div>
|
| 89 |
+
<input
|
| 90 |
+
type="text"
|
| 91 |
+
name="username"
|
| 92 |
+
placeholder="用户名"
|
| 93 |
+
required
|
| 94 |
+
class="form-input w-full pl-10 pr-4 py-3 rounded-lg border border-gray-300 focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-200 text-gray-700"
|
| 95 |
+
>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
|
| 99 |
+
<div>
|
| 100 |
+
<div class="relative">
|
| 101 |
+
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
| 102 |
+
<i class="fas fa-lock text-gray-400"></i>
|
| 103 |
+
</div>
|
| 104 |
+
<input
|
| 105 |
+
type="password"
|
| 106 |
+
name="password"
|
| 107 |
+
placeholder="密码"
|
| 108 |
+
required
|
| 109 |
+
class="form-input w-full pl-10 pr-4 py-3 rounded-lg border border-gray-300 focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-200 text-gray-700"
|
| 110 |
+
>
|
| 111 |
+
</div>
|
| 112 |
+
</div>
|
| 113 |
+
<!-- 记住我选项 -->
|
| 114 |
+
<div class="flex items-center">
|
| 115 |
+
<input
|
| 116 |
+
type="checkbox"
|
| 117 |
+
id="remember_me"
|
| 118 |
+
name="remember_me"
|
| 119 |
+
class="h-4 w-4 text-blue-500 focus:ring-blue-500 border-gray-300 rounded"
|
| 120 |
+
>
|
| 121 |
+
<label for="remember_me" class="ml-2 block text-sm text-gray-700">
|
| 122 |
+
记住我
|
| 123 |
+
</label>
|
| 124 |
+
</div>
|
| 125 |
+
<!-- 下拉菜单:选择登录到 GPT 或者 Claude -->
|
| 126 |
+
<div class="relative">
|
| 127 |
+
<select
|
| 128 |
+
name="login_target"
|
| 129 |
+
required
|
| 130 |
+
class="form-input w-full py-3 pl-4 pr-4 rounded-lg border border-gray-300 focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-200 text-gray-700"
|
| 131 |
+
>
|
| 132 |
+
<option value="" disabled selected>请选择登录目标</option>
|
| 133 |
+
<option value="manage">登录到管理端</option>
|
| 134 |
+
<option value="gpt">登录到 GPT</option>
|
| 135 |
+
<option value="claude">登录到 Claude</option>
|
| 136 |
+
|
| 137 |
+
</select>
|
| 138 |
+
</div>
|
| 139 |
+
<button
|
| 140 |
+
type="submit"
|
| 141 |
+
class="login-button w-full bg-blue-500 hover:bg-blue-600 text-white font-semibold py-3 px-6 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50"
|
| 142 |
+
>
|
| 143 |
+
登录
|
| 144 |
+
<i class="fas fa-arrow-right ml-2"></i>
|
| 145 |
+
</button>
|
| 146 |
+
</form>
|
| 147 |
+
|
| 148 |
+
<!-- 额外链接 -->
|
| 149 |
+
<div class="mt-6 text-center text-sm">
|
| 150 |
+
<a href="mailto:your@email.com" class="text-blue-500 hover:text-blue-600 font-medium">联系管理员</a>
|
| 151 |
+
</div>
|
| 152 |
+
|
| 153 |
+
<!-- 页脚 -->
|
| 154 |
+
<div class="mt-8 text-center text-sm text-gray-500">
|
| 155 |
+
<p>© 2024 ChatGPT共享. Created by <a style="color: rgb(106, 149, 228);" href="https://github.com/h88782481/Chat-Share" target="_blank" rel="noopener noreferrer">Baimo</a></p>
|
| 156 |
+
</div>
|
| 157 |
+
</div>
|
| 158 |
+
<!-- JavaScript for Remember Me functionality -->
|
| 159 |
+
<script>
|
| 160 |
+
// 页面加载时检查并填充保存的用户信息
|
| 161 |
+
window.onload = function() {
|
| 162 |
+
const savedUsername = localStorage.getItem('rememberedUsername');
|
| 163 |
+
const savedLoginTarget = localStorage.getItem('rememberedLoginTarget');
|
| 164 |
+
const rememberMe = localStorage.getItem('rememberMe') === 'true';
|
| 165 |
+
|
| 166 |
+
if (savedUsername && rememberMe) {
|
| 167 |
+
document.getElementById('username').value = savedUsername;
|
| 168 |
+
document.getElementById('remember_me').checked = true;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
if (savedLoginTarget && rememberMe) {
|
| 172 |
+
document.getElementById('login_target').value = savedLoginTarget;
|
| 173 |
+
}
|
| 174 |
+
};
|
| 175 |
+
|
| 176 |
+
// 表单提交时保存用户信息
|
| 177 |
+
document.getElementById('loginForm').addEventListener('submit', function() {
|
| 178 |
+
const rememberMe = document.getElementById('remember_me').checked;
|
| 179 |
+
const username = document.getElementById('username').value;
|
| 180 |
+
const loginTarget = document.getElementById('login_target').value;
|
| 181 |
+
|
| 182 |
+
if (rememberMe) {
|
| 183 |
+
localStorage.setItem('rememberedUsername', username);
|
| 184 |
+
localStorage.setItem('rememberedLoginTarget', loginTarget);
|
| 185 |
+
localStorage.setItem('rememberMe', 'true');
|
| 186 |
+
} else {
|
| 187 |
+
localStorage.removeItem('rememberedUsername');
|
| 188 |
+
localStorage.removeItem('rememberedLoginTarget');
|
| 189 |
+
localStorage.setItem('rememberMe', 'false');
|
| 190 |
+
}
|
| 191 |
+
});
|
| 192 |
+
</script>
|
| 193 |
+
</body>
|
| 194 |
+
</html>
|
templates/user_management.html
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}用户管理{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
| 7 |
+
<div class="flex justify-between items-center mb-6">
|
| 8 |
+
<h2 class="text-2xl font-bold text-gray-800">用户列表</h2>
|
| 9 |
+
<button id="sycGatewayBtn" class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded">
|
| 10 |
+
一键同步GPT网关
|
| 11 |
+
</button>
|
| 12 |
+
<button id="addUserBtn" class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded">
|
| 13 |
+
添加用户
|
| 14 |
+
</button>
|
| 15 |
+
</div>
|
| 16 |
+
|
| 17 |
+
<!-- 用户列表 -->
|
| 18 |
+
<table class="min-w-full">
|
| 19 |
+
<thead>
|
| 20 |
+
<tr>
|
| 21 |
+
<th
|
| 22 |
+
class="px-6 py-3 border-b-2 border-gray-200 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
| 23 |
+
用户名
|
| 24 |
+
</th>
|
| 25 |
+
<th
|
| 26 |
+
class="px-6 py-3 border-b-2 border-gray-200 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
| 27 |
+
角色
|
| 28 |
+
</th>
|
| 29 |
+
<th
|
| 30 |
+
class="px-6 py-3 border-b-2 border-gray-200 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
| 31 |
+
绑定GPT账号
|
| 32 |
+
</th>
|
| 33 |
+
<th
|
| 34 |
+
class="px-6 py-3 border-b-2 border-gray-200 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
| 35 |
+
GPT过期时间
|
| 36 |
+
</th>
|
| 37 |
+
<th
|
| 38 |
+
class="px-6 py-3 border-b-2 border-gray-200 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
| 39 |
+
绑定Claude账号
|
| 40 |
+
</th>
|
| 41 |
+
<th
|
| 42 |
+
class="px-6 py-3 border-b-2 border-gray-200 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
| 43 |
+
Claude过期时间
|
| 44 |
+
</th>
|
| 45 |
+
<th
|
| 46 |
+
class="px-6 py-3 border-b-2 border-gray-200 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
| 47 |
+
操作
|
| 48 |
+
</th>
|
| 49 |
+
</tr>
|
| 50 |
+
</thead>
|
| 51 |
+
<tbody id="userTableBody">
|
| 52 |
+
<!-- 用户数据将动态插入这里 -->
|
| 53 |
+
</tbody>
|
| 54 |
+
</table>
|
| 55 |
+
|
| 56 |
+
<!-- 用户表单模态框 -->
|
| 57 |
+
<div id="userModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden">
|
| 58 |
+
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
| 59 |
+
<div class="mt-3">
|
| 60 |
+
<h3 class="text-lg font-medium text-gray-900 mb-4">用户信息</h3>
|
| 61 |
+
<form id="userForm">
|
| 62 |
+
<input type="hidden" id="userId">
|
| 63 |
+
<div class="mb-4">
|
| 64 |
+
<label class="block text-gray-700 text-sm font-bold mb-2" for="username">
|
| 65 |
+
用户名
|
| 66 |
+
</label>
|
| 67 |
+
<input
|
| 68 |
+
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
| 69 |
+
id="username" type="text" required>
|
| 70 |
+
</div>
|
| 71 |
+
<div class="mb-4">
|
| 72 |
+
<label class="block text-gray-700 text-sm font-bold mb-2" for="password">
|
| 73 |
+
密码
|
| 74 |
+
</label>
|
| 75 |
+
<input
|
| 76 |
+
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
| 77 |
+
id="password" type="password">
|
| 78 |
+
</div>
|
| 79 |
+
<div class="mb-4">
|
| 80 |
+
<label class="block text-gray-700 text-sm font-bold mb-2" for="role">
|
| 81 |
+
角色
|
| 82 |
+
</label>
|
| 83 |
+
<select
|
| 84 |
+
class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
| 85 |
+
id="role" required>
|
| 86 |
+
<option value="admin">管理员</option>
|
| 87 |
+
<option value="user">普通用户</option>
|
| 88 |
+
</select>
|
| 89 |
+
</div>
|
| 90 |
+
<!-- 添加GPT过期时间选择 -->
|
| 91 |
+
<div class="mb-4">
|
| 92 |
+
<label class="block text-gray-700 text-sm font-bold mb-2" for="expirationDate">
|
| 93 |
+
GPT过期时间
|
| 94 |
+
</label>
|
| 95 |
+
<input
|
| 96 |
+
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
| 97 |
+
id="expirationDate" type="datetime-local">
|
| 98 |
+
</div>
|
| 99 |
+
<!-- 添加Claude过期时间选择 -->
|
| 100 |
+
<div class="mb-4">
|
| 101 |
+
<label class="block text-gray-700 text-sm font-bold mb-2" for="expirationClaude">
|
| 102 |
+
Claude过期时间
|
| 103 |
+
</label>
|
| 104 |
+
<input
|
| 105 |
+
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
| 106 |
+
id="expirationClaude" type="datetime-local">
|
| 107 |
+
</div>
|
| 108 |
+
<div class="flex items-center justify-end">
|
| 109 |
+
<button type="button" onclick="closeModal()"
|
| 110 |
+
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline mr-2">
|
| 111 |
+
取消
|
| 112 |
+
</button>
|
| 113 |
+
<button type="submit"
|
| 114 |
+
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
| 115 |
+
保存
|
| 116 |
+
</button>
|
| 117 |
+
</div>
|
| 118 |
+
</form>
|
| 119 |
+
</div>
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
|
| 123 |
+
<!-- 绑定账号模态框 -->
|
| 124 |
+
<div id="bindModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden">
|
| 125 |
+
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
| 126 |
+
<div class="mt-3">
|
| 127 |
+
<h3 class="text-lg font-medium text-gray-900 mb-4">请选择要绑定的账号</h3>
|
| 128 |
+
<form id="bindForm">
|
| 129 |
+
<input type="hidden" id="userId">
|
| 130 |
+
<div class="mb-4">
|
| 131 |
+
<select
|
| 132 |
+
class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
| 133 |
+
id="email">
|
| 134 |
+
</select>
|
| 135 |
+
</div>
|
| 136 |
+
<div class="mb-4">
|
| 137 |
+
<select
|
| 138 |
+
class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
| 139 |
+
id="claudeEmail">
|
| 140 |
+
</select>
|
| 141 |
+
</div>
|
| 142 |
+
<div class="flex items-center justify-end">
|
| 143 |
+
<button type="button" onclick="closeBindModal()"
|
| 144 |
+
class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline mr-2">
|
| 145 |
+
取消
|
| 146 |
+
</button>
|
| 147 |
+
<button type="submit"
|
| 148 |
+
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
| 149 |
+
保存
|
| 150 |
+
</button>
|
| 151 |
+
</div>
|
| 152 |
+
</form>
|
| 153 |
+
</div>
|
| 154 |
+
</div>
|
| 155 |
+
</div>
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
</div>
|
| 159 |
+
{% endblock %}
|
| 160 |
+
{% block extra_js %}
|
| 161 |
+
<script>
|
| 162 |
+
let users = [];
|
| 163 |
+
const bindModal = document.getElementById('bindModal');
|
| 164 |
+
const modal = document.getElementById('userModal');
|
| 165 |
+
const userForm = document.getElementById('userForm');
|
| 166 |
+
const bindForm = document.getElementById('bindForm');
|
| 167 |
+
const addUserBtn = document.getElementById('addUserBtn');
|
| 168 |
+
const sycGateway = document.getElementById('sycGatewayBtn');
|
| 169 |
+
// 加载用户列表
|
| 170 |
+
function loadUsers() {
|
| 171 |
+
fetch('/api/users')
|
| 172 |
+
.then(response => response.json())
|
| 173 |
+
.then(data => {
|
| 174 |
+
users = data;
|
| 175 |
+
renderUsers();
|
| 176 |
+
})
|
| 177 |
+
.catch(error => console.error('Error:', error));
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
// 渲染用户列表
|
| 181 |
+
function renderUsers() {
|
| 182 |
+
const tbody = document.getElementById('userTableBody');
|
| 183 |
+
tbody.innerHTML = '';
|
| 184 |
+
users.forEach(user => {
|
| 185 |
+
const tr = document.createElement('tr');
|
| 186 |
+
tr.innerHTML = `
|
| 187 |
+
<td class="px-6 py-4 whitespace-nowrap">${user.username}</td>
|
| 188 |
+
<td class="px-6 py-4 whitespace-nowrap">${user.role}</td>
|
| 189 |
+
<td class="px-6 py-4 whitespace-nowrap">${user.bind_email}</td>
|
| 190 |
+
<td class="px-6 py-4 whitespace-nowrap">${user.expiration_time}</td>
|
| 191 |
+
<td class="px-6 py-4 whitespace-nowrap">${user.bind_claude_email}</td>
|
| 192 |
+
<td class="px-6 py-4 whitespace-nowrap">${user.claude_expiration_time}</td>
|
| 193 |
+
<td class="px-6 py-4 whitespace-nowrap">
|
| 194 |
+
<button onclick="bindChatGPT('${user.id}')" class="text-blue-600 hover:text-blue-900 mr-4">绑定GPT</button>
|
| 195 |
+
<button onclick="bindClaude('${user.id}')" class="text-blue-600 hover:text-blue-900 mr-4">绑定Claude</button>
|
| 196 |
+
<button onclick="editUser('${user.id}')" class="text-blue-600 hover:text-blue-900 mr-4">编辑</button>
|
| 197 |
+
<button onclick="deleteUser('${user.id}')" class="text-red-600 hover:text-red-900">删除</button>
|
| 198 |
+
<button onclick="deleteBind('${user.id}')" class="text-red-600 hover:text-red-900">解绑GPT</button>
|
| 199 |
+
<button onclick="deleteClaudeBind('${user.id}')" class="text-red-600 hover:text-red-900">解绑Claude</button>
|
| 200 |
+
</td>
|
| 201 |
+
`;
|
| 202 |
+
tbody.appendChild(tr);
|
| 203 |
+
});
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
// 打开用户模态框
|
| 207 |
+
function openModal(user = null) {
|
| 208 |
+
const form = document.getElementById('userForm');
|
| 209 |
+
if (user) {
|
| 210 |
+
document.getElementById('userId').value = user.id;
|
| 211 |
+
document.getElementById('username').value = user.username;
|
| 212 |
+
document.getElementById('password').value = '';
|
| 213 |
+
document.getElementById('role').value = user.role;
|
| 214 |
+
document.getElementById('expirationDate').value = user.expiration_time;
|
| 215 |
+
document.getElementById('expirationClaude').value = user.claude_expiration_time;
|
| 216 |
+
} else {
|
| 217 |
+
form.reset();
|
| 218 |
+
document.getElementById('userId').value = '';
|
| 219 |
+
}
|
| 220 |
+
modal.classList.remove('hidden');
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
// 关闭用户模态框
|
| 224 |
+
function closeModal() {
|
| 225 |
+
modal.classList.add('hidden');
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
// 编辑用户
|
| 229 |
+
function editUser(userId) {
|
| 230 |
+
const user = users.find(u => u.id === userId);
|
| 231 |
+
if (user) {
|
| 232 |
+
openModal(user);
|
| 233 |
+
}
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
// 打开绑定模态框
|
| 237 |
+
function openBindModal(user = null,type) {
|
| 238 |
+
if(type == 1){
|
| 239 |
+
url = "/api/all_email"
|
| 240 |
+
fetch(url)
|
| 241 |
+
.then(response => response.json())
|
| 242 |
+
.then(data => {
|
| 243 |
+
const emailSelect = document.getElementById('email');
|
| 244 |
+
document.getElementById('email').classList.remove('hidden')
|
| 245 |
+
document.getElementById('claudeEmail').classList.add('hidden')
|
| 246 |
+
emailSelect.innerHTML = '';
|
| 247 |
+
document.getElementById('userId').value = user.id;
|
| 248 |
+
data.forEach(email => {
|
| 249 |
+
const option = document.createElement('option');
|
| 250 |
+
option.value = email;
|
| 251 |
+
option.textContent = email;
|
| 252 |
+
emailSelect.appendChild(option);
|
| 253 |
+
});
|
| 254 |
+
|
| 255 |
+
if (user && user.email) {
|
| 256 |
+
emailSelect.value = user.email;
|
| 257 |
+
}
|
| 258 |
+
})
|
| 259 |
+
.catch(error => console.error('Error:', error));
|
| 260 |
+
}else{
|
| 261 |
+
url = "/api/all_claude_email"
|
| 262 |
+
fetch(url)
|
| 263 |
+
.then(response => response.json())
|
| 264 |
+
.then(data => {
|
| 265 |
+
const emailSelect = document.getElementById('claudeEmail');
|
| 266 |
+
document.getElementById('claudeEmail').classList.remove('hidden')
|
| 267 |
+
document.getElementById('email').classList.add('hidden')
|
| 268 |
+
emailSelect.innerHTML = '';
|
| 269 |
+
document.getElementById('userId').value = user.id;
|
| 270 |
+
data.forEach(email => {
|
| 271 |
+
const option = document.createElement('option');
|
| 272 |
+
option.value = email;
|
| 273 |
+
option.textContent = email;
|
| 274 |
+
emailSelect.appendChild(option);
|
| 275 |
+
});
|
| 276 |
+
|
| 277 |
+
if (user && user.email) {
|
| 278 |
+
emailSelect.value = user.email;
|
| 279 |
+
}
|
| 280 |
+
})
|
| 281 |
+
.catch(error => console.error('Error:', error));
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
bindModal.classList.remove('hidden');
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
// 关闭绑定模态框
|
| 288 |
+
function closeBindModal() {
|
| 289 |
+
bindModal.classList.add('hidden');
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
// 绑定GPT账号
|
| 293 |
+
function bindChatGPT(userId) {
|
| 294 |
+
const user = users.find(u => u.id === userId);
|
| 295 |
+
if (user) {
|
| 296 |
+
openBindModal(user,1);
|
| 297 |
+
}
|
| 298 |
+
}
|
| 299 |
+
// 绑定Claude账号
|
| 300 |
+
function bindClaude(userId) {
|
| 301 |
+
const user = users.find(u => u.id === userId);
|
| 302 |
+
if (user) {
|
| 303 |
+
openBindModal(user,0);
|
| 304 |
+
}
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
// 删除用户
|
| 309 |
+
function deleteUser(userId) {
|
| 310 |
+
if (confirm('确定要删除这个用户吗?')) {
|
| 311 |
+
fetch(`/api/users/${userId}`, {
|
| 312 |
+
method: 'DELETE'
|
| 313 |
+
})
|
| 314 |
+
.then(response => response.json())
|
| 315 |
+
.then(data => {
|
| 316 |
+
if (data.success) {
|
| 317 |
+
loadUsers();
|
| 318 |
+
}
|
| 319 |
+
})
|
| 320 |
+
.catch(error => console.error('Error:', error));
|
| 321 |
+
}
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
// 解绑GPT账号
|
| 325 |
+
function deleteBind(userId) {
|
| 326 |
+
if (confirm('确定要解除这个用户绑定的ChatGPT账号吗?')) {
|
| 327 |
+
fetch(`/api/del_bind/${userId}`, {
|
| 328 |
+
method: 'DELETE'
|
| 329 |
+
})
|
| 330 |
+
.then(response => response.json())
|
| 331 |
+
.then(data => {
|
| 332 |
+
if (data.success) {
|
| 333 |
+
loadUsers();
|
| 334 |
+
}
|
| 335 |
+
})
|
| 336 |
+
.catch(error => console.error('Error:', error));
|
| 337 |
+
}
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
// 解绑Claude账号
|
| 341 |
+
function deleteClaudeBind(userId) {
|
| 342 |
+
if (confirm('确定要解除这个用户绑定的Claude账号吗?')) {
|
| 343 |
+
fetch(`/api/del_bindClaude/${userId}`, {
|
| 344 |
+
method: 'DELETE'
|
| 345 |
+
})
|
| 346 |
+
.then(response => response.json())
|
| 347 |
+
.then(data => {
|
| 348 |
+
if (data.success) {
|
| 349 |
+
loadUsers();
|
| 350 |
+
}
|
| 351 |
+
})
|
| 352 |
+
.catch(error => console.error('Error:', error));
|
| 353 |
+
}
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
// 表单提交处理
|
| 357 |
+
userForm.addEventListener('submit', function (e) {
|
| 358 |
+
e.preventDefault();
|
| 359 |
+
const userId = document.getElementById('userId').value;
|
| 360 |
+
const userData = {
|
| 361 |
+
username: document.getElementById('username').value,
|
| 362 |
+
password: document.getElementById('password').value,
|
| 363 |
+
expiration_time: document.getElementById('expirationDate').value,
|
| 364 |
+
claude_expiration_time: document.getElementById('expirationClaude').value,
|
| 365 |
+
role: document.getElementById('role').value
|
| 366 |
+
};
|
| 367 |
+
|
| 368 |
+
const url = userId ? `/api/users/${userId}` : '/api/users';
|
| 369 |
+
const method = userId ? 'PUT' : 'POST';
|
| 370 |
+
|
| 371 |
+
fetch(url, {
|
| 372 |
+
method: method,
|
| 373 |
+
headers: {
|
| 374 |
+
'Content-Type': 'application/json'
|
| 375 |
+
},
|
| 376 |
+
body: JSON.stringify(userData)
|
| 377 |
+
})
|
| 378 |
+
.then(response => response.json())
|
| 379 |
+
.then(data => {
|
| 380 |
+
if (data.success) {
|
| 381 |
+
closeModal();
|
| 382 |
+
loadUsers();
|
| 383 |
+
}
|
| 384 |
+
})
|
| 385 |
+
.catch(error => console.error('Error:', error));
|
| 386 |
+
});
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
// 表单提交处理
|
| 390 |
+
bindForm.addEventListener('submit', function (e) {
|
| 391 |
+
e.preventDefault();
|
| 392 |
+
const userId = document.getElementById('userId').value;
|
| 393 |
+
const emailSelect = document.getElementById('email');
|
| 394 |
+
const emailSelectClaude = document.getElementById('claudeEmail');
|
| 395 |
+
const bindEmail = {email: emailSelect.value};
|
| 396 |
+
const bindClaudeEmail = {email: emailSelectClaude.value};
|
| 397 |
+
|
| 398 |
+
const method = 'PUT';
|
| 399 |
+
if(document.getElementById('claudeEmail').classList.contains('hidden')){
|
| 400 |
+
url = `/api/bind/${userId}`;
|
| 401 |
+
fetch(url, {
|
| 402 |
+
method: method,
|
| 403 |
+
headers: {
|
| 404 |
+
'Content-Type': 'application/json'
|
| 405 |
+
},
|
| 406 |
+
body: JSON.stringify(bindEmail)
|
| 407 |
+
})
|
| 408 |
+
.then(response => response.json())
|
| 409 |
+
.then(data => {
|
| 410 |
+
if (data.success) {
|
| 411 |
+
closeBindModal();
|
| 412 |
+
loadUsers();
|
| 413 |
+
}
|
| 414 |
+
})
|
| 415 |
+
.catch(error => console.error('Error:', error));
|
| 416 |
+
}else{
|
| 417 |
+
url = `/api/bindClaude/${userId}`;
|
| 418 |
+
fetch(url, {
|
| 419 |
+
method: method,
|
| 420 |
+
headers: {
|
| 421 |
+
'Content-Type': 'application/json'
|
| 422 |
+
},
|
| 423 |
+
body: JSON.stringify(bindClaudeEmail)
|
| 424 |
+
})
|
| 425 |
+
.then(response => response.json())
|
| 426 |
+
.then(data => {
|
| 427 |
+
if (data.success) {
|
| 428 |
+
closeBindModal();
|
| 429 |
+
loadUsers();
|
| 430 |
+
}
|
| 431 |
+
})
|
| 432 |
+
.catch(error => console.error('Error:', error));
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
});
|
| 436 |
+
|
| 437 |
+
// 添加用户按钮点击事件
|
| 438 |
+
addUserBtn.addEventListener('click', () => openModal());
|
| 439 |
+
|
| 440 |
+
// 一键同步
|
| 441 |
+
function syc() {
|
| 442 |
+
if (confirm('确定是否同步网关?')) {
|
| 443 |
+
fetch(`/api/syc`, {
|
| 444 |
+
method: 'GET'
|
| 445 |
+
})
|
| 446 |
+
.then(response => response.json())
|
| 447 |
+
.then(data => {
|
| 448 |
+
if (data.success) {
|
| 449 |
+
loadUsers();
|
| 450 |
+
alert('同步成功!'); // 添加弹窗
|
| 451 |
+
} else {
|
| 452 |
+
alert('同步失败!'); // 可选,处理失败情况
|
| 453 |
+
}
|
| 454 |
+
})
|
| 455 |
+
.catch(error => console.error('Error:', error));
|
| 456 |
+
}
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
|
| 460 |
+
// 一键同步按钮点击事件
|
| 461 |
+
sycGateway.addEventListener('click', () => syc());
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
// 初始加载用户列表
|
| 465 |
+
loadUsers();
|
| 466 |
+
</script>
|
| 467 |
+
{% endblock %}
|
utils/Logger.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
|
| 3 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s | %(levelname)s | %(message)s')
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Logger:
|
| 7 |
+
@staticmethod
|
| 8 |
+
def info(message):
|
| 9 |
+
logging.info(str(message))
|
| 10 |
+
|
| 11 |
+
@staticmethod
|
| 12 |
+
def warning(message):
|
| 13 |
+
logging.warning("\033[0;33m" + str(message) + "\033[0m")
|
| 14 |
+
|
| 15 |
+
@staticmethod
|
| 16 |
+
def error(message):
|
| 17 |
+
logging.error("\033[0;31m" + "-" * 50 + '\n| ' + str(message) + "\033[0m" + "\n" + "└" + "-" * 80)
|
| 18 |
+
|
| 19 |
+
@staticmethod
|
| 20 |
+
def debug(message):
|
| 21 |
+
logging.debug("\033[0;37m" + str(message) + "\033[0m")
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
logger = Logger()
|
utils/configs.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
from utils.Logger import logger
|
| 4 |
+
|
| 5 |
+
load_dotenv(encoding="ascii")
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
secret_key = os.getenv('SECRET_KEY', None)
|
| 10 |
+
authorization = os.getenv('AUTHORIZATION', False)
|
| 11 |
+
domain_chatgpt = os.getenv('DOMAIN_CHATGPT', '')
|
| 12 |
+
domain_claude = os.getenv('DOMAIN_CLAUDE', '')
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
logger.info("-" * 60)
|
| 16 |
+
logger.info(f"Chat-Share | https://github.com/h88782481/Chat-Share")
|
| 17 |
+
logger.info("-" * 60)
|
| 18 |
+
logger.info("Environment variables:")
|
| 19 |
+
logger.info("SECRET_KEY: " + str(secret_key))
|
| 20 |
+
logger.info("AUTHORIZATION: " + str(authorization))
|
| 21 |
+
logger.info("DOMAIN_CHATGPT: " + str(domain_chatgpt))
|
| 22 |
+
logger.info("DOMAIN_CLAUDE: " + str(domain_claude))
|
| 23 |
+
logger.info("-" * 60)
|
utils/globals.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
from datetime import datetime, timedelta
|
| 4 |
+
|
| 5 |
+
DATA_FOLDER = "data"
|
| 6 |
+
AUTO_REFRESH_CONFIG = os.path.join(DATA_FOLDER, "auto_refresh_config.json")
|
| 7 |
+
CHAT_TOKEN = os.path.join(DATA_FOLDER, "chatToken.json")
|
| 8 |
+
FAILED_TOKENS = os.path.join(DATA_FOLDER, "failed_tokens.json")
|
| 9 |
+
REFRESH_HISTORY = os.path.join(DATA_FOLDER, "refresh_history.json")
|
| 10 |
+
CLAUDE_TOKEN = os.path.join(DATA_FOLDER, "claudeToken.json")
|
| 11 |
+
USERS = os.path.join(DATA_FOLDER, "users.json")
|
| 12 |
+
|
| 13 |
+
auto_refresh_config = {}
|
| 14 |
+
chatToken = []
|
| 15 |
+
failed_tokens = []
|
| 16 |
+
refresh_history = []
|
| 17 |
+
users = []
|
| 18 |
+
cluadeToken = []
|
| 19 |
+
|
| 20 |
+
# 上一级目录
|
| 21 |
+
if not os.path.exists(DATA_FOLDER):
|
| 22 |
+
os.makedirs(DATA_FOLDER)
|
| 23 |
+
|
| 24 |
+
# 读取 auto_refresh_config.json 文件
|
| 25 |
+
if os.path.exists(AUTO_REFRESH_CONFIG):
|
| 26 |
+
with open(AUTO_REFRESH_CONFIG, "r") as f:
|
| 27 |
+
try:
|
| 28 |
+
auto_refresh_config = json.load(f)
|
| 29 |
+
except:
|
| 30 |
+
auto_refresh_config = {"auto_refresh_enabled": False, "refresh_interval_days": 9, "next_refresh_time": None}
|
| 31 |
+
else:
|
| 32 |
+
auto_refresh_config = {"auto_refresh_enabled": False, "refresh_interval_days": 9, "next_refresh_time": None}
|
| 33 |
+
|
| 34 |
+
# 如果 next_refresh_time 为 None,则设置为当前时间加 9 天
|
| 35 |
+
if auto_refresh_config["next_refresh_time"] is None:
|
| 36 |
+
next_refresh_time = datetime.now() + timedelta(days=9)
|
| 37 |
+
auto_refresh_config["next_refresh_time"] = next_refresh_time.isoformat() # 转换为 ISO 8601 格式字符串
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
# 读取 chatToken.json 文件
|
| 41 |
+
if os.path.exists(CHAT_TOKEN):
|
| 42 |
+
with open(CHAT_TOKEN, "r") as f:
|
| 43 |
+
try:
|
| 44 |
+
chatToken = json.load(f)
|
| 45 |
+
except:
|
| 46 |
+
chatToken = []
|
| 47 |
+
else:
|
| 48 |
+
chatToken = []
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
# 读取 claudeToken.json 文件
|
| 52 |
+
if os.path.exists(CLAUDE_TOKEN):
|
| 53 |
+
with open(CLAUDE_TOKEN, "r") as f:
|
| 54 |
+
try:
|
| 55 |
+
cluadeToken = json.load(f)
|
| 56 |
+
except:
|
| 57 |
+
cluadeToken = []
|
| 58 |
+
else:
|
| 59 |
+
cluadeToken = []
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# 读取 failed_tokens.json 文件
|
| 63 |
+
if os.path.exists(FAILED_TOKENS):
|
| 64 |
+
with open(FAILED_TOKENS, "r") as f:
|
| 65 |
+
try:
|
| 66 |
+
failed_tokens = json.load(f)
|
| 67 |
+
except:
|
| 68 |
+
failed_tokens = []
|
| 69 |
+
else:
|
| 70 |
+
failed_tokens = []
|
| 71 |
+
|
| 72 |
+
# 读取刷新历史
|
| 73 |
+
if os.path.exists(REFRESH_HISTORY):
|
| 74 |
+
with open(REFRESH_HISTORY, "r") as f:
|
| 75 |
+
try:
|
| 76 |
+
refresh_history = json.load(f)
|
| 77 |
+
except:
|
| 78 |
+
refresh_history = []
|
| 79 |
+
else:
|
| 80 |
+
refresh_history = []
|
| 81 |
+
|
| 82 |
+
# 加载用户表
|
| 83 |
+
if os.path.exists(USERS):
|
| 84 |
+
with open(USERS, "r", encoding="utf-8") as f:
|
| 85 |
+
try:
|
| 86 |
+
users = json.load(f)
|
| 87 |
+
except:
|
| 88 |
+
users = []
|
| 89 |
+
else:
|
| 90 |
+
users = []
|
| 91 |
+
|
| 92 |
+
# 如果 users 为空,设置为默认用户
|
| 93 |
+
if not users:
|
| 94 |
+
users = [
|
| 95 |
+
{
|
| 96 |
+
"id": "d87a64eb-3eda-4f87-9268-d79387d1dfe6",
|
| 97 |
+
"username": "admin",
|
| 98 |
+
"password": "pbkdf2:sha256:260000$CqvLDzNaezUTatZ2$61963529d02a0c0eb74212775872a910a1315d160c4df11da528ad3c03a5ea85",
|
| 99 |
+
"role": "admin",
|
| 100 |
+
"bind_token": "",
|
| 101 |
+
"bind_email": "",
|
| 102 |
+
"expiration_time": "",
|
| 103 |
+
"bind_claude_token": "",
|
| 104 |
+
"bind_claude_email": "",
|
| 105 |
+
"claude_expiration_time": ""
|
| 106 |
+
}
|
| 107 |
+
]
|
| 108 |
+
|
| 109 |
+
# 保存更新后的 chatToken.json 文件
|
| 110 |
+
def save_retoken(updated_tokens):
|
| 111 |
+
with open('data/chatToken.json', 'w') as f:
|
| 112 |
+
json.dump(updated_tokens, f, indent=4)
|
| 113 |
+
|
| 114 |
+
# 保存更新后的 claudeToken.json 文件
|
| 115 |
+
def save_cltoken(updated_tokens):
|
| 116 |
+
with open('data/claudeToken.json', 'w') as f:
|
| 117 |
+
json.dump(updated_tokens, f, indent=4)
|
| 118 |
+
|
| 119 |
+
# 写入 failed_tokens.json 文件
|
| 120 |
+
def save_failed_tokens(failed_tokens):
|
| 121 |
+
with open('data/failed_tokens.json', 'w') as f:
|
| 122 |
+
json.dump(failed_tokens, f, indent=4)
|
| 123 |
+
|
| 124 |
+
# 保存定时任务信息
|
| 125 |
+
def save_auto_refresh_config(config):
|
| 126 |
+
with open('data/auto_refresh_config.json', 'w') as f:
|
| 127 |
+
json.dump(config, f)
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
# 保存刷新历史
|
| 131 |
+
def save_refresh_history(history):
|
| 132 |
+
with open('data/refresh_history.json', 'w') as f:
|
| 133 |
+
json.dump(history, f, indent=4)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
# 保存用户信息
|
| 137 |
+
def save_users(users):
|
| 138 |
+
with open('data/users.json', 'w', encoding='utf-8') as f:
|
| 139 |
+
json.dump(users, f, ensure_ascii=False, indent=2)
|
| 140 |
+
|
| 141 |
+
save_users(users)
|
utils/tools.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import requests
|
| 3 |
+
from flask import redirect, url_for, session, flash
|
| 4 |
+
from functools import wraps
|
| 5 |
+
import utils.configs as configs
|
| 6 |
+
import utils.globals as globals
|
| 7 |
+
from utils.globals import *
|
| 8 |
+
from utils.tools import *
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
# 刷新 access_token 的主函数
|
| 12 |
+
def refresh_access_tokens():
|
| 13 |
+
globals.failed_tokens = []
|
| 14 |
+
# 遍历 refresh_token 列表
|
| 15 |
+
for token_info in globals.chatToken:
|
| 16 |
+
email = token_info['email']
|
| 17 |
+
refresh_token = token_info['refresh_token']
|
| 18 |
+
|
| 19 |
+
# 如果 refresh_token 为空,跳过这一行
|
| 20 |
+
if not refresh_token:
|
| 21 |
+
continue
|
| 22 |
+
|
| 23 |
+
try:
|
| 24 |
+
|
| 25 |
+
# 使用 POST 请求通过 refresh_token 获取 access_token
|
| 26 |
+
url = "https://auth0.openai.com/oauth/token"
|
| 27 |
+
headers = {"Content-Type": "application/json"}
|
| 28 |
+
data = {
|
| 29 |
+
"redirect_uri": "com.openai.chat://auth0.openai.com/ios/com.openai.chat/callback",
|
| 30 |
+
"grant_type": "refresh_token",
|
| 31 |
+
"client_id": "pdlLIX2Y72MIl2rhLhTE9VV9bN905kBh",
|
| 32 |
+
"refresh_token": refresh_token
|
| 33 |
+
}
|
| 34 |
+
response = requests.post(url, headers=headers, data=json.dumps(data))
|
| 35 |
+
response_data = response.json()
|
| 36 |
+
|
| 37 |
+
access_token = response_data.get("access_token")
|
| 38 |
+
if access_token: # 如果成功获取到 access_token
|
| 39 |
+
# 更新 access_token 和状态为 True
|
| 40 |
+
for i, user in enumerate(globals.users):
|
| 41 |
+
if user['bind_email'] == email:
|
| 42 |
+
globals.users[i]['bind_token'] = access_token
|
| 43 |
+
set_seedmap(globals.users[i]['id'],access_token)
|
| 44 |
+
save_users(globals.users)
|
| 45 |
+
token_info['access_token'] = access_token
|
| 46 |
+
token_info['status'] = True
|
| 47 |
+
else:
|
| 48 |
+
# 如果获取失败,设置状态为 False
|
| 49 |
+
token_info['status'] = False
|
| 50 |
+
globals.failed_tokens.append(token_info)
|
| 51 |
+
|
| 52 |
+
except Exception as e:
|
| 53 |
+
# 捕获请求错误并记录失败的 token,状态为 False
|
| 54 |
+
token_info['status'] = False
|
| 55 |
+
globals.failed_tokens.append(token_info)
|
| 56 |
+
|
| 57 |
+
# 保存更新后的 retoken 数据
|
| 58 |
+
save_retoken(globals.chatToken)
|
| 59 |
+
|
| 60 |
+
# 如果有失败的 token,记录到 failed_tokens.json
|
| 61 |
+
save_failed_tokens(globals.failed_tokens)
|
| 62 |
+
|
| 63 |
+
return globals.chatToken
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# 获取登陆链接
|
| 68 |
+
def getoauth(seed_token):
|
| 69 |
+
domain = configs.domain_chatgpt
|
| 70 |
+
|
| 71 |
+
url = f'{domain}/?token={seed_token}'
|
| 72 |
+
try:
|
| 73 |
+
return url
|
| 74 |
+
except requests.RequestException as e:
|
| 75 |
+
return None
|
| 76 |
+
|
| 77 |
+
# 验证是否登录的装饰器
|
| 78 |
+
def login_required(f):
|
| 79 |
+
@wraps(f)
|
| 80 |
+
def decorated_function(*args, **kwargs):
|
| 81 |
+
if not session.get('logged_in'):
|
| 82 |
+
flash('请先登录。', 'warning')
|
| 83 |
+
return redirect(url_for('login'))
|
| 84 |
+
return f(*args, **kwargs)
|
| 85 |
+
return decorated_function
|
| 86 |
+
|
| 87 |
+
# 验证是否为管理员的装饰器
|
| 88 |
+
def admin_required(f):
|
| 89 |
+
@wraps(f)
|
| 90 |
+
def decorated_function(*args, **kwargs):
|
| 91 |
+
if not session.get('logged_in'):
|
| 92 |
+
flash('请先登录。', 'warning')
|
| 93 |
+
return redirect(url_for('login'))
|
| 94 |
+
if session.get('role') != 'admin':
|
| 95 |
+
flash('需要管理员权限。', 'danger')
|
| 96 |
+
return redirect(url_for('index'))
|
| 97 |
+
return f(*args, **kwargs)
|
| 98 |
+
return decorated_function
|
| 99 |
+
|
| 100 |
+
# 设置网关seedmap
|
| 101 |
+
def set_seedmap(user_id,token):
|
| 102 |
+
|
| 103 |
+
domain = configs.domain_chatgpt
|
| 104 |
+
|
| 105 |
+
url = f'{domain}/seedtoken'
|
| 106 |
+
headers = {
|
| 107 |
+
"Authorization": f"Bearer {configs.authorization}",
|
| 108 |
+
"Content-Type": "application/json"
|
| 109 |
+
}
|
| 110 |
+
data = {
|
| 111 |
+
"seed": user_id,
|
| 112 |
+
"token": token,
|
| 113 |
+
}
|
| 114 |
+
response = requests.post(url, headers=headers, data=json.dumps(data))
|
| 115 |
+
return response.status_code
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
# 删除网关seedmap
|
| 119 |
+
def del_seedmap(user_id):
|
| 120 |
+
|
| 121 |
+
domain = configs.domain_chatgpt
|
| 122 |
+
|
| 123 |
+
url = f'{domain}/seedtoken'
|
| 124 |
+
headers = {
|
| 125 |
+
"Authorization": f"Bearer {configs.authorization}",
|
| 126 |
+
"Content-Type": "application/json"
|
| 127 |
+
}
|
| 128 |
+
data = {
|
| 129 |
+
"seed": user_id
|
| 130 |
+
}
|
| 131 |
+
response = requests.delete(url, headers=headers, data=json.dumps(data))
|
| 132 |
+
return response.status_code
|
| 133 |
+
|
| 134 |
+
# 获取Claude登陆链接
|
| 135 |
+
def get_claude_login_url(session_key,uname):
|
| 136 |
+
domain = configs.domain_claude
|
| 137 |
+
url = f'{domain}/manage-api/auth/oauth_token'
|
| 138 |
+
|
| 139 |
+
# 请求体参数
|
| 140 |
+
data = {
|
| 141 |
+
'session_key': session_key,
|
| 142 |
+
'unique_name': uname, # 生成唯一标识符
|
| 143 |
+
"expires_in": 3600 #过期时间1小时
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
# 设置请求头
|
| 147 |
+
headers = {'Content-Type': 'application/json'}
|
| 148 |
+
|
| 149 |
+
try:
|
| 150 |
+
# 发送 POST 请求
|
| 151 |
+
response = requests.post(url, headers=headers, data=json.dumps(data))
|
| 152 |
+
|
| 153 |
+
# 检查响应状态码是否为200
|
| 154 |
+
if response.status_code == 200:
|
| 155 |
+
response_data = response.json()
|
| 156 |
+
|
| 157 |
+
# 检查 'login_url' 是否存在
|
| 158 |
+
if 'login_url' in response_data:
|
| 159 |
+
login_url = response_data['login_url']
|
| 160 |
+
|
| 161 |
+
# 如果URL没有以http开头,拼接基础URL
|
| 162 |
+
if not login_url.startswith('http'):
|
| 163 |
+
return f'{domain}' + login_url
|
| 164 |
+
return login_url
|
| 165 |
+
|
| 166 |
+
# 如果状态码不是200或login_url不存在,返回None
|
| 167 |
+
return None
|
| 168 |
+
|
| 169 |
+
except requests.RequestException as e:
|
| 170 |
+
# 捕获异常并返回错误信息
|
| 171 |
+
return None
|