feat(openvpn): suggest available OIDC ports and add deployment assets

This commit is contained in:
opsx committed 2026-10-21 01:19:49 +08:00
1 parent d6a9a92ad0
commit 003f755b37
14 files changed
+244 -7

No files matched your search

+36 -2
View File
@@ -30,7 +30,11 @@
<div class="field-tip">{{ $t('oidcGuide.baseTip') }}</div>
</el-form-item>
<el-form-item :label="$t('oidcGuide.listenPort')">
<el-input-number v-model="config.oidc_http_port" :min="1024" :max="65535" />
<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')">
@@ -44,9 +48,38 @@
</template>
<script setup>
import { computed } from 'vue'
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
@@ -61,6 +94,7 @@ const guideStep = computed(() => !props.config.oidc_client_id ? 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>
+2 -1
View File
@@ -248,7 +248,8 @@
"baseUrl": "Public sign-in URL",
"baseTip": "Use a dedicated HTTPS root URL reachable before VPN connection. Give each OIDC tunnel its own endpoint.",
"listenPort": "Local auth service port",
"portTip": "The service listens on 127.0.0.1 only; each tunnel needs a distinct port.",
"portTip": "Listens only on 127.0.0.1; no public access needed. New tunnels increment from 19090; +/- and manual entry skip configured and locally occupied ports.",
"portLookupFailed": "Could not find an available port. Retry or enter one manually.",
"callbackTitle": "Register this callback with the final OIDC provider",
"callbackTip": "With an identity broker, register this URL in the broker's OIDC client, and register the broker's upstream callback in Feishu. Reverse proxy the public endpoint to 127.0.0.1:{port}; :18080 does not handle this callback.",
"testTip": "Install openvpn-auth-oauth2 and OpenVPN ≥ 2.6.2, then test with a WebAuth-capable client (e.g. OpenVPN Community for Windows or Tunnelblick/Viscosity for macOS). OpenVPN Connect v3 needs individual testing.",
+2 -1
View File
@@ -248,7 +248,8 @@
"baseUrl": "认证入口公网地址",
"baseTip": "填写独立 HTTPS 域名根地址;客户端未连上 VPN 时也必须能访问。每条 OIDC 专线使用独立入口。",
"listenPort": "本机认证服务端口",
"portTip": "认证服务仅监听 127.0.0.1;多条专线各用不同端口。",
"portTip": "仅监听本机 127.0.0.1,无需公网开放。新建专线自动从 19090 起递增;加减或手动输入会跳过已配置和本机正在监听的端口。",
"portLookupFailed": "无法获取可用端口,请稍后重试或手动选择",
"callbackTitle": "在最终 OIDC 身份提供方的客户端中登记此回调地址",
"callbackTip": "如果通过身份代理接入飞书,本地址登记在代理的 OIDC 客户端中,飞书应用应登记代理的上游回调。将公网入口的所有路径反向代理至本机 127.0.0.1:{port};不由 :18080 处理。",
"testTip": "安装 openvpn-auth-oauth2 与 OpenVPN ≥ 2.6.2 后启动专线;使用支持 WebAuth 的客户端试连(如 Windows OpenVPN Community、macOS Tunnelblick/Viscosity)。OpenVPN Connect v3 部分兼容,须实测。",
+1 -1
View File
@@ -4,7 +4,7 @@
<div class="login-left">
<div class="brand-content fade-in">
<div class="brand-logo">
<el-icon :size="32" color="#7dd3fc"><Lock /></el-icon>
<img src="/favicon.svg" alt="" width="40" height="40" />
</div>
<h1 class="brand-title">{{ $t('login.title') }}</h1>
<p class="brand-subtitle">{{ $t('login.subtitle') }}</p>
+1
View File
@@ -162,6 +162,7 @@ async function save() {
for (const key of ['oidc_issuer', 'oidc_client_id', 'oidc_base_url', ...(originalAuthMode.value === 'oidc' ? [] : ['oidc_client_secret'])]) {
if (!form.value.config[key]) return ElMessage.warning(t('oidcGuide.fillRequired'))
}
if (!form.value.config.oidc_http_port) return ElMessage.warning(t('oidcGuide.portLookupFailed'))
}
trafficMode.value = trafficMode.value
if (isOpenVPN.value && routeListRef.value && !routeListRef.value.validate()) return
+1
View File
@@ -233,6 +233,7 @@ async function create() {
for (const key of ['oidc_issuer', 'oidc_client_id', 'oidc_client_secret', 'oidc_base_url']) {
if (!form.value.config[key]) return ElMessage.warning(t('oidcGuide.fillRequired'))
}
if (!form.value.config.oidc_http_port) return ElMessage.warning(t('oidcGuide.portLookupFailed'))
}
trafficMode.value = trafficMode.value
if (isOpenVPN.value && routeListRef.value && !routeListRef.value.validate()) return