summaryrefslogtreecommitdiff
path: root/packages/backend/src/server/api
diff options
context:
space:
mode:
authordakkar <dakkar@thenautilus.net>2024-11-08 15:52:37 +0000
committerdakkar <dakkar@thenautilus.net>2024-11-08 15:52:37 +0000
commitf079edaf3ccc1fea9242f0f8522ebbfc7e8242e4 (patch)
treeead184cf29c147bc74ed92ce905b46e5e42209c1 /packages/backend/src/server/api
parentmerge: Bump version number (!735) (diff)
parentRelease: 2024.10.1 (diff)
downloadsharkey-f079edaf3ccc1fea9242f0f8522ebbfc7e8242e4.tar.gz
sharkey-f079edaf3ccc1fea9242f0f8522ebbfc7e8242e4.tar.bz2
sharkey-f079edaf3ccc1fea9242f0f8522ebbfc7e8242e4.zip
Merge tag '2024.10.1' into feature/2024.10
Diffstat (limited to 'packages/backend/src/server/api')
-rw-r--r--packages/backend/src/server/api/ApiServerService.ts19
-rw-r--r--packages/backend/src/server/api/EndpointsModule.ts8
-rw-r--r--packages/backend/src/server/api/SigninApiService.ts104
-rw-r--r--packages/backend/src/server/api/SigninService.ts26
-rw-r--r--packages/backend/src/server/api/SignupApiService.ts7
-rw-r--r--packages/backend/src/server/api/endpoints.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts16
-rw-r--r--packages/backend/src/server/api/endpoints/admin/accounts/create.ts93
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/update.ts33
-rw-r--r--packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts55
-rw-r--r--packages/backend/src/server/api/endpoints/admin/meta.ts18
-rw-r--r--packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/show-users.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts58
-rw-r--r--packages/backend/src/server/api/endpoints/admin/update-meta.ts20
-rw-r--r--packages/backend/src/server/api/endpoints/flash/featured.ts22
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts22
17 files changed, 425 insertions, 88 deletions
diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts
index ac3b982742..0d77309537 100644
--- a/packages/backend/src/server/api/ApiServerService.ts
+++ b/packages/backend/src/server/api/ApiServerService.ts
@@ -119,25 +119,30 @@ export class ApiServerService {
'g-recaptcha-response'?: string;
'turnstile-response'?: string;
'frc-captcha-solution'?: string;
+ 'm-captcha-response'?: string;
+ 'testcaptcha-response'?: string;
}
}>('/signup', (request, reply) => this.signupApiService.signup(request, reply));
fastify.post<{
Body: {
username: string;
- password: string;
+ password?: string;
token?: string;
- signature?: string;
- authenticatorData?: string;
- clientDataJSON?: string;
- credentialId?: string;
- challengeId?: string;
+ credential?: AuthenticationResponseJSON;
+ 'hcaptcha-response'?: string;
+ 'g-recaptcha-response'?: string;
+ 'turnstile-response'?: string;
+ 'frc-captcha-solution'?: string;
+ 'm-captcha-response'?: string;
+ 'testcaptcha-response'?: string;
};
- }>('/signin', (request, reply) => this.signinApiService.signin(request, reply));
+ }>('/signin-flow', (request, reply) => this.signinApiService.signin(request, reply));
fastify.post<{
Body: {
credential?: AuthenticationResponseJSON;
+ context?: string;
};
}>('/signin-with-passkey', (request, reply) => this.signinWithPasskeyApiService.signin(request, reply));
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index 5bdd7cf650..9a387e3f11 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -68,6 +68,8 @@ import * as ep___admin_relays_list from './endpoints/admin/relays/list.js';
import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js';
import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js';
import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js';
+import * as ep___admin_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js';
+import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-abuse-user-report.js';
import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
@@ -470,6 +472,8 @@ const $admin_relays_list: Provider = { provide: 'ep:admin/relays/list', useClass
const $admin_relays_remove: Provider = { provide: 'ep:admin/relays/remove', useClass: ep___admin_relays_remove.default };
const $admin_resetPassword: Provider = { provide: 'ep:admin/reset-password', useClass: ep___admin_resetPassword.default };
const $admin_resolveAbuseUserReport: Provider = { provide: 'ep:admin/resolve-abuse-user-report', useClass: ep___admin_resolveAbuseUserReport.default };
+const $admin_forwardAbuseUserReport: Provider = { provide: 'ep:admin/forward-abuse-user-report', useClass: ep___admin_forwardAbuseUserReport.default };
+const $admin_updateAbuseUserReport: Provider = { provide: 'ep:admin/update-abuse-user-report', useClass: ep___admin_updateAbuseUserReport.default };
const $admin_sendEmail: Provider = { provide: 'ep:admin/send-email', useClass: ep___admin_sendEmail.default };
const $admin_serverInfo: Provider = { provide: 'ep:admin/server-info', useClass: ep___admin_serverInfo.default };
const $admin_showModerationLogs: Provider = { provide: 'ep:admin/show-moderation-logs', useClass: ep___admin_showModerationLogs.default };
@@ -876,6 +880,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_relays_remove,
$admin_resetPassword,
$admin_resolveAbuseUserReport,
+ $admin_forwardAbuseUserReport,
+ $admin_updateAbuseUserReport,
$admin_sendEmail,
$admin_serverInfo,
$admin_showModerationLogs,
@@ -1276,6 +1282,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_relays_remove,
$admin_resetPassword,
$admin_resolveAbuseUserReport,
+ $admin_forwardAbuseUserReport,
+ $admin_updateAbuseUserReport,
$admin_sendEmail,
$admin_serverInfo,
$admin_showModerationLogs,
diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts
index 64af7da7a6..1e7a2e80ef 100644
--- a/packages/backend/src/server/api/SigninApiService.ts
+++ b/packages/backend/src/server/api/SigninApiService.ts
@@ -6,12 +6,14 @@
import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
-import * as OTPAuth from 'otpauth';
import { IsNull } from 'typeorm';
+import * as Misskey from 'misskey-js';
import { DI } from '@/di-symbols.js';
import type {
+ MiMeta,
SigninsRepository,
UserProfilesRepository,
+ UserSecurityKeysRepository,
UsersRepository,
} from '@/models/_.js';
import type { Config } from '@/config.js';
@@ -21,6 +23,8 @@ import { IdService } from '@/core/IdService.js';
import { bindThis } from '@/decorators.js';
import { WebAuthnService } from '@/core/WebAuthnService.js';
import { UserAuthService } from '@/core/UserAuthService.js';
+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';
@@ -43,6 +47,9 @@ export class SigninApiService {
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
+ @Inject(DI.userSecurityKeysRepository)
+ private userSecurityKeysRepository: UserSecurityKeysRepository,
+
@Inject(DI.signinsRepository)
private signinsRepository: SigninsRepository,
@@ -51,6 +58,7 @@ export class SigninApiService {
private signinService: SigninService,
private userAuthService: UserAuthService,
private webAuthnService: WebAuthnService,
+ private captchaService: CaptchaService,
) {
}
@@ -59,9 +67,15 @@ export class SigninApiService {
request: FastifyRequest<{
Body: {
username: string;
- password: string;
+ password?: string;
token?: string;
credential?: AuthenticationResponseJSON;
+ 'hcaptcha-response'?: string;
+ 'g-recaptcha-response'?: string;
+ 'turnstile-response'?: string;
+ 'frc-captcha-solution'?: string;
+ 'm-captcha-response'?: string;
+ 'testcaptcha-response'?: string;
};
}>,
reply: FastifyReply,
@@ -98,11 +112,6 @@ export class SigninApiService {
return;
}
- if (typeof password !== 'string') {
- reply.code(400);
- return;
- }
-
if (token != null && typeof token !== 'string') {
reply.code(400);
return;
@@ -133,6 +142,27 @@ 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(200);
+ if (profile.twoFactorEnabled) {
+ return {
+ finished: false,
+ next: 'password',
+ } satisfies Misskey.entities.SigninFlowResponse;
+ } else {
+ return {
+ finished: false,
+ next: 'captcha',
+ } satisfies Misskey.entities.SigninFlowResponse;
+ }
+ }
+
+ if (typeof password !== 'string') {
+ reply.code(400);
+ return;
+ }
if (!user.approved && this.meta.approvalRequiredForSignup) {
reply.code(403);
@@ -148,7 +178,7 @@ export class SigninApiService {
// Compare password
const same = await argon2.verify(profile.password!, password) || bcrypt.compareSync(password, profile.password!);
- const fail = async (status?: number, failure?: { id: string }) => {
+ const fail = async (status?: number, failure?: { id: string; }) => {
// Append signin history
await this.signinsRepository.insert({
id: this.idService.gen(),
@@ -162,6 +192,44 @@ export class SigninApiService {
};
if (!profile.twoFactorEnabled) {
+ if (process.env.NODE_ENV !== 'test') {
+ if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) {
+ await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
+ throw new FastifyReplyError(400, err);
+ });
+ }
+
+ if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) {
+ await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
+ throw new FastifyReplyError(400, err);
+ });
+ }
+
+ if (this.meta.enableFC && this.meta.fcSecretKey) {
+ await this.captchaService.verifyFriendlyCaptcha(this.meta.fcSecretKey, body['frc-captcha-solution']).catch(err => {
+ throw new FastifyReplyError(400, err);
+ });
+ }
+
+ if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) {
+ await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
+ throw new FastifyReplyError(400, err);
+ });
+ }
+
+ if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) {
+ await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => {
+ throw new FastifyReplyError(400, err);
+ });
+ }
+
+ if (this.meta.enableTestcaptcha) {
+ await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => {
+ throw new FastifyReplyError(400, err);
+ });
+ }
+ }
+
if (same) {
if (profile.password!.startsWith('$2')) {
const newHash = await argon2.hash(password);
@@ -220,7 +288,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',
@@ -230,7 +298,23 @@ export class SigninApiService {
const authRequest = await this.webAuthnService.initiateAuthentication(user.id);
reply.code(200);
- return authRequest;
+ return {
+ finished: false,
+ next: 'passkey',
+ authRequest,
+ } satisfies Misskey.entities.SigninFlowResponse;
+ } else {
+ if (!same || !profile.twoFactorEnabled) {
+ return await fail(403, {
+ id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
+ });
+ } else {
+ reply.code(200);
+ return {
+ finished: false,
+ next: 'totp',
+ } satisfies Misskey.entities.SigninFlowResponse;
+ }
}
// never get here
}
diff --git a/packages/backend/src/server/api/SigninService.ts b/packages/backend/src/server/api/SigninService.ts
index 70306c3113..640356b50c 100644
--- a/packages/backend/src/server/api/SigninService.ts
+++ b/packages/backend/src/server/api/SigninService.ts
@@ -4,13 +4,16 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import * as Misskey from 'misskey-js';
import { DI } from '@/di-symbols.js';
-import type { SigninsRepository } from '@/models/_.js';
+import type { SigninsRepository, UserProfilesRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
import type { MiLocalUser } from '@/models/User.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { SigninEntityService } from '@/core/entities/SigninEntityService.js';
import { bindThis } from '@/decorators.js';
+import { EmailService } from '@/core/EmailService.js';
+import { NotificationService } from '@/core/NotificationService.js';
import type { FastifyRequest, FastifyReply } from 'fastify';
@Injectable()
@@ -19,7 +22,12 @@ export class SigninService {
@Inject(DI.signinsRepository)
private signinsRepository: SigninsRepository,
+ @Inject(DI.userProfilesRepository)
+ private userProfilesRepository: UserProfilesRepository,
+
private signinEntityService: SigninEntityService,
+ private emailService: EmailService,
+ private notificationService: NotificationService,
private idService: IdService,
private globalEventService: GlobalEventService,
) {
@@ -28,7 +36,8 @@ export class SigninService {
@bindThis
public signin(request: FastifyRequest, reply: FastifyReply, user: MiLocalUser) {
setImmediate(async () => {
- // Append signin history
+ this.notificationService.createNotification(user.id, 'login', {});
+
const record = await this.signinsRepository.insertOne({
id: this.idService.gen(),
userId: user.id,
@@ -37,15 +46,22 @@ export class SigninService {
success: true,
});
- // Publish signin event
this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record));
+
+ const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
+ if (profile.email && profile.emailVerified) {
+ this.emailService.sendEmail(profile.email, 'New login / ログインがありました',
+ 'There is a new login. If you do not recognize this login, update the security status of your account, including changing your password. / 新しいログインがありました。このログインに心当たりがない場合は、パスワードを変更するなど、アカウントのセキュリティ状態を更新してください。',
+ 'There is a new login. If you do not recognize this login, update the security status of your account, including changing your password. / 新しいログインがありました。このログインに心当たりがない場合は、パスワードを変更するなど、アカウントのセキュリティ状態を更新してください。');
+ }
});
reply.code(200);
return {
+ finished: true,
id: user.id,
- i: user.token,
- };
+ i: user.token!,
+ } satisfies Misskey.entities.SigninFlowResponse;
}
}
diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts
index db860d710a..7aea6a0e56 100644
--- a/packages/backend/src/server/api/SignupApiService.ts
+++ b/packages/backend/src/server/api/SignupApiService.ts
@@ -73,6 +73,7 @@ export class SignupApiService {
'turnstile-response'?: string;
'm-captcha-response'?: string;
'frc-captcha-solution'?: string;
+ 'testcaptcha-response'?: string;
}
}>,
reply: FastifyReply,
@@ -111,6 +112,12 @@ export class SignupApiService {
throw new FastifyReplyError(400, err);
});
}
+
+ if (this.meta.enableTestcaptcha) {
+ await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => {
+ throw new FastifyReplyError(400, err);
+ });
+ }
}
const username = body['username'];
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 14e002929a..3dc287331c 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -74,6 +74,8 @@ import * as ep___admin_relays_list from './endpoints/admin/relays/list.js';
import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js';
import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js';
import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js';
+import * as ep___admin_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js';
+import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-abuse-user-report.js';
import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
@@ -474,6 +476,8 @@ const eps = [
['admin/relays/remove', ep___admin_relays_remove],
['admin/reset-password', ep___admin_resetPassword],
['admin/resolve-abuse-user-report', ep___admin_resolveAbuseUserReport],
+ ['admin/forward-abuse-user-report', ep___admin_forwardAbuseUserReport],
+ ['admin/update-abuse-user-report', ep___admin_updateAbuseUserReport],
['admin/send-email', ep___admin_sendEmail],
['admin/server-info', ep___admin_serverInfo],
['admin/show-moderation-logs', ep___admin_showModerationLogs],
diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
index cf3f257ca6..0dbfaae054 100644
--- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
+++ b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
@@ -71,9 +71,22 @@ export const meta = {
},
assignee: {
type: 'object',
- nullable: true, optional: true,
+ nullable: true, optional: false,
ref: 'UserDetailedNotMe',
},
+ forwarded: {
+ type: 'boolean',
+ nullable: false, optional: false,
+ },
+ resolvedAs: {
+ type: 'string',
+ nullable: true, optional: false,
+ enum: ['accept', 'reject', null],
+ },
+ moderationNote: {
+ type: 'string',
+ nullable: false, optional: false,
+ },
},
},
},
@@ -88,7 +101,6 @@ export const paramDef = {
state: { type: 'string', nullable: true, default: null },
reporterOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
targetUserOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
- forwarded: { type: 'boolean', default: false },
},
required: [],
} as const;
diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
index 7754899b95..d5d2e909a2 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
@@ -10,6 +10,9 @@ import { SignupService } from '@/core/SignupService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import { localUsernameSchema, passwordSchema } from '@/models/User.js';
+import { DI } from '@/di-symbols.js';
+import type { Config } from '@/config.js';
+import { ApiError } from '@/server/api/error.js';
import { Packed } from '@/misc/json-schema.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '@/server/api/error.js';
@@ -17,19 +20,19 @@ import { ApiError } from '@/server/api/error.js';
export const meta = {
tags: ['admin'],
- res: {
- type: 'object',
- optional: false, nullable: false,
- ref: 'MeDetailed',
- properties: {
- token: {
- type: 'string',
- optional: false, nullable: false,
- },
+ errors: {
+ accessDenied: {
+ message: 'Access denied.',
+ code: 'ACCESS_DENIED',
+ id: '1fb7cb09-d46a-4fff-b8df-057708cce513',
+ },
+
+ wrongInitialPassword: {
+ message: 'Initial password is incorrect.',
+ code: 'INCORRECT_INITIAL_PASSWORD',
+ id: '97147c55-1ae1-4f6f-91d6-e1c3e0e76d62',
},
- },
- errors: {
// From ApiCallService.ts
noCredential: {
message: 'Credential required.',
@@ -51,6 +54,18 @@ export const meta = {
},
},
+ res: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'MeDetailed',
+ properties: {
+ token: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ },
+ },
+
// Required token permissions, but we need to check them manually.
// ApiCallService checks access in a way that would prevent creating the first account.
softPermissions: [
@@ -64,6 +79,7 @@ export const paramDef = {
properties: {
username: localUsernameSchema,
password: passwordSchema,
+ setupPassword: { type: 'string', nullable: true },
},
required: ['username', 'password'],
} as const;
@@ -71,13 +87,49 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.config)
+ private config: Config,
+
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
+
private roleService: RoleService,
private userEntityService: UserEntityService,
private signupService: SignupService,
private instanceActorService: InstanceActorService,
) {
super(meta, paramDef, async (ps, _me, token) => {
- await this.ensurePermissions(_me, token);
+ const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null;
+ const realUsers = await this.instanceActorService.realLocalUsersPresent();
+
+ if (!realUsers && me == null && token == null) {
+ // 初回セットアップの場合
+ if (this.config.setupPassword != null) {
+ // 初期パスワードが設定されている場合
+ if (ps.setupPassword !== this.config.setupPassword) {
+ // 初期パスワードが違う場合
+ throw new ApiError(meta.errors.wrongInitialPassword);
+ }
+ } else if (ps.setupPassword != null && ps.setupPassword.trim() !== '') {
+ // 初期パスワードが設定されていないのに初期パスワードが入力された場合
+ throw new ApiError(meta.errors.wrongInitialPassword);
+ }
+ } else {
+ if (token && !meta.softPermissions.every(p => token.permission.includes(p))) {
+ // Tokens have scoped permissions which may be *less* than the user's official role, so we need to check.
+ throw new ApiError(meta.errors.noPermission);
+ }
+
+ if (me && !await this.roleService.isAdministrator(me)) {
+ // Only administrators (including root) can create users.
+ throw new ApiError(meta.errors.noAdmin);
+ }
+
+ // Anonymous access is only allowed for initial instance setup (this check may be redundant)
+ if (!me && realUsers) {
+ throw new ApiError(meta.errors.noCredential);
+ }
+ }
const { account, secret } = await this.signupService.signup({
username: ps.username,
@@ -96,21 +148,4 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return res;
});
}
-
- private async ensurePermissions(me: MiUser | null, token: MiAccessToken | null): Promise<void> {
- // Tokens have scoped permissions which may be *less* than the user's official role, so we need to check.
- if (token && !meta.softPermissions.every(p => token.permission.includes(p))) {
- throw new ApiError(meta.errors.noPermission);
- }
-
- // Only administrators (including root) can create users.
- if (me && !await this.roleService.isAdministrator(me)) {
- throw new ApiError(meta.errors.noAdmin);
- }
-
- // Anonymous access is only allowed for initial instance setup.
- if (!me && await this.instanceActorService.realLocalUsersPresent()) {
- throw new ApiError(meta.errors.noCredential);
- }
- }
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
index 3caa0f84a3..071ddbef18 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
@@ -6,7 +6,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
-import type { DriveFilesRepository } from '@/models/_.js';
+import type { DriveFilesRepository, MiEmoji } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
@@ -79,25 +79,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
}
- let emojiId;
- if (ps.id) {
- emojiId = ps.id;
- const emoji = await this.customEmojiService.getEmojiById(ps.id);
- if (!emoji) throw new ApiError(meta.errors.noSuchEmoji);
- if (nameNfc && (nameNfc !== emoji.name)) {
- const isDuplicate = await this.customEmojiService.checkDuplicate(nameNfc);
- if (isDuplicate) throw new ApiError(meta.errors.sameNameEmojiExists);
- }
- } else {
- if (!nameNfc) throw new Error('Invalid Params unexpectedly passed. This is a BUG. Please report it to the development team.');
- const emoji = await this.customEmojiService.getEmojiByName(nameNfc);
- if (!emoji) throw new ApiError(meta.errors.noSuchEmoji);
- emojiId = emoji.id;
- }
+ // JSON schemeのanyOfの型変換がうまくいっていないらしい
+ const required = { id: ps.id, name: nameNfc } as
+ | { id: MiEmoji['id']; name?: string }
+ | { id?: MiEmoji['id']; name: string };
- await this.customEmojiService.update(emojiId, {
+ const error = await this.customEmojiService.update({
+ ...required,
driveFile,
- name: nameNfc,
category: ps.category?.normalize('NFC'),
aliases: ps.aliases?.map(a => a.normalize('NFC')),
license: ps.license,
@@ -105,6 +94,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
localOnly: ps.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction,
}, me);
+
+ switch (error) {
+ case null: return;
+ case 'NO_SUCH_EMOJI': throw new ApiError(meta.errors.noSuchEmoji);
+ case 'SAME_NAME_EMOJI_EXISTS': throw new ApiError(meta.errors.sameNameEmojiExists);
+ }
+ // 網羅性チェック
+ const mustBeNever: never = error;
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts
new file mode 100644
index 0000000000..3e42c91fed
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts
@@ -0,0 +1,55 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { AbuseUserReportsRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'write:admin:resolve-abuse-user-report',
+
+ errors: {
+ noSuchAbuseReport: {
+ message: 'No such abuse report.',
+ code: 'NO_SUCH_ABUSE_REPORT',
+ id: '8763e21b-d9bc-40be-acf6-54c1a6986493',
+ kind: 'server',
+ httpStatusCode: 404,
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ reportId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['reportId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.abuseUserReportsRepository)
+ private abuseUserReportsRepository: AbuseUserReportsRepository,
+ private abuseReportService: AbuseReportService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
+ if (!report) {
+ throw new ApiError(meta.errors.noSuchAbuseReport);
+ }
+
+ await this.abuseReportService.forward(report.id, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index 6e368eff43..6495e3b7da 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -81,6 +81,10 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
+ enableTestcaptcha: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
swPublickey: {
type: 'string',
optional: false, nullable: true,
@@ -189,6 +193,13 @@ export const meta = {
type: 'string',
},
},
+ prohibitedWordsForNameOfUser: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'string',
+ },
+ },
bannedEmailDomains: {
type: 'array',
optional: true, nullable: false,
@@ -368,6 +379,10 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
+ enableStatsForFederatedInstances: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
enableServerMachineStats: {
type: 'boolean',
optional: false, nullable: false,
@@ -614,6 +629,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
turnstileSiteKey: instance.turnstileSiteKey,
enableFC: instance.enableFC,
fcSiteKey: instance.fcSiteKey,
+ enableTestcaptcha: instance.enableTestcaptcha,
swPublickey: instance.swPublicKey,
themeColor: instance.themeColor,
mascotImageUrl: instance.mascotImageUrl,
@@ -642,6 +658,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
mediaSilencedHosts: instance.mediaSilencedHosts,
sensitiveWords: instance.sensitiveWords,
prohibitedWords: instance.prohibitedWords,
+ prohibitedWordsForNameOfUser: instance.prohibitedWordsForNameOfUser,
preservedUsernames: instance.preservedUsernames,
bubbleInstances: instance.bubbleInstances,
hcaptchaSecretKey: instance.hcaptchaSecretKey,
@@ -688,6 +705,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
truemailAuthKey: instance.truemailAuthKey,
enableChartsForRemoteUser: instance.enableChartsForRemoteUser,
enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances,
+ enableStatsForFederatedInstances: instance.enableStatsForFederatedInstances,
enableServerMachineStats: instance.enableServerMachineStats,
enableAchievements: instance.enableAchievements,
enableIdenticonGeneration: instance.enableIdenticonGeneration,
diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
index 9b79100fcf..554d324ff2 100644
--- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
+++ b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
@@ -32,7 +32,7 @@ export const paramDef = {
type: 'object',
properties: {
reportId: { type: 'string', format: 'misskey:id' },
- forward: { type: 'boolean', default: false },
+ resolvedAs: { type: 'string', enum: ['accept', 'reject', null], nullable: true },
},
required: ['reportId'],
} as const;
@@ -50,7 +50,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchAbuseReport);
}
- await this.abuseReportService.resolve([{ reportId: report.id, forward: ps.forward }], me);
+ await this.abuseReportService.resolve([{ reportId: report.id, resolvedAs: ps.resolvedAs ?? null }], me);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts
index 5f16519403..cc65ed2cf0 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-users.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts
@@ -72,13 +72,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
break;
}
case 'moderator': {
- const moderatorIds = await this.roleService.getModeratorIds(false);
+ const moderatorIds = await this.roleService.getModeratorIds({ includeAdmins: false });
if (moderatorIds.length === 0) return [];
query.where('user.id IN (:...moderatorIds)', { moderatorIds: moderatorIds });
break;
}
case 'adminOrModerator': {
- const adminOrModeratorIds = await this.roleService.getModeratorIds();
+ const adminOrModeratorIds = await this.roleService.getModeratorIds({ includeAdmins: true });
if (adminOrModeratorIds.length === 0) return [];
query.where('user.id IN (:...adminOrModeratorIds)', { adminOrModeratorIds: adminOrModeratorIds });
break;
diff --git a/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts
new file mode 100644
index 0000000000..73d4b843f0
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts
@@ -0,0 +1,58 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { AbuseUserReportsRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'write:admin:resolve-abuse-user-report',
+
+ errors: {
+ noSuchAbuseReport: {
+ message: 'No such abuse report.',
+ code: 'NO_SUCH_ABUSE_REPORT',
+ id: '15f51cf5-46d1-4b1d-a618-b35bcbed0662',
+ kind: 'server',
+ httpStatusCode: 404,
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ reportId: { type: 'string', format: 'misskey:id' },
+ moderationNote: { type: 'string' },
+ },
+ required: ['reportId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.abuseUserReportsRepository)
+ private abuseUserReportsRepository: AbuseUserReportsRepository,
+ private abuseReportService: AbuseReportService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
+ if (!report) {
+ throw new ApiError(meta.errors.noSuchAbuseReport);
+ }
+
+ await this.abuseReportService.update(report.id, {
+ moderationNote: ps.moderationNote,
+ }, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index 98760bbcc3..72f428d85f 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -46,6 +46,11 @@ export const paramDef = {
type: 'string',
},
},
+ prohibitedWordsForNameOfUser: {
+ type: 'array', nullable: true, items: {
+ type: 'string',
+ },
+ },
themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
mascotImageUrl: { type: 'string', nullable: true },
bannerUrl: { type: 'string', nullable: true },
@@ -84,6 +89,7 @@ export const paramDef = {
enableFC: { type: 'boolean' },
fcSiteKey: { type: 'string', nullable: true },
fcSecretKey: { type: 'string', nullable: true },
+ enableTestcaptcha: { type: 'boolean' },
sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] },
sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] },
setSensitiveFlagAutomatically: { type: 'boolean' },
@@ -140,6 +146,7 @@ export const paramDef = {
truemailAuthKey: { type: 'string', nullable: true },
enableChartsForRemoteUser: { type: 'boolean' },
enableChartsForFederatedInstances: { type: 'boolean' },
+ enableStatsForFederatedInstances: { type: 'boolean' },
enableServerMachineStats: { type: 'boolean' },
enableAchievements: { type: 'boolean' },
enableIdenticonGeneration: { type: 'boolean' },
@@ -230,6 +237,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (Array.isArray(ps.prohibitedWords)) {
set.prohibitedWords = ps.prohibitedWords.filter(Boolean);
}
+ if (Array.isArray(ps.prohibitedWordsForNameOfUser)) {
+ set.prohibitedWordsForNameOfUser = ps.prohibitedWordsForNameOfUser.filter(Boolean);
+ }
if (Array.isArray(ps.silencedHosts)) {
let lastValue = '';
set.silencedHosts = ps.silencedHosts.sort().filter((h) => {
@@ -390,6 +400,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.enableFC = ps.enableFC;
}
+ if (ps.enableTestcaptcha !== undefined) {
+ set.enableTestcaptcha = ps.enableTestcaptcha;
+ }
+
if (ps.fcSiteKey !== undefined) {
set.fcSiteKey = ps.fcSiteKey;
}
@@ -610,6 +624,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.enableChartsForFederatedInstances = ps.enableChartsForFederatedInstances;
}
+ if (ps.enableStatsForFederatedInstances !== undefined) {
+ set.enableStatsForFederatedInstances = ps.enableStatsForFederatedInstances;
+ }
+
if (ps.enableServerMachineStats !== undefined) {
set.enableServerMachineStats = ps.enableServerMachineStats;
}
@@ -709,7 +727,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
if (Array.isArray(ps.federationHosts)) {
- set.blockedHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase());
+ set.federationHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase());
}
const before = await this.metaService.fetch(true);
diff --git a/packages/backend/src/server/api/endpoints/flash/featured.ts b/packages/backend/src/server/api/endpoints/flash/featured.ts
index c2d6ab5085..9a0cb461f2 100644
--- a/packages/backend/src/server/api/endpoints/flash/featured.ts
+++ b/packages/backend/src/server/api/endpoints/flash/featured.ts
@@ -8,6 +8,7 @@ import type { FlashsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
import { DI } from '@/di-symbols.js';
+import { FlashService } from '@/core/FlashService.js';
export const meta = {
tags: ['flash'],
@@ -27,26 +28,25 @@ export const meta = {
export const paramDef = {
type: 'object',
- properties: {},
+ properties: {
+ offset: { type: 'integer', minimum: 0, default: 0 },
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ },
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
- @Inject(DI.flashsRepository)
- private flashsRepository: FlashsRepository,
-
+ private flashService: FlashService,
private flashEntityService: FlashEntityService,
) {
super(meta, paramDef, async (ps, me) => {
- const query = this.flashsRepository.createQueryBuilder('flash')
- .andWhere('flash.likedCount > 0')
- .orderBy('flash.likedCount', 'DESC');
-
- const flashs = await query.limit(10).getMany();
-
- return await this.flashEntityService.packMany(flashs, me);
+ const result = await this.flashService.featured({
+ offset: ps.offset,
+ limit: ps.limit,
+ });
+ return await this.flashEntityService.packMany(result, me);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 8994c3fff6..a504441df3 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -11,7 +11,7 @@ import { JSDOM } from 'jsdom';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
import { extractHashtags } from '@/misc/extract-hashtags.js';
import * as Acct from '@/misc/acct.js';
-import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js';
+import type { UsersRepository, DriveFilesRepository, MiMeta, UserProfilesRepository, PagesRepository } from '@/models/_.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
import { birthdaySchema, listenbrainzSchema, descriptionSchema, followedMessageSchema, locationSchema, nameSchema } from '@/models/User.js';
import type { MiUserProfile } from '@/models/UserProfile.js';
@@ -22,6 +22,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
import { AccountUpdateService } from '@/core/AccountUpdateService.js';
+import { UtilityService } from '@/core/UtilityService.js';
import { HashtagService } from '@/core/HashtagService.js';
import { DI } from '@/di-symbols.js';
import { RolePolicies, RoleService } from '@/core/RoleService.js';
@@ -126,6 +127,13 @@ export const meta = {
code: 'RESTRICTED_BY_ROLE',
id: '8feff0ba-5ab5-585b-31f4-4df816663fad',
},
+
+ nameContainsProhibitedWords: {
+ message: 'Your new name contains prohibited words.',
+ code: 'YOUR_NAME_CONTAINS_PROHIBITED_WORDS',
+ id: '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191',
+ httpStatusCode: 422,
+ },
},
res: {
@@ -241,6 +249,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private instanceMeta: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -265,6 +276,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private cacheService: CacheService,
private httpRequestService: HttpRequestService,
private avatarDecorationService: AvatarDecorationService,
+ private utilityService: UtilityService,
) {
super(meta, paramDef, async (ps, _user, token) => {
const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser;
@@ -485,6 +497,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields;
if (newName != null) {
+ let hasProhibitedWords = false;
+ if (!await this.roleService.isModerator(user)) {
+ hasProhibitedWords = this.utilityService.isKeyWordIncluded(newName, this.instanceMeta.prohibitedWordsForNameOfUser);
+ }
+ if (hasProhibitedWords) {
+ throw new ApiError(meta.errors.nameContainsProhibitedWords);
+ }
+
const tokens = mfm.parseSimple(newName);
emojis = emojis.concat(extractCustomEmojisFromMfm(tokens));
}