优化账号管理页面:调整表格列宽,美化按钮样式,完善角色显示

This commit is contained in:
xin 2025-05-05 03:08:42 +08:00
parent a6163bfc1c
commit 802c57003c
10 changed files with 380 additions and 56 deletions

View File

@ -203,6 +203,40 @@ class AccountManager:
logger.error(f"更新密码失败: {e}")
raise
@staticmethod
def reset_password(account_id: str):
"""重置账号密码"""
try:
# 验证account_id是否为有效UUID
try:
uuid.UUID(account_id)
except ValueError:
logger.warning(f"无效的account_id格式: {account_id}")
return False
# 使用固定密码
new_password = "Welcome123!"
hashed_password, password_salt = AccountManager.hash_password(new_password)
# 更新密码
updated_at = datetime.now(timezone.utc)
update_query = """
UPDATE accounts
SET password = %s, password_salt = %s, updated_at = %s
WHERE id = %s::uuid;
"""
rows_affected = execute_update(update_query, (hashed_password, password_salt, updated_at, account_id))
if rows_affected > 0:
logger.info(f"账号 {account_id} 的密码已重置!")
return True
else:
logger.warning(f"未找到ID为 {account_id} 的账号。")
return False
except Exception as e:
logger.error(f"重置密码失败: {e}")
raise
@staticmethod
def associate_with_tenant(account_id, tenant_id, role="normal", invited_by=None, current=False):
"""将账户与租户关联"""
@ -250,3 +284,35 @@ class AccountManager:
except Exception as e:
logger.error(f"获取租户账户失败: {e}")
return []
@staticmethod
def get_account_tenants(account_id):
"""获取账户关联的所有租户"""
try:
# 验证account_id是否为有效UUID
try:
uuid.UUID(account_id)
except ValueError:
logger.error(f"无效的account_id格式: {account_id}")
return []
query = """
SELECT t.id, t.name, j.role, j.current
FROM tenants t
JOIN tenant_account_joins j ON t.id = j.tenant_id
WHERE j.account_id = %s::uuid;
"""
tenants = execute_query(query, (account_id,))
if not tenants:
logger.info(f"账号 {account_id} 未关联任何租户")
return []
return [{
"tenant_id": str(t[0]),
"tenant_name": t[1],
"role": str(t[2]).lower(), # 确保角色值统一为小写
"current": t[3]
} for t in tenants]
except Exception as e:
logger.error(f"获取账户租户失败: {e}", exc_info=True)
raise

View File

