Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 531831e84b | |||
| 90bb21e38b | |||
| 2f1a137342 | |||
| adf576fbe1 | |||
| 4264026bbe | |||
| 24b4a314a9 | |||
| ba825823a0 | |||
| fb16b7bbb8 | |||
| 8404125ff0 | |||
| 61ad2d61c1 |
@@ -43,6 +43,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Log in to Container Registry
|
- name: Log in to Container Registry
|
||||||
if: github.event_name != 'pull_request' || github.event.inputs.push == 'true'
|
if: github.event_name != 'pull_request' || github.event.inputs.push == 'true'
|
||||||
|
id: login-ghcr
|
||||||
|
continue-on-error: true
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
@@ -73,7 +75,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: ./backend
|
context: ./backend
|
||||||
file: ./backend/Dockerfile
|
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 }}
|
tags: ${{ steps.meta-backend.outputs.tags }}
|
||||||
labels: ${{ steps.meta-backend.outputs.labels }}
|
labels: ${{ steps.meta-backend.outputs.labels }}
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
@@ -86,7 +89,7 @@ jobs:
|
|||||||
VERSION=${{ steps.meta-backend.outputs.version }}
|
VERSION=${{ steps.meta-backend.outputs.version }}
|
||||||
|
|
||||||
- name: Run Trivy vulnerability scanner
|
- 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
|
uses: aquasecurity/trivy-action@master
|
||||||
with:
|
with:
|
||||||
image-ref: ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE_NAME }}:${{ steps.meta-backend.outputs.version }}
|
image-ref: ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE_NAME }}:${{ steps.meta-backend.outputs.version }}
|
||||||
@@ -96,7 +99,7 @@ jobs:
|
|||||||
timeout: '10m'
|
timeout: '10m'
|
||||||
|
|
||||||
- name: Upload Trivy scan results to GitHub Security tab
|
- 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
|
uses: github/codeql-action/upload-sarif@v3
|
||||||
with:
|
with:
|
||||||
sarif_file: 'trivy-backend.sarif'
|
sarif_file: 'trivy-backend.sarif'
|
||||||
@@ -120,6 +123,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Log in to Container Registry
|
- name: Log in to Container Registry
|
||||||
if: github.event_name != 'pull_request' || github.event.inputs.push == 'true'
|
if: github.event_name != 'pull_request' || github.event.inputs.push == 'true'
|
||||||
|
id: login-ghcr
|
||||||
|
continue-on-error: true
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
@@ -150,7 +155,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: ./frontend
|
context: ./frontend
|
||||||
file: ./frontend/Dockerfile
|
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 }}
|
tags: ${{ steps.meta-frontend.outputs.tags }}
|
||||||
labels: ${{ steps.meta-frontend.outputs.labels }}
|
labels: ${{ steps.meta-frontend.outputs.labels }}
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
@@ -163,7 +169,7 @@ jobs:
|
|||||||
VERSION=${{ steps.meta-frontend.outputs.version }}
|
VERSION=${{ steps.meta-frontend.outputs.version }}
|
||||||
|
|
||||||
- name: Run Trivy vulnerability scanner
|
- 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
|
uses: aquasecurity/trivy-action@master
|
||||||
with:
|
with:
|
||||||
image-ref: ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE_NAME }}:${{ steps.meta-frontend.outputs.version }}
|
image-ref: ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE_NAME }}:${{ steps.meta-frontend.outputs.version }}
|
||||||
@@ -173,7 +179,7 @@ jobs:
|
|||||||
timeout: '10m'
|
timeout: '10m'
|
||||||
|
|
||||||
- name: Upload Trivy scan results to GitHub Security tab
|
- 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
|
uses: github/codeql-action/upload-sarif@v3
|
||||||
with:
|
with:
|
||||||
sarif_file: 'trivy-frontend.sarif'
|
sarif_file: 'trivy-frontend.sarif'
|
||||||
@@ -220,4 +226,4 @@ jobs:
|
|||||||
echo "- PR number (for pull requests)" >> $GITHUB_STEP_SUMMARY
|
echo "- PR number (for pull requests)" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- Version tags (for releases)" >> $GITHUB_STEP_SUMMARY
|
echo "- Version tags (for releases)" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- Short SHA with branch prefix" >> $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
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.116",
|
"version": "1.0.120",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.116",
|
"version": "1.0.120",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.850.0",
|
"@aws-sdk/client-s3": "^3.850.0",
|
||||||
"@aws-sdk/lib-storage": "^3.850.0",
|
"@aws-sdk/lib-storage": "^3.850.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.116",
|
"version": "1.0.120",
|
||||||
"description": "Backend for PicPeak event photo sharing platform",
|
"description": "Backend for PicPeak event photo sharing platform",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+40
-22
@@ -43,25 +43,36 @@ const PORT = process.env.PORT || 3000;
|
|||||||
app.set('trust proxy', 'loopback, linklocal, uniquelocal');
|
app.set('trust proxy', 'loopback, linklocal, uniquelocal');
|
||||||
|
|
||||||
// Security middleware with custom CSP
|
// 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({
|
app.use(helmet({
|
||||||
contentSecurityPolicy: {
|
contentSecurityPolicy: {
|
||||||
directives: {
|
// Avoid helmet adding defaults like upgrade-insecure-requests when not desired
|
||||||
defaultSrc: ["'self'"],
|
useDefaults: false,
|
||||||
scriptSrc: ["'self'", "'unsafe-inline'"], // Required for React
|
directives: cspDirectives,
|
||||||
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
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
hsts: {
|
hsts: enableHsts ? {
|
||||||
maxAge: 31536000, // 1 year
|
maxAge: 31536000, // 1 year
|
||||||
includeSubDomains: true,
|
includeSubDomains: true,
|
||||||
preload: true
|
preload: true
|
||||||
},
|
} : false,
|
||||||
permittedCrossDomainPolicies: false,
|
permittedCrossDomainPolicies: false,
|
||||||
referrerPolicy: { policy: "strict-origin-when-cross-origin" }
|
referrerPolicy: { policy: "strict-origin-when-cross-origin" }
|
||||||
}));
|
}));
|
||||||
@@ -73,14 +84,14 @@ app.use((req, res, next) => {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// CORS configuration
|
// CORS configuration (apply only to API routes)
|
||||||
const corsOptions = {
|
const corsOptions = {
|
||||||
origin: function (origin, callback) {
|
origin: function (origin, callback) {
|
||||||
const allowedOrigins = [
|
const allowedOrigins = [
|
||||||
process.env.FRONTEND_URL || 'http://localhost:3005',
|
process.env.FRONTEND_URL || 'http://localhost:3005',
|
||||||
process.env.ADMIN_URL || 'http://localhost:3005'
|
process.env.ADMIN_URL || 'http://localhost:3005'
|
||||||
];
|
];
|
||||||
|
|
||||||
// In development, also allow localhost origins
|
// In development, also allow localhost origins
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
allowedOrigins.push(
|
allowedOrigins.push(
|
||||||
@@ -90,18 +101,20 @@ const corsOptions = {
|
|||||||
'http://localhost:3000' // Direct backend access
|
'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) {
|
if (!origin || allowedOrigins.indexOf(origin) !== -1) {
|
||||||
callback(null, true);
|
callback(null, true);
|
||||||
} else {
|
} else {
|
||||||
callback(new Error('Not allowed by CORS'));
|
// Do not error globally; just omit CORS headers on disallowed origins
|
||||||
|
callback(null, false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
credentials: true
|
credentials: true
|
||||||
};
|
};
|
||||||
|
|
||||||
app.use(cors(corsOptions));
|
// Only attach CORS to API endpoints, not static assets
|
||||||
|
app.use('/api', cors(corsOptions));
|
||||||
|
|
||||||
// Initialize rate limiters (they will be created dynamically)
|
// Initialize rate limiters (they will be created dynamically)
|
||||||
let generalRateLimiter;
|
let generalRateLimiter;
|
||||||
@@ -222,15 +235,20 @@ app.use('/api/secure-images', secureImagesRoutes);
|
|||||||
|
|
||||||
// Optional: Serve built frontend (native installs)
|
// Optional: Serve built frontend (native installs)
|
||||||
try {
|
try {
|
||||||
const serveFrontend = process.env.SERVE_FRONTEND === 'true';
|
const serveFrontendEnv = process.env.SERVE_FRONTEND; // 'true' | 'false' | undefined
|
||||||
const frontendDir = process.env.FRONTEND_DIR || path.join(__dirname, '../frontend/dist');
|
const frontendDir = process.env.FRONTEND_DIR || path.join(__dirname, '../frontend/dist');
|
||||||
if (serveFrontend && fs.existsSync(frontendDir)) {
|
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}`);
|
logger.info(`Serving frontend from ${frontendDir}`);
|
||||||
app.use(express.static(frontendDir));
|
app.use(express.static(frontendDir));
|
||||||
// SPA fallback for non-API routes
|
// SPA fallback for non-API routes
|
||||||
app.get([ '/', '/admin', '/admin/*', '/gallery/*' ], (req, res) => {
|
app.get([ '/', '/admin', '/admin/*', '/gallery/*' ], (req, res) => {
|
||||||
res.sendFile(path.join(frontendDir, 'index.html'));
|
res.sendFile(indexPath);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
logger.info('Frontend static serving disabled or dist not found', { serveFrontendEnv, frontendDir });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn('Failed to enable frontend static serving', { error: e.message });
|
logger.warn('Failed to enable frontend static serving', { error: e.message });
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"version": "1.0.116",
|
"version": "1.0.117",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"version": "1.0.116",
|
"version": "1.0.117",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.0.0",
|
"@tanstack/react-query": "^5.0.0",
|
||||||
"@tiptap/extension-character-count": "^2.26.1",
|
"@tiptap/extension-character-count": "^2.26.1",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.116",
|
"version": "1.0.117",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
+39
-8
@@ -944,16 +944,31 @@ print_success_message() {
|
|||||||
|
|
||||||
update_installation() {
|
update_installation() {
|
||||||
print_header "Updating PicPeak"
|
print_header "Updating PicPeak"
|
||||||
|
|
||||||
# Detect existing installation
|
# Prefer explicit native install detection first
|
||||||
if [[ -d "$DOCKER_APP_DIR" ]] || [[ -d "/home/${SUDO_USER:-}/picpeak" ]]; then
|
native_detected=false
|
||||||
INSTALL_METHOD="docker"
|
docker_detected=false
|
||||||
update_docker_installation
|
|
||||||
elif [[ -d "$NATIVE_APP_DIR" ]]; then
|
# 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"
|
INSTALL_METHOD="native"
|
||||||
update_native_installation
|
update_native_installation
|
||||||
|
elif [[ "$docker_detected" == true ]]; then
|
||||||
|
INSTALL_METHOD="docker"
|
||||||
|
update_docker_installation
|
||||||
else
|
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
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1009,9 +1024,25 @@ update_native_installation() {
|
|||||||
|
|
||||||
# Run migrations
|
# Run migrations
|
||||||
run_as_user "npm run migrate"
|
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
|
# Restart services
|
||||||
systemctl start picpeak-backend picpeak-workers
|
systemctl restart picpeak-backend picpeak-workers
|
||||||
|
|
||||||
log_success "Native installation updated successfully!"
|
log_success "Native installation updated successfully!"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user