Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4bcca58a11 | |||
| 9fa5ba1cf7 | |||
| 88919fa0d3 | |||
| 5e43fc9cd9 | |||
| f053f42b6d | |||
| dc17e7d59d | |||
| f05ad87602 |
@@ -1,39 +0,0 @@
|
|||||||
name: Mirror to GitHub (Archive Method)
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
mirror:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Mirror using git archive
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
# Configure git
|
|
||||||
git config --global user.name "Gitea Mirror Bot"
|
|
||||||
git config --global user.email "bot@noreply.gitea.local"
|
|
||||||
|
|
||||||
# Copy gitattributes
|
|
||||||
cp .gitattributes-github .gitattributes
|
|
||||||
|
|
||||||
# Create archive excluding files
|
|
||||||
git archive --format=tar HEAD | tar -x -C /tmp/export
|
|
||||||
|
|
||||||
# Initialize new repo in export directory
|
|
||||||
cd /tmp/export
|
|
||||||
git init
|
|
||||||
git add .
|
|
||||||
git commit -m "Mirror from Gitea: $(date '+%Y-%m-%d %H:%M:%S')"
|
|
||||||
|
|
||||||
# Push to GitHub
|
|
||||||
git remote add origin https://x-access-token:${GITHUB_TOKEN}@github.com/YOUR_GITHUB_USERNAME/YOUR_REPO_NAME.git
|
|
||||||
git push -f origin main
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
name: Mirror to GitHub (Rsync Method)
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
mirror:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Prepare mirror directory
|
|
||||||
run: |
|
|
||||||
# Create mirror directory
|
|
||||||
mkdir -p /tmp/github-mirror
|
|
||||||
|
|
||||||
# Use rsync to copy files, excluding sensitive ones
|
|
||||||
rsync -av --exclude-from='.github-mirror-exclude' ./ /tmp/github-mirror/
|
|
||||||
|
|
||||||
- name: Push to GitHub
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
cd /tmp/github-mirror
|
|
||||||
|
|
||||||
# Initialize git repo
|
|
||||||
git init
|
|
||||||
git config user.name "Gitea Mirror Bot"
|
|
||||||
git config user.email "bot@noreply.gitea.local"
|
|
||||||
|
|
||||||
# Add all files and commit
|
|
||||||
git add .
|
|
||||||
git commit -m "Mirror from Gitea: $(git --git-dir=$GITHUB_WORKSPACE/.git log -1 --format='%h %s')"
|
|
||||||
|
|
||||||
# Push to GitHub
|
|
||||||
git remote add origin https://x-access-token:${GITHUB_TOKEN}@github.com/YOUR_GITHUB_USERNAME/YOUR_REPO_NAME.git
|
|
||||||
git push -f origin main
|
|
||||||
@@ -21,8 +21,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Create filtered branch
|
- name: Create filtered branch
|
||||||
run: |
|
run: |
|
||||||
|
|
||||||
# Create a new branch for GitHub
|
# Create a new branch for GitHub
|
||||||
git checkout -b github-mirror
|
git checkout --orphan -b github-mirror
|
||||||
|
|
||||||
# Remove sensitive files/directories
|
# Remove sensitive files/directories
|
||||||
# Example: Remove .env files, private configs, etc.
|
# Example: Remove .env files, private configs, etc.
|
||||||
@@ -49,7 +50,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Push to GitHub
|
- name: Push to GitHub
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUBTOKEN }}
|
GITHUBTOKEN: ${{ secrets.GITHUBTOKEN }}
|
||||||
run: |
|
run: |
|
||||||
# Add GitHub remote
|
# Add GitHub remote
|
||||||
git remote add github https://x-access-token:${GITHUBTOKEN}@github.com/the-luap/picpeak.git
|
git remote add github https://x-access-token:${GITHUBTOKEN}@github.com/the-luap/picpeak.git
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to PicPeak will be documented in this file.
|
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
||||||
|
|
||||||
## [Unreleased]
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Gitea workflows for selective GitHub mirroring
|
|
||||||
- Password visibility toggle on event creation form
|
|
||||||
- Translation for password requirement errors
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Relaxed password requirements from 12 to 8 characters minimum
|
|
||||||
- Made special characters optional for gallery passwords
|
|
||||||
- Improved password strength requirements for better usability
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Production deployment issues with Traefik routing
|
|
||||||
- Database migration failures for email_queue table
|
|
||||||
- JSON parsing errors in production environment
|
|
||||||
- PostgreSQL compatibility issues
|
|
||||||
- Connection stability in production
|
|
||||||
|
|
||||||
## [1.0.22] - 2024-01-14
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Comprehensive error boundaries for better error handling
|
|
||||||
- Skeleton loading screens for improved perceived performance
|
|
||||||
- Offline indicator for network status
|
|
||||||
- Keyboard navigation support in gallery lightbox
|
|
||||||
- Skip links for accessibility
|
|
||||||
- Focus trap management for modals
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Improved accessibility to WCAG 2.1 AA compliance
|
|
||||||
- Enhanced loading states with skeleton screens
|
|
||||||
- Better error recovery with component-level boundaries
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Missing translations in German locale
|
|
||||||
- Session timeout caching issues
|
|
||||||
- Email template JSON parsing errors
|
|
||||||
|
|
||||||
## [1.0.0] - 2024-01-01
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Initial release of PicPeak
|
|
||||||
- Photo gallery management system
|
|
||||||
- Automatic file watching and gallery creation
|
|
||||||
- Password-protected galleries
|
|
||||||
- Expiration system with email notifications
|
|
||||||
- Admin dashboard with analytics
|
|
||||||
- Multi-language support (EN, DE)
|
|
||||||
- Docker deployment support
|
|
||||||
- Email template customization
|
|
||||||
- User upload functionality
|
|
||||||
- Bulk download features
|
|
||||||
- Mobile-responsive design
|
|
||||||
- Theme customization options
|
|
||||||
|
|
||||||
[Unreleased]: https://github.com/the-luap/picpeak/compare/v1.0.22...HEAD
|
|
||||||
[1.0.22]: https://github.com/the-luap/picpeak/compare/v1.0.0...v1.0.22
|
|
||||||
[1.0.0]: https://github.com/the-luap/picpeak/releases/tag/v1.0.0
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Ensure PostgreSQL compatibility for all insert operations
|
||||||
|
* This migration doesn't change the schema but ensures all tables
|
||||||
|
* are compatible with .returning() syntax
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.up = async function(knex) {
|
||||||
|
// This migration is informational only
|
||||||
|
// All insert operations should use .returning('id') going forward
|
||||||
|
|
||||||
|
console.log('PostgreSQL compatibility check:');
|
||||||
|
console.log('- All INSERT operations should use .returning("id")');
|
||||||
|
console.log('- All date operations should use ISO strings');
|
||||||
|
console.log('- Boolean values are handled automatically by Knex');
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = async function(knex) {
|
||||||
|
// No rollback needed
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Fix boolean compatibility issues between PostgreSQL and SQLite
|
||||||
|
* This migration updates the database configuration and existing data
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.up = async function(knex) {
|
||||||
|
const isPostgres = knex.client.config.client === 'pg';
|
||||||
|
|
||||||
|
if (!isPostgres) {
|
||||||
|
// Enable foreign keys for SQLite
|
||||||
|
await knex.raw('PRAGMA foreign_keys = ON');
|
||||||
|
|
||||||
|
// Note: SQLite stores booleans as 0/1
|
||||||
|
// No data migration needed as Knex handles this automatically
|
||||||
|
// But queries must use formatBoolean() helper
|
||||||
|
|
||||||
|
console.log('SQLite boolean compatibility check:');
|
||||||
|
console.log('- SQLite stores booleans as 0/1');
|
||||||
|
console.log('- All boolean comparisons should use formatBoolean() helper');
|
||||||
|
console.log('- Foreign keys enabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = async function(knex) {
|
||||||
|
// No rollback needed
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.25",
|
"version": "1.0.28",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.25",
|
"version": "1.0.28",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.25",
|
"version": "1.0.28",
|
||||||
"description": "Backend for PicPeak event photo sharing platform",
|
"description": "Backend for PicPeak event photo sharing platform",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ async function createTestEvent() {
|
|||||||
await db('events').where('slug', eventData.slug).delete();
|
await db('events').where('slug', eventData.slug).delete();
|
||||||
|
|
||||||
// Insert new event
|
// Insert new event
|
||||||
const [eventId] = await db('events').insert(eventData);
|
const insertResult = await db('events').insert(eventData).returning('id');
|
||||||
|
const eventId = insertResult[0]?.id || insertResult[0];
|
||||||
console.log('Event created with ID:', eventId);
|
console.log('Event created with ID:', eventId);
|
||||||
|
|
||||||
console.log('\nTest event created successfully!');
|
console.log('\nTest event created successfully!');
|
||||||
|
|||||||
@@ -211,12 +211,14 @@ router.post('/:id/restore', adminAuth, async (req, res) => {
|
|||||||
categoriesMap.set(categoryName, existingCategory.id);
|
categoriesMap.set(categoryName, existingCategory.id);
|
||||||
} else {
|
} else {
|
||||||
// Create the category if it doesn't exist
|
// Create the category if it doesn't exist
|
||||||
const [newCategoryId] = await db('photo_categories').insert({
|
const insertResult = await db('photo_categories').insert({
|
||||||
event_id: archive.id,
|
event_id: archive.id,
|
||||||
name: categoryName,
|
name: categoryName,
|
||||||
slug: categoryName.toLowerCase().replace(/[^a-z0-9]/g, '-'),
|
slug: categoryName.toLowerCase().replace(/[^a-z0-9]/g, '-'),
|
||||||
created_at: new Date()
|
created_at: new Date()
|
||||||
});
|
}).returning('id');
|
||||||
|
|
||||||
|
const newCategoryId = insertResult[0]?.id || insertResult[0];
|
||||||
categoriesMap.set(categoryName, newCategoryId);
|
categoriesMap.set(categoryName, newCategoryId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -266,6 +268,9 @@ router.post('/:id/restore', adminAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update event status
|
// Update event status
|
||||||
|
const thirtyDaysFromNow = new Date();
|
||||||
|
thirtyDaysFromNow.setDate(thirtyDaysFromNow.getDate() + 30);
|
||||||
|
|
||||||
await db('events')
|
await db('events')
|
||||||
.where('id', req.params.id)
|
.where('id', req.params.id)
|
||||||
.update({
|
.update({
|
||||||
@@ -273,7 +278,7 @@ router.post('/:id/restore', adminAuth, async (req, res) => {
|
|||||||
is_active: true,
|
is_active: true,
|
||||||
archive_path: null,
|
archive_path: null,
|
||||||
archived_at: null,
|
archived_at: null,
|
||||||
expires_at: db.raw("datetime('now', '+30 days')") // Reset expiration
|
expires_at: thirtyDaysFromNow.toISOString() // Reset expiration - works on both DBs
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log activity
|
// Log activity
|
||||||
|
|||||||
@@ -77,12 +77,14 @@ router.post('/', adminAuth, [
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create category
|
// Create category
|
||||||
const [categoryId] = await db('photo_categories').insert({
|
const insertResult = await db('photo_categories').insert({
|
||||||
name,
|
name,
|
||||||
slug: categorySlug,
|
slug: categorySlug,
|
||||||
is_global,
|
is_global,
|
||||||
event_id: is_global ? null : event_id
|
event_id: is_global ? null : event_id
|
||||||
});
|
}).returning('id');
|
||||||
|
|
||||||
|
const categoryId = insertResult[0]?.id || insertResult[0];
|
||||||
|
|
||||||
const category = await db('photo_categories').where('id', categoryId).first();
|
const category = await db('photo_categories').where('id', categoryId).first();
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const express = require('express');
|
|||||||
const { db } = require('../database/db');
|
const { db } = require('../database/db');
|
||||||
const { adminAuth } = require('../middleware/auth-enhanced-v2');
|
const { adminAuth } = require('../middleware/auth-enhanced-v2');
|
||||||
const { sanitizeDays, addDateRangeCondition } = require('../utils/sqlSecurity');
|
const { sanitizeDays, addDateRangeCondition } = require('../utils/sqlSecurity');
|
||||||
|
const { formatBoolean } = require('../utils/dbCompat');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Get dashboard statistics
|
// Get dashboard statistics
|
||||||
@@ -9,8 +10,8 @@ router.get('/stats', adminAuth, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
// Get active events count
|
// Get active events count
|
||||||
const activeEvents = await db('events')
|
const activeEvents = await db('events')
|
||||||
.where('is_active', true)
|
.where('is_active', formatBoolean(true))
|
||||||
.where('is_archived', false)
|
.where('is_archived', formatBoolean(false))
|
||||||
.count('id as count')
|
.count('id as count')
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
@@ -20,8 +21,8 @@ router.get('/stats', adminAuth, async (req, res) => {
|
|||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
const expiringEvents = await db('events')
|
const expiringEvents = await db('events')
|
||||||
.where('is_active', true)
|
.where('is_active', formatBoolean(true))
|
||||||
.where('is_archived', false)
|
.where('is_archived', formatBoolean(false))
|
||||||
.where('expires_at', '<=', sevenDaysFromNow.toISOString())
|
.where('expires_at', '<=', sevenDaysFromNow.toISOString())
|
||||||
.where('expires_at', '>', now.toISOString())
|
.where('expires_at', '>', now.toISOString())
|
||||||
.count('id as count')
|
.count('id as count')
|
||||||
@@ -56,7 +57,7 @@ router.get('/stats', adminAuth, async (req, res) => {
|
|||||||
|
|
||||||
// Get archived events count
|
// Get archived events count
|
||||||
const archivedEvents = await db('events')
|
const archivedEvents = await db('events')
|
||||||
.where('is_archived', true)
|
.where('is_archived', formatBoolean(true))
|
||||||
.count('id as count')
|
.count('id as count')
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ router.post('/', adminAuth, [
|
|||||||
await fs.mkdir(path.join(eventPath, 'individual'), { recursive: true });
|
await fs.mkdir(path.join(eventPath, 'individual'), { recursive: true });
|
||||||
|
|
||||||
// Insert into database
|
// Insert into database
|
||||||
const [eventId] = await db('events').insert({
|
const insertResult = await db('events').insert({
|
||||||
slug,
|
slug,
|
||||||
event_type,
|
event_type,
|
||||||
event_name,
|
event_name,
|
||||||
@@ -99,7 +99,10 @@ router.post('/', adminAuth, [
|
|||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
allow_user_uploads,
|
allow_user_uploads,
|
||||||
upload_category_id
|
upload_category_id
|
||||||
});
|
}).returning('id');
|
||||||
|
|
||||||
|
// Handle both PostgreSQL (returns array of objects) and SQLite (returns array of IDs)
|
||||||
|
const eventId = insertResult[0]?.id || insertResult[0];
|
||||||
|
|
||||||
// Log activity
|
// Log activity
|
||||||
await logActivity('event_created',
|
await logActivity('event_created',
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const { archiveEvent } = require('../services/archiveService');
|
|||||||
const { escapeLikePattern } = require('../utils/sqlSecurity');
|
const { escapeLikePattern } = require('../utils/sqlSecurity');
|
||||||
const { formatDate } = require('../utils/dateFormatter');
|
const { formatDate } = require('../utils/dateFormatter');
|
||||||
const { validatePasswordInContext, getBcryptRounds } = require('../utils/passwordValidation');
|
const { validatePasswordInContext, getBcryptRounds } = require('../utils/passwordValidation');
|
||||||
|
const { formatBoolean } = require('../utils/dbCompat');
|
||||||
|
|
||||||
// Create new event
|
// Create new event
|
||||||
router.post('/', adminAuth, [
|
router.post('/', adminAuth, [
|
||||||
@@ -92,7 +93,7 @@ router.post('/', adminAuth, [
|
|||||||
await fs.mkdir(path.join(eventPath, 'individual'), { recursive: true });
|
await fs.mkdir(path.join(eventPath, 'individual'), { recursive: true });
|
||||||
|
|
||||||
// Insert into database
|
// Insert into database
|
||||||
const [eventId] = await db('events').insert({
|
const insertResult = await db('events').insert({
|
||||||
slug,
|
slug,
|
||||||
event_type,
|
event_type,
|
||||||
event_name,
|
event_name,
|
||||||
@@ -108,7 +109,10 @@ router.post('/', adminAuth, [
|
|||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
allow_user_uploads,
|
allow_user_uploads,
|
||||||
upload_category_id
|
upload_category_id
|
||||||
});
|
}).returning('id');
|
||||||
|
|
||||||
|
// Handle both PostgreSQL (returns array of objects) and SQLite (returns array of IDs)
|
||||||
|
const eventId = insertResult[0]?.id || insertResult[0];
|
||||||
|
|
||||||
// Log activity
|
// Log activity
|
||||||
await logActivity('event_created',
|
await logActivity('event_created',
|
||||||
@@ -133,7 +137,9 @@ router.post('/', adminAuth, [
|
|||||||
gallery_password: password,
|
gallery_password: password,
|
||||||
expiry_date: await formatDate(expires_at, emailLang),
|
expiry_date: await formatDate(expires_at, emailLang),
|
||||||
welcome_message: welcome_message || ''
|
welcome_message: welcome_message || ''
|
||||||
})
|
}),
|
||||||
|
status: 'pending',
|
||||||
|
created_at: new Date()
|
||||||
// scheduled_at will use default value
|
// scheduled_at will use default value
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -178,17 +184,17 @@ router.get('/', adminAuth, async (req, res) => {
|
|||||||
|
|
||||||
// Apply status filter
|
// Apply status filter
|
||||||
if (status === 'active') {
|
if (status === 'active') {
|
||||||
query = query.where('is_active', true).where('is_archived', false);
|
query = query.where('is_active', formatBoolean(true)).where('is_archived', formatBoolean(false));
|
||||||
} else if (status === 'archived') {
|
} else if (status === 'archived') {
|
||||||
query = query.where('is_archived', true);
|
query = query.where('is_archived', formatBoolean(true));
|
||||||
} else if (status === 'inactive') {
|
} else if (status === 'inactive') {
|
||||||
query = query.where('is_active', false).where('is_archived', false);
|
query = query.where('is_active', formatBoolean(false)).where('is_archived', formatBoolean(false));
|
||||||
} else if (status === 'expiring') {
|
} else if (status === 'expiring') {
|
||||||
const sevenDaysFromNow = new Date();
|
const sevenDaysFromNow = new Date();
|
||||||
sevenDaysFromNow.setDate(sevenDaysFromNow.getDate() + 7);
|
sevenDaysFromNow.setDate(sevenDaysFromNow.getDate() + 7);
|
||||||
query = query
|
query = query
|
||||||
.where('is_active', true)
|
.where('is_active', formatBoolean(true))
|
||||||
.where('is_archived', false)
|
.where('is_archived', formatBoolean(false))
|
||||||
.where('expires_at', '<=', sevenDaysFromNow.toISOString())
|
.where('expires_at', '<=', sevenDaysFromNow.toISOString())
|
||||||
.where('expires_at', '>', new Date().toISOString());
|
.where('expires_at', '>', new Date().toISOString());
|
||||||
}
|
}
|
||||||
@@ -549,7 +555,7 @@ router.post('/bulk-archive', adminAuth, [
|
|||||||
// Get all events to archive
|
// Get all events to archive
|
||||||
const events = await db('events')
|
const events = await db('events')
|
||||||
.whereIn('id', eventIds)
|
.whereIn('id', eventIds)
|
||||||
.where('is_archived', false);
|
.where('is_archived', formatBoolean(false));
|
||||||
|
|
||||||
if (events.length === 0) {
|
if (events.length === 0) {
|
||||||
return res.status(400).json({ error: 'No valid events found to archive' });
|
return res.status(400).json({ error: 'No valid events found to archive' });
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ router.post('/', adminAuth, [
|
|||||||
await fs.mkdir(path.join(eventPath, 'individual'), { recursive: true });
|
await fs.mkdir(path.join(eventPath, 'individual'), { recursive: true });
|
||||||
|
|
||||||
// Insert into database
|
// Insert into database
|
||||||
const [eventId] = await db('events').insert({
|
const insertResult = await db('events').insert({
|
||||||
slug,
|
slug,
|
||||||
event_type,
|
event_type,
|
||||||
event_name,
|
event_name,
|
||||||
@@ -76,7 +76,10 @@ router.post('/', adminAuth, [
|
|||||||
color_theme,
|
color_theme,
|
||||||
share_link: shareLink,
|
share_link: shareLink,
|
||||||
expires_at
|
expires_at
|
||||||
});
|
}).returning('id');
|
||||||
|
|
||||||
|
// Handle both PostgreSQL (returns array of objects) and SQLite (returns array of IDs)
|
||||||
|
const eventId = insertResult[0]?.id || insertResult[0];
|
||||||
|
|
||||||
// Queue creation email
|
// Queue creation email
|
||||||
const { queueEmail } = require('../services/emailProcessor');
|
const { queueEmail } = require('../services/emailProcessor');
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const { archiveEvent } = require('./archiveService');
|
|||||||
const { queueEmail } = require('./emailProcessor');
|
const { queueEmail } = require('./emailProcessor');
|
||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
const { formatDate } = require('../utils/dateFormatter');
|
const { formatDate } = require('../utils/dateFormatter');
|
||||||
|
const { formatBoolean } = require('../utils/dbCompat');
|
||||||
|
|
||||||
function startExpirationChecker() {
|
function startExpirationChecker() {
|
||||||
// Check every hour for expired events and warnings
|
// Check every hour for expired events and warnings
|
||||||
@@ -21,8 +22,8 @@ async function checkExpirations() {
|
|||||||
|
|
||||||
// Check for events needing warning emails
|
// Check for events needing warning emails
|
||||||
const eventsNeedingWarning = await db('events')
|
const eventsNeedingWarning = await db('events')
|
||||||
.where('is_active', true)
|
.where('is_active', formatBoolean(true))
|
||||||
.where('is_archived', false)
|
.where('is_archived', formatBoolean(false))
|
||||||
.where('expires_at', '<=', warningDate)
|
.where('expires_at', '<=', warningDate)
|
||||||
.where('expires_at', '>', now);
|
.where('expires_at', '>', now);
|
||||||
|
|
||||||
@@ -40,8 +41,8 @@ async function checkExpirations() {
|
|||||||
|
|
||||||
// Check for expired events
|
// Check for expired events
|
||||||
const expiredEvents = await db('events')
|
const expiredEvents = await db('events')
|
||||||
.where('is_active', true)
|
.where('is_active', formatBoolean(true))
|
||||||
.where('is_archived', false)
|
.where('is_archived', formatBoolean(false))
|
||||||
.where('expires_at', '<=', now);
|
.where('expires_at', '<=', now);
|
||||||
|
|
||||||
for (const event of expiredEvents) {
|
for (const event of expiredEvents) {
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/**
|
||||||
|
* Database Compatibility Utilities
|
||||||
|
* Handles differences between PostgreSQL and SQLite
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Note: Requiring db here creates circular dependency
|
||||||
|
// db should be passed as parameter or required where needed
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get database client type
|
||||||
|
* @returns {string} 'pg' or 'sqlite3'
|
||||||
|
*/
|
||||||
|
function getDbClient() {
|
||||||
|
return process.env.DATABASE_CLIENT || 'sqlite3';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if using PostgreSQL
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isPostgreSQL() {
|
||||||
|
return getDbClient() === 'pg';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle insert operations that return IDs
|
||||||
|
* Works with both PostgreSQL and SQLite
|
||||||
|
* @param {object} query - Knex query builder
|
||||||
|
* @returns {Promise<number>} The inserted ID
|
||||||
|
*/
|
||||||
|
async function insertAndGetId(query) {
|
||||||
|
const result = await query.returning('id');
|
||||||
|
|
||||||
|
// PostgreSQL returns array of objects [{id: 1}]
|
||||||
|
// SQLite returns array of IDs [1]
|
||||||
|
return result[0]?.id || result[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format date for database compatibility
|
||||||
|
* @param {Date} date - JavaScript Date object
|
||||||
|
* @returns {string} ISO string format that works on both databases
|
||||||
|
*/
|
||||||
|
function formatDateForDB(date) {
|
||||||
|
return date.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add days to a date (database agnostic)
|
||||||
|
* @param {Date} date - Starting date
|
||||||
|
* @param {number} days - Number of days to add
|
||||||
|
* @returns {Date} New date
|
||||||
|
*/
|
||||||
|
function addDays(date, days) {
|
||||||
|
const result = new Date(date);
|
||||||
|
result.setDate(result.getDate() + days);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get date extraction SQL that works on both databases
|
||||||
|
* @param {object} db - Knex database instance
|
||||||
|
* @param {string} column - Column name
|
||||||
|
* @returns {object} Knex raw query
|
||||||
|
*/
|
||||||
|
function dateExtractSQL(db, column) {
|
||||||
|
if (isPostgreSQL()) {
|
||||||
|
return db.raw(`DATE(${column})`);
|
||||||
|
} else {
|
||||||
|
// SQLite uses date() function
|
||||||
|
return db.raw(`date(${column})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get database size query
|
||||||
|
* @param {object} db - Knex database instance
|
||||||
|
* @param {string} dbName - Database name
|
||||||
|
* @returns {Promise<number>} Size in bytes
|
||||||
|
*/
|
||||||
|
async function getDatabaseSize(db, dbName) {
|
||||||
|
if (isPostgreSQL()) {
|
||||||
|
const result = await db.raw('SELECT pg_database_size(?) as size', [dbName]);
|
||||||
|
return result.rows[0]?.size || 0;
|
||||||
|
} else {
|
||||||
|
// For SQLite, check file size
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const path = require('path');
|
||||||
|
const dbPath = process.env.DATABASE_PATH || path.join(__dirname, '../../data/photo_sharing.db');
|
||||||
|
try {
|
||||||
|
const stats = await fs.stat(dbPath);
|
||||||
|
return stats.size;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting SQLite database size:', error);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle boolean values for database compatibility
|
||||||
|
* @param {boolean} value - Boolean value
|
||||||
|
* @returns {any} Database-appropriate boolean representation
|
||||||
|
*/
|
||||||
|
function formatBoolean(value) {
|
||||||
|
if (isPostgreSQL()) {
|
||||||
|
return value;
|
||||||
|
} else {
|
||||||
|
// SQLite stores booleans as 0/1
|
||||||
|
return value ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse boolean from database
|
||||||
|
* @param {any} value - Database boolean value
|
||||||
|
* @returns {boolean} JavaScript boolean
|
||||||
|
*/
|
||||||
|
function parseBoolean(value) {
|
||||||
|
return Boolean(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getDbClient,
|
||||||
|
isPostgreSQL,
|
||||||
|
insertAndGetId,
|
||||||
|
formatDateForDB,
|
||||||
|
addDays,
|
||||||
|
dateExtractSQL,
|
||||||
|
getDatabaseSize,
|
||||||
|
formatBoolean,
|
||||||
|
parseBoolean
|
||||||
|
};
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"version": "1.0.25",
|
"version": "1.0.28",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"version": "1.0.25",
|
"version": "1.0.28",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.0.0",
|
"@tanstack/react-query": "^5.0.0",
|
||||||
"@tiptap/extension-link": "^2.25.0",
|
"@tiptap/extension-link": "^2.25.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.25",
|
"version": "1.0.28",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
Reference in New Issue
Block a user