fix: Resolve branding display issues and invitation parsing errors (v2.2.1) (#86)
Fixes #84, Fixes #85 - Fix uploads proxy routing in nginx and vite dev server - Fix logo/favicon state handling in BrandingPage - Fix invitation API response field transformation (snake_case → camelCase) - Add hide_powered_by to public settings API - Mark Multiple Administrators as implemented in roadmap - Bump version to 2.2.1
This commit is contained in:
@@ -45,14 +45,14 @@ jobs:
|
|||||||
- name: Determine build platforms
|
- name: Determine build platforms
|
||||||
id: platforms
|
id: platforms
|
||||||
run: |
|
run: |
|
||||||
# For PRs, build only amd64 to avoid QEMU emulation issues with Sharp
|
# Only build ARM64 for tagged releases (v*.*.*)
|
||||||
# For main/develop/tags, build multi-arch
|
# QEMU emulation is too slow/unreliable for npm operations on regular builds
|
||||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
||||||
echo "platforms=linux/amd64" >> $GITHUB_OUTPUT
|
|
||||||
echo "skip_qemu=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
|
echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
|
||||||
echo "skip_qemu=false" >> $GITHUB_OUTPUT
|
echo "skip_qemu=false" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "platforms=linux/amd64" >> $GITHUB_OUTPUT
|
||||||
|
echo "skip_qemu=true" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
@@ -142,14 +142,14 @@ jobs:
|
|||||||
- name: Determine build platforms
|
- name: Determine build platforms
|
||||||
id: platforms
|
id: platforms
|
||||||
run: |
|
run: |
|
||||||
# For PRs, build only amd64 to avoid QEMU emulation issues
|
# Only build ARM64 for tagged releases (v*.*.*)
|
||||||
# For main/develop/tags, build multi-arch
|
# QEMU emulation is too slow/unreliable for npm operations on regular builds
|
||||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
||||||
echo "platforms=linux/amd64" >> $GITHUB_OUTPUT
|
|
||||||
echo "skip_qemu=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
|
echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
|
||||||
echo "skip_qemu=false" >> $GITHUB_OUTPUT
|
echo "skip_qemu=false" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "platforms=linux/amd64" >> $GITHUB_OUTPUT
|
||||||
|
echo "skip_qemu=true" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ These features are currently in beta testing and may have limited functionality
|
|||||||
| **Face Recognition** | AI-powered face detection to help guests find their photos and create automatic person-based albums | Low | 🔄 Open |
|
| **Face Recognition** | AI-powered face detection to help guests find their photos and create automatic person-based albums | Low | 🔄 Open |
|
||||||
| **Gallery Feedback** | Allow guests to like, rate, and comment on photos with admin notifications and moderation | Medium | ✅ Implemented |
|
| **Gallery Feedback** | Allow guests to like, rate, and comment on photos with admin notifications and moderation | Medium | ✅ Implemented |
|
||||||
| **Video Support** | Upload and display videos alongside photos in galleries with streaming support | Low | ✅ Implemented |
|
| **Video Support** | Upload and display videos alongside photos in galleries with streaming support | Low | ✅ Implemented |
|
||||||
| **Multiple Administrators** | Support for multiple admin accounts with role-based permissions and activity tracking | Low | 📋 Planned |
|
| **Multiple Administrators** | Support for multiple admin accounts with role-based permissions and activity tracking | Low | ✅ Implemented |
|
||||||
| **Filtering & Export Options** | Filter photos by likes, ratings, comments, or favorites. Search by filename. Sort by date, name, size, or rating. Export filtered selections as ZIP or generate Capture One/Lightroom-compatible file lists for professional workflows | Medium | ✅ Implemented |
|
| **Filtering & Export Options** | Filter photos by likes, ratings, comments, or favorites. Search by filename. Sort by date, name, size, or rating. Export filtered selections as ZIP or generate Capture One/Lightroom-compatible file lists for professional workflows | Medium | ✅ Implemented |
|
||||||
|
|
||||||
**Status Legend:** ✅ Implemented | 🚧 In Progress | 🔄 Open | 📋 Planned
|
**Status Legend:** ✅ Implemented | 🚧 In Progress | 🔄 Open | 📋 Planned
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "2.2.0",
|
"version": "2.2.1",
|
||||||
"description": "Backend for PicPeak event photo sharing platform",
|
"description": "Backend for PicPeak event photo sharing platform",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -326,8 +326,12 @@ router.post('/logo', adminAuth, requirePermission('settings.edit'), upload.singl
|
|||||||
.first();
|
.first();
|
||||||
|
|
||||||
if (oldLogoSetting && oldLogoSetting.setting_value) {
|
if (oldLogoSetting && oldLogoSetting.setting_value) {
|
||||||
const oldPath = JSON.parse(oldLogoSetting.setting_value);
|
|
||||||
try {
|
try {
|
||||||
|
// Handle both JSON-serialized and legacy raw path values
|
||||||
|
let oldPath = oldLogoSetting.setting_value;
|
||||||
|
if (oldPath.startsWith('"')) {
|
||||||
|
oldPath = JSON.parse(oldPath);
|
||||||
|
}
|
||||||
await fs.unlink(oldPath);
|
await fs.unlink(oldPath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete old logo:', error);
|
console.error('Failed to delete old logo:', error);
|
||||||
@@ -355,13 +359,13 @@ router.post('/logo', adminAuth, requirePermission('settings.edit'), upload.singl
|
|||||||
await db('app_settings')
|
await db('app_settings')
|
||||||
.insert({
|
.insert({
|
||||||
setting_key: 'branding_logo_url',
|
setting_key: 'branding_logo_url',
|
||||||
setting_value: publicPath,
|
setting_value: JSON.stringify(publicPath),
|
||||||
setting_type: 'branding',
|
setting_type: 'branding',
|
||||||
updated_at: new Date()
|
updated_at: new Date()
|
||||||
})
|
})
|
||||||
.onConflict('setting_key')
|
.onConflict('setting_key')
|
||||||
.merge({
|
.merge({
|
||||||
setting_value: publicPath,
|
setting_value: JSON.stringify(publicPath),
|
||||||
updated_at: new Date()
|
updated_at: new Date()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -891,13 +895,13 @@ router.post('/favicon', adminAuth, requirePermission('settings.edit'), faviconUp
|
|||||||
await db('app_settings')
|
await db('app_settings')
|
||||||
.insert({
|
.insert({
|
||||||
setting_key: 'branding_favicon_url',
|
setting_key: 'branding_favicon_url',
|
||||||
setting_value: faviconUrl,
|
setting_value: JSON.stringify(faviconUrl),
|
||||||
setting_type: 'branding',
|
setting_type: 'branding',
|
||||||
updated_at: new Date()
|
updated_at: new Date()
|
||||||
})
|
})
|
||||||
.onConflict('setting_key')
|
.onConflict('setting_key')
|
||||||
.merge({
|
.merge({
|
||||||
setting_value: faviconUrl,
|
setting_value: JSON.stringify(faviconUrl),
|
||||||
updated_at: new Date()
|
updated_at: new Date()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,20 @@ function transformRole(role) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform invitation object from snake_case (DB) to camelCase (API)
|
||||||
|
*/
|
||||||
|
function transformInvitation(invitation) {
|
||||||
|
return {
|
||||||
|
id: invitation.id,
|
||||||
|
email: invitation.email,
|
||||||
|
expiresAt: invitation.expires_at,
|
||||||
|
createdAt: invitation.created_at,
|
||||||
|
roleName: invitation.role_name,
|
||||||
|
invitedBy: invitation.invited_by
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /me/permissions
|
* GET /me/permissions
|
||||||
* Get current user's permissions
|
* Get current user's permissions
|
||||||
@@ -81,7 +95,7 @@ router.get('/roles', adminAuth, requirePermission('users.view'), handleAsync(asy
|
|||||||
*/
|
*/
|
||||||
router.get('/invitations', adminAuth, requirePermission('users.view'), handleAsync(async (req, res) => {
|
router.get('/invitations', adminAuth, requirePermission('users.view'), handleAsync(async (req, res) => {
|
||||||
const invitations = await userManagementService.getPendingInvitations();
|
const invitations = await userManagementService.getPendingInvitations();
|
||||||
res.json({ invitations });
|
res.json({ invitations: invitations.map(transformInvitation) });
|
||||||
}));
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ router.get('/', async (req, res) => {
|
|||||||
branding_logo_display_header: settingsObject.branding_logo_display_header !== false,
|
branding_logo_display_header: settingsObject.branding_logo_display_header !== false,
|
||||||
branding_logo_display_hero: settingsObject.branding_logo_display_hero !== false,
|
branding_logo_display_hero: settingsObject.branding_logo_display_hero !== false,
|
||||||
branding_logo_display_mode: settingsObject.branding_logo_display_mode || 'logo_and_text',
|
branding_logo_display_mode: settingsObject.branding_logo_display_mode || 'logo_and_text',
|
||||||
|
branding_hide_powered_by: settingsObject.branding_hide_powered_by === true,
|
||||||
theme_config: settingsObject.theme_config || null,
|
theme_config: settingsObject.theme_config || null,
|
||||||
default_language: settingsObject.general_default_language || 'en',
|
default_language: settingsObject.general_default_language || 'en',
|
||||||
enable_analytics: settingsObject.general_enable_analytics !== false,
|
enable_analytics: settingsObject.general_enable_analytics !== false,
|
||||||
|
|||||||
+2
-1
@@ -88,7 +88,8 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Uploads serving proxy (logos, favicons, watermarks)
|
# Uploads serving proxy (logos, favicons, watermarks)
|
||||||
location /uploads {
|
# ^~ modifier stops regex matching, ensuring uploads are proxied not served locally
|
||||||
|
location ^~ /uploads {
|
||||||
proxy_pass http://backend:3001;
|
proxy_pass http://backend:3001;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.2.0",
|
"version": "2.2.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -81,9 +81,8 @@ export const BrandingPage: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (settings) {
|
if (settings) {
|
||||||
const formatted = settingsService.formatBrandingSettings(settings);
|
const formatted = settingsService.formatBrandingSettings(settings);
|
||||||
// Don't set logo_url here - it will be synced from theme
|
// Include logo_url from branding settings
|
||||||
const { logo_url, ...brandingWithoutLogo } = formatted;
|
setBrandingSettings(prev => ({ ...prev, ...formatted }));
|
||||||
setBrandingSettings(prev => ({ ...prev, ...brandingWithoutLogo }));
|
|
||||||
}
|
}
|
||||||
}, [settings]);
|
}, [settings]);
|
||||||
|
|
||||||
@@ -97,8 +96,10 @@ export const BrandingPage: React.FC = () => {
|
|||||||
setCurrentTheme(formatted);
|
setCurrentTheme(formatted);
|
||||||
setTheme(formatted);
|
setTheme(formatted);
|
||||||
|
|
||||||
// Always sync the logo URL from theme to branding settings - theme is source of truth
|
// Only sync logo URL from theme if it exists there (logo is stored in branding settings)
|
||||||
setBrandingSettings(prev => ({ ...prev, logo_url: formatted.logoUrl || '' }));
|
if (formatted.logoUrl) {
|
||||||
|
setBrandingSettings(prev => ({ ...prev, logo_url: formatted.logoUrl }));
|
||||||
|
}
|
||||||
|
|
||||||
// Try to identify which preset this matches
|
// Try to identify which preset this matches
|
||||||
for (const [key, preset] of Object.entries(GALLERY_THEME_PRESETS)) {
|
for (const [key, preset] of Object.entries(GALLERY_THEME_PRESETS)) {
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ const config: VitestUserConfig = {
|
|||||||
target: 'http://localhost:7101',
|
target: 'http://localhost:7101',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
|
'/uploads': {
|
||||||
|
target: 'http://localhost:7101',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user