Skip to content
Snippets Groups Projects
MkSignin.vue 6.98 KiB
Newer Older
syuilo's avatar
wip
syuilo committed
<template>
syuilo's avatar
syuilo committed
<form class="eppvobhk" :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
syuilo's avatar
syuilo committed
	<div class="auth _gaps_m">
		<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>
syuilo's avatar
syuilo committed
		<div v-if="!totpLogin" class="normal-signin _gaps_m">
			<MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username" autofocus required data-cy-signin-username @update:model-value="onUsernameChange">
syuilo's avatar
syuilo committed
				<template #prefix>@</template>
				<template #suffix>@{{ host }}</template>
			</MkInput>
syuilo's avatar
syuilo committed
			<MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" :placeholder="i18n.ts.password" type="password" :with-password-toggle="true" required data-cy-signin-password>
syuilo's avatar
syuilo committed
				<template #prefix><i class="ti ti-lock"></i></template>
				<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
syuilo's avatar
syuilo committed
			</MkInput>
			<MkButton type="submit" large primary rounded :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
syuilo's avatar
syuilo committed
		</div>
syuilo's avatar
syuilo committed
		<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
syuilo's avatar
syuilo committed
			<div v-if="user && user.securityKeys" class="twofa-group tap-group">
				<p>{{ i18n.ts.tapSecurityKey }}</p>
syuilo's avatar
syuilo committed
				<MkButton v-if="!queryingKey" @click="queryKey">
syuilo's avatar
syuilo committed
				</MkButton>
			</div>
syuilo's avatar
syuilo committed
			<div v-if="user && user.securityKeys" class="or-hr">
				<p class="or-msg">{{ i18n.ts.or }}</p>
syuilo's avatar
syuilo committed
			</div>
			<div class="twofa-group totp-group">
				<p style="margin-bottom:0;">{{ i18n.ts.twoStepAuthentication }}</p>
syuilo's avatar
syuilo committed
				<MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" :with-password-toggle="true" required>
					<template #label>{{ i18n.ts.password }}</template>
syuilo's avatar
syuilo committed
					<template #prefix><i class="ti ti-lock"></i></template>
syuilo's avatar
syuilo committed
				</MkInput>
				<MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" :spellcheck="false" required>
					<template #label>{{ i18n.ts.token }}</template>
syuilo's avatar
syuilo committed
					<template #prefix><i class="ti ti-123"></i></template>
syuilo's avatar
syuilo committed
				</MkInput>
				<MkButton type="submit" :disabled="signing" large primary rounded style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
syuilo's avatar
syuilo committed
			</div>
Mary's avatar
Mary committed
		</div>
	</div>
syuilo's avatar
wip
syuilo committed
</form>
</template>

<script lang="ts" setup>
import { defineAsyncComponent } from 'vue';
import { toUnicode } from 'punycode/';
syuilo's avatar
syuilo committed
import { showSuspendedDialog } from '../scripts/show-suspended-dialog';
import MkButton from '@/components/MkButton.vue';
syuilo's avatar
syuilo committed
import MkInput from '@/components/MkInput.vue';
import MkInfo from '@/components/MkInfo.vue';
import { apiUrl, host as configHost } from '@/config';
syuilo's avatar
syuilo committed
import { byteify, hexify } from '@/scripts/2fa';
import * as os from '@/os';
import { login } from '@/account';
import { instance } from '@/instance';
import { i18n } from '@/i18n';
syuilo's avatar
wip
syuilo committed

let signing = $ref(false);
let user = $ref(null);
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);
syuilo's avatar
syuilo committed

const meta = $computed(() => instance);
syuilo's avatar
syuilo committed

const emit = defineEmits<{
	(ev: 'login', v: any): void;
}>();
syuilo's avatar
syuilo committed

const props = defineProps({
	withAvatar: {
		type: Boolean,
		required: false,
syuilo's avatar
syuilo committed
		default: true,
	autoSet: {
		type: Boolean,
		required: false,
		default: false,
	},
	message: {
		type: String,
		required: false,
syuilo's avatar
syuilo committed
		default: '',
	},
syuilo's avatar
syuilo committed

function onUsernameChange() {
	os.api('users/show', {
syuilo's avatar
syuilo committed
		username: username,
	}).then(userResponse => {
		user = userResponse;
function onLogin(res) {
	if (props.autoSet) {
		return login(res.i);
	}
}
function queryKey() {
	queryingKey = true;
	return navigator.credentials.get({
		publicKey: {
			challenge: byteify(challengeData.challenge, 'base64'),
			allowCredentials: challengeData.securityKeys.map(key => ({
				id: byteify(key.id, 'hex'),
				type: 'public-key',
syuilo's avatar
syuilo committed
				transports: ['usb', 'nfc', 'ble', 'internal'],
syuilo's avatar
syuilo committed
			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,
syuilo's avatar
syuilo committed
			'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',
syuilo's avatar
syuilo committed
			text: i18n.ts.signinFailed,
function onSubmit() {
	signing = true;
	if (!totpLogin && user && user.twoFactorEnabled) {
		if (window.PublicKeyCredential && user.securityKeys) {
			os.api('signin', {
				username,
				password,
syuilo's avatar
syuilo committed
				'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 {
		os.api('signin', {
			username,
			password,
syuilo's avatar
syuilo committed
			'hcaptcha-response': hCaptchaResponse,
			'g-recaptcha-response': reCaptchaResponse,
syuilo's avatar
syuilo committed
			token: user && user.twoFactorEnabled ? token : undefined,
		}).then(res => {
			emit('login', res);
			onLogin(res);
		}).catch(loginFailed);
	}
}
syuilo's avatar
syuilo committed

function loginFailed(err) {
	switch (err.id) {
		case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
			os.alert({
				type: 'error',
				title: i18n.ts.loginFailed,
syuilo's avatar
syuilo committed
				text: i18n.ts.noSuchUser,
			});
			break;
		}
		case '932c904e-9460-45b7-9ce6-7ed33be7eb2c': {
			os.alert({
				type: 'error',
				title: i18n.ts.loginFailed,
				text: i18n.ts.incorrectPassword,
			});
			break;
		}
		case 'e03a5f46-d309-4865-9b69-56282d94e1eb': {
			showSuspendedDialog();
			break;
		}
		case '22d05606-fbcf-421a-a2db-b32610dcfd1b': {
			os.alert({
				type: 'error',
				title: i18n.ts.loginFailed,
				text: i18n.ts.rateLimitExceeded,
			});
			break;
		}
			console.log(err);
			os.alert({
				type: 'error',
				title: i18n.ts.loginFailed,
syuilo's avatar
syuilo committed
				text: JSON.stringify(err),
syuilo's avatar
wip
syuilo committed
		}
	}

	challengeData = null;
	totpLogin = false;
	signing = false;
}

function resetPassword() {
	os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, {
syuilo's avatar
wip
syuilo committed
</script>

syuilo's avatar
syuilo committed
.eppvobhk {
syuilo's avatar
syuilo committed
	> .auth {
		> .avatar {
			margin: 0 auto 0 auto;
			width: 64px;
			height: 64px;
			background: #ddd;
			background-position: center;
			background-size: cover;
			border-radius: 100%;
		}
syuilo's avatar
syuilo committed
	}
}
syuilo's avatar
wip
syuilo committed
</style>