From 975c2e7bc567618c3f8b0082afcba6530d679dae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Fri, 4 Oct 2024 15:23:33 +0900 Subject: [PATCH] =?UTF-8?q?enhance(frontend):=20=E3=82=B5=E3=82=A4?= =?UTF-8?q?=E3=83=B3=E3=82=A4=E3=83=B3=E7=94=BB=E9=9D=A2=E3=81=AE=E6=94=B9?= =?UTF-8?q?=E5=96=84=20(#14658)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * Update MkSignin.vue * Update MkSignin.vue * wip * Update CHANGELOG.md * enhance(frontend): サインイン画面の改善 * Update Changelog * 14655の変更取り込み * spdx * fix * fix * fix * :art: * :art: * :art: * :art: * Captchaがリセットされない問題を修正 * 次の処理をsignin apiから読み取るように * Add Comments * fix * fix test * attempt to fix test * fix test * fix test * fix test * fix * fix test * fix: 一部のエラーがちゃんと出るように * Update Changelog * :art: * :art: * remove border --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- CHANGELOG.md | 4 + cypress/e2e/basic.cy.ts | 14 +- cypress/support/commands.ts | 4 +- locales/index.d.ts | 4 + locales/ja-JP.yml | 1 + .../src/core/entities/UserEntityService.ts | 13 +- .../backend/src/models/json-schema/user.ts | 42 +- .../src/server/api/SigninApiService.ts | 86 ++- packages/backend/test/e2e/2fa.ts | 99 +-- packages/backend/test/e2e/users.ts | 15 +- .../src/components/MkSignin.input.vue | 206 +++++ .../src/components/MkSignin.passkey.vue | 92 +++ .../src/components/MkSignin.password.vue | 181 +++++ .../frontend/src/components/MkSignin.totp.vue | 74 ++ packages/frontend/src/components/MkSignin.vue | 716 +++++++++--------- .../src/components/MkSigninDialog.vue | 80 +- packages/misskey-js/etc/misskey-js.api.md | 2 +- packages/misskey-js/src/autogen/types.ts | 15 +- packages/misskey-js/src/entities.ts | 2 +- 19 files changed, 1161 insertions(+), 489 deletions(-) create mode 100644 packages/frontend/src/components/MkSignin.input.vue create mode 100644 packages/frontend/src/components/MkSignin.passkey.vue create mode 100644 packages/frontend/src/components/MkSignin.password.vue create mode 100644 packages/frontend/src/components/MkSignin.totp.vue diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e4ddabd55..a31be063f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - サーバー初期設定時に使用する初期パスワードを設定できるようになりました。今後Misskeyサーバーを新たに設置する際には、初回の起動前にコンフィグファイルの`setupPassword`をコメントアウトし、初期パスワードを設定することをおすすめします。(すでに初期設定を完了しているサーバーについては、この変更に伴い対応する必要はありません) - ホスティングサービスを運営している場合は、コンフィグファイルを構築する際に`setupPassword`をランダムな値に設定し、ユーザーに通知するようにシステムを更新することをおすすめします。 - なお、初期パスワードが設定されていない場合でも初期設定を行うことが可能です(UI上で初期パスワードの入力欄を空欄にすると続行できます)。 +- ユーザーデータを読み込む際の型が一部変更されました。 + - `twoFactorEnabled`, `usePasswordLessLogin`, `securityKeys`: 自分とモデレーター以外のユーザーからは取得できなくなりました ### General - Feat: サーバー初期設定時に初期パスワードを設定できるように @@ -14,9 +16,11 @@ ### Client - Enhance: デザインの調整 +- Enhance: ログイン画面の認証フローを改善 ### Server - Enhance: セキュリティ向上のため、ログイン時にメール通知を行うように +- Enhance: 自分とモデレーター以外のユーザーから二要素認証関連のデータが取得できないように ## 2024.9.0 diff --git a/cypress/e2e/basic.cy.ts b/cypress/e2e/basic.cy.ts index e4baeacbf3..c9d7e0a24a 100644 --- a/cypress/e2e/basic.cy.ts +++ b/cypress/e2e/basic.cy.ts @@ -123,8 +123,13 @@ describe('After user signup', () => { cy.intercept('POST', '/api/signin').as('signin'); cy.get('[data-cy-signin]').click(); - cy.get('[data-cy-signin-username] input').type('alice'); - // Enterキーでサインインできるかの確認も兼ねる + + cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 }); + // Enterキーで続行できるかの確認も兼ねる + cy.get('[data-cy-signin-username] input').type('alice{enter}'); + + cy.get('[data-cy-signin-page-password]').should('be.visible', { timeout: 10000 }); + // Enterキーで続行できるかの確認も兼ねる cy.get('[data-cy-signin-password] input').type('alice1234{enter}'); cy.wait('@signin'); @@ -139,8 +144,9 @@ describe('After user signup', () => { cy.visitHome(); cy.get('[data-cy-signin]').click(); - cy.get('[data-cy-signin-username] input').type('alice'); - cy.get('[data-cy-signin-password] input').type('alice1234{enter}'); + + cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 }); + cy.get('[data-cy-signin-username] input').type('alice{enter}'); // TODO: cypressにブラウザの言語指定できる機能が実装され次第英語のみテストするようにする cy.contains(/アカウントが凍結されています|This account has been suspended due to/gi); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 3cdf4e2087..ed5cda31b0 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -58,7 +58,9 @@ Cypress.Commands.add('login', (username, password) => { cy.intercept('POST', '/api/signin').as('signin'); cy.get('[data-cy-signin]').click(); - cy.get('[data-cy-signin-username] input').type(username); + cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 }); + cy.get('[data-cy-signin-username] input').type(`${username}{enter}`); + cy.get('[data-cy-signin-page-password]').should('be.visible', { timeout: 10000 }); cy.get('[data-cy-signin-password] input').type(`${password}{enter}`); cy.wait('@signin').as('signedIn'); diff --git a/locales/index.d.ts b/locales/index.d.ts index 86a6df3100..1a0547ebc6 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -3714,6 +3714,10 @@ export interface Locale extends ILocale { * パスワードが間違っています。 */ "incorrectPassword": string; + /** + * ワンタイムパスワードが間違っているか、期限切れになっています。 + */ + "incorrectTotp": string; /** * 「{choice}」に投票しますか? */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 62317cd5e6..92014c8abc 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -924,6 +924,7 @@ followersVisibility: "フォロワーの公開範囲" continueThread: "さらにスレッドを見る" deleteAccountConfirm: "アカウントが削除されます。よろしいですか?" incorrectPassword: "パスワードが間違っています。" +incorrectTotp: "ワンタイムパスワードが間違っているか、期限切れになっています。" voteConfirm: "「{choice}」に投票しますか?" hide: "隠す" useDrawerReactionPickerForMobile: "モバイルデバイスのときドロワーで表示" diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 69e2d6fc89..c9939adf11 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -545,11 +545,6 @@ export class UserEntityService implements OnModuleInit { publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964 followersVisibility: profile!.followersVisibility, 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 => ({ id: role.id, name: role.name, @@ -564,6 +559,14 @@ export class UserEntityService implements OnModuleInit { 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 ? { avatarId: user.avatarId, bannerId: user.bannerId, diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 16c8a5a097..9cffd680f2 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -346,21 +346,6 @@ export const packedUserDetailedNotMeOnlySchema = { nullable: false, optional: false, 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: { type: 'array', nullable: false, optional: false, @@ -382,6 +367,18 @@ export const packedUserDetailedNotMeOnlySchema = { type: 'string', 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 isFollowing: { type: 'boolean', @@ -630,6 +627,21 @@ export const packedMeDetailedOnlySchema = { nullable: false, optional: false, ref: 'RolePolicies', }, + 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, + }, //#region secrets email: { type: 'string', diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index 2ccc75da00..81684beb3c 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -12,6 +12,7 @@ import type { MiMeta, SigninsRepository, UserProfilesRepository, + UserSecurityKeysRepository, UsersRepository, } from '@/models/_.js'; import type { Config } from '@/config.js'; @@ -25,9 +26,27 @@ import { CaptchaService } from '@/core/CaptchaService.js'; import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; import { RateLimiterService } from './RateLimiterService.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'; +/** + * next を指定すると、次にクライアント側で行うべき処理を指定できる。 + * + * - `captcha`: パスワードと、(有効になっている場合は)CAPTCHAを求める + * - `password`: パスワードを求める + * - `totp`: ワンタイムパスワードを求める + * - `passkey`: WebAuthn認証を求める(WebAuthnに対応していないブラウザの場合はワンタイムパスワード) + */ + +type SigninErrorResponse = { + id: string; + next?: 'captcha' | 'password' | 'totp'; +} | { + id: string; + next: 'passkey'; + authRequest: PublicKeyCredentialRequestOptionsJSON; +}; + @Injectable() export class SigninApiService { constructor( @@ -43,6 +62,9 @@ export class SigninApiService { @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, + @Inject(DI.userSecurityKeysRepository) + private userSecurityKeysRepository: UserSecurityKeysRepository, + @Inject(DI.signinsRepository) private signinsRepository: SigninsRepository, @@ -60,7 +82,7 @@ export class SigninApiService { request: FastifyRequest<{ Body: { username: string; - password: string; + password?: string; token?: string; credential?: AuthenticationResponseJSON; 'hcaptcha-response'?: string; @@ -79,7 +101,7 @@ export class SigninApiService { const password = body['password']; const token = body['token']; - function error(status: number, error: { id: string }) { + function error(status: number, error: SigninErrorResponse) { reply.code(status); return { error }; } @@ -103,11 +125,6 @@ export class SigninApiService { return; } - if (typeof password !== 'string') { - reply.code(400); - return; - } - if (token != null && typeof token !== 'string') { reply.code(400); return; @@ -132,11 +149,36 @@ export class SigninApiService { } 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 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 await this.signinsRepository.insert({ id: this.idService.gen(), @@ -217,7 +259,7 @@ export class SigninApiService { id: '93b86c4b-72f9-40eb-9815-798928603d1e', }); } - } else { + } else if (securityKeysAvailable) { if (!same && !profile.usePasswordLessLogin) { return await fail(403, { id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', @@ -226,8 +268,28 @@ export class SigninApiService { const authRequest = await this.webAuthnService.initiateAuthentication(user.id); - reply.code(200); - return authRequest; + reply.code(403); + 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 } diff --git a/packages/backend/test/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts index 06548fa7da..88c32b4346 100644 --- a/packages/backend/test/e2e/2fa.ts +++ b/packages/backend/test/e2e/2fa.ts @@ -136,13 +136,7 @@ describe('2要素認証', () => { keyName: string, credentialId: Buffer, requestOptions: PublicKeyCredentialRequestOptionsJSON, - }): { - username: string, - password: string, - credential: AuthenticationResponseJSON, - 'g-recaptcha-response'?: string | null, - 'hcaptcha-response'?: string | null, - } => { + }): misskey.entities.SigninRequest => { // AuthenticatorAssertionResponse.authenticatorData // https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData const authenticatorData = Buffer.concat([ @@ -202,11 +196,16 @@ describe('2要素認証', () => { }, alice); assert.strictEqual(doneResponse.status, 200); - const usersShowResponse = await api('users/show', { - username, - }, alice); - assert.strictEqual(usersShowResponse.status, 200); - assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true); + const signinWithoutTokenResponse = await api('signin', { + ...signinParam(), + }); + assert.strictEqual(signinWithoutTokenResponse.status, 403); + assert.deepStrictEqual(signinWithoutTokenResponse.body, { + error: { + id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf', + next: 'totp', + }, + }); const signinResponse = await api('signin', { ...signinParam(), @@ -253,26 +252,28 @@ describe('2要素認証', () => { assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url')); assert.strictEqual(keyDoneResponse.body.name, keyName); - const usersShowResponse = await api('users/show', { - username, - }); - assert.strictEqual(usersShowResponse.status, 200); - assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, true); - const signinResponse = await api('signin', { ...signinParam(), }); - assert.strictEqual(signinResponse.status, 200); - assert.strictEqual(signinResponse.body.i, undefined); - assert.notEqual((signinResponse.body as unknown as { challenge: unknown | undefined }).challenge, undefined); - assert.notEqual((signinResponse.body as unknown as { allowCredentials: unknown | undefined }).allowCredentials, undefined); - assert.strictEqual((signinResponse.body as unknown as { allowCredentials: {id: string}[] }).allowCredentials[0].id, credentialId.toString('base64url')); + const signinResponseBody = signinResponse.body as unknown as { + error: { + id: string; + next: 'passkey'; + authRequest: PublicKeyCredentialRequestOptionsJSON; + }; + }; + assert.strictEqual(signinResponse.status, 403); + assert.strictEqual(signinResponseBody.error.id, '06e661b9-8146-4ae3-bde5-47138c0ae0c4'); + assert.strictEqual(signinResponseBody.error.next, 'passkey'); + assert.notEqual(signinResponseBody.error.authRequest.challenge, undefined); + assert.notEqual(signinResponseBody.error.authRequest.allowCredentials, undefined); + assert.strictEqual(signinResponseBody.error.authRequest.allowCredentials && signinResponseBody.error.authRequest.allowCredentials[0]?.id, credentialId.toString('base64url')); const signinResponse2 = await api('signin', signinWithSecurityKeyParam({ keyName, credentialId, - requestOptions: signinResponse.body, - } as any)); + requestOptions: signinResponseBody.error.authRequest, + })); assert.strictEqual(signinResponse2.status, 200); assert.notEqual(signinResponse2.body.i, undefined); @@ -315,24 +316,32 @@ describe('2要素認証', () => { }, alice); assert.strictEqual(passwordLessResponse.status, 204); - const usersShowResponse = await api('users/show', { - username, - }); - assert.strictEqual(usersShowResponse.status, 200); - assert.strictEqual((usersShowResponse.body as unknown as { usePasswordLessLogin: boolean }).usePasswordLessLogin, true); + const iResponse = await api('i', {}, alice); + assert.strictEqual(iResponse.status, 200); + assert.strictEqual(iResponse.body.usePasswordLessLogin, true); const signinResponse = await api('signin', { ...signinParam(), password: '', }); - assert.strictEqual(signinResponse.status, 200); - assert.strictEqual(signinResponse.body.i, undefined); + const signinResponseBody = signinResponse.body as unknown as { + error: { + id: string; + next: 'passkey'; + authRequest: PublicKeyCredentialRequestOptionsJSON; + }; + }; + assert.strictEqual(signinResponse.status, 403); + assert.strictEqual(signinResponseBody.error.id, '06e661b9-8146-4ae3-bde5-47138c0ae0c4'); + assert.strictEqual(signinResponseBody.error.next, 'passkey'); + assert.notEqual(signinResponseBody.error.authRequest.challenge, undefined); + assert.notEqual(signinResponseBody.error.authRequest.allowCredentials, undefined); const signinResponse2 = await api('signin', { ...signinWithSecurityKeyParam({ keyName, credentialId, - requestOptions: signinResponse.body, + requestOptions: signinResponseBody.error.authRequest, } as any), password: '', }); @@ -424,11 +433,11 @@ describe('2要素認証', () => { assert.strictEqual(keyDoneResponse.status, 200); // テストの実行順によっては複数残ってるので全部消す - const iResponse = await api('i', { + const beforeIResponse = await api('i', { }, alice); - assert.strictEqual(iResponse.status, 200); - assert.ok(iResponse.body.securityKeysList); - for (const key of iResponse.body.securityKeysList) { + assert.strictEqual(beforeIResponse.status, 200); + assert.ok(beforeIResponse.body.securityKeysList); + for (const key of beforeIResponse.body.securityKeysList) { const removeKeyResponse = await api('i/2fa/remove-key', { token: otpToken(registerResponse.body.secret), password, @@ -437,11 +446,9 @@ describe('2要素認証', () => { assert.strictEqual(removeKeyResponse.status, 200); } - const usersShowResponse = await api('users/show', { - username, - }); - assert.strictEqual(usersShowResponse.status, 200); - assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, false); + const afterIResponse = await api('i', {}, alice); + assert.strictEqual(afterIResponse.status, 200); + assert.strictEqual(afterIResponse.body.securityKeys, false); const signinResponse = await api('signin', { ...signinParam(), @@ -468,11 +475,9 @@ describe('2要素認証', () => { }, alice); assert.strictEqual(doneResponse.status, 200); - const usersShowResponse = await api('users/show', { - username, - }); - assert.strictEqual(usersShowResponse.status, 200); - assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true); + const iResponse = await api('i', {}, alice); + assert.strictEqual(iResponse.status, 200); + assert.strictEqual(iResponse.body.twoFactorEnabled, true); const unregisterResponse = await api('i/2fa/unregister', { token: otpToken(registerResponse.body.secret), diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index 8ebe9af792..822ca14ae6 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -83,9 +83,6 @@ describe('ユーザー', () => { publicReactions: user.publicReactions, followingVisibility: user.followingVisibility, followersVisibility: user.followersVisibility, - twoFactorEnabled: user.twoFactorEnabled, - usePasswordLessLogin: user.usePasswordLessLogin, - securityKeys: user.securityKeys, roles: user.roles, memo: user.memo, }); @@ -149,6 +146,9 @@ describe('ユーザー', () => { achievements: user.achievements, loggedInDays: user.loggedInDays, policies: user.policies, + twoFactorEnabled: user.twoFactorEnabled, + usePasswordLessLogin: user.usePasswordLessLogin, + securityKeys: user.securityKeys, ...(security ? { email: user.email, emailVerified: user.emailVerified, @@ -343,9 +343,6 @@ describe('ユーザー', () => { assert.strictEqual(response.publicReactions, true); assert.strictEqual(response.followingVisibility, 'public'); assert.strictEqual(response.followersVisibility, 'public'); - assert.strictEqual(response.twoFactorEnabled, false); - assert.strictEqual(response.usePasswordLessLogin, false); - assert.strictEqual(response.securityKeys, false); assert.deepStrictEqual(response.roles, []); assert.strictEqual(response.memo, null); @@ -385,6 +382,9 @@ describe('ユーザー', () => { assert.deepStrictEqual(response.achievements, []); assert.deepStrictEqual(response.loggedInDays, 0); assert.deepStrictEqual(response.policies, DEFAULT_POLICIES); + assert.strictEqual(response.twoFactorEnabled, false); + assert.strictEqual(response.usePasswordLessLogin, false); + assert.strictEqual(response.securityKeys, false); assert.notStrictEqual(response.email, undefined); assert.strictEqual(response.emailVerified, false); assert.deepStrictEqual(response.securityKeysList, []); @@ -618,6 +618,9 @@ describe('ユーザー', () => { { label: 'Moderatorになっている', user: () => userModerator, me: () => userModerator, selector: (user: misskey.entities.MeDetailed) => user.isModerator }, // @ts-expect-error UserDetailedNotMe doesn't include isModerator { label: '自分以外から見たときはModeratorか判定できない', user: () => userModerator, selector: (user: misskey.entities.UserDetailedNotMe) => user.isModerator, expected: () => undefined }, + { label: '自分から見た場合に二要素認証関連のプロパティがセットされている', user: () => alice, me: () => alice, selector: (user: misskey.entities.MeDetailed) => user.twoFactorEnabled, expected: () => false }, + { label: '自分以外から見た場合に二要素認証関連のプロパティがセットされていない', user: () => alice, me: () => bob, selector: (user: misskey.entities.UserDetailedNotMe) => user.twoFactorEnabled, expected: () => undefined }, + { label: 'モデレーターから見た場合に二要素認証関連のプロパティがセットされている', user: () => alice, me: () => userModerator, selector: (user: misskey.entities.UserDetailedNotMe) => user.twoFactorEnabled, expected: () => false }, { label: 'サイレンスになっている', user: () => userSilenced, selector: (user: misskey.entities.UserDetailed) => user.isSilenced }, // FIXME: 落ちる //{ label: 'サスペンドになっている', user: () => userSuspended, selector: (user: misskey.entities.UserDetailed) => user.isSuspended }, diff --git a/packages/frontend/src/components/MkSignin.input.vue b/packages/frontend/src/components/MkSignin.input.vue new file mode 100644 index 0000000000..6336b78c80 --- /dev/null +++ b/packages/frontend/src/components/MkSignin.input.vue @@ -0,0 +1,206 @@ + + + + + + + diff --git a/packages/frontend/src/components/MkSignin.passkey.vue b/packages/frontend/src/components/MkSignin.passkey.vue new file mode 100644 index 0000000000..0d68955fab --- /dev/null +++ b/packages/frontend/src/components/MkSignin.passkey.vue @@ -0,0 +1,92 @@ + + + + + + + diff --git a/packages/frontend/src/components/MkSignin.password.vue b/packages/frontend/src/components/MkSignin.password.vue new file mode 100644 index 0000000000..2d79e2aeb1 --- /dev/null +++ b/packages/frontend/src/components/MkSignin.password.vue @@ -0,0 +1,181 @@ + + + + + + + + + diff --git a/packages/frontend/src/components/MkSignin.totp.vue b/packages/frontend/src/components/MkSignin.totp.vue new file mode 100644 index 0000000000..880c08315e --- /dev/null +++ b/packages/frontend/src/components/MkSignin.totp.vue @@ -0,0 +1,74 @@ + + + + + + + diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index abbff8e1f2..81a98cae0e 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -4,438 +4,402 @@ SPDX-License-Identifier: AGPL-3.0-only --> - diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue index d48780e9de..8351d7d5e0 100644 --- a/packages/frontend/src/components/MkSigninDialog.vue +++ b/packages/frontend/src/components/MkSigninDialog.vue @@ -4,26 +4,29 @@ SPDX-License-Identifier: AGPL-3.0-only --> + + diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 5f4792eb74..9ad784c296 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -3040,7 +3040,7 @@ type Signin = components['schemas']['Signin']; // @public (undocumented) type SigninRequest = { username: string; - password: string; + password?: string; token?: string; credential?: AuthenticationResponseJSON; 'hcaptcha-response'?: string | null; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 32646d28ed..3876a0bfe5 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -3782,16 +3782,13 @@ export type components = { followingVisibility: 'public' | 'followers' | 'private'; /** @enum {string} */ followersVisibility: 'public' | 'followers' | 'private'; - /** @default false */ - twoFactorEnabled: boolean; - /** @default false */ - usePasswordLessLogin: boolean; - /** @default false */ - securityKeys: boolean; roles: components['schemas']['RoleLite'][]; followedMessage?: string | null; memo: string | null; moderationNote?: string; + twoFactorEnabled?: boolean; + usePasswordLessLogin?: boolean; + securityKeys?: boolean; isFollowing?: boolean; isFollowed?: boolean; hasPendingFollowRequestFromYou?: boolean; @@ -3972,6 +3969,12 @@ export type components = { }[]; loggedInDays: number; policies: components['schemas']['RolePolicies']; + /** @default false */ + twoFactorEnabled: boolean; + /** @default false */ + usePasswordLessLogin: boolean; + /** @default false */ + securityKeys: boolean; email?: string | null; emailVerified?: boolean | null; securityKeysList?: { diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 36b7f5bca3..98ac50e5a1 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -269,7 +269,7 @@ export type SignupPendingResponse = { export type SigninRequest = { username: string; - password: string; + password?: string; token?: string; credential?: AuthenticationResponseJSON; 'hcaptcha-response'?: string | null;