mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-08-18 08:27:14 +02:00
Initial open source release
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
// max number of items to fetch per request
|
||||
const global_pagination_max = 1000;
|
||||
|
||||
/**
|
||||
* @callback FetchFn
|
||||
* @param {TableURLParams} options
|
||||
* @returns {Promise<import("$lib/api/client").ApiResponse>}
|
||||
* @async
|
||||
*/
|
||||
|
||||
/**
|
||||
* TableURLParams is a type that represents the query parameters for a table.
|
||||
*
|
||||
* @typedef {object} TableURLParams
|
||||
* @property {number} currentPage
|
||||
* @property {number} perPage
|
||||
* @property {string} sortBy
|
||||
* @property {string} sortOrder
|
||||
* @property {string} search
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {FetchFn} fetchFn2
|
||||
* @param {TableURLParams} options
|
||||
* @returns {Promise<Object[]>}
|
||||
*/
|
||||
|
||||
export const defaultOptions = {
|
||||
currentPage: 1,
|
||||
perPage: global_pagination_max,
|
||||
sortBy: 'name',
|
||||
sortOrder: 'asc',
|
||||
search: ''
|
||||
};
|
||||
|
||||
export const fetchAllRows = async (fetchFn2, options = defaultOptions) => {
|
||||
let items = [];
|
||||
let res = await fetchFn2(options);
|
||||
|
||||
// Add initial rows
|
||||
if (res.data?.rows) {
|
||||
items = items.concat(res.data.rows);
|
||||
}
|
||||
|
||||
while (res.data?.hasNextPage) {
|
||||
// Update the page number
|
||||
options.currentPage += 1;
|
||||
// Fetch next page
|
||||
res = await fetchFn2({ ...options });
|
||||
// Add new rows
|
||||
if (res.data?.rows) {
|
||||
items = items.concat(res.data.rows);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Date} date
|
||||
* @returns {string}
|
||||
*/
|
||||
export const utc_yyyy_mm_dd = (date) => {
|
||||
return date.toISOString().split('T')[0];
|
||||
};
|
||||
|
||||
export const local_yyyy_mm_dd = (date) => {
|
||||
return (
|
||||
date.getFullYear() +
|
||||
'-' +
|
||||
String(date.getMonth() + 1).padStart(2, '0') +
|
||||
'-' +
|
||||
String(date.getDate()).padStart(2, '0')
|
||||
);
|
||||
};
|
||||
|
||||
// converts a local time formatted as "HH:MM" to a UTC time formatted as "HH:MM"
|
||||
export function localTimeToUTC(localTime) {
|
||||
// Split the local time string into hours and minutes
|
||||
const [hours, minutes] = localTime.split(':').map(Number);
|
||||
|
||||
// Get the current date
|
||||
const now = new Date();
|
||||
|
||||
// Create a new Date object with the current date and the local time
|
||||
const localDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hours, minutes);
|
||||
|
||||
// Get the UTC time components
|
||||
const utcHours = localDate.getUTCHours();
|
||||
const utcMinutes = localDate.getUTCMinutes();
|
||||
|
||||
// Format the UTC time as a string
|
||||
const utcTime = `${String(utcHours).padStart(2, '0')}:${String(utcMinutes).padStart(2, '0')}`;
|
||||
|
||||
return utcTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a UTC time formatted as "HH:MM" to a local time formatted as "HH:MM"
|
||||
* @param {string} utcTime - The UTC time to convert
|
||||
* @returns {string} - The local time formatted as "HH:MM"
|
||||
*/
|
||||
export function utcTimeToLocal(utcTime) {
|
||||
if (!utcTime) {
|
||||
return '';
|
||||
}
|
||||
// Split the UTC time string into hours and minutes
|
||||
const [hours, minutes] = utcTime.split(':').map(Number);
|
||||
|
||||
// Get the current date
|
||||
const now = new Date();
|
||||
|
||||
// Create a new Date object with the current date and the UTC time
|
||||
const utcDate = new Date(
|
||||
Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), hours, minutes)
|
||||
);
|
||||
|
||||
// Get the local time components
|
||||
const localHours = utcDate.getHours();
|
||||
const localMinutes = utcDate.getMinutes();
|
||||
|
||||
// Format the local time as a string
|
||||
const localTime = `${String(localHours).padStart(2, '0')}:${String(localMinutes).padStart(2, '0')}`;
|
||||
|
||||
return localTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two times formatted as "HH:MM" to determine if the first is larger than the second.
|
||||
* @param {string} time1 - The first time to compare
|
||||
* @param {string} time2 - The second time to compare
|
||||
* @returns {boolean} - True if time1 is larger than time2, false otherwise
|
||||
*/
|
||||
export function isTimeLarger(time1, time2) {
|
||||
// Split the time strings into hours and minutes
|
||||
const [hours1, minutes1] = time1.split(':').map(Number);
|
||||
const [hours2, minutes2] = time2.split(':').map(Number);
|
||||
|
||||
// Compare hours first
|
||||
if (hours1 > hours2) {
|
||||
return true;
|
||||
} else if (hours1 < hours2) {
|
||||
return false;
|
||||
}
|
||||
// If hours are equal, compare minutes
|
||||
return minutes1 >= minutes2;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { addToast } from '$lib/store/toast';
|
||||
|
||||
export const getModalText = (name, mode) => {
|
||||
let t = '';
|
||||
switch (mode) {
|
||||
case 'create':
|
||||
t = `New ${name}`;
|
||||
break;
|
||||
case 'copy':
|
||||
t = `New ${name}`;
|
||||
break;
|
||||
case 'update':
|
||||
t = `Update ${name}`;
|
||||
break;
|
||||
}
|
||||
return t;
|
||||
};
|
||||
|
||||
export const debounce = (func, delay) => {
|
||||
let timeoutId;
|
||||
return (...args) => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
timeoutId = setTimeout(() => {
|
||||
func(...args);
|
||||
}, delay);
|
||||
};
|
||||
};
|
||||
|
||||
export const debounceTyping = (func) => debounce(func, 350);
|
||||
|
||||
export const shouldHideMenuItem = (route) => {
|
||||
// All menu items are now accessible - no edition restrictions
|
||||
return false;
|
||||
};
|
||||
|
||||
export const onClickCopy = (text) => {
|
||||
navigator.clipboard
|
||||
.writeText(text)
|
||||
.then(() => {
|
||||
addToast('Copied to clipboard', 'Success');
|
||||
})
|
||||
.catch((err) => {
|
||||
addToast('Failed to copy to clipboard', 'Error');
|
||||
console.error('failed to copy to clipboard', err);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import papaparse from 'papaparse';
|
||||
|
||||
/**
|
||||
* Parse CSV file to recipients
|
||||
* @param {File} file - CSV file
|
||||
* @returns {Promise<Array<*>>}
|
||||
**/
|
||||
export const parseCSVToRecipients = async (file) => {
|
||||
const p = new Promise((resolve, reject) => {
|
||||
const recipients = {};
|
||||
papaparse.parse(file, {
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
complete: (results) => {
|
||||
if (results.errors) {
|
||||
console.info('CSV import errors', results.errors);
|
||||
}
|
||||
if (!results.data) {
|
||||
reject('No data found in CSV file');
|
||||
return;
|
||||
}
|
||||
// lowercased map of headers
|
||||
const fieldsMap = {};
|
||||
for (let i = 0; i < results.meta.fields.length; i++) {
|
||||
const field = results.meta.fields[i];
|
||||
fieldsMap[field.toLowerCase()] = field;
|
||||
}
|
||||
|
||||
results.data.forEach((row) => {
|
||||
const email = row[fieldsMap['email']];
|
||||
if (!email) {
|
||||
return;
|
||||
}
|
||||
recipients[email] = {
|
||||
email: email,
|
||||
phone: row[fieldsMap['phone']] ?? null,
|
||||
extraIdentifier: row[fieldsMap['extraIdentifier'.toLocaleLowerCase()]] ?? null,
|
||||
firstName: row[fieldsMap['firstname']] ?? null,
|
||||
lastName: row[fieldsMap['lastname']] ?? null,
|
||||
position: row[fieldsMap['position']] ?? null,
|
||||
department: row[fieldsMap['department']] ?? null,
|
||||
city: row[fieldsMap['city']] ?? null,
|
||||
country: row[fieldsMap['country']] ?? null,
|
||||
misc: row[fieldsMap['misc']] ?? null
|
||||
};
|
||||
});
|
||||
resolve(Object.values(recipients));
|
||||
}
|
||||
});
|
||||
});
|
||||
return await p;
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
const DAYS = [
|
||||
{ short: 'Sun', full: 'Sunday', num: '1' },
|
||||
{ short: 'Mon', full: 'Monday', num: '2' },
|
||||
{ short: 'Tue', full: 'Tuesday', num: '3' },
|
||||
{ short: 'Wed', full: 'Wednesday', num: '4' },
|
||||
{ short: 'Thu', full: 'Thursday', num: '5' },
|
||||
{ short: 'Fri', full: 'Friday', num: '6' },
|
||||
{ short: 'Sat', full: 'Saturday', num: '7' }
|
||||
];
|
||||
|
||||
export const timeFormat = writable(false); // false = 12h, true = 24h
|
||||
|
||||
export const formatWeekDays = (binaryDays) => {
|
||||
const days = DAYS.map((day) => ({
|
||||
...day,
|
||||
isActive: !!(binaryDays & (1 << DAYS.indexOf(day)))
|
||||
}));
|
||||
|
||||
const activeDays = days.filter((d) => d.isActive);
|
||||
const isWeekdaysOnly =
|
||||
activeDays.length === 5 &&
|
||||
!days[0].isActive && // Sunday inactive
|
||||
!days[6].isActive; // Saturday inactive
|
||||
const isAllDays = activeDays.length === 7;
|
||||
|
||||
return {
|
||||
days,
|
||||
summary: isAllDays ? 'Every day' : isWeekdaysOnly ? 'Weekdays only' : null
|
||||
};
|
||||
};
|
||||
|
||||
export const formatTimeConstraint = (time, use24Hour = false) => {
|
||||
if (!time) return '';
|
||||
|
||||
const [hours, minutes] = time.split(':').map(Number);
|
||||
|
||||
if (use24Hour) {
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
||||
} else {
|
||||
const h = hours % 12 || 12;
|
||||
const ampm = hours >= 12 ? 'PM' : 'AM';
|
||||
return `${h}:${minutes.toString().padStart(2, '0')} ${ampm}`;
|
||||
}
|
||||
};
|
||||
|
||||
export const isCurrentlyActive = (startTime, endTime, activeDays) => {
|
||||
if (!startTime || !endTime || !activeDays) return false;
|
||||
|
||||
const now = new Date();
|
||||
const currentDay = now.getDay(); // 0-6
|
||||
const currentTime = now.getHours() * 60 + now.getMinutes();
|
||||
|
||||
const [startHour, startMin] = startTime.split(':').map(Number);
|
||||
const [endHour, endMin] = endTime.split(':').map(Number);
|
||||
|
||||
const scheduleStart = startHour * 60 + startMin;
|
||||
const scheduleEnd = endHour * 60 + endMin;
|
||||
|
||||
const isDayActive = !!(activeDays & (1 << currentDay));
|
||||
const isTimeActive = currentTime >= scheduleStart && currentTime <= scheduleEnd;
|
||||
|
||||
return isDayActive && isTimeActive;
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
// map between system event name and a human readable event name
|
||||
const eventNameMap = {
|
||||
// campaign recipient events
|
||||
campaign_recipient_scheduled: { name: 'Scheduled', priority: 10, color: 'bg-scheduled' },
|
||||
campaign_recipient_cancelled: { name: 'Cancelled', priority: 15, color: 'bg-black' },
|
||||
campaign_recipient_message_sent: { name: 'Message Sent', priority: 30, color: 'bg-message-sent' },
|
||||
campaign_recipient_message_failed: {
|
||||
name: 'Failed Sending',
|
||||
priority: 80,
|
||||
color: 'bg-failed-sending'
|
||||
},
|
||||
campaign_recipient_message_read: { name: 'Message Read', priority: 40, color: 'bg-message-read' },
|
||||
campaign_recipient_before_page_visited: {
|
||||
name: 'Before Page Visited',
|
||||
priority: 50,
|
||||
color: 'bg-before-page-visited'
|
||||
},
|
||||
campaign_recipient_page_visited: { name: 'Page Visited', priority: 60, color: 'bg-page-visited' },
|
||||
campaign_recipient_after_page_visited: {
|
||||
name: 'After Page Visited',
|
||||
priority: 70,
|
||||
color: 'bg-after-page-visited'
|
||||
},
|
||||
campaign_recipient_submitted_data: {
|
||||
name: 'Submitted Data',
|
||||
priority: 90,
|
||||
color: 'bg-submitted-data'
|
||||
},
|
||||
// campaign events
|
||||
campaign_scheduled: { name: 'Scheduled', priority: 10 },
|
||||
campaign_active: { name: 'Active', priority: 20 },
|
||||
campaign_self_managed: { name: 'Self managed', priority: 20 },
|
||||
campaign_closed: { name: 'Closed', priority: 30, color: 'bg-closed' }
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} systemEventName
|
||||
* @returns {{name: string, priority: number, color: string}}
|
||||
*/
|
||||
export const toEvent = (systemEventName) => {
|
||||
if (systemEventName === '') {
|
||||
return { name: '', priority: 0, color: '' };
|
||||
}
|
||||
return eventNameMap[systemEventName] ?? { name: 'Unknown Event', priority: 80, color: '' };
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Resets a collection of form elements to their default values
|
||||
* or empty values.
|
||||
* @param {{[key: string]: FormElement}} formElementsMap
|
||||
*/
|
||||
export const resetForm = (formElementsMap) => {
|
||||
const elements = Object.values(formElementsMap);
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const formElement = elements[i];
|
||||
if (formElement.element.type === 'checkbox') {
|
||||
if (formElement.default) {
|
||||
formElement.element.checked = !!formElement.default;
|
||||
continue;
|
||||
}
|
||||
formElement.element.checked = false;
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
formElement.element.type === 'text' ||
|
||||
formElement.element.type === 'textarea' ||
|
||||
formElement.element.type === 'password'
|
||||
) {
|
||||
if (formElement.default) {
|
||||
formElement.element.value = formElement.default.toString();
|
||||
continue;
|
||||
}
|
||||
formElement.element.value = '';
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef FormElement
|
||||
* @type {{element: HTMLInputElement, default: string|boolean|null, value: string, checked: boolean, _element: HTMLInputElement }}}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef FormElementMap
|
||||
* @type {{[key: string]: FormElement}}
|
||||
*/
|
||||
|
||||
/**
|
||||
* A wrapper for a form elements that includes the default value
|
||||
*
|
||||
* @param {string|boolean|null} defaultValue
|
||||
* @returns {FormElement}
|
||||
*/
|
||||
export const newFormElement = (defaultValue = null) => {
|
||||
return {
|
||||
default: defaultValue,
|
||||
|
||||
// dont use this directly
|
||||
_element: null,
|
||||
// use directly instead
|
||||
get element() {
|
||||
return this._element;
|
||||
},
|
||||
|
||||
set element(element) {
|
||||
this._element = element;
|
||||
if (this._element.type === 'checkbox') {
|
||||
this.default = !!this._element.checked;
|
||||
}
|
||||
if (
|
||||
this._element.type === 'text' ||
|
||||
this._element.type === 'textarea' ||
|
||||
this._element.type === 'password'
|
||||
) {
|
||||
this.default = this._element.value;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* shortcut for element.value
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
get value() {
|
||||
return this.element.value ?? '';
|
||||
},
|
||||
/**
|
||||
* shortcut for element.checked
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get checked() {
|
||||
return !!this.element.checked;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const buttonDisabledAttributes = (element, attribute, reason) => {
|
||||
if (!element[attribute]) {
|
||||
return { disabled: true, title: reason };
|
||||
}
|
||||
return { disabled: false, title: '' };
|
||||
};
|
||||
|
||||
export const globalButtonDisabledAttributes = (element, context) => {
|
||||
if (context) {
|
||||
return buttonDisabledAttributes(element, 'companyID', 'Only available in shared view.');
|
||||
}
|
||||
return { disabled: false, title: '' };
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
export class BiMap {
|
||||
#keys = [];
|
||||
#values = [];
|
||||
/**
|
||||
* BiMap is a two-way map that allows you to look up values by key, and keys by value.
|
||||
* Can only accept maps with unique values.
|
||||
*
|
||||
* @param {{[key: string]: string}} map
|
||||
*/
|
||||
constructor(map) {
|
||||
// Check for duplicate values
|
||||
const values = Object.values(map);
|
||||
if (new Set(values).size !== values.length) {
|
||||
console.error('BiMap has received a map with duplicate values. This is not allowed.', values);
|
||||
}
|
||||
const keys = Object.keys(map);
|
||||
if (new Set(keys).size !== keys.length) {
|
||||
console.error('BiMap has received a map with duplicate keys. This is not allowed.', keys);
|
||||
}
|
||||
this.#keys = Object.keys(map);
|
||||
this.#values = Object.values(map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform an array of objects into a BiMap
|
||||
* Ex. [{key: 'a', value: 'b'}, {key: 'c', value: 'd'}] => BiMap {a: 'b', c: 'd'}
|
||||
* Default to key = id and value = name
|
||||
* @param {Object[]} arr
|
||||
* @param {string} keyName
|
||||
* @param {string} valueName
|
||||
* @returns
|
||||
*/
|
||||
static FromArrayOfObjects(arr, keyName = 'id', valueName = 'name') {
|
||||
/**
|
||||
* @type {{[key: string]: string}} arr
|
||||
**/
|
||||
const map = {};
|
||||
arr.forEach((obj) => {
|
||||
map[obj[keyName]] = obj[valueName];
|
||||
});
|
||||
return new BiMap(map);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string[]}
|
||||
*/
|
||||
keys() {
|
||||
return this.#keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string[]}
|
||||
*/
|
||||
values() {
|
||||
return this.#values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value by key
|
||||
* Returns an empty string if the key is not found
|
||||
*
|
||||
* @param {string} key
|
||||
* @returns {any}
|
||||
*/
|
||||
byKey(key) {
|
||||
const v = this.#values[this.#keys.indexOf(key)];
|
||||
if (v === undefined) {
|
||||
return "";
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
/**
|
||||
* Get key by value
|
||||
* Returns an empty string if the value is not found
|
||||
*
|
||||
* @param {string} value
|
||||
* @returns {string}
|
||||
*/
|
||||
byValue(value) {
|
||||
const v = this.#keys[this.#values.indexOf(value)];
|
||||
if (v === undefined) {
|
||||
return "";
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value by key or null
|
||||
*
|
||||
* @param {string} value
|
||||
* @param {*} value
|
||||
* @returns {*}
|
||||
*/
|
||||
byValueOrNull(value) {
|
||||
const v = this.#keys[this.#values.indexOf(value)];
|
||||
if(!v) {
|
||||
return null;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { api } from '$lib/api/apiProxy.js';
|
||||
|
||||
export let previewQR = async (url = 'https://empty.test', dotSize = 4) => {
|
||||
const res = await api.utils.qr({
|
||||
url,
|
||||
dotSize
|
||||
});
|
||||
if (!res.success) {
|
||||
throw res.error;
|
||||
}
|
||||
return res.data;
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
export const scrollBarClassesHorizontal = `
|
||||
[&::-webkit-scrollbar]:h-2
|
||||
[&::-webkit-scrollbar-track]:bg-gray-100
|
||||
[&::-webkit-scrollbar-thumb]:h-1
|
||||
[&::-webkit-scrollbar-thumb]:my-1
|
||||
[&::-webkit-scrollbar-thumb]:rounded-md
|
||||
[&::-webkit-scrollbar-thumb]:bg-pc-dusty-light-blue
|
||||
`;
|
||||
|
||||
export const scrollBarClassesVertical = `
|
||||
[&::-webkit-scrollbar]:w-2
|
||||
[&::-webkit-scrollbar-track]:bg-gray-100
|
||||
[&::-webkit-scrollbar-thumb]:rounded-md
|
||||
[&::-webkit-scrollbar-thumb]:bg-pc-dusty-light-blue
|
||||
`;
|
||||
Reference in New Issue
Block a user