Add short gallery URL toggle and token support (#38)
This commit is contained in:
@@ -3,6 +3,7 @@ const path = require('path');
|
||||
const knex = require('knex');
|
||||
const knexConfig = require('../../knexfile');
|
||||
const logger = require('../utils/logger');
|
||||
const { extractShareToken } = require('../utils/shareLinkUtils');
|
||||
|
||||
// Ensure SQLite directory exists when using file-based DB (native installs)
|
||||
try {
|
||||
@@ -66,11 +67,13 @@ async function initializeDatabase() {
|
||||
table.string('customer_name');
|
||||
table.string('customer_email');
|
||||
table.string('host_email').notNullable();
|
||||
table.string('host_name');
|
||||
table.string('admin_email').notNullable();
|
||||
table.string('password_hash').notNullable();
|
||||
table.text('welcome_message');
|
||||
table.text('color_theme');
|
||||
table.string('share_link').unique().notNullable();
|
||||
table.string('share_token').unique();
|
||||
table.datetime('created_at').defaultTo(db.fn.now());
|
||||
table.datetime('expires_at').notNullable();
|
||||
table.boolean('is_active').defaultTo(true);
|
||||
@@ -103,12 +106,14 @@ async function initializeDatabase() {
|
||||
event_date DATE NOT NULL,
|
||||
customer_name TEXT,
|
||||
customer_email TEXT,
|
||||
host_name TEXT,
|
||||
host_email TEXT NOT NULL,
|
||||
admin_email TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
welcome_message TEXT,
|
||||
color_theme TEXT,
|
||||
share_link TEXT UNIQUE NOT NULL,
|
||||
share_token TEXT UNIQUE,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at DATETIME NOT NULL,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
@@ -161,6 +166,37 @@ async function initializeDatabase() {
|
||||
}
|
||||
}
|
||||
|
||||
const hasShareTokenColumn = await db.schema.hasColumn('events', 'share_token');
|
||||
if (!hasShareTokenColumn) {
|
||||
await db.schema.table('events', (table) => {
|
||||
table.string('share_token').unique();
|
||||
});
|
||||
}
|
||||
|
||||
const hasHostNameColumn = await db.schema.hasColumn('events', 'host_name');
|
||||
if (!hasHostNameColumn) {
|
||||
await db.schema.table('events', (table) => {
|
||||
table.string('host_name');
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const eventsWithoutToken = await db('events')
|
||||
.whereNull('share_token')
|
||||
.select('id', 'share_link');
|
||||
|
||||
for (const event of eventsWithoutToken) {
|
||||
const token = extractShareToken(event.share_link);
|
||||
if (token) {
|
||||
await db('events')
|
||||
.where({ id: event.id })
|
||||
.update({ share_token: token });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('Share token backfill skipped', { error: error.message });
|
||||
}
|
||||
|
||||
// Photo metadata table
|
||||
const hasPhotosTable = await db.schema.hasTable('photos');
|
||||
if (!hasPhotosTable) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Only the relevant parts are shown - merge with existing adminEvents.js
|
||||
|
||||
const { validatePasswordInContext, getBcryptRounds } = require('../utils/passwordValidation');
|
||||
const { buildShareLinkVariants } = require('../services/shareLinkService');
|
||||
|
||||
// Enhanced event creation with password validation
|
||||
router.post('/', adminAuth, [
|
||||
@@ -65,9 +66,9 @@ router.post('/', adminAuth, [
|
||||
counter++;
|
||||
}
|
||||
|
||||
// Generate share link
|
||||
// Generate share link based on configured style
|
||||
const shareToken = crypto.randomBytes(16).toString('hex');
|
||||
const shareLink = `${process.env.FRONTEND_URL}/gallery/${slug}/${shareToken}`;
|
||||
const { shareUrl, shareLinkToStore } = await buildShareLinkVariants({ slug, shareToken });
|
||||
|
||||
// Hash password with configurable rounds
|
||||
const password_hash = await bcrypt.hash(password, getBcryptRounds());
|
||||
@@ -96,7 +97,8 @@ router.post('/', adminAuth, [
|
||||
password_hash,
|
||||
welcome_message,
|
||||
color_theme,
|
||||
share_link: shareLink,
|
||||
share_link: shareLinkToStore,
|
||||
share_token: shareToken,
|
||||
expires_at: expires_at.toISOString(),
|
||||
created_at: new Date().toISOString(),
|
||||
allow_user_uploads,
|
||||
|
||||
@@ -14,6 +14,7 @@ const { escapeLikePattern } = require('../utils/sqlSecurity');
|
||||
// formatDate import removed - dates are formatted by email processor
|
||||
const { validatePasswordInContext, getBcryptRounds } = require('../utils/passwordValidation');
|
||||
const logger = require('../utils/logger');
|
||||
const { buildShareLinkVariants } = require('../services/shareLinkService');
|
||||
|
||||
const parseBooleanInput = (value, defaultValue = true) => {
|
||||
if (value === undefined || value === null) {
|
||||
@@ -225,11 +226,9 @@ router.post('/', adminAuth, [
|
||||
counter++;
|
||||
}
|
||||
|
||||
// Generate share link
|
||||
// Generate share link respecting configured format
|
||||
const shareToken = crypto.randomBytes(16).toString('hex');
|
||||
const sharePath = `/gallery/${slug}/${shareToken}`;
|
||||
const frontendBase = (process.env.FRONTEND_URL || '').replace(/\/$/, '');
|
||||
const shareLink = frontendBase ? `${frontendBase}${sharePath}` : sharePath;
|
||||
const { sharePath, shareUrl, shareLinkToStore } = await buildShareLinkVariants({ slug, shareToken });
|
||||
|
||||
// Hash password with configurable rounds (random placeholder when not required)
|
||||
const password_hash = requirePassword
|
||||
@@ -266,7 +265,8 @@ router.post('/', adminAuth, [
|
||||
password_hash,
|
||||
welcome_message,
|
||||
color_theme,
|
||||
share_link: shareLink,
|
||||
share_link: shareLinkToStore,
|
||||
share_token: shareToken,
|
||||
expires_at: expires_at.toISOString(),
|
||||
created_at: new Date().toISOString(),
|
||||
allow_user_uploads,
|
||||
@@ -318,7 +318,7 @@ router.post('/', adminAuth, [
|
||||
host_name: customerName || (customerEmail ? customerEmail.split('@')[0] : null),
|
||||
event_name,
|
||||
event_date: event_date, // Pass raw date - will be formatted by email processor
|
||||
gallery_link: shareLink,
|
||||
gallery_link: shareUrl,
|
||||
gallery_password: requirePassword ? password : 'No password required',
|
||||
expiry_date: expires_at.toISOString(), // Pass ISO string - will be formatted by email processor
|
||||
welcome_message: welcome_message || ''
|
||||
@@ -336,7 +336,7 @@ router.post('/', adminAuth, [
|
||||
customer_name: customerName,
|
||||
customer_email: customerEmail,
|
||||
require_password: requirePassword,
|
||||
share_link: shareLink,
|
||||
share_link: shareUrl,
|
||||
expires_at: expires_at.toISOString(),
|
||||
created_at: new Date().toISOString()
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ const {
|
||||
getRawPublicSiteSettings,
|
||||
} = require('../services/publicSiteService');
|
||||
const { sanitizeCss } = require('../utils/cssSanitizer');
|
||||
const { clearShareLinkSettingsCache } = require('../services/shareLinkService');
|
||||
const router = express.Router();
|
||||
const { clearMaxFilesPerUploadCache, MAX_ALLOWED_FILES_PER_UPLOAD } = require('../services/uploadSettings');
|
||||
|
||||
@@ -548,6 +549,9 @@ router.put('/general', adminAuth, async (req, res) => {
|
||||
if (uploadLimitTouched) {
|
||||
clearMaxFilesPerUploadCache();
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(settings, 'general_short_gallery_urls')) {
|
||||
clearShareLinkSettingsCache();
|
||||
}
|
||||
|
||||
// Log activity
|
||||
await db('activity_logs').insert({
|
||||
|
||||
@@ -22,6 +22,7 @@ const {
|
||||
getAdminTokenFromRequest,
|
||||
getGalleryTokenFromRequest,
|
||||
} = require('../utils/tokenUtils');
|
||||
const { getEventShareToken, resolveShareIdentifier } = require('../services/shareLinkService');
|
||||
const router = express.Router();
|
||||
|
||||
// Admin login with enhanced security
|
||||
@@ -284,18 +285,22 @@ router.post('/gallery/share-login', [
|
||||
const ipAddress = req.ip || req.connection.remoteAddress;
|
||||
const userAgent = req.headers['user-agent'] || '';
|
||||
|
||||
const event = await db('events')
|
||||
let event = await db('events')
|
||||
.where({ slug, is_active: formatBoolean(true), is_archived: formatBoolean(false) })
|
||||
.first();
|
||||
|
||||
if (!event) {
|
||||
const resolved = await resolveShareIdentifier(slug);
|
||||
if (resolved?.event) {
|
||||
event = resolved.event;
|
||||
}
|
||||
}
|
||||
|
||||
if (!event) {
|
||||
return res.status(404).json({ error: 'Gallery not found' });
|
||||
}
|
||||
|
||||
let expectedToken = event.share_link;
|
||||
if (expectedToken && expectedToken.includes('/')) {
|
||||
expectedToken = expectedToken.split('/').pop();
|
||||
}
|
||||
const expectedToken = getEventShareToken(event);
|
||||
|
||||
if (!expectedToken || token !== expectedToken) {
|
||||
return res.status(401).json({ error: 'Invalid or expired share link' });
|
||||
@@ -312,7 +317,7 @@ router.post('/gallery/share-login', [
|
||||
issuer: 'picpeak-auth'
|
||||
});
|
||||
|
||||
await trackSuccessfulLogin(`gallery:${slug}:share`, ipAddress, userAgent);
|
||||
await trackSuccessfulLogin(`gallery:${event.slug}:share`, ipAddress, userAgent);
|
||||
setGalleryAuthCookies(res, jwtToken, event.slug);
|
||||
|
||||
const requiresPassword = !(event.require_password === false || event.require_password === 0 || event.require_password === '0');
|
||||
|
||||
@@ -9,6 +9,7 @@ const { adminAuth } = require('../middleware/auth-enhanced-v2');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const router = express.Router();
|
||||
const { buildShareLinkVariants } = require('../services/shareLinkService');
|
||||
|
||||
const parseBooleanInput = (value, defaultValue = true) => {
|
||||
if (value === undefined || value === null) {
|
||||
@@ -160,12 +161,9 @@ router.post('/', adminAuth, [
|
||||
counter++;
|
||||
}
|
||||
|
||||
// Generate share link (just slug/token, not full URL)
|
||||
// Generate share link variants (auto-detects short URL preference)
|
||||
const shareToken = crypto.randomBytes(16).toString('hex');
|
||||
const sharePath = `/gallery/${slug}/${shareToken}`;
|
||||
const frontendBase = (process.env.FRONTEND_URL || '').replace(/\/$/, '');
|
||||
const fullShareLink = frontendBase ? `${frontendBase}${sharePath}` : sharePath;
|
||||
const shareLinkSlug = `${slug}/${shareToken}`;
|
||||
const { sharePath, shareUrl, shareLinkToStore } = await buildShareLinkVariants({ slug, shareToken });
|
||||
|
||||
// Hash password (or placeholder when not required)
|
||||
const password_hash = requirePassword
|
||||
@@ -195,7 +193,8 @@ router.post('/', adminAuth, [
|
||||
password_hash,
|
||||
welcome_message,
|
||||
color_theme,
|
||||
share_link: shareLinkSlug,
|
||||
share_link: shareLinkToStore,
|
||||
share_token: shareToken,
|
||||
expires_at,
|
||||
require_password: formatBoolean(requirePassword)
|
||||
}).returning('id');
|
||||
@@ -211,7 +210,7 @@ router.post('/', adminAuth, [
|
||||
host_name: customerName,
|
||||
event_name,
|
||||
event_date: event_date, // Pass raw date - will be formatted by email processor
|
||||
gallery_link: fullShareLink,
|
||||
gallery_link: shareUrl,
|
||||
gallery_password: requirePassword ? password : 'No password required',
|
||||
expiry_date: expires_at.toISOString(), // Pass ISO string - will be formatted by email processor
|
||||
welcome_message: welcome_message || ''
|
||||
@@ -220,7 +219,7 @@ router.post('/', adminAuth, [
|
||||
res.json({
|
||||
id: eventId,
|
||||
slug,
|
||||
share_link: fullShareLink,
|
||||
share_link: shareUrl,
|
||||
expires_at,
|
||||
require_password: requirePassword,
|
||||
customer_name: customerName,
|
||||
|
||||
@@ -9,10 +9,41 @@ const { verifyGalleryAccess } = require('../middleware/gallery');
|
||||
const secureImageService = require('../services/secureImageService');
|
||||
const logger = require('../utils/logger');
|
||||
const { resolvePhotoFilePath } = require('../services/photoResolver');
|
||||
const { getEventShareToken, resolveShareIdentifier, buildShareLinkVariants } = require('../services/shareLinkService');
|
||||
|
||||
// Get storage path from environment or default
|
||||
const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../storage');
|
||||
|
||||
// Resolve gallery identifier (slug or token) to canonical data
|
||||
router.get('/resolve/:identifier', async (req, res) => {
|
||||
try {
|
||||
const { identifier } = req.params;
|
||||
const result = await resolveShareIdentifier(identifier);
|
||||
|
||||
if (!result) {
|
||||
return res.status(404).json({ error: 'Gallery not found' });
|
||||
}
|
||||
|
||||
const { event, matchType, shareToken } = result;
|
||||
const linkVariants = await buildShareLinkVariants({ slug: event.slug, shareToken });
|
||||
const requiresPassword = !(event.require_password === false || event.require_password === 0 || event.require_password === '0');
|
||||
|
||||
res.json({
|
||||
slug: event.slug,
|
||||
token: shareToken,
|
||||
matchType,
|
||||
share_link: event.share_link,
|
||||
share_path: linkVariants.sharePath,
|
||||
share_url: linkVariants.shareUrl,
|
||||
short_enabled: linkVariants.shortEnabled,
|
||||
requires_password: requiresPassword
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error resolving gallery identifier:', error);
|
||||
res.status(500).json({ error: 'Failed to resolve gallery link' });
|
||||
}
|
||||
});
|
||||
|
||||
// Verify share token
|
||||
router.get('/:slug/verify-token/:token', async (req, res) => {
|
||||
try {
|
||||
@@ -20,15 +51,14 @@ router.get('/:slug/verify-token/:token', async (req, res) => {
|
||||
|
||||
const event = await db('events')
|
||||
.where({ slug, is_active: formatBoolean(true), is_archived: formatBoolean(false) })
|
||||
.select('id', 'share_link')
|
||||
.select('id', 'share_link', 'share_token')
|
||||
.first();
|
||||
|
||||
if (!event) {
|
||||
return res.status(404).json({ error: 'Gallery not found' });
|
||||
}
|
||||
|
||||
// Extract token from share link and verify
|
||||
const expectedToken = event.share_link.split('/').pop();
|
||||
const expectedToken = getEventShareToken(event);
|
||||
if (token !== expectedToken) {
|
||||
return res.status(404).json({ error: 'Invalid gallery link' });
|
||||
}
|
||||
@@ -56,6 +86,7 @@ router.get('/:slug/info', async (req, res) => {
|
||||
'is_active',
|
||||
'is_archived',
|
||||
'share_link',
|
||||
'share_token',
|
||||
'allow_downloads',
|
||||
'disable_right_click',
|
||||
'watermark_downloads',
|
||||
@@ -76,12 +107,8 @@ router.get('/:slug/info', async (req, res) => {
|
||||
|
||||
// If token provided, verify it matches the share link
|
||||
if (token) {
|
||||
let expectedToken = event.share_link;
|
||||
// Handle both formats: full URL or just token
|
||||
if (event.share_link && event.share_link.includes('/')) {
|
||||
expectedToken = event.share_link.split('/').pop();
|
||||
}
|
||||
if (token !== expectedToken) {
|
||||
const expectedToken = getEventShareToken(event);
|
||||
if (!expectedToken || token !== expectedToken) {
|
||||
return res.status(404).json({ error: 'Invalid gallery link' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
const { db } = require('../database/db');
|
||||
const { formatBoolean } = require('../utils/dbCompat');
|
||||
const { extractShareToken, isPotentialShareToken, buildSharePath } = require('../utils/shareLinkUtils');
|
||||
|
||||
const SETTING_KEY = 'general_short_gallery_urls';
|
||||
const CACHE_TTL_MS = 60_000;
|
||||
|
||||
let cachedSetting = null;
|
||||
let cacheExpiresAt = 0;
|
||||
|
||||
const parseSettingValue = (rawValue) => {
|
||||
if (rawValue === undefined || rawValue === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof rawValue === 'boolean') {
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
if (typeof rawValue === 'number') {
|
||||
return rawValue !== 0;
|
||||
}
|
||||
|
||||
if (typeof rawValue === 'string') {
|
||||
const trimmed = rawValue.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
return parseSettingValue(parsed);
|
||||
} catch {
|
||||
const normalized = trimmed.toLowerCase();
|
||||
if (normalized === 'true' || normalized === '1' || normalized === 'yes') {
|
||||
return true;
|
||||
}
|
||||
if (normalized === 'false' || normalized === '0' || normalized === 'no') {
|
||||
return false;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof rawValue === 'object') {
|
||||
try {
|
||||
return parseSettingValue(JSON.parse(JSON.stringify(rawValue)));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getRawSettingValue = async () => {
|
||||
try {
|
||||
const setting = await db('app_settings').where({ setting_key: SETTING_KEY }).first();
|
||||
return setting?.setting_value ?? null;
|
||||
} catch (error) {
|
||||
console.error('Failed to read gallery URL setting:', error.message);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const isShortGalleryUrlsEnabled = async () => {
|
||||
if (cachedSetting !== null && Date.now() < cacheExpiresAt) {
|
||||
return cachedSetting;
|
||||
}
|
||||
|
||||
const rawValue = await getRawSettingValue();
|
||||
const parsed = parseSettingValue(rawValue);
|
||||
cachedSetting = parsed === null ? false : Boolean(parsed);
|
||||
cacheExpiresAt = Date.now() + CACHE_TTL_MS;
|
||||
return cachedSetting;
|
||||
};
|
||||
|
||||
const clearShareLinkSettingsCache = () => {
|
||||
cachedSetting = null;
|
||||
cacheExpiresAt = 0;
|
||||
};
|
||||
|
||||
const buildShareLinkVariants = async ({ slug, shareToken }) => {
|
||||
if (!shareToken) {
|
||||
throw new Error('shareToken is required to build share link variants');
|
||||
}
|
||||
|
||||
const shortEnabled = await isShortGalleryUrlsEnabled();
|
||||
const sharePath = buildSharePath(slug, shareToken, shortEnabled);
|
||||
const frontendBase = (process.env.FRONTEND_URL || '').replace(/\/$/, '');
|
||||
const shareUrl = frontendBase ? `${frontendBase}${sharePath}` : sharePath;
|
||||
|
||||
return {
|
||||
shortEnabled,
|
||||
sharePath,
|
||||
shareUrl,
|
||||
shareLinkToStore: sharePath
|
||||
};
|
||||
};
|
||||
|
||||
const getEventShareToken = (event) => {
|
||||
if (!event) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (event.share_token) {
|
||||
return event.share_token;
|
||||
}
|
||||
|
||||
return extractShareToken(event.share_link);
|
||||
};
|
||||
|
||||
const ACTIVE_EVENT_FILTER = {
|
||||
is_active: formatBoolean(true),
|
||||
is_archived: formatBoolean(false)
|
||||
};
|
||||
|
||||
const resolveShareIdentifier = async (identifier) => {
|
||||
if (!identifier) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = String(identifier).trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseQuery = db('events')
|
||||
.select(
|
||||
'id',
|
||||
'slug',
|
||||
'share_link',
|
||||
'share_token',
|
||||
'require_password',
|
||||
'event_name',
|
||||
'event_type',
|
||||
'event_date',
|
||||
'expires_at',
|
||||
'is_active',
|
||||
'is_archived'
|
||||
)
|
||||
.where(ACTIVE_EVENT_FILTER);
|
||||
|
||||
let event = await baseQuery.clone().where({ slug: trimmed }).first();
|
||||
if (event) {
|
||||
return { event, matchType: 'slug', shareToken: getEventShareToken(event) };
|
||||
}
|
||||
|
||||
event = await baseQuery.clone().where({ share_token: trimmed }).first();
|
||||
if (event) {
|
||||
return { event, matchType: 'token', shareToken: getEventShareToken(event) };
|
||||
}
|
||||
|
||||
event = await baseQuery.clone().where({ share_link: trimmed }).first();
|
||||
if (event) {
|
||||
return { event, matchType: 'link', shareToken: getEventShareToken(event) };
|
||||
}
|
||||
|
||||
event = await baseQuery.clone().where('share_link', 'like', `%/${trimmed}`).first();
|
||||
if (event) {
|
||||
return { event, matchType: 'link_partial', shareToken: getEventShareToken(event) };
|
||||
}
|
||||
|
||||
// As a final fallback, if identifier looks like a token but we did not match via share_token
|
||||
if (isPotentialShareToken(trimmed)) {
|
||||
event = await baseQuery.clone().whereRaw('LOWER(share_token) = ?', [trimmed.toLowerCase()]).first();
|
||||
if (event) {
|
||||
return { event, matchType: 'token_case_insensitive', shareToken: getEventShareToken(event) };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
isShortGalleryUrlsEnabled,
|
||||
clearShareLinkSettingsCache,
|
||||
buildShareLinkVariants,
|
||||
getEventShareToken,
|
||||
resolveShareIdentifier
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
const SHARE_TOKEN_REGEX = /^[0-9a-fA-F]{32}$/;
|
||||
|
||||
/**
|
||||
* Extracts the share token portion from a stored share link.
|
||||
* Supports full URLs, absolute paths, and legacy slug/token formats.
|
||||
* @param {string|null|undefined} shareLink
|
||||
* @returns {string|null}
|
||||
*/
|
||||
function extractShareToken(shareLink) {
|
||||
if (!shareLink) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = String(shareLink).trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove protocol + host when a full URL is stored
|
||||
const path = trimmed.replace(/^https?:\/\/[^/]+/i, '');
|
||||
const segments = path.split('/').filter(Boolean);
|
||||
if (segments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidate = segments[segments.length - 1];
|
||||
return candidate || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the provided identifier looks like a generated share token.
|
||||
* @param {string|null|undefined} identifier
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isPotentialShareToken(identifier) {
|
||||
if (!identifier) {
|
||||
return false;
|
||||
}
|
||||
return SHARE_TOKEN_REGEX.test(String(identifier).trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the gallery share path depending on whether short URLs are enabled.
|
||||
* @param {string} slug
|
||||
* @param {string} shareToken
|
||||
* @param {boolean} useShort
|
||||
* @returns {string}
|
||||
*/
|
||||
function buildSharePath(slug, shareToken, useShort) {
|
||||
if (!shareToken) {
|
||||
throw new Error('shareToken is required to build share path');
|
||||
}
|
||||
if (useShort || !slug) {
|
||||
return `/gallery/${shareToken}`;
|
||||
}
|
||||
return `/gallery/${slug}/${shareToken}`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
extractShareToken,
|
||||
isPotentialShareToken,
|
||||
buildSharePath
|
||||
};
|
||||
Reference in New Issue
Block a user