Initial commit: Angular frontend and Expressjs backend

This commit is contained in:
chamikaJ
2024-05-17 09:32:30 +05:30
parent eb0a0d77d6
commit 298ca6beeb
3548 changed files with 193558 additions and 3 deletions

View File

@@ -0,0 +1,133 @@
export const DUPLICATE_KEY_VALUE = "23505";
export const FOREIGN_KEY_VIOLATION = "23503";
export const DEFAULT_ERROR_MESSAGE = "Unknown error has occurred.";
export const SessionsStatus = {
IDLE: "IDLE",
STARTED: "STARTED",
ENDED: "ENDED"
};
export const LOG_DESCRIPTIONS = {
PROJECT_CREATED: "Project created by @user",
PROJECT_UPDATED: "Project updated by @user",
TASK_CREATED: "Task created by @user",
TASK_UPDATED: "Task updated by @user",
PROJECT_MEMBER_ADDED: "was added to the project by",
PROJECT_MEMBER_REMOVED: "was removed from the project by",
};
export const WorklenzColorCodes = [
"#154c9b",
"#3b7ad4",
"#70a6f3",
"#7781ca",
"#9877ca",
"#c178c9",
"#ee87c5",
"#ca7881",
"#75c9c0",
"#75c997",
"#80ca79",
"#aacb78",
"#cbbc78",
"#cb9878",
"#bb774c",
"#905b39",
"#903737",
"#bf4949",
"#f37070",
"#ff9c3c",
"#fbc84c",
"#cbc8a1",
"#a9a9a9",
"#767676"
];
export const AvatarNamesMap: { [x: string]: string } = {
"A": "#154c9b",
"B": "#3b7ad4",
"C": "#70a6f3",
"D": "#7781ca",
"E": "#9877ca",
"F": "#c178c9",
"G": "#ee87c5",
"H": "#ca7881",
"I": "#75c9c0",
"J": "#75c997",
"K": "#80ca79",
"L": "#aacb78",
"M": "#cbbc78",
"N": "#cb9878",
"O": "#bb774c",
"P": "#905b39",
"Q": "#903737",
"R": "#bf4949",
"S": "#f37070",
"T": "#ff9c3c",
"U": "#fbc84c",
"V": "#cbc8a1",
"W": "#a9a9a9",
"X": "#767676",
"Y": "#cb9878",
"Z": "#903737",
"+": "#9e9e9e"
};
export const NumbersColorMap: { [x: string]: string } = {
"0": "#154c9b",
"1": "#3b7ad4",
"2": "#70a6f3",
"3": "#7781ca",
"4": "#9877ca",
"5": "#c178c9",
"6": "#ee87c5",
"7": "#ca7881",
"8": "#75c9c0",
"9": "#75c997"
};
export const PriorityColorCodes: { [x: number]: string; } = {
0: "#75c997",
1: "#fbc84c",
2: "#f37070"
};
export const TASK_STATUS_TODO_COLOR = "#a9a9a9";
export const TASK_STATUS_DOING_COLOR = "#70a6f3";
export const TASK_STATUS_DONE_COLOR = "#75c997";
export const TASK_PRIORITY_LOW_COLOR = "#75c997";
export const TASK_PRIORITY_MEDIUM_COLOR = "#fbc84c";
export const TASK_PRIORITY_HIGH_COLOR = "#f37070";
export const TASK_DUE_COMPLETED_COLOR = "#75c997";
export const TASK_DUE_UPCOMING_COLOR = "#70a6f3";
export const TASK_DUE_OVERDUE_COLOR = "#f37070";
export const TASK_DUE_NO_DUE_COLOR = "#a9a9a9";
export const DEFAULT_PAGE_SIZE = 20;
export const TASK_STATUS_COLOR_ALPHA = "69";
export const TASK_PRIORITY_COLOR_ALPHA = "69";
export const TEAM_MEMBER_TREE_MAP_COLOR_ALPHA = "40";
// *Sync with the client
export const PASSWORD_POLICY = "Minimum of 8 characters, with upper and lowercase and a number and a symbol.";
// paddle status to exclude
export const statusExclude = ["past_due", "paused", "deleted"];
export const HTML_TAG_REGEXP = /<\/?[^>]+>/gi;
export const UNMAPPED = "Unmapped";
export const DATE_RANGES = {
YESTERDAY: "YESTERDAY",
LAST_WEEK: "LAST_WEEK",
LAST_MONTH: "LAST_MONTH",
LAST_QUARTER: "LAST_QUARTER",
ALL_TIME: "ALL_TIME"
};

View File

@@ -0,0 +1,69 @@
/**
* Map all unique indexes, constrains from a database here
* to send a meaningful message
*
* NOTE:
* Adding "$" sign to the start of the message will ignore displaying it
* to the end-user by the client's interceptor
*
* Adding "[IGNORE]" as the message sends a success empty response to the client
* */
export const DB_CONSTRAINS: { [x: string]: string | null } = {
// Unique indexes
project_access_levels_key_uindex: "",
project_access_levels_name_uindex: "",
task_priorities_name_uindex: "",
clients_name_team_id_uindex: "Client name already exists. Please choose a different name.",
job_titles_name_team_id_uindex: "Job title already exists. Please choose a different name.",
users_email_uindex: "A Worklenz account already exists for this email address. Please choose a different email.",
users_google_id_uindex: "",
users_socket_id_uindex: "",
team_members_user_id_team_id_uindex: "Team member with this email already exists.",
project_members_team_member_project_uindex: "[IGNORE]",
task_statuses_project_id_name_uindex: "Status already exists. Please choose a different name.",
tasks_name_project_uindex: "Task name already exists. Please choose a different name.",
tasks_assignee_task_project_uindex: "",
permissions_name_uindex: "",
roles_name_team_id_uindex: "",
roles_default_uindex: "",
roles_owner_uindex: "",
personal_todo_list_index_uindex: "",
team_labels_name_team_uindex: "Labels cannot be duplicated.",
projects_key_team_id_uindex: "Try to use a different team name.",
project_folders_team_id_name_uindex: "Folder already exists. Use a different folder name.",
project_phases_name_project_uindex: "Option name already exists. Use a different name.",
project_categories_name_team_id_uindex: "Category already exists. Use a different name",
// Status already exists. Please choose a different name.
// Keys
tasks_project_fk: "This project has tasks associated with it. Please delete all tasks before deleting the project.",
tasks_assignees_pk: null,
tasks_status_id_fk: "$One or more tasks (archived/non-archived) will be affected. Please select another status to move the tasks",
// Check constrains
projects_color_code_check: "",
sys_task_status_categories_color_code_check: "",
tasks_total_minutes_check: "",
tasks_task_order_check: "",
personal_todo_list_color_code_check: "",
task_work_log_time_spent_check: "Invalid log details",
task_comment_contents_content_check: "",
teams_name_check: "Team name size exceeded",
clients_name_check: "Client name size exceeded",
job_titles_name_check: "Job title name size exceeded",
users_name_check: "User name size exceeded",
users_email_check: "Email size exceeded",
projects_name_check: "Project name size exceeded",
projects_notes_check: "Project note size exceeded",
task_statuses_name_check: "Task status name size exceeded",
tasks_name_check: "Task name size exceeded",
tasks_description_check: "Task description size exceeded",
team_labels_name_check: "Label name size exceeded",
personal_todo_list_name_check: "Name size exceeded",
personal_todo_list_description_check: "Description size exceeded",
task_work_log_description_check: "Description size exceeded",
task_comment_contents_name_check: "Comment size exceeded",
task_attachments_name_check: "File name size exceeded",
};

