添加sso
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { apiErrorMessage } from '../api/http'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import http from '../api/http'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const loading = ref(false)
|
||||
const ssoLoading = ref(false)
|
||||
const sso = reactive({ enabled: false, displayName: '学校统一身份认证' })
|
||||
const form = reactive({
|
||||
userName: String(route.query.userName ?? ''),
|
||||
password: '',
|
||||
@@ -24,6 +27,36 @@ async function submit() {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function startSso() {
|
||||
ssoLoading.value = true
|
||||
const apiBaseUrl = String(import.meta.env.VITE_API_BASE_URL ?? '/api').replace(/\/$/, '')
|
||||
const redirect = String(route.query.redirect ?? '/dashboard')
|
||||
window.location.assign(
|
||||
`${apiBaseUrl}/auth/sso/login?returnUrl=${encodeURIComponent(redirect)}`,
|
||||
)
|
||||
}
|
||||
|
||||
const ssoErrors: Record<string, string> = {
|
||||
authentication_failed: '统一身份认证未完成,请重新尝试。',
|
||||
missing_subject: 'Keycloak 未返回用户唯一标识,请联系管理员检查客户端映射。',
|
||||
account_disabled: '教务系统账号已停用或锁定,请联系管理员。',
|
||||
account_link_failed: '统一身份账号绑定失败,请联系管理员。',
|
||||
account_update_failed: '登录状态更新失败,请稍后重试。',
|
||||
binding_expired: '账户绑定请求已失效,请重新使用统一身份认证登录。',
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const ssoError = String(route.query.ssoError ?? '')
|
||||
if (ssoError) ElMessage.error(ssoErrors[ssoError] ?? '统一身份认证失败,请重新尝试。')
|
||||
try {
|
||||
const { data } = await http.get('/auth/sso/settings')
|
||||
sso.enabled = Boolean(data.enabled)
|
||||
sso.displayName = String(data.displayName || sso.displayName)
|
||||
} catch {
|
||||
sso.enabled = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -82,6 +115,16 @@ async function submit() {
|
||||
>
|
||||
进入工作台
|
||||
</el-button>
|
||||
<div v-if="sso.enabled" class="login-divider"><span>或</span></div>
|
||||
<el-button
|
||||
v-if="sso.enabled"
|
||||
class="sso-login-submit"
|
||||
size="large"
|
||||
:loading="ssoLoading"
|
||||
@click="startSso"
|
||||
>
|
||||
使用{{ sso.displayName }}登录
|
||||
</el-button>
|
||||
<router-link class="public-timetable-link" to="/timetable">无需登录,查询班级课表 →</router-link>
|
||||
<router-link class="account-activation-link" to="/activate">学生首次登录?自助激活账号 →</router-link>
|
||||
</form>
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import http, { apiErrorMessage } from '../api/http'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const loading = ref(false)
|
||||
const checking = ref(true)
|
||||
const binding = reactive({
|
||||
providerDisplayName: '学校统一身份认证',
|
||||
externalUserName: '',
|
||||
})
|
||||
const form = reactive({ userName: '', password: '' })
|
||||
|
||||
function safeRedirect(value: unknown) {
|
||||
const path = String(value ?? '')
|
||||
return path.startsWith('/') && !path.startsWith('//') ? path : '/dashboard'
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!form.userName.trim() || !form.password) {
|
||||
ElMessage.warning('请填写现有教务系统账号和密码。')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
await auth.bindSso(String(route.query.code ?? ''), form.userName, form.password)
|
||||
ElMessage.success('账户绑定成功。')
|
||||
await router.replace(safeRedirect(route.query.redirect))
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const code = String(route.query.code ?? '')
|
||||
if (!code) {
|
||||
await router.replace({ name: 'login', query: { ssoError: 'authentication_failed' } })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await http.get('/auth/sso/binding', { params: { code } })
|
||||
binding.providerDisplayName = String(data.providerDisplayName || binding.providerDisplayName)
|
||||
binding.externalUserName = String(data.externalUserName || '')
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
await router.replace({ name: 'login', query: { ssoError: 'binding_expired' } })
|
||||
} finally {
|
||||
checking.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="sso-binding-page">
|
||||
<section v-loading="checking" class="sso-binding-card">
|
||||
<div class="brand-mark" aria-hidden="true">
|
||||
<span v-for="index in 9" :key="index" />
|
||||
</div>
|
||||
<span class="eyebrow">首次使用统一身份认证</span>
|
||||
<h1>绑定现有教务系统账号</h1>
|
||||
<p class="binding-description">
|
||||
已通过{{ binding.providerDisplayName }}验证
|
||||
<strong v-if="binding.externalUserName">{{ binding.externalUserName }}</strong>。
|
||||
请输入一次现有教务系统账号和密码,今后即可直接使用单点登录。
|
||||
</p>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<label>
|
||||
<span>教务系统账号</span>
|
||||
<el-input v-model="form.userName" size="large" autocomplete="username" />
|
||||
</label>
|
||||
<label>
|
||||
<span>教务系统密码</span>
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
size="large"
|
||||
type="password"
|
||||
show-password
|
||||
autocomplete="current-password"
|
||||
@keyup.enter="submit"
|
||||
/>
|
||||
</label>
|
||||
<el-button
|
||||
class="binding-submit"
|
||||
type="primary"
|
||||
size="large"
|
||||
native-type="submit"
|
||||
:loading="loading"
|
||||
:disabled="checking"
|
||||
>
|
||||
确认绑定并登录
|
||||
</el-button>
|
||||
</form>
|
||||
|
||||
<p class="binding-security-note">
|
||||
绑定只建立登录关联,不会修改您的角色、学院或人员档案。
|
||||
</p>
|
||||
<router-link to="/login">取消并返回登录页</router-link>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { apiErrorMessage } from '../api/http'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
|
||||
function safeRedirect(value: unknown) {
|
||||
const path = String(value ?? '')
|
||||
return path.startsWith('/') && !path.startsWith('//') ? path : '/dashboard'
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const code = String(route.query.code ?? '')
|
||||
if (!code) {
|
||||
await router.replace({ name: 'login', query: { ssoError: 'authentication_failed' } })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await auth.exchangeSso(code)
|
||||
await router.replace(safeRedirect(route.query.redirect))
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
await router.replace({ name: 'login', query: { ssoError: 'authentication_failed' } })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="sso-callback-page">
|
||||
<el-icon class="is-loading" :size="34"><Loading /></el-icon>
|
||||
<h1>正在完成统一身份认证</h1>
|
||||
<p>请稍候,不要关闭此页面。</p>
|
||||
</main>
|
||||
</template>
|
||||
Reference in New Issue
Block a user