enhance(moderation): require 2FA to use moderator perms
This commit is contained in:
parent
b546ab3252
commit
a8d51ffe4c
4
locales/index.d.ts
vendored
4
locales/index.d.ts
vendored
@ -5159,6 +5159,10 @@ export interface Locale extends ILocale {
|
|||||||
* 相互リンク
|
* 相互リンク
|
||||||
*/
|
*/
|
||||||
"mutualLink": string;
|
"mutualLink": string;
|
||||||
|
/**
|
||||||
|
* モデレーター権限を利用するには、まず二要素認証を有効にする必要があります。
|
||||||
|
*/
|
||||||
|
"youNeedToEnableTwoFactor": string;
|
||||||
"_bubbleGame": {
|
"_bubbleGame": {
|
||||||
/**
|
/**
|
||||||
* 遊び方
|
* 遊び方
|
||||||
|
@ -1285,6 +1285,7 @@ refreshMetadata: "サーバー情報を更新"
|
|||||||
removeAllFollowings: "相互フォロー解除"
|
removeAllFollowings: "相互フォロー解除"
|
||||||
areYouSureToRemoveAllFollowings: "本当に{host}とのすべてのフォロー関係を削除しますか? 実行後は元に戻せません。 相手インスタンスが閉鎖されたと判断した場合のみ実行してください。"
|
areYouSureToRemoveAllFollowings: "本当に{host}とのすべてのフォロー関係を削除しますか? 実行後は元に戻せません。 相手インスタンスが閉鎖されたと判断した場合のみ実行してください。"
|
||||||
mutualLink: "相互リンク"
|
mutualLink: "相互リンク"
|
||||||
|
youNeedToEnableTwoFactor: "モデレーター権限を利用するには、まず二要素認証を有効にする必要があります。"
|
||||||
|
|
||||||
_bubbleGame:
|
_bubbleGame:
|
||||||
howToPlay: "遊び方"
|
howToPlay: "遊び方"
|
||||||
|
@ -1270,6 +1270,7 @@ refreshMetadata: "서버 정보를 갱신하기"
|
|||||||
removeAllFollowings: "모든 팔로우 관계를 제거하기"
|
removeAllFollowings: "모든 팔로우 관계를 제거하기"
|
||||||
areYouSureToRemoveAllFollowings: "정말로 {host}와의 모든 팔로우 관계를 제거하시겠습니까? 실행한 후에는 되돌릴 수 없습니다. 상대 인스턴스가 폐쇄됐다고 판단되는 경우에만 실행하세요."
|
areYouSureToRemoveAllFollowings: "정말로 {host}와의 모든 팔로우 관계를 제거하시겠습니까? 실행한 후에는 되돌릴 수 없습니다. 상대 인스턴스가 폐쇄됐다고 판단되는 경우에만 실행하세요."
|
||||||
mutualLink: "서로링크"
|
mutualLink: "서로링크"
|
||||||
|
youNeedToEnableTwoFactor: "관리 권한을 이용하려면 먼저 2단계 인증을 활성화해야 합니다."
|
||||||
_bubbleGame:
|
_bubbleGame:
|
||||||
howToPlay: "설명"
|
howToPlay: "설명"
|
||||||
hold: "홀드"
|
hold: "홀드"
|
||||||
|
@ -17,6 +17,7 @@ import { MetaService } from '@/core/MetaService.js';
|
|||||||
import { createTemp } from '@/misc/create-temp.js';
|
import { createTemp } from '@/misc/create-temp.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { ApiError } from './error.js';
|
import { ApiError } from './error.js';
|
||||||
@ -46,6 +47,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
|||||||
private userIpsRepository: UserIpsRepository,
|
private userIpsRepository: UserIpsRepository,
|
||||||
|
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
|
private userEntityService: UserEntityService,
|
||||||
private authenticateService: AuthenticateService,
|
private authenticateService: AuthenticateService,
|
||||||
private rateLimiterService: RateLimiterService,
|
private rateLimiterService: RateLimiterService,
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
@ -218,12 +220,13 @@ export class ApiCallService implements OnApplicationShutdown {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.userIpsRepository.createQueryBuilder().insert().values({
|
await this.userIpsRepository.createQueryBuilder().insert().values({
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
ip: ip,
|
ip: ip,
|
||||||
}).orIgnore(true).execute();
|
}).orIgnore(true).execute();
|
||||||
} catch {
|
} 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) {
|
if (user == null) {
|
||||||
throw new ApiError({
|
throw new ApiError({
|
||||||
message: 'Credential required.',
|
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);
|
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({
|
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',
|
code: 'ROLE_PERMISSION_DENIED',
|
||||||
kind: 'permission',
|
kind: 'permission',
|
||||||
id: 'd33d5333-db36-423d-a8f9-1a2b9549da41',
|
id: 'd33d5333-db36-423d-a8f9-1a2b9549da41',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (ep.meta.requireAdmin && !myRoles.some(r => r.isAdministrator)) {
|
if (ep.meta.requireModerator && !isModerator) {
|
||||||
throw new ApiError({
|
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',
|
code: 'ROLE_PERMISSION_DENIED',
|
||||||
kind: 'permission',
|
kind: 'permission',
|
||||||
id: 'c3d38592-54c0-429d-be96-5636b0431a61',
|
id: 'c3d38592-54c0-429d-be96-5636b0431a61',
|
||||||
@ -332,7 +355,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
|||||||
const policies = await this.roleService.getUserPolicies(user!.id);
|
const policies = await this.roleService.getUserPolicies(user!.id);
|
||||||
if (!policies[ep.meta.requireRolePolicy] && !myRoles.some(r => r.isAdministrator)) {
|
if (!policies[ep.meta.requireRolePolicy] && !myRoles.some(r => r.isAdministrator)) {
|
||||||
throw new ApiError({
|
throw new ApiError({
|
||||||
message: 'You are not assigned to a required role.',
|
message: 'Your role doesn\'t have proper permission.',
|
||||||
code: 'ROLE_PERMISSION_DENIED',
|
code: 'ROLE_PERMISSION_DENIED',
|
||||||
kind: 'permission',
|
kind: 'permission',
|
||||||
id: '7f86f06f-7e15-4057-8561-f4b6d4ac755a',
|
id: '7f86f06f-7e15-4057-8561-f4b6d4ac755a',
|
||||||
|
@ -25,11 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkInfo>
|
<MkInfo>
|
||||||
<Mfm :text="i18n.tsx._2fa.detailedGuide({ link: `[${i18n.ts.here}](https://go.misskey.io/howto-2fa)`})"/>
|
<Mfm :text="i18n.tsx._2fa.detailedGuide({ link: `[${i18n.ts.here}](https://go.misskey.io/howto-2fa)`})"/>
|
||||||
</MkInfo>
|
</MkInfo>
|
||||||
<MkInfo v-if="$i.securityKeysList.length > 0">{{ i18n.ts._2fa.whyTOTPOnlyRenew }}</MkInfo>
|
<MkInfo v-if="$i.securityKeysList && $i.securityKeysList.length > 0">{{ i18n.ts._2fa.whyTOTPOnlyRenew }}</MkInfo>
|
||||||
|
|
||||||
<div v-if="$i.twoFactorEnabled" class="_gaps_s">
|
<div v-if="$i.twoFactorEnabled" class="_gaps_s">
|
||||||
<div v-text="i18n.ts._2fa.alreadyRegistered"/>
|
<div v-text="i18n.ts._2fa.alreadyRegistered"/>
|
||||||
<MkButton v-if="$i.securityKeysList.length > 0" @click="renewTOTP">{{ i18n.ts._2fa.renewTOTP }}</MkButton>
|
<MkButton v-if="$i.securityKeysList && $i.securityKeysList.length > 0" @click="renewTOTP">{{ i18n.ts._2fa.renewTOTP }}</MkButton>
|
||||||
<MkButton v-else danger @click="unregisterTOTP">{{ i18n.ts.unregister }}</MkButton>
|
<MkButton v-else danger @click="unregisterTOTP">{{ i18n.ts.unregister }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
<MkSwitch :disabled="!$i.twoFactorEnabled || $i.securityKeysList.length === 0" :modelValue="usePasswordLessLogin" @update:modelValue="v => updatePasswordLessLogin(v)">
|
<MkSwitch :disabled="!$i.twoFactorEnabled || ($i.securityKeysList && $i.securityKeysList.length === 0)" :modelValue="usePasswordLessLogin" @update:modelValue="v => updatePasswordLessLogin(v)">
|
||||||
<template #label>{{ i18n.ts.passwordLessLogin }}</template>
|
<template #label>{{ i18n.ts.passwordLessLogin }}</template>
|
||||||
<template #caption>{{ i18n.ts.passwordLessLoginDescription }}</template>
|
<template #caption>{{ i18n.ts.passwordLessLoginDescription }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
|
@ -17,14 +17,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</span>
|
</span>
|
||||||
<span :class="$style.title">{{ i18n.ts.verificationEmailSent }}</span>
|
<span :class="$style.title">{{ i18n.ts.verificationEmailSent }}</span>
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkA v-if="unresolvedReportCount > 0" :class="$style.item" to="/admin/">
|
<MkA v-if="unresolvedReportCount > 0" :class="$style.item" to="/admin/abuses">
|
||||||
<span :class="$style.icon">
|
<span :class="$style.icon">
|
||||||
<i class="ti ti-circle-x" style="color: var(--error);"></i>
|
<i class="ti ti-alert-triangle" style="color: var(--warn);"></i>
|
||||||
</span>
|
</span>
|
||||||
<span :class="$style.title">{{ i18n.tsx.thereIsUnresolvedAbuseReport({ left: unresolvedReportCount }) }}</span>
|
<span :class="$style.title">{{ i18n.tsx.thereIsUnresolvedAbuseReport({ left: unresolvedReportCount }) }}</span>
|
||||||
</MkA>
|
</MkA>
|
||||||
|
<MkA v-if="($i?.isModerator ?? $i?.isAdmin) && !$i?.twoFactorEnabled" :class="$style.item" to="/settings/security">
|
||||||
|
<span :class="$style.icon">
|
||||||
|
<i class="ti ti-circle-key" style="color: var(--error);"></i>
|
||||||
|
</span>
|
||||||
|
<span :class="$style.title">{{ i18n.ts.youNeedToEnableTwoFactor }}</span>
|
||||||
|
</MkA>
|
||||||
<MkA
|
<MkA
|
||||||
v-for="announcement in $i.unreadAnnouncements.filter(x => x.display === 'banner')"
|
v-for="announcement in $i?.unreadAnnouncements.filter(x => x.display === 'banner')"
|
||||||
:key="announcement.id"
|
:key="announcement.id"
|
||||||
:class="$style.item"
|
:class="$style.item"
|
||||||
to="/announcements"
|
to="/announcements"
|
||||||
|
Loading…
Reference in New Issue
Block a user