@ -1,3 +1,4 @@
import uuid
from datetime import datetime, timedelta, timezone
from fastapi import FastAPI, Depends, HTTPException, status, Request, Body, Response
from fastapi.middleware.cors import CORSMiddleware
@ -25,7 +26,7 @@ logger = logging.getLogger(__name__)
# JWT配置
SECRET_KEY = "your-secret-key-here" # 生产环境应该从环境变量获取
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
ACCESS_TOKEN_EXPIRE_MINUTES = 1440 # 延长至24小时
# 密码哈希
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
@ -114,18 +115,21 @@ async def get_current_user(token: str = Depends(oauth2_scheme)):
username: str = payload.get("sub")
if username is None:
raise credentials_exception
# 验证用户(兼容前后端用户)
user = backend_account_manager.get_user_by_username(username) or \
AccountManager.get_user_by_username(username)
if not user:
raise credentials_exception
return {
"id": str(user.get("id")),
"username": user.get("username"),
"email": user.get("email", "")
}
except JWTError:
raise credentials_exception
user = AccountManager.get_user_by_username(username)
if user is None:
raise credentials_exception
return {
"id": user["id"],
"username": user["username"],
"email": user["email"]
}
# 认证路由组
@app.options("/api/auth/register", include_in_schema=False)
async def auth_register_options():
@ -260,29 +264,46 @@ async def search_accounts(
@app.get("/api/dify_accounts/{username}")
async def get_dify_account(username: str, current_user: dict = Depends(get_current_user)):
"""查询Dify账户信息"""
"""查询Dify账户信息及关联租户"""
try:
account = AccountManager.get_user_by_username(username)
if not account:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Dify账户不存在")
# 获取关联租户信息
tenant_info = AccountManager.get_account_tenants(account["id"])
return {
"user_id": str(account["id"]),
"username": account["username"],
"email": account["email"],
"created_at": account["created_at"]
"created_at": account["created_at"],
"tenants": tenant_info # 直接使用已格式化的租户信息
}
except ValueError as e:
logger.error(f"参数格式错误: {e}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="参数格式错误"
)
except Exception as e:
logger.error(f"查询Dify账户失败: {e}")
logger.error(f"查询Dify账户失败: {e}", exc_info=True)
if "404" in str(e):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Dify账户不存在")
raise HTTPException(status_code=400, detail="查询Dify账户失败")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Dify账户不存在"
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="服务器内部错误"
)
@app.put("/api/accounts/password")
async def change_password(
password_change: PasswordChange,
current_user: dict = Depends(get_current_user)
):
"""修改密码"""
"""修改当前用户密码"""
try:
user = AccountManager.get_user_by_username(current_user["username"])
if not AccountManager.verify_password(
@ -305,6 +326,55 @@ async def change_password(
logger.error(f"修改密码失败: {e}")
raise HTTPException(status_code=400, detail="修改密码失败")
@app.post("/api/accounts/{account_id}/reset-password")
async def reset_password(
account_id: str,
current_user: dict = Depends(get_current_user)
):
"""管理员重置用户密码"""
try:
# 检查当前用户是否是admin
admin_user = backend_account_manager.get_user_by_username(current_user["username"])
if not admin_user or admin_user.get("username") != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="需要管理员权限"
)
# 增强account_id格式验证
logger.info(f"完整请求参数: account_id={account_id}")
# 去除可能的空格和引号
clean_account_id = account_id.strip().strip('"').strip("'")
logger.info(f"清理后的account_id: {clean_account_id}")
try:
# 严格验证UUID格式
if len(clean_account_id) != 36 or clean_account_id.count("-") != 4:
raise ValueError("UUID格式不正确")
parsed_uuid = uuid.UUID(clean_account_id)
logger.info(f"成功解析为UUID: {parsed_uuid}")
except ValueError as e:
logger.error(f"无效的account_id格式: {clean_account_id}, 原始值: {account_id}, 错误: {str(e)}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"账号ID格式无效: {clean_account_id} (必须是标准的UUID格式)"
)
# 重置密码
success = AccountManager.reset_password(account_id)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="账号不存在"
)
return {"message": "密码重置成功"}
except Exception as e:
logger.error(f"重置密码失败: {e}")
raise HTTPException(status_code=400, detail="重置密码失败")
# 租户管理路由组
@app.post("/api/tenants/")
async def create_tenant(tenant: TenantCreate, current_user: dict = Depends(get_current_user)):

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA5j3p4ndQP1Qc8v2Jl8CVHu13phNaJ/gBEtt9Rrxmx149n/7X
Wgs3GY7/MMV+AvrZuDDy8tY1J7B1yQySG10Z/I25MHrgSvuNfdzxxcN64wb2cgKz
hsfjqGYOtd5dRRTjLAP1H3+lN7hd9Y0izPN4rM8I9Nz+GUpcks3nxcw5BEHolWIo
LM5L2Or9S4ZIcVtI65rkwoucSzfCDfVbda4rXEFCZjkDQyI/pwRfta6EZQrez/Lf
M6b3sJjBP7Ub9EaKxwAbc2YgcFHIvq8HCtEkSCsjvq+75inADUet0hXlaL1xl0Uf
PYEKk6CftEX/PKEdsWrDdJEth9+R1/xJEYavOQIDAQABAoIBAApagNMMLliOHvf/
w7lGUc5b4BRObOeF7iS3KDA86MhHoPnw0dy8yxYopM5lAf4jT8uA0/9sidHPlWIT
TtpjcXekbeCKuW/DekDfwNoaTVWVxhG7tKZJ4B9HIC3KxyMWWCduDUWW+6Y3cbdb
SMHXEUmkqFCFaGbrdMYbPLRJRYLRWBQQIfHWJKoWfngUr5iRqStmSoUhyY1PwgAO
rD4DhQZOqOYE8DotIA7q5raawKcX5qIf6sEjm+QtmVNQ3g53QPNSTTwlXVQ7vTYz
znkQZBgHIc/6Wb0iLDGUyMT7abHVcr8/EuRCLYSn5Xlcp9nESYE0y4agOx4MPHxw
/x5Kn1MCgYEA7Gab2umVqcS+e+OP8vVPpVvC+G2XeALg7g0M5xnHRYLmlm1GoycO
tG8XcEpI6IsZFBqKtyrpmE+qE2F1mf5oxab6pI0IGvH6IVVibpKdi60CV3lF+d5T
ayZ/7nvMBrXSdhOXacuk+o3qqhjTsqKoy85CNQqwNcYAZu+4+UT0k58CgYEA+VSW
HYnlya0AKWXza8SxU8n3Y1UYMZbbjpUt5Xq6n7nfxQlMRcqwii21u6WeXza2binH
KTOzWW2fLyT4n67bhM6IGYLcJY6hSjvvXGAzftt+tbOGu9PJlWVe4ELLz+kHdhV3
rbjZDEISRi8VGTVZRQghjLU8yerkMMLxcQ5ejicCgYEAyzjuVMOnMFl88x3Oeqtd
+6YlttDnfHjlCl/Xrrefcec0+S4ZoloKLxytRo/lm1swhPLIOuw+AfzCFYUbxvVI
9lk0cM74n8lTIOK5CpspqpBhSfdsK4Bvr9ZZ9hcgbshRk8YFzSIOwoHLsMxE+PUS
LJo0mkqE7sU3RUZhepBHvLsCgYEA20KglK9tDXL+/mjyrSYXD2lADfGKSimxQO09
pF3OeqJ5/4uSsJlzsMBL3g3ifTbfLXe99iTKJu25HDt2DO83is4Zb93dfYW1n1Of
xmuvPXMHNgD/jnPMBX5U9gCnvVnfPt/YFETHUvlTmrbS5g09SPDCmDvVjnfrXlpA
+zw4uOcCgYEAthxwyIrBFJfSR1YWsuhFIXcjXb68UODOL0uJJsnQB61A1+mVLlO9
SO/zQ9ItZYGu4/OFB1fYb7r9n0P0TMICJ0d/uK8AfAcgV6Y1ruTvzgVkhyoPb/p4
eE/C8xdf99RLUfd30D3UeQXYIi9EU57sHCfoupjNGk7P+g7XwrWojQ8=
-----END RSA PRIVATE KEY-----

