main
vue 172 lines 4.06 KB
Raw
1 <template>
2 <div>
3 <!-- ── Normal login form ── -->
4 <n-collapse-transition :show="!show2fa">
5 <div class="flex flex-col">
6 <n-form ref="formRef" :model :rules>
7 <n-form-item path="username" label="Username">
8 <n-input
9 v-model:value="model.username"
10 placeholder="Insert your username"
11 :input-props="{ autocomplete: 'username' }"
12 size="large"
13 @keydown.enter="signIn"
14 />
15 </n-form-item>
16 <n-form-item path="password" label="Password">
17 <n-input
18 v-model:value="model.password"
19 type="password"
20 show-password-on="click"
21 placeholder="Insert your password"
22 :input-props="{ autocomplete: 'password' }"
23 size="large"
24 @keydown.enter="signIn"
25 />
26 </n-form-item>
27 <div class="flex flex-col items-end gap-6">
28 <div class="w-full">
29 <n-button
30 type="primary"
31 class="w-full!"
32 size="large"
33 :loading
34 :disabled="!isValid"
35 @click="signIn"
36 >
37 Sign in
38 </n-button>
39 </div>
40 </div>
41 </n-form>
42
43 <SsoOptions @show2fa-form="handleShow2faForm" @login-success="handleLoginSuccess" />
44 </div>
45 </n-collapse-transition>
46
47 <n-collapse-transition :show="show2fa">
48 <TotpForm v-model:two-fa-temp-token="twoFaTempToken" @cancel="cancel2fa" />
49 </n-collapse-transition>
50 </div>
51 </template>
52
53 <script lang="ts" setup>
54 import type { FormInst, FormRules, FormValidationError } from "naive-ui"
55 import type { LoginPayload } from "@/types/auth.d"
56 import { NButton, NCollapseTransition, NForm, NFormItem, NInput, useMessage } from "naive-ui"
57 import { computed, onBeforeMount, ref, watch } from "vue"
58 import { useRouter } from "vue-router"
59 import { useAuthStore } from "@/stores/auth"
60 import SsoOptions from "./SsoOptions.vue"
61 import TotpForm from "./TotpForm.vue"
62
63 interface ModelType {
64 username: string | null
65 password: string | null
66 }
67
68 const router = useRouter()
69 const authStore = useAuthStore()
70 const message = useMessage()
71
72 const loading = ref(false)
73 const formRef = ref<FormInst | null>(null)
74 const model = ref<ModelType>({
75 username: null,
76 password: null
77 })
78 const show2fa = ref(false)
79 const twoFaTempToken = ref("")
80
81 const rules: FormRules = {
82 username: [
83 {
84 required: true,
85 trigger: ["blur"],
86 message: "Username is required"
87 }
88 ],
89 password: [
90 {
91 required: true,
92 trigger: ["blur"],
93 message: "Password is required"
94 }
95 ]
96 }
97
98 const isValid = computed(() => {
99 return model.value.username && model.value.password
100 })
101
102 function cancel2fa() {
103 show2fa.value = false
104 twoFaTempToken.value = ""
105 }
106
107 function handleShow2faForm(token: string) {
108 twoFaTempToken.value = token
109 show2fa.value = true
110 }
111
112 function handleLoginSuccess(token: string) {
113 authStore.setLogged(token)
114 router.push({ path: "/", replace: true })
115 }
116
117 function signIn(e: Event) {
118 e.preventDefault()
119
120 formRef.value?.validate((errors: Array<FormValidationError> | undefined) => {
121 if (!errors) {
122 loading.value = true
123
124 const payload: LoginPayload = {
125 username: model.value.username || "",
126 password: model.value.password || ""
127 }
128
129 authStore
130 .login(payload)
131 .then(res => {
132 // Check if 2FA is required
133 if (res?.requires_2fa) {
134 handleShow2faForm(res.access_token)
135 return
136 }
137 router.push({ path: "/", replace: true })
138 })
139 .catch(err => {
140 message.error(err?.message || "An error occurred. Please try again later.")
141 })
142 .finally(() => {
143 loading.value = false
144 })
145 } else {
146 message.error("Invalid credentials")
147 }
148 })
149 }
150
151 watch(isValid, val => {
152 if (val) {
153 formRef.value?.validate()
154 }
155 })
156
157 onBeforeMount(() => {
158 // Check if we're returning from SSO callback (token in URL fragment, not query)
159 const params = new URLSearchParams(window.location.hash.substring(1) || window.location.search)
160 const error_message = params.get("error_message")
161
162 if (params.has("error_message")) {
163 params.delete("error_message")
164 }
165
166 if (error_message) {
167 message.error(error_message, { duration: 6_000 })
168 }
169
170 history.replaceState(null, "", `${window.location.pathname}?${params.toString()}`)
171 })
172 </script>