101 lines
5.4 KiB
Vue
101 lines
5.4 KiB
Vue
<template>
|
||
<section class="oidc-guide">
|
||
<div class="guide-title"><b>{{ $t('oidcGuide.title') }}</b><el-tag type="warning" effect="plain">{{ $t('oidcGuide.externalService') }}</el-tag></div>
|
||
<el-steps :active="guideStep" finish-status="success" align-center class="guide-steps">
|
||
<el-step :title="$t('oidcGuide.app')" :description="$t('oidcGuide.appShort')" />
|
||
<el-step :title="$t('oidcGuide.identity')" :description="$t('oidcGuide.identityShort')" />
|
||
<el-step :title="$t('oidcGuide.callback')" :description="$t('oidcGuide.callbackShort')" />
|
||
<el-step :title="$t('oidcGuide.test')" :description="$t('oidcGuide.testShort')" />
|
||
</el-steps>
|
||
<el-alert type="info" :closable="false" :title="$t('oidcGuide.appTip')" class="guide-alert">
|
||
<template #default>
|
||
<a href="https://open.feishu.cn/app" target="_blank" rel="noopener noreferrer">{{ $t('oidcGuide.openConsole') }} ↗</a>
|
||
<ol><li>{{ $t('oidcGuide.appInstruction1') }}</li><li>{{ $t('oidcGuide.appInstruction2') }}</li><li>{{ $t('oidcGuide.appInstruction3') }}</li></ol>
|
||
<p>{{ $t('oidcGuide.scopeTip') }}</p>
|
||
</template>
|
||
</el-alert>
|
||
<el-form-item :label="$t('oidcGuide.issuer')" required>
|
||
<el-input v-model.trim="config.oidc_issuer" placeholder="https://idp.example.com" />
|
||
<div class="field-tip">{{ $t('oidcGuide.issuerTip') }}</div>
|
||
</el-form-item>
|
||
<el-form-item :label="$t('oidcGuide.clientId')" required>
|
||
<el-input v-model.trim="config.oidc_client_id" />
|
||
</el-form-item>
|
||
<el-form-item :label="$t('oidcGuide.clientSecret')" :required="!editing">
|
||
<el-input v-model="config.oidc_client_secret" type="password" show-password autocomplete="new-password" :placeholder="editing ? $t('oidcGuide.keepSecret') : $t('oidcGuide.copySecret')" />
|
||
<div class="field-tip">{{ $t('oidcGuide.secretTip') }}</div>
|
||
</el-form-item>
|
||
<el-form-item :label="$t('oidcGuide.baseUrl')" required>
|
||
<el-input v-model.trim="config.oidc_base_url" placeholder="https://vpn-login.example.com" />
|
||
<div class="field-tip">{{ $t('oidcGuide.baseTip') }}</div>
|
||
</el-form-item>
|
||
<el-form-item :label="$t('oidcGuide.listenPort')">
|
||
<div class="port-controls">
|
||
<el-button :disabled="portLoading || !config.oidc_http_port || config.oidc_http_port <= 1024" @click="stepPort(-1)">−</el-button>
|
||
<el-input-number v-model="config.oidc_http_port" :controls="false" :min="1024" :max="65535" :disabled="portLoading" @change="changePort" />
|
||
<el-button :disabled="portLoading || !config.oidc_http_port || config.oidc_http_port >= 65535" @click="stepPort(1)">+</el-button>
|
||
</div>
|
||
<div class="field-tip">{{ $t('oidcGuide.portTip') }}</div>
|
||
</el-form-item>
|
||
<el-alert type="warning" :closable="false" class="guide-alert" :title="$t('oidcGuide.callbackTitle')">
|
||
<template #default>
|
||
<code>{{ callbackUrl }}</code>
|
||
<p>{{ $t('oidcGuide.callbackTip', { port: config.oidc_http_port || 19090 }) }}</p>
|
||
</template>
|
||
</el-alert>
|
||
<el-alert type="info" :closable="false" :title="$t('oidcGuide.testTip')" />
|
||
</section>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, onMounted, ref } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { useI18n } from 'vue-i18n'
|
||
import api from '../api'
|
||
|
||
const props = defineProps({ config: { type: Object, required: true }, editing: { type: Boolean, default: false } })
|
||
const { t } = useI18n()
|
||
const portLoading = ref(false)
|
||
async function requestPort(from, direction, previous = null) {
|
||
portLoading.value = true
|
||
try {
|
||
const params = from == null ? {} : { from, direction }
|
||
const { data } = await api.get('/tunnels/openvpn/oidc-port', { params })
|
||
props.config.oidc_http_port = data.port
|
||
} catch (error) {
|
||
if (Number.isInteger(previous)) props.config.oidc_http_port = previous
|
||
ElMessage.error(error.response?.data?.error || t('oidcGuide.portLookupFailed'))
|
||
} finally {
|
||
portLoading.value = false
|
||
}
|
||
}
|
||
function stepPort(direction) {
|
||
const previous = Number(props.config.oidc_http_port)
|
||
requestPort(previous + direction, direction, previous)
|
||
}
|
||
function changePort(value, previous) {
|
||
if (!Number.isInteger(value) || value === previous) return
|
||
requestPort(value, value < previous ? -1 : 1, previous)
|
||
}
|
||
onMounted(() => {
|
||
if (!props.editing) requestPort(null, 1)
|
||
})
|
||
const callbackUrl = computed(() => `${(props.config.oidc_base_url || 'https://vpn-login.example.com').replace(/\/+$/, '')}/oauth2/callback`)
|
||
const guideStep = computed(() => !props.config.oidc_client_id ? 0
|
||
: !props.config.oidc_issuer || (!props.config.oidc_client_secret && !props.editing) ? 1
|
||
: !props.config.oidc_base_url ? 2 : 3)
|
||
</script>
|
||
|
||
<style scoped>
|
||
.oidc-guide { margin: 10px 0 24px; padding: 18px; border: 1px solid var(--border-base); border-radius: var(--radius-sm); background: var(--bg-inset); }
|
||
.guide-title { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
||
.guide-steps { margin: 24px 0; }
|
||
.guide-alert { margin: 16px 0; }
|
||
.guide-alert p { margin: 8px 0 0; line-height: 1.5; }
|
||
.guide-alert ol { padding-left: 20px; line-height: 1.7; }
|
||
.field-tip { margin-top: 4px; color: var(--text-secondary); font-size: 12px; line-height: 1.5; }
|
||
.port-controls { display: flex; gap: 6px; align-items: center; }
|
||
code { overflow-wrap: anywhere; }
|
||
@media (max-width: 720px) { .guide-steps { overflow-x: auto; justify-content: flex-start; } .guide-steps :deep(.el-step) { min-width: 120px; } }
|
||
</style>
|