202 lines
6.6 KiB
TypeScript
202 lines
6.6 KiB
TypeScript
|
import { Inject, Injectable } from '@nestjs/common';
|
||
|
import Redis from 'ioredis';
|
||
|
import { In } from 'typeorm';
|
||
|
import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js';
|
||
|
import { Cache } from '@/misc/cache.js';
|
||
|
import type { CacheableLocalUser, CacheableUser, ILocalUser, User } from '@/models/entities/User.js';
|
||
|
import { DI } from '@/di-symbols.js';
|
||
|
import { bindThis } from '@/decorators.js';
|
||
|
import { MetaService } from '@/core/MetaService.js';
|
||
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||
|
|
||
|
export type RoleOptions = {
|
||
|
gtlAvailable: boolean;
|
||
|
ltlAvailable: boolean;
|
||
|
canPublicNote: boolean;
|
||
|
driveCapacityMb: number;
|
||
|
antennaLimit: number;
|
||
|
};
|
||
|
|
||
|
export const DEFAULT_ROLE: RoleOptions = {
|
||
|
gtlAvailable: true,
|
||
|
ltlAvailable: true,
|
||
|
canPublicNote: true,
|
||
|
driveCapacityMb: 100,
|
||
|
antennaLimit: 5,
|
||
|
};
|
||
|
|
||
|
@Injectable()
|
||
|
export class RoleService implements OnApplicationShutdown {
|
||
|
private rolesCache: Cache<Role[]>;
|
||
|
private roleAssignmentByUserIdCache: Cache<RoleAssignment[]>;
|
||
|
|
||
|
constructor(
|
||
|
@Inject(DI.redisSubscriber)
|
||
|
private redisSubscriber: Redis.Redis,
|
||
|
|
||
|
@Inject(DI.usersRepository)
|
||
|
private usersRepository: UsersRepository,
|
||
|
|
||
|
@Inject(DI.rolesRepository)
|
||
|
private rolesRepository: RolesRepository,
|
||
|
|
||
|
@Inject(DI.roleAssignmentsRepository)
|
||
|
private roleAssignmentsRepository: RoleAssignmentsRepository,
|
||
|
|
||
|
private metaService: MetaService,
|
||
|
) {
|
||
|
//this.onMessage = this.onMessage.bind(this);
|
||
|
|
||
|
this.rolesCache = new Cache<Role[]>(Infinity);
|
||
|
this.roleAssignmentByUserIdCache = new Cache<RoleAssignment[]>(Infinity);
|
||
|
|
||
|
this.redisSubscriber.on('message', this.onMessage);
|
||
|
}
|
||
|
|
||
|
@bindThis
|
||
|
private async onMessage(_: string, data: string): Promise<void> {
|
||
|
const obj = JSON.parse(data);
|
||
|
|
||
|
if (obj.channel === 'internal') {
|
||
|
const { type, body } = obj.message;
|
||
|
switch (type) {
|
||
|
case 'roleCreated': {
|
||
|
const cached = this.rolesCache.get(null);
|
||
|
if (cached) {
|
||
|
body.createdAt = new Date(body.createdAt);
|
||
|
body.updatedAt = new Date(body.updatedAt);
|
||
|
body.lastUsedAt = new Date(body.lastUsedAt);
|
||
|
cached.push(body);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case 'roleUpdated': {
|
||
|
const cached = this.rolesCache.get(null);
|
||
|
if (cached) {
|
||
|
const i = cached.findIndex(x => x.id === body.id);
|
||
|
if (i > -1) {
|
||
|
body.createdAt = new Date(body.createdAt);
|
||
|
body.updatedAt = new Date(body.updatedAt);
|
||
|
body.lastUsedAt = new Date(body.lastUsedAt);
|
||
|
cached[i] = body;
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case 'roleDeleted': {
|
||
|
const cached = this.rolesCache.get(null);
|
||
|
if (cached) {
|
||
|
this.rolesCache.set(null, cached.filter(x => x.id !== body.id));
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case 'userRoleAssigned': {
|
||
|
const cached = this.roleAssignmentByUserIdCache.get(body.userId);
|
||
|
if (cached) {
|
||
|
body.createdAt = new Date(body.createdAt);
|
||
|
cached.push(body);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
case 'userRoleUnassigned': {
|
||
|
const cached = this.roleAssignmentByUserIdCache.get(body.userId);
|
||
|
if (cached) {
|
||
|
this.roleAssignmentByUserIdCache.set(body.userId, cached.filter(x => x.id !== body.id));
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
default:
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@bindThis
|
||
|
public async getUserRoles(userId: User['id']) {
|
||
|
const assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
|
||
|
const assignedRoleIds = assigns.map(x => x.roleId);
|
||
|
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
|
||
|
return roles.filter(r => assignedRoleIds.includes(r.id));
|
||
|
}
|
||
|
|
||
|
@bindThis
|
||
|
public async getUserRoleOptions(userId: User['id'] | null): Promise<RoleOptions> {
|
||
|
const meta = await this.metaService.fetch();
|
||
|
const baseRoleOptions = { ...DEFAULT_ROLE, ...meta.defaultRoleOverride };
|
||
|
|
||
|
if (userId == null) return baseRoleOptions;
|
||
|
|
||
|
const roles = await this.getUserRoles(userId);
|
||
|
|
||
|
function getOptionValues(option: keyof RoleOptions) {
|
||
|
if (roles.length === 0) return [baseRoleOptions[option]];
|
||
|
return roles.map(role => (role.options[option] && (role.options[option].useDefault !== true)) ? role.options[option].value : baseRoleOptions[option]);
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
gtlAvailable: getOptionValues('gtlAvailable').some(x => x === true),
|
||
|
ltlAvailable: getOptionValues('ltlAvailable').some(x => x === true),
|
||
|
canPublicNote: getOptionValues('canPublicNote').some(x => x === true),
|
||
|
driveCapacityMb: Math.max(...getOptionValues('driveCapacityMb')),
|
||
|
antennaLimit: Math.max(...getOptionValues('antennaLimit')),
|
||
|
};
|
||
|
}
|
||
|
|
||
|
@bindThis
|
||
|
public async isModerator(user: { id: User['id']; isRoot: User['isRoot'] } | null): Promise<boolean> {
|
||
|
if (user == null) return false;
|
||
|
return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isModerator || r.isAdministrator);
|
||
|
}
|
||
|
|
||
|
@bindThis
|
||
|
public async isAdministrator(user: { id: User['id']; isRoot: User['isRoot'] } | null): Promise<boolean> {
|
||
|
if (user == null) return false;
|
||
|
return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isAdministrator);
|
||
|
}
|
||
|
|
||
|
@bindThis
|
||
|
public async getModeratorIds(includeAdmins = true): Promise<User['id'][]> {
|
||
|
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
|
||
|
const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator);
|
||
|
const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
|
||
|
roleId: In(moderatorRoles.map(r => r.id)),
|
||
|
}) : [];
|
||
|
// TODO: isRootなアカウントも含める
|
||
|
return assigns.map(a => a.userId);
|
||
|
}
|
||
|
|
||
|
@bindThis
|
||
|
public async getModerators(includeAdmins = true): Promise<User[]> {
|
||
|
const ids = await this.getModeratorIds(includeAdmins);
|
||
|
const users = ids.length > 0 ? await this.usersRepository.findBy({
|
||
|
id: In(ids),
|
||
|
}) : [];
|
||
|
return users;
|
||
|
}
|
||
|
|
||
|
@bindThis
|
||
|
public async getAdministratorIds(): Promise<User['id'][]> {
|
||
|
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
|
||
|
const administratorRoles = roles.filter(r => r.isAdministrator);
|
||
|
const assigns = administratorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
|
||
|
roleId: In(administratorRoles.map(r => r.id)),
|
||
|
}) : [];
|
||
|
// TODO: isRootなアカウントも含める
|
||
|
return assigns.map(a => a.userId);
|
||
|
}
|
||
|
|
||
|
@bindThis
|
||
|
public async getAdministrators(): Promise<User[]> {
|
||
|
const ids = await this.getAdministratorIds();
|
||
|
const users = ids.length > 0 ? await this.usersRepository.findBy({
|
||
|
id: In(ids),
|
||
|
}) : [];
|
||
|
return users;
|
||
|
}
|
||
|
|
||
|
@bindThis
|
||
|
public onApplicationShutdown(signal?: string | undefined) {
|
||
|
this.redisSubscriber.off('message', this.onMessage);
|
||
|
}
|
||
|
}
|