diff --git a/backend/src/ai/LocalAIApi.js b/backend/src/ai/LocalAIApi.js index 4eb7665..31dd270 100644 --- a/backend/src/ai/LocalAIApi.js +++ b/backend/src/ai/LocalAIApi.js @@ -1,10 +1,10 @@ -"use strict"; +'use strict'; -const fs = require("fs"); -const path = require("path"); -const http = require("http"); -const https = require("https"); -const { URL } = require("url"); +const fs = require('fs'); +const path = require('path'); +const http = require('http'); +const https = require('https'); +const { URL } = require('url'); let CONFIG_CACHE = null; @@ -40,7 +40,7 @@ async function createResponse(params, options = {}) { if (!Array.isArray(payload.input) || payload.input.length === 0) { return { success: false, - error: "input_missing", + error: 'input_missing', message: 'Parameter "input" is required and must be a non-empty array.', }; } @@ -56,7 +56,7 @@ async function createResponse(params, options = {}) { } const data = initial.data; - if (data && typeof data === "object" && data.ai_request_id) { + if (data && typeof data === 'object' && data.ai_request_id) { const pollTimeout = Number(options.poll_timeout ?? 300); const pollInterval = Number(options.poll_interval ?? 5); return await awaitResponse(data.ai_request_id, { @@ -78,16 +78,16 @@ async function request(pathValue, payload = {}, options = {}) { if (!resolvedPath) { return { success: false, - error: "project_id_missing", - message: "PROJECT_ID is not defined; cannot resolve AI proxy endpoint.", + error: 'project_id_missing', + message: 'PROJECT_ID is not defined; cannot resolve AI proxy endpoint.', }; } if (!cfg.projectUuid) { return { success: false, - error: "project_uuid_missing", - message: "PROJECT_UUID is not defined; aborting AI request.", + error: 'project_uuid_missing', + message: 'PROJECT_UUID is not defined; aborting AI request.', }; } @@ -101,21 +101,21 @@ async function request(pathValue, payload = {}, options = {}) { const verifyTls = resolveVerifyTls(options.verify_tls, cfg.verifyTls); const headers = { - Accept: "application/json", - "Content-Type": "application/json", + Accept: 'application/json', + 'Content-Type': 'application/json', [cfg.projectHeader]: cfg.projectUuid, }; if (Array.isArray(options.headers)) { for (const header of options.headers) { - if (typeof header === "string" && header.includes(":")) { - const [name, value] = header.split(":", 2); + if (typeof header === 'string' && header.includes(':')) { + const [name, value] = header.split(':', 2); headers[name.trim()] = value.trim(); } } } const body = JSON.stringify(bodyPayload); - return sendRequest(url, "POST", body, headers, timeout, verifyTls); + return sendRequest(url, 'POST', body, headers, timeout, verifyTls); } async function fetchStatus(aiRequestId, options = {}) { @@ -123,8 +123,8 @@ async function fetchStatus(aiRequestId, options = {}) { if (!cfg.projectUuid) { return { success: false, - error: "project_uuid_missing", - message: "PROJECT_UUID is not defined; aborting status check.", + error: 'project_uuid_missing', + message: 'PROJECT_UUID is not defined; aborting status check.', }; } @@ -134,19 +134,19 @@ async function fetchStatus(aiRequestId, options = {}) { const verifyTls = resolveVerifyTls(options.verify_tls, cfg.verifyTls); const headers = { - Accept: "application/json", + Accept: 'application/json', [cfg.projectHeader]: cfg.projectUuid, }; if (Array.isArray(options.headers)) { for (const header of options.headers) { - if (typeof header === "string" && header.includes(":")) { - const [name, value] = header.split(":", 2); + if (typeof header === 'string' && header.includes(':')) { + const [name, value] = header.split(':', 2); headers[name.trim()] = value.trim(); } } } - return sendRequest(url, "GET", null, headers, timeout, verifyTls); + return sendRequest(url, 'GET', null, headers, timeout, verifyTls); } async function awaitResponse(aiRequestId, options = {}) { @@ -165,8 +165,8 @@ async function awaitResponse(aiRequestId, options = {}) { if (statusResp.success) { const data = statusResp.data || {}; - if (data && typeof data === "object") { - if (data.status === "success") { + if (data && typeof data === 'object') { + if (data.status === 'success') { isPending = false; return { success: true, @@ -174,12 +174,12 @@ async function awaitResponse(aiRequestId, options = {}) { data: data.response || data, }; } - if (data.status === "failed") { + if (data.status === 'failed') { isPending = false; return { success: false, status: 500, - error: String(data.error || "AI request failed"), + error: String(data.error || 'AI request failed'), data, }; } @@ -191,8 +191,8 @@ async function awaitResponse(aiRequestId, options = {}) { if (Date.now() >= deadline) { return { success: false, - error: "timeout", - message: "Timed out waiting for AI response.", + error: 'timeout', + message: 'Timed out waiting for AI response.', }; } @@ -201,13 +201,14 @@ async function awaitResponse(aiRequestId, options = {}) { } function extractText(response) { - const payload = response && typeof response === "object" ? response.data || response : null; - if (!payload || typeof payload !== "object") { - return ""; + const payload = + response && typeof response === 'object' ? response.data || response : null; + if (!payload || typeof payload !== 'object') { + return ''; } if (Array.isArray(payload.output)) { - let combined = ""; + let combined = ''; for (const item of payload.output) { if (!item || !Array.isArray(item.content)) { continue; @@ -215,9 +216,9 @@ function extractText(response) { for (const block of item.content) { if ( block && - typeof block === "object" && - block.type === "output_text" && - typeof block.text === "string" && + typeof block === 'object' && + block.type === 'output_text' && + typeof block.text === 'string' && block.text.length > 0 ) { combined += block.text; @@ -233,32 +234,38 @@ function extractText(response) { payload.choices && payload.choices[0] && payload.choices[0].message && - typeof payload.choices[0].message.content === "string" + typeof payload.choices[0].message.content === 'string' ) { return payload.choices[0].message.content; } - return ""; + return ''; } function decodeJsonFromResponse(response) { const text = extractText(response); if (!text) { - throw new Error("No text found in AI response."); + throw new Error('No text found in AI response.'); } const parsed = parseJson(text); - if (parsed.ok && parsed.value && typeof parsed.value === "object") { + if (parsed.ok && parsed.value && typeof parsed.value === 'object') { return parsed.value; } const stripped = stripJsonFence(text); if (stripped !== text) { const parsedStripped = parseJson(stripped); - if (parsedStripped.ok && parsedStripped.value && typeof parsedStripped.value === "object") { + if ( + parsedStripped.ok && + parsedStripped.value && + typeof parsedStripped.value === 'object' + ) { return parsedStripped.value; } - throw new Error(`JSON parse failed after stripping fences: ${parsedStripped.error}`); + throw new Error( + `JSON parse failed after stripping fences: ${parsedStripped.error}`, + ); } throw new Error(`JSON parse failed: ${parsed.error}`); @@ -271,7 +278,7 @@ function config() { ensureEnvLoaded(); - const baseUrl = process.env.AI_PROXY_BASE_URL || "https://flatlogic.com"; + const baseUrl = process.env.AI_PROXY_BASE_URL || 'https://flatlogic.com'; const projectId = process.env.PROJECT_ID || null; let responsesPath = process.env.AI_RESPONSES_PATH || null; if (!responsesPath && projectId) { @@ -286,8 +293,8 @@ function config() { responsesPath, projectId, projectUuid: process.env.PROJECT_UUID || null, - projectHeader: process.env.AI_PROJECT_HEADER || "project-uuid", - defaultModel: process.env.AI_DEFAULT_MODEL || "gpt-5-mini", + projectHeader: process.env.AI_PROJECT_HEADER || 'project-uuid', + defaultModel: process.env.AI_DEFAULT_MODEL || 'gpt-5-mini', timeout, verifyTls, }; @@ -296,29 +303,38 @@ function config() { } function buildUrl(pathValue, baseUrl) { - const trimmed = String(pathValue || "").trim(); - if (trimmed === "") { + const trimmed = String(pathValue || '').trim(); + if (trimmed === '') { return baseUrl; } - if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) { + if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) { return trimmed; } - if (trimmed.startsWith("/")) { + if (trimmed.startsWith('/')) { return `${baseUrl}${trimmed}`; } return `${baseUrl}/${trimmed}`; } function resolveStatusPath(aiRequestId, cfg) { - const basePath = (cfg.responsesPath || "").replace(/\/+$/, ""); + const basePath = (cfg.responsesPath || '').replace(/\/+$/, ''); if (!basePath) { return `/ai-request/${encodeURIComponent(String(aiRequestId))}/status`; } - const normalized = basePath.endsWith("/ai-request") ? basePath : `${basePath}/ai-request`; + const normalized = basePath.endsWith('/ai-request') + ? basePath + : `${basePath}/ai-request`; return `${normalized}/${encodeURIComponent(String(aiRequestId))}/status`; } -function sendRequest(urlString, method, body, headers, timeoutSeconds, verifyTls) { +function sendRequest( + urlString, + method, + body, + headers, + timeoutSeconds, + verifyTls, +) { return new Promise((resolve) => { let targetUrl; try { @@ -326,13 +342,13 @@ function sendRequest(urlString, method, body, headers, timeoutSeconds, verifyTls } catch (err) { resolve({ success: false, - error: "invalid_url", + error: 'invalid_url', message: err.message, }); return; } - const isHttps = targetUrl.protocol === "https:"; + const isHttps = targetUrl.protocol === 'https:'; const requestFn = isHttps ? https.request : http.request; const options = { protocol: targetUrl.protocol, @@ -348,12 +364,12 @@ function sendRequest(urlString, method, body, headers, timeoutSeconds, verifyTls } const req = requestFn(options, (res) => { - let responseBody = ""; - res.setEncoding("utf8"); - res.on("data", (chunk) => { + let responseBody = ''; + res.setEncoding('utf8'); + res.on('data', (chunk) => { responseBody += chunk; }); - res.on("end", () => { + res.on('end', () => { const status = res.statusCode || 0; const parsed = parseJson(responseBody); const payload = parsed.ok ? parsed.value : responseBody; @@ -372,9 +388,11 @@ function sendRequest(urlString, method, body, headers, timeoutSeconds, verifyTls } const errorMessage = - parsed.ok && payload && typeof payload === "object" - ? String(payload.error || payload.message || "AI proxy request failed") - : String(responseBody || "AI proxy request failed"); + parsed.ok && payload && typeof payload === 'object' + ? String( + payload.error || payload.message || 'AI proxy request failed', + ) + : String(responseBody || 'AI proxy request failed'); resolve({ success: false, @@ -386,14 +404,14 @@ function sendRequest(urlString, method, body, headers, timeoutSeconds, verifyTls }); }); - req.on("timeout", () => { - req.destroy(new Error("request_timeout")); + req.on('timeout', () => { + req.destroy(new Error('request_timeout')); }); - req.on("error", (err) => { + req.on('error', (err) => { resolve({ success: false, - error: "request_failed", + error: 'request_failed', message: err.message, }); }); @@ -406,8 +424,8 @@ function sendRequest(urlString, method, body, headers, timeoutSeconds, verifyTls } function parseJson(value) { - if (typeof value !== "string" || value.trim() === "") { - return { ok: false, error: "empty_response" }; + if (typeof value !== 'string' || value.trim() === '') { + return { ok: false, error: 'empty_response' }; } try { return { ok: true, value: JSON.parse(value) }; @@ -418,11 +436,14 @@ function parseJson(value) { function stripJsonFence(text) { const trimmed = text.trim(); - if (trimmed.startsWith("```json")) { - return trimmed.replace(/^```json/, "").replace(/```$/, "").trim(); + if (trimmed.startsWith('```json')) { + return trimmed + .replace(/^```json/, '') + .replace(/```$/, '') + .trim(); } - if (trimmed.startsWith("```")) { - return trimmed.replace(/^```/, "").replace(/```$/, "").trim(); + if (trimmed.startsWith('```')) { + return trimmed.replace(/^```/, '').replace(/```$/, '').trim(); } return text; } @@ -436,7 +457,7 @@ function resolveVerifyTls(value, fallback) { if (value === undefined || value === null) { return Boolean(fallback); } - return String(value).toLowerCase() !== "false" && String(value) !== "0"; + return String(value).toLowerCase() !== 'false' && String(value) !== '0'; } function ensureEnvLoaded() { @@ -444,29 +465,32 @@ function ensureEnvLoaded() { return; } - const envPath = path.resolve(__dirname, "../../../../.env"); + const envPath = path.resolve(__dirname, '../../../../.env'); if (!fs.existsSync(envPath)) { return; } let content; try { - content = fs.readFileSync(envPath, "utf8"); + content = fs.readFileSync(envPath, 'utf8'); } catch (err) { throw new Error(`Failed to read executor .env: ${err.message}`); } for (const line of content.split(/\r?\n/)) { const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) { + if (!trimmed || trimmed.startsWith('#') || !trimmed.includes('=')) { continue; } - const [rawKey, ...rest] = trimmed.split("="); + const [rawKey, ...rest] = trimmed.split('='); const key = rawKey.trim(); if (!key) { continue; } - const value = rest.join("=").trim().replace(/^['"]|['"]$/g, ""); + const value = rest + .join('=') + .trim() + .replace(/^['"]|['"]$/g, ''); if (!process.env[key]) { process.env[key] = value; } diff --git a/backend/src/auth/auth.js b/backend/src/auth/auth.js index a544932..630eb0e 100644 --- a/backend/src/auth/auth.js +++ b/backend/src/auth/auth.js @@ -10,59 +10,68 @@ const GoogleStrategy = require('passport-google-oauth2').Strategy; const MicrosoftStrategy = require('passport-microsoft').Strategy; const UsersDBApi = require('../db/api/users'); +passport.use( + new JWTstrategy( + { + passReqToCallback: true, + secretOrKey: config.secret_key, + jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken(), + }, + async (req, token, done) => { + try { + const user = await UsersDBApi.findBy({ email: token.user.email }); -passport.use(new JWTstrategy({ - passReqToCallback: true, - secretOrKey: config.secret_key, - jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken() -}, async (req, token, done) => { - try { - const user = await UsersDBApi.findBy( {email: token.user.email}); + if (user && user.disabled) { + return done(new Error(`User '${user.email}' is disabled`)); + } - if (user && user.disabled) { - return done (new Error(`User '${user.email}' is disabled`)); - } + req.currentUser = user; - req.currentUser = user; + return done(null, user); + } catch (error) { + done(error); + } + }, + ), +); - return done(null, user); - } catch (error) { - done(error); - } -})); +passport.use( + new GoogleStrategy( + { + clientID: config.google.clientId, + clientSecret: config.google.clientSecret, + callbackURL: config.apiUrl + '/auth/signin/google/callback', + passReqToCallback: true, + }, + function (request, accessToken, refreshToken, profile, done) { + socialStrategy(profile.email, profile, providers.GOOGLE, done); + }, + ), +); -passport.use(new GoogleStrategy({ - clientID: config.google.clientId, - clientSecret: config.google.clientSecret, - callbackURL: config.apiUrl + '/auth/signin/google/callback', - passReqToCallback: true - }, - function (request, accessToken, refreshToken, profile, done) { - socialStrategy(profile.email, profile, providers.GOOGLE, done); - } -)); - - -passport.use(new MicrosoftStrategy({ - clientID: config.microsoft.clientId, - clientSecret: config.microsoft.clientSecret, - callbackURL: config.apiUrl + '/auth/signin/microsoft/callback', - passReqToCallback: true - }, - function (request, accessToken, refreshToken, profile, done) { - const email = profile._json.mail || profile._json.userPrincipalName; - socialStrategy(email, profile, providers.MICROSOFT, done); - } -)); +passport.use( + new MicrosoftStrategy( + { + clientID: config.microsoft.clientId, + clientSecret: config.microsoft.clientSecret, + callbackURL: config.apiUrl + '/auth/signin/microsoft/callback', + passReqToCallback: true, + }, + function (request, accessToken, refreshToken, profile, done) { + const email = profile._json.mail || profile._json.userPrincipalName; + socialStrategy(email, profile, providers.MICROSOFT, done); + }, + ), +); function socialStrategy(email, profile, provider, done) { - db.users.findOrCreate({where: {email, provider}}).then(([user]) => { + db.users.findOrCreate({ where: { email, provider } }).then(([user]) => { const body = { id: user.id, email: user.email, name: profile.displayName, }; - const token = helpers.jwtSign({user: body}); - return done(null, {token}); + const token = helpers.jwtSign({ user: body }); + return done(null, { token }); }); } diff --git a/backend/src/config.js b/backend/src/config.js index 5313a08..4ee80c9 100644 --- a/backend/src/config.js +++ b/backend/src/config.js @@ -8,8 +8,8 @@ validateEnv(); const config = { gcloud: { - bucket: "fldemo-files", - hash: "afeefb9d49f5b7977577876b99532ac7" + bucket: 'fldemo-files', + hash: 'afeefb9d49f5b7977577876b99532ac7', }, s3: { bucket: process.env.AWS_S3_BUCKET || '', @@ -19,33 +19,33 @@ const config = { prefix: process.env.AWS_S3_PREFIX || 'afeefb9d49f5b7977577876b99532ac7', }, bcrypt: { - saltRounds: 12 + saltRounds: 12, }, - admin_pass: process.env.ADMIN_PASS || "88dbeaf8", - user_pass: process.env.USER_PASS || "c3baadeda5c6", - admin_email: process.env.ADMIN_EMAIL || "admin@flatlogic.com", + admin_pass: process.env.ADMIN_PASS || '88dbeaf8', + user_pass: process.env.USER_PASS || 'c3baadeda5c6', + admin_email: process.env.ADMIN_EMAIL || 'admin@flatlogic.com', providers: { LOCAL: 'local', GOOGLE: 'google', - MICROSOFT: 'microsoft' + MICROSOFT: 'microsoft', }, secret_key: process.env.SECRET_KEY || '88dbeaf8-e906-405e-9e41-c3baadeda5c6', remote: '', - port: process.env.NODE_ENV === "production" ? "" : "8080", - hostUI: process.env.NODE_ENV === "production" ? "" : "http://localhost", - portUI: process.env.NODE_ENV === "production" ? "" : "3000", + port: process.env.NODE_ENV === 'production' ? '' : '8080', + hostUI: process.env.NODE_ENV === 'production' ? '' : 'http://localhost', + portUI: process.env.NODE_ENV === 'production' ? '' : '3000', - portUIProd: process.env.NODE_ENV === "production" ? "" : ":3000", + portUIProd: process.env.NODE_ENV === 'production' ? '' : ':3000', - swaggerUI: process.env.NODE_ENV === "production" ? "" : "http://localhost", - swaggerPort: process.env.NODE_ENV === "production" ? "" : ":8080", + swaggerUI: process.env.NODE_ENV === 'production' ? '' : 'http://localhost', + swaggerPort: process.env.NODE_ENV === 'production' ? '' : ':8080', google: { clientId: process.env.GOOGLE_CLIENT_ID || '', clientSecret: process.env.GOOGLE_CLIENT_SECRET || '', }, microsoft: { - clientId: process.env.MS_CLIENT_ID || '', - clientSecret: process.env.MS_CLIENT_SECRET || '', + clientId: process.env.MS_CLIENT_ID || '', + clientSecret: process.env.MS_CLIENT_SECRET || '', }, uploadDir: os.tmpdir(), email: { @@ -58,26 +58,26 @@ const config = { }, tls: { rejectUnauthorized: process.env.EMAIL_TLS_REJECT_UNAUTHORIZED !== 'false', - } + }, }, roles: { - admin: 'Administrator', - - - user: 'Analytics Viewer', - + user: 'Analytics Viewer', }, project_uuid: '88dbeaf8-e906-405e-9e41-c3baadeda5c6', - flHost: process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'dev_stage' ? 'https://flatlogic.com/projects' : 'http://localhost:3000/projects', - + flHost: + process.env.NODE_ENV === 'production' || + process.env.NODE_ENV === 'dev_stage' + ? 'https://flatlogic.com/projects' + : 'http://localhost:3000/projects', gpt_key: process.env.GPT_KEY || '', }; -config.host = process.env.NODE_ENV === "production" ? config.remote : "http://localhost"; +config.host = + process.env.NODE_ENV === 'production' ? config.remote : 'http://localhost'; config.apiUrl = `${config.host}${config.port ? `:${config.port}` : ``}/api`; config.swaggerUrl = `${config.swaggerUI}${config.swaggerPort}`; config.uiUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}/#`; diff --git a/backend/src/db/api/access_logs.js b/backend/src/db/api/access_logs.js index 5b967f3..7854a73 100644 --- a/backend/src/db/api/access_logs.js +++ b/backend/src/db/api/access_logs.js @@ -27,7 +27,15 @@ class Access_logsDBApi extends GenericDBApi { } static get CSV_FIELDS() { - return ['id', 'environment', 'path', 'ip_address', 'user_agent', 'accessed_at', 'createdAt']; + return [ + 'id', + 'environment', + 'path', + 'ip_address', + 'user_agent', + 'accessed_at', + 'createdAt', + ]; } static get AUTOCOMPLETE_FIELD() { @@ -42,10 +50,7 @@ class Access_logsDBApi extends GenericDBApi { } static get FIND_BY_INCLUDES() { - return [ - { association: 'project' }, - { association: 'user' }, - ]; + return [{ association: 'project' }, { association: 'user' }]; } static getFieldMapping(data) { @@ -71,30 +76,50 @@ class Access_logsDBApi extends GenericDBApi { { model: db.projects, as: 'project', - where: filter.project ? { - [Op.or]: [ - { id: { [Op.in]: filter.project.split('|').map(term => Utils.uuid(term)) } }, - { - name: { - [Op.or]: filter.project.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) - } - }, - ] - } : {}, + where: filter.project + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.project + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.project + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, }, { model: db.users, as: 'user', - where: filter.user ? { - [Op.or]: [ - { id: { [Op.in]: filter.user.split('|').map(term => Utils.uuid(term)) } }, - { - firstName: { - [Op.or]: filter.user.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) - } - }, - ] - } : {}, + where: filter.user + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.user + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + firstName: { + [Op.or]: filter.user + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, }, ]; @@ -145,9 +170,10 @@ class Access_logsDBApi extends GenericDBApi { where, include, distinct: true, - order: filter.field && filter.sort - ? [[filter.field, filter.sort]] - : [['createdAt', 'desc']], + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], transaction: options.transaction, }; diff --git a/backend/src/db/api/asset_variants.js b/backend/src/db/api/asset_variants.js index a902937..c6bfc85 100644 --- a/backend/src/db/api/asset_variants.js +++ b/backend/src/db/api/asset_variants.js @@ -27,7 +27,15 @@ class Asset_variantsDBApi extends GenericDBApi { } static get CSV_FIELDS() { - return ['id', 'variant_type', 'cdn_url', 'width_px', 'height_px', 'size_mb', 'createdAt']; + return [ + 'id', + 'variant_type', + 'cdn_url', + 'width_px', + 'height_px', + 'size_mb', + 'createdAt', + ]; } static get AUTOCOMPLETE_FIELD() { @@ -35,9 +43,7 @@ class Asset_variantsDBApi extends GenericDBApi { } static get ASSOCIATIONS() { - return [ - { field: 'asset', setter: 'setAsset', isArray: false }, - ]; + return [{ field: 'asset', setter: 'setAsset', isArray: false }]; } static get FIND_BY_INCLUDES() { @@ -67,16 +73,26 @@ class Asset_variantsDBApi extends GenericDBApi { { model: db.assets, as: 'asset', - where: filter.asset ? { - [Op.or]: [ - { id: { [Op.in]: filter.asset.split('|').map(term => Utils.uuid(term)) } }, - { - name: { - [Op.or]: filter.asset.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) - } - }, - ] - } : {}, + where: filter.asset + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.asset + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.asset + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, }, ]; @@ -127,9 +143,10 @@ class Asset_variantsDBApi extends GenericDBApi { where, include, distinct: true, - order: filter.field && filter.sort - ? [[filter.field, filter.sort]] - : [['createdAt', 'desc']], + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], transaction: options.transaction, }; diff --git a/backend/src/db/api/assets.js b/backend/src/db/api/assets.js index 0d966f1..42b34e4 100644 --- a/backend/src/db/api/assets.js +++ b/backend/src/db/api/assets.js @@ -27,7 +27,17 @@ class AssetsDBApi extends GenericDBApi { } static get CSV_FIELDS() { - return ['id', 'name', 'asset_type', 'type', 'cdn_url', 'storage_key', 'mime_type', 'size_mb', 'createdAt']; + return [ + 'id', + 'name', + 'asset_type', + 'type', + 'cdn_url', + 'storage_key', + 'mime_type', + 'size_mb', + 'createdAt', + ]; } static get AUTOCOMPLETE_FIELD() { @@ -35,9 +45,7 @@ class AssetsDBApi extends GenericDBApi { } static get ASSOCIATIONS() { - return [ - { field: 'project', setter: 'setProject', isArray: false }, - ]; + return [{ field: 'project', setter: 'setProject', isArray: false }]; } static get FIND_BY_INCLUDES() { @@ -49,7 +57,12 @@ class AssetsDBApi extends GenericDBApi { static get RELATION_FILTERS() { return [ - { filterKey: 'project', model: db.projects, as: 'project', searchField: 'name' }, + { + filterKey: 'project', + model: db.projects, + as: 'project', + searchField: 'name', + }, ]; } @@ -83,16 +96,26 @@ class AssetsDBApi extends GenericDBApi { { model: db.projects, as: 'project', - where: filter.project ? { - [Op.or]: [ - { id: { [Op.in]: filter.project.split('|').map(term => Utils.uuid(term)) } }, - { - name: { - [Op.or]: filter.project.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) - } - }, - ] - } : {}, + where: filter.project + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.project + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.project + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, }, ]; @@ -143,9 +166,10 @@ class AssetsDBApi extends GenericDBApi { where, include, distinct: true, - order: filter.field && filter.sort - ? [[filter.field, filter.sort]] - : [['createdAt', 'desc']], + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], transaction: options.transaction, }; diff --git a/backend/src/db/api/base.api.js b/backend/src/db/api/base.api.js index a317bdc..a81bfdd 100644 --- a/backend/src/db/api/base.api.js +++ b/backend/src/db/api/base.api.js @@ -67,12 +67,15 @@ class GenericDBApi { createdById: currentUser.id, updatedById: currentUser.id, }, - { transaction } + { transaction }, ); for (const assoc of this.ASSOCIATIONS) { if (data[assoc.field] !== undefined) { - await record[assoc.setter](data[assoc.field] || (assoc.isArray ? [] : null), { transaction }); + await record[assoc.setter]( + data[assoc.field] || (assoc.isArray ? [] : null), + { transaction }, + ); } } @@ -161,9 +164,8 @@ class GenericDBApi { static async findBy(where, options = {}) { const transaction = options.transaction; - const include = options.include !== undefined - ? options.include - : this.FIND_BY_INCLUDES; + const include = + options.include !== undefined ? options.include : this.FIND_BY_INCLUDES; const record = await this.MODEL.findOne({ where, @@ -237,16 +239,27 @@ class GenericDBApi { model: rel.model, as: rel.as, required: searchTerms.length > 0, - where: searchTerms.length > 0 ? { - [Op.or]: [ - { id: { [Op.in]: searchTerms.map(term => Utils.uuid(term)) } }, - rel.searchField ? { - [rel.searchField]: { - [Op.or]: searchTerms.map(term => ({ [Op.iLike]: `%${term}%` })) + where: + searchTerms.length > 0 + ? { + [Op.or]: [ + { + id: { + [Op.in]: searchTerms.map((term) => Utils.uuid(term)), + }, + }, + rel.searchField + ? { + [rel.searchField]: { + [Op.or]: searchTerms.map((term) => ({ + [Op.iLike]: `%${term}%`, + })), + }, + } + : {}, + ], } - } : {} - ] - } : undefined + : undefined, }; include = [relInclude, ...include]; } @@ -256,9 +269,10 @@ class GenericDBApi { where, include, distinct: true, - order: filter.field && filter.sort - ? [[filter.field, filter.sort]] - : [['createdAt', 'desc']], + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], transaction: options.transaction, }; diff --git a/backend/src/db/api/element_type_defaults.js b/backend/src/db/api/element_type_defaults.js index fd793a2..d71dc88 100644 --- a/backend/src/db/api/element_type_defaults.js +++ b/backend/src/db/api/element_type_defaults.js @@ -23,7 +23,14 @@ class Element_type_defaultsDBApi extends GenericDBApi { } static get CSV_FIELDS() { - return ['id', 'element_type', 'name', 'sort_order', 'is_active', 'createdAt']; + return [ + 'id', + 'element_type', + 'name', + 'sort_order', + 'is_active', + 'createdAt', + ]; } static get AUTOCOMPLETE_FIELD() { @@ -37,7 +44,8 @@ class Element_type_defaultsDBApi extends GenericDBApi { name: data.name ?? null, sort_order: data.sort_order ?? 0, default_settings_json: - data.default_settings_json === null || data.default_settings_json === undefined + data.default_settings_json === null || + data.default_settings_json === undefined ? null : typeof data.default_settings_json === 'string' ? data.default_settings_json diff --git a/backend/src/db/api/file.js b/backend/src/db/api/file.js index 7b65318..ce0a33e 100644 --- a/backend/src/db/api/file.js +++ b/backend/src/db/api/file.js @@ -3,16 +3,9 @@ const assert = require('assert'); const services = require('../../services/file'); module.exports = class FileDBApi { - static async replaceRelationFiles( - relation, - rawFiles, - options, - ) { + static async replaceRelationFiles(relation, rawFiles, options) { assert(relation.belongsTo, 'belongsTo is required'); - assert( - relation.belongsToColumn, - 'belongsToColumn is required', - ); + assert(relation.belongsToColumn, 'belongsToColumn is required'); assert(relation.belongsToId, 'belongsToId is required'); let files = []; @@ -29,11 +22,9 @@ module.exports = class FileDBApi { static async _addFiles(relation, files, options) { const transaction = (options && options.transaction) || undefined; - const currentUser = (options && options.currentUser) || {id: null}; + const currentUser = (options && options.currentUser) || { id: null }; - const inexistentFiles = files.filter( - (file) => !!file.new, - ); + const inexistentFiles = files.filter((file) => !!file.new); for (const file of inexistentFiles) { await db.file.create( @@ -55,11 +46,7 @@ module.exports = class FileDBApi { } } - static async _removeLegacyFiles( - relation, - files, - options, - ) { + static async _removeLegacyFiles(relation, files, options) { const transaction = (options && options.transaction) || undefined; const filesToDelete = await db.file.findAll({ @@ -68,10 +55,9 @@ module.exports = class FileDBApi { belongsToId: relation.belongsToId, belongsToColumn: relation.belongsToColumn, id: { - [db.Sequelize.Op - .notIn]: files + [db.Sequelize.Op.notIn]: files .filter((file) => !file.new) - .map((file) => file.id) + .map((file) => file.id), }, }, transaction, diff --git a/backend/src/db/api/presigned_url_requests.js b/backend/src/db/api/presigned_url_requests.js index b478188..2e0324c 100644 --- a/backend/src/db/api/presigned_url_requests.js +++ b/backend/src/db/api/presigned_url_requests.js @@ -27,7 +27,15 @@ class Presigned_url_requestsDBApi extends GenericDBApi { } static get CSV_FIELDS() { - return ['id', 'purpose', 'asset_type', 'requested_key', 'mime_type', 'status', 'createdAt']; + return [ + 'id', + 'purpose', + 'asset_type', + 'requested_key', + 'mime_type', + 'status', + 'createdAt', + ]; } static get AUTOCOMPLETE_FIELD() { @@ -42,10 +50,7 @@ class Presigned_url_requestsDBApi extends GenericDBApi { } static get FIND_BY_INCLUDES() { - return [ - { association: 'project' }, - { association: 'user' }, - ]; + return [{ association: 'project' }, { association: 'user' }]; } static getFieldMapping(data) { @@ -73,30 +78,50 @@ class Presigned_url_requestsDBApi extends GenericDBApi { { model: db.projects, as: 'project', - where: filter.project ? { - [Op.or]: [ - { id: { [Op.in]: filter.project.split('|').map(term => Utils.uuid(term)) } }, - { - name: { - [Op.or]: filter.project.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) - } - }, - ] - } : {}, + where: filter.project + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.project + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.project + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, }, { model: db.users, as: 'user', - where: filter.user ? { - [Op.or]: [ - { id: { [Op.in]: filter.user.split('|').map(term => Utils.uuid(term)) } }, - { - firstName: { - [Op.or]: filter.user.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) - } - }, - ] - } : {}, + where: filter.user + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.user + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + firstName: { + [Op.or]: filter.user + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, }, ]; @@ -147,9 +172,10 @@ class Presigned_url_requestsDBApi extends GenericDBApi { where, include, distinct: true, - order: filter.field && filter.sort - ? [[filter.field, filter.sort]] - : [['createdAt', 'desc']], + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], transaction: options.transaction, }; diff --git a/backend/src/db/api/project_audio_tracks.js b/backend/src/db/api/project_audio_tracks.js index 9d2859a..3ad952d 100644 --- a/backend/src/db/api/project_audio_tracks.js +++ b/backend/src/db/api/project_audio_tracks.js @@ -31,7 +31,17 @@ class Project_audio_tracksDBApi extends GenericDBApi { } static get CSV_FIELDS() { - return ['id', 'environment', 'source_key', 'name', 'slug', 'url', 'loop', 'volume', 'createdAt']; + return [ + 'id', + 'environment', + 'source_key', + 'name', + 'slug', + 'url', + 'loop', + 'volume', + 'createdAt', + ]; } static get AUTOCOMPLETE_FIELD() { @@ -39,9 +49,7 @@ class Project_audio_tracksDBApi extends GenericDBApi { } static get ASSOCIATIONS() { - return [ - { field: 'project', setter: 'setProject', isArray: false }, - ]; + return [{ field: 'project', setter: 'setProject', isArray: false }]; } static getFieldMapping(data) { @@ -64,7 +72,7 @@ class Project_audio_tracksDBApi extends GenericDBApi { const queryWhere = applyRuntimeEnvironment({ ...where }, options); const projectInclude = applyRuntimeProjectFilter( { model: db.projects, as: 'project' }, - options + options, ); const record = await this.MODEL.findOne({ @@ -89,16 +97,26 @@ class Project_audio_tracksDBApi extends GenericDBApi { { model: db.projects, as: 'project', - where: filter.project ? { - [Op.or]: [ - { id: { [Op.in]: filter.project.split('|').map(term => Utils.uuid(term)) } }, - { - name: { - [Op.or]: filter.project.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) - } - }, - ] - } : {}, + where: filter.project + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.project + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.project + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, }, ]; @@ -153,9 +171,10 @@ class Project_audio_tracksDBApi extends GenericDBApi { where, include, distinct: true, - order: filter.field && filter.sort - ? [[filter.field, filter.sort]] - : [['createdAt', 'desc']], + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], transaction: options.transaction, }; diff --git a/backend/src/db/api/project_element_defaults.js b/backend/src/db/api/project_element_defaults.js index e623c0e..a65bff2 100644 --- a/backend/src/db/api/project_element_defaults.js +++ b/backend/src/db/api/project_element_defaults.js @@ -27,26 +27,34 @@ class Project_element_defaultsDBApi extends GenericDBApi { } static get ASSOCIATIONS() { - return [ - { field: 'project', setter: 'setProject', isArray: false }, - ]; + return [{ field: 'project', setter: 'setProject', isArray: false }]; } static get RELATION_FILTERS() { return [ - { filterKey: 'project', model: db.projects, as: 'project', searchField: 'name' }, + { + filterKey: 'project', + model: db.projects, + as: 'project', + searchField: 'name', + }, ]; } static get FIND_ALL_INCLUDES() { - return [ - { association: 'project' }, - { association: 'source_element' }, - ]; + return [{ association: 'project' }, { association: 'source_element' }]; } static get CSV_FIELDS() { - return ['id', 'element_type', 'name', 'sort_order', 'projectId', 'snapshot_version', 'createdAt']; + return [ + 'id', + 'element_type', + 'name', + 'sort_order', + 'projectId', + 'snapshot_version', + 'createdAt', + ]; } static get AUTOCOMPLETE_FIELD() { @@ -90,16 +98,26 @@ class Project_element_defaultsDBApi extends GenericDBApi { { model: db.projects, as: 'project', - where: projectFilter ? { - [Op.or]: [ - { id: { [Op.in]: projectFilter.split('|').map(term => Utils.uuid(term)) } }, - { - name: { - [Op.or]: projectFilter.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) - } - }, - ] - } : {}, + where: projectFilter + ? { + [Op.or]: [ + { + id: { + [Op.in]: projectFilter + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: projectFilter + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, }, { model: db.element_type_defaults, @@ -151,9 +169,10 @@ class Project_element_defaultsDBApi extends GenericDBApi { where, include, distinct: true, - order: filter.field && filter.sort - ? [[filter.field, filter.sort]] - : [['sort_order', 'asc']], + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['sort_order', 'asc']], transaction: options.transaction, }; @@ -223,7 +242,7 @@ class Project_element_defaultsDBApi extends GenericDBApi { { transaction: options.transaction, returning: true, - } + }, ); return projectDefaults; @@ -253,7 +272,9 @@ class Project_element_defaultsDBApi extends GenericDBApi { }); if (!globalDefault) { - throw new Error(`No global default found for element type: ${projectDefault.element_type}`); + throw new Error( + `No global default found for element type: ${projectDefault.element_type}`, + ); } // Update with global settings and increment version @@ -270,7 +291,7 @@ class Project_element_defaultsDBApi extends GenericDBApi { }, { transaction: options.transaction, - } + }, ); return projectDefault.reload(); @@ -309,15 +330,18 @@ class Project_element_defaultsDBApi extends GenericDBApi { } // Parse JSON settings for comparison - const projectSettings = typeof projectDefault.settings_json === 'string' - ? JSON.parse(projectDefault.settings_json || '{}') - : projectDefault.settings_json || {}; + const projectSettings = + typeof projectDefault.settings_json === 'string' + ? JSON.parse(projectDefault.settings_json || '{}') + : projectDefault.settings_json || {}; - const globalSettings = typeof globalDefault.default_settings_json === 'string' - ? JSON.parse(globalDefault.default_settings_json || '{}') - : globalDefault.default_settings_json || {}; + const globalSettings = + typeof globalDefault.default_settings_json === 'string' + ? JSON.parse(globalDefault.default_settings_json || '{}') + : globalDefault.default_settings_json || {}; - const isDifferent = JSON.stringify(projectSettings) !== JSON.stringify(globalSettings) || + const isDifferent = + JSON.stringify(projectSettings) !== JSON.stringify(globalSettings) || projectDefault.name !== globalDefault.name || projectDefault.sort_order !== globalDefault.sort_order; diff --git a/backend/src/db/api/project_memberships.js b/backend/src/db/api/project_memberships.js index 39226d5..8284d7e 100644 --- a/backend/src/db/api/project_memberships.js +++ b/backend/src/db/api/project_memberships.js @@ -27,7 +27,14 @@ class Project_membershipsDBApi extends GenericDBApi { } static get CSV_FIELDS() { - return ['id', 'access_level', 'is_active', 'invited_at', 'accepted_at', 'createdAt']; + return [ + 'id', + 'access_level', + 'is_active', + 'invited_at', + 'accepted_at', + 'createdAt', + ]; } static get AUTOCOMPLETE_FIELD() { @@ -42,10 +49,7 @@ class Project_membershipsDBApi extends GenericDBApi { } static get FIND_BY_INCLUDES() { - return [ - { association: 'project' }, - { association: 'user' }, - ]; + return [{ association: 'project' }, { association: 'user' }]; } static getFieldMapping(data) { @@ -70,30 +74,50 @@ class Project_membershipsDBApi extends GenericDBApi { { model: db.projects, as: 'project', - where: filter.project ? { - [Op.or]: [ - { id: { [Op.in]: filter.project.split('|').map(term => Utils.uuid(term)) } }, - { - name: { - [Op.or]: filter.project.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) - } - }, - ] - } : {}, + where: filter.project + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.project + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.project + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, }, { model: db.users, as: 'user', - where: filter.user ? { - [Op.or]: [ - { id: { [Op.in]: filter.user.split('|').map(term => Utils.uuid(term)) } }, - { - firstName: { - [Op.or]: filter.user.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) - } - }, - ] - } : {}, + where: filter.user + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.user + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + firstName: { + [Op.or]: filter.user + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, }, ]; @@ -138,9 +162,10 @@ class Project_membershipsDBApi extends GenericDBApi { where, include, distinct: true, - order: filter.field && filter.sort - ? [[filter.field, filter.sort]] - : [['createdAt', 'desc']], + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], transaction: options.transaction, }; diff --git a/backend/src/db/api/projects.js b/backend/src/db/api/projects.js index 91ba507..4365a63 100644 --- a/backend/src/db/api/projects.js +++ b/backend/src/db/api/projects.js @@ -16,7 +16,17 @@ class ProjectsDBApi extends GenericDBApi { } static get SEARCHABLE_FIELDS() { - return ['name', 'slug', 'description', 'logo_url', 'favicon_url', 'og_image_url', 'theme_config_json', 'custom_css_json', 'cdn_base_url']; + return [ + 'name', + 'slug', + 'description', + 'logo_url', + 'favicon_url', + 'og_image_url', + 'theme_config_json', + 'custom_css_json', + 'cdn_base_url', + ]; } static get RANGE_FIELDS() { @@ -28,7 +38,15 @@ class ProjectsDBApi extends GenericDBApi { } static get CSV_FIELDS() { - return ['id', 'name', 'slug', 'description', 'logo_url', 'cdn_base_url', 'createdAt']; + return [ + 'id', + 'name', + 'slug', + 'description', + 'logo_url', + 'cdn_base_url', + 'createdAt', + ]; } static get AUTOCOMPLETE_FIELD() { @@ -81,9 +99,8 @@ class ProjectsDBApi extends GenericDBApi { queryWhere.slug = runtimeProjectSlug; } - const include = options.include !== undefined - ? options.include - : this.DEFAULT_INCLUDES; + const include = + options.include !== undefined ? options.include : this.DEFAULT_INCLUDES; const record = await this.MODEL.findOne({ where: queryWhere, @@ -113,7 +130,10 @@ class ProjectsDBApi extends GenericDBApi { }); } catch (error) { // Log but don't fail project creation if snapshot fails - console.error('Failed to snapshot global element defaults to project:', error); + console.error( + 'Failed to snapshot global element defaults to project:', + error, + ); } return project; @@ -182,9 +202,10 @@ class ProjectsDBApi extends GenericDBApi { where, include, distinct: true, - order: filter.field && filter.sort - ? [[filter.field, filter.sort]] - : [['createdAt', 'desc']], + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], transaction: options.transaction, }; diff --git a/backend/src/db/api/publish_events.js b/backend/src/db/api/publish_events.js index e61fa66..4348df9 100644 --- a/backend/src/db/api/publish_events.js +++ b/backend/src/db/api/publish_events.js @@ -19,7 +19,13 @@ class Publish_eventsDBApi extends GenericDBApi { } static get RANGE_FIELDS() { - return ['started_at', 'finished_at', 'pages_copied', 'transitions_copied', 'audios_copied']; + return [ + 'started_at', + 'finished_at', + 'pages_copied', + 'transitions_copied', + 'audios_copied', + ]; } static get ENUM_FIELDS() { @@ -27,7 +33,16 @@ class Publish_eventsDBApi extends GenericDBApi { } static get CSV_FIELDS() { - return ['id', 'title', 'description', 'from_environment', 'to_environment', 'status', 'pages_copied', 'createdAt']; + return [ + 'id', + 'title', + 'description', + 'from_environment', + 'to_environment', + 'status', + 'pages_copied', + 'createdAt', + ]; } static get AUTOCOMPLETE_FIELD() { @@ -42,10 +57,7 @@ class Publish_eventsDBApi extends GenericDBApi { } static get FIND_BY_INCLUDES() { - return [ - { association: 'project' }, - { association: 'user' }, - ]; + return [{ association: 'project' }, { association: 'user' }]; } static getFieldMapping(data) { @@ -77,30 +89,50 @@ class Publish_eventsDBApi extends GenericDBApi { { model: db.projects, as: 'project', - where: filter.project ? { - [Op.or]: [ - { id: { [Op.in]: filter.project.split('|').map(term => Utils.uuid(term)) } }, - { - name: { - [Op.or]: filter.project.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) - } - }, - ] - } : {}, + where: filter.project + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.project + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.project + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, }, { model: db.users, as: 'user', - where: filter.user ? { - [Op.or]: [ - { id: { [Op.in]: filter.user.split('|').map(term => Utils.uuid(term)) } }, - { - firstName: { - [Op.or]: filter.user.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) - } - }, - ] - } : {}, + where: filter.user + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.user + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + firstName: { + [Op.or]: filter.user + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, }, ]; @@ -151,9 +183,10 @@ class Publish_eventsDBApi extends GenericDBApi { where, include, distinct: true, - order: filter.field && filter.sort - ? [[filter.field, filter.sort]] - : [['createdAt', 'desc']], + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], transaction: options.transaction, }; diff --git a/backend/src/db/api/pwa_caches.js b/backend/src/db/api/pwa_caches.js index e722cd8..03e30ec 100644 --- a/backend/src/db/api/pwa_caches.js +++ b/backend/src/db/api/pwa_caches.js @@ -27,7 +27,14 @@ class Pwa_cachesDBApi extends GenericDBApi { } static get CSV_FIELDS() { - return ['id', 'environment', 'cache_version', 'is_active', 'generated_at', 'createdAt']; + return [ + 'id', + 'environment', + 'cache_version', + 'is_active', + 'generated_at', + 'createdAt', + ]; } static get AUTOCOMPLETE_FIELD() { @@ -35,9 +42,7 @@ class Pwa_cachesDBApi extends GenericDBApi { } static get ASSOCIATIONS() { - return [ - { field: 'project', setter: 'setProject', isArray: false }, - ]; + return [{ field: 'project', setter: 'setProject', isArray: false }]; } static get FIND_BY_INCLUDES() { @@ -68,16 +73,26 @@ class Pwa_cachesDBApi extends GenericDBApi { { model: db.projects, as: 'project', - where: filter.project ? { - [Op.or]: [ - { id: { [Op.in]: filter.project.split('|').map(term => Utils.uuid(term)) } }, - { - name: { - [Op.or]: filter.project.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) - } - }, - ] - } : {}, + where: filter.project + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.project + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.project + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, }, ]; @@ -128,9 +143,10 @@ class Pwa_cachesDBApi extends GenericDBApi { where, include, distinct: true, - order: filter.field && filter.sort - ? [[filter.field, filter.sort]] - : [['createdAt', 'desc']], + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], transaction: options.transaction, }; diff --git a/backend/src/db/api/roles.js b/backend/src/db/api/roles.js index 595812b..43f4daf 100644 --- a/backend/src/db/api/roles.js +++ b/backend/src/db/api/roles.js @@ -35,16 +35,11 @@ class RolesDBApi extends GenericDBApi { } static get ASSOCIATIONS() { - return [ - { field: 'permissions', setter: 'setPermissions', isArray: true }, - ]; + return [{ field: 'permissions', setter: 'setPermissions', isArray: true }]; } static get FIND_BY_INCLUDES() { - return [ - { association: 'users_app_role' }, - { association: 'permissions' }, - ]; + return [{ association: 'users_app_role' }, { association: 'permissions' }]; } static getFieldMapping(data) { @@ -92,16 +87,25 @@ class RolesDBApi extends GenericDBApi { model: db.permissions, as: 'permissions_filter', required: searchTerms.length > 0, - where: searchTerms.length > 0 ? { - [Op.or]: [ - { id: { [Op.in]: searchTerms.map(term => Utils.uuid(term)) } }, - { - name: { - [Op.or]: searchTerms.map(term => ({ [Op.iLike]: `%${term}%` })) + where: + searchTerms.length > 0 + ? { + [Op.or]: [ + { + id: { + [Op.in]: searchTerms.map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: searchTerms.map((term) => ({ + [Op.iLike]: `%${term}%`, + })), + }, + }, + ], } - } - ] - } : undefined + : undefined, }, ...include, ]; @@ -121,9 +125,10 @@ class RolesDBApi extends GenericDBApi { where, include, distinct: true, - order: filter.field && filter.sort - ? [[filter.field, filter.sort]] - : [['createdAt', 'desc']], + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], transaction: options.transaction, }; diff --git a/backend/src/db/api/tour_pages.js b/backend/src/db/api/tour_pages.js index 4fc353a..a6513d0 100644 --- a/backend/src/db/api/tour_pages.js +++ b/backend/src/db/api/tour_pages.js @@ -19,7 +19,15 @@ class Tour_pagesDBApi extends GenericDBApi { } static get SEARCHABLE_FIELDS() { - return ['source_key', 'name', 'slug', 'background_image_url', 'background_video_url', 'background_audio_url', 'ui_schema_json']; + return [ + 'source_key', + 'name', + 'slug', + 'background_image_url', + 'background_video_url', + 'background_audio_url', + 'ui_schema_json', + ]; } static get RANGE_FIELDS() { @@ -31,7 +39,15 @@ class Tour_pagesDBApi extends GenericDBApi { } static get CSV_FIELDS() { - return ['id', 'environment', 'source_key', 'name', 'slug', 'sort_order', 'createdAt']; + return [ + 'id', + 'environment', + 'source_key', + 'name', + 'slug', + 'sort_order', + 'createdAt', + ]; } static get AUTOCOMPLETE_FIELD() { @@ -39,9 +55,7 @@ class Tour_pagesDBApi extends GenericDBApi { } static get ASSOCIATIONS() { - return [ - { field: 'project', setter: 'setProject', isArray: false }, - ]; + return [{ field: 'project', setter: 'setProject', isArray: false }]; } static getFieldMapping(data) { @@ -74,7 +88,7 @@ class Tour_pagesDBApi extends GenericDBApi { createdById: currentUser.id, updatedById: currentUser.id, }, - { transaction } + { transaction }, ); await record.setProject(projectId, { transaction }); @@ -96,9 +110,7 @@ class Tour_pagesDBApi extends GenericDBApi { const record = await this.MODEL.findOne({ where: queryWhere, transaction, - include: [ - projectInclude, - ], + include: [projectInclude], }); if (!record) return null; @@ -117,16 +129,26 @@ class Tour_pagesDBApi extends GenericDBApi { { model: db.projects, as: 'project', - where: filter.project ? { - [Op.or]: [ - { id: { [Op.in]: filter.project.split('|').map(term => Utils.uuid(term)) } }, - { - name: { - [Op.or]: filter.project.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) - } - }, - ] - } : {}, + where: filter.project + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.project + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.project + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, }, ]; @@ -181,9 +203,10 @@ class Tour_pagesDBApi extends GenericDBApi { where, include, distinct: true, - order: filter.field && filter.sort - ? [[filter.field, filter.sort]] - : [['createdAt', 'desc']], + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], transaction: options.transaction, }; diff --git a/backend/src/db/api/users.js b/backend/src/db/api/users.js index 03519dc..7b39b6d 100644 --- a/backend/src/db/api/users.js +++ b/backend/src/db/api/users.js @@ -1,4 +1,3 @@ - const db = require('../models'); const FileDBApi = require('./file'); const crypto = require('crypto'); @@ -7,452 +6,324 @@ const Utils = require('../utils'); const bcrypt = require('bcrypt'); const config = require('../../config'); - - const Sequelize = db.Sequelize; const Op = Sequelize.Op; module.exports = class UsersDBApi { - - static async create(data, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const users = await db.users.create( + { + id: data.data.id || undefined, - const users = await db.users.create( - { - id: data.data.id || undefined, - - firstName: data.data.firstName - || - null - , - - lastName: data.data.lastName - || - null - , - - phoneNumber: data.data.phoneNumber - || - null - , - - email: data.data.email - || - null - , - - disabled: data.data.disabled - || - false - - , - - password: data.data.password - || - null - , - - emailVerified: data.data.emailVerified - || - true - - , - - emailVerificationToken: data.data.emailVerificationToken - || - null - , - - emailVerificationTokenExpiresAt: data.data.emailVerificationTokenExpiresAt - || - null - , - - passwordResetToken: data.data.passwordResetToken - || - null - , - - passwordResetTokenExpiresAt: data.data.passwordResetTokenExpiresAt - || - null - , - - provider: data.data.provider - || - null - , - - importHash: data.data.importHash || null, - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { transaction }, + firstName: data.data.firstName || null, + lastName: data.data.lastName || null, + phoneNumber: data.data.phoneNumber || null, + email: data.data.email || null, + disabled: data.data.disabled || false, + + password: data.data.password || null, + emailVerified: data.data.emailVerified || true, + + emailVerificationToken: data.data.emailVerificationToken || null, + emailVerificationTokenExpiresAt: + data.data.emailVerificationTokenExpiresAt || null, + passwordResetToken: data.data.passwordResetToken || null, + passwordResetTokenExpiresAt: + data.data.passwordResetTokenExpiresAt || null, + provider: data.data.provider || null, + importHash: data.data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, ); - - - if (!data.data.app_role) { - const role = await db.roles.findOne({ - where: { name: 'User' }, - }); - if (role) { - await users.setApp_role(role, { - transaction, - }); - } - }else{ - await users.setApp_role(data.data.app_role || null, { - transaction, - }); - } - - - - - await users.setCustom_permissions(data.data.custom_permissions || [], { - transaction, + if (!data.data.app_role) { + const role = await db.roles.findOne({ + where: { name: 'User' }, + }); + if (role) { + await users.setApp_role(role, { + transaction, }); - - - - await FileDBApi.replaceRelationFiles( - { - belongsTo: db.users.getTableName(), - belongsToColumn: 'avatar', - belongsToId: users.id, - }, - data.data.avatar, - options, - ); - - - return users; + } + } else { + await users.setApp_role(data.data.app_role || null, { + transaction, + }); } - - + await users.setCustom_permissions(data.data.custom_permissions || [], { + transaction, + }); - static async bulkImport(data, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; + await FileDBApi.replaceRelationFiles( + { + belongsTo: db.users.getTableName(), + belongsToColumn: 'avatar', + belongsToId: users.id, + }, + data.data.avatar, + options, + ); - // Prepare data - wrapping individual data transformations in a map() method - const usersData = data.map((item, index) => ({ - id: item.id || undefined, - - firstName: item.firstName - || - null - , - - lastName: item.lastName - || - null - , - - phoneNumber: item.phoneNumber - || - null - , - - email: item.email - || - null - , - - disabled: item.disabled - || - false - - , - - password: item.password - || - null - , - - emailVerified: item.emailVerified - || - false - - , - - emailVerificationToken: item.emailVerificationToken - || - null - , - - emailVerificationTokenExpiresAt: item.emailVerificationTokenExpiresAt - || - null - , - - passwordResetToken: item.passwordResetToken - || - null - , - - passwordResetTokenExpiresAt: item.passwordResetTokenExpiresAt - || - null - , - - provider: item.provider - || - null - , - - importHash: item.importHash || null, - createdById: currentUser.id, - updatedById: currentUser.id, - createdAt: new Date(Date.now() + index * 1000), + return users; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const usersData = data.map((item, index) => ({ + id: item.id || undefined, + + firstName: item.firstName || null, + lastName: item.lastName || null, + phoneNumber: item.phoneNumber || null, + email: item.email || null, + disabled: item.disabled || false, + + password: item.password || null, + emailVerified: item.emailVerified || false, + + emailVerificationToken: item.emailVerificationToken || null, + emailVerificationTokenExpiresAt: + item.emailVerificationTokenExpiresAt || null, + passwordResetToken: item.passwordResetToken || null, + passwordResetTokenExpiresAt: item.passwordResetTokenExpiresAt || null, + provider: item.provider || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), })); - // Bulk create items - const users = await db.users.bulkCreate(usersData, { transaction }); + // Bulk create items + const users = await db.users.bulkCreate(usersData, { transaction }); - // For each item created, replace relation files - - for (let i = 0; i < users.length; i++) { - await FileDBApi.replaceRelationFiles( - { - belongsTo: db.users.getTableName(), - belongsToColumn: 'avatar', - belongsToId: users[i].id, - }, - data[i].avatar, - options, - ); - } - + // For each item created, replace relation files - return users; + for (let i = 0; i < users.length; i++) { + await FileDBApi.replaceRelationFiles( + { + belongsTo: db.users.getTableName(), + belongsToColumn: 'avatar', + belongsToId: users[i].id, + }, + data[i].avatar, + options, + ); } - static async update(id, data, options) { - const currentUser = (options && options.currentUser) || {id: null}; - const transaction = (options && options.transaction) || undefined; - + return users; + } - const users = await db.users.findByPk(id, {transaction}); + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const users = await db.users.findByPk(id, { transaction }); - - - if (!data?.app_role) { - data.app_role = users?.app_role?.id; - } - if (!data?.custom_permissions) { - data.custom_permissions = users?.custom_permissions?.map(item => item.id); - } - - if (data.password) { - data.password = bcrypt.hashSync( - data.password, - config.bcrypt.saltRounds, - ); - } else { - data.password = users.password; - } - - - const updatePayload = {}; - - if (data.firstName !== undefined) updatePayload.firstName = data.firstName; - - - if (data.lastName !== undefined) updatePayload.lastName = data.lastName; - - - if (data.phoneNumber !== undefined) updatePayload.phoneNumber = data.phoneNumber; - - - if (data.email !== undefined) updatePayload.email = data.email; - - - if (data.disabled !== undefined) updatePayload.disabled = data.disabled; - - - if (data.password !== undefined) updatePayload.password = data.password; - - - if (data.emailVerified !== undefined) updatePayload.emailVerified = data.emailVerified; - - else updatePayload.emailVerified = true; - - - if (data.emailVerificationToken !== undefined) updatePayload.emailVerificationToken = data.emailVerificationToken; - - - if (data.emailVerificationTokenExpiresAt !== undefined) updatePayload.emailVerificationTokenExpiresAt = data.emailVerificationTokenExpiresAt; - - - if (data.passwordResetToken !== undefined) updatePayload.passwordResetToken = data.passwordResetToken; - - - if (data.passwordResetTokenExpiresAt !== undefined) updatePayload.passwordResetTokenExpiresAt = data.passwordResetTokenExpiresAt; - - - if (data.provider !== undefined) updatePayload.provider = data.provider; - - - updatePayload.updatedById = currentUser.id; - - await users.update(updatePayload, {transaction}); - - - - if (data.app_role !== undefined) { - await users.setApp_role( - - data.app_role, - - { transaction } - ); - } - - - - - if (data.custom_permissions !== undefined) { - await users.setCustom_permissions(data.custom_permissions, { transaction }); - } - - - - await FileDBApi.replaceRelationFiles( - { - belongsTo: db.users.getTableName(), - belongsToColumn: 'avatar', - belongsToId: users.id, - }, - data.avatar, - options, - ); - - - return users; + if (!data?.app_role) { + data.app_role = users?.app_role?.id; + } + if (!data?.custom_permissions) { + data.custom_permissions = users?.custom_permissions?.map( + (item) => item.id, + ); } - static async deleteByIds(ids, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; - - const users = await db.users.findAll({ - where: { - id: { - [Op.in]: ids, - }, - }, - transaction, - }); - - await db.sequelize.transaction(async (transaction) => { - for (const record of users) { - await record.update( - {deletedBy: currentUser.id}, - {transaction} - ); - } - for (const record of users) { - await record.destroy({transaction}); - } - }); - - - return users; + if (data.password) { + data.password = bcrypt.hashSync(data.password, config.bcrypt.saltRounds); + } else { + data.password = users.password; } - static async remove(id, options) { - const currentUser = (options && options.currentUser) || {id: null}; - const transaction = (options && options.transaction) || undefined; + const updatePayload = {}; - const users = await db.users.findByPk(id, options); + if (data.firstName !== undefined) updatePayload.firstName = data.firstName; - await users.update({ - deletedBy: currentUser.id - }, { - transaction, - }); + if (data.lastName !== undefined) updatePayload.lastName = data.lastName; - await users.destroy({ - transaction - }); + if (data.phoneNumber !== undefined) + updatePayload.phoneNumber = data.phoneNumber; - return users; + if (data.email !== undefined) updatePayload.email = data.email; + + if (data.disabled !== undefined) updatePayload.disabled = data.disabled; + + if (data.password !== undefined) updatePayload.password = data.password; + + if (data.emailVerified !== undefined) + updatePayload.emailVerified = data.emailVerified; + else updatePayload.emailVerified = true; + + if (data.emailVerificationToken !== undefined) + updatePayload.emailVerificationToken = data.emailVerificationToken; + + if (data.emailVerificationTokenExpiresAt !== undefined) + updatePayload.emailVerificationTokenExpiresAt = + data.emailVerificationTokenExpiresAt; + + if (data.passwordResetToken !== undefined) + updatePayload.passwordResetToken = data.passwordResetToken; + + if (data.passwordResetTokenExpiresAt !== undefined) + updatePayload.passwordResetTokenExpiresAt = + data.passwordResetTokenExpiresAt; + + if (data.provider !== undefined) updatePayload.provider = data.provider; + + updatePayload.updatedById = currentUser.id; + + await users.update(updatePayload, { transaction }); + + if (data.app_role !== undefined) { + await users.setApp_role( + data.app_role, + + { transaction }, + ); } - static async findBy(where, options) { - const transaction = (options && options.transaction) || undefined; - - const users = await db.users.findOne({ - where, - transaction, - include: [ - { association: 'project_memberships_user' }, - { association: 'presigned_url_requests_user' }, - { association: 'publish_events_user' }, - { association: 'access_logs_user' }, - { association: 'avatar' }, - { - association: 'app_role', - include: [{ association: 'permissions' }], - }, - { association: 'custom_permissions' }, - ], - }); - - if (!users) { - return users; - } - - const output = users.get({plain: true}); - - // Map nested permissions from app_role for backward compatibility - if (output.app_role) { - output.app_role_permissions = output.app_role.permissions || []; - } - - return output; + if (data.custom_permissions !== undefined) { + await users.setCustom_permissions(data.custom_permissions, { + transaction, + }); } - static async findAll( - filter, - options - ) { - const limit = filter.limit || 0; - let offset = 0; - let where = {}; - const currentPage = +filter.page; + await FileDBApi.replaceRelationFiles( + { + belongsTo: db.users.getTableName(), + belongsToColumn: 'avatar', + belongsToId: users.id, + }, + data.avatar, + options, + ); - + return users; + } - + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; - offset = currentPage * limit; + const users = await db.users.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of users) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of users) { + await record.destroy({ transaction }); + } + }); + + return users; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const users = await db.users.findByPk(id, options); + + await users.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await users.destroy({ + transaction, + }); + + return users; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const users = await db.users.findOne({ + where, + transaction, + include: [ + { association: 'project_memberships_user' }, + { association: 'presigned_url_requests_user' }, + { association: 'publish_events_user' }, + { association: 'access_logs_user' }, + { association: 'avatar' }, + { + association: 'app_role', + include: [{ association: 'permissions' }], + }, + { association: 'custom_permissions' }, + ], + }); + + if (!users) { + return users; + } + + const output = users.get({ plain: true }); + + // Map nested permissions from app_role for backward compatibility + if (output.app_role) { + output.app_role_permissions = output.app_role.permissions || []; + } + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + offset = currentPage * limit; let include = [ - { model: db.roles, as: 'app_role', - - where: filter.app_role ? { - [Op.or]: [ - { id: { [Op.in]: filter.app_role.split('|').map(term => Utils.uuid(term)) } }, - { - name: { - [Op.or]: filter.app_role.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) - } - }, - ] - } : {}, - - }, + where: filter.app_role + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.app_role + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.app_role + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, { model: db.permissions, @@ -460,191 +331,152 @@ module.exports = class UsersDBApi { required: false, }, - { model: db.file, as: 'avatar', }, - ]; - if (filter) { - if (filter.id) { - where = { - ...where, - ['id']: Utils.uuid(filter.id), - }; - } + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } - - if (filter.firstName) { - where = { - ...where, - [Op.and]: Utils.ilike( - 'users', - 'firstName', - filter.firstName, - ), - }; - } - - if (filter.lastName) { - where = { - ...where, - [Op.and]: Utils.ilike( - 'users', - 'lastName', - filter.lastName, - ), - }; - } - - if (filter.phoneNumber) { - where = { - ...where, - [Op.and]: Utils.ilike( - 'users', - 'phoneNumber', - filter.phoneNumber, - ), - }; - } - - if (filter.email) { - where = { - ...where, - [Op.and]: Utils.ilike( - 'users', - 'email', - filter.email, - ), - }; - } - - if (filter.password) { - where = { - ...where, - [Op.and]: Utils.ilike( - 'users', - 'password', - filter.password, - ), - }; - } - - if (filter.emailVerificationToken) { - where = { - ...where, - [Op.and]: Utils.ilike( - 'users', - 'emailVerificationToken', - filter.emailVerificationToken, - ), - }; - } - - if (filter.passwordResetToken) { - where = { - ...where, - [Op.and]: Utils.ilike( - 'users', - 'passwordResetToken', - filter.passwordResetToken, - ), - }; - } - - if (filter.provider) { - where = { - ...where, - [Op.and]: Utils.ilike( - 'users', - 'provider', - filter.provider, - ), - }; - } - + if (filter.firstName) { + where = { + ...where, + [Op.and]: Utils.ilike('users', 'firstName', filter.firstName), + }; + } - - + if (filter.lastName) { + where = { + ...where, + [Op.and]: Utils.ilike('users', 'lastName', filter.lastName), + }; + } - - if (filter.emailVerificationTokenExpiresAtRange) { - const [start, end] = filter.emailVerificationTokenExpiresAtRange; + if (filter.phoneNumber) { + where = { + ...where, + [Op.and]: Utils.ilike('users', 'phoneNumber', filter.phoneNumber), + }; + } - if (start !== undefined && start !== null && start !== '') { - where = { - ...where, - emailVerificationTokenExpiresAt: { - ...where.emailVerificationTokenExpiresAt, - [Op.gte]: start, - }, - }; - } + if (filter.email) { + where = { + ...where, + [Op.and]: Utils.ilike('users', 'email', filter.email), + }; + } - if (end !== undefined && end !== null && end !== '') { - where = { - ...where, - emailVerificationTokenExpiresAt: { - ...where.emailVerificationTokenExpiresAt, - [Op.lte]: end, - }, - }; - } - } - - if (filter.passwordResetTokenExpiresAtRange) { - const [start, end] = filter.passwordResetTokenExpiresAtRange; + if (filter.password) { + where = { + ...where, + [Op.and]: Utils.ilike('users', 'password', filter.password), + }; + } - if (start !== undefined && start !== null && start !== '') { - where = { - ...where, - passwordResetTokenExpiresAt: { - ...where.passwordResetTokenExpiresAt, - [Op.gte]: start, - }, - }; - } + if (filter.emailVerificationToken) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'users', + 'emailVerificationToken', + filter.emailVerificationToken, + ), + }; + } - if (end !== undefined && end !== null && end !== '') { - where = { - ...where, - passwordResetTokenExpiresAt: { - ...where.passwordResetTokenExpiresAt, - [Op.lte]: end, - }, - }; - } - } - + if (filter.passwordResetToken) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'users', + 'passwordResetToken', + filter.passwordResetToken, + ), + }; + } - if (filter.active !== undefined) { - where = { - ...where, - active: filter.active === true || filter.active === 'true' - }; - } + if (filter.provider) { + where = { + ...where, + [Op.and]: Utils.ilike('users', 'provider', filter.provider), + }; + } - - if (filter.disabled) { - where = { - ...where, - disabled: filter.disabled, - }; - } - - if (filter.emailVerified) { - where = { - ...where, - emailVerified: filter.emailVerified, - }; - } - + if (filter.emailVerificationTokenExpiresAtRange) { + const [start, end] = filter.emailVerificationTokenExpiresAtRange; + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + emailVerificationTokenExpiresAt: { + ...where.emailVerificationTokenExpiresAt, + [Op.gte]: start, + }, + }; + } - + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + emailVerificationTokenExpiresAt: { + ...where.emailVerificationTokenExpiresAt, + [Op.lte]: end, + }, + }; + } + } + if (filter.passwordResetTokenExpiresAtRange) { + const [start, end] = filter.passwordResetTokenExpiresAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + passwordResetTokenExpiresAt: { + ...where.passwordResetTokenExpiresAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + passwordResetTokenExpiresAt: { + ...where.passwordResetTokenExpiresAt, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.disabled) { + where = { + ...where, + disabled: filter.disabled, + }; + } + + if (filter.emailVerified) { + where = { + ...where, + emailVerified: filter.emailVerified, + }; + } if (filter.custom_permissions) { const searchTerms = filter.custom_permissions.split('|'); @@ -654,252 +486,249 @@ module.exports = class UsersDBApi { model: db.permissions, as: 'custom_permissions_filter', required: searchTerms.length > 0, - where: searchTerms.length > 0 ? { - [Op.or]: [ - { id: { [Op.in]: searchTerms.map(term => Utils.uuid(term)) } }, - { - name: { - [Op.or]: searchTerms.map(term => ({ [Op.iLike]: `%${term}%` })) + where: + searchTerms.length > 0 + ? { + [Op.or]: [ + { + id: { + [Op.in]: searchTerms.map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: searchTerms.map((term) => ({ + [Op.iLike]: `%${term}%`, + })), + }, + }, + ], } - } - ] - } : undefined + : undefined, }, ...include, - ] + ]; } + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; - if (filter.createdAtRange) { - const [start, end] = filter.createdAtRange; - - if (start !== undefined && start !== null && start !== '') { - where = { - ...where, - ['createdAt']: { - ...where.createdAt, - [Op.gte]: start, - }, - }; - } - - if (end !== undefined && end !== null && end !== '') { - where = { - ...where, - ['createdAt']: { - ...where.createdAt, - [Op.lte]: end, - }, - }; - } - } - } - - - - - const queryOptions = { - where, - include, - distinct: true, - order: filter.field && filter.sort - ? [[filter.field, filter.sort]] - : [['createdAt', 'desc']], - transaction: options?.transaction - }; - - if (!options?.countOnly) { - queryOptions.limit = limit ? Number(limit) : undefined; - queryOptions.offset = offset ? Number(offset) : undefined; + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; } - try { - const { rows, count } = await db.users.findAndCountAll(queryOptions); - - return { - rows: options?.countOnly ? [] : rows, - count: count - }; - } catch (error) { - console.error('Error executing query:', error); - throw error; + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; } + } } - static async findAllAutocomplete(query, limit, offset, ) { - let where = {}; - - + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + }; - if (query) { - where = { - [Op.or]: [ - { ['id']: Utils.uuid(query) }, - Utils.ilike( - 'users', - 'firstName', - query, - ), - ], - }; - } - - const records = await db.users.findAll({ - attributes: [ 'id', 'firstName' ], - where, - limit: limit ? Number(limit) : undefined, - offset: offset ? Number(offset) : undefined, - orderBy: [['firstName', 'ASC']], - }); - - return records.map((record) => ({ - id: record.id, - label: record.firstName, - })); + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; } - - static async createFromAuth(data, options) { - const transaction = (options && options.transaction) || undefined; - const users = await db.users.create( - { - email: data.email, - firstName: data.firstName, - authenticationUid: data.authenticationUid, - password: data.password, - - }, - { transaction }, - ); + try { + const { rows, count } = await db.users.findAndCountAll(queryOptions); - const app_role = await db.roles.findOne({ - where: { name: config.roles?.user || "User" }, - }); - if (app_role?.id) { - await users.setApp_role(app_role?.id || null, { - transaction, - }); - } + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } - await users.update( - { - authenticationUid: users.id, - }, - { transaction }, - ); + static async findAllAutocomplete(query, limit, offset) { + let where = {}; - delete users.password; - return users; + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('users', 'firstName', query), + ], + }; } - static async updatePassword(id, password, options) { - const currentUser = (options && options.currentUser) || { id: null }; + const records = await db.users.findAll({ + attributes: ['id', 'firstName'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['firstName', 'ASC']], + }); - const transaction = (options && options.transaction) || undefined; + return records.map((record) => ({ + id: record.id, + label: record.firstName, + })); + } - const users = await db.users.findByPk(id, { - transaction, - }); + static async createFromAuth(data, options) { + const transaction = (options && options.transaction) || undefined; + const users = await db.users.create( + { + email: data.email, + firstName: data.firstName, + authenticationUid: data.authenticationUid, + password: data.password, + }, + { transaction }, + ); - await users.update( - { - password, - authenticationUid: id, - updatedById: currentUser.id, - }, - { transaction }, - ); - - return users; + const app_role = await db.roles.findOne({ + where: { name: config.roles?.user || 'User' }, + }); + if (app_role?.id) { + await users.setApp_role(app_role?.id || null, { + transaction, + }); } - static async generateEmailVerificationToken(email, options) { - return this._generateToken(['emailVerificationToken', 'emailVerificationTokenExpiresAt'], email, options); + await users.update( + { + authenticationUid: users.id, + }, + { transaction }, + ); + + delete users.password; + return users; + } + + static async updatePassword(id, password, options) { + const currentUser = (options && options.currentUser) || { id: null }; + + const transaction = (options && options.transaction) || undefined; + + const users = await db.users.findByPk(id, { + transaction, + }); + + await users.update( + { + password, + authenticationUid: id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return users; + } + + static async generateEmailVerificationToken(email, options) { + return this._generateToken( + ['emailVerificationToken', 'emailVerificationTokenExpiresAt'], + email, + options, + ); + } + + static async generatePasswordResetToken(email, options) { + return this._generateToken( + ['passwordResetToken', 'passwordResetTokenExpiresAt'], + email, + options, + ); + } + + static async findByPasswordResetToken(token, options) { + const transaction = (options && options.transaction) || undefined; + + return db.users.findOne({ + where: { + passwordResetToken: token, + passwordResetTokenExpiresAt: { + [db.Sequelize.Op.gt]: Date.now(), + }, + }, + transaction, + }); + } + + static async findByEmailVerificationToken(token, options) { + const transaction = (options && options.transaction) || undefined; + return db.users.findOne({ + where: { + emailVerificationToken: token, + emailVerificationTokenExpiresAt: { + [db.Sequelize.Op.gt]: Date.now(), + }, + }, + transaction, + }); + } + + static async markEmailVerified(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const users = await db.users.findByPk(id, { + transaction, + }); + + await users.update( + { + emailVerified: true, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return true; + } + + static async _generateToken(keyNames, email, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const users = await db.users.findOne({ + where: { email: email.toLowerCase() }, + transaction, + }); + + const token = crypto.randomBytes(20).toString('hex'); + // Token expires in 24 hours (was 6 minutes - too short for email verification flows) + const TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours + const tokenExpiresAt = Date.now() + TOKEN_EXPIRY_MS; + + if (users) { + await users.update( + { + [keyNames[0]]: token, + [keyNames[1]]: tokenExpiresAt, + updatedById: currentUser.id, + }, + { transaction }, + ); } - static async generatePasswordResetToken(email, options) { - return this._generateToken(['passwordResetToken', 'passwordResetTokenExpiresAt'], email, options); - } - - static async findByPasswordResetToken(token, options) { - const transaction = (options && options.transaction) || undefined; - - return db.users.findOne({ - where: { - passwordResetToken: token, - passwordResetTokenExpiresAt: { - [db.Sequelize.Op.gt]: Date.now(), - }, - }, - transaction, - }); - } - - static async findByEmailVerificationToken( - token, - options, - ) { - const transaction = (options && options.transaction) || undefined; - return db.users.findOne({ - where: { - emailVerificationToken: token, - emailVerificationTokenExpiresAt: { - [db.Sequelize.Op.gt]: Date.now(), - }, - }, - transaction, - }); - } - - static async markEmailVerified(id, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; - - const users = await db.users.findByPk(id, { - transaction, - }); - - await users.update( - { - emailVerified: true, - updatedById: currentUser.id, - }, - { transaction }, - ); - - return true; - } - - static async _generateToken(keyNames, email, options) { - const currentUser = (options && options.currentUser) || {id: null}; - const transaction = (options && options.transaction) || undefined; - const users = await db.users.findOne({ - where: { email: email.toLowerCase() }, - transaction, - }); - - const token = crypto - .randomBytes(20) - .toString('hex'); - // Token expires in 24 hours (was 6 minutes - too short for email verification flows) - const TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours - const tokenExpiresAt = Date.now() + TOKEN_EXPIRY_MS; - - if(users){ - await users.update( - { - [keyNames[0]]: token, - [keyNames[1]]: tokenExpiresAt, - updatedById: currentUser.id, - }, - {transaction}, - ); - } - - - return token; - } - - - + return token; + } }; diff --git a/backend/src/db/db.config.js b/backend/src/db/db.config.js index 82d2d3d..81e77e5 100644 --- a/backend/src/db/db.config.js +++ b/backend/src/db/db.config.js @@ -1,5 +1,3 @@ - - module.exports = { production: { dialect: 'postgres', @@ -35,5 +33,5 @@ module.exports = { seederStorage: 'sequelize', migrationStorage: 'sequelize', migrationStorageTableName: 'SequelizeMeta', - } + }, }; diff --git a/backend/src/db/migrations/20260319000001-add-foreign-key-constraints.js b/backend/src/db/migrations/20260319000001-add-foreign-key-constraints.js index 415eb19..c0aaaa0 100644 --- a/backend/src/db/migrations/20260319000001-add-foreign-key-constraints.js +++ b/backend/src/db/migrations/20260319000001-add-foreign-key-constraints.js @@ -11,14 +11,20 @@ module.exports = { try { // Helper to add FK constraint safely (checks if exists first) - const addForeignKey = async (tableName, columnName, references, onDelete = 'CASCADE', onUpdate = 'CASCADE') => { + const addForeignKey = async ( + tableName, + columnName, + references, + onDelete = 'CASCADE', + onUpdate = 'CASCADE', + ) => { const constraintName = `${tableName}_${columnName}_fkey`; // Check if constraint already exists const [results] = await queryInterface.sequelize.query( `SELECT constraint_name FROM information_schema.table_constraints WHERE table_name = '${tableName}' AND constraint_name = '${constraintName}'`, - { transaction } + { transaction }, ); if (results.length === 0) { @@ -41,61 +47,175 @@ module.exports = { }; // asset_variants -> assets - await addForeignKey('asset_variants', 'assetId', { table: 'assets', field: 'id' }, 'CASCADE', 'CASCADE'); + await addForeignKey( + 'asset_variants', + 'assetId', + { table: 'assets', field: 'id' }, + 'CASCADE', + 'CASCADE', + ); // page_elements -> tour_pages - await addForeignKey('page_elements', 'pageId', { table: 'tour_pages', field: 'id' }, 'CASCADE', 'CASCADE'); + await addForeignKey( + 'page_elements', + 'pageId', + { table: 'tour_pages', field: 'id' }, + 'CASCADE', + 'CASCADE', + ); // page_links -> tour_pages (from_page) - await addForeignKey('page_links', 'from_pageId', { table: 'tour_pages', field: 'id' }, 'CASCADE', 'CASCADE'); + await addForeignKey( + 'page_links', + 'from_pageId', + { table: 'tour_pages', field: 'id' }, + 'CASCADE', + 'CASCADE', + ); // page_links -> tour_pages (to_page) - await addForeignKey('page_links', 'to_pageId', { table: 'tour_pages', field: 'id' }, 'SET NULL', 'CASCADE'); + await addForeignKey( + 'page_links', + 'to_pageId', + { table: 'tour_pages', field: 'id' }, + 'SET NULL', + 'CASCADE', + ); // page_links -> transitions - await addForeignKey('page_links', 'transitionId', { table: 'transitions', field: 'id' }, 'SET NULL', 'CASCADE'); + await addForeignKey( + 'page_links', + 'transitionId', + { table: 'transitions', field: 'id' }, + 'SET NULL', + 'CASCADE', + ); // assets -> projects - await addForeignKey('assets', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE'); + await addForeignKey( + 'assets', + 'projectId', + { table: 'projects', field: 'id' }, + 'CASCADE', + 'CASCADE', + ); // tour_pages -> projects - await addForeignKey('tour_pages', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE'); + await addForeignKey( + 'tour_pages', + 'projectId', + { table: 'projects', field: 'id' }, + 'CASCADE', + 'CASCADE', + ); // transitions -> projects - await addForeignKey('transitions', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE'); + await addForeignKey( + 'transitions', + 'projectId', + { table: 'projects', field: 'id' }, + 'CASCADE', + 'CASCADE', + ); // project_memberships -> projects - await addForeignKey('project_memberships', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE'); + await addForeignKey( + 'project_memberships', + 'projectId', + { table: 'projects', field: 'id' }, + 'CASCADE', + 'CASCADE', + ); // project_memberships -> users - await addForeignKey('project_memberships', 'userId', { table: 'users', field: 'id' }, 'CASCADE', 'CASCADE'); + await addForeignKey( + 'project_memberships', + 'userId', + { table: 'users', field: 'id' }, + 'CASCADE', + 'CASCADE', + ); // presigned_url_requests -> projects - await addForeignKey('presigned_url_requests', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE'); + await addForeignKey( + 'presigned_url_requests', + 'projectId', + { table: 'projects', field: 'id' }, + 'CASCADE', + 'CASCADE', + ); // presigned_url_requests -> users - await addForeignKey('presigned_url_requests', 'userId', { table: 'users', field: 'id' }, 'CASCADE', 'CASCADE'); + await addForeignKey( + 'presigned_url_requests', + 'userId', + { table: 'users', field: 'id' }, + 'CASCADE', + 'CASCADE', + ); // project_audio_tracks -> projects - await addForeignKey('project_audio_tracks', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE'); + await addForeignKey( + 'project_audio_tracks', + 'projectId', + { table: 'projects', field: 'id' }, + 'CASCADE', + 'CASCADE', + ); // publish_events -> projects - await addForeignKey('publish_events', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE'); + await addForeignKey( + 'publish_events', + 'projectId', + { table: 'projects', field: 'id' }, + 'CASCADE', + 'CASCADE', + ); // publish_events -> users (SET NULL to preserve audit trail) - await addForeignKey('publish_events', 'userId', { table: 'users', field: 'id' }, 'SET NULL', 'CASCADE'); + await addForeignKey( + 'publish_events', + 'userId', + { table: 'users', field: 'id' }, + 'SET NULL', + 'CASCADE', + ); // pwa_caches -> projects - await addForeignKey('pwa_caches', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE'); + await addForeignKey( + 'pwa_caches', + 'projectId', + { table: 'projects', field: 'id' }, + 'CASCADE', + 'CASCADE', + ); // access_logs -> projects - await addForeignKey('access_logs', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE'); + await addForeignKey( + 'access_logs', + 'projectId', + { table: 'projects', field: 'id' }, + 'CASCADE', + 'CASCADE', + ); // access_logs -> users (SET NULL to preserve audit trail) - await addForeignKey('access_logs', 'userId', { table: 'users', field: 'id' }, 'SET NULL', 'CASCADE'); + await addForeignKey( + 'access_logs', + 'userId', + { table: 'users', field: 'id' }, + 'SET NULL', + 'CASCADE', + ); // users -> roles (SET NULL so deleting role doesn't delete users) - await addForeignKey('users', 'app_roleId', { table: 'roles', field: 'id' }, 'SET NULL', 'CASCADE'); + await addForeignKey( + 'users', + 'app_roleId', + { table: 'roles', field: 'id' }, + 'SET NULL', + 'CASCADE', + ); await transaction.commit(); console.log('All FK constraints added successfully'); @@ -112,10 +232,14 @@ module.exports = { const dropForeignKey = async (tableName, columnName) => { const constraintName = `${tableName}_${columnName}_fkey`; try { - await queryInterface.removeConstraint(tableName, constraintName, { transaction }); + await queryInterface.removeConstraint(tableName, constraintName, { + transaction, + }); console.log(`Removed FK constraint: ${constraintName}`); } catch (error) { - console.log(`FK constraint not found (may not exist): ${constraintName}`); + console.log( + `FK constraint not found (may not exist): ${constraintName}`, + ); } }; @@ -146,5 +270,5 @@ module.exports = { await transaction.rollback(); throw error; } - } + }, }; diff --git a/backend/src/db/migrations/20260319000002-remove-redundant-deletion-columns.js b/backend/src/db/migrations/20260319000002-remove-redundant-deletion-columns.js index 0152ba5..c280255 100644 --- a/backend/src/db/migrations/20260319000002-remove-redundant-deletion-columns.js +++ b/backend/src/db/migrations/20260319000002-remove-redundant-deletion-columns.js @@ -21,20 +21,26 @@ module.exports = { const [results] = await queryInterface.sequelize.query( `SELECT column_name FROM information_schema.columns WHERE table_name = '${tableName}' AND column_name = '${columnName}'`, - { transaction } + { transaction }, ); if (results.length > 0) { - await queryInterface.removeColumn(tableName, columnName, { transaction }); + await queryInterface.removeColumn(tableName, columnName, { + transaction, + }); console.log(`Removed column: ${tableName}.${columnName}`); } else { - console.log(`Column does not exist (skipping): ${tableName}.${columnName}`); + console.log( + `Column does not exist (skipping): ${tableName}.${columnName}`, + ); } }; // Remove is_deleted index from assets first (if exists) try { - await queryInterface.removeIndex('assets', 'assets_is_deleted', { transaction }); + await queryInterface.removeIndex('assets', 'assets_is_deleted', { + transaction, + }); console.log('Removed index: assets_is_deleted'); } catch (error) { console.log('Index assets_is_deleted not found (may not exist)'); @@ -61,16 +67,26 @@ module.exports = { try { // Re-add columns to assets table - await queryInterface.addColumn('assets', 'is_deleted', { - type: Sequelize.BOOLEAN, - allowNull: false, - defaultValue: false, - }, { transaction }); + await queryInterface.addColumn( + 'assets', + 'is_deleted', + { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + { transaction }, + ); - await queryInterface.addColumn('assets', 'deleted_at_time', { - type: Sequelize.DATE, - allowNull: true, - }, { transaction }); + await queryInterface.addColumn( + 'assets', + 'deleted_at_time', + { + type: Sequelize.DATE, + allowNull: true, + }, + { transaction }, + ); // Re-add index await queryInterface.addIndex('assets', ['is_deleted'], { @@ -79,16 +95,26 @@ module.exports = { }); // Re-add columns to projects table - await queryInterface.addColumn('projects', 'is_deleted', { - type: Sequelize.BOOLEAN, - allowNull: false, - defaultValue: false, - }, { transaction }); + await queryInterface.addColumn( + 'projects', + 'is_deleted', + { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + { transaction }, + ); - await queryInterface.addColumn('projects', 'deleted_at_time', { - type: Sequelize.DATE, - allowNull: true, - }, { transaction }); + await queryInterface.addColumn( + 'projects', + 'deleted_at_time', + { + type: Sequelize.DATE, + allowNull: true, + }, + { transaction }, + ); await transaction.commit(); console.log('Redundant deletion columns restored successfully'); @@ -96,5 +122,5 @@ module.exports = { await transaction.rollback(); throw error; } - } + }, }; diff --git a/backend/src/db/migrations/20260326000001-rename-ui-elements-to-element-type-defaults.js b/backend/src/db/migrations/20260326000001-rename-ui-elements-to-element-type-defaults.js index 2116828..5795d9c 100644 --- a/backend/src/db/migrations/20260326000001-rename-ui-elements-to-element-type-defaults.js +++ b/backend/src/db/migrations/20260326000001-rename-ui-elements-to-element-type-defaults.js @@ -17,7 +17,7 @@ module.exports = { WHERE table_schema = 'public' AND table_name = 'ui_elements' );`, - { type: Sequelize.QueryTypes.SELECT } + { type: Sequelize.QueryTypes.SELECT }, ); if (!tableExists[0]?.exists) { @@ -32,11 +32,13 @@ module.exports = { WHERE table_schema = 'public' AND table_name = 'element_type_defaults' );`, - { type: Sequelize.QueryTypes.SELECT } + { type: Sequelize.QueryTypes.SELECT }, ); if (newTableExists[0]?.exists) { - console.log('Table element_type_defaults already exists, skipping rename'); + console.log( + 'Table element_type_defaults already exists, skipping rename', + ); return; } @@ -57,17 +59,21 @@ module.exports = { WHERE table_schema = 'public' AND table_name = 'element_type_defaults' );`, - { type: Sequelize.QueryTypes.SELECT } + { type: Sequelize.QueryTypes.SELECT }, ); if (!tableExists[0]?.exists) { - console.log('Table element_type_defaults does not exist, skipping rollback'); + console.log( + 'Table element_type_defaults does not exist, skipping rollback', + ); return; } // Rename table back await queryInterface.renameTable('element_type_defaults', 'ui_elements'); - console.log('Successfully rolled back: renamed element_type_defaults to ui_elements'); + console.log( + 'Successfully rolled back: renamed element_type_defaults to ui_elements', + ); }, }; diff --git a/backend/src/db/migrations/20260326000002-convert-element-type-enum-to-text.js b/backend/src/db/migrations/20260326000002-convert-element-type-enum-to-text.js index 5773b3a..9bd81db 100644 --- a/backend/src/db/migrations/20260326000002-convert-element-type-enum-to-text.js +++ b/backend/src/db/migrations/20260326000002-convert-element-type-enum-to-text.js @@ -26,13 +26,13 @@ module.exports = { type: Sequelize.TEXT, allowNull: true, }, - { transaction } + { transaction }, ); // Step 2: Copy ENUM values to TEXT column await queryInterface.sequelize.query( `UPDATE page_elements SET element_type_text = element_type::TEXT`, - { transaction } + { transaction }, ); // Step 3: Drop the old ENUM column @@ -45,7 +45,7 @@ module.exports = { 'page_elements', 'element_type_text', 'element_type', - { transaction } + { transaction }, ); // Step 5: Add NOT NULL constraint @@ -56,7 +56,7 @@ module.exports = { type: Sequelize.TEXT, allowNull: false, }, - { transaction } + { transaction }, ); // Step 6: Now map nav_button to specific navigation types (column is TEXT now) @@ -70,7 +70,7 @@ module.exports = { OR content_json::jsonb->>'navType' = 'forward' OR content_json::jsonb->>'navType' IS NULL )`, - { transaction } + { transaction }, ); // Back navigation @@ -80,17 +80,19 @@ module.exports = { WHERE element_type = 'nav_button' AND content_json IS NOT NULL AND content_json::jsonb->>'navType' = 'back'`, - { transaction } + { transaction }, ); // Step 7: Drop the old ENUM type if it exists await queryInterface.sequelize.query( `DROP TYPE IF EXISTS "enum_page_elements_element_type"`, - { transaction } + { transaction }, ); await transaction.commit(); - console.log('Successfully converted element_type from ENUM to TEXT and mapped nav_button types'); + console.log( + 'Successfully converted element_type from ENUM to TEXT and mapped nav_button types', + ); } catch (error) { await transaction.rollback(); throw error; @@ -106,17 +108,17 @@ module.exports = { `UPDATE page_elements SET element_type = 'nav_button' WHERE element_type IN ('navigation_next', 'navigation_prev')`, - { transaction } + { transaction }, ); // Step 2: Drop any existing ENUM types that might conflict await queryInterface.sequelize.query( `DROP TYPE IF EXISTS "enum_page_elements_element_type" CASCADE`, - { transaction } + { transaction }, ); await queryInterface.sequelize.query( `DROP TYPE IF EXISTS "enum_page_elements_element_type_enum" CASCADE`, - { transaction } + { transaction }, ); // Step 3: Create the ENUM type with original values @@ -132,19 +134,19 @@ module.exports = { 'video_player', 'popup' )`, - { transaction } + { transaction }, ); // Step 4: Add ENUM column directly via raw SQL to avoid Sequelize creating another type await queryInterface.sequelize.query( `ALTER TABLE page_elements ADD COLUMN element_type_enum "enum_page_elements_element_type"`, - { transaction } + { transaction }, ); // Step 5: Copy TEXT values to ENUM column await queryInterface.sequelize.query( `UPDATE page_elements SET element_type_enum = element_type::"enum_page_elements_element_type"`, - { transaction } + { transaction }, ); // Step 6: Drop TEXT column @@ -157,13 +159,13 @@ module.exports = { 'page_elements', 'element_type_enum', 'element_type', - { transaction } + { transaction }, ); // Step 8: Add NOT NULL constraint await queryInterface.sequelize.query( `ALTER TABLE page_elements ALTER COLUMN element_type SET NOT NULL`, - { transaction } + { transaction }, ); await transaction.commit(); diff --git a/backend/src/db/migrations/20260326000003-create-project-element-defaults.js b/backend/src/db/migrations/20260326000003-create-project-element-defaults.js index e0f8139..cf82871 100644 --- a/backend/src/db/migrations/20260326000003-create-project-element-defaults.js +++ b/backend/src/db/migrations/20260326000003-create-project-element-defaults.js @@ -21,11 +21,13 @@ module.exports = { WHERE table_schema = 'public' AND table_name = 'project_element_defaults' );`, - { type: Sequelize.QueryTypes.SELECT } + { type: Sequelize.QueryTypes.SELECT }, ); if (tableExists[0]?.exists) { - console.log('Table project_element_defaults already exists, skipping creation'); + console.log( + 'Table project_element_defaults already exists, skipping creation', + ); return; } @@ -121,19 +123,31 @@ module.exports = { name: 'project_element_defaults_projectId', }); - await queryInterface.addIndex('project_element_defaults', ['projectId', 'element_type'], { - name: 'project_element_defaults_projectId_element_type', - unique: true, - where: { deletedAt: null }, - }); + await queryInterface.addIndex( + 'project_element_defaults', + ['projectId', 'element_type'], + { + name: 'project_element_defaults_projectId_element_type', + unique: true, + where: { deletedAt: null }, + }, + ); - await queryInterface.addIndex('project_element_defaults', ['element_type'], { - name: 'project_element_defaults_element_type', - }); + await queryInterface.addIndex( + 'project_element_defaults', + ['element_type'], + { + name: 'project_element_defaults_element_type', + }, + ); - await queryInterface.addIndex('project_element_defaults', ['source_element_id'], { - name: 'project_element_defaults_source_element_id', - }); + await queryInterface.addIndex( + 'project_element_defaults', + ['source_element_id'], + { + name: 'project_element_defaults_source_element_id', + }, + ); await queryInterface.addIndex('project_element_defaults', ['deletedAt'], { name: 'project_element_defaults_deletedAt', @@ -144,11 +158,26 @@ module.exports = { async down(queryInterface, _Sequelize) { // Drop indexes first - await queryInterface.removeIndex('project_element_defaults', 'project_element_defaults_projectId'); - await queryInterface.removeIndex('project_element_defaults', 'project_element_defaults_projectId_element_type'); - await queryInterface.removeIndex('project_element_defaults', 'project_element_defaults_element_type'); - await queryInterface.removeIndex('project_element_defaults', 'project_element_defaults_source_element_id'); - await queryInterface.removeIndex('project_element_defaults', 'project_element_defaults_deletedAt'); + await queryInterface.removeIndex( + 'project_element_defaults', + 'project_element_defaults_projectId', + ); + await queryInterface.removeIndex( + 'project_element_defaults', + 'project_element_defaults_projectId_element_type', + ); + await queryInterface.removeIndex( + 'project_element_defaults', + 'project_element_defaults_element_type', + ); + await queryInterface.removeIndex( + 'project_element_defaults', + 'project_element_defaults_source_element_id', + ); + await queryInterface.removeIndex( + 'project_element_defaults', + 'project_element_defaults_deletedAt', + ); // Drop table await queryInterface.dropTable('project_element_defaults'); diff --git a/backend/src/db/migrations/20260326000004-backfill-project-element-defaults.js b/backend/src/db/migrations/20260326000004-backfill-project-element-defaults.js index 5c02c7a..016647b 100644 --- a/backend/src/db/migrations/20260326000004-backfill-project-element-defaults.js +++ b/backend/src/db/migrations/20260326000004-backfill-project-element-defaults.js @@ -130,7 +130,7 @@ module.exports = { // This is needed because the API's lazy initialization won't have run yet during migration const [existingTypes] = await queryInterface.sequelize.query( `SELECT element_type FROM element_type_defaults WHERE "deletedAt" IS NULL`, - { type: Sequelize.QueryTypes.SELECT } + { type: Sequelize.QueryTypes.SELECT }, ); const existingTypeSet = new Set( @@ -138,7 +138,7 @@ module.exports = { ? existingTypes.map((t) => t.element_type) : existingTypes ? [existingTypes.element_type] - : [] + : [], ); // Insert missing element types @@ -154,16 +154,18 @@ module.exports = { sort_order: defaultType.sort_order, settings_json: defaultType.settings_json, }, - } + }, + ); + console.log( + `Created missing element_type_default: ${defaultType.element_type}`, ); - console.log(`Created missing element_type_default: ${defaultType.element_type}`); } } // Get all existing projects const [projects] = await queryInterface.sequelize.query( `SELECT id FROM projects WHERE "deletedAt" IS NULL`, - { type: Sequelize.QueryTypes.SELECT } + { type: Sequelize.QueryTypes.SELECT }, ); if (!projects || projects.length === 0) { @@ -176,7 +178,7 @@ module.exports = { `SELECT id, element_type, name, sort_order, settings_json FROM element_type_defaults WHERE "deletedAt" IS NULL`, - { type: Sequelize.QueryTypes.SELECT } + { type: Sequelize.QueryTypes.SELECT }, ); if (!globalDefaults || globalDefaults.length === 0) { @@ -184,8 +186,12 @@ module.exports = { return; } - const projectIds = Array.isArray(projects) ? projects.map((p) => p.id) : [projects.id]; - const globalDefaultRows = Array.isArray(globalDefaults) ? globalDefaults : [globalDefaults]; + const projectIds = Array.isArray(projects) + ? projects.map((p) => p.id) + : [projects.id]; + const globalDefaultRows = Array.isArray(globalDefaults) + ? globalDefaults + : [globalDefaults]; // For each project, add any missing element type defaults for (const projectId of projectIds) { @@ -196,7 +202,7 @@ module.exports = { { replacements: { projectId }, type: Sequelize.QueryTypes.SELECT, - } + }, ); const existingProjectTypes = new Set( @@ -204,7 +210,7 @@ module.exports = { ? existingDefaults.map((d) => d.element_type) : existingDefaults ? [existingDefaults.element_type] - : [] + : [], ); // Create project element defaults for missing types @@ -239,26 +245,30 @@ module.exports = { projectId, }, type: Sequelize.QueryTypes.INSERT, - } + }, ); addedCount++; } if (addedCount > 0) { - console.log(`Backfilled ${addedCount} element defaults for project ${projectId}`); + console.log( + `Backfilled ${addedCount} element defaults for project ${projectId}`, + ); } else { console.log(`Project ${projectId} already has all element defaults`); } } - console.log('Successfully backfilled project_element_defaults for existing projects'); + console.log( + 'Successfully backfilled project_element_defaults for existing projects', + ); }, async down(queryInterface, _Sequelize) { // Delete all project_element_defaults with snapshot_version = 1 // (only the ones we created during backfill) await queryInterface.sequelize.query( - `DELETE FROM project_element_defaults WHERE snapshot_version = 1` + `DELETE FROM project_element_defaults WHERE snapshot_version = 1`, ); console.log('Successfully removed backfilled project_element_defaults'); diff --git a/backend/src/db/migrations/20260326000005-fix-project-audio-tracks-environment.js b/backend/src/db/migrations/20260326000005-fix-project-audio-tracks-environment.js index 8adf98e..12f957d 100644 --- a/backend/src/db/migrations/20260326000005-fix-project-audio-tracks-environment.js +++ b/backend/src/db/migrations/20260326000005-fix-project-audio-tracks-environment.js @@ -12,17 +12,19 @@ module.exports = { const [columns] = await queryInterface.sequelize.query( `SELECT column_name FROM information_schema.columns WHERE table_name = 'project_audio_tracks' AND column_name = 'environment'`, - { type: Sequelize.QueryTypes.SELECT } + { type: Sequelize.QueryTypes.SELECT }, ); if (!columns) { - console.log('Column project_audio_tracks.environment does not exist, skipping'); + console.log( + 'Column project_audio_tracks.environment does not exist, skipping', + ); return; } // Set NULL values to 'dev' await queryInterface.sequelize.query( - `UPDATE project_audio_tracks SET environment = 'dev' WHERE environment IS NULL` + `UPDATE project_audio_tracks SET environment = 'dev' WHERE environment IS NULL`, ); // Alter column to NOT NULL with default @@ -32,7 +34,9 @@ module.exports = { defaultValue: 'dev', }); - console.log('Successfully fixed project_audio_tracks.environment to NOT NULL with default dev'); + console.log( + 'Successfully fixed project_audio_tracks.environment to NOT NULL with default dev', + ); }, async down(queryInterface, Sequelize) { diff --git a/backend/src/db/migrations/20260326000006-copy-dev-to-stage.js b/backend/src/db/migrations/20260326000006-copy-dev-to-stage.js index c3b64fa..15ca361 100644 --- a/backend/src/db/migrations/20260326000006-copy-dev-to-stage.js +++ b/backend/src/db/migrations/20260326000006-copy-dev-to-stage.js @@ -12,7 +12,7 @@ module.exports = { // Get all projects const projects = await queryInterface.sequelize.query( `SELECT id FROM projects WHERE "deletedAt" IS NULL`, - { type: Sequelize.QueryTypes.SELECT } + { type: Sequelize.QueryTypes.SELECT }, ); if (!projects || projects.length === 0) { @@ -27,7 +27,7 @@ module.exports = { const [stageCheck] = await queryInterface.sequelize.query( `SELECT COUNT(*)::int as count FROM tour_pages WHERE "projectId" = '${projectId}' AND environment = 'stage' AND "deletedAt" IS NULL`, - { type: Sequelize.QueryTypes.SELECT } + { type: Sequelize.QueryTypes.SELECT }, ); if (stageCheck?.count > 0) { @@ -39,7 +39,7 @@ module.exports = { const [devPageCount] = await queryInterface.sequelize.query( `SELECT COUNT(*)::int as count FROM tour_pages WHERE "projectId" = '${projectId}' AND environment = 'dev' AND "deletedAt" IS NULL`, - { type: Sequelize.QueryTypes.SELECT } + { type: Sequelize.QueryTypes.SELECT }, ); if (!devPageCount || devPageCount.count === 0) { @@ -188,19 +188,19 @@ module.exports = { async down(queryInterface, _Sequelize) { // Delete all stage content that has a source_key (meaning it was created by this migration) await queryInterface.sequelize.query( - `DELETE FROM page_links WHERE "from_pageId" IN (SELECT id FROM tour_pages WHERE environment = 'stage' AND source_key IS NOT NULL)` + `DELETE FROM page_links WHERE "from_pageId" IN (SELECT id FROM tour_pages WHERE environment = 'stage' AND source_key IS NOT NULL)`, ); await queryInterface.sequelize.query( - `DELETE FROM page_elements WHERE "pageId" IN (SELECT id FROM tour_pages WHERE environment = 'stage' AND source_key IS NOT NULL)` + `DELETE FROM page_elements WHERE "pageId" IN (SELECT id FROM tour_pages WHERE environment = 'stage' AND source_key IS NOT NULL)`, ); await queryInterface.sequelize.query( - `DELETE FROM tour_pages WHERE environment = 'stage' AND source_key IS NOT NULL` + `DELETE FROM tour_pages WHERE environment = 'stage' AND source_key IS NOT NULL`, ); await queryInterface.sequelize.query( - `DELETE FROM transitions WHERE environment = 'stage' AND source_key IS NOT NULL` + `DELETE FROM transitions WHERE environment = 'stage' AND source_key IS NOT NULL`, ); await queryInterface.sequelize.query( - `DELETE FROM project_audio_tracks WHERE environment = 'stage' AND source_key IS NOT NULL` + `DELETE FROM project_audio_tracks WHERE environment = 'stage' AND source_key IS NOT NULL`, ); console.log('Removed stage content created by migration'); diff --git a/backend/src/db/migrations/20260326043002-enforce-environment-not-null.js b/backend/src/db/migrations/20260326043002-enforce-environment-not-null.js index 3bc0774..7539458 100644 --- a/backend/src/db/migrations/20260326043002-enforce-environment-not-null.js +++ b/backend/src/db/migrations/20260326043002-enforce-environment-not-null.js @@ -13,12 +13,12 @@ module.exports = { async up(queryInterface, _Sequelize) { // Fix any NULL environments in tour_pages await queryInterface.sequelize.query( - `UPDATE tour_pages SET environment = 'dev' WHERE environment IS NULL` + `UPDATE tour_pages SET environment = 'dev' WHERE environment IS NULL`, ); // Fix any NULL environments in transitions await queryInterface.sequelize.query( - `UPDATE transitions SET environment = 'dev' WHERE environment IS NULL` + `UPDATE transitions SET environment = 'dev' WHERE environment IS NULL`, ); // Add NOT NULL constraint with default to tour_pages.environment diff --git a/backend/src/db/migrations/20260326050442-remove-project-phase-column.js b/backend/src/db/migrations/20260326050442-remove-project-phase-column.js index 2a02325..2e06b54 100644 --- a/backend/src/db/migrations/20260326050442-remove-project-phase-column.js +++ b/backend/src/db/migrations/20260326050442-remove-project-phase-column.js @@ -11,7 +11,7 @@ module.exports = { // Drop the ENUM type await queryInterface.sequelize.query( - `DROP TYPE IF EXISTS "enum_projects_phase";` + `DROP TYPE IF EXISTS "enum_projects_phase";`, ); }, diff --git a/backend/src/db/migrations/20260326060000-convert-targetpageid-to-slug.js b/backend/src/db/migrations/20260326060000-convert-targetpageid-to-slug.js index 0c825d4..034133d 100644 --- a/backend/src/db/migrations/20260326060000-convert-targetpageid-to-slug.js +++ b/backend/src/db/migrations/20260326060000-convert-targetpageid-to-slug.js @@ -18,25 +18,26 @@ module.exports = { // Get all tour pages with their ui_schema_json const [tourPages] = await queryInterface.sequelize.query( `SELECT id, "projectId", environment, slug, ui_schema_json FROM tour_pages WHERE ui_schema_json IS NOT NULL`, - { transaction } + { transaction }, ); // Build a lookup map: pageId -> { projectId, environment, slug } const pageInfoById = new Map(); - tourPages.forEach(page => { + tourPages.forEach((page) => { pageInfoById.set(page.id, { projectId: page.projectId, environment: page.environment, - slug: page.slug + slug: page.slug, }); }); // Process each page and convert targetPageId to targetPageSlug for (const page of tourPages) { try { - const uiSchema = typeof page.ui_schema_json === 'string' - ? JSON.parse(page.ui_schema_json) - : page.ui_schema_json; + const uiSchema = + typeof page.ui_schema_json === 'string' + ? JSON.parse(page.ui_schema_json) + : page.ui_schema_json; if (!uiSchema || !Array.isArray(uiSchema.elements)) { continue; @@ -44,14 +45,19 @@ module.exports = { let hasChanges = false; - uiSchema.elements.forEach(element => { + uiSchema.elements.forEach((element) => { // Convert targetPageId to targetPageSlug - if (element.targetPageId && typeof element.targetPageId === 'string') { + if ( + element.targetPageId && + typeof element.targetPageId === 'string' + ) { const targetPageInfo = pageInfoById.get(element.targetPageId); if (targetPageInfo && targetPageInfo.slug) { // Only convert if target page is in the same project and environment - if (targetPageInfo.projectId === page.projectId && - targetPageInfo.environment === page.environment) { + if ( + targetPageInfo.projectId === page.projectId && + targetPageInfo.environment === page.environment + ) { element.targetPageSlug = targetPageInfo.slug; delete element.targetPageId; hasChanges = true; @@ -66,11 +72,11 @@ module.exports = { { replacements: { json: JSON.stringify(uiSchema), - id: page.id + id: page.id, }, type: Sequelize.QueryTypes.UPDATE, - transaction - } + transaction, + }, ); } } catch (parseError) { @@ -80,7 +86,9 @@ module.exports = { } await transaction.commit(); - console.log('Migration complete: Converted targetPageId to targetPageSlug'); + console.log( + 'Migration complete: Converted targetPageId to targetPageSlug', + ); } catch (error) { await transaction.rollback(); throw error; @@ -94,12 +102,12 @@ module.exports = { // Get all tour pages const [tourPages] = await queryInterface.sequelize.query( `SELECT id, "projectId", environment, slug, ui_schema_json FROM tour_pages WHERE ui_schema_json IS NOT NULL`, - { transaction } + { transaction }, ); // Build lookup: (projectId, environment, slug) -> pageId const pageIdByKey = new Map(); - tourPages.forEach(page => { + tourPages.forEach((page) => { const key = `${page.projectId}:${page.environment}:${page.slug}`; pageIdByKey.set(key, page.id); }); @@ -107,9 +115,10 @@ module.exports = { // Process each page and convert targetPageSlug back to targetPageId for (const page of tourPages) { try { - const uiSchema = typeof page.ui_schema_json === 'string' - ? JSON.parse(page.ui_schema_json) - : page.ui_schema_json; + const uiSchema = + typeof page.ui_schema_json === 'string' + ? JSON.parse(page.ui_schema_json) + : page.ui_schema_json; if (!uiSchema || !Array.isArray(uiSchema.elements)) { continue; @@ -117,8 +126,11 @@ module.exports = { let hasChanges = false; - uiSchema.elements.forEach(element => { - if (element.targetPageSlug && typeof element.targetPageSlug === 'string') { + uiSchema.elements.forEach((element) => { + if ( + element.targetPageSlug && + typeof element.targetPageSlug === 'string' + ) { const key = `${page.projectId}:${page.environment}:${element.targetPageSlug}`; const targetPageId = pageIdByKey.get(key); if (targetPageId) { @@ -135,11 +147,11 @@ module.exports = { { replacements: { json: JSON.stringify(uiSchema), - id: page.id + id: page.id, }, type: Sequelize.QueryTypes.UPDATE, - transaction - } + transaction, + }, ); } } catch (parseError) { @@ -148,10 +160,12 @@ module.exports = { } await transaction.commit(); - console.log('Rollback complete: Converted targetPageSlug back to targetPageId'); + console.log( + 'Rollback complete: Converted targetPageSlug back to targetPageId', + ); } catch (error) { await transaction.rollback(); throw error; } - } + }, }; diff --git a/backend/src/db/migrations/20260326060001-drop-page-elements-table.js b/backend/src/db/migrations/20260326060001-drop-page-elements-table.js index e7c6037..4287521 100644 --- a/backend/src/db/migrations/20260326060001-drop-page-elements-table.js +++ b/backend/src/db/migrations/20260326060001-drop-page-elements-table.js @@ -10,12 +10,14 @@ module.exports = { async up(queryInterface, _Sequelize) { // Verify the table is empty before dropping const [results] = await queryInterface.sequelize.query( - 'SELECT COUNT(*) as count FROM page_elements' + 'SELECT COUNT(*) as count FROM page_elements', ); const count = parseInt(results[0].count, 10); if (count > 0) { - throw new Error(`Cannot drop page_elements table: it contains ${count} records. Please migrate or delete them first.`); + throw new Error( + `Cannot drop page_elements table: it contains ${count} records. Please migrate or delete them first.`, + ); } await queryInterface.dropTable('page_elements'); @@ -94,5 +96,5 @@ module.exports = { unique: true, }, }); - } + }, }; diff --git a/backend/src/db/migrations/20260326060002-drop-page-links-table.js b/backend/src/db/migrations/20260326060002-drop-page-links-table.js index 59418c4..48e7d3c 100644 --- a/backend/src/db/migrations/20260326060002-drop-page-links-table.js +++ b/backend/src/db/migrations/20260326060002-drop-page-links-table.js @@ -10,12 +10,14 @@ module.exports = { async up(queryInterface, _Sequelize) { // Verify the table is empty before dropping const [results] = await queryInterface.sequelize.query( - 'SELECT COUNT(*) as count FROM page_links' + 'SELECT COUNT(*) as count FROM page_links', ); const count = parseInt(results[0].count, 10); if (count > 0) { - throw new Error(`Cannot drop page_links table: it contains ${count} records. Please migrate or delete them first.`); + throw new Error( + `Cannot drop page_links table: it contains ${count} records. Please migrate or delete them first.`, + ); } await queryInterface.dropTable('page_links'); @@ -102,5 +104,5 @@ module.exports = { unique: true, }, }); - } + }, }; diff --git a/backend/src/db/migrations/20260326060003-drop-transitions-table.js b/backend/src/db/migrations/20260326060003-drop-transitions-table.js index ea2c1bf..1c58860 100644 --- a/backend/src/db/migrations/20260326060003-drop-transitions-table.js +++ b/backend/src/db/migrations/20260326060003-drop-transitions-table.js @@ -10,12 +10,14 @@ module.exports = { async up(queryInterface, _Sequelize) { // Verify the table is empty before dropping const [results] = await queryInterface.sequelize.query( - 'SELECT COUNT(*) as count FROM transitions' + 'SELECT COUNT(*) as count FROM transitions', ); const count = parseInt(results[0].count, 10); if (count > 0) { - throw new Error(`Cannot drop transitions table: it contains ${count} records. Please migrate or delete them first.`); + throw new Error( + `Cannot drop transitions table: it contains ${count} records. Please migrate or delete them first.`, + ); } await queryInterface.dropTable('transitions'); @@ -99,5 +101,5 @@ module.exports = { unique: true, }, }); - } + }, }; diff --git a/backend/src/db/migrations/20260326171017-add-missing-element-type-defaults.js b/backend/src/db/migrations/20260326171017-add-missing-element-type-defaults.js index be130d1..3598959 100644 --- a/backend/src/db/migrations/20260326171017-add-missing-element-type-defaults.js +++ b/backend/src/db/migrations/20260326171017-add-missing-element-type-defaults.js @@ -67,14 +67,16 @@ module.exports = { { replacements: { element_type: elementType.element_type }, type: Sequelize.QueryTypes.SELECT, - } + }, ); if (!existing) { await queryInterface.bulkInsert('element_type_defaults', [elementType]); console.log(`Added global default for: ${elementType.element_type}`); } else { - console.log(`Global default already exists for: ${elementType.element_type}`); + console.log( + `Global default already exists for: ${elementType.element_type}`, + ); } } @@ -83,16 +85,18 @@ module.exports = { `SELECT id, element_type, name, sort_order, settings_json FROM element_type_defaults WHERE element_type IN ('spot', 'logo', 'popup') AND "deletedAt" IS NULL`, - { type: Sequelize.QueryTypes.SELECT } + { type: Sequelize.QueryTypes.SELECT }, ); // Get all projects const projects = await queryInterface.sequelize.query( `SELECT id FROM projects WHERE "deletedAt" IS NULL`, - { type: Sequelize.QueryTypes.SELECT } + { type: Sequelize.QueryTypes.SELECT }, ); - console.log(`Backfilling ${globalDefaults.length} element types to ${projects.length} projects...`); + console.log( + `Backfilling ${globalDefaults.length} element types to ${projects.length} projects...`, + ); // Backfill project_element_defaults for each project for (const project of projects) { @@ -102,24 +106,29 @@ module.exports = { `SELECT id FROM project_element_defaults WHERE "projectId" = :projectId AND element_type = :element_type AND "deletedAt" IS NULL`, { - replacements: { projectId: project.id, element_type: globalDefault.element_type }, + replacements: { + projectId: project.id, + element_type: globalDefault.element_type, + }, type: Sequelize.QueryTypes.SELECT, - } + }, ); if (!existing) { - await queryInterface.bulkInsert('project_element_defaults', [{ - id: uuidv4(), - projectId: project.id, - element_type: globalDefault.element_type, - name: globalDefault.name, - sort_order: globalDefault.sort_order, - settings_json: globalDefault.settings_json, - source_element_id: globalDefault.id, - snapshot_version: 1, - createdAt: now, - updatedAt: now, - }]); + await queryInterface.bulkInsert('project_element_defaults', [ + { + id: uuidv4(), + projectId: project.id, + element_type: globalDefault.element_type, + name: globalDefault.name, + sort_order: globalDefault.sort_order, + settings_json: globalDefault.settings_json, + source_element_id: globalDefault.id, + snapshot_version: 1, + createdAt: now, + updatedAt: now, + }, + ]); } } } @@ -130,12 +139,12 @@ module.exports = { async down(queryInterface, _Sequelize) { // Remove the added element types from project_element_defaults await queryInterface.sequelize.query( - `DELETE FROM project_element_defaults WHERE element_type IN ('spot', 'logo', 'popup')` + `DELETE FROM project_element_defaults WHERE element_type IN ('spot', 'logo', 'popup')`, ); // Remove from element_type_defaults await queryInterface.sequelize.query( - `DELETE FROM element_type_defaults WHERE element_type IN ('spot', 'logo', 'popup')` + `DELETE FROM element_type_defaults WHERE element_type IN ('spot', 'logo', 'popup')`, ); }, }; diff --git a/backend/src/db/migrations/20260327000001-sync-all-element-type-defaults.js b/backend/src/db/migrations/20260327000001-sync-all-element-type-defaults.js new file mode 100644 index 0000000..76acf4e --- /dev/null +++ b/backend/src/db/migrations/20260327000001-sync-all-element-type-defaults.js @@ -0,0 +1,295 @@ +'use strict'; + +const { v4: uuidv4 } = require('uuid'); + +/** + * Sync all 11 element type defaults with correct sort_order. + * This migration ensures all element types exist in element_type_defaults + * and backfills any missing project_element_defaults for existing projects. + */ +module.exports = { + async up(queryInterface, Sequelize) { + const now = new Date(); + + // Define all 11 element types with correct sort_order + const DEFAULT_ELEMENT_TYPES = [ + { + element_type: 'navigation_next', + name: 'Navigation Forward Button', + sort_order: 1, + default_settings_json: { + label: 'Navigation: Forward', + navLabel: 'Forward', + navType: 'forward', + navDisabled: false, + transitionReverseMode: 'auto_reverse', + transitionDurationSec: 0.7, + appearDelaySec: 0, + appearDurationSec: null, + }, + }, + { + element_type: 'navigation_prev', + name: 'Navigation Back Button', + sort_order: 2, + default_settings_json: { + label: 'Navigation: Back', + navLabel: 'Back', + navType: 'back', + navDisabled: false, + transitionReverseMode: 'auto_reverse', + transitionDurationSec: 0.7, + appearDelaySec: 0, + appearDurationSec: null, + }, + }, + { + element_type: 'tooltip', + name: 'Tooltip', + sort_order: 3, + default_settings_json: { + label: 'Tooltip', + tooltipTitle: 'Tooltip title', + tooltipText: 'Tooltip text', + appearDelaySec: 0, + appearDurationSec: null, + }, + }, + { + element_type: 'description', + name: 'Description', + sort_order: 4, + default_settings_json: { + label: 'Description', + descriptionTitle: 'TITLE', + descriptionText: '', + descriptionTitleFontSize: '48px', + descriptionTextFontSize: '36px', + descriptionTitleFontFamily: 'inherit', + descriptionTextFontFamily: 'inherit', + descriptionTitleColor: '#000000', + descriptionTextColor: '#4B5563', + descriptionBackgroundColor: 'transparent', + appearDelaySec: 0, + appearDurationSec: null, + }, + }, + { + element_type: 'gallery', + name: 'Gallery', + sort_order: 5, + default_settings_json: { + label: 'Gallery', + galleryCards: [{ imageUrl: '', title: 'Card 1', description: '' }], + appearDelaySec: 0, + appearDurationSec: null, + }, + }, + { + element_type: 'carousel', + name: 'Carousel', + sort_order: 6, + default_settings_json: { + label: 'Carousel', + carouselSlides: [{ imageUrl: '', caption: 'Slide 1' }], + carouselPrevIconUrl: '', + carouselNextIconUrl: '', + appearDelaySec: 0, + appearDurationSec: null, + }, + }, + { + element_type: 'video_player', + name: 'Video Player', + sort_order: 7, + default_settings_json: { + label: 'Video Player', + mediaUrl: '', + mediaAutoplay: true, + mediaLoop: true, + mediaMuted: true, + appearDelaySec: 0, + appearDurationSec: null, + }, + }, + { + element_type: 'audio_player', + name: 'Audio Player', + sort_order: 8, + default_settings_json: { + label: 'Audio Player', + mediaUrl: '', + mediaAutoplay: true, + mediaLoop: true, + mediaMuted: false, + appearDelaySec: 0, + appearDurationSec: null, + }, + }, + { + element_type: 'spot', + name: 'Hotspot', + sort_order: 9, + default_settings_json: { + label: 'Hotspot', + iconUrl: '', + appearDelaySec: 0, + appearDurationSec: null, + }, + }, + { + element_type: 'logo', + name: 'Logo', + sort_order: 10, + default_settings_json: { + label: 'Logo', + iconUrl: '', + backgroundImageUrl: '', + appearDelaySec: 0, + appearDurationSec: null, + }, + }, + { + element_type: 'popup', + name: 'Popup', + sort_order: 11, + default_settings_json: { + label: 'Popup', + iconUrl: '', + popupTitle: '', + popupContent: '', + appearDelaySec: 0, + appearDurationSec: null, + }, + }, + ]; + + console.log('Syncing all 11 element type defaults...'); + + // Track inserted/updated global defaults for backfill + const globalDefaultIds = new Map(); + + // For each element type: insert if not exists, update sort_order if wrong + for (const elementType of DEFAULT_ELEMENT_TYPES) { + const [existing] = await queryInterface.sequelize.query( + `SELECT id, sort_order FROM element_type_defaults + WHERE element_type = :element_type AND "deletedAt" IS NULL`, + { + replacements: { element_type: elementType.element_type }, + type: Sequelize.QueryTypes.SELECT, + }, + ); + + if (!existing) { + // Insert new element type + const newId = uuidv4(); + await queryInterface.bulkInsert('element_type_defaults', [ + { + id: newId, + element_type: elementType.element_type, + name: elementType.name, + sort_order: elementType.sort_order, + settings_json: JSON.stringify(elementType.default_settings_json), + createdAt: now, + updatedAt: now, + }, + ]); + globalDefaultIds.set(elementType.element_type, newId); + console.log( + `Inserted: ${elementType.element_type} (sort_order: ${elementType.sort_order})`, + ); + } else { + globalDefaultIds.set(elementType.element_type, existing.id); + // Update sort_order if different + if (existing.sort_order !== elementType.sort_order) { + await queryInterface.sequelize.query( + `UPDATE element_type_defaults + SET sort_order = :sort_order, "updatedAt" = :now + WHERE id = :id`, + { + replacements: { + sort_order: elementType.sort_order, + now, + id: existing.id, + }, + }, + ); + console.log( + `Updated sort_order for ${elementType.element_type}: ${existing.sort_order} -> ${elementType.sort_order}`, + ); + } else { + console.log( + `Already exists: ${elementType.element_type} (sort_order: ${elementType.sort_order})`, + ); + } + } + } + + // Get all projects + const projects = await queryInterface.sequelize.query( + `SELECT id FROM projects WHERE "deletedAt" IS NULL`, + { type: Sequelize.QueryTypes.SELECT }, + ); + + console.log( + `Backfilling missing project_element_defaults for ${projects.length} projects...`, + ); + + // Get all global defaults for backfill + const globalDefaults = await queryInterface.sequelize.query( + `SELECT id, element_type, name, sort_order, settings_json + FROM element_type_defaults + WHERE "deletedAt" IS NULL`, + { type: Sequelize.QueryTypes.SELECT }, + ); + + let backfillCount = 0; + + // Backfill project_element_defaults for each project + for (const project of projects) { + for (const globalDefault of globalDefaults) { + // Check if project already has this element type + const [existing] = await queryInterface.sequelize.query( + `SELECT id FROM project_element_defaults + WHERE "projectId" = :projectId AND element_type = :element_type AND "deletedAt" IS NULL`, + { + replacements: { + projectId: project.id, + element_type: globalDefault.element_type, + }, + type: Sequelize.QueryTypes.SELECT, + }, + ); + + if (!existing) { + await queryInterface.bulkInsert('project_element_defaults', [ + { + id: uuidv4(), + projectId: project.id, + element_type: globalDefault.element_type, + name: globalDefault.name, + sort_order: globalDefault.sort_order, + settings_json: globalDefault.settings_json, + source_element_id: globalDefault.id, + snapshot_version: 1, + createdAt: now, + updatedAt: now, + }, + ]); + backfillCount++; + } + } + } + + console.log(`Backfilled ${backfillCount} project element defaults.`); + console.log('Sync complete.'); + }, + + async down(_queryInterface, _Sequelize) { + // This migration is safe - it only adds missing data + // No destructive down migration needed + console.log( + 'No down migration needed - this migration only adds missing data.', + ); + }, +}; diff --git a/backend/src/db/models/access_logs.js b/backend/src/db/models/access_logs.js index 706ffc1..f4c6366 100644 --- a/backend/src/db/models/access_logs.js +++ b/backend/src/db/models/access_logs.js @@ -1,4 +1,4 @@ -module.exports = function(sequelize, DataTypes) { +module.exports = function (sequelize, DataTypes) { const access_logs = sequelize.define( 'access_logs', { @@ -8,50 +8,44 @@ module.exports = function(sequelize, DataTypes) { primaryKey: true, }, -environment: { + environment: { type: DataTypes.ENUM, allowNull: false, - values: [ - -"admin", - - -"stage", - - -"production" - - ], - + values: ['admin', 'stage', 'production'], }, -path: { + path: { type: DataTypes.TEXT, validate: { len: { args: [0, 2048], msg: 'Path must be at most 2048 characters' }, }, }, -ip_address: { + ip_address: { type: DataTypes.TEXT, validate: { - len: { args: [0, 45], msg: 'IP address must be at most 45 characters' }, + len: { + args: [0, 45], + msg: 'IP address must be at most 45 characters', + }, }, }, -user_agent: { + user_agent: { type: DataTypes.TEXT, validate: { - len: { args: [0, 1024], msg: 'User agent must be at most 1024 characters' }, + len: { + args: [0, 1024], + msg: 'User agent must be at most 1024 characters', + }, }, }, -accessed_at: { + accessed_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW, - }, importHash: { @@ -74,31 +68,9 @@ accessed_at: { ); access_logs.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - -/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - - - - - - - - - - - - - - - - - - -//end loop - - + //end loop db.access_logs.belongsTo(db.projects, { as: 'project', @@ -120,9 +92,6 @@ accessed_at: { onUpdate: 'CASCADE', }); - - - db.access_logs.belongsTo(db.users, { as: 'createdBy', }); @@ -132,8 +101,5 @@ accessed_at: { }); }; - - return access_logs; }; - diff --git a/backend/src/db/models/asset_variants.js b/backend/src/db/models/asset_variants.js index daeb513..f7354f0 100644 --- a/backend/src/db/models/asset_variants.js +++ b/backend/src/db/models/asset_variants.js @@ -1,4 +1,4 @@ -module.exports = function(sequelize, DataTypes) { +module.exports = function (sequelize, DataTypes) { const asset_variants = sequelize.define( 'asset_variants', { @@ -8,38 +8,31 @@ module.exports = function(sequelize, DataTypes) { primaryKey: true, }, -variant_type: { + variant_type: { type: DataTypes.ENUM, - - values: [ + 'thumbnail', -"thumbnail", + 'preview', + 'webp', -"preview", + 'mp4_low', + 'mp4_high', -"webp", - - -"mp4_low", - - -"mp4_high", - - -"original" - + 'original', ], - }, -cdn_url: { + cdn_url: { type: DataTypes.TEXT, validate: { - len: { args: [0, 2048], msg: 'CDN URL must be at most 2048 characters' }, + len: { + args: [0, 2048], + msg: 'CDN URL must be at most 2048 characters', + }, isUrlOrEmpty(value) { if (value && value.length > 0 && !/^https?:\/\/.+/.test(value)) { throw new Error('CDN URL must be a valid URL'); @@ -48,21 +41,21 @@ cdn_url: { }, }, -width_px: { + width_px: { type: DataTypes.INTEGER, validate: { min: { args: [0], msg: 'Width must be a non-negative integer' }, }, }, -height_px: { + height_px: { type: DataTypes.INTEGER, validate: { min: { args: [0], msg: 'Height must be a non-negative integer' }, }, }, -size_mb: { + size_mb: { type: DataTypes.DECIMAL, validate: { min: { args: [0], msg: 'Size must be a non-negative number' }, @@ -83,31 +76,9 @@ size_mb: { ); asset_variants.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - -/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - - - - - - - - - - - - - - - - - - -//end loop - - + //end loop db.asset_variants.belongsTo(db.assets, { as: 'asset', @@ -119,9 +90,6 @@ size_mb: { onUpdate: 'CASCADE', }); - - - db.asset_variants.belongsTo(db.users, { as: 'createdBy', }); @@ -131,9 +99,5 @@ size_mb: { }); }; - - return asset_variants; }; - - diff --git a/backend/src/db/models/assets.js b/backend/src/db/models/assets.js index deab765..176a46f 100644 --- a/backend/src/db/models/assets.js +++ b/backend/src/db/models/assets.js @@ -1,4 +1,4 @@ -module.exports = function(sequelize, DataTypes) { +module.exports = function (sequelize, DataTypes) { const assets = sequelize.define( 'assets', { @@ -8,135 +8,92 @@ module.exports = function(sequelize, DataTypes) { primaryKey: true, }, -name: { + name: { type: DataTypes.TEXT, validate: { - len: { args: [0, 255], msg: 'Asset name must be at most 255 characters' }, + len: { + args: [0, 255], + msg: 'Asset name must be at most 255 characters', + }, }, }, -asset_type: { + asset_type: { type: DataTypes.ENUM, allowNull: false, - values: [ - -"image", - - -"video", - - -"audio", - - -"file" - - ], - + values: ['image', 'video', 'audio', 'file'], }, -type: { + type: { type: DataTypes.ENUM, allowNull: false, - defaultValue: "general", + defaultValue: 'general', values: [ + 'icon', -"icon", + 'background_image', + 'audio', -"background_image", + 'video', + 'transition', -"audio", + 'logo', + 'favicon', -"video", - - -"transition", - - -"logo", - - -"favicon", - - -"document", - - -"general" + 'document', + 'general', ], - }, -cdn_url: { + cdn_url: { type: DataTypes.TEXT, - - - }, -storage_key: { + storage_key: { type: DataTypes.TEXT, - - - }, -mime_type: { + mime_type: { type: DataTypes.TEXT, validate: { - is: { args: /^[a-z0-9]+\/[a-z0-9.+-]+$/i, msg: 'Invalid MIME type format' }, + is: { + args: /^[a-z0-9]+\/[a-z0-9.+-]+$/i, + msg: 'Invalid MIME type format', + }, }, }, -size_mb: { + size_mb: { type: DataTypes.DECIMAL, - - - }, -width_px: { + width_px: { type: DataTypes.INTEGER, - - - }, -height_px: { + height_px: { type: DataTypes.INTEGER, - - - }, -duration_sec: { + duration_sec: { type: DataTypes.DECIMAL, - - - }, -checksum: { + checksum: { type: DataTypes.TEXT, - - - }, -is_public: { + is_public: { type: DataTypes.BOOLEAN, - + allowNull: false, defaultValue: false, - - - }, importHash: { @@ -160,41 +117,19 @@ is_public: { ); assets.associate = (db) => { - - -/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - - - - - - + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity db.assets.hasMany(db.asset_variants, { as: 'asset_variants_asset', foreignKey: { - name: 'assetId', + name: 'assetId', }, constraints: true, onDelete: 'CASCADE', onUpdate: 'CASCADE', }); - - - - - - - - - - - -//end loop - - + //end loop db.assets.belongsTo(db.projects, { as: 'project', @@ -206,9 +141,6 @@ is_public: { onUpdate: 'CASCADE', }); - - - db.assets.belongsTo(db.users, { as: 'createdBy', }); @@ -218,7 +150,5 @@ is_public: { }); }; - - return assets; }; diff --git a/backend/src/db/models/element_type_defaults.js b/backend/src/db/models/element_type_defaults.js index 62da74a..70b3e21 100644 --- a/backend/src/db/models/element_type_defaults.js +++ b/backend/src/db/models/element_type_defaults.js @@ -13,7 +13,10 @@ module.exports = function (sequelize, DataTypes) { unique: true, validate: { notEmpty: { msg: 'Element type is required' }, - len: { args: [1, 100], msg: 'Element type must be between 1 and 100 characters' }, + len: { + args: [1, 100], + msg: 'Element type must be between 1 and 100 characters', + }, }, }, name: { @@ -21,7 +24,10 @@ module.exports = function (sequelize, DataTypes) { allowNull: false, validate: { notEmpty: { msg: 'Name is required' }, - len: { args: [1, 255], msg: 'Name must be between 1 and 255 characters' }, + len: { + args: [1, 255], + msg: 'Name must be between 1 and 255 characters', + }, }, }, sort_order: { diff --git a/backend/src/db/models/file.js b/backend/src/db/models/file.js index 7703bb6..84ee670 100644 --- a/backend/src/db/models/file.js +++ b/backend/src/db/models/file.js @@ -1,4 +1,4 @@ -module.exports = function(sequelize, DataTypes) { +module.exports = function (sequelize, DataTypes) { const file = sequelize.define( 'file', { diff --git a/backend/src/db/models/index.js b/backend/src/db/models/index.js index 4a3852f..e326416 100644 --- a/backend/src/db/models/index.js +++ b/backend/src/db/models/index.js @@ -5,7 +5,7 @@ const path = require('path'); const Sequelize = require('sequelize'); const basename = path.basename(__filename); const env = process.env.NODE_ENV || 'development'; -const config = require("../db.config")[env]; +const config = require('../db.config')[env]; const db = {}; let sequelize; @@ -13,20 +13,29 @@ console.log(env); if (config.use_env_variable) { sequelize = new Sequelize(process.env[config.use_env_variable], config); } else { - sequelize = new Sequelize(config.database, config.username, config.password, config); + sequelize = new Sequelize( + config.database, + config.username, + config.password, + config, + ); } -fs - .readdirSync(__dirname) - .filter(file => { - return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'); +fs.readdirSync(__dirname) + .filter((file) => { + return ( + file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js' + ); }) - .forEach(file => { - const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes) + .forEach((file) => { + const model = require(path.join(__dirname, file))( + sequelize, + Sequelize.DataTypes, + ); db[model.name] = model; }); -Object.keys(db).forEach(modelName => { +Object.keys(db).forEach((modelName) => { if (db[modelName].associate) { db[modelName].associate(db); } diff --git a/backend/src/db/models/permissions.js b/backend/src/db/models/permissions.js index f50552a..b0c6ef0 100644 --- a/backend/src/db/models/permissions.js +++ b/backend/src/db/models/permissions.js @@ -1,4 +1,4 @@ -module.exports = function(sequelize, DataTypes) { +module.exports = function (sequelize, DataTypes) { const permissions = sequelize.define( 'permissions', { @@ -8,13 +8,16 @@ module.exports = function(sequelize, DataTypes) { primaryKey: true, }, -name: { + name: { type: DataTypes.TEXT, allowNull: false, unique: true, validate: { notEmpty: { msg: 'Permission name is required' }, - len: { args: [1, 100], msg: 'Permission name must be between 1 and 100 characters' }, + len: { + args: [1, 100], + msg: 'Permission name must be between 1 and 100 characters', + }, }, }, @@ -32,34 +35,9 @@ name: { ); permissions.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - -/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - - - - - - - - - - - - - - - - - - -//end loop - - - - - + //end loop db.permissions.belongsTo(db.users, { as: 'createdBy', @@ -70,9 +48,5 @@ name: { }); }; - - return permissions; }; - - diff --git a/backend/src/db/models/presigned_url_requests.js b/backend/src/db/models/presigned_url_requests.js index 5d73be4..46baf02 100644 --- a/backend/src/db/models/presigned_url_requests.js +++ b/backend/src/db/models/presigned_url_requests.js @@ -1,4 +1,4 @@ -module.exports = function(sequelize, DataTypes) { +module.exports = function (sequelize, DataTypes) { const presigned_url_requests = sequelize.define( 'presigned_url_requests', { @@ -8,82 +8,63 @@ module.exports = function(sequelize, DataTypes) { primaryKey: true, }, -purpose: { + purpose: { type: DataTypes.ENUM, - - - - values: [ - -"upload", - - -"download" - - ], + values: ['upload', 'download'], }, -asset_type: { + asset_type: { type: DataTypes.ENUM, - - - - values: [ - -"image", - - -"video", - - -"audio", - - -"file" - - ], + values: ['image', 'video', 'audio', 'file'], }, -requested_key: { + requested_key: { type: DataTypes.TEXT, validate: { - len: { args: [0, 1024], msg: 'Requested key must be at most 1024 characters' }, + len: { + args: [0, 1024], + msg: 'Requested key must be at most 1024 characters', + }, }, }, -mime_type: { + mime_type: { type: DataTypes.TEXT, validate: { - len: { args: [0, 255], msg: 'MIME type must be at most 255 characters' }, + len: { + args: [0, 255], + msg: 'MIME type must be at most 255 characters', + }, isMimeTypeOrEmpty(value) { - if (value && value.length > 0 && !/^[\w.-]+\/[\w.+-]+$/.test(value)) { + if ( + value && + value.length > 0 && + !/^[\w.-]+\/[\w.+-]+$/.test(value) + ) { throw new Error('MIME type must be in format type/subtype'); } }, }, }, -requested_size_mb: { + requested_size_mb: { type: DataTypes.DECIMAL, validate: { - min: { args: [0], msg: 'Requested size must be a non-negative number' }, + min: { + args: [0], + msg: 'Requested size must be a non-negative number', + }, }, }, -expires_at: { + expires_at: { type: DataTypes.DATE, - - - }, -status: { + status: { type: DataTypes.TEXT, - - - }, importHash: { @@ -100,31 +81,9 @@ status: { ); presigned_url_requests.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - -/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - - - - - - - - - - - - - - - - - - -//end loop - - + //end loop db.presigned_url_requests.belongsTo(db.projects, { as: 'project', @@ -146,9 +105,6 @@ status: { onUpdate: 'CASCADE', }); - - - db.presigned_url_requests.belongsTo(db.users, { as: 'createdBy', }); @@ -158,9 +114,5 @@ status: { }); }; - - return presigned_url_requests; }; - - diff --git a/backend/src/db/models/project_audio_tracks.js b/backend/src/db/models/project_audio_tracks.js index 7fb2a5c..f38fd55 100644 --- a/backend/src/db/models/project_audio_tracks.js +++ b/backend/src/db/models/project_audio_tracks.js @@ -1,4 +1,4 @@ -module.exports = function(sequelize, DataTypes) { +module.exports = function (sequelize, DataTypes) { const project_audio_tracks = sequelize.define( 'project_audio_tracks', { @@ -8,64 +8,42 @@ module.exports = function(sequelize, DataTypes) { primaryKey: true, }, -environment: { + environment: { type: DataTypes.ENUM, - - - - values: [ - -"dev", - - -"stage", - - -"production" - - ], + values: ['dev', 'stage', 'production'], }, -source_key: { + source_key: { type: DataTypes.TEXT, - - - }, -name: { + name: { type: DataTypes.TEXT, validate: { - len: { args: [0, 255], msg: 'Audio track name must be at most 255 characters' }, + len: { + args: [0, 255], + msg: 'Audio track name must be at most 255 characters', + }, }, }, -slug: { + slug: { type: DataTypes.TEXT, - - - }, -url: { + url: { type: DataTypes.TEXT, - - - }, -loop: { + loop: { type: DataTypes.BOOLEAN, - + allowNull: false, defaultValue: false, - - - }, -volume: { + volume: { type: DataTypes.DECIMAL, validate: { min: { args: [0], msg: 'Volume must be at least 0' }, @@ -73,21 +51,15 @@ volume: { }, }, -sort_order: { + sort_order: { type: DataTypes.INTEGER, - - - }, -is_enabled: { + is_enabled: { type: DataTypes.BOOLEAN, - + allowNull: false, defaultValue: false, - - - }, importHash: { @@ -104,31 +76,9 @@ is_enabled: { ); project_audio_tracks.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - -/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - - - - - - - - - - - - - - - - - - -//end loop - - + //end loop db.project_audio_tracks.belongsTo(db.projects, { as: 'project', @@ -140,9 +90,6 @@ is_enabled: { onUpdate: 'CASCADE', }); - - - db.project_audio_tracks.belongsTo(db.users, { as: 'createdBy', }); @@ -152,9 +99,5 @@ is_enabled: { }); }; - - return project_audio_tracks; }; - - diff --git a/backend/src/db/models/project_element_defaults.js b/backend/src/db/models/project_element_defaults.js index ff6d96b..4d981d4 100644 --- a/backend/src/db/models/project_element_defaults.js +++ b/backend/src/db/models/project_element_defaults.js @@ -13,7 +13,10 @@ module.exports = function (sequelize, DataTypes) { allowNull: false, validate: { notEmpty: { msg: 'Element type is required' }, - len: { args: [1, 100], msg: 'Element type must be between 1 and 100 characters' }, + len: { + args: [1, 100], + msg: 'Element type must be between 1 and 100 characters', + }, }, }, name: { diff --git a/backend/src/db/models/project_memberships.js b/backend/src/db/models/project_memberships.js index 67ef4ca..2ef0572 100644 --- a/backend/src/db/models/project_memberships.js +++ b/backend/src/db/models/project_memberships.js @@ -1,4 +1,4 @@ -module.exports = function(sequelize, DataTypes) { +module.exports = function (sequelize, DataTypes) { const project_memberships = sequelize.define( 'project_memberships', { @@ -8,50 +8,27 @@ module.exports = function(sequelize, DataTypes) { primaryKey: true, }, -access_level: { + access_level: { type: DataTypes.ENUM, allowNull: false, defaultValue: 'viewer', - values: [ - -"owner", - - -"editor", - - -"reviewer", - - -"viewer" - - ], - + values: ['owner', 'editor', 'reviewer', 'viewer'], }, -is_active: { + is_active: { type: DataTypes.BOOLEAN, - + allowNull: false, defaultValue: false, - - - }, -invited_at: { + invited_at: { type: DataTypes.DATE, - - - }, -accepted_at: { + accepted_at: { type: DataTypes.DATE, - - - }, importHash: { @@ -75,31 +52,9 @@ accepted_at: { ); project_memberships.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - -/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - - - - - - - - - - - - - - - - - - -//end loop - - + //end loop db.project_memberships.belongsTo(db.projects, { as: 'project', @@ -121,9 +76,6 @@ accepted_at: { onUpdate: 'CASCADE', }); - - - db.project_memberships.belongsTo(db.users, { as: 'createdBy', }); @@ -133,8 +85,5 @@ accepted_at: { }); }; - - return project_memberships; }; - diff --git a/backend/src/db/models/projects.js b/backend/src/db/models/projects.js index 5dfd43b..b9fa860 100644 --- a/backend/src/db/models/projects.js +++ b/backend/src/db/models/projects.js @@ -1,4 +1,4 @@ -module.exports = function(sequelize, DataTypes) { +module.exports = function (sequelize, DataTypes) { const projects = sequelize.define( 'projects', { @@ -8,67 +8,61 @@ module.exports = function(sequelize, DataTypes) { primaryKey: true, }, -name: { + name: { type: DataTypes.TEXT, allowNull: false, validate: { notEmpty: { msg: 'Project name is required' }, - len: { args: [1, 255], msg: 'Project name must be between 1 and 255 characters' }, + len: { + args: [1, 255], + msg: 'Project name must be between 1 and 255 characters', + }, }, }, -slug: { + slug: { type: DataTypes.TEXT, allowNull: false, unique: true, validate: { notEmpty: { msg: 'Slug is required' }, - is: { args: /^[a-z0-9_-]+$/i, msg: 'Slug can only contain letters, numbers, dashes, and underscores' }, - len: { args: [1, 255], msg: 'Slug must be between 1 and 255 characters' }, + is: { + args: /^[a-z0-9_-]+$/i, + msg: 'Slug can only contain letters, numbers, dashes, and underscores', + }, + len: { + args: [1, 255], + msg: 'Slug must be between 1 and 255 characters', + }, }, }, -description: { + description: { type: DataTypes.TEXT, - - - }, -logo_url: { + logo_url: { type: DataTypes.TEXT, - - - }, -favicon_url: { + favicon_url: { type: DataTypes.TEXT, - - - }, -og_image_url: { + og_image_url: { type: DataTypes.TEXT, - - - }, -theme_config_json: { + theme_config_json: { type: DataTypes.JSON, }, -custom_css_json: { + custom_css_json: { type: DataTypes.JSON, }, -cdn_base_url: { + cdn_base_url: { type: DataTypes.TEXT, - - - }, importHash: { @@ -81,130 +75,104 @@ cdn_base_url: { timestamps: true, paranoid: true, freezeTableName: true, - indexes: [ - { fields: ['slug'], unique: true }, - { fields: ['deletedAt'] }, - ], + indexes: [{ fields: ['slug'], unique: true }, { fields: ['deletedAt'] }], }, ); projects.associate = (db) => { - - -/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - - - - + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity db.projects.hasMany(db.project_memberships, { as: 'project_memberships_project', foreignKey: { - name: 'projectId', + name: 'projectId', }, constraints: true, onDelete: 'CASCADE', onUpdate: 'CASCADE', }); - db.projects.hasMany(db.assets, { as: 'assets_project', foreignKey: { - name: 'projectId', + name: 'projectId', }, constraints: true, onDelete: 'CASCADE', onUpdate: 'CASCADE', }); - - db.projects.hasMany(db.presigned_url_requests, { as: 'presigned_url_requests_project', foreignKey: { - name: 'projectId', + name: 'projectId', }, constraints: true, onDelete: 'CASCADE', onUpdate: 'CASCADE', }); - db.projects.hasMany(db.tour_pages, { as: 'tour_pages_project', foreignKey: { - name: 'projectId', + name: 'projectId', }, constraints: true, onDelete: 'CASCADE', onUpdate: 'CASCADE', }); - - - db.projects.hasMany(db.project_audio_tracks, { as: 'project_audio_tracks_project', foreignKey: { - name: 'projectId', + name: 'projectId', }, constraints: true, onDelete: 'CASCADE', onUpdate: 'CASCADE', }); - db.projects.hasMany(db.publish_events, { as: 'publish_events_project', foreignKey: { - name: 'projectId', + name: 'projectId', }, constraints: true, onDelete: 'CASCADE', onUpdate: 'CASCADE', }); - db.projects.hasMany(db.pwa_caches, { as: 'pwa_caches_project', foreignKey: { - name: 'projectId', + name: 'projectId', }, constraints: true, onDelete: 'CASCADE', onUpdate: 'CASCADE', }); - db.projects.hasMany(db.access_logs, { as: 'access_logs_project', foreignKey: { - name: 'projectId', + name: 'projectId', }, constraints: true, onDelete: 'CASCADE', onUpdate: 'CASCADE', }); - db.projects.hasMany(db.project_element_defaults, { as: 'project_element_defaults_project', foreignKey: { - name: 'projectId', + name: 'projectId', }, constraints: true, onDelete: 'CASCADE', onUpdate: 'CASCADE', }); -//end loop - - - - - + //end loop db.projects.belongsTo(db.users, { as: 'createdBy', @@ -215,8 +183,5 @@ cdn_base_url: { }); }; - - return projects; }; - diff --git a/backend/src/db/models/publish_events.js b/backend/src/db/models/publish_events.js index c600459..7c09129 100644 --- a/backend/src/db/models/publish_events.js +++ b/backend/src/db/models/publish_events.js @@ -1,4 +1,4 @@ -module.exports = function(sequelize, DataTypes) { +module.exports = function (sequelize, DataTypes) { const publish_events = sequelize.define( 'publish_events', { @@ -8,7 +8,7 @@ module.exports = function(sequelize, DataTypes) { primaryKey: true, }, -title: { + title: { type: DataTypes.STRING, allowNull: true, validate: { @@ -16,111 +16,78 @@ title: { }, }, -description: { + description: { type: DataTypes.TEXT, allowNull: true, validate: { - len: { args: [0, 5000], msg: 'Description must be at most 5000 characters' }, + len: { + args: [0, 5000], + msg: 'Description must be at most 5000 characters', + }, }, }, -from_environment: { + from_environment: { type: DataTypes.ENUM, allowNull: false, - values: [ - -"dev", - - -"stage", - - -"production" - - ], - + values: ['dev', 'stage', 'production'], }, -to_environment: { + to_environment: { type: DataTypes.ENUM, allowNull: false, - values: [ - -"dev", - - -"stage", - - -"production" - - ], - + values: ['dev', 'stage', 'production'], }, -started_at: { + started_at: { type: DataTypes.DATE, - - - }, -finished_at: { + finished_at: { type: DataTypes.DATE, - - - }, -status: { + status: { type: DataTypes.ENUM, allowNull: false, defaultValue: 'queued', - values: [ - -"queued", - - -"running", - - -"success", - - -"failed" - - ], - + values: ['queued', 'running', 'success', 'failed'], }, -error_message: { + error_message: { type: DataTypes.TEXT, - - - }, -pages_copied: { + pages_copied: { type: DataTypes.INTEGER, validate: { - min: { args: [0], msg: 'Pages copied must be a non-negative integer' }, + min: { + args: [0], + msg: 'Pages copied must be a non-negative integer', + }, }, }, -transitions_copied: { + transitions_copied: { type: DataTypes.INTEGER, validate: { - min: { args: [0], msg: 'Transitions copied must be a non-negative integer' }, + min: { + args: [0], + msg: 'Transitions copied must be a non-negative integer', + }, }, }, -audios_copied: { + audios_copied: { type: DataTypes.INTEGER, validate: { - min: { args: [0], msg: 'Audios copied must be a non-negative integer' }, + min: { + args: [0], + msg: 'Audios copied must be a non-negative integer', + }, }, }, @@ -144,31 +111,9 @@ audios_copied: { ); publish_events.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - -/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - - - - - - - - - - - - - - - - - - -//end loop - - + //end loop db.publish_events.belongsTo(db.projects, { as: 'project', @@ -190,9 +135,6 @@ audios_copied: { onUpdate: 'CASCADE', }); - - - db.publish_events.belongsTo(db.users, { as: 'createdBy', }); @@ -202,7 +144,5 @@ audios_copied: { }); }; - - return publish_events; }; diff --git a/backend/src/db/models/pwa_caches.js b/backend/src/db/models/pwa_caches.js index b31d65c..7f84fc4 100644 --- a/backend/src/db/models/pwa_caches.js +++ b/backend/src/db/models/pwa_caches.js @@ -1,4 +1,4 @@ -module.exports = function(sequelize, DataTypes) { +module.exports = function (sequelize, DataTypes) { const pwa_caches = sequelize.define( 'pwa_caches', { @@ -8,55 +8,39 @@ module.exports = function(sequelize, DataTypes) { primaryKey: true, }, -environment: { + environment: { type: DataTypes.ENUM, - - - - values: [ - -"dev", - - -"stage", - - -"production" - - ], + values: ['dev', 'stage', 'production'], }, -cache_version: { + cache_version: { type: DataTypes.TEXT, validate: { - len: { args: [0, 255], msg: 'Cache version must be at most 255 characters' }, + len: { + args: [0, 255], + msg: 'Cache version must be at most 255 characters', + }, }, }, -manifest_json: { + manifest_json: { type: DataTypes.JSON, }, -asset_list_json: { + asset_list_json: { type: DataTypes.JSON, }, -generated_at: { + generated_at: { type: DataTypes.DATE, - - - }, -is_active: { + is_active: { type: DataTypes.BOOLEAN, - + allowNull: false, defaultValue: false, - - - }, importHash: { @@ -73,31 +57,9 @@ is_active: { ); pwa_caches.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - -/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - - - - - - - - - - - - - - - - - - -//end loop - - + //end loop db.pwa_caches.belongsTo(db.projects, { as: 'project', @@ -109,9 +71,6 @@ is_active: { onUpdate: 'CASCADE', }); - - - db.pwa_caches.belongsTo(db.users, { as: 'createdBy', }); @@ -121,9 +80,5 @@ is_active: { }); }; - - return pwa_caches; }; - - diff --git a/backend/src/db/models/roles.js b/backend/src/db/models/roles.js index 327b107..6765d3e 100644 --- a/backend/src/db/models/roles.js +++ b/backend/src/db/models/roles.js @@ -1,4 +1,4 @@ -module.exports = function(sequelize, DataTypes) { +module.exports = function (sequelize, DataTypes) { const roles = sequelize.define( 'roles', { @@ -8,20 +8,20 @@ module.exports = function(sequelize, DataTypes) { primaryKey: true, }, -name: { + name: { type: DataTypes.TEXT, allowNull: false, validate: { notEmpty: { msg: 'Role name is required' }, - len: { args: [1, 100], msg: 'Role name must be between 1 and 100 characters' }, + len: { + args: [1, 100], + msg: 'Role name must be between 1 and 100 characters', + }, }, }, -role_customization: { + role_customization: { type: DataTypes.TEXT, - - - }, importHash: { @@ -38,7 +38,6 @@ role_customization: { ); roles.associate = (db) => { - db.roles.belongsToMany(db.permissions, { as: 'permissions', foreignKey: { @@ -59,43 +58,19 @@ role_customization: { through: 'rolesPermissionsPermissions', }); - -/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity db.roles.hasMany(db.users, { as: 'users_app_role', foreignKey: { - name: 'app_roleId', + name: 'app_roleId', }, constraints: true, onDelete: 'SET NULL', onUpdate: 'CASCADE', }); - - - - - - - - - - - - - - - - - -//end loop - - - - - + //end loop db.roles.belongsTo(db.users, { as: 'createdBy', @@ -106,9 +81,5 @@ role_customization: { }); }; - - return roles; }; - - diff --git a/backend/src/db/models/tour_pages.js b/backend/src/db/models/tour_pages.js index 861ae0b..4c5e78f 100644 --- a/backend/src/db/models/tour_pages.js +++ b/backend/src/db/models/tour_pages.js @@ -1,4 +1,4 @@ -module.exports = function(sequelize, DataTypes) { +module.exports = function (sequelize, DataTypes) { const tour_pages = sequelize.define( 'tour_pages', { @@ -8,100 +8,79 @@ module.exports = function(sequelize, DataTypes) { primaryKey: true, }, -environment: { + environment: { type: DataTypes.ENUM, allowNull: false, defaultValue: 'dev', - values: [ - -"dev", - - -"stage", - - -"production" - - ], - + values: ['dev', 'stage', 'production'], }, -source_key: { + source_key: { type: DataTypes.TEXT, - - - }, -name: { + name: { type: DataTypes.TEXT, allowNull: false, validate: { notEmpty: { msg: 'Page name is required' }, - len: { args: [1, 255], msg: 'Page name must be between 1 and 255 characters' }, + len: { + args: [1, 255], + msg: 'Page name must be between 1 and 255 characters', + }, }, }, -slug: { + slug: { type: DataTypes.TEXT, allowNull: false, validate: { notEmpty: { msg: 'Slug is required' }, - is: { args: /^[a-z0-9_-]+$/i, msg: 'Slug can only contain letters, numbers, dashes, and underscores' }, - len: { args: [1, 255], msg: 'Slug must be between 1 and 255 characters' }, + is: { + args: /^[a-z0-9_-]+$/i, + msg: 'Slug can only contain letters, numbers, dashes, and underscores', + }, + len: { + args: [1, 255], + msg: 'Slug must be between 1 and 255 characters', + }, }, }, -sort_order: { + sort_order: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, - }, -background_image_url: { + background_image_url: { type: DataTypes.TEXT, - - - }, -background_video_url: { + background_video_url: { type: DataTypes.TEXT, - - - }, -background_audio_url: { + background_audio_url: { type: DataTypes.TEXT, - - - }, -background_loop: { + background_loop: { type: DataTypes.BOOLEAN, - + allowNull: false, defaultValue: false, - - - }, -requires_auth: { + requires_auth: { type: DataTypes.BOOLEAN, - + allowNull: false, defaultValue: false, - - - }, -ui_schema_json: { + ui_schema_json: { type: DataTypes.JSON, }, @@ -125,31 +104,9 @@ ui_schema_json: { ); tour_pages.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - -/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - - - - - - - - - - - - - - - - - - -//end loop - - + //end loop db.tour_pages.belongsTo(db.projects, { as: 'project', @@ -161,9 +118,6 @@ ui_schema_json: { onUpdate: 'CASCADE', }); - - - db.tour_pages.belongsTo(db.users, { as: 'createdBy', }); @@ -173,8 +127,5 @@ ui_schema_json: { }); }; - - return tour_pages; }; - diff --git a/backend/src/db/models/users.js b/backend/src/db/models/users.js index 6fd8a5f..741ce82 100644 --- a/backend/src/db/models/users.js +++ b/backend/src/db/models/users.js @@ -3,7 +3,7 @@ const providers = config.providers; const crypto = require('crypto'); const bcrypt = require('bcrypt'); -module.exports = function(sequelize, DataTypes) { +module.exports = function (sequelize, DataTypes) { const users = sequelize.define( 'users', { @@ -13,28 +13,19 @@ module.exports = function(sequelize, DataTypes) { primaryKey: true, }, -firstName: { + firstName: { type: DataTypes.TEXT, - - - }, -lastName: { + lastName: { type: DataTypes.TEXT, - - - }, -phoneNumber: { + phoneNumber: { type: DataTypes.TEXT, - - - }, -email: { + email: { type: DataTypes.TEXT, allowNull: false, unique: true, @@ -44,65 +35,45 @@ email: { }, }, -disabled: { + disabled: { type: DataTypes.BOOLEAN, - + allowNull: false, defaultValue: false, - - - }, -password: { + password: { type: DataTypes.TEXT, allowNull: false, - }, -emailVerified: { + emailVerified: { type: DataTypes.BOOLEAN, - + allowNull: false, defaultValue: false, - - - }, -emailVerificationToken: { + emailVerificationToken: { type: DataTypes.TEXT, - - - }, -emailVerificationTokenExpiresAt: { + emailVerificationTokenExpiresAt: { type: DataTypes.DATE, - - - }, -passwordResetToken: { + passwordResetToken: { type: DataTypes.TEXT, - - - }, -passwordResetTokenExpiresAt: { + passwordResetTokenExpiresAt: { type: DataTypes.DATE, - - - }, -provider: { + provider: { type: DataTypes.TEXT, allowNull: false, defaultValue: providers.LOCAL, - }, importHash: { @@ -124,7 +95,6 @@ provider: { ); users.associate = (db) => { - db.users.belongsToMany(db.permissions, { as: 'custom_permissions', foreignKey: { @@ -145,70 +115,49 @@ provider: { through: 'usersCustom_permissionsPermissions', }); - -/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - - - - + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity db.users.hasMany(db.project_memberships, { as: 'project_memberships_user', foreignKey: { - name: 'userId', + name: 'userId', }, constraints: true, onDelete: 'CASCADE', onUpdate: 'CASCADE', }); - - - db.users.hasMany(db.presigned_url_requests, { as: 'presigned_url_requests_user', foreignKey: { - name: 'userId', + name: 'userId', }, constraints: true, onDelete: 'CASCADE', onUpdate: 'CASCADE', }); - - - - - - db.users.hasMany(db.publish_events, { as: 'publish_events_user', foreignKey: { - name: 'userId', + name: 'userId', }, constraints: true, onDelete: 'SET NULL', onUpdate: 'CASCADE', }); - - db.users.hasMany(db.access_logs, { as: 'access_logs_user', foreignKey: { - name: 'userId', + name: 'userId', }, constraints: true, onDelete: 'SET NULL', onUpdate: 'CASCADE', }); - - -//end loop - - + //end loop db.users.belongsTo(db.roles, { as: 'app_role', @@ -220,8 +169,6 @@ provider: { onUpdate: 'CASCADE', }); - - db.users.hasMany(db.file, { as: 'avatar', foreignKey: 'belongsToId', @@ -234,7 +181,6 @@ provider: { }, }); - db.users.belongsTo(db.users, { as: 'createdBy', }); @@ -244,47 +190,41 @@ provider: { }); }; + users.beforeCreate((users) => { + users = trimStringFields(users); - users.beforeCreate((users) => { - users = trimStringFields(users); + if ( + users.provider !== providers.LOCAL && + Object.values(providers).indexOf(users.provider) > -1 + ) { + users.emailVerified = true; - if (users.provider !== providers.LOCAL && Object.values(providers).indexOf(users.provider) > -1) { - users.emailVerified = true; + if (!users.password) { + const password = crypto.randomBytes(20).toString('hex'); - if (!users.password) { - const password = crypto - .randomBytes(20) - .toString('hex'); - - const hashedPassword = bcrypt.hashSync( - password, - config.bcrypt.saltRounds, + const hashedPassword = bcrypt.hashSync( + password, + config.bcrypt.saltRounds, ); - users.password = hashedPassword - } - } - }); + users.password = hashedPassword; + } + } + }); users.beforeUpdate((users) => { trimStringFields(users); }); - return users; }; - function trimStringFields(users) { users.email = users.email.trim(); - users.firstName = users.firstName - ? users.firstName.trim() - : null; + users.firstName = users.firstName ? users.firstName.trim() : null; - users.lastName = users.lastName - ? users.lastName.trim() - : null; + users.lastName = users.lastName ? users.lastName.trim() : null; return users; } diff --git a/backend/src/db/reset.js b/backend/src/db/reset.js index 5904d4b..bc0b5f9 100644 --- a/backend/src/db/reset.js +++ b/backend/src/db/reset.js @@ -1,12 +1,12 @@ const db = require('./models'); -const {execSync} = require("child_process"); +const { execSync } = require('child_process'); console.log('Resetting Database'); db.sequelize .sync({ force: true }) .then(() => { - execSync("sequelize db:seed:all"); + execSync('sequelize db:seed:all'); console.log('OK'); process.exit(); }) diff --git a/backend/src/db/seeders/20200430130759-admin-user.js b/backend/src/db/seeders/20200430130759-admin-user.js index 7cc989e..bb6ec0b 100644 --- a/backend/src/db/seeders/20200430130759-admin-user.js +++ b/backend/src/db/seeders/20200430130759-admin-user.js @@ -1,51 +1,54 @@ 'use strict'; -const bcrypt = require("bcrypt"); -const config = require("../../config"); +const bcrypt = require('bcrypt'); +const config = require('../../config'); const ids = [ - '193bf4b5-9f07-4bd5-9a43-e7e41f3e96af', - 'af5a87be-8f9c-4630-902a-37a60b7005ba', - '5bc531ab-611f-41f3-9373-b7cc5d09c93d', -] + '193bf4b5-9f07-4bd5-9a43-e7e41f3e96af', + 'af5a87be-8f9c-4630-902a-37a60b7005ba', + '5bc531ab-611f-41f3-9373-b7cc5d09c93d', +]; module.exports = { up: async (queryInterface) => { - let admin_hash = bcrypt.hashSync(config.admin_pass, config.bcrypt.saltRounds); + let admin_hash = bcrypt.hashSync( + config.admin_pass, + config.bcrypt.saltRounds, + ); let user_hash = bcrypt.hashSync(config.user_pass, config.bcrypt.saltRounds); try { - await queryInterface.bulkInsert('users', [ - { - id: ids[0], - firstName: 'Admin', - email: config.admin_email, - emailVerified: true, - provider: config.providers.LOCAL, - password: admin_hash, - createdAt: new Date(), - updatedAt: new Date() - }, - { - id: ids[1], - firstName: 'John', - email: 'john@doe.com', - emailVerified: true, - provider: config.providers.LOCAL, - password: user_hash, - createdAt: new Date(), - updatedAt: new Date() - }, - { - id: ids[2], - firstName: 'Client', - email: 'client@hello.com', - emailVerified: true, - provider: config.providers.LOCAL, - password: user_hash, - createdAt: new Date(), - updatedAt: new Date() - }, - ]); + await queryInterface.bulkInsert('users', [ + { + id: ids[0], + firstName: 'Admin', + email: config.admin_email, + emailVerified: true, + provider: config.providers.LOCAL, + password: admin_hash, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: ids[1], + firstName: 'John', + email: 'john@doe.com', + emailVerified: true, + provider: config.providers.LOCAL, + password: user_hash, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: ids[2], + firstName: 'Client', + email: 'client@hello.com', + emailVerified: true, + provider: config.providers.LOCAL, + password: user_hash, + createdAt: new Date(), + updatedAt: new Date(), + }, + ]); } catch (error) { console.error('Error during bulkInsert:', error); throw error; @@ -53,14 +56,18 @@ module.exports = { }, down: async (queryInterface, Sequelize) => { try { - await queryInterface.bulkDelete('users', { - id: { - [Sequelize.Op.in]: ids, - }, - }, {}); - } catch (error) { - console.error('Error during bulkDelete:', error); - throw error; - } -} -} + await queryInterface.bulkDelete( + 'users', + { + id: { + [Sequelize.Op.in]: ids, + }, + }, + {}, + ); + } catch (error) { + console.error('Error during bulkDelete:', error); + throw error; + } + }, +}; diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.js index df4cdc3..dc7cfd9 100644 --- a/backend/src/db/seeders/20200430130760-user-roles.js +++ b/backend/src/db/seeders/20200430130760-user-roles.js @@ -1,5 +1,4 @@ - -const { v4: uuid } = require("uuid"); +const { v4: uuid } = require('uuid'); module.exports = { /** @@ -26,26 +25,50 @@ module.exports = { return id; } - await queryInterface.bulkInsert("roles", [ - - - { id: getId("Administrator"), name: "Administrator", createdAt, updatedAt }, - - - - { id: getId("PlatformOwner"), name: "Platform Owner", createdAt, updatedAt }, - - { id: getId("AccountManager"), name: "Account Manager", createdAt, updatedAt }, - - { id: getId("TourDesigner"), name: "Tour Designer", createdAt, updatedAt }, - - { id: getId("ContentReviewer"), name: "Content Reviewer", createdAt, updatedAt }, - - { id: getId("AnalyticsViewer"), name: "Analytics Viewer", createdAt, updatedAt }, - - - - { id: getId("Public"), name: "Public", createdAt, updatedAt }, + await queryInterface.bulkInsert('roles', [ + { + id: getId('Administrator'), + name: 'Administrator', + createdAt, + updatedAt, + }, + + { + id: getId('PlatformOwner'), + name: 'Platform Owner', + createdAt, + updatedAt, + }, + + { + id: getId('AccountManager'), + name: 'Account Manager', + createdAt, + updatedAt, + }, + + { + id: getId('TourDesigner'), + name: 'Tour Designer', + createdAt, + updatedAt, + }, + + { + id: getId('ContentReviewer'), + name: 'Content Reviewer', + createdAt, + updatedAt, + }, + + { + id: getId('AnalyticsViewer'), + name: 'Analytics Viewer', + createdAt, + updatedAt, + }, + + { id: getId('Public'), name: 'Public', createdAt, updatedAt }, ]); /** @@ -53,22 +76,71 @@ module.exports = { */ function createPermissions(name) { return [ - { id: getId(`CREATE_${name.toUpperCase()}`), createdAt, updatedAt, name: `CREATE_${name.toUpperCase()}` }, - { id: getId(`READ_${name.toUpperCase()}`), createdAt, updatedAt, name: `READ_${name.toUpperCase()}` }, - { id: getId(`UPDATE_${name.toUpperCase()}`), createdAt, updatedAt, name: `UPDATE_${name.toUpperCase()}` }, - { id: getId(`DELETE_${name.toUpperCase()}`), createdAt, updatedAt, name: `DELETE_${name.toUpperCase()}` } + { + id: getId(`CREATE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `CREATE_${name.toUpperCase()}`, + }, + { + id: getId(`READ_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `READ_${name.toUpperCase()}`, + }, + { + id: getId(`UPDATE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `UPDATE_${name.toUpperCase()}`, + }, + { + id: getId(`DELETE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `DELETE_${name.toUpperCase()}`, + }, ]; } const entities = [ - "users","roles","permissions","projects","project_memberships","assets","asset_variants","presigned_url_requests","tour_pages","project_audio_tracks","publish_events","pwa_caches","access_logs", + 'users', + 'roles', + 'permissions', + 'projects', + 'project_memberships', + 'assets', + 'asset_variants', + 'presigned_url_requests', + 'tour_pages', + 'project_audio_tracks', + 'publish_events', + 'pwa_caches', + 'access_logs', ]; -await queryInterface.bulkInsert("permissions", entities.flatMap(createPermissions)); -await queryInterface.bulkInsert("permissions", [{ id: getId(`READ_API_DOCS`), createdAt, updatedAt, name: `READ_API_DOCS` }]); -await queryInterface.bulkInsert("permissions", [{ id: getId(`CREATE_SEARCH`), createdAt, updatedAt, name: `CREATE_SEARCH`}]); + await queryInterface.bulkInsert( + 'permissions', + entities.flatMap(createPermissions), + ); + await queryInterface.bulkInsert('permissions', [ + { + id: getId(`READ_API_DOCS`), + createdAt, + updatedAt, + name: `READ_API_DOCS`, + }, + ]); + await queryInterface.bulkInsert('permissions', [ + { + id: getId(`CREATE_SEARCH`), + createdAt, + updatedAt, + name: `CREATE_SEARCH`, + }, + ]); - -await queryInterface.sequelize.query(`CREATE TABLE IF NOT EXISTS "rolesPermissionsPermissions" + await queryInterface.sequelize + .query(`CREATE TABLE IF NOT EXISTS "rolesPermissionsPermissions" ( "createdAt" timestamp with time zone not null, "updatedAt" timestamp with time zone not null, @@ -83,1509 +155,1613 @@ constraint "rolesPermissionsPermissions_permission_fk" on delete cascade on update cascade );`); -await queryInterface.sequelize.query( - 'CREATE INDEX IF NOT EXISTS "rolesPermissionsPermissions_permission_idx" ON "rolesPermissionsPermissions" ("permissionId");', -); + await queryInterface.sequelize.query( + 'CREATE INDEX IF NOT EXISTS "rolesPermissionsPermissions_permission_idx" ON "rolesPermissionsPermissions" ("permissionId");', + ); + await queryInterface.bulkInsert('rolesPermissionsPermissions', [ + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('CREATE_USERS'), + }, -await queryInterface.bulkInsert("rolesPermissionsPermissions", [ - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('CREATE_USERS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('READ_USERS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('UPDATE_USERS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('DELETE_USERS') }, - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('READ_USERS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('UPDATE_USERS') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('READ_USERS') }, - - - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("ContentReviewer"), permissionId: getId('READ_USERS') }, - - - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AnalyticsViewer"), permissionId: getId('READ_USERS') }, - - - - - - - - - - - - - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('CREATE_PROJECTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('READ_PROJECTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('UPDATE_PROJECTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('DELETE_PROJECTS') }, - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('CREATE_PROJECTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('READ_PROJECTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('UPDATE_PROJECTS') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('READ_PROJECTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('UPDATE_PROJECTS') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("ContentReviewer"), permissionId: getId('READ_PROJECTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("ContentReviewer"), permissionId: getId('UPDATE_PROJECTS') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AnalyticsViewer"), permissionId: getId('READ_PROJECTS') }, - - - - - - - - - - - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('CREATE_PROJECT_MEMBERSHIPS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('READ_PROJECT_MEMBERSHIPS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('UPDATE_PROJECT_MEMBERSHIPS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('DELETE_PROJECT_MEMBERSHIPS') }, - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('CREATE_PROJECT_MEMBERSHIPS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('READ_PROJECT_MEMBERSHIPS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('UPDATE_PROJECT_MEMBERSHIPS') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('READ_PROJECT_MEMBERSHIPS') }, - - - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("ContentReviewer"), permissionId: getId('READ_PROJECT_MEMBERSHIPS') }, - - - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AnalyticsViewer"), permissionId: getId('READ_PROJECT_MEMBERSHIPS') }, - - - - - - - - - - - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('CREATE_ASSETS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('READ_ASSETS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('UPDATE_ASSETS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('DELETE_ASSETS') }, - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('CREATE_ASSETS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('READ_ASSETS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('UPDATE_ASSETS') }, - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('CREATE_ASSETS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('READ_ASSETS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('UPDATE_ASSETS') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("ContentReviewer"), permissionId: getId('READ_ASSETS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("ContentReviewer"), permissionId: getId('UPDATE_ASSETS') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AnalyticsViewer"), permissionId: getId('READ_ASSETS') }, - - - - - - - - - - - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('CREATE_ASSET_VARIANTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('READ_ASSET_VARIANTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('UPDATE_ASSET_VARIANTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('DELETE_ASSET_VARIANTS') }, - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('READ_ASSET_VARIANTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('UPDATE_ASSET_VARIANTS') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('READ_ASSET_VARIANTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('UPDATE_ASSET_VARIANTS') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("ContentReviewer"), permissionId: getId('READ_ASSET_VARIANTS') }, - - - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AnalyticsViewer"), permissionId: getId('READ_ASSET_VARIANTS') }, - - - - - - - - - - - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('CREATE_PRESIGNED_URL_REQUESTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('READ_PRESIGNED_URL_REQUESTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('UPDATE_PRESIGNED_URL_REQUESTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('DELETE_PRESIGNED_URL_REQUESTS') }, - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('CREATE_PRESIGNED_URL_REQUESTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('READ_PRESIGNED_URL_REQUESTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('UPDATE_PRESIGNED_URL_REQUESTS') }, - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('CREATE_PRESIGNED_URL_REQUESTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('READ_PRESIGNED_URL_REQUESTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('UPDATE_PRESIGNED_URL_REQUESTS') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("ContentReviewer"), permissionId: getId('READ_PRESIGNED_URL_REQUESTS') }, - - - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AnalyticsViewer"), permissionId: getId('READ_PRESIGNED_URL_REQUESTS') }, - - - - - - - - - - - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('CREATE_TOUR_PAGES') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('READ_TOUR_PAGES') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('UPDATE_TOUR_PAGES') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('DELETE_TOUR_PAGES') }, - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('CREATE_TOUR_PAGES') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('READ_TOUR_PAGES') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('UPDATE_TOUR_PAGES') }, - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('CREATE_TOUR_PAGES') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('READ_TOUR_PAGES') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('UPDATE_TOUR_PAGES') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("ContentReviewer"), permissionId: getId('READ_TOUR_PAGES') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("ContentReviewer"), permissionId: getId('UPDATE_TOUR_PAGES') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AnalyticsViewer"), permissionId: getId('READ_TOUR_PAGES') }, - - - - - - - - - - - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('CREATE_PAGE_ELEMENTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('READ_PAGE_ELEMENTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('UPDATE_PAGE_ELEMENTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('DELETE_PAGE_ELEMENTS') }, - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('CREATE_PAGE_ELEMENTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('READ_PAGE_ELEMENTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('UPDATE_PAGE_ELEMENTS') }, - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('CREATE_PAGE_ELEMENTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('READ_PAGE_ELEMENTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('UPDATE_PAGE_ELEMENTS') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("ContentReviewer"), permissionId: getId('READ_PAGE_ELEMENTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("ContentReviewer"), permissionId: getId('UPDATE_PAGE_ELEMENTS') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AnalyticsViewer"), permissionId: getId('READ_PAGE_ELEMENTS') }, - - - - - - - - - - - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('CREATE_PAGE_LINKS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('READ_PAGE_LINKS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('UPDATE_PAGE_LINKS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('DELETE_PAGE_LINKS') }, - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('CREATE_PAGE_LINKS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('READ_PAGE_LINKS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('UPDATE_PAGE_LINKS') }, - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('CREATE_PAGE_LINKS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('READ_PAGE_LINKS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('UPDATE_PAGE_LINKS') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("ContentReviewer"), permissionId: getId('READ_PAGE_LINKS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("ContentReviewer"), permissionId: getId('UPDATE_PAGE_LINKS') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AnalyticsViewer"), permissionId: getId('READ_PAGE_LINKS') }, - - - - - - - - - - - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('CREATE_TRANSITIONS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('READ_TRANSITIONS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('UPDATE_TRANSITIONS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('DELETE_TRANSITIONS') }, - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('CREATE_TRANSITIONS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('READ_TRANSITIONS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('UPDATE_TRANSITIONS') }, - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('CREATE_TRANSITIONS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('READ_TRANSITIONS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('UPDATE_TRANSITIONS') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("ContentReviewer"), permissionId: getId('READ_TRANSITIONS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("ContentReviewer"), permissionId: getId('UPDATE_TRANSITIONS') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AnalyticsViewer"), permissionId: getId('READ_TRANSITIONS') }, - - - - - - - - - - - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('CREATE_PROJECT_AUDIO_TRACKS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('READ_PROJECT_AUDIO_TRACKS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('UPDATE_PROJECT_AUDIO_TRACKS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('DELETE_PROJECT_AUDIO_TRACKS') }, - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('CREATE_PROJECT_AUDIO_TRACKS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('READ_PROJECT_AUDIO_TRACKS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('UPDATE_PROJECT_AUDIO_TRACKS') }, - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('CREATE_PROJECT_AUDIO_TRACKS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('READ_PROJECT_AUDIO_TRACKS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('UPDATE_PROJECT_AUDIO_TRACKS') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("ContentReviewer"), permissionId: getId('READ_PROJECT_AUDIO_TRACKS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("ContentReviewer"), permissionId: getId('UPDATE_PROJECT_AUDIO_TRACKS') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AnalyticsViewer"), permissionId: getId('READ_PROJECT_AUDIO_TRACKS') }, - - - - - - - - - - - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('CREATE_PUBLISH_EVENTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('READ_PUBLISH_EVENTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('UPDATE_PUBLISH_EVENTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('DELETE_PUBLISH_EVENTS') }, - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('CREATE_PUBLISH_EVENTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('READ_PUBLISH_EVENTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('UPDATE_PUBLISH_EVENTS') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('READ_PUBLISH_EVENTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('UPDATE_PUBLISH_EVENTS') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("ContentReviewer"), permissionId: getId('READ_PUBLISH_EVENTS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("ContentReviewer"), permissionId: getId('UPDATE_PUBLISH_EVENTS') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AnalyticsViewer"), permissionId: getId('READ_PUBLISH_EVENTS') }, - - - - - - - - - - - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('CREATE_PWA_CACHES') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('READ_PWA_CACHES') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('UPDATE_PWA_CACHES') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('DELETE_PWA_CACHES') }, - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('CREATE_PWA_CACHES') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('READ_PWA_CACHES') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('UPDATE_PWA_CACHES') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('READ_PWA_CACHES') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('UPDATE_PWA_CACHES') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("ContentReviewer"), permissionId: getId('READ_PWA_CACHES') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("ContentReviewer"), permissionId: getId('UPDATE_PWA_CACHES') }, - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AnalyticsViewer"), permissionId: getId('READ_PWA_CACHES') }, - - - - - - - - - - - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('CREATE_ACCESS_LOGS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('READ_ACCESS_LOGS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('UPDATE_ACCESS_LOGS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('DELETE_ACCESS_LOGS') }, - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('READ_ACCESS_LOGS') }, - - - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('READ_ACCESS_LOGS') }, - - - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("ContentReviewer"), permissionId: getId('READ_ACCESS_LOGS') }, - - - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("AnalyticsViewer"), permissionId: getId('READ_ACCESS_LOGS') }, - - - - - - - - - - - - - - - - - - { createdAt, updatedAt, roles_permissionsId: getId("PlatformOwner"), permissionId: getId('CREATE_SEARCH') }, - - { createdAt, updatedAt, roles_permissionsId: getId("AccountManager"), permissionId: getId('CREATE_SEARCH') }, - - { createdAt, updatedAt, roles_permissionsId: getId("TourDesigner"), permissionId: getId('CREATE_SEARCH') }, - - { createdAt, updatedAt, roles_permissionsId: getId("ContentReviewer"), permissionId: getId('CREATE_SEARCH') }, - - { createdAt, updatedAt, roles_permissionsId: getId("AnalyticsViewer"), permissionId: getId('CREATE_SEARCH') }, - - - - - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('CREATE_USERS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('READ_USERS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('UPDATE_USERS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('DELETE_USERS') }, - - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('CREATE_ROLES') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('READ_ROLES') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('UPDATE_ROLES') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('DELETE_ROLES') }, - - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('CREATE_PERMISSIONS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('READ_PERMISSIONS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('UPDATE_PERMISSIONS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('DELETE_PERMISSIONS') }, - - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('CREATE_PROJECTS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('READ_PROJECTS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('UPDATE_PROJECTS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('DELETE_PROJECTS') }, - - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('CREATE_PROJECT_MEMBERSHIPS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('READ_PROJECT_MEMBERSHIPS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('UPDATE_PROJECT_MEMBERSHIPS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('DELETE_PROJECT_MEMBERSHIPS') }, - - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('CREATE_ASSETS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('READ_ASSETS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('UPDATE_ASSETS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('DELETE_ASSETS') }, - - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('CREATE_ASSET_VARIANTS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('READ_ASSET_VARIANTS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('UPDATE_ASSET_VARIANTS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('DELETE_ASSET_VARIANTS') }, - - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('CREATE_PRESIGNED_URL_REQUESTS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('READ_PRESIGNED_URL_REQUESTS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('UPDATE_PRESIGNED_URL_REQUESTS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('DELETE_PRESIGNED_URL_REQUESTS') }, - - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('CREATE_TOUR_PAGES') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('READ_TOUR_PAGES') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('UPDATE_TOUR_PAGES') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('DELETE_TOUR_PAGES') }, - - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('CREATE_PAGE_ELEMENTS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('READ_PAGE_ELEMENTS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('UPDATE_PAGE_ELEMENTS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('DELETE_PAGE_ELEMENTS') }, - - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('CREATE_PAGE_LINKS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('READ_PAGE_LINKS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('UPDATE_PAGE_LINKS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('DELETE_PAGE_LINKS') }, - - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('CREATE_TRANSITIONS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('READ_TRANSITIONS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('UPDATE_TRANSITIONS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('DELETE_TRANSITIONS') }, - - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('CREATE_PROJECT_AUDIO_TRACKS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('READ_PROJECT_AUDIO_TRACKS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('UPDATE_PROJECT_AUDIO_TRACKS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('DELETE_PROJECT_AUDIO_TRACKS') }, - - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('CREATE_PUBLISH_EVENTS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('READ_PUBLISH_EVENTS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('UPDATE_PUBLISH_EVENTS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('DELETE_PUBLISH_EVENTS') }, - - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('CREATE_PWA_CACHES') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('READ_PWA_CACHES') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('UPDATE_PWA_CACHES') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('DELETE_PWA_CACHES') }, - - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('CREATE_ACCESS_LOGS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('READ_ACCESS_LOGS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('UPDATE_ACCESS_LOGS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('DELETE_ACCESS_LOGS') }, - - - - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('READ_API_DOCS') }, - { createdAt, updatedAt, roles_permissionsId: getId("Administrator"), permissionId: getId('CREATE_SEARCH') }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('READ_USERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('UPDATE_USERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('DELETE_USERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('READ_USERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('UPDATE_USERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('READ_USERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentReviewer'), + permissionId: getId('READ_USERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AnalyticsViewer'), + permissionId: getId('READ_USERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('CREATE_PROJECTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('READ_PROJECTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('UPDATE_PROJECTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('DELETE_PROJECTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('CREATE_PROJECTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('READ_PROJECTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('UPDATE_PROJECTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('READ_PROJECTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('UPDATE_PROJECTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentReviewer'), + permissionId: getId('READ_PROJECTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentReviewer'), + permissionId: getId('UPDATE_PROJECTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AnalyticsViewer'), + permissionId: getId('READ_PROJECTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('CREATE_PROJECT_MEMBERSHIPS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('READ_PROJECT_MEMBERSHIPS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('UPDATE_PROJECT_MEMBERSHIPS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('DELETE_PROJECT_MEMBERSHIPS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('CREATE_PROJECT_MEMBERSHIPS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('READ_PROJECT_MEMBERSHIPS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('UPDATE_PROJECT_MEMBERSHIPS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('READ_PROJECT_MEMBERSHIPS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentReviewer'), + permissionId: getId('READ_PROJECT_MEMBERSHIPS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AnalyticsViewer'), + permissionId: getId('READ_PROJECT_MEMBERSHIPS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('CREATE_ASSETS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('READ_ASSETS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('UPDATE_ASSETS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('DELETE_ASSETS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('CREATE_ASSETS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('READ_ASSETS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('UPDATE_ASSETS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('CREATE_ASSETS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('READ_ASSETS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('UPDATE_ASSETS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentReviewer'), + permissionId: getId('READ_ASSETS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentReviewer'), + permissionId: getId('UPDATE_ASSETS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AnalyticsViewer'), + permissionId: getId('READ_ASSETS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('CREATE_ASSET_VARIANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('READ_ASSET_VARIANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('UPDATE_ASSET_VARIANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('DELETE_ASSET_VARIANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('READ_ASSET_VARIANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('UPDATE_ASSET_VARIANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('READ_ASSET_VARIANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('UPDATE_ASSET_VARIANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentReviewer'), + permissionId: getId('READ_ASSET_VARIANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AnalyticsViewer'), + permissionId: getId('READ_ASSET_VARIANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('CREATE_PRESIGNED_URL_REQUESTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('READ_PRESIGNED_URL_REQUESTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('UPDATE_PRESIGNED_URL_REQUESTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('DELETE_PRESIGNED_URL_REQUESTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('CREATE_PRESIGNED_URL_REQUESTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('READ_PRESIGNED_URL_REQUESTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('UPDATE_PRESIGNED_URL_REQUESTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('CREATE_PRESIGNED_URL_REQUESTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('READ_PRESIGNED_URL_REQUESTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('UPDATE_PRESIGNED_URL_REQUESTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentReviewer'), + permissionId: getId('READ_PRESIGNED_URL_REQUESTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AnalyticsViewer'), + permissionId: getId('READ_PRESIGNED_URL_REQUESTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('CREATE_TOUR_PAGES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('READ_TOUR_PAGES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('UPDATE_TOUR_PAGES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('DELETE_TOUR_PAGES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('CREATE_TOUR_PAGES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('READ_TOUR_PAGES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('UPDATE_TOUR_PAGES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('CREATE_TOUR_PAGES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('READ_TOUR_PAGES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('UPDATE_TOUR_PAGES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentReviewer'), + permissionId: getId('READ_TOUR_PAGES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentReviewer'), + permissionId: getId('UPDATE_TOUR_PAGES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AnalyticsViewer'), + permissionId: getId('READ_TOUR_PAGES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('CREATE_PAGE_ELEMENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('READ_PAGE_ELEMENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('UPDATE_PAGE_ELEMENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('DELETE_PAGE_ELEMENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('CREATE_PAGE_ELEMENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('READ_PAGE_ELEMENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('UPDATE_PAGE_ELEMENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('CREATE_PAGE_ELEMENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('READ_PAGE_ELEMENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('UPDATE_PAGE_ELEMENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentReviewer'), + permissionId: getId('READ_PAGE_ELEMENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentReviewer'), + permissionId: getId('UPDATE_PAGE_ELEMENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AnalyticsViewer'), + permissionId: getId('READ_PAGE_ELEMENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('CREATE_PAGE_LINKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('READ_PAGE_LINKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('UPDATE_PAGE_LINKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('DELETE_PAGE_LINKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('CREATE_PAGE_LINKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('READ_PAGE_LINKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('UPDATE_PAGE_LINKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('CREATE_PAGE_LINKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('READ_PAGE_LINKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('UPDATE_PAGE_LINKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentReviewer'), + permissionId: getId('READ_PAGE_LINKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentReviewer'), + permissionId: getId('UPDATE_PAGE_LINKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AnalyticsViewer'), + permissionId: getId('READ_PAGE_LINKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('CREATE_TRANSITIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('READ_TRANSITIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('UPDATE_TRANSITIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('DELETE_TRANSITIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('CREATE_TRANSITIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('READ_TRANSITIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('UPDATE_TRANSITIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('CREATE_TRANSITIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('READ_TRANSITIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('UPDATE_TRANSITIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentReviewer'), + permissionId: getId('READ_TRANSITIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentReviewer'), + permissionId: getId('UPDATE_TRANSITIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AnalyticsViewer'), + permissionId: getId('READ_TRANSITIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('CREATE_PROJECT_AUDIO_TRACKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('READ_PROJECT_AUDIO_TRACKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('UPDATE_PROJECT_AUDIO_TRACKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('DELETE_PROJECT_AUDIO_TRACKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('CREATE_PROJECT_AUDIO_TRACKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('READ_PROJECT_AUDIO_TRACKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('UPDATE_PROJECT_AUDIO_TRACKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('CREATE_PROJECT_AUDIO_TRACKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('READ_PROJECT_AUDIO_TRACKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('UPDATE_PROJECT_AUDIO_TRACKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentReviewer'), + permissionId: getId('READ_PROJECT_AUDIO_TRACKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentReviewer'), + permissionId: getId('UPDATE_PROJECT_AUDIO_TRACKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AnalyticsViewer'), + permissionId: getId('READ_PROJECT_AUDIO_TRACKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('CREATE_PUBLISH_EVENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('READ_PUBLISH_EVENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('UPDATE_PUBLISH_EVENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('DELETE_PUBLISH_EVENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('CREATE_PUBLISH_EVENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('READ_PUBLISH_EVENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('UPDATE_PUBLISH_EVENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('READ_PUBLISH_EVENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('UPDATE_PUBLISH_EVENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentReviewer'), + permissionId: getId('READ_PUBLISH_EVENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentReviewer'), + permissionId: getId('UPDATE_PUBLISH_EVENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AnalyticsViewer'), + permissionId: getId('READ_PUBLISH_EVENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('CREATE_PWA_CACHES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('READ_PWA_CACHES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('UPDATE_PWA_CACHES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('DELETE_PWA_CACHES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('CREATE_PWA_CACHES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('READ_PWA_CACHES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('UPDATE_PWA_CACHES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('READ_PWA_CACHES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('UPDATE_PWA_CACHES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentReviewer'), + permissionId: getId('READ_PWA_CACHES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentReviewer'), + permissionId: getId('UPDATE_PWA_CACHES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AnalyticsViewer'), + permissionId: getId('READ_PWA_CACHES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('CREATE_ACCESS_LOGS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('READ_ACCESS_LOGS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('UPDATE_ACCESS_LOGS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('DELETE_ACCESS_LOGS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('READ_ACCESS_LOGS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('READ_ACCESS_LOGS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentReviewer'), + permissionId: getId('READ_ACCESS_LOGS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AnalyticsViewer'), + permissionId: getId('READ_ACCESS_LOGS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('PlatformOwner'), + permissionId: getId('CREATE_SEARCH'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AccountManager'), + permissionId: getId('CREATE_SEARCH'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('TourDesigner'), + permissionId: getId('CREATE_SEARCH'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('ContentReviewer'), + permissionId: getId('CREATE_SEARCH'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('AnalyticsViewer'), + permissionId: getId('CREATE_SEARCH'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_USERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_USERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_USERS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_USERS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_ROLES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_ROLES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_ROLES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_ROLES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_PERMISSIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_PERMISSIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_PERMISSIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_PERMISSIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_PROJECTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_PROJECTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_PROJECTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_PROJECTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_PROJECT_MEMBERSHIPS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_PROJECT_MEMBERSHIPS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_PROJECT_MEMBERSHIPS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_PROJECT_MEMBERSHIPS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_ASSETS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_ASSETS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_ASSETS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_ASSETS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_ASSET_VARIANTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_ASSET_VARIANTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_ASSET_VARIANTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_ASSET_VARIANTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_PRESIGNED_URL_REQUESTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_PRESIGNED_URL_REQUESTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_PRESIGNED_URL_REQUESTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_PRESIGNED_URL_REQUESTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_TOUR_PAGES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_TOUR_PAGES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_TOUR_PAGES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_TOUR_PAGES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_PAGE_ELEMENTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_PAGE_ELEMENTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_PAGE_ELEMENTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_PAGE_ELEMENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_PAGE_LINKS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_PAGE_LINKS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_PAGE_LINKS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_PAGE_LINKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_TRANSITIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_TRANSITIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_TRANSITIONS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_TRANSITIONS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_PROJECT_AUDIO_TRACKS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_PROJECT_AUDIO_TRACKS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_PROJECT_AUDIO_TRACKS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_PROJECT_AUDIO_TRACKS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_PUBLISH_EVENTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_PUBLISH_EVENTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_PUBLISH_EVENTS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_PUBLISH_EVENTS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_PWA_CACHES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_PWA_CACHES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_PWA_CACHES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_PWA_CACHES'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_ACCESS_LOGS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_ACCESS_LOGS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_ACCESS_LOGS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_ACCESS_LOGS'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_API_DOCS'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_SEARCH'), + }, ]); + await queryInterface.sequelize.query( + `UPDATE "users" SET "app_roleId"='${getId('SuperAdmin')}' WHERE "email"='super_admin@flatlogic.com'`, + ); + await queryInterface.sequelize.query( + `UPDATE "users" SET "app_roleId"='${getId('Administrator')}' WHERE "email"='admin@flatlogic.com'`, + ); - await queryInterface.sequelize.query(`UPDATE "users" SET "app_roleId"='${getId("SuperAdmin")}' WHERE "email"='super_admin@flatlogic.com'`); - await queryInterface.sequelize.query(`UPDATE "users" SET "app_roleId"='${getId("Administrator")}' WHERE "email"='admin@flatlogic.com'`); - - - - - - - await queryInterface.sequelize.query(`UPDATE "users" SET "app_roleId"='${getId("PlatformOwner")}' WHERE "email"='client@hello.com'`); - await queryInterface.sequelize.query(`UPDATE "users" SET "app_roleId"='${getId("AccountManager")}' WHERE "email"='john@doe.com'`); - - - - -} + await queryInterface.sequelize.query( + `UPDATE "users" SET "app_roleId"='${getId('PlatformOwner')}' WHERE "email"='client@hello.com'`, + ); + await queryInterface.sequelize.query( + `UPDATE "users" SET "app_roleId"='${getId('AccountManager')}' WHERE "email"='john@doe.com'`, + ); + }, }; diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js index 7213266..d946de9 100644 --- a/backend/src/db/seeders/20231127130745-sample-data.js +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -1,33 +1,6 @@ - - - - - - - - - - - - - - - - - - - - - - const db = require('../models'); const Users = db.users; - - - - - const Projects = db.projects; const ProjectMemberships = db.project_memberships; @@ -48,3576 +21,1194 @@ const PwaCaches = db.pwa_caches; const AccessLogs = db.access_logs; - - - - - - const ProjectsData = [ - - { - - - - - "name": "Cardiff Arena Tour", - - - - - - - "slug": "cardiff-arena", - - - - - - - "description": "Interactive arena tour for visitors and event planners.", - - - + { + name: 'Cardiff Arena Tour', + slug: 'cardiff-arena', + description: 'Interactive arena tour for visitors and event planners.', - "logo_url": "https://cdn.platform.com/cardiff/logo.png", - - - - - - - "favicon_url": "https://cdn.platform.com/cardiff/favicon.ico", - - - - - - - "og_image_url": "https://cdn.platform.com/cardiff/og.jpg", - - - - - - - "theme_config_json": "{colors:{primary:#0EA5E9,background:#0B1220},fonts:{base:Inter}}", - - - - - - - "custom_css_json": "{buttons:{radius:14px}}", - - - - - - - "cdn_base_url": "https://cdn.platform.com/cardiff", - - - - - - - "is_deleted": true, - - - - - - - "deleted_at_time": new Date('2026-01-01T00:00:00Z'), - - - - }, - - { - - - - - "name": "Riverside Park Walkthrough", - - - - - - - "slug": "riverside-park", - - - - - - - "description": "Offline-ready guided walkthrough for the city park.", - - - + logo_url: 'https://cdn.platform.com/cardiff/logo.png', + favicon_url: 'https://cdn.platform.com/cardiff/favicon.ico', + og_image_url: 'https://cdn.platform.com/cardiff/og.jpg', - "logo_url": "https://cdn.platform.com/riverside/logo.png", - - - - - - - "favicon_url": "https://cdn.platform.com/riverside/favicon.ico", - - - - - - - "og_image_url": "https://cdn.platform.com/riverside/og.jpg", - - - - - - - "theme_config_json": "{colors:{primary:#22C55E,background:#FFFFFF},fonts:{base:Manrope}}", - - - - - - - "custom_css_json": "{header:{shadow:md}}", - - - - - - - "cdn_base_url": "https://cdn.platform.com/riverside", - - - - - - - "is_deleted": false, - - - - - - - "deleted_at_time": new Date('2026-01-01T00:00:00Z'), - - - - }, - - { - - - - - "name": "Mall Central Experience", - - - - - - - "slug": "mall-central", - - - - - - - "description": "Retail complex presentation with navigation and galleries.", - - - + theme_config_json: + '{colors:{primary:#0EA5E9,background:#0B1220},fonts:{base:Inter}}', + custom_css_json: '{buttons:{radius:14px}}', - "logo_url": "https://cdn.platform.com/mall/logo.png", - - - - - - - "favicon_url": "https://cdn.platform.com/mall/favicon.ico", - - - - - - - "og_image_url": "https://cdn.platform.com/mall/og.jpg", - - - - - - - "theme_config_json": "{colors:{primary:#A855F7,background:#0F172A},fonts:{base:Poppins}}", - - - - - - - "custom_css_json": "{cards:{border:1px solid rgba(255,255,255,0.12)}}", - - - - - - - "cdn_base_url": "https://cdn.platform.com/mall", - - - - - - - - "is_deleted": false, - - - - - - - "deleted_at_time": new Date('2026-01-01T00:00:00Z'), - - - - }, - + cdn_base_url: 'https://cdn.platform.com/cardiff', + + is_deleted: true, + + deleted_at_time: new Date('2026-01-01T00:00:00Z'), + }, + + { + name: 'Riverside Park Walkthrough', + + slug: 'riverside-park', + + description: 'Offline-ready guided walkthrough for the city park.', + + logo_url: 'https://cdn.platform.com/riverside/logo.png', + + favicon_url: 'https://cdn.platform.com/riverside/favicon.ico', + + og_image_url: 'https://cdn.platform.com/riverside/og.jpg', + + theme_config_json: + '{colors:{primary:#22C55E,background:#FFFFFF},fonts:{base:Manrope}}', + + custom_css_json: '{header:{shadow:md}}', + + cdn_base_url: 'https://cdn.platform.com/riverside', + + is_deleted: false, + + deleted_at_time: new Date('2026-01-01T00:00:00Z'), + }, + + { + name: 'Mall Central Experience', + + slug: 'mall-central', + + description: 'Retail complex presentation with navigation and galleries.', + + logo_url: 'https://cdn.platform.com/mall/logo.png', + + favicon_url: 'https://cdn.platform.com/mall/favicon.ico', + + og_image_url: 'https://cdn.platform.com/mall/og.jpg', + + theme_config_json: + '{colors:{primary:#A855F7,background:#0F172A},fonts:{base:Poppins}}', + + custom_css_json: '{cards:{border:1px solid rgba(255,255,255,0.12)}}', + + cdn_base_url: 'https://cdn.platform.com/mall', + + is_deleted: false, + + deleted_at_time: new Date('2026-01-01T00:00:00Z'), + }, ]; - - const ProjectMembershipsData = [ - - { - - - - - // type code here for "relation_one" field - - - - - - - // type code here for "relation_one" field - - - - - - - "access_level": "editor", - - - - - - - "is_active": true, - - - - - - - "invited_at": new Date('2026-02-01T10:00:00Z'), - - - - - - - "accepted_at": new Date('2026-02-01T10:05:00Z'), - - - - }, - - { - - - - - // type code here for "relation_one" field - - - - - - - // type code here for "relation_one" field - - - - - - - "access_level": "reviewer", - - - - - - - "is_active": true, - - - - - - - "invited_at": new Date('2026-02-02T09:00:00Z'), - - - - - - - "accepted_at": new Date('2026-02-02T09:10:00Z'), - - - - }, - - { - - - - - // type code here for "relation_one" field - - - - - - - // type code here for "relation_one" field - - - - - - - "access_level": "viewer", - - - - - - - "is_active": true, - - - - - - - "invited_at": new Date('2026-02-10T14:00:00Z'), - - - - - - - "accepted_at": new Date('2026-02-10T14:30:00Z'), - - - - }, - + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + access_level: 'editor', + + is_active: true, + + invited_at: new Date('2026-02-01T10:00:00Z'), + + accepted_at: new Date('2026-02-01T10:05:00Z'), + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + access_level: 'reviewer', + + is_active: true, + + invited_at: new Date('2026-02-02T09:00:00Z'), + + accepted_at: new Date('2026-02-02T09:10:00Z'), + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + access_level: 'viewer', + + is_active: true, + + invited_at: new Date('2026-02-10T14:00:00Z'), + + accepted_at: new Date('2026-02-10T14:30:00Z'), + }, ]; - - const AssetsData = [ - - { - - - - - // type code here for "relation_one" field - - - - - - - "name": "Arena Lobby Background", - - - - - - - "asset_type": "image", - - - - - - - "cdn_url": "https://cdn.platform.com/cardiff/images/lobby.webp", - - - - - - - "storage_key": "cardiff/images/lobby.webp", - - - - - - - "mime_type": "image/webp", - - - - - - - "size_mb": 2.45, - - - - - - - "width_px": 3840, - - - - - - - "height_px": 2160, - - - - - - - "duration_sec": 0.0, - - - - - - - "checksum": "sha256_lobby_001", - - - - - - - "is_public": true, - - - - - - - "is_deleted": false, - - - - - - - "deleted_at_time": new Date('2026-01-01T00:00:00Z'), - - - - }, - - { - - - - - // type code here for "relation_one" field - - - - - - - "name": "Hallway Transition Video", - - - - - - - "asset_type": "video", - - - - - - - "cdn_url": "https://cdn.platform.com/cardiff/video/transition-hallway.mp4", - - - - - - - "storage_key": "cardiff/video/transition-hallway.mp4", - - - - - - - "mime_type": "video/mp4", - - - - - - - "size_mb": 18.2, - - - - - - - "width_px": 1920, - - - - - - - "height_px": 1080, - - - - - - - "duration_sec": 4.8, - - - - - - - "checksum": "sha256_vid_002", - - - - - - - "is_public": true, - - - - - - - "is_deleted": true, - - - - - - - "deleted_at_time": new Date('2026-01-01T00:00:00Z'), - - - - }, - - { - - - - - // type code here for "relation_one" field - - - - - - - "name": "Park Ambience Loop", - - - - - - - "asset_type": "video", - - - - - - - "cdn_url": "https://cdn.platform.com/riverside/audio/ambience.mp3", - - - - - - - "storage_key": "riverside/audio/ambience.mp3", - - - - - - - "mime_type": "audio/mpeg", - - - - - - - "size_mb": 5.6, - - - - - - - "width_px": 0, - - - - - - - "height_px": 0, - - - - - - - "duration_sec": 120.0, - - - - - - - "checksum": "sha256_aud_003", - - - - - - - "is_public": true, - - - - - - - "is_deleted": true, - - - - - - - "deleted_at_time": new Date('2026-01-01T00:00:00Z'), - - - - }, - + { + // type code here for "relation_one" field + + name: 'Arena Lobby Background', + + asset_type: 'image', + + cdn_url: 'https://cdn.platform.com/cardiff/images/lobby.webp', + + storage_key: 'cardiff/images/lobby.webp', + + mime_type: 'image/webp', + + size_mb: 2.45, + + width_px: 3840, + + height_px: 2160, + + duration_sec: 0.0, + + checksum: 'sha256_lobby_001', + + is_public: true, + + is_deleted: false, + + deleted_at_time: new Date('2026-01-01T00:00:00Z'), + }, + + { + // type code here for "relation_one" field + + name: 'Hallway Transition Video', + + asset_type: 'video', + + cdn_url: 'https://cdn.platform.com/cardiff/video/transition-hallway.mp4', + + storage_key: 'cardiff/video/transition-hallway.mp4', + + mime_type: 'video/mp4', + + size_mb: 18.2, + + width_px: 1920, + + height_px: 1080, + + duration_sec: 4.8, + + checksum: 'sha256_vid_002', + + is_public: true, + + is_deleted: true, + + deleted_at_time: new Date('2026-01-01T00:00:00Z'), + }, + + { + // type code here for "relation_one" field + + name: 'Park Ambience Loop', + + asset_type: 'video', + + cdn_url: 'https://cdn.platform.com/riverside/audio/ambience.mp3', + + storage_key: 'riverside/audio/ambience.mp3', + + mime_type: 'audio/mpeg', + + size_mb: 5.6, + + width_px: 0, + + height_px: 0, + + duration_sec: 120.0, + + checksum: 'sha256_aud_003', + + is_public: true, + + is_deleted: true, + + deleted_at_time: new Date('2026-01-01T00:00:00Z'), + }, ]; - - const AssetVariantsData = [ - - { - - - - - // type code here for "relation_one" field - - - - - - - "variant_type": "mp4_low", - - - - - - - "cdn_url": "https://cdn.platform.com/cardiff/images/lobby_thumb.webp", - - - - - - - "width_px": 640, - - - - - - - "height_px": 360, - - - - - - - "size_mb": 0.12, - - - - }, - - { - - - - - // type code here for "relation_one" field - - - - - - - "variant_type": "preview", - - - - - - - "cdn_url": "https://cdn.platform.com/cardiff/images/lobby_preview.webp", - - - - - - - "width_px": 1280, - - - - - - - "height_px": 720, - - - - - - - "size_mb": 0.35, - - - - }, - - { - - - - - // type code here for "relation_one" field - - - - - - - "variant_type": "preview", - - - - - - - "cdn_url": "https://cdn.platform.com/cardiff/video/transition-hallway_720p.mp4", - - - - - - - "width_px": 1280, - - - - - - - "height_px": 720, - - - - - - - "size_mb": 9.4, - - - - }, - + { + // type code here for "relation_one" field + + variant_type: 'mp4_low', + + cdn_url: 'https://cdn.platform.com/cardiff/images/lobby_thumb.webp', + + width_px: 640, + + height_px: 360, + + size_mb: 0.12, + }, + + { + // type code here for "relation_one" field + + variant_type: 'preview', + + cdn_url: 'https://cdn.platform.com/cardiff/images/lobby_preview.webp', + + width_px: 1280, + + height_px: 720, + + size_mb: 0.35, + }, + + { + // type code here for "relation_one" field + + variant_type: 'preview', + + cdn_url: + 'https://cdn.platform.com/cardiff/video/transition-hallway_720p.mp4', + + width_px: 1280, + + height_px: 720, + + size_mb: 9.4, + }, ]; - - const PresignedUrlRequestsData = [ - - { - - - - - // type code here for "relation_one" field - - - - - - - // type code here for "relation_one" field - - - - - - - "purpose": "download", - - - - - - - "asset_type": "video", - - - - - - - "requested_key": "cardiff/images/section-a.webp", - - - - - - - "mime_type": "image/webp", - - - - - - - "requested_size_mb": 3.2, - - - - - - - "expires_at": new Date('2026-03-16T12:30:00Z'), - - - - - - - "status": "issued", - - - - }, - - { - - - - - // type code here for "relation_one" field - - - - - - - // type code here for "relation_one" field - - - - - - - "purpose": "upload", - - - - - - - "asset_type": "video", - - - - - - - "requested_key": "cardiff/video/transition-gate.mp4", - - - - - - - "mime_type": "video/mp4", - - - - - - - "requested_size_mb": 28.0, - - - - - - - "expires_at": new Date('2026-03-16T12:35:00Z'), - - - - - - - "status": "issued", - - - - }, - - { - - - - - // type code here for "relation_one" field - - - - - - - // type code here for "relation_one" field - - - - - - - "purpose": "upload", - - - - - - - "asset_type": "audio", - - - - - - - "requested_key": "riverside/audio/ambience.mp3", - - - - - - - "mime_type": "audio/mpeg", - - - - - - - "requested_size_mb": 6.0, - - - - - - - "expires_at": new Date('2026-03-16T09:10:00Z'), - - - - - - - "status": "issued", - - - - }, - + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + purpose: 'download', + + asset_type: 'video', + + requested_key: 'cardiff/images/section-a.webp', + + mime_type: 'image/webp', + + requested_size_mb: 3.2, + + expires_at: new Date('2026-03-16T12:30:00Z'), + + status: 'issued', + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + purpose: 'upload', + + asset_type: 'video', + + requested_key: 'cardiff/video/transition-gate.mp4', + + mime_type: 'video/mp4', + + requested_size_mb: 28.0, + + expires_at: new Date('2026-03-16T12:35:00Z'), + + status: 'issued', + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + purpose: 'upload', + + asset_type: 'audio', + + requested_key: 'riverside/audio/ambience.mp3', + + mime_type: 'audio/mpeg', + + requested_size_mb: 6.0, + + expires_at: new Date('2026-03-16T09:10:00Z'), + + status: 'issued', + }, ]; - - const TourPagesData = [ - - { - - - - - // type code here for "relation_one" field - - - - - - - "environment": "stage", - - - - - - - "source_key": "cardiff_welcome_stage", - - - - - - - "name": "Welcome", - - - - - - - "slug": "welcome", - - - - - - - "sort_order": 1, - - - - - - - "background_image_url": "https://cdn.platform.com/cardiff/images/lobby.webp", - - - - - - - "background_video_url": "", - - - - - - - "background_audio_url": "", - - - - - - - "background_loop": true, - - - - - - - "requires_auth": true, - - - - - - - "ui_schema_json": "{menu:{enabled:false},grid:responsive}", - - - - }, - - { - - - - - // type code here for "relation_one" field - - - - - - - "environment": "stage", - - - - - - - "source_key": "cardiff_arena_floor_stage", - - - - - - - "name": "Arena Floor", - - - - - - - "slug": "arena-floor", - - - - - - - "sort_order": 2, - - - - - - - "background_image_url": "", - - - - - - - "background_video_url": "https://cdn.platform.com/cardiff/video/floor-loop.mp4", - - - - - - - "background_audio_url": "", - - - - - - - "background_loop": true, - - - - - - - "requires_auth": true, - - - - - - - "ui_schema_json": "{safeArea:true}", - - - - }, - - { - - - - - // type code here for "relation_one" field - - - - - - - "environment": "stage", - - - - - - - "source_key": "riverside_start_prod", - - - - - - - "name": "Start", - - - - - - - "slug": "start", - - - - - - - "sort_order": 1, - - - - - - - "background_image_url": "https://cdn.platform.com/riverside/images/start.webp", - - - - - - - "background_video_url": "", - - - - - - - "background_audio_url": "", - - - - - - - "background_loop": true, - - - - - - - "requires_auth": true, - - - - - - - "ui_schema_json": "{hint:Swipe to explore}", - - - - }, - + { + // type code here for "relation_one" field + + environment: 'stage', + + source_key: 'cardiff_welcome_stage', + + name: 'Welcome', + + slug: 'welcome', + + sort_order: 1, + + background_image_url: 'https://cdn.platform.com/cardiff/images/lobby.webp', + + background_video_url: '', + + background_audio_url: '', + + background_loop: true, + + requires_auth: true, + + ui_schema_json: '{menu:{enabled:false},grid:responsive}', + }, + + { + // type code here for "relation_one" field + + environment: 'stage', + + source_key: 'cardiff_arena_floor_stage', + + name: 'Arena Floor', + + slug: 'arena-floor', + + sort_order: 2, + + background_image_url: '', + + background_video_url: + 'https://cdn.platform.com/cardiff/video/floor-loop.mp4', + + background_audio_url: '', + + background_loop: true, + + requires_auth: true, + + ui_schema_json: '{safeArea:true}', + }, + + { + // type code here for "relation_one" field + + environment: 'stage', + + source_key: 'riverside_start_prod', + + name: 'Start', + + slug: 'start', + + sort_order: 1, + + background_image_url: + 'https://cdn.platform.com/riverside/images/start.webp', + + background_video_url: '', + + background_audio_url: '', + + background_loop: true, + + requires_auth: true, + + ui_schema_json: '{hint:Swipe to explore}', + }, ]; const ProjectAudioTracksData = [ - - { - - - - - // type code here for "relation_one" field - - - - - - - "environment": "dev", - - - - - - - "source_key": "cardiff_global_stage", - - - - - - - "name": "Arena Ambient", - - - - - - - "slug": "arena-ambient", - - - - - - - "url": "https://cdn.platform.com/cardiff/audio/ambient.mp3", - - - - - - - "loop": true, - - - - - - - "volume": 0.6, - - - - - - - "sort_order": 1, - - - - - - - "is_enabled": true, - - - - }, - - { - - - - - // type code here for "relation_one" field - - - - - - - "environment": "production", - - - - - - - "source_key": "riverside_global_prod", - - - - - - - "name": "Park Ambience", - - - - - - - "slug": "park-ambience", - - - - - - - "url": "https://cdn.platform.com/riverside/audio/ambience.mp3", - - - - - - - "loop": true, - - - - - - - "volume": 0.7, - - - - - - - "sort_order": 1, - - - - - - - "is_enabled": true, - - - - }, - - { - - - - - // type code here for "relation_one" field - - - - - - - "environment": "dev", - - - - - - - "source_key": "mall_global_dev", - - - - - - - "name": "Soft Lounge", - - - - - - - "slug": "soft-lounge", - - - - - - - "url": "https://cdn.platform.com/mall/audio/lounge.mp3", - - - - - - - "loop": true, - - - - - - - "volume": 0.5, - - - - - - - "sort_order": 1, - - - - - - - "is_enabled": false, - - - - }, - + { + // type code here for "relation_one" field + + environment: 'dev', + + source_key: 'cardiff_global_stage', + + name: 'Arena Ambient', + + slug: 'arena-ambient', + + url: 'https://cdn.platform.com/cardiff/audio/ambient.mp3', + + loop: true, + + volume: 0.6, + + sort_order: 1, + + is_enabled: true, + }, + + { + // type code here for "relation_one" field + + environment: 'production', + + source_key: 'riverside_global_prod', + + name: 'Park Ambience', + + slug: 'park-ambience', + + url: 'https://cdn.platform.com/riverside/audio/ambience.mp3', + + loop: true, + + volume: 0.7, + + sort_order: 1, + + is_enabled: true, + }, + + { + // type code here for "relation_one" field + + environment: 'dev', + + source_key: 'mall_global_dev', + + name: 'Soft Lounge', + + slug: 'soft-lounge', + + url: 'https://cdn.platform.com/mall/audio/lounge.mp3', + + loop: true, + + volume: 0.5, + + sort_order: 1, + + is_enabled: false, + }, ]; - - const PublishEventsData = [ - - { - - - - - // type code here for "relation_one" field - - - - - - - // type code here for "relation_one" field - - - - - - - "from_environment": "dev", - - - - - - - "to_environment": "dev", - - - - - - - "started_at": new Date('2026-03-01T10:00:00Z'), - - - - - - - "finished_at": new Date('2026-03-01T10:01:10Z'), - - - - - - - "status": "queued", - - - - - - - "error_message": "", - - - - - - - "pages_copied": 6, - - - - - - - "transitions_copied": 2, - - - - - - - "audios_copied": 1, - - - - }, - - { - - - - - // type code here for "relation_one" field - - - - - - - // type code here for "relation_one" field - - - - - - - "from_environment": "production", - - - - - - - "to_environment": "production", - - - - - - - "started_at": new Date('2026-03-15T18:00:00Z'), - - - - - - - "finished_at": new Date('2026-03-15T18:02:40Z'), - - - - - - - "status": "queued", - - - - - - - "error_message": "Asset preload list generation failed due to missing CDN URL.", - - - - - - - "pages_copied": 6, - - - - - - - "transitions_copied": 2, - - - - - - - "audios_copied": 1, - - - - }, - - { - - - - - // type code here for "relation_one" field - - - - - - - // type code here for "relation_one" field - - - - - - - "from_environment": "dev", - - - - - - - "to_environment": "production", - - - - - - - "started_at": new Date('2026-02-05T09:00:00Z'), - - - - - - - "finished_at": new Date('2026-02-05T09:01:05Z'), - - - - - - - "status": "running", - - - - - - - "error_message": "", - - - - - - - "pages_copied": 4, - - - - - - - "transitions_copied": 1, - - - - - - - "audios_copied": 1, - - - - }, - + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + from_environment: 'dev', + + to_environment: 'dev', + + started_at: new Date('2026-03-01T10:00:00Z'), + + finished_at: new Date('2026-03-01T10:01:10Z'), + + status: 'queued', + + error_message: '', + + pages_copied: 6, + + transitions_copied: 2, + + audios_copied: 1, + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + from_environment: 'production', + + to_environment: 'production', + + started_at: new Date('2026-03-15T18:00:00Z'), + + finished_at: new Date('2026-03-15T18:02:40Z'), + + status: 'queued', + + error_message: + 'Asset preload list generation failed due to missing CDN URL.', + + pages_copied: 6, + + transitions_copied: 2, + + audios_copied: 1, + }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + from_environment: 'dev', + + to_environment: 'production', + + started_at: new Date('2026-02-05T09:00:00Z'), + + finished_at: new Date('2026-02-05T09:01:05Z'), + + status: 'running', + + error_message: '', + + pages_copied: 4, + + transitions_copied: 1, + + audios_copied: 1, + }, ]; - - const PwaCachesData = [ - - { - - - - - // type code here for "relation_one" field - - - - - - - "environment": "production", - - - - - - - "cache_version": "cardiff-stage-v12", - - - - - - - "manifest_json": "{name:Cardiff Arena Tour,start_url:/welcome,display:standalone}", - - - - - - - "asset_list_json": "{assets:[/images/lobby.webp,/video/transition-hallway.mp4]}", - - - - - - - "generated_at": new Date('2026-03-15T17:40:00Z'), - - - - - - - "is_active": true, - - - - }, - - { - - - - - // type code here for "relation_one" field - - - - - - - "environment": "stage", - - - - - - - "cache_version": "riverside-prod-v7", - - - - - - - "manifest_json": "{name:Riverside Park Walkthrough,start_url:/start,display:standalone}", - - - - - - - "asset_list_json": "{assets:[/images/start.webp,/audio/ambience.mp3]}", - - - - - - - "generated_at": new Date('2026-02-05T09:02:00Z'), - - - - - - - "is_active": true, - - - - }, - - { - - - - - // type code here for "relation_one" field - - - - - - - "environment": "stage", - - - - - - - "cache_version": "seaside-stage-v3", - - - - - - - "manifest_json": "{name:Seaside Concert Hall,start_url:/lobby,display:standalone}", - - - - - - - "asset_list_json": "{assets:[/images/lobby.webp,/video/spotlight.mp4,/audio/foyer-loop.mp3]}", - - - - - - - "generated_at": new Date('2026-03-05T12:22:00Z'), - - - - - - - "is_active": true, - - - - }, - + { + // type code here for "relation_one" field + + environment: 'production', + + cache_version: 'cardiff-stage-v12', + + manifest_json: + '{name:Cardiff Arena Tour,start_url:/welcome,display:standalone}', + + asset_list_json: + '{assets:[/images/lobby.webp,/video/transition-hallway.mp4]}', + + generated_at: new Date('2026-03-15T17:40:00Z'), + + is_active: true, + }, + + { + // type code here for "relation_one" field + + environment: 'stage', + + cache_version: 'riverside-prod-v7', + + manifest_json: + '{name:Riverside Park Walkthrough,start_url:/start,display:standalone}', + + asset_list_json: '{assets:[/images/start.webp,/audio/ambience.mp3]}', + + generated_at: new Date('2026-02-05T09:02:00Z'), + + is_active: true, + }, + + { + // type code here for "relation_one" field + + environment: 'stage', + + cache_version: 'seaside-stage-v3', + + manifest_json: + '{name:Seaside Concert Hall,start_url:/lobby,display:standalone}', + + asset_list_json: + '{assets:[/images/lobby.webp,/video/spotlight.mp4,/audio/foyer-loop.mp3]}', + + generated_at: new Date('2026-03-05T12:22:00Z'), + + is_active: true, + }, ]; - - const AccessLogsData = [ - - { - - - - - // type code here for "relation_one" field - - - - - - - "environment": "stage", - - - - - - - // type code here for "relation_one" field - - - - - - - "path": "/welcome", - - - - - - - "ip_address": "203.0.113.10", - - - - - - - "user_agent": "Mozilla/5.0 Chrome/122.0 Desktop", - - - - - - - "accessed_at": new Date('2026-03-14T08:06:10Z'), - - - - }, - - { - - - - - // type code here for "relation_one" field - - - - - - - "environment": "production", - - - - - - - // type code here for "relation_one" field - - - - - - - "path": "/admin/projects/cardiff-arena", - - - - - - - "ip_address": "203.0.113.11", - - - - - - - "user_agent": "Mozilla/5.0 Chrome/122.0 Desktop", - - - - - - - "accessed_at": new Date('2026-03-15T17:55:44Z'), - - - - }, - - { - - - - - // type code here for "relation_one" field - - - - - - - "environment": "stage", - - - - - - - // type code here for "relation_one" field - - - - - - - "path": "/start", - - - - - - - "ip_address": "198.51.100.55", - - - - - - - "user_agent": "Mozilla/5.0 Safari/17.3 iOS", - - - - - - - "accessed_at": new Date('2026-03-12T10:20:00Z'), - - - - }, - + { + // type code here for "relation_one" field + + environment: 'stage', + + // type code here for "relation_one" field + + path: '/welcome', + + ip_address: '203.0.113.10', + + user_agent: 'Mozilla/5.0 Chrome/122.0 Desktop', + + accessed_at: new Date('2026-03-14T08:06:10Z'), + }, + + { + // type code here for "relation_one" field + + environment: 'production', + + // type code here for "relation_one" field + + path: '/admin/projects/cardiff-arena', + + ip_address: '203.0.113.11', + + user_agent: 'Mozilla/5.0 Chrome/122.0 Desktop', + + accessed_at: new Date('2026-03-15T17:55:44Z'), + }, + + { + // type code here for "relation_one" field + + environment: 'stage', + + // type code here for "relation_one" field + + path: '/start', + + ip_address: '198.51.100.55', + + user_agent: 'Mozilla/5.0 Safari/17.3 iOS', + + accessed_at: new Date('2026-03-12T10:20:00Z'), + }, ]; +// Similar logic for "relation_many" +async function associateProjectMembershipWithProject() { + const relatedProject0 = await Projects.findOne({ + offset: Math.floor(Math.random() * (await Projects.count())), + }); + const ProjectMembership0 = await ProjectMemberships.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (ProjectMembership0?.setProject) { + await ProjectMembership0.setProject(relatedProject0); + } + const relatedProject1 = await Projects.findOne({ + offset: Math.floor(Math.random() * (await Projects.count())), + }); + const ProjectMembership1 = await ProjectMemberships.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (ProjectMembership1?.setProject) { + await ProjectMembership1.setProject(relatedProject1); + } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Similar logic for "relation_many" - - + const relatedProject2 = await Projects.findOne({ + offset: Math.floor(Math.random() * (await Projects.count())), + }); + const ProjectMembership2 = await ProjectMemberships.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (ProjectMembership2?.setProject) { + await ProjectMembership2.setProject(relatedProject2); + } +} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +async function associateProjectMembershipWithUser() { + const relatedUser0 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const ProjectMembership0 = await ProjectMemberships.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (ProjectMembership0?.setUser) { + await ProjectMembership0.setUser(relatedUser0); + } - - - - - - async function associateProjectMembershipWithProject() { - - const relatedProject0 = await Projects.findOne({ - offset: Math.floor(Math.random() * (await Projects.count())), - }); - const ProjectMembership0 = await ProjectMemberships.findOne({ - order: [['id', 'ASC']], - offset: 0 - }); - if (ProjectMembership0?.setProject) - { - await - ProjectMembership0. - setProject(relatedProject0); - } - - const relatedProject1 = await Projects.findOne({ - offset: Math.floor(Math.random() * (await Projects.count())), - }); - const ProjectMembership1 = await ProjectMemberships.findOne({ - order: [['id', 'ASC']], - offset: 1 - }); - if (ProjectMembership1?.setProject) - { - await - ProjectMembership1. - setProject(relatedProject1); - } - - const relatedProject2 = await Projects.findOne({ - offset: Math.floor(Math.random() * (await Projects.count())), - }); - const ProjectMembership2 = await ProjectMemberships.findOne({ - order: [['id', 'ASC']], - offset: 2 - }); - if (ProjectMembership2?.setProject) - { - await - ProjectMembership2. - setProject(relatedProject2); - } - - } - - - - - async function associateProjectMembershipWithUser() { - - const relatedUser0 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const ProjectMembership0 = await ProjectMemberships.findOne({ - order: [['id', 'ASC']], - offset: 0 - }); - if (ProjectMembership0?.setUser) - { - await - ProjectMembership0. - setUser(relatedUser0); - } - - const relatedUser1 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const ProjectMembership1 = await ProjectMemberships.findOne({ - order: [['id', 'ASC']], - offset: 1 - }); - if (ProjectMembership1?.setUser) - { - await - ProjectMembership1. - setUser(relatedUser1); - } - - const relatedUser2 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const ProjectMembership2 = await ProjectMemberships.findOne({ - order: [['id', 'ASC']], - offset: 2 - }); - if (ProjectMembership2?.setUser) - { - await - ProjectMembership2. - setUser(relatedUser2); - } - - } - - - - - - - - - - + const relatedUser1 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const ProjectMembership1 = await ProjectMemberships.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (ProjectMembership1?.setUser) { + await ProjectMembership1.setUser(relatedUser1); + } - - - - - - async function associateAssetWithProject() { - - const relatedProject0 = await Projects.findOne({ - offset: Math.floor(Math.random() * (await Projects.count())), - }); - const Asset0 = await Assets.findOne({ - order: [['id', 'ASC']], - offset: 0 - }); - if (Asset0?.setProject) - { - await - Asset0. - setProject(relatedProject0); - } - - const relatedProject1 = await Projects.findOne({ - offset: Math.floor(Math.random() * (await Projects.count())), - }); - const Asset1 = await Assets.findOne({ - order: [['id', 'ASC']], - offset: 1 - }); - if (Asset1?.setProject) - { - await - Asset1. - setProject(relatedProject1); - } - - const relatedProject2 = await Projects.findOne({ - offset: Math.floor(Math.random() * (await Projects.count())), - }); - const Asset2 = await Assets.findOne({ - order: [['id', 'ASC']], - offset: 2 - }); - if (Asset2?.setProject) - { - await - Asset2. - setProject(relatedProject2); - } - - } - - - - - - - - - - - - - - - - - - - - - - - - - - - - + const relatedUser2 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const ProjectMembership2 = await ProjectMemberships.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (ProjectMembership2?.setUser) { + await ProjectMembership2.setUser(relatedUser2); + } +} - - - - - - async function associateAssetVariantWithAsset() { - - const relatedAsset0 = await Assets.findOne({ - offset: Math.floor(Math.random() * (await Assets.count())), - }); - const AssetVariant0 = await AssetVariants.findOne({ - order: [['id', 'ASC']], - offset: 0 - }); - if (AssetVariant0?.setAsset) - { - await - AssetVariant0. - setAsset(relatedAsset0); - } - - const relatedAsset1 = await Assets.findOne({ - offset: Math.floor(Math.random() * (await Assets.count())), - }); - const AssetVariant1 = await AssetVariants.findOne({ - order: [['id', 'ASC']], - offset: 1 - }); - if (AssetVariant1?.setAsset) - { - await - AssetVariant1. - setAsset(relatedAsset1); - } - - const relatedAsset2 = await Assets.findOne({ - offset: Math.floor(Math.random() * (await Assets.count())), - }); - const AssetVariant2 = await AssetVariants.findOne({ - order: [['id', 'ASC']], - offset: 2 - }); - if (AssetVariant2?.setAsset) - { - await - AssetVariant2. - setAsset(relatedAsset2); - } - - } - - - - - - - - - - - - +async function associateAssetWithProject() { + const relatedProject0 = await Projects.findOne({ + offset: Math.floor(Math.random() * (await Projects.count())), + }); + const Asset0 = await Assets.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Asset0?.setProject) { + await Asset0.setProject(relatedProject0); + } - - - - - - async function associatePresignedUrlRequestWithProject() { - - const relatedProject0 = await Projects.findOne({ - offset: Math.floor(Math.random() * (await Projects.count())), - }); - const PresignedUrlRequest0 = await PresignedUrlRequests.findOne({ - order: [['id', 'ASC']], - offset: 0 - }); - if (PresignedUrlRequest0?.setProject) - { - await - PresignedUrlRequest0. - setProject(relatedProject0); - } - - const relatedProject1 = await Projects.findOne({ - offset: Math.floor(Math.random() * (await Projects.count())), - }); - const PresignedUrlRequest1 = await PresignedUrlRequests.findOne({ - order: [['id', 'ASC']], - offset: 1 - }); - if (PresignedUrlRequest1?.setProject) - { - await - PresignedUrlRequest1. - setProject(relatedProject1); - } - - const relatedProject2 = await Projects.findOne({ - offset: Math.floor(Math.random() * (await Projects.count())), - }); - const PresignedUrlRequest2 = await PresignedUrlRequests.findOne({ - order: [['id', 'ASC']], - offset: 2 - }); - if (PresignedUrlRequest2?.setProject) - { - await - PresignedUrlRequest2. - setProject(relatedProject2); - } - - } - - - - - async function associatePresignedUrlRequestWithUser() { - - const relatedUser0 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const PresignedUrlRequest0 = await PresignedUrlRequests.findOne({ - order: [['id', 'ASC']], - offset: 0 - }); - if (PresignedUrlRequest0?.setUser) - { - await - PresignedUrlRequest0. - setUser(relatedUser0); - } - - const relatedUser1 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const PresignedUrlRequest1 = await PresignedUrlRequests.findOne({ - order: [['id', 'ASC']], - offset: 1 - }); - if (PresignedUrlRequest1?.setUser) - { - await - PresignedUrlRequest1. - setUser(relatedUser1); - } - - const relatedUser2 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const PresignedUrlRequest2 = await PresignedUrlRequests.findOne({ - order: [['id', 'ASC']], - offset: 2 - }); - if (PresignedUrlRequest2?.setUser) - { - await - PresignedUrlRequest2. - setUser(relatedUser2); - } - - } - - - - - - - - - - - - - - - - + const relatedProject1 = await Projects.findOne({ + offset: Math.floor(Math.random() * (await Projects.count())), + }); + const Asset1 = await Assets.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Asset1?.setProject) { + await Asset1.setProject(relatedProject1); + } - - - - - - async function associateTourPageWithProject() { - - const relatedProject0 = await Projects.findOne({ - offset: Math.floor(Math.random() * (await Projects.count())), - }); - const TourPage0 = await TourPages.findOne({ - order: [['id', 'ASC']], - offset: 0 - }); - if (TourPage0?.setProject) - { - await - TourPage0. - setProject(relatedProject0); - } - - const relatedProject1 = await Projects.findOne({ - offset: Math.floor(Math.random() * (await Projects.count())), - }); - const TourPage1 = await TourPages.findOne({ - order: [['id', 'ASC']], - offset: 1 - }); - if (TourPage1?.setProject) - { - await - TourPage1. - setProject(relatedProject1); - } - - const relatedProject2 = await Projects.findOne({ - offset: Math.floor(Math.random() * (await Projects.count())), - }); - const TourPage2 = await TourPages.findOne({ - order: [['id', 'ASC']], - offset: 2 - }); - if (TourPage2?.setProject) - { - await - TourPage2. - setProject(relatedProject2); - } - - } - - - - - - - - - - - - - - - - - - - - - - - - + const relatedProject2 = await Projects.findOne({ + offset: Math.floor(Math.random() * (await Projects.count())), + }); + const Asset2 = await Assets.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Asset2?.setProject) { + await Asset2.setProject(relatedProject2); + } +} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +async function associateAssetVariantWithAsset() { + const relatedAsset0 = await Assets.findOne({ + offset: Math.floor(Math.random() * (await Assets.count())), + }); + const AssetVariant0 = await AssetVariants.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (AssetVariant0?.setAsset) { + await AssetVariant0.setAsset(relatedAsset0); + } - - - - - - - + const relatedAsset1 = await Assets.findOne({ + offset: Math.floor(Math.random() * (await Assets.count())), + }); + const AssetVariant1 = await AssetVariants.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (AssetVariant1?.setAsset) { + await AssetVariant1.setAsset(relatedAsset1); + } - - - - - - + const relatedAsset2 = await Assets.findOne({ + offset: Math.floor(Math.random() * (await Assets.count())), + }); + const AssetVariant2 = await AssetVariants.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (AssetVariant2?.setAsset) { + await AssetVariant2.setAsset(relatedAsset2); + } +} - - - - - - +async function associatePresignedUrlRequestWithProject() { + const relatedProject0 = await Projects.findOne({ + offset: Math.floor(Math.random() * (await Projects.count())), + }); + const PresignedUrlRequest0 = await PresignedUrlRequests.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (PresignedUrlRequest0?.setProject) { + await PresignedUrlRequest0.setProject(relatedProject0); + } - - - + const relatedProject1 = await Projects.findOne({ + offset: Math.floor(Math.random() * (await Projects.count())), + }); + const PresignedUrlRequest1 = await PresignedUrlRequests.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (PresignedUrlRequest1?.setProject) { + await PresignedUrlRequest1.setProject(relatedProject1); + } - - - - - - - - - - - - - - - - - - + const relatedProject2 = await Projects.findOne({ + offset: Math.floor(Math.random() * (await Projects.count())), + }); + const PresignedUrlRequest2 = await PresignedUrlRequests.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (PresignedUrlRequest2?.setProject) { + await PresignedUrlRequest2.setProject(relatedProject2); + } +} - - - - - - async function associateProjectAudioTrackWithProject() { - - const relatedProject0 = await Projects.findOne({ - offset: Math.floor(Math.random() * (await Projects.count())), - }); - const ProjectAudioTrack0 = await ProjectAudioTracks.findOne({ - order: [['id', 'ASC']], - offset: 0 - }); - if (ProjectAudioTrack0?.setProject) - { - await - ProjectAudioTrack0. - setProject(relatedProject0); - } - - const relatedProject1 = await Projects.findOne({ - offset: Math.floor(Math.random() * (await Projects.count())), - }); - const ProjectAudioTrack1 = await ProjectAudioTracks.findOne({ - order: [['id', 'ASC']], - offset: 1 - }); - if (ProjectAudioTrack1?.setProject) - { - await - ProjectAudioTrack1. - setProject(relatedProject1); - } - - const relatedProject2 = await Projects.findOne({ - offset: Math.floor(Math.random() * (await Projects.count())), - }); - const ProjectAudioTrack2 = await ProjectAudioTracks.findOne({ - order: [['id', 'ASC']], - offset: 2 - }); - if (ProjectAudioTrack2?.setProject) - { - await - ProjectAudioTrack2. - setProject(relatedProject2); - } - - } - - - - - - - - - - - - - - - - - - - - +async function associatePresignedUrlRequestWithUser() { + const relatedUser0 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const PresignedUrlRequest0 = await PresignedUrlRequests.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (PresignedUrlRequest0?.setUser) { + await PresignedUrlRequest0.setUser(relatedUser0); + } - - - - - - async function associatePublishEventWithProject() { - - const relatedProject0 = await Projects.findOne({ - offset: Math.floor(Math.random() * (await Projects.count())), - }); - const PublishEvent0 = await PublishEvents.findOne({ - order: [['id', 'ASC']], - offset: 0 - }); - if (PublishEvent0?.setProject) - { - await - PublishEvent0. - setProject(relatedProject0); - } - - const relatedProject1 = await Projects.findOne({ - offset: Math.floor(Math.random() * (await Projects.count())), - }); - const PublishEvent1 = await PublishEvents.findOne({ - order: [['id', 'ASC']], - offset: 1 - }); - if (PublishEvent1?.setProject) - { - await - PublishEvent1. - setProject(relatedProject1); - } - - const relatedProject2 = await Projects.findOne({ - offset: Math.floor(Math.random() * (await Projects.count())), - }); - const PublishEvent2 = await PublishEvents.findOne({ - order: [['id', 'ASC']], - offset: 2 - }); - if (PublishEvent2?.setProject) - { - await - PublishEvent2. - setProject(relatedProject2); - } - - } - - - - - async function associatePublishEventWithUser() { - - const relatedUser0 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const PublishEvent0 = await PublishEvents.findOne({ - order: [['id', 'ASC']], - offset: 0 - }); - if (PublishEvent0?.setUser) - { - await - PublishEvent0. - setUser(relatedUser0); - } - - const relatedUser1 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const PublishEvent1 = await PublishEvents.findOne({ - order: [['id', 'ASC']], - offset: 1 - }); - if (PublishEvent1?.setUser) - { - await - PublishEvent1. - setUser(relatedUser1); - } - - const relatedUser2 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const PublishEvent2 = await PublishEvents.findOne({ - order: [['id', 'ASC']], - offset: 2 - }); - if (PublishEvent2?.setUser) - { - await - PublishEvent2. - setUser(relatedUser2); - } - - } - - - - - - - - - - - - - - - - - - - - + const relatedUser1 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const PresignedUrlRequest1 = await PresignedUrlRequests.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (PresignedUrlRequest1?.setUser) { + await PresignedUrlRequest1.setUser(relatedUser1); + } - - - - - - async function associatePwaCacheWithProject() { - - const relatedProject0 = await Projects.findOne({ - offset: Math.floor(Math.random() * (await Projects.count())), - }); - const PwaCache0 = await PwaCaches.findOne({ - order: [['id', 'ASC']], - offset: 0 - }); - if (PwaCache0?.setProject) - { - await - PwaCache0. - setProject(relatedProject0); - } - - const relatedProject1 = await Projects.findOne({ - offset: Math.floor(Math.random() * (await Projects.count())), - }); - const PwaCache1 = await PwaCaches.findOne({ - order: [['id', 'ASC']], - offset: 1 - }); - if (PwaCache1?.setProject) - { - await - PwaCache1. - setProject(relatedProject1); - } - - const relatedProject2 = await Projects.findOne({ - offset: Math.floor(Math.random() * (await Projects.count())), - }); - const PwaCache2 = await PwaCaches.findOne({ - order: [['id', 'ASC']], - offset: 2 - }); - if (PwaCache2?.setProject) - { - await - PwaCache2. - setProject(relatedProject2); - } - - } - - - - - - - - - - - - - - + const relatedUser2 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const PresignedUrlRequest2 = await PresignedUrlRequests.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (PresignedUrlRequest2?.setUser) { + await PresignedUrlRequest2.setUser(relatedUser2); + } +} - - - - - - async function associateAccessLogWithProject() { - - const relatedProject0 = await Projects.findOne({ - offset: Math.floor(Math.random() * (await Projects.count())), - }); - const AccessLog0 = await AccessLogs.findOne({ - order: [['id', 'ASC']], - offset: 0 - }); - if (AccessLog0?.setProject) - { - await - AccessLog0. - setProject(relatedProject0); - } - - const relatedProject1 = await Projects.findOne({ - offset: Math.floor(Math.random() * (await Projects.count())), - }); - const AccessLog1 = await AccessLogs.findOne({ - order: [['id', 'ASC']], - offset: 1 - }); - if (AccessLog1?.setProject) - { - await - AccessLog1. - setProject(relatedProject1); - } - - const relatedProject2 = await Projects.findOne({ - offset: Math.floor(Math.random() * (await Projects.count())), - }); - const AccessLog2 = await AccessLogs.findOne({ - order: [['id', 'ASC']], - offset: 2 - }); - if (AccessLog2?.setProject) - { - await - AccessLog2. - setProject(relatedProject2); - } - - } - - - - - - - async function associateAccessLogWithUser() { - - const relatedUser0 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const AccessLog0 = await AccessLogs.findOne({ - order: [['id', 'ASC']], - offset: 0 - }); - if (AccessLog0?.setUser) - { - await - AccessLog0. - setUser(relatedUser0); - } - - const relatedUser1 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const AccessLog1 = await AccessLogs.findOne({ - order: [['id', 'ASC']], - offset: 1 - }); - if (AccessLog1?.setUser) - { - await - AccessLog1. - setUser(relatedUser1); - } - - const relatedUser2 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const AccessLog2 = await AccessLogs.findOne({ - order: [['id', 'ASC']], - offset: 2 - }); - if (AccessLog2?.setUser) - { - await - AccessLog2. - setUser(relatedUser2); - } - - } - - - - - - - - - - +async function associateTourPageWithProject() { + const relatedProject0 = await Projects.findOne({ + offset: Math.floor(Math.random() * (await Projects.count())), + }); + const TourPage0 = await TourPages.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (TourPage0?.setProject) { + await TourPage0.setProject(relatedProject0); + } + const relatedProject1 = await Projects.findOne({ + offset: Math.floor(Math.random() * (await Projects.count())), + }); + const TourPage1 = await TourPages.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (TourPage1?.setProject) { + await TourPage1.setProject(relatedProject1); + } + + const relatedProject2 = await Projects.findOne({ + offset: Math.floor(Math.random() * (await Projects.count())), + }); + const TourPage2 = await TourPages.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (TourPage2?.setProject) { + await TourPage2.setProject(relatedProject2); + } +} + +async function associateProjectAudioTrackWithProject() { + const relatedProject0 = await Projects.findOne({ + offset: Math.floor(Math.random() * (await Projects.count())), + }); + const ProjectAudioTrack0 = await ProjectAudioTracks.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (ProjectAudioTrack0?.setProject) { + await ProjectAudioTrack0.setProject(relatedProject0); + } + + const relatedProject1 = await Projects.findOne({ + offset: Math.floor(Math.random() * (await Projects.count())), + }); + const ProjectAudioTrack1 = await ProjectAudioTracks.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (ProjectAudioTrack1?.setProject) { + await ProjectAudioTrack1.setProject(relatedProject1); + } + + const relatedProject2 = await Projects.findOne({ + offset: Math.floor(Math.random() * (await Projects.count())), + }); + const ProjectAudioTrack2 = await ProjectAudioTracks.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (ProjectAudioTrack2?.setProject) { + await ProjectAudioTrack2.setProject(relatedProject2); + } +} + +async function associatePublishEventWithProject() { + const relatedProject0 = await Projects.findOne({ + offset: Math.floor(Math.random() * (await Projects.count())), + }); + const PublishEvent0 = await PublishEvents.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (PublishEvent0?.setProject) { + await PublishEvent0.setProject(relatedProject0); + } + + const relatedProject1 = await Projects.findOne({ + offset: Math.floor(Math.random() * (await Projects.count())), + }); + const PublishEvent1 = await PublishEvents.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (PublishEvent1?.setProject) { + await PublishEvent1.setProject(relatedProject1); + } + + const relatedProject2 = await Projects.findOne({ + offset: Math.floor(Math.random() * (await Projects.count())), + }); + const PublishEvent2 = await PublishEvents.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (PublishEvent2?.setProject) { + await PublishEvent2.setProject(relatedProject2); + } +} + +async function associatePublishEventWithUser() { + const relatedUser0 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const PublishEvent0 = await PublishEvents.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (PublishEvent0?.setUser) { + await PublishEvent0.setUser(relatedUser0); + } + + const relatedUser1 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const PublishEvent1 = await PublishEvents.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (PublishEvent1?.setUser) { + await PublishEvent1.setUser(relatedUser1); + } + + const relatedUser2 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const PublishEvent2 = await PublishEvents.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (PublishEvent2?.setUser) { + await PublishEvent2.setUser(relatedUser2); + } +} + +async function associatePwaCacheWithProject() { + const relatedProject0 = await Projects.findOne({ + offset: Math.floor(Math.random() * (await Projects.count())), + }); + const PwaCache0 = await PwaCaches.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (PwaCache0?.setProject) { + await PwaCache0.setProject(relatedProject0); + } + + const relatedProject1 = await Projects.findOne({ + offset: Math.floor(Math.random() * (await Projects.count())), + }); + const PwaCache1 = await PwaCaches.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (PwaCache1?.setProject) { + await PwaCache1.setProject(relatedProject1); + } + + const relatedProject2 = await Projects.findOne({ + offset: Math.floor(Math.random() * (await Projects.count())), + }); + const PwaCache2 = await PwaCaches.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (PwaCache2?.setProject) { + await PwaCache2.setProject(relatedProject2); + } +} + +async function associateAccessLogWithProject() { + const relatedProject0 = await Projects.findOne({ + offset: Math.floor(Math.random() * (await Projects.count())), + }); + const AccessLog0 = await AccessLogs.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (AccessLog0?.setProject) { + await AccessLog0.setProject(relatedProject0); + } + + const relatedProject1 = await Projects.findOne({ + offset: Math.floor(Math.random() * (await Projects.count())), + }); + const AccessLog1 = await AccessLogs.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (AccessLog1?.setProject) { + await AccessLog1.setProject(relatedProject1); + } + + const relatedProject2 = await Projects.findOne({ + offset: Math.floor(Math.random() * (await Projects.count())), + }); + const AccessLog2 = await AccessLogs.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (AccessLog2?.setProject) { + await AccessLog2.setProject(relatedProject2); + } +} + +async function associateAccessLogWithUser() { + const relatedUser0 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const AccessLog0 = await AccessLogs.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (AccessLog0?.setUser) { + await AccessLog0.setUser(relatedUser0); + } + + const relatedUser1 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const AccessLog1 = await AccessLogs.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (AccessLog1?.setUser) { + await AccessLog1.setUser(relatedUser1); + } + + const relatedUser2 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const AccessLog2 = await AccessLogs.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (AccessLog2?.setUser) { + await AccessLog2.setUser(relatedUser2); + } +} module.exports = { - up: async () => { - // Keep production-like schema strict; skip auto sample payload inserts by default. - if (process.env.ENABLE_SAMPLE_DATA !== 'true') { - return; - } - - - - - - - - await Projects.bulkCreate(ProjectsData); - - - - - await ProjectMemberships.bulkCreate(ProjectMembershipsData); - - - - - await Assets.bulkCreate(AssetsData); - - - - - await AssetVariants.bulkCreate(AssetVariantsData); - - - - - await PresignedUrlRequests.bulkCreate(PresignedUrlRequestsData); - - - + up: async () => { + // Keep production-like schema strict; skip auto sample payload inserts by default. + if (process.env.ENABLE_SAMPLE_DATA !== 'true') { + return; + } - await TourPages.bulkCreate(TourPagesData); + await Projects.bulkCreate(ProjectsData); + await ProjectMemberships.bulkCreate(ProjectMembershipsData); + await Assets.bulkCreate(AssetsData); + await AssetVariants.bulkCreate(AssetVariantsData); - await ProjectAudioTracks.bulkCreate(ProjectAudioTracksData); - - - - - await PublishEvents.bulkCreate(PublishEventsData); - - - - - await PwaCaches.bulkCreate(PwaCachesData); - - - - - await AccessLogs.bulkCreate(AccessLogsData); - - - await Promise.all([ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Similar logic for "relation_many" - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - await associateProjectMembershipWithProject(), - - - - - await associateProjectMembershipWithUser(), - - - - - - - - - - - - - - - - await associateAssetWithProject(), - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - await associateAssetVariantWithAsset(), - - - - - - - - - - - - - - - - - - await associatePresignedUrlRequestWithProject(), - - - - - await associatePresignedUrlRequestWithUser(), - - - - - - - - - - - - - - - - - - - - - - await associateTourPageWithProject(), - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - await associateProjectAudioTrackWithProject(), - - - - - - - - - - - - - - - - - - - - - - - - - - await associatePublishEventWithProject(), - - - - - await associatePublishEventWithUser(), - - - - - - - - - - - - - - - - - - - - - - - - - - await associatePwaCacheWithProject(), - - - - - - - - - - - - - - - - - - - - await associateAccessLogWithProject(), - - - - - - - await associateAccessLogWithUser(), - - - - - - - - - - - - ]); - - }, + await PresignedUrlRequests.bulkCreate(PresignedUrlRequestsData); - down: async (queryInterface) => { - if (process.env.ENABLE_SAMPLE_DATA !== 'true') { - return; - } - - - - - - - await queryInterface.bulkDelete('projects', null, {}); - - - await queryInterface.bulkDelete('project_memberships', null, {}); - - - await queryInterface.bulkDelete('assets', null, {}); - - - await queryInterface.bulkDelete('asset_variants', null, {}); - - - await queryInterface.bulkDelete('presigned_url_requests', null, {}); - - - await queryInterface.bulkDelete('tour_pages', null, {}); - + await TourPages.bulkCreate(TourPagesData); - await queryInterface.bulkDelete('project_audio_tracks', null, {}); - - - await queryInterface.bulkDelete('publish_events', null, {}); - - - await queryInterface.bulkDelete('pwa_caches', null, {}); - - - await queryInterface.bulkDelete('access_logs', null, {}); - - - }, + await ProjectAudioTracks.bulkCreate(ProjectAudioTracksData); + + await PublishEvents.bulkCreate(PublishEventsData); + + await PwaCaches.bulkCreate(PwaCachesData); + + await AccessLogs.bulkCreate(AccessLogsData); + + await Promise.all([ + // Similar logic for "relation_many" + + await associateProjectMembershipWithProject(), + + await associateProjectMembershipWithUser(), + + await associateAssetWithProject(), + + await associateAssetVariantWithAsset(), + + await associatePresignedUrlRequestWithProject(), + + await associatePresignedUrlRequestWithUser(), + + await associateTourPageWithProject(), + + await associateProjectAudioTrackWithProject(), + + await associatePublishEventWithProject(), + + await associatePublishEventWithUser(), + + await associatePwaCacheWithProject(), + + await associateAccessLogWithProject(), + + await associateAccessLogWithUser(), + ]); + }, + + down: async (queryInterface) => { + if (process.env.ENABLE_SAMPLE_DATA !== 'true') { + return; + } + + await queryInterface.bulkDelete('projects', null, {}); + + await queryInterface.bulkDelete('project_memberships', null, {}); + + await queryInterface.bulkDelete('assets', null, {}); + + await queryInterface.bulkDelete('asset_variants', null, {}); + + await queryInterface.bulkDelete('presigned_url_requests', null, {}); + + await queryInterface.bulkDelete('tour_pages', null, {}); + + await queryInterface.bulkDelete('project_audio_tracks', null, {}); + + await queryInterface.bulkDelete('publish_events', null, {}); + + await queryInterface.bulkDelete('pwa_caches', null, {}); + + await queryInterface.bulkDelete('access_logs', null, {}); + }, }; diff --git a/backend/src/db/sync.js b/backend/src/db/sync.js index 2f921da..d844f1e 100644 --- a/backend/src/db/sync.js +++ b/backend/src/db/sync.js @@ -2,7 +2,9 @@ const db = require('./models'); async function syncDatabase() { if (process.env.NODE_ENV === 'production') { - console.error('ERROR: sync.js should not be run in production. Use migrations instead.'); + console.error( + 'ERROR: sync.js should not be run in production. Use migrations instead.', + ); process.exit(1); } diff --git a/backend/src/db/utils.js b/backend/src/db/utils.js index c253a07..c257f8d 100644 --- a/backend/src/db/utils.js +++ b/backend/src/db/utils.js @@ -15,10 +15,7 @@ module.exports = class Utils { static ilike(model, column, value) { return Sequelize.where( - Sequelize.fn( - 'lower', - Sequelize.col(`${model}.${column}`), - ), + Sequelize.fn('lower', Sequelize.col(`${model}.${column}`)), { [Sequelize.Op.like]: `%${value}%`.toLowerCase(), }, diff --git a/backend/src/factories/router.factory.js b/backend/src/factories/router.factory.js index bc7dc51..ad00668 100644 --- a/backend/src/factories/router.factory.js +++ b/backend/src/factories/router.factory.js @@ -9,82 +9,129 @@ function createEntityRouter(entityName, Service, DBApi, options = {}) { const permissionEntity = options.permissionEntity || entityName; router.use(checkCrudPermissions(permissionEntity)); - router.post('/', wrapAsync(async (req, res) => { - const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; - const link = new URL(referer); - const payload = await Service.create(req.body.data, req.currentUser, true, link.host); - res.status(200).send(payload); - })); - - router.post('/bulk-import', wrapAsync(async (req, res) => { - const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; - const link = new URL(referer); - await Service.bulkImport(req, res, true, link.host); - res.status(200).send(true); - })); - - router.put('/:id', wrapAsync(async (req, res) => { - await Service.update(req.body.data, req.body.id, req.currentUser); - res.status(200).send(true); - })); - - router.delete('/:id', wrapAsync(async (req, res) => { - await Service.remove(req.params.id, req.currentUser); - res.status(200).send(true); - })); - - router.post('/deleteByIds', wrapAsync(async (req, res) => { - await Service.deleteByIds(req.body.data, req.currentUser); - res.status(200).send(true); - })); - - router.get('/', wrapAsync(async (req, res) => { - const filetype = req.query.filetype; - const currentUser = req.currentUser; - const runtimeContext = req.runtimeContext; - - const payload = await DBApi.findAll(req.query, { currentUser, runtimeContext }); - - if (filetype === 'csv') { - const fields = options.csvFields || DBApi.CSV_FIELDS || ['id', 'createdAt']; - const opts = { fields }; - try { - const csv = parse(payload.rows, opts); - res.status(200).attachment('export.csv').send(csv); - } catch (err) { - console.error(err); - res.status(500).send('CSV export error'); - } - } else { + router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + const payload = await Service.create( + req.body.data, + req.currentUser, + true, + link.host, + ); res.status(200).send(payload); - } - })); + }), + ); - router.get('/count', wrapAsync(async (req, res) => { - const currentUser = req.currentUser; - const runtimeContext = req.runtimeContext; - const payload = await DBApi.findAll(req.query, { countOnly: true, currentUser, runtimeContext }); - res.status(200).send(payload); - })); + router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Service.bulkImport(req, res, true, link.host); + res.status(200).send(true); + }), + ); - router.get('/autocomplete', wrapAsync(async (req, res) => { - const payload = await DBApi.findAllAutocomplete( - req.query.query, - req.query.limit, - req.query.offset - ); - res.status(200).send(payload); - })); + router.put( + '/:id', + wrapAsync(async (req, res) => { + await Service.update(req.body.data, req.body.id, req.currentUser); + res.status(200).send(true); + }), + ); - router.get('/:id', wrapAsync(async (req, res) => { - if (!isUuidV4(req.params.id)) { - return res.status(400).send(`Invalid ${entityName} id`); - } + router.delete( + '/:id', + wrapAsync(async (req, res) => { + await Service.remove(req.params.id, req.currentUser); + res.status(200).send(true); + }), + ); - const runtimeContext = req.runtimeContext; - const payload = await DBApi.findBy({ id: req.params.id }, { runtimeContext }); - res.status(200).send(payload); - })); + router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await Service.deleteByIds(req.body.data, req.currentUser); + res.status(200).send(true); + }), + ); + + router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; + const currentUser = req.currentUser; + const runtimeContext = req.runtimeContext; + + const payload = await DBApi.findAll(req.query, { + currentUser, + runtimeContext, + }); + + if (filetype === 'csv') { + const fields = options.csvFields || + DBApi.CSV_FIELDS || ['id', 'createdAt']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment('export.csv').send(csv); + } catch (err) { + console.error(err); + res.status(500).send('CSV export error'); + } + } else { + res.status(200).send(payload); + } + }), + ); + + router.get( + '/count', + wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const runtimeContext = req.runtimeContext; + const payload = await DBApi.findAll(req.query, { + countOnly: true, + currentUser, + runtimeContext, + }); + res.status(200).send(payload); + }), + ); + + router.get( + '/autocomplete', + wrapAsync(async (req, res) => { + const payload = await DBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + res.status(200).send(payload); + }), + ); + + router.get( + '/:id', + wrapAsync(async (req, res) => { + if (!isUuidV4(req.params.id)) { + return res.status(400).send(`Invalid ${entityName} id`); + } + + const runtimeContext = req.runtimeContext; + const payload = await DBApi.findBy( + { id: req.params.id }, + { runtimeContext }, + ); + res.status(200).send(payload); + }), + ); if (options.customRoutes) { options.customRoutes(router, Service, DBApi); diff --git a/backend/src/factories/service.factory.js b/backend/src/factories/service.factory.js index fb4f100..80b866c 100644 --- a/backend/src/factories/service.factory.js +++ b/backend/src/factories/service.factory.js @@ -61,7 +61,10 @@ function createEntityService(DBApi, options = {}) { throw new ValidationError(`${entityName}NotFound`); } - const updated = await DBApi.update(id, data, { currentUser, transaction }); + const updated = await DBApi.update(id, data, { + currentUser, + transaction, + }); await transaction.commit(); return updated; } catch (error) { diff --git a/backend/src/helpers.js b/backend/src/helpers.js index 183e2eb..2b2d4b7 100644 --- a/backend/src/helpers.js +++ b/backend/src/helpers.js @@ -20,10 +20,12 @@ module.exports = class Helpers { } static jwtSign(data) { - return jwt.sign(data, config.secret_key, {expiresIn: '6h'}); + return jwt.sign(data, config.secret_key, { expiresIn: '6h' }); } static isUuidV4(value) { - return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value); + return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test( + value, + ); } }; diff --git a/backend/src/index.js b/backend/src/index.js index d7907b7..fbc8a2a 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -18,8 +18,6 @@ const sqlRoutes = require('./routes/sql'); const openaiRoutes = require('./routes/openai'); - - const usersRoutes = require('./routes/users'); const rolesRoutes = require('./routes/roles'); @@ -56,7 +54,6 @@ const { sanitizePublicRuntimeListResponse, } = require('./middlewares/runtime-public'); - const getBaseUrl = (url) => { if (!url) return ''; return url.endsWith('/api') ? url.slice(0, -4) : url; @@ -64,17 +61,18 @@ const getBaseUrl = (url) => { const options = { definition: { - openapi: "3.0.0", - info: { - version: "1.0.0", - title: "Tour Builder Platform", - description: "Tour Builder Platform Online REST API for Testing and Prototyping application. You can perform all major operations with your entities - create, delete and etc.", - }, + openapi: '3.0.0', + info: { + version: '1.0.0', + title: 'Tour Builder Platform', + description: + 'Tour Builder Platform Online REST API for Testing and Prototyping application. You can perform all major operations with your entities - create, delete and etc.', + }, servers: [ { url: getBaseUrl(process.env.NEXT_PUBLIC_BACK_API) || config.swaggerUrl, - description: "Development server", - } + description: 'Development server', + }, ], components: { securitySchemes: { @@ -82,26 +80,34 @@ const options = { type: 'http', scheme: 'bearer', bearerFormat: 'JWT', - } + }, }, responses: { UnauthorizedError: { - description: "Access token is missing or invalid" - } - } + description: 'Access token is missing or invalid', + }, + }, }, - security: [{ - bearerAuth: [] - }] + security: [ + { + bearerAuth: [], + }, + ], }, - apis: ["./src/routes/*.js"], + apis: ['./src/routes/*.js'], }; const specs = swaggerJsDoc(options); -app.use('/api-docs', function (req, res, next) { - swaggerUI.host = getBaseUrl(process.env.NEXT_PUBLIC_BACK_API) || req.get('host'); - next() - }, swaggerUI.serve, swaggerUI.setup(specs)) +app.use( + '/api-docs', + function (req, res, next) { + swaggerUI.host = + getBaseUrl(process.env.NEXT_PUBLIC_BACK_API) || req.get('host'); + next(); + }, + swaggerUI.serve, + swaggerUI.setup(specs), +); app.enable('trust proxy'); app.use( @@ -110,7 +116,7 @@ app.use( crossOriginEmbedderPolicy: false, }), ); -app.use(cors({origin: true})); +app.use(cors({ origin: true })); require('./auth/auth'); // Request logger applied early so all routes are logged @@ -171,7 +177,6 @@ app.get('/api/health', async (req, res) => { app.use('/api/auth', authRoutes); app.use('/api/runtime-context', runtimeContextRoutes); - app.use('/api/users', jwtAuth, usersRoutes); app.use('/api/roles', jwtAuth, rolesRoutes); @@ -200,7 +205,11 @@ app.use('/api/presigned_url_requests', jwtAuth, presigned_url_requestsRoutes); mountRuntimeEntityRoute('/api/tour_pages', 'tour_pages', tour_pagesRoutes); -mountRuntimeEntityRoute('/api/project_audio_tracks', 'project_audio_tracks', project_audio_tracksRoutes); +mountRuntimeEntityRoute( + '/api/project_audio_tracks', + 'project_audio_tracks', + project_audio_tracksRoutes, +); app.use('/api/publish_events', jwtAuth, publish_eventsRoutes); @@ -210,43 +219,27 @@ app.use('/api/access_logs', jwtAuth, access_logsRoutes); app.use('/api/element-type-defaults', jwtAuth, element_type_defaultsRoutes); // Backwards compatibility alias for old API endpoint app.use('/api/ui-elements', jwtAuth, element_type_defaultsRoutes); -app.use('/api/project-element-defaults', jwtAuth, project_element_defaultsRoutes); +app.use( + '/api/project-element-defaults', + jwtAuth, + project_element_defaultsRoutes, +); app.use('/api/publish', jwtAuth, publishRoutes); -app.use( - '/api/openai', - jwtAuth, - openaiRoutes, -); -app.use( - '/api/ai', - jwtAuth, - openaiRoutes, -); +app.use('/api/openai', jwtAuth, openaiRoutes); +app.use('/api/ai', jwtAuth, openaiRoutes); -app.use( - '/api/search', - jwtAuth, - searchRoutes); -app.use( - '/api/sql', - jwtAuth, - sqlRoutes); +app.use('/api/search', jwtAuth, searchRoutes); +app.use('/api/sql', jwtAuth, sqlRoutes); - -const publicDir = path.join( - __dirname, - '../public', -); +const publicDir = path.join(__dirname, '../public'); if (fs.existsSync(publicDir)) { app.use('/', express.static(publicDir)); - app.get('*', function(request, response) { - response.sendFile( - path.resolve(publicDir, 'index.html'), - ); + app.get('*', function (request, response) { + response.sendFile(path.resolve(publicDir, 'index.html')); }); } @@ -260,8 +253,11 @@ app.use((err, req, res, _next) => { const PORT = process.env.NODE_ENV === 'dev_stage' ? 3000 : 8080; - app.listen(PORT, () => { - logger.info({ port: PORT, env: process.env.NODE_ENV || 'development' }, 'Server started'); - }); +app.listen(PORT, () => { + logger.info( + { port: PORT, env: process.env.NODE_ENV || 'development' }, + 'Server started', + ); +}); module.exports = app; diff --git a/backend/src/middlewares/check-permissions.js b/backend/src/middlewares/check-permissions.js index c6dbd90..b7e0a6b 100644 --- a/backend/src/middlewares/check-permissions.js +++ b/backend/src/middlewares/check-permissions.js @@ -1,4 +1,3 @@ - const ValidationError = require('../services/notifications/errors/validation'); const RolesDBApi = require('../db/api/roles'); @@ -7,30 +6,38 @@ let publicRoleCache = null; // Function to asynchronously fetch and cache the 'Public' role async function fetchAndCachePublicRole() { - try { - // Use RolesDBApi to find the role by name 'Public' - publicRoleCache = await RolesDBApi.findBy({ name: 'Public' }); + try { + // Use RolesDBApi to find the role by name 'Public' + publicRoleCache = await RolesDBApi.findBy({ name: 'Public' }); - if (!publicRoleCache) { - console.error("WARNING: Role 'Public' not found in database during middleware startup. Check your migrations."); - // The system might not function correctly without this role. May need to throw an error or use a fallback stub. - } else { - console.log("'Public' role successfully loaded and cached."); - } - } catch (error) { - console.error("Error fetching 'Public' role during middleware startup:", error); - // Handle the error during startup fetch - throw error; // Important to know if the app can proceed without the Public role + if (!publicRoleCache) { + console.error( + "WARNING: Role 'Public' not found in database during middleware startup. Check your migrations.", + ); + // The system might not function correctly without this role. May need to throw an error or use a fallback stub. + } else { + console.log("'Public' role successfully loaded and cached."); } + } catch (error) { + console.error( + "Error fetching 'Public' role during middleware startup:", + error, + ); + // Handle the error during startup fetch + throw error; // Important to know if the app can proceed without the Public role + } } // Trigger the role fetching when the check-permissions.js module is imported/loaded // This should happen during application startup when routes are being configured. -fetchAndCachePublicRole().catch(error => { - // Handle the case where the fetchAndCachePublicRole promise is rejected - console.error("Critical error during permissions middleware initialization:", error); - // Decide here if the process should exit if the Public role is essential. - // process.exit(1); +fetchAndCachePublicRole().catch((error) => { + // Handle the case where the fetchAndCachePublicRole promise is rejected + console.error( + 'Critical error during permissions middleware initialization:', + error, + ); + // Decide here if the process should exit if the Public role is essential. + // process.exit(1); }); /** @@ -39,85 +46,106 @@ fetchAndCachePublicRole().catch(error => { * @return {import("express").RequestHandler} Express middleware function. */ function checkPermissions(permission) { - return async (req, res, next) => { - const { currentUser } = req; + return async (req, res, next) => { + const { currentUser } = req; - // 1. Check self-access bypass (only if the user is authenticated) - if (currentUser && (currentUser.id === req.params.id || currentUser.id === req.body.id)) { - return next(); // User has access to their own resource - } + // 1. Check self-access bypass (only if the user is authenticated) + if ( + currentUser && + (currentUser.id === req.params.id || currentUser.id === req.body.id) + ) { + return next(); // User has access to their own resource + } - // 2. Check Custom Permissions (only if the user is authenticated) - if (currentUser) { - // Ensure custom_permissions is an array before using find - const customPermissions = Array.isArray(currentUser.custom_permissions) - ? currentUser.custom_permissions - : []; - const userPermission = customPermissions.find( - (cp) => cp.name === permission, + // 2. Check Custom Permissions (only if the user is authenticated) + if (currentUser) { + // Ensure custom_permissions is an array before using find + const customPermissions = Array.isArray(currentUser.custom_permissions) + ? currentUser.custom_permissions + : []; + const userPermission = customPermissions.find( + (cp) => cp.name === permission, + ); + if (userPermission) { + return next(); // User has a custom permission + } + } + + // 3. Determine the "effective" role for permission check + let effectiveRole = null; + try { + if (currentUser && currentUser.app_role) { + // User is authenticated and has an assigned role + effectiveRole = currentUser.app_role; + } else { + // User is NOT authenticated OR is authenticated but has no role + // Use the cached 'Public' role + if (!publicRoleCache) { + // If the cache is unexpectedly empty (e.g., startup error caught), + // we can try fetching the role again synchronously (less ideal) or just deny access. + console.error( + 'Public role cache is empty. Attempting synchronous fetch...', + ); + // Less efficient fallback option: + effectiveRole = await RolesDBApi.findBy({ name: 'Public' }); // Could be slow + if (!effectiveRole) { + // If even the synchronous attempt failed + return next( + new Error( + 'Internal Server Error: Public role missing and cannot be fetched.', + ), ); - if (userPermission) { - return next(); // User has a custom permission - } + } + } else { + effectiveRole = publicRoleCache; // Use the cached object } + } - // 3. Determine the "effective" role for permission check - let effectiveRole = null; - try { - if (currentUser && currentUser.app_role) { - // User is authenticated and has an assigned role - effectiveRole = currentUser.app_role; - } else { - // User is NOT authenticated OR is authenticated but has no role - // Use the cached 'Public' role - if (!publicRoleCache) { - // If the cache is unexpectedly empty (e.g., startup error caught), - // we can try fetching the role again synchronously (less ideal) or just deny access. - console.error("Public role cache is empty. Attempting synchronous fetch..."); - // Less efficient fallback option: - effectiveRole = await RolesDBApi.findBy({ name: 'Public' }); // Could be slow - if (!effectiveRole) { - // If even the synchronous attempt failed - return next(new Error("Internal Server Error: Public role missing and cannot be fetched.")); - } - } else { - effectiveRole = publicRoleCache; // Use the cached object - } - } + // Check if we got a valid role object + if (!effectiveRole) { + return next( + new Error( + 'Internal Server Error: Could not determine effective role.', + ), + ); + } - // Check if we got a valid role object - if (!effectiveRole) { - return next(new Error("Internal Server Error: Could not determine effective role.")); - } + // 4. Check Permissions on the "effective" role + // Assume the effectiveRole object (from app_role or RolesDBApi) has a getPermissions() method + // or a 'permissions' property (if permissions are eagerly loaded). + let rolePermissions = []; + if (typeof effectiveRole.getPermissions === 'function') { + rolePermissions = await effectiveRole.getPermissions(); // Get permissions asynchronously if the method exists + } else if (Array.isArray(effectiveRole.permissions)) { + rolePermissions = effectiveRole.permissions; // Or take from property if permissions are pre-loaded + } else { + console.error( + 'Role object lacks getPermissions() method or permissions property:', + effectiveRole, + ); + return next( + new Error('Internal Server Error: Invalid role object format.'), + ); + } - // 4. Check Permissions on the "effective" role - // Assume the effectiveRole object (from app_role or RolesDBApi) has a getPermissions() method - // or a 'permissions' property (if permissions are eagerly loaded). - let rolePermissions = []; - if (typeof effectiveRole.getPermissions === 'function') { - rolePermissions = await effectiveRole.getPermissions(); // Get permissions asynchronously if the method exists - } else if (Array.isArray(effectiveRole.permissions)) { - rolePermissions = effectiveRole.permissions; // Or take from property if permissions are pre-loaded - } else { - console.error("Role object lacks getPermissions() method or permissions property:", effectiveRole); - return next(new Error("Internal Server Error: Invalid role object format.")); - } - - - if (rolePermissions.find((p) => p.name === permission)) { - next(); // The "effective" role has the required permission - } else { - // The "effective" role does not have the required permission - const roleName = effectiveRole.name || 'unknown role'; - next(new ValidationError('auth.forbidden', `Role '${roleName}' denied access to '${permission}'.`)); - } - - } catch (e) { - // Handle errors during role or permission fetching - console.error("Error during permission check:", e); - next(e); // Pass the error to the next middleware - } - }; + if (rolePermissions.find((p) => p.name === permission)) { + next(); // The "effective" role has the required permission + } else { + // The "effective" role does not have the required permission + const roleName = effectiveRole.name || 'unknown role'; + next( + new ValidationError( + 'auth.forbidden', + `Role '${roleName}' denied access to '${permission}'.`, + ), + ); + } + } catch (e) { + // Handle errors during role or permission fetching + console.error('Error during permission check:', e); + next(e); // Pass the error to the next middleware + } + }; } const METHOD_MAP = { @@ -143,23 +171,24 @@ const RUNTIME_PUBLIC_READ_ENTITIES = new Set([ * @return {import("express").RequestHandler} Express middleware function. */ function checkCrudPermissions(name) { - return (req, res, next) => { - const isRuntimePublicRead = req.isRuntimePublicRequest === true - && req.method === 'GET' - && RUNTIME_PUBLIC_READ_ENTITIES.has(name.toUpperCase()); + return (req, res, next) => { + const isRuntimePublicRead = + req.isRuntimePublicRequest === true && + req.method === 'GET' && + RUNTIME_PUBLIC_READ_ENTITIES.has(name.toUpperCase()); - if (isRuntimePublicRead) { - return next(); - } + if (isRuntimePublicRead) { + return next(); + } - // Dynamically determine the permission name (e.g., 'READ_USERS') - const permissionName = `${METHOD_MAP[req.method]}_${name.toUpperCase()}`; - // Call the checkPermissions middleware with the determined permission - return checkPermissions(permissionName)(req, res, next); - }; + // Dynamically determine the permission name (e.g., 'READ_USERS') + const permissionName = `${METHOD_MAP[req.method]}_${name.toUpperCase()}`; + // Call the checkPermissions middleware with the determined permission + return checkPermissions(permissionName)(req, res, next); + }; } module.exports = { - checkPermissions, - checkCrudPermissions, + checkPermissions, + checkCrudPermissions, }; diff --git a/backend/src/middlewares/runtime-context.js b/backend/src/middlewares/runtime-context.js index 9f483e2..1e2ac2b 100644 --- a/backend/src/middlewares/runtime-context.js +++ b/backend/src/middlewares/runtime-context.js @@ -12,7 +12,10 @@ function runtimeContextMiddleware(req, res, next) { // Read environment from header (X-Runtime-Environment) const headerEnvironment = req.headers['x-runtime-environment']; - if (headerEnvironment && ['production', 'stage', 'dev'].includes(headerEnvironment)) { + if ( + headerEnvironment && + ['production', 'stage', 'dev'].includes(headerEnvironment) + ) { context.headerEnvironment = headerEnvironment; } diff --git a/backend/src/middlewares/runtime-public.js b/backend/src/middlewares/runtime-public.js index 145b731..a4f94f3 100644 --- a/backend/src/middlewares/runtime-public.js +++ b/backend/src/middlewares/runtime-public.js @@ -49,7 +49,8 @@ const pickFields = (record, fields) => { } // Convert Sequelize instance to plain object if needed - const plainRecord = typeof record.get === 'function' ? record.get({ plain: true }) : record; + const plainRecord = + typeof record.get === 'function' ? record.get({ plain: true }) : record; return fields.reduce((acc, field) => { if (field in plainRecord && plainRecord[field] !== undefined) { @@ -83,7 +84,11 @@ const sanitizePublicRuntimeListResponse = (entityName) => { const fields = PUBLIC_RUNTIME_ENTITY_FIELDS[entityName] || []; return (req, res, next) => { - if (!isPublicRuntimeReadRequest(req) || req.path !== PUBLIC_RUNTIME_ALLOWED_PATH || fields.length === 0) { + if ( + !isPublicRuntimeReadRequest(req) || + req.path !== PUBLIC_RUNTIME_ALLOWED_PATH || + fields.length === 0 + ) { return next(); } diff --git a/backend/src/middlewares/upload.js b/backend/src/middlewares/upload.js index de1014e..db07aeb 100644 --- a/backend/src/middlewares/upload.js +++ b/backend/src/middlewares/upload.js @@ -3,7 +3,7 @@ const Multer = require('multer'); let processFile = Multer({ storage: Multer.memoryStorage(), -}).single("file"); +}).single('file'); let processFileMiddleware = util.promisify(processFile); module.exports = processFileMiddleware; diff --git a/backend/src/middlewares/validate.js b/backend/src/middlewares/validate.js index 026d88f..a6148f1 100644 --- a/backend/src/middlewares/validate.js +++ b/backend/src/middlewares/validate.js @@ -5,7 +5,7 @@ const handleValidationErrors = (req, res, next) => { if (!errors.isEmpty()) { return res.status(400).json({ error: 'Validation failed', - details: errors.array().map(err => ({ + details: errors.array().map((err) => ({ field: err.path, message: err.msg, value: err.value, @@ -24,48 +24,64 @@ const validators = { requiredString: (field, min = 1, max = 255) => body(field) .trim() - .notEmpty().withMessage(`${field} is required`) - .isLength({ min, max }).withMessage(`${field} must be ${min}-${max} characters`), + .notEmpty() + .withMessage(`${field} is required`) + .isLength({ min, max }) + .withMessage(`${field} must be ${min}-${max} characters`), optionalString: (field, max = 255) => body(field) .optional() .trim() - .isLength({ max }).withMessage(`${field} must be at most ${max} characters`), + .isLength({ max }) + .withMessage(`${field} must be at most ${max} characters`), slug: (field) => body(field) .optional() .trim() - .matches(/^[a-z0-9_-]+$/i).withMessage(`${field} can only contain letters, numbers, dashes, underscores`), + .matches(/^[a-z0-9_-]+$/i) + .withMessage( + `${field} can only contain letters, numbers, dashes, underscores`, + ), email: (field) => body(field) .trim() - .isEmail().withMessage('Must be a valid email') + .isEmail() + .withMessage('Must be a valid email') .normalizeEmail(), optionalEmail: (field) => body(field) .optional() .trim() - .isEmail().withMessage('Must be a valid email') + .isEmail() + .withMessage('Must be a valid email') .normalizeEmail(), enum: (field, values) => body(field) .optional() - .isIn(values).withMessage(`${field} must be one of: ${values.join(', ')}`), + .isIn(values) + .withMessage(`${field} must be one of: ${values.join(', ')}`), boolean: (field) => body(field) .optional() - .isBoolean().withMessage(`${field} must be a boolean`), + .isBoolean() + .withMessage(`${field} must be a boolean`), integer: (field, min, max) => { let validator = body(field).optional().isInt(); - if (min !== undefined) validator = validator.custom(val => val >= min).withMessage(`${field} must be at least ${min}`); - if (max !== undefined) validator = validator.custom(val => val <= max).withMessage(`${field} must be at most ${max}`); + if (min !== undefined) + validator = validator + .custom((val) => val >= min) + .withMessage(`${field} must be at least ${min}`); + if (max !== undefined) + validator = validator + .custom((val) => val <= max) + .withMessage(`${field} must be at most ${max}`); return validator; }, @@ -78,15 +94,20 @@ const validators = { body(field) .optional() .trim() - .isURL().withMessage(`${field} must be a valid URL`), + .isURL() + .withMessage(`${field} must be a valid URL`), }; function createEntityValidation(entityConfig = {}) { const { fields = [], requiredFields = [] } = entityConfig; - const fieldValidators = fields.map(field => { + const fieldValidators = fields.map((field) => { if (requiredFields.includes(field.name)) { - return validators.requiredString(`data.${field.name}`, field.min || 1, field.max || 255); + return validators.requiredString( + `data.${field.name}`, + field.min || 1, + field.max || 255, + ); } return validators.optionalString(`data.${field.name}`, field.max || 255); }); diff --git a/backend/src/routes/access_logs.js b/backend/src/routes/access_logs.js index 3798435..ac92b6a 100644 --- a/backend/src/routes/access_logs.js +++ b/backend/src/routes/access_logs.js @@ -138,4 +138,8 @@ const { createEntityRouter } = require('../factories/router.factory'); * description: Some server error */ -module.exports = createEntityRouter('access_logs', Access_logsService, Access_logsDBApi); +module.exports = createEntityRouter( + 'access_logs', + Access_logsService, + Access_logsDBApi, +); diff --git a/backend/src/routes/asset_variants.js b/backend/src/routes/asset_variants.js index 161c05c..491649a 100644 --- a/backend/src/routes/asset_variants.js +++ b/backend/src/routes/asset_variants.js @@ -140,4 +140,8 @@ const { createEntityRouter } = require('../factories/router.factory'); * description: Some server error */ -module.exports = createEntityRouter('asset_variants', Asset_variantsService, Asset_variantsDBApi); +module.exports = createEntityRouter( + 'asset_variants', + Asset_variantsService, + Asset_variantsDBApi, +); diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index a6d8cd0..0533b08 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -10,30 +10,28 @@ const wrapAsync = require('../helpers').wrapAsync; const router = express.Router(); const authRateLimitStore = new Map(); -const createMemoryRateLimiter = ({ - keyPrefix, - maxRequests, - windowMs, -}) => (req, res, next) => { - const key = `${keyPrefix}:${req.ip || 'unknown'}`; - const now = Date.now(); - const current = authRateLimitStore.get(key); +const createMemoryRateLimiter = + ({ keyPrefix, maxRequests, windowMs }) => + (req, res, next) => { + const key = `${keyPrefix}:${req.ip || 'unknown'}`; + const now = Date.now(); + const current = authRateLimitStore.get(key); - if (!current || current.expiresAt <= now) { - authRateLimitStore.set(key, { count: 1, expiresAt: now + windowMs }); + if (!current || current.expiresAt <= now) { + authRateLimitStore.set(key, { count: 1, expiresAt: now + windowMs }); + return next(); + } + + if (current.count >= maxRequests) { + return res.status(429).send({ + message: 'Too many requests. Please try again later.', + }); + } + + current.count += 1; + authRateLimitStore.set(key, current); return next(); - } - - if (current.count >= maxRequests) { - return res.status(429).send({ - message: 'Too many requests. Please try again later.', - }); - } - - current.count += 1; - authRateLimitStore.set(key, current); - return next(); -}; + }; const signinLimiter = createMemoryRateLimiter({ keyPrefix: 'signin', @@ -71,7 +69,9 @@ function safeParseUrl(value) { function getRequestHost(req) { const uiUrl = safeParseUrl(config.uiUrl); - const fallbackHost = uiUrl ? uiUrl.origin : config.backUrl || 'http://localhost:3000'; + const fallbackHost = uiUrl + ? uiUrl.origin + : config.backUrl || 'http://localhost:3000'; const origin = safeParseUrl(req.headers.origin); const referer = safeParseUrl(req.headers.referer); @@ -142,10 +142,18 @@ function getRequestHost(req) { * x-codegen-request-body-name: body */ -router.post('/signin/local', signinLimiter, wrapAsync(async (req, res) => { - const payload = await AuthService.signin(req.body.email, req.body.password, req,); - res.status(200).send(payload); -})); +router.post( + '/signin/local', + signinLimiter, + wrapAsync(async (req, res) => { + const payload = await AuthService.signin( + req.body.email, + req.body.password, + req, + ); + res.status(200).send(payload); + }), +); /** * @swagger @@ -164,42 +172,69 @@ router.post('/signin/local', signinLimiter, wrapAsync(async (req, res) => { * x-codegen-request-body-name: body */ -router.get('/me', passport.authenticate('jwt', {session: false}), (req, res) => { - if (!req.currentUser || !req.currentUser.id) { - throw new ForbiddenError(); - } +router.get( + '/me', + passport.authenticate('jwt', { session: false }), + (req, res) => { + if (!req.currentUser || !req.currentUser.id) { + throw new ForbiddenError(); + } - const payload = req.currentUser; - delete payload.password; - res.status(200).send(payload); -}); + const payload = req.currentUser; + delete payload.password; + res.status(200).send(payload); + }, +); -router.put('/password-reset', wrapAsync(async (req, res) => { - const payload = await AuthService.passwordReset(req.body.token, req.body.password, req,); - res.status(200).send(payload); -})); +router.put( + '/password-reset', + wrapAsync(async (req, res) => { + const payload = await AuthService.passwordReset( + req.body.token, + req.body.password, + req, + ); + res.status(200).send(payload); + }), +); -router.put('/password-update', passport.authenticate('jwt', {session: false}), wrapAsync(async (req, res) => { - const payload = await AuthService.passwordUpdate(req.body.currentPassword, req.body.newPassword, req); - res.status(200).send(payload); -})); +router.put( + '/password-update', + passport.authenticate('jwt', { session: false }), + wrapAsync(async (req, res) => { + const payload = await AuthService.passwordUpdate( + req.body.currentPassword, + req.body.newPassword, + req, + ); + res.status(200).send(payload); + }), +); -router.post('/send-email-address-verification-email', passport.authenticate('jwt', {session: false}), wrapAsync(async (req, res) => { - if (!req.currentUser) { - throw new ForbiddenError(); - } +router.post( + '/send-email-address-verification-email', + passport.authenticate('jwt', { session: false }), + wrapAsync(async (req, res) => { + if (!req.currentUser) { + throw new ForbiddenError(); + } - await AuthService.sendEmailAddressVerificationEmail(req.currentUser.email); - const payload = true; - res.status(200).send(payload); -})); + await AuthService.sendEmailAddressVerificationEmail(req.currentUser.email); + const payload = true; + res.status(200).send(payload); + }), +); -router.post('/send-password-reset-email', passwordResetLimiter, wrapAsync(async (req, res) => { - const host = getRequestHost(req); - await AuthService.sendPasswordResetEmail(req.body.email, 'register', host); - const payload = true; - res.status(200).send(payload); -})); +router.post( + '/send-password-reset-email', + passwordResetLimiter, + wrapAsync(async (req, res) => { + const host = getRequestHost(req); + await AuthService.sendPasswordResetEmail(req.body.email, 'register', host); + const payload = true; + res.status(200).send(payload); + }), +); /** * @swagger @@ -224,32 +259,47 @@ router.post('/send-password-reset-email', passwordResetLimiter, wrapAsync(async * x-codegen-request-body-name: body */ -router.post('/signup', signupLimiter, wrapAsync(async (req, res) => { - const host = getRequestHost(req); - const payload = await AuthService.signup( +router.post( + '/signup', + signupLimiter, + wrapAsync(async (req, res) => { + const host = getRequestHost(req); + const payload = await AuthService.signup( req.body.email, req.body.password, - + req, host, - ) - res.status(200).send(payload); -})); + ); + res.status(200).send(payload); + }), +); -router.put('/profile', passport.authenticate('jwt', {session: false}), wrapAsync(async (req, res) => { - if (!req.currentUser || !req.currentUser.id) { - throw new ForbiddenError(); - } +router.put( + '/profile', + passport.authenticate('jwt', { session: false }), + wrapAsync(async (req, res) => { + if (!req.currentUser || !req.currentUser.id) { + throw new ForbiddenError(); + } - await AuthService.updateProfile(req.body.profile, req.currentUser); - const payload = true; - res.status(200).send(payload); -})); + await AuthService.updateProfile(req.body.profile, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); -router.put('/verify-email', wrapAsync(async (req, res) => { - const payload = await AuthService.verifyEmail(req.body.token, req, req.headers.referer) - res.status(200).send(payload); -})); +router.put( + '/verify-email', + wrapAsync(async (req, res) => { + const payload = await AuthService.verifyEmail( + req.body.token, + req, + req.headers.referer, + ); + res.status(200).send(payload); + }), +); router.get('/email-configured', (req, res) => { const payload = EmailSender.isConfigured; @@ -257,36 +307,46 @@ router.get('/email-configured', (req, res) => { }); router.get('/signin/google', (req, res, next) => { - passport.authenticate("google", {scope: ["profile", "email"], state: req.query.app})(req, res, next); -}); - -router.get('/signin/google/callback', passport.authenticate("google", {failureRedirect: "/login", session: false}), - - function (req, res) { - socialRedirect(res, req.query.state, req.user.token, config); - } -); - -router.get('/signin/microsoft', (req, res, next) => { - passport.authenticate("microsoft", { - scope: ["https://graph.microsoft.com/user.read openid"], - state: req.query.app + passport.authenticate('google', { + scope: ['profile', 'email'], + state: req.query.app, })(req, res, next); }); -router.get('/signin/microsoft/callback', passport.authenticate("microsoft", { - failureRedirect: "/login", - session: false +router.get( + '/signin/google/callback', + passport.authenticate('google', { + failureRedirect: '/login', + session: false, + }), + + function (req, res) { + socialRedirect(res, req.query.state, req.user.token, config); + }, +); + +router.get('/signin/microsoft', (req, res, next) => { + passport.authenticate('microsoft', { + scope: ['https://graph.microsoft.com/user.read openid'], + state: req.query.app, + })(req, res, next); +}); + +router.get( + '/signin/microsoft/callback', + passport.authenticate('microsoft', { + failureRedirect: '/login', + session: false, }), function (req, res) { socialRedirect(res, req.query.state, req.user.token, config); - } + }, ); router.use('/', require('../helpers').commonErrorHandler); function socialRedirect(res, state, token, config) { - res.redirect(config.uiUrl + "/login?token=" + token); + res.redirect(config.uiUrl + '/login?token=' + token); } module.exports = router; diff --git a/backend/src/routes/element_type_defaults.js b/backend/src/routes/element_type_defaults.js index e071192..f383a64 100644 --- a/backend/src/routes/element_type_defaults.js +++ b/backend/src/routes/element_type_defaults.js @@ -2,6 +2,11 @@ const Element_type_defaultsService = require('../services/element_type_defaults' const Element_type_defaultsDBApi = require('../db/api/element_type_defaults'); const { createEntityRouter } = require('../factories/router.factory'); -module.exports = createEntityRouter('element_type_defaults', Element_type_defaultsService, Element_type_defaultsDBApi, { - permissionEntity: 'page_elements', -}); +module.exports = createEntityRouter( + 'element_type_defaults', + Element_type_defaultsService, + Element_type_defaultsDBApi, + { + permissionEntity: 'page_elements', + }, +); diff --git a/backend/src/routes/file.js b/backend/src/routes/file.js index ec91434..e19c35d 100644 --- a/backend/src/routes/file.js +++ b/backend/src/routes/file.js @@ -31,9 +31,13 @@ router.post('/presign', jsonParser, async (req, res) => { } // Validate that all URLs are strings - const invalidUrls = urls.filter((url) => typeof url !== 'string' || !url.trim()); + const invalidUrls = urls.filter( + (url) => typeof url !== 'string' || !url.trim(), + ); if (invalidUrls.length > 0) { - return res.status(400).json({ error: 'All URLs must be non-empty strings' }); + return res + .status(400) + .json({ error: 'All URLs must be non-empty strings' }); } try { @@ -45,11 +49,15 @@ router.post('/presign', jsonParser, async (req, res) => { } }); -router.post('/upload/:table/:field', passport.authenticate('jwt', {session: false}), (req, res) => { - const fileName = `${req.params.table}/${req.params.field}`; +router.post( + '/upload/:table/:field', + passport.authenticate('jwt', { session: false }), + (req, res) => { + const fileName = `${req.params.table}/${req.params.field}`; - services.uploadFile(fileName, req, res); -}); + services.uploadFile(fileName, req, res); + }, +); router.post( '/upload-sessions/init', diff --git a/backend/src/routes/openai.js b/backend/src/routes/openai.js index b051074..06235c6 100644 --- a/backend/src/routes/openai.js +++ b/backend/src/routes/openai.js @@ -7,17 +7,22 @@ const { getWidget, askGpt } = require('../services/openai'); const { LocalAIApi } = require('../ai/LocalAIApi'); const loadRolesModules = () => { - try { - return { - RolesService: require('../services/roles'), - RolesDBApi: require('../db/api/roles'), - }; - } catch (error) { - console.error('Roles modules are missing. Advanced roles are required for this endpoint.', error); - const err = new Error('Roles modules are missing. Advanced roles are required for this endpoint.'); - err.originalError = error; - throw err; - } + try { + return { + RolesService: require('../services/roles'), + RolesDBApi: require('../db/api/roles'), + }; + } catch (error) { + console.error( + 'Roles modules are missing. Advanced roles are required for this endpoint.', + error, + ); + const err = new Error( + 'Roles modules are missing. Advanced roles are required for this endpoint.', + ); + err.originalError = error; + throw err; + } }; /** @@ -70,18 +75,18 @@ const loadRolesModules = () => { */ router.delete( - '/roles-info/:infoId', - wrapAsync(async (req, res) => { - const { RolesService } = loadRolesModules(); - const role = await RolesService.removeRoleInfoById( - req.query.infoId, - req.query.roleId, - req.query.key, - req.currentUser, - ); + '/roles-info/:infoId', + wrapAsync(async (req, res) => { + const { RolesService } = loadRolesModules(); + const role = await RolesService.removeRoleInfoById( + req.query.infoId, + req.query.roleId, + req.query.key, + req.currentUser, + ); - res.status(200).send(role); - }), + res.status(200).send(role); + }), ); /** @@ -128,83 +133,80 @@ router.delete( */ router.get( - '/info-by-key', - wrapAsync(async (req, res) => { - const { RolesService, RolesDBApi } = loadRolesModules(); - const roleId = req.query.roleId; - const key = req.query.key; - const currentUser = req.currentUser; - let info = await RolesService.getRoleInfoByKey( - key, - roleId, - currentUser, - ); - const role = await RolesDBApi.findBy({ id: roleId }); - if (!role?.role_customization) { - try { - await Promise.all(["pie", "bar"].map(async (e) => { - const schema = await sjs.getSequelizeSchema(db.sequelize, {}); - const payload = { - description: `Create some cool ${e} chart`, - modelDefinition: schema.definitions, - }; - const widgetId = await getWidget(payload, currentUser?.id, roleId); - if (widgetId) { - await RolesService.addRoleInfo( - roleId, - currentUser?.id, - 'widgets', - widgetId, - req.currentUser, - ); - } - })); - info = await RolesService.getRoleInfoByKey( - key, - roleId, - currentUser, - ); - } catch (error) { - console.warn('Widget creation skipped (external API unavailable):', error.message); - // Return empty widgets when external API is unavailable - if (key === 'widgets') { - info = []; - } + '/info-by-key', + wrapAsync(async (req, res) => { + const { RolesService, RolesDBApi } = loadRolesModules(); + const roleId = req.query.roleId; + const key = req.query.key; + const currentUser = req.currentUser; + let info = await RolesService.getRoleInfoByKey(key, roleId, currentUser); + const role = await RolesDBApi.findBy({ id: roleId }); + if (!role?.role_customization) { + try { + await Promise.all( + ['pie', 'bar'].map(async (e) => { + const schema = await sjs.getSequelizeSchema(db.sequelize, {}); + const payload = { + description: `Create some cool ${e} chart`, + modelDefinition: schema.definitions, + }; + const widgetId = await getWidget(payload, currentUser?.id, roleId); + if (widgetId) { + await RolesService.addRoleInfo( + roleId, + currentUser?.id, + 'widgets', + widgetId, + req.currentUser, + ); } + }), + ); + info = await RolesService.getRoleInfoByKey(key, roleId, currentUser); + } catch (error) { + console.warn( + 'Widget creation skipped (external API unavailable):', + error.message, + ); + // Return empty widgets when external API is unavailable + if (key === 'widgets') { + info = []; } - res.status(200).send(info); - }), + } + } + res.status(200).send(info); + }), ); router.post( - '/create_widget', - wrapAsync(async (req, res) => { - const { RolesService } = loadRolesModules(); - const { description, userId, roleId } = req.body; + '/create_widget', + wrapAsync(async (req, res) => { + const { RolesService } = loadRolesModules(); + const { description, userId, roleId } = req.body; - const currentUser = req.currentUser; - const schema = await sjs.getSequelizeSchema(db.sequelize, {}); - const payload = { - description, - modelDefinition: schema.definitions, - }; + const currentUser = req.currentUser; + const schema = await sjs.getSequelizeSchema(db.sequelize, {}); + const payload = { + description, + modelDefinition: schema.definitions, + }; - const widgetId = await getWidget(payload, userId, roleId); + const widgetId = await getWidget(payload, userId, roleId); - if (widgetId) { - await RolesService.addRoleInfo( - roleId, - userId, - 'widgets', - widgetId, - currentUser, - ); + if (widgetId) { + await RolesService.addRoleInfo( + roleId, + userId, + 'widgets', + widgetId, + currentUser, + ); - return res.status(200).send(widgetId); - } else { - return res.status(400).send(widgetId); - } - }), + return res.status(200).send(widgetId); + } else { + return res.status(400).send(widgetId); + } + }), ); /** @@ -252,23 +254,23 @@ router.post( * description: Proxy error */ router.post( - '/response', - wrapAsync(async (req, res) => { - const body = req.body || {}; - const options = body.options || {}; - const payload = { ...body }; - delete payload.options; + '/response', + wrapAsync(async (req, res) => { + const body = req.body || {}; + const options = body.options || {}; + const payload = { ...body }; + delete payload.options; - const response = await LocalAIApi.createResponse(payload, options); + const response = await LocalAIApi.createResponse(payload, options); - if (response.success) { - return res.status(200).send(response); - } + if (response.success) { + return res.status(200).send(response); + } - console.error('AI proxy error:', response); - const status = response.error === 'input_missing' ? 400 : 502; - return res.status(status).send(response); - }), + console.error('AI proxy error:', response); + const status = response.error === 'input_missing' ? 400 : 502; + return res.status(status).send(response); + }), ); /** @@ -312,25 +314,24 @@ router.post( * description: Some server error */ router.post( - '/ask-gpt', - wrapAsync(async (req, res) => { - const { prompt } = req.body; - if (!prompt) { - return res.status(400).send({ - success: false, - error: 'Prompt is required', - }); - } + '/ask-gpt', + wrapAsync(async (req, res) => { + const { prompt } = req.body; + if (!prompt) { + return res.status(400).send({ + success: false, + error: 'Prompt is required', + }); + } - const response = await askGpt(prompt); + const response = await askGpt(prompt); - if (response.success) { - return res.status(200).send(response); - } else { - return res.status(500).send(response); - } - }), + if (response.success) { + return res.status(200).send(response); + } else { + return res.status(500).send(response); + } + }), ); - module.exports = router; diff --git a/backend/src/routes/organizationLogin.js b/backend/src/routes/organizationLogin.js index 139597f..e69de29 100644 --- a/backend/src/routes/organizationLogin.js +++ b/backend/src/routes/organizationLogin.js @@ -1,2 +0,0 @@ - - diff --git a/backend/src/routes/permissions.js b/backend/src/routes/permissions.js index 98edd5a..b73fa37 100644 --- a/backend/src/routes/permissions.js +++ b/backend/src/routes/permissions.js @@ -181,4 +181,8 @@ const { createEntityRouter } = require('../factories/router.factory'); * description: Some server error */ -module.exports = createEntityRouter('permissions', PermissionsService, PermissionsDBApi); +module.exports = createEntityRouter( + 'permissions', + PermissionsService, + PermissionsDBApi, +); diff --git a/backend/src/routes/presigned_url_requests.js b/backend/src/routes/presigned_url_requests.js index 2752a7f..c6afed1 100644 --- a/backend/src/routes/presigned_url_requests.js +++ b/backend/src/routes/presigned_url_requests.js @@ -143,4 +143,8 @@ const { createEntityRouter } = require('../factories/router.factory'); * description: Some server error */ -module.exports = createEntityRouter('presigned_url_requests', Presigned_url_requestsService, Presigned_url_requestsDBApi); +module.exports = createEntityRouter( + 'presigned_url_requests', + Presigned_url_requestsService, + Presigned_url_requestsDBApi, +); diff --git a/backend/src/routes/project_audio_tracks.js b/backend/src/routes/project_audio_tracks.js index a148ca2..138865d 100644 --- a/backend/src/routes/project_audio_tracks.js +++ b/backend/src/routes/project_audio_tracks.js @@ -144,4 +144,8 @@ const { createEntityRouter } = require('../factories/router.factory'); * description: Some server error */ -module.exports = createEntityRouter('project_audio_tracks', Project_audio_tracksService, Project_audio_tracksDBApi); +module.exports = createEntityRouter( + 'project_audio_tracks', + Project_audio_tracksService, + Project_audio_tracksDBApi, +); diff --git a/backend/src/routes/project_element_defaults.js b/backend/src/routes/project_element_defaults.js index 9ffe6f6..fe8c751 100644 --- a/backend/src/routes/project_element_defaults.js +++ b/backend/src/routes/project_element_defaults.js @@ -10,7 +10,7 @@ const baseRouter = createEntityRouter( Project_element_defaultsDBApi, { permissionEntity: 'page_elements', - } + }, ); /** @@ -40,10 +40,10 @@ baseRouter.post( wrapAsync(async (req, res) => { const payload = await Project_element_defaultsService.resetToGlobal( req.params.id, - { currentUser: req.currentUser } + { currentUser: req.currentUser }, ); res.status(200).json(payload); - }) + }), ); /** @@ -72,10 +72,10 @@ baseRouter.get( '/:id/diff', wrapAsync(async (req, res) => { const payload = await Project_element_defaultsService.getDiffFromGlobal( - req.params.id + req.params.id, ); res.status(200).json(payload); - }) + }), ); module.exports = baseRouter; diff --git a/backend/src/routes/project_memberships.js b/backend/src/routes/project_memberships.js index b7f22e2..a7d918d 100644 --- a/backend/src/routes/project_memberships.js +++ b/backend/src/routes/project_memberships.js @@ -138,4 +138,8 @@ const { createEntityRouter } = require('../factories/router.factory'); * description: Some server error */ -module.exports = createEntityRouter('project_memberships', Project_membershipsService, Project_membershipsDBApi); +module.exports = createEntityRouter( + 'project_memberships', + Project_membershipsService, + Project_membershipsDBApi, +); diff --git a/backend/src/routes/projects.js b/backend/src/routes/projects.js index ae5dd22..03d0d22 100644 --- a/backend/src/routes/projects.js +++ b/backend/src/routes/projects.js @@ -1,23 +1,17 @@ - const express = require('express'); const ProjectsService = require('../services/projects'); const ProjectsDBApi = require('../db/api/projects'); const { wrapAsync, isUuidV4 } = require('../helpers'); - const router = express.Router(); const { parse } = require('json2csv'); - -const { - checkCrudPermissions, -} = require('../middlewares/check-permissions'); +const { checkCrudPermissions } = require('../middlewares/check-permissions'); router.use(checkCrudPermissions('projects')); - /** * @swagger * components: @@ -67,53 +61,69 @@ router.use(checkCrudPermissions('projects')); */ /** -* @swagger -* /api/projects: -* post: -* security: -* - bearerAuth: [] -* tags: [Projects] -* summary: Add new item -* description: Add new item -* requestBody: -* required: true -* content: -* application/json: -* schema: -* properties: -* data: -* description: Data of the updated item -* type: object -* $ref: "#/components/schemas/Projects" -* responses: -* 200: -* description: The item was successfully added -* content: -* application/json: -* schema: -* $ref: "#/components/schemas/Projects" -* 401: -* $ref: "#/components/responses/UnauthorizedError" -* 405: -* description: Invalid input data -* 500: -* description: Some server error -*/ -router.post('/', wrapAsync(async (req, res) => { - const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + * @swagger + * /api/projects: + * post: + * security: + * - bearerAuth: [] + * tags: [Projects] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Projects" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Projects" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; const link = new URL(referer); - const payload = await ProjectsService.create(req.body.data, req.currentUser, true, link.host); + const payload = await ProjectsService.create( + req.body.data, + req.currentUser, + true, + link.host, + ); res.status(200).send(payload); -})); + }), +); -router.post('/:id/clone', wrapAsync(async (req, res) => { +router.post( + '/:id/clone', + wrapAsync(async (req, res) => { if (!isUuidV4(req.params.id)) { return res.status(400).send('Invalid project id'); } - const payload = await ProjectsService.cloneFromProject(req.params.id, req.currentUser); + const payload = await ProjectsService.cloneFromProject( + req.params.id, + req.currentUser, + ); res.status(200).send(payload); -})); + }), +); /** * @swagger @@ -150,193 +160,220 @@ router.post('/:id/clone', wrapAsync(async (req, res) => { * description: Some server error * */ -router.post('/bulk-import', wrapAsync(async (req, res) => { - const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; const link = new URL(referer); await ProjectsService.bulkImport(req, res, true, link.host); const payload = true; res.status(200).send(payload); -})); + }), +); /** - * @swagger - * /api/projects/{id}: - * put: - * security: - * - bearerAuth: [] - * tags: [Projects] - * summary: Update the data of the selected item - * description: Update the data of the selected item - * parameters: - * - in: path - * name: id - * description: Item ID to update - * required: true - * schema: - * type: string - * requestBody: - * description: Set new item data - * required: true - * content: - * application/json: - * schema: - * properties: - * id: - * description: ID of the updated item - * type: string - * data: - * description: Data of the updated item - * type: object - * $ref: "#/components/schemas/Projects" - * required: - * - id - * responses: - * 200: - * description: The item data was successfully updated - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Projects" - * 400: - * description: Invalid ID supplied - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Item not found - * 500: - * description: Some server error - */ -router.put('/:id', wrapAsync(async (req, res) => { - await ProjectsService.update(req.body.data, req.body.id, req.currentUser); - const payload = true; - res.status(200).send(payload); -})); + * @swagger + * /api/projects/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Projects] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Projects" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Projects" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await ProjectsService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); /** - * @swagger - * /api/projects/{id}: - * delete: - * security: - * - bearerAuth: [] - * tags: [Projects] - * summary: Delete the selected item - * description: Delete the selected item - * parameters: - * - in: path - * name: id - * description: Item ID to delete - * required: true - * schema: - * type: string - * responses: - * 200: - * description: The item was successfully deleted - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Projects" - * 400: - * description: Invalid ID supplied - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Item not found - * 500: - * description: Some server error - */ -router.delete('/:id', wrapAsync(async (req, res) => { - await ProjectsService.remove(req.params.id, req.currentUser); - const payload = true; - res.status(200).send(payload); -})); + * @swagger + * /api/projects/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Projects] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Projects" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await ProjectsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); /** - * @swagger - * /api/projects/deleteByIds: - * post: - * security: - * - bearerAuth: [] - * tags: [Projects] - * summary: Delete the selected item list - * description: Delete the selected item list - * requestBody: - * required: true - * content: - * application/json: - * schema: - * properties: - * ids: - * description: IDs of the updated items - * type: array - * responses: - * 200: - * description: The items was successfully deleted - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Projects" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Items not found - * 500: - * description: Some server error - */ -router.post('/deleteByIds', wrapAsync(async (req, res) => { + * @swagger + * /api/projects/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Projects] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Projects" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { await ProjectsService.deleteByIds(req.body.data, req.currentUser); const payload = true; res.status(200).send(payload); - })); + }), +); /** - * @swagger - * /api/projects: - * get: - * security: - * - bearerAuth: [] - * tags: [Projects] - * summary: Get all projects - * description: Get all projects - * responses: - * 200: - * description: Projects list successfully received - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: "#/components/schemas/Projects" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Data not found - * 500: - * description: Some server error -*/ -router.get('/', wrapAsync(async (req, res) => { - const filetype = req.query.filetype - - const currentUser = req.currentUser; - const runtimeContext = req.runtimeContext; - const payload = await ProjectsDBApi.findAll( - req.query, { currentUser, runtimeContext } - ); - if (filetype && filetype === 'csv') { - const fields = ['id','name','slug','description','logo_url','favicon_url','og_image_url','theme_config_json','custom_css_json','cdn_base_url']; - const opts = { fields }; - try { - const csv = parse(payload.rows, opts); - res.status(200).attachment(csv); - res.send(csv) + * @swagger + * /api/projects: + * get: + * security: + * - bearerAuth: [] + * tags: [Projects] + * summary: Get all projects + * description: Get all projects + * responses: + * 200: + * description: Projects list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Projects" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; - } catch (err) { - console.error(err); + const currentUser = req.currentUser; + const runtimeContext = req.runtimeContext; + const payload = await ProjectsDBApi.findAll(req.query, { + currentUser, + runtimeContext, + }); + if (filetype && filetype === 'csv') { + const fields = [ + 'id', + 'name', + 'slug', + 'description', + 'logo_url', + 'favicon_url', + 'og_image_url', + 'theme_config_json', + 'custom_css_json', + 'cdn_base_url', + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); } - } else { - res.status(200).send(payload); - } - -})); + }), +); /** * @swagger @@ -363,17 +400,20 @@ router.get('/', wrapAsync(async (req, res) => { * 500: * description: Some server error */ -router.get('/count', wrapAsync(async (req, res) => { - +router.get( + '/count', + wrapAsync(async (req, res) => { const currentUser = req.currentUser; const runtimeContext = req.runtimeContext; - const payload = await ProjectsDBApi.findAll( - req.query, - { countOnly: true, currentUser, runtimeContext } - ); + const payload = await ProjectsDBApi.findAll(req.query, { + countOnly: true, + currentUser, + runtimeContext, + }); res.status(200).send(payload); -})); + }), +); /** * @swagger @@ -401,64 +441,63 @@ router.get('/count', wrapAsync(async (req, res) => { * description: Some server error */ router.get('/autocomplete', async (req, res) => { - const payload = await ProjectsDBApi.findAllAutocomplete( req.query.query, req.query.limit, req.query.offset, - ); res.status(200).send(payload); }); /** - * @swagger - * /api/projects/{id}: - * get: - * security: - * - bearerAuth: [] - * tags: [Projects] - * summary: Get selected item - * description: Get selected item - * parameters: - * - in: path - * name: id - * description: ID of item to get - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Selected item successfully received - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Projects" - * 400: - * description: Invalid ID supplied - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Item not found - * 500: - * description: Some server error - */ -router.get('/:id', wrapAsync(async (req, res) => { - if (!isUuidV4(req.params.id)) { - return res.status(400).send('Invalid project id'); - } + * @swagger + * /api/projects/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Projects] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Projects" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + if (!isUuidV4(req.params.id)) { + return res.status(400).send('Invalid project id'); + } - const runtimeContext = req.runtimeContext; - const payload = await ProjectsDBApi.findBy( - { id: req.params.id }, - { runtimeContext }, - ); + const runtimeContext = req.runtimeContext; + const payload = await ProjectsDBApi.findBy( + { id: req.params.id }, + { runtimeContext }, + ); - - - res.status(200).send(payload); -})); + res.status(200).send(payload); + }), +); /** * @swagger @@ -493,21 +532,24 @@ router.get('/:id', wrapAsync(async (req, res) => { * 500: * description: Server error */ -router.get('/:id/offline-manifest', wrapAsync(async (req, res) => { - if (!isUuidV4(req.params.id)) { - return res.status(400).send('Invalid project id'); - } +router.get( + '/:id/offline-manifest', + wrapAsync(async (req, res) => { + if (!isUuidV4(req.params.id)) { + return res.status(400).send('Invalid project id'); + } - const PWAManifestService = require('../services/pwa_manifest'); - const { variant = 'desktop' } = req.query; + const PWAManifestService = require('../services/pwa_manifest'); + const { variant = 'desktop' } = req.query; - const manifest = await PWAManifestService.generateManifest( - req.params.id, - variant - ); + const manifest = await PWAManifestService.generateManifest( + req.params.id, + variant, + ); - res.status(200).json(manifest); -})); + res.status(200).json(manifest); + }), +); router.use('/', require('../helpers').commonErrorHandler); diff --git a/backend/src/routes/publish.js b/backend/src/routes/publish.js index 3355b86..ea3fa06 100644 --- a/backend/src/routes/publish.js +++ b/backend/src/routes/publish.js @@ -9,7 +9,12 @@ router.use(checkCrudPermissions('publish_events')); const publishHandler = wrapAsync(async (req, res) => { const { projectId, title, description } = req.body; - const result = await PublishService.publishToProduction(projectId, req.currentUser, title, description); + const result = await PublishService.publishToProduction( + projectId, + req.currentUser, + title, + description, + ); res.status(200).send(result); }); diff --git a/backend/src/routes/publish_events.js b/backend/src/routes/publish_events.js index 1f9a197..e73d8ad 100644 --- a/backend/src/routes/publish_events.js +++ b/backend/src/routes/publish_events.js @@ -150,4 +150,8 @@ const { createEntityRouter } = require('../factories/router.factory'); * description: Some server error */ -module.exports = createEntityRouter('publish_events', Publish_eventsService, Publish_eventsDBApi); +module.exports = createEntityRouter( + 'publish_events', + Publish_eventsService, + Publish_eventsDBApi, +); diff --git a/backend/src/routes/pwa_caches.js b/backend/src/routes/pwa_caches.js index d4c7135..268731a 100644 --- a/backend/src/routes/pwa_caches.js +++ b/backend/src/routes/pwa_caches.js @@ -141,4 +141,8 @@ const { createEntityRouter } = require('../factories/router.factory'); * description: Some server error */ -module.exports = createEntityRouter('pwa_caches', Pwa_cachesService, Pwa_cachesDBApi); +module.exports = createEntityRouter( + 'pwa_caches', + Pwa_cachesService, + Pwa_cachesDBApi, +); diff --git a/backend/src/routes/runtime-context.js b/backend/src/routes/runtime-context.js index 015f92f..5149c1f 100644 --- a/backend/src/routes/runtime-context.js +++ b/backend/src/routes/runtime-context.js @@ -3,7 +3,9 @@ const express = require('express'); const router = express.Router(); router.get('/', (req, res) => { - res.status(200).send(req.runtimeContext || { mode: 'unknown', projectSlug: null }); + res + .status(200) + .send(req.runtimeContext || { mode: 'unknown', projectSlug: null }); }); module.exports = router; diff --git a/backend/src/routes/search.js b/backend/src/routes/search.js index 164b376..b848adb 100644 --- a/backend/src/routes/search.js +++ b/backend/src/routes/search.js @@ -1,7 +1,6 @@ const express = require('express'); const SearchService = require('../services/search'); - const router = express.Router(); const { checkCrudPermissions } = require('../middlewares/check-permissions'); @@ -34,19 +33,22 @@ router.use(checkCrudPermissions('search')); */ router.post('/', async (req, res) => { - const { searchQuery } = req.body; - - if (!searchQuery) { - return res.status(400).json({ error: 'Please enter a search query' }); - } - - try { - const foundMatches = await SearchService.search(searchQuery, req.currentUser ); - res.json(foundMatches); - } catch (error) { - console.error('Internal Server Error', error); - res.status(500).json({ error: 'Internal Server Error' }); - } - }); + const { searchQuery } = req.body; -module.exports = router; \ No newline at end of file + if (!searchQuery) { + return res.status(400).json({ error: 'Please enter a search query' }); + } + + try { + const foundMatches = await SearchService.search( + searchQuery, + req.currentUser, + ); + res.json(foundMatches); + } catch (error) { + console.error('Internal Server Error', error); + res.status(500).json({ error: 'Internal Server Error' }); + } +}); + +module.exports = router; diff --git a/backend/src/routes/sql.js b/backend/src/routes/sql.js index 3325499..c2307c4 100644 --- a/backend/src/routes/sql.js +++ b/backend/src/routes/sql.js @@ -71,13 +71,16 @@ router.post( wrapAsync(async (req, res) => { const { currentUser } = req; const isAdminUser = Boolean( - currentUser - && currentUser.app_role - && (currentUser.app_role.name === 'Administrator' || currentUser.app_role.globalAccess === true), + currentUser && + currentUser.app_role && + (currentUser.app_role.name === 'Administrator' || + currentUser.app_role.globalAccess === true), ); if (!isAdminUser) { - return res.status(403).json({ error: 'Only administrators can execute SQL queries' }); + return res + .status(403) + .json({ error: 'Only administrators can execute SQL queries' }); } const { sql } = req.body; @@ -90,7 +93,10 @@ router.post( const wrappedSql = `SELECT * FROM (${normalized}) AS query_result LIMIT ${MAX_SQL_ROWS}`; const rows = await db.sequelize.transaction(async (transaction) => { - await db.sequelize.query(`SET LOCAL statement_timeout = ${SQL_TIMEOUT_MS}`, { transaction }); + await db.sequelize.query( + `SET LOCAL statement_timeout = ${SQL_TIMEOUT_MS}`, + { transaction }, + ); return db.sequelize.query(wrappedSql, { transaction, type: db.Sequelize.QueryTypes.SELECT, diff --git a/backend/src/routes/tour_pages.js b/backend/src/routes/tour_pages.js index ed1da62..c0044f2 100644 --- a/backend/src/routes/tour_pages.js +++ b/backend/src/routes/tour_pages.js @@ -148,4 +148,8 @@ const { createEntityRouter } = require('../factories/router.factory'); * description: Some server error */ -module.exports = createEntityRouter('tour_pages', Tour_pagesService, Tour_pagesDBApi); +module.exports = createEntityRouter( + 'tour_pages', + Tour_pagesService, + Tour_pagesDBApi, +); diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js index 19df9ae..da6a403 100644 --- a/backend/src/routes/users.js +++ b/backend/src/routes/users.js @@ -1,23 +1,17 @@ - const express = require('express'); const UsersService = require('../services/users'); const UsersDBApi = require('../db/api/users'); const wrapAsync = require('../helpers').wrapAsync; - const router = express.Router(); const { parse } = require('json2csv'); - -const { - checkCrudPermissions, -} = require('../middlewares/check-permissions'); +const { checkCrudPermissions } = require('../middlewares/check-permissions'); router.use(checkCrudPermissions('users')); - /** * @swagger * components: @@ -51,45 +45,50 @@ router.use(checkCrudPermissions('users')); */ /** -* @swagger -* /api/users: -* post: -* security: -* - bearerAuth: [] -* tags: [Users] -* summary: Add new item -* description: Add new item -* requestBody: -* required: true -* content: -* application/json: -* schema: -* properties: -* data: -* description: Data of the updated item -* type: object -* $ref: "#/components/schemas/Users" -* responses: -* 200: -* description: The item was successfully added -* content: -* application/json: -* schema: -* $ref: "#/components/schemas/Users" -* 401: -* $ref: "#/components/responses/UnauthorizedError" -* 405: -* description: Invalid input data -* 500: -* description: Some server error -*/ -router.post('/', wrapAsync(async (req, res) => { - const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + * @swagger + * /api/users: + * post: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Add new item + * description: Add new item + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Users" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Users" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + */ +router.post( + '/', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; const link = new URL(referer); await UsersService.create(req.body.data, req.currentUser, true, link.host); const payload = true; res.status(200).send(payload); -})); + }), +); /** * @swagger @@ -126,196 +125,205 @@ router.post('/', wrapAsync(async (req, res) => { * description: Some server error * */ -router.post('/bulk-import', wrapAsync(async (req, res) => { - const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; +router.post( + '/bulk-import', + wrapAsync(async (req, res) => { + const referer = + req.headers.referer || + `${req.protocol}://${req.hostname}${req.originalUrl}`; const link = new URL(referer); await UsersService.bulkImport(req, res, true, link.host); const payload = true; res.status(200).send(payload); -})); + }), +); /** - * @swagger - * /api/users/{id}: - * put: - * security: - * - bearerAuth: [] - * tags: [Users] - * summary: Update the data of the selected item - * description: Update the data of the selected item - * parameters: - * - in: path - * name: id - * description: Item ID to update - * required: true - * schema: - * type: string - * requestBody: - * description: Set new item data - * required: true - * content: - * application/json: - * schema: - * properties: - * id: - * description: ID of the updated item - * type: string - * data: - * description: Data of the updated item - * type: object - * $ref: "#/components/schemas/Users" - * required: - * - id - * responses: - * 200: - * description: The item data was successfully updated - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Users" - * 400: - * description: Invalid ID supplied - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Item not found - * 500: - * description: Some server error - */ -router.put('/:id', wrapAsync(async (req, res) => { - await UsersService.update(req.body.data, req.body.id, req.currentUser); - const payload = true; - res.status(200).send(payload); -})); + * @swagger + * /api/users/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Users" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Users" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put( + '/:id', + wrapAsync(async (req, res) => { + await UsersService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); /** - * @swagger - * /api/users/{id}: - * delete: - * security: - * - bearerAuth: [] - * tags: [Users] - * summary: Delete the selected item - * description: Delete the selected item - * parameters: - * - in: path - * name: id - * description: Item ID to delete - * required: true - * schema: - * type: string - * responses: - * 200: - * description: The item was successfully deleted - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Users" - * 400: - * description: Invalid ID supplied - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Item not found - * 500: - * description: Some server error - */ -router.delete('/:id', wrapAsync(async (req, res) => { - await UsersService.remove(req.params.id, req.currentUser); - const payload = true; - res.status(200).send(payload); -})); + * @swagger + * /api/users/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Users" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete( + '/:id', + wrapAsync(async (req, res) => { + await UsersService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); /** - * @swagger - * /api/users/deleteByIds: - * post: - * security: - * - bearerAuth: [] - * tags: [Users] - * summary: Delete the selected item list - * description: Delete the selected item list - * requestBody: - * required: true - * content: - * application/json: - * schema: - * properties: - * ids: - * description: IDs of the updated items - * type: array - * responses: - * 200: - * description: The items was successfully deleted - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Users" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Items not found - * 500: - * description: Some server error - */ -router.post('/deleteByIds', wrapAsync(async (req, res) => { + * @swagger + * /api/users/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Users" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { await UsersService.deleteByIds(req.body.data, req.currentUser); const payload = true; res.status(200).send(payload); - })); + }), +); /** - * @swagger - * /api/users: - * get: - * security: - * - bearerAuth: [] - * tags: [Users] - * summary: Get all users - * description: Get all users - * responses: - * 200: - * description: Users list successfully received - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: "#/components/schemas/Users" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Data not found - * 500: - * description: Some server error -*/ -router.get('/', wrapAsync(async (req, res) => { - const filetype = req.query.filetype - - const currentUser = req.currentUser; - const payload = await UsersDBApi.findAll( - req.query, { currentUser } - ); - if (filetype && filetype === 'csv') { - const fields = ['id','firstName','lastName','phoneNumber','email', - - - - ]; - const opts = { fields }; - try { - const csv = parse(payload.rows, opts); - res.status(200).attachment(csv); - res.send(csv) + * @swagger + * /api/users: + * get: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Get all users + * description: Get all users + * responses: + * 200: + * description: Users list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Users" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/', + wrapAsync(async (req, res) => { + const filetype = req.query.filetype; - } catch (err) { - console.error(err); + const currentUser = req.currentUser; + const payload = await UsersDBApi.findAll(req.query, { currentUser }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'firstName', 'lastName', 'phoneNumber', 'email']; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv); + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); } - } else { - res.status(200).send(payload); - } - -})); + }), +); /** * @swagger @@ -342,17 +350,18 @@ router.get('/', wrapAsync(async (req, res) => { * 500: * description: Some server error */ -router.get('/count', wrapAsync(async (req, res) => { - +router.get( + '/count', + wrapAsync(async (req, res) => { const currentUser = req.currentUser; - const payload = await UsersDBApi.findAll( - req.query, - null, - { countOnly: true, currentUser } - ); + const payload = await UsersDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); res.status(200).send(payload); -})); + }), +); /** * @swagger @@ -380,60 +389,57 @@ router.get('/count', wrapAsync(async (req, res) => { * description: Some server error */ router.get('/autocomplete', async (req, res) => { - const payload = await UsersDBApi.findAllAutocomplete( req.query.query, req.query.limit, req.query.offset, - ); res.status(200).send(payload); }); /** - * @swagger - * /api/users/{id}: - * get: - * security: - * - bearerAuth: [] - * tags: [Users] - * summary: Get selected item - * description: Get selected item - * parameters: - * - in: path - * name: id - * description: ID of item to get - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Selected item successfully received - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Users" - * 400: - * description: Invalid ID supplied - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Item not found - * 500: - * description: Some server error - */ -router.get('/:id', wrapAsync(async (req, res) => { - const payload = await UsersDBApi.findBy( - { id: req.params.id }, - ); - - - delete payload.password; - + * @swagger + * /api/users/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Users] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Users" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get( + '/:id', + wrapAsync(async (req, res) => { + const payload = await UsersDBApi.findBy({ id: req.params.id }); - res.status(200).send(payload); -})); + delete payload.password; + + res.status(200).send(payload); + }), +); router.use('/', require('../helpers').commonErrorHandler); diff --git a/backend/src/services/auth.js b/backend/src/services/auth.js index 18688ce..0fc416f 100644 --- a/backend/src/services/auth.js +++ b/backend/src/services/auth.js @@ -4,7 +4,7 @@ const ValidationError = require('./notifications/errors/validation'); const ForbiddenError = require('./notifications/errors/forbidden'); const bcrypt = require('bcrypt'); const EmailAddressVerificationEmail = require('./email/list/addressVerification'); -const InvitationEmail = require("./email/list/invitation"); +const InvitationEmail = require('./email/list/invitation'); const PasswordResetEmail = require('./email/list/passwordReset'); const EmailSender = require('./email'); const config = require('../config'); @@ -12,7 +12,7 @@ const helpers = require('../helpers'); class Auth { static async signup(email, password, options = {}, host) { - const user = await UsersDBApi.findBy({email}); + const user = await UsersDBApi.findBy({ email }); const hashedPassword = await bcrypt.hash( password, @@ -21,35 +21,24 @@ class Auth { if (user) { if (user.authenticationUid) { - throw new ValidationError( - 'auth.emailAlreadyInUse', - ); + throw new ValidationError('auth.emailAlreadyInUse'); } if (user.disabled) { - throw new ValidationError( - 'auth.userDisabled', - ); + throw new ValidationError('auth.userDisabled'); } - await UsersDBApi.updatePassword( - user.id, - hashedPassword, - options, - ); + await UsersDBApi.updatePassword(user.id, hashedPassword, options); if (EmailSender.isConfigured) { - await this.sendEmailAddressVerificationEmail( - user.email, - host, - ); + await this.sendEmailAddressVerificationEmail(user.email, host); } const data = { user: { id: user.id, - email: user.email - } + email: user.email, + }, }; return helpers.jwtSign(data); @@ -60,47 +49,37 @@ class Auth { firstName: email.split('@')[0], password: hashedPassword, email: email, - }, options, ); if (EmailSender.isConfigured) { - await this.sendEmailAddressVerificationEmail( - newUser.email, - host, - ); + await this.sendEmailAddressVerificationEmail(newUser.email, host); } const data = { user: { id: newUser.id, - email: newUser.email - } + email: newUser.email, + }, }; return helpers.jwtSign(data); } static async signin(email, password) { - const user = await UsersDBApi.findBy({email}); + const user = await UsersDBApi.findBy({ email }); if (!user) { - throw new ValidationError( - 'auth.userNotFound', - ); + throw new ValidationError('auth.userNotFound'); } if (user.disabled) { - throw new ValidationError( - 'auth.userDisabled', - ); + throw new ValidationError('auth.userDisabled'); } if (!user.password) { - throw new ValidationError( - 'auth.wrongPassword', - ); + throw new ValidationError('auth.wrongPassword'); } if (!EmailSender.isConfigured) { @@ -108,49 +87,33 @@ class Auth { } if (!user.emailVerified) { - throw new ValidationError( - 'auth.userNotVerified', - ); + throw new ValidationError('auth.userNotVerified'); } - const passwordsMatch = await bcrypt.compare( - password, - user.password, - ); + const passwordsMatch = await bcrypt.compare(password, user.password); if (!passwordsMatch) { - throw new ValidationError( - 'auth.wrongPassword', - ); + throw new ValidationError('auth.wrongPassword'); } const data = { user: { id: user.id, - email: user.email - } + email: user.email, + }, }; return helpers.jwtSign(data); } - static async sendEmailAddressVerificationEmail( - email, - host, - ) { - - + static async sendEmailAddressVerificationEmail(email, host) { let link; try { - const token = await UsersDBApi.generateEmailVerificationToken( - email, - ); + const token = await UsersDBApi.generateEmailVerificationToken(email); link = `${host}/verify-email?token=${token}`; } catch (error) { console.error(error); - throw new ValidationError( - 'auth.emailAddressVerificationEmail.error', - ); + throw new ValidationError('auth.emailAddressVerificationEmail.error'); } const emailAddressVerificationEmail = new EmailAddressVerificationEmail( @@ -158,50 +121,33 @@ class Auth { link, ); - return new EmailSender( - emailAddressVerificationEmail, - ).send(); + return new EmailSender(emailAddressVerificationEmail).send(); } static async sendPasswordResetEmail(email, type = 'register', host) { - - let link; try { - const token = await UsersDBApi.generatePasswordResetToken( - email, - ); + const token = await UsersDBApi.generatePasswordResetToken(email); link = `${host}/password-reset?token=${token}`; } catch (error) { console.error(error); - throw new ValidationError( - 'auth.passwordReset.error', - ); + throw new ValidationError('auth.passwordReset.error'); } let passwordResetEmail; if (type === 'register') { - passwordResetEmail = new PasswordResetEmail( - email, - link, - ); + passwordResetEmail = new PasswordResetEmail(email, link); } if (type === 'invitation') { - passwordResetEmail = new InvitationEmail( - email, - link, - ); + passwordResetEmail = new InvitationEmail(email, link); } return new EmailSender(passwordResetEmail).send(); } static async verifyEmail(token, options = {}) { - const user = await UsersDBApi.findByEmailVerificationToken( - token, - options, - ); + const user = await UsersDBApi.findByEmailVerificationToken(token, options); if (!user) { throw new ValidationError( @@ -209,10 +155,7 @@ class Auth { ); } - return UsersDBApi.markEmailVerified( - user.id, - options, - ); + return UsersDBApi.markEmailVerified(user.id, options); } static async passwordUpdate(currentPassword, newPassword, options) { @@ -227,9 +170,7 @@ class Auth { ); if (!currentPasswordMatch) { - throw new ValidationError( - 'auth.wrongPassword' - ) + throw new ValidationError('auth.wrongPassword'); } const newPasswordMatch = await bcrypt.compare( @@ -238,9 +179,7 @@ class Auth { ); if (newPasswordMatch) { - throw new ValidationError( - 'auth.passwordUpdate.samePassword' - ) + throw new ValidationError('auth.passwordUpdate.samePassword'); } const hashedPassword = await bcrypt.hash( @@ -248,27 +187,14 @@ class Auth { config.bcrypt.saltRounds, ); - return UsersDBApi.updatePassword( - currentUser.id, - hashedPassword, - options, - ); + return UsersDBApi.updatePassword(currentUser.id, hashedPassword, options); } - static async passwordReset( - token, - password, - options = {}, - ) { - const user = await UsersDBApi.findByPasswordResetToken( - token, - options, - ); + static async passwordReset(token, password, options = {}) { + const user = await UsersDBApi.findByPasswordResetToken(token, options); if (!user) { - throw new ValidationError( - 'auth.passwordReset.invalidToken', - ); + throw new ValidationError('auth.passwordReset.invalidToken'); } const hashedPassword = await bcrypt.hash( @@ -276,31 +202,19 @@ class Auth { config.bcrypt.saltRounds, ); - return UsersDBApi.updatePassword( - user.id, - hashedPassword, - options, - ); + return UsersDBApi.updatePassword(user.id, hashedPassword, options); } static async updateProfile(data, currentUser) { let transaction = await db.sequelize.transaction(); try { - await UsersDBApi.findBy( - {id: currentUser.id}, - {transaction}, - ); - - await UsersDBApi.update( - currentUser.id, - data, - { - currentUser, - transaction - }, - ); + await UsersDBApi.findBy({ id: currentUser.id }, { transaction }); + await UsersDBApi.update(currentUser.id, data, { + currentUser, + transaction, + }); await transaction.commit(); } catch (error) { diff --git a/backend/src/services/email/list/addressVerification.js b/backend/src/services/email/list/addressVerification.js index 695e199..89be6d3 100644 --- a/backend/src/services/email/list/addressVerification.js +++ b/backend/src/services/email/list/addressVerification.js @@ -10,23 +10,27 @@ module.exports = class EmailAddressVerificationEmail { get subject() { return getNotification( - 'emails.emailAddressVerification.subject', - getNotification('app.title'), + 'emails.emailAddressVerification.subject', + getNotification('app.title'), ); } async html() { try { - const templatePath = path.join(__dirname, '../../email/htmlTemplates/addressVerification/emailAddressVerification.html'); + const templatePath = path.join( + __dirname, + '../../email/htmlTemplates/addressVerification/emailAddressVerification.html', + ); const template = await fs.readFile(templatePath, 'utf8'); const appTitle = getNotification('app.title'); const signupUrl = this.link; - let html = template.replace(/{appTitle}/g, appTitle) - .replace(/{signupUrl}/g, signupUrl) - .replace(/{to}/g, this.to); + let html = template + .replace(/{appTitle}/g, appTitle) + .replace(/{signupUrl}/g, signupUrl) + .replace(/{to}/g, this.to); return html; } catch (error) { @@ -34,5 +38,4 @@ module.exports = class EmailAddressVerificationEmail { throw error; } } - }; diff --git a/backend/src/services/email/list/invitation.js b/backend/src/services/email/list/invitation.js index 928c537..d2afc1e 100644 --- a/backend/src/services/email/list/invitation.js +++ b/backend/src/services/email/list/invitation.js @@ -10,23 +10,27 @@ module.exports = class InvitationEmail { get subject() { return getNotification( - 'emails.invitation.subject', - getNotification('app.title'), + 'emails.invitation.subject', + getNotification('app.title'), ); } async html() { try { - const templatePath = path.join(__dirname, '../../email/htmlTemplates/invitation/invitationTemplate.html'); + const templatePath = path.join( + __dirname, + '../../email/htmlTemplates/invitation/invitationTemplate.html', + ); const template = await fs.readFile(templatePath, 'utf8'); const appTitle = getNotification('app.title'); const signupUrl = `${this.host}&invitation=true`; - let html = template.replace(/{appTitle}/g, appTitle) - .replace(/{signupUrl}/g, signupUrl) - .replace(/{to}/g, this.to); + let html = template + .replace(/{appTitle}/g, appTitle) + .replace(/{signupUrl}/g, signupUrl) + .replace(/{to}/g, this.to); return html; } catch (error) { @@ -34,4 +38,4 @@ module.exports = class InvitationEmail { throw error; } } -}; \ No newline at end of file +}; diff --git a/backend/src/services/email/list/passwordReset.js b/backend/src/services/email/list/passwordReset.js index c1fd105..68ba353 100644 --- a/backend/src/services/email/list/passwordReset.js +++ b/backend/src/services/email/list/passwordReset.js @@ -1,6 +1,6 @@ const { getNotification } = require('../../notifications/helpers'); -const path = require("path"); -const {promises: fs} = require("fs"); +const path = require('path'); +const { promises: fs } = require('fs'); module.exports = class PasswordResetEmail { constructor(to, link) { @@ -17,7 +17,10 @@ module.exports = class PasswordResetEmail { async html() { try { - const templatePath = path.join(__dirname, '../../email/htmlTemplates/passwordReset/passwordResetEmail.html'); + const templatePath = path.join( + __dirname, + '../../email/htmlTemplates/passwordReset/passwordResetEmail.html', + ); const template = await fs.readFile(templatePath, 'utf8'); @@ -25,9 +28,10 @@ module.exports = class PasswordResetEmail { const resetUrl = this.link; const accountName = this.to; - let html = template.replace(/{appTitle}/g, appTitle) - .replace(/{resetUrl}/g, resetUrl) - .replace(/{accountName}/g, accountName); + let html = template + .replace(/{appTitle}/g, appTitle) + .replace(/{resetUrl}/g, resetUrl) + .replace(/{accountName}/g, accountName); return html; } catch (error) { diff --git a/backend/src/services/file.js b/backend/src/services/file.js index 9de1388..ed28eae 100644 --- a/backend/src/services/file.js +++ b/backend/src/services/file.js @@ -4,7 +4,7 @@ const config = require('../config'); const path = require('path'); const { pipeline } = require('stream/promises'); const { v4: uuid } = require('uuid'); -const { format } = require("util"); +const { format } = require('util'); const { S3Client, PutObjectCommand, @@ -24,7 +24,7 @@ const ensureDirectoryExistence = (filePath) => { ensureDirectoryExistence(dirname); fs.mkdirSync(dirname); -} +}; const UPLOAD_SESSIONS_DIR = path.join(config.uploadDir, 'upload_sessions'); const UPLOAD_SESSION_TTL_MS = 24 * 60 * 60 * 1000; @@ -39,7 +39,7 @@ const sanitizeFolder = (folder) => { } return value; -} +}; const sanitizeFilename = (filename) => { const value = path.basename(String(filename || '').trim()); @@ -49,11 +49,13 @@ const sanitizeFilename = (filename) => { } return value; -} +}; const getSessionDir = (sessionId) => path.join(UPLOAD_SESSIONS_DIR, sessionId); -const getSessionMetaPath = (sessionId) => path.join(getSessionDir(sessionId), 'meta.json'); -const getSessionChunksDir = (sessionId) => path.join(getSessionDir(sessionId), 'chunks'); +const getSessionMetaPath = (sessionId) => + path.join(getSessionDir(sessionId), 'meta.json'); +const getSessionChunksDir = (sessionId) => + path.join(getSessionDir(sessionId), 'chunks'); const getSessionChunkPath = (sessionId, chunkIndex) => path.join(getSessionChunksDir(sessionId), `${String(chunkIndex)}.part`); @@ -66,13 +68,13 @@ const readSessionMeta = (sessionId) => { const raw = fs.readFileSync(metaPath, 'utf8'); return JSON.parse(raw); -} +}; const writeSessionMeta = (sessionId, payload) => { const metaPath = getSessionMetaPath(sessionId); ensureDirectoryExistence(metaPath); fs.writeFileSync(metaPath, JSON.stringify(payload, null, 2), 'utf8'); -} +}; const removeUploadSession = (sessionId) => { const sessionDir = getSessionDir(sessionId); @@ -80,7 +82,7 @@ const removeUploadSession = (sessionId) => { if (fs.existsSync(sessionDir)) { fs.rmSync(sessionDir, { recursive: true, force: true }); } -} +}; const cleanupExpiredUploadSessions = () => { if (!fs.existsSync(UPLOAD_SESSIONS_DIR)) { @@ -98,7 +100,9 @@ const cleanupExpiredUploadSessions = () => { return; } - const updatedAt = new Date(meta.updatedAt || meta.createdAt || 0).getTime(); + const updatedAt = new Date( + meta.updatedAt || meta.createdAt || 0, + ).getTime(); if (!updatedAt || now - updatedAt > UPLOAD_SESSION_TTL_MS) { removeUploadSession(sessionId); } @@ -107,7 +111,7 @@ const cleanupExpiredUploadSessions = () => { removeUploadSession(sessionId); } }); -} +}; const streamAppendFile = async (targetPath, sourcePath) => { await new Promise((resolve, reject) => { @@ -119,7 +123,7 @@ const streamAppendFile = async (targetPath, sourcePath) => { writeStream.on('finish', resolve); readStream.pipe(writeStream, { end: true }); }); -} +}; // S3 session storage helpers const S3_UPLOAD_SESSIONS_PREFIX = '_upload_sessions'; @@ -163,7 +167,13 @@ const readS3SessionMeta = async (client, bucket, prefix, sessionId) => { } }; -const writeS3SessionMeta = async (client, bucket, prefix, sessionId, payload) => { +const writeS3SessionMeta = async ( + client, + bucket, + prefix, + sessionId, + payload, +) => { const key = getS3SessionMetaKey(prefix, sessionId); await client.send( new PutObjectCommand({ @@ -175,7 +185,14 @@ const writeS3SessionMeta = async (client, bucket, prefix, sessionId, payload) => ); }; -const uploadS3Chunk = async (client, bucket, prefix, sessionId, chunkIndex, body) => { +const uploadS3Chunk = async ( + client, + bucket, + prefix, + sessionId, + chunkIndex, + body, +) => { const key = getS3SessionChunkKey(prefix, sessionId, chunkIndex); await client.send( new PutObjectCommand({ @@ -186,7 +203,13 @@ const uploadS3Chunk = async (client, bucket, prefix, sessionId, chunkIndex, body ); }; -const downloadS3Chunk = async (client, bucket, prefix, sessionId, chunkIndex) => { +const downloadS3Chunk = async ( + client, + bucket, + prefix, + sessionId, + chunkIndex, +) => { const key = getS3SessionChunkKey(prefix, sessionId, chunkIndex); try { const output = await client.send( @@ -236,7 +259,9 @@ const removeS3UploadSession = async (client, bucket, prefix, sessionId) => { return; } - const objectsToDelete = listResult.Contents.map((obj) => ({ Key: obj.Key })); + const objectsToDelete = listResult.Contents.map((obj) => ({ + Key: obj.Key, + })); await client.send( new DeleteObjectsCommand({ @@ -291,12 +316,17 @@ const cleanupExpiredS3UploadSessions = async () => { continue; } - const updatedAt = new Date(meta.updatedAt || meta.createdAt || 0).getTime(); + const updatedAt = new Date( + meta.updatedAt || meta.createdAt || 0, + ).getTime(); if (!updatedAt || now - updatedAt > UPLOAD_SESSION_TTL_MS) { await removeS3UploadSession(client, bucket, prefix, sessionId); } } catch (error) { - console.error(`Failed to cleanup S3 upload session ${sessionId}`, error); + console.error( + `Failed to cleanup S3 upload session ${sessionId}`, + error, + ); await removeS3UploadSession(client, bucket, prefix, sessionId); } } @@ -318,18 +348,13 @@ const uploadLocal = ( return; } - if ( - validations.entity - ) { + if (validations.entity) { res.sendStatus(403); return; } if (validations.folderIncludesAuthenticationUid) { - folder = folder.replace( - ':userId', - req.currentUser.authenticationUid, - ); + folder = folder.replace(':userId', req.currentUser.authenticationUid); if ( !req.currentUser.authenticationUid || !folder.includes(req.currentUser.authenticationUid) @@ -352,11 +377,7 @@ const uploadLocal = ( return; } - const privateUrl = path.join( - form.uploadDir, - folder, - filename, - ); + const privateUrl = path.join(form.uploadDir, folder, filename); ensureDirectoryExistence(privateUrl); fs.renameSync(fileTempUrl, privateUrl); res.sendStatus(200); @@ -365,17 +386,17 @@ const uploadLocal = ( form.on('error', function (err) { res.status(500).send(err); }); - } -} + }; +}; const downloadLocal = async (req, res) => { - const privateUrl = req.query.privateUrl; - if (!privateUrl) { - return res.sendStatus(404); - } - res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin'); - res.download(path.join(config.uploadDir, privateUrl)); -} + const privateUrl = req.query.privateUrl; + if (!privateUrl) { + return res.sendStatus(404); + } + res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin'); + res.download(path.join(config.uploadDir, privateUrl)); +}; const deleteLocal = async (privateUrl) => { try { @@ -392,30 +413,32 @@ const deleteLocal = async (privateUrl) => { console.error(`Cannot delete local file ${privateUrl}`, error); throw error; } -} +}; const initGCloud = () => { - const processFile = require("../middlewares/upload"); - const { Storage } = require("@google-cloud/storage"); + const processFile = require('../middlewares/upload'); + const { Storage } = require('@google-cloud/storage'); - const hash = config.gcloud.hash + const hash = config.gcloud.hash; - const privateKey = process.env.GC_PRIVATE_KEY.replace(/\\\n/g, "\n"); + const privateKey = process.env.GC_PRIVATE_KEY.replace(/\\\n/g, '\n'); const storage = new Storage({ - projectId: process.env.GC_PROJECT_ID, - credentials: { - client_email: process.env.GC_CLIENT_EMAIL, - private_key: privateKey - } + projectId: process.env.GC_PROJECT_ID, + credentials: { + client_email: process.env.GC_CLIENT_EMAIL, + private_key: privateKey, + }, }); const bucket = storage.bucket(config.gcloud.bucket); - return {hash, bucket, processFile}; -} + return { hash, bucket, processFile }; +}; const getFileStorageProvider = () => { - const provider = (process.env.FILE_STORAGE_PROVIDER || '').trim().toLowerCase(); + const provider = (process.env.FILE_STORAGE_PROVIDER || '') + .trim() + .toLowerCase(); if (provider) { return provider; @@ -445,7 +468,7 @@ const getFileStorageProvider = () => { } return 'local'; -} +}; const initS3 = () => { const processFile = require('../middlewares/upload'); @@ -464,7 +487,7 @@ const initS3 = () => { prefix: config.s3.prefix, processFile, }; -} +}; const buildStoragePath = (prefix, privateUrl) => { const cleanPrefix = (prefix || '').replace(/^\/+|\/+$/g, ''); @@ -475,17 +498,17 @@ const buildStoragePath = (prefix, privateUrl) => { } return `${cleanPrefix}/${cleanPrivateUrl}`; -} +}; const uploadGCloud = async (folder, req, res) => { try { - const {hash, bucket, processFile} = initGCloud(); + const { hash, bucket, processFile } = initGCloud(); await processFile(req, res); let buffer = await req.file.buffer; let filename = await req.body.filename; if (!req.file) { - return res.status(400).send({ message: "Please upload a file!" }); + return res.status(400).send({ message: 'Please upload a file!' }); } let path = `${hash}/${folder}/${filename}`; @@ -497,7 +520,7 @@ const uploadGCloud = async (folder, req, res) => { resumable: false, }); - blobStream.on("error", (err) => { + blobStream.on('error', (err) => { console.log('Upload error'); console.log(err.message); res.status(500).send({ message: err.message }); @@ -505,52 +528,51 @@ const uploadGCloud = async (folder, req, res) => { console.log(`https://storage.googleapis.com/${bucket.name}/${blob.name}`); - blobStream.on("finish", async () => { + blobStream.on('finish', async () => { const publicUrl = format( - `https://storage.googleapis.com/${bucket.name}/${blob.name}` + `https://storage.googleapis.com/${bucket.name}/${blob.name}`, ); res.status(200).send({ - message: "Uploaded the file successfully: " + path, + message: 'Uploaded the file successfully: ' + path, url: publicUrl, }); }); - blobStream.end(buffer) + blobStream.end(buffer); } catch (err) { console.log(err); res.status(500).send({ - message: `Could not upload the file. ${err}` + message: `Could not upload the file. ${err}`, }); } -} +}; const downloadGCloud = async (req, res) => { try { - const {hash, bucket} = initGCloud(); + const { hash, bucket } = initGCloud(); const privateUrl = await req.query.privateUrl; const filePath = `${hash}/${privateUrl}`; - const file = bucket.file(filePath) + const file = bucket.file(filePath); const fileExists = await file.exists(); if (fileExists[0]) { res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin'); const stream = file.createReadStream(); stream.pipe(res); - } - else { + } else { res.status(404).send({ - message: "Could not download the file.", - }); + message: 'Could not download the file.', + }); } } catch (err) { res.status(404).send({ - message: "Could not download the file. " + err, + message: 'Could not download the file. ' + err, }); } -} +}; const uploadS3 = async (folder, req, res) => { try { @@ -558,13 +580,13 @@ const uploadS3 = async (folder, req, res) => { await processFile(req, res); if (!req.file) { - return res.status(400).send({ message: "Please upload a file!" }); + return res.status(400).send({ message: 'Please upload a file!' }); } const filename = req.body.filename; if (!filename) { - return res.status(400).send({ message: "Missing filename" }); + return res.status(400).send({ message: 'Missing filename' }); } const privateUrl = `${folder}/${filename}`; @@ -589,14 +611,14 @@ const uploadS3 = async (folder, req, res) => { message: `Could not upload the file. ${error.message || error}`, }); } -} +}; const downloadS3 = async (req, res) => { try { const privateUrl = req.query.privateUrl; if (!privateUrl) { - return res.status(404).send({ message: "Missing privateUrl" }); + return res.status(404).send({ message: 'Missing privateUrl' }); } const { client, bucket, prefix } = initS3(); @@ -610,7 +632,7 @@ const downloadS3 = async (req, res) => { if (!output || !output.Body) { return res.status(404).send({ - message: "Could not download the file.", + message: 'Could not download the file.', }); } @@ -637,14 +659,14 @@ const downloadS3 = async (req, res) => { message: `Could not download the file. ${error.message || error}`, }); } -} +}; const deleteGCloud = async (privateUrl) => { try { - const {hash, bucket} = initGCloud(); + const { hash, bucket } = initGCloud(); const filePath = `${hash}/${privateUrl}`; - const file = bucket.file(filePath) + const file = bucket.file(filePath); const fileExists = await file.exists(); if (fileExists[0]) { @@ -653,7 +675,7 @@ const deleteGCloud = async (privateUrl) => { } catch (err) { console.log(`Cannot find the file ${privateUrl}`); } -} +}; const deleteS3 = async (privateUrl) => { try { @@ -673,7 +695,7 @@ const deleteS3 = async (privateUrl) => { console.error(`Cannot delete S3 file ${privateUrl}`, error); throw error; } -} +}; const uploadStreamToGCloud = async (privateUrl, sourcePath) => { const { hash, bucket } = initGCloud(); @@ -686,7 +708,7 @@ const uploadStreamToGCloud = async (privateUrl, sourcePath) => { ); return format(`https://storage.googleapis.com/${bucket.name}/${blob.name}`); -} +}; const initUploadSession = async (req, res) => { try { @@ -755,9 +777,11 @@ const initUploadSession = async (req, res) => { }); } catch (error) { console.error('Failed to initialize upload session', error); - return res.status(500).send({ message: 'Failed to initialize upload session' }); + return res + .status(500) + .send({ message: 'Failed to initialize upload session' }); } -} +}; const getUploadSession = async (req, res) => { try { @@ -794,7 +818,7 @@ const getUploadSession = async (req, res) => { console.error('Failed to get upload session', error); return res.status(500).send({ message: 'Failed to get upload session' }); } -} +}; const uploadChunk = async (req, res) => { try { @@ -818,7 +842,12 @@ const uploadChunk = async (req, res) => { s3Client = s3.client; s3Bucket = s3.bucket; s3Prefix = s3.prefix; - session = await readS3SessionMeta(s3Client, s3Bucket, s3Prefix, sessionId); + session = await readS3SessionMeta( + s3Client, + s3Bucket, + s3Prefix, + sessionId, + ); } else { session = readSessionMeta(sessionId); } @@ -844,7 +873,14 @@ const uploadChunk = async (req, res) => { const chunkBuffer = Buffer.concat(chunks); // Upload chunk directly to S3 - await uploadS3Chunk(s3Client, s3Bucket, s3Prefix, sessionId, chunkIndex, chunkBuffer); + await uploadS3Chunk( + s3Client, + s3Bucket, + s3Prefix, + sessionId, + chunkIndex, + chunkBuffer, + ); } else { // Local storage - write to temp file then rename const chunkDir = getSessionChunksDir(sessionId); @@ -869,7 +905,13 @@ const uploadChunk = async (req, res) => { session.updatedAt = new Date().toISOString(); if (provider === 's3') { - await writeS3SessionMeta(s3Client, s3Bucket, s3Prefix, sessionId, session); + await writeS3SessionMeta( + s3Client, + s3Bucket, + s3Prefix, + sessionId, + session, + ); } else { writeSessionMeta(sessionId, session); } @@ -884,7 +926,7 @@ const uploadChunk = async (req, res) => { console.error('Failed to upload chunk', error); return res.status(500).send({ message: 'Failed to upload chunk' }); } -} +}; const finalizeUploadSession = async (req, res) => { try { @@ -903,7 +945,12 @@ const finalizeUploadSession = async (req, res) => { s3Bucket = s3.bucket; s3Prefix = s3.prefix; s3Region = s3.region; - session = await readS3SessionMeta(s3Client, s3Bucket, s3Prefix, sessionId); + session = await readS3SessionMeta( + s3Client, + s3Bucket, + s3Prefix, + sessionId, + ); } else { session = readSessionMeta(sessionId); } @@ -921,7 +968,13 @@ const finalizeUploadSession = async (req, res) => { // Verify all chunks exist if (provider === 's3') { for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex += 1) { - const exists = await s3ChunkExists(s3Client, s3Bucket, s3Prefix, sessionId, chunkIndex); + const exists = await s3ChunkExists( + s3Client, + s3Bucket, + s3Prefix, + sessionId, + chunkIndex, + ); if (!exists) { return res.status(400).send({ message: `Missing chunk ${chunkIndex}`, @@ -953,7 +1006,13 @@ const finalizeUploadSession = async (req, res) => { // Download and assemble chunks if (provider === 's3') { for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex += 1) { - const chunkStream = await downloadS3Chunk(s3Client, s3Bucket, s3Prefix, sessionId, chunkIndex); + const chunkStream = await downloadS3Chunk( + s3Client, + s3Bucket, + s3Prefix, + sessionId, + chunkIndex, + ); if (!chunkStream) { // Cleanup and return error if (fs.existsSync(assembledPath)) fs.unlinkSync(assembledPath); @@ -965,7 +1024,9 @@ const finalizeUploadSession = async (req, res) => { // Write chunk to assembled file await new Promise((resolve, reject) => { - const writeStream = fs.createWriteStream(assembledPath, { flags: 'a' }); + const writeStream = fs.createWriteStream(assembledPath, { + flags: 'a', + }); writeStream.on('error', reject); writeStream.on('finish', resolve); @@ -973,7 +1034,8 @@ const finalizeUploadSession = async (req, res) => { chunkStream.on('error', reject); chunkStream.pipe(writeStream, { end: true }); } else if (typeof chunkStream.transformToByteArray === 'function') { - chunkStream.transformToByteArray() + chunkStream + .transformToByteArray() .then((bytes) => { writeStream.write(Buffer.from(bytes)); writeStream.end(); @@ -993,7 +1055,10 @@ const finalizeUploadSession = async (req, res) => { } const assembledStats = fs.statSync(assembledPath); - if (Number.isFinite(Number(session.size)) && Number(session.size) !== assembledStats.size) { + if ( + Number.isFinite(Number(session.size)) && + Number(session.size) !== assembledStats.size + ) { // Cleanup if (fs.existsSync(assembledPath)) fs.unlinkSync(assembledPath); return res.status(400).send({ @@ -1042,9 +1107,11 @@ const finalizeUploadSession = async (req, res) => { }); } catch (error) { console.error('Failed to finalize upload session', error); - return res.status(500).send({ message: 'Failed to finalize upload session' }); + return res + .status(500) + .send({ message: 'Failed to finalize upload session' }); } -} +}; const uploadFile = async (folder, req, res) => { const provider = getFileStorageProvider(); @@ -1061,7 +1128,7 @@ const uploadFile = async (folder, req, res) => { entity: null, folderIncludesAuthenticationUid: false, })(req, res); -} +}; const downloadFile = async (req, res) => { const provider = getFileStorageProvider(); @@ -1075,7 +1142,7 @@ const downloadFile = async (req, res) => { } return downloadLocal(req, res); -} +}; const deleteFile = async (privateUrl) => { const provider = getFileStorageProvider(); @@ -1089,7 +1156,7 @@ const deleteFile = async (privateUrl) => { } return deleteLocal(privateUrl); -} +}; const PRESIGN_EXPIRY_SECONDS = 3600; // 1 hour @@ -1123,7 +1190,7 @@ const generatePresignedUrls = async (urls) => { presignedUrls[url] = await getSignedUrl(client, command, { expiresIn: PRESIGN_EXPIRY_SECONDS, }); - }) + }), ); return presignedUrls; @@ -1150,4 +1217,4 @@ module.exports = { uploadS3, downloadS3, generatePresignedUrls, -} +}; diff --git a/backend/src/services/notifications/errors/forbidden.js b/backend/src/services/notifications/errors/forbidden.js index 33e5dc2..192fa10 100644 --- a/backend/src/services/notifications/errors/forbidden.js +++ b/backend/src/services/notifications/errors/forbidden.js @@ -8,8 +8,7 @@ module.exports = class ForbiddenError extends Error { message = getNotification(messageCode); } - message = - message || getNotification('errors.forbidden.message'); + message = message || getNotification('errors.forbidden.message'); super(message); this.code = 403; diff --git a/backend/src/services/notifications/errors/validation.js b/backend/src/services/notifications/errors/validation.js index cf3130c..464550c 100644 --- a/backend/src/services/notifications/errors/validation.js +++ b/backend/src/services/notifications/errors/validation.js @@ -8,9 +8,7 @@ module.exports = class ValidationError extends Error { message = getNotification(messageCode); } - message = - message || - getNotification('errors.validation.message'); + message = message || getNotification('errors.validation.message'); super(message); this.code = 400; diff --git a/backend/src/services/notifications/helpers.js b/backend/src/services/notifications/helpers.js index b2f31fd..1c3a60f 100644 --- a/backend/src/services/notifications/helpers.js +++ b/backend/src/services/notifications/helpers.js @@ -6,13 +6,8 @@ function format(message, args) { return null; } - return message.replace(/{(\d+)}/g, function ( - match, - number, - ) { - return typeof args[number] != 'undefined' - ? args[number] - : match; + return message.replace(/{(\d+)}/g, function (match, number) { + return typeof args[number] != 'undefined' ? args[number] : match; }); } diff --git a/backend/src/services/notifications/list.js b/backend/src/services/notifications/list.js index 4302c42..29b198d 100644 --- a/backend/src/services/notifications/list.js +++ b/backend/src/services/notifications/list.js @@ -13,25 +13,22 @@ const errors = { emailAlreadyInUse: 'Email is already in use', invalidEmail: 'Please provide a valid email', passwordReset: { - invalidToken: - 'Password reset link is invalid or has expired', + invalidToken: 'Password reset link is invalid or has expired', error: `Email not recognized`, }, passwordUpdate: { - samePassword: `You can't use the same password. Please create new password` + samePassword: `You can't use the same password. Please create new password`, }, userNotVerified: `Sorry, your email has not been verified yet`, emailAddressVerificationEmail: { - invalidToken: - 'Email verification link is invalid or has expired', + invalidToken: 'Email verification link is invalid or has expired', error: `Email not recognized`, }, }, iam: { errors: { - userAlreadyExists: - 'User with this email already exists', + userAlreadyExists: 'User with this email already exists', userNotFound: 'User not found', disablingHimself: `You can't disable yourself`, revokingOwnPermission: `You can't revoke your own owner permission`, @@ -43,8 +40,7 @@ const errors = { importer: { errors: { invalidFileEmpty: 'The file is empty', - invalidFileExcel: - 'Only excel (.xlsx) files are allowed', + invalidFileExcel: 'Only excel (.xlsx) files are allowed', invalidFileUpload: 'Invalid file. Make sure you are using the last version of the template.', importHashRequired: 'Import hash is required', @@ -61,7 +57,7 @@ const errors = { message: 'An error occurred', }, searchQueryRequired: { - message: 'Search query is required', + message: 'Search query is required', }, }, diff --git a/backend/src/services/openai.js b/backend/src/services/openai.js index 3793398..7cb6ff0 100644 --- a/backend/src/services/openai.js +++ b/backend/src/services/openai.js @@ -6,8 +6,13 @@ const loadRoleService = () => { try { return require('./roles'); } catch (error) { - console.error('Role service is missing. Advanced roles are required for this operation.', error); - const err = new Error('Role service is missing. Advanced roles are required for this operation.'); + console.error( + 'Role service is missing. Advanced roles are required for this operation.', + error, + ); + const err = new Error( + 'Role service is missing. Advanced roles are required for this operation.', + ); err.originalError = error; throw err; } @@ -35,7 +40,7 @@ module.exports = class OpenAiService { if (!prompt) { return { success: false, - error: 'Prompt is required' + error: 'Prompt is required', }; } diff --git a/backend/src/services/project_audio_tracks.js b/backend/src/services/project_audio_tracks.js index f179766..7b9dbea 100644 --- a/backend/src/services/project_audio_tracks.js +++ b/backend/src/services/project_audio_tracks.js @@ -1,25 +1,18 @@ const db = require('../db/models'); const Project_audio_tracksDBApi = require('../db/api/project_audio_tracks'); -const processFile = require("../middlewares/upload"); +const processFile = require('../middlewares/upload'); const ValidationError = require('./notifications/errors/validation'); const csv = require('csv-parser'); const stream = require('stream'); - - - - module.exports = class Project_audio_tracksService { static async create(data, currentUser) { const transaction = await db.sequelize.transaction(); try { - const createdTrack = await Project_audio_tracksDBApi.create( - data, - { - currentUser, - transaction, - }, - ); + const createdTrack = await Project_audio_tracksDBApi.create(data, { + currentUser, + transaction, + }); await transaction.commit(); return createdTrack; @@ -37,7 +30,7 @@ module.exports = class Project_audio_tracksService { const bufferStream = new stream.PassThrough(); const results = []; - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream await new Promise((resolve, reject) => { bufferStream @@ -48,13 +41,13 @@ module.exports = class Project_audio_tracksService { resolve(); }) .on('error', (error) => reject(error)); - }) + }); await Project_audio_tracksDBApi.bulkImport(results, { - transaction, - ignoreDuplicates: true, - validate: true, - currentUser: req.currentUser + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, }); await transaction.commit(); @@ -68,28 +61,22 @@ module.exports = class Project_audio_tracksService { const transaction = await db.sequelize.transaction(); try { let project_audio_tracks = await Project_audio_tracksDBApi.findBy( - {id}, - {transaction}, + { id }, + { transaction }, ); if (!project_audio_tracks) { - throw new ValidationError( - 'project_audio_tracksNotFound', - ); + throw new ValidationError('project_audio_tracksNotFound'); } - const updatedProject_audio_tracks = await Project_audio_tracksDBApi.update( - id, - data, - { + const updatedProject_audio_tracks = + await Project_audio_tracksDBApi.update(id, data, { currentUser, transaction, - }, - ); + }); await transaction.commit(); return updatedProject_audio_tracks; - } catch (error) { await transaction.rollback(); throw error; @@ -116,13 +103,10 @@ module.exports = class Project_audio_tracksService { const transaction = await db.sequelize.transaction(); try { - await Project_audio_tracksDBApi.remove( - id, - { - currentUser, - transaction, - }, - ); + await Project_audio_tracksDBApi.remove(id, { + currentUser, + transaction, + }); await transaction.commit(); } catch (error) { @@ -130,8 +114,4 @@ module.exports = class Project_audio_tracksService { throw error; } } - - }; - - diff --git a/backend/src/services/project_element_defaults.js b/backend/src/services/project_element_defaults.js index c67d03c..73a6164 100644 --- a/backend/src/services/project_element_defaults.js +++ b/backend/src/services/project_element_defaults.js @@ -24,7 +24,10 @@ class Project_element_defaultsService extends BaseService { * Snapshot all global element defaults to a project */ static async snapshotGlobalDefaults(projectId, options = {}) { - return Project_element_defaultsDBApi.snapshotGlobalDefaults(projectId, options); + return Project_element_defaultsDBApi.snapshotGlobalDefaults( + projectId, + options, + ); } } diff --git a/backend/src/services/projects.js b/backend/src/services/projects.js index c5efc57..e2e069f 100644 --- a/backend/src/services/projects.js +++ b/backend/src/services/projects.js @@ -1,22 +1,19 @@ const db = require('../db/models'); const ProjectsDBApi = require('../db/api/projects'); -const processFile = require("../middlewares/upload"); +const processFile = require('../middlewares/upload'); const ValidationError = require('./notifications/errors/validation'); const csv = require('csv-parser'); const stream = require('stream'); - - - - module.exports = class ProjectsService { static normalizeSlug(value) { - return String(value || 'project') - .toLowerCase() - .trim() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, '') - || 'project'; + return ( + String(value || 'project') + .toLowerCase() + .trim() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') || 'project' + ); } static async generateUniqueSlug(baseSlug, transaction) { @@ -49,13 +46,10 @@ module.exports = class ProjectsService { static async create(data, currentUser) { const transaction = await db.sequelize.transaction(); try { - const createdProject = await ProjectsDBApi.create( - data, - { - currentUser, - transaction, - }, - ); + const createdProject = await ProjectsDBApi.create(data, { + currentUser, + transaction, + }); await transaction.commit(); return createdProject; @@ -91,7 +85,10 @@ module.exports = class ProjectsService { throw new ValidationError('projectsNotFound'); } - const uniqueSlug = await ProjectsService.generateUniqueSlug(sourceProject.slug, transaction); + const uniqueSlug = await ProjectsService.generateUniqueSlug( + sourceProject.slug, + transaction, + ); const clonedProject = await ProjectsDBApi.create( { @@ -166,7 +163,7 @@ module.exports = class ProjectsService { const bufferStream = new stream.PassThrough(); const results = []; - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream await new Promise((resolve, reject) => { bufferStream @@ -177,13 +174,13 @@ module.exports = class ProjectsService { resolve(); }) .on('error', (error) => reject(error)); - }) + }); await ProjectsDBApi.bulkImport(results, { - transaction, - ignoreDuplicates: true, - validate: true, - currentUser: req.currentUser + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, }); await transaction.commit(); @@ -196,29 +193,19 @@ module.exports = class ProjectsService { static async update(data, id, currentUser) { const transaction = await db.sequelize.transaction(); try { - let projects = await ProjectsDBApi.findBy( - {id}, - {transaction}, - ); + let projects = await ProjectsDBApi.findBy({ id }, { transaction }); if (!projects) { - throw new ValidationError( - 'projectsNotFound', - ); + throw new ValidationError('projectsNotFound'); } - const updatedProjects = await ProjectsDBApi.update( - id, - data, - { - currentUser, - transaction, - }, - ); + const updatedProjects = await ProjectsDBApi.update(id, data, { + currentUser, + transaction, + }); await transaction.commit(); return updatedProjects; - } catch (error) { await transaction.rollback(); throw error; @@ -245,13 +232,10 @@ module.exports = class ProjectsService { const transaction = await db.sequelize.transaction(); try { - await ProjectsDBApi.remove( - id, - { - currentUser, - transaction, - }, - ); + await ProjectsDBApi.remove(id, { + currentUser, + transaction, + }); await transaction.commit(); } catch (error) { @@ -259,6 +243,4 @@ module.exports = class ProjectsService { throw error; } } - - }; diff --git a/backend/src/services/publish.js b/backend/src/services/publish.js index 1c8139e..0c2a094 100644 --- a/backend/src/services/publish.js +++ b/backend/src/services/publish.js @@ -76,7 +76,8 @@ module.exports = class PublishService { } const eventTitle = typeof title === 'string' ? title.trim() : ''; - const eventDescription = typeof description === 'string' ? description.trim() : ''; + const eventDescription = + typeof description === 'string' ? description.trim() : ''; if (!eventTitle) { const error = new Error('title is required'); @@ -103,19 +104,26 @@ module.exports = class PublishService { }); try { - const summary = await this.withProjectPublishLock(projectId, async (transaction) => { - await publishEvent.update( - { - started_at: new Date(), - status: EVENT_STATUS.RUNNING, - error_message: null, - updatedById: currentUser?.id || null, - }, - { transaction }, - ); + const summary = await this.withProjectPublishLock( + projectId, + async (transaction) => { + await publishEvent.update( + { + started_at: new Date(), + status: EVENT_STATUS.RUNNING, + error_message: null, + updatedById: currentUser?.id || null, + }, + { transaction }, + ); - return this.copyStageToProduction(projectId, currentUser, transaction); - }); + return this.copyStageToProduction( + projectId, + currentUser, + transaction, + ); + }, + ); await publishEvent.update({ status: EVENT_STATUS.SUCCESS, @@ -166,19 +174,22 @@ module.exports = class PublishService { }); try { - const summary = await this.withProjectPublishLock(projectId, async (transaction) => { - await publishEvent.update( - { - started_at: new Date(), - status: EVENT_STATUS.RUNNING, - error_message: null, - updatedById: currentUser?.id || null, - }, - { transaction }, - ); + const summary = await this.withProjectPublishLock( + projectId, + async (transaction) => { + await publishEvent.update( + { + started_at: new Date(), + status: EVENT_STATUS.RUNNING, + error_message: null, + updatedById: currentUser?.id || null, + }, + { transaction }, + ); - return this.copyDevToStage(projectId, currentUser, transaction); - }); + return this.copyDevToStage(projectId, currentUser, transaction); + }, + ); await publishEvent.update({ status: EVENT_STATUS.SUCCESS, @@ -209,11 +220,23 @@ module.exports = class PublishService { * Copy dev content to stage environment */ static async copyDevToStage(projectId, currentUser, transaction) { - return this.copyEnvironment(projectId, ENVIRONMENT.DEV, ENVIRONMENT.STAGE, currentUser, transaction); + return this.copyEnvironment( + projectId, + ENVIRONMENT.DEV, + ENVIRONMENT.STAGE, + currentUser, + transaction, + ); } static async copyStageToProduction(projectId, currentUser, transaction) { - return this.copyEnvironment(projectId, ENVIRONMENT.STAGE, ENVIRONMENT.PRODUCTION, currentUser, transaction); + return this.copyEnvironment( + projectId, + ENVIRONMENT.STAGE, + ENVIRONMENT.PRODUCTION, + currentUser, + transaction, + ); } /** @@ -230,7 +253,13 @@ module.exports = class PublishService { * @param {object} transaction - Sequelize transaction * @returns {object} Summary of copied items */ - static async copyEnvironment(projectId, fromEnv, toEnv, currentUser, transaction) { + static async copyEnvironment( + projectId, + fromEnv, + toEnv, + currentUser, + transaction, + ) { // Get source content const [sourcePages, sourceAudioTracks] = await Promise.all([ db.tour_pages.findAll({ diff --git a/backend/src/services/pwa_manifest.js b/backend/src/services/pwa_manifest.js index 2f7b0a1..614bd53 100644 --- a/backend/src/services/pwa_manifest.js +++ b/backend/src/services/pwa_manifest.js @@ -21,7 +21,10 @@ function getAssetType(mimeType, filename) { const mime = (mimeType || '').toLowerCase(); const name = (filename || '').toLowerCase(); - if (mime.startsWith('image/') || /\.(jpg|jpeg|png|gif|webp|svg)$/.test(name)) { + if ( + mime.startsWith('image/') || + /\.(jpg|jpeg|png|gif|webp|svg)$/.test(name) + ) { return 'image'; } if (mime.startsWith('video/') || /\.(mp4|webm|mov)$/.test(name)) { @@ -114,7 +117,16 @@ class PWAManifestService { }; // Helper to add an asset to the manifest - const addAsset = (id, url, filename, variantType, assetType, mimeType, sizeBytes, pageIds) => { + const addAsset = ( + id, + url, + filename, + variantType, + assetType, + mimeType, + sizeBytes, + pageIds, + ) => { if (!url || seenUrls.has(url)) return; seenUrls.add(url); @@ -133,7 +145,10 @@ class PWAManifestService { // Add assets with their variants for (const asset of assets) { // Get asset variants - const variants = await AssetVariantsDBApi.findAll({ asset: asset.id }, {}); + const variants = await AssetVariantsDBApi.findAll( + { asset: asset.id }, + {}, + ); const variantRows = variants?.rows || []; // Select appropriate variants based on device type @@ -148,12 +163,15 @@ class PWAManifestService { asset.type || getAssetType(asset.mime_type, asset.filename), variant.mime_type || asset.mime_type, variant.size_bytes || mbToBytes(variant.size_mb), - asset.pages?.map((p) => p.id) || [] + asset.pages?.map((p) => p.id) || [], ); } // If no variants, add original (use cdn_url as primary, fall back to storage_key) - if (selectedVariants.length === 0 && (asset.cdn_url || asset.storage_key)) { + if ( + selectedVariants.length === 0 && + (asset.cdn_url || asset.storage_key) + ) { addAsset( asset.id, asset.cdn_url || asset.storage_key, @@ -162,7 +180,7 @@ class PWAManifestService { asset.type || getAssetType(asset.mime_type, asset.name), asset.mime_type, mbToBytes(asset.size_mb), - asset.pages?.map((p) => p.id) || [] + asset.pages?.map((p) => p.id) || [], ); } } @@ -178,7 +196,7 @@ class PWAManifestService { 'image', 'image/jpeg', 0, - [page.id] + [page.id], ); } if (page.background_video_url) { @@ -190,7 +208,7 @@ class PWAManifestService { 'video', 'video/mp4', 0, - [page.id] + [page.id], ); } @@ -201,7 +219,9 @@ class PWAManifestService { ? JSON.parse(page.ui_schema_json) : page.ui_schema_json; - const elements = Array.isArray(uiSchema?.elements) ? uiSchema.elements : []; + const elements = Array.isArray(uiSchema?.elements) + ? uiSchema.elements + : []; for (const element of elements) { const contentUrls = extractUrlsFromContent(element); @@ -220,7 +240,7 @@ class PWAManifestService { assetType, null, 0, - [page.id] + [page.id], ); } } @@ -230,7 +250,10 @@ class PWAManifestService { } // Calculate total size - const totalSizeBytes = manifestAssets.reduce((sum, a) => sum + (a.sizeBytes || 0), 0); + const totalSizeBytes = manifestAssets.reduce( + (sum, a) => sum + (a.sizeBytes || 0), + 0, + ); return { version: `v${Date.now()}`, diff --git a/backend/src/services/roles.js b/backend/src/services/roles.js index 25555ce..10164da 100644 --- a/backend/src/services/roles.js +++ b/backend/src/services/roles.js @@ -1,6 +1,6 @@ const db = require('../db/models'); const RolesDBApi = require('../db/api/roles'); -const processFile = require("../middlewares/upload"); +const processFile = require('../middlewares/upload'); const ValidationError = require('./notifications/errors/validation'); const csv = require('csv-parser'); const axios = require('axios'); @@ -17,7 +17,9 @@ const validateWidgetSql = (sql) => { } if (sql.length > WIDGET_SQL_MAX_LENGTH) { - throw new ValidationError(`Widget query is too long (max ${WIDGET_SQL_MAX_LENGTH} characters)`); + throw new ValidationError( + `Widget query is too long (max ${WIDGET_SQL_MAX_LENGTH} characters)`, + ); } const normalized = sql.trim().replace(/;+\s*$/, ''); @@ -35,7 +37,9 @@ const validateWidgetSql = (sql) => { } if (/\b(pg_sleep|set_config|copy)\b/i.test(normalized)) { - throw new ValidationError('Restricted SQL function detected in widget query'); + throw new ValidationError( + 'Restricted SQL function detected in widget query', + ); } return normalized; @@ -46,26 +50,22 @@ const runSafeWidgetQuery = async (sql) => { const wrappedSql = `SELECT * FROM (${normalized}) AS widget_query_result LIMIT ${WIDGET_SQL_MAX_ROWS}`; return db.sequelize.transaction(async (transaction) => { - await db.sequelize.query(`SET LOCAL statement_timeout = ${WIDGET_SQL_TIMEOUT_MS}`, { transaction }); + await db.sequelize.query( + `SET LOCAL statement_timeout = ${WIDGET_SQL_TIMEOUT_MS}`, + { transaction }, + ); return db.sequelize.query(wrappedSql, { transaction }); }); }; - - - - module.exports = class RolesService { static async create(data, currentUser) { const transaction = await db.sequelize.transaction(); try { - const createdRole = await RolesDBApi.create( - data, - { - currentUser, - transaction, - }, - ); + const createdRole = await RolesDBApi.create(data, { + currentUser, + transaction, + }); await transaction.commit(); return createdRole; @@ -83,7 +83,7 @@ module.exports = class RolesService { const bufferStream = new stream.PassThrough(); const results = []; - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream await new Promise((resolve, reject) => { bufferStream @@ -94,13 +94,13 @@ module.exports = class RolesService { resolve(); }) .on('error', (error) => reject(error)); - }) + }); await RolesDBApi.bulkImport(results, { - transaction, - ignoreDuplicates: true, - validate: true, - currentUser: req.currentUser + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, }); await transaction.commit(); @@ -113,29 +113,19 @@ module.exports = class RolesService { static async update(data, id, currentUser) { const transaction = await db.sequelize.transaction(); try { - let roles = await RolesDBApi.findBy( - {id}, - {transaction}, - ); + let roles = await RolesDBApi.findBy({ id }, { transaction }); if (!roles) { - throw new ValidationError( - 'rolesNotFound', - ); + throw new ValidationError('rolesNotFound'); } - const updatedRoles = await RolesDBApi.update( - id, - data, - { - currentUser, - transaction, - }, - ); + const updatedRoles = await RolesDBApi.update(id, data, { + currentUser, + transaction, + }); await transaction.commit(); return updatedRoles; - } catch (error) { await transaction.rollback(); throw error; @@ -162,13 +152,10 @@ module.exports = class RolesService { const transaction = await db.sequelize.transaction(); try { - await RolesDBApi.remove( - id, - { - currentUser, - transaction, - }, - ); + await RolesDBApi.remove(id, { + currentUser, + transaction, + }); await transaction.commit(); } catch (error) { @@ -177,177 +164,172 @@ module.exports = class RolesService { } } - static async addRoleInfo(roleId, userId, key, widgetId, currentUser) { - const regexExpForUuid = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi; - const widgetIdIsUUID = regexExpForUuid.test(widgetId); + const regexExpForUuid = + /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi; + const widgetIdIsUUID = regexExpForUuid.test(widgetId); - const transaction = await db.sequelize.transaction(); - let role; - if (roleId) { - role = await RolesDBApi.findBy({ id: roleId }, { transaction }); - } else { - role = await RolesDBApi.findBy({ name: 'User' }, { transaction }); - } + const transaction = await db.sequelize.transaction(); + let role; + if (roleId) { + role = await RolesDBApi.findBy({ id: roleId }, { transaction }); + } else { + role = await RolesDBApi.findBy({ name: 'User' }, { transaction }); + } - if (!role) { - throw new ValidationError('rolesNotFound'); - } + if (!role) { + throw new ValidationError('rolesNotFound'); + } + try { + let customization = {}; try { - let customization = {}; - try { - customization = JSON.parse(role.role_customization || '{}'); - } catch (e) { - console.log(e); - } - - if (widgetIdIsUUID && Array.isArray(customization[key])) { - const el = customization[key].find((e) => e === widgetId); - !el ? customization[key].unshift(widgetId) : null; - } - - if (widgetIdIsUUID && !customization[key]) { - customization[key] = [widgetId]; - } - - const newRole = await RolesDBApi.update( - role.id, - { - role_customization: JSON.stringify(customization), - name: role.name, - permissions: role.permissions, - - }, - { - currentUser, - transaction, - }, - ); - - await transaction.commit(); - - return newRole; - } catch (error) { - await transaction.rollback(); - throw error; + customization = JSON.parse(role.role_customization || '{}'); + } catch (e) { + console.log(e); } + + if (widgetIdIsUUID && Array.isArray(customization[key])) { + const el = customization[key].find((e) => e === widgetId); + !el ? customization[key].unshift(widgetId) : null; + } + + if (widgetIdIsUUID && !customization[key]) { + customization[key] = [widgetId]; + } + + const newRole = await RolesDBApi.update( + role.id, + { + role_customization: JSON.stringify(customization), + name: role.name, + permissions: role.permissions, + }, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + + return newRole; + } catch (error) { + await transaction.rollback(); + throw error; + } } static async removeRoleInfoById(infoId, roleId, key, currentUser) { - const transaction = await db.sequelize.transaction(); + const transaction = await db.sequelize.transaction(); - let role; - if (roleId) { - role = await RolesDBApi.findBy({ id: roleId }, { transaction }); - } else { - role = await RolesDBApi.findBy({ name: 'User' }, { transaction }); - } - if (!role) { - await transaction.rollback(); - throw new ValidationError('rolesNotFound'); - } + let role; + if (roleId) { + role = await RolesDBApi.findBy({ id: roleId }, { transaction }); + } else { + role = await RolesDBApi.findBy({ name: 'User' }, { transaction }); + } + if (!role) { + await transaction.rollback(); + throw new ValidationError('rolesNotFound'); + } - let customization = {}; - try { - customization = JSON.parse(role.role_customization || '{}'); - } catch (e) { - console.log(e); - } + let customization = {}; + try { + customization = JSON.parse(role.role_customization || '{}'); + } catch (e) { + console.log(e); + } - customization[key] = customization[key].filter( - (item) => item !== infoId, + customization[key] = customization[key].filter((item) => item !== infoId); + + await axios.delete( + `${config.flHost}/${config.project_uuid}/project_customization_widgets/${infoId}.json`, + ); + try { + const result = await RolesDBApi.update( + role.id, + { + role_customization: JSON.stringify(customization), + name: role.name, + permissions: role.permissions, + }, + { + currentUser, + transaction, + }, ); - await axios.delete(`${config.flHost}/${config.project_uuid}/project_customization_widgets/${infoId}.json`); - try { - const result = await RolesDBApi.update( - role.id, - { - role_customization: JSON.stringify(customization), - name: role.name, - permissions: role.permissions, - - }, - { - currentUser, - transaction, - }, - ); - - await transaction.commit(); - return result; - } catch (error) { - await transaction.rollback(); - throw error; - } + await transaction.commit(); + return result; + } catch (error) { + await transaction.rollback(); + throw error; + } } static async getRoleInfoByKey(key, roleId) { - const transaction = await db.sequelize.transaction(); + const transaction = await db.sequelize.transaction(); - - let role; - try { - if (roleId) { - role = await RolesDBApi.findBy({ id: roleId }, { transaction }); + let role; + try { + if (roleId) { + role = await RolesDBApi.findBy({ id: roleId }, { transaction }); + } else { + role = await RolesDBApi.findBy({ name: 'User' }, { transaction }); + } + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + + if (!role) { + throw new ValidationError('Role not found'); + } + + let customization = '{}'; + + try { + customization = JSON.parse(role.role_customization || '{}'); + } catch (e) { + console.error('Failed to parse role customization JSON:', e); + throw e; + } + + if (key === 'widgets') { + const widgets = customization[key] || []; + const widgetArray = widgets.map((widget) => { + return axios.get( + `${config.flHost}/${config.project_uuid}/project_customization_widgets/${widget}.json`, + ); + }); + const widgetResults = await Promise.allSettled(widgetArray); + + const fulfilledWidgets = widgetResults + .filter((result) => result.status === 'fulfilled') + .map((result) => result.value.data); + + const widgetsResults = []; + + if (Array.isArray(fulfilledWidgets)) { + for (const widget of fulfilledWidgets) { + const result = await runSafeWidgetQuery(widget.query); + + if (result[0] && result[0].length) { + const key = Object.keys(result[0][0])[0]; + const value = + widget.widget_type === 'scalar' ? result[0][0][key] : result[0]; + const widgetData = JSON.parse(widget.data); + widgetsResults.push({ ...widget, ...widgetData, value }); } else { - role = await RolesDBApi.findBy({ name: 'User' }, { transaction }); + widgetsResults.push({ ...widget, value: null }); } - - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; + } } - - if (!role) { - throw new ValidationError('Role not found'); - } - - let customization = '{}'; - - try { - customization = JSON.parse(role.role_customization || '{}'); - } catch (e) { - console.error('Failed to parse role customization JSON:', e); - throw e; - } - - if (key === 'widgets') { - const widgets = (customization[key] || []); - const widgetArray = widgets.map(widget => { - return axios.get(`${config.flHost}/${config.project_uuid}/project_customization_widgets/${widget}.json`) - }) - const widgetResults = await Promise.allSettled(widgetArray); - - const fulfilledWidgets = widgetResults - .filter((result) => result.status === 'fulfilled') - .map((result) => result.value.data); - - const widgetsResults = []; - - if (Array.isArray(fulfilledWidgets)) { - for (const widget of fulfilledWidgets) { - const result = await runSafeWidgetQuery(widget.query); - - if (result[0] && result[0].length) { - const key = Object.keys(result[0][0])[0]; - const value = - widget.widget_type === 'scalar' ? result[0][0][key] : result[0]; - const widgetData = JSON.parse(widget.data); - widgetsResults.push({ ...widget, ...widgetData, value }); - } else { - widgetsResults.push({ ...widget, value: null }); - } - } - } - return widgetsResults; - } - return customization[key]; - - + return widgetsResults; + } + return customization[key]; } - - }; diff --git a/backend/src/services/search.js b/backend/src/services/search.js index c076374..23222d2 100644 --- a/backend/src/services/search.js +++ b/backend/src/services/search.js @@ -14,9 +14,7 @@ async function getUserPermissions(currentUser) { } const permissions = new Set( - (currentUser.custom_permissions || []) - .map((cp) => cp.name) - .filter(Boolean), + (currentUser.custom_permissions || []).map((cp) => cp.name).filter(Boolean), ); if (!currentUser.app_role) { @@ -42,352 +40,150 @@ function hasPermission(permissions, permissionName) { } module.exports = class SearchService { - static async search(searchQuery, currentUser ) { - if (!searchQuery) { - throw new ValidationError('iam.errors.searchQueryRequired'); + static async search(searchQuery, currentUser) { + if (!searchQuery) { + throw new ValidationError('iam.errors.searchQueryRequired'); + } + const tableColumns = { + users: ['firstName', 'lastName', 'phoneNumber', 'email'], + + projects: [ + 'name', + + 'slug', + + 'description', + + 'logo_url', + + 'favicon_url', + + 'og_image_url', + + 'theme_config_json', + + 'custom_css_json', + + 'cdn_base_url', + ], + + assets: ['name', 'cdn_url', 'storage_key', 'mime_type', 'checksum'], + + asset_variants: ['cdn_url'], + + presigned_url_requests: ['requested_key', 'mime_type', 'status'], + + tour_pages: [ + 'source_key', + + 'name', + + 'slug', + + 'background_image_url', + + 'background_video_url', + + 'background_audio_url', + + 'ui_schema_json', + ], + + project_audio_tracks: ['source_key', 'name', 'slug', 'url'], + + publish_events: ['error_message'], + + pwa_caches: ['cache_version', 'manifest_json', 'asset_list_json'], + + access_logs: ['path', 'ip_address', 'user_agent'], + }; + const columnsInt = { + assets: ['size_mb', 'width_px', 'height_px', 'duration_sec'], + + asset_variants: ['width_px', 'height_px', 'size_mb'], + + presigned_url_requests: ['requested_size_mb'], + + tour_pages: ['sort_order'], + + project_audio_tracks: ['volume', 'sort_order'], + + publish_events: ['pages_copied', 'transitions_copied', 'audios_copied'], + }; + + const permissionSet = await getUserPermissions(currentUser); + const normalizedSearchQuery = searchQuery.toLowerCase(); + const SEARCH_LIMIT_PER_TABLE = 50; + + const searchTasks = Object.keys(tableColumns).map(async (tableName) => { + if ( + !Object.prototype.hasOwnProperty.call(tableColumns, tableName) || + !db[tableName] + ) { + return []; } - const tableColumns = { - - - - - "users": [ - - "firstName", - - "lastName", - - "phoneNumber", - - "email", - - ], - - - - - - + if (!hasPermission(permissionSet, `READ_${tableName.toUpperCase()}`)) { + return []; + } - - "projects": [ - - "name", - - "slug", - - "description", - - "logo_url", - - "favicon_url", - - "og_image_url", - - "theme_config_json", - - "custom_css_json", - - "cdn_base_url", - - ], - - - - - - - - - - - - "assets": [ - - "name", - - "cdn_url", - - "storage_key", - - "mime_type", - - "checksum", - - ], - - - - - - - "asset_variants": [ - - "cdn_url", - - ], - - - - - - - "presigned_url_requests": [ - - "requested_key", - - "mime_type", - - "status", - - ], - - - - - - - "tour_pages": [ - - "source_key", - - "name", - - "slug", - - "background_image_url", - - "background_video_url", - - "background_audio_url", - - "ui_schema_json", - - ], - - - - - - - "project_audio_tracks": [ - - "source_key", - - "name", - - "slug", - - "url", - - ], - - - - - - - "publish_events": [ - - "error_message", - - ], - - - - - - - "pwa_caches": [ - - "cache_version", - - "manifest_json", - - "asset_list_json", - - ], - - - - - - - "access_logs": [ - - "path", - - "ip_address", - - "user_agent", - - ], - - - }; - const columnsInt = { - - - - - - - - - - - - - - - - - - - "assets": [ - - "size_mb", - - "width_px", - - "height_px", - - "duration_sec", - - ], - - - - - - "asset_variants": [ - - "width_px", - - "height_px", - - "size_mb", - - ], - - - - - - "presigned_url_requests": [ - - "requested_size_mb", - - ], - - - - - - "tour_pages": [ - - "sort_order", - - ], - - - - "project_audio_tracks": [ - - "volume", - - "sort_order", - - ], - - - - - - "publish_events": [ - - "pages_copied", - - "transitions_copied", - - "audios_copied", - - ], - - - - - - - - - - + const attributesToSearch = tableColumns[tableName]; + const attributesIntToSearch = columnsInt[tableName] || []; + const whereCondition = { + [Op.or]: [ + ...attributesToSearch.map((attribute) => ({ + [attribute]: { + [Op.iLike]: `%${searchQuery}%`, + }, + })), + ...attributesIntToSearch.map((attribute) => + Sequelize.where( + Sequelize.cast( + Sequelize.col(`${tableName}.${attribute}`), + 'varchar', + ), + { [Op.iLike]: `%${searchQuery}%` }, + ), + ), + ], }; - const permissionSet = await getUserPermissions(currentUser); - const normalizedSearchQuery = searchQuery.toLowerCase(); - const SEARCH_LIMIT_PER_TABLE = 50; - - const searchTasks = Object.keys(tableColumns).map(async (tableName) => { - if (!Object.prototype.hasOwnProperty.call(tableColumns, tableName) || !db[tableName]) { - return []; - } - - if (!hasPermission(permissionSet, `READ_${tableName.toUpperCase()}`)) { - return []; - } - - const attributesToSearch = tableColumns[tableName]; - const attributesIntToSearch = columnsInt[tableName] || []; - const whereCondition = { - [Op.or]: [ - ...attributesToSearch.map((attribute) => ({ - [attribute]: { - [Op.iLike] : `%${searchQuery}%`, - }, - })), - ...attributesIntToSearch.map((attribute) => ( - Sequelize.where( - Sequelize.cast(Sequelize.col(`${tableName}.${attribute}`), 'varchar'), - { [Op.iLike]: `%${searchQuery}%` }, - ) - )), - ], - }; - - const foundRecords = await db[tableName].findAll({ - where: whereCondition, - attributes: [...attributesToSearch, 'id', ...attributesIntToSearch], - limit: SEARCH_LIMIT_PER_TABLE, - }); - - return foundRecords.map((record) => { - const matchAttribute = []; - - for (const attribute of attributesToSearch) { - if (record[attribute]?.toLowerCase()?.includes(normalizedSearchQuery)) { - matchAttribute.push(attribute); - } - } - - for (const attribute of attributesIntToSearch) { - const castedValue = String(record[attribute]); - if (castedValue && castedValue.toLowerCase().includes(normalizedSearchQuery)) { - matchAttribute.push(attribute); - } - } - - return { - ...record.get(), - matchAttribute, - tableName, - }; - }); + const foundRecords = await db[tableName].findAll({ + where: whereCondition, + attributes: [...attributesToSearch, 'id', ...attributesIntToSearch], + limit: SEARCH_LIMIT_PER_TABLE, }); - const resultsByTable = await Promise.all(searchTasks); - return resultsByTable.flat(); + return foundRecords.map((record) => { + const matchAttribute = []; + + for (const attribute of attributesToSearch) { + if ( + record[attribute]?.toLowerCase()?.includes(normalizedSearchQuery) + ) { + matchAttribute.push(attribute); + } + } + + for (const attribute of attributesIntToSearch) { + const castedValue = String(record[attribute]); + if ( + castedValue && + castedValue.toLowerCase().includes(normalizedSearchQuery) + ) { + matchAttribute.push(attribute); + } + } + + return { + ...record.get(), + matchAttribute, + tableName, + }; + }); + }); + + const resultsByTable = await Promise.all(searchTasks); + return resultsByTable.flat(); } -} +}; diff --git a/backend/src/services/users.js b/backend/src/services/users.js index 980058d..96a0004 100644 --- a/backend/src/services/users.js +++ b/backend/src/services/users.js @@ -1,31 +1,28 @@ const db = require('../db/models'); const UsersDBApi = require('../db/api/users'); -const processFile = require("../middlewares/upload"); +const processFile = require('../middlewares/upload'); const ValidationError = require('./notifications/errors/validation'); const csv = require('csv-parser'); const config = require('../config'); const stream = require('stream'); - const AuthService = require('./auth'); module.exports = class UsersService { static async create(data, currentUser, sendInvitationEmails = true, host) { let transaction = await db.sequelize.transaction(); - + let email = data.email; let emailsToInvite = []; try { if (email) { - let user = await UsersDBApi.findBy({email}, {transaction}); + let user = await UsersDBApi.findBy({ email }, { transaction }); if (user) { - throw new ValidationError( - 'iam.errors.userAlreadyExists', - ); + throw new ValidationError('iam.errors.userAlreadyExists'); } else { await UsersDBApi.create( - {data}, - + { data }, + { currentUser, transaction, @@ -34,7 +31,7 @@ module.exports = class UsersService { emailsToInvite.push(email); } } else { - throw new ValidationError('iam.errors.emailRequired') + throw new ValidationError('iam.errors.emailRequired'); } await transaction.commit(); } catch (error) { @@ -57,15 +54,15 @@ module.exports = class UsersService { const bufferStream = new stream.PassThrough(); const results = []; - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream await new Promise((resolve, reject) => { bufferStream .pipe(csv()) .on('data', (data) => results.push(data)) .on('end', () => { - console.log('results csv', results); - resolve(); + console.log('results csv', results); + resolve(); }) .on('error', (error) => reject(error)); }); @@ -77,10 +74,10 @@ module.exports = class UsersService { } await UsersDBApi.bulkImport(results, { - transaction, - ignoreDuplicates: true, - validate: true, - currentUser: req.currentUser + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, }); emailsToInvite = results.map((result) => result.email); @@ -92,7 +89,6 @@ module.exports = class UsersService { } if (emailsToInvite && emailsToInvite.length && !sendInvitationEmails) { - emailsToInvite.forEach((email) => { AuthService.sendPasswordResetEmail(email, 'invitation', host); }); @@ -101,17 +97,12 @@ module.exports = class UsersService { static async update(data, id, currentUser) { const transaction = await db.sequelize.transaction(); - + try { - let users = await UsersDBApi.findBy( - {id}, - {transaction}, - ); + let users = await UsersDBApi.findBy({ id }, { transaction }); if (!users) { - throw new ValidationError( - 'iam.errors.userNotFound', - ); + throw new ValidationError('iam.errors.userNotFound'); } const updatedUser = await UsersDBApi.update( @@ -126,7 +117,6 @@ module.exports = class UsersService { await transaction.commit(); return updatedUser; - } catch (error) { await transaction.rollback(); throw error; @@ -138,24 +128,17 @@ module.exports = class UsersService { try { if (currentUser.id === id) { - throw new ValidationError( - 'iam.errors.deletingHimself', - ); + throw new ValidationError('iam.errors.deletingHimself'); } - if (currentUser.app_role?.name !== config.roles.admin ) { - throw new ValidationError( - 'errors.forbidden.message', - ); + if (currentUser.app_role?.name !== config.roles.admin) { + throw new ValidationError('errors.forbidden.message'); } - await UsersDBApi.remove( - id, - { - currentUser, - transaction, - }, - ); + await UsersDBApi.remove(id, { + currentUser, + transaction, + }); await transaction.commit(); } catch (error) { @@ -164,5 +147,3 @@ module.exports = class UsersService { } } }; - - diff --git a/backend/src/utils/circuit-breaker.js b/backend/src/utils/circuit-breaker.js index 72b0b94..557f443 100644 --- a/backend/src/utils/circuit-breaker.js +++ b/backend/src/utils/circuit-breaker.js @@ -13,11 +13,17 @@ class CircuitBreaker { async execute(fn) { if (this.state === 'OPEN') { if (Date.now() < this.nextAttempt) { - logger.warn({ circuitBreaker: this.name, state: this.state }, 'Circuit breaker is OPEN, rejecting request'); + logger.warn( + { circuitBreaker: this.name, state: this.state }, + 'Circuit breaker is OPEN, rejecting request', + ); throw new Error(`Circuit breaker ${this.name} is OPEN`); } this.state = 'HALF-OPEN'; - logger.info({ circuitBreaker: this.name }, 'Circuit breaker moved to HALF-OPEN'); + logger.info( + { circuitBreaker: this.name }, + 'Circuit breaker moved to HALF-OPEN', + ); } try { @@ -32,7 +38,10 @@ class CircuitBreaker { onSuccess() { if (this.state === 'HALF-OPEN') { - logger.info({ circuitBreaker: this.name }, 'Circuit breaker recovered, moving to CLOSED'); + logger.info( + { circuitBreaker: this.name }, + 'Circuit breaker recovered, moving to CLOSED', + ); } this.failures = 0; this.state = 'CLOSED'; @@ -40,20 +49,26 @@ class CircuitBreaker { onFailure(error) { this.failures++; - logger.warn({ - circuitBreaker: this.name, - failures: this.failures, - threshold: this.failureThreshold, - error: error.message, - }, 'Circuit breaker recorded failure'); + logger.warn( + { + circuitBreaker: this.name, + failures: this.failures, + threshold: this.failureThreshold, + error: error.message, + }, + 'Circuit breaker recorded failure', + ); if (this.failures >= this.failureThreshold) { this.state = 'OPEN'; this.nextAttempt = Date.now() + this.resetTimeout; - logger.error({ - circuitBreaker: this.name, - resetAt: new Date(this.nextAttempt).toISOString(), - }, 'Circuit breaker tripped to OPEN'); + logger.error( + { + circuitBreaker: this.name, + resetAt: new Date(this.nextAttempt).toISOString(), + }, + 'Circuit breaker tripped to OPEN', + ); } } @@ -62,7 +77,8 @@ class CircuitBreaker { name: this.name, state: this.state, failures: this.failures, - nextAttempt: this.state === 'OPEN' ? new Date(this.nextAttempt).toISOString() : null, + nextAttempt: + this.state === 'OPEN' ? new Date(this.nextAttempt).toISOString() : null, }; } } diff --git a/backend/src/utils/env-validation.js b/backend/src/utils/env-validation.js index e2e862c..c43ca4d 100644 --- a/backend/src/utils/env-validation.js +++ b/backend/src/utils/env-validation.js @@ -13,7 +13,9 @@ const envSchema = Joi.object({ DB_USER: Joi.string().default('postgres'), DB_PASS: Joi.string().allow('').default(''), - SECRET_KEY: Joi.string().min(16).default('88dbeaf8-e906-405e-9e41-c3baadeda5c6'), + SECRET_KEY: Joi.string() + .min(16) + .default('88dbeaf8-e906-405e-9e41-c3baadeda5c6'), ADMIN_PASS: Joi.string().default('88dbeaf8'), USER_PASS: Joi.string().default('c3baadeda5c6'), @@ -32,7 +34,9 @@ const envSchema = Joi.object({ EMAIL_USER: Joi.string().allow('').default(''), EMAIL_PASS: Joi.string().allow('').default(''), - EMAIL_TLS_REJECT_UNAUTHORIZED: Joi.string().valid('true', 'false').default('true'), + EMAIL_TLS_REJECT_UNAUTHORIZED: Joi.string() + .valid('true', 'false') + .default('true'), GPT_KEY: Joi.string().allow('').default(''), PEXELS_KEY: Joi.string().allow('').default(''), @@ -49,7 +53,7 @@ function validateEnv() { }); if (error) { - const messages = error.details.map(d => ` - ${d.message}`).join('\n'); + const messages = error.details.map((d) => ` - ${d.message}`).join('\n'); console.error('Environment validation failed:\n' + messages); if (process.env.NODE_ENV === 'production') { diff --git a/backend/src/utils/events.js b/backend/src/utils/events.js index 1388a0f..7d2cee4 100644 --- a/backend/src/utils/events.js +++ b/backend/src/utils/events.js @@ -3,19 +3,28 @@ const { logger } = require('./logger'); class AppEventEmitter extends EventEmitter { emit(event, data) { - logger.debug({ event, hasListeners: this.listenerCount(event) > 0 }, 'Event emitted'); + logger.debug( + { event, hasListeners: this.listenerCount(event) > 0 }, + 'Event emitted', + ); return super.emit(event, data); } async emitAsync(event, data) { - logger.debug({ event, listenerCount: this.listenerCount(event) }, 'Async event emitted'); + logger.debug( + { event, listenerCount: this.listenerCount(event) }, + 'Async event emitted', + ); const results = await Promise.allSettled( - this.listeners(event).map(listener => listener(data)) + this.listeners(event).map((listener) => listener(data)), ); - const failures = results.filter(r => r.status === 'rejected'); + const failures = results.filter((r) => r.status === 'rejected'); if (failures.length > 0) { - logger.warn({ event, failures: failures.map(f => f.reason?.message) }, 'Some event listeners failed'); + logger.warn( + { event, failures: failures.map((f) => f.reason?.message) }, + 'Some event listeners failed', + ); } return results; @@ -26,7 +35,10 @@ class AppEventEmitter extends EventEmitter { try { await listener(data); } catch (error) { - logger.error({ event, error: error.message }, 'Async event listener error'); + logger.error( + { event, error: error.message }, + 'Async event listener error', + ); } }); } @@ -35,15 +47,24 @@ class AppEventEmitter extends EventEmitter { const appEvents = new AppEventEmitter(); appEvents.on('user.created', (user) => { - logger.info({ userId: user.id, email: user.email }, 'User created event received'); + logger.info( + { userId: user.id, email: user.email }, + 'User created event received', + ); }); appEvents.on('project.created', (project) => { - logger.info({ projectId: project.id, name: project.name }, 'Project created event received'); + logger.info( + { projectId: project.id, name: project.name }, + 'Project created event received', + ); }); appEvents.on('project.published', (project) => { - logger.info({ projectId: project.id, slug: project.slug }, 'Project published event received'); + logger.info( + { projectId: project.id, slug: project.slug }, + 'Project published event received', + ); }); appEvents.on('error', (error) => { diff --git a/frontend/src/components/ElementSettings/CommonSettingsSection.tsx b/frontend/src/components/ElementSettings/CommonSettingsSection.tsx index f774333..b6e1722 100644 --- a/frontend/src/components/ElementSettings/CommonSettingsSection.tsx +++ b/frontend/src/components/ElementSettings/CommonSettingsSection.tsx @@ -16,16 +16,19 @@ const CommonSettingsSection: React.FC = ({ appearDelaySec, appearDurationSec, onChange, + showLabel = true, showPosition = true, }) => { return (
- - onChange('label', event.target.value)} - /> - + {showLabel && ( + + onChange('label', event.target.value)} + /> + + )} {showPosition && (
diff --git a/frontend/src/components/ElementSettings/EffectsSettingsSection.tsx b/frontend/src/components/ElementSettings/EffectsSettingsSection.tsx new file mode 100644 index 0000000..3950647 --- /dev/null +++ b/frontend/src/components/ElementSettings/EffectsSettingsSection.tsx @@ -0,0 +1,194 @@ +/** + * EffectsSettingsSection + * + * Animation and interaction effect settings for UI elements. + * Used in element-type-defaults, project-element-defaults, and constructor pages. + */ + +import React from 'react'; +import FormField from '../FormField'; +import type { EffectsSettingsSectionProps } from './types'; + +const EffectsSettingsSection: React.FC = ({ + values, + onChange, +}) => { + return ( +
+ {/* Appear Animation */} +
+

Appear Animation

+

+ Animation played when the element appears on screen. +

+
+ + + + + + onChange('appearAnimationDuration', e.target.value) + } + placeholder='e.g. 0.3s' + /> + + + + +
+
+ + {/* Hover Effects */} +
+

Hover Effects

+

+ Visual changes when hovering over the element. +

+
+ + onChange('hoverScale', e.target.value)} + placeholder='e.g. 1.05' + /> + + + onChange('hoverOpacity', e.target.value)} + placeholder='0..1' + /> + + + onChange('hoverBackgroundColor', e.target.value)} + placeholder='e.g. #B39368' + /> + + + onChange('hoverColor', e.target.value)} + placeholder='e.g. #FFFFFF' + /> + + + onChange('hoverBoxShadow', e.target.value)} + placeholder='e.g. 0 4px 12px rgba(...)' + /> + + + + onChange('hoverTransitionDuration', e.target.value) + } + placeholder='e.g. 0.2s' + /> + +
+
+ + {/* Focus Effects */} +
+

Focus Effects

+

+ Visual changes when the element receives keyboard focus. +

+
+ + onChange('focusScale', e.target.value)} + placeholder='e.g. 1.02' + /> + + + onChange('focusOutline', e.target.value)} + placeholder='e.g. 2px solid #B39368' + /> + + + onChange('focusBoxShadow', e.target.value)} + placeholder='e.g. 0 0 0 3px rgba(...)' + /> + + + onChange('focusOpacity', e.target.value)} + placeholder='0..1' + /> + +
+
+ + {/* Active/Press Effects */} +
+

Active/Press Effects

+

+ Visual changes when the element is clicked or pressed. +

+
+ + onChange('activeScale', e.target.value)} + placeholder='e.g. 0.95' + /> + + + onChange('activeOpacity', e.target.value)} + placeholder='0..1' + /> + + + + onChange('activeBackgroundColor', e.target.value) + } + placeholder='e.g. #131C22' + /> + +
+
+
+ ); +}; + +export default EffectsSettingsSection; diff --git a/frontend/src/components/ElementSettings/EffectsSettingsSectionCompact.tsx b/frontend/src/components/ElementSettings/EffectsSettingsSectionCompact.tsx new file mode 100644 index 0000000..7ea9910 --- /dev/null +++ b/frontend/src/components/ElementSettings/EffectsSettingsSectionCompact.tsx @@ -0,0 +1,250 @@ +/** + * EffectsSettingsSectionCompact + * + * Compact animation and interaction effect settings for the constructor sidebar. + * Uses smaller inputs and labels to fit in the narrow sidebar. + */ + +import React from 'react'; +import type { EffectsSettingsSectionProps } from './types'; + +const EffectsSettingsSectionCompact: React.FC = ({ + values, + onChange, +}) => { + return ( +
+ {/* Appear Animation */} +
+

+ Appear Animation +

+
+
+ + +
+
+ + + onChange('appearAnimationDuration', e.target.value) + } + placeholder='0.3s' + /> +
+
+ + +
+
+
+ + {/* Hover Effects */} +
+

+ Hover Effects +

+
+
+ + onChange('hoverScale', e.target.value)} + placeholder='1.05' + /> +
+
+ + onChange('hoverOpacity', e.target.value)} + placeholder='0..1' + /> +
+
+ + onChange('hoverBackgroundColor', e.target.value)} + placeholder='#B39368' + /> +
+
+ + onChange('hoverColor', e.target.value)} + placeholder='#FFFFFF' + /> +
+
+ + onChange('hoverBoxShadow', e.target.value)} + placeholder='0 4px 12px rgba(...)' + /> +
+
+ + + onChange('hoverTransitionDuration', e.target.value) + } + placeholder='0.2s' + /> +
+
+
+ + {/* Focus Effects */} +
+

+ Focus Effects +

+
+
+ + onChange('focusScale', e.target.value)} + placeholder='1.02' + /> +
+
+ + onChange('focusOpacity', e.target.value)} + placeholder='0..1' + /> +
+
+ + onChange('focusOutline', e.target.value)} + placeholder='2px solid #B39368' + /> +
+
+ + onChange('focusBoxShadow', e.target.value)} + placeholder='0 0 0 3px rgba(...)' + /> +
+
+
+ + {/* Active/Press Effects */} +
+

+ Active/Press Effects +

+
+
+ + onChange('activeScale', e.target.value)} + placeholder='0.95' + /> +
+
+ + onChange('activeOpacity', e.target.value)} + placeholder='0..1' + /> +
+
+ + + onChange('activeBackgroundColor', e.target.value) + } + placeholder='#131C22' + /> +
+
+
+
+ ); +}; + +export default EffectsSettingsSectionCompact; diff --git a/frontend/src/components/ElementSettings/NavigationSettingsSection.tsx b/frontend/src/components/ElementSettings/NavigationSettingsSection.tsx index 7d70cdd..6dc4845 100644 --- a/frontend/src/components/ElementSettings/NavigationSettingsSection.tsx +++ b/frontend/src/components/ElementSettings/NavigationSettingsSection.tsx @@ -18,7 +18,6 @@ const NavigationSettingsSection: React.FC = ({ transitionVideoUrl, transitionReverseMode, reverseVideoUrl, - transitionDurationSec, onChange, context, iconAssetOptions = [], @@ -26,10 +25,13 @@ const NavigationSettingsSection: React.FC = ({ pageOptions = [], }) => { const isConstructor = context === 'constructor'; + const isGlobal = context === 'global'; + const isProject = context === 'project'; return (
- {isConstructor ? ( + {/* Icon: asset selector for constructor/project, text input for global */} + {isConstructor || isProject ? ( onChange('navLabel', event.target.value)} - /> - - - - - - - - - - - {isConstructor && pageOptions.length > 0 ? ( - - - - ) : ( - - onChange('targetPageId', event.target.value)} - /> - - )} - - {isConstructor ? ( - - - - ) : ( - - - onChange('transitionVideoUrl', event.target.value) - } - /> - - )} - - - - - - {transitionReverseMode === 'separate_video' && ( + {/* Instance-specific fields only shown for constructor */} + {isConstructor && ( <> - {isConstructor ? ( + + onChange('navLabel', event.target.value)} + /> + + + + + + + )} + + {/* Disabled checkbox only shown for constructor */} + {isConstructor && ( + + + + )} + + {/* Target page only shown for constructor */} + {isConstructor && ( + <> + {pageOptions.length > 0 ? ( + + + + ) : ( + + + onChange('targetPageId', event.target.value) + } + /> + + )} + + )} + + {/* Transition settings only shown for constructor */} + {isConstructor && ( + <> + + + + + + + + + {transitionReverseMode === 'separate_video' && ( - ) : ( - - - onChange('reverseVideoUrl', event.target.value) - } - /> - )} )} - - - - onChange('transitionDurationSec', event.target.value) - } - /> -
); }; diff --git a/frontend/src/components/ElementSettings/StyleSettingsSection.tsx b/frontend/src/components/ElementSettings/StyleSettingsSection.tsx index 50ce275..65f6d27 100644 --- a/frontend/src/components/ElementSettings/StyleSettingsSection.tsx +++ b/frontend/src/components/ElementSettings/StyleSettingsSection.tsx @@ -224,6 +224,29 @@ const StyleSettingsSection: React.FC = ({ placeholder='e.g. 1 / 10' /> + + + onChange('backgroundColor', event.target.value) + } + placeholder='e.g. #E7DDB5 / rgba(...)' + /> + + + onChange('color', event.target.value)} + placeholder='e.g. #131C22 / inherit' + /> + + + onChange('fontFamily', event.target.value)} + placeholder='e.g. Montserrat, sans-serif' + /> +
); diff --git a/frontend/src/components/ElementSettings/StyleSettingsSectionCompact.tsx b/frontend/src/components/ElementSettings/StyleSettingsSectionCompact.tsx index 4c17a0c..b4efe5e 100644 --- a/frontend/src/components/ElementSettings/StyleSettingsSectionCompact.tsx +++ b/frontend/src/components/ElementSettings/StyleSettingsSectionCompact.tsx @@ -313,6 +313,40 @@ const StyleSettingsSectionCompact: React.FC = ({ +
+ + onChange('backgroundColor', e.target.value)} + placeholder='#E7DDB5' + /> +
+
+ + onChange('color', e.target.value)} + placeholder='#131C22' + /> +
+ + +
+ + onChange('fontFamily', e.target.value)} + placeholder='Montserrat, sans-serif' + />
); diff --git a/frontend/src/components/ElementSettings/index.ts b/frontend/src/components/ElementSettings/index.ts index d66e15c..fa49a97 100644 --- a/frontend/src/components/ElementSettings/index.ts +++ b/frontend/src/components/ElementSettings/index.ts @@ -11,6 +11,8 @@ export { } from './ElementSettingsTabs'; export { default as StyleSettingsSection } from './StyleSettingsSection'; export { default as StyleSettingsSectionCompact } from './StyleSettingsSectionCompact'; +export { default as EffectsSettingsSection } from './EffectsSettingsSection'; +export { default as EffectsSettingsSectionCompact } from './EffectsSettingsSectionCompact'; export { default as CommonSettingsSection } from './CommonSettingsSection'; export { default as NavigationSettingsSection } from './NavigationSettingsSection'; export { default as TooltipSettingsSection } from './TooltipSettingsSection'; diff --git a/frontend/src/components/ElementSettings/types.ts b/frontend/src/components/ElementSettings/types.ts index 63e8887..0d22113 100644 --- a/frontend/src/components/ElementSettings/types.ts +++ b/frontend/src/components/ElementSettings/types.ts @@ -6,6 +6,7 @@ */ import type { ElementStyleProperties } from '../../lib/elementStyles'; +import type { ElementEffectProperties } from '../../lib/elementEffects'; import type { CanvasElement, CanvasElementType, @@ -31,6 +32,36 @@ export interface StyleSettingsSectionProps { onChange: (prop: keyof ElementStyleProperties, value: string) => void; } +/** + * Form values for effects settings section (uses strings for form inputs) + */ +export interface EffectsSettingsFormValues { + appearAnimation?: string; + appearAnimationDuration?: string; + appearAnimationEasing?: string; + hoverScale?: string; + hoverOpacity?: string; + hoverBackgroundColor?: string; + hoverColor?: string; + hoverBoxShadow?: string; + hoverTransitionDuration?: string; + focusScale?: string; + focusOpacity?: string; + focusOutline?: string; + focusBoxShadow?: string; + activeScale?: string; + activeOpacity?: string; + activeBackgroundColor?: string; +} + +/** + * Props for effects settings section + */ +export interface EffectsSettingsSectionProps { + values: EffectsSettingsFormValues; + onChange: (prop: keyof EffectsSettingsFormValues, value: string) => void; +} + /** * Props for common settings section (label, position, appear timing) */ @@ -41,6 +72,7 @@ export interface CommonSettingsSectionProps { appearDelaySec: string; appearDurationSec: string; onChange: (field: string, value: string) => void; + showLabel?: boolean; showPosition?: boolean; } @@ -57,7 +89,6 @@ export interface NavigationSettingsSectionProps { transitionVideoUrl: string; transitionReverseMode: 'auto_reverse' | 'separate_video'; reverseVideoUrl: string; - transitionDurationSec: string; onChange: (field: string, value: string | boolean) => void; context: ElementSettingsContext; iconAssetOptions?: AssetOption[]; diff --git a/frontend/src/components/ElementSettings/useElementSettingsForm.ts b/frontend/src/components/ElementSettings/useElementSettingsForm.ts index 4945b48..8f6f674 100644 --- a/frontend/src/components/ElementSettings/useElementSettingsForm.ts +++ b/frontend/src/components/ElementSettings/useElementSettingsForm.ts @@ -58,6 +58,27 @@ interface FormState { alignItems: string; textAlign: string; zIndex: string; + backgroundColor: string; + color: string; + fontFamily: string; + + // Effect settings + appearAnimation: string; + appearAnimationDuration: string; + appearAnimationEasing: string; + hoverScale: string; + hoverOpacity: string; + hoverBackgroundColor: string; + hoverColor: string; + hoverBoxShadow: string; + hoverTransitionDuration: string; + focusScale: string; + focusOpacity: string; + focusOutline: string; + focusBoxShadow: string; + activeScale: string; + activeOpacity: string; + activeBackgroundColor: string; // Navigation settings iconUrl: string; @@ -69,7 +90,6 @@ interface FormState { transitionVideoUrl: string; transitionReverseMode: 'auto_reverse' | 'separate_video'; reverseVideoUrl: string; - transitionDurationSec: string; // Tooltip settings tooltipTitle: string; @@ -129,6 +149,26 @@ const initialState: FormState = { alignItems: '', textAlign: '', zIndex: '', + backgroundColor: '', + color: '', + fontFamily: '', + // Effect settings + appearAnimation: '', + appearAnimationDuration: '', + appearAnimationEasing: '', + hoverScale: '', + hoverOpacity: '', + hoverBackgroundColor: '', + hoverColor: '', + hoverBoxShadow: '', + hoverTransitionDuration: '', + focusScale: '', + focusOpacity: '', + focusOutline: '', + focusBoxShadow: '', + activeScale: '', + activeOpacity: '', + activeBackgroundColor: '', iconUrl: '', navLabel: '', navType: 'forward', @@ -138,7 +178,6 @@ const initialState: FormState = { transitionVideoUrl: '', transitionReverseMode: 'auto_reverse', reverseVideoUrl: '', - transitionDurationSec: '0.7', tooltipTitle: '', tooltipText: '', descriptionTitle: '', @@ -210,6 +249,26 @@ export function useElementSettingsForm(options: UseElementSettingsFormOptions) { alignItems: String(settings.alignItems || ''), textAlign: String(settings.textAlign || ''), zIndex: String(settings.zIndex || ''), + backgroundColor: String(settings.backgroundColor || ''), + color: String(settings.color || ''), + fontFamily: String(settings.fontFamily || ''), + // Effect settings + appearAnimation: String(settings.appearAnimation || ''), + appearAnimationDuration: String(settings.appearAnimationDuration || ''), + appearAnimationEasing: String(settings.appearAnimationEasing || ''), + hoverScale: String(settings.hoverScale || ''), + hoverOpacity: String(settings.hoverOpacity || ''), + hoverBackgroundColor: String(settings.hoverBackgroundColor || ''), + hoverColor: String(settings.hoverColor || ''), + hoverBoxShadow: String(settings.hoverBoxShadow || ''), + hoverTransitionDuration: String(settings.hoverTransitionDuration || ''), + focusScale: String(settings.focusScale || ''), + focusOpacity: String(settings.focusOpacity || ''), + focusOutline: String(settings.focusOutline || ''), + focusBoxShadow: String(settings.focusBoxShadow || ''), + activeScale: String(settings.activeScale || ''), + activeOpacity: String(settings.activeOpacity || ''), + activeBackgroundColor: String(settings.activeBackgroundColor || ''), appearDelaySec: String(settings.appearDelaySec ?? 0), appearDurationSec: settings.appearDurationSec === null || @@ -228,7 +287,6 @@ export function useElementSettingsForm(options: UseElementSettingsFormOptions) { ? 'separate_video' : 'auto_reverse', reverseVideoUrl: String(settings.reverseVideoUrl || ''), - transitionDurationSec: String(settings.transitionDurationSec ?? 0.7), tooltipTitle: String(settings.tooltipTitle || ''), tooltipText: String(settings.tooltipText || ''), descriptionTitle: String(settings.descriptionTitle || ''), @@ -322,6 +380,33 @@ export function useElementSettingsForm(options: UseElementSettingsFormOptions) { alignItems: state.alignItems, textAlign: state.textAlign, zIndex: state.zIndex, + backgroundColor: state.backgroundColor, + color: state.color, + fontFamily: state.fontFamily, + }; + }, [state]); + + /** + * Get effect values for EffectsSettingsSection + */ + const getEffectValues = useCallback(() => { + return { + appearAnimation: state.appearAnimation, + appearAnimationDuration: state.appearAnimationDuration, + appearAnimationEasing: state.appearAnimationEasing, + hoverScale: state.hoverScale, + hoverOpacity: state.hoverOpacity, + hoverBackgroundColor: state.hoverBackgroundColor, + hoverColor: state.hoverColor, + hoverBoxShadow: state.hoverBoxShadow, + hoverTransitionDuration: state.hoverTransitionDuration, + focusScale: state.focusScale, + focusOpacity: state.focusOpacity, + focusOutline: state.focusOutline, + focusBoxShadow: state.focusBoxShadow, + activeScale: state.activeScale, + activeOpacity: state.activeOpacity, + activeBackgroundColor: state.activeBackgroundColor, }; }, [state]); @@ -405,6 +490,7 @@ export function useElementSettingsForm(options: UseElementSettingsFormOptions) { */ const buildSettingsJson = useCallback((): Record => { const borderWidthValue = toUnitValue(state.border, 'px'); + const borderRadiusValue = toUnitValue(state.borderRadius, 'px'); const settings: Record = { label: state.label.trim(), xPercent: clampPercent(state.xPercent), @@ -412,12 +498,14 @@ export function useElementSettingsForm(options: UseElementSettingsFormOptions) { border: borderWidthValue ? `${borderWidthValue} solid currentColor` : 'none', - borderRadius: toUnitValue(state.borderRadius, 'px') || '0px', appearDelaySec: Number(state.appearDelaySec) >= 0 ? Number(state.appearDelaySec) : 0, appearDurationSec: parseNullableNumber(state.appearDurationSec), }; + // borderRadius: only include if has value (allows cascade from global defaults) + if (borderRadiusValue) settings.borderRadius = borderRadiusValue; + // Dimensional CSS properties const widthValue = toUnitValue(state.width, 'vw'); const heightValue = toUnitValue(state.height, 'vh'); @@ -464,6 +552,65 @@ export function useElementSettingsForm(options: UseElementSettingsFormOptions) { if (textAlignValue) settings.textAlign = textAlignValue; if (zIndexValue) settings.zIndex = zIndexValue; + // Additional CSS properties + const backgroundColorValue = toOptionalTrimmed(state.backgroundColor); + const colorValue = toOptionalTrimmed(state.color); + const fontFamilyValue = toOptionalTrimmed(state.fontFamily); + + if (backgroundColorValue) settings.backgroundColor = backgroundColorValue; + if (colorValue) settings.color = colorValue; + if (fontFamilyValue) settings.fontFamily = fontFamilyValue; + + // Effect properties + const appearAnimationValue = toOptionalTrimmed(state.appearAnimation); + const appearAnimationDurationValue = toOptionalTrimmed( + state.appearAnimationDuration, + ); + const appearAnimationEasingValue = toOptionalTrimmed( + state.appearAnimationEasing, + ); + const hoverScaleValue = toOptionalTrimmed(state.hoverScale); + const hoverOpacityValue = toOptionalTrimmed(state.hoverOpacity); + const hoverBackgroundColorValue = toOptionalTrimmed( + state.hoverBackgroundColor, + ); + const hoverColorValue = toOptionalTrimmed(state.hoverColor); + const hoverBoxShadowValue = toOptionalTrimmed(state.hoverBoxShadow); + const hoverTransitionDurationValue = toOptionalTrimmed( + state.hoverTransitionDuration, + ); + const focusScaleValue = toOptionalTrimmed(state.focusScale); + const focusOpacityValue = toOptionalTrimmed(state.focusOpacity); + const focusOutlineValue = toOptionalTrimmed(state.focusOutline); + const focusBoxShadowValue = toOptionalTrimmed(state.focusBoxShadow); + const activeScaleValue = toOptionalTrimmed(state.activeScale); + const activeOpacityValue = toOptionalTrimmed(state.activeOpacity); + const activeBackgroundColorValue = toOptionalTrimmed( + state.activeBackgroundColor, + ); + + if (appearAnimationValue) settings.appearAnimation = appearAnimationValue; + if (appearAnimationDurationValue) + settings.appearAnimationDuration = appearAnimationDurationValue; + if (appearAnimationEasingValue) + settings.appearAnimationEasing = appearAnimationEasingValue; + if (hoverScaleValue) settings.hoverScale = hoverScaleValue; + if (hoverOpacityValue) settings.hoverOpacity = hoverOpacityValue; + if (hoverBackgroundColorValue) + settings.hoverBackgroundColor = hoverBackgroundColorValue; + if (hoverColorValue) settings.hoverColor = hoverColorValue; + if (hoverBoxShadowValue) settings.hoverBoxShadow = hoverBoxShadowValue; + if (hoverTransitionDurationValue) + settings.hoverTransitionDuration = hoverTransitionDurationValue; + if (focusScaleValue) settings.focusScale = focusScaleValue; + if (focusOpacityValue) settings.focusOpacity = focusOpacityValue; + if (focusOutlineValue) settings.focusOutline = focusOutlineValue; + if (focusBoxShadowValue) settings.focusBoxShadow = focusBoxShadowValue; + if (activeScaleValue) settings.activeScale = activeScaleValue; + if (activeOpacityValue) settings.activeOpacity = activeOpacityValue; + if (activeBackgroundColorValue) + settings.activeBackgroundColor = activeBackgroundColorValue; + // Navigation type settings if (isNavigationType) { settings.iconUrl = state.iconUrl.trim(); @@ -475,10 +622,6 @@ export function useElementSettingsForm(options: UseElementSettingsFormOptions) { settings.transitionVideoUrl = state.transitionVideoUrl.trim(); settings.transitionReverseMode = state.transitionReverseMode; settings.reverseVideoUrl = state.reverseVideoUrl.trim(); - settings.transitionDurationSec = - Number(state.transitionDurationSec) > 0 - ? Number(state.transitionDurationSec) - : 0.7; } // Tooltip type settings @@ -555,6 +698,7 @@ export function useElementSettingsForm(options: UseElementSettingsFormOptions) { setFields, applySettings, getStyleValues, + getEffectValues, // Type checks isNavigationType, diff --git a/frontend/src/components/RuntimeElement.tsx b/frontend/src/components/RuntimeElement.tsx new file mode 100644 index 0000000..4462be4 --- /dev/null +++ b/frontend/src/components/RuntimeElement.tsx @@ -0,0 +1,103 @@ +/** + * RuntimeElement Component + * + * Renders a single UI element with interactive effects at runtime. + * Handles hover, focus, and active states for element effects. + */ + +import React from 'react'; +import { useElementEffects } from '../hooks/useElementEffects'; +import { buildElementStyle } from '../lib/elementStyles'; +import { + buildTransitionStyle, + buildAppearAnimationStyle, + hasAnyEffects, + type ElementEffectProperties, +} from '../lib/elementEffects'; + +interface RuntimeElementProps { + element: any; + onClick: () => void; + children: React.ReactNode; +} + +const RuntimeElement: React.FC = ({ + element, + onClick, + children, +}) => { + const xPercent = element.xPercent ?? 0; + const yPercent = element.yPercent ?? 0; + const rotation = element.rotation ?? 0; + + // Extract effect properties from element + const effectProperties: Partial = { + appearAnimation: element.appearAnimation, + appearAnimationDuration: element.appearAnimationDuration, + appearAnimationEasing: element.appearAnimationEasing, + hoverScale: element.hoverScale, + hoverOpacity: element.hoverOpacity, + hoverBackgroundColor: element.hoverBackgroundColor, + hoverColor: element.hoverColor, + hoverBoxShadow: element.hoverBoxShadow, + hoverTransitionDuration: element.hoverTransitionDuration, + focusScale: element.focusScale, + focusOpacity: element.focusOpacity, + focusOutline: element.focusOutline, + focusBoxShadow: element.focusBoxShadow, + activeScale: element.activeScale, + activeOpacity: element.activeOpacity, + activeBackgroundColor: element.activeBackgroundColor, + }; + + // Use effects hook for interactive states + const { effectStyle, eventHandlers } = useElementEffects(effectProperties); + + // Build base element style + const baseStyle: React.CSSProperties = { + left: `${xPercent}%`, + top: `${yPercent}%`, + transform: `translate(-50%, -50%)${rotation ? ` rotate(${rotation}deg)` : ''}`, + ...buildElementStyle(element), + }; + + // Merge transform if effect style has transform + let mergedStyle: React.CSSProperties = { ...baseStyle }; + + // Handle transform merging - effect transform overrides base (except for position) + if (effectStyle.transform) { + // Preserve the translate and rotation, add effect transform + mergedStyle.transform = `translate(-50%, -50%)${rotation ? ` rotate(${rotation}deg)` : ''} ${effectStyle.transform}`; + // Remove transform from effectStyle to avoid double application + const { transform, ...restEffectStyle } = effectStyle; + mergedStyle = { ...mergedStyle, ...restEffectStyle }; + } else { + mergedStyle = { ...mergedStyle, ...effectStyle }; + } + + // Add transition if element has any effects + if (hasAnyEffects(effectProperties)) { + const transitionStyle = buildTransitionStyle(effectProperties); + mergedStyle = { ...mergedStyle, ...transitionStyle }; + } + + // Add appear animation if configured + if (effectProperties.appearAnimation) { + const animationStyle = buildAppearAnimationStyle(effectProperties); + mergedStyle = { ...mergedStyle, ...animationStyle }; + } + + return ( +
+ {children} +
+ ); +}; + +export default RuntimeElement; diff --git a/frontend/src/components/RuntimePresentation.tsx b/frontend/src/components/RuntimePresentation.tsx index 06acfae..dd04a1a 100644 --- a/frontend/src/components/RuntimePresentation.tsx +++ b/frontend/src/components/RuntimePresentation.tsx @@ -20,6 +20,7 @@ import React, { import BaseButton from './BaseButton'; import CardBox from './CardBox'; import { OfflineToggle } from './Offline/OfflineToggle'; +import RuntimeElement from './RuntimeElement'; import LayoutGuest from '../layouts/Guest'; import { getPageTitle } from '../config'; import { usePreloadOrchestrator } from '../hooks/usePreloadOrchestrator'; @@ -28,7 +29,7 @@ import { usePageSwitch } from '../hooks/usePageSwitch'; import { useTransitionPlayback } from '../hooks/useTransitionPlayback'; import { resolveAssetPlaybackUrl } from '../lib/assetUrl'; import { logger } from '../lib/logger'; -import { buildElementStyle } from '../lib/elementStyles'; +// buildElementStyle is now used in RuntimeElement component import type { RuntimeProject, RuntimePage } from '../types/runtime'; interface RuntimePresentationProps { @@ -735,30 +736,15 @@ export default function RuntimePresentation({ {/* Page elements */}
- {pageElements.map((element: any) => { - const xPercent = element.xPercent ?? 0; - const yPercent = element.yPercent ?? 0; - const rotation = element.rotation ?? 0; - - // Build element style using shared utility - const elementStyle: React.CSSProperties = { - left: `${xPercent}%`, - top: `${yPercent}%`, - transform: `translate(-50%, -50%)${rotation ? ` rotate(${rotation}deg)` : ''}`, - ...buildElementStyle(element), - }; - - return ( -
handleElementClick(element)} - > - {renderElementContent(element)} -
- ); - })} + {pageElements.map((element: any) => ( + handleElementClick(element)} + > + {renderElementContent(element)} + + ))}
{/* Controls: Offline toggle and Fullscreen button */} diff --git a/frontend/src/css/main.css b/frontend/src/css/main.css index f061e28..49ab188 100644 --- a/frontend/src/css/main.css +++ b/frontend/src/css/main.css @@ -33,3 +33,68 @@ .introjs-prevbutton{ @apply bg-transparent border border-blue-600 text-blue-600 !important; } + +/* Element appear animation keyframes */ +@keyframes element-fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes element-slide-up { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes element-slide-down { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes element-slide-left { + from { + opacity: 0; + transform: translateX(20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes element-slide-right { + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes element-scale-in { + from { + opacity: 0; + transform: scale(0.8); + } + to { + opacity: 1; + transform: scale(1); + } +} diff --git a/frontend/src/hooks/useElementEffects.ts b/frontend/src/hooks/useElementEffects.ts new file mode 100644 index 0000000..9839349 --- /dev/null +++ b/frontend/src/hooks/useElementEffects.ts @@ -0,0 +1,128 @@ +/** + * useElementEffects Hook + * + * Manages element interactive effects (hover, focus, active) at runtime. + * Since CSS pseudo-classes don't work with inline styles, this hook + * handles state-based style application via JavaScript events. + */ + +import { useCallback, useState } from 'react'; +import type { CSSProperties } from 'react'; +import { + buildHoverStyle, + buildFocusStyle, + buildActiveStyle, + buildTransitionStyle, + hasHoverEffects, + hasFocusEffects, + hasActiveEffects, + type ElementEffectProperties, +} from '../lib/elementEffects'; + +interface ElementEffectState { + isHovered: boolean; + isFocused: boolean; + isActive: boolean; +} + +interface UseElementEffectsResult { + /** Current effect style to merge with base element style */ + effectStyle: CSSProperties; + /** Event handlers to attach to the element */ + eventHandlers: { + onMouseEnter: () => void; + onMouseLeave: () => void; + onFocus: () => void; + onBlur: () => void; + onMouseDown: () => void; + onMouseUp: () => void; + }; +} + +/** + * Hook for managing element interactive effects. + * + * @param effects - Element effect properties + * @returns Object with effectStyle and eventHandlers + * + * @example + * const { effectStyle, eventHandlers } = useElementEffects(element); + * return ( + *
+ * {content} + *
+ * ); + */ +export function useElementEffects( + effects: Partial, +): UseElementEffectsResult { + const [state, setState] = useState({ + isHovered: false, + isFocused: false, + isActive: false, + }); + + const onMouseEnter = useCallback(() => { + setState((prev) => ({ ...prev, isHovered: true })); + }, []); + + const onMouseLeave = useCallback(() => { + setState((prev) => ({ ...prev, isHovered: false, isActive: false })); + }, []); + + const onFocus = useCallback(() => { + setState((prev) => ({ ...prev, isFocused: true })); + }, []); + + const onBlur = useCallback(() => { + setState((prev) => ({ ...prev, isFocused: false })); + }, []); + + const onMouseDown = useCallback(() => { + setState((prev) => ({ ...prev, isActive: true })); + }, []); + + const onMouseUp = useCallback(() => { + setState((prev) => ({ ...prev, isActive: false })); + }, []); + + // Build effective style based on current state + // Priority: active > focus > hover > base + let effectStyle: CSSProperties = {}; + + // Add transition for smooth effect changes + if ( + hasHoverEffects(effects) || + hasFocusEffects(effects) || + hasActiveEffects(effects) + ) { + effectStyle = { ...effectStyle, ...buildTransitionStyle(effects) }; + } + + // Apply hover effects when hovered (but not active) + if (state.isHovered && !state.isActive && hasHoverEffects(effects)) { + effectStyle = { ...effectStyle, ...buildHoverStyle(effects) }; + } + + // Apply focus effects when focused + if (state.isFocused && hasFocusEffects(effects)) { + effectStyle = { ...effectStyle, ...buildFocusStyle(effects) }; + } + + // Apply active effects when pressed (highest priority) + if (state.isActive && hasActiveEffects(effects)) { + effectStyle = { ...effectStyle, ...buildActiveStyle(effects) }; + } + + return { + effectStyle, + eventHandlers: { + onMouseEnter, + onMouseLeave, + onFocus, + onBlur, + onMouseDown, + onMouseUp, + }, + }; +} diff --git a/frontend/src/lib/elementEffects.ts b/frontend/src/lib/elementEffects.ts new file mode 100644 index 0000000..fb3d92c --- /dev/null +++ b/frontend/src/lib/elementEffects.ts @@ -0,0 +1,267 @@ +/** + * Element Effects + * + * Types and utilities for UI element animations and interactions. + * Used by constructor, RuntimePresentation, and element settings pages. + */ + +import type { CSSProperties } from 'react'; + +/** + * Appear animation types + */ +export type AppearAnimationType = + | '' + | 'fade' + | 'slide-up' + | 'slide-down' + | 'slide-left' + | 'slide-right' + | 'scale'; + +/** + * Effect properties supported by UI elements. + * These properties can be set in element defaults and applied at runtime. + */ +export interface ElementEffectProperties { + // Appear animation + appearAnimation?: AppearAnimationType; + appearAnimationDuration?: string; + appearAnimationEasing?: string; + // Hover effects + hoverScale?: string; + hoverOpacity?: string; + hoverBackgroundColor?: string; + hoverColor?: string; + hoverBoxShadow?: string; + hoverTransitionDuration?: string; + // Focus effects + focusScale?: string; + focusOpacity?: string; + focusOutline?: string; + focusBoxShadow?: string; + // Active/press effects + activeScale?: string; + activeOpacity?: string; + activeBackgroundColor?: string; +} + +/** + * Array of effect property names for iteration. + */ +export const EFFECT_PROPS = [ + 'appearAnimation', + 'appearAnimationDuration', + 'appearAnimationEasing', + 'hoverScale', + 'hoverOpacity', + 'hoverBackgroundColor', + 'hoverColor', + 'hoverBoxShadow', + 'hoverTransitionDuration', + 'focusScale', + 'focusOpacity', + 'focusOutline', + 'focusBoxShadow', + 'activeScale', + 'activeOpacity', + 'activeBackgroundColor', +] as const; + +export type EffectPropName = (typeof EFFECT_PROPS)[number]; + +/** + * Build base transition style for smooth state changes. + */ +export function buildTransitionStyle( + effects: Partial, +): CSSProperties { + const duration = effects.hoverTransitionDuration || '0.2s'; + return { + transition: `all ${duration} ease`, + }; +} + +/** + * Build hover state style overrides. + */ +export function buildHoverStyle( + effects: Partial, +): CSSProperties { + const style: CSSProperties = {}; + + if (effects.hoverScale) { + style.transform = `scale(${effects.hoverScale})`; + } + if (effects.hoverOpacity) { + const parsed = Number(effects.hoverOpacity); + if (Number.isFinite(parsed)) { + style.opacity = parsed; + } + } + if (effects.hoverBackgroundColor) { + style.backgroundColor = effects.hoverBackgroundColor; + } + if (effects.hoverColor) { + style.color = effects.hoverColor; + } + if (effects.hoverBoxShadow) { + style.boxShadow = effects.hoverBoxShadow; + } + + return style; +} + +/** + * Build focus state style overrides. + */ +export function buildFocusStyle( + effects: Partial, +): CSSProperties { + const style: CSSProperties = {}; + + if (effects.focusScale) { + style.transform = `scale(${effects.focusScale})`; + } + if (effects.focusOpacity) { + const parsed = Number(effects.focusOpacity); + if (Number.isFinite(parsed)) { + style.opacity = parsed; + } + } + if (effects.focusOutline) { + style.outline = effects.focusOutline; + } + if (effects.focusBoxShadow) { + style.boxShadow = effects.focusBoxShadow; + } + + return style; +} + +/** + * Build active/press state style overrides. + */ +export function buildActiveStyle( + effects: Partial, +): CSSProperties { + const style: CSSProperties = {}; + + if (effects.activeScale) { + style.transform = `scale(${effects.activeScale})`; + } + if (effects.activeOpacity) { + const parsed = Number(effects.activeOpacity); + if (Number.isFinite(parsed)) { + style.opacity = parsed; + } + } + if (effects.activeBackgroundColor) { + style.backgroundColor = effects.activeBackgroundColor; + } + + return style; +} + +/** + * Get CSS keyframes for appear animation. + */ +export function getAppearAnimationKeyframes( + animationType: AppearAnimationType, +): string { + switch (animationType) { + case 'fade': + return 'element-fade-in'; + case 'slide-up': + return 'element-slide-up'; + case 'slide-down': + return 'element-slide-down'; + case 'slide-left': + return 'element-slide-left'; + case 'slide-right': + return 'element-slide-right'; + case 'scale': + return 'element-scale-in'; + default: + return ''; + } +} + +/** + * Build appear animation style. + */ +export function buildAppearAnimationStyle( + effects: Partial, +): CSSProperties { + if (!effects.appearAnimation) { + return {}; + } + + const animationName = getAppearAnimationKeyframes(effects.appearAnimation); + if (!animationName) { + return {}; + } + + const duration = effects.appearAnimationDuration || '0.3s'; + const easing = effects.appearAnimationEasing || 'ease'; + + return { + animation: `${animationName} ${duration} ${easing} forwards`, + }; +} + +/** + * Check if element has any hover effects configured. + */ +export function hasHoverEffects( + effects: Partial, +): boolean { + return Boolean( + effects.hoverScale || + effects.hoverOpacity || + effects.hoverBackgroundColor || + effects.hoverColor || + effects.hoverBoxShadow, + ); +} + +/** + * Check if element has any focus effects configured. + */ +export function hasFocusEffects( + effects: Partial, +): boolean { + return Boolean( + effects.focusScale || + effects.focusOpacity || + effects.focusOutline || + effects.focusBoxShadow, + ); +} + +/** + * Check if element has any active effects configured. + */ +export function hasActiveEffects( + effects: Partial, +): boolean { + return Boolean( + effects.activeScale || + effects.activeOpacity || + effects.activeBackgroundColor, + ); +} + +/** + * Check if element has any effects configured. + */ +export function hasAnyEffects( + effects: Partial, +): boolean { + return ( + Boolean(effects.appearAnimation) || + hasHoverEffects(effects) || + hasFocusEffects(effects) || + hasActiveEffects(effects) + ); +} diff --git a/frontend/src/lib/elementStyles.ts b/frontend/src/lib/elementStyles.ts index 9b8b1ed..a966603 100644 --- a/frontend/src/lib/elementStyles.ts +++ b/frontend/src/lib/elementStyles.ts @@ -34,6 +34,9 @@ export interface ElementStyleProperties { alignItems?: string; textAlign?: string; zIndex?: string; + backgroundColor?: string; + color?: string; + fontFamily?: string; } /** @@ -62,6 +65,9 @@ export const ELEMENT_STYLE_PROPS = [ 'alignItems', 'textAlign', 'zIndex', + 'backgroundColor', + 'color', + 'fontFamily', ] as const; /** diff --git a/frontend/src/pages/constructor.tsx b/frontend/src/pages/constructor.tsx index 6d97793..ac267ce 100644 --- a/frontend/src/pages/constructor.tsx +++ b/frontend/src/pages/constructor.tsx @@ -13,6 +13,7 @@ import { import { ElementSettingsTabsCompact, StyleSettingsSectionCompact, + EffectsSettingsSectionCompact, extractNumericValue, } from '../components/ElementSettings'; import axios from 'axios'; @@ -582,9 +583,9 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { const [isMenuOpen, setIsMenuOpen] = useState(false); const [editorPosition, setEditorPosition] = useState({ x: 0, y: 72 }); const [isEditorCollapsed, setIsEditorCollapsed] = useState(false); - const [elementEditorTab, setElementEditorTab] = useState<'general' | 'css'>( - 'general', - ); + const [elementEditorTab, setElementEditorTab] = useState< + 'general' | 'css' | 'effects' + >('general'); const constructorControlsDragRef = useRef<{ pointerOffsetX: number; @@ -2926,11 +2927,14 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { - setElementEditorTab(tab as 'general' | 'css') + setElementEditorTab( + tab as 'general' | 'css' | 'effects', + ) } tabs={[ { id: 'general', label: 'General' }, - { id: 'css', label: 'CSS Styles' }, + { id: 'css', label: 'CSS' }, + { id: 'effects', label: 'Effects' }, ]} /> @@ -3844,6 +3848,10 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { alignItems: selectedElement.alignItems || '', textAlign: selectedElement.textAlign || '', zIndex: selectedElement.zIndex || '', + backgroundColor: + selectedElement.backgroundColor || '', + color: selectedElement.color || '', + fontFamily: selectedElement.fontFamily || '', }} onChange={(prop, value) => { // Handle CSS property changes @@ -3881,7 +3889,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { }); } else if (prop === 'borderRadius') { updateSelectedElement({ - [prop]: trimmed ? `${trimmed}px` : '0px', + [prop]: trimmed ? `${trimmed}px` : undefined, }); } else { updateSelectedElement({ @@ -3898,6 +3906,41 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { }} /> )} + + {/* Effects Tab */} + {elementEditorTab === 'effects' && ( + { + updateSelectedElement({ + [prop]: value || undefined, + }); + }} + /> + )} )} diff --git a/frontend/src/pages/element-type-defaults/[id].tsx b/frontend/src/pages/element-type-defaults/[id].tsx index f6b42e1..143c9c2 100644 --- a/frontend/src/pages/element-type-defaults/[id].tsx +++ b/frontend/src/pages/element-type-defaults/[id].tsx @@ -27,6 +27,7 @@ import type { import { ElementSettingsTabs, StyleSettingsSection, + EffectsSettingsSection, CommonSettingsSection, NavigationSettingsSection, TooltipSettingsSection, @@ -46,6 +47,7 @@ type ElementTypeDefault = UiElementDefault & { const SETTINGS_TABS = [ { id: 'general', label: 'General Settings' }, { id: 'css', label: 'CSS Styles' }, + { id: 'effects', label: 'Effects' }, ]; const ElementTypeDefaultDetailsPage = () => { @@ -57,7 +59,6 @@ const ElementTypeDefaultDetailsPage = () => { }, [router.query.id]); const [item, setItem] = useState(null); - const [name, setName] = useState(''); const [sortOrder, setSortOrder] = useState(0); const [isActive, setIsActive] = useState(true); const [activeTab, setActiveTab] = useState('general'); @@ -93,7 +94,6 @@ const ElementTypeDefaultDetailsPage = () => { } setItem(nextItem); - setName(String(nextItem.name || '')); setSortOrder(Number(nextItem.sort_order || 0)); setIsActive(Boolean(nextItem.is_active)); applySettings(nextItem.default_settings_json); @@ -139,7 +139,7 @@ const ElementTypeDefaultDetailsPage = () => { id, data: { element_type: item.element_type, - name: String(name || '').trim() || item.element_type, + name: item.name || item.element_type, sort_order: Number.isFinite(Number(sortOrder)) ? Number(sortOrder) : 0, @@ -168,7 +168,7 @@ const ElementTypeDefaultDetailsPage = () => { } finally { setIsSaving(false); } - }, [buildSettingsJson, id, isActive, item, loadItem, name, sortOrder]); + }, [buildSettingsJson, id, isActive, item, loadItem, sortOrder]); // Extract stable callback reference for setField const setField = form.setField; @@ -181,6 +181,14 @@ const ElementTypeDefaultDetailsPage = () => { [setField], ); + // Handler for effects section changes + const handleEffectChange = useCallback( + (prop: string, value: string) => { + setField(prop as keyof typeof form.state, value); + }, + [setField], + ); + // Handler for common section changes const handleCommonChange = useCallback( (field: string, value: string) => { @@ -243,13 +251,6 @@ const ElementTypeDefaultDetailsPage = () => { - - setName(event.target.value)} - /> - - { appearDelaySec={form.state.appearDelaySec} appearDurationSec={form.state.appearDurationSec} onChange={handleCommonChange} + showLabel={false} /> {/* Type-specific sections */} @@ -302,7 +304,6 @@ const ElementTypeDefaultDetailsPage = () => { transitionVideoUrl={form.state.transitionVideoUrl} transitionReverseMode={form.state.transitionReverseMode} reverseVideoUrl={form.state.reverseVideoUrl} - transitionDurationSec={form.state.transitionDurationSec} onChange={handleTypeChange} context='global' /> @@ -394,6 +395,16 @@ const ElementTypeDefaultDetailsPage = () => { )} + {/* Effects Tab */} + {activeTab === 'effects' && ( + + + + )} + {errorMessage && (

{errorMessage}

)} diff --git a/frontend/src/pages/project-element-defaults/[id].tsx b/frontend/src/pages/project-element-defaults/[id].tsx index 6699643..a5ce59a 100644 --- a/frontend/src/pages/project-element-defaults/[id].tsx +++ b/frontend/src/pages/project-element-defaults/[id].tsx @@ -18,12 +18,17 @@ import SectionMain from '../../components/SectionMain'; import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; import { getPageTitle } from '../../config'; import { logger } from '../../lib/logger'; -import type { CanvasElementType } from '../../types/constructor'; +import type { + CanvasElementType, + ConstructorAsset, + AssetOption, +} from '../../types/constructor'; // Import shared element settings components import { ElementSettingsTabs, StyleSettingsSection, + EffectsSettingsSection, CommonSettingsSection, NavigationSettingsSection, TooltipSettingsSection, @@ -61,6 +66,7 @@ const toHumanLabel = (value: string) => const SETTINGS_TABS = [ { id: 'general', label: 'General Settings' }, { id: 'css', label: 'CSS Styles' }, + { id: 'effects', label: 'Effects' }, ]; const ProjectElementDefaultDetailsPage = () => { @@ -81,6 +87,7 @@ const ProjectElementDefaultDetailsPage = () => { const [name, setName] = useState(''); const [sortOrder, setSortOrder] = useState(0); const [activeTab, setActiveTab] = useState('general'); + const [assets, setAssets] = useState([]); const [diff, setDiff] = useState(null); const [isLoading, setIsLoading] = useState(false); @@ -94,6 +101,33 @@ const ProjectElementDefaultDetailsPage = () => { // Use shared form hook const form = useElementSettingsForm({ elementType: currentElementType }); + // Helper functions for asset options + const getAssetSourceValue = (asset: ConstructorAsset) => + String(asset.storage_key || asset.cdn_url || '').trim(); + + const getAssetLabel = (asset: ConstructorAsset) => { + const baseName = asset.name?.trim() || 'Untitled asset'; + const source = getAssetSourceValue(asset); + return `${baseName}${source ? ` ยท ${source}` : ''}`; + }; + + // Build icon asset options from project assets + const iconAssetOptions: AssetOption[] = useMemo( + () => + assets + .filter( + (asset) => + asset.type === 'icon' && + asset.asset_type === 'image' && + getAssetSourceValue(asset), + ) + .map((asset) => ({ + value: getAssetSourceValue(asset), + label: getAssetLabel(asset), + })), + [assets], + ); + // Extract stable callback reference to avoid infinite loop const applySettings = form.applySettings; @@ -118,6 +152,27 @@ const ProjectElementDefaultDetailsPage = () => { setName(String(nextItem.name || '')); setSortOrder(Number(nextItem.sort_order || 0)); applySettings(nextItem.settings_json); + + // Load project assets for icon selector + if (nextItem.projectId) { + try { + const assetsResponse = await axios.get( + `/assets?limit=500&page=0&sort=desc&field=createdAt&project=${nextItem.projectId}`, + ); + const assetRows: ConstructorAsset[] = Array.isArray( + assetsResponse?.data?.rows, + ) + ? assetsResponse.data.rows + : []; + setAssets(assetRows); + } catch (assetError) { + logger.error( + 'Failed to load project assets:', + assetError instanceof Error ? assetError : { error: assetError }, + ); + setAssets([]); + } + } } catch (error: unknown) { const err = error as { response?: { data?: { message?: string } }; @@ -175,6 +230,7 @@ const ProjectElementDefaultDetailsPage = () => { await axios.put(`/project-element-defaults/${id}`, { id, data: { + element_type: item.element_type, name: name.trim() || item.element_type, sort_order: sortOrder, settings_json: settings, @@ -252,6 +308,14 @@ const ProjectElementDefaultDetailsPage = () => { [setField], ); + // Handler for effects section changes + const handleEffectChange = useCallback( + (prop: string, value: string) => { + setField(prop as keyof typeof form.state, value); + }, + [setField], + ); + // Handler for common section changes const handleCommonChange = useCallback( (field: string, value: string) => { @@ -426,9 +490,9 @@ const ProjectElementDefaultDetailsPage = () => { transitionVideoUrl={form.state.transitionVideoUrl} transitionReverseMode={form.state.transitionReverseMode} reverseVideoUrl={form.state.reverseVideoUrl} - transitionDurationSec={form.state.transitionDurationSec} onChange={handleTypeChange} context='project' + iconAssetOptions={iconAssetOptions} /> )} @@ -515,6 +579,16 @@ const ProjectElementDefaultDetailsPage = () => { /> )} + + {/* Effects Tab */} + {activeTab === 'effects' && ( + + + + )} diff --git a/frontend/src/pages/projects/projects-edit.tsx b/frontend/src/pages/projects/projects-edit.tsx index 00c5a1b..32c8eef 100644 --- a/frontend/src/pages/projects/projects-edit.tsx +++ b/frontend/src/pages/projects/projects-edit.tsx @@ -35,13 +35,93 @@ const initVals = { logo_url: '', favicon_url: '', og_image_url: '', - theme_config_json: '', - custom_css_json: '', + // Theme config fields (stored as JSON in theme_config_json) + themePrimaryColor: '', + themeBackgroundColor: '', + themeTextColor: '', + // Custom CSS fields (stored as JSON in custom_css_json) + customFontFamily: '', cdn_base_url: '', is_deleted: false, deleted_at_time: new Date(), }; +/** + * Parse theme_config_json into individual fields + */ +const parseThemeConfig = ( + json: string | Record | null | undefined, +): { primaryColor: string; backgroundColor: string; textColor: string } => { + const defaults = { primaryColor: '', backgroundColor: '', textColor: '' }; + if (!json) return defaults; + + try { + const parsed = typeof json === 'string' ? JSON.parse(json) : json; + return { + primaryColor: String(parsed?.primaryColor || ''), + backgroundColor: String(parsed?.backgroundColor || ''), + textColor: String(parsed?.textColor || ''), + }; + } catch { + return defaults; + } +}; + +/** + * Parse custom_css_json into individual fields + */ +const parseCustomCss = ( + json: string | Record | null | undefined, +): { fontFamily: string } => { + const defaults = { fontFamily: '' }; + if (!json) return defaults; + + try { + const parsed = typeof json === 'string' ? JSON.parse(json) : json; + return { + fontFamily: String(parsed?.fontFamily || ''), + }; + } catch { + return defaults; + } +}; + +/** + * Build theme_config_json from individual fields + */ +const buildThemeConfigJson = (values: { + themePrimaryColor: string; + themeBackgroundColor: string; + themeTextColor: string; +}): string | null => { + const config: Record = {}; + + if (values.themePrimaryColor.trim()) { + config.primaryColor = values.themePrimaryColor.trim(); + } + if (values.themeBackgroundColor.trim()) { + config.backgroundColor = values.themeBackgroundColor.trim(); + } + if (values.themeTextColor.trim()) { + config.textColor = values.themeTextColor.trim(); + } + + return Object.keys(config).length > 0 ? JSON.stringify(config) : null; +}; + +/** + * Build custom_css_json from individual fields + */ +const buildCustomCssJson = (values: { customFontFamily: string }): string | null => { + const config: Record = {}; + + if (values.customFontFamily.trim()) { + config.fontFamily = values.customFontFamily.trim(); + } + + return Object.keys(config).length > 0 ? JSON.stringify(config) : null; +}; + const EditProjectsPage = () => { const router = useRouter(); const dispatch = useAppDispatch(); @@ -117,36 +197,64 @@ const EditProjectsPage = () => { // Sync form values with fetched data (consolidated from redundant useEffects) useEffect(() => { if (typeof project === 'object' && project !== null) { - const newInitialVal = { ...initVals }; - Object.keys(initVals).forEach((el) => { - const projectValue = (project as unknown as Record)[ - el - ]; - const defaultValue = initVals[el as keyof typeof initVals]; + const projectData = project as unknown as Record; - // Handle null/undefined values based on the default type - if (projectValue === null || projectValue === undefined) { - newInitialVal[el as keyof typeof initVals] = defaultValue as never; - } else if (typeof defaultValue === 'boolean') { - newInitialVal[el as keyof typeof initVals] = Boolean( - projectValue, - ) as never; - } else if (typeof defaultValue === 'string') { - newInitialVal[el as keyof typeof initVals] = String( - projectValue, - ) as never; - } else { - newInitialVal[el as keyof typeof initVals] = projectValue as never; - } + // Parse theme_config_json into individual fields + const themeConfig = parseThemeConfig( + projectData.theme_config_json as string | Record, + ); + + // Parse custom_css_json into individual fields + const customCss = parseCustomCss( + projectData.custom_css_json as string | Record, + ); + + setInitialValues({ + name: String(projectData.name || ''), + slug: String(projectData.slug || ''), + description: String(projectData.description || ''), + logo_url: String(projectData.logo_url || ''), + favicon_url: String(projectData.favicon_url || ''), + og_image_url: String(projectData.og_image_url || ''), + themePrimaryColor: themeConfig.primaryColor, + themeBackgroundColor: themeConfig.backgroundColor, + themeTextColor: themeConfig.textColor, + customFontFamily: customCss.fontFamily, + cdn_base_url: String(projectData.cdn_base_url || ''), + is_deleted: Boolean(projectData.is_deleted), + deleted_at_time: projectData.deleted_at_time + ? new Date(projectData.deleted_at_time as string) + : new Date(), }); - setInitialValues(newInitialVal); } }, [project]); const handleSubmit = async (data: typeof initVals) => { - await dispatch( - update({ id: id as string, data: data as unknown as Partial }), - ); + // Build JSON fields from individual values + const theme_config_json = buildThemeConfigJson({ + themePrimaryColor: data.themePrimaryColor, + themeBackgroundColor: data.themeBackgroundColor, + themeTextColor: data.themeTextColor, + }); + + const custom_css_json = buildCustomCssJson({ + customFontFamily: data.customFontFamily, + }); + + // Prepare data for API (exclude expanded fields, include JSON) + const apiData: Partial = { + name: data.name, + slug: data.slug, + description: data.description, + logo_url: data.logo_url, + favicon_url: data.favicon_url, + og_image_url: data.og_image_url, + theme_config_json: theme_config_json as string | undefined, + custom_css_json: custom_css_json as string | undefined, + cdn_base_url: data.cdn_base_url, + }; + + await dispatch(update({ id: id as string, data: apiData })); await router.push('/projects/projects-list'); }; @@ -301,19 +409,31 @@ const EditProjectsPage = () => { )} - + - + + + + + + + + + diff --git a/frontend/src/pages/projects/projects-list.tsx b/frontend/src/pages/projects/projects-list.tsx index 22de9c1..1c4334b 100644 --- a/frontend/src/pages/projects/projects-list.tsx +++ b/frontend/src/pages/projects/projects-list.tsx @@ -22,9 +22,13 @@ const ProjectsListPage = () => { const router = useRouter(); const dispatch = useAppDispatch(); - const projects = useAppSelector( - (state) => state.projects.projects, - ) as Project[]; + const projectsRaw = useAppSelector((state) => state.projects.projects); + // Handle both array (from list fetch) and single object (after edit fetch) + const projects: Project[] = Array.isArray(projectsRaw) + ? projectsRaw + : projectsRaw + ? [projectsRaw as Project] + : []; const isLoading = useAppSelector((state) => state.projects.loading); const [isCreating, setIsCreating] = useState(false); diff --git a/frontend/src/types/constructor.ts b/frontend/src/types/constructor.ts index c0e84fe..eba4c61 100644 --- a/frontend/src/types/constructor.ts +++ b/frontend/src/types/constructor.ts @@ -6,6 +6,7 @@ */ import type { ElementStyleProperties } from '../lib/elementStyles'; +import type { ElementEffectProperties } from '../lib/elementEffects'; /** * Element types available in the constructor canvas. @@ -51,9 +52,11 @@ export interface CarouselSlide { /** * Base canvas element with common positioning and styling fields. - * Extends ElementStyleProperties for CSS styling. + * Extends ElementStyleProperties for CSS styling and ElementEffectProperties for effects. */ -export interface BaseCanvasElement extends ElementStyleProperties { +export interface BaseCanvasElement + extends ElementStyleProperties, + ElementEffectProperties { id: string; type: CanvasElementType | string; label?: string;