View File

@@ -0,0 +1,56 @@
const policies = {
"default-src": ["'self'"],
"script-src": [
"'self'",
"data:",
"'unsafe-inline'",
"https://js.usemessages.com",
"https://*.tiny.cloud",
],
"media-src": [
"https://s3.us-west-2.amazonaws.com"
],
"style-src": [
"'self'",
"'unsafe-inline'",
"data:",
"https://cdnjs.cloudflare.com",
"https://fonts.googleapis.com",
"https://*.tiny.cloud",
],
"font-src": [
"'self'",
"data:",
"https://fonts.gstatic.com",
"https://cdnjs.cloudflare.com",
],
"worker-src": ["'self'"],
"connect-src": [
"'self'",
"data",
"https://js.usemessages.com",
"https://cdnjs.cloudflare.com",
"https://fonts.googleapis.com",
"https://fonts.gstatic.com",
"https://s3.us-west-2.amazonaws.com",
"https://s3.scriptcdn.net",
"https://*.tiny.cloud",
"https://*.tinymce.com",
],
"img-src": [
"'self'",
"data:",
"https://s3.us-west-2.amazonaws.com",
"https://*.tinymce.com",
],
"frame-src": ["https://docs.google.com"],
"frame-ancestors": ["'none'"],
"object-src": ["'none'"],
"report-to": [`https://${process.env.HOSTNAME}/-/csp`]
};
const policyString = Object.entries(policies)
.map(([key, value]) => `${key} ${value.join(" ")}`)
.join("; ");
export const CSP_POLICIES = policyString;

View File

@@ -0,0 +1,122 @@
import {compileTemplate} from "pug";
import db from "../config/db";
import {IDailyDigest} from "../interfaces/daily-digest";
import {IEmailTemplateType} from "../interfaces/email-template-type";
import {ITaskAssignmentsModel} from "../interfaces/task-assignments-model";
import {sendEmail} from "./email";
import FileConstants from "./file-constants";
import {log_error} from "./utils";
import {ITaskMovedToDoneRecord} from "../interfaces/task-moved-to-done";
import {IProjectDigest} from "../interfaces/project-digest";
import {ICommentEmailNotification, IProjectCommentEmailNotification} from "../interfaces/comment-email-notification";
async function updateTaskUpdatesStatus(isSent: boolean) {
try {
const q = isSent
? "DELETE FROM task_updates WHERE is_sent IS TRUE;"
: "UPDATE task_updates SET is_sent = FALSE;";
await db.query(q, []);
} catch (error) {
log_error(error);
}
}
async function addToEmailLogs(email: string, subject: string, html: string) {
try {
const q = `INSERT INTO email_logs (email, subject, html) VALUES ($1, $2, $3);`;
await db.query(q, [email, subject, html]);
} catch (error) {
log_error(error);
}
}
export async function sendAssignmentUpdate(toEmail: string, assignment: ITaskAssignmentsModel) {
try {
const template = FileConstants.getEmailTemplate(IEmailTemplateType.TaskAssigneeChange) as compileTemplate;
const isSent = assignment.teams?.length
? await sendEmail({
subject: "You have new assignments on Worklenz",
to: [toEmail],
html: template(assignment)
})
: true;
await updateTaskUpdatesStatus(!!isSent);
addToEmailLogs(toEmail, "You have new assignments on Worklenz", template(assignment));
} catch (e) {
log_error(e);
await updateTaskUpdatesStatus(false);
}
}
export async function sendDailyDigest(toEmail: string, digest: IDailyDigest) {
try {
const template = FileConstants.getEmailTemplate(IEmailTemplateType.DailyDigest) as compileTemplate;
await sendEmail({
subject: digest.note as string,
to: [toEmail],
html: template(digest)
});
} catch (e) {
log_error(e);
}
}
export async function sendTaskDone(toEmails: string[], data: ITaskMovedToDoneRecord) {
try {
const template = FileConstants.getEmailTemplate(IEmailTemplateType.TaskDone) as compileTemplate;
await sendEmail({
subject: data.summary,
to: toEmails,
html: template(data)
});
} catch (e) {
log_error(e);
}
}
export async function sendProjectDailyDigest(toEmail: string, digest: IProjectDigest) {
try {
const template = FileConstants.getEmailTemplate(IEmailTemplateType.ProjectDailyDigest) as compileTemplate;
await sendEmail({
subject: digest.summary,
to: [toEmail],
html: template(digest)
});
} catch (e) {
log_error(e);
}
}
export async function sendTaskComment(toEmail: string, data: ICommentEmailNotification) {
try {
const template = FileConstants.getEmailTemplate(IEmailTemplateType.TaskComment) as compileTemplate;
return await sendEmail({
subject: data.summary,
to: [toEmail],
html: template(data)
});
} catch (e) {
log_error(e);
}
return null;
}
export async function sendProjectComment(toEmail: string, data: IProjectCommentEmailNotification) {
try {
const template = FileConstants.getEmailTemplate(IEmailTemplateType.ProjectComment) as compileTemplate;
return await sendEmail({
subject: data.summary,
to: [toEmail],
html: template(data)
});
} catch (e) {
log_error(e);
}
return null;
}

View File