View File

@ -23,3 +23,6 @@ python-jose>=3.3.0
passlib>=1.7.4
python-multipart>=0.0.6
werkzeug>=2.3.7
# 邮箱验证
email_validator>=2.2.0

View File

@ -12,8 +12,12 @@ export const fetchAccounts = (params: AccountListParams) =>
total: number
}>({
method: 'GET',
url: '/accounts/search',
params
url: '/api/accounts/search',
params: {
page: params.page,
page_size: params.page_size,
search: params.search
}
})
export const updateAccount = (id: string, data: UpdateAccountParams) =>
@ -21,7 +25,7 @@ export const updateAccount = (id: string, data: UpdateAccountParams) =>
message: string
}>({
method: 'PATCH',
url: `/accounts/${id}`,
url: `/api/accounts/${id}`,
data
})
@ -30,7 +34,7 @@ export const resetPassword = (id: string, data: ResetPasswordParams) =>
message: string
}>({
method: 'POST',
url: `/accounts/${id}/reset-password`,
url: `/api/accounts/${id}/reset-password`,
data
})
@ -39,7 +43,7 @@ export const toggleAccountStatus = (id: string) =>
message: string
}>({
method: 'POST',
url: `/accounts/${id}/toggle-status`
url: `/api/accounts/${id}/toggle-status`
})
export const createAccount = (data: { username: string }) =>
@ -48,6 +52,56 @@ export const createAccount = (data: { username: string }) =>
account: AccountItem
}>({
method: 'POST',
url: '/accounts',
url: '/api/accounts',
data
})
// Dify账号相关API
export const getDifyAccount = (username: string) =>
request<{
user_id: string
username: string
email: string
created_at: string
tenants: Array<{
tenant_id: string
tenant_name: string
role: string
current: boolean
}>
}>({
method: 'GET',
url: `/api/dify_accounts/${username}`
})
export const getDifyAccounts = (params: { page: number; page_size: number }) =>
request<{
accounts: AccountItem[]
total: number
}>({
method: 'GET',
url: '/api/dify_accounts',
params
})
export const createDifyAccount = (data: {
username: string
email: string
password: string
}) =>
request<{
message: string
account: AccountItem
}>({
method: 'POST',
url: '/api/dify_accounts',
data
})
export const resetDifyPassword = (id: string) =>
request<{
message: string
}>({
method: 'POST',
url: `/api/accounts/${id}/reset-password`
})

