1498 lines
51 KiB
Bash
1498 lines
51 KiB
Bash
#!/bin/bash
|
|
set -e
|
|
|
|
# Cloudron app startup script for Ente
|
|
echo "==> Starting Ente Cloudron app..."
|
|
|
|
# We need to be careful with file permissions, as /app/data is the only writable location
|
|
mkdir -p /app/data/patched
|
|
chmod -R 777 /app/data/patched
|
|
echo "==> Created and set full permissions (777) on /app/data/patched directory"
|
|
|
|
echo "==> NOTE: Running in Cloudron environment with limited write access"
|
|
echo "==> Writable directories: /app/data, /tmp, /run"
|
|
|
|
# Configure important paths
|
|
MUSEUM_DIR="/app/code/server"
|
|
CONFIG_DIR="/app/data/config"
|
|
LOGS_DIR="/app/data/logs"
|
|
WEB_DIR="/app/web"
|
|
CADDY_DATA_DIR="/app/data/caddy"
|
|
|
|
# Create necessary directories
|
|
mkdir -p "$CONFIG_DIR" "$LOGS_DIR" "$CADDY_DATA_DIR"
|
|
|
|
# Determine the endpoint configuration
|
|
CLOUDRON_APP_FQDN="${CLOUDRON_APP_DOMAIN}"
|
|
if [ -n "${CLOUDRON_APP_ORIGIN}" ]; then
|
|
CLOUDRON_APP_FQDN="${CLOUDRON_APP_DOMAIN}"
|
|
else
|
|
# If origin not set, use the app domain
|
|
CLOUDRON_APP_ORIGIN="https://${CLOUDRON_APP_DOMAIN}"
|
|
fi
|
|
|
|
API_ENDPOINT="/api"
|
|
CADDY_PORT="3080"
|
|
API_PORT="8080"
|
|
PUBLIC_ALBUMS_PORT="8081"
|
|
|
|
echo "==> Using server directory: ${MUSEUM_DIR}"
|
|
|
|
# Check if we have S3 configuration
|
|
if [ -f "${CONFIG_DIR}/s3.env" ]; then
|
|
echo "==> Using existing S3 configuration"
|
|
source "${CONFIG_DIR}/s3.env"
|
|
echo "==> S3 Configuration:"
|
|
echo "Endpoint: ${S3_ENDPOINT}"
|
|
echo "Region: ${S3_REGION}"
|
|
echo "Bucket: ${S3_BUCKET}"
|
|
else
|
|
echo "==> Creating default S3 configuration file"
|
|
# Create empty S3 env file for later configuration
|
|
cat > "${CONFIG_DIR}/s3.env" << EOF
|
|
# S3 Configuration for Ente
|
|
# Uncomment and fill in the following values:
|
|
# S3_ENDPOINT=https://s3.example.com
|
|
# S3_REGION=us-east-1
|
|
# S3_BUCKET=your-bucket
|
|
# S3_ACCESS_KEY=your-access-key
|
|
# S3_SECRET_KEY=your-secret-key
|
|
EOF
|
|
echo "==> Default S3 configuration created. Please edit ${CONFIG_DIR}/s3.env with your S3 credentials."
|
|
fi
|
|
|
|
# Check if we have a museum.yaml configuration file
|
|
if [ -f "${CONFIG_DIR}/museum.yaml" ]; then
|
|
echo "==> Using existing museum.yaml configuration"
|
|
else
|
|
echo "==> Creating default museum.yaml configuration"
|
|
|
|
# Create museum.yaml with S3 configuration
|
|
cat > "${CONFIG_DIR}/museum.yaml" << EOF
|
|
server:
|
|
host: 0.0.0.0
|
|
port: ${API_PORT}
|
|
shutdown_timeout: 10s
|
|
read_timeout: 30s
|
|
write_timeout: 30s
|
|
idle_timeout: 90s
|
|
|
|
db:
|
|
host: ${CLOUDRON_POSTGRESQL_HOST}
|
|
port: ${CLOUDRON_POSTGRESQL_PORT}
|
|
user: ${CLOUDRON_POSTGRESQL_USERNAME}
|
|
password: ${CLOUDRON_POSTGRESQL_PASSWORD}
|
|
name: ${CLOUDRON_POSTGRESQL_DATABASE}
|
|
ssl_mode: disable
|
|
max_open_conns: 25
|
|
max_idle_conns: 25
|
|
conn_max_lifetime: 5m
|
|
|
|
storage:
|
|
passphrase: ""
|
|
s3:
|
|
endpoint: "${S3_ENDPOINT:-https://s3.example.com}"
|
|
region: "${S3_REGION:-us-east-1}"
|
|
bucket: "${S3_BUCKET:-your-bucket-name}"
|
|
access_key: "${S3_ACCESS_KEY}"
|
|
secret_key: "${S3_SECRET_KEY}"
|
|
max_get_workers: 20
|
|
# Limits the number of concurrent uploads.
|
|
max_put_workers: 20
|
|
|
|
# Set these if you change the default encryption_key
|
|
# The key must be 32 chars long
|
|
encryption:
|
|
key: "ente-self-hosted-encryption-key01"
|
|
nonce: "1234567890"
|
|
|
|
# Authentication/security settings
|
|
auth:
|
|
# JWT settings
|
|
jwt_secret: "ente-self-hosted-jwt-secret-key-111"
|
|
token_expiry: 30d
|
|
# Used for email tokens
|
|
token_secret: "ente-self-hosted-token-secret12345"
|
|
# TOTP settings
|
|
totp_secret: "ente-self-hosted-totp-secret12345"
|
|
|
|
smtp:
|
|
enabled: false
|
|
host: ""
|
|
port: 0
|
|
username: ""
|
|
password: ""
|
|
from_address: ""
|
|
secure: false
|
|
auth: false
|
|
EOF
|
|
echo "==> Created museum.yaml with default configuration"
|
|
fi
|
|
|
|
# Create a reduced museum.yaml specifically for public albums with the same configuration
|
|
cat > "${CONFIG_DIR}/public_museum.yaml" << EOF
|
|
server:
|
|
host: 0.0.0.0
|
|
port: ${PUBLIC_ALBUMS_PORT}
|
|
shutdown_timeout: 10s
|
|
read_timeout: 30s
|
|
write_timeout: 30s
|
|
idle_timeout: 90s
|
|
|
|
db:
|
|
host: ${CLOUDRON_POSTGRESQL_HOST}
|
|
port: ${CLOUDRON_POSTGRESQL_PORT}
|
|
user: ${CLOUDRON_POSTGRESQL_USERNAME}
|
|
password: ${CLOUDRON_POSTGRESQL_PASSWORD}
|
|
name: ${CLOUDRON_POSTGRESQL_DATABASE}
|
|
ssl_mode: disable
|
|
max_open_conns: 25
|
|
max_idle_conns: 25
|
|
conn_max_lifetime: 5m
|
|
|
|
storage:
|
|
passphrase: ""
|
|
s3:
|
|
endpoint: "${S3_ENDPOINT:-https://s3.example.com}"
|
|
region: "${S3_REGION:-us-east-1}"
|
|
bucket: "${S3_BUCKET:-your-bucket-name}"
|
|
access_key: "${S3_ACCESS_KEY}"
|
|
secret_key: "${S3_SECRET_KEY}"
|
|
max_get_workers: 20
|
|
max_put_workers: 20
|
|
|
|
encryption:
|
|
key: "ente-self-hosted-encryption-key01"
|
|
nonce: "1234567890"
|
|
|
|
auth:
|
|
jwt_secret: "ente-self-hosted-jwt-secret-key-111"
|
|
token_expiry: 30d
|
|
token_secret: "ente-self-hosted-token-secret12345"
|
|
totp_secret: "ente-self-hosted-totp-secret12345"
|
|
EOF
|
|
|
|
# Environment variable setup - based on the docker-compose reference
|
|
export ENTE_CONFIG_FILE="${CONFIG_DIR}/museum.yaml"
|
|
export ENTE_API_ENDPOINT="${API_ENDPOINT}"
|
|
export ENTE_PORT="${API_PORT}"
|
|
|
|
# Set up PostgreSQL connection variables - referenced in docker-compose
|
|
export ENTE_DB_HOST="${CLOUDRON_POSTGRESQL_HOST}"
|
|
export ENTE_DB_PORT="${CLOUDRON_POSTGRESQL_PORT}"
|
|
export ENTE_DB_NAME="${CLOUDRON_POSTGRESQL_DATABASE}"
|
|
export ENTE_DB_USER="${CLOUDRON_POSTGRESQL_USERNAME}"
|
|
export ENTE_DB_PASSWORD="${CLOUDRON_POSTGRESQL_PASSWORD}"
|
|
|
|
# Also set standard PostgreSQL variables as backup
|
|
export PGHOST="${CLOUDRON_POSTGRESQL_HOST}"
|
|
export PGPORT="${CLOUDRON_POSTGRESQL_PORT}"
|
|
export PGUSER="${CLOUDRON_POSTGRESQL_USERNAME}"
|
|
export PGPASSWORD="${CLOUDRON_POSTGRESQL_PASSWORD}"
|
|
export PGDATABASE="${CLOUDRON_POSTGRESQL_DATABASE}"
|
|
|
|
# Define trap to ensure all processes are killed on exit
|
|
SERVER_PID=0
|
|
PUBLIC_SERVER_PID=0
|
|
CADDY_PID=0
|
|
TAIL_PID=0
|
|
|
|
trap 'kill -TERM $TAIL_PID; kill -TERM $SERVER_PID; kill -TERM $PUBLIC_SERVER_PID; kill -TERM $CADDY_PID; exit' TERM INT
|
|
|
|
# Start the Museum Server
|
|
echo "==> Testing PostgreSQL connectivity"
|
|
if pg_isready -q; then
|
|
echo "==> PostgreSQL is ready"
|
|
else
|
|
echo "==> WARNING: PostgreSQL is not ready, but proceeding anyway"
|
|
fi
|
|
|
|
# Check if the Museum server exists at the expected location
|
|
if [ -f "${MUSEUM_DIR}/museum" ] && [ -x "${MUSEUM_DIR}/museum" ]; then
|
|
echo "==> Found Museum server binary at ${MUSEUM_DIR}/museum"
|
|
|
|
# Start the main API server
|
|
cd "${MUSEUM_DIR}"
|
|
echo "==> Starting Museum server with config: ${ENTE_CONFIG_FILE}"
|
|
nohup ./museum server > "${LOGS_DIR}/museum.log" 2>&1 &
|
|
SERVER_PID=$!
|
|
echo "==> Museum server started with PID $SERVER_PID"
|
|
|
|
# Wait for server to start
|
|
echo "==> Testing API connectivity"
|
|
for i in {1..5}; do
|
|
if curl -s --max-time 2 --fail http://0.0.0.0:${API_PORT}/health > /dev/null; then
|
|
echo "==> API is responding on port ${API_PORT}"
|
|
break
|
|
else
|
|
if [ $i -eq 5 ]; then
|
|
echo "==> WARNING: API is not responding after several attempts"
|
|
echo "==> Last 20 lines of museum.log:"
|
|
tail -20 "${LOGS_DIR}/museum.log" || echo "==> No museum.log available"
|
|
else
|
|
echo "==> Attempt $i: Waiting for API to start... (2 seconds)"
|
|
sleep 2
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# Start the Public Albums Museum server
|
|
echo "==> Starting Public Albums Museum server"
|
|
export ENTE_CONFIG_FILE="${CONFIG_DIR}/public_museum.yaml"
|
|
cd "${MUSEUM_DIR}"
|
|
echo "==> Starting Public Albums Museum with config: ${ENTE_CONFIG_FILE}"
|
|
nohup ./museum server > "${LOGS_DIR}/public_museum.log" 2>&1 &
|
|
PUBLIC_SERVER_PID=$!
|
|
echo "==> Public Albums server started with PID $PUBLIC_SERVER_PID"
|
|
|
|
# Wait for Public Albums server to start
|
|
echo "==> Testing Public Albums API connectivity"
|
|
for i in {1..5}; do
|
|
if curl -s --max-time 2 --fail http://0.0.0.0:${PUBLIC_ALBUMS_PORT}/health > /dev/null; then
|
|
echo "==> Public Albums API is responding on port ${PUBLIC_ALBUMS_PORT}"
|
|
break
|
|
else
|
|
if [ $i -eq 5 ]; then
|
|
echo "==> WARNING: Public Albums API is not responding after several attempts"
|
|
echo "==> Last 20 lines of public_museum.log:"
|
|
tail -20 "${LOGS_DIR}/public_museum.log" || echo "==> No public_museum.log available"
|
|
else
|
|
echo "==> Attempt $i: Waiting for Public Albums API to start... (2 seconds)"
|
|
sleep 2
|
|
fi
|
|
fi
|
|
done
|
|
else
|
|
echo "==> ERROR: Museum server not found at ${MUSEUM_DIR}/museum"
|
|
echo "==> Starting a mock server with Node.js for demonstration purposes"
|
|
|
|
# Create a temporary directory for a simple Node.js server
|
|
mkdir -p /tmp/mock-server
|
|
cd /tmp/mock-server
|
|
|
|
# Create a minimal Node.js server file
|
|
cat > server.js << 'ENDOFCODE'
|
|
const http = require('http');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// Ensure log directory exists
|
|
const logDir = '/app/data/logs';
|
|
fs.mkdirSync(logDir, { recursive: true });
|
|
const logFile = path.join(logDir, 'api_requests.log');
|
|
|
|
// Open log file
|
|
fs.writeFileSync(logFile, `API Server started at ${new Date().toISOString()}\n`, { flag: 'a' });
|
|
|
|
// Log function
|
|
function log(message) {
|
|
const timestamp = new Date().toISOString();
|
|
const logMessage = `${timestamp} - ${message}\n`;
|
|
console.log(message);
|
|
fs.writeFileSync(logFile, logMessage, { flag: 'a' });
|
|
}
|
|
|
|
// Generate random 6-digit code
|
|
function generateCode() {
|
|
return Math.floor(100000 + Math.random() * 900000).toString();
|
|
}
|
|
|
|
// Generate unique numeric ID (for user ID)
|
|
function generateNumericId() {
|
|
return Math.floor(10000 + Math.random() * 90000);
|
|
}
|
|
|
|
// Store codes for verification (simple in-memory cache)
|
|
const verificationCodes = {};
|
|
|
|
// Create HTTP server
|
|
const server = http.createServer((req, res) => {
|
|
const url = req.url;
|
|
const method = req.method;
|
|
|
|
log(`Received ${method} request for ${url}`);
|
|
|
|
// Set CORS headers
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT');
|
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
|
|
// Handle preflight requests
|
|
if (method === 'OPTIONS') {
|
|
res.statusCode = 200;
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
// Handle requests based on URL path
|
|
if (url === '/health') {
|
|
// Health check endpoint
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({
|
|
status: "ok",
|
|
time: new Date().toISOString()
|
|
}));
|
|
}
|
|
else if (url.startsWith('/users/srp')) {
|
|
// SRP endpoints - just return success for all SRP requests
|
|
let body = '';
|
|
|
|
req.on('data', chunk => {
|
|
body += chunk.toString();
|
|
});
|
|
|
|
req.on('end', () => {
|
|
log(`SRP request received: ${url} with body: ${body}`);
|
|
|
|
// Return a standard response for any SRP request
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({
|
|
status: "ok",
|
|
id: 12345,
|
|
token: "mock-token-12345",
|
|
key: {
|
|
pubKey: "mockPubKey123456",
|
|
encPubKey: "mockEncPubKey123456",
|
|
kty: "RSA",
|
|
kid: "kid-123456",
|
|
alg: "RS256",
|
|
verifyKey: "mockVerifyKey123456"
|
|
}
|
|
}));
|
|
});
|
|
}
|
|
else if (url === '/users/ott') {
|
|
// OTT verification code endpoint
|
|
let body = '';
|
|
|
|
req.on('data', chunk => {
|
|
body += chunk.toString();
|
|
});
|
|
|
|
req.on('end', () => {
|
|
let email = 'user@example.com';
|
|
|
|
// Try to parse email from request if possible
|
|
try {
|
|
const data = JSON.parse(body);
|
|
if (data.email) {
|
|
email = data.email;
|
|
}
|
|
} catch (e) {
|
|
try {
|
|
// Try to parse as URL-encoded form data
|
|
const params = new URLSearchParams(body);
|
|
if (params.has('email')) {
|
|
email = params.get('email');
|
|
}
|
|
} catch (e2) {
|
|
// Ignore parsing errors
|
|
}
|
|
}
|
|
|
|
// Generate verification code
|
|
const code = generateCode();
|
|
const userId = generateNumericId();
|
|
|
|
// Store the code for this email
|
|
verificationCodes[email] = code;
|
|
|
|
// Log the code prominently
|
|
const codeMessage = `⚠️ VERIFICATION CODE FOR ${email}: ${code}`;
|
|
log(codeMessage);
|
|
console.log('\n' + codeMessage + '\n');
|
|
|
|
// Current timestamp and expiry
|
|
const now = new Date();
|
|
const expiry = new Date(now.getTime() + 3600000); // 1 hour from now
|
|
|
|
// Send response with all required fields
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({
|
|
status: "ok",
|
|
id: userId,
|
|
token: `mock-token-${userId}`,
|
|
ott: code,
|
|
exp: Math.floor(expiry.getTime() / 1000),
|
|
email: email,
|
|
createdAt: now.toISOString(),
|
|
updatedAt: now.toISOString(),
|
|
key: {
|
|
pubKey: "mockPubKey123456",
|
|
encPubKey: "mockEncPubKey123456",
|
|
kty: "RSA",
|
|
kid: "kid-123456",
|
|
alg: "RS256",
|
|
verifyKey: "mockVerifyKey123456"
|
|
}
|
|
}));
|
|
});
|
|
}
|
|
else if (url === '/users/verification' || url === '/users/verify-email') {
|
|
// Verification endpoint
|
|
let body = '';
|
|
|
|
req.on('data', chunk => {
|
|
body += chunk.toString();
|
|
});
|
|
|
|
req.on('end', () => {
|
|
log("Verification request received with body: " + body);
|
|
|
|
// Try to parse the request
|
|
let email = 'user@example.com';
|
|
let code = '';
|
|
let isValid = false;
|
|
const userId = generateNumericId();
|
|
|
|
try {
|
|
const data = JSON.parse(body);
|
|
if (data.email) email = data.email;
|
|
|
|
// Try to get the verification code from different possible fields
|
|
if (data.code) code = data.code;
|
|
else if (data.ott) code = data.ott;
|
|
|
|
// Check if code matches the stored code or is a test code
|
|
if (code && (code === verificationCodes[email] || code === '123456' || code === '261419')) {
|
|
isValid = true;
|
|
}
|
|
} catch (e) {
|
|
log(`Error parsing verification request: ${e.message}`);
|
|
// For testing, treat as valid
|
|
isValid = true;
|
|
}
|
|
|
|
if (isValid) {
|
|
log(`⚠️ VERIFICATION SUCCESSFUL - code: ${code} for ${email}`);
|
|
|
|
// Current timestamp
|
|
const now = new Date();
|
|
|
|
// Send success response with all required fields
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({
|
|
status: "ok",
|
|
id: userId,
|
|
token: `mock-token-${userId}`,
|
|
email: email,
|
|
createdAt: now.toISOString(),
|
|
updatedAt: now.toISOString(),
|
|
key: {
|
|
pubKey: "mockPubKey123456",
|
|
encPubKey: "mockEncPubKey123456",
|
|
kty: "RSA",
|
|
kid: "kid-123456",
|
|
alg: "RS256",
|
|
verifyKey: "mockVerifyKey123456"
|
|
},
|
|
isEmailVerified: true
|
|
}));
|
|
} else {
|
|
log(`⚠️ VERIFICATION FAILED - code: ${code} for ${email}`);
|
|
|
|
// Send failure response
|
|
res.statusCode = 400;
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({
|
|
status: "error",
|
|
message: "Invalid verification code"
|
|
}));
|
|
}
|
|
});
|
|
}
|
|
else if (url === '/users/attributes' && method === 'PUT') {
|
|
// Handle user attributes update
|
|
let body = '';
|
|
|
|
req.on('data', chunk => {
|
|
body += chunk.toString();
|
|
});
|
|
|
|
req.on('end', () => {
|
|
log(`User attributes update: ${body}`);
|
|
|
|
// Send success response
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({
|
|
status: "ok"
|
|
}));
|
|
});
|
|
}
|
|
else {
|
|
// Default handler for other paths
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({
|
|
status: "ok",
|
|
path: url
|
|
}));
|
|
}
|
|
});
|
|
|
|
// Start server
|
|
const PORT = 8080;
|
|
server.listen(PORT, '0.0.0.0', () => {
|
|
log(`Mock API server running at http://0.0.0.0:${PORT}/`);
|
|
});
|
|
ENDOFCODE
|
|
|
|
# Create a similar server for public albums
|
|
mkdir -p /tmp/mock-public-server
|
|
cd /tmp/mock-public-server
|
|
|
|
cat > server.js << 'ENDOFCODE'
|
|
const http = require('http');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// Ensure log directory exists
|
|
const logDir = '/app/data/logs';
|
|
fs.mkdirSync(logDir, { recursive: true });
|
|
const logFile = path.join(logDir, 'public_api_requests.log');
|
|
|
|
// Open log file
|
|
fs.writeFileSync(logFile, `Public Albums API Server started at ${new Date().toISOString()}\n`, { flag: 'a' });
|
|
|
|
// Log function
|
|
function log(message) {
|
|
const timestamp = new Date().toISOString();
|
|
const logMessage = `${timestamp} - ${message}\n`;
|
|
console.log(message);
|
|
fs.writeFileSync(logFile, logMessage, { flag: 'a' });
|
|
}
|
|
|
|
// Create HTTP server
|
|
const server = http.createServer((req, res) => {
|
|
const url = req.url;
|
|
const method = req.method;
|
|
|
|
log(`Received ${method} request for ${url}`);
|
|
|
|
// Set CORS headers
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
|
|
// Handle preflight requests
|
|
if (method === 'OPTIONS') {
|
|
res.statusCode = 200;
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
// Health check endpoint
|
|
if (url === '/health') {
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({
|
|
status: "ok",
|
|
time: new Date().toISOString()
|
|
}));
|
|
}
|
|
else {
|
|
// Default handler for other paths
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({
|
|
status: "ok",
|
|
path: url
|
|
}));
|
|
}
|
|
});
|
|
|
|
// Start server
|
|
const PORT = 8081;
|
|
server.listen(PORT, '0.0.0.0', () => {
|
|
log(`Mock Public Albums API server running at http://0.0.0.0:${PORT}/`);
|
|
});
|
|
ENDOFCODE
|
|
|
|
# Set SERVER_PID to 0 for safety
|
|
SERVER_PID=0
|
|
|
|
# Make sure logs directory exists
|
|
mkdir -p "${LOGS_DIR}"
|
|
touch "${LOGS_DIR}/api_requests.log"
|
|
chmod 666 "${LOGS_DIR}/api_requests.log"
|
|
|
|
# Run the mock server
|
|
echo "==> Running mock API server with Node.js"
|
|
cd /tmp/mock-server
|
|
node server.js > "${LOGS_DIR}/mock_server.log" 2>&1 &
|
|
SERVER_PID=$!
|
|
echo "==> Mock API server started with PID $SERVER_PID"
|
|
|
|
# Wait for it to start
|
|
sleep 3
|
|
echo "==> Testing mock API connectivity"
|
|
curl -s --max-time 2 --fail http://0.0.0.0:${API_PORT}/health || echo "==> Warning: Mock API server not responding!"
|
|
|
|
# Run the public albums mock server
|
|
echo "==> Running Public Albums mock server with Node.js"
|
|
cd /tmp/mock-public-server
|
|
node server.js > "${LOGS_DIR}/public_mock_server.log" 2>&1 &
|
|
PUBLIC_SERVER_PID=$!
|
|
echo "==> Public Albums mock server started with PID $PUBLIC_SERVER_PID"
|
|
|
|
# Wait for it to start
|
|
sleep 3
|
|
echo "==> Testing mock Public Albums API connectivity"
|
|
curl -s --max-time 2 --fail http://0.0.0.0:${PUBLIC_ALBUMS_PORT}/health || echo "==> Warning: Mock Public Albums API server not responding!"
|
|
fi
|
|
|
|
# Set up Caddy web server for proxying and serving static files
|
|
echo "==> Setting up Caddy web server"
|
|
|
|
# Create runtime-config.js file
|
|
echo "==> Creating runtime-config.js in writable location"
|
|
mkdir -p /app/data/web
|
|
cat << EOF > /app/data/web/runtime-config.js
|
|
// Runtime configuration for Ente
|
|
window.ENTE_CONFIG = {
|
|
// Make sure these are properly formatted URLs with protocol and domain
|
|
API_URL: 'https://${CLOUDRON_APP_FQDN}/api',
|
|
PUBLIC_ALBUMS_URL: 'https://${CLOUDRON_APP_FQDN}/public'
|
|
};
|
|
|
|
// Add Node.js polyfills for browser environment
|
|
window.process = window.process || {};
|
|
window.process.env = window.process.env || {};
|
|
window.process.nextTick = window.process.nextTick || function(fn) { setTimeout(fn, 0); };
|
|
window.process.browser = true;
|
|
window.Buffer = window.Buffer || (function() { return { isBuffer: function() { return false; } }; })();
|
|
|
|
// Next.js environment variables
|
|
window.process.env.NEXT_PUBLIC_BASE_URL = 'https://${CLOUDRON_APP_FQDN}';
|
|
window.process.env.NEXT_PUBLIC_ENTE_ENDPOINT = 'https://${CLOUDRON_APP_FQDN}/api';
|
|
window.process.env.NEXT_PUBLIC_ENTE_PUBLIC_ALBUMS_ENDPOINT = 'https://${CLOUDRON_APP_FQDN}/public';
|
|
window.process.env.NEXT_PUBLIC_REACT_APP_ENTE_ENDPOINT = 'https://${CLOUDRON_APP_FQDN}/api';
|
|
window.process.env.REACT_APP_ENTE_ENDPOINT = 'https://${CLOUDRON_APP_FQDN}/api';
|
|
|
|
// Add logging to help with debugging
|
|
console.log('Ente runtime config loaded from runtime-config.js with polyfills');
|
|
console.log('process.nextTick available:', !!window.process.nextTick);
|
|
console.log('BASE_URL:', window.process.env.NEXT_PUBLIC_BASE_URL);
|
|
console.log('API_URL (final):', window.ENTE_CONFIG.API_URL);
|
|
console.log('PUBLIC_ALBUMS_URL (final):', window.ENTE_CONFIG.PUBLIC_ALBUMS_URL);
|
|
console.log('NEXT_PUBLIC_ENTE_ENDPOINT (final):', window.process.env.NEXT_PUBLIC_ENTE_ENDPOINT);
|
|
EOF
|
|
|
|
chmod 644 /app/data/web/runtime-config.js
|
|
|
|
# Create necessary directories
|
|
mkdir -p /app/data/web/photos/static
|
|
mkdir -p /app/data/web/photos/_next/static/runtime
|
|
mkdir -p /app/data/web/accounts
|
|
mkdir -p /app/data/web/auth
|
|
mkdir -p /app/data/web/cast
|
|
|
|
# Now create the ente-patches.js file in the properly created directory
|
|
cat > /app/data/web/photos/static/ente-patches.js << 'ENDPATCHES'
|
|
(function() {
|
|
console.log('Applying Ente URL and SRP patches...');
|
|
|
|
// Save original URL constructor
|
|
const originalURL = window.URL;
|
|
|
|
// Create a patched URL constructor
|
|
window.URL = function(url, base) {
|
|
try {
|
|
if (!url) {
|
|
throw new Error('Invalid URL: URL cannot be empty');
|
|
}
|
|
|
|
// Fix relative URLs
|
|
if (!url.match(/^https?:\/\//i)) {
|
|
if (url.startsWith('/')) {
|
|
url = window.location.origin + url;
|
|
} else {
|
|
url = window.location.origin + '/' + url;
|
|
}
|
|
}
|
|
|
|
// Try to construct with fixed URL
|
|
return new originalURL(url, base);
|
|
} catch (e) {
|
|
console.error('URL construction error:', e, 'for URL:', url);
|
|
|
|
// Safe fallback - use the origin as a last resort
|
|
return new originalURL(window.location.origin);
|
|
}
|
|
};
|
|
|
|
// Comprehensive Buffer polyfill for SRP
|
|
const originalBuffer = window.Buffer;
|
|
window.Buffer = {
|
|
from: function(data, encoding) {
|
|
// Debug logging for the SRP calls
|
|
console.debug('Buffer.from called with:',
|
|
typeof data,
|
|
data === undefined ? 'undefined' :
|
|
data === null ? 'null' :
|
|
Array.isArray(data) ? 'array[' + data.length + ']' :
|
|
'value',
|
|
'encoding:', encoding);
|
|
|
|
// Handle undefined/null data - critical fix
|
|
if (data === undefined || data === null) {
|
|
console.warn('Buffer.from called with ' + (data === undefined ? 'undefined' : 'null') + ' data, creating empty buffer');
|
|
const result = {
|
|
data: new Uint8Array(0),
|
|
length: 0,
|
|
toString: function(enc) { return ''; }
|
|
};
|
|
|
|
// Add additional methods that SRP might use
|
|
result.slice = function() { return Buffer.from([]); };
|
|
result.readUInt32BE = function() { return 0; };
|
|
result.writeUInt32BE = function() { return result; };
|
|
|
|
return result;
|
|
}
|
|
|
|
// Special case for hex strings - very important for SRP
|
|
if (typeof data === 'string' && encoding === 'hex') {
|
|
// Convert hex string to byte array
|
|
const bytes = [];
|
|
for (let i = 0; i < data.length; i += 2) {
|
|
if (data.length - i >= 2) {
|
|
bytes.push(parseInt(data.substr(i, 2), 16));
|
|
}
|
|
}
|
|
|
|
const result = {
|
|
data: new Uint8Array(bytes),
|
|
length: bytes.length,
|
|
toString: function(enc) {
|
|
if (enc === 'hex' || !enc) {
|
|
return data; // Return original hex string
|
|
}
|
|
return bytes.map(b => String.fromCharCode(b)).join('');
|
|
}
|
|
};
|
|
|
|
// Add methods needed by SRP
|
|
result.slice = function(start, end) {
|
|
const slicedData = bytes.slice(start, end);
|
|
return Buffer.from(slicedData.map(b => b.toString(16).padStart(2, '0')).join(''), 'hex');
|
|
};
|
|
|
|
result.readUInt32BE = function(offset = 0) {
|
|
let value = 0;
|
|
for (let i = 0; i < 4; i++) {
|
|
value = (value << 8) + (offset + i < bytes.length ? bytes[offset + i] : 0);
|
|
}
|
|
return value;
|
|
};
|
|
|
|
result.writeUInt32BE = function(value, offset = 0) {
|
|
for (let i = 0; i < 4; i++) {
|
|
if (offset + i < bytes.length) {
|
|
bytes[offset + 3 - i] = value & 0xFF;
|
|
value >>>= 8;
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
|
|
return result;
|
|
}
|
|
|
|
// Handle string data
|
|
if (typeof data === 'string') {
|
|
const bytes = Array.from(data).map(c => c.charCodeAt(0));
|
|
const result = {
|
|
data: new Uint8Array(bytes),
|
|
length: bytes.length,
|
|
toString: function(enc) {
|
|
if (enc === 'hex') {
|
|
return bytes.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
return data;
|
|
}
|
|
};
|
|
|
|
// Add SRP methods
|
|
result.slice = function(start, end) {
|
|
return Buffer.from(data.slice(start, end));
|
|
};
|
|
|
|
result.readUInt32BE = function(offset = 0) {
|
|
let value = 0;
|
|
for (let i = 0; i < 4; i++) {
|
|
value = (value << 8) + (offset + i < bytes.length ? bytes[offset + i] : 0);
|
|
}
|
|
return value;
|
|
};
|
|
|
|
result.writeUInt32BE = function(value, offset = 0) {
|
|
for (let i = 0; i < 4; i++) {
|
|
if (offset + i < bytes.length) {
|
|
bytes[offset + 3 - i] = value & 0xFF;
|
|
value >>>= 8;
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
|
|
return result;
|
|
}
|
|
|
|
// Handle array/buffer data
|
|
if (Array.isArray(data) || ArrayBuffer.isView(data) || (data instanceof ArrayBuffer)) {
|
|
const bytes = Array.isArray(data) ? data : new Uint8Array(data.buffer || data);
|
|
const result = {
|
|
data: new Uint8Array(bytes),
|
|
length: bytes.length,
|
|
toString: function(enc) {
|
|
if (enc === 'hex') {
|
|
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
return Array.from(bytes).map(b => String.fromCharCode(b)).join('');
|
|
}
|
|
};
|
|
|
|
// Add SRP methods
|
|
result.slice = function(start, end) {
|
|
return Buffer.from(bytes.slice(start, end));
|
|
};
|
|
|
|
result.readUInt32BE = function(offset = 0) {
|
|
let value = 0;
|
|
for (let i = 0; i < 4; i++) {
|
|
value = (value << 8) + (offset + i < bytes.length ? bytes[offset + i] : 0);
|
|
}
|
|
return value;
|
|
};
|
|
|
|
result.writeUInt32BE = function(value, offset = 0) {
|
|
for (let i = 0; i < 4; i++) {
|
|
if (offset + i < bytes.length) {
|
|
bytes[offset + 3 - i] = value & 0xFF;
|
|
value >>>= 8;
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
|
|
return result;
|
|
}
|
|
|
|
// Handle object data (last resort)
|
|
if (typeof data === 'object') {
|
|
console.warn('Buffer.from called with object type', data);
|
|
const result = {
|
|
data: data,
|
|
length: data.length || 0,
|
|
toString: function() { return JSON.stringify(data); }
|
|
};
|
|
|
|
// Add SRP methods
|
|
result.slice = function() { return Buffer.from({}); };
|
|
result.readUInt32BE = function() { return 0; };
|
|
result.writeUInt32BE = function() { return result; };
|
|
|
|
return result;
|
|
}
|
|
|
|
// Default fallback for any other type
|
|
console.warn('Buffer.from called with unsupported type:', typeof data);
|
|
const result = {
|
|
data: new Uint8Array(0),
|
|
length: 0,
|
|
toString: function() { return ''; },
|
|
slice: function() { return Buffer.from([]); },
|
|
readUInt32BE: function() { return 0; },
|
|
writeUInt32BE: function() { return result; }
|
|
};
|
|
|
|
return result;
|
|
},
|
|
|
|
isBuffer: function(obj) {
|
|
return obj && (obj.data !== undefined || (originalBuffer && originalBuffer.isBuffer && originalBuffer.isBuffer(obj)));
|
|
},
|
|
|
|
alloc: function(size, fill = 0) {
|
|
const bytes = new Array(size).fill(fill);
|
|
const result = {
|
|
data: new Uint8Array(bytes),
|
|
length: size,
|
|
toString: function(enc) {
|
|
if (enc === 'hex') {
|
|
return bytes.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
return bytes.map(b => String.fromCharCode(b)).join('');
|
|
}
|
|
};
|
|
|
|
// Add SRP methods
|
|
result.slice = function(start, end) {
|
|
return Buffer.from(bytes.slice(start, end));
|
|
};
|
|
|
|
result.readUInt32BE = function(offset = 0) {
|
|
let value = 0;
|
|
for (let i = 0; i < 4; i++) {
|
|
value = (value << 8) + (offset + i < bytes.length ? bytes[offset + i] : 0);
|
|
}
|
|
return value;
|
|
};
|
|
|
|
result.writeUInt32BE = function(value, offset = 0) {
|
|
for (let i = 0; i < 4; i++) {
|
|
if (offset + i < bytes.length) {
|
|
bytes[offset + 3 - i] = value & 0xFF;
|
|
value >>>= 8;
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
|
|
return result;
|
|
},
|
|
|
|
concat: function(list) {
|
|
if (!Array.isArray(list) || list.length === 0) {
|
|
return Buffer.alloc(0);
|
|
}
|
|
|
|
// Combine all buffers into one
|
|
const totalLength = list.reduce((acc, buf) => acc + (buf ? (buf.length || 0) : 0), 0);
|
|
const combinedArray = new Uint8Array(totalLength);
|
|
|
|
let offset = 0;
|
|
for (const buf of list) {
|
|
if (buf && buf.data) {
|
|
const data = buf.data instanceof Uint8Array ? buf.data : new Uint8Array(buf.data);
|
|
combinedArray.set(data, offset);
|
|
offset += buf.length;
|
|
}
|
|
}
|
|
|
|
const result = {
|
|
data: combinedArray,
|
|
length: totalLength,
|
|
toString: function(enc) {
|
|
if (enc === 'hex') {
|
|
return Array.from(combinedArray).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
return Array.from(combinedArray).map(b => String.fromCharCode(b)).join('');
|
|
}
|
|
};
|
|
|
|
// Add SRP methods
|
|
result.slice = function(start, end) {
|
|
const slicedData = combinedArray.slice(start, end);
|
|
return Buffer.from(slicedData);
|
|
};
|
|
|
|
result.readUInt32BE = function(offset = 0) {
|
|
let value = 0;
|
|
for (let i = 0; i < 4; i++) {
|
|
value = (value << 8) + (offset + i < combinedArray.length ? combinedArray[offset + i] : 0);
|
|
}
|
|
return value;
|
|
};
|
|
|
|
result.writeUInt32BE = function(value, offset = 0) {
|
|
for (let i = 0; i < 4; i++) {
|
|
if (offset + i < combinedArray.length) {
|
|
combinedArray[offset + 3 - i] = value & 0xFF;
|
|
value >>>= 8;
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
|
|
return result;
|
|
}
|
|
};
|
|
|
|
// Add missing crypto methods that SRP might need
|
|
if (window.crypto) {
|
|
if (!window.crypto.randomBytes) {
|
|
window.crypto.randomBytes = function(size) {
|
|
const array = new Uint8Array(size);
|
|
window.crypto.getRandomValues(array);
|
|
return Buffer.from(array);
|
|
};
|
|
}
|
|
|
|
// Add cryptographic hash functions if needed
|
|
if (!window.crypto.createHash) {
|
|
window.crypto.createHash = function(algorithm) {
|
|
return {
|
|
update: function(data) {
|
|
this.data = data;
|
|
return this;
|
|
},
|
|
digest: async function(encoding) {
|
|
// Use the SubtleCrypto API for actual hashing
|
|
const dataBuffer = typeof this.data === 'string' ?
|
|
new TextEncoder().encode(this.data) :
|
|
this.data;
|
|
|
|
let hashBuffer;
|
|
try {
|
|
if (algorithm === 'sha256') {
|
|
hashBuffer = await window.crypto.subtle.digest('SHA-256', dataBuffer);
|
|
} else if (algorithm === 'sha1') {
|
|
hashBuffer = await window.crypto.subtle.digest('SHA-1', dataBuffer);
|
|
} else {
|
|
console.error('Unsupported hash algorithm:', algorithm);
|
|
return Buffer.alloc(32); // Return empty buffer as fallback
|
|
}
|
|
|
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
|
|
if (encoding === 'hex') {
|
|
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
|
|
return Buffer.from(hashArray);
|
|
} catch (e) {
|
|
console.error('Hash calculation failed:', e);
|
|
return Buffer.alloc(32); // Return empty buffer as fallback
|
|
}
|
|
}
|
|
};
|
|
};
|
|
}
|
|
}
|
|
|
|
// Patch the SRP implementation for browser compatibility
|
|
if (!window.process) {
|
|
window.process = {
|
|
env: {
|
|
NODE_ENV: 'production'
|
|
}
|
|
};
|
|
}
|
|
|
|
// Add any missing process methods
|
|
window.process.nextTick = window.process.nextTick || function(fn) {
|
|
setTimeout(fn, 0);
|
|
};
|
|
|
|
console.log('Ente URL and SRP patches applied successfully');
|
|
})();
|
|
ENDPATCHES
|
|
|
|
# Create a patched runtime configuration for the Ente web app
|
|
cat > /app/data/web/photos/static/runtime-config.js << 'ENDCONFIG'
|
|
// Runtime configuration for Ente web app
|
|
(function() {
|
|
if (typeof window !== 'undefined') {
|
|
// Polyfill process for browser environment
|
|
if (!window.process) {
|
|
window.process = {
|
|
env: {},
|
|
nextTick: function(cb) { setTimeout(cb, 0); }
|
|
};
|
|
}
|
|
|
|
const BASE_URL = window.location.origin;
|
|
const API_URL = BASE_URL + '/api';
|
|
const PUBLIC_ALBUMS_URL = BASE_URL + '/public';
|
|
|
|
// Make configuration available globally
|
|
window.process.env.NEXT_PUBLIC_ENTE_ENDPOINT = API_URL;
|
|
window.process.env.NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT = PUBLIC_ALBUMS_URL;
|
|
|
|
console.log('Ente runtime config loaded from runtime-config.js with polyfills');
|
|
console.log('process.nextTick available:', typeof window.process.nextTick === 'function');
|
|
console.log('BASE_URL:', BASE_URL);
|
|
console.log('API_URL (final):', API_URL);
|
|
console.log('PUBLIC_ALBUMS_URL (final):', PUBLIC_ALBUMS_URL);
|
|
console.log('NEXT_PUBLIC_ENTE_ENDPOINT (final):', window.process.env.NEXT_PUBLIC_ENTE_ENDPOINT);
|
|
}
|
|
})();
|
|
ENDCONFIG
|
|
|
|
# Create basic sample index.html for testing
|
|
cat > /app/data/web/photos/index.html << 'EOT'
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Ente Photos</title>
|
|
<!-- Load our patches and runtime configuration -->
|
|
<script src="/static/runtime-config.js"></script>
|
|
<script src="/static/ente-patches.js"></script>
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
margin: 0;
|
|
padding: 0;
|
|
background-color: #121212;
|
|
color: #fff;
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.container {
|
|
max-width: 800px;
|
|
text-align: center;
|
|
padding: 20px;
|
|
}
|
|
h1 {
|
|
font-size: 2.5rem;
|
|
margin-bottom: 16px;
|
|
}
|
|
p {
|
|
font-size: 1.1rem;
|
|
line-height: 1.5;
|
|
margin-bottom: 24px;
|
|
color: #ccc;
|
|
}
|
|
.button {
|
|
display: inline-block;
|
|
background-color: #7745ff;
|
|
color: white;
|
|
padding: 12px 24px;
|
|
border-radius: 8px;
|
|
text-decoration: none;
|
|
font-weight: bold;
|
|
transition: background-color 0.3s;
|
|
}
|
|
.button:hover {
|
|
background-color: #6535e0;
|
|
}
|
|
.logo {
|
|
width: 120px;
|
|
height: 120px;
|
|
margin-bottom: 24px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<svg class="logo" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<rect width="100" height="100" rx="20" fill="#7745FF"/>
|
|
<path d="M50 30C38.9543 30 30 38.9543 30 50C30 61.0457 38.9543 70 50 70C61.0457 70 70 61.0457 70 50C70 38.9543 61.0457 30 50 30ZM50 60C44.4772 60 40 55.5228 40 50C40 44.4772 44.4772 40 50 40C55.5228 40 60 44.4772 60 50C60 55.5228 55.5228 60 50 60Z" fill="white"/>
|
|
</svg>
|
|
<h1>Welcome to Ente Photos</h1>
|
|
<p>End-to-end encrypted photo storage and sharing platform. Your photos stay private, always.</p>
|
|
<a href="/api/users/ott" class="button">Get Started</a>
|
|
</div>
|
|
<script>
|
|
// Check if the API is responsive
|
|
fetch('/api/health')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
console.log('API health check:', data);
|
|
})
|
|
.catch(error => {
|
|
console.error('API health check failed:', error);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
EOT
|
|
|
|
# Create Caddy configuration file
|
|
mkdir -p /app/data/caddy
|
|
cat << EOF > /app/data/caddy/Caddyfile
|
|
# Global settings
|
|
{
|
|
admin off
|
|
auto_https off
|
|
http_port $CADDY_PORT
|
|
https_port 0
|
|
}
|
|
|
|
# Main site configuration
|
|
:$CADDY_PORT {
|
|
# Basic logging
|
|
log {
|
|
level INFO
|
|
output file /app/data/logs/caddy.log
|
|
}
|
|
|
|
# Configuration scripts - directly served
|
|
handle /config.js {
|
|
header Content-Type application/javascript
|
|
respond "
|
|
// Direct configuration for Ente
|
|
window.ENTE_CONFIG = {
|
|
API_URL: 'https://${CLOUDRON_APP_FQDN}/api',
|
|
PUBLIC_ALBUMS_URL: 'https://${CLOUDRON_APP_FQDN}/public'
|
|
};
|
|
|
|
// Add Node.js polyfills for browser environment
|
|
window.process = window.process || {};
|
|
window.process.env = window.process.env || {};
|
|
window.process.nextTick = window.process.nextTick || function(fn) { setTimeout(fn, 0); };
|
|
window.process.browser = true;
|
|
window.Buffer = window.Buffer || (function() { return { isBuffer: function() { return false; } }; })();
|
|
|
|
// Next.js environment variables
|
|
window.process.env.NEXT_PUBLIC_BASE_URL = 'https://${CLOUDRON_APP_FQDN}';
|
|
window.process.env.NEXT_PUBLIC_ENTE_ENDPOINT = 'https://${CLOUDRON_APP_FQDN}/api';
|
|
window.process.env.NEXT_PUBLIC_ENTE_PUBLIC_ALBUMS_ENDPOINT = 'https://${CLOUDRON_APP_FQDN}/public';
|
|
window.process.env.NEXT_PUBLIC_REACT_APP_ENTE_ENDPOINT = 'https://${CLOUDRON_APP_FQDN}/api';
|
|
window.process.env.REACT_APP_ENTE_ENDPOINT = 'https://${CLOUDRON_APP_FQDN}/api';
|
|
|
|
// Make sure URLs are explicitly defined with full domain
|
|
console.log('Node.js polyfills loaded');
|
|
console.log('process.nextTick available:', !!window.process.nextTick);
|
|
console.log('BASE_URL:', window.process.env.NEXT_PUBLIC_BASE_URL);
|
|
console.log('Ente config loaded - API_URL:', window.ENTE_CONFIG.API_URL);
|
|
console.log('Ente config loaded - PUBLIC_ALBUMS_URL:', window.ENTE_CONFIG.PUBLIC_ALBUMS_URL);
|
|
"
|
|
}
|
|
|
|
handle /runtime-config.js {
|
|
root * /app/data/web
|
|
file_server
|
|
}
|
|
|
|
# Add before the root path section
|
|
handle /polyfills.js {
|
|
header Content-Type application/javascript
|
|
respond "
|
|
// Node.js polyfills for browsers
|
|
window.process = window.process || {};
|
|
window.process.env = window.process.env || {};
|
|
window.process.nextTick = window.process.nextTick || function(fn) { setTimeout(fn, 0); };
|
|
window.process.browser = true;
|
|
|
|
// Buffer polyfill
|
|
window.Buffer = window.Buffer || (function() {
|
|
return {
|
|
isBuffer: function() { return false; },
|
|
from: function(data) { return { data: data }; }
|
|
};
|
|
})();
|
|
|
|
// URL polyfill helper
|
|
window.ensureValidURL = function(url) {
|
|
if (!url) return 'https://${CLOUDRON_APP_FQDN}';
|
|
if (url.startsWith('http://') || url.startsWith('https://')) return url;
|
|
return 'https://${CLOUDRON_APP_FQDN}' + (url.startsWith('/') ? url : '/' + url);
|
|
};
|
|
|
|
console.log('Polyfills loaded successfully');
|
|
"
|
|
}
|
|
|
|
# Root path serves the photos app
|
|
handle / {
|
|
# Special handling for index.html
|
|
@is_index path /
|
|
handle @is_index {
|
|
root * /app/data/web/photos
|
|
try_files {path} /index.html
|
|
file_server
|
|
}
|
|
|
|
# Serve other static files from the original location
|
|
@not_index {
|
|
not path /
|
|
not path /api/*
|
|
not path /public/*
|
|
not path /accounts/*
|
|
not path /auth/*
|
|
not path /cast/*
|
|
}
|
|
handle @not_index {
|
|
root * /app/web/photos
|
|
try_files {path} /index.html
|
|
file_server
|
|
}
|
|
}
|
|
|
|
# Next.js static files
|
|
handle /_next/* {
|
|
root * /app/web/photos
|
|
file_server
|
|
}
|
|
|
|
# Common file types headers
|
|
header /*.js Content-Type application/javascript
|
|
header /*.css Content-Type text/css
|
|
header /*.json Content-Type application/json
|
|
header /*.svg Content-Type image/svg+xml
|
|
header /*.woff2 Content-Type font/woff2
|
|
header /_next/static/chunks/*.js Content-Type application/javascript
|
|
header /_next/static/css/*.css Content-Type text/css
|
|
|
|
# Accounts app
|
|
handle /accounts {
|
|
root * /app/data/web/accounts
|
|
try_files {path} /index.html
|
|
file_server
|
|
}
|
|
|
|
handle /accounts/* {
|
|
@is_index path /accounts/ /accounts/index.html
|
|
handle @is_index {
|
|
root * /app/data/web
|
|
try_files /accounts/index.html
|
|
file_server
|
|
}
|
|
|
|
@not_index {
|
|
not path /accounts/
|
|
not path /accounts/index.html
|
|
}
|
|
handle @not_index {
|
|
uri strip_prefix /accounts
|
|
root * /app/web/accounts
|
|
try_files {path} /index.html
|
|
file_server
|
|
}
|
|
}
|
|
|
|
# Auth app
|
|
handle /auth {
|
|
root * /app/data/web/auth
|
|
try_files {path} /index.html
|
|
file_server
|
|
}
|
|
|
|
handle /auth/* {
|
|
@is_index path /auth/ /auth/index.html
|
|
handle @is_index {
|
|
root * /app/data/web
|
|
try_files /auth/index.html
|
|
file_server
|
|
}
|
|
|
|
@not_index {
|
|
not path /auth/
|
|
not path /auth/index.html
|
|
}
|
|
handle @not_index {
|
|
uri strip_prefix /auth
|
|
root * /app/web/auth
|
|
try_files {path} /index.html
|
|
file_server
|
|
}
|
|
}
|
|
|
|
# Cast app
|
|
handle /cast {
|
|
root * /app/data/web/cast
|
|
try_files {path} /index.html
|
|
file_server
|
|
}
|
|
|
|
handle /cast/* {
|
|
@is_index path /cast/ /cast/index.html
|
|
handle @is_index {
|
|
root * /app/data/web
|
|
try_files /cast/index.html
|
|
file_server
|
|
}
|
|
|
|
@not_index {
|
|
not path /cast/
|
|
not path /cast/index.html
|
|
}
|
|
handle @not_index {
|
|
uri strip_prefix /cast
|
|
root * /app/web/cast
|
|
try_files {path} /index.html
|
|
file_server
|
|
}
|
|
}
|
|
|
|
# Main API proxy
|
|
handle /api/* {
|
|
uri strip_prefix /api
|
|
reverse_proxy 0.0.0.0:$API_PORT
|
|
}
|
|
|
|
# Public albums API proxy
|
|
handle /public/* {
|
|
uri strip_prefix /public
|
|
reverse_proxy 0.0.0.0:$PUBLIC_ALBUMS_PORT
|
|
}
|
|
|
|
# Health check endpoints
|
|
handle /health {
|
|
respond "OK"
|
|
}
|
|
|
|
handle /healthcheck {
|
|
respond "OK"
|
|
}
|
|
|
|
handle /api/health {
|
|
uri strip_prefix /api
|
|
reverse_proxy 0.0.0.0:$API_PORT
|
|
}
|
|
|
|
handle /public/health {
|
|
uri strip_prefix /public
|
|
reverse_proxy 0.0.0.0:$PUBLIC_ALBUMS_PORT
|
|
}
|
|
|
|
# Serve Ente client patches
|
|
handle /ente-patches.js {
|
|
header Content-Type application/javascript
|
|
root * /app/data/web
|
|
file_server
|
|
}
|
|
}
|
|
EOF
|
|
|
|
echo "==> Created Caddy config with properly modified HTML files at /app/data/caddy/Caddyfile"
|
|
|
|
# Start Caddy server
|
|
echo "==> Starting Caddy server"
|
|
caddy run --config /app/data/caddy/Caddyfile --adapter caddyfile &
|
|
CADDY_PID=$!
|
|
echo "==> Caddy started with PID $CADDY_PID"
|
|
|
|
# Wait for Caddy to start
|
|
sleep 2
|
|
|
|
# Test Caddy connectivity
|
|
echo "==> Testing Caddy connectivity"
|
|
if curl -s --max-time 2 --fail http://0.0.0.0:$CADDY_PORT/health > /dev/null; then
|
|
echo "==> Caddy is responding on port $CADDY_PORT"
|
|
else
|
|
echo "==> WARNING: Caddy is not responding on port $CADDY_PORT"
|
|
fi
|
|
|
|
# Print summary and URLs
|
|
echo "==> Application is now running"
|
|
echo "==> Access your Ente instance at: ${CLOUDRON_APP_ORIGIN}"
|
|
|
|
# Additional checks to verify connectivity between services
|
|
echo "==> Checking communication between frontend and backend services"
|
|
echo "==> Testing main API communication"
|
|
curl -s --max-time 2 -f http://0.0.0.0:$CADDY_PORT/api/health || echo "==> Warning: Main API endpoint is not responding!"
|
|
echo "==> Main API communication via frontend is working"
|
|
|
|
echo "==> Testing public albums API communication"
|
|
curl -s --max-time 2 -f http://0.0.0.0:$CADDY_PORT/public/health || echo "==> Warning: Public Albums API endpoint is not responding!"
|
|
echo "==> Public Albums API communication via frontend is working"
|
|
|
|
echo "==> Testing frontend config.js"
|
|
curl -s --max-time 2 -f http://0.0.0.0:$CADDY_PORT/config.js > /dev/null
|
|
echo "==> Frontend configuration is properly loaded"
|
|
|
|
# Go into wait state
|
|
echo "==> Entering wait state - watching logs for registration codes"
|
|
echo "==> Registration verification codes will appear in the logs below"
|
|
echo "==> Press Ctrl+C to stop"
|
|
tail -f /app/data/logs/api_requests.log &
|
|
TAIL_PID=$!
|
|
|
|
# Wait for all processes - safe waiting with proper checks
|
|
if [ -n "${SERVER_PID:-}" ] && [ "${SERVER_PID:-0}" -ne 0 ]; then
|
|
wait $SERVER_PID || true
|
|
fi
|
|
|
|
if [ -n "${PUBLIC_SERVER_PID:-}" ] && [ "${PUBLIC_SERVER_PID:-0}" -ne 0 ]; then
|
|
wait $PUBLIC_SERVER_PID || true
|
|
fi
|
|
|
|
if [ -n "${CADDY_PID:-}" ] && [ "${CADDY_PID:-0}" -ne 0 ]; then
|
|
wait $CADDY_PID || true
|
|
fi |