@@ -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";
|
||||
}
|
||||
Reference in New Issue
Block a user