2018-02-10 08:22:14 +01:00
|
|
|
<template>
|
2023-04-08 02:01:42 +02:00
|
|
|
<form
|
|
|
|
class="eppvobhk _monolithic_"
|
|
|
|
:class="{ signing, totpLogin }"
|
|
|
|
@submit.prevent="onSubmit"
|
|
|
|
>
|
|
|
|
<div class="auth _section _formRoot">
|
|
|
|
<div
|
|
|
|
v-show="withAvatar"
|
|
|
|
class="avatar"
|
|
|
|
:style="{
|
|
|
|
backgroundImage: user ? `url('${user.avatarUrl}')` : null,
|
|
|
|
marginBottom: message ? '1.5em' : null,
|
|
|
|
}"
|
|
|
|
></div>
|
|
|
|
<MkInfo v-if="message">
|
|
|
|
{{ message }}
|
|
|
|
</MkInfo>
|
|
|
|
<div v-if="!totpLogin" class="normal-signin">
|
|
|
|
<MkInput
|
|
|
|
v-model="username"
|
|
|
|
class="_formBlock"
|
|
|
|
:placeholder="i18n.ts.username"
|
|
|
|
type="text"
|
|
|
|
pattern="^[a-zA-Z0-9_]+$"
|
|
|
|
:spellcheck="false"
|
|
|
|
autofocus
|
|
|
|
required
|
|
|
|
data-cy-signin-username
|
|
|
|
@update:modelValue="onUsernameChange"
|
|
|
|
>
|
|
|
|
<template #prefix>@</template>
|
|
|
|
<template #suffix>@{{ host }}</template>
|
2020-10-31 01:39:22 +01:00
|
|
|
</MkInput>
|
2023-04-08 02:01:42 +02:00
|
|
|
<MkInput
|
|
|
|
v-if="!user || (user && !user.usePasswordLessLogin)"
|
|
|
|
v-model="password"
|
|
|
|
class="_formBlock"
|
|
|
|
:placeholder="i18n.ts.password"
|
|
|
|
type="password"
|
|
|
|
:with-password-toggle="true"
|
2023-06-16 01:12:32 +02:00
|
|
|
autocomplete="current-password"
|
2023-04-08 02:01:42 +02:00
|
|
|
required
|
|
|
|
data-cy-signin-password
|
|
|
|
>
|
|
|
|
<template #prefix
|
|
|
|
><i class="ph-lock ph-bold ph-lg"></i
|
|
|
|
></template>
|
|
|
|
<template #caption
|
|
|
|
><button
|
|
|
|
class="_textButton"
|
|
|
|
type="button"
|
|
|
|
@click="resetPassword"
|
|
|
|
>
|
|
|
|
{{ i18n.ts.forgotPassword }}
|
|
|
|
</button></template
|
|
|
|
>
|
2020-10-31 01:39:22 +01:00
|
|
|
</MkInput>
|
2023-04-08 02:01:42 +02:00
|
|
|
<MkButton
|
|
|
|
class="_formBlock"
|
|
|
|
type="submit"
|
|
|
|
primary
|
|
|
|
:disabled="signing"
|
|
|
|
style="margin: 1rem auto"
|
|
|
|
>{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton
|
|
|
|
>
|
2020-10-31 01:39:22 +01:00
|
|
|
</div>
|
2023-04-08 02:01:42 +02:00
|
|
|
<div
|
|
|
|
v-if="totpLogin"
|
|
|
|
class="2fa-signin"
|
|
|
|
:class="{ securityKeys: user && user.securityKeys }"
|
|
|
|
>
|
|
|
|
<div
|
|
|
|
v-if="user && user.securityKeys"
|
|
|
|
class="twofa-group tap-group"
|
|
|
|
>
|
|
|
|
<p>{{ i18n.ts.tapSecurityKey }}</p>
|
|
|
|
<MkButton v-if="!queryingKey" @click="queryKey">
|
|
|
|
{{ i18n.ts.retry }}
|
|
|
|
</MkButton>
|
|
|
|
</div>
|
|
|
|
<div v-if="user && user.securityKeys" class="or-hr">
|
|
|
|
<p class="or-msg">{{ i18n.ts.or }}</p>
|
|
|
|
</div>
|
|
|
|
<div class="twofa-group totp-group">
|
|
|
|
<p style="margin-bottom: 0">
|
|
|
|
{{ i18n.ts.twoStepAuthentication }}
|
|
|
|
</p>
|
|
|
|
<MkInput
|
|
|
|
v-if="user && user.usePasswordLessLogin"
|
|
|
|
v-model="password"
|
|
|
|
type="password"
|
|
|
|
:with-password-toggle="true"
|
2023-06-16 01:12:32 +02:00
|
|
|
autocomplete="current-password"
|
2023-04-08 02:01:42 +02:00
|
|
|
required
|
|
|
|
>
|
|
|
|
<template #label>{{ i18n.ts.password }}</template>
|
|
|
|
<template #prefix
|
|
|
|
><i class="ph-lock ph-bold ph-lg"></i
|
|
|
|
></template>
|
|
|
|
</MkInput>
|
2023-06-16 04:32:27 +02:00
|
|
|
<vue3-otp-input
|
2023-06-16 04:52:51 +02:00
|
|
|
input-classes="_otp_input"
|
2023-06-16 23:11:33 +02:00
|
|
|
inputType="letter-numeric"
|
2023-06-16 04:52:51 +02:00
|
|
|
separator=""
|
2023-06-16 04:32:27 +02:00
|
|
|
:num-inputs="6"
|
2023-04-08 02:01:42 +02:00
|
|
|
v-model="token"
|
2023-06-16 04:32:27 +02:00
|
|
|
:should-auto-focus="true"
|
2023-06-16 05:06:47 +02:00
|
|
|
@on-change="updateToken"
|
2023-06-16 04:32:27 +02:00
|
|
|
@on-complete="onSubmit"
|
2023-04-08 02:01:42 +02:00
|
|
|
required
|
2023-06-16 04:32:27 +02:00
|
|
|
/>
|
2023-04-08 02:01:42 +02:00
|
|
|
<MkButton
|
|
|
|
type="submit"
|
|
|
|
:disabled="signing"
|
|
|
|
primary
|
2023-06-16 01:50:04 +02:00
|
|
|
style="margin: 1rem auto auto"
|
2023-04-08 02:01:42 +02:00
|
|
|
>{{
|
|
|
|
signing ? i18n.ts.loggingIn : i18n.ts.login
|
|
|
|
}}</MkButton
|
|
|
|
>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="social _section">
|
|
|
|
<a
|
|
|
|
v-if="meta && meta.enableTwitterIntegration"
|
|
|
|
class="_borderButton _gap"
|
|
|
|
:href="`${apiUrl}/signin/twitter`"
|
|
|
|
><i
|
|
|
|
class="ph-twitter-logo ph-bold ph-lg"
|
|
|
|
style="margin-right: 4px"
|
|
|
|
></i
|
|
|
|
>{{ i18n.t("signinWith", { x: "Twitter" }) }}</a
|
|
|
|
>
|
|
|
|
<a
|
|
|
|
v-if="meta && meta.enableGithubIntegration"
|
|
|
|
class="_borderButton _gap"
|
|
|
|
:href="`${apiUrl}/signin/github`"
|
|
|
|
><i
|
|
|
|
class="ph-github-logo ph-bold ph-lg"
|
|
|
|
style="margin-right: 4px"
|
|
|
|
></i
|
|
|
|
>{{ i18n.t("signinWith", { x: "GitHub" }) }}</a
|
|
|
|
>
|
|
|
|
<a
|
|
|
|
v-if="meta && meta.enableDiscordIntegration"
|
|
|
|
class="_borderButton _gap"
|
|
|
|
:href="`${apiUrl}/signin/discord`"
|
|
|
|
><i
|
|
|
|
class="ph-discord-logo ph-bold ph-lg"
|
|
|
|
style="margin-right: 4px"
|
|
|
|
></i
|
|
|
|
>{{ i18n.t("signinWith", { x: "Discord" }) }}</a
|
|
|
|
>
|
2019-07-03 13:18:07 +02:00
|
|
|
</div>
|
2023-04-08 02:01:42 +02:00
|
|
|
</form>
|
2018-02-10 08:22:14 +01:00
|
|
|
</template>
|
|
|
|
|
2022-05-19 13:28:08 +02:00
|
|
|
<script lang="ts" setup>
|
2023-06-16 04:32:27 +02:00
|
|
|
import Vue3OtpInput from "vue3-otp-input";
|
2023-06-16 05:06:47 +02:00
|
|
|
import { ref } from "vue";
|
2023-04-08 02:01:42 +02:00
|
|
|
import { defineAsyncComponent } from "vue";
|
|
|
|
import { toUnicode } from "punycode/";
|
|
|
|
import MkButton from "@/components/MkButton.vue";
|
|
|
|
import MkInput from "@/components/form/input.vue";
|
|
|
|
import MkInfo from "@/components/MkInfo.vue";
|
|
|
|
import { apiUrl, host as configHost } from "@/config";
|
|
|
|
import { byteify, hexify } from "@/scripts/2fa";
|
|
|
|
import * as os from "@/os";
|
|
|
|
import { login } from "@/account";
|
|
|
|
import { instance } from "@/instance";
|
|
|
|
import { i18n } from "@/i18n";
|
2018-02-10 08:22:14 +01:00
|
|
|
|
2022-05-19 13:28:08 +02:00
|
|
|
let signing = $ref(false);
|
|
|
|
let user = $ref(null);
|
2023-04-08 02:01:42 +02:00
|
|
|
let username = $ref("");
|
|
|
|
let password = $ref("");
|
|
|
|
let token = $ref("");
|
2022-05-19 13:28:08 +02:00
|
|
|
let host = $ref(toUnicode(configHost));
|
|
|
|
let totpLogin = $ref(false);
|
|
|
|
let credential = $ref(null);
|
|
|
|
let challengeData = $ref(null);
|
|
|
|
let queryingKey = $ref(false);
|
|
|
|
let hCaptchaResponse = $ref(null);
|
|
|
|
let reCaptchaResponse = $ref(null);
|
2018-12-18 16:40:29 +01:00
|
|
|
|
2023-06-16 05:06:47 +02:00
|
|
|
const updateToken = (value: string) => {
|
2023-06-16 07:16:21 +02:00
|
|
|
token = value.toString();
|
2023-06-16 05:06:47 +02:00
|
|
|
};
|
|
|
|
|
2022-05-19 13:28:08 +02:00
|
|
|
const meta = $computed(() => instance);
|
2020-10-17 13:12:00 +02:00
|
|
|
|
2022-05-19 13:28:08 +02:00
|
|
|
const emit = defineEmits<{
|
2023-04-08 02:01:42 +02:00
|
|
|
(ev: "login", v: any): void;
|
2022-05-19 13:28:08 +02:00
|
|
|
}>();
|
2018-12-18 16:40:29 +01:00
|
|
|
|
2022-05-19 13:28:08 +02:00
|
|
|
const props = defineProps({
|
|
|
|
withAvatar: {
|
|
|
|
type: Boolean,
|
|
|
|
required: false,
|
2022-11-16 10:29:18 +01:00
|
|
|
default: true,
|
2020-02-10 15:17:42 +01:00
|
|
|
},
|
2022-05-19 13:28:08 +02:00
|
|
|
autoSet: {
|
|
|
|
type: Boolean,
|
|
|
|
required: false,
|
|
|
|
default: false,
|
|
|
|
},
|
|
|
|
message: {
|
|
|
|
type: String,
|
|
|
|
required: false,
|
2023-04-08 02:01:42 +02:00
|
|
|
default: "",
|
2022-11-16 10:29:18 +01:00
|
|
|
},
|
2022-05-19 13:28:08 +02:00
|
|
|
});
|
2020-01-29 20:37:25 +01:00
|
|
|
|
2022-05-19 13:28:08 +02:00
|
|
|
function onUsernameChange() {
|
2023-04-08 02:01:42 +02:00
|
|
|
os.api("users/show", {
|
2022-11-16 10:29:18 +01:00
|
|
|
username: username,
|
2023-04-08 02:01:42 +02:00
|
|
|
}).then(
|
|
|
|
(userResponse) => {
|
|
|
|
user = userResponse;
|
|
|
|
},
|
|
|
|
() => {
|
|
|
|
user = null;
|
|
|
|
}
|
|
|
|
);
|
2022-05-19 13:28:08 +02:00
|
|
|
}
|
2019-07-03 13:18:07 +02:00
|
|
|
|
2022-05-19 13:28:08 +02:00
|
|
|
function onLogin(res) {
|
|
|
|
if (props.autoSet) {
|
|
|
|
return login(res.i);
|
|
|
|
}
|
|
|
|
}
|
2021-09-18 19:23:12 +02:00
|
|
|
|
2022-05-19 13:28:08 +02:00
|
|
|
function queryKey() {
|
|
|
|
queryingKey = true;
|
2023-04-08 02:01:42 +02:00
|
|
|
return navigator.credentials
|
|
|
|
.get({
|
|
|
|
publicKey: {
|
|
|
|
challenge: byteify(challengeData.challenge, "base64"),
|
|
|
|
allowCredentials: challengeData.securityKeys.map((key) => ({
|
|
|
|
id: byteify(key.id, "hex"),
|
|
|
|
type: "public-key",
|
|
|
|
transports: ["usb", "nfc", "ble", "internal"],
|
|
|
|
})),
|
|
|
|
timeout: 60 * 1000,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
.catch(() => {
|
|
|
|
queryingKey = false;
|
|
|
|
return Promise.reject(null);
|
|
|
|
})
|
|
|
|
.then((credential) => {
|
|
|
|
queryingKey = false;
|
|
|
|
signing = true;
|
|
|
|
return os.api("signin", {
|
|
|
|
username,
|
|
|
|
password,
|
|
|
|
signature: hexify(credential.response.signature),
|
|
|
|
authenticatorData: hexify(
|
|
|
|
credential.response.authenticatorData
|
|
|
|
),
|
|
|
|
clientDataJSON: hexify(credential.response.clientDataJSON),
|
|
|
|
credentialId: credential.id,
|
|
|
|
challengeId: challengeData.challengeId,
|
|
|
|
"hcaptcha-response": hCaptchaResponse,
|
|
|
|
"g-recaptcha-response": reCaptchaResponse,
|
|
|
|
});
|
|
|
|
})
|
|
|
|
.then((res) => {
|
|
|
|
emit("login", res);
|
|
|
|
return onLogin(res);
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
if (err === null) return;
|
|
|
|
os.alert({
|
|
|
|
type: "error",
|
|
|
|
text: i18n.ts.signinFailed,
|
|
|
|
});
|
|
|
|
signing = false;
|
2022-05-19 13:28:08 +02:00
|
|
|
});
|
|
|
|
}
|
2021-09-18 19:23:12 +02:00
|
|
|
|
2022-05-19 13:28:08 +02:00
|
|
|
function onSubmit() {
|
|
|
|
signing = true;
|
2023-04-08 02:01:42 +02:00
|
|
|
console.log("submit");
|
2022-05-19 13:28:08 +02:00
|
|
|
if (!totpLogin && user && user.twoFactorEnabled) {
|
|
|
|
if (window.PublicKeyCredential && user.securityKeys) {
|
2023-04-08 02:01:42 +02:00
|
|
|
os.api("signin", {
|
2022-05-19 13:28:08 +02:00
|
|
|
username,
|
|
|
|
password,
|
2023-04-08 02:01:42 +02:00
|
|
|
"hcaptcha-response": hCaptchaResponse,
|
|
|
|
"g-recaptcha-response": reCaptchaResponse,
|
|
|
|
})
|
|
|
|
.then((res) => {
|
|
|
|
totpLogin = true;
|
|
|
|
signing = false;
|
|
|
|
challengeData = res;
|
|
|
|
return queryKey();
|
|
|
|
})
|
|
|
|
.catch(loginFailed);
|
2022-05-19 13:28:08 +02:00
|
|
|
} else {
|
|
|
|
totpLogin = true;
|
|
|
|
signing = false;
|
|
|
|
}
|
|
|
|
} else {
|
2023-04-08 02:01:42 +02:00
|
|
|
os.api("signin", {
|
2022-05-19 13:28:08 +02:00
|
|
|
username,
|
|
|
|
password,
|
2023-04-08 02:01:42 +02:00
|
|
|
"hcaptcha-response": hCaptchaResponse,
|
|
|
|
"g-recaptcha-response": reCaptchaResponse,
|
2022-11-16 10:29:18 +01:00
|
|
|
token: user && user.twoFactorEnabled ? token : undefined,
|
2023-04-08 02:01:42 +02:00
|
|
|
})
|
|
|
|
.then((res) => {
|
|
|
|
emit("login", res);
|
|
|
|
onLogin(res);
|
|
|
|
})
|
|
|
|
.catch(loginFailed);
|
2022-05-19 13:28:08 +02:00
|
|
|
}
|
|
|
|
}
|
2021-05-04 08:05:34 +02:00
|
|
|
|
2022-05-19 13:28:08 +02:00
|
|
|
function loginFailed(err) {
|
|
|
|
switch (err.id) {
|
2023-04-08 02:01:42 +02:00
|
|
|
case "6cc579cc-885d-43d8-95c2-b8c7fc963280": {
|
2022-05-19 13:28:08 +02:00
|
|
|
os.alert({
|
2023-04-08 02:01:42 +02:00
|
|
|
type: "error",
|
2022-05-19 13:28:08 +02:00
|
|
|
title: i18n.ts.loginFailed,
|
2022-11-16 10:29:18 +01:00
|
|
|
text: i18n.ts.noSuchUser,
|
2022-05-19 13:28:08 +02:00
|
|
|
});
|
|
|
|
break;
|
|
|
|
}
|
2023-04-08 02:01:42 +02:00
|
|
|
case "932c904e-9460-45b7-9ce6-7ed33be7eb2c": {
|
2022-05-19 13:28:08 +02:00
|
|
|
os.alert({
|
2023-04-08 02:01:42 +02:00
|
|
|
type: "error",
|
2022-05-19 13:28:08 +02:00
|
|
|
title: i18n.ts.loginFailed,
|
|
|
|
text: i18n.ts.incorrectPassword,
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
}
|
2023-04-08 02:01:42 +02:00
|
|
|
case "e03a5f46-d309-4865-9b69-56282d94e1eb": {
|
2022-05-19 13:28:08 +02:00
|
|
|
showSuspendedDialog();
|
|
|
|
break;
|
|
|
|
}
|
2023-04-08 02:01:42 +02:00
|
|
|
case "22d05606-fbcf-421a-a2db-b32610dcfd1b": {
|
2022-05-28 05:06:47 +02:00
|
|
|
os.alert({
|
2023-04-08 02:01:42 +02:00
|
|
|
type: "error",
|
2022-05-28 05:06:47 +02:00
|
|
|
title: i18n.ts.loginFailed,
|
|
|
|
text: i18n.ts.rateLimitExceeded,
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
}
|
2022-05-19 13:28:08 +02:00
|
|
|
default: {
|
2022-06-10 07:36:55 +02:00
|
|
|
console.log(err);
|
2022-05-19 13:28:08 +02:00
|
|
|
os.alert({
|
2023-04-08 02:01:42 +02:00
|
|
|
type: "error",
|
2022-05-19 13:28:08 +02:00
|
|
|
title: i18n.ts.loginFailed,
|
2022-11-16 10:29:18 +01:00
|
|
|
text: JSON.stringify(err),
|
2022-05-19 13:28:08 +02:00
|
|
|
});
|
2018-02-10 08:22:14 +01:00
|
|
|
}
|
|
|
|
}
|
2022-05-19 13:28:08 +02:00
|
|
|
|
|
|
|
challengeData = null;
|
|
|
|
totpLogin = false;
|
|
|
|
signing = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
function resetPassword() {
|
2023-04-08 02:01:42 +02:00
|
|
|
os.popup(
|
|
|
|
defineAsyncComponent(() => import("@/components/MkForgotPassword.vue")),
|
|
|
|
{},
|
|
|
|
{},
|
|
|
|
"closed"
|
|
|
|
);
|
2022-05-19 13:28:08 +02:00
|
|
|
}
|
2023-06-14 02:15:00 +02:00
|
|
|
|
|
|
|
function showSuspendedDialog() {
|
|
|
|
os.alert({
|
|
|
|
type: "error",
|
|
|
|
title: i18n.ts.yourAccountSuspendedTitle,
|
|
|
|
text: i18n.ts.yourAccountSuspendedDescription,
|
|
|
|
});
|
|
|
|
}
|
2018-02-10 08:22:14 +01:00
|
|
|
</script>
|
|
|
|
|
2020-01-29 20:37:25 +01:00
|
|
|
<style lang="scss" scoped>
|
|
|
|
.eppvobhk {
|
2020-10-31 01:39:22 +01:00
|
|
|
> .auth {
|
|
|
|
> .avatar {
|
|
|
|
margin: 0 auto 0 auto;
|
|
|
|
width: 64px;
|
|
|
|
height: 64px;
|
2023-06-16 01:12:32 +02:00
|
|
|
background: var(--accentedBg);
|
2020-10-31 01:39:22 +01:00
|
|
|
background-position: center;
|
|
|
|
background-size: cover;
|
|
|
|
border-radius: 100%;
|
2023-06-16 01:12:32 +02:00
|
|
|
transition: background-image 0.2s ease-in;
|
2020-10-31 01:39:22 +01:00
|
|
|
}
|
2020-01-29 20:37:25 +01:00
|
|
|
}
|
|
|
|
}
|
2018-02-10 08:22:14 +01:00
|
|
|
</style>
|