summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-09-08 14:05:03 +0900
committerGitHub <noreply@github.com>2023-09-08 14:05:03 +0900
commitff9a65e8faa46a101d3ed3dc8915dd1f269ef556 (patch)
treea6b1ae734e61da58b4205cd08a505ce392b317a9 /packages/frontend/src/components
parentUpdate CHANGELOG.md (diff)
downloadsharkey-ff9a65e8faa46a101d3ed3dc8915dd1f269ef556.tar.gz
sharkey-ff9a65e8faa46a101d3ed3dc8915dd1f269ef556.tar.bz2
sharkey-ff9a65e8faa46a101d3ed3dc8915dd1f269ef556.zip
feat: passkey support (#11804)
https://github.com/MisskeyIO/misskey/pull/149
Diffstat (limited to 'packages/frontend/src/components')
-rw-r--r--packages/frontend/src/components/MkSignin.vue113
1 files changed, 50 insertions, 63 deletions
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index 19f418b48d..247fcb4b29 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -6,16 +6,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<form :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
<div class="_gaps_m">
- <div v-show="withAvatar" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null, marginBottom: message ? '1.5em' : null }"></div>
+ <div v-show="withAvatar" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : undefined, marginBottom: message ? '1.5em' : undefined }"></div>
<MkInfo v-if="message">
{{ message }}
</MkInfo>
<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:modelValue="onUsernameChange">
+ <MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
</MkInput>
- <MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password" :withPasswordToggle="true" required data-cy-signin-password>
+ <MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true" required data-cy-signin-password>
<template #prefix><i class="ti ti-lock"></i></template>
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
</MkInput>
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<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>
+ <p>{{ i18n.ts.useSecurityKey }}</p>
<MkButton v-if="!queryingKey" @click="queryKey">
{{ i18n.ts.retry }}
</MkButton>
@@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<p class="or-msg">{{ i18n.ts.or }}</p>
</div>
<div class="twofa-group totp-group">
- <p style="margin-bottom:0;">{{ i18n.ts.twoStepAuthentication }}</p>
+ <p style="margin-bottom:0;">{{ i18n.ts['2fa'] }}</p>
<MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" autocomplete="current-password" :withPasswordToggle="true" required>
<template #label>{{ i18n.ts.password }}</template>
<template #prefix><i class="ti ti-lock"></i></template>
@@ -51,32 +51,29 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { defineAsyncComponent } from 'vue';
import { toUnicode } from 'punycode/';
-import { showSuspendedDialog } from '../scripts/show-suspended-dialog';
+import { UserDetailed } from 'misskey-js/built/entities';
+import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
+import { showSuspendedDialog } from '@/scripts/show-suspended-dialog';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkInfo from '@/components/MkInfo.vue';
import { 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';
let signing = $ref(false);
-let user = $ref(null);
+let user = $ref<UserDetailed | null>(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 credentialRequest = $ref<CredentialRequestOptions | null>(null);
let hCaptchaResponse = $ref(null);
let reCaptchaResponse = $ref(null);
-const meta = $computed(() => instance);
-
const emit = defineEmits<{
(ev: 'login', v: any): void;
}>();
@@ -99,7 +96,7 @@ const props = defineProps({
},
});
-function onUsernameChange() {
+function onUsernameChange(): void {
os.api('users/show', {
username: username,
}).then(userResponse => {
@@ -109,58 +106,46 @@ function onUsernameChange() {
});
}
-function onLogin(res) {
+function onLogin(res: any): Promise<void> | void {
if (props.autoSet) {
return login(res.i);
}
}
-function queryKey() {
+async function queryKey(): Promise<void> {
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',
- 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,
+ await webAuthnRequest(credentialRequest)
+ .catch(() => {
+ queryingKey = false;
+ return Promise.reject(null);
+ }).then(credential => {
+ credentialRequest = null;
+ queryingKey = false;
+ signing = true;
+ return os.api('signin', {
+ username,
+ password,
+ credential: credential.toJSON(),
+ '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;
});
- signing = false;
- });
}
-function onSubmit() {
+function onSubmit(): void {
signing = true;
if (!totpLogin && user && user.twoFactorEnabled) {
- if (window.PublicKeyCredential && user.securityKeys) {
+ if (webAuthnSupported() && user.securityKeys) {
os.api('signin', {
username,
password,
@@ -169,9 +154,12 @@ function onSubmit() {
}).then(res => {
totpLogin = true;
signing = false;
- challengeData = res;
- return queryKey();
- }).catch(loginFailed);
+ credentialRequest = parseRequestOptionsFromJSON({
+ publicKey: res,
+ });
+ })
+ .then(() => queryKey())
+ .catch(loginFailed);
} else {
totpLogin = true;
signing = false;
@@ -182,7 +170,7 @@ function onSubmit() {
password,
'hcaptcha-response': hCaptchaResponse,
'g-recaptcha-response': reCaptchaResponse,
- token: user && user.twoFactorEnabled ? token : undefined,
+ token: user?.twoFactorEnabled ? token : undefined,
}).then(res => {
emit('login', res);
onLogin(res);
@@ -190,7 +178,7 @@ function onSubmit() {
}
}
-function loginFailed(err) {
+function loginFailed(err: any): void {
switch (err.id) {
case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
os.alert({
@@ -221,7 +209,7 @@ function loginFailed(err) {
break;
}
default: {
- console.log(err);
+ console.error(err);
os.alert({
type: 'error',
title: i18n.ts.loginFailed,
@@ -230,12 +218,11 @@ function loginFailed(err) {
}
}
- challengeData = null;
totpLogin = false;
signing = false;
}
-function resetPassword() {
+function resetPassword(): void {
os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, {
}, 'closed');
}