Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a619d52d17 | |||
| dd8cc14d30 | |||
| 3b7d723c2a | |||
| dc6252ff56 |
Binary file not shown.
@@ -0,0 +1,121 @@
|
||||
exports.up = async function(knex) {
|
||||
// Check if CMS pages already exist
|
||||
const impressumExists = await knex('cms_pages')
|
||||
.where('slug', 'impressum')
|
||||
.first();
|
||||
|
||||
const datenschutzExists = await knex('cms_pages')
|
||||
.where('slug', 'datenschutz')
|
||||
.first();
|
||||
|
||||
const pagesToInsert = [];
|
||||
|
||||
// Add Impressum page if it doesn't exist
|
||||
if (!impressumExists) {
|
||||
pagesToInsert.push({
|
||||
slug: 'impressum',
|
||||
title_en: 'Legal Notice',
|
||||
title_de: 'Impressum',
|
||||
content_en: `<h1>Legal Notice</h1>
|
||||
<p>Information according to § 5 TMG</p>
|
||||
|
||||
<h2>Responsible for content</h2>
|
||||
<p>[Your Name]<br>
|
||||
[Your Address]<br>
|
||||
[Postal Code City]</p>
|
||||
|
||||
<h2>Contact</h2>
|
||||
<p>Email: [Your Email Address]<br>
|
||||
Phone: [Your Phone Number]</p>
|
||||
|
||||
<h2>Disclaimer</h2>
|
||||
<h3>Liability for content</h3>
|
||||
<p>The contents of our pages were created with great care. However, we cannot guarantee the accuracy, completeness and timeliness of the content.</p>
|
||||
|
||||
<h3>Liability for links</h3>
|
||||
<p>Our website contains links to external third-party websites over whose content we have no influence. Therefore, we cannot accept any liability for this third-party content.</p>`,
|
||||
content_de: `<h1>Impressum</h1>
|
||||
<p>Angaben gemäß § 5 TMG</p>
|
||||
|
||||
<h2>Verantwortlich für den Inhalt</h2>
|
||||
<p>[Ihr Name]<br>
|
||||
[Ihre Adresse]<br>
|
||||
[PLZ Ort]</p>
|
||||
|
||||
<h2>Kontakt</h2>
|
||||
<p>E-Mail: [Ihre E-Mail-Adresse]<br>
|
||||
Telefon: [Ihre Telefonnummer]</p>
|
||||
|
||||
<h2>Haftungsausschluss</h2>
|
||||
<h3>Haftung für Inhalte</h3>
|
||||
<p>Die Inhalte unserer Seiten wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte können wir jedoch keine Gewähr übernehmen.</p>
|
||||
|
||||
<h3>Haftung für Links</h3>
|
||||
<p>Unser Angebot enthält Links zu externen Webseiten Dritter, auf deren Inhalte wir keinen Einfluss haben. Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen.</p>`,
|
||||
updated_at: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
// Add Datenschutz page if it doesn't exist
|
||||
if (!datenschutzExists) {
|
||||
pagesToInsert.push({
|
||||
slug: 'datenschutz',
|
||||
title_en: 'Privacy Policy',
|
||||
title_de: 'Datenschutzerklärung',
|
||||
content_en: `<h1>Privacy Policy</h1>
|
||||
|
||||
<h2>1. Privacy at a Glance</h2>
|
||||
<h3>General Information</h3>
|
||||
<p>The following information provides a simple overview of what happens to your personal data when you visit this website.</p>
|
||||
|
||||
<h3>Data Collection on This Website</h3>
|
||||
<p><strong>Who is responsible for data collection on this website?</strong></p>
|
||||
<p>Data processing on this website is carried out by the website operator. Their contact details can be found in the legal notice of this website.</p>
|
||||
|
||||
<p><strong>How do we collect your data?</strong></p>
|
||||
<p>Your data is collected when you provide it to us. This could be data that you enter into a contact form, for example.</p>
|
||||
|
||||
<p><strong>What do we use your data for?</strong></p>
|
||||
<p>Some of the data is collected to ensure error-free provision of the website. Other data may be used to analyze your user behavior.</p>
|
||||
|
||||
<h2>2. Hosting</h2>
|
||||
<p>This website is hosted externally. The personal data collected on this website is stored on the servers of the host.</p>
|
||||
|
||||
<h2>3. General Information and Mandatory Information</h2>
|
||||
<h3>Data Protection</h3>
|
||||
<p>The operators of these pages take the protection of your personal data very seriously. We treat your personal data confidentially and in accordance with the statutory data protection regulations and this privacy policy.</p>`,
|
||||
content_de: `<h1>Datenschutzerklärung</h1>
|
||||
|
||||
<h2>1. Datenschutz auf einen Blick</h2>
|
||||
<h3>Allgemeine Hinweise</h3>
|
||||
<p>Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren personenbezogenen Daten passiert, wenn Sie diese Website besuchen.</p>
|
||||
|
||||
<h3>Datenerfassung auf dieser Website</h3>
|
||||
<p><strong>Wer ist verantwortlich für die Datenerfassung auf dieser Website?</strong></p>
|
||||
<p>Die Datenverarbeitung auf dieser Website erfolgt durch den Websitebetreiber. Dessen Kontaktdaten können Sie dem Impressum dieser Website entnehmen.</p>
|
||||
|
||||
<p><strong>Wie erfassen wir Ihre Daten?</strong></p>
|
||||
<p>Ihre Daten werden zum einen dadurch erhoben, dass Sie uns diese mitteilen. Hierbei kann es sich z.B. um Daten handeln, die Sie in ein Kontaktformular eingeben.</p>
|
||||
|
||||
<p><strong>Wofür nutzen wir Ihre Daten?</strong></p>
|
||||
<p>Ein Teil der Daten wird erhoben, um eine fehlerfreie Bereitstellung der Website zu gewährleisten. Andere Daten können zur Analyse Ihres Nutzerverhaltens verwendet werden.</p>
|
||||
|
||||
<h2>2. Hosting</h2>
|
||||
<p>Diese Website wird extern gehostet. Die personenbezogenen Daten, die auf dieser Website erfasst werden, werden auf den Servern des Hosters gespeichert.</p>
|
||||
|
||||
<h2>3. Allgemeine Hinweise und Pflichtinformationen</h2>
|
||||
<h3>Datenschutz</h3>
|
||||
<p>Die Betreiber dieser Seiten nehmen den Schutz Ihrer persönlichen Daten sehr ernst. Wir behandeln Ihre personenbezogenen Daten vertraulich und entsprechend der gesetzlichen Datenschutzvorschriften sowie dieser Datenschutzerklärung.</p>`,
|
||||
updated_at: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
// Insert pages if any need to be added
|
||||
if (pagesToInsert.length > 0) {
|
||||
await knex('cms_pages').insert(pagesToInsert);
|
||||
}
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
// Don't remove CMS pages on rollback as they might have been customized
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
exports.up = async function(knex) {
|
||||
console.log('Fixing JSON columns in database...');
|
||||
|
||||
// Fix email_templates variables column
|
||||
const templates = await knex('email_templates').select('id', 'template_key', 'variables');
|
||||
|
||||
for (const template of templates) {
|
||||
if (template.variables && typeof template.variables === 'string') {
|
||||
try {
|
||||
// Check if it's already valid JSON
|
||||
JSON.parse(template.variables);
|
||||
} catch (e) {
|
||||
console.log(`Fixing invalid JSON in email template ${template.template_key}`);
|
||||
// Attempt to fix common issues
|
||||
let fixed = template.variables;
|
||||
|
||||
// If it looks like an array but isn't valid JSON, try to fix it
|
||||
if (fixed.startsWith('[') && fixed.endsWith(']')) {
|
||||
// Extract the content and properly format it
|
||||
const content = fixed.slice(1, -1);
|
||||
const items = content.split(',').map(item => item.trim().replace(/['"]/g, ''));
|
||||
fixed = JSON.stringify(items);
|
||||
} else {
|
||||
// Default to empty array if we can't fix it
|
||||
fixed = JSON.stringify([]);
|
||||
}
|
||||
|
||||
await knex('email_templates')
|
||||
.where('id', template.id)
|
||||
.update({ variables: fixed });
|
||||
}
|
||||
} else if (!template.variables) {
|
||||
// Set default empty array for null values
|
||||
await knex('email_templates')
|
||||
.where('id', template.id)
|
||||
.update({ variables: JSON.stringify([]) });
|
||||
}
|
||||
}
|
||||
|
||||
// Fix activity_logs metadata column
|
||||
const activities = await knex('activity_logs').select('id', 'metadata');
|
||||
|
||||
for (const activity of activities) {
|
||||
if (activity.metadata && typeof activity.metadata === 'string') {
|
||||
try {
|
||||
// Check if it's already valid JSON
|
||||
JSON.parse(activity.metadata);
|
||||
} catch (e) {
|
||||
console.log(`Fixing invalid JSON in activity log ${activity.id}`);
|
||||
// Default to empty object if we can't parse it
|
||||
await knex('activity_logs')
|
||||
.where('id', activity.id)
|
||||
.update({ metadata: JSON.stringify({}) });
|
||||
}
|
||||
} else if (!activity.metadata) {
|
||||
// Set default empty object for null values
|
||||
await knex('activity_logs')
|
||||
.where('id', activity.id)
|
||||
.update({ metadata: JSON.stringify({}) });
|
||||
}
|
||||
}
|
||||
|
||||
console.log('JSON columns fixed successfully');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
// No rollback needed - data fixes only
|
||||
};
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "picpeak-backend",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.18",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "picpeak-backend",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.18",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.16",
|
||||
"archiver": "^5.3.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "picpeak-backend",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.18",
|
||||
"description": "Backend for PicPeak event photo sharing platform",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
require('dotenv').config();
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function debugEndpoints() {
|
||||
console.log('Debugging 500 errors...\n');
|
||||
|
||||
try {
|
||||
// Test email templates query
|
||||
console.log('1. Testing email templates query:');
|
||||
try {
|
||||
const templates = await db('email_templates')
|
||||
.select('*')
|
||||
.orderBy('template_key');
|
||||
|
||||
console.log(`Found ${templates.length} templates`);
|
||||
if (templates.length > 0) {
|
||||
console.log('First template columns:', Object.keys(templates[0]));
|
||||
console.log('Template keys:', templates.map(t => t.template_key));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Email templates query failed:', error.message);
|
||||
console.error('Error code:', error.code);
|
||||
}
|
||||
|
||||
// Test notifications query
|
||||
console.log('\n2. Testing notifications query:');
|
||||
try {
|
||||
const notifications = await db('activity_logs')
|
||||
.select(
|
||||
'activity_logs.*',
|
||||
'events.event_name'
|
||||
)
|
||||
.leftJoin('events', 'activity_logs.event_id', 'events.id')
|
||||
.whereNull('activity_logs.read_at')
|
||||
.orderBy('activity_logs.created_at', 'desc')
|
||||
.limit(5);
|
||||
|
||||
console.log(`Found ${notifications.length} unread notifications`);
|
||||
} catch (error) {
|
||||
console.error('Notifications query failed:', error.message);
|
||||
console.error('Error code:', error.code);
|
||||
|
||||
// Check if it's a column issue
|
||||
if (error.message.includes('column')) {
|
||||
console.log('\nChecking activity_logs columns:');
|
||||
const columns = await db('activity_logs').columnInfo();
|
||||
console.log('Columns:', Object.keys(columns));
|
||||
}
|
||||
}
|
||||
|
||||
// Test specific template query
|
||||
console.log('\n3. Testing specific template query (gallery_created):');
|
||||
try {
|
||||
const template = await db('email_templates')
|
||||
.where('template_key', 'gallery_created')
|
||||
.first();
|
||||
|
||||
if (template) {
|
||||
console.log('Template found:', template.template_key);
|
||||
console.log('Has subject_en?', template.subject_en !== undefined);
|
||||
console.log('Has subject?', template.subject !== undefined);
|
||||
} else {
|
||||
console.log('Template not found');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Template query failed:', error.message);
|
||||
}
|
||||
|
||||
// Check CMS pages
|
||||
console.log('\n4. Checking CMS pages:');
|
||||
try {
|
||||
const pages = await db('cms_pages')
|
||||
.select('slug', 'title', 'is_published')
|
||||
.orderBy('slug');
|
||||
|
||||
console.log(`Found ${pages.length} CMS pages:`);
|
||||
pages.forEach(page => {
|
||||
console.log(` - ${page.slug}: ${page.title} (published: ${page.is_published})`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('CMS pages query failed:', error.message);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('General error:', error);
|
||||
} finally {
|
||||
await db.destroy();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
debugEndpoints();
|
||||
@@ -0,0 +1,145 @@
|
||||
require('dotenv').config();
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function fixProductionIssues() {
|
||||
console.log('Fixing production database issues...\n');
|
||||
|
||||
try {
|
||||
// 1. Check and fix email_templates structure
|
||||
console.log('1. Checking email_templates structure:');
|
||||
const emailColumns = await db('email_templates').columnInfo();
|
||||
console.log('Current columns:', Object.keys(emailColumns));
|
||||
|
||||
// Check if we need to add basic columns back
|
||||
const hasSubject = 'subject' in emailColumns;
|
||||
const hasSubjectEn = 'subject_en' in emailColumns;
|
||||
|
||||
if (hasSubjectEn && !hasSubject) {
|
||||
console.log('Adding basic columns back to email_templates...');
|
||||
await db.schema.alterTable('email_templates', (table) => {
|
||||
table.string('subject');
|
||||
table.text('body_html');
|
||||
table.text('body_text');
|
||||
});
|
||||
|
||||
// Copy values from _en columns
|
||||
await db('email_templates').update({
|
||||
subject: db.raw('subject_en'),
|
||||
body_html: db.raw('body_html_en'),
|
||||
body_text: db.raw('body_text_en')
|
||||
});
|
||||
console.log('Basic columns added successfully');
|
||||
}
|
||||
|
||||
// 2. Ensure default templates exist
|
||||
console.log('\n2. Checking email templates:');
|
||||
const templateCount = await db('email_templates').count('* as count');
|
||||
console.log('Template count:', templateCount[0].count);
|
||||
|
||||
if (templateCount[0].count === 0) {
|
||||
console.log('No templates found, inserting defaults...');
|
||||
const defaultTemplates = [
|
||||
{
|
||||
template_key: 'gallery_created',
|
||||
subject: 'Your Photo Gallery is Ready!',
|
||||
body_html: '<h2>Gallery Created Successfully</h2>...',
|
||||
body_text: 'Gallery Created Successfully...',
|
||||
variables: JSON.stringify(['host_name', 'event_name', 'event_date', 'gallery_link', 'gallery_password', 'expiry_date'])
|
||||
},
|
||||
{
|
||||
template_key: 'expiration_warning',
|
||||
subject: 'Your Photo Gallery Expires Soon',
|
||||
body_html: '<h2>Gallery Expiring Soon</h2>...',
|
||||
body_text: 'Gallery Expiring Soon...',
|
||||
variables: JSON.stringify(['host_name', 'event_name', 'days_remaining', 'gallery_link'])
|
||||
},
|
||||
{
|
||||
template_key: 'gallery_expired',
|
||||
subject: 'Your Photo Gallery Has Expired',
|
||||
body_html: '<h2>Gallery Expired</h2>...',
|
||||
body_text: 'Gallery Expired...',
|
||||
variables: JSON.stringify(['host_name', 'event_name'])
|
||||
},
|
||||
{
|
||||
template_key: 'archive_complete',
|
||||
subject: 'Gallery Archive Complete',
|
||||
body_html: '<h2>Archive Complete</h2>...',
|
||||
body_text: 'Archive Complete...',
|
||||
variables: JSON.stringify(['host_name', 'event_name', 'archive_size'])
|
||||
}
|
||||
];
|
||||
|
||||
for (const template of defaultTemplates) {
|
||||
// Add language columns if they exist
|
||||
if (hasSubjectEn) {
|
||||
template.subject_en = template.subject;
|
||||
template.body_html_en = template.body_html;
|
||||
template.body_text_en = template.body_text;
|
||||
template.subject_de = template.subject;
|
||||
template.body_html_de = template.body_html;
|
||||
template.body_text_de = template.body_text;
|
||||
}
|
||||
|
||||
await db('email_templates').insert(template);
|
||||
}
|
||||
console.log('Default templates inserted');
|
||||
}
|
||||
|
||||
// 3. Check activity_logs structure
|
||||
console.log('\n3. Checking activity_logs structure:');
|
||||
const activityColumns = await db('activity_logs').columnInfo();
|
||||
console.log('Columns:', Object.keys(activityColumns));
|
||||
|
||||
// Check if read_at exists
|
||||
if (!('read_at' in activityColumns)) {
|
||||
console.log('Adding read_at column to activity_logs...');
|
||||
await db.schema.alterTable('activity_logs', (table) => {
|
||||
table.datetime('read_at').nullable();
|
||||
});
|
||||
console.log('read_at column added');
|
||||
}
|
||||
|
||||
// 4. Check and add CMS pages
|
||||
console.log('\n4. Checking CMS pages:');
|
||||
const cmsColumns = await db('cms_pages').columnInfo();
|
||||
console.log('CMS columns:', Object.keys(cmsColumns));
|
||||
|
||||
const impressum = await db('cms_pages').where('slug', 'impressum').first();
|
||||
const datenschutz = await db('cms_pages').where('slug', 'datenschutz').first();
|
||||
|
||||
if (!impressum) {
|
||||
console.log('Adding Impressum page...');
|
||||
await db('cms_pages').insert({
|
||||
slug: 'impressum',
|
||||
title_en: 'Legal Notice',
|
||||
title_de: 'Impressum',
|
||||
content_en: '<h1>Legal Notice</h1><p>Your legal information here...</p>',
|
||||
content_de: '<h1>Impressum</h1><p>Ihre rechtlichen Informationen hier...</p>',
|
||||
updated_at: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
if (!datenschutz) {
|
||||
console.log('Adding Datenschutz page...');
|
||||
await db('cms_pages').insert({
|
||||
slug: 'datenschutz',
|
||||
title_en: 'Privacy Policy',
|
||||
title_de: 'Datenschutzerklärung',
|
||||
content_en: '<h1>Privacy Policy</h1><p>Your privacy policy here...</p>',
|
||||
content_de: '<h1>Datenschutzerklärung</h1><p>Ihre Datenschutzerklärung hier...</p>',
|
||||
updated_at: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\n✅ All fixes applied successfully!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fixing issues:', error);
|
||||
console.error('Stack:', error.stack);
|
||||
} finally {
|
||||
await db.destroy();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
fixProductionIssues();
|
||||
@@ -28,6 +28,9 @@ const adminAuthRoutes = require('./src/routes/adminAuth');
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Trust proxy headers (required for Traefik/nginx)
|
||||
app.set('trust proxy', true);
|
||||
|
||||
// Security middleware with custom CSP
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
|
||||
@@ -160,20 +160,44 @@ router.get('/templates', adminAuth, async (req, res) => {
|
||||
.orderBy('template_key');
|
||||
|
||||
// Parse variables JSON and format for multi-language support
|
||||
const formattedTemplates = templates.map(template => ({
|
||||
id: template.id,
|
||||
template_key: template.template_key,
|
||||
// English versions
|
||||
subject_en: template.subject_en || template.subject,
|
||||
body_html_en: template.body_html_en || template.body_html,
|
||||
body_text_en: template.body_text_en || template.body_text,
|
||||
// German versions
|
||||
subject_de: template.subject_de || template.subject_en || template.subject,
|
||||
body_html_de: template.body_html_de || template.body_html_en || template.body_html,
|
||||
body_text_de: template.body_text_de || template.body_text_en || template.body_text,
|
||||
variables: template.variables ? JSON.parse(template.variables) : [],
|
||||
updated_at: template.updated_at
|
||||
}));
|
||||
const formattedTemplates = templates.map(template => {
|
||||
const result = {
|
||||
id: template.id,
|
||||
template_key: template.template_key,
|
||||
variables: (() => {
|
||||
try {
|
||||
if (!template.variables) return [];
|
||||
if (typeof template.variables === 'object') return template.variables;
|
||||
return JSON.parse(template.variables);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse variables for template:', template.template_key, e.message);
|
||||
return [];
|
||||
}
|
||||
})(),
|
||||
updated_at: template.updated_at
|
||||
};
|
||||
|
||||
// Handle both old and new schema formats
|
||||
if (template.subject_en !== undefined) {
|
||||
// New schema with language columns
|
||||
result.subject_en = template.subject_en;
|
||||
result.body_html_en = template.body_html_en;
|
||||
result.body_text_en = template.body_text_en;
|
||||
result.subject_de = template.subject_de;
|
||||
result.body_html_de = template.body_html_de;
|
||||
result.body_text_de = template.body_text_de;
|
||||
} else {
|
||||
// Old schema - use basic columns for both languages
|
||||
result.subject_en = template.subject;
|
||||
result.body_html_en = template.body_html;
|
||||
result.body_text_en = template.body_text;
|
||||
result.subject_de = template.subject;
|
||||
result.body_html_de = template.body_html;
|
||||
result.body_text_de = template.body_text;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
res.json(formattedTemplates);
|
||||
} catch (error) {
|
||||
@@ -197,7 +221,16 @@ router.get('/templates/:key', adminAuth, async (req, res) => {
|
||||
const response = {
|
||||
id: template.id,
|
||||
template_key: template.template_key,
|
||||
variables: template.variables ? JSON.parse(template.variables) : [],
|
||||
variables: (() => {
|
||||
try {
|
||||
if (!template.variables) return [];
|
||||
if (typeof template.variables === 'object') return template.variables;
|
||||
return JSON.parse(template.variables);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse variables for template:', template.template_key, e.message);
|
||||
return [];
|
||||
}
|
||||
})(),
|
||||
updated_at: template.updated_at
|
||||
};
|
||||
|
||||
|
||||
@@ -32,7 +32,16 @@ router.get('/', adminAuth, async (req, res) => {
|
||||
actorName: notification.actor_name,
|
||||
eventName: notification.event_name,
|
||||
eventId: notification.event_id,
|
||||
metadata: notification.metadata ? JSON.parse(notification.metadata) : {},
|
||||
metadata: (() => {
|
||||
try {
|
||||
if (!notification.metadata) return {};
|
||||
if (typeof notification.metadata === 'object') return notification.metadata;
|
||||
return JSON.parse(notification.metadata);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse metadata for notification:', notification.id, e.message);
|
||||
return {};
|
||||
}
|
||||
})(),
|
||||
createdAt: notification.created_at,
|
||||
readAt: notification.read_at,
|
||||
isRead: !!notification.read_at
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "picpeak-frontend",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.18",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "picpeak-frontend",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.18",
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.0.0",
|
||||
"@tiptap/extension-link": "^2.25.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "picpeak-frontend",
|
||||
"private": true,
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.18",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
Reference in New Issue
Block a user