|
|
{% extends "base.html" %}
|
|
|
|
|
|
{% block title %}用户管理{% endblock %}
|
|
|
|
|
|
{% block content %}
|
|
|
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
|
|
<div class="flex justify-between items-center mb-6">
|
|
|
<h2 class="text-2xl font-bold text-gray-800">用户列表</h2>
|
|
|
<button id="sycGatewayBtn" class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded">
|
|
|
一键同步GPT网关
|
|
|
</button>
|
|
|
<button id="addUserBtn" class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded">
|
|
|
添加用户
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<table class="min-w-full">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th
|
|
|
class="px-6 py-3 border-b-2 border-gray-200 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
|
用户名
|
|
|
</th>
|
|
|
<th
|
|
|
class="px-6 py-3 border-b-2 border-gray-200 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
|
角色
|
|
|
</th>
|
|
|
<th
|
|
|
class="px-6 py-3 border-b-2 border-gray-200 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
|
绑定GPT账号
|
|
|
</th>
|
|
|
<th
|
|
|
class="px-6 py-3 border-b-2 border-gray-200 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
|
GPT过期时间
|
|
|
</th>
|
|
|
<th
|
|
|
class="px-6 py-3 border-b-2 border-gray-200 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
|
绑定Claude账号
|
|
|
</th>
|
|
|
<th
|
|
|
class="px-6 py-3 border-b-2 border-gray-200 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
|
Claude过期时间
|
|
|
</th>
|
|
|
<th
|
|
|
class="px-6 py-3 border-b-2 border-gray-200 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
|
操作
|
|
|
</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody id="userTableBody">
|
|
|
|
|
|
</tbody>
|
|
|
</table>
|
|
|
|
|
|
|
|
|
<div id="userModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden">
|
|
|
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
|
|
<div class="mt-3">
|
|
|
<h3 class="text-lg font-medium text-gray-900 mb-4">用户信息</h3>
|
|
|
<form id="userForm">
|
|
|
<input type="hidden" id="userId">
|
|
|
<div class="mb-4">
|
|
|
<label class="block text-gray-700 text-sm font-bold mb-2" for="username">
|
|
|
用户名
|
|
|
</label>
|
|
|
<input
|
|
|
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
|
|
id="username" type="text" required>
|
|
|
</div>
|
|
|
<div class="mb-4">
|
|
|
<label class="block text-gray-700 text-sm font-bold mb-2" for="password">
|
|
|
密码
|
|
|
</label>
|
|
|
<input
|
|
|
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
|
|
id="password" type="password">
|
|
|
</div>
|
|
|
<div class="mb-4">
|
|
|
<label class="block text-gray-700 text-sm font-bold mb-2" for="role">
|
|
|
角色
|
|
|
</label>
|
|
|
<select
|
|
|
class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
|
|
id="role" required>
|
|
|
<option value="admin">管理员</option>
|
|
|
<option value="user">普通用户</option>
|
|
|
</select>
|
|
|
</div>
|
|
|
|
|
|
<div class="mb-4">
|
|
|
<label class="block text-gray-700 text-sm font-bold mb-2" for="expirationDate">
|
|
|
GPT过期时间
|
|
|
</label>
|
|
|
<input
|
|
|
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
|
|
id="expirationDate" type="datetime-local">
|
|
|
</div>
|
|
|
|
|
|
<div class="mb-4">
|
|
|
<label class="block text-gray-700 text-sm font-bold mb-2" for="expirationClaude">
|
|
|
Claude过期时间
|
|
|
</label>
|
|
|
<input
|
|
|
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
|
|
id="expirationClaude" type="datetime-local">
|
|
|
</div>
|
|
|
<div class="flex items-center justify-end">
|
|
|
<button type="button" onclick="closeModal()"
|
|
|
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">
|
|
|
取消
|
|
|
</button>
|
|
|
<button type="submit"
|
|
|
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
|
|
保存
|
|
|
</button>
|
|
|
</div>
|
|
|
</form>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div id="bindModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden">
|
|
|
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
|
|
<div class="mt-3">
|
|
|
<h3 class="text-lg font-medium text-gray-900 mb-4">请选择要绑定的账号</h3>
|
|
|
<form id="bindForm">
|
|
|
<input type="hidden" id="userId">
|
|
|
<div class="mb-4">
|
|
|
<select
|
|
|
class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
|
|
id="email">
|
|
|
</select>
|
|
|
</div>
|
|
|
<div class="mb-4">
|
|
|
<select
|
|
|
class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
|
|
id="claudeEmail">
|
|
|
</select>
|
|
|
</div>
|
|
|
<div class="flex items-center justify-end">
|
|
|
<button type="button" onclick="closeBindModal()"
|
|
|
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">
|
|
|
取消
|
|
|
</button>
|
|
|
<button type="submit"
|
|
|
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
|
|
保存
|
|
|
</button>
|
|
|
</div>
|
|
|
</form>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
</div>
|
|
|
{% endblock %}
|
|
|
{% block extra_js %}
|
|
|
<script>
|
|
|
let users = [];
|
|
|
const bindModal = document.getElementById('bindModal');
|
|
|
const modal = document.getElementById('userModal');
|
|
|
const userForm = document.getElementById('userForm');
|
|
|
const bindForm = document.getElementById('bindForm');
|
|
|
const addUserBtn = document.getElementById('addUserBtn');
|
|
|
const sycGateway = document.getElementById('sycGatewayBtn');
|
|
|
|
|
|
function loadUsers() {
|
|
|
fetch('/api/users')
|
|
|
.then(response => response.json())
|
|
|
.then(data => {
|
|
|
users = data;
|
|
|
renderUsers();
|
|
|
})
|
|
|
.catch(error => console.error('Error:', error));
|
|
|
}
|
|
|
|
|
|
|
|
|
function renderUsers() {
|
|
|
const tbody = document.getElementById('userTableBody');
|
|
|
tbody.innerHTML = '';
|
|
|
users.forEach(user => {
|
|
|
const tr = document.createElement('tr');
|
|
|
tr.innerHTML = `
|
|
|
<td class="px-6 py-4 whitespace-nowrap">${user.username}</td>
|
|
|
<td class="px-6 py-4 whitespace-nowrap">${user.role}</td>
|
|
|
<td class="px-6 py-4 whitespace-nowrap">${user.bind_email}</td>
|
|
|
<td class="px-6 py-4 whitespace-nowrap">${user.expiration_time}</td>
|
|
|
<td class="px-6 py-4 whitespace-nowrap">${user.bind_claude_email}</td>
|
|
|
<td class="px-6 py-4 whitespace-nowrap">${user.claude_expiration_time}</td>
|
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
|
<button onclick="bindChatGPT('${user.id}')" class="text-blue-600 hover:text-blue-900 mr-4">绑定GPT</button>
|
|
|
<button onclick="bindClaude('${user.id}')" class="text-blue-600 hover:text-blue-900 mr-4">绑定Claude</button>
|
|
|
<button onclick="editUser('${user.id}')" class="text-blue-600 hover:text-blue-900 mr-4">编辑</button>
|
|
|
<button onclick="deleteUser('${user.id}')" class="text-red-600 hover:text-red-900">删除</button>
|
|
|
<button onclick="deleteBind('${user.id}')" class="text-red-600 hover:text-red-900">解绑GPT</button>
|
|
|
<button onclick="deleteClaudeBind('${user.id}')" class="text-red-600 hover:text-red-900">解绑Claude</button>
|
|
|
</td>
|
|
|
`;
|
|
|
tbody.appendChild(tr);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
function openModal(user = null) {
|
|
|
const form = document.getElementById('userForm');
|
|
|
if (user) {
|
|
|
document.getElementById('userId').value = user.id;
|
|
|
document.getElementById('username').value = user.username;
|
|
|
document.getElementById('password').value = '';
|
|
|
document.getElementById('role').value = user.role;
|
|
|
document.getElementById('expirationDate').value = user.expiration_time;
|
|
|
document.getElementById('expirationClaude').value = user.claude_expiration_time;
|
|
|
} else {
|
|
|
form.reset();
|
|
|
document.getElementById('userId').value = '';
|
|
|
}
|
|
|
modal.classList.remove('hidden');
|
|
|
}
|
|
|
|
|
|
|
|
|
function closeModal() {
|
|
|
modal.classList.add('hidden');
|
|
|
}
|
|
|
|
|
|
|
|
|
function editUser(userId) {
|
|
|
const user = users.find(u => u.id === userId);
|
|
|
if (user) {
|
|
|
openModal(user);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
function openBindModal(user = null,type) {
|
|
|
if(type == 1){
|
|
|
url = "/api/all_email"
|
|
|
fetch(url)
|
|
|
.then(response => response.json())
|
|
|
.then(data => {
|
|
|
const emailSelect = document.getElementById('email');
|
|
|
document.getElementById('email').classList.remove('hidden')
|
|
|
document.getElementById('claudeEmail').classList.add('hidden')
|
|
|
emailSelect.innerHTML = '';
|
|
|
document.getElementById('userId').value = user.id;
|
|
|
data.forEach(email => {
|
|
|
const option = document.createElement('option');
|
|
|
option.value = email;
|
|
|
option.textContent = email;
|
|
|
emailSelect.appendChild(option);
|
|
|
});
|
|
|
|
|
|
if (user && user.email) {
|
|
|
emailSelect.value = user.email;
|
|
|
}
|
|
|
})
|
|
|
.catch(error => console.error('Error:', error));
|
|
|
}else{
|
|
|
url = "/api/all_claude_email"
|
|
|
fetch(url)
|
|
|
.then(response => response.json())
|
|
|
.then(data => {
|
|
|
const emailSelect = document.getElementById('claudeEmail');
|
|
|
document.getElementById('claudeEmail').classList.remove('hidden')
|
|
|
document.getElementById('email').classList.add('hidden')
|
|
|
emailSelect.innerHTML = '';
|
|
|
document.getElementById('userId').value = user.id;
|
|
|
data.forEach(email => {
|
|
|
const option = document.createElement('option');
|
|
|
option.value = email;
|
|
|
option.textContent = email;
|
|
|
emailSelect.appendChild(option);
|
|
|
});
|
|
|
|
|
|
if (user && user.email) {
|
|
|
emailSelect.value = user.email;
|
|
|
}
|
|
|
})
|
|
|
.catch(error => console.error('Error:', error));
|
|
|
}
|
|
|
|
|
|
bindModal.classList.remove('hidden');
|
|
|
}
|
|
|
|
|
|
|
|
|
function closeBindModal() {
|
|
|
bindModal.classList.add('hidden');
|
|
|
}
|
|
|
|
|
|
|
|
|
function bindChatGPT(userId) {
|
|
|
const user = users.find(u => u.id === userId);
|
|
|
if (user) {
|
|
|
openBindModal(user,1);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function bindClaude(userId) {
|
|
|
const user = users.find(u => u.id === userId);
|
|
|
if (user) {
|
|
|
openBindModal(user,0);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function deleteUser(userId) {
|
|
|
if (confirm('确定要删除这个用户吗?')) {
|
|
|
fetch(`/api/users/${userId}`, {
|
|
|
method: 'DELETE'
|
|
|
})
|
|
|
.then(response => response.json())
|
|
|
.then(data => {
|
|
|
if (data.success) {
|
|
|
loadUsers();
|
|
|
}
|
|
|
})
|
|
|
.catch(error => console.error('Error:', error));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
function deleteBind(userId) {
|
|
|
if (confirm('确定要解除这个用户绑定的ChatGPT账号吗?')) {
|
|
|
fetch(`/api/del_bind/${userId}`, {
|
|
|
method: 'DELETE'
|
|
|
})
|
|
|
.then(response => response.json())
|
|
|
.then(data => {
|
|
|
if (data.success) {
|
|
|
loadUsers();
|
|
|
}
|
|
|
})
|
|
|
.catch(error => console.error('Error:', error));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
function deleteClaudeBind(userId) {
|
|
|
if (confirm('确定要解除这个用户绑定的Claude账号吗?')) {
|
|
|
fetch(`/api/del_bindClaude/${userId}`, {
|
|
|
method: 'DELETE'
|
|
|
})
|
|
|
.then(response => response.json())
|
|
|
.then(data => {
|
|
|
if (data.success) {
|
|
|
loadUsers();
|
|
|
}
|
|
|
})
|
|
|
.catch(error => console.error('Error:', error));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
userForm.addEventListener('submit', function (e) {
|
|
|
e.preventDefault();
|
|
|
const userId = document.getElementById('userId').value;
|
|
|
const userData = {
|
|
|
username: document.getElementById('username').value,
|
|
|
password: document.getElementById('password').value,
|
|
|
expiration_time: document.getElementById('expirationDate').value,
|
|
|
claude_expiration_time: document.getElementById('expirationClaude').value,
|
|
|
role: document.getElementById('role').value
|
|
|
};
|
|
|
|
|
|
const url = userId ? `/api/users/${userId}` : '/api/users';
|
|
|
const method = userId ? 'PUT' : 'POST';
|
|
|
|
|
|
fetch(url, {
|
|
|
method: method,
|
|
|
headers: {
|
|
|
'Content-Type': 'application/json'
|
|
|
},
|
|
|
body: JSON.stringify(userData)
|
|
|
})
|
|
|
.then(response => response.json())
|
|
|
.then(data => {
|
|
|
if (data.success) {
|
|
|
closeModal();
|
|
|
loadUsers();
|
|
|
}
|
|
|
})
|
|
|
.catch(error => console.error('Error:', error));
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
bindForm.addEventListener('submit', function (e) {
|
|
|
e.preventDefault();
|
|
|
const userId = document.getElementById('userId').value;
|
|
|
const emailSelect = document.getElementById('email');
|
|
|
const emailSelectClaude = document.getElementById('claudeEmail');
|
|
|
const bindEmail = {email: emailSelect.value};
|
|
|
const bindClaudeEmail = {email: emailSelectClaude.value};
|
|
|
|
|
|
const method = 'PUT';
|
|
|
if(document.getElementById('claudeEmail').classList.contains('hidden')){
|
|
|
url = `/api/bind/${userId}`;
|
|
|
fetch(url, {
|
|
|
method: method,
|
|
|
headers: {
|
|
|
'Content-Type': 'application/json'
|
|
|
},
|
|
|
body: JSON.stringify(bindEmail)
|
|
|
})
|
|
|
.then(response => response.json())
|
|
|
.then(data => {
|
|
|
if (data.success) {
|
|
|
closeBindModal();
|
|
|
loadUsers();
|
|
|
}
|
|
|
})
|
|
|
.catch(error => console.error('Error:', error));
|
|
|
}else{
|
|
|
url = `/api/bindClaude/${userId}`;
|
|
|
fetch(url, {
|
|
|
method: method,
|
|
|
headers: {
|
|
|
'Content-Type': 'application/json'
|
|
|
},
|
|
|
body: JSON.stringify(bindClaudeEmail)
|
|
|
})
|
|
|
.then(response => response.json())
|
|
|
.then(data => {
|
|
|
if (data.success) {
|
|
|
closeBindModal();
|
|
|
loadUsers();
|
|
|
}
|
|
|
})
|
|
|
.catch(error => console.error('Error:', error));
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
addUserBtn.addEventListener('click', () => openModal());
|
|
|
|
|
|
|
|
|
function syc() {
|
|
|
if (confirm('确定是否同步网关?')) {
|
|
|
fetch(`/api/syc`, {
|
|
|
method: 'GET'
|
|
|
})
|
|
|
.then(response => response.json())
|
|
|
.then(data => {
|
|
|
if (data.success) {
|
|
|
loadUsers();
|
|
|
alert('同步成功!');
|
|
|
} else {
|
|
|
alert('同步失败!');
|
|
|
}
|
|
|
})
|
|
|
.catch(error => console.error('Error:', error));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sycGateway.addEventListener('click', () => syc());
|
|
|
|
|
|
|
|
|
|
|
|
loadUsers();
|
|
|
</script>
|
|
|
{% endblock %} |