@@ -0,0 +1,108 @@
import {IEmailTemplateType} from "../interfaces/email-template-type";
import {IPassportSession} from "../interfaces/passport-session";
import {sendEmail} from "./email";
import {sanitize} from "./utils";
import FileConstants from "./file-constants";
const HOSTNAME = process.env.HOSTNAME || "worklenz.com";
export function sendWelcomeEmail(email: string, name: string) {
let content = FileConstants.getEmailTemplate(IEmailTemplateType.Welcome) as string;
if (!content) return;
content = content.replace("[VAR_USER_NAME]", sanitize(name));
content = content.replace("[VAR_HOSTNAME]", sanitize(HOSTNAME));
sendEmail({
to: [email],
subject: "Welcome to Worklenz.",
html: content
});
}
export function sendNewSubscriberNotification(subscriberEmail: string) {
let content = FileConstants.getEmailTemplate(IEmailTemplateType.NewSubscriber) as string;
if (!content) return;
content = content.replace("[VAR_EMAIL]", sanitize(subscriberEmail));
sendEmail({
subject: "Worklenz - New Subscriber.",
html: content
});
}
export function sendJoinTeamInvitation(myName: string, teamName: string, teamId: string, userName: string, toEmail: string, userId: string, projectId?: string) {
let content = FileConstants.getEmailTemplate(IEmailTemplateType.TeamMemberInvitation) as string;
if (!content) return;
content = content.replace("[VAR_USER_NAME]", sanitize(userName));
content = content.replace("[VAR_TEAM_NAME]", sanitize(teamName));
content = content.replace("[VAR_HOSTNAME]", sanitize(HOSTNAME));
content = content.replace("[VAR_TEAM_ID]", sanitize(teamId));
content = content.replace("[VAR_USER_ID]", sanitize(userId));
content = content.replace("[PROJECT_ID]", projectId ? sanitize(projectId as string) : "");
sendEmail({
to: [toEmail],
subject: `${myName} has invited you to work with ${teamName} in Worklenz`,
html: content
});
}
export function sendRegisterAndJoinTeamInvitation(myName: string, userName: string, teamName: string, teamId: string, userId: string, toEmail: string, projectId?: string) {
let content = FileConstants.getEmailTemplate(IEmailTemplateType.UnregisteredTeamMemberInvitation) as string;
if (!content) return;
content = content.replace("[VAR_EMAIL]", sanitize(toEmail));
content = content.replace("[VAR_USER_ID]", sanitize(userId));
content = content.replace("[VAR_USER_NAME]", sanitize(userName));
content = content.replace("[VAR_TEAM_NAME]", sanitize(teamName));
content = content.replace("[VAR_HOSTNAME]", sanitize(HOSTNAME));
content = content.replace("[VAR_TEAM_ID]", sanitize(teamId));
content = content.replace("[PROJECT_ID]", projectId ? sanitize(projectId as string) : "");
sendEmail({
to: [toEmail],
subject: `${myName} has invited you to work with ${teamName} in Worklenz`,
html: content
});
}
export function sendResetEmail(toEmail: string, user_id: string, hash: string) {
let content = FileConstants.getEmailTemplate(IEmailTemplateType.ResetPassword) as string;
if (!content) return;
content = content.replace("[VAR_HOSTNAME]", sanitize(HOSTNAME));
content = content.replace("[VAR_USER_ID]", sanitize(user_id));
content = content.replace("[VAR_HASH]", hash);
sendEmail({
to: [toEmail],
subject: "Reset your password on Worklenz.",
html: content
});
}
export function sendResetSuccessEmail(toEmail: string) {
const content = FileConstants.getEmailTemplate(IEmailTemplateType.PasswordChange) as string;
if (!content) return;
sendEmail({
to: [toEmail],
subject: "Your password was reset.",
html: content
});
}
// * This implementation should be improved
export function sendInvitationEmail(isNewMember: boolean, user: IPassportSession, userNameOrId: string, email: string, userId: string, userName?: string, projectId?: string) {
if (isNewMember) {
// userNameOrId = userName
sendJoinTeamInvitation(user?.name as string, user?.team_name as string, user.team_id as string, userNameOrId, email, userId, projectId);
} else {
// userNameOrId = userId
sendRegisterAndJoinTeamInvitation(user?.name as string, userName as string, user?.team_name as string, user.team_id as string, userNameOrId, email, projectId);
}
}

View File

@@ -0,0 +1,92 @@
import {SendEmailCommand, SESClient} from "@aws-sdk/client-ses";
import {Validator} from "jsonschema";
import {QueryResult} from "pg";
import {log_error} from "./utils";
import emailRequestSchema from "../json_schemas/email-request-schema";
import db from "../config/db";
const sesClient = new SESClient({region: process.env.AWS_REGION});
export interface IEmail {
to?: string[];
subject: string;
html: string;
}
export class EmailRequest implements IEmail {
public readonly html: string;
public readonly subject: string;
public readonly to: string[];
constructor(toEmails: string[], subject: string, content: string) {
this.to = toEmails;
this.subject = subject;
this.html = content;
}
}
function isValidMailBody(body: IEmail) {
const validator = new Validator();
return validator.validate(body, emailRequestSchema).valid;
}
async function removeMails(query: string, emails: string[]) {
const result: QueryResult<{ email: string; }> = await db.query(query, []);
const bouncedEmails = result.rows.map(e => e.email);
for (let i = 0; i < emails.length; i++) {
const email = emails[i];
if (bouncedEmails.includes(email)) {
emails.splice(i, 1);
}
}
}
async function filterSpamEmails(emails: string[]): Promise<void> {
await removeMails("SELECT email FROM spam_emails ORDER BY email;", emails);
}
async function filterBouncedEmails(emails: string[]): Promise<void> {
await removeMails("SELECT email FROM bounced_emails ORDER BY email;", emails);
}
export async function sendEmail(email: IEmail): Promise<string | null> {
try {
const options = {...email} as IEmail;
options.to = Array.isArray(options.to) ? Array.from(new Set(options.to)) : [];
if (options.to.length) {
await filterBouncedEmails(options.to);
await filterSpamEmails(options.to);
}
if (!isValidMailBody(options)) return null;
const charset = "UTF-8";
const command = new SendEmailCommand({
Destination: {
ToAddresses: options.to
},
Message: {
Subject: {
Charset: charset,
Data: options.subject
},
Body: {
Html: {
Charset: charset,
Data: options.html
}
}
},
Source: "SOURCE_EMAIL_HERE" // Ex: Worklenz <noreply@worklenz.com>
});
const res = await sesClient.send(command);
return res.MessageId || null;
} catch (e) {
log_error(e);
}
return null;
}

View File

