📁 Changed TDrive root folder (#16)

📁 Changed TDrive root folder
This commit is contained in:
Montassar Ghanmy
2023-04-11 10:04:29 +01:00
committed by GitHub
parent 3b3f67af07
commit e0615fa867
10548 changed files with 48 additions and 48 deletions
@@ -0,0 +1,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;
};
+47
View File
@@ -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;
}
+23
View File
@@ -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;
};
+106
View File
@@ -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;
};
}
+9
View File
@@ -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>);
}
+140
View File
@@ -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;
}
+95
View File
@@ -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
);
};
+14
View File
@@ -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;
// }