feat: 完善登录角色切换和本地代理
This commit is contained in:
@@ -30,7 +30,7 @@ pnpm install
|
|||||||
pnpm dev
|
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`。
|
||||||
|
|
||||||
## 构建检查
|
## 构建检查
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import type {
|
|||||||
CreateMemberPayload,
|
CreateMemberPayload,
|
||||||
DashboardSummary,
|
DashboardSummary,
|
||||||
EnvironmentName,
|
EnvironmentName,
|
||||||
|
ForgotPasswordPayload,
|
||||||
HealthState,
|
HealthState,
|
||||||
IntegrationConfigKey,
|
IntegrationConfigKey,
|
||||||
IntegrationConfigState,
|
IntegrationConfigState,
|
||||||
@@ -140,6 +141,8 @@ interface BackendIntegrationConfigItem {
|
|||||||
configured?: string[]
|
configured?: string[]
|
||||||
missing?: string[]
|
missing?: string[]
|
||||||
optional?: string[]
|
optional?: string[]
|
||||||
|
enabled?: string[]
|
||||||
|
disabled?: string[]
|
||||||
note?: string
|
note?: string
|
||||||
variableHelp?: Record<string, BackendVariableHelpValue>
|
variableHelp?: Record<string, BackendVariableHelpValue>
|
||||||
variableDescriptions?: Record<string, unknown>
|
variableDescriptions?: Record<string, unknown>
|
||||||
@@ -259,6 +262,11 @@ export const devopsApi = {
|
|||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async forgotPassword(payload: ForgotPasswordPayload): Promise<void> {
|
||||||
|
await http.post('/auth/forgot-password', payload)
|
||||||
|
lastDataSource = 'backend'
|
||||||
|
},
|
||||||
|
|
||||||
async logout(): Promise<void> {
|
async logout(): Promise<void> {
|
||||||
await http.post('/auth/logout')
|
await http.post('/auth/logout')
|
||||||
lastDataSource = 'backend'
|
lastDataSource = 'backend'
|
||||||
@@ -594,6 +602,8 @@ function sanitizeIntegrationConfigStatus(
|
|||||||
const configured = sanitizeVariableNames(item.configured)
|
const configured = sanitizeVariableNames(item.configured)
|
||||||
const missing = sanitizeVariableNames(item.missing)
|
const missing = sanitizeVariableNames(item.missing)
|
||||||
const optional = sanitizeVariableNames(item.optional)
|
const optional = sanitizeVariableNames(item.optional)
|
||||||
|
const enabled = sanitizeVariableNames(item.enabled)
|
||||||
|
const disabled = sanitizeVariableNames(item.disabled)
|
||||||
const variableHelp = sanitizeVariableHelp(item)
|
const variableHelp = sanitizeVariableHelp(item)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -604,6 +614,8 @@ function sanitizeIntegrationConfigStatus(
|
|||||||
configured,
|
configured,
|
||||||
missing,
|
missing,
|
||||||
optional,
|
optional,
|
||||||
|
enabled,
|
||||||
|
disabled,
|
||||||
note: sanitizeIntegrationNote(item.note),
|
note: sanitizeIntegrationNote(item.note),
|
||||||
...(Object.keys(variableHelp).length > 0 ? { variableHelp } : {}),
|
...(Object.keys(variableHelp).length > 0 ? { variableHelp } : {}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ export interface LoginPayload {
|
|||||||
password: string
|
password: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ForgotPasswordPayload {
|
||||||
|
account: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface LoginResult {
|
export interface LoginResult {
|
||||||
token: string
|
token: string
|
||||||
expiresAt: string
|
expiresAt: string
|
||||||
@@ -156,6 +160,8 @@ export interface IntegrationConfigItem {
|
|||||||
configured: string[]
|
configured: string[]
|
||||||
missing: string[]
|
missing: string[]
|
||||||
optional?: string[]
|
optional?: string[]
|
||||||
|
enabled?: string[]
|
||||||
|
disabled?: string[]
|
||||||
note?: string
|
note?: string
|
||||||
variableHelp?: Record<string, IntegrationVariableHelp>
|
variableHelp?: Record<string, IntegrationVariableHelp>
|
||||||
}
|
}
|
||||||
|
|||||||
+124
-4
@@ -4,17 +4,23 @@
|
|||||||
*/
|
*/
|
||||||
import { Lock, Monitor } from '@element-plus/icons-vue'
|
import { Lock, Monitor } from '@element-plus/icons-vue'
|
||||||
import { computed, reactive, ref } from '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 { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { devopsApi } from '../services/devopsApi'
|
||||||
import { useSessionStore } from '../stores/session'
|
import { useSessionStore } from '../stores/session'
|
||||||
|
|
||||||
|
type LoginRole = 'super_admin' | 'member'
|
||||||
|
|
||||||
|
const SUPER_ADMIN_ACCOUNT = 'zhanxi'
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const session = useSessionStore()
|
const session = useSessionStore()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const forgotPasswordLoading = ref(false)
|
||||||
|
const loginRole = ref<LoginRole>('super_admin')
|
||||||
|
|
||||||
const loginForm = reactive({
|
const loginForm = reactive({
|
||||||
account: '',
|
account: SUPER_ADMIN_ACCOUNT,
|
||||||
password: '',
|
password: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -27,12 +33,33 @@ const passwordForm = reactive({
|
|||||||
const redirect = computed(() =>
|
const redirect = computed(() =>
|
||||||
typeof route.query.redirect === 'string' ? route.query.redirect : '/overview',
|
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() {
|
async function submitLogin() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
try {
|
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
|
passwordForm.currentPassword = loginForm.password
|
||||||
|
|
||||||
if (user.mustChangePassword) {
|
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() {
|
async function submitPasswordChange() {
|
||||||
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
||||||
ElMessage.error('两次输入的新密码不一致')
|
ElMessage.error('两次输入的新密码不一致')
|
||||||
@@ -102,8 +167,24 @@ function accountLabel() {
|
|||||||
</div>
|
</div>
|
||||||
<ElForm class="login-form" label-position="top" @submit.prevent="submit">
|
<ElForm class="login-form" label-position="top" @submit.prevent="submit">
|
||||||
<template v-if="!session.mustChangePassword">
|
<template v-if="!session.mustChangePassword">
|
||||||
|
<div class="role-switch">
|
||||||
|
<ElRadioGroup
|
||||||
|
:model-value="loginRole"
|
||||||
|
size="large"
|
||||||
|
@change="handleLoginRoleChange"
|
||||||
|
>
|
||||||
|
<ElRadioButton value="super_admin">超级管理员</ElRadioButton>
|
||||||
|
<ElRadioButton value="member">普通成员</ElRadioButton>
|
||||||
|
</ElRadioGroup>
|
||||||
|
<p>{{ roleHint }}</p>
|
||||||
|
</div>
|
||||||
<ElFormItem label="账号">
|
<ElFormItem label="账号">
|
||||||
<ElInput v-model="loginForm.account" size="large" autocomplete="username" />
|
<ElInput
|
||||||
|
v-model="loginForm.account"
|
||||||
|
size="large"
|
||||||
|
autocomplete="username"
|
||||||
|
:readonly="isSuperAdminLogin"
|
||||||
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="密码">
|
<ElFormItem label="密码">
|
||||||
<ElInput
|
<ElInput
|
||||||
@@ -123,6 +204,15 @@ function accountLabel() {
|
|||||||
>
|
>
|
||||||
登录
|
登录
|
||||||
</ElButton>
|
</ElButton>
|
||||||
|
<ElButton
|
||||||
|
v-if="!isSuperAdminLogin"
|
||||||
|
class="forgot-password"
|
||||||
|
text
|
||||||
|
:loading="forgotPasswordLoading"
|
||||||
|
@click="requestPasswordReset"
|
||||||
|
>
|
||||||
|
忘记密码
|
||||||
|
</ElButton>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -224,11 +314,41 @@ function accountLabel() {
|
|||||||
padding: 44px 36px;
|
padding: 44px 36px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.role-switch {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-switch :deep(.el-radio-group) {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-switch :deep(.el-radio-button__inner) {
|
||||||
|
width: 100%;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-switch p {
|
||||||
|
min-height: 20px;
|
||||||
|
margin: 0;
|
||||||
|
color: #697386;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
.login-form .el-button {
|
.login-form .el-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.forgot-password {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.change-title {
|
.change-title {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|||||||
@@ -178,6 +178,20 @@ function variableGroups(item: IntegrationConfigItem): VariableGroup[] {
|
|||||||
emptyText: '无',
|
emptyText: '无',
|
||||||
tagType: 'danger',
|
tagType: 'danger',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'enabled',
|
||||||
|
label: '已开启',
|
||||||
|
names: item.enabled ?? [],
|
||||||
|
emptyText: '暂无',
|
||||||
|
tagType: 'success',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'disabled',
|
||||||
|
label: '已关闭',
|
||||||
|
names: item.disabled ?? [],
|
||||||
|
emptyText: '暂无',
|
||||||
|
tagType: 'warning',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'optional',
|
key: 'optional',
|
||||||
label: '可选变量',
|
label: '可选变量',
|
||||||
|
|||||||
+3
-2
@@ -11,9 +11,10 @@ export default defineConfig({
|
|||||||
port: 4301,
|
port: 4301,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: process.env.DEVOPS_API_PROXY_TARGET ?? 'https://devops.mrzhan.top',
|
target: process.env.DEVOPS_API_PROXY_TARGET ?? 'http://localhost:4300',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: true,
|
secure: false,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user