feat: 接入真实平台登录态
- login/session: 对接后端登录、鉴权恢复和首次改密 - api/store: 恢复 BPMN XML 真实接口并携带 Bearer token - layout/settings: 展示真实角色、审计 JSON 和通知平台配置文档
This commit is contained in:
@@ -45,7 +45,10 @@ NOTIFICATION_WEBHOOK_URL=https://notification.example.internal/webhook/replace-w
|
|||||||
|
|
||||||
## 企业微信配置
|
## 企业微信配置
|
||||||
|
|
||||||
官方参考:`https://developer.work.weixin.qq.com/document/path/91770`
|
参考文档:
|
||||||
|
|
||||||
|
- 企业微信开发者文档:`https://developer.work.weixin.qq.com/document/path/91770`
|
||||||
|
- 腾讯云消息中心企业微信机器人接收消息:`https://cloud.tencent.com/document/product/1263/71731`
|
||||||
|
|
||||||
1. 在企业微信群中添加群机器人。
|
1. 在企业微信群中添加群机器人。
|
||||||
2. 复制机器人 webhook,保存到服务端环境变量 `WECOM_WEBHOOK_URL`。
|
2. 复制机器人 webhook,保存到服务端环境变量 `WECOM_WEBHOOK_URL`。
|
||||||
|
|||||||
@@ -44,13 +44,44 @@ const previewText = computed(() => {
|
|||||||
return formattedJson.value.replace(/\s+/g, ' ')
|
return formattedJson.value.replace(/\s+/g, ' ')
|
||||||
})
|
})
|
||||||
|
|
||||||
const canToggle = computed(
|
const hasContent = computed(() => formattedJson.value !== '-')
|
||||||
() => formattedJson.value.length > 80 || formattedJson.value.includes('\n'),
|
|
||||||
)
|
|
||||||
|
|
||||||
async function copyJson() {
|
async function copyJson() {
|
||||||
await navigator.clipboard.writeText(formattedJson.value)
|
try {
|
||||||
ElMessage.success('已复制 JSON')
|
if (navigator.clipboard?.writeText) {
|
||||||
|
await navigator.clipboard.writeText(formattedJson.value)
|
||||||
|
} else {
|
||||||
|
fallbackCopy(formattedJson.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.success('已复制 JSON')
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
fallbackCopy(formattedJson.value)
|
||||||
|
ElMessage.success('已复制 JSON')
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('复制失败,请手动选择 JSON 内容')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallbackCopy(text: string) {
|
||||||
|
const textarea = document.createElement('textarea')
|
||||||
|
textarea.value = text
|
||||||
|
textarea.setAttribute('readonly', 'readonly')
|
||||||
|
textarea.style.position = 'fixed'
|
||||||
|
textarea.style.left = '-9999px'
|
||||||
|
document.body.appendChild(textarea)
|
||||||
|
|
||||||
|
try {
|
||||||
|
textarea.select()
|
||||||
|
|
||||||
|
if (!document.execCommand('copy')) {
|
||||||
|
throw new Error('copy command rejected')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
document.body.removeChild(textarea)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -61,7 +92,7 @@ async function copyJson() {
|
|||||||
:icon="expanded ? ArrowDown : ArrowRight"
|
:icon="expanded ? ArrowDown : ArrowRight"
|
||||||
size="small"
|
size="small"
|
||||||
text
|
text
|
||||||
:disabled="!canToggle"
|
:disabled="!hasContent"
|
||||||
@click="expanded = !expanded"
|
@click="expanded = !expanded"
|
||||||
>
|
>
|
||||||
{{ expanded ? '收起' : '展开' }}
|
{{ expanded ? '收起' : '展开' }}
|
||||||
|
|||||||
@@ -50,9 +50,9 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function logout() {
|
async function logout() {
|
||||||
session.logout()
|
await session.logout()
|
||||||
void router.push('/login')
|
await router.push('/login')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -98,12 +98,12 @@ function logout() {
|
|||||||
</ElTag>
|
</ElTag>
|
||||||
<ElDropdown>
|
<ElDropdown>
|
||||||
<ElButton>
|
<ElButton>
|
||||||
{{ session.operator }}
|
{{ session.displayName || session.operator }}
|
||||||
<ElIcon class="el-icon--right"><Tools /></ElIcon>
|
<ElIcon class="el-icon--right"><Tools /></ElIcon>
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<ElDropdownMenu>
|
<ElDropdownMenu>
|
||||||
<ElDropdownItem disabled>角色:{{ session.role }}</ElDropdownItem>
|
<ElDropdownItem disabled>角色:{{ session.roleLabel }}</ElDropdownItem>
|
||||||
<ElDropdownItem :icon="SwitchButton" @click="logout">退出登录</ElDropdownItem>
|
<ElDropdownItem :icon="SwitchButton" @click="logout">退出登录</ElDropdownItem>
|
||||||
</ElDropdownMenu>
|
</ElDropdownMenu>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+4
-3
@@ -60,10 +60,11 @@ const router = createRouter({
|
|||||||
routes,
|
routes,
|
||||||
})
|
})
|
||||||
|
|
||||||
router.beforeEach((to) => {
|
router.beforeEach(async (to) => {
|
||||||
const session = useSessionStore()
|
const session = useSessionStore()
|
||||||
|
const authenticated = await session.bootstrap()
|
||||||
|
|
||||||
if (!to.meta.public && !session.isAuthenticated) {
|
if (!to.meta.public && (!authenticated || session.mustChangePassword)) {
|
||||||
return {
|
return {
|
||||||
name: 'login',
|
name: 'login',
|
||||||
query: {
|
query: {
|
||||||
@@ -72,7 +73,7 @@ router.beforeEach((to) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (to.name === 'login' && session.isAuthenticated) {
|
if (to.name === 'login' && authenticated && !session.mustChangePassword) {
|
||||||
return {
|
return {
|
||||||
name: 'overview',
|
name: 'overview',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
import type {
|
import type {
|
||||||
AgentCard,
|
AgentCard,
|
||||||
AgentInvocation,
|
AgentInvocation,
|
||||||
|
AuthenticatedUser,
|
||||||
AuditLogEntry,
|
AuditLogEntry,
|
||||||
|
ChangePasswordPayload,
|
||||||
CreateAgentInvocationPayload,
|
CreateAgentInvocationPayload,
|
||||||
CreateDeployRunPayload,
|
CreateDeployRunPayload,
|
||||||
DashboardSummary,
|
DashboardSummary,
|
||||||
@@ -16,6 +18,8 @@ import type {
|
|||||||
IntegrationVariableHelp,
|
IntegrationVariableHelp,
|
||||||
IntegrationStatus,
|
IntegrationStatus,
|
||||||
JenkinsSyncSummary,
|
JenkinsSyncSummary,
|
||||||
|
LoginPayload,
|
||||||
|
LoginResult,
|
||||||
PipelineStep,
|
PipelineStep,
|
||||||
ProjectConfig,
|
ProjectConfig,
|
||||||
ProjectRefs,
|
ProjectRefs,
|
||||||
@@ -216,6 +220,29 @@ export const devopsApi = {
|
|||||||
return lastDataSource
|
return lastDataSource
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async login(payload: LoginPayload): Promise<LoginResult> {
|
||||||
|
const { data } = await http.post<LoginResult>('/auth/login', payload)
|
||||||
|
lastDataSource = 'backend'
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
|
||||||
|
async getCurrentUser(): Promise<AuthenticatedUser> {
|
||||||
|
const { data } = await http.get<AuthenticatedUser>('/auth/me')
|
||||||
|
lastDataSource = 'backend'
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
|
||||||
|
async changePassword(payload: ChangePasswordPayload): Promise<AuthenticatedUser> {
|
||||||
|
const { data } = await http.post<AuthenticatedUser>('/auth/change-password', payload)
|
||||||
|
lastDataSource = 'backend'
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
|
||||||
|
async logout(): Promise<void> {
|
||||||
|
await http.post('/auth/logout')
|
||||||
|
lastDataSource = 'backend'
|
||||||
|
},
|
||||||
|
|
||||||
async getDashboard(): Promise<DashboardSummary> {
|
async getDashboard(): Promise<DashboardSummary> {
|
||||||
const [health, backendProjects, backendRuns, integrationConfig] = await Promise.all([
|
const [health, backendProjects, backendRuns, integrationConfig] = await Promise.all([
|
||||||
fetchHealth(),
|
fetchHealth(),
|
||||||
|
|||||||
@@ -9,6 +9,18 @@ export const http = axios.create({
|
|||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录令牌只用于调用后端 API,不会传给 Jenkins/Gitea/LLM 等第三方服务。
|
||||||
|
*/
|
||||||
|
export function setAuthToken(token?: string) {
|
||||||
|
if (token) {
|
||||||
|
http.defaults.headers.common.Authorization = `Bearer ${token}`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
delete http.defaults.headers.common.Authorization
|
||||||
|
}
|
||||||
|
|
||||||
http.interceptors.response.use(
|
http.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
response.data = unwrapApiPayload(response.data)
|
response.data = unwrapApiPayload(response.data)
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* 平台 Pinia store 聚合后端项目、发布、设置和审计数据。
|
||||||
|
*/
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { devopsApi } from '../services/devopsApi'
|
import { devopsApi } from '../services/devopsApi'
|
||||||
|
|||||||
+151
-8
@@ -1,28 +1,171 @@
|
|||||||
|
/**
|
||||||
|
* 维护后端真实登录态,前端只保存 DevOps API token,不保存任何第三方密钥。
|
||||||
|
*/
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
import { devopsApi } from '../services/devopsApi'
|
||||||
|
import { setAuthToken } from '../services/http'
|
||||||
|
import type {
|
||||||
|
AuthenticatedUser,
|
||||||
|
ChangePasswordPayload,
|
||||||
|
LoginPayload,
|
||||||
|
} from '../types/devops'
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'devops-platform-session'
|
||||||
|
|
||||||
|
type StoredSession = {
|
||||||
|
token: string
|
||||||
|
expiresAt: string
|
||||||
|
user: AuthenticatedUser
|
||||||
|
}
|
||||||
|
|
||||||
export const useSessionStore = defineStore('session', () => {
|
export const useSessionStore = defineStore('session', () => {
|
||||||
const operator = ref('ops-admin')
|
const restored = loadStoredSession()
|
||||||
const role = ref<'admin' | 'operator' | 'viewer'>('admin')
|
const operator = ref(restored?.user.account ?? '')
|
||||||
const token = ref('local-session')
|
const displayName = ref(restored?.user.displayName ?? '')
|
||||||
|
const role = ref<AuthenticatedUser['role'] | ''>(restored?.user.role ?? '')
|
||||||
|
const token = ref(restored?.token ?? '')
|
||||||
|
const expiresAt = ref(restored?.expiresAt ?? '')
|
||||||
|
const currentUser = ref<AuthenticatedUser | null>(restored?.user ?? null)
|
||||||
|
const bootstrapped = ref(false)
|
||||||
|
|
||||||
const isAuthenticated = computed(() => Boolean(token.value))
|
setAuthToken(token.value)
|
||||||
|
|
||||||
function login(name: string) {
|
const isAuthenticated = computed(() => Boolean(token.value && currentUser.value))
|
||||||
operator.value = name || 'ops-admin'
|
const mustChangePassword = computed(() =>
|
||||||
token.value = 'local-session'
|
Boolean(currentUser.value?.mustChangePassword),
|
||||||
|
)
|
||||||
|
const roleLabel = computed(() => {
|
||||||
|
if (role.value === 'super_admin') {
|
||||||
|
return '超级管理员'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role.value === 'member') {
|
||||||
|
return '普通成员'
|
||||||
|
}
|
||||||
|
|
||||||
|
return '未登录'
|
||||||
|
})
|
||||||
|
|
||||||
|
async function bootstrap(): Promise<boolean> {
|
||||||
|
if (bootstrapped.value) {
|
||||||
|
return isAuthenticated.value
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrapped.value = true
|
||||||
|
|
||||||
|
if (!token.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await devopsApi.getCurrentUser()
|
||||||
|
applyUser(user)
|
||||||
|
persist()
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
clearSession()
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function logout() {
|
async function login(payload: LoginPayload) {
|
||||||
|
const result = await devopsApi.login(payload)
|
||||||
|
token.value = result.token
|
||||||
|
expiresAt.value = result.expiresAt
|
||||||
|
setAuthToken(result.token)
|
||||||
|
applyUser(result.user)
|
||||||
|
persist()
|
||||||
|
bootstrapped.value = true
|
||||||
|
return result.user
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changePassword(payload: ChangePasswordPayload) {
|
||||||
|
const user = await devopsApi.changePassword(payload)
|
||||||
|
applyUser(user)
|
||||||
|
persist()
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
if (token.value) {
|
||||||
|
try {
|
||||||
|
await devopsApi.logout()
|
||||||
|
} catch {
|
||||||
|
// 退出以清理本地登录态为准,后端当前无状态 token 无需强依赖 logout 成功。
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyUser(user: AuthenticatedUser) {
|
||||||
|
currentUser.value = user
|
||||||
|
operator.value = user.account
|
||||||
|
displayName.value = user.displayName
|
||||||
|
role.value = user.role
|
||||||
|
}
|
||||||
|
|
||||||
|
function persist() {
|
||||||
|
if (!token.value || !currentUser.value) {
|
||||||
|
localStorage.removeItem(STORAGE_KEY)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const stored: StoredSession = {
|
||||||
|
token: token.value,
|
||||||
|
expiresAt: expiresAt.value,
|
||||||
|
user: currentUser.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(stored))
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSession() {
|
||||||
|
operator.value = ''
|
||||||
|
displayName.value = ''
|
||||||
|
role.value = ''
|
||||||
token.value = ''
|
token.value = ''
|
||||||
|
expiresAt.value = ''
|
||||||
|
currentUser.value = null
|
||||||
|
setAuthToken()
|
||||||
|
localStorage.removeItem(STORAGE_KEY)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
operator,
|
operator,
|
||||||
|
displayName,
|
||||||
role,
|
role,
|
||||||
|
roleLabel,
|
||||||
token,
|
token,
|
||||||
|
expiresAt,
|
||||||
|
currentUser,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
|
mustChangePassword,
|
||||||
|
bootstrap,
|
||||||
login,
|
login,
|
||||||
|
changePassword,
|
||||||
logout,
|
logout,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function loadStoredSession(): StoredSession | null {
|
||||||
|
try {
|
||||||
|
const rawValue = localStorage.getItem(STORAGE_KEY)
|
||||||
|
|
||||||
|
if (!rawValue) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = JSON.parse(rawValue) as StoredSession
|
||||||
|
|
||||||
|
if (!parsed.token || !parsed.user?.account) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed
|
||||||
|
} catch {
|
||||||
|
localStorage.removeItem(STORAGE_KEY)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+31
-1
@@ -31,6 +31,36 @@ export type AgentPurpose =
|
|||||||
| 'release-note'
|
| 'release-note'
|
||||||
| 'incident-review'
|
| 'incident-review'
|
||||||
|
|
||||||
|
export type AuthRole = 'super_admin' | 'member'
|
||||||
|
|
||||||
|
export type AuthStatus = 'active' | 'disabled'
|
||||||
|
|
||||||
|
export interface AuthenticatedUser {
|
||||||
|
id: string
|
||||||
|
account: string
|
||||||
|
displayName: string
|
||||||
|
role: AuthRole
|
||||||
|
status: AuthStatus
|
||||||
|
mustChangePassword: boolean
|
||||||
|
passwordVersion: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginPayload {
|
||||||
|
account: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResult {
|
||||||
|
token: string
|
||||||
|
expiresAt: string
|
||||||
|
user: AuthenticatedUser
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChangePasswordPayload {
|
||||||
|
currentPassword: string
|
||||||
|
newPassword: string
|
||||||
|
}
|
||||||
|
|
||||||
export type IntegrationConfigKey =
|
export type IntegrationConfigKey =
|
||||||
| 'jenkins'
|
| 'jenkins'
|
||||||
| 'gitea'
|
| 'gitea'
|
||||||
@@ -221,7 +251,7 @@ export interface CreateDeployRunPayload {
|
|||||||
projectKey: string
|
projectKey: string
|
||||||
environment: EnvironmentName
|
environment: EnvironmentName
|
||||||
ref: string
|
ref: string
|
||||||
operator: string
|
operator?: string
|
||||||
remark?: string
|
remark?: string
|
||||||
idempotencyKey?: string
|
idempotencyKey?: string
|
||||||
}
|
}
|
||||||
|
|||||||
+165
-21
@@ -1,22 +1,94 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 登录页对接后端真实鉴权,并处理普通成员首次登录强制改密。
|
||||||
|
*/
|
||||||
import { Lock, Monitor } from '@element-plus/icons-vue'
|
import { Lock, Monitor } from '@element-plus/icons-vue'
|
||||||
import { reactive } from 'vue'
|
import { computed, reactive, ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useSessionStore } from '../stores/session'
|
import { useSessionStore } from '../stores/session'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const session = useSessionStore()
|
const session = useSessionStore()
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
const form = reactive({
|
const loginForm = reactive({
|
||||||
operator: 'ops-admin',
|
account: '',
|
||||||
password: '',
|
password: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const passwordForm = reactive({
|
||||||
|
currentPassword: '',
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const redirect = computed(() =>
|
||||||
|
typeof route.query.redirect === 'string' ? route.query.redirect : '/overview',
|
||||||
|
)
|
||||||
|
|
||||||
|
async function submitLogin() {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await session.login(loginForm)
|
||||||
|
passwordForm.currentPassword = loginForm.password
|
||||||
|
|
||||||
|
if (user.mustChangePassword) {
|
||||||
|
ElMessage.warning('首次登录请先修改密码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await router.push(redirect.value)
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error instanceof Error ? error.message : '登录失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitPasswordChange() {
|
||||||
|
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
||||||
|
ElMessage.error('两次输入的新密码不一致')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await session.changePassword({
|
||||||
|
currentPassword: passwordForm.currentPassword,
|
||||||
|
newPassword: passwordForm.newPassword,
|
||||||
|
})
|
||||||
|
ElMessage.success('密码已更新')
|
||||||
|
await router.push(redirect.value)
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error instanceof Error ? error.message : '修改密码失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logoutForAccountSwitch() {
|
||||||
|
await session.logout()
|
||||||
|
loginForm.password = ''
|
||||||
|
passwordForm.currentPassword = ''
|
||||||
|
passwordForm.newPassword = ''
|
||||||
|
passwordForm.confirmPassword = ''
|
||||||
|
}
|
||||||
|
|
||||||
function submit() {
|
function submit() {
|
||||||
session.login(form.operator)
|
if (session.mustChangePassword) {
|
||||||
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/overview'
|
void submitPasswordChange()
|
||||||
void router.push(redirect)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
void submitLogin()
|
||||||
|
}
|
||||||
|
|
||||||
|
function accountLabel() {
|
||||||
|
return session.displayName || session.operator
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -26,24 +98,78 @@ function submit() {
|
|||||||
<div class="login-copy">
|
<div class="login-copy">
|
||||||
<ElIcon><Monitor /></ElIcon>
|
<ElIcon><Monitor /></ElIcon>
|
||||||
<h1>DevOps 运维平台</h1>
|
<h1>DevOps 运维平台</h1>
|
||||||
<p>当前使用本地操作员会话进入控制台;真实身份、RBAC 和审计由后端认证模块接入。</p>
|
<p>使用平台账号进入控制台。</p>
|
||||||
</div>
|
</div>
|
||||||
<ElForm class="login-form" label-position="top" @submit.prevent="submit">
|
<ElForm class="login-form" label-position="top" @submit.prevent="submit">
|
||||||
<ElFormItem label="操作员">
|
<template v-if="!session.mustChangePassword">
|
||||||
<ElInput v-model="form.operator" size="large" autocomplete="username" />
|
<ElFormItem label="账号">
|
||||||
</ElFormItem>
|
<ElInput v-model="loginForm.account" size="large" autocomplete="username" />
|
||||||
<ElFormItem label="口令">
|
</ElFormItem>
|
||||||
<ElInput
|
<ElFormItem label="密码">
|
||||||
v-model="form.password"
|
<ElInput
|
||||||
|
v-model="loginForm.password"
|
||||||
|
size="large"
|
||||||
|
type="password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElButton
|
||||||
|
type="primary"
|
||||||
size="large"
|
size="large"
|
||||||
type="password"
|
:icon="Lock"
|
||||||
autocomplete="current-password"
|
:loading="loading"
|
||||||
show-password
|
native-type="submit"
|
||||||
/>
|
>
|
||||||
</ElFormItem>
|
登录
|
||||||
<ElButton type="primary" size="large" :icon="Lock" native-type="submit">
|
</ElButton>
|
||||||
进入控制台
|
</template>
|
||||||
</ElButton>
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="change-title">
|
||||||
|
<strong>{{ accountLabel() }}</strong>
|
||||||
|
<span>需要更新密码</span>
|
||||||
|
</div>
|
||||||
|
<ElFormItem label="当前密码">
|
||||||
|
<ElInput
|
||||||
|
v-model="passwordForm.currentPassword"
|
||||||
|
size="large"
|
||||||
|
type="password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="新密码">
|
||||||
|
<ElInput
|
||||||
|
v-model="passwordForm.newPassword"
|
||||||
|
size="large"
|
||||||
|
type="password"
|
||||||
|
autocomplete="new-password"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="确认新密码">
|
||||||
|
<ElInput
|
||||||
|
v-model="passwordForm.confirmPassword"
|
||||||
|
size="large"
|
||||||
|
type="password"
|
||||||
|
autocomplete="new-password"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElButton
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
:icon="Lock"
|
||||||
|
:loading="loading"
|
||||||
|
native-type="submit"
|
||||||
|
>
|
||||||
|
更新并进入
|
||||||
|
</ElButton>
|
||||||
|
<ElButton class="switch-account" text @click="logoutForAccountSwitch">
|
||||||
|
切换账号
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
@@ -103,6 +229,24 @@ function submit() {
|
|||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.change-title {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
color: #111827;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-title span {
|
||||||
|
color: #697386;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-account {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 780px) {
|
@media (max-width: 780px) {
|
||||||
.login-panel {
|
.login-panel {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 发布中心负责创建发布单、同步 Jenkins 状态并展示当前运行流程。
|
||||||
|
*/
|
||||||
import { CircleClose, Promotion, RefreshRight, VideoPause } from '@element-plus/icons-vue'
|
import { CircleClose, Promotion, RefreshRight, VideoPause } from '@element-plus/icons-vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { computed, reactive, ref, watch } from 'vue'
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 运行记录页展示历史发布单、Jenkins 同步入口和后端步骤流转图。
|
||||||
|
*/
|
||||||
import { RefreshRight } from '@element-plus/icons-vue'
|
import { RefreshRight } from '@element-plus/icons-vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 系统配置页展示集成变量说明、配置边界和审计日志脱敏 JSON。
|
||||||
|
*/
|
||||||
import { Refresh } from '@element-plus/icons-vue'
|
import { Refresh } from '@element-plus/icons-vue'
|
||||||
import AuditJsonCell from '../components/AuditJsonCell.vue'
|
import AuditJsonCell from '../components/AuditJsonCell.vue'
|
||||||
import StatusPill from '../components/StatusPill.vue'
|
import StatusPill from '../components/StatusPill.vue'
|
||||||
@@ -68,8 +71,26 @@ function auditResource(record: AuditLogEntry): string {
|
|||||||
: record.resourceType
|
: record.resourceType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasAuditRecord(value?: Record<string, unknown>): value is Record<string, unknown> {
|
||||||
|
return Boolean(value && Object.keys(value).length > 0)
|
||||||
|
}
|
||||||
|
|
||||||
function auditPayload(record: AuditLogEntry): unknown {
|
function auditPayload(record: AuditLogEntry): unknown {
|
||||||
return record.after ?? record.parameterDigest ?? record.before ?? null
|
const payload: Record<string, unknown> = {}
|
||||||
|
|
||||||
|
if (hasAuditRecord(record.parameterDigest)) {
|
||||||
|
payload.parameterDigest = record.parameterDigest
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAuditRecord(record.before)) {
|
||||||
|
payload.before = record.before
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAuditRecord(record.after)) {
|
||||||
|
payload.after = record.after
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(payload).length > 0 ? payload : null
|
||||||
}
|
}
|
||||||
|
|
||||||
function variableGroups(item: IntegrationConfigItem): VariableGroup[] {
|
function variableGroups(item: IntegrationConfigItem): VariableGroup[] {
|
||||||
|
|||||||
Reference in New Issue
Block a user