import { Dictionary } from 'lodash'; import { FilterQuery } from 'mongodb'; import { BUILTIN_ROLES, PRIV } from './builtin'; import { DomainDoc } from '../interface'; import { ArgMethod } from '../utils'; import { NumberKeys } from '../typeutils'; import * as bus from '../service/bus'; import db from '../service/db'; const coll = db.collection('domain'); const collUser = db.collection('domain.user'); interface DomainUserArg { _id: number, priv: number, } class DomainModel { static JOIN_METHOD_NONE = 0; static JOIN_METHOD_ALL = 1; static JOIN_METHOD_CODE = 2; static JOIN_METHOD_RANGE = { [DomainModel.JOIN_METHOD_NONE]: 'No user is allowed to join this domain', [DomainModel.JOIN_METHOD_ALL]: 'Any user is allowed to join this domain', [DomainModel.JOIN_METHOD_CODE]: 'Any user is allowed to join this domain with an invitation code', }; static JOIN_EXPIRATION_KEEP_CURRENT = 0; static JOIN_EXPIRATION_UNLIMITED = -1; static JOIN_EXPIRATION_RANGE = { [DomainModel.JOIN_EXPIRATION_KEEP_CURRENT]: 'Keep current expiration', 3: 'In 3 hours', 24: 'In 1 day', [24 * 3]: 'In 3 days', [24 * 7]: 'In 1 week', [24 * 30]: 'In 1 month', [DomainModel.JOIN_EXPIRATION_UNLIMITED]: 'Never expire', }; @ArgMethod static async add(domainId: string, owner: number, name: string, bulletin: string) { const ddoc: DomainDoc = { _id: domainId, owner, name, bulletin, roles: {}, gravatar: '', pidCounter: 0, }; await bus.serial('domain/create', ddoc); await coll.insertOne(ddoc); return domainId; } @ArgMethod static async get(domainId: string): Promise { const query: FilterQuery = { _id: domainId }; await bus.serial('domain/before-get', query); const result = await coll.findOne(query); await bus.serial('domain/get', result); return result; } @ArgMethod static async getByHost(host: string): Promise { const query: FilterQuery = { host }; await bus.serial('domain/before-get', query); const result = await coll.findOne(query); await bus.serial('domain/get', result); return result; } static getMulti(query: FilterQuery = {}) { return coll.find(query); } static async edit(domainId: string, $set: Partial) { await bus.serial('domain/before-update', domainId, $set); const result = await coll.findOneAndUpdate({ _id: domainId }, { $set }, { returnOriginal: false }); await bus.serial('domain/update', domainId, $set, result.value); return result.value; } @ArgMethod static async inc(domainId: string, field: NumberKeys, n: number): Promise { const res = await coll.findOneAndUpdate( { _id: domainId }, // FIXME // @ts-expect-error { $inc: { [field]: n } }, { returnOriginal: false }, ); return res.value[field]; } @ArgMethod static async getList(domainIds: string[]): Promise> { const r = {}; // eslint-disable-next-line no-await-in-loop for (const domainId of domainIds) r[domainId] = await DomainModel.get(domainId); return r; } @ArgMethod static setUserRole(domainId: string, uid: number, role: string) { return collUser.updateOne({ uid, domainId }, { $set: { role } }, { upsert: true }); } static async getRoles(domainId: string): Promise; static async getRoles(domain: DomainDoc): Promise; @ArgMethod static async getRoles(arg: string | DomainDoc) { let ddoc: DomainDoc; if (typeof arg === 'string') ddoc = await DomainModel.get(arg); else ddoc = arg; const roles = []; const r = []; for (const role in ddoc.roles) { roles.push({ _id: role, perm: BigInt(ddoc.roles[role]) }); r.push(role); } for (const role in BUILTIN_ROLES) { if (!r.includes(role)) { roles.push({ _id: role, perm: BUILTIN_ROLES[role] }); } } return roles; } static async setRoles(domainId: string, roles: Dictionary) { const current = await DomainModel.get(domainId); for (const role in roles) { current.roles[role] = roles[role].toString(); } return await coll.updateOne({ _id: domainId }, { $set: { roles: current.roles } }); } static async addRole(domainId: string, name: string, permission: bigint) { const current = await DomainModel.get(domainId); current.roles[name] = permission.toString(); return await coll.updateOne({ _id: domainId }, { $set: { roles: current.roles } }); } static async deleteRoles(domainId: string, roles: string[]) { const current = await DomainModel.get(domainId); for (const role of roles) delete current.roles[role]; await Promise.all([ coll.updateOne({ _id: domainId }, { $set: current }), collUser.updateMany({ domainId, role: { $in: roles } }, { $set: { role: 'default' } }), ]); } static async getDomainUser(domainId: string, udoc: DomainUserArg) { let dudoc = await collUser.findOne({ domainId, uid: udoc._id }); dudoc = dudoc || {}; if (!(udoc.priv & PRIV.PRIV_USER_PROFILE)) dudoc.role = 'guest'; if (udoc.priv & PRIV.PRIV_MANAGE_ALL_DOMAIN) dudoc.role = 'admin'; dudoc.role = dudoc.role || 'default'; const ddoc = await DomainModel.get(domainId); if (ddoc.owner === udoc._id) dudoc.role = 'admin'; dudoc.perm = ddoc.roles[dudoc.role] ? BigInt(ddoc.roles[dudoc.role]) : BUILTIN_ROLES[dudoc.role]; return dudoc; } static setMultiUserInDomain(domainId: string, query: any, params: any) { return collUser.updateMany({ domainId, ...query }, { $set: params }); } static getMultiUserInDomain(domainId: string, query: any = {}) { return collUser.find({ domainId, ...query }); } static setUserInDomain(domainId: string, uid: number, params: any) { return collUser.updateOne({ domainId, uid }, { $set: params }); } @ArgMethod static async incUserInDomain(domainId: string, uid: number, field: string, n: number = 1) { // @ts-ignore const dudoc = await DomainModel.getDomainUser(domainId, { _id: uid }); dudoc[field] = dudoc[field] + n || n; await DomainModel.setUserInDomain(domainId, uid, { [field]: dudoc[field] }); return dudoc; } @ArgMethod static async getDictUserByDomainId(uid: number) { const [dudocs, ddocs] = await Promise.all([ collUser.find({ uid }).toArray(), coll.find({ owner: uid }).toArray(), ]); const dudict = {}; for (const dudoc of dudocs) { // eslint-disable-next-line no-await-in-loop dudict[dudoc.domainId] = await DomainModel.get(dudoc.domainId); dudict[dudoc.domainId].role = dudoc.role; } for (const ddoc of ddocs) dudict[ddoc._id] = 'admin'; return dudict; } static getJoinSettings(ddoc: DomainDoc, roles: string[]) { if (!ddoc.join) return null; const joinSettings = ddoc.join; if (joinSettings.method === DomainModel.JOIN_METHOD_NONE) return null; if (!roles.includes(joinSettings.role)) return null; if (joinSettings.expire && joinSettings.expire < new Date()) return null; return joinSettings; } @ArgMethod static async getPrefixSearch(prefix: string, limit: number = 50) { const $regex = new RegExp(prefix, 'mi'); const ddocs = await coll.find({ $or: [{ _id: { $regex } }, { name: { $regex } }], }).limit(limit).toArray(); return ddocs; } } export = DomainModel; global.Hydro.model.domain = DomainModel;