summaryrefslogtreecommitdiff
path: root/packages/backend/src/server
diff options
context:
space:
mode:
authorKagami Sascha Rosylight <saschanaz@outlook.com>2023-02-25 20:04:48 +0100
committerGitHub <noreply@github.com>2023-02-25 20:04:48 +0100
commitb468330ed944cd2aefb93183786855e990bd3df3 (patch)
treeaae515a3d90bc6646854ea718c054540b2b654e9 /packages/backend/src/server
parentAdd test (diff)
parentrefactor(frontend): fix eslint error (#10084) (diff)
downloadsharkey-b468330ed944cd2aefb93183786855e990bd3df3.tar.gz
sharkey-b468330ed944cd2aefb93183786855e990bd3df3.tar.bz2
sharkey-b468330ed944cd2aefb93183786855e990bd3df3.zip
Merge branch 'develop' into mkusername-empty
Diffstat (limited to 'packages/backend/src/server')
-rw-r--r--packages/backend/src/server/api/ApiCallService.ts4
-rw-r--r--packages/backend/src/server/api/EndpointsModule.ts20
-rw-r--r--packages/backend/src/server/api/GetterService.ts4
-rw-r--r--packages/backend/src/server/api/SigninApiService.ts18
-rw-r--r--packages/backend/src/server/api/endpoint-base.ts4
-rw-r--r--packages/backend/src/server/api/endpoints.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/admin/drive/show-file.ts16
-rw-r--r--packages/backend/src/server/api/endpoints/admin/roles/list.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/roles/show.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/roles/users.ts71
-rw-r--r--packages/backend/src/server/api/endpoints/admin/show-user.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/done.ts28
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/key-done.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/password-less.ts47
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/register.ts17
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts18
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/unregister.ts12
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/update-key.ts78
-rw-r--r--packages/backend/src/server/api/endpoints/i/notifications.ts19
-rw-r--r--packages/backend/src/server/api/endpoints/notes/create.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/notes/featured.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/roles/list.ts37
-rw-r--r--packages/backend/src/server/api/endpoints/roles/show.ts52
-rw-r--r--packages/backend/src/server/api/endpoints/roles/users.ts71
-rw-r--r--packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts111
-rw-r--r--packages/backend/src/server/web/manifest.json14
26 files changed, 571 insertions, 111 deletions
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index cc27b36966..347fa59d36 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -108,9 +108,9 @@ export class ApiCallService implements OnApplicationShutdown {
const [path] = await createTemp();
await pump(multipartData.file, fs.createWriteStream(path));
- const fields = {} as Record<string, string | undefined>;
+ const fields = {} as Record<string, unknown>;
for (const [k, v] of Object.entries(multipartData.fields)) {
- fields[k] = v.value;
+ fields[k] = typeof v === 'object' && 'value' in v ? v.value : undefined;
}
const token = fields['i'];
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index 30101a2c60..d3e2219bd5 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -66,6 +66,7 @@ import * as ep___admin_roles_update from './endpoints/admin/roles/update.js';
import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js';
import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js';
import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
+import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
@@ -170,6 +171,7 @@ import * as ep___i_2fa_keyDone from './endpoints/i/2fa/key-done.js';
import * as ep___i_2fa_passwordLess from './endpoints/i/2fa/password-less.js';
import * as ep___i_2fa_registerKey from './endpoints/i/2fa/register-key.js';
import * as ep___i_2fa_register from './endpoints/i/2fa/register.js';
+import * as ep___i_2fa_updateKey from './endpoints/i/2fa/update-key.js';
import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js';
import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js';
import * as ep___i_apps from './endpoints/i/apps.js';
@@ -276,6 +278,9 @@ import * as ep___flash_myLikes from './endpoints/flash/my-likes.js';
import * as ep___ping from './endpoints/ping.js';
import * as ep___pinnedUsers from './endpoints/pinned-users.js';
import * as ep___promo_read from './endpoints/promo/read.js';
+import * as ep___roles_list from './endpoints/roles/list.js';
+import * as ep___roles_show from './endpoints/roles/show.js';
+import * as ep___roles_users from './endpoints/roles/users.js';
import * as ep___requestResetPassword from './endpoints/request-reset-password.js';
import * as ep___resetDb from './endpoints/reset-db.js';
import * as ep___resetPassword from './endpoints/reset-password.js';
@@ -382,6 +387,7 @@ const $admin_roles_update: Provider = { provide: 'ep:admin/roles/update', useCla
const $admin_roles_assign: Provider = { provide: 'ep:admin/roles/assign', useClass: ep___admin_roles_assign.default };
const $admin_roles_unassign: Provider = { provide: 'ep:admin/roles/unassign', useClass: ep___admin_roles_unassign.default };
const $admin_roles_updateDefaultPolicies: Provider = { provide: 'ep:admin/roles/update-default-policies', useClass: ep___admin_roles_updateDefaultPolicies.default };
+const $admin_roles_users: Provider = { provide: 'ep:admin/roles/users', useClass: ep___admin_roles_users.default };
const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default };
const $antennas_delete: Provider = { provide: 'ep:antennas/delete', useClass: ep___antennas_delete.default };
@@ -486,6 +492,7 @@ const $i_2fa_keyDone: Provider = { provide: 'ep:i/2fa/key-done', useClass: ep___
const $i_2fa_passwordLess: Provider = { provide: 'ep:i/2fa/password-less', useClass: ep___i_2fa_passwordLess.default };
const $i_2fa_registerKey: Provider = { provide: 'ep:i/2fa/register-key', useClass: ep___i_2fa_registerKey.default };
const $i_2fa_register: Provider = { provide: 'ep:i/2fa/register', useClass: ep___i_2fa_register.default };
+const $i_2fa_updateKey: Provider = { provide: 'ep:i/2fa/update-key', useClass: ep___i_2fa_updateKey.default };
const $i_2fa_removeKey: Provider = { provide: 'ep:i/2fa/remove-key', useClass: ep___i_2fa_removeKey.default };
const $i_2fa_unregister: Provider = { provide: 'ep:i/2fa/unregister', useClass: ep___i_2fa_unregister.default };
const $i_apps: Provider = { provide: 'ep:i/apps', useClass: ep___i_apps.default };
@@ -592,6 +599,9 @@ const $flash_myLikes: Provider = { provide: 'ep:flash/my-likes', useClass: ep___
const $ping: Provider = { provide: 'ep:ping', useClass: ep___ping.default };
const $pinnedUsers: Provider = { provide: 'ep:pinned-users', useClass: ep___pinnedUsers.default };
const $promo_read: Provider = { provide: 'ep:promo/read', useClass: ep___promo_read.default };
+const $roles_list: Provider = { provide: 'ep:roles/list', useClass: ep___roles_list.default };
+const $roles_show: Provider = { provide: 'ep:roles/show', useClass: ep___roles_show.default };
+const $roles_users: Provider = { provide: 'ep:roles/users', useClass: ep___roles_users.default };
const $requestResetPassword: Provider = { provide: 'ep:request-reset-password', useClass: ep___requestResetPassword.default };
const $resetDb: Provider = { provide: 'ep:reset-db', useClass: ep___resetDb.default };
const $resetPassword: Provider = { provide: 'ep:reset-password', useClass: ep___resetPassword.default };
@@ -702,6 +712,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$admin_roles_assign,
$admin_roles_unassign,
$admin_roles_updateDefaultPolicies,
+ $admin_roles_users,
$announcements,
$antennas_create,
$antennas_delete,
@@ -806,6 +817,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_2fa_passwordLess,
$i_2fa_registerKey,
$i_2fa_register,
+ $i_2fa_updateKey,
$i_2fa_removeKey,
$i_2fa_unregister,
$i_apps,
@@ -912,6 +924,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$ping,
$pinnedUsers,
$promo_read,
+ $roles_list,
+ $roles_show,
+ $roles_users,
$requestResetPassword,
$resetDb,
$resetPassword,
@@ -1016,6 +1031,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$admin_roles_assign,
$admin_roles_unassign,
$admin_roles_updateDefaultPolicies,
+ $admin_roles_users,
$announcements,
$antennas_create,
$antennas_delete,
@@ -1120,6 +1136,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_2fa_passwordLess,
$i_2fa_registerKey,
$i_2fa_register,
+ $i_2fa_updateKey,
$i_2fa_removeKey,
$i_2fa_unregister,
$i_apps,
@@ -1226,6 +1243,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$ping,
$pinnedUsers,
$promo_read,
+ $roles_list,
+ $roles_show,
+ $roles_users,
$requestResetPassword,
$resetDb,
$resetPassword,
diff --git a/packages/backend/src/server/api/GetterService.ts b/packages/backend/src/server/api/GetterService.ts
index c7f9916f97..c94884a78c 100644
--- a/packages/backend/src/server/api/GetterService.ts
+++ b/packages/backend/src/server/api/GetterService.ts
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { NotesRepository, UsersRepository } from '@/models/index.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
-import type { User } from '@/models/entities/User.js';
+import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
import type { Note } from '@/models/entities/Note.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
@@ -45,7 +45,7 @@ export class GetterService {
throw new IdentifiableError('15348ddd-432d-49c2-8a5a-8069753becff', 'No such user.');
}
- return user;
+ return user as LocalUser | RemoteUser;
}
/**
diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts
index f1164b9957..bd3d8a28da 100644
--- a/packages/backend/src/server/api/SigninApiService.ts
+++ b/packages/backend/src/server/api/SigninApiService.ts
@@ -1,7 +1,7 @@
import { randomBytes } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs';
-import * as speakeasy from 'speakeasy';
+import * as OTPAuth from 'otpauth';
import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { UserSecurityKeysRepository, SigninsRepository, UserProfilesRepository, AttestationChallengesRepository, UsersRepository } from '@/models/index.js';
@@ -155,19 +155,19 @@ export class SigninApiService {
});
}
- const verified = (speakeasy as any).totp.verify({
- secret: profile.twoFactorSecret,
- encoding: 'base32',
- token: token,
- window: 2,
+ const delta = OTPAuth.TOTP.validate({
+ secret: OTPAuth.Secret.fromBase32(profile.twoFactorSecret!),
+ digits: 6,
+ token,
+ window: 1,
});
- if (verified) {
- return this.signinService.signin(request, reply, user);
- } else {
+ if (delta === null) {
return await fail(403, {
id: 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f',
});
+ } else {
+ return this.signinService.signin(request, reply, user);
}
} else if (body.credentialId && body.clientDataJSON && body.authenticatorData && body.signature) {
if (!same && !profile.usePasswordLessLogin) {
diff --git a/packages/backend/src/server/api/endpoint-base.ts b/packages/backend/src/server/api/endpoint-base.ts
index 115526d997..ed283eb834 100644
--- a/packages/backend/src/server/api/endpoint-base.ts
+++ b/packages/backend/src/server/api/endpoint-base.ts
@@ -20,14 +20,14 @@ type File = {
};
// TODO: paramsの型をT['params']のスキーマ定義から推論する
-type executor<T extends IEndpointMeta, Ps extends Schema> =
+type Executor<T extends IEndpointMeta, Ps extends Schema> =
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> {
public exec: (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => Promise<any>;
- constructor(meta: T, paramDef: Ps, cb: executor<T, Ps>) {
+ constructor(meta: T, paramDef: Ps, cb: Executor<T, Ps>) {
const validate = ajv.compile(paramDef);
this.exec = (params: any, user: T['requireCredential'] extends true ? LocalUser : LocalUser | null, token: AccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => {
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index d05005b078..4d5ed9fb62 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -66,6 +66,7 @@ import * as ep___admin_roles_update from './endpoints/admin/roles/update.js';
import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js';
import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js';
import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
+import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
@@ -170,6 +171,7 @@ import * as ep___i_2fa_keyDone from './endpoints/i/2fa/key-done.js';
import * as ep___i_2fa_passwordLess from './endpoints/i/2fa/password-less.js';
import * as ep___i_2fa_registerKey from './endpoints/i/2fa/register-key.js';
import * as ep___i_2fa_register from './endpoints/i/2fa/register.js';
+import * as ep___i_2fa_updateKey from './endpoints/i/2fa/update-key.js';
import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js';
import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js';
import * as ep___i_apps from './endpoints/i/apps.js';
@@ -276,6 +278,9 @@ import * as ep___flash_myLikes from './endpoints/flash/my-likes.js';
import * as ep___ping from './endpoints/ping.js';
import * as ep___pinnedUsers from './endpoints/pinned-users.js';
import * as ep___promo_read from './endpoints/promo/read.js';
+import * as ep___roles_list from './endpoints/roles/list.js';
+import * as ep___roles_show from './endpoints/roles/show.js';
+import * as ep___roles_users from './endpoints/roles/users.js';
import * as ep___requestResetPassword from './endpoints/request-reset-password.js';
import * as ep___resetDb from './endpoints/reset-db.js';
import * as ep___resetPassword from './endpoints/reset-password.js';
@@ -380,6 +385,7 @@ const eps = [
['admin/roles/assign', ep___admin_roles_assign],
['admin/roles/unassign', ep___admin_roles_unassign],
['admin/roles/update-default-policies', ep___admin_roles_updateDefaultPolicies],
+ ['admin/roles/users', ep___admin_roles_users],
['announcements', ep___announcements],
['antennas/create', ep___antennas_create],
['antennas/delete', ep___antennas_delete],
@@ -484,6 +490,7 @@ const eps = [
['i/2fa/password-less', ep___i_2fa_passwordLess],
['i/2fa/register-key', ep___i_2fa_registerKey],
['i/2fa/register', ep___i_2fa_register],
+ ['i/2fa/update-key', ep___i_2fa_updateKey],
['i/2fa/remove-key', ep___i_2fa_removeKey],
['i/2fa/unregister', ep___i_2fa_unregister],
['i/apps', ep___i_apps],
@@ -590,6 +597,9 @@ const eps = [
['ping', ep___ping],
['pinned-users', ep___pinnedUsers],
['promo/read', ep___promo_read],
+ ['roles/list', ep___roles_list],
+ ['roles/show', ep___roles_show],
+ ['roles/users', ep___roles_users],
['request-reset-password', ep___requestResetPassword],
['reset-db', ep___resetDb],
['reset-password', ep___resetPassword],
diff --git a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
index 6376cb153c..85b566aabe 100644
--- a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
+++ b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
-import type { DriveFilesRepository } from '@/models/index.js';
+import type { DriveFilesRepository, UsersRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
@@ -161,6 +161,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
+
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
@@ -178,7 +181,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchFile);
}
- const isModerator = await this.roleService.isModerator(me);
+ const owner = file.userId ? await this.usersRepository.findOneByOrFail({
+ id: file.userId,
+ }) : null;
+
+ const iAmModerator = await this.roleService.isModerator(me);
+ const ownerIsModerator = owner ? await this.roleService.isModerator(owner) : false;
return {
id: file.id,
@@ -207,8 +215,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
name: file.name,
md5: file.md5,
createdAt: file.createdAt.toISOString(),
- requestIp: isModerator ? file.requestIp : null,
- requestHeaders: isModerator ? file.requestHeaders : null,
+ requestIp: iAmModerator ? file.requestIp : null,
+ requestHeaders: iAmModerator && !ownerIsModerator ? file.requestHeaders : null,
};
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/list.ts b/packages/backend/src/server/api/endpoints/admin/roles/list.ts
index ac56de56b9..edaf638ea9 100644
--- a/packages/backend/src/server/api/endpoints/admin/roles/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/roles/list.ts
@@ -32,7 +32,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const roles = await this.rolesRepository.find({
order: { lastUsedAt: 'DESC' },
});
- return await this.roleEntityService.packMany(roles, me, { detail: false });
+ return await this.roleEntityService.packMany(roles, me);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/show.ts b/packages/backend/src/server/api/endpoints/admin/roles/show.ts
index c83f96191d..01028a086f 100644
--- a/packages/backend/src/server/api/endpoints/admin/roles/show.ts
+++ b/packages/backend/src/server/api/endpoints/admin/roles/show.ts
@@ -39,12 +39,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private roleEntityService: RoleEntityService,
) {
- super(meta, paramDef, async (ps) => {
+ super(meta, paramDef, async (ps, me) => {
const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
if (role == null) {
throw new ApiError(meta.errors.noSuchRole);
}
- return await this.roleEntityService.pack(role);
+ return await this.roleEntityService.pack(role, me);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/users.ts b/packages/backend/src/server/api/endpoints/admin/roles/users.ts
new file mode 100644
index 0000000000..bb016a8425
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/roles/users.ts
@@ -0,0 +1,71 @@
+import { Inject, Injectable } from '@nestjs/common';
+import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { QueryService } from '@/core/QueryService.js';
+import { DI } from '@/di-symbols.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { ApiError } from '../../../error.js';
+
+export const meta = {
+ tags: ['admin', 'role', 'users'],
+
+ requireCredential: false,
+ requireAdmin: true,
+
+ errors: {
+ noSuchRole: {
+ message: 'No such role.',
+ code: 'NO_SUCH_ROLE',
+ id: '224eff5e-2488-4b18-b3e7-f50d94421648',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ roleId: { type: 'string', format: 'misskey:id' },
+ sinceId: { type: 'string', format: 'misskey:id' },
+ untilId: { type: 'string', format: 'misskey:id' },
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ },
+ required: ['roleId'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.rolesRepository)
+ private rolesRepository: RolesRepository,
+
+ @Inject(DI.roleAssignmentsRepository)
+ private roleAssignmentsRepository: RoleAssignmentsRepository,
+
+ private queryService: QueryService,
+ private userEntityService: UserEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const role = await this.rolesRepository.findOneBy({
+ id: ps.roleId,
+ });
+
+ if (role == null) {
+ throw new ApiError(meta.errors.noSuchRole);
+ }
+
+ const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId)
+ .andWhere('assign.roleId = :roleId', { roleId: role.id })
+ .innerJoinAndSelect('assign.user', 'user');
+
+ const assigns = await query
+ .take(ps.limit)
+ .getMany();
+
+ return await Promise.all(assigns.map(async assign => ({
+ id: assign.id,
+ user: await this.userEntityService.pack(assign.user!, me, { detail: true }),
+ })));
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts
index 823af6d8be..9d19efbbcf 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts
@@ -59,12 +59,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new Error('cannot show info of admin');
}
- if (!await this.roleService.isAdministrator(_me)) {
- return {
- isSuspended: user.isSuspended,
- };
- }
-
const signins = await this.signinsRepository.findBy({ userId: user.id });
const roles = await this.roleService.getUserRoles(user.id);
@@ -89,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
moderationNote: profile.moderationNote,
signins,
policies: await this.roleService.getUserPolicies(user.id),
- roles: await this.roleEntityService.packMany(roles, me, { detail: false }),
+ roles: await this.roleEntityService.packMany(roles, me),
};
});
}
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/done.ts b/packages/backend/src/server/api/endpoints/i/2fa/done.ts
index ec9ac1ef90..6c31075e05 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/done.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/done.ts
@@ -1,7 +1,10 @@
-import * as speakeasy from 'speakeasy';
+import * as OTPAuth from 'otpauth';
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { UserProfilesRepository } from '@/models/index.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
export const meta = {
@@ -22,8 +25,14 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
+ @Inject(DI.config)
+ private config: Config,
+
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
+
+ private userEntityService: UserEntityService,
+ private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const token = ps.token.replace(/\s/g, '');
@@ -34,13 +43,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new Error('二段階認証の設定が開始されていません');
}
- const verified = (speakeasy as any).totp.verify({
- secret: profile.twoFactorTempSecret,
- encoding: 'base32',
- token: token,
+ const delta = OTPAuth.TOTP.validate({
+ secret: OTPAuth.Secret.fromBase32(profile.twoFactorTempSecret),
+ digits: 6,
+ token,
+ window: 1,
});
- if (!verified) {
+ if (delta === null) {
throw new Error('not verified');
}
@@ -48,6 +58,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
twoFactorSecret: profile.twoFactorTempSecret,
twoFactorEnabled: true,
});
+
+ // Publish meUpdated event
+ this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
+ detail: true,
+ includeSecrets: true,
+ }));
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
index 6e0849f2b2..ad33398da6 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
@@ -25,7 +25,7 @@ export const paramDef = {
attestationObject: { type: 'string' },
password: { type: 'string' },
challengeId: { type: 'string' },
- name: { type: 'string' },
+ name: { type: 'string', minLength: 1, maxLength: 30 },
},
required: ['clientDataJSON', 'attestationObject', 'password', 'challengeId', 'name'],
} as const;
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts
index 0655a86350..0ee9f556a8 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts
@@ -1,12 +1,23 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { UserProfilesRepository } from '@/models/index.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
+import { ApiError } from '../../../error.js';
export const meta = {
requireCredential: true,
secure: true,
+
+ errors: {
+ noKey: {
+ message: 'No security key.',
+ code: 'NO_SECURITY_KEY',
+ id: 'f9c54d7f-d4c2-4d3c-9a8g-a70daac86512',
+ },
+ },
} as const;
export const paramDef = {
@@ -23,11 +34,45 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
+
+ @Inject(DI.userSecurityKeysRepository)
+ private userSecurityKeysRepository: UserSecurityKeysRepository,
+
+ private userEntityService: UserEntityService,
+ private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
+ if (ps.value === true) {
+ // セキュリティキーがなければパスワードレスを有効にはできない
+ const keyCount = await this.userSecurityKeysRepository.count({
+ where: {
+ userId: me.id,
+ },
+ select: {
+ id: true,
+ name: true,
+ lastUsed: true,
+ },
+ });
+
+ if (keyCount === 0) {
+ await this.userProfilesRepository.update(me.id, {
+ usePasswordLessLogin: false,
+ });
+
+ throw new ApiError(meta.errors.noKey);
+ }
+ }
+
await this.userProfilesRepository.update(me.id, {
usePasswordLessLogin: ps.value,
});
+
+ // Publish meUpdated event
+ this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
+ detail: true,
+ includeSecrets: true,
+ }));
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
index a539c5c221..eb4d7f9c14 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
@@ -1,5 +1,5 @@
import bcrypt from 'bcryptjs';
-import * as speakeasy from 'speakeasy';
+import * as OTPAuth from 'otpauth';
import * as QRCode from 'qrcode';
import { Inject, Injectable } from '@nestjs/common';
import type { UserProfilesRepository } from '@/models/index.js';
@@ -42,25 +42,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
// Generate user's secret key
- const secret = speakeasy.generateSecret({
- length: 32,
- });
+ const secret = new OTPAuth.Secret();
await this.userProfilesRepository.update(me.id, {
twoFactorTempSecret: secret.base32,
});
// Get the data URL of the authenticator URL
- const url = speakeasy.otpauthURL({
- secret: secret.base32,
- encoding: 'base32',
+ const totp = new OTPAuth.TOTP({
+ secret,
+ digits: 6,
label: me.username,
issuer: this.config.host,
});
- const dataUrl = await QRCode.toDataURL(url);
+ const url = totp.toString();
+ const qr = await QRCode.toDataURL(url);
return {
- qr: dataUrl,
+ qr,
url,
secret: secret.base32,
label: me.username,
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
index 0f2b0fd7ee..4b726aed80 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
@@ -50,6 +50,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
id: ps.credentialId,
});
+ // 使われているキーがなくなったらパスワードレスログインをやめる
+ const keyCount = await this.userSecurityKeysRepository.count({
+ where: {
+ userId: me.id,
+ },
+ select: {
+ id: true,
+ name: true,
+ lastUsed: true,
+ },
+ });
+
+ if (keyCount === 0) {
+ await this.userProfilesRepository.update(me.id, {
+ usePasswordLessLogin: false,
+ });
+ }
+
// Publish meUpdated event
this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
detail: true,
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
index 4c5b151f78..e0e7ba6658 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
@@ -1,7 +1,9 @@
import bcrypt from 'bcryptjs';
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { UserProfilesRepository } from '@/models/index.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
export const meta = {
@@ -24,6 +26,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
+
+ private userEntityService: UserEntityService,
+ private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
@@ -38,7 +43,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
await this.userProfilesRepository.update(me.id, {
twoFactorSecret: null,
twoFactorEnabled: false,
+ usePasswordLessLogin: false,
});
+
+ // Publish meUpdated event
+ this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
+ detail: true,
+ includeSecrets: true,
+ }));
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts
new file mode 100644
index 0000000000..d98f60fa5f
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts
@@ -0,0 +1,78 @@
+import bcrypt from 'bcryptjs';
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '../../../error.js';
+
+export const meta = {
+ requireCredential: true,
+
+ secure: true,
+
+ errors: {
+ noSuchKey: {
+ message: 'No such key.',
+ code: 'NO_SUCH_KEY',
+ id: 'f9c5467f-d492-4d3c-9a8g-a70dacc86512',
+ },
+
+ accessDenied: {
+ message: 'You do not have edit privilege of the channel.',
+ code: 'ACCESS_DENIED',
+ id: '1fb7cb09-d46a-4fff-b8df-057708cce513',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ name: { type: 'string', minLength: 1, maxLength: 30 },
+ credentialId: { type: 'string' },
+ },
+ required: ['name', 'credentialId'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.userSecurityKeysRepository)
+ private userSecurityKeysRepository: UserSecurityKeysRepository,
+
+ @Inject(DI.userProfilesRepository)
+ private userProfilesRepository: UserProfilesRepository,
+
+ private userEntityService: UserEntityService,
+ private globalEventService: GlobalEventService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const key = await this.userSecurityKeysRepository.findOneBy({
+ id: ps.credentialId,
+ });
+
+ if (key == null) {
+ throw new ApiError(meta.errors.noSuchKey);
+ }
+
+ if (key.userId !== me.id) {
+ throw new ApiError(meta.errors.accessDenied);
+ }
+
+ await this.userSecurityKeysRepository.update(key.id, {
+ name: ps.name,
+ });
+
+ // Publish meUpdated event
+ this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
+ detail: true,
+ includeSecrets: true,
+ }));
+
+ return {};
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts
index 706e0d2089..e3897d38bd 100644
--- a/packages/backend/src/server/api/endpoints/i/notifications.ts
+++ b/packages/backend/src/server/api/endpoints/i/notifications.ts
@@ -1,7 +1,7 @@
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, NotificationsRepository } from '@/models/index.js';
-import { notificationTypes } from '@/types.js';
+import { obsoleteNotificationTypes, notificationTypes } from '@/types.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteReadService } from '@/core/NoteReadService.js';
@@ -41,11 +41,12 @@ export const paramDef = {
following: { type: 'boolean', default: false },
unreadOnly: { type: 'boolean', default: false },
markAsRead: { type: 'boolean', default: true },
+ // 後方互換のため、廃止された通知タイプも受け付ける
includeTypes: { type: 'array', items: {
- type: 'string', enum: notificationTypes,
+ type: 'string', enum: [...notificationTypes, ...obsoleteNotificationTypes],
} },
excludeTypes: { type: 'array', items: {
- type: 'string', enum: notificationTypes,
+ type: 'string', enum: [...notificationTypes, ...obsoleteNotificationTypes],
} },
},
required: [],
@@ -84,6 +85,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (notificationTypes.every(type => ps.excludeTypes?.includes(type))) {
return [];
}
+
+ const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
+ const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
+
const followingQuery = this.followingsRepository.createQueryBuilder('following')
.select('following.followeeId')
.where('following.followerId = :followerId', { followerId: me.id });
@@ -143,10 +148,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
query.setParameters(followingQuery.getParameters());
}
- if (ps.includeTypes && ps.includeTypes.length > 0) {
- query.andWhere('notification.type IN (:...includeTypes)', { includeTypes: ps.includeTypes });
- } else if (ps.excludeTypes && ps.excludeTypes.length > 0) {
- query.andWhere('notification.type NOT IN (:...excludeTypes)', { excludeTypes: ps.excludeTypes });
+ if (includeTypes && includeTypes.length > 0) {
+ query.andWhere('notification.type IN (:...includeTypes)', { includeTypes });
+ } else if (excludeTypes && excludeTypes.length > 0) {
+ query.andWhere('notification.type NOT IN (:...excludeTypes)', { excludeTypes });
}
if (ps.unreadOnly) {
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index 593444968e..f4c5a84a4f 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -79,6 +79,12 @@ export const meta = {
code: 'YOU_HAVE_BEEN_BLOCKED',
id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3',
},
+
+ noSuchFile: {
+ message: 'Some files are not found.',
+ code: 'NO_SUCH_FILE',
+ id: 'b6992544-63e7-67f0-fa7f-32444b1b5306',
+ },
},
} as const;
@@ -207,6 +213,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.orderBy('array_position(ARRAY[:...fileIds], "id"::text)')
.setParameters({ fileIds })
.getMany();
+
+ if (files.length !== fileIds.length) {
+ throw new ApiError(meta.errors.noSuchFile);
+ }
}
let renote: Note | null = null;
diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts
index 8eff8fdb22..cf939f6631 100644
--- a/packages/backend/src/server/api/endpoints/notes/featured.ts
+++ b/packages/backend/src/server/api/endpoints/notes/featured.ts
@@ -28,6 +28,7 @@ export const paramDef = {
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
offset: { type: 'integer', default: 0 },
+ channelId: { type: 'string', nullable: true, format: 'misskey:id' },
},
required: [],
} as const;
@@ -63,12 +64,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
+ if (ps.channelId) query.andWhere('note.channelId = :channelId', { channelId: ps.channelId });
+
if (me) this.queryService.generateMutedUserQuery(query, me);
if (me) this.queryService.generateBlockedUserQuery(query, me);
let notes = await query
.orderBy('note.score', 'DESC')
- .take(ps.limit)
+ .take(50)
.getMany();
notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
diff --git a/packages/backend/src/server/api/endpoints/roles/list.ts b/packages/backend/src/server/api/endpoints/roles/list.ts
new file mode 100644
index 0000000000..d61c6b8dc6
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/roles/list.ts
@@ -0,0 +1,37 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { RolesRepository } from '@/models/index.js';
+import { DI } from '@/di-symbols.js';
+import { RoleEntityService } from '@/core/entities/RoleEntityService.js';
+
+export const meta = {
+ tags: ['role'],
+
+ requireCredential: true,
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ },
+ required: [
+ ],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.rolesRepository)
+ private rolesRepository: RolesRepository,
+
+ private roleEntityService: RoleEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const roles = await this.rolesRepository.findBy({
+ isPublic: true,
+ });
+ return await this.roleEntityService.packMany(roles, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/roles/show.ts b/packages/backend/src/server/api/endpoints/roles/show.ts
new file mode 100644
index 0000000000..cc755dcc76
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/roles/show.ts
@@ -0,0 +1,52 @@
+import { Inject, Injectable } from '@nestjs/common';
+import type { RolesRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { RoleEntityService } from '@/core/entities/RoleEntityService.js';
+import { ApiError } from '../../error.js';
+
+export const meta = {
+ tags: ['role', 'users'],
+
+ requireCredential: false,
+
+ errors: {
+ noSuchRole: {
+ message: 'No such role.',
+ code: 'NO_SUCH_ROLE',
+ id: 'de5502bf-009a-4639-86c1-fec349e46dcb',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ roleId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['roleId'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.rolesRepository)
+ private rolesRepository: RolesRepository,
+
+ private roleEntityService: RoleEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const role = await this.rolesRepository.findOneBy({
+ id: ps.roleId,
+ isPublic: true,
+ });
+
+ if (role == null) {
+ throw new ApiError(meta.errors.noSuchRole);
+ }
+
+ return await this.roleEntityService.pack(role, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/roles/users.ts b/packages/backend/src/server/api/endpoints/roles/users.ts
new file mode 100644
index 0000000000..6e221b6c67
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/roles/users.ts
@@ -0,0 +1,71 @@
+import { Inject, Injectable } from '@nestjs/common';
+import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { QueryService } from '@/core/QueryService.js';
+import { DI } from '@/di-symbols.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { ApiError } from '../../error.js';
+
+export const meta = {
+ tags: ['role', 'users'],
+
+ requireCredential: false,
+
+ errors: {
+ noSuchRole: {
+ message: 'No such role.',
+ code: 'NO_SUCH_ROLE',
+ id: '30aaaee3-4792-48dc-ab0d-cf501a575ac5',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ roleId: { type: 'string', format: 'misskey:id' },
+ sinceId: { type: 'string', format: 'misskey:id' },
+ untilId: { type: 'string', format: 'misskey:id' },
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ },
+ required: ['roleId'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.rolesRepository)
+ private rolesRepository: RolesRepository,
+
+ @Inject(DI.roleAssignmentsRepository)
+ private roleAssignmentsRepository: RoleAssignmentsRepository,
+
+ private queryService: QueryService,
+ private userEntityService: UserEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const role = await this.rolesRepository.findOneBy({
+ id: ps.roleId,
+ isPublic: true,
+ });
+
+ if (role == null) {
+ throw new ApiError(meta.errors.noSuchRole);
+ }
+
+ const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId)
+ .andWhere('assign.roleId = :roleId', { roleId: role.id })
+ .innerJoinAndSelect('assign.user', 'user');
+
+ const assigns = await query
+ .take(ps.limit)
+ .getMany();
+
+ return await Promise.all(assigns.map(async assign => ({
+ id: assign.id,
+ user: await this.userEntityService.pack(assign.user!, me, { detail: true }),
+ })));
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
index 8b22f913d2..1cefcf2707 100644
--- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
+++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
@@ -1,6 +1,7 @@
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository, FollowingsRepository } from '@/models/index.js';
+import type { Config } from '@/config.js';
import type { User } from '@/models/entities/User.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
@@ -36,13 +37,13 @@ export const paramDef = {
properties: {
username: { type: 'string', nullable: true },
},
- required: ['username']
+ required: ['username'],
},
{
properties: {
host: { type: 'string', nullable: true },
},
- required: ['host']
+ required: ['host'],
},
],
} as const;
@@ -53,6 +54,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
+ @Inject(DI.config)
+ private config: Config,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -62,79 +66,76 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private userEntityService: UserEntityService,
) {
super(meta, paramDef, async (ps, me) => {
- const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
-
- if (ps.host) {
- const q = this.usersRepository.createQueryBuilder('user')
- .where('user.isSuspended = FALSE')
- .andWhere('user.host LIKE :host', { host: sqlLikeEscape(ps.host.toLowerCase()) + '%' });
-
+ const setUsernameAndHostQuery = (query = this.usersRepository.createQueryBuilder('user')) => {
if (ps.username) {
- q.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' });
+ query.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' });
}
- q.andWhere('user.updatedAt IS NOT NULL');
- q.orderBy('user.updatedAt', 'DESC');
+ if (ps.host) {
+ if (ps.host === this.config.hostname || ps.host === '.') {
+ query.andWhere('user.host IS NULL');
+ } else {
+ query.andWhere('user.host LIKE :host', {
+ host: sqlLikeEscape(ps.host.toLowerCase()) + '%',
+ });
+ }
+ }
- const users = await q.take(ps.limit).getMany();
+ return query;
+ };
- return await this.userEntityService.packMany(users, me, { detail: ps.detail });
- } else if (ps.username) {
- let users: User[] = [];
+ const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
- if (me) {
- const followingQuery = this.followingsRepository.createQueryBuilder('following')
- .select('following.followeeId')
- .where('following.followerId = :followerId', { followerId: me.id });
+ let users: User[] = [];
- const query = this.usersRepository.createQueryBuilder('user')
- .where(`user.id IN (${ followingQuery.getQuery() })`)
- .andWhere('user.id != :meId', { meId: me.id })
- .andWhere('user.isSuspended = FALSE')
- .andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' })
- .andWhere(new Brackets(qb => { qb
- .where('user.updatedAt IS NULL')
- .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
- }));
+ if (me) {
+ const followingQuery = this.followingsRepository.createQueryBuilder('following')
+ .select('following.followeeId')
+ .where('following.followerId = :followerId', { followerId: me.id });
- query.setParameters(followingQuery.getParameters());
+ const query = setUsernameAndHostQuery()
+ .andWhere(`user.id IN (${ followingQuery.getQuery() })`)
+ .andWhere('user.id != :meId', { meId: me.id })
+ .andWhere('user.isSuspended = FALSE')
+ .andWhere(new Brackets(qb => { qb
+ .where('user.updatedAt IS NULL')
+ .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
+ }));
- users = await query
- .orderBy('user.usernameLower', 'ASC')
- .take(ps.limit)
- .getMany();
+ query.setParameters(followingQuery.getParameters());
- if (users.length < ps.limit) {
- const otherQuery = await this.usersRepository.createQueryBuilder('user')
- .where(`user.id NOT IN (${ followingQuery.getQuery() })`)
- .andWhere('user.id != :meId', { meId: me.id })
- .andWhere('user.isSuspended = FALSE')
- .andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' })
- .andWhere('user.updatedAt IS NOT NULL');
+ users = await query
+ .orderBy('user.usernameLower', 'ASC')
+ .take(ps.limit)
+ .getMany();
- otherQuery.setParameters(followingQuery.getParameters());
+ if (users.length < ps.limit) {
+ const otherQuery = setUsernameAndHostQuery()
+ .andWhere(`user.id NOT IN (${ followingQuery.getQuery() })`)
+ .andWhere('user.isSuspended = FALSE')
+ .andWhere('user.updatedAt IS NOT NULL');
- const otherUsers = await otherQuery
- .orderBy('user.updatedAt', 'DESC')
- .take(ps.limit - users.length)
- .getMany();
+ otherQuery.setParameters(followingQuery.getParameters());
- users = users.concat(otherUsers);
- }
- } else {
- users = await this.usersRepository.createQueryBuilder('user')
- .where('user.isSuspended = FALSE')
- .andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' })
- .andWhere('user.updatedAt IS NOT NULL')
+ const otherUsers = await otherQuery
.orderBy('user.updatedAt', 'DESC')
.take(ps.limit - users.length)
.getMany();
+
+ users = users.concat(otherUsers);
}
+ } else {
+ const query = setUsernameAndHostQuery()
+ .andWhere('user.isSuspended = FALSE')
+ .andWhere('user.updatedAt IS NOT NULL');
- return await this.userEntityService.packMany(users, me, { detail: !!ps.detail });
+ users = await query
+ .orderBy('user.updatedAt', 'DESC')
+ .take(ps.limit - users.length)
+ .getMany();
}
- return [];
+ return await this.userEntityService.packMany(users, me, { detail: !!ps.detail });
});
}
}
diff --git a/packages/backend/src/server/web/manifest.json b/packages/backend/src/server/web/manifest.json
index 48030a2980..41171d62a1 100644
--- a/packages/backend/src/server/web/manifest.json
+++ b/packages/backend/src/server/web/manifest.json
@@ -9,16 +9,26 @@
{
"src": "/static-assets/icons/192.png",
"sizes": "192x192",
- "type": "image/png"
+ "type": "image/png",
+ "purpose": "maskable"
},
{
"src": "/static-assets/icons/512.png",
"sizes": "512x512",
- "type": "image/png"
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "/static-assets/splash.png",
+ "sizes": "300x300",
+ "type": "image/png",
+ "purpose": "any"
}
],
"share_target": {
"action": "/share/",
+ "method": "GET",
+ "enctype": "application/x-www-form-urlencoded",
"params": {
"title": "title",
"text": "text",