1
0
mirror of https://github.com/MisskeyIO/misskey synced 2024-11-30 15:58:16 +09:00

fix(backend): アンテナ等がポリシーで定められた上限を超えている場合、変更や追加ができないように (MisskeyIO#646)

This commit is contained in:
kabo2468 2024-06-16 20:11:25 +09:00 committed by GitHub
parent 5ff34d2f8f
commit 2b2975c0dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 281 additions and 19 deletions

View File

@ -20,6 +20,8 @@ export class ClipService {
public static AlreadyAddedError = class extends Error {}; public static AlreadyAddedError = class extends Error {};
public static TooManyClipNotesError = class extends Error {}; public static TooManyClipNotesError = class extends Error {};
public static TooManyClipsError = class extends Error {}; public static TooManyClipsError = class extends Error {};
public static ClipLimitExceededError = class extends Error {};
public static ClipNotesLimitExceededError = class extends Error {};
constructor( constructor(
@Inject(DI.clipsRepository) @Inject(DI.clipsRepository)
@ -38,13 +40,26 @@ export class ClipService {
@bindThis @bindThis
public async create(me: MiLocalUser, name: string, isPublic: boolean, description: string | null): Promise<MiClip> { public async create(me: MiLocalUser, name: string, isPublic: boolean, description: string | null): Promise<MiClip> {
const policies = await this.roleService.getUserPolicies(me.id);
const currentCount = await this.clipsRepository.countBy({ const currentCount = await this.clipsRepository.countBy({
userId: me.id, userId: me.id,
}); });
if (currentCount > (await this.roleService.getUserPolicies(me.id)).clipLimit) { if (currentCount >= policies.clipLimit) {
throw new ClipService.TooManyClipsError(); throw new ClipService.TooManyClipsError();
} }
const currentNoteCounts = await this.clipNotesRepository
.createQueryBuilder('cn')
.select('COUNT(*)')
.innerJoin('cn.clip', 'c')
.where('c.userId = :userId', { userId: me.id })
.groupBy('cn.clipId')
.getRawMany<{ count: number }>();
if (currentNoteCounts.some((x) => x.count > policies.noteEachClipsLimit)) {
throw new ClipService.ClipNotesLimitExceededError();
}
const clip = await this.clipsRepository.insert({ const clip = await this.clipsRepository.insert({
id: this.idService.gen(), id: this.idService.gen(),
userId: me.id, userId: me.id,
@ -67,6 +82,26 @@ export class ClipService {
throw new ClipService.NoSuchClipError(); throw new ClipService.NoSuchClipError();
} }
const policies = await this.roleService.getUserPolicies(me.id);
const currentCount = await this.clipsRepository.countBy({
userId: me.id,
});
if (currentCount > policies.clipLimit) {
throw new ClipService.ClipLimitExceededError();
}
const currentNoteCounts = await this.clipNotesRepository
.createQueryBuilder('cn')
.select('COUNT(*)')
.innerJoin('cn.clip', 'c')
.where('c.userId = :userId', { userId: me.id })
.groupBy('cn.clipId')
.getRawMany<{ count: number }>();
if (currentNoteCounts.some((x) => x.count > policies.noteEachClipsLimit)) {
throw new ClipService.ClipNotesLimitExceededError();
}
await this.clipsRepository.update(clip.id, { await this.clipsRepository.update(clip.id, {
name: name, name: name,
description: description, description: description,
@ -99,10 +134,30 @@ export class ClipService {
throw new ClipService.NoSuchClipError(); throw new ClipService.NoSuchClipError();
} }
const currentCount = await this.clipNotesRepository.countBy({ const policies = await this.roleService.getUserPolicies(me.id);
const currentClipCount = await this.clipsRepository.countBy({
userId: me.id,
});
if (currentClipCount > policies.clipLimit) {
throw new ClipService.ClipLimitExceededError();
}
const currentNoteCounts = await this.clipNotesRepository
.createQueryBuilder('cn')
.select('COUNT(*)')
.innerJoin('cn.clip', 'c')
.where('c.userId = :userId', { userId: me.id })
.groupBy('cn.clipId')
.getRawMany<{ count: number }>();
if (currentNoteCounts.some((x) => x.count > policies.noteEachClipsLimit)) {
throw new ClipService.ClipNotesLimitExceededError();
}
const currentNoteCount = await this.clipNotesRepository.countBy({
clipId: clip.id, clipId: clip.id,
}); });
if (currentCount > (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) { if (currentNoteCount >= policies.noteEachClipsLimit) {
throw new ClipService.TooManyClipNotesError(); throw new ClipService.TooManyClipNotesError();
} }

View File

@ -95,7 +95,7 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit {
const currentCount = await this.userListMembershipsRepository.countBy({ const currentCount = await this.userListMembershipsRepository.countBy({
userListId: list.id, userListId: list.id,
}); });
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) { if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
throw new UserListService.TooManyUsersError(); throw new UserListService.TooManyUsersError();
} }

View File

@ -95,7 +95,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const currentAntennasCount = await this.antennasRepository.countBy({ const currentAntennasCount = await this.antennasRepository.countBy({
userId: me.id, userId: me.id,
}); });
if (currentAntennasCount > (await this.roleService.getUserPolicies(me.id)).antennaLimit) { if (currentAntennasCount >= (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
throw new ApiError(meta.errors.tooManyAntennas); throw new ApiError(meta.errors.tooManyAntennas);
} }

View File

@ -9,6 +9,7 @@ import type { AntennasRepository, UserListsRepository } from '@/models/_.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
export const meta = { export const meta = {
@ -33,6 +34,12 @@ export const meta = {
code: 'NO_SUCH_USER_LIST', code: 'NO_SUCH_USER_LIST',
id: '1c6b35c9-943e-48c2-81e4-2844989407f7', id: '1c6b35c9-943e-48c2-81e4-2844989407f7',
}, },
antennaLimitExceeded: {
message: 'You cannot update the antenna because you have exceeded the limit of antennas.',
code: 'ANTENNA_LIMIT_EXCEEDED',
id: '3166a92e-09d9-4c09-afa3-1dbe34a3afcf',
},
}, },
res: { res: {
@ -83,6 +90,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private antennaEntityService: AntennaEntityService, private antennaEntityService: AntennaEntityService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private roleService: RoleService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
if (ps.keywords && ps.excludeKeywords) { if (ps.keywords && ps.excludeKeywords) {
@ -100,6 +108,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchAntenna); throw new ApiError(meta.errors.noSuchAntenna);
} }
const currentAntennasCount = await this.antennasRepository.countBy({
userId: me.id,
});
if (currentAntennasCount > (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
throw new ApiError(meta.errors.antennaLimitExceeded);
}
let userList; let userList;
if ((ps.src === 'list' || antenna.src === 'list') && ps.userListId) { if ((ps.src === 'list' || antenna.src === 'list') && ps.userListId) {

View File

@ -48,6 +48,18 @@ export const meta = {
code: 'TOO_MANY_CLIP_NOTES', code: 'TOO_MANY_CLIP_NOTES',
id: 'f0dba960-ff73-4615-8df4-d6ac5d9dc118', id: 'f0dba960-ff73-4615-8df4-d6ac5d9dc118',
}, },
clipLimitExceeded: {
message: 'You cannot add a note to the clip because you have exceeded the limit of clips.',
code: 'CLIP_LIMIT_EXCEEDED',
id: '456cd06d-9f5b-4793-8108-dffe6e257d98',
},
clipNotesLimitExceeded: {
message: 'You cannot add a note to the clip because you have exceeded the limit of notes in other clips.',
code: 'CLIP_NOTES_LIMIT_EXCEEDED',
id: 'f3d6de24-ad27-418d-9d13-b50165dbce66',
},
}, },
} as const; } as const;
@ -77,6 +89,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.alreadyClipped); throw new ApiError(meta.errors.alreadyClipped);
} else if (e instanceof ClipService.TooManyClipNotesError) { } else if (e instanceof ClipService.TooManyClipNotesError) {
throw new ApiError(meta.errors.tooManyClipNotes); throw new ApiError(meta.errors.tooManyClipNotes);
} else if (e instanceof ClipService.ClipLimitExceededError) {
throw new ApiError(meta.errors.clipLimitExceeded);
} else if (e instanceof ClipService.ClipNotesLimitExceededError) {
throw new ApiError(meta.errors.clipNotesLimitExceeded);
} else { } else {
throw e; throw e;
} }

View File

@ -32,6 +32,12 @@ export const meta = {
code: 'TOO_MANY_CLIPS', code: 'TOO_MANY_CLIPS',
id: '920f7c2d-6208-4b76-8082-e632020f5883', id: '920f7c2d-6208-4b76-8082-e632020f5883',
}, },
clipNotesLimitExceeded: {
message: 'You cannot create a clip any more because you have exceeded the limit of notes in a clip.',
code: 'CLIP_NOTES_LIMIT_EXCEEDED',
id: '1fdd390f-dcd3-4b65-88d9-6476159bc5c8',
},
}, },
} as const; } as const;
@ -58,6 +64,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} catch (e) { } catch (e) {
if (e instanceof ClipService.TooManyClipsError) { if (e instanceof ClipService.TooManyClipsError) {
throw new ApiError(meta.errors.tooManyClips); throw new ApiError(meta.errors.tooManyClips);
} else if (e instanceof ClipService.ClipNotesLimitExceededError) {
throw new ApiError(meta.errors.clipNotesLimitExceeded);
} }
throw e; throw e;
} }

View File

@ -25,6 +25,18 @@ export const meta = {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: 'b4d92d70-b216-46fa-9a3f-a8c811699257', id: 'b4d92d70-b216-46fa-9a3f-a8c811699257',
}, },
clipLimitExceeded: {
message: 'You cannot update the clip because you have exceeded the limit of clips.',
code: 'CLIP_LIMIT_EXCEEDED',
id: 'fed46dd9-d99a-4a88-b23f-8d31c80b5b25',
},
clipNotesLimitExceeded: {
message: 'You cannot update the clip because you have exceeded the limit of notes in a clip.',
code: 'CLIP_NOTES_LIMIT_EXCEEDED',
id: '6f02ab37-66a4-4285-afaf-a8b1000e8f3f',
},
}, },
res: { res: {
@ -58,6 +70,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} catch (e) { } catch (e) {
if (e instanceof ClipService.NoSuchClipError) { if (e instanceof ClipService.NoSuchClipError) {
throw new ApiError(meta.errors.noSuchClip); throw new ApiError(meta.errors.noSuchClip);
} else if (e instanceof ClipService.ClipLimitExceededError) {
throw new ApiError(meta.errors.clipLimitExceeded);
} else if (e instanceof ClipService.ClipNotesLimitExceededError) {
throw new ApiError(meta.errors.clipNotesLimitExceeded);
} }
throw e; throw e;
} }