@@ -0,0 +1,95 @@
/* eslint-disable security/detect-object-injection */
import fs from "node:fs";
import path from "node:path";
import pug from "pug";
import {IEmailTemplateType} from "../interfaces/email-template-type";
class FileConstants {
private static release: string | null = null;
private static readonly EMAIL_TEMPLATES_MAP: { [x: string]: string } = {};
private static readonly PUG_EMAIL_TEMPLATES_MAP: { [x: string]: pug.compileTemplate } = {};
private static readonly EMAIL_TEMPLATES_BASE = "../../worklenz-email-templates";
static init() {
FileConstants.getRelease();
FileConstants.initEmailTemplates();
}
private static readHtmlEmailTemplate(fileName: string) {
const key = fileName.toString();
if (!FileConstants.EMAIL_TEMPLATES_MAP[key]) {
const url = path.join(__dirname, FileConstants.EMAIL_TEMPLATES_BASE, `${fileName}.html`);
FileConstants.EMAIL_TEMPLATES_MAP[key] = fs.readFileSync(url, "utf8");
}
return FileConstants.EMAIL_TEMPLATES_MAP[key];
}
private static readPugEmailTemplate(fileName: string) {
const key = fileName.toString();
if (!FileConstants.PUG_EMAIL_TEMPLATES_MAP[key]) {
const filePath = path.join(__dirname, FileConstants.EMAIL_TEMPLATES_BASE, "email-notifications", `${fileName}.pug`);
const template = pug.compileFile(filePath);
FileConstants.PUG_EMAIL_TEMPLATES_MAP[key] = template;
}
return FileConstants.PUG_EMAIL_TEMPLATES_MAP[key];
}
private static initEmailTemplates() {
FileConstants.getEmailTemplate(IEmailTemplateType.NewSubscriber);
FileConstants.getEmailTemplate(IEmailTemplateType.TeamMemberInvitation);
FileConstants.getEmailTemplate(IEmailTemplateType.UnregisteredTeamMemberInvitation);
FileConstants.getEmailTemplate(IEmailTemplateType.PasswordChange);
FileConstants.getEmailTemplate(IEmailTemplateType.Welcome);
FileConstants.getEmailTemplate(IEmailTemplateType.OTPVerification);
FileConstants.getEmailTemplate(IEmailTemplateType.ResetPassword);
FileConstants.getEmailTemplate(IEmailTemplateType.TaskAssigneeChange);
FileConstants.getEmailTemplate(IEmailTemplateType.DailyDigest);
FileConstants.getEmailTemplate(IEmailTemplateType.TaskDone);
FileConstants.getEmailTemplate(IEmailTemplateType.ProjectDailyDigest);
FileConstants.getEmailTemplate(IEmailTemplateType.TaskComment);
}
static getEmailTemplate(type: IEmailTemplateType) {
switch (type) {
case IEmailTemplateType.NewSubscriber:
return FileConstants.readHtmlEmailTemplate("admin-new-subscriber-notification");
case IEmailTemplateType.TeamMemberInvitation:
return FileConstants.readHtmlEmailTemplate("team-invitation");
case IEmailTemplateType.UnregisteredTeamMemberInvitation:
return FileConstants.readHtmlEmailTemplate("unregistered-team-invitation-notification");
case IEmailTemplateType.PasswordChange:
return FileConstants.readHtmlEmailTemplate("password-changed-notification");
case IEmailTemplateType.Welcome:
return FileConstants.readHtmlEmailTemplate("welcome");
case IEmailTemplateType.OTPVerification:
return FileConstants.readHtmlEmailTemplate("otp-verfication-code");
case IEmailTemplateType.ResetPassword:
return FileConstants.readHtmlEmailTemplate("reset-password");
case IEmailTemplateType.TaskAssigneeChange:
return FileConstants.readPugEmailTemplate("task-assignee-change");
case IEmailTemplateType.DailyDigest:
return FileConstants.readPugEmailTemplate("daily-digest");
case IEmailTemplateType.TaskDone:
return FileConstants.readPugEmailTemplate("task-moved-to-done");
case IEmailTemplateType.ProjectDailyDigest:
return FileConstants.readPugEmailTemplate("project-daily-digest");
case IEmailTemplateType.TaskComment:
return FileConstants.readPugEmailTemplate("task-comment");
default:
return null;
}
}
static getRelease() {
if (FileConstants.release === null) {
FileConstants.release = fs.readFileSync(path.join(__dirname, "../../release"), "utf8").trim();
}
return FileConstants.release;
}
}
export default FileConstants;

View File

@@ -0,0 +1,48 @@
import {Server} from "socket.io";
import db from "../config/db";
import {SocketEvents} from "../socket.io/events";
export class IO {
private static instance: Server | null = null;
public static setInstance(io: Server) {
this.instance = io;
}
public static getInstance() {
return this.instance;
}
public static getSocketById(socketId: string) {
return this.instance?.sockets.sockets?.get(socketId) || null;
}
public static emit(event: SocketEvents, socketId: string, data?: any) {
this.getSocketById(socketId)?.emit(event.toString(), data);
}
public static async emitByUserId(id: string, userId: string | null, event: SocketEvents, data?: any) {
try {
if (id === userId) return;
const q = `SELECT socket_id FROM users WHERE id = $1;`;
const result = await db.query(q, [id]);
const [user] = result.rows;
if (!user) return;
this.emit(event, user.socket_id, data);
} catch (error) {
// ignored
}
}
public static async emitByTeamMemberId(id: string, userId: string | null, event: SocketEvents, data?: any) {
try {
const q = `SELECT socket_id FROM users WHERE id != $1 AND id IN (SELECT user_id FROM team_members WHERE id = $2);`;
const result = await db.query(q, [userId, id]);
const [user] = result.rows;
if (!user) return;
this.emit(event, user.socket_id, data);
} catch (error) {
// ignored
}
}
}

View File

@@ -0,0 +1,78 @@
import {IPasswordValidityResult} from "../interfaces/password-validity-result";
export class PasswordStrengthChecker {
private static readonly defaultOptions = [
{
value: 0,
text: "Too weak",
minDiversity: 0,
minLength: 0
},
{
value: 1,
text: "Weak",
minDiversity: 2,
minLength: 6
},
{
value: 2,
text: "Strong",
minDiversity: 4,
minLength: 8
},
{
value: 3,
text: "Excellent",
minDiversity: 4,
minLength: 10
}
];
private static readonly defaultAllowedSymbols = "!\"#\$%&'\(\)\*\+,-\./:;<=>\?@\[\\\\\\]\^_`\{|\}~";
public static validate(password: string, options = this.defaultOptions, allowedSymbols = this.defaultAllowedSymbols): IPasswordValidityResult {
const passwordCopy = password || "";
options[0].minDiversity = 0;
options[0].minLength = 0;
const rules = [
{
regex: "[a-z]",
message: "lowercase"
},
{
regex: "[A-Z]",
message: "uppercase"
},
{
regex: "[0-9]",
message: "number"
},
];
if (allowedSymbols) {
rules.push({
regex: `[${allowedSymbols}]`,
message: "symbol"
});
}
const strength: any = {};
strength.contains = rules
.filter(rule => new RegExp(`${rule.regex}`).test(passwordCopy))
.map(rule => rule.message);
strength.length = passwordCopy.length;
const fulfilledOptions = options
.filter(option => strength.contains.length >= option.minDiversity)
.filter(option => strength.length >= option.minLength)
.sort((o1, o2) => o2.value - o1.value)
.map(option => ({value: option.value, text: option.text}));
Object.assign(strength, fulfilledOptions[0]);
return strength;
}
}

