📁 Changed TDrive root folder (#16)

📁 Changed TDrive root folder
This commit is contained in:
Montassar Ghanmy
2023-04-11 10:04:29 +01:00
committed by GitHub
parent 3b3f67af07
commit e0615fa867
10548 changed files with 48 additions and 48 deletions
@@ -0,0 +1,62 @@
import { merge } from "lodash";
import { Column, Entity } from "../../../core/platform/services/database/services/orm/decorators";
import { CompanyFeaturesObject, CompanyLimitsObject } from "../web/types";
// backward compatibility with PHP where companies used to be `group_entity`
export const TYPE = "group_entity";
@Entity(TYPE, {
primaryKey: [["id"]],
type: TYPE,
})
export default class Company {
@Column("id", "timeuuid")
id: string;
@Column("name", "encoded_string")
name: string;
@Column("display_name", "encoded_string")
displayName: string;
@Column("plan", "encoded_json")
plan?: {
name: string;
limits: CompanyLimitsObject;
features: CompanyFeaturesObject;
};
@Column("stats", "encoded_json")
stats: any;
@Column("logo_id", "timeuuid")
logofile: string;
@Column("logo", "encoded_string")
logo: string;
@Column("date_added", "number")
dateAdded: number;
@Column("on_creation_data", "encoded_json")
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onCreationData: any;
@Column("member_count", "number")
memberCount: number;
@Column("identity_provider", "encoded_string")
identity_provider: string;
@Column("identity_provider_id", "encoded_string")
identity_provider_id: string;
}
export type CompanyPrimaryKey = Pick<Company, "id">;
export type CompanySearchKey = Partial<
Pick<Company, "id" | "identity_provider_id" | "identity_provider">
>;
export function getInstance(company: Partial<Company>): Company {
return merge(new Company(), company);
}
@@ -0,0 +1,63 @@
import { merge } from "lodash";
import { Column, Entity } from "../../../core/platform/services/database/services/orm/decorators";
import { CompanyUserRole } from "../web/types";
// backward compatibility with PHP where companies used to be `group`
export const TYPE = "group_user";
/**
* Link between a company and a user
*/
@Entity(TYPE, {
primaryKey: [["user_id"], "group_id", "id"],
type: TYPE,
})
export default class CompanyUser {
@Column("group_id", "timeuuid")
group_id: string;
@Column("user_id", "timeuuid")
user_id: string;
@Column("id", "timeuuid")
id: string;
@Column("role", "string")
role: CompanyUserRole = "member";
@Column("applications", "json")
applications: string[] = [];
@Column("nb_workspace", "number")
nbWorkspaces: number;
@Column("date_added", "number")
dateAdded: number;
@Column("last_update_day", "number")
lastUpdateDay: number;
/**
* 0: member,
* 1, 2, 3: admin,
*/
@Column("level", "number")
level: number; //Depreciated
@Column("is_externe", "tdrive_boolean")
isExterne: boolean; //Depreciated
@Column("did_connect_today", "tdrive_boolean")
didConnectToday: boolean; //Depreciated
@Column("app_used_today", "json")
appUsedToday: Array<string>; //Depreciated
}
export type CompanyUserPrimaryKey = Partial<Pick<CompanyUser, "group_id" | "user_id">>;
export function getInstance(
companyUser: Partial<CompanyUser> & CompanyUserPrimaryKey,
): CompanyUser {
return merge(new CompanyUser(), companyUser);
}
@@ -0,0 +1,32 @@
import { merge } from "lodash";
import { Column, Entity } from "../../../core/platform/services/database/services/orm/decorators";
import { uuid } from "../../../utils/types";
export const TYPE = "devices";
@Entity(TYPE, {
primaryKey: [["id"]],
type: TYPE,
})
export default class Device {
@Column("id", "string")
id: string;
@Column("user_id", "uuid")
user_id: uuid;
@Column("type", "string")
type: string;
@Column("version", "string")
version: string;
@Column("push_notifications", "boolean")
push_notifications: boolean;
}
export type UserDevicePrimaryKey = Pick<Device, "id">;
export function getInstance(userDevice: Partial<Device> & UserDevicePrimaryKey): Device {
return merge(new Device(), userDevice);
}
@@ -0,0 +1,27 @@
import { merge } from "lodash";
import { Column, Entity } from "../../../core/platform/services/database/services/orm/decorators";
export const TYPE = "external_group_repository";
@Entity(TYPE, {
primaryKey: [["service_id"], "external_id", "company_id"],
type: TYPE,
})
export default class ExternalGroup {
@Column("service_id", "string")
service_id: string;
@Column("external_id", "string")
external_id: string;
@Column("company_id", "string")
company_id: string;
}
export type ExternalGroupPrimaryKey = Pick<ExternalGroup, "service_id" | "external_id">;
export function getInstance(
group: Partial<ExternalGroup> & ExternalGroupPrimaryKey,
): ExternalGroup {
return merge(new ExternalGroup(), group);
}
@@ -0,0 +1,25 @@
import { merge } from "lodash";
import { Column, Entity } from "../../../core/platform/services/database/services/orm/decorators";
export const TYPE = "external_user_repository";
@Entity(TYPE, {
primaryKey: [["service_id"], "external_id", "user_id"],
type: TYPE,
})
export default class ExternalUser {
@Column("service_id", "string")
service_id: string;
@Column("external_id", "string")
external_id: string;
@Column("user_id", "timeuuid")
user_id: string;
}
export type ExternalUserPrimaryKey = Pick<ExternalUser, "service_id">;
export function getInstance(user: Partial<ExternalUser> & ExternalUserPrimaryKey): ExternalUser {
return merge(new ExternalUser(), user);
}
@@ -0,0 +1,43 @@
import User from "./user";
export default {
index: "user",
source: (entity: User) => {
const source: any = {
first_name: entity.first_name,
last_name: entity.last_name,
email: entity.email_canonical,
username: entity.username_canonical,
};
if (entity.cache?.companies) {
return {
companies: entity.cache?.companies,
...source,
};
}
return source;
},
mongoMapping: {
text: {
first_name: "text",
last_name: "text",
email: "text",
username: "text",
},
prefix: {
first_name: "prefix",
last_name: "prefix",
email: "prefix",
username: "prefix",
},
},
esMapping: {
properties: {
first_name: { type: "text", index_prefixes: { min_chars: 1 } },
last_name: { type: "text", index_prefixes: { min_chars: 1 } },
email: { type: "text", index_prefixes: { min_chars: 1 } },
username: { type: "text", index_prefixes: { min_chars: 1 } },
companies: { type: "keyword" },
},
},
};
@@ -0,0 +1,147 @@
import { isNumber, merge } from "lodash";
import { Column, Entity } from "../../../core/platform/services/database/services/orm/decorators";
import search from "./user.search";
import { uuid } from "../../../utils/types";
export const TYPE = "user";
@Entity(TYPE, {
primaryKey: [["id"]],
globalIndexes: [["email_canonical"], ["username_canonical"]],
type: TYPE,
search,
})
export default class User {
@Column("id", "timeuuid")
id: uuid;
@Column("first_name", "encoded_string")
first_name: string;
@Column("last_name", "encoded_string")
last_name: string;
@Column("picture", "encoded_string")
picture: string;
@Column("status_icon", "string")
private _status_icon: string; // = '["", ""]';
public get status_icon(): string {
if (this._status_icon && this._status_icon.startsWith("[\\")) {
try {
// eslint-disable-next-line quotes
const parsed = JSON.parse(this._status_icon.replace(/\\"/g, '"').replace(/\\\\/g, "\\"));
return `${parsed[0]} ${parsed[1]}`;
} catch (e) {
return "";
}
}
return this._status_icon;
}
public set status_icon(status: string) {
this._status_icon = status;
}
@Column("last_activity", "number")
last_activity: number;
@Column("creation_date", "tdrive_datetime")
creation_date: number;
@Column("notification_preference", "encoded_json")
// FIXME= Which type to use with encoded json? This is an object at the end
notification_preference: any;
@Column("identity_provider", "encoded_string")
identity_provider: string;
@Column("identity_provider_id", "encoded_string")
identity_provider_id: string;
@Column("token_login", "encoded_string")
token_login: string;
// TODO: Index
@Column("username_canonical", "string")
username_canonical: string;
// TODO: Index
@Column("email_canonical", "string")
email_canonical: string;
@Column("password", "string")
password: string;
@Column("deleted", "tdrive_boolean")
deleted: boolean;
@Column("mail_verified", "tdrive_boolean")
mail_verified: boolean;
@Column("phone", "string")
phone: string;
@Column("thumbnail_id", "timeuuid")
thumbnail_id: string;
@Column("language", "string")
language: string; //Depreciated (php legacy)
@Column("timezone", "string")
timezone: string; //Depreciated (php legacy)
@Column("preferences", "encoded_json")
preferences: null | {
locale?: string;
timezone?: number;
language?: string;
allow_tracking?: boolean;
tutorial_done?: boolean;
channel_ordering?: "chronological" | "alphabetical";
recent_workspaces?: { company_id: string; workspace_id: string }[];
knowledge_graph?: "all" | "nothing" | "metadata";
notifications?: UserNotificationPreferences[];
};
@Column("cache", "encoded_json")
cache: null | {
companies: string[];
};
@Column("devices", "encoded_json")
devices: Array<string>;
@Column("salt", "string")
salt: string;
constructor(id?: string) {
this.id = id;
}
}
export type UserNotificationPreferences = {
company_id: string | "all";
workspace_id: string | "all";
preferences: {
highlight_words?: string[];
night_break?: {
enable: boolean;
from: number;
to: number;
};
private_message_content?: boolean;
mobile_notifications?: "never" | "when_inactive" | "always";
email_notifications_delay?: number; //0: never send email, 1 and more in minutes from first unread notification
deactivate_notifications_until?: number;
notification_sound?: "default" | "none" | string;
};
};
export type UserPrimaryKey = Pick<User, "id">;
export function getInstance(user: Partial<User>): User {
user.creation_date = !isNumber(user.creation_date) ? Date.now() : user.creation_date;
return merge(new User(), user);
}
@@ -0,0 +1,23 @@
import { Prefix, TdriveService } from "../../core/platform/framework";
import WebServerAPI from "../../core/platform/services/webserver/provider";
import web from "./web";
@Prefix("/internal/services/users/v1")
export default class UserService extends TdriveService<undefined> {
version = "1";
name = "user";
public async doInit(): Promise<this> {
const fastify = this.context.getProvider<WebServerAPI>("webserver").getServer();
fastify.register((instance, _opts, next) => {
web(instance, { prefix: this.prefix });
next();
});
return this;
}
// TODO: remove
api(): undefined {
return undefined;
}
}
@@ -0,0 +1,45 @@
import { WebsocketMetadata } from "../../utils/types";
import CompanyEntity from "./entities/company";
import UserEntity from "./entities/user";
/**
* User Rooms
*/
export function getUserRooms(user: UserEntity): WebsocketMetadata[] {
return [
{
room: getUserRoom(user.id),
},
];
}
export function getUserRoom(userId: string): string {
return `/me/${userId}`;
}
export function getPublicUserRoom(userId: string): string {
return `/users/${userId}`;
}
export function getUserName(userId: string): string {
return `user-room-${userId}`;
}
/**
* Company Rooms
*/
export function getCompanyRooms(company: CompanyEntity): WebsocketMetadata[] {
return [
{
room: getCompanyRoom(company.id),
},
];
}
export function getCompanyRoom(companyId: string): string {
return `/company/${companyId}`;
}
export function getCompanyName(companyId: string): string {
return `company-room-${companyId}`;
}
@@ -0,0 +1,355 @@
import _, { merge } from "lodash";
import {
CrudException,
DeleteResult,
ExecutionContext,
ListResult,
OperationType,
Paginable,
Pagination,
SaveResult,
} from "../../../core/platform/framework/api/crud-service";
import Repository, {
FindOptions,
} from "../../../core/platform/services/database/services/orm/repository/repository";
import { UserPrimaryKey } from "../entities/user";
import Company, {
CompanyPrimaryKey,
CompanySearchKey,
getInstance as getCompanyInstance,
} from "../entities/company";
import CompanyUser, {
CompanyUserPrimaryKey,
getInstance as getCompanyUserInstance,
} from "../entities/company_user";
import { ListUserOptions } from "./users/types";
import { CompanyUserRole } from "../web/types";
import { ResourceEventsPayload, uuid } from "../../../utils/types";
import ExternalGroup, {
ExternalGroupPrimaryKey,
getInstance as getExternalGroupInstance,
} from "../entities/external_company";
import { logger, RealtimeSaved } from "../../../core/platform/framework";
import { getCompanyRoom, getUserRoom } from "../realtime";
import gr from "../../global-resolver";
import { localEventBus } from "../../../core/platform/framework/event-bus";
import {
KnowledgeGraphEvents,
KnowledgeGraphGenericEventPayload,
} from "../../../core/platform/services/knowledge-graph/types";
export class CompanyServiceImpl {
version: "1";
companyRepository: Repository<Company>;
externalCompanyRepository: Repository<ExternalGroup>;
companyUserRepository: Repository<CompanyUser>;
async init(): Promise<this> {
this.companyRepository = await gr.database.getRepository<Company>("group_entity", Company);
this.companyUserRepository = await gr.database.getRepository<CompanyUser>(
"group_user",
CompanyUser,
);
this.externalCompanyRepository = await gr.database.getRepository<ExternalGroup>(
"external_group_repository",
ExternalGroup,
);
return this;
}
private getExtCompany(pk: ExternalGroupPrimaryKey, context?: ExecutionContext) {
return this.externalCompanyRepository.findOne(pk, {}, context);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@RealtimeSaved<Company>((company, _context) => {
return [
{
room: getCompanyRoom(company.id),
resource: company,
},
];
})
async updateCompany<SaveOptions>(
company: Company,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_options?: SaveOptions,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
context?: ExecutionContext,
): Promise<SaveResult<Company>> {
if (company.identity_provider_id && !company.identity_provider) {
company.identity_provider = "console";
}
await this.companyRepository.save(company, context);
if (company.identity_provider_id) {
const key = {
service_id: company.identity_provider,
external_id: company.identity_provider_id,
};
const extCompany = (await this.getExtCompany(key, context)) || getExternalGroupInstance(key);
extCompany.company_id = company.id;
extCompany.external_id = company.identity_provider_id;
extCompany.service_id = company.identity_provider;
await this.externalCompanyRepository.save(extCompany, context);
}
localEventBus.publish<KnowledgeGraphGenericEventPayload<Company>>(
KnowledgeGraphEvents.COMPANY_UPSERT,
{
id: company.id,
resource: company,
links: [],
},
);
return new SaveResult<Company>("company", company, OperationType.UPDATE);
}
async createCompany(company: Company): Promise<Company> {
const companyToCreate: Company = getCompanyInstance({
...company,
...{
dateAdded: Date.now(),
},
});
const result = await this.updateCompany(companyToCreate);
return result.entity;
}
async getCompany(
companySearchKey: CompanySearchKey,
context?: ExecutionContext,
): Promise<Company> {
if (companySearchKey.id) {
return this.companyRepository.findOne(companySearchKey, {}, context);
} else if (companySearchKey.identity_provider_id) {
const extCompany = await this.getExtCompany(
{
external_id: companySearchKey.identity_provider_id,
service_id: companySearchKey.identity_provider || "console",
},
context,
);
if (!extCompany) {
return null;
}
return await this.companyRepository.findOne({ id: extCompany.company_id }, {}, context);
}
}
async getCompanyUser(
company: CompanyPrimaryKey,
user: UserPrimaryKey,
context?: ExecutionContext,
): Promise<CompanyUser> {
const companyUser = await this.companyUserRepository.findOne(
{
group_id: company.id,
user_id: user.id,
},
{},
context,
);
if (companyUser) companyUser.applications = [];
return companyUser;
}
async getAllForUser(userId: uuid, context?: ExecutionContext): Promise<CompanyUser[]> {
const list = await this.companyUserRepository
.find({ user_id: userId }, {}, context)
.then(a => a.getEntities());
// Update user cache with companies
const user = await gr.services.users.get({ id: userId });
if (
user.cache?.companies.length === 0 ||
_.difference(
list.map(c => c.group_id),
user.cache?.companies || [],
).length != 0
) {
if (!user.cache) user.cache = { companies: [] };
user.cache.companies = list.map(c => c.group_id);
await gr.services.users.save(user, { user: { id: user.id, server_request: true } });
}
return list;
}
getCompanies(paginable?: Paginable, context?: ExecutionContext): Promise<ListResult<Company>> {
return this.companyRepository.find(
{},
{
pagination: new Pagination(
paginable?.page_token,
paginable?.limitStr || "100",
paginable?.reversed,
),
},
context,
);
}
@RealtimeSaved<CompanyUser>((companyUser, _) => {
return [
{
room: getUserRoom(companyUser?.user_id),
resource: companyUser,
},
];
})
async removeUserFromCompany(
companyPk: CompanyPrimaryKey,
userPk: UserPrimaryKey,
context?: ExecutionContext,
): Promise<DeleteResult<CompanyUser>> {
const entity = await this.companyUserRepository.findOne(
{
group_id: companyPk.id,
user_id: userPk.id,
},
{},
context,
);
if (entity) {
await Promise.all([this.companyUserRepository.remove(entity, context)]);
const user = await gr.services.users.get(userPk);
if ((user.cache?.companies || []).includes(companyPk.id)) {
// Update user cache with companies
user.cache.companies = user.cache.companies.filter(id => id != companyPk.id);
await gr.services.users.save(user, { user: { id: user.id, server_request: true } });
}
localEventBus.publish<ResourceEventsPayload>("company:user:deleted", {
user: user,
company: companyPk,
});
}
return new DeleteResult("company_user", entity, true);
}
async getUsers(
companyId: CompanyUserPrimaryKey,
pagination?: Pagination,
options?: ListUserOptions,
context?: ExecutionContext,
): Promise<ListResult<CompanyUser>> {
const findOptions: FindOptions = {
pagination,
};
if (options?.userIds) {
findOptions.$in = [["user_id", options.userIds]];
}
return this.companyUserRepository.find({ group_id: companyId.group_id }, findOptions, context);
}
async delete(pk: CompanyPrimaryKey, context?: ExecutionContext): Promise<DeleteResult<Company>> {
const instance = await this.companyRepository.findOne(pk, {}, context);
if (instance) await this.companyRepository.remove(instance, context);
return new DeleteResult<Company>("company", instance, !!instance);
}
@RealtimeSaved<CompanyUser>((companyUser, _) => {
return [
{
room: getUserRoom(companyUser?.user_id),
resource: companyUser,
},
];
})
async setUserRole(
companyId: uuid,
userId: uuid,
role: CompanyUserRole = "member",
applications: string[] = [],
context?: ExecutionContext,
): Promise<SaveResult<CompanyUser>> {
const key = {
group_id: companyId,
user_id: userId,
};
let entity = await this.companyUserRepository.findOne(key, {}, context);
if (entity == null) {
entity = getCompanyUserInstance(merge(key, { dateAdded: Date.now() }));
}
entity.role = role;
entity.applications = applications;
await this.companyUserRepository.save(entity, context);
const user = await gr.services.users.get({ id: userId });
if (!(user.cache?.companies || []).includes(companyId)) {
// Update user cache with companies
if (!user.cache) user.cache = { companies: [] };
user.cache.companies.push(companyId);
await gr.services.users.save(user, { user: { id: user.id, server_request: true } });
}
return new SaveResult("company_user", entity, OperationType.UPDATE);
}
async removeCompany(searchKey: CompanySearchKey, context?: ExecutionContext): Promise<void> {
if (searchKey.identity_provider_id) {
const extCompany = await this.getExtCompany(
{
service_id: searchKey.identity_provider,
external_id: searchKey.identity_provider_id,
},
context,
);
if (!extCompany) {
throw CrudException.notFound(`Company ${searchKey.identity_provider_id} not found`);
}
await this.externalCompanyRepository.remove(extCompany, context);
searchKey.id = extCompany.company_id;
}
const company = await this.getCompany({ id: searchKey.id });
if (!company) {
throw CrudException.notFound(`Company ${searchKey.id} not found`);
}
await this.companyRepository.remove(company, context);
return Promise.resolve(null);
}
async getUsersCount(companyId: string): Promise<number> {
return this.getCompany({ id: companyId }).then(a => a.memberCount);
}
async getUserRole(companyId: uuid, userId: uuid): Promise<CompanyUserRole> {
if (!userId) return "guest";
const companyUser = await this.getCompanyUser({ id: companyId }, { id: userId });
if (!companyUser) {
return "guest";
}
return companyUser.role;
}
async ensureDeletedUserNotInCompanies(userPk: UserPrimaryKey): Promise<void> {
const user = await gr.services.users.get(userPk);
if (user.deleted) {
const companies = await this.getAllForUser(user.id);
for (const company of companies) {
logger.warn(`User ${userPk.id} is deleted so removed from company ${company.id}`);
await this.removeUserFromCompany(company, user);
await gr.services.workspaces.ensureUserNotInCompanyIsNotInWorkspace(userPk, company.id);
}
}
}
}
@@ -0,0 +1,66 @@
import Repository from "../../../../core/platform/services/database/services/orm/repository/repository";
import ExternalUser from "../../entities/external_user";
import ExternalGroup from "../../entities/external_company";
import Company from "../../entities/company";
import User from "../../entities/user";
import gr from "../../../global-resolver";
import { ExecutionContext } from "../../../../core/platform/framework/api/crud-service";
export class UserExternalLinksServiceImpl {
version: "1";
private externalUserRepository: Repository<ExternalUser>;
private externalGroupRepository: Repository<ExternalGroup>;
private companyRepository: Repository<Company>;
private userRepository: Repository<User>;
async init(): Promise<this> {
this.externalUserRepository = await gr.database.getRepository<ExternalUser>(
"external_user_repository",
ExternalUser,
);
this.externalGroupRepository = await gr.database.getRepository<ExternalGroup>(
"external_group_repository",
ExternalGroup,
);
this.companyRepository = await gr.database.getRepository<Company>("group_entity", Company);
this.userRepository = await gr.database.getRepository<User>("user", User);
return this;
}
async createExternalUser(user: ExternalUser, context?: ExecutionContext): Promise<ExternalUser> {
await this.externalUserRepository.save(user, context);
//Save user provider and provider id here
const internalUser = await this.userRepository.findOne({ id: user.user_id }, {}, context);
if (internalUser) {
internalUser.identity_provider = user.service_id;
internalUser.identity_provider_id = user.external_id;
this.userRepository.save(internalUser, context);
}
return user;
}
async createExternalGroup(
group: ExternalGroup,
context?: ExecutionContext,
): Promise<ExternalGroup> {
await this.externalGroupRepository.save(group, context);
//Save company provider and provider id here
const internalCompany = await this.companyRepository.findOne(
{ id: group.company_id },
{},
context,
);
if (internalCompany) {
internalCompany.identity_provider = group.service_id;
internalCompany.identity_provider_id = group.external_id;
this.companyRepository.save(internalCompany, context);
}
return group;
}
}
@@ -0,0 +1,394 @@
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/ban-ts-comment */
import {
CreateResult,
CrudException,
DeleteResult,
ExecutionContext,
ListResult,
OperationType,
Pagination,
SaveResult,
UpdateResult,
} from "../../../../core/platform/framework/api/crud-service";
import Repository, {
FindFilter,
FindOptions,
} from "../../../../core/platform/services/database/services/orm/repository/repository";
import User, { UserPrimaryKey } from "../../entities/user";
import { ListUserOptions, SearchUserOptions } from "./types";
import CompanyUser from "../../entities/company_user";
import SearchRepository from "../../../../core/platform/services/search/repository";
import ExternalUser, { getInstance as getExternalUserInstance } from "../../entities/external_user";
import Device, {
getInstance as getDeviceInstance,
TYPE as DeviceType,
} from "../../entities/device";
import PasswordEncoder from "../../../../utils/password-encoder";
import assert from "assert";
import { localEventBus } from "../../../../core/platform/framework/event-bus";
import { ResourceEventsPayload } from "../../../../utils/types";
import { isNumber, isString } from "lodash";
import { RealtimeSaved } from "../../../../core/platform/framework";
import { getPublicUserRoom, getUserRoom } from "../../realtime";
import NodeCache from "node-cache";
import gr from "../../../global-resolver";
import {
KnowledgeGraphEvents,
KnowledgeGraphGenericEventPayload,
} from "../../../../core/platform/services/knowledge-graph/types";
import { formatUser } from "../../../../utils/users";
export class UserServiceImpl {
version: "1";
repository: Repository<User>;
searchRepository: SearchRepository<User>;
companyUserRepository: Repository<CompanyUser>;
extUserRepository: Repository<ExternalUser>;
private deviceRepository: Repository<Device>;
private cache: NodeCache;
async init(): Promise<this> {
this.searchRepository = gr.platformServices.search.getRepository<User>("user", User);
this.repository = await gr.database.getRepository<User>("user", User);
this.companyUserRepository = await gr.database.getRepository<CompanyUser>(
"group_user",
CompanyUser,
);
this.extUserRepository = await gr.database.getRepository<ExternalUser>(
"external_user_repository",
ExternalUser,
);
this.deviceRepository = await gr.database.getRepository<Device>(DeviceType, Device);
this.cache = new NodeCache({ stdTTL: 0.2, checkperiod: 120 });
//If user deleted from Tdrive, remove it from all companies
localEventBus.subscribe<ResourceEventsPayload>("user:deleted", async data => {
if (data?.user?.id) gr.services.companies.ensureDeletedUserNotInCompanies(data.user);
});
return this;
}
private async updateExtRepository(user: User, context?: ExecutionContext) {
if (user.identity_provider_id) {
const key = { service_id: user.identity_provider, external_id: user.identity_provider_id };
const extUser =
(await this.extUserRepository.findOne(key, {}, context)) || getExternalUserInstance(key);
extUser.user_id = user.id;
await this.extUserRepository.save(extUser, context);
}
}
private assignDefaults(user: User) {
user.creation_date = !isNumber(user.creation_date) ? Date.now() : user.creation_date;
if (user.identity_provider_id && !user.identity_provider) user.identity_provider = "console";
if (user.email_canonical) user.email_canonical = user.email_canonical.toLocaleLowerCase();
if (user.username_canonical)
user.username_canonical = (user.username_canonical || "")
.toLocaleLowerCase()
.replace(/[^a-z0-9_-]/, "");
}
async create(user: User, context?: ExecutionContext): Promise<CreateResult<User>> {
await this.save(user, context);
return new CreateResult("user", user);
}
update(pk: Partial<User>, item: User, context?: ExecutionContext): Promise<UpdateResult<User>> {
throw new Error("Method not implemented.");
}
@RealtimeSaved<User>((user, _context) => {
return [
{
room: getPublicUserRoom(user.id),
resource: user,
},
];
})
async publishPublicUserRealtime(userId: string): Promise<void> {
const user = await this.get({ id: userId });
new SaveResult("user", formatUser(user, { includeCompanies: true }), OperationType.UPDATE);
}
@RealtimeSaved<User>((user, _context) => {
return [
{
room: getUserRoom(user.id),
resource: formatUser(user), // FIX ME we should formatUser here
},
];
})
async save<SaveOptions>(user: User, context?: ExecutionContext): Promise<SaveResult<User>> {
this.assignDefaults(user);
await this.repository.save(user, context);
await this.updateExtRepository(user);
localEventBus.publish<KnowledgeGraphGenericEventPayload<User>>(
KnowledgeGraphEvents.USER_UPSERT,
{
id: user.id,
resource: user,
links: [
{
// FIXME: We should provide the company id here
id: "",
relation: "parent",
type: "company",
},
],
},
);
await this.publishPublicUserRealtime(user.id);
return new SaveResult("user", user, OperationType.UPDATE);
}
async delete(pk: Partial<User>, context?: ExecutionContext): Promise<DeleteResult<User>> {
const instance = await this.repository.findOne(pk, {}, context);
if (instance) await this.repository.remove(instance, context);
return new DeleteResult<User>("user", instance, !!instance);
}
async anonymizeAndDelete(pk: UserPrimaryKey, context?: ExecutionContext) {
const user = await this.get(pk);
if (context.user.server_request || context.user.id === user.id) {
//We keep a part of the user id as new name
const partialId = user.id.toString().split("-")[0];
user.username_canonical = `deleted-user-${partialId}`;
user.email_canonical = `${partialId}@tdrive.removed`;
user.first_name = "";
user.last_name = "";
user.phone = "";
user.picture = "";
user.thumbnail_id = null;
user.status_icon = null;
user.deleted = true;
await this.save(user);
localEventBus.publish<ResourceEventsPayload>("user:deleted", {
user: user,
});
}
}
async search(
pagination: Pagination,
options?: SearchUserOptions,
context?: ExecutionContext,
): Promise<ListResult<User>> {
return await this.searchRepository.search(
{},
{
pagination,
...(options.companyId ? { $in: [["companies", [options.companyId]]] } : {}),
...(options.workspaceId ? { $in: [["workspaces", [options.workspaceId]]] } : {}),
$text: {
$search: options.search,
},
},
context,
);
}
async list(
pagination: Pagination,
options?: ListUserOptions,
context?: ExecutionContext,
): Promise<ListResult<User>> {
const findFilter: FindFilter = {};
const findOptions: FindOptions = {
pagination,
};
if (options?.userIds) {
findOptions.$in = [["id", options.userIds]];
}
return this.repository.find(findFilter, findOptions, context);
}
getByEmail(email: string, context?: ExecutionContext): Promise<User> {
return this.repository.findOne({ email_canonical: email }, {}, context);
}
getByEmails(emails: string[], context?: ExecutionContext): Promise<User[]> {
return Promise.all(emails.map(email => this.getByEmail(email))).then(emails =>
emails.filter(a => a),
);
}
async setPreferences(
pk: UserPrimaryKey,
preferences: User["preferences"],
context?: ExecutionContext,
): Promise<User["preferences"]> {
const user = await this.repository.findOne(pk, {}, context);
if (!user.preferences) user.preferences = {};
for (const key in preferences) {
//@ts-ignore
user.preferences[key] = preferences[key];
}
await this.save(user);
return user.preferences;
}
async get(pk: UserPrimaryKey, context?: ExecutionContext): Promise<User> {
return await this.repository.findOne(pk, {}, context);
}
async getCached(pk: UserPrimaryKey, context?: ExecutionContext): Promise<User> {
if (!(pk.id && isString(pk.id))) return null;
if (this.cache.has(pk.id)) return this.cache.get<User>(pk.id);
const entity = await this.get(pk);
this.cache.set<User>(pk.id, entity);
return entity;
}
async getByUsername(username: string, context?: ExecutionContext): Promise<User> {
return await this.repository.findOne(
{
username_canonical: (username || "").toLocaleLowerCase(),
},
{},
context,
);
}
async getByConsoleId(
id: string,
service_id: string = "console",
context?: ExecutionContext,
): Promise<User> {
const extUser = await this.extUserRepository.findOne(
{ service_id, external_id: id },
{},
context,
);
if (!extUser) {
return null;
}
return this.repository.findOne({ id: extUser.user_id }, {}, context);
}
async getUserCompanies(pk: UserPrimaryKey, context?: ExecutionContext): Promise<CompanyUser[]> {
return await this.companyUserRepository
.find({ user_id: pk.id }, {}, context)
.then(a => a.getEntities());
}
async isEmailAlreadyInUse(email: string, context?: ExecutionContext): Promise<boolean> {
return this.repository
.findOne({ email_canonical: email }, {}, context)
.then(user => Boolean(user));
}
async getAvailableUsername(username: string, context?: ExecutionContext): Promise<string> {
const user = await this.getByUsername(username);
if (!user) {
return username;
}
let suitableUsername = null;
for (let i = 1; i < 1000; i++) {
const dynamicUsername = username + i;
if (!(await this.getByUsername(dynamicUsername.toLocaleLowerCase()))) {
suitableUsername = dynamicUsername;
break;
}
}
return suitableUsername;
}
async getUserDevices(
userPrimaryKey: UserPrimaryKey,
context?: ExecutionContext,
): Promise<Device[]> {
const user = await this.get(userPrimaryKey);
if (!user) {
throw CrudException.notFound(`User ${userPrimaryKey} not found`);
}
if (!user.devices || user.devices.length == 0) {
return [];
}
return Promise.all(
user.devices.map(id => this.deviceRepository.findOne({ id }, {}, context)),
).then(a => a.filter(a => a));
}
async registerUserDevice(
userPrimaryKey: UserPrimaryKey,
id: string,
type: string,
version: string,
context?: ExecutionContext,
): Promise<void> {
await this.deregisterUserDevice(id);
const user = await this.get(userPrimaryKey);
if (!user) {
throw CrudException.notFound(`User ${userPrimaryKey} not found`);
}
user.devices = user.devices || [];
user.devices.push(id);
await this.repository.save(user, context);
await this.deviceRepository.save(
getDeviceInstance({ id, type, version, user_id: user.id }),
context,
);
}
async deregisterUserDevice(id: string, context?: ExecutionContext): Promise<void> {
const existedDevice = await this.deviceRepository.findOne({ id }, {}, context);
if (existedDevice) {
const user = await this.get({ id: existedDevice.user_id });
if (user) {
user.devices = (user.devices || []).filter(d => d !== id);
await this.repository.save(user, context);
}
await this.deviceRepository.remove(existedDevice, context);
}
}
async setPassword(
userPrimaryKey: UserPrimaryKey,
password: string,
context?: ExecutionContext,
): Promise<void> {
assert(password, "UserAPI.setPassword: Password is not defined");
const passwordEncoder = new PasswordEncoder();
const user = await this.get(userPrimaryKey);
if (!user) {
throw CrudException.notFound(`User ${userPrimaryKey.id} not found`);
}
user.password = await passwordEncoder.encodePassword(password);
user.salt = null;
await this.repository.save(user, context);
}
async getHashedPassword(
userPrimaryKey: UserPrimaryKey,
context?: ExecutionContext,
): Promise<[string, string]> {
const user = await this.get(userPrimaryKey);
if (!user) {
throw CrudException.notFound(`User ${userPrimaryKey.id} not found`);
}
if (user.salt) {
return [user.password, user.salt];
}
return [user.password, null];
}
}
@@ -0,0 +1,10 @@
export type ListUserOptions = {
userIds?: Array<string>;
};
export type SearchUserOptions = {
search?: string;
companyId?: string;
workspaceId?: string;
channelId?: string;
};
@@ -0,0 +1,4 @@
export type ListWorkspaceOptions = {
companiesIds?: string[];
company_id?: string;
};
@@ -0,0 +1,75 @@
import Company from "./entities/company";
import {
CompanyFeaturesEnum,
CompanyLimitsEnum,
CompanyObject,
CompanyStatsObject,
CompanyUserObject,
} from "./web/types";
export function formatCompany(
companyEntity: Company,
companyUserObject?: CompanyUserObject,
companyStats?: CompanyStatsObject,
): CompanyObject {
const res: CompanyObject = {
id: companyEntity.id,
name: companyEntity.name || "",
logo: companyEntity.logo || "",
plan: companyEntity.plan,
identity_provider: companyEntity.identity_provider,
identity_provider_id: companyEntity.identity_provider_id,
};
if (companyUserObject) {
res.status = "active"; // FIXME: Deactivated console user are removed from company on tdrive side
res.role = companyUserObject.role;
}
if (companyStats) {
res.stats = companyStats;
}
res.plan = {
name: res.plan?.name || "free",
limits: res.plan?.limits || {},
features: res.plan?.features || {},
};
res.plan.limits = Object.assign(
{
[CompanyLimitsEnum.CHAT_MESSAGE_HISTORY_LIMIT]: 10000,
[CompanyLimitsEnum.COMPANY_MEMBERS_LIMIT]: -1,
[CompanyLimitsEnum.COMPANY_GUESTS_LIMIT]: -1,
},
res.plan?.limits || {},
);
res.plan.features = Object.assign(
{
[CompanyFeaturesEnum.CHAT_GUESTS]: true,
[CompanyFeaturesEnum.CHAT_MESSAGE_HISTORY]: true,
[CompanyFeaturesEnum.CHAT_MULTIPLE_WORKSPACES]: true,
[CompanyFeaturesEnum.CHAT_EDIT_FILES]: true,
[CompanyFeaturesEnum.CHAT_UNLIMITED_STORAGE]: true,
[CompanyFeaturesEnum.COMPANY_INVITE_MEMBER]: true,
},
{
...(res.plan?.features || {}),
[CompanyFeaturesEnum.COMPANY_INVITE_MEMBER]:
res.plan?.limits[CompanyLimitsEnum.COMPANY_MEMBERS_LIMIT] <= 0 ||
res.stats.total_members < res.plan?.limits[CompanyLimitsEnum.COMPANY_MEMBERS_LIMIT],
},
);
return res;
}
export function getCompanyStats(company: Company, total_messages: number = 0): CompanyStatsObject {
return {
created_at: company.dateAdded,
total_members: company.stats?.total_members || 0,
total_guests: company.stats?.total_guests || 0,
total_messages,
};
}
@@ -0,0 +1,329 @@
import { FastifyReply, FastifyRequest } from "fastify";
import {
CrudException,
ExecutionContext,
ListResult,
Pagination,
} from "../../../core/platform/framework/api/crud-service";
import { uniq } from "lodash";
import { CrudController } from "../../../core/platform/services/webserver/types";
import {
ResourceCreateResponse,
ResourceDeleteResponse,
ResourceGetResponse,
ResourceListResponse,
} from "../../../utils/types";
import User from "../entities/user";
import {
CompanyObject,
CompanyParameters,
CompanyStatsObject,
CompanyUserObject,
DeregisterDeviceParams,
RegisterDeviceBody,
RegisterDeviceParams,
UserListQueryParameters,
UserObject,
UserParameters,
} from "./types";
import Company from "../entities/company";
import CompanyUser from "../entities/company_user";
import coalesce from "../../../utils/coalesce";
import { getCompanyRooms, getUserRooms } from "../realtime";
import { formatCompany, getCompanyStats } from "../utils";
import { formatUser } from "../../../utils/users";
import gr from "../../global-resolver";
import { UserChannel, UsersIncludedChannel } from "../../channels/entities";
export class UsersCrudController
implements
CrudController<
ResourceGetResponse<UserObject>,
ResourceCreateResponse<UserObject>,
ResourceListResponse<UserObject>,
ResourceDeleteResponse
>
{
async get(
request: FastifyRequest<{ Params: UserParameters }>,
_reply: FastifyReply,
): Promise<ResourceGetResponse<UserObject>> {
const context = getExecutionContext(request);
let id = request.params.id;
if (request.params.id === "me") {
id = context.user.id;
}
const user = await gr.services.users.get({ id: id });
if (!user) {
throw CrudException.notFound(`User ${id} not found`);
}
const userObject = await formatUser(user, {
includeCompanies: context.user.id === id,
});
return {
resource: userObject,
websocket: context.user.id
? gr.platformServices.realtime.sign(getUserRooms(user), context.user.id)[0]
: undefined,
};
}
async save(
request: FastifyRequest<{ Body: { resource: Partial<UserObject> }; Params: UserParameters }>,
reply: FastifyReply,
): Promise<ResourceCreateResponse<UserObject>> {
const context = getExecutionContext(request);
const user = await gr.services.users.get({ id: context.user.id });
if (!user) {
reply.notFound(`User ${context.user.id} not found`);
return;
}
user.status_icon = coalesce(request.body.resource, user.status_icon);
await gr.services.users.save(user, context);
return {
resource: await formatUser(user),
};
}
async setPreferences(
request: FastifyRequest<{ Body: User["preferences"] }>,
): Promise<User["preferences"]> {
const preferences = await gr.services.users.setPreferences(
{ id: request.currentUser.id },
request.body,
);
return preferences;
}
async list(
request: FastifyRequest<{ Querystring: UserListQueryParameters }>,
): Promise<ResourceListResponse<UserObject>> {
const context = getExecutionContext(request);
const userIds = request.query.user_ids ? request.query.user_ids.split(",") : [];
let users: ListResult<User>;
if (request.query.search) {
users = await gr.services.users.search(
new Pagination(request.query.page_token, request.query.limit),
{
search: request.query.search,
companyId: request.query.search_company_id,
},
context,
);
} else {
users = await gr.services.users.list(
new Pagination(request.query.page_token, request.query.limit),
{ userIds },
context,
);
}
const resUsers = await Promise.all(
users.getEntities().map(user =>
formatUser(user, {
includeCompanies: request.query.include_companies,
}),
),
);
// return users;
return {
resources: resUsers,
websockets: gr.platformServices.realtime.sign([], context.user.id),
};
}
async getUserCompanies(
request: FastifyRequest<{ Params: UserParameters }>,
_reply: FastifyReply,
): Promise<ResourceListResponse<CompanyObject>> {
const context = getExecutionContext(request);
const user = await gr.services.users.get({ id: request.params.id });
if (!user) {
throw CrudException.notFound(`User ${request.params.id} not found`);
}
const [currentUserCompanies, requestedUserCompanies] = await Promise.all(
[context.user.id, request.params.id].map(userId =>
gr.services.users.getUserCompanies({ id: userId }),
),
);
const currentUserCompaniesIds = new Set(currentUserCompanies.map(a => a.group_id));
const companiesCache = new Map<string, Company>();
const retrieveCompanyCached = async (companyId: string): Promise<Company> => {
const company =
companiesCache.get(companyId) ||
(await gr.services.companies.getCompany({ id: companyId }));
companiesCache.set(companyId, company);
return company;
};
const combos = (await Promise.all(
requestedUserCompanies
.filter(a => currentUserCompaniesIds.has(a.group_id))
.map((uc: CompanyUser) =>
retrieveCompanyCached(uc.group_id).then(async (c: Company) => [
c,
uc,
getCompanyStats(c, await gr.services.statistics.get(c.id, "messages")),
]),
),
)) as [Company, CompanyUserObject, CompanyStatsObject][];
return {
resources: combos.map(combo => formatCompany(...combo)),
websockets: gr.platformServices.realtime.sign([], context.user.id),
};
}
async getCompany(
request: FastifyRequest<{ Params: CompanyParameters }>,
_reply: FastifyReply,
): Promise<ResourceGetResponse<CompanyObject>> {
const company = await gr.services.companies.getCompany({ id: request.params.id });
const context = getExecutionContext(request);
if (!company) {
throw CrudException.notFound(`Company ${request.params.id} not found`);
}
let companyUserObj: CompanyUserObject | null = null;
if (context?.user?.id) {
const companyUser = await gr.services.companies.getCompanyUser(company, {
id: context.user.id,
});
companyUserObj = {
company: company,
role: companyUser.role,
status: "active",
};
}
return {
resource: formatCompany(
company,
companyUserObj,
getCompanyStats(company, await gr.services.statistics.get(company.id, "messages")),
),
websocket: context.user?.id
? gr.platformServices.realtime.sign(getCompanyRooms(company), context.user.id)[0]
: undefined,
};
}
async registerUserDevice(
request: FastifyRequest<{ Body: RegisterDeviceBody }>,
_reply: FastifyReply,
): Promise<ResourceGetResponse<RegisterDeviceParams>> {
const resource = request.body.resource;
if (resource.type !== "FCM") {
throw CrudException.badRequest("Type should be FCM only");
}
const context = getExecutionContext(request);
await gr.services.users.registerUserDevice(
{ id: context.user.id },
resource.value,
resource.type,
resource.version,
);
return {
resource: request.body.resource,
};
}
async getRegisteredDevices(
request: FastifyRequest<{ Params: UserParameters }>,
_reply: FastifyReply,
): Promise<ResourceListResponse<RegisterDeviceParams>> {
const context = getExecutionContext(request);
const userDevices = await gr.services.users.getUserDevices({ id: context.user.id });
return {
resources: userDevices.map(
ud => ({ type: ud.type, value: ud.id, version: ud.version } as RegisterDeviceParams),
),
};
}
async deregisterUserDevice(
request: FastifyRequest<{ Params: DeregisterDeviceParams }>,
reply: FastifyReply,
): Promise<ResourceDeleteResponse> {
const context = getExecutionContext(request);
const userDevices = await gr.services.users.getUserDevices({ id: context.user.id });
const device = await userDevices.find(ud => ud.id == request.params.value);
if (device) {
await gr.services.users.deregisterUserDevice(device.id);
}
reply.status(204);
return {
status: "success",
};
}
async recent(
request: FastifyRequest<{ Params: CompanyParameters; Querystring: { limit: 100 } }>,
_reply: FastifyReply,
): Promise<ResourceListResponse<UserObject>> {
const context = getExecutionContext(request);
const userId = context.user.id;
const companyId = request.params.id;
let channels: UserChannel[] = await gr.services.channels.channels
.getChannelsForUsersInWorkspace(companyId, "direct", userId, undefined, context)
.then(list => list.getEntities());
channels = channels.sort((a, b) => b.last_activity - a.last_activity);
channels = channels.slice(0, 100);
const userIncludedChannels: UsersIncludedChannel[] = await Promise.all(
channels.map(
channel =>
gr.services.channels.channels.includeUsersInDirectChannel(
channel,
userId,
) as Promise<UsersIncludedChannel>,
),
);
const users: UserObject[] = [];
for (const channel of userIncludedChannels) {
for (const user of channel.users) {
if (user.id != request.currentUser.id) users.push(user);
}
}
return {
resources: [...uniq(users).slice(0, request.query.limit)],
};
}
}
function getExecutionContext(request: FastifyRequest): ExecutionContext {
return {
user: request.currentUser,
url: request.url,
method: request.routerMethod,
transport: "http",
};
}
@@ -0,0 +1,13 @@
import { FastifyInstance, FastifyRegisterOptions } from "fastify";
import routes from "./routes";
export default (
fastify: FastifyInstance,
options: FastifyRegisterOptions<{
prefix: string;
}>,
): void => {
fastify.log.debug("Configuring /users routes");
fastify.register(routes, options);
};
@@ -0,0 +1,120 @@
import { FastifyInstance, FastifyPluginCallback } from "fastify";
import { UsersCrudController } from "./controller";
import {
deleteDeviceSchema,
getCompanySchema,
getDevicesSchema,
getUserCompaniesSchema,
getUserSchema,
getUsersSchema,
postDevicesSchema,
setUserPreferencesSchema,
} from "./schemas";
const usersUrl = "/users";
const routes: FastifyPluginCallback = (fastify: FastifyInstance, options, next) => {
const usersController = new UsersCrudController();
const accessControl = async () => {
// TODO: Handle Access Control
const authorized = true;
if (!authorized) {
throw fastify.httpErrors.badRequest("Invalid company/workspace");
}
};
fastify.route({
method: "GET",
url: `${usersUrl}/:id`,
preHandler: accessControl,
preValidation: [fastify.authenticate],
schema: getUserSchema,
handler: usersController.get.bind(usersController),
});
fastify.route({
method: "POST",
url: `${usersUrl}/me/preferences`,
preHandler: accessControl,
preValidation: [fastify.authenticate],
schema: setUserPreferencesSchema,
handler: usersController.setPreferences.bind(usersController),
});
fastify.route({
method: "POST",
url: `${usersUrl}/me`,
preHandler: accessControl,
preValidation: [fastify.authenticate],
schema: setUserPreferencesSchema,
handler: usersController.save.bind(usersController),
});
fastify.route({
method: "GET",
url: `${usersUrl}`,
preHandler: accessControl,
preValidation: [fastify.authenticate],
schema: getUsersSchema,
handler: usersController.list.bind(usersController),
});
// Get a list of companies for a user, only common companies with current user are returned.
fastify.route({
method: "GET",
url: `${usersUrl}/:id/companies`,
preHandler: accessControl,
preValidation: [fastify.authenticate],
schema: getUserCompaniesSchema, //Fixme currently not working because we don't know features in advances and so it doesn't pass
handler: usersController.getUserCompanies.bind(usersController),
});
//Get a company by id and public information (this route is public and doesn't need to be authenticated)
fastify.route({
method: "GET",
url: "/companies/:id",
preValidation: [fastify.authenticateOptional],
schema: getCompanySchema, //Fixme currently not working because we don't know features in advances and so it doesn't pass
handler: usersController.getCompany.bind(usersController),
});
fastify.route({
method: "POST",
url: "/devices",
preHandler: accessControl,
preValidation: [fastify.authenticate],
schema: postDevicesSchema,
handler: usersController.registerUserDevice.bind(usersController),
});
fastify.route({
method: "GET",
url: "/devices",
preHandler: accessControl,
preValidation: [fastify.authenticate],
schema: getDevicesSchema,
handler: usersController.getRegisteredDevices.bind(usersController),
});
fastify.route({
method: "DELETE",
url: "/devices/:value",
preHandler: accessControl,
preValidation: [fastify.authenticate],
schema: deleteDeviceSchema,
handler: usersController.deregisterUserDevice.bind(usersController),
});
// recent users the current user has interacted with
fastify.route({
method: "GET",
url: "/companies/:id/users/recent",
preValidation: [fastify.authenticateOptional],
handler: usersController.recent.bind(usersController),
});
next();
};
export default routes;
@@ -0,0 +1,247 @@
import { webSocketSchema } from "../../../utils/types";
import { CompanyFeaturesEnum, CompanyLimitsEnum } from "./types";
export const userObjectSchema = {
type: "object",
properties: {
id: { type: "string" },
provider: { type: "string" },
provider_id: { type: "string" },
email: { type: "string" },
username: { type: "string" },
is_verified: { type: "boolean" },
picture: { type: "string" },
first_name: { type: "string" },
last_name: { type: "string" },
created_at: { type: "number" },
deleted: { type: "boolean" },
status: { type: "string" },
last_activity: { type: "number" },
// cache: { type: ["object", "null"] },
cache: {
type: "object",
properties: {
companies: { type: ["array", "null"] },
},
},
//Below is only if this is myself
preferences: {
type: "object",
properties: {
tutorial_done: { type: ["boolean", "null"] },
channel_ordering: { type: ["string", "null"] },
recent_workspaces: { type: ["array", "null"] },
knowledge_graph: { type: ["string", "null"] },
locale: { type: ["string", "null"] },
timezone: { type: ["number", "null"] },
language: { type: ["string", "null"] },
allow_tracking: { type: ["boolean", "null"] },
},
},
companies: {
type: "array",
items: {
type: "object",
properties: {
role: { type: "string", enum: ["owner", "admin", "member", "guest"] },
status: { type: "string", enum: ["owner", "deactivated", "invited"] },
company: {
type: "object",
properties: {
id: { type: "string" },
name: { type: "string" },
logo: { type: "string" },
},
},
},
},
},
// TODO this is temporary, should be deleted
preference: {},
},
};
export const companyObjectSchema = {
type: "object",
properties: {
id: { type: "string" },
name: { type: "string" },
logo: { type: "string" },
plan: {
type: ["object", "null"],
properties: {
name: { type: "string" },
limits: {
type: ["object", "null"],
properties: {
[CompanyLimitsEnum.CHAT_MESSAGE_HISTORY_LIMIT]: { type: "number" },
[CompanyLimitsEnum.COMPANY_MEMBERS_LIMIT]: { type: "number" },
[CompanyLimitsEnum.COMPANY_GUESTS_LIMIT]: { type: "number" },
},
},
features: {
type: "object",
properties: {
[CompanyFeaturesEnum.CHAT_EDIT_FILES]: { type: ["boolean"] },
[CompanyFeaturesEnum.CHAT_GUESTS]: { type: ["boolean"] },
[CompanyFeaturesEnum.CHAT_MESSAGE_HISTORY]: { type: "boolean" },
[CompanyFeaturesEnum.CHAT_MULTIPLE_WORKSPACES]: { type: "boolean" },
[CompanyFeaturesEnum.CHAT_UNLIMITED_STORAGE]: { type: "boolean" },
[CompanyFeaturesEnum.COMPANY_INVITE_MEMBER]: { type: "boolean" },
guests: { type: "number" }, // to rename or delete
members: { type: "number" }, // to rename or delete
storage: { type: "number" }, // to rename or delete
},
required: [] as string[],
},
},
},
stats: {
type: ["object", "null"],
properties: {
created_at: { type: "number" },
total_members: { type: "number" },
total_guests: { type: "number" },
total_messages: { type: "number" },
},
},
identity_provider_id: { type: "string" },
identity_provider: { type: "string" },
role: { type: "string", enum: ["owner", "admin", "member", "guest"] },
status: { type: "string", enum: ["owner", "deactivated", "invited"] },
},
};
export const getUserSchema = {
response: {
"2xx": {
type: "object",
properties: {
resource: userObjectSchema,
websocket: webSocketSchema,
},
required: ["resource"],
},
},
};
export const setUserPreferencesSchema = {
request: {
properties: {
tutorial_done: { type: ["boolean", "null"] },
channel_ordering: { type: ["string", "null"] },
recent_workspaces: { type: ["array", "null"] },
knowledge_graph: { type: ["string", "null"] },
locale: { type: ["string", "null"] },
timezone: { type: ["number", "null"] },
language: { type: ["string", "null"] },
allow_tracking: { type: ["boolean", "null"] },
},
required: [] as any[],
},
response: {
"2xx": userObjectSchema.properties.preferences,
},
};
export const getUsersSchema = {
type: "object",
properties: {
user_ids: { type: "string" },
include_companies: { type: "boolean" },
},
response: {
"2xx": {
type: "object",
properties: {
resources: { type: "array", items: userObjectSchema },
},
required: ["resources"],
},
},
};
//Not used because it causes issues with the features json object
export const getUserCompaniesSchema = {
type: "object",
response: {
"2xx": {
type: "object",
properties: {
resources: { type: "array", items: companyObjectSchema },
},
required: ["resources"],
},
},
};
//Not used because it causes issues with the features json object
export const getCompanySchema = {
type: "object",
response: {
"2xx": {
type: "object",
properties: {
resource: companyObjectSchema,
websocket: webSocketSchema,
},
required: ["resource"],
},
},
};
const deviceSchema = {
type: "object",
properties: {
type: { type: "string" },
value: { type: "string" },
version: { type: "string" },
},
required: ["type", "value", "version"],
};
export const postDevicesSchema = {
type: "object",
body: {
type: "object",
properties: {
resource: deviceSchema,
},
},
required: ["resource"],
response: {
"2xx": {
type: "object",
properties: {
resource: deviceSchema,
},
required: ["resource"],
},
},
};
export const getDevicesSchema = {
response: {
"2xx": {
type: "object",
properties: {
resources: { type: "array", items: deviceSchema },
},
},
},
};
export const deleteDeviceSchema = {
response: {
"2xx": {
type: "object",
properties: {
success: { type: "boolean" },
},
},
},
};
@@ -0,0 +1,135 @@
import { PaginationQueryParameters } from "../../../utils/types";
import User from "../entities/user";
export interface UserListQueryParameters extends PaginationQueryParameters {
user_ids?: string;
include_companies?: boolean;
search?: string;
search_company_id?: string;
}
export interface UserParameters {
/* user id */
id: string;
}
export interface CompanyParameters {
/* company id */
id: string;
}
export interface UsersParameters {
ids?: string;
companies?: string;
}
export type CompanyUserRole = "owner" | "admin" | "member" | "guest";
export type CompanyUserStatus = "active" | "deactivated" | "invited";
export interface CompanyShort {
id: string; //Related to console "code"
name: string;
logo: string;
}
export interface CompanyUserObject {
company: CompanyShort;
role: CompanyUserRole;
status: CompanyUserStatus;
}
export interface UserObject {
id: string;
provider: string;
provider_id: string;
email: string;
username: string;
is_verified: boolean;
picture: string;
first_name: string;
last_name: string;
full_name: string;
created_at: number;
deleted: boolean;
status: string; //Single string for the status
last_activity: number;
last_seen?: number;
is_connected?: boolean;
cache: { companies: string[] };
//Below is only if this is myself
preferences?: User["preferences"];
companies?: CompanyUserObject[];
// TODO this is temporary, should be deleted
preference?: User["preferences"];
}
export enum CompanyLimitsEnum {
CHAT_MESSAGE_HISTORY_LIMIT = "chat:message_history_limit",
COMPANY_MEMBERS_LIMIT = "company:members_limit", // 100
COMPANY_GUESTS_LIMIT = "company:guests_limit",
}
export enum CompanyFeaturesEnum {
CHAT_GUESTS = "chat:guests",
CHAT_MESSAGE_HISTORY = "chat:message_history",
CHAT_MULTIPLE_WORKSPACES = "chat:multiple_workspaces",
CHAT_EDIT_FILES = "chat:edit_files",
CHAT_UNLIMITED_STORAGE = "chat:unlimited_storage",
COMPANY_INVITE_MEMBER = "company:invite_member",
}
export type CompanyFeaturesObject = {
[CompanyFeaturesEnum.CHAT_GUESTS]?: boolean;
[CompanyFeaturesEnum.CHAT_MESSAGE_HISTORY]?: boolean;
[CompanyFeaturesEnum.CHAT_MULTIPLE_WORKSPACES]?: boolean;
[CompanyFeaturesEnum.CHAT_EDIT_FILES]?: boolean;
[CompanyFeaturesEnum.CHAT_UNLIMITED_STORAGE]?: boolean;
[CompanyFeaturesEnum.COMPANY_INVITE_MEMBER]?: boolean;
};
export type CompanyLimitsObject = {
[CompanyLimitsEnum.CHAT_MESSAGE_HISTORY_LIMIT]?: number;
[CompanyLimitsEnum.COMPANY_MEMBERS_LIMIT]?: number;
[CompanyLimitsEnum.COMPANY_GUESTS_LIMIT]?: number;
};
export interface CompanyPlanObject {
name: string;
limits?: CompanyLimitsObject;
features?: CompanyFeaturesObject;
}
export interface CompanyStatsObject {
created_at: number;
total_members: number;
total_guests: number;
total_messages: number;
}
export interface CompanyObject {
id: string;
name: string;
logo: string;
plan?: CompanyPlanObject;
stats?: CompanyStatsObject;
role?: CompanyUserRole;
status?: CompanyUserStatus;
identity_provider: string;
identity_provider_id: string;
}
export interface RegisterDeviceBody {
resource: RegisterDeviceParams;
}
export interface RegisterDeviceParams {
type: "FCM";
value: string;
version: string;
}
export interface DeregisterDeviceParams {
value: "string";
}