View File

@ -80,7 +80,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (file.size === 0) throw new ApiError(meta.errors.emptyFile); if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
const antennas: (_Antenna & { userListAccts: string[] | null })[] = JSON.parse(await this.downloadService.downloadTextFile(file.url)); const antennas: (_Antenna & { userListAccts: string[] | null })[] = JSON.parse(await this.downloadService.downloadTextFile(file.url));
const currentAntennasCount = await this.antennasRepository.countBy({ userId: me.id }); const currentAntennasCount = await this.antennasRepository.countBy({ userId: me.id });
if (currentAntennasCount + antennas.length > (await this.roleService.getUserPolicies(me.id)).antennaLimit) { if (currentAntennasCount + antennas.length >= (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
throw new ApiError(meta.errors.tooManyAntennas); throw new ApiError(meta.errors.tooManyAntennas);
} }
this.queueService.createImportAntennasJob(me, antennas); this.queueService.createImportAntennasJob(me, antennas);

View File

@ -91,7 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const currentWebhooksCount = await this.webhooksRepository.countBy({ const currentWebhooksCount = await this.webhooksRepository.countBy({
userId: me.id, userId: me.id,
}); });
if (currentWebhooksCount > (await this.roleService.getUserPolicies(me.id)).webhookLimit) { if (currentWebhooksCount >= (await this.roleService.getUserPolicies(me.id)).webhookLimit) {
throw new ApiError(meta.errors.tooManyWebhooks); throw new ApiError(meta.errors.tooManyWebhooks);
} }

View File

@ -31,6 +31,11 @@ export const meta = {
code: 'YOU_ARE_NOT_ADMIN', code: 'YOU_ARE_NOT_ADMIN',
id: 'a70c7643-1db5-4ebf-becd-ff4b4223cf23', id: 'a70c7643-1db5-4ebf-becd-ff4b4223cf23',
}, },
webhookLimitExceeded: {
message: 'You cannot update the webhook because you have exceeded the limit of webhooks.',
code: 'WEBHOOK_LIMIT_EXCEEDED',
id: 'a261cb2d-867d-47a8-a743-8bbd2c1438b1',
},
}, },
} as const; } as const;
@ -62,6 +67,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private roleService: RoleService, private roleService: RoleService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const currentWebhooksCount = await this.webhooksRepository.countBy({
userId: me.id,
});
if (currentWebhooksCount > (await this.roleService.getUserPolicies(me.id)).webhookLimit) {
throw new ApiError(meta.errors.webhookLimitExceeded);
}
const webhook = await this.webhooksRepository.findOneBy({ const webhook = await this.webhooksRepository.findOneBy({
id: ps.webhookId, id: ps.webhookId,
userId: me.id, userId: me.id,

View File

@ -61,6 +61,12 @@ export const meta = {
code: 'TOO_MANY_USERS', code: 'TOO_MANY_USERS',
id: '1845ea77-38d1-426e-8e4e-8b83b24f5bd7', id: '1845ea77-38d1-426e-8e4e-8b83b24f5bd7',
}, },
listUsersLimitExceeded: {
message: 'You cannot create a list because you have exceeded the limit of users in a list.',
code: 'LIST_USERS_LIMIT_EXCEEDED',
id: '3e205e58-0798-40f2-a589-a78a619ee3d4',
},
}, },
} as const; } as const;
@ -99,13 +105,25 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}, },
}); });
if (!listExist) throw new ApiError(meta.errors.noSuchList); if (!listExist) throw new ApiError(meta.errors.noSuchList);
const policies = await this.roleService.getUserPolicies(me.id);
const currentCount = await this.userListsRepository.countBy({ const currentCount = await this.userListsRepository.countBy({
userId: me.id, userId: me.id,
}); });
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) { if (currentCount >= policies.userListLimit) {
throw new ApiError(meta.errors.tooManyUserLists); throw new ApiError(meta.errors.tooManyUserLists);
} }
const currentUserCounts = await this.userListMembershipsRepository
.createQueryBuilder('ulm')
.select('COUNT(*)')
.where('ulm.userListUserId = :userId', { userId: me.id })
.groupBy('ulm.userListId')
.getRawMany<{ count: number }>();
if (currentUserCounts.some((x) => x.count > policies.userEachUserListsLimit)) {
throw new ApiError(meta.errors.listUsersLimitExceeded);
}
const userList = await this.userListsRepository.insert({ const userList = await this.userListsRepository.insert({
id: this.idService.gen(), id: this.idService.gen(),
userId: me.id, userId: me.id,

View File

@ -4,7 +4,7 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { UserListsRepository } from '@/models/_.js'; import type { UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import type { MiUserList } from '@/models/UserList.js'; import type { MiUserList } from '@/models/UserList.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
@ -37,6 +37,12 @@ export const meta = {
code: 'TOO_MANY_USERLISTS', code: 'TOO_MANY_USERLISTS',
id: '0cf21a28-7715-4f39-a20d-777bfdb8d138', id: '0cf21a28-7715-4f39-a20d-777bfdb8d138',
}, },
listUsersLimitExceeded: {
message: 'You cannot create a list because you have exceeded the limit of users in a list.',
code: 'LIST_USERS_LIMIT_EXCEEDED',
id: 'af66c10d-b0e6-418c-a205-4dd46a482e30',
},
}, },
} as const; } as const;
@ -54,18 +60,32 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.userListsRepository) @Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository, private userListsRepository: UserListsRepository,
@Inject(DI.userListMembershipsRepository)
private userListMembershipsRepository: UserListMembershipsRepository,
private userListEntityService: UserListEntityService, private userListEntityService: UserListEntityService,
private idService: IdService, private idService: IdService,
private roleService: RoleService, private roleService: RoleService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const policies = await this.roleService.getUserPolicies(me.id);
const currentCount = await this.userListsRepository.countBy({ const currentCount = await this.userListsRepository.countBy({
userId: me.id, userId: me.id,
}); });
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) { if (currentCount >= policies.userListLimit) {
throw new ApiError(meta.errors.tooManyUserLists); throw new ApiError(meta.errors.tooManyUserLists);
} }
const currentUserCounts = await this.userListMembershipsRepository
.createQueryBuilder('ulm')
.select('COUNT(*)')
.where('ulm.userListUserId = :userId', { userId: me.id })
.groupBy('ulm.userListId')
.getRawMany<{ count: number }>();
if (currentUserCounts.some((x) => x.count > policies.userEachUserListsLimit)) {
throw new ApiError(meta.errors.listUsersLimitExceeded);
}
const userList = await this.userListsRepository.insert({ const userList = await this.userListsRepository.insert({
id: this.idService.gen(), id: this.idService.gen(),
userId: me.id, userId: me.id,

View File

@ -10,6 +10,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { GetterService } from '@/server/api/GetterService.js'; import { GetterService } from '@/server/api/GetterService.js';
import { UserListService } from '@/core/UserListService.js'; import { UserListService } from '@/core/UserListService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../../error.js'; import { ApiError } from '../../../error.js';
export const meta = { export const meta = {
@ -59,6 +60,18 @@ export const meta = {
code: 'TOO_MANY_USERS', code: 'TOO_MANY_USERS',
id: '2dd9752e-a338-413d-8eec-41814430989b', id: '2dd9752e-a338-413d-8eec-41814430989b',
}, },
listLimitExceeded: {
message: 'You cannot add a user to the list because you have exceeded the limit of lists.',
code: 'LIST_LIMIT_EXCEEDED',
id: '5906ab2d-c164-44bc-a60a-464beba52be9',
},
listUsersLimitExceeded: {
message: 'You cannot add a user to the list because you have exceeded the limit of users in other list.',
code: 'LIST_USERS_LIMIT_EXCEEDED',
id: 'd1054b77-908a-4f7d-a9cc-44d665267108',
},
}, },
} as const; } as const;
@ -85,6 +98,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private getterService: GetterService, private getterService: GetterService,
private userListService: UserListService, private userListService: UserListService,
private roleService: RoleService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
// Fetch the list // Fetch the list
@ -97,6 +111,25 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchList); throw new ApiError(meta.errors.noSuchList);
} }
// Check the list limit
const policies = await this.roleService.getUserPolicies(me.id);
const currentCount = await this.userListsRepository.countBy({
userId: me.id,
});
if (currentCount > policies.userListLimit) {
throw new ApiError(meta.errors.listLimitExceeded);
}
const currentUserCounts = await this.userListMembershipsRepository
.createQueryBuilder('ulm')
.select('COUNT(*)')
.where('ulm.userListUserId = :userId', { userId: me.id })
.groupBy('ulm.userListId')
.getRawMany<{ count: number }>();
if (currentUserCounts.some((x) => x.count > policies.userEachUserListsLimit)) {
throw new ApiError(meta.errors.listUsersLimitExceeded);
}
// Fetch the user // Fetch the user
const user = await this.getterService.getUser(ps.userId).catch(err => { const user = await this.getterService.getUser(ps.userId).catch(err => {
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);

View File

@ -4,11 +4,12 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { UserListsRepository } from '@/models/_.js'; import type { UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { GetterService } from '@/server/api/GetterService.js'; import { GetterService } from '@/server/api/GetterService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { UserListService } from '@/core/UserListService.js'; import { UserListService } from '@/core/UserListService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../../error.js'; import { ApiError } from '../../../error.js';
export const meta = { export const meta = {
@ -32,6 +33,18 @@ export const meta = {
code: 'NO_SUCH_USER', code: 'NO_SUCH_USER',
id: '588e7f72-c744-4a61-b180-d354e912bda2', id: '588e7f72-c744-4a61-b180-d354e912bda2',
}, },
listLimitExceeded: {
message: 'You cannot update the user because you have exceeded the limit of lists.',
code: 'LIST_LIMIT_EXCEEDED',
id: 'd4005118-e773-4132-bafb-10ba22c78da3',
},
listUsersLimitExceeded: {
message: 'You cannot update the user because you have exceeded the limit of users in a list.',
code: 'LIST_USERS_LIMIT_EXCEEDED',
id: 'db7fe164-73d0-4788-8ed1-b6a19e95990d',
},
}, },
} as const; } as const;
@ -51,8 +64,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.userListsRepository) @Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository, private userListsRepository: UserListsRepository,
@Inject(DI.userListMembershipsRepository)
private userListMembershipsRepository: UserListMembershipsRepository,
private userListService: UserListService, private userListService: UserListService,
private getterService: GetterService, private getterService: GetterService,
private roleService: RoleService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
// Fetch the list // Fetch the list
@ -65,6 +82,25 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchList); throw new ApiError(meta.errors.noSuchList);
} }
// Check the list limit
const policies = await this.roleService.getUserPolicies(me.id);
const currentCount = await this.userListsRepository.countBy({
userId: me.id,
});
if (currentCount > policies.userListLimit) {
throw new ApiError(meta.errors.listLimitExceeded);
}
const currentUserCounts = await this.userListMembershipsRepository
.createQueryBuilder('ulm')
.select('COUNT(*)')
.where('ulm.userListUserId = :userId', { userId: me.id })
.groupBy('ulm.userListId')
.getRawMany<{ count: number }>();
if (currentUserCounts.some((x) => x.count > policies.userEachUserListsLimit)) {
throw new ApiError(meta.errors.listUsersLimitExceeded);
}
// Fetch the user // Fetch the user
const user = await this.getterService.getUser(ps.userId).catch(err => { const user = await this.getterService.getUser(ps.userId).catch(err => {
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);

View File

@ -4,10 +4,11 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { UserListsRepository } from '@/models/_.js'; import type { UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; import { UserListEntityService } from '@/core/entities/UserListEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../../error.js'; import { ApiError } from '../../../error.js';
export const meta = { export const meta = {
@ -32,6 +33,18 @@ export const meta = {
code: 'NO_SUCH_LIST', code: 'NO_SUCH_LIST',
id: '796666fe-3dff-4d39-becb-8a5932c1d5b7', id: '796666fe-3dff-4d39-becb-8a5932c1d5b7',
}, },
listLimitExceeded: {
message: 'You cannot update the list because you have exceeded the limit of lists.',
code: 'LIST_LIMIT_EXCEEDED',
id: '0a1fa63e-3e4c-4bc2-afd1-1ff853b4560e',
},
listUsersLimitExceeded: {
message: 'You cannot update the list because you have exceeded the limit of users in a list.',
code: 'LIST_USERS_LIMIT_EXCEEDED',
id: '831fd3b2-4ac8-421e-89db-bfd98944e529',
},
}, },
} as const; } as const;
@ -51,7 +64,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.userListsRepository) @Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository, private userListsRepository: UserListsRepository,
@Inject(DI.userListMembershipsRepository)
private userListMembershipsRepository: UserListMembershipsRepository,
private userListEntityService: UserListEntityService, private userListEntityService: UserListEntityService,
private roleService: RoleService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const userList = await this.userListsRepository.findOneBy({ const userList = await this.userListsRepository.findOneBy({
@ -63,6 +80,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchList); throw new ApiError(meta.errors.noSuchList);
} }
const policies = await this.roleService.getUserPolicies(me.id);
const currentCount = await this.userListsRepository.countBy({
userId: me.id,
});
if (currentCount > policies.userListLimit) {
throw new ApiError(meta.errors.listLimitExceeded);
}
const currentUserCounts = await this.userListMembershipsRepository
.createQueryBuilder('ulm')
.select('COUNT(*)')
.where('ulm.userListUserId = :userId', { userId: me.id })
.groupBy('ulm.userListId')
.getRawMany<{ count: number }>();
if (currentUserCounts.some((x) => x.count > policies.userEachUserListsLimit)) {
throw new ApiError(meta.errors.listUsersLimitExceeded);
}
await this.userListsRepository.update(userList.id, { await this.userListsRepository.update(userList.id, {
name: ps.name, name: ps.name,
isPublic: ps.isPublic, isPublic: ps.isPublic,

View File

@ -167,8 +167,7 @@ describe('アンテナ', () => {
}); });
test('が上限いっぱいまで作成できること', async () => { test('が上限いっぱいまで作成できること', async () => {
// antennaLimit + 1まで作れるのがキモ const response = await Promise.all([...Array(DEFAULT_POLICIES.antennaLimit)].map(() => successfulApiCall({
const response = await Promise.all([...Array(DEFAULT_POLICIES.antennaLimit + 1)].map(() => successfulApiCall({
endpoint: 'antennas/create', endpoint: 'antennas/create',
parameters: { ...defaultParam }, parameters: { ...defaultParam },
user: alice, user: alice,

View File

@ -153,8 +153,7 @@ describe('クリップ', () => {
}); });
test('の作成はポリシーで定められた数以上はできない。', async () => { test('の作成はポリシーで定められた数以上はできない。', async () => {
// ポリシー + 1まで作れるという所がミソ const clipLimit = DEFAULT_POLICIES.clipLimit;
const clipLimit = DEFAULT_POLICIES.clipLimit + 1;
for (let i = 0; i < clipLimit; i++) { for (let i = 0; i < clipLimit; i++) {
await create(); await create();
} }
@ -327,10 +326,10 @@ describe('クリップ', () => {
}); });
test('の一覧(clips/list)が取得できる(上限いっぱい)', async () => { test('の一覧(clips/list)が取得できる(上限いっぱい)', async () => {
const clipLimit = DEFAULT_POLICIES.clipLimit + 1; const clipLimit = DEFAULT_POLICIES.clipLimit;
const clips = await createMany({}, clipLimit); const clips = await createMany({}, clipLimit);
const res = await list({ const res = await list({
parameters: { limit: 1 }, // FIXME: 無視されて11全部返ってくる parameters: { limit: 1 }, // FIXME: 無視されて10全部返ってくる
}); });
// 返ってくる配列には順序保障がないのでidでソートして厳密比較 // 返ってくる配列には順序保障がないのでidでソートして厳密比較
@ -705,7 +704,7 @@ describe('クリップ', () => {
// TODO: 17000msくらいかかる... // TODO: 17000msくらいかかる...
test('をポリシーで定められた上限いっぱい(200)を超えて追加はできない。', async () => { test('をポリシーで定められた上限いっぱい(200)を超えて追加はできない。', async () => {
const noteLimit = DEFAULT_POLICIES.noteEachClipsLimit + 1; const noteLimit = DEFAULT_POLICIES.noteEachClipsLimit;
const noteList = await Promise.all([...Array(noteLimit)].map((_, i) => post(alice, { const noteList = await Promise.all([...Array(noteLimit)].map((_, i) => post(alice, {
text: `test ${i}`, text: `test ${i}`,
}) as unknown)) as Misskey.entities.Note[]; }) as unknown)) as Misskey.entities.Note[];