View File

@@ -0,0 +1,242 @@
{
"00000": "successful_completion",
"01000": "warning",
"0100C": "dynamic_result_sets_returned",
"01008": "implicit_zero_bit_padding",
"01003": "null_value_eliminated_in_set_function",
"01007": "privilege_not_granted",
"01006": "privilege_not_revoked",
"01004": "string_data_right_truncation",
"01P01": "deprecated_feature",
"02000": "no_data",
"02001": "no_additional_dynamic_result_sets_returned",
"03000": "sql_statement_not_yet_complete",
"08000": "connection_exception",
"08003": "connection_does_not_exist",
"08006": "connection_failure",
"08001": "sqlclient_unable_to_establish_sqlconnection",
"08004": "sqlserver_rejected_establishment_of_sqlconnection",
"08007": "transaction_resolution_unknown",
"08P01": "protocol_violation",
"09000": "triggered_action_exception",
"0A000": "feature_not_supported",
"0B000": "invalid_transaction_initiation",
"0F000": "locator_exception",
"0F001": "invalid_locator_specification",
"0L000": "invalid_grantor",
"0LP01": "invalid_grant_operation",
"0P000": "invalid_role_specification",
"0Z000": "diagnostics_exception",
"0Z002": "stacked_diagnostics_accessed_without_active_handler",
"20000": "case_not_found",
"21000": "cardinality_violation",
"22000": "data_exception",
"2202E": "array_subscript_error",
"22021": "character_not_in_repertoire",
"22008": "datetime_field_overflow",
"22012": "division_by_zero",
"22005": "error_in_assignment",
"2200B": "escape_character_conflict",
"22022": "indicator_overflow",
"22015": "interval_field_overflow",
"2201E": "invalid_argument_for_logarithm",
"22014": "invalid_argument_for_ntile_function",
"22016": "invalid_argument_for_nth_value_function",
"2201F": "invalid_argument_for_power_function",
"2201G": "invalid_argument_for_width_bucket_function",
"22018": "invalid_character_value_for_cast",
"22007": "invalid_datetime_format",
"22019": "invalid_escape_character",
"2200D": "invalid_escape_octet",
"22025": "invalid_escape_sequence",
"22P06": "nonstandard_use_of_escape_character",
"22010": "invalid_indicator_parameter_value",
"22023": "invalid_parameter_value",
"2201B": "invalid_regular_expression",
"2201W": "invalid_row_count_in_limit_clause",
"2201X": "invalid_row_count_in_result_offset_clause",
"2202H": "invalid_tablesample_argument",
"2202G": "invalid_tablesample_repeat",
"22009": "invalid_time_zone_displacement_value",
"2200C": "invalid_use_of_escape_character",
"2200G": "most_specific_type_mismatch",
"22004": "null_value_not_allowed",
"22002": "null_value_no_indicator_parameter",
"22003": "numeric_value_out_of_range",
"2200H": "sequence_generator_limit_exceeded",
"22026": "string_data_length_mismatch",
"22001": "string_data_right_truncation",
"22011": "substring_error",
"22027": "trim_error",
"22024": "unterminated_c_string",
"2200F": "zero_length_character_string",
"22P01": "floating_point_exception",
"22P02": "invalid_text_representation",
"22P03": "invalid_binary_representation",
"22P04": "bad_copy_file_format",
"22P05": "untranslatable_character",
"2200L": "not_an_xml_document",
"2200M": "invalid_xml_document",
"2200N": "invalid_xml_content",
"2200S": "invalid_xml_comment",
"2200T": "invalid_xml_processing_instruction",
"23000": "integrity_constraint_violation",
"23001": "restrict_violation",
"23502": "not_null_violation",
"23503": "foreign_key_violation",
"23505": "unique_violation",
"23514": "check_violation",
"23P01": "exclusion_violation",
"24000": "invalid_cursor_state",
"25000": "invalid_transaction_state",
"25001": "active_sql_transaction",
"25002": "branch_transaction_already_active",
"25008": "held_cursor_requires_same_isolation_level",
"25003": "inappropriate_access_mode_for_branch_transaction",
"25004": "inappropriate_isolation_level_for_branch_transaction",
"25005": "no_active_sql_transaction_for_branch_transaction",
"25006": "read_only_sql_transaction",
"25007": "schema_and_data_statement_mixing_not_supported",
"25P01": "no_active_sql_transaction",
"25P02": "in_failed_sql_transaction",
"25P03": "idle_in_transaction_session_timeout",
"26000": "invalid_sql_statement_name",
"27000": "triggered_data_change_violation",
"28000": "invalid_authorization_specification",
"28P01": "invalid_password",
"2B000": "dependent_privilege_descriptors_still_exist",
"2BP01": "dependent_objects_still_exist",
"2D000": "invalid_transaction_termination",
"2F000": "sql_routine_exception",
"2F005": "function_executed_no_return_statement",
"2F002": "modifying_sql_data_not_permitted",
"2F003": "prohibited_sql_statement_attempted",
"2F004": "reading_sql_data_not_permitted",
"34000": "invalid_cursor_name",
"38000": "external_routine_exception",
"38001": "containing_sql_not_permitted",
"38002": "modifying_sql_data_not_permitted",
"38003": "prohibited_sql_statement_attempted",
"38004": "reading_sql_data_not_permitted",
"39000": "external_routine_invocation_exception",
"39001": "invalid_sqlstate_returned",
"39004": "null_value_not_allowed",
"39P01": "trigger_protocol_violated",
"39P02": "srf_protocol_violated",
"39P03": "event_trigger_protocol_violated",
"3B000": "savepoint_exception",
"3B001": "invalid_savepoint_specification",
"3D000": "invalid_catalog_name",
"3F000": "invalid_schema_name",
"40000": "transaction_rollback",
"40002": "transaction_integrity_constraint_violation",
"40001": "serialization_failure",
"40003": "statement_completion_unknown",
"40P01": "deadlock_detected",
"42000": "syntax_error_or_access_rule_violation",
"42601": "syntax_error",
"42501": "insufficient_privilege",
"42846": "cannot_coerce",
"42803": "grouping_error",
"42P20": "windowing_error",
"42P19": "invalid_recursion",
"42830": "invalid_foreign_key",
"42602": "invalid_name",
"42622": "name_too_long",
"42939": "reserved_name",
"42804": "datatype_mismatch",
"42P18": "indeterminate_datatype",
"42P21": "collation_mismatch",
"42P22": "indeterminate_collation",
"42809": "wrong_object_type",
"428C9": "generated_always",
"42703": "undefined_column",
"42883": "undefined_function",
"42P01": "undefined_table",
"42P02": "undefined_parameter",
"42704": "undefined_object",
"42701": "duplicate_column",
"42P03": "duplicate_cursor",
"42P04": "duplicate_database",
"42723": "duplicate_function",
"42P05": "duplicate_prepared_statement",
"42P06": "duplicate_schema",
"42P07": "duplicate_table",
"42712": "duplicate_alias",
"42710": "duplicate_object",
"42702": "ambiguous_column",
"42725": "ambiguous_function",
"42P08": "ambiguous_parameter",
"42P09": "ambiguous_alias",
"42P10": "invalid_column_reference",
"42611": "invalid_column_definition",
"42P11": "invalid_cursor_definition",
"42P12": "invalid_database_definition",
"42P13": "invalid_function_definition",
"42P14": "invalid_prepared_statement_definition",
"42P15": "invalid_schema_definition",
"42P16": "invalid_table_definition",
"42P17": "invalid_object_definition",
"44000": "with_check_option_violation",
"53000": "insufficient_resources",
"53100": "disk_full",
"53200": "out_of_memory",
"53300": "too_many_connections",
"53400": "configuration_limit_exceeded",
"54000": "program_limit_exceeded",
"54001": "statement_too_complex",
"54011": "too_many_columns",
"54023": "too_many_arguments",
"55000": "object_not_in_prerequisite_state",
"55006": "object_in_use",
"55P02": "cant_change_runtime_param",
"55P03": "lock_not_available",
"57000": "operator_intervention",
"57014": "query_canceled",
"57P01": "admin_shutdown",
"57P02": "crash_shutdown",
"57P03": "cannot_connect_now",
"57P04": "database_dropped",
"58000": "system_error",
"58030": "io_error",
"58P01": "undefined_file",
"58P02": "duplicate_file",
"72000": "snapshot_too_old",
"F0000": "config_file_error",
"F0001": "lock_file_exists",
"HV000": "fdw_error",
"HV005": "fdw_column_name_not_found",
"HV002": "fdw_dynamic_parameter_value_needed",
"HV010": "fdw_function_sequence_error",
"HV021": "fdw_inconsistent_descriptor_information",
"HV024": "fdw_invalid_attribute_value",
"HV007": "fdw_invalid_column_name",
"HV008": "fdw_invalid_column_number",
"HV004": "fdw_invalid_data_type",
"HV006": "fdw_invalid_data_type_descriptors",
"HV091": "fdw_invalid_descriptor_field_identifier",
"HV00B": "fdw_invalid_handle",
"HV00C": "fdw_invalid_option_index",
"HV00D": "fdw_invalid_option_name",
"HV090": "fdw_invalid_string_length_or_buffer_length",
"HV00A": "fdw_invalid_string_format",
"HV009": "fdw_invalid_use_of_null_pointer",
"HV014": "fdw_too_many_handles",
"HV001": "fdw_out_of_memory",
"HV00P": "fdw_no_schemas",
"HV00J": "fdw_option_name_not_found",
"HV00K": "fdw_reply_handle",
"HV00Q": "fdw_schema_not_found",
"HV00R": "fdw_table_not_found",
"HV00L": "fdw_unable_to_create_execution",
"HV00M": "fdw_unable_to_create_reply",
"HV00N": "fdw_unable_to_establish_connection",
"P0000": "plpgsql_error",
"P0001": "raise_exception",
"P0002": "no_data_found",
"P0003": "too_many_rows",
"P0004": "assert_failure",
"XX000": "internal_error",
"XX001": "data_corrupted",
"XX002": "index_corrupted"
}

