rudeshark.net/packages/client/src/components/MkSignin.vue

400 lines
8.8 KiB
Vue
Raw Normal View History

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"
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"
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"
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
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
>
</div>
2023-04-08 02:01:42 +02:00
</form>
2018-02-10 08:22:14 +01:00
</template>
<script lang="ts" setup>
2023-06-16 04:32:27 +02:00
import Vue3OtpInput from "vue3-otp-input";
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
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("");
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) => {
token = value.toString();
2023-06-16 05:06:47 +02:00
};
const meta = $computed(() => instance);
Migrate to Vue3 (#6587) * Update reaction.vue * fix bug * wip * wip * wjio * wip * Revert "wip" This reverts commit e427f2160adf4e8a4147006e25a89854edab0033. * wip * wip * wip * Update init.ts * Update drive-window.vue * wip * wip * Use PascalCase for components * Use PascalCase for components * update dep * wip * wip * wip * Update init.ts * wip * Update paging.ts * Update test.vue * watch deep * wip * lint * wip * wip * wip * wip * wiop * wip * Update webpack.config.ts * alllow null poll * wip * wip * wip * wiop * UI redesign & refactor (#6714) * wip * wip * wip * wip * wip * Update drive.vue * Update word-mute.vue * wip * wip * wip * clean up * wip * Update default.vue * wip * Update notes.vue * Update mfm.ts * Update index.home.vue * Update post-form.vue * Update post-form-attaches.vue * wip * Update post-form.vue * Update sidebar.vue * wip * wip * Update index.vue * wip * Update default.vue * Update index.vue * Update index.vue * wip * Update post-form-attaches.vue * Update note.vue * wip * clean up * Update notes.vue * wip * wip * Update ja-JP.yml * wip * wip * Update index.vue * wip * wip * wip * wip * wip * wip * wip * wip * Update default.vue * wip * Update _dark.json5 * wip * wip * wip * clean up * wip * wip * Update index.vue * Update test.vue * wip * wip * fix * wip * wip * wip * wip * clena yop * wip * wip * Update store.ts * Update messaging-room.vue * Update default.widgets.vue * fix * wip * wip * Update modal.vue * wip * Update os.ts * Update os.ts * Update deck.vue * Update init.ts * wip * Update ja-JP.yml * v-sizeは単にwindowのresizeを監視するだけで良いかもしれない * Update modal.vue * wip * Update tooltip.ts * wip * wip * wip * wip * wip * Update image-viewer.vue * wip * wip * Update style.scss * Update style.scss * Update visitor.vue * wip * Update init.ts * Update init.ts * wip * wip * Update visitor.vue * Update visitor.vue * Update visitor.vue * Update visitor.vue * wip * wip * Update modal.vue * Update header.vue * Update menu.vue * Update about.vue * Update about-misskey.vue * wip * wip * Update visitor.vue * Update tooltip.ts * wip * Update drive.vue * wip * Update style.scss * Update header.vue * wip * wip * Update users.user.vue * Update announcements.vue * wip * wip * wip * Update emojis.vue * wip * Update emojis.vue * Update style.scss * Update users.vue * wip * Update style.scss * wip * Update welcome.entrance.vue * Update radio.vue * Update size.ts * Update emoji-edit-dialog.vue * wip * Update emojis.vue * wip * Update emojis.vue * Update emojis.vue * Update emojis.vue * wip * wip * wip * wip * Update file-dialog.vue * wip * wip * Update token-generate-window.vue * Update notification-setting-window.vue * wip * wip * Update _error_.vue * Update ja-JP.yml * wip * wip * Update store.ts * Update emojis.vue * Update emojis.vue * Update emojis.vue * Update announcements.vue * Update store.ts * wip * Update page-editor.vue * wip * wip * Update modal.vue * wip * Update select-file.ts * Update timeline.vue * Update emojis.vue * Update os.ts * wip * Update user-select.vue * Update mfm.ts * Update get-file-info.ts * Update drive.vue * Update init.ts * Update mfm.ts * wip * wip * Update window.vue * Update note.vue * wip * wip * Update user-info.vue * wip * wip * wip * wip * wip * Update header.vue * Update header.vue * wip * Update explore.vue * wip * wip * wip * Update webpack.config.ts * wip * wip * wip * wip * wip * wip * Update autocomplete.ts * wip * wip * wip * Update toast.vue * wip * Update post-form-dialog.vue * wip * wip * wip * wip * wip * Update users.vue * wip * Update explore.vue * wip * wip * wip * Update package.json * wip * Update icon-dialog.vue * wip * wip * Update user-preview.ts * wip * wip * wip * wip * wip * Update instance.vue * Update user-name.vue * Update federation.vue * Update instance.vue * wip * wip * Update tag.vue * wip * wip * wip * wip * wip * Update instance.vue * wip * Update os.ts * Update os.ts * wip * wip * wip * Update router.ts * wip * Update init.ts * Update note.vue * Update messages.vue * wip * wip * wip * wip * wip * google * wip * wip * wip * wip * Update theme-editor.vue * wip * wip * Update room.vue * Update channel-editor.vue * wip * Update window.vue * Update window.vue * wip * Update window.vue * Update window.vue * wip * Update menu.vue * wip * wip * wip * wip * Update messaging-room.vue * wip * Update post-form.vue * Update default.widgets.vue * Update window.vue * wip
2020-10-17 13:12:00 +02:00
const emit = defineEmits<{
2023-04-08 02:01:42 +02:00
(ev: "login", v: any): void;
}>();
2018-12-18 16:40:29 +01:00
const props = defineProps({
withAvatar: {
type: Boolean,
required: false,
2022-11-16 10:29:18 +01:00
default: true,
},
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
},
});
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;
}
);
}
function onLogin(res) {
if (props.autoSet) {
return login(res.i);
}
}
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;
});
}
function onSubmit() {
signing = true;
2023-04-08 02:01:42 +02:00
console.log("submit");
if (!totpLogin && user && user.twoFactorEnabled) {
if (window.PublicKeyCredential && user.securityKeys) {
2023-04-08 02:01:42 +02:00
os.api("signin", {
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);
} else {
totpLogin = true;
signing = false;
}
} else {
2023-04-08 02:01:42 +02:00
os.api("signin", {
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);
}
}
function loginFailed(err) {
switch (err.id) {
2023-04-08 02:01:42 +02:00
case "6cc579cc-885d-43d8-95c2-b8c7fc963280": {
os.alert({
2023-04-08 02:01:42 +02:00
type: "error",
title: i18n.ts.loginFailed,
2022-11-16 10:29:18 +01:00
text: i18n.ts.noSuchUser,
});
break;
}
2023-04-08 02:01:42 +02:00
case "932c904e-9460-45b7-9ce6-7ed33be7eb2c": {
os.alert({
2023-04-08 02:01:42 +02:00
type: "error",
title: i18n.ts.loginFailed,
text: i18n.ts.incorrectPassword,
});
break;
}
2023-04-08 02:01:42 +02:00
case "e03a5f46-d309-4865-9b69-56282d94e1eb": {
showSuspendedDialog();
break;
}
2023-04-08 02:01:42 +02:00
case "22d05606-fbcf-421a-a2db-b32610dcfd1b": {
os.alert({
2023-04-08 02:01:42 +02:00
type: "error",
title: i18n.ts.loginFailed,
text: i18n.ts.rateLimitExceeded,
});
break;
}
default: {
console.log(err);
os.alert({
2023-04-08 02:01:42 +02:00
type: "error",
title: i18n.ts.loginFailed,
2022-11-16 10:29:18 +01:00
text: JSON.stringify(err),
});
2018-02-10 08:22:14 +01: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"
);
}
function showSuspendedDialog() {
os.alert({
type: "error",
title: i18n.ts.yourAccountSuspendedTitle,
text: i18n.ts.yourAccountSuspendedDescription,
});
}
2018-02-10 08:22:14 +01:00
</script>
<style lang="scss" scoped>
.eppvobhk {
2020-10-31 01:39:22 +01:00
> .auth {
> .avatar {
margin: 0 auto 0 auto;
width: 64px;
height: 64px;
background: var(--accentedBg);
2020-10-31 01:39:22 +01:00
background-position: center;
background-size: cover;
border-radius: 100%;
transition: background-image 0.2s ease-in;
2020-10-31 01:39:22 +01:00
}
}
}
2018-02-10 08:22:14 +01:00
</style>