View File

@ -1,3 +1,10 @@
export interface TenantInfo {
tenant_id: string
tenant_name: string
role: string
current: boolean
}
export interface AccountItem {
id: string
username: string
@ -5,11 +12,12 @@ export interface AccountItem {
status: 'active' | 'disabled'
createdAt: string
updatedAt: string
tenants?: TenantInfo[]
}
export interface AccountListParams {
page?: number
pageSize?: number
page_size?: number
username?: string
email?: string
status?: 'active' | 'disabled'

View File

@ -1,7 +1,7 @@
import { request } from '../../axios/service'
import type { LoginParams, RegisterParams } from '@/api/auth/types'
import type { LoginParams, RegisterParams, LoginForm } from '@/api/auth/types'
export const login = (formData: FormData) =>
export const login = (formData: FormData | LoginForm) =>
request<{ access_token: string }>({
method: 'POST',
url: '/api/auth/login',

View File

@ -1,9 +1,14 @@
export type LoginParams = {
export interface LoginParams {
username: string
password: string
tenantId?: string
}
export interface LoginForm {
username: string
password: string
}
export type LoginFormData = FormData
export type RegisterParams = {

View File

@ -1,7 +1,7 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { loginApi } from '../../api/login'
import type { LoginForm } from '../../api/login/types'
import { login } from '../../api/auth'
import type { LoginForm } from '../../api/auth/types'
import { setToken as setStorageToken, removeToken } from '../../utils/storage'
export const useUserStore = defineStore('user', () => {
@ -17,10 +17,13 @@ export const useUserStore = defineStore('user', () => {
userInfo.value = info
}
const login = async (form: LoginForm) => {
const res = await loginApi(form)
setToken(res.token)
setUserInfo(res.userInfo)
const login = async (params: LoginForm): Promise<{ access_token: string }> => {
const formData = new FormData()
formData.append('username', params.username)
formData.append('password', params.password)
const res = await login(formData as FormData)
setToken(res.access_token)
return res
}

View File

@ -21,9 +21,10 @@
</div>
<el-table :data="filteredAccountList" border style="width: 100%">
<el-table-column prop="id" label="ID" width="240" />
<el-table-column prop="username" label="用户名" width="180" />
<el-table-column prop="email" label="邮箱" width="220" />
<el-table-column prop="status" label="状态" width="120">
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 'active' ? 'success' : 'danger'">
{{ row.status === 'active' ? '启用' : '禁用' }}
@ -31,9 +32,24 @@
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="180" />
<el-table-column label="操作" width="220">
<el-table-column label="操作" width="280" align="center">
<template #default="{ row }">
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
<el-button
plain
size="small"
@click="handleResetPassword(row)"
style="color: #409EFF; border-color: #c6e2ff; background-color: #ecf5ff;"
>
重置密码
</el-button>
<el-button
plain
size="small"
@click="handleShowTenants(row.username)"
style="color: #67C23A; border-color: #e1f3d8; background-color: #f0f9eb; margin-left: 10px;"
>
关联租户
</el-button>
<el-button
size="small"
type="danger"
@ -49,10 +65,32 @@
v-model:current-page="pagination.current"
v-model:page-size="pagination.size"
:total="pagination.total"
@current-change="fetchAccounts"
@current-change="fetchAccountData"
layout="total, sizes, prev, pager, next, jumper"
/>
</el-card>
<!-- 租户信息弹窗 -->
<el-dialog v-model="tenantDialogVisible" title="关联租户信息" width="60%">
<el-table :data="tenantList" border v-loading="loading">
<el-table-column prop="tenant_id" label="租户ID" width="220" />
<el-table-column prop="tenant_name" label="租户名称" width="180" />
<el-table-column prop="role" label="角色" width="120">
<template #default="{ row }">
<el-tag :type="getRoleTagType(row.role)">
{{ getRoleDisplayName(row.role) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="current" label="当前租户" width="120">
<template #default="{ row }">
<el-tag :type="row.current ? 'success' : 'info'">
{{ row.current ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
</el-table>
</el-dialog>
</div>
</template>
@ -62,7 +100,8 @@ import {
fetchAccounts,
toggleAccountStatus,
createAccount,
updateAccount
resetPassword,
getDifyAccount
} from '@/api/account'
import { ElMessage, ElMessageBox } from 'element-plus'
@ -72,12 +111,15 @@ interface Account {
email: string
status: 'active' | 'disabled'
createdAt: string
isDify?: boolean
}
const accountList = ref<Account[]>([])
const filteredAccountList = ref<Account[]>([])
const tenantList = ref<any[]>([])
const searchQuery = ref('')
const loading = ref(false)
const tenantDialogVisible = ref(false)
const pagination = ref({
current: 1,
@ -90,10 +132,15 @@ const fetchAccountData = async () => {
loading.value = true
const res = await fetchAccounts({
page: pagination.value.current,
pageSize: pagination.value.size
page_size: pagination.value.size,
search: searchQuery.value || undefined
})
accountList.value = res.accounts
accountList.value = res.accounts.map(account => ({
...account,
isDify: account.email?.endsWith('@dify.com')
}))
pagination.value.total = res.total
filteredAccountList.value = res.accounts
} finally {
loading.value = false
}
@ -125,38 +172,79 @@ const handleCreate = () => {
})
}
const handleEdit = (account: Account) => {
ElMessageBox.prompt('请输入新用户名', '编辑账号', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputValue: account.username,
inputPattern: /^[a-zA-Z0-9_]{4,20}$/,
inputErrorMessage: '用户名必须为4-20位字母数字或下划线'
}).then(async ({ value }) => {
try {
await updateAccount(account.id, { username: value })
ElMessage.success('账号更新成功')
await fetchAccountData()
} catch (error) {
ElMessage.error('账号更新失败')
const handleResetPassword = async (account: Account) => {
try {
await ElMessageBox.confirm('确定要重置该账号的密码吗?', '重置密码', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await resetPassword(account.id)
ElMessage.success('密码重置成功')
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('密码重置失败')
}
})
}
}
const handleSearchClear = () => {
const handleShowTenants = async (username: string) => {
try {
loading.value = true
const res = await getDifyAccount(username)
console.log('租户信息:', res) //
tenantList.value = res.tenants || []
if (tenantList.value.length === 0) {
ElMessage.warning('该账号未关联任何租户')
} else {
tenantDialogVisible.value = true
}
} catch (error) {
console.error('获取租户信息失败:', error)
ElMessage.error('获取租户信息失败: ' + error.message)
} finally {
loading.value = false
}
}
const handleSearchClear = async () => {
searchQuery.value = ''
await fetchAccountData()
filteredAccountList.value = accountList.value
}
const getRoleTagType = (role) => {
const roleMap = {
'owner': 'danger',
'administrator': 'warning',
'editor': 'primary',
'member': 'info'
}
return roleMap[role.toLowerCase()] || 'primary'
}
const getRoleDisplayName = (role) => {
const roleMap = {
'owner': '所有者',
'administrator': '管理员',
'editor': '编辑者',
'member': '成员'
}
return roleMap[role.toLowerCase()] || role
}
const handleSearch = async () => {
try {
loading.value = true
pagination.value.current = 1
const res = await fetchAccounts({
page: 1,
pageSize: pagination.value.size,
page_size: pagination.value.size,
search: searchQuery.value
})
filteredAccountList.value = res.accounts
pagination.value.total = res.total
accountList.value = res.accounts
} finally {
loading.value = false
}