diff --git a/api/account_manager.py b/api/account_manager.py index 974ad2a..5a6a2ed 100644 --- a/api/account_manager.py +++ b/api/account_manager.py @@ -202,6 +202,40 @@ class AccountManager: except Exception as e: 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 diff --git a/api/app.py b/api/app.py index 0835879..cc07e1b 100644 --- a/api/app.py +++ b/api/app.py @@ -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,17 +115,20 @@ 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) @@ -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)): diff --git a/api/keys/privkeys/default/private.pem b/api/keys/privkeys/default/private.pem new file mode 100644 index 0000000..0e91c3f --- /dev/null +++ b/api/keys/privkeys/default/private.pem @@ -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----- \ No newline at end of file diff --git a/api/requirements.txt b/api/requirements.txt index 8b3f2a5..bc50b74 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -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 \ No newline at end of file diff --git a/web/src/api/account/index.ts b/web/src/api/account/index.ts index 97beae8..a102139 100644 --- a/web/src/api/account/index.ts +++ b/web/src/api/account/index.ts @@ -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` + }) diff --git a/web/src/api/account/types.ts b/web/src/api/account/types.ts index bfc4aef..f30a0ff 100644 --- a/web/src/api/account/types.ts +++ b/web/src/api/account/types.ts @@ -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' diff --git a/web/src/api/auth/index.ts b/web/src/api/auth/index.ts index c2355d0..72de31b 100644 --- a/web/src/api/auth/index.ts +++ b/web/src/api/auth/index.ts @@ -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', diff --git a/web/src/api/auth/types.ts b/web/src/api/auth/types.ts index 49e7dd0..be226d2 100644 --- a/web/src/api/auth/types.ts +++ b/web/src/api/auth/types.ts @@ -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 = { diff --git a/web/src/store/modules/user.ts b/web/src/store/modules/user.ts index 2d633f3..5cfec40 100644 --- a/web/src/store/modules/user.ts +++ b/web/src/store/modules/user.ts @@ -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 } diff --git a/web/src/views/Account/index.vue b/web/src/views/Account/index.vue index 33461b4..d4de586 100644 --- a/web/src/views/Account/index.vue +++ b/web/src/views/Account/index.vue @@ -21,9 +21,10 @@ + - + - + @@ -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([]) const filteredAccountList = ref([]) +const tenantList = ref([]) 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 }