diff --git a/README.md b/README.md index 1559afe..99cf9b8 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ pnpm install pnpm dev ``` -The local web app listens on `http://localhost:4301` by default. Set `VITE_API_BASE_URL=http://localhost:4300` to point the web app at the local API. +本地前端默认监听 `http://localhost:4301`。如使用 `VITE_API_BASE_URL=http://localhost:4300`,浏览器会直接请求本地 API;如不设置该变量,Vite 会把 `/api` 默认代理到 `http://localhost:4300`,并自动去掉 `/api` 前缀。需要通过 `/api` 代理联调远端 API 时,可临时设置 `DEVOPS_API_PROXY_TARGET=https://devops.mrzhan.top/api`。 ## 构建检查 diff --git a/src/services/devopsApi.ts b/src/services/devopsApi.ts index 620dbea..6130984 100644 --- a/src/services/devopsApi.ts +++ b/src/services/devopsApi.ts @@ -14,6 +14,7 @@ import type { CreateMemberPayload, DashboardSummary, EnvironmentName, + ForgotPasswordPayload, HealthState, IntegrationConfigKey, IntegrationConfigState, @@ -140,6 +141,8 @@ interface BackendIntegrationConfigItem { configured?: string[] missing?: string[] optional?: string[] + enabled?: string[] + disabled?: string[] note?: string variableHelp?: Record variableDescriptions?: Record @@ -259,6 +262,11 @@ export const devopsApi = { return data }, + async forgotPassword(payload: ForgotPasswordPayload): Promise { + await http.post('/auth/forgot-password', payload) + lastDataSource = 'backend' + }, + async logout(): Promise { await http.post('/auth/logout') lastDataSource = 'backend' @@ -594,6 +602,8 @@ function sanitizeIntegrationConfigStatus( const configured = sanitizeVariableNames(item.configured) const missing = sanitizeVariableNames(item.missing) const optional = sanitizeVariableNames(item.optional) + const enabled = sanitizeVariableNames(item.enabled) + const disabled = sanitizeVariableNames(item.disabled) const variableHelp = sanitizeVariableHelp(item) return { @@ -604,6 +614,8 @@ function sanitizeIntegrationConfigStatus( configured, missing, optional, + enabled, + disabled, note: sanitizeIntegrationNote(item.note), ...(Object.keys(variableHelp).length > 0 ? { variableHelp } : {}), } diff --git a/src/types/devops.ts b/src/types/devops.ts index f2d4ecd..474905d 100644 --- a/src/types/devops.ts +++ b/src/types/devops.ts @@ -56,6 +56,10 @@ export interface LoginPayload { password: string } +export interface ForgotPasswordPayload { + account: string +} + export interface LoginResult { token: string expiresAt: string @@ -156,6 +160,8 @@ export interface IntegrationConfigItem { configured: string[] missing: string[] optional?: string[] + enabled?: string[] + disabled?: string[] note?: string variableHelp?: Record } diff --git a/src/views/LoginView.vue b/src/views/LoginView.vue index 015d139..0edb8a1 100644 --- a/src/views/LoginView.vue +++ b/src/views/LoginView.vue @@ -4,17 +4,23 @@ */ import { Lock, Monitor } from '@element-plus/icons-vue' import { computed, reactive, ref } from 'vue' -import { ElMessage } from 'element-plus' +import { ElMessage, ElMessageBox } from 'element-plus' import { useRoute, useRouter } from 'vue-router' +import { devopsApi } from '../services/devopsApi' import { useSessionStore } from '../stores/session' +type LoginRole = 'super_admin' | 'member' + +const SUPER_ADMIN_ACCOUNT = 'zhanxi' const route = useRoute() const router = useRouter() const session = useSessionStore() const loading = ref(false) +const forgotPasswordLoading = ref(false) +const loginRole = ref('super_admin') const loginForm = reactive({ - account: '', + account: SUPER_ADMIN_ACCOUNT, password: '', }) @@ -27,12 +33,33 @@ const passwordForm = reactive({ const redirect = computed(() => typeof route.query.redirect === 'string' ? route.query.redirect : '/overview', ) +const roleHint = computed(() => + loginRole.value === 'super_admin' + ? '超级管理员不提供忘记密码入口,请通过服务器运维方式重置。' + : '普通成员首次登录或重置密码后,必须先修改初始密码。', +) +const isSuperAdminLogin = computed(() => loginRole.value === 'super_admin') + +function switchLoginRole(role: LoginRole) { + loginRole.value = role + loginForm.account = role === 'super_admin' ? SUPER_ADMIN_ACCOUNT : '' + loginForm.password = '' +} + +function handleLoginRoleChange(value: unknown) { + if (value === 'super_admin' || value === 'member') { + switchLoginRole(value) + } +} async function submitLogin() { loading.value = true try { - const user = await session.login(loginForm) + const user = await session.login({ + account: isSuperAdminLogin.value ? SUPER_ADMIN_ACCOUNT : loginForm.account.trim(), + password: loginForm.password, + }) passwordForm.currentPassword = loginForm.password if (user.mustChangePassword) { @@ -48,6 +75,44 @@ async function submitLogin() { } } +async function requestPasswordReset() { + if (isSuperAdminLogin.value) { + return + } + + const account = loginForm.account.trim() + + if (!account) { + ElMessage.warning('请先填写需要重置密码的成员账号') + return + } + + try { + await ElMessageBox.confirm( + `确认向超级管理员发送账号 ${account} 的密码重置请求?`, + '忘记密码', + { + confirmButtonText: '发送请求', + cancelButtonText: '取消', + type: 'warning', + }, + ) + } catch { + return + } + + forgotPasswordLoading.value = true + + try { + await devopsApi.forgotPassword({ account }) + ElMessage.success('已通知超级管理员协助重置密码') + } catch (error) { + ElMessage.error(error instanceof Error ? error.message : '发送重置请求失败') + } finally { + forgotPasswordLoading.value = false + } +} + async function submitPasswordChange() { if (passwordForm.newPassword !== passwordForm.confirmPassword) { ElMessage.error('两次输入的新密码不一致') @@ -102,8 +167,24 @@ function accountLabel() {