View File

@@ -0,0 +1,135 @@
import path from "path";
import {
DeleteObjectCommand,
DeleteObjectCommandInput,
GetObjectCommand,
ListObjectsV2Command,
PutObjectCommand,
PutObjectCommandInput,
S3Client
} from "@aws-sdk/client-s3";
import {isProduction, isTestServer, log_error} from "./utils";
import {getSignedUrl} from "@aws-sdk/s3-request-presigner";
import mime from "mime";
const {BUCKET, REGION, S3_URL, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY} = process.env;
if (!S3_ACCESS_KEY_ID || !S3_SECRET_ACCESS_KEY) {
log_error("Invalid S3_ACCESS_KEY_ID or S3_SECRET_ACCESS_KEY. Please check .env file.");
}
const s3Client = new S3Client({
region: REGION,
credentials: {
accessKeyId: S3_ACCESS_KEY_ID as string,
secretAccessKey: S3_SECRET_ACCESS_KEY as string,
}
});
export function getRootDir() {
if (isTestServer()) return "test-server";
if (isProduction()) return "secure";
return "local-server";
}
export function getKey(teamId: string, projectId: string, attachmentId: string, type: string) {
return path.join(getRootDir(), teamId, projectId, `${attachmentId}.${type}`).replace(/\\/g, "/");
}
export function getAvatarKey(userId: string, type: string) {
return path.join("avatars", getRootDir(), `${userId}.${type}`).replace(/\\/g, "/");
}
export async function uploadBuffer(buffer: Buffer, type: string, location: string): Promise<string | null> {
try {
// Set the parameters.
const bucketParams: PutObjectCommandInput = {
Bucket: BUCKET,
// Specify the name of the new object. For example, 'index.html.'
// To create a directory for the object, use '/'. For example, 'myApp/package.json'.
Key: location,
// Content of the new object.
Body: buffer,
ContentEncoding: "base64",
ContentType: type
};
await s3Client.send(new PutObjectCommand(bucketParams));
return `${S3_URL}/${location}`;
} catch (error) {
log_error(error);
}
return null;
}
export async function uploadBase64(base64Data: string, location: string) {
try {
const buffer = Buffer.from(base64Data.replace(/^data:(.*?);base64,/, ""), "base64");
const type = base64Data.split(";")[0].split(":")[1] || null;
if (!type) return null;
await uploadBuffer(buffer, type, location);
return `${S3_URL}/${location}`;
} catch (error) {
log_error(error);
}
return null;
}
export async function deleteObject(key: string) {
try {
const input: DeleteObjectCommandInput = {
Bucket: BUCKET,
Key: key,
};
return await s3Client.send(new DeleteObjectCommand(input));
} catch (error) {
return null;
}
}
export async function calculateStorage(prefix: string) {
try {
let totalSize = 0;
let continuationToken;
let response: any | null = null;
do {
// Use the listObjectsV2 method to list objects in the folder
const command: any = new ListObjectsV2Command({
Bucket: BUCKET,
Prefix: `${getRootDir()}/${prefix}`,
ContinuationToken: continuationToken,
});
response = await s3Client.send(command);
// Iterate over the objects and add their size to the total
if (response?.Contents) {
for (const obj of response.Contents) {
totalSize += obj.Size;
}
}
// If there are more objects to retrieve, set the continuation token
continuationToken = response.NextContinuationToken;
} while (continuationToken);
return totalSize;
} catch (error) {
log_error(error);
}
}
export async function createPresignedUrlWithClient(key: string, file: string) {
const fileExtension = path.extname(key).toLowerCase();
const contentType = mime.lookup(fileExtension);
const command = new GetObjectCommand({
Bucket: BUCKET,
Key: key,
ResponseContentType: `${contentType}`,
ResponseContentDisposition: `attachment; filename=${file}`,
});
return getSignedUrl(s3Client, command, {expiresIn: 3600});
}

