Compare commits

...

11 Commits

Author SHA1 Message Date
Paul Nothaft ff9fb64e75 Merge pull request #273 from the-luap/release-please--branches--main
Build and Push Docker Images / build-backend (push) Waiting to run
Build and Push Docker Images / build-frontend (push) Waiting to run
Build and Push Docker Images / summary (push) Blocked by required conditions
chore(main): release 2.6.3
2026-04-07 20:40:47 +02:00
github-actions[bot] 9cbbe74051 chore(main): release 2.6.3 2026-04-07 18:40:34 +00:00
Paul Nothaft 2e1c71c1ab Merge pull request #272 from the-luap/docs/external-media-library-270
docs: add External Media Library section to deployment guide (#270)
2026-04-07 20:40:11 +02:00
Paul Nothaft f6ca713a6e docs: add External Media Library section to deployment guide (#270)
Add the missing "External Media Library" chapter to DEPLOYMENT_GUIDE.md
that was referenced in the TOC but never written. Covers configuration,
Docker volume mounting, folder structure, usage workflow, limitations,
and troubleshooting.

Closes #270
2026-04-07 19:52:02 +02:00
Paul Nothaft 197cd8e1e0 Merge pull request #268 from the-luap/security/pin-axios-main
security: pin axios to 1.14.0 — supply chain attack prevention
2026-04-05 18:41:54 +02:00
Paul Nothaft 681b440381 security: pin axios to 1.14.0 to prevent supply chain attack
Axios versions 1.14.1 and 0.30.4 were compromised on March 31, 2026
with a RAT dropper attributed to North Korean threat actor. Pin to
exact 1.14.0 (latest safe release) to prevent resolution to compromised
versions. See https://github.com/axios/axios/issues/10604
2026-04-05 18:41:45 +02:00
Paul Nothaft 3daeac9e53 Merge pull request #246 from the-luap/release-please--branches--main
Build and Push Docker Images / build-backend (push) Waiting to run
Build and Push Docker Images / build-frontend (push) Waiting to run
Build and Push Docker Images / summary (push) Blocked by required conditions
chore(main): release 2.6.2
2026-03-16 22:37:56 +01:00
github-actions[bot] 7febba2d9c chore(main): release 2.6.2 2026-03-16 21:37:35 +00:00
Paul Nothaft 0a3a53763c Merge pull request #245 from the-luap/fix/security-session-invalidation-main
fix(security): token invalidation on password change, session timeout enforcement
2026-03-16 22:37:16 +01:00
Paul Nothaft 85a60a2dc7 fix(security): invalidate tokens on password change, enforce session timeout, fix role update
- Set password_changed_at when changing password via adminAuth route so
  existing JWT tokens are rejected by the auth middleware check
- Enforce session timeout on first request with unseen tokens by checking
  token iat against configured timeout (prevents bypass after server restart)
- Convert camelCase roleId/isActive to snake_case role_id/is_active in
  frontend updateUser service (fixes silent role update failures)

Resolves GHSA-rqg3-47p5-vgwg
2026-03-16 22:36:52 +01:00
Paul Nothaft e74e73a3a0 Merge pull request #231 from the-luap/i18n/ru-missing-keys
i18n: add missing Russian translations for thumbnails and photo dimensions
2026-03-15 20:01:15 +01:00
10 changed files with 177 additions and 37 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "2.6.1"
".": "2.6.3"
}
+16
View File
@@ -5,6 +5,22 @@ 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).
## [2.6.3](https://github.com/the-luap/picpeak/compare/v2.6.2...v2.6.3) (2026-04-07)
### Documentation
* add External Media Library section to deployment guide ([#270](https://github.com/the-luap/picpeak/issues/270)) ([2e1c71c](https://github.com/the-luap/picpeak/commit/2e1c71c1ab073e488ac93e35337a2d3955dfef3d))
* add External Media Library section to deployment guide ([#270](https://github.com/the-luap/picpeak/issues/270)) ([f6ca713](https://github.com/the-luap/picpeak/commit/f6ca713a6edc8ba371db790daba05ecb85ea4872))
## [2.6.2](https://github.com/the-luap/picpeak/compare/v2.6.1...v2.6.2) (2026-03-16)
### Bug Fixes
* **security:** invalidate tokens on password change, enforce session timeout, fix role update ([85a60a2](https://github.com/the-luap/picpeak/commit/85a60a2dc7526aa6b673e2a04e9fdfba7de7117f))
* **security:** token invalidation on password change, session timeout enforcement ([0a3a537](https://github.com/the-luap/picpeak/commit/0a3a53763c9f3caef9fdceccf9fdbfdefe9bd8bf))
## [2.6.1](https://github.com/the-luap/picpeak/compare/v2.6.0...v2.6.1) (2026-03-11)
+90 -1
View File
@@ -11,7 +11,7 @@ This guide covers multiple deployment options for PicPeak, from simple local set
- [First Login](#-first-login)
- [Release Channels](#-release-channels)
- [Reverse Proxy Setup](#-reverse-proxy-setup)
- [External Media Library](#external-media-library)
- [External Media Library](#-external-media-library)
- [Maintenance](#-maintenance)
- [Troubleshooting](#-troubleshooting)
@@ -552,6 +552,95 @@ sudo certbot certonly --webroot -w /var/www/certbot -d your-domain.com
# Or use your reverse proxy's built-in ACME support
```
## 📂 External Media Library
The External Media Library allows events to reference photos stored directly on your host filesystem instead of uploading them through the admin UI. This is useful for photographers who already have organized photo libraries and want to share them without re-uploading.
### How It Works
- **Managed mode** (default): Photos are uploaded through the admin UI and stored inside PicPeak's storage directory.
- **Reference mode**: Photos remain on your host filesystem. PicPeak reads them directly and generates thumbnails on demand.
Each event can use either mode. Reference mode events point to a folder under the configured external media root.
### Configuration
Add the following to your `.env` file:
```bash
# Path where your photo library is stored on the host
EXTERNAL_MEDIA_ROOT=/path/to/your/photos
```
Then mount this path into the backend container in your `docker-compose.yml` or `docker-compose.production.yml`:
```yaml
services:
backend:
environment:
- EXTERNAL_MEDIA_ROOT=/external-media
volumes:
- /path/to/your/photos:/external-media:ro # read-only is recommended
```
> **Permissions**: Ensure the container user (`PUID`/`PGID`) has read access to the mounted directory. If thumbnails fail to generate, this is usually a permissions issue.
### Folder Structure
Organize your photos with subdirectories for each event. Within each event folder, use `individual/` and `collages/` subdirectories to classify photos:
```
/path/to/your/photos/
├── wedding-smith-2026/
│ ├── individual/
│ │ ├── IMG_0001.jpg
│ │ ├── IMG_0002.jpg
│ │ └── ...
│ └── collages/
│ ├── group-photo.jpg
│ └── ...
├── corporate-event/
│ ├── individual/
│ │ └── ...
│ └── collages/
│ └── ...
```
Supported file formats: `.jpg`, `.jpeg`, `.png`, `.webp`
### Usage
1. **Create an event** in the admin panel as usual (name, date, email, etc.).
2. **Switch source mode** to "Reference external folder" in the event details under Source Mode.
3. **Browse and select** the external folder using the folder picker that appears. Navigate to the event's directory.
4. **Import photos** by clicking "Import from External Folder" in the Photos tab. PicPeak will:
- Recursively scan the selected folder
- Classify photos by subfolder name (`individual/` or `collages/`)
- Deduplicate by filename (keeps the largest file if duplicates exist)
- Extract image dimensions for gallery layout
- Register the photos in the database
5. **Thumbnails** are generated on demand when a guest first views the gallery. There is no upfront processing delay.
### Limitations
- **Images only** — video files are not supported for external media.
- **Read-only** — PicPeak does not modify or delete files in the external media directory.
- **No automatic sync** — If you add new photos to the external folder, you need to re-import from the admin UI.
- **Backup caveat** — External media originals are excluded from PicPeak's built-in backup system. Only thumbnails and database records are backed up. You are responsible for backing up the source files separately.
### Troubleshooting
| Problem | Solution |
|---------|----------|
| Folder picker shows empty directory | Check that the volume is mounted correctly and `EXTERNAL_MEDIA_ROOT` matches the container path |
| "Permission denied" errors | Ensure `PUID`/`PGID` in `.env` match the owner of the external media files on the host |
| Thumbnails not generating | Verify the backend container can read the files: `docker exec picpeak-backend ls /external-media/your-folder/` |
| Import finds 0 photos | Only `.jpg`, `.jpeg`, `.png`, `.webp` files are supported. Check file extensions. |
## 🔧 Maintenance
### Viewing Logs
+27 -11
View File
@@ -1,12 +1,12 @@
{
"name": "picpeak-backend",
"version": "2.5.0",
"version": "2.6.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "picpeak-backend",
"version": "2.5.0",
"version": "2.6.2",
"dependencies": {
"@aws-sdk/client-s3": "^3.850.0",
"@aws-sdk/lib-storage": "^3.850.0",
@@ -14,7 +14,7 @@
"@ffmpeg-installer/ffmpeg": "^1.1.0",
"adm-zip": "^0.5.16",
"archiver": "^5.3.1",
"axios": "^1.12.2",
"axios": "1.14.0",
"bcrypt": "6.0.0",
"chokidar": "4.0.3",
"cookie-parser": "^1.4.7",
@@ -4071,14 +4071,14 @@
"license": "MIT"
},
"node_modules/axios": {
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz",
"integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
"proxy-from-env": "^2.1.0"
}
},
"node_modules/babel-jest": {
@@ -6664,6 +6664,19 @@
"cross-fetch": "4.0.0"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"optional": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -9579,10 +9592,13 @@
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/pstree.remy": {
"version": "1.1.8",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "picpeak-backend",
"version": "2.6.1",
"version": "2.6.3",
"description": "Backend for PicPeak event photo sharing platform",
"main": "server.js",
"scripts": {
@@ -19,7 +19,7 @@
"@ffmpeg-installer/ffmpeg": "^1.1.0",
"adm-zip": "^0.5.16",
"archiver": "^5.3.1",
"axios": "^1.12.2",
"axios": "1.14.0",
"bcrypt": "6.0.0",
"chokidar": "4.0.3",
"cookie-parser": "^1.4.7",
+16 -6
View File
@@ -85,18 +85,28 @@ async function sessionTimeoutMiddleware(req, res, next) {
const now = Date.now();
const lastActivity = sessions.get(token);
const timeout = await getSessionTimeout();
// If session exists, check if it's expired
if (lastActivity) {
// Existing session — check if idle too long
if (now - lastActivity > timeout) {
sessions.delete(token);
return res.status(401).json({
error: 'Session expired',
code: 'SESSION_TIMEOUT'
return res.status(401).json({
error: 'Session expired',
code: 'SESSION_TIMEOUT'
});
}
} else {
// First request with this token — check if token was issued longer ago than the timeout
// This prevents old/stolen tokens from bypassing session timeout after server restart
const tokenIssuedAt = (decoded.iat || 0) * 1000; // iat is in seconds
if (now - tokenIssuedAt > timeout) {
return res.status(401).json({
error: 'Session expired',
code: 'SESSION_TIMEOUT'
});
}
}
// Update last activity
sessions.set(token, now);
+4 -2
View File
@@ -122,13 +122,15 @@ router.post('/change-password', [
// Hash new password with more rounds
const newPasswordHash = await bcrypt.hash(newPassword, 12);
// Update password and clear must_change_password flag
// Update password, set password_changed_at to invalidate existing tokens, and clear must_change_password flag
const now = new Date();
await db('admin_users')
.where('id', userId)
.update({
password_hash: newPasswordHash,
password_changed_at: now,
must_change_password: false,
updated_at: new Date()
updated_at: now
});
// Log activity
+14 -11
View File
@@ -1,12 +1,12 @@
{
"name": "picpeak-frontend",
"version": "2.5.0",
"version": "2.6.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "picpeak-frontend",
"version": "2.5.0",
"version": "2.6.2",
"dependencies": {
"@tanstack/react-query": "^5.0.0",
"@tiptap/extension-character-count": "^2.26.1",
@@ -20,7 +20,7 @@
"@types/dompurify": "^3.0.5",
"@types/lodash": "^4.17.20",
"@types/react-google-recaptcha": "^2.1.9",
"axios": "^1.12.2",
"axios": "1.14.0",
"clsx": "^2.0.0",
"date-fns": "4.1.0",
"dompurify": "^3.2.6",
@@ -3020,14 +3020,14 @@
}
},
"node_modules/axios": {
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz",
"integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
"proxy-from-env": "^2.1.0"
}
},
"node_modules/balanced-match": {
@@ -5751,10 +5751,13 @@
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/punycode": {
"version": "2.3.1",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "picpeak-frontend",
"private": true,
"version": "2.6.1",
"version": "2.6.3",
"type": "module",
"scripts": {
"dev": "vite",
@@ -24,7 +24,7 @@
"@types/dompurify": "^3.0.5",
"@types/lodash": "^4.17.20",
"@types/react-google-recaptcha": "^2.1.9",
"axios": "^1.12.2",
"axios": "1.14.0",
"clsx": "^2.0.0",
"date-fns": "4.1.0",
"dompurify": "^3.2.6",
@@ -149,7 +149,11 @@ export const userManagementService = {
* Update an admin user
*/
async updateUser(id: number, data: UpdateUserData): Promise<AdminUser> {
const response = await api.put<UpdateUserResponse>(`/admin/users/${id}`, data);
// Convert camelCase to snake_case for backend API
const payload: Record<string, unknown> = {};
if (data.roleId !== undefined) payload.role_id = data.roleId;
if (data.isActive !== undefined) payload.is_active = data.isActive;
const response = await api.put<UpdateUserResponse>(`/admin/users/${id}`, payload);
return transformUser(response.data.user);
},