main
vue 127 lines 3.5 KB
Raw
1 <template>
2 <n-collapse-transition :show>
3 <div class="flex flex-col">
4 <n-divider>
5 <span class="text-secondary text-xs">or sign in with</span>
6 </n-divider>
7 <div class="flex flex-col gap-3">
8 <n-button v-if="ssoStatus?.azure_enabled" size="large" class="w-full!" @click="loginWithAzure">
9 <template #icon>
10 <Icon :name="AzureIcon" :size="15" />
11 </template>
12 Microsoft Azure
13 </n-button>
14 <n-button v-if="ssoStatus?.google_enabled" size="large" class="w-full!" @click="loginWithGoogle">
15 <template #icon>
16 <Icon :name="GoogleIcon" :size="16" />
17 </template>
18 Google
19 </n-button>
20 <n-button
21 v-if="ssoStatus?.cf_enabled"
22 size="large"
23 class="w-full!"
24 :loading="cfLoading"
25 @click="loginWithCloudflare"
26 >
27 <template #icon>
28 <Icon :name="CloudflareIcon" :size="18" />
29 </template>
30 Cloudflare Access
31 </n-button>
32 </div>
33 </div>
34 </n-collapse-transition>
35 </template>
36
37 <script lang="ts" setup>
38 import type { SSOPublicStatus } from "@/api/endpoints/sso"
39 import { NButton, NCollapseTransition, NDivider, useMessage } from "naive-ui"
40 import { computed, onBeforeMount, ref } from "vue"
41 import Api from "@/api"
42 import Icon from "@/components/common/Icon.vue"
43
44 const emit = defineEmits<{
45 (e: "show2faForm", value: string): void
46 (e: "loginSuccess", value: string): void
47 }>()
48
49 const AzureIcon = "devicon-plain:azure"
50 const GoogleIcon = "devicon-plain:google"
51 const CloudflareIcon = "simple-icons:cloudflare"
52
53 const cfLoading = ref(false)
54 const message = useMessage()
55 const ssoStatus = ref<SSOPublicStatus | null>(null)
56
57 const show = computed(() => {
58 return (
59 (ssoStatus.value?.sso_enabled &&
60 (ssoStatus.value.azure_enabled || ssoStatus.value.google_enabled || ssoStatus.value.cf_enabled)) ||
61 false
62 )
63 })
64
65 function loginWithAzure() {
66 window.location.href = "/api/auth/sso/azure/login"
67 }
68
69 function loginWithGoogle() {
70 window.location.href = "/api/auth/sso/google/login"
71 }
72
73 async function loginWithCloudflare() {
74 cfLoading.value = true
75
76 try {
77 const res = await Api.sso.cloudflareVerify()
78 if (res.data?.requires_2fa && res.data?.access_token) {
79 // Cloudflare auth OK but user has 2FA — show verification step
80 emit("show2faForm", res.data.access_token)
81 } else if (res.data?.access_token) {
82 emit("loginSuccess", res.data.access_token)
83 }
84 } catch (err: any) {
85 message.error(
86 err.response?.data?.message || err.response?.data?.detail
87 ? (err.response?.data?.message || err.response?.data?.detail)?.toString()
88 : "Cloudflare Access authentication failed. Make sure you are behind Cloudflare Access."
89 )
90 } finally {
91 cfLoading.value = false
92 }
93 }
94
95 async function loadSSOStatus() {
96 try {
97 const res = await Api.sso.getStatus()
98 ssoStatus.value = res.data
99 } catch {
100 ssoStatus.value = null
101 }
102 }
103
104 onBeforeMount(() => {
105 loadSSOStatus()
106
107 // Check if we're returning from SSO callback (token in URL fragment, not query)
108 const params = new URLSearchParams(window.location.hash.substring(1) || window.location.search)
109 const token = params.get("token")
110 const requires2fa = params.get("requires_2fa")?.toLowerCase() === "true"
111
112 if (params.has("token")) {
113 params.delete("token")
114 }
115 if (params.has("requires_2fa")) {
116 params.delete("requires_2fa")
117 }
118
119 if (token && requires2fa) {
120 emit("show2faForm", token)
121 } else if (token) {
122 // Remove fragment from history so the token isn't stored in browser history
123 history.replaceState(null, "", `${window.location.pathname}?${params.toString()}`)
124 emit("loginSuccess", token)
125 }
126 })
127 </script>