次の処理をsignin apiから読み取るように
This commit is contained in:
parent
65aac75a33
commit
adabff708d
|
@ -545,11 +545,6 @@ export class UserEntityService implements OnModuleInit {
|
||||||
publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964
|
publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964
|
||||||
followersVisibility: profile!.followersVisibility,
|
followersVisibility: profile!.followersVisibility,
|
||||||
followingVisibility: profile!.followingVisibility,
|
followingVisibility: profile!.followingVisibility,
|
||||||
twoFactorEnabled: profile!.twoFactorEnabled,
|
|
||||||
usePasswordLessLogin: profile!.usePasswordLessLogin,
|
|
||||||
securityKeys: profile!.twoFactorEnabled
|
|
||||||
? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
|
|
||||||
: false,
|
|
||||||
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
|
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
|
||||||
id: role.id,
|
id: role.id,
|
||||||
name: role.name,
|
name: role.name,
|
||||||
|
@ -564,6 +559,14 @@ export class UserEntityService implements OnModuleInit {
|
||||||
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
|
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
|
||||||
} : {}),
|
} : {}),
|
||||||
|
|
||||||
|
...(isDetailed && (isMe || iAmModerator) ? {
|
||||||
|
twoFactorEnabled: profile!.twoFactorEnabled,
|
||||||
|
usePasswordLessLogin: profile!.usePasswordLessLogin,
|
||||||
|
securityKeys: profile!.twoFactorEnabled
|
||||||
|
? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
|
||||||
|
: false,
|
||||||
|
} : {}),
|
||||||
|
|
||||||
...(isDetailed && isMe ? {
|
...(isDetailed && isMe ? {
|
||||||
avatarId: user.avatarId,
|
avatarId: user.avatarId,
|
||||||
bannerId: user.bannerId,
|
bannerId: user.bannerId,
|
||||||
|
|
|
@ -346,21 +346,6 @@ export const packedUserDetailedNotMeOnlySchema = {
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
enum: ['public', 'followers', 'private'],
|
enum: ['public', 'followers', 'private'],
|
||||||
},
|
},
|
||||||
twoFactorEnabled: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
usePasswordLessLogin: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
securityKeys: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
roles: {
|
roles: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
|
@ -382,6 +367,18 @@ export const packedUserDetailedNotMeOnlySchema = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: false, optional: true,
|
nullable: false, optional: true,
|
||||||
},
|
},
|
||||||
|
twoFactorEnabled: {
|
||||||
|
type: 'boolean',
|
||||||
|
nullable: false, optional: true,
|
||||||
|
},
|
||||||
|
usePasswordLessLogin: {
|
||||||
|
type: 'boolean',
|
||||||
|
nullable: false, optional: true,
|
||||||
|
},
|
||||||
|
securityKeys: {
|
||||||
|
type: 'boolean',
|
||||||
|
nullable: false, optional: true,
|
||||||
|
},
|
||||||
//#region relations
|
//#region relations
|
||||||
isFollowing: {
|
isFollowing: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
|
|
@ -12,6 +12,7 @@ import type {
|
||||||
MiMeta,
|
MiMeta,
|
||||||
SigninsRepository,
|
SigninsRepository,
|
||||||
UserProfilesRepository,
|
UserProfilesRepository,
|
||||||
|
UserSecurityKeysRepository,
|
||||||
UsersRepository,
|
UsersRepository,
|
||||||
} from '@/models/_.js';
|
} from '@/models/_.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
|
@ -25,9 +26,18 @@ import { CaptchaService } from '@/core/CaptchaService.js';
|
||||||
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
|
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
|
||||||
import { RateLimiterService } from './RateLimiterService.js';
|
import { RateLimiterService } from './RateLimiterService.js';
|
||||||
import { SigninService } from './SigninService.js';
|
import { SigninService } from './SigninService.js';
|
||||||
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
|
import type { AuthenticationResponseJSON, PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types';
|
||||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||||
|
|
||||||
|
type SigninErrorResponse = {
|
||||||
|
id: string;
|
||||||
|
next?: 'captcha' | 'password' | 'totp';
|
||||||
|
} | {
|
||||||
|
id: string;
|
||||||
|
next: 'passkey';
|
||||||
|
authRequest: PublicKeyCredentialRequestOptionsJSON;
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SigninApiService {
|
export class SigninApiService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -43,6 +53,9 @@ export class SigninApiService {
|
||||||
@Inject(DI.userProfilesRepository)
|
@Inject(DI.userProfilesRepository)
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
|
||||||
|
@Inject(DI.userSecurityKeysRepository)
|
||||||
|
private userSecurityKeysRepository: UserSecurityKeysRepository,
|
||||||
|
|
||||||
@Inject(DI.signinsRepository)
|
@Inject(DI.signinsRepository)
|
||||||
private signinsRepository: SigninsRepository,
|
private signinsRepository: SigninsRepository,
|
||||||
|
|
||||||
|
@ -60,7 +73,7 @@ export class SigninApiService {
|
||||||
request: FastifyRequest<{
|
request: FastifyRequest<{
|
||||||
Body: {
|
Body: {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password?: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
credential?: AuthenticationResponseJSON;
|
credential?: AuthenticationResponseJSON;
|
||||||
'hcaptcha-response'?: string;
|
'hcaptcha-response'?: string;
|
||||||
|
@ -79,7 +92,7 @@ export class SigninApiService {
|
||||||
const password = body['password'];
|
const password = body['password'];
|
||||||
const token = body['token'];
|
const token = body['token'];
|
||||||
|
|
||||||
function error(status: number, error: { id: string }) {
|
function error(status: number, error: SigninErrorResponse) {
|
||||||
reply.code(status);
|
reply.code(status);
|
||||||
return { error };
|
return { error };
|
||||||
}
|
}
|
||||||
|
@ -103,11 +116,6 @@ export class SigninApiService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof password !== 'string') {
|
|
||||||
reply.code(400);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (token != null && typeof token !== 'string') {
|
if (token != null && typeof token !== 'string') {
|
||||||
reply.code(400);
|
reply.code(400);
|
||||||
return;
|
return;
|
||||||
|
@ -132,11 +140,36 @@ export class SigninApiService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||||
|
const securityKeysAvailable = await this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1);
|
||||||
|
|
||||||
|
if (password == null) {
|
||||||
|
reply.code(403);
|
||||||
|
if (profile.twoFactorEnabled) {
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
|
||||||
|
next: 'password',
|
||||||
|
},
|
||||||
|
} satisfies { error: SigninErrorResponse };
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
|
||||||
|
next: 'captcha',
|
||||||
|
},
|
||||||
|
} satisfies { error: SigninErrorResponse };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof password !== 'string') {
|
||||||
|
reply.code(400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Compare password
|
// Compare password
|
||||||
const same = await bcrypt.compare(password, profile.password!);
|
const same = await bcrypt.compare(password, profile.password!);
|
||||||
|
|
||||||
const fail = async (status?: number, failure?: { id: string }) => {
|
const fail = async (status?: number, failure?: SigninErrorResponse) => {
|
||||||
// Append signin history
|
// Append signin history
|
||||||
await this.signinsRepository.insert({
|
await this.signinsRepository.insert({
|
||||||
id: this.idService.gen(),
|
id: this.idService.gen(),
|
||||||
|
@ -217,7 +250,7 @@ export class SigninApiService {
|
||||||
id: '93b86c4b-72f9-40eb-9815-798928603d1e',
|
id: '93b86c4b-72f9-40eb-9815-798928603d1e',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else if (securityKeysAvailable) {
|
||||||
if (!same && !profile.usePasswordLessLogin) {
|
if (!same && !profile.usePasswordLessLogin) {
|
||||||
return await fail(403, {
|
return await fail(403, {
|
||||||
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
|
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
|
||||||
|
@ -226,8 +259,28 @@ export class SigninApiService {
|
||||||
|
|
||||||
const authRequest = await this.webAuthnService.initiateAuthentication(user.id);
|
const authRequest = await this.webAuthnService.initiateAuthentication(user.id);
|
||||||
|
|
||||||
reply.code(200);
|
reply.code(403);
|
||||||
return authRequest;
|
return {
|
||||||
|
error: {
|
||||||
|
id: '06e661b9-8146-4ae3-bde5-47138c0ae0c4',
|
||||||
|
next: 'passkey',
|
||||||
|
authRequest,
|
||||||
|
},
|
||||||
|
} satisfies { error: SigninErrorResponse };
|
||||||
|
} else {
|
||||||
|
if (!same || !profile.twoFactorEnabled) {
|
||||||
|
return await fail(403, {
|
||||||
|
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
reply.code(403);
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
|
||||||
|
next: 'totp',
|
||||||
|
},
|
||||||
|
} satisfies { error: SigninErrorResponse };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// never get here
|
// never get here
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
|
||||||
import { get as webAuthnRequest } from '@github/webauthn-json/browser-ponyfill';
|
import { get as webAuthnRequest } from '@github/webauthn-json/browser-ponyfill';
|
||||||
|
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
@ -32,7 +31,6 @@ import MkButton from '@/components/MkButton.vue';
|
||||||
import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill';
|
import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
user: Misskey.entities.UserDetailed;
|
|
||||||
credentialRequest: CredentialRequestOptions;
|
credentialRequest: CredentialRequestOptions;
|
||||||
isPerformingPasswordlessLogin?: boolean;
|
isPerformingPasswordlessLogin?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
|
@ -23,14 +23,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
|
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<div v-if="!user.twoFactorEnabled">
|
<div v-if="needCaptcha">
|
||||||
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
|
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
|
||||||
<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
|
<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
|
||||||
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
|
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
|
||||||
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
|
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MkButton type="submit" :disabled="!user.twoFactorEnabled && captchaFailed" large primary rounded style="margin: 0 auto;" data-cy-signin-page-password-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
<MkButton type="submit" :disabled="needCaptcha && captchaFailed" large primary rounded style="margin: 0 auto;" data-cy-signin-page-password-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -62,6 +62,7 @@ import MkCaptcha from '@/components/MkCaptcha.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
user: Misskey.entities.UserDetailed;
|
user: Misskey.entities.UserDetailed;
|
||||||
|
needCaptcha: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -82,10 +83,11 @@ const turnstileResponse = ref<string | null>(null);
|
||||||
|
|
||||||
const captchaFailed = computed((): boolean => {
|
const captchaFailed = computed((): boolean => {
|
||||||
return (
|
return (
|
||||||
instance.enableHcaptcha && !hCaptchaResponse.value ||
|
(instance.enableHcaptcha && !hCaptchaResponse.value) ||
|
||||||
instance.enableMcaptcha && !mCaptchaResponse.value ||
|
(instance.enableMcaptcha && !mCaptchaResponse.value) ||
|
||||||
instance.enableRecaptcha && !reCaptchaResponse.value ||
|
(instance.enableRecaptcha && !reCaptchaResponse.value) ||
|
||||||
instance.enableTurnstile && !turnstileResponse.value);
|
(instance.enableTurnstile && !turnstileResponse.value)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
function resetPassword(): void {
|
function resetPassword(): void {
|
||||||
|
|
|
@ -32,6 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
ref="passwordPageEl"
|
ref="passwordPageEl"
|
||||||
|
|
||||||
:user="userInfo!"
|
:user="userInfo!"
|
||||||
|
:needCaptcha="needCaptcha"
|
||||||
|
|
||||||
@passwordSubmitted="onPasswordSubmitted"
|
@passwordSubmitted="onPasswordSubmitted"
|
||||||
/>
|
/>
|
||||||
|
@ -49,7 +50,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
v-else-if="page === 'passkey'"
|
v-else-if="page === 'passkey'"
|
||||||
key="passkey"
|
key="passkey"
|
||||||
|
|
||||||
:user="userInfo!"
|
|
||||||
:credentialRequest="credentialRequest!"
|
:credentialRequest="credentialRequest!"
|
||||||
:isPerformingPasswordlessLogin="doingPasskeyFromInputPage"
|
:isPerformingPasswordlessLogin="doingPasskeyFromInputPage"
|
||||||
|
|
||||||
|
@ -64,14 +64,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, shallowRef, useTemplateRef } from 'vue';
|
import { nextTick, onBeforeUnmount, ref, shallowRef, useTemplateRef } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { supported as webAuthnSupported, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
|
import { supported as webAuthnSupported, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
|
||||||
|
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
|
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
|
||||||
import { login } from '@/account.js';
|
import { login } from '@/account.js';
|
||||||
import { instance } from '@/instance.js';
|
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
|
||||||
|
@ -98,9 +97,11 @@ const props = withDefaults(defineProps<{
|
||||||
});
|
});
|
||||||
|
|
||||||
const page = ref<'input' | 'password' | 'totp' | 'passkey'>('input');
|
const page = ref<'input' | 'password' | 'totp' | 'passkey'>('input');
|
||||||
const passwordPageEl = useTemplateRef('passwordPageEl');
|
|
||||||
const waiting = ref(false);
|
const waiting = ref(false);
|
||||||
|
|
||||||
|
const passwordPageEl = useTemplateRef('passwordPageEl');
|
||||||
|
const needCaptcha = ref(false);
|
||||||
|
|
||||||
const userInfo = ref<null | Misskey.entities.UserDetailed>(null);
|
const userInfo = ref<null | Misskey.entities.UserDetailed>(null);
|
||||||
const password = ref('');
|
const password = ref('');
|
||||||
|
|
||||||
|
@ -142,14 +143,11 @@ function onPasskeyDone(credential: AuthenticationPublicKeyCredential): void {
|
||||||
emit('login', res.signinResponse);
|
emit('login', res.signinResponse);
|
||||||
}).catch(onLoginFailed);
|
}).catch(onLoginFailed);
|
||||||
} else if (userInfo.value != null) {
|
} else if (userInfo.value != null) {
|
||||||
misskeyApi('signin', {
|
tryLogin({
|
||||||
username: userInfo.value.username,
|
username: userInfo.value.username,
|
||||||
password: password.value,
|
password: password.value,
|
||||||
credential: credential.toJSON(),
|
credential: credential.toJSON(),
|
||||||
}).then(async (res) => {
|
});
|
||||||
emit('login', res);
|
|
||||||
await onLoginSucceeded(res);
|
|
||||||
}).catch(onLoginFailed);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,17 +169,18 @@ async function onUsernameSubmitted(username: string) {
|
||||||
title: i18n.ts.noSuchUser,
|
title: i18n.ts.noSuchUser,
|
||||||
text: i18n.ts.signinFailed,
|
text: i18n.ts.signinFailed,
|
||||||
});
|
});
|
||||||
} else if (userInfo.value.usePasswordLessLogin) {
|
waiting.value = false;
|
||||||
page.value = 'passkey';
|
return;
|
||||||
} else {
|
|
||||||
page.value = 'password';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
waiting.value = false;
|
await tryLogin({
|
||||||
|
username,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onPasswordSubmitted(pw: PwResponse) {
|
async function onPasswordSubmitted(pw: PwResponse) {
|
||||||
waiting.value = true;
|
waiting.value = true;
|
||||||
|
password.value = pw.password;
|
||||||
|
|
||||||
if (userInfo.value == null) {
|
if (userInfo.value == null) {
|
||||||
await os.alert({
|
await os.alert({
|
||||||
|
@ -189,48 +188,17 @@ async function onPasswordSubmitted(pw: PwResponse) {
|
||||||
title: i18n.ts.noSuchUser,
|
title: i18n.ts.noSuchUser,
|
||||||
text: i18n.ts.signinFailed,
|
text: i18n.ts.signinFailed,
|
||||||
});
|
});
|
||||||
|
waiting.value = false;
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
if (!userInfo.value.twoFactorEnabled) {
|
await tryLogin({
|
||||||
if (
|
username: userInfo.value.username,
|
||||||
(instance.enableHcaptcha || instance.enableMcaptcha || instance.enableRecaptcha || instance.enableTurnstile) &&
|
password: pw.password,
|
||||||
(pw.captcha.hCaptchaResponse == null && pw.captcha.mCaptchaResponse == null && pw.captcha.reCaptchaResponse == null && pw.captcha.turnstileResponse == null)
|
'hcaptcha-response': pw.captcha.hCaptchaResponse,
|
||||||
) {
|
'm-captcha-response': pw.captcha.mCaptchaResponse,
|
||||||
// 2FAが無効で、CAPTCHAが有効で、かつCAPTCHAが未入力の場合
|
'g-recaptcha-response': pw.captcha.reCaptchaResponse,
|
||||||
onLoginFailed();
|
'turnstile-response': pw.captcha.turnstileResponse,
|
||||||
waiting.value = false;
|
});
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
await misskeyApi('signin', {
|
|
||||||
username: userInfo.value.username,
|
|
||||||
password: pw.password,
|
|
||||||
'h-captcha-response': pw.captcha.hCaptchaResponse,
|
|
||||||
'm-captcha-response': pw.captcha.mCaptchaResponse,
|
|
||||||
'g-recaptcha-response': pw.captcha.reCaptchaResponse,
|
|
||||||
'turnstile-response': pw.captcha.turnstileResponse,
|
|
||||||
}).then(async (res) => {
|
|
||||||
emit('login', res);
|
|
||||||
await onLoginSucceeded(res);
|
|
||||||
}).catch(onLoginFailed);
|
|
||||||
}
|
|
||||||
} else if (userInfo.value.securityKeys) {
|
|
||||||
password.value = pw.password;
|
|
||||||
|
|
||||||
await misskeyApi('signin', {
|
|
||||||
username: userInfo.value.username,
|
|
||||||
password: pw.password,
|
|
||||||
}).then((res) => {
|
|
||||||
credentialRequest.value = parseRequestOptionsFromJSON({
|
|
||||||
publicKey: res,
|
|
||||||
});
|
|
||||||
page.value = 'passkey';
|
|
||||||
waiting.value = false;
|
|
||||||
}).catch(onLoginFailed);
|
|
||||||
} else {
|
|
||||||
password.value = pw.password;
|
|
||||||
page.value = 'totp';
|
|
||||||
waiting.value = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,19 +211,37 @@ async function onTotpSubmitted(token: string) {
|
||||||
title: i18n.ts.noSuchUser,
|
title: i18n.ts.noSuchUser,
|
||||||
text: i18n.ts.signinFailed,
|
text: i18n.ts.signinFailed,
|
||||||
});
|
});
|
||||||
|
waiting.value = false;
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
await misskeyApi('signin', {
|
await tryLogin({
|
||||||
username: userInfo.value.username,
|
username: userInfo.value.username,
|
||||||
password: password.value,
|
password: password.value,
|
||||||
token,
|
token,
|
||||||
}).then(async (res) => {
|
});
|
||||||
emit('login', res);
|
|
||||||
await onLoginSucceeded(res);
|
|
||||||
}).catch(onLoginFailed);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function tryLogin(req: Partial<Misskey.entities.SigninRequest>): Promise<Misskey.entities.SigninResponse> {
|
||||||
|
if (userInfo.value == null) {
|
||||||
|
throw new Error('No such user');
|
||||||
|
}
|
||||||
|
|
||||||
|
const _req = {
|
||||||
|
username: userInfo.value.username,
|
||||||
|
...req,
|
||||||
|
};
|
||||||
|
|
||||||
|
return await misskeyApi('signin', _req).then(async (res) => {
|
||||||
|
emit('login', res);
|
||||||
|
await onLoginSucceeded(res);
|
||||||
|
return res;
|
||||||
|
}).catch((err) => {
|
||||||
|
onLoginFailed(err);
|
||||||
|
return Promise.reject(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function onLoginSucceeded(res: Misskey.entities.SigninResponse) {
|
async function onLoginSucceeded(res: Misskey.entities.SigninResponse) {
|
||||||
if (props.autoSet) {
|
if (props.autoSet) {
|
||||||
await login(res.i);
|
await login(res.i);
|
||||||
|
@ -265,92 +251,128 @@ async function onLoginSucceeded(res: Misskey.entities.SigninResponse) {
|
||||||
function onLoginFailed(err?: any): void {
|
function onLoginFailed(err?: any): void {
|
||||||
const id = err?.id ?? null;
|
const id = err?.id ?? null;
|
||||||
|
|
||||||
switch (id) {
|
if (typeof err === 'object' && 'next' in err) {
|
||||||
case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
|
switch (err.next) {
|
||||||
os.alert({
|
case 'captcha': {
|
||||||
type: 'error',
|
page.value = 'password';
|
||||||
title: i18n.ts.loginFailed,
|
break;
|
||||||
text: i18n.ts.noSuchUser,
|
}
|
||||||
});
|
case 'password': {
|
||||||
break;
|
page.value = 'password';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'totp': {
|
||||||
|
page.value = 'totp';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'passkey': {
|
||||||
|
if (webAuthnSupported() && 'authRequest' in err) {
|
||||||
|
credentialRequest.value = parseRequestOptionsFromJSON({
|
||||||
|
publicKey: err.authRequest,
|
||||||
|
});
|
||||||
|
page.value = 'passkey';
|
||||||
|
} else {
|
||||||
|
page.value = 'totp';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case '932c904e-9460-45b7-9ce6-7ed33be7eb2c': {
|
} else {
|
||||||
os.alert({
|
switch (id) {
|
||||||
type: 'error',
|
case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
|
||||||
title: i18n.ts.loginFailed,
|
os.alert({
|
||||||
text: i18n.ts.incorrectPassword,
|
type: 'error',
|
||||||
});
|
title: i18n.ts.loginFailed,
|
||||||
break;
|
text: i18n.ts.noSuchUser,
|
||||||
}
|
});
|
||||||
case 'e03a5f46-d309-4865-9b69-56282d94e1eb': {
|
break;
|
||||||
showSuspendedDialog();
|
}
|
||||||
break;
|
case '932c904e-9460-45b7-9ce6-7ed33be7eb2c': {
|
||||||
}
|
os.alert({
|
||||||
case '22d05606-fbcf-421a-a2db-b32610dcfd1b': {
|
type: 'error',
|
||||||
os.alert({
|
title: i18n.ts.loginFailed,
|
||||||
type: 'error',
|
text: i18n.ts.incorrectPassword,
|
||||||
title: i18n.ts.loginFailed,
|
});
|
||||||
text: i18n.ts.rateLimitExceeded,
|
break;
|
||||||
});
|
}
|
||||||
break;
|
case 'e03a5f46-d309-4865-9b69-56282d94e1eb': {
|
||||||
}
|
showSuspendedDialog();
|
||||||
case 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f': {
|
break;
|
||||||
os.alert({
|
}
|
||||||
type: 'error',
|
case '22d05606-fbcf-421a-a2db-b32610dcfd1b': {
|
||||||
title: i18n.ts.loginFailed,
|
os.alert({
|
||||||
text: i18n.ts.incorrectTotp,
|
type: 'error',
|
||||||
});
|
title: i18n.ts.loginFailed,
|
||||||
break;
|
text: i18n.ts.rateLimitExceeded,
|
||||||
}
|
});
|
||||||
case '36b96a7d-b547-412d-aeed-2d611cdc8cdc': {
|
break;
|
||||||
os.alert({
|
}
|
||||||
type: 'error',
|
case 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f': {
|
||||||
title: i18n.ts.loginFailed,
|
os.alert({
|
||||||
text: i18n.ts.unknownWebAuthnKey,
|
type: 'error',
|
||||||
});
|
title: i18n.ts.loginFailed,
|
||||||
break;
|
text: i18n.ts.incorrectTotp,
|
||||||
}
|
});
|
||||||
case '93b86c4b-72f9-40eb-9815-798928603d1e': {
|
break;
|
||||||
os.alert({
|
}
|
||||||
type: 'error',
|
case '36b96a7d-b547-412d-aeed-2d611cdc8cdc': {
|
||||||
title: i18n.ts.loginFailed,
|
os.alert({
|
||||||
text: i18n.ts.passkeyVerificationFailed,
|
type: 'error',
|
||||||
});
|
title: i18n.ts.loginFailed,
|
||||||
break;
|
text: i18n.ts.unknownWebAuthnKey,
|
||||||
}
|
});
|
||||||
case 'b18c89a7-5b5e-4cec-bb5b-0419f332d430': {
|
break;
|
||||||
os.alert({
|
}
|
||||||
type: 'error',
|
case '93b86c4b-72f9-40eb-9815-798928603d1e': {
|
||||||
title: i18n.ts.loginFailed,
|
os.alert({
|
||||||
text: i18n.ts.passkeyVerificationFailed,
|
type: 'error',
|
||||||
});
|
title: i18n.ts.loginFailed,
|
||||||
break;
|
text: i18n.ts.passkeyVerificationFailed,
|
||||||
}
|
});
|
||||||
case '2d84773e-f7b7-4d0b-8f72-bb69b584c912': {
|
break;
|
||||||
os.alert({
|
}
|
||||||
type: 'error',
|
case 'b18c89a7-5b5e-4cec-bb5b-0419f332d430': {
|
||||||
title: i18n.ts.loginFailed,
|
os.alert({
|
||||||
text: i18n.ts.passkeyVerificationSucceededButPasswordlessLoginDisabled,
|
type: 'error',
|
||||||
});
|
title: i18n.ts.loginFailed,
|
||||||
break;
|
text: i18n.ts.passkeyVerificationFailed,
|
||||||
}
|
});
|
||||||
default: {
|
break;
|
||||||
console.error(err);
|
}
|
||||||
os.alert({
|
case '2d84773e-f7b7-4d0b-8f72-bb69b584c912': {
|
||||||
type: 'error',
|
os.alert({
|
||||||
title: i18n.ts.loginFailed,
|
type: 'error',
|
||||||
text: JSON.stringify(err),
|
title: i18n.ts.loginFailed,
|
||||||
});
|
text: i18n.ts.passkeyVerificationSucceededButPasswordlessLoginDisabled,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
console.error(err);
|
||||||
|
os.alert({
|
||||||
|
type: 'error',
|
||||||
|
title: i18n.ts.loginFailed,
|
||||||
|
text: JSON.stringify(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (doingPasskeyFromInputPage.value === true) {
|
if (doingPasskeyFromInputPage.value === true) {
|
||||||
doingPasskeyFromInputPage.value = false;
|
doingPasskeyFromInputPage.value = false;
|
||||||
page.value = 'input';
|
page.value = 'input';
|
||||||
|
password.value = '';
|
||||||
}
|
}
|
||||||
passwordPageEl.value?.resetCaptcha();
|
passwordPageEl.value?.resetCaptcha();
|
||||||
waiting.value = false;
|
nextTick(() => {
|
||||||
|
waiting.value = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
password.value = '';
|
||||||
|
userInfo.value = null;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -68,7 +68,6 @@ function onLogin(res) {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-height: 450px;
|
max-height: 450px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
text-align: center;
|
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3040,7 +3040,7 @@ type Signin = components['schemas']['Signin'];
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type SigninRequest = {
|
type SigninRequest = {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password?: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
credential?: AuthenticationResponseJSON;
|
credential?: AuthenticationResponseJSON;
|
||||||
'hcaptcha-response'?: string | null;
|
'hcaptcha-response'?: string | null;
|
||||||
|
|
|
@ -3782,16 +3782,13 @@ export type components = {
|
||||||
followingVisibility: 'public' | 'followers' | 'private';
|
followingVisibility: 'public' | 'followers' | 'private';
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
followersVisibility: 'public' | 'followers' | 'private';
|
followersVisibility: 'public' | 'followers' | 'private';
|
||||||
/** @default false */
|
|
||||||
twoFactorEnabled: boolean;
|
|
||||||
/** @default false */
|
|
||||||
usePasswordLessLogin: boolean;
|
|
||||||
/** @default false */
|
|
||||||
securityKeys: boolean;
|
|
||||||
roles: components['schemas']['RoleLite'][];
|
roles: components['schemas']['RoleLite'][];
|
||||||
followedMessage?: string | null;
|
followedMessage?: string | null;
|
||||||
memo: string | null;
|
memo: string | null;
|
||||||
moderationNote?: string;
|
moderationNote?: string;
|
||||||
|
twoFactorEnabled?: boolean;
|
||||||
|
usePasswordLessLogin?: boolean;
|
||||||
|
securityKeys?: boolean;
|
||||||
isFollowing?: boolean;
|
isFollowing?: boolean;
|
||||||
isFollowed?: boolean;
|
isFollowed?: boolean;
|
||||||
hasPendingFollowRequestFromYou?: boolean;
|
hasPendingFollowRequestFromYou?: boolean;
|
||||||
|
|
|
@ -269,7 +269,7 @@ export type SignupPendingResponse = {
|
||||||
|
|
||||||
export type SigninRequest = {
|
export type SigninRequest = {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password?: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
credential?: AuthenticationResponseJSON;
|
credential?: AuthenticationResponseJSON;
|
||||||
'hcaptcha-response'?: string | null;
|
'hcaptcha-response'?: string | null;
|
||||||
|
|
Loading…
Reference in New Issue