diff --git a/locales/index.d.ts b/locales/index.d.ts
index 079a5b330..f337d4b78 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -5159,6 +5159,10 @@ export interface Locale extends ILocale {
* 相互リンク
*/
"mutualLink": string;
+ /**
+ * モデレーター権限を利用するには、まず二要素認証を有効にする必要があります。
+ */
+ "youNeedToEnableTwoFactor": string;
"_bubbleGame": {
/**
* 遊び方
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index fa9da9829..727af73f1 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1285,6 +1285,7 @@ refreshMetadata: "サーバー情報を更新"
removeAllFollowings: "相互フォロー解除"
areYouSureToRemoveAllFollowings: "本当に{host}とのすべてのフォロー関係を削除しますか? 実行後は元に戻せません。 相手インスタンスが閉鎖されたと判断した場合のみ実行してください。"
mutualLink: "相互リンク"
+youNeedToEnableTwoFactor: "モデレーター権限を利用するには、まず二要素認証を有効にする必要があります。"
_bubbleGame:
howToPlay: "遊び方"
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
index 9986bbe2f..445a81c29 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -1270,6 +1270,7 @@ refreshMetadata: "서버 정보를 갱신하기"
removeAllFollowings: "모든 팔로우 관계를 제거하기"
areYouSureToRemoveAllFollowings: "정말로 {host}와의 모든 팔로우 관계를 제거하시겠습니까? 실행한 후에는 되돌릴 수 없습니다. 상대 인스턴스가 폐쇄됐다고 판단되는 경우에만 실행하세요."
mutualLink: "서로링크"
+youNeedToEnableTwoFactor: "관리 권한을 이용하려면 먼저 2단계 인증을 활성화해야 합니다."
_bubbleGame:
howToPlay: "설명"
hold: "홀드"
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index cd15721b0..39596d041 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -17,6 +17,7 @@ import { MetaService } from '@/core/MetaService.js';
import { createTemp } from '@/misc/create-temp.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import type { Config } from '@/config.js';
import { ApiError } from './error.js';
@@ -46,6 +47,7 @@ export class ApiCallService implements OnApplicationShutdown {
private userIpsRepository: UserIpsRepository,
private metaService: MetaService,
+ private userEntityService: UserEntityService,
private authenticateService: AuthenticateService,
private rateLimiterService: RateLimiterService,
private roleService: RoleService,
@@ -218,12 +220,13 @@ export class ApiCallService implements OnApplicationShutdown {
}
try {
- this.userIpsRepository.createQueryBuilder().insert().values({
+ await this.userIpsRepository.createQueryBuilder().insert().values({
createdAt: new Date(),
userId: user.id,
ip: ip,
}).orIgnore(true).execute();
} catch {
+ /* empty */
}
}
}
@@ -278,7 +281,7 @@ export class ApiCallService implements OnApplicationShutdown {
}
}
- if (ep.meta.requireCredential || ep.meta.requireModerator || ep.meta.requireAdmin) {
+ if (ep.meta.requireCredential ?? ep.meta.requireModerator ?? ep.meta.requireAdmin) {
if (user == null) {
throw new ApiError({
message: 'Credential required.',
@@ -307,19 +310,39 @@ export class ApiCallService implements OnApplicationShutdown {
}
}
- if ((ep.meta.requireModerator || ep.meta.requireAdmin) && !user!.isRoot) {
+ if ((ep.meta.requireModerator ?? ep.meta.requireAdmin)) {
const myRoles = await this.roleService.getUserRoles(user!.id);
- if (ep.meta.requireModerator && !myRoles.some(r => r.isModerator || r.isAdministrator)) {
+ const isModerator = myRoles.some(r => r.isModerator || r.isAdministrator);
+ const isAdmin = myRoles.some(r => r.isAdministrator);
+ const userProfile = await this.userEntityService.pack(user!.id, user, { schema: 'MeDetailed' });
+ const isMFAEnabled = userProfile.twoFactorEnabled;
+ if (!isMFAEnabled) {
throw new ApiError({
- message: 'You are not assigned to a moderator role.',
+ message: 'You need to enable 2FA to access this endpoint.',
+ code: 'REQUIRES_MFA_ENABLED',
+ kind: 'permission',
+ id: 'abce13fe-1d9f-4e85-8f00-4a5251155470',
+ });
+ }
+ if (!user!.isRoot) {
+ throw new ApiError({
+ message: 'You are not assigned to a proper role.',
code: 'ROLE_PERMISSION_DENIED',
kind: 'permission',
id: 'd33d5333-db36-423d-a8f9-1a2b9549da41',
});
}
- if (ep.meta.requireAdmin && !myRoles.some(r => r.isAdministrator)) {
+ if (ep.meta.requireModerator && !isModerator) {
throw new ApiError({
- message: 'You are not assigned to an administrator role.',
+ message: 'You are not assigned to a proper role.',
+ code: 'ROLE_PERMISSION_DENIED',
+ kind: 'permission',
+ id: 'd33d5333-db36-423d-a8f9-1a2b9549da41',
+ });
+ }
+ if (ep.meta.requireAdmin && !isAdmin) {
+ throw new ApiError({
+ message: 'You are not assigned to a proper role.',
code: 'ROLE_PERMISSION_DENIED',
kind: 'permission',
id: 'c3d38592-54c0-429d-be96-5636b0431a61',
@@ -332,7 +355,7 @@ export class ApiCallService implements OnApplicationShutdown {
const policies = await this.roleService.getUserPolicies(user!.id);
if (!policies[ep.meta.requireRolePolicy] && !myRoles.some(r => r.isAdministrator)) {
throw new ApiError({
- message: 'You are not assigned to a required role.',
+ message: 'Your role doesn\'t have proper permission.',
code: 'ROLE_PERMISSION_DENIED',
kind: 'permission',
id: '7f86f06f-7e15-4057-8561-f4b6d4ac755a',
diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue
index 57e41caf5..6db9cb957 100644
--- a/packages/frontend/src/pages/settings/2fa.vue
+++ b/packages/frontend/src/pages/settings/2fa.vue
@@ -25,11 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only