feat: 時限ロール (#10145)
* feat: 時限ロール * クライアントから期限を確認できるように * リファクタとか * fix test * fix test * fix test * clean up
This commit is contained in:
parent
7c3a390763
commit
1c5291f818
|
@ -13,6 +13,7 @@ You should also include the user name that made the change.
|
||||||
## 13.x.x (unreleased)
|
## 13.x.x (unreleased)
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
|
- 時限ロール
|
||||||
- プッシュ通知でカスタム絵文字リアクションを表示できるように
|
- プッシュ通知でカスタム絵文字リアクションを表示できるように
|
||||||
- アンテナでCWも検索対象にするように
|
- アンテナでCWも検索対象にするように
|
||||||
- ノートの操作部をホバー時のみ表示するオプションを追加
|
- ノートの操作部をホバー時のみ表示するオプションを追加
|
||||||
|
|
|
@ -848,11 +848,13 @@ instanceDefaultLightTheme: "インスタンスデフォルトのライトテー
|
||||||
instanceDefaultDarkTheme: "インスタンスデフォルトのダークテーマ"
|
instanceDefaultDarkTheme: "インスタンスデフォルトのダークテーマ"
|
||||||
instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入します。"
|
instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入します。"
|
||||||
mutePeriod: "ミュートする期限"
|
mutePeriod: "ミュートする期限"
|
||||||
|
period: "期限"
|
||||||
indefinitely: "無期限"
|
indefinitely: "無期限"
|
||||||
tenMinutes: "10分"
|
tenMinutes: "10分"
|
||||||
oneHour: "1時間"
|
oneHour: "1時間"
|
||||||
oneDay: "1日"
|
oneDay: "1日"
|
||||||
oneWeek: "1週間"
|
oneWeek: "1週間"
|
||||||
|
oneMonth: "1ヶ月"
|
||||||
reflectMayTakeTime: "反映されるまで時間がかかる場合があります。"
|
reflectMayTakeTime: "反映されるまで時間がかかる場合があります。"
|
||||||
failedToFetchAccountInformation: "アカウント情報の取得に失敗しました"
|
failedToFetchAccountInformation: "アカウント情報の取得に失敗しました"
|
||||||
rateLimitExceeded: "レート制限を超えました"
|
rateLimitExceeded: "レート制限を超えました"
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
export class roleAssignmentExpiresAt1677570181236 {
|
||||||
|
name = 'roleAssignmentExpiresAt1677570181236'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "role_assignment" ADD "expiresAt" TIMESTAMP WITH TIME ZONE`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_539b6c08c05067599743bb6389" ON "role_assignment" ("expiresAt") `);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_539b6c08c05067599743bb6389"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "role_assignment" DROP COLUMN "expiresAt"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,8 @@ import { UserCacheService } from '@/core/UserCacheService.js';
|
||||||
import type { RoleCondFormulaValue } from '@/models/entities/Role.js';
|
import type { RoleCondFormulaValue } from '@/models/entities/Role.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
export type RolePolicies = {
|
export type RolePolicies = {
|
||||||
|
@ -56,6 +58,9 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
private rolesCache: Cache<Role[]>;
|
private rolesCache: Cache<Role[]>;
|
||||||
private roleAssignmentByUserIdCache: Cache<RoleAssignment[]>;
|
private roleAssignmentByUserIdCache: Cache<RoleAssignment[]>;
|
||||||
|
|
||||||
|
public static AlreadyAssignedError = class extends Error {};
|
||||||
|
public static NotAssignedError = class extends Error {};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redisSubscriber)
|
@Inject(DI.redisSubscriber)
|
||||||
private redisSubscriber: Redis.Redis,
|
private redisSubscriber: Redis.Redis,
|
||||||
|
@ -72,6 +77,8 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
private userCacheService: UserCacheService,
|
private userCacheService: UserCacheService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
|
private idService: IdService,
|
||||||
) {
|
) {
|
||||||
//this.onMessage = this.onMessage.bind(this);
|
//this.onMessage = this.onMessage.bind(this);
|
||||||
|
|
||||||
|
@ -128,6 +135,7 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
cached.push({
|
cached.push({
|
||||||
...body,
|
...body,
|
||||||
createdAt: new Date(body.createdAt),
|
createdAt: new Date(body.createdAt),
|
||||||
|
expiresAt: body.expiresAt ? new Date(body.expiresAt) : null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -193,7 +201,10 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getUserRoles(userId: User['id']) {
|
public async getUserRoles(userId: User['id']) {
|
||||||
const assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
|
const now = Date.now();
|
||||||
|
let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
|
||||||
|
// 期限切れのロールを除外
|
||||||
|
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
|
||||||
const assignedRoleIds = assigns.map(x => x.roleId);
|
const assignedRoleIds = assigns.map(x => x.roleId);
|
||||||
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
|
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
|
||||||
const assignedRoles = roles.filter(r => assignedRoleIds.includes(r.id));
|
const assignedRoles = roles.filter(r => assignedRoleIds.includes(r.id));
|
||||||
|
@ -207,7 +218,10 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getUserBadgeRoles(userId: User['id']) {
|
public async getUserBadgeRoles(userId: User['id']) {
|
||||||
const assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
|
const now = Date.now();
|
||||||
|
let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
|
||||||
|
// 期限切れのロールを除外
|
||||||
|
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
|
||||||
const assignedRoleIds = assigns.map(x => x.roleId);
|
const assignedRoleIds = assigns.map(x => x.roleId);
|
||||||
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
|
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
|
||||||
const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id));
|
const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id));
|
||||||
|
@ -316,6 +330,65 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
return users;
|
return users;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async assign(userId: User['id'], roleId: Role['id'], expiresAt: Date | null = null): Promise<void> {
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const existing = await this.roleAssignmentsRepository.findOneBy({
|
||||||
|
roleId: roleId,
|
||||||
|
userId: userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
if (existing.expiresAt && (existing.expiresAt.getTime() < now.getTime())) {
|
||||||
|
await this.roleAssignmentsRepository.delete({
|
||||||
|
roleId: roleId,
|
||||||
|
userId: userId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new RoleService.AlreadyAssignedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await this.roleAssignmentsRepository.insert({
|
||||||
|
id: this.idService.genId(),
|
||||||
|
createdAt: now,
|
||||||
|
expiresAt: expiresAt,
|
||||||
|
roleId: roleId,
|
||||||
|
userId: userId,
|
||||||
|
}).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
|
this.rolesRepository.update(roleId, {
|
||||||
|
lastUsedAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.globalEventService.publishInternalEvent('userRoleAssigned', created);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async unassign(userId: User['id'], roleId: Role['id']): Promise<void> {
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const existing = await this.roleAssignmentsRepository.findOneBy({ roleId, userId });
|
||||||
|
if (existing == null) {
|
||||||
|
throw new RoleService.NotAssignedError();
|
||||||
|
} else if (existing.expiresAt && (existing.expiresAt.getTime() < now.getTime())) {
|
||||||
|
await this.roleAssignmentsRepository.delete({
|
||||||
|
roleId: roleId,
|
||||||
|
userId: userId,
|
||||||
|
});
|
||||||
|
throw new RoleService.NotAssignedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.roleAssignmentsRepository.delete(existing.id);
|
||||||
|
|
||||||
|
this.rolesRepository.update(roleId, {
|
||||||
|
lastUsedAt: now,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.globalEventService.publishInternalEvent('userRoleUnassigned', existing);
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public onApplicationShutdown(signal?: string | undefined) {
|
public onApplicationShutdown(signal?: string | undefined) {
|
||||||
this.redisSubscriber.off('message', this.onMessage);
|
this.redisSubscriber.off('message', this.onMessage);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Brackets } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js';
|
import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js';
|
||||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||||
|
@ -28,9 +29,13 @@ export class RoleEntityService {
|
||||||
) {
|
) {
|
||||||
const role = typeof src === 'object' ? src : await this.rolesRepository.findOneByOrFail({ id: src });
|
const role = typeof src === 'object' ? src : await this.rolesRepository.findOneByOrFail({ id: src });
|
||||||
|
|
||||||
const assigns = await this.roleAssignmentsRepository.findBy({
|
const assignedCount = await this.roleAssignmentsRepository.createQueryBuilder('assign')
|
||||||
roleId: role.id,
|
.where('assign.roleId = :roleId', { roleId: role.id })
|
||||||
});
|
.andWhere(new Brackets(qb => { qb
|
||||||
|
.where('assign.expiresAt IS NOT NULL')
|
||||||
|
.orWhere('assign.expiresAt > :now', { now: new Date() });
|
||||||
|
}))
|
||||||
|
.getCount();
|
||||||
|
|
||||||
const policies = { ...role.policies };
|
const policies = { ...role.policies };
|
||||||
for (const [k, v] of Object.entries(DEFAULT_POLICIES)) {
|
for (const [k, v] of Object.entries(DEFAULT_POLICIES)) {
|
||||||
|
@ -57,7 +62,7 @@ export class RoleEntityService {
|
||||||
asBadge: role.asBadge,
|
asBadge: role.asBadge,
|
||||||
canEditMembersByModerator: role.canEditMembersByModerator,
|
canEditMembersByModerator: role.canEditMembersByModerator,
|
||||||
policies: policies,
|
policies: policies,
|
||||||
usersCount: assigns.length,
|
usersCount: assignedCount,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,4 +39,10 @@ export class RoleAssignment {
|
||||||
})
|
})
|
||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
public role: Role | null;
|
public role: Role | null;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column('timestamp with time zone', {
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public expiresAt: Date | null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { LessThan } from 'typeorm';
|
import { In, LessThan } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { AntennaNotesRepository, MutedNotesRepository, NotificationsRepository, UserIpsRepository } from '@/models/index.js';
|
import type { AntennaNotesRepository, MutedNotesRepository, NotificationsRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/index.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
@ -29,6 +29,9 @@ export class CleanProcessorService {
|
||||||
@Inject(DI.antennaNotesRepository)
|
@Inject(DI.antennaNotesRepository)
|
||||||
private antennaNotesRepository: AntennaNotesRepository,
|
private antennaNotesRepository: AntennaNotesRepository,
|
||||||
|
|
||||||
|
@Inject(DI.roleAssignmentsRepository)
|
||||||
|
private roleAssignmentsRepository: RoleAssignmentsRepository,
|
||||||
|
|
||||||
private queueLoggerService: QueueLoggerService,
|
private queueLoggerService: QueueLoggerService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
) {
|
) {
|
||||||
|
@ -56,6 +59,17 @@ export class CleanProcessorService {
|
||||||
id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))),
|
id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const expiredRoleAssignments = await this.roleAssignmentsRepository.createQueryBuilder('assign')
|
||||||
|
.where('assign.expiresAt IS NOT NULL')
|
||||||
|
.andWhere('assign.expiresAt < :now', { now: new Date() })
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
if (expiredRoleAssignments.length > 0) {
|
||||||
|
await this.roleAssignmentsRepository.delete({
|
||||||
|
id: In(expiredRoleAssignments.map(x => x.id)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.logger.succ('Cleaned.');
|
this.logger.succ('Cleaned.');
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js';
|
import type { RolesRepository, UsersRepository } from '@/models/index.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ApiError } from '@/server/api/error.js';
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -39,6 +37,10 @@ export const paramDef = {
|
||||||
properties: {
|
properties: {
|
||||||
roleId: { type: 'string', format: 'misskey:id' },
|
roleId: { type: 'string', format: 'misskey:id' },
|
||||||
userId: { type: 'string', format: 'misskey:id' },
|
userId: { type: 'string', format: 'misskey:id' },
|
||||||
|
expiresAt: {
|
||||||
|
type: 'integer',
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required: [
|
required: [
|
||||||
'roleId',
|
'roleId',
|
||||||
|
@ -56,12 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
@Inject(DI.rolesRepository)
|
@Inject(DI.rolesRepository)
|
||||||
private rolesRepository: RolesRepository,
|
private rolesRepository: RolesRepository,
|
||||||
|
|
||||||
@Inject(DI.roleAssignmentsRepository)
|
|
||||||
private roleAssignmentsRepository: RoleAssignmentsRepository,
|
|
||||||
|
|
||||||
private globalEventService: GlobalEventService,
|
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
private idService: IdService,
|
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
|
const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
|
||||||
|
@ -78,19 +75,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
throw new ApiError(meta.errors.noSuchUser);
|
throw new ApiError(meta.errors.noSuchUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
const date = new Date();
|
if (ps.expiresAt && ps.expiresAt <= Date.now()) {
|
||||||
const created = await this.roleAssignmentsRepository.insert({
|
return;
|
||||||
id: this.idService.genId(),
|
}
|
||||||
createdAt: date,
|
|
||||||
roleId: role.id,
|
|
||||||
userId: user.id,
|
|
||||||
}).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0]));
|
|
||||||
|
|
||||||
this.rolesRepository.update(ps.roleId, {
|
await this.roleService.assign(user.id, role.id, ps.expiresAt ? new Date(ps.expiresAt) : null);
|
||||||
lastUsedAt: new Date(),
|
|
||||||
});
|
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('userRoleAssigned', created);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js';
|
import type { RolesRepository, UsersRepository } from '@/models/index.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ApiError } from '@/server/api/error.js';
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -62,12 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
@Inject(DI.rolesRepository)
|
@Inject(DI.rolesRepository)
|
||||||
private rolesRepository: RolesRepository,
|
private rolesRepository: RolesRepository,
|
||||||
|
|
||||||
@Inject(DI.roleAssignmentsRepository)
|
|
||||||
private roleAssignmentsRepository: RoleAssignmentsRepository,
|
|
||||||
|
|
||||||
private globalEventService: GlobalEventService,
|
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
private idService: IdService,
|
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
|
const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
|
||||||
|
@ -84,18 +77,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
throw new ApiError(meta.errors.noSuchUser);
|
throw new ApiError(meta.errors.noSuchUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleAssignment = await this.roleAssignmentsRepository.findOneBy({ userId: user.id, roleId: role.id });
|
await this.roleService.unassign(user.id, role.id);
|
||||||
if (roleAssignment == null) {
|
|
||||||
throw new ApiError(meta.errors.notAssigned);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.roleAssignmentsRepository.delete(roleAssignment.id);
|
|
||||||
|
|
||||||
this.rolesRepository.update(ps.roleId, {
|
|
||||||
lastUsedAt: new Date(),
|
|
||||||
});
|
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('userRoleUnassigned', roleAssignment);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Brackets } from 'typeorm';
|
||||||
import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js';
|
import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
|
@ -56,6 +57,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
|
||||||
const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId)
|
const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId)
|
||||||
.andWhere('assign.roleId = :roleId', { roleId: role.id })
|
.andWhere('assign.roleId = :roleId', { roleId: role.id })
|
||||||
|
.andWhere(new Brackets(qb => { qb
|
||||||
|
.where('assign.expiresAt IS NOT NULL')
|
||||||
|
.orWhere('assign.expiresAt > :now', { now: new Date() });
|
||||||
|
}))
|
||||||
.innerJoinAndSelect('assign.user', 'user');
|
.innerJoinAndSelect('assign.user', 'user');
|
||||||
|
|
||||||
const assigns = await query
|
const assigns = await query
|
||||||
|
@ -65,6 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
return await Promise.all(assigns.map(async assign => ({
|
return await Promise.all(assigns.map(async assign => ({
|
||||||
id: assign.id,
|
id: assign.id,
|
||||||
user: await this.userEntityService.pack(assign.user!, me, { detail: true }),
|
user: await this.userEntityService.pack(assign.user!, me, { detail: true }),
|
||||||
|
expiresAt: assign.expiresAt,
|
||||||
})));
|
})));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Brackets } from 'typeorm';
|
||||||
import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js';
|
import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
|
@ -56,6 +57,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
|
||||||
const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId)
|
const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId)
|
||||||
.andWhere('assign.roleId = :roleId', { roleId: role.id })
|
.andWhere('assign.roleId = :roleId', { roleId: role.id })
|
||||||
|
.andWhere(new Brackets(qb => { qb
|
||||||
|
.where('assign.expiresAt IS NOT NULL')
|
||||||
|
.orWhere('assign.expiresAt > :now', { now: new Date() });
|
||||||
|
}))
|
||||||
.innerJoinAndSelect('assign.user', 'user');
|
.innerJoinAndSelect('assign.user', 'user');
|
||||||
|
|
||||||
const assigns = await query
|
const assigns = await query
|
||||||
|
|
|
@ -3,16 +3,18 @@ process.env.NODE_ENV = 'test';
|
||||||
import { jest } from '@jest/globals';
|
import { jest } from '@jest/globals';
|
||||||
import { ModuleMocker } from 'jest-mock';
|
import { ModuleMocker } from 'jest-mock';
|
||||||
import { Test } from '@nestjs/testing';
|
import { Test } from '@nestjs/testing';
|
||||||
import { DataSource } from 'typeorm';
|
import * as lolex from '@sinonjs/fake-timers';
|
||||||
import rndstr from 'rndstr';
|
import rndstr from 'rndstr';
|
||||||
import { GlobalModule } from '@/GlobalModule.js';
|
import { GlobalModule } from '@/GlobalModule.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import type { Role, RolesRepository, RoleAssignmentsRepository, UsersRepository, User } from '@/models/index.js';
|
import type { Role, RolesRepository, RoleAssignmentsRepository, UsersRepository, User } from '@/models/index.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { CoreModule } from '@/core/CoreModule.js';
|
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { genAid } from '@/misc/id/aid.js';
|
import { genAid } from '@/misc/id/aid.js';
|
||||||
import { UserCacheService } from '@/core/UserCacheService.js';
|
import { UserCacheService } from '@/core/UserCacheService.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
import { sleep } from '../utils.js';
|
||||||
import type { TestingModule } from '@nestjs/testing';
|
import type { TestingModule } from '@nestjs/testing';
|
||||||
import type { MockFunctionMetadata } from 'jest-mock';
|
import type { MockFunctionMetadata } from 'jest-mock';
|
||||||
|
|
||||||
|
@ -25,6 +27,7 @@ describe('RoleService', () => {
|
||||||
let rolesRepository: RolesRepository;
|
let rolesRepository: RolesRepository;
|
||||||
let roleAssignmentsRepository: RoleAssignmentsRepository;
|
let roleAssignmentsRepository: RoleAssignmentsRepository;
|
||||||
let metaService: jest.Mocked<MetaService>;
|
let metaService: jest.Mocked<MetaService>;
|
||||||
|
let clock: lolex.InstalledClock;
|
||||||
|
|
||||||
function createUser(data: Partial<User> = {}) {
|
function createUser(data: Partial<User> = {}) {
|
||||||
const un = rndstr('a-z0-9', 16);
|
const un = rndstr('a-z0-9', 16);
|
||||||
|
@ -50,16 +53,12 @@ describe('RoleService', () => {
|
||||||
.then(x => rolesRepository.findOneByOrFail(x.identifiers[0]));
|
.then(x => rolesRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function assign(roleId: Role['id'], userId: User['id']) {
|
|
||||||
await roleAssignmentsRepository.insert({
|
|
||||||
id: genAid(new Date()),
|
|
||||||
createdAt: new Date(),
|
|
||||||
roleId,
|
|
||||||
userId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
clock = lolex.install({
|
||||||
|
now: new Date(),
|
||||||
|
shouldClearNativeTimers: true,
|
||||||
|
});
|
||||||
|
|
||||||
app = await Test.createTestingModule({
|
app = await Test.createTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
GlobalModule,
|
GlobalModule,
|
||||||
|
@ -67,6 +66,8 @@ describe('RoleService', () => {
|
||||||
providers: [
|
providers: [
|
||||||
RoleService,
|
RoleService,
|
||||||
UserCacheService,
|
UserCacheService,
|
||||||
|
IdService,
|
||||||
|
GlobalEventService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.useMocker((token) => {
|
.useMocker((token) => {
|
||||||
|
@ -92,12 +93,15 @@ describe('RoleService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
clock.uninstall();
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
app.get(DI.metasRepository).delete({}),
|
app.get(DI.metasRepository).delete({}),
|
||||||
usersRepository.delete({}),
|
usersRepository.delete({}),
|
||||||
rolesRepository.delete({}),
|
rolesRepository.delete({}),
|
||||||
roleAssignmentsRepository.delete({}),
|
roleAssignmentsRepository.delete({}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await app.close();
|
await app.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -140,7 +144,7 @@ describe('RoleService', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await assign(role.id, user.id);
|
await roleService.assign(user.id, role.id);
|
||||||
metaService.fetch.mockResolvedValue({
|
metaService.fetch.mockResolvedValue({
|
||||||
policies: {
|
policies: {
|
||||||
canManageCustomEmojis: false,
|
canManageCustomEmojis: false,
|
||||||
|
@ -174,8 +178,8 @@ describe('RoleService', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await assign(role1.id, user.id);
|
await roleService.assign(user.id, role1.id);
|
||||||
await assign(role2.id, user.id);
|
await roleService.assign(user.id, role2.id);
|
||||||
metaService.fetch.mockResolvedValue({
|
metaService.fetch.mockResolvedValue({
|
||||||
policies: {
|
policies: {
|
||||||
driveCapacityMb: 50,
|
driveCapacityMb: 50,
|
||||||
|
@ -228,5 +232,42 @@ describe('RoleService', () => {
|
||||||
expect(user1Policies.canManageCustomEmojis).toBe(false);
|
expect(user1Policies.canManageCustomEmojis).toBe(false);
|
||||||
expect(user2Policies.canManageCustomEmojis).toBe(true);
|
expect(user2Policies.canManageCustomEmojis).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('expired role', async () => {
|
||||||
|
const user = await createUser();
|
||||||
|
const role = await createRole({
|
||||||
|
name: 'a',
|
||||||
|
policies: {
|
||||||
|
canManageCustomEmojis: {
|
||||||
|
useDefault: false,
|
||||||
|
priority: 0,
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await roleService.assign(user.id, role.id, new Date(Date.now() + (1000 * 60 * 60 * 24)));
|
||||||
|
metaService.fetch.mockResolvedValue({
|
||||||
|
policies: {
|
||||||
|
canManageCustomEmojis: false,
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const result = await roleService.getUserPolicies(user.id);
|
||||||
|
expect(result.canManageCustomEmojis).toBe(true);
|
||||||
|
|
||||||
|
clock.tick('25:00:00');
|
||||||
|
|
||||||
|
const resultAfter25h = await roleService.getUserPolicies(user.id);
|
||||||
|
expect(resultAfter25h.canManageCustomEmojis).toBe(false);
|
||||||
|
|
||||||
|
await roleService.assign(user.id, role.id);
|
||||||
|
|
||||||
|
// ストリーミング経由で反映されるまでちょっと待つ
|
||||||
|
clock.uninstall();
|
||||||
|
await sleep(100);
|
||||||
|
|
||||||
|
const resultAfter25hAgain = await roleService.getUserPolicies(user.id);
|
||||||
|
expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,320 +1,3 @@
|
||||||
import * as fs from 'node:fs';
|
|
||||||
import * as path from 'node:path';
|
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
import { dirname } from 'node:path';
|
|
||||||
import * as childProcess from 'child_process';
|
|
||||||
import * as http from 'node:http';
|
|
||||||
import { SIGKILL } from 'constants';
|
|
||||||
import WebSocket from 'ws';
|
|
||||||
import fetch from 'node-fetch';
|
|
||||||
import FormData from 'form-data';
|
|
||||||
import { DataSource } from 'typeorm';
|
|
||||||
import got, { RequestError } from 'got';
|
|
||||||
import loadConfig from '../src/config/load.js';
|
|
||||||
import { entities } from '@/postgres.js';
|
|
||||||
import type * as misskey from 'misskey-js';
|
|
||||||
|
|
||||||
const _filename = fileURLToPath(import.meta.url);
|
|
||||||
const _dirname = dirname(_filename);
|
|
||||||
|
|
||||||
const config = loadConfig();
|
|
||||||
export const port = config.port;
|
|
||||||
|
|
||||||
export const api = async (endpoint: string, params: any, me?: any) => {
|
|
||||||
endpoint = endpoint.replace(/^\//, '');
|
|
||||||
|
|
||||||
const auth = me ? {
|
|
||||||
i: me.token,
|
|
||||||
} : {};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await got<string>(`http://localhost:${port}/api/${endpoint}`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(Object.assign(auth, params)),
|
|
||||||
retry: {
|
|
||||||
limit: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const status = res.statusCode;
|
|
||||||
const body = res.statusCode !== 204 ? await JSON.parse(res.body) : null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
status,
|
|
||||||
body,
|
|
||||||
};
|
|
||||||
} catch (err: unknown) {
|
|
||||||
if (err instanceof RequestError && err.response) {
|
|
||||||
const status = err.response.statusCode;
|
|
||||||
const body = await JSON.parse(err.response.body as string);
|
|
||||||
|
|
||||||
return {
|
|
||||||
status,
|
|
||||||
body,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const request = async (path: string, params: any, me?: any): Promise<{ body: any, status: number }> => {
|
|
||||||
const auth = me ? {
|
|
||||||
i: me.token,
|
|
||||||
} : {};
|
|
||||||
|
|
||||||
const res = await fetch(`http://localhost:${port}/${path}`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(Object.assign(auth, params)),
|
|
||||||
});
|
|
||||||
|
|
||||||
const status = res.status;
|
|
||||||
const body = res.status === 200 ? await res.json().catch() : null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
body, status,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const signup = async (params?: any): Promise<any> => {
|
|
||||||
const q = Object.assign({
|
|
||||||
username: 'test',
|
|
||||||
password: 'test',
|
|
||||||
}, params);
|
|
||||||
|
|
||||||
const res = await api('signup', q);
|
|
||||||
|
|
||||||
return res.body;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const post = async (user: any, params?: misskey.Endpoints['notes/create']['req']): Promise<misskey.entities.Note> => {
|
|
||||||
const q = Object.assign({
|
|
||||||
text: 'test',
|
|
||||||
}, params);
|
|
||||||
|
|
||||||
const res = await api('notes/create', q, user);
|
|
||||||
|
|
||||||
return res.body ? res.body.createdNote : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const react = async (user: any, note: any, reaction: string): Promise<any> => {
|
|
||||||
await api('notes/reactions/create', {
|
|
||||||
noteId: note.id,
|
|
||||||
reaction: reaction,
|
|
||||||
}, user);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload file
|
|
||||||
* @param user User
|
|
||||||
* @param _path Optional, absolute path or relative from ./resources/
|
|
||||||
*/
|
|
||||||
export const uploadFile = async (user: any, _path?: string): Promise<any> => {
|
|
||||||
const absPath = _path == null ? `${_dirname}/resources/Lenna.jpg` : path.isAbsolute(_path) ? _path : `${_dirname}/resources/${_path}`;
|
|
||||||
|
|
||||||
const formData = new FormData() as any;
|
|
||||||
formData.append('i', user.token);
|
|
||||||
formData.append('file', fs.createReadStream(absPath));
|
|
||||||
formData.append('force', 'true');
|
|
||||||
|
|
||||||
const res = await got<string>(`http://localhost:${port}/api/drive/files/create`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData,
|
|
||||||
retry: {
|
|
||||||
limit: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const body = res.statusCode !== 204 ? await JSON.parse(res.body) : null;
|
|
||||||
|
|
||||||
return body;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const uploadUrl = async (user: any, url: string) => {
|
|
||||||
let file: any;
|
|
||||||
const marker = Math.random().toString();
|
|
||||||
|
|
||||||
const ws = await connectStream(user, 'main', (msg) => {
|
|
||||||
if (msg.type === 'urlUploadFinished' && msg.body.marker === marker) {
|
|
||||||
file = msg.body.file;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await api('drive/files/upload-from-url', {
|
|
||||||
url,
|
|
||||||
marker,
|
|
||||||
force: true,
|
|
||||||
}, user);
|
|
||||||
|
|
||||||
await sleep(7000);
|
|
||||||
ws.close();
|
|
||||||
|
|
||||||
return file;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function connectStream(user: any, channel: string, listener: (message: Record<string, any>) => any, params?: any): Promise<WebSocket> {
|
|
||||||
return new Promise((res, rej) => {
|
|
||||||
const ws = new WebSocket(`ws://localhost:${port}/streaming?i=${user.token}`);
|
|
||||||
|
|
||||||
ws.on('open', () => {
|
|
||||||
ws.on('message', data => {
|
|
||||||
const msg = JSON.parse(data.toString());
|
|
||||||
if (msg.type === 'channel' && msg.body.id === 'a') {
|
|
||||||
listener(msg.body);
|
|
||||||
} else if (msg.type === 'connected' && msg.body.id === 'a') {
|
|
||||||
res(ws);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.send(JSON.stringify({
|
|
||||||
type: 'connect',
|
|
||||||
body: {
|
|
||||||
channel: channel,
|
|
||||||
id: 'a',
|
|
||||||
pong: true,
|
|
||||||
params: params,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const waitFire = async (user: any, channel: string, trgr: () => any, cond: (msg: Record<string, any>) => boolean, params?: any) => {
|
|
||||||
return new Promise<boolean>(async (res, rej) => {
|
|
||||||
let timer: NodeJS.Timeout;
|
|
||||||
|
|
||||||
let ws: WebSocket;
|
|
||||||
try {
|
|
||||||
ws = await connectStream(user, channel, msg => {
|
|
||||||
if (cond(msg)) {
|
|
||||||
ws.close();
|
|
||||||
if (timer) clearTimeout(timer);
|
|
||||||
res(true);
|
|
||||||
}
|
|
||||||
}, params);
|
|
||||||
} catch (e) {
|
|
||||||
rej(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ws!) return;
|
|
||||||
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
ws.close();
|
|
||||||
res(false);
|
|
||||||
}, 3000);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await trgr();
|
|
||||||
} catch (e) {
|
|
||||||
ws.close();
|
|
||||||
if (timer) clearTimeout(timer);
|
|
||||||
rej(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status?: number, type?: string, location?: string }> => {
|
|
||||||
// node-fetchだと3xxを取れない
|
|
||||||
return await new Promise((resolve, reject) => {
|
|
||||||
const req = http.request(`http://localhost:${port}${path}`, {
|
|
||||||
headers: {
|
|
||||||
Accept: accept,
|
|
||||||
},
|
|
||||||
}, res => {
|
|
||||||
if (res.statusCode! >= 400) {
|
|
||||||
reject(res);
|
|
||||||
} else {
|
|
||||||
resolve({
|
|
||||||
status: res.statusCode,
|
|
||||||
type: res.headers['content-type'],
|
|
||||||
location: res.headers.location,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
req.end();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export function launchServer(callbackSpawnedProcess: (p: childProcess.ChildProcess) => void, moreProcess: () => Promise<void> = async () => {}) {
|
|
||||||
return (done: (err?: Error) => any) => {
|
|
||||||
const p = childProcess.spawn('node', [_dirname + '/../index.js'], {
|
|
||||||
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
|
|
||||||
env: { NODE_ENV: 'test', PATH: process.env.PATH },
|
|
||||||
});
|
|
||||||
callbackSpawnedProcess(p);
|
|
||||||
p.on('message', message => {
|
|
||||||
if (message === 'ok') moreProcess().then(() => done()).catch(e => done(e));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function initTestDb(justBorrow = false, initEntities?: any[]) {
|
|
||||||
if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test';
|
|
||||||
|
|
||||||
const db = new DataSource({
|
|
||||||
type: 'postgres',
|
|
||||||
host: config.db.host,
|
|
||||||
port: config.db.port,
|
|
||||||
username: config.db.user,
|
|
||||||
password: config.db.pass,
|
|
||||||
database: config.db.db,
|
|
||||||
synchronize: true && !justBorrow,
|
|
||||||
dropSchema: true && !justBorrow,
|
|
||||||
entities: initEntities ?? entities,
|
|
||||||
});
|
|
||||||
|
|
||||||
await db.initialize();
|
|
||||||
|
|
||||||
return db;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function startServer(timeout = 60 * 1000): Promise<childProcess.ChildProcess> {
|
|
||||||
return new Promise((res, rej) => {
|
|
||||||
const t = setTimeout(() => {
|
|
||||||
p.kill(SIGKILL);
|
|
||||||
rej('timeout to start');
|
|
||||||
}, timeout);
|
|
||||||
|
|
||||||
const p = childProcess.spawn('node', [_dirname + '/../built/index.js'], {
|
|
||||||
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
|
|
||||||
env: { NODE_ENV: 'test', PATH: process.env.PATH },
|
|
||||||
});
|
|
||||||
|
|
||||||
p.on('error', e => rej(e));
|
|
||||||
|
|
||||||
p.on('message', message => {
|
|
||||||
if (message === 'ok') {
|
|
||||||
clearTimeout(t);
|
|
||||||
res(p);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function shutdownServer(p: childProcess.ChildProcess | undefined, timeout = 20 * 1000) {
|
|
||||||
if (p == null) return Promise.resolve('nop');
|
|
||||||
return new Promise((res, rej) => {
|
|
||||||
const t = setTimeout(() => {
|
|
||||||
p.kill(SIGKILL);
|
|
||||||
res('force exit');
|
|
||||||
}, timeout);
|
|
||||||
|
|
||||||
p.once('exit', () => {
|
|
||||||
clearTimeout(t);
|
|
||||||
res('exited');
|
|
||||||
});
|
|
||||||
|
|
||||||
p.kill();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sleep(msec: number) {
|
export function sleep(msec: number) {
|
||||||
return new Promise<void>(res => {
|
return new Promise<void>(res => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
<MkA :class="$style.user" :to="`/user-info/${item.user.id}`">
|
<MkA :class="$style.user" :to="`/user-info/${item.user.id}`">
|
||||||
<MkUserCardMini :user="item.user"/>
|
<MkUserCardMini :user="item.user"/>
|
||||||
</MkA>
|
</MkA>
|
||||||
|
<button v-if="item.expiresAt != null" class="_button" :class="$style.expiresAt" @click="showExpireInfo(item, $event)"><i class="ti ti-clock-hour-3"></i></button>
|
||||||
<button class="_button" :class="$style.unassign" @click="unassign(item.user, $event)"><i class="ti ti-x"></i></button>
|
<button class="_button" :class="$style.unassign" @click="unassign(item.user, $event)"><i class="ti ti-x"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -98,13 +99,37 @@ async function del() {
|
||||||
router.push('/admin/roles');
|
router.push('/admin/roles');
|
||||||
}
|
}
|
||||||
|
|
||||||
function assign() {
|
async function assign() {
|
||||||
os.selectUser({
|
const user = await os.selectUser({
|
||||||
includeSelf: true,
|
includeSelf: true,
|
||||||
}).then(async (user) => {
|
|
||||||
await os.apiWithDialog('admin/roles/assign', { roleId: role.id, userId: user.id });
|
|
||||||
role.users.push(user);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { canceled: canceled2, result: period } = await os.select({
|
||||||
|
title: i18n.ts.period,
|
||||||
|
items: [{
|
||||||
|
value: 'indefinitely', text: i18n.ts.indefinitely,
|
||||||
|
}, {
|
||||||
|
value: 'oneHour', text: i18n.ts.oneHour,
|
||||||
|
}, {
|
||||||
|
value: 'oneDay', text: i18n.ts.oneDay,
|
||||||
|
}, {
|
||||||
|
value: 'oneWeek', text: i18n.ts.oneWeek,
|
||||||
|
}, {
|
||||||
|
value: 'oneMonth', text: i18n.ts.oneMonth,
|
||||||
|
}],
|
||||||
|
default: 'indefinitely',
|
||||||
|
});
|
||||||
|
if (canceled2) return;
|
||||||
|
|
||||||
|
const expiresAt = period === 'indefinitely' ? null
|
||||||
|
: period === 'oneHour' ? Date.now() + (1000 * 60 * 60)
|
||||||
|
: period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24)
|
||||||
|
: period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7)
|
||||||
|
: period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
await os.apiWithDialog('admin/roles/assign', { roleId: role.id, userId: user.id, expiresAt });
|
||||||
|
role.users.push(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function unassign(user, ev) {
|
async function unassign(user, ev) {
|
||||||
|
@ -119,6 +144,13 @@ async function unassign(user, ev) {
|
||||||
}], ev.currentTarget ?? ev.target);
|
}], ev.currentTarget ?? ev.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function showExpireInfo(assignment) {
|
||||||
|
os.alert({
|
||||||
|
type: 'info',
|
||||||
|
text: assignment.expiresAt.toLocaleString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const headerActions = $computed(() => []);
|
const headerActions = $computed(() => []);
|
||||||
|
|
||||||
const headerTabs = $computed(() => []);
|
const headerTabs = $computed(() => []);
|
||||||
|
@ -139,10 +171,15 @@ definePageMetadata(computed(() => ({
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.expiresAt,
|
||||||
.unassign {
|
.unassign {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.expiresAt + .unassign {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -337,7 +337,31 @@ async function assignRole() {
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
await os.apiWithDialog('admin/roles/assign', { roleId, userId: user.id });
|
const { canceled: canceled2, result: period } = await os.select({
|
||||||
|
title: i18n.ts.period,
|
||||||
|
items: [{
|
||||||
|
value: 'indefinitely', text: i18n.ts.indefinitely,
|
||||||
|
}, {
|
||||||
|
value: 'oneHour', text: i18n.ts.oneHour,
|
||||||
|
}, {
|
||||||
|
value: 'oneDay', text: i18n.ts.oneDay,
|
||||||
|
}, {
|
||||||
|
value: 'oneWeek', text: i18n.ts.oneWeek,
|
||||||
|
}, {
|
||||||
|
value: 'oneMonth', text: i18n.ts.oneMonth,
|
||||||
|
}],
|
||||||
|
default: 'indefinitely',
|
||||||
|
});
|
||||||
|
if (canceled2) return;
|
||||||
|
|
||||||
|
const expiresAt = period === 'indefinitely' ? null
|
||||||
|
: period === 'oneHour' ? Date.now() + (1000 * 60 * 60)
|
||||||
|
: period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24)
|
||||||
|
: period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7)
|
||||||
|
: period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
await os.apiWithDialog('admin/roles/assign', { roleId, userId: user.id, expiresAt });
|
||||||
refreshUser();
|
refreshUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -143,8 +143,32 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
|
||||||
|
|
||||||
return roles.filter(r => r.target === 'manual').map(r => ({
|
return roles.filter(r => r.target === 'manual').map(r => ({
|
||||||
text: r.name,
|
text: r.name,
|
||||||
action: () => {
|
action: async () => {
|
||||||
os.apiWithDialog('admin/roles/assign', { roleId: r.id, userId: user.id });
|
const { canceled, result: period } = await os.select({
|
||||||
|
title: i18n.ts.period,
|
||||||
|
items: [{
|
||||||
|
value: 'indefinitely', text: i18n.ts.indefinitely,
|
||||||
|
}, {
|
||||||
|
value: 'oneHour', text: i18n.ts.oneHour,
|
||||||
|
}, {
|
||||||
|
value: 'oneDay', text: i18n.ts.oneDay,
|
||||||
|
}, {
|
||||||
|
value: 'oneWeek', text: i18n.ts.oneWeek,
|
||||||
|
}, {
|
||||||
|
value: 'oneMonth', text: i18n.ts.oneMonth,
|
||||||
|
}],
|
||||||
|
default: 'indefinitely',
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
|
||||||
|
const expiresAt = period === 'indefinitely' ? null
|
||||||
|
: period === 'oneHour' ? Date.now() + (1000 * 60 * 60)
|
||||||
|
: period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24)
|
||||||
|
: period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7)
|
||||||
|
: period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
os.apiWithDialog('admin/roles/assign', { roleId: r.id, userId: user.id, expiresAt });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue