@openinc/parse-server-opendash
Version:
Parse Server Cloud Code for open.INC Stack.
626 lines (625 loc) • 25.8 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.sendTemplateEmail = exports.sendSimpleEmail = void 0;
exports.init = init;
exports.hasPermission = hasPermission;
exports.requirePermission = requirePermission;
exports.getConfig = getConfig;
exports.getConfigBoolean = getConfigBoolean;
exports.ensureRole = ensureRole;
exports.ensureUserRole = ensureUserRole;
exports.immutableField = immutableField;
exports.defaultHandler = defaultHandler;
exports.defaultAclHandler = defaultAclHandler;
exports.beforeSaveHook = beforeSaveHook;
exports.afterSaveHook = afterSaveHook;
exports.beforeDeleteHook = beforeDeleteHook;
exports.afterDeleteHook = afterDeleteHook;
exports.autoloadCloudCode = autoloadCloudCode;
exports.ensurePermission = ensurePermission;
const parse_server_schema_1 = require("@openinc/parse-server-schema");
const dayjs_1 = __importDefault(require("dayjs"));
const dayOfYear_1 = __importDefault(require("dayjs/plugin/dayOfYear"));
const duration_1 = __importDefault(require("dayjs/plugin/duration"));
const objectSupport_1 = __importDefault(require("dayjs/plugin/objectSupport"));
const weekday_1 = __importDefault(require("dayjs/plugin/weekday"));
const weekOfYear_1 = __importDefault(require("dayjs/plugin/weekOfYear"));
const fast_equals_1 = require("fast-equals");
const fs_1 = __importDefault(require("fs"));
const path_1 = require("path");
const web_push_1 = __importDefault(require("web-push"));
const featuremap_json_1 = __importDefault(require("./featuremap.json"));
const config_1 = require("./features/config");
const createRandomPassword_1 = require("./helper/createRandomPassword");
const types_1 = require("./types");
const notifications_1 = require("./features/notifications");
const permissions_1 = require("./features/permissions");
const settings_1 = require("./features/user/settings");
const Core_Email_1 = require("./hooks/Core_Email");
const openservice_1 = require("./features/openservice");
const _init_1 = require("./functions/_init");
const _init_2 = require("./hooks/_init");
dayjs_1.default.extend(objectSupport_1.default);
dayjs_1.default.extend(weekday_1.default);
dayjs_1.default.extend(dayOfYear_1.default);
dayjs_1.default.extend(weekOfYear_1.default);
dayjs_1.default.extend(duration_1.default);
var Core_Email_2 = require("./hooks/Core_Email");
Object.defineProperty(exports, "sendSimpleEmail", { enumerable: true, get: function () { return Core_Email_2.sendSimpleEmail; } });
Object.defineProperty(exports, "sendTemplateEmail", { enumerable: true, get: function () { return Core_Email_2.sendTemplateEmail; } });
const PREFIX = "OD3_";
let config;
// const lang_deu = require("../i18n/deu.json");
let schema = {};
/**
* Initializes the Cloud Code for open.DASH.
* This function performs various initialization tasks such as
* initializing configuration, setting up email transport, initializing web push,
* initializing class schema, setting up default roles, initializing parse lifecycle hooks,
* loading default data, initializing cloud functions, and setting up cloud code autoloading.
*
* @returns {Promise<void>} A promise that resolves when the initialization is complete.
*/
async function init() {
//Initialize tracing SDK
// const logger = startTracingSDK();
// try {
// } catch (error) {
// console.error("Error while initializing tracing SDK");
// console.error(error);
// // process.exit(1);
// }
// logger.info(
// "[@openinc/parse-server-opendash] Initializing open.DASH Cloud Code..."
// );
try {
const pkg = require("../package.json");
console.log(`[${pkg.name}] init (v${pkg.version})`);
}
catch (error) { }
config = config_1.ConfigInstance.getInstance();
await config.init(true);
await initTranslations();
await (0, Core_Email_1.initEmailTransport)();
await initWebPush();
await initSchema();
await initDefaultRoles();
await (0, _init_2.init)();
await initDefaultData();
await (0, _init_1.init)();
await initAutoload();
await (0, permissions_1.initPermissions)();
await (0, notifications_1.initNotifications)();
await (0, settings_1.initUserSettings)();
if ((0, config_1.isFeatureEnabled)("MAINTENANCE"))
await (0, openservice_1.init)();
}
async function initTranslations() {
// try {
// console.log("init translations");
// registerLanguage("deu", "Deutsch", "eng", true);
// registerTranslationResolver("deu", "server", async () => lang_deu);
// } catch (error) {
// console.error("Error while initializing translations");
// console.error(error);
// process.exit(1);
// }
}
async function initWebPush() {
try {
if (config.getBoolean("WEB_PUSH_ENABLED")) {
web_push_1.default.setVapidDetails(config.get("WEB_PUSH_HOST"), config.get("WEB_PUSH_VAPID_PUBLIC_KEY"), config.get("WEB_PUSH_VAPID_PRIVATE_KEY"));
web_push_1.default.setGCMAPIKey(config.get("WEB_PUSH_FCM_KEY"));
}
}
catch (error) {
console.error(error);
process.exit(1);
}
}
async function initSchema() {
const schemaConfig = await (0, parse_server_schema_1.loadConfig)();
try {
await (0, parse_server_schema_1.up)(schemaConfig, (0, path_1.resolve)(__dirname, "../schema"), {
prefix: PREFIX,
deleteClasses: config.getBoolean("FORCE_SCHEMA"),
deleteFields: config.getBoolean("FORCE_SCHEMA"),
deleteNonEmptyClass: config.getBoolean("FORCE_DELETE_CLASS"),
filter: (className) => isClassEnabled(className),
});
schema = Object.fromEntries((await Parse.Schema.all()).map((schema) => [
schema.className,
schema,
]));
}
catch (error) {
console.error("Error while updating schema");
console.error(error);
process.exit(1);
}
}
async function initDefaultRoles() {
try {
await ensureRole("od-user");
await ensureRole("od-admin");
await ensureRole("od-tenant-user");
await ensureRole("od-tenant-verified");
await ensureRole("od-tenant-admin");
}
catch (error) {
console.error(error);
}
}
async function initDefaultData() {
try {
const permissions = [await ensurePermission("parse-admin")];
const tenantCount = await new Parse.Query(types_1.Tenant).count({
useMasterKey: true,
});
if (tenantCount > 0) {
return null;
}
const tenant = new types_1.Tenant({
label: "Default Tenant",
});
await tenant.save(null, { useMasterKey: true });
const password = await (0, createRandomPassword_1.createRandomPassword)();
const user = new Parse.User({
name: "Default User",
username: "openinc",
password: password,
email: "noreply@openinc.de",
emailVerified: true,
tenant,
tenantVerified: true,
tenantAdmin: true,
});
await user.save(null, { useMasterKey: true });
for (const permission of permissions) {
permission.getACL()?.setReadAccess(user, true);
await permission.save(null, { useMasterKey: true });
}
console.log("Default User and Tenant created.");
console.log("Username: " + user.get("username"));
console.log("Password: " + password);
console.log("User: " + user.id);
console.log("Tenant: " + tenant.id);
}
catch (error) {
console.error(error);
}
}
/**
* Checks if a user has a specific permission in OD3_Permission.
* @param sessionToken - The session token of the user.
* @param key - The key of the permission to check.
* @returns A promise that resolves to a boolean indicating whether the user has the permission.
*/
async function hasPermission(sessionToken, key) {
const result = await new Parse.Query(types_1.Permission)
.equalTo("key", key)
.first({ sessionToken });
return !!result;
}
/**
* Checks if the user has the required permission to perform an operation using hasPermission().
* If the user does not have the required permission, an error is thrown.
*
* @param request - The request object containing the user's session token and masterkey status.
* @param key - The permission key required for the operation.
* @param message - The error message to be thrown if the user does not have the required permission.
* @returns A Promise that resolves if the user has the required permission, or throws an error if not.
*/
async function requirePermission(request, key, message) {
if (request.master) {
return;
}
if (!key) {
throw new Parse.Error(119, "Missing Permission (1): " + (message || key || "Master Key Only"));
}
if (!request.sessionToken) {
throw new Parse.Error(119, "Missing Permission (2): " + (message || key || "Master Key Only"));
}
const p = await hasPermission(request.sessionToken, key);
if (!p) {
throw new Parse.Error(119, "Missing Permission (3): " + (message || key || "Master Key Only"));
}
}
/**
* Retrieves the value of a configuration key from OD3_Config.
* @param key - The key of the configuration to retrieve.
* @returns A promise that resolves to the value of the configuration key, or undefined if not found.
*/
async function getConfig(key) {
const result = await new Parse.Query(types_1.Config).equalTo("key", key).first({
useMasterKey: true,
});
const value = result?.get("value");
console.log("[@openinc/parse-server-opendash][Config]", key, value);
return value || "";
}
/**
* Retrieves the value of a configuration key from OD3_Config as a boolean
* Converts the string value to a boolean value using the following rules:
* - "true" or "1" -> true
* - "false" or "0" -> false
* - undefined/null -> false
* - Any other value -> true
*
* @param key - The key of the configuration value.
* @returns A boolean value indicating the configuration value.
*/
async function getConfigBoolean(key) {
const value = await getConfig(key);
if (!value || value.toLowerCase() === "false" || value === "0") {
return false;
}
return true;
}
/**
* Ensures the existence of a role with the specified name.
* If the role does not exist, it creates a new role with the given name and options.
* If the role already exists, it updates the role with the provided options.
*
* @param name - The name of the role.
* @param options - Optional parameters for the role.
* @param options.label - The label for the role.
* @param options.acl - The Parse.ACL for the role.
* @param options.childRoles - An array of child role names to be associated with the role.
* @returns A Promise that resolves when the role is successfully created or updated.
*/
async function ensureRole(name, options) {
const label = options?.label || undefined;
const acl = options?.acl || new Parse.ACL();
const childRoles = options?.childRoles || undefined;
console.log(`[@openinc/parse-server-opendash] ensureRole(${name})`
// JSON.stringify({ label, acl, childRoles }, null, 2)
);
let role = await new Parse.Query(Parse.Role)
.equalTo("name", name)
.first({ useMasterKey: true });
if (!role) {
role = new Parse.Role(name, acl);
role.set("label", label);
await role.save(null, {
useMasterKey: true,
});
}
let changed = false;
if (role.get("label") !== label) {
role.set("label", label);
changed = true;
}
if (!(0, fast_equals_1.deepEqual)(acl.toJSON(), role.getACL()?.toJSON())) {
role.setACL(acl);
changed = true;
}
if (Array.isArray(childRoles) && childRoles.length > 0) {
const relation = role.getRoles();
const currentChildRoles = await relation
.query()
.find({ useMasterKey: true });
const currentChildRoleNames = currentChildRoles.map((role) => role.get("name"));
for (const childRoleName of childRoles) {
if (currentChildRoleNames.includes(childRoleName)) {
continue;
}
const childRole = await new Parse.Query(Parse.Role)
.equalTo("name", childRoleName)
.find({ useMasterKey: true });
relation.add(childRole);
changed = true;
}
}
if (changed) {
await role.save(null, { useMasterKey: true });
}
}
/**
* Ensures that a user has a specific role.
* @param user The user to assign the role to.
* @param roleName The name of the role.
* @param add Specifies whether to add or remove the user from the role. Default is false (remove).
*/
async function ensureUserRole(user, roleName, add = false) {
const role = await new Parse.Query(Parse.Role)
.equalTo("name", roleName)
.first({ useMasterKey: true });
if (role) {
const relation = role.relation("users");
if (add) {
relation.add(user);
}
else {
relation.remove(user);
}
await role.save(null, { useMasterKey: true });
console.log("userRole", user.get("username"), roleName, add);
}
}
/**
* Checks if a field is immutable and throws an error if an attempt is made to edit it in a beforeSave hook.
*
* This will not throw an error if the request is made with the master key.
*
* @param request - The request object containing the original and object Parse objects, master flag, and session token.
* @param fieldName - The name of the field to check for immutability.
* @param permissionName - The name of the permission required to edit the field. Defaults to "parse:edit-immutable-fields".
* @returns Promise<void>
*/
async function immutableField(request, fieldName, permissionName = "parse:edit-immutable-fields") {
if (!request.original || request.master) {
return;
}
const previousValue = JSON.stringify(request.original.get(fieldName));
const nextValue = JSON.stringify(request.object.get(fieldName));
if ((0, fast_equals_1.deepEqual)(previousValue, nextValue)) {
return;
}
await requirePermission(request, permissionName, `You are not allowed to edit the '${fieldName}' field.`);
}
/**
* Handles the default beforeSave() logic for a Parse.Object.
*
* This function will check if a user or tenant field is present in the schema and set the value to the current user and/or the tenant of the current user if not provided.
* This will also mark the user and tenant fields as immutable, preventing them from being edited in the future.
*
* @param request - The Parse.Cloud.BeforeSaveRequest object.
* @returns - Returns void.
* @throws - Throws an error if the user is not provided.
*/
async function defaultHandler(request) {
const className = request.object.className;
if (request.master) {
console.log(`[@openinc/parse-server-opendash] Skipping default beforeSave() handler of ${className} for masterkey`);
return;
}
if (!request.user) {
throw new Error();
}
const currentSchema = schema[className];
if (!currentSchema) {
console.warn(`[@openinc/parse-server-opendash] Skipping default beforeSave() handler, no schema found for ${className}`);
return;
}
const userField = !!currentSchema?.fields?.user;
const tenantField = !!currentSchema?.fields?.tenant;
if (userField) {
await immutableField(request, "user");
}
if (tenantField) {
await immutableField(request, "tenant");
}
if (!request.original) {
if (userField) {
request.object.set("user", request.user);
}
if (tenantField) {
const user = (await request.user?.fetch({
useMasterKey: true,
}));
try {
await requirePermission(request, "opendash:can-admin-tenants", "User is not allowed to use tag which ignores tenant prefix");
if (!request.object.get("tenant")) {
request.object.set("tenant", user.get("tenant"));
}
}
catch (error) {
request.object.set("tenant", user.get("tenant"));
}
}
}
}
/**
* Handles the default Parse.ACL beforeSave() logic for a Parse.Object.
*
* This function will set the default ACL for the object based on the user and tenant fields. By default, the object will be readable and writable by the tenant role and readable by tenant users.
*
* @param request - The Parse.Cloud.BeforeSaveRequest object.
* @param options - Optional configuration options.
* @param options.allowCustomACL - Whether to allow custom ACL (default: false).
* @param options.allowTenantUserWrite - Whether to allow tenant users to write (default: false).
* @param options.denyTenantUserRead - Whether to deny tenant users from reading (default: false).
*/
async function defaultAclHandler(request, options) {
const className = request.object.className;
const currentSchema = schema[className];
if (!currentSchema) {
console.warn(`[@openinc/parse-server-opendash] Skipping default beforeSave() ACL handler, no schema found for ${className}`);
return;
}
const userField = !!currentSchema?.fields?.user;
const tenantField = !!currentSchema?.fields?.tenant;
if (!options?.allowCustomACL || !request.object.getACL()) {
request.object.setACL(new Parse.ACL());
}
const acl = request.object.getACL();
acl.setRoleReadAccess("od-admin", true);
acl.setRoleWriteAccess("od-admin", true);
if (userField) {
const user = request.object.get("user");
if (user) {
acl.setReadAccess(user.id, true);
acl.setWriteAccess(user.id, true);
}
}
if (tenantField) {
const tenant = request.object.get("tenant");
if (tenant) {
if (!options?.denyTenantUserRead) {
acl.setRoleReadAccess(`od-tenant-user-${tenant.id}`, true);
}
if (options?.allowTenantUserWrite) {
acl.setRoleWriteAccess(`od-tenant-user-${tenant.id}`, true);
}
acl.setRoleReadAccess(`od-tenant-admin-${tenant.id}`, true);
acl.setRoleWriteAccess(`od-tenant-admin-${tenant.id}`, true);
}
}
}
const beforeSaveHooks = {};
/**
* Registers a beforeSave hook for a Parse class. In comparison to Parse.Cloud.beforeSave(), this function allows multiple beforeSave hooks to be registered for the same class.
*
* @template T - The type of the Parse.Object.
* @param {string | { new (): T }} target - The name of the Parse class or the class itself.
* @param {beforeSaveHookType<T>} callback - The callback function to be executed before saving the object.
* @returns {void}
*/
function beforeSaveHook(target, callback) {
// @ts-ignore
const className = typeof target === "string" ? target : target.className;
if (!beforeSaveHooks[className]) {
beforeSaveHooks[className] = [];
Parse.Cloud.beforeSave(className, async function beforeSaveHookFunction(request) {
const sessionToken = request.user?.getSessionToken();
const newRequest = { ...request, sessionToken };
for (const fn of beforeSaveHooks[className]) {
await fn(newRequest);
}
});
}
beforeSaveHooks[className].push(callback);
}
const afterSaveHooks = {};
/**
* Registers a afterSave hook for a Parse class. In comparison to Parse.Cloud.afterSave(), this function allows multiple beforeSave hooks to be registered for the same class.
*
* @template T - The type of the Parse.Object.
* @param {string | { new (): T }} target - The name of the Parse class or the class itself.
* @param {afterSaveHookType<T>} callback - The callback function to be executed after saving the object.
* @returns {void}
*/
function afterSaveHook(target, callback) {
// @ts-ignore
const className = typeof target === "string" ? target : target.className;
if (!afterSaveHooks[className]) {
afterSaveHooks[className] = [];
Parse.Cloud.afterSave(className, async function afterSaveHookFunction(request) {
const sessionToken = request.user?.getSessionToken();
const newRequest = { ...request, sessionToken };
for (const fn of afterSaveHooks[className]) {
await fn(newRequest);
}
});
}
afterSaveHooks[className].push(callback);
}
const beforeDeleteHooks = {};
/**
* Registers a beforeDelete hook for a Parse class. In comparison to Parse.Cloud.beforeDelete(), this function allows multiple beforeSave hooks to be registered for the same class.
*
* @template T - The type of the Parse.Object.
* @param {string | { new (): T }} target - The name of the Parse class or the class itself.
* @param {beforeDeleteHookType<T>} callback - The callback function to be executed before deleting an object.
*/
function beforeDeleteHook(target, callback) {
// @ts-ignore
const className = typeof target === "string" ? target : target.className;
if (!beforeDeleteHooks[className]) {
beforeDeleteHooks[className] = [];
Parse.Cloud.beforeDelete(className, async function beforeDeleteHookFunction(request) {
const sessionToken = request.user?.getSessionToken();
const newRequest = { ...request, sessionToken };
for (const fn of beforeDeleteHooks[className]) {
await fn(newRequest);
}
});
}
beforeDeleteHooks[className].push(callback);
}
const afterDeleteHooks = {};
/**
* Registers a afterDelete hook for a Parse class. In comparison to Parse.Cloud.afterDelete(), this function allows multiple beforeSave hooks to be registered for the same class.
*
* @template T - The type of the Parse.Object.
* @param {string | { new (): T }} target - The name of the Parse class or the class itself.
* @param {afterDeleteHookType<T>} callback - The callback function to be executed after a delete operation.
* @returns {void}
*/
function afterDeleteHook(target, callback) {
// @ts-ignore
const className = typeof target === "string" ? target : target.className;
if (!afterDeleteHooks[className]) {
afterDeleteHooks[className] = [];
Parse.Cloud.afterDelete(className, async function afterDeleteHookFunction(request) {
const sessionToken = request.user?.getSessionToken();
const newRequest = { ...request, sessionToken };
for (const fn of afterDeleteHooks[className]) {
await fn(newRequest);
}
});
}
afterDeleteHooks[className].push(callback);
}
/**
* Autoloads cloud code files from the specified path. Each file MUST export an init() function that takes the file name (without it's extension) as an argument.
*
* @param path - The path to the directory containing the cloud code files.
* @param regex - Optional regular expression to filter the file names. Defaults to /^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/.
* @returns A promise that resolves when all the cloud code files have been loaded and initialized.
*/
async function autoloadCloudCode(path, regex = /^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/) {
const fns = fs_1.default
.readdirSync(path)
.filter((filename) => filename.endsWith(".js"))
.map((filename) => filename.replace(".js", ""))
.filter((name) => regex.test(name));
for (const name of fns) {
const { init } = require((0, path_1.join)(path, name));
if (!init || typeof init !== "function") {
continue;
}
console.log(`[@openinc/parse-server-opendash] Autoloading ${name}.js`);
await init(name).catch((e) => console.error(e));
}
}
async function initAutoload() {
const path = config.get("AUTOLOAD_DIR");
if (path) {
await autoloadCloudCode((0, path_1.resolve)(process.cwd(), path));
}
}
function getFeatureForClassName(className) {
const map = featuremap_json_1.default;
return map[className] || "unknown";
}
function isClassEnabled(className) {
// if (!config.getBoolean("FEATURES_ENABLED")) return true;
const feature = getFeatureForClassName(className);
const enabled = (0, config_1.isFeatureEnabled)(feature);
return enabled;
}
/**
* Ensures that a OD3_Permission with the specified key exists in the database.
* If the permission already exists, it updates the ACL if provided.
* If the permission does not exist, it creates a new permission with the specified key and ACL.
* @param key - The key of the permission.
* @param acl - The Parse.ACL to be set for the permission.
* @returns The updated or newly created permission.
*/
async function ensurePermission(key, acl) {
const permission = await new Parse.Query(types_1.Permission)
.equalTo("key", key)
.first({ useMasterKey: true });
if (permission) {
if (acl) {
permission.setACL(acl);
return await permission.save(null, { useMasterKey: true });
}
return permission;
}
else {
const newPermission = new types_1.Permission({ key });
if (acl) {
newPermission.setACL(acl);
}
else {
newPermission.setACL(new Parse.ACL());
}
return await newPermission.save(null, { useMasterKey: true });
}
}