Compare commits

...

21 Commits

Author SHA1 Message Date
Gitea Actions Bot 0d77a3a0a8 chore: bump version to 1.0.124 (backend + frontend) 2025-09-14 14:25:37 +00:00
paul 0618b78725 feat(setup/docker): auto-set PUID/PGID from invoking user and chown bind-mount folders; create missing data/events dirs
Mirror to GitHub / mirror (push) Successful in 35s
Test and Lint / backend-test (push) Successful in 1m33s
Test and Lint / frontend-test (push) Successful in 2m12s
Version and Release / version-bump (push) Successful in 1m3s
Version and Release / trigger-drone (push) Successful in 3s
2025-09-14 16:20:08 +02:00
paul 0178e71c67 docs: add PUID/PGID note for Docker bind mounts to avoid permission issues
Mirror to GitHub / mirror (push) Successful in 40s
Test and Lint / backend-test (push) Successful in 1m31s
Test and Lint / frontend-test (push) Successful in 2m16s
2025-09-14 16:10:46 +02:00
Gitea Actions Bot aa9b3a0227 chore: bump version to 1.0.123 (backend + frontend) 2025-09-14 13:53:37 +00:00
paul 410a33fecf feat(docker): add PUID/PGID and user mapping to avoid bind mount permission issues; feat(setup): prompt for admin email interactively; docs: PUID/PGID in .env.example
Mirror to GitHub / mirror (push) Successful in 1m41s
Test and Lint / backend-test (push) Successful in 1m47s
Test and Lint / frontend-test (push) Successful in 2m10s
Version and Release / version-bump (push) Successful in 1m5s
Version and Release / trigger-drone (push) Successful in 3s
2025-09-14 15:46:35 +02:00
Gitea Actions Bot 05ebaaeedb chore: bump version to 1.0.122 (backend + frontend) 2025-09-09 18:54:09 +00:00
paul 84d0f63d36 feat(setup): remove --admin-password; print admin credentials from ADMIN_CREDENTIALS.txt; fix ADMIN_URL to avoid /admin/admin; update native service commands
Mirror to GitHub / mirror (push) Successful in 37s
Test and Lint / backend-test (push) Successful in 1m29s
Test and Lint / frontend-test (push) Successful in 2m10s
Version and Release / version-bump (push) Successful in 58s
Version and Release / trigger-drone (push) Successful in 3s
2025-09-09 20:48:49 +02:00
Gitea Actions Bot 6a4b549d9f chore: bump version to 1.0.121 (backend + frontend) 2025-09-09 18:45:28 +00:00
paul f3604b438b fix(native): remove obsolete workers service; restart only backend; add API request logging and preflight handler; keep static assets outside CORS
Mirror to GitHub / mirror (push) Successful in 33s
Test and Lint / backend-test (push) Successful in 1m31s
Test and Lint / frontend-test (push) Successful in 2m8s
Version and Release / version-bump (push) Successful in 56s
Version and Release / trigger-drone (push) Successful in 3s
2025-09-09 20:40:17 +02:00
Gitea Actions Bot 531831e84b chore: bump backend version to 1.0.120 2025-09-09 18:28:39 +00:00
paul 90bb21e38b fix(cors): scope CORS to /api only and avoid throwing on disallowed origins; prevents static asset 500s on native
Mirror to GitHub / mirror (push) Successful in 37s
Test and Lint / backend-test (push) Successful in 1m28s
Test and Lint / frontend-test (push) Successful in 2m9s
Version and Release / version-bump (push) Successful in 52s
Version and Release / trigger-drone (push) Successful in 3s
2025-09-09 20:23:25 +02:00
paul 2f1a137342 ci: make ghcr login non-fatal and gate pushes/scans on login success; build images regardless (supports transient GHCR outages)
Mirror to GitHub / mirror (push) Successful in 45s
Test and Lint / backend-test (push) Successful in 1m31s
Test and Lint / frontend-test (push) Successful in 2m8s
Version and Release / version-bump (push) Successful in 44s
Version and Release / trigger-drone (push) Has been skipped
2025-09-09 20:12:15 +02:00
paul adf576fbe1 fix(setup/update): detect native installs first (/opt/picpeak/app/backend or systemd unit); avoid false docker updates on root
Mirror to GitHub / mirror (push) Successful in 35s
Test and Lint / backend-test (push) Successful in 1m29s
Test and Lint / frontend-test (push) Has been cancelled
Version and Release / version-bump (push) Has been cancelled
Version and Release / trigger-drone (push) Has been cancelled
2025-09-09 20:08:51 +02:00
Gitea Actions Bot 4264026bbe chore: bump backend version to 1.0.119 2025-09-09 18:06:15 +00:00
paul 24b4a314a9 fix(native/http): disable CSP upgrade-insecure-requests and HSTS unless ENABLE_HSTS=true; prevents HTTPS upgrades on HTTP installs
Mirror to GitHub / mirror (push) Successful in 39s
Test and Lint / backend-test (push) Successful in 1m31s
Test and Lint / frontend-test (push) Successful in 2m12s
Version and Release / version-bump (push) Successful in 1m3s
Version and Release / trigger-drone (push) Successful in 3s
2025-09-09 20:00:48 +02:00
Gitea Actions Bot ba825823a0 chore: bump backend version to 1.0.118 2025-09-09 17:58:50 +00:00
paul fb16b7bbb8 feat(native): auto-serve SPA when dist exists (unless SERVE_FRONTEND=false); add clear logging; serve index.html for /admin
Mirror to GitHub / mirror (push) Successful in 42s
Test and Lint / backend-test (push) Successful in 1m34s
Test and Lint / frontend-test (push) Successful in 2m10s
Version and Release / version-bump (push) Successful in 59s
Version and Release / trigger-drone (push) Successful in 3s
2025-09-09 19:52:46 +02:00
Gitea Actions Bot 8404125ff0 chore: bump version to 1.0.117 (backend + frontend) 2025-09-09 17:10:38 +00:00
paul 61ad2d61c1 feat(native): serve built frontend from backend; build frontend during install/update; ensure env flags (SERVE_FRONTEND, FRONTEND_DIR)
Mirror to GitHub / mirror (push) Successful in 43s
Test and Lint / backend-test (push) Successful in 1m31s
Test and Lint / frontend-test (push) Successful in 2m10s
Version and Release / version-bump (push) Successful in 58s
Version and Release / trigger-drone (push) Successful in 3s
2025-09-09 19:04:39 +02:00
Gitea Actions Bot 9fd6b44487 chore: bump version to 1.0.116 (backend + frontend) 2025-09-09 15:47:41 +00:00
paul 9fe10bcce2 feat(native): build frontend and serve SPA from backend (SERVE_FRONTEND); fix Cannot GET /admin on native installs
Mirror to GitHub / mirror (push) Successful in 42s
Test and Lint / backend-test (push) Successful in 1m33s
Test and Lint / frontend-test (push) Successful in 2m7s
Version and Release / version-bump (push) Successful in 1m0s
Version and Release / trigger-drone (push) Successful in 3s
2025-09-09 17:41:41 +02:00
10 changed files with 245 additions and 63 deletions
+6
View File
@@ -53,6 +53,12 @@ VITE_API_URL=/api
# Timezone
TZ=UTC
# Runtime user mapping for Docker (optional)
# Set these to your host user's UID/GID to avoid permission issues on bind mounts.
# Run `id -u` and `id -g` on host to get values. Defaults to 1001.
PUID=1001
PGID=1001
# Analytics (Optional - Umami)
VITE_UMAMI_URL=
VITE_UMAMI_WEBSITE_ID=
+13 -7
View File
@@ -43,6 +43,8 @@ jobs:
- name: Log in to Container Registry
if: github.event_name != 'pull_request' || github.event.inputs.push == 'true'
id: login-ghcr
continue-on-error: true
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
@@ -73,7 +75,8 @@ jobs:
with:
context: ./backend
file: ./backend/Dockerfile
push: ${{ github.event_name != 'pull_request' || github.event.inputs.push == 'true' }}
# Always build; only push when registry login succeeded
push: ${{ (github.event_name != 'pull_request' || github.event.inputs.push == 'true') && steps.login-ghcr.outcome == 'success' }}
tags: ${{ steps.meta-backend.outputs.tags }}
labels: ${{ steps.meta-backend.outputs.labels }}
platforms: linux/amd64,linux/arm64
@@ -86,7 +89,7 @@ jobs:
VERSION=${{ steps.meta-backend.outputs.version }}
- name: Run Trivy vulnerability scanner
if: github.event_name != 'pull_request'
if: github.event_name != 'pull_request' && steps.login-ghcr.outcome == 'success'
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE_NAME }}:${{ steps.meta-backend.outputs.version }}
@@ -96,7 +99,7 @@ jobs:
timeout: '10m'
- name: Upload Trivy scan results to GitHub Security tab
if: github.event_name != 'pull_request'
if: github.event_name != 'pull_request' && steps.login-ghcr.outcome == 'success'
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-backend.sarif'
@@ -120,6 +123,8 @@ jobs:
- name: Log in to Container Registry
if: github.event_name != 'pull_request' || github.event.inputs.push == 'true'
id: login-ghcr
continue-on-error: true
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
@@ -150,7 +155,8 @@ jobs:
with:
context: ./frontend
file: ./frontend/Dockerfile
push: ${{ github.event_name != 'pull_request' || github.event.inputs.push == 'true' }}
# Always build; only push when registry login succeeded
push: ${{ (github.event_name != 'pull_request' || github.event.inputs.push == 'true') && steps.login-ghcr.outcome == 'success' }}
tags: ${{ steps.meta-frontend.outputs.tags }}
labels: ${{ steps.meta-frontend.outputs.labels }}
platforms: linux/amd64,linux/arm64
@@ -163,7 +169,7 @@ jobs:
VERSION=${{ steps.meta-frontend.outputs.version }}
- name: Run Trivy vulnerability scanner
if: github.event_name != 'pull_request'
if: github.event_name != 'pull_request' && steps.login-ghcr.outcome == 'success'
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE_NAME }}:${{ steps.meta-frontend.outputs.version }}
@@ -173,7 +179,7 @@ jobs:
timeout: '10m'
- name: Upload Trivy scan results to GitHub Security tab
if: github.event_name != 'pull_request'
if: github.event_name != 'pull_request' && steps.login-ghcr.outcome == 'success'
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-frontend.sarif'
@@ -220,4 +226,4 @@ jobs:
echo "- PR number (for pull requests)" >> $GITHUB_STEP_SUMMARY
echo "- Version tags (for releases)" >> $GITHUB_STEP_SUMMARY
echo "- Short SHA with branch prefix" >> $GITHUB_STEP_SUMMARY
echo "- \`latest\` (for main branch)" >> $GITHUB_STEP_SUMMARY
echo "- \`latest\` (for main branch)" >> $GITHUB_STEP_SUMMARY
+8
View File
@@ -72,6 +72,14 @@ docker-compose up -d
# Access at http://localhost:3005
```
Note on Docker file permissions (PUID/PGID)
- When using bind mounts (e.g., `./storage`, `./data`, `./logs`, `./events`), ensure the container user can write to these host folders. The backend runs as a nonroot user by default.
- Set `PUID` and `PGID` in your `.env` to match your host users UID/GID (run `id -u` and `id -g` on the host). Compose maps the container user to these values.
- Example in `.env`:
- `PUID=1000`
- `PGID=1000`
- Without this, creating events, uploads, thumbnails, or logs can fail with “Permission denied”.
## 📖 Documentation
- 📘 [**Deployment Guide**](DEPLOYMENT_GUIDE.md) - Detailed installation instructions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "picpeak-backend",
"version": "1.0.115",
"version": "1.0.124",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "picpeak-backend",
"version": "1.0.115",
"version": "1.0.124",
"dependencies": {
"@aws-sdk/client-s3": "^3.850.0",
"@aws-sdk/lib-storage": "^3.850.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "picpeak-backend",
"version": "1.0.115",
"version": "1.0.124",
"description": "Backend for PicPeak event photo sharing platform",
"main": "server.js",
"scripts": {
+72 -19
View File
@@ -12,6 +12,7 @@ logger.info('Server starting up', {
timestamp: new Date().toISOString()
});
const fs = require('fs');
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
@@ -42,25 +43,36 @@ const PORT = process.env.PORT || 3000;
app.set('trust proxy', 'loopback, linklocal, uniquelocal');
// Security middleware with custom CSP
// In native HTTP installs, do NOT force HTTPS for subresources.
const enableHsts = process.env.ENABLE_HSTS === 'true';
const cspDirectives = {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"], // Required for React
styleSrc: ["'self'", "'unsafe-inline'", "https:"], // Required for styled components
imgSrc: ["'self'", "data:", "https:", "blob:"], // Allow data URLs and external images
connectSrc: ["'self'"], // API connections
fontSrc: ["'self'", "https:", "data:"], // Web fonts
objectSrc: ["'none'"], // Disable plugins
mediaSrc: ["'self'"], // Audio/video
frameSrc: ["'none'"], // Disable iframes
};
// Only upgrade insecure requests when HSTS explicitly enabled (HTTPS deployment)
if (enableHsts) {
// In helmet, an empty array enables the directive
cspDirectives.upgradeInsecureRequests = [];
}
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"], // Required for React
styleSrc: ["'self'", "'unsafe-inline'", "https:"], // Required for styled components
imgSrc: ["'self'", "data:", "https:", "blob:"], // Allow data URLs and external images
connectSrc: ["'self'"], // API connections
fontSrc: ["'self'", "https:", "data:"], // Web fonts
objectSrc: ["'none'"], // Disable plugins
mediaSrc: ["'self'"], // Audio/video
frameSrc: ["'none'"], // Disable iframes
},
// Avoid helmet adding defaults like upgrade-insecure-requests when not desired
useDefaults: false,
directives: cspDirectives,
},
hsts: {
hsts: enableHsts ? {
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: true
},
} : false,
permittedCrossDomainPolicies: false,
referrerPolicy: { policy: "strict-origin-when-cross-origin" }
}));
@@ -72,14 +84,14 @@ app.use((req, res, next) => {
next();
});
// CORS configuration
// CORS configuration (apply only to API routes)
const corsOptions = {
origin: function (origin, callback) {
const allowedOrigins = [
process.env.FRONTEND_URL || 'http://localhost:3005',
process.env.ADMIN_URL || 'http://localhost:3005'
];
// In development, also allow localhost origins
if (process.env.NODE_ENV === 'development') {
allowedOrigins.push(
@@ -89,18 +101,22 @@ const corsOptions = {
'http://localhost:3000' // Direct backend access
);
}
// Allow requests with no origin (like mobile apps or curl)
// Allow requests with no origin (like curl) and allow-listed origins
if (!origin || allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
// Do not error globally; just omit CORS headers on disallowed origins
callback(null, false);
}
},
credentials: true
};
app.use(cors(corsOptions));
// Only attach CORS to API endpoints, not static assets
app.use('/api', cors(corsOptions));
// Handle preflight explicitly for API paths
app.options('/api/*', cors(corsOptions));
// Initialize rate limiters (they will be created dynamically)
let generalRateLimiter;
@@ -124,6 +140,22 @@ async function initializeRateLimiters() {
app.use(express.json({ limit: '100mb' }));
app.use(express.urlencoded({ extended: true, limit: '100mb' }));
// Request logging for API routes (with timestamps)
const apiRequestLogger = (req, res, next) => {
try {
const started = Date.now();
const ts = new Date().toISOString();
logger.info(`[${ts}] ${req.method} ${req.originalUrl}`);
res.on('finish', () => {
const ms = Date.now() - started;
const tsDone = new Date().toISOString();
logger.info(`[${tsDone}] ${req.method} ${req.originalUrl} -> ${res.statusCode} (${ms}ms)`);
});
} catch (_) {}
next();
};
app.use('/api', apiRequestLogger);
// Maintenance mode middleware - add after body parsing but before routes
app.use(maintenanceMiddleware);
@@ -219,6 +251,27 @@ app.use('/api/public', require('./src/routes/publicCMS'));
app.use('/api/images', require('./src/routes/protectedImages'));
app.use('/api/secure-images', secureImagesRoutes);
// Optional: Serve built frontend (native installs)
try {
const serveFrontendEnv = process.env.SERVE_FRONTEND; // 'true' | 'false' | undefined
const frontendDir = process.env.FRONTEND_DIR || path.join(__dirname, '../frontend/dist');
const indexPath = path.join(frontendDir, 'index.html');
// Auto-serve when dist exists unless explicitly disabled
const shouldServe = (serveFrontendEnv === 'true') || ((serveFrontendEnv === undefined || serveFrontendEnv === 'auto') && fs.existsSync(indexPath));
if (shouldServe) {
logger.info(`Serving frontend from ${frontendDir}`);
app.use(express.static(frontendDir));
// SPA fallback for non-API routes
app.get([ '/', '/admin', '/admin/*', '/gallery/*' ], (req, res) => {
res.sendFile(indexPath);
});
} else {
logger.info('Frontend static serving disabled or dist not found', { serveFrontendEnv, frontendDir });
}
} catch (e) {
logger.warn('Failed to enable frontend static serving', { error: e.message });
}
// Error handling middleware
app.use((err, req, res, next) => {
console.error('EXPRESS ERROR HANDLER:', err);
+6 -1
View File
@@ -29,6 +29,11 @@ services:
- ADMIN_URL=${ADMIN_URL:-http://localhost:3001}
- TZ=${TZ:-UTC}
- STORAGE_PATH=/app/storage
# Optional: run container as matching host user to avoid bind mount permission issues
- PUID=${PUID:-1001}
- PGID=${PGID:-1001}
# Use host-matching user ID/GID so bind-mounted folders are writable
user: "${PUID:-1001}:${PGID:-1001}"
volumes:
- ./events:/app/events
- ./data:/app/data
@@ -122,4 +127,4 @@ volumes:
networks:
picpeak-network:
driver: bridge
driver: bridge
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "picpeak-frontend",
"version": "1.0.115",
"version": "1.0.124",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "picpeak-frontend",
"version": "1.0.115",
"version": "1.0.124",
"dependencies": {
"@tanstack/react-query": "^5.0.0",
"@tiptap/extension-character-count": "^2.26.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "picpeak-frontend",
"private": true,
"version": "1.0.115",
"version": "1.0.124",
"type": "module",
"scripts": {
"dev": "vite",
+134 -30
View File
@@ -44,7 +44,6 @@ INSTALL_METHOD="" # docker or native
OS_TYPE=""
OS_VERSION=""
PACKAGE_MANAGER=""
ADMIN_PASSWORD=""
ADMIN_EMAIL="admin@example.com"
DOMAIN_NAME=""
SMTP_HOST=""
@@ -78,6 +77,21 @@ run_as_user() {
fi
}
# Prompt for admin email interactively (unless provided or unattended)
prompt_admin_email() {
if [[ -n "$ADMIN_EMAIL" ]]; then
return
fi
if [[ "$UNATTENDED" == "true" ]]; then
ADMIN_EMAIL="admin@example.com"
return
fi
echo
echo "Please enter the admin email address (used for the initial admin account):"
read -p "Admin email [admin@example.com]: " ADMIN_EMAIL
ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.com}
}
print_banner() {
echo -e "${PURPLE}"
echo "╔════════════════════════════════════════════════════════════════════════╗"
@@ -338,7 +352,7 @@ setup_docker_installation() {
fi
log_step "Creating application directory at $app_dir"
mkdir -p "$app_dir"/{storage/events/{active,archived},logs,backup,config}
mkdir -p "$app_dir"/{storage/events/{active,archived},logs,backup,config,data,events}
# Clone repository
log_step "Downloading PicPeak..."
@@ -349,11 +363,20 @@ setup_docker_installation() {
git clone "$REPO_URL" "$app_dir"
fi
# Determine host user for container mapping (PUID/PGID)
local host_uid host_gid
if [[ -n "${SUDO_USER:-}" ]]; then
host_uid=$(id -u "$SUDO_USER" 2>/dev/null || echo 1000)
host_gid=$(id -g "$SUDO_USER" 2>/dev/null || echo 1000)
else
host_uid=$(id -u 2>/dev/null || echo 1000)
host_gid=$(id -g 2>/dev/null || echo 1000)
fi
# Generate secrets
local jwt_secret=$(generate_jwt_secret)
local db_password=$(generate_password)
local redis_password=$(generate_password)
[[ -z "$ADMIN_PASSWORD" ]] && ADMIN_PASSWORD=$(generate_password)
# Create .env file
log_step "Creating configuration..."
@@ -368,7 +391,10 @@ JWT_SECRET=$jwt_secret
# Admin
ADMIN_EMAIL=$ADMIN_EMAIL
ADMIN_PASSWORD=$ADMIN_PASSWORD
# Runtime user mapping for Docker bind mounts
PUID=$host_uid
PGID=$host_gid
# Database
DB_HOST=postgres
@@ -395,7 +421,7 @@ SMTP_FROM=${SMTP_USER:-noreply@localhost}
# URLs
FRONTEND_URL=${DOMAIN_NAME:+https://$DOMAIN_NAME}
ADMIN_URL=${DOMAIN_NAME:+https://$DOMAIN_NAME/admin}
ADMIN_URL=${DOMAIN_NAME:+https://$DOMAIN_NAME}
# Features
ENABLE_FILE_WATCHER=true
@@ -403,6 +429,9 @@ ENABLE_EXPIRATION_CHECKER=true
ENABLE_EMAIL_SERVICE=true
DEFAULT_EXPIRY_DAYS=30
EOF
# Ensure bind mounts are writable by mapped user
chown -R "$host_uid":"$host_gid" "$app_dir"/storage "$app_dir"/logs "$app_dir"/backup "$app_dir"/data "$app_dir"/events 2>/dev/null || true
# Create docker-compose.yml if it doesn't exist
if [[ ! -f "$app_dir/docker-compose.yml" ]]; then
@@ -602,13 +631,22 @@ setup_native_installation() {
# Install backend production dependencies
cd "$NATIVE_APP_DIR/app/backend"
npm install --production
# Ensure SQLite data directory exists for native installs
mkdir -p "$NATIVE_APP_DIR/app/backend/data"
# Build frontend for native serving
log_step "Building frontend..."
if [[ -d "$NATIVE_APP_DIR/app/frontend" ]]; then
cd "$NATIVE_APP_DIR/app/frontend"
# Try ci (faster/clean) then fallback to install
run_as_user "npm ci --include=dev" || run_as_user "npm install"
run_as_user "npm run build"
else
log_warn "Frontend directory not found; admin UI will not be served by backend"
fi
# Generate secrets
local jwt_secret=$(generate_jwt_secret)
[[ -z "$ADMIN_PASSWORD" ]] && ADMIN_PASSWORD=$(generate_password)
# Create .env file
log_step "Creating configuration..."
@@ -623,7 +661,6 @@ JWT_SECRET=$jwt_secret
# Admin
ADMIN_USERNAME=admin
ADMIN_PASSWORD=$ADMIN_PASSWORD
ADMIN_EMAIL=$ADMIN_EMAIL
# Database (native uses SQLite by default)
@@ -643,7 +680,7 @@ SMTP_FROM=${SMTP_USER:-noreply@localhost}
# URLs
FRONTEND_URL=${DOMAIN_NAME:+https://$DOMAIN_NAME}
ADMIN_URL=${DOMAIN_NAME:+https://$DOMAIN_NAME/admin}
ADMIN_URL=${DOMAIN_NAME:+https://$DOMAIN_NAME}
# Features
ENABLE_FILE_WATCHER=true
@@ -654,6 +691,10 @@ DEFAULT_EXPIRY_DAYS=30
# Logging
LOG_DIR=$NATIVE_APP_DIR/logs
LOG_LEVEL=info
# Frontend serving (native installs)
SERVE_FRONTEND=true
FRONTEND_DIR=$NATIVE_APP_DIR/app/frontend/dist
EOF
# Set permissions
@@ -676,8 +717,15 @@ EOF
# Start services
log_step "Starting services..."
systemctl daemon-reload
systemctl enable picpeak-backend picpeak-workers
systemctl start picpeak-backend picpeak-workers
systemctl enable picpeak-backend
# Stop/remove legacy workers service if present
if systemctl list-unit-files | grep -q '^picpeak-workers.service'; then
systemctl disable picpeak-workers || true
systemctl stop picpeak-workers || true
rm -f /etc/systemd/system/picpeak-workers.service
systemctl daemon-reload
fi
systemctl start picpeak-backend
log_success "Native installation completed!"
}
@@ -885,8 +933,32 @@ print_success_message() {
echo
echo "🔐 Admin Credentials:"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo -e "Email: ${CYAN}$ADMIN_EMAIL${NC}"
echo -e "Password: ${CYAN}$ADMIN_PASSWORD${NC}"
# Read from ADMIN_CREDENTIALS.txt when available
local cred_file email_line pass_line admin_email_val admin_pass_val
if [[ "$INSTALL_METHOD" == "docker" ]]; then
cred_file="$app_dir/data/ADMIN_CREDENTIALS.txt"
else
cred_file="$NATIVE_APP_DIR/app/backend/data/ADMIN_CREDENTIALS.txt"
fi
if [[ -f "$cred_file" ]]; then
email_line=$(grep -m1 '^Email:' "$cred_file" || true)
pass_line=$(grep -m1 '^Password:' "$cred_file" || true)
admin_email_val=${email_line#Email: }
admin_pass_val=${pass_line#Password: }
if [[ -n "$admin_email_val" ]]; then
echo -e "Email: ${CYAN}$admin_email_val${NC}"
else
echo -e "Email: ${CYAN}$ADMIN_EMAIL${NC}"
fi
if [[ -n "$admin_pass_val" ]]; then
echo -e "Password: ${CYAN}$admin_pass_val${NC}"
else
echo -e "Password: ${YELLOW}(see $cred_file)${NC}"
fi
else
echo -e "Email: ${CYAN}$ADMIN_EMAIL${NC}"
echo -e "Password: ${YELLOW}(credentials file not found)${NC}"
fi
echo
echo -e "${YELLOW}⚠️ IMPORTANT: Change the admin password on first login!${NC}"
@@ -910,8 +982,8 @@ print_success_message() {
echo "🔧 Service Commands:"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "View logs: sudo journalctl -u picpeak-backend -f"
echo "Stop: sudo systemctl stop picpeak-backend picpeak-workers"
echo "Start: sudo systemctl start picpeak-backend picpeak-workers"
echo "Stop: sudo systemctl stop picpeak-backend"
echo "Start: sudo systemctl start picpeak-backend"
echo "Status: sudo systemctl status picpeak-backend"
fi
@@ -930,16 +1002,31 @@ print_success_message() {
update_installation() {
print_header "Updating PicPeak"
# Detect existing installation
if [[ -d "$DOCKER_APP_DIR" ]] || [[ -d "/home/${SUDO_USER:-}/picpeak" ]]; then
INSTALL_METHOD="docker"
update_docker_installation
elif [[ -d "$NATIVE_APP_DIR" ]]; then
# Prefer explicit native install detection first
native_detected=false
docker_detected=false
# Native detection: app/backend exists OR systemd unit present
if [[ -d "$NATIVE_APP_DIR/app/backend" ]]; then
native_detected=true
elif command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files | grep -q '^picpeak-backend.service'; then
native_detected=true
fi
# Docker detection: docker app dir or user home picpeak dir exists
if [[ -d "$DOCKER_APP_DIR" ]] || [[ -n "${SUDO_USER:-}" && -d "/home/${SUDO_USER}/picpeak" ]]; then
docker_detected=true
fi
if [[ "$native_detected" == true ]]; then
INSTALL_METHOD="native"
update_native_installation
elif [[ "$docker_detected" == true ]]; then
INSTALL_METHOD="docker"
update_docker_installation
else
die "No existing PicPeak installation found"
die "No existing PicPeak installation found (native dir $NATIVE_APP_DIR/app/backend or docker dir $DOCKER_APP_DIR not present)"
fi
}
@@ -972,7 +1059,10 @@ update_native_installation() {
log_step "Updating native installation..."
# Stop services
systemctl stop picpeak-backend picpeak-workers
systemctl stop picpeak-backend || true
if systemctl list-unit-files | grep -q '^picpeak-workers.service'; then
systemctl stop picpeak-workers || true
fi
# Backup current configuration
if [[ -f "$NATIVE_APP_DIR/app/backend/.env" ]]; then
@@ -995,9 +1085,25 @@ update_native_installation() {
# Run migrations
run_as_user "npm run migrate"
# Rebuild frontend (ensure admin UI for native installs)
if [[ -d "$NATIVE_APP_DIR/app/frontend" ]]; then
log_step "Rebuilding frontend..."
cd "$NATIVE_APP_DIR/app/frontend"
run_as_user "npm ci --include=dev" || run_as_user "npm install"
run_as_user "npm run build"
fi
# Ensure env has frontend serving flags
if ! grep -q '^SERVE_FRONTEND=' "$NATIVE_APP_DIR/app/backend/.env"; then
echo "SERVE_FRONTEND=true" >> "$NATIVE_APP_DIR/app/backend/.env"
fi
if ! grep -q '^FRONTEND_DIR=' "$NATIVE_APP_DIR/app/backend/.env"; then
echo "FRONTEND_DIR=$NATIVE_APP_DIR/app/frontend/dist" >> "$NATIVE_APP_DIR/app/backend/.env"
fi
# Restart services
systemctl start picpeak-backend picpeak-workers
systemctl restart picpeak-backend
log_success "Native installation updated successfully!"
}
@@ -1091,10 +1197,6 @@ parse_arguments() {
ADMIN_EMAIL="$2"
shift 2
;;
--admin-password)
ADMIN_PASSWORD="$2"
shift 2
;;
--smtp-host)
SMTP_HOST="$2"
shift 2
@@ -1152,7 +1254,6 @@ Options:
--unattended Run without prompts
--domain DOMAIN Set domain name for HTTPS
--email EMAIL Admin email address
--admin-password Admin password (auto-generated if not set)
--smtp-host HOST SMTP server hostname
--smtp-port PORT SMTP server port
--smtp-user USER SMTP username
@@ -1175,7 +1276,7 @@ Examples:
# Fully automated Docker setup
sudo $0 --docker --unattended --domain photos.example.com \\
--email admin@example.com --admin-password SecurePass123 \\
--email admin@example.com \\
--smtp-host smtp.gmail.com --smtp-port 587 \\
--smtp-user user@gmail.com --smtp-pass app-password \\
--enable-ssl
@@ -1215,6 +1316,9 @@ main() {
# Check system requirements
check_system_requirements
# Prompt for admin email (design choice: always ask unless provided)
prompt_admin_email
# Configure email (optional)
configure_email