From 64e135174adee07523050c1451181f26dea7a449 Mon Sep 17 00:00:00 2001 From: mattyatea Date: Sun, 8 Dec 2024 01:44:42 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=E3=83=A1=E3=83=BC=E3=83=AB=E3=82=A2?= =?UTF-8?q?=E3=83=89=E3=83=AC=E3=82=B9=E3=83=AD=E3=82=B0=E3=82=A4=E3=83=B3?= =?UTF-8?q?=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/index.d.ts | 8 +++ locales/ja-JP.yml | 2 + .../backend/src/server/api/EndpointsModule.ts | 4 ++ .../src/server/api/SigninApiService.ts | 43 ++++++++---- packages/backend/src/server/api/endpoints.ts | 2 + .../endpoints/users/get-twofactor-enable.ts | 63 ++++++++++++++++++ packages/frontend/src/account.ts | 6 +- packages/frontend/src/components/MkSignin.vue | 62 ++++++++++++++--- .../src/components/MkSigninDialog.vue | 6 +- packages/misskey-js/etc/misskey-js.api.md | 8 +++ .../misskey-js/src/autogen/apiClientJSDoc.ts | 11 ++++ packages/misskey-js/src/autogen/endpoint.ts | 3 + packages/misskey-js/src/autogen/entities.ts | 2 + packages/misskey-js/src/autogen/types.ts | 66 +++++++++++++++++++ 14 files changed, 259 insertions(+), 27 deletions(-) create mode 100644 packages/backend/src/server/api/endpoints/users/get-twofactor-enable.ts diff --git a/locales/index.d.ts b/locales/index.d.ts index eba90728a1d0..370cb5c17548 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5318,6 +5318,14 @@ export interface Locale extends ILocale { * 選択した項目のみ許可 */ "consentSelected": string; + /** + * メールアドレスでログイン + */ + "emailAddressLogin": string; + /** + * ユーザー名でログイン + */ + "usernameLogin": string; "_bubbleGame": { /** * 遊び方 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 746854467de4..3b2d0932e384 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1323,6 +1323,8 @@ pleaseConsentToTracking: "{host}は[プライバシーポリシー]({privacyPoli consentEssential: "必須項目のみ許可" consentAll: "全て許可" consentSelected: "選択した項目のみ許可" +emailAddressLogin: "メールアドレスでログイン" +usernameLogin: "ユーザー名でログイン" _bubbleGame: howToPlay: "遊び方" diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 7b564affeb5d..f4624abf3fcd 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -382,6 +382,7 @@ import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by import * as ep___users_search from './endpoints/users/search.js'; import * as ep___users_show from './endpoints/users/show.js'; import * as ep___users_stats from './endpoints/users/stats.js'; +import * as ep___users_get_twofactor_enable from './endpoints/users/get-twofactor-enable.js'; import * as ep___users_achievements from './endpoints/users/achievements.js'; import * as ep___users_updateMemo from './endpoints/users/update-memo.js'; import * as ep___fetchRss from './endpoints/fetch-rss.js'; @@ -776,6 +777,7 @@ const $users_searchByUsernameAndHost: Provider = { provide: 'ep:users/search-by- const $users_search: Provider = { provide: 'ep:users/search', useClass: ep___users_search.default }; const $users_show: Provider = { provide: 'ep:users/show', useClass: ep___users_show.default }; const $users_stats: Provider = { provide: 'ep:users/stats', useClass: ep___users_stats.default }; +const $users_twofactor_enable: Provider = { provide: 'ep:users/get-twofactor-enable', useClass: ep___users_get_twofactor_enable.default }; const $users_achievements: Provider = { provide: 'ep:users/achievements', useClass: ep___users_achievements.default }; const $users_updateMemo: Provider = { provide: 'ep:users/update-memo', useClass: ep___users_updateMemo.default }; const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default }; @@ -1175,6 +1177,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $users_show, $users_stats, $users_achievements, + $users_twofactor_enable, $users_updateMemo, $fetchRss, $fetchExternalResources, @@ -1563,6 +1566,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $users_searchByUsernameAndHost, $users_search, $users_show, + $users_twofactor_enable, $users_stats, $users_achievements, $users_updateMemo, diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index ed661a1fabce..c6d6d08261fc 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -3,11 +3,13 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { randomUUID } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { + MiUserProfile, SigninsRepository, UserProfilesRepository, UsersRepository, @@ -27,7 +29,6 @@ import { RateLimiterService } from './RateLimiterService.js'; import { SigninService } from './SigninService.js'; import type { AuthenticationResponseJSON } from '@simplewebauthn/types'; import type { FastifyReply, FastifyRequest } from 'fastify'; -import { randomUUID } from 'node:crypto'; @Injectable() export class SigninApiService { @@ -121,23 +122,43 @@ export class SigninApiService { return; } + let user: MiLocalUser | null = null; + let profile: MiUserProfile | null = null; // Fetch user - const user = await this.usersRepository.findOneBy({ - usernameLower: username.toLowerCase(), - host: IsNull(), - }) as MiLocalUser; + if (username.includes('@')) { + profile = await this.userProfilesRepository.findOneBy({ + email: username.toLowerCase(), + emailVerified: true, + }) as MiUserProfile; + + user = await this.usersRepository.findOneBy({ + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + id: profile?.userId, + }) as MiLocalUser; + } else { + user = await this.usersRepository.findOneBy({ + usernameLower: username.toLowerCase(), + host: IsNull(), + }) as MiLocalUser; + + if (user !== null) { + profile = await this.userProfilesRepository.findOneByOrFail({ + userId: user.id, + }) as MiUserProfile; + } + } - if (user == null) { + if (user == null || profile == null) { logger.error('No such user.'); - return error(404, { - id: '6cc579cc-885d-43d8-95c2-b8c7fc963280', + return error(403, { + id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', }); } if (user.isDeleted && user.isSuspended) { logger.error('No such user. (logical deletion)'); - return error(404, { - id: '6cc579cc-885d-43d8-95c2-b8c7fc963280', + return error(403, { + id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', }); } @@ -148,8 +169,6 @@ export class SigninApiService { }); } - const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - // Compare password const same = await bcrypt.compare(password, profile.password!); diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index a603a7077c74..e09ea09efece 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -381,6 +381,7 @@ import * as ep___users_reportAbuse from './endpoints/users/report-abuse.js'; import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by-username-and-host.js'; import * as ep___users_search from './endpoints/users/search.js'; import * as ep___users_show from './endpoints/users/show.js'; +import * as ep___users_get_twofactor_enable from './endpoints/users/get-twofactor-enable.js'; import * as ep___users_stats from './endpoints/users/stats.js'; import * as ep___users_achievements from './endpoints/users/achievements.js'; import * as ep___users_updateMemo from './endpoints/users/update-memo.js'; @@ -773,6 +774,7 @@ const eps = [ ['users/search-by-username-and-host', ep___users_searchByUsernameAndHost], ['users/search', ep___users_search], ['users/show', ep___users_show], + ['users/get-twofactor-enable', ep___users_get_twofactor_enable], ['users/stats', ep___users_stats], ['users/achievements', ep___users_achievements], ['users/update-memo', ep___users_updateMemo], diff --git a/packages/backend/src/server/api/endpoints/users/get-twofactor-enable.ts b/packages/backend/src/server/api/endpoints/users/get-twofactor-enable.ts new file mode 100644 index 000000000000..dc7ce920cfbe --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/get-twofactor-enable.ts @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + tags: ['users'], + + requireCredential: false, + res: { + type: 'object', + properties: { + twoFactorEnabled: { type: 'boolean' }, + usePasswordLessLogin: { type: 'boolean' }, + securityKeys: { type: 'boolean' }, + }, + }, + errors: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + email: { type: 'string' }, + }, + required: ['email'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.userSecurityKeysRepository) + private userSecurityKeysRepository: UserSecurityKeysRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const userProfile = await this.userProfilesRepository.findOneBy({ + email: ps.email, + }); + + return userProfile ? { + twoFactorEnabled: userProfile.twoFactorEnabled, + usePasswordLessLogin: userProfile.usePasswordLessLogin, + securityKeys: userProfile.twoFactorEnabled + ? await this.userSecurityKeysRepository.countBy({ userId: userProfile.userId }).then(result => result >= 1) + : false, + } : { + twoFactorEnabled: false, + usePasswordLessLogin: false, + securityKeys: false, + }; + }); + } +} + diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts index a34a70df0e10..386c0f2d64eb 100644 --- a/packages/frontend/src/account.ts +++ b/packages/frontend/src/account.ts @@ -5,6 +5,7 @@ import { defineAsyncComponent, reactive, ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { set as gtagSet, time as gtagTime } from 'vue-gtag'; import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js'; import { i18n } from '@/i18n.js'; import { miLocalStorage } from '@/local-storage.js'; @@ -14,7 +15,6 @@ import { apiUrl } from '@/config.js'; import { waiting, popup, popupMenu, success, alert } from '@/os.js'; import { generateClientTransactionId, misskeyApi } from '@/scripts/misskey-api.js'; import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js'; -import { set as gtagSet, time as gtagTime } from 'vue-gtag'; import { instance } from '@/instance.js'; // TODO: 他のタブと永続化されたstateを同期 @@ -340,9 +340,9 @@ export async function openAccountMenu(opts: { } } -export function getAccountWithSigninDialog(): Promise<{ id: string, token: string } | null> { +export function getAccountWithSigninDialog(emailMode = false): Promise<{ id: string, token: string } | null> { return new Promise((resolve) => { - const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, { + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), { emailMode }, { done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => { await addAccount(res.id, res.i); resolve({ id: res.id, token: res.i }); diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index 3cae3ff59232..850437cad196 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -6,24 +6,32 @@ SPDX-License-Identifier: AGPL-3.0-only