Login.vue 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. <script setup lang="ts">
  2. import { KeyOutlined, LockOutlined, UserOutlined } from '@ant-design/icons-vue'
  3. import { startAuthentication } from '@simplewebauthn/browser'
  4. import { Form, message } from 'ant-design-vue'
  5. import auth from '@/api/auth'
  6. import install from '@/api/install'
  7. import passkey from '@/api/passkey'
  8. import ICP from '@/components/ICP'
  9. import SetLanguage from '@/components/SetLanguage'
  10. import SwitchAppearance from '@/components/SwitchAppearance'
  11. import Authorization from '@/components/TwoFA'
  12. import gettext from '@/gettext'
  13. import { useUserStore } from '@/pinia'
  14. const thisYear = new Date().getFullYear()
  15. const route = useRoute()
  16. const router = useRouter()
  17. install.get_lock().then(async (r: { lock: boolean }) => {
  18. if (!r.lock)
  19. await router.push('/install')
  20. })
  21. const loading = ref(false)
  22. const enabled2FA = ref(false)
  23. const refOTP = useTemplateRef('refOTP')
  24. const passcode = ref('')
  25. const recoveryCode = ref('')
  26. const passkeyConfigStatus = ref(false)
  27. const modelRef = reactive({
  28. username: '',
  29. password: '',
  30. })
  31. const rulesRef = reactive({
  32. username: [
  33. {
  34. required: true,
  35. message: () => $gettext('Please input your username!'),
  36. },
  37. ],
  38. password: [
  39. {
  40. required: true,
  41. message: () => $gettext('Please input your password!'),
  42. },
  43. ],
  44. })
  45. const { validate, validateInfos, clearValidate } = Form.useForm(modelRef, rulesRef)
  46. const userStore = useUserStore()
  47. const { login, passkeyLogin } = userStore
  48. const { secureSessionId } = storeToRefs(userStore)
  49. function onSubmit() {
  50. validate().then(async () => {
  51. loading.value = true
  52. await auth.login(modelRef.username, modelRef.password, passcode.value, recoveryCode.value).then(async r => {
  53. const next = (route.query?.next || '').toString() || '/'
  54. switch (r.code) {
  55. case 200:
  56. message.success($gettext('Login successful'), 1)
  57. login(r.token)
  58. await nextTick()
  59. secureSessionId.value = r.secure_session_id
  60. await userStore.getCurrentUser()
  61. await nextTick()
  62. await router.push(next)
  63. break
  64. case 199:
  65. enabled2FA.value = true
  66. break
  67. }
  68. }).catch(e => {
  69. if (e.code === 4043) {
  70. refOTP.value?.clearInput()
  71. }
  72. })
  73. loading.value = false
  74. })
  75. }
  76. const user = useUserStore()
  77. if (user.isLogin) {
  78. const next = (route.query?.next || '').toString() || '/dashboard'
  79. router.push(next)
  80. }
  81. watch(() => gettext.current, () => {
  82. clearValidate()
  83. })
  84. const has_casdoor = ref(false)
  85. const casdoor_uri = ref('')
  86. auth.get_casdoor_uri()
  87. .then(r => {
  88. if (r?.uri) {
  89. has_casdoor.value = true
  90. casdoor_uri.value = r.uri
  91. }
  92. })
  93. function loginWithCasdoor() {
  94. window.location.href = casdoor_uri.value
  95. }
  96. if (route.query?.code !== undefined && route.query?.state !== undefined) {
  97. loading.value = true
  98. auth.casdoor_login(route.query?.code?.toString(), route.query?.state?.toString()).then(async () => {
  99. message.success($gettext('Login successful'), 1)
  100. const next = (route.query?.next || '').toString() || '/'
  101. await userStore.getCurrentUser()
  102. await nextTick()
  103. await router.push(next)
  104. })
  105. loading.value = false
  106. }
  107. function handleOTPSubmit(code: string, recovery: string) {
  108. passcode.value = code
  109. recoveryCode.value = recovery
  110. nextTick(() => {
  111. onSubmit()
  112. })
  113. }
  114. passkey.get_config_status().then(r => {
  115. passkeyConfigStatus.value = r.status
  116. })
  117. const passkeyLoginLoading = ref(false)
  118. async function handlePasskeyLogin() {
  119. passkeyLoginLoading.value = true
  120. const begin = await auth.begin_passkey_login()
  121. const asseResp = await startAuthentication({ optionsJSON: begin.options.publicKey })
  122. const r = await auth.finish_passkey_login({
  123. session_id: begin.session_id,
  124. options: asseResp,
  125. })
  126. if (r.token) {
  127. const next = (route.query?.next || '').toString() || '/'
  128. passkeyLogin(asseResp.rawId, r.token)
  129. secureSessionId.value = r.secure_session_id
  130. await userStore.getCurrentUser()
  131. await nextTick()
  132. await router.push(next)
  133. }
  134. passkeyLoginLoading.value = false
  135. }
  136. </script>
  137. <template>
  138. <ALayout>
  139. <ALayoutContent>
  140. <div class="login-container">
  141. <div class="login-form">
  142. <div class="project-title">
  143. <h1>Nginx UI</h1>
  144. </div>
  145. <AForm id="components-form-demo-normal-login">
  146. <template v-if="!enabled2FA">
  147. <AFormItem v-bind="validateInfos.username">
  148. <AInput
  149. v-model:value="modelRef.username"
  150. :placeholder="$gettext('Username')"
  151. >
  152. <template #prefix>
  153. <UserOutlined style="color: rgba(0, 0, 0, 0.25)" />
  154. </template>
  155. </AInput>
  156. </AFormItem>
  157. <AFormItem v-bind="validateInfos.password">
  158. <AInputPassword
  159. v-model:value="modelRef.password"
  160. :placeholder="$gettext('Password')"
  161. >
  162. <template #prefix>
  163. <LockOutlined style="color: rgba(0, 0, 0, 0.25)" />
  164. </template>
  165. </AInputPassword>
  166. </AFormItem>
  167. <AButton
  168. v-if="has_casdoor"
  169. block
  170. :loading="loading"
  171. class="mb-5"
  172. @click="loginWithCasdoor"
  173. >
  174. {{ $gettext('SSO Login') }}
  175. </AButton>
  176. </template>
  177. <div v-else>
  178. <Authorization
  179. ref="refOTP"
  180. :two-f-a-status="{
  181. enabled: true,
  182. otp_status: true,
  183. passkey_status: false,
  184. recovery_codes_generated: true,
  185. }"
  186. @submit-o-t-p="handleOTPSubmit"
  187. />
  188. </div>
  189. <AFormItem v-if="!enabled2FA">
  190. <AButton
  191. type="primary"
  192. block
  193. html-type="submit"
  194. :loading="loading"
  195. class="mb-2"
  196. @click="onSubmit"
  197. >
  198. {{ $gettext('Login') }}
  199. </AButton>
  200. <div
  201. v-if="passkeyConfigStatus"
  202. class="flex flex-col justify-center"
  203. >
  204. <ADivider>
  205. <div class="text-sm font-normal opacity-75">
  206. {{ $gettext('Or') }}
  207. </div>
  208. </ADivider>
  209. <AButton
  210. :loading="passkeyLoginLoading"
  211. @click="handlePasskeyLogin"
  212. >
  213. <KeyOutlined />
  214. {{ $gettext('Sign in with a passkey') }}
  215. </AButton>
  216. </div>
  217. </AFormItem>
  218. </AForm>
  219. <div class="footer">
  220. <p class="mb-4">
  221. Copyright © 2021 - {{ thisYear }} Nginx UI
  222. </p>
  223. <ICP class="mb-4" />
  224. Language
  225. <SetLanguage class="inline" />
  226. <div class="flex justify-center mt-4">
  227. <SwitchAppearance />
  228. </div>
  229. </div>
  230. </div>
  231. </div>
  232. </ALayoutContent>
  233. </ALayout>
  234. </template>
  235. <style lang="less" scoped>
  236. .ant-layout-content {
  237. background: #fff;
  238. }
  239. .dark .ant-layout-content {
  240. background: transparent;
  241. }
  242. .login-container {
  243. display: flex;
  244. align-items: center;
  245. justify-content: center;
  246. height: 100vh;
  247. .login-form {
  248. max-width: 420px;
  249. width: 80%;
  250. .project-title {
  251. margin: 50px;
  252. h1 {
  253. font-size: 50px;
  254. font-weight: 100;
  255. text-align: center;
  256. }
  257. }
  258. .anticon {
  259. color: #a8a5a5 !important;
  260. }
  261. .footer {
  262. padding: 30px 20px;
  263. text-align: center;
  264. font-size: 14px;
  265. }
  266. }
  267. }
  268. </style>