View File

@@ -0,0 +1,10 @@
import {NextFunction, Request, Response} from "express";
import {IWorkLenzResponse} from "../interfaces/worklenz-response";
export default (fn: (_req: Request, _res: Response, next: NextFunction) => Promise<IWorkLenzResponse | void>)
: (req: Request, res: Response, next: NextFunction) => void => {
return (req: Request, res: Response, next: NextFunction): void => {
void fn(req, res, next);
};
};

View File

@@ -0,0 +1,35 @@
import axios from "axios";
import {isProduction, log_error} from "./utils";
export async function send_to_slack(error: any) {
if (!isProduction()) return;
if (!process.env.SLACK_WEBHOOK) return;
try {
const url = process.env.SLACK_WEBHOOK;
const blocks = [];
const title = error.message || "Error";
blocks.push({
"type": "header",
"text": {
"type": "plain_text",
"text": title,
"emoji": true
}
});
blocks.push({
type: "section",
text: {
type: "mrkdwn",
text: `\`\`\`\n${JSON.stringify(error)}\`\`\``
}
});
const request = {blocks};
await axios.post(url, JSON.stringify(request));
} catch (e) {
log_error(e);
}
}

View File

@@ -0,0 +1,147 @@
import moment, {max, min} from "moment";
import db from "../config/db";
import {IGanttDateRange, IGanttWeekRange} from "../interfaces/gantt-chart";
const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
export function isWeekend(date: Date) {
return date.getDay() == 0 || date.getDay() == 6;
}
export function isSunday(date: Date) {
return date.getDay() == 0;
}
export function isLastDayOfWeek(date: Date) {
return isSunday(new Date(date));
}
export function isToday(date: Date) {
return moment().isSame(moment(date).format("YYYY-MM-DD"), "day");
}
export async function getDates(startDate = "", endDate = "") {
let datesToAdd = 21;
const currentDuration = moment(endDate).diff(moment(startDate), "days");
if (currentDuration < 100) datesToAdd = 100 - currentDuration;
const start = moment(startDate).subtract("1", "day").format("YYYY-MM-DD");
const end = moment(endDate).add(datesToAdd, "days").format("YYYY-MM-DD");
let dates: IGanttDateRange[] = [];
const theDate = new Date(start);
while (theDate < new Date(end)) {
const data: IGanttDateRange = {
isSunday: isSunday(theDate),
isToday: isToday(theDate),
isWeekend: isWeekend(theDate),
isLastDayOfWeek: isLastDayOfWeek(theDate),
date: new Date(theDate)
};
dates = [...dates, data];
theDate.setDate(theDate.getDate() + 1);
}
dates.splice(-1);
return dates;
}
export async function getWeekRange(dates: IGanttDateRange[]) {
const weekData: IGanttWeekRange[] = [];
const weeks: number[] = [];
for (const [index, element] of dates.entries()) {
const weekIndex = moment(element.date).week();
if (!weeks.includes(weekIndex)) {
const d: any = {};
const monthData: string[] = [];
d.week_index = weekIndex;
d.days_of_week = dates.filter(e => {
return moment(e.date).week() === moment(element.date).week();
});
for (const item of d.days_of_week) {
const monthIndex = moment(item.date).month();
if (!monthData.includes(monthNames[monthIndex])) monthData.push(monthNames[monthIndex]);
}
d.month_name = monthData.join(" - ");
d.min = dates.findIndex(e => e.date?.valueOf() === min(d.days_of_week.map((days: any) => moment(days.date))).valueOf());
d.min = index !== 0 ? d.min + 2 : d.min + 1;
d.max = dates.findIndex(e => e.date?.valueOf() === max(d.days_of_week.map((days: any) => moment(days.date))).valueOf()) + 3;
weeks.push(weekIndex);
weekData.push(d);
}
}
return weekData;
}
export async function getMonthRange(dates: any[]) {
const monthData = [];
const months: any[] = [];
for (const [, date] of dates.entries()) {
const monthIndex = moment(date.date).month();
if (!months.includes(monthIndex)) {
const d: any = {};
d.month_name = monthNames[monthIndex];
d.month_index = monthIndex;
d.days_of_month = dates.filter(e => {
return moment(e.date).month() === moment(date.date).month();
});
d.min = dates.findIndex(e => e.date.valueOf() === min(d.days_of_month.map((days: any) => moment(days.date))).valueOf()) + 1;
d.max = dates.findIndex(e => e.date.valueOf() === max(d.days_of_month.map((days: any) => moment(days.date))).valueOf()) + 2;
months.push(monthIndex);
monthData.push(d);
}
}
return monthData;
}
export async function getMinMaxOfTaskDates(projectId: string) {
const q = `SELECT MIN(start_date) as min_date, MAX(end_date) as max_date
FROM tasks
WHERE project_id = $1;`;
const result = await db.query(q, [projectId]);
const [data] = result.rows;
if (!data.min_date) {
const minDateQ = `SELECT MIN(created_at) as min_date
FROM tasks
WHERE project_id = $1;`;
const q1Result = await db.query(minDateQ, [projectId]);
const [dataMinDate] = q1Result.rows;
data.min_date = dataMinDate?.min_date;
}
if (!data.max_date) {
const maxDateQ = `SELECT MAX(created_at) as max_date
FROM tasks
WHERE project_id = $1;`;
const q1Result = await db.query(maxDateQ, [projectId]);
const [dataMaxDate] = q1Result.rows;
data.max_date = dataMaxDate?.max_date;
}
return data;
}
export async function getDatesForResourceAllocation(startDate = "", endDate = "") {
const end = moment(endDate).add(4, "weeks").format("YYYY-MM-DD");
let dates: IGanttDateRange[] = [];
const theDate = new Date(startDate);
while (theDate < new Date(end)) {
const data: IGanttDateRange = {
isSunday: isSunday(theDate),
isToday: isToday(theDate),
isWeekend: isWeekend(theDate),
isLastDayOfWeek: isLastDayOfWeek(theDate),
date: new Date(theDate)
};
dates = [...dates, data];
theDate.setDate(theDate.getDate() + 1);
}
return dates;
}

