@@ -0,0 +1,7 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
|
||||
export default (a: any, b: any): any => {
|
||||
if (a === undefined) {
|
||||
return b;
|
||||
}
|
||||
return a;
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import gr from "../services/global-resolver";
|
||||
import CompanyUser from "../services/user/entities/company_user";
|
||||
|
||||
export function hasCompanyAdminLevel(role: string): boolean {
|
||||
return role === "admin" || isCompanyOwnerRole(role);
|
||||
}
|
||||
|
||||
export function hasCompanyMemberLevel(role: string): boolean {
|
||||
return role === "member" || hasCompanyAdminLevel(role);
|
||||
}
|
||||
|
||||
export function hasCompanyGuestLevel(role: string): boolean {
|
||||
return role === "guest" || hasCompanyMemberLevel(role);
|
||||
}
|
||||
|
||||
export function isCompanyOwnerRole(role: string): boolean {
|
||||
return role === "owner";
|
||||
}
|
||||
|
||||
export function isCompanyAdminRole(role: string): boolean {
|
||||
return role === "admin";
|
||||
}
|
||||
|
||||
export function isCompanyMemberRole(role: string): boolean {
|
||||
return role === "member";
|
||||
}
|
||||
|
||||
export function isCompanyGuestRole(role: string): boolean {
|
||||
return role === "guest";
|
||||
}
|
||||
|
||||
export async function checkUserBelongsToCompany(
|
||||
userId: string,
|
||||
companyId: string,
|
||||
): Promise<CompanyUser> {
|
||||
const companyUser = await gr.services.companies.getCompanyUser({ id: companyId }, { id: userId });
|
||||
|
||||
if (!companyUser) {
|
||||
const company = await gr.services.companies.getCompany({ id: companyId });
|
||||
if (!company) {
|
||||
throw gr.fastify.httpErrors.notFound(`Company ${companyId} not found`);
|
||||
}
|
||||
throw gr.fastify.httpErrors.forbidden("User does not belong to this company");
|
||||
}
|
||||
|
||||
return companyUser;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import Repository, {
|
||||
FindFilter,
|
||||
} from "../core/platform/services/database/services/orm/repository/repository";
|
||||
import { ExecutionContext, Pagination } from "../core/platform/framework/api/crud-service";
|
||||
import { isMatch } from "lodash";
|
||||
|
||||
export const countRepositoryItems = async (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
repository: Repository<any>,
|
||||
pk: FindFilter,
|
||||
filter: Record<string, unknown> = {},
|
||||
context?: ExecutionContext,
|
||||
): Promise<number> => {
|
||||
let pagination: Pagination = { limitStr: "100" };
|
||||
let total = 0;
|
||||
do {
|
||||
const listResult = await repository.find(pk, { pagination }, context);
|
||||
listResult.filterEntities(a => isMatch(a, filter));
|
||||
total += listResult.getEntities().length;
|
||||
pagination = listResult.nextPage as Pagination;
|
||||
} while (pagination.page_token);
|
||||
return total;
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
import { mkdirSync, existsSync, promises as fsPromise, createWriteStream, readFileSync } from "fs";
|
||||
import { Readable } from "stream";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
const { unlink } = fsPromise;
|
||||
|
||||
/**
|
||||
* Generates a random temporary file path
|
||||
*
|
||||
* @param {string} suffix - the file extension.
|
||||
* @returns {string} - the temporary file.
|
||||
*/
|
||||
export const getTmpFile = (suffix: string = ""): string => {
|
||||
const targetDir = "/tmp/";
|
||||
mkdirSync(targetDir, { recursive: true });
|
||||
return `${targetDir}${uuidv4()}${suffix}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes files from disk
|
||||
*
|
||||
* @param {string[]} paths - the paths to be deleted.
|
||||
*/
|
||||
export const cleanFiles = async (paths: string[]): Promise<void> => {
|
||||
for (const path of paths) {
|
||||
if (existsSync(path)) await unlink(path);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes a File stream into a temporary file path
|
||||
*
|
||||
* @param {Readable} input - the input stream.
|
||||
* @param {string} extension - the file extension.
|
||||
* @returns {Promise<string>} - the generated file.
|
||||
*/
|
||||
export const writeToTemporaryFile = async (input: Readable, extension: string): Promise<string> => {
|
||||
try {
|
||||
const temporaryFilePath = getTmpFile(`.${extension}`);
|
||||
|
||||
const writable = createWriteStream(temporaryFilePath);
|
||||
|
||||
input.pipe(writable);
|
||||
|
||||
await new Promise(r => {
|
||||
writable.on("finish", r);
|
||||
});
|
||||
|
||||
writable.end();
|
||||
|
||||
return temporaryFilePath;
|
||||
} catch (error) {
|
||||
console.debug(error);
|
||||
|
||||
throw Error(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reads a file from the disk
|
||||
*
|
||||
* @returns {Promise<Buffer>} - the file readable stream.
|
||||
*/
|
||||
export const readFromTemporaryFile = async (path: string): Promise<Buffer> => {
|
||||
return readFileSync(path);
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a readable stream into a Buffer.
|
||||
*
|
||||
* @param {Readable} input - the input stream.
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
export const readableToBuffer = async (input: Readable): Promise<Buffer> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const buffer: Uint8Array[] = [];
|
||||
|
||||
input.on("data", chunk => buffer.push(chunk));
|
||||
input.on("end", () => resolve(Buffer.concat(buffer)));
|
||||
input.on("error", err => reject(err));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a file readable stream to string
|
||||
*
|
||||
* @param {Readable} readable - the file stream
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export const readableToString = async (readable: Readable): Promise<string> => {
|
||||
let content = "";
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
readable.on("data", data => {
|
||||
content += data.toString();
|
||||
});
|
||||
|
||||
readable.on("end", () => {
|
||||
resolve(content);
|
||||
});
|
||||
|
||||
readable.on("error", error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { FastifyReply } from "fastify";
|
||||
import { HttpErrorCodes } from "fastify-sensible/lib/httpError";
|
||||
import { CrudException } from "../core/platform/framework/api/crud-service";
|
||||
|
||||
export function handleError(reply: FastifyReply, err: Error): void {
|
||||
if (err instanceof CrudException) {
|
||||
const crudException: CrudException = err;
|
||||
reply.getHttpError(crudException.status as HttpErrorCodes, crudException.message);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,82 @@
|
||||
import { CrudException } from "../core/platform/framework/api/crud-service";
|
||||
import crypto from "crypto";
|
||||
import assert from "assert";
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
export default class {
|
||||
private readonly algorithm = "sha512";
|
||||
private readonly encodeHashAsBase64 = true;
|
||||
private readonly iterations = 5000;
|
||||
private readonly encodedLength: number;
|
||||
|
||||
constructor() {
|
||||
this.encodedLength = this.encodePasswordOldWay("", "salt").length;
|
||||
}
|
||||
|
||||
protected mergePasswordAndSalt(password: string, salt?: string): string {
|
||||
if (!salt) {
|
||||
return password;
|
||||
}
|
||||
|
||||
return password + "{" + salt + "}";
|
||||
}
|
||||
|
||||
protected isPasswordTooLong(password: string): boolean {
|
||||
return password.length > 4096;
|
||||
}
|
||||
|
||||
public encodePassword(password: string): Promise<string> {
|
||||
assert(password, "Password is not defined for encodePassword");
|
||||
return bcrypt.hash(password, 10);
|
||||
}
|
||||
|
||||
public encodePasswordOldWay(raw: string, salt?: string): string {
|
||||
if (this.isPasswordTooLong(raw)) {
|
||||
throw CrudException.badRequest("Invalid password.");
|
||||
}
|
||||
const salted = Buffer.from(this.mergePasswordAndSalt(raw, salt));
|
||||
let digest = crypto.createHash(this.algorithm).update(salted).digest();
|
||||
for (let i = 1; i < this.iterations; ++i) {
|
||||
digest = crypto
|
||||
.createHash(this.algorithm)
|
||||
.update(Buffer.concat([digest, salted]))
|
||||
.digest();
|
||||
}
|
||||
return Buffer.from(digest).toString("base64");
|
||||
}
|
||||
|
||||
public isPasswordValid(encoded: string, raw: string, salt?: string): Promise<boolean> {
|
||||
if (salt) {
|
||||
// Old implementation
|
||||
return Promise.resolve().then(() => {
|
||||
if (encoded.length !== this.encodedLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.isPasswordTooLong(raw) &&
|
||||
this.hashEquals(encoded, this.encodePasswordOldWay(raw, salt))
|
||||
);
|
||||
});
|
||||
} else {
|
||||
return bcrypt.compare(raw, encoded);
|
||||
}
|
||||
}
|
||||
|
||||
private hashEquals = (answer: string, guess: string) => {
|
||||
assert(
|
||||
typeof answer === "string" && typeof guess === "string",
|
||||
"both arguments should be strings",
|
||||
);
|
||||
|
||||
const rb = crypto.pseudoRandomBytes(32);
|
||||
const ahmac = crypto.createHmac("sha256", rb).update(answer).digest("hex");
|
||||
const ghmac = crypto.createHmac("sha256", rb).update(guess).digest("hex");
|
||||
const len = ahmac.length;
|
||||
let result = 0;
|
||||
for (let i = 0; i < len; ++i) {
|
||||
result |= ahmac.charCodeAt(i) ^ ghmac.charCodeAt(i);
|
||||
}
|
||||
return result === 0;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Picks properties from an input object and return them in a new object
|
||||
*
|
||||
* @param obj
|
||||
* @param keys
|
||||
*/
|
||||
export function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> {
|
||||
return keys.reduce((o, k) => ((o[k] = obj[k]), o), {} as Pick<T, K>);
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Common types for business services
|
||||
*/
|
||||
|
||||
import { WorkspacePrimaryKey } from "../services/workspaces/entities/workspace";
|
||||
import {
|
||||
Channel as ChannelEntity,
|
||||
ChannelMember,
|
||||
ChannelPendingEmails,
|
||||
ChannelTab,
|
||||
} from "../services/channels/entities";
|
||||
import { ChannelParameters } from "../services/channels/web/types";
|
||||
|
||||
export type uuid = string;
|
||||
|
||||
export type Maybe<T> = T | undefined;
|
||||
|
||||
/**
|
||||
* User in platform:
|
||||
*
|
||||
* {
|
||||
* id: "uuid",
|
||||
* }
|
||||
*/
|
||||
export interface User {
|
||||
// unique user id
|
||||
id: uuid;
|
||||
// unique console user id
|
||||
identity_provider_id?: uuid;
|
||||
// user email
|
||||
email?: string;
|
||||
// server request
|
||||
server_request?: boolean; //Set to true if request if from the user, can be used to cancel any access restriction
|
||||
// application call
|
||||
application_id?: string;
|
||||
// allow_tracking
|
||||
allow_tracking?: boolean;
|
||||
}
|
||||
|
||||
export const webSocketSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
room: { type: "string" },
|
||||
token: { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
export interface Channel extends Workspace {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export enum ChannelType {
|
||||
DIRECT = "direct",
|
||||
}
|
||||
|
||||
export interface Workspace {
|
||||
company_id: string;
|
||||
workspace_id: string;
|
||||
}
|
||||
|
||||
export interface WebsocketMetadata {
|
||||
room: string;
|
||||
name?: string;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
export class ResourceListResponse<T> {
|
||||
resources: T[];
|
||||
websockets?: ResourceWebsocket[];
|
||||
next_page_token?: string;
|
||||
total?: number;
|
||||
}
|
||||
|
||||
export class ResourceGetResponse<T> {
|
||||
websocket?: ResourceWebsocket;
|
||||
resource: T;
|
||||
}
|
||||
export class ResourceUpdateResponse<T> {
|
||||
websocket?: ResourceWebsocket;
|
||||
resource: T;
|
||||
}
|
||||
export class ResourceCreateResponse<T> {
|
||||
websocket?: ResourceWebsocket;
|
||||
resource: T;
|
||||
}
|
||||
|
||||
export class ResourceDeleteResponse {
|
||||
status: DeleteStatus;
|
||||
}
|
||||
|
||||
export interface ResourceListQueryParameters extends PaginationQueryParameters {
|
||||
search_query?: string;
|
||||
mine?: boolean;
|
||||
}
|
||||
|
||||
export declare type DeleteStatus = "success" | "error";
|
||||
export interface ResourceWebsocket {
|
||||
room: string;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
export interface ResourceEventsPayload {
|
||||
user: User;
|
||||
channel?: ChannelEntity;
|
||||
channelParameters?: ChannelParameters;
|
||||
guest?: ChannelPendingEmails;
|
||||
member?: ChannelMember;
|
||||
actor?: User;
|
||||
resourcesBefore?: (User | ChannelEntity | ChannelTab | ChannelMember)[];
|
||||
resourcesAfter?: (User | ChannelEntity | ChannelTab | ChannelMember)[];
|
||||
tab?: ChannelTab;
|
||||
company?: { id: string };
|
||||
workspace?: WorkspacePrimaryKey;
|
||||
}
|
||||
|
||||
export interface PaginationQueryParameters {
|
||||
page_token?: string;
|
||||
limit?: string;
|
||||
websockets?: boolean;
|
||||
}
|
||||
|
||||
export interface AccessToken {
|
||||
time: number;
|
||||
expiration: number;
|
||||
refresh_expiration: number;
|
||||
value: string;
|
||||
refresh: string;
|
||||
type: "Bearer";
|
||||
}
|
||||
|
||||
export interface JWTObject {
|
||||
exp: number;
|
||||
type: string;
|
||||
iat: number;
|
||||
nbf: number;
|
||||
sub: string;
|
||||
email: string;
|
||||
track: boolean;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import User from "../services/user/entities/user";
|
||||
import {
|
||||
CompanyShort,
|
||||
CompanyUserObject,
|
||||
CompanyUserRole,
|
||||
CompanyUserStatus,
|
||||
UserObject,
|
||||
} from "../services/user/web/types";
|
||||
import gr from "../services/global-resolver";
|
||||
|
||||
export async function formatUser(
|
||||
user: User,
|
||||
options?: { includeCompanies?: boolean },
|
||||
): Promise<UserObject> {
|
||||
if (!user) return null;
|
||||
|
||||
let resUser = {
|
||||
id: user.id,
|
||||
provider: user.identity_provider,
|
||||
provider_id: user.identity_provider_id,
|
||||
email: user.email_canonical,
|
||||
username: user.username_canonical,
|
||||
is_verified: Boolean(user.mail_verified),
|
||||
picture: user.picture,
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
full_name: [user.first_name, user.last_name].join(" "),
|
||||
created_at: user.creation_date,
|
||||
deleted: Boolean(user.deleted),
|
||||
status: user.status_icon,
|
||||
last_activity: user.last_activity,
|
||||
cache: { companies: user.cache?.companies || [] },
|
||||
} as UserObject;
|
||||
|
||||
const userOnline = await gr.services.online.get({ user_id: user.id });
|
||||
if (userOnline) {
|
||||
const { last_seen, is_connected } = userOnline;
|
||||
|
||||
resUser = {
|
||||
...resUser,
|
||||
last_seen,
|
||||
is_connected,
|
||||
last_activity: last_seen,
|
||||
};
|
||||
}
|
||||
|
||||
if (options?.includeCompanies) {
|
||||
const userCompanies = await gr.services.users.getUserCompanies({ id: user.id });
|
||||
|
||||
const companies = await Promise.all(
|
||||
userCompanies.map(async uc => {
|
||||
const company = await gr.services.companies.getCompany({ id: uc.group_id });
|
||||
return {
|
||||
role: uc.role as CompanyUserRole,
|
||||
status: "active" as CompanyUserStatus, // FIXME: with real status
|
||||
company: {
|
||||
id: uc.group_id,
|
||||
name: company.name,
|
||||
logo: company.logo,
|
||||
} as CompanyShort,
|
||||
} as CompanyUserObject;
|
||||
}),
|
||||
);
|
||||
|
||||
resUser = {
|
||||
...resUser,
|
||||
preferences: {
|
||||
...user.preferences,
|
||||
locale: user.preferences?.language || user.language || "en",
|
||||
timezone: user.preferences?.timezone || parseInt(user.timezone) || 0,
|
||||
allow_tracking: user.preferences?.allow_tracking || false,
|
||||
},
|
||||
|
||||
companies,
|
||||
};
|
||||
|
||||
// Fixme: this is for retro compatibility, should be deleted after march 2022 if mobile did implement it https://github.com/linagora/Tdrive-Mobile/issues/1265
|
||||
resUser.preference = resUser.preferences;
|
||||
|
||||
let name: string = resUser?.username;
|
||||
if (!name) {
|
||||
resUser.full_name = "Anonymous";
|
||||
} else {
|
||||
if (resUser.deleted) {
|
||||
name = "Deleted user";
|
||||
} else {
|
||||
name = [resUser.first_name, resUser.last_name].filter(a => a).join(" ");
|
||||
name = name || resUser.username;
|
||||
}
|
||||
resUser.full_name = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
return resUser;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export const reduceUUID4 = function (id: string): string {
|
||||
if (!id) return undefined;
|
||||
|
||||
return id
|
||||
.replace(/(.)\1{2,3}/g, "$1i")
|
||||
.replace(/(.)\1{1,2}/g, "$1h")
|
||||
.replace(/-/g, "g");
|
||||
};
|
||||
|
||||
export const expandUUID4 = function (id: string): string {
|
||||
if (!id) return undefined;
|
||||
|
||||
return (
|
||||
id
|
||||
.replace(/(.)i/g, "$1$1$1")
|
||||
.replace(/(.)h/g, "$1$1")
|
||||
.replace(/[^0-9a-g]/g, "")
|
||||
.replace(/g/g, "-") || undefined
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
export function compareTimeuuid(a?: string, b?: string): number {
|
||||
return timeuuidToDate(a || "") - timeuuidToDate(b || "");
|
||||
}
|
||||
|
||||
export function timeuuidToDate(time_str: string): number {
|
||||
if (!time_str) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uuid_arr = time_str.split("-");
|
||||
const time_string = [uuid_arr[2].substring(1), uuid_arr[1], uuid_arr[0]].join("");
|
||||
|
||||
return parseInt(time_string, 16);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { User, Workspace } from "./types";
|
||||
import WorkspaceUser from "../services/workspaces/entities/workspace_user";
|
||||
import { hasCompanyAdminLevel } from "./company";
|
||||
import gr from "../services/global-resolver";
|
||||
|
||||
export async function isWorkspaceAdmin(user?: User, workspace?: Workspace): Promise<boolean> {
|
||||
if (!user || !workspace) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const companyUser = await gr.services.companies.getCompanyUser(
|
||||
{ id: workspace.company_id },
|
||||
{ id: user.id },
|
||||
);
|
||||
if (companyUser && hasCompanyAdminLevel(companyUser.role)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const workspaceUser = await getWorkspaceUser(user, workspace);
|
||||
|
||||
if (!workspaceUser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return workspaceUser.role === "moderator";
|
||||
}
|
||||
|
||||
export function hasWorkspaceAdminLevel(role: string, companyRole: string): boolean {
|
||||
return role === "moderator" || hasCompanyAdminLevel(companyRole);
|
||||
}
|
||||
|
||||
export function hasWorkspaceMemberLevel(role: string, companyRole: string): boolean {
|
||||
return role === "member" || hasWorkspaceAdminLevel(role, companyRole);
|
||||
}
|
||||
|
||||
export async function getWorkspaceUser(user?: User, workspace?: Workspace): Promise<WorkspaceUser> {
|
||||
if (!user || !workspace) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const workspaceUser = await gr.services.workspaces.getUser({
|
||||
workspaceId: workspace.workspace_id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
return workspaceUser;
|
||||
}
|
||||
|
||||
// export async function getCompanyUser(user?: User, company?: Company): Promise<CompanyUser> {
|
||||
// if (!user || !company) {
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// const companyUser = await gr.services.companies.getCompanyUser(
|
||||
// {
|
||||
// id: company.id,
|
||||
// },
|
||||
// {
|
||||
// id: user.id,
|
||||
// },
|
||||
// );
|
||||
//
|
||||
// return companyUser;
|
||||
// }
|
||||
Reference in New Issue
Block a user