View File

@@ -0,0 +1,217 @@
import slug from "slugify";
import moment from "moment";
import lodash from "lodash";
import sanitizeHtml from "sanitize-html";
import { customAlphabet } from "nanoid";
import { AvatarNamesMap, NumbersColorMap, WorklenzColorCodes } from "./constants";
import { send_to_slack } from "./slack";
import { IActivityLogChangeType } from "../services/activity-logs/interfaces";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const error_codes = require("./postgresql-error-codes");
export function log_error(error: any, user: any | null = null, sendToSlack = true) {
const msg = error_codes[error.code];
if (msg) {
console.log("\n==== BEGIN ERROR ====\n");
console.trace(`ERROR [${error.code}]: ${msg}\n`);
}
console.log("\n");
console.error(error);
console.log("\n==== END ERROR ====\n");
const err = user ? {
user: user || null,
error
} : error;
if (sendToSlack)
send_to_slack(err);
}
/** Returns true if node env is production */
export function isProduction() {
return process.env.NODE_ENV === "production";
}
/** Returns true if uat or dev */
export function isTestServer() {
const hostname = process.env.HOSTNAME;
return hostname === "dev.worklenz.com" || hostname === "uat.app.worklenz.com";
}
/** Returns true if localhost:3000 or localhost:4200 */
export function isLocalServer() {
const hostname = process.env.HOSTNAME;
return hostname === "localhost:4200" || hostname === "localhost:3000" || hostname === "127.0.0.1:3000";
}
/** Returns true of isLocal or isTest server */
export function isInternalServer() {
return isLocalServer() || isTestServer();
}
/**
* String value to a URL-friendly string
* @param str {String}
* @returns string
*/
export function slugify(str: string): string {
return slug(str || "", {
replacement: "-", // replace spaces with replacement
remove: /[*+~.()'"!:@]/g, // regex to remove characters
lower: true, // result in lower case
});
}
export function smallId(len: number) {
/**
* Create nanoid instance with a specific alphabet
* `Alphabet: 0123456789`
* @returns e.g. 458652
*/
return customAlphabet("0123456789", len)();
}
export function isValidateEmail(email: string) {
const re =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(String(email).toLowerCase());
}
export function toTsQuery(value: string) {
return `${value.replace(/\s/g, "+").replace(/\(|\)/g, "")}:*`;
}
function nextChar(c: string) {
return String.fromCharCode(c.charCodeAt(0) + 1);
}
function numberToAlpha(num: number) {
if (num < 1 || num > 26) {
throw new Error("Number must be between 1 and 26.");
}
// Convert the number to an ASCII code by adding 64
const asciiCode = num + 64;
// Convert the ASCII code to the corresponding character
return String.fromCharCode(asciiCode);
}
export function getColor(name?: string, next = false) {
const char = name?.replace(/[^a-zA-Z0-9]/g, "").charAt(0).toUpperCase() || "A";
const map = /\d/.test(char)
? NumbersColorMap
: AvatarNamesMap;
return map[next ? nextChar(char) || char : char];
}
export function toMinutes(hours?: number, minutes?: number) {
return ~~((hours || 0) * 60) + (minutes || 0);
}
export function toSeconds(hours: number, minutes: number, seconds: number) {
return (hours * 3600) + (minutes * 60) + seconds;
}
export function toMilliseconds(hours: number, minutes: number, seconds: number) {
return ((hours * 3600) + (minutes * 60) + seconds) * 1000;
}
export function toRound(value: string | number) {
return /\d+/.test(value as string)
? Math.ceil(+value)
: 0;
}
/** Convert bytes to human-readable format (e.g. 1000 bytes - 1 kb) */
export function humanFileSize(size: number) {
const i = size == 0 ? 0 : ~~(Math.log(size) / Math.log(1024));
return `${(+(size / Math.pow(1024, i)).toFixed(2))} ${["B", "KB", "MB", "GB", "TB"][i]}`;
}
export function getRandomColorCode() {
// Using bitwise is faster than Math.floor
return WorklenzColorCodes[~~(Math.random() * WorklenzColorCodes.length)];
}
export function sanitize(value: string) {
if (!value) return "";
const escapedString = value
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#x27;");
return sanitizeHtml(escapedString);
}
export function escape(value: string) {
return lodash.escape(sanitizeHtml(value));
}
export function unescape(value: string) {
return lodash.unescape(value);
}
export function isUnicode(value: string) {
for (let i = 0, n = value.length; i < n; i++) {
if (value.charCodeAt(i) > 255) return true;
}
return false;
}
export function formatDuration(duration: moment.Duration) {
const empty = "0h 0m";
let format = "";
if (duration.asMilliseconds() === 0) return empty;
const h = ~~(duration.asHours());
const m = duration.minutes();
const s = duration.seconds();
if (h === 0 && s > 0) {
format = `${m}m ${s}s`;
} else if (h > 0 && s === 0) {
format = `${h}h ${m}m`;
} else if (h > 0 && s > 0) {
format = `${h}h ${m}m ${s}s`;
} else {
format = `${h}h ${m}m`;
}
return format;
}
export function calculateMonthDays(startDate: string, endDate: string): string {
const start: Date = new Date(startDate);
const end: Date = new Date(endDate);
const diffInMilliseconds: number = Math.abs(end.getTime() - start.getTime());
const days: number = Math.floor(diffInMilliseconds / (1000 * 60 * 60 * 24));
const months: number = Math.floor(days / 30);
const remainingDays: number = days % 30;
return `${months} ${months > 1 ? "months" : "month"} ${remainingDays} ${remainingDays !== 1 ? "days" : "day"}`;
}
export function int<T>(value: T) {
return isNaN(+value) ? 0 : +value;
}
export function formatLogText(log: { log_type: IActivityLogChangeType; }) {
if (log.log_type === IActivityLogChangeType.ASSIGN) return "added an ";
if (log.log_type === IActivityLogChangeType.UNASSIGN) return "removed an ";
if (log.log_type === IActivityLogChangeType.UPDATE) return "updated the ";
if (log.log_type === IActivityLogChangeType.CREATE) return "added a ";
if (log.log_type === IActivityLogChangeType.DELETE) return "removed a ";
return log.log_type;
}