4 Commits

Author SHA1 Message Date
Your Name
da50bf4773 Add OTP email monitor to handle Museum skipped emails
- Implement comprehensive OTP email monitoring service
- Monitor Museum logs for "Skipping sending email" pattern
- Send verification emails using Cloudron email addon
- Add specific regex pattern for Museum's skip email format
- Version bump to 0.1.62

The monitor captures OTP codes from logs when Museum skips sending
emails and sends them via Cloudron's email system. This ensures
users receive their verification codes even when Museum's email
configuration is not sending directly.
2025-07-22 12:27:44 -06:00
Your Name
4290a33ba9 Fix JavaScript URL construction error for API endpoint
- Change NEXT_PUBLIC_ENTE_ENDPOINT from "/api" to "https://example.com/api" during build to satisfy URL constructor requirements
- Add runtime replacement in start.sh to replace placeholder with actual domain endpoint
- This resolves the "TypeError: Failed to construct 'URL': Invalid URL" error in the frontend

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-22 08:58:53 -06:00
Your Name
62b6f7f9ac Fix S3 configuration - set are_local_buckets to true
- Changed are_local_buckets from false to true (required for external S3)
- Simplified S3 configuration to only use b2-eu-cen bucket
- Removed unnecessary replication buckets for single bucket setup

This aligns with Ente's documentation where are_local_buckets=true
is used for external S3 services like Wasabi.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-21 20:44:19 -06:00
Your Name
e3eb1b0491 Hardcode Wasabi S3 configuration with proper Ente format
- Remove dynamic S3 configuration loading
- Hardcode Wasabi credentials as requested
- Use proper Ente S3 configuration format with datacenter names
- Configure all three storage buckets (b2-eu-cen, wasabi-eu-central-2-v3, scw-eu-fr-v3)
- Set are_local_buckets to false for external S3
- Add compliance flag for Wasabi bucket

This should fix the MissingRegion error by properly configuring S3 storage
according to Ente's expected format.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-21 20:41:58 -06:00
5 changed files with 639 additions and 332 deletions

View File

@@ -7,13 +7,14 @@
"contactEmail": "contact@ente.io", "contactEmail": "contact@ente.io",
"tagline": "Open Source End-to-End Encrypted Photos & Authentication", "tagline": "Open Source End-to-End Encrypted Photos & Authentication",
"upstreamVersion": "1.0.0", "upstreamVersion": "1.0.0",
"version": "1.0.1", "version": "0.1.62",
"healthCheckPath": "/health", "healthCheckPath": "/ping",
"httpPort": 3080, "httpPort": 3080,
"memoryLimit": 1073741824, "memoryLimit": 1073741824,
"addons": { "addons": {
"localstorage": {}, "localstorage": {},
"postgresql": {}, "postgresql": {},
"email": {},
"sendmail": { "sendmail": {
"supportsDisplayName": true "supportsDisplayName": true
} }

View File

@@ -1,3 +1,19 @@
# Build Museum server from source
FROM golang:1.24-bookworm AS museum-builder
WORKDIR /ente
# Clone the repository for server building
RUN apt-get update && apt-get install -y git libsodium-dev && \
git clone --depth=1 https://github.com/ente-io/ente.git . && \
apt-get clean && apt-get autoremove && \
rm -rf /var/cache/apt /var/lib/apt/lists
# Build the Museum server
WORKDIR /ente/server
RUN go mod download && \
CGO_ENABLED=1 GOOS=linux go build -a -o museum ./cmd/museum
FROM node:20-bookworm-slim as web-builder FROM node:20-bookworm-slim as web-builder
WORKDIR /ente WORKDIR /ente
@@ -12,10 +28,10 @@ RUN apt-get update && apt-get install -y git && \
RUN corepack enable RUN corepack enable
# Set environment variables for web app build # Set environment variables for web app build
# Use "/api" as the endpoint which will be replaced at runtime with the full URL # Set the API endpoint to use current origin - this will work at runtime
ENV NEXT_PUBLIC_ENTE_ENDPOINT="/api" ENV NEXT_PUBLIC_ENTE_ENDPOINT="https://example.com/api"
# Add a note for clarity # Add a note for clarity
RUN echo "Building with NEXT_PUBLIC_ENTE_ENDPOINT=/api, will be replaced at runtime with full URL" RUN echo "Building with placeholder NEXT_PUBLIC_ENTE_ENDPOINT, will be served by Caddy proxy at /api"
# Debugging the repository structure # Debugging the repository structure
RUN find . -type d -maxdepth 3 | sort RUN find . -type d -maxdepth 3 | sort
@@ -136,6 +152,11 @@ COPY --from=web-builder /build/web/accounts /app/web/accounts
COPY --from=web-builder /build/web/auth /app/web/auth COPY --from=web-builder /build/web/auth /app/web/auth
COPY --from=web-builder /build/web/cast /app/web/cast COPY --from=web-builder /build/web/cast /app/web/cast
# Copy Museum server binary from builder stage to app directory (not data volume)
RUN mkdir -p /app/museum-bin
COPY --from=museum-builder /ente/server/museum /app/museum-bin/museum
RUN chmod +x /app/museum-bin/museum
# Copy configuration and startup scripts # Copy configuration and startup scripts
ADD start.sh /app/pkg/ ADD start.sh /app/pkg/
ADD config.template.yaml /app/pkg/ ADD config.template.yaml /app/pkg/

389
otp-email-monitor.js Normal file
View File

@@ -0,0 +1,389 @@
#!/usr/bin/env node
/**
* Ente OTP Email Monitor
*
* Monitors Museum server logs for OTP generation events and sends
* verification emails using Cloudron's email addon.
*/
const fs = require('fs');
const path = require('path');
const { spawn } = require('child_process');
const nodemailer = require('nodemailer');
// Configuration
const CONFIG = {
LOG_FILE: '/app/data/logs/museum.log',
EMAIL_TEMPLATES_DIR: '/app/data/ente/server/mail-templates',
FROM_EMAIL: `noreply@${process.env.CLOUDRON_EMAIL_DOMAIN || 'localhost'}`,
FROM_NAME: 'Ente Photos',
SMTP: {
host: process.env.CLOUDRON_EMAIL_SMTP_SERVER,
port: parseInt(process.env.CLOUDRON_EMAIL_SMTP_PORT) || 587,
secure: false, // STARTTLS disabled on this port
auth: false // Internal mail server
}
};
// Logging utility
class Logger {
static log(level, message, data = null) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] [OTP-EMAIL-${level}] ${message}`;
console.log(logMessage);
if (data) {
console.log(JSON.stringify(data, null, 2));
}
// Also write to file
try {
fs.appendFileSync('/app/data/logs/otp-email.log', logMessage + '\n');
} catch (err) {
console.error('Failed to write to log file:', err.message);
}
}
static info(message, data) { this.log('INFO', message, data); }
static warn(message, data) { this.log('WARN', message, data); }
static error(message, data) { this.log('ERROR', message, data); }
}
// Email template handler
class EmailTemplate {
constructor(templateDir) {
this.templateDir = templateDir;
this.templates = new Map();
this.loadTemplates();
}
loadTemplates() {
try {
const ottTemplate = fs.readFileSync(path.join(this.templateDir, 'ott.html'), 'utf8');
const changeEmailTemplate = fs.readFileSync(path.join(this.templateDir, 'ott_change_email.html'), 'utf8');
this.templates.set('ott', ottTemplate);
this.templates.set('ott_change_email', changeEmailTemplate);
Logger.info('Email templates loaded successfully');
} catch (err) {
Logger.error('Failed to load email templates:', err.message);
throw err;
}
}
render(templateName, variables) {
const template = this.templates.get(templateName);
if (!template) {
throw new Error(`Template ${templateName} not found`);
}
let html = template;
// Replace template variables
for (const [key, value] of Object.entries(variables)) {
const placeholder = `{{.${key}}}`;
html = html.replace(new RegExp(placeholder, 'g'), value);
}
return html;
}
}
// Email sender using Cloudron email addon
class EmailSender {
constructor(config) {
this.config = config;
this.transporter = null;
this.initializeTransporter();
}
initializeTransporter() {
try {
this.transporter = nodemailer.createTransport({
host: this.config.SMTP.host,
port: this.config.SMTP.port,
secure: this.config.SMTP.secure,
// No auth needed for internal Cloudron mail server
tls: {
rejectUnauthorized: false // Accept self-signed certificates
}
});
Logger.info('Email transporter initialized', {
host: this.config.SMTP.host,
port: this.config.SMTP.port
});
} catch (err) {
Logger.error('Failed to initialize email transporter:', err.message);
throw err;
}
}
async sendEmail(to, subject, html) {
try {
const mailOptions = {
from: `${this.config.FROM_NAME} <${this.config.FROM_EMAIL}>`,
to: to,
subject: subject,
html: html
};
const result = await this.transporter.sendMail(mailOptions);
Logger.info('Email sent successfully', {
to: to,
subject: subject,
messageId: result.messageId
});
return result;
} catch (err) {
Logger.error('Failed to send email:', {
error: err.message,
to: to,
subject: subject
});
throw err;
}
}
}
// Log monitor for OTP events
class LogMonitor {
constructor(logFile, emailSender, emailTemplate) {
this.logFile = logFile;
this.emailSender = emailSender;
this.emailTemplate = emailTemplate;
this.tail = null;
this.processedOTPs = new Set(); // Prevent duplicate sends
}
start() {
Logger.info('Starting log monitor', { logFile: this.logFile });
// Use tail -F to follow log file
this.tail = spawn('tail', ['-F', this.logFile]);
this.tail.stdout.on('data', (data) => {
const lines = data.toString().split('\n');
for (const line of lines) {
if (line.trim()) {
this.processLogLine(line);
}
}
});
this.tail.stderr.on('data', (data) => {
Logger.warn('Tail stderr:', data.toString());
});
this.tail.on('close', (code) => {
Logger.warn('Tail process closed', { code });
// Restart after 5 seconds
setTimeout(() => this.start(), 5000);
});
this.tail.on('error', (err) => {
Logger.error('Tail process error:', err.message);
setTimeout(() => this.start(), 5000);
});
}
stop() {
if (this.tail) {
this.tail.kill();
this.tail = null;
Logger.info('Log monitor stopped');
}
}
processLogLine(line) {
try {
// Look for OTP-related log entries
// Museum server logs OTP generation in various formats
const patterns = [
// Pattern 1: Museum skipping email - MOST IMPORTANT (matches "Skipping sending email to andreas@due.ren: Verification code: 192305")
/Skipping sending email to\s+([^\s:]+):\s*Verification code:\s*(\d{6})/i,
// Pattern 2: Direct OTP generation logs
/sendOTT.*email[:\s]+([^\s]+).*code[:\s]+(\d{6})/i,
// Pattern 3: User registration/login with OTP
/generateOTT.*user[:\s]+([^\s]+).*verification.*code[:\s]+(\d{6})/i,
// Pattern 4: Email change OTP
/changeEmail.*email[:\s]+([^\s]+).*otp[:\s]+(\d{6})/i,
// Pattern 5: Generic OTP patterns in logs
/ott.*([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}).*(\d{6})/i
];
for (const pattern of patterns) {
const match = line.match(pattern);
if (match) {
const email = match[1];
const otpCode = match[2];
// Create unique identifier to prevent duplicates
const otpId = `${email}:${otpCode}:${Date.now().toString().slice(-6)}`;
if (!this.processedOTPs.has(otpId)) {
this.processedOTPs.add(otpId);
this.sendOTPEmail(email, otpCode, line);
// Clean up old OTPs (keep last 100)
if (this.processedOTPs.size > 100) {
const oldOTPs = Array.from(this.processedOTPs).slice(0, 50);
oldOTPs.forEach(otp => this.processedOTPs.delete(otp));
}
}
break;
}
}
} catch (err) {
Logger.error('Error processing log line:', {
error: err.message,
line: line.substring(0, 100)
});
}
}
async sendOTPEmail(email, otpCode, logLine) {
try {
Logger.info('Processing OTP email request', {
email: email,
otpCode: otpCode.substring(0, 2) + '****', // Partial OTP for logging
source: logLine.substring(0, 100)
});
// Determine template type based on context
let templateName = 'ott';
let subject = 'Ente - Verification Code';
if (logLine.toLowerCase().includes('change') || logLine.toLowerCase().includes('email')) {
templateName = 'ott_change_email';
subject = 'Ente - Email Change Verification';
}
// Render email template
const html = this.emailTemplate.render(templateName, {
VerificationCode: otpCode
});
// Send email
await this.emailSender.sendEmail(email, subject, html);
Logger.info('OTP email sent successfully', {
email: email,
template: templateName
});
} catch (err) {
Logger.error('Failed to send OTP email:', {
error: err.message,
email: email,
otpCode: otpCode.substring(0, 2) + '****'
});
}
}
}
// Health check endpoint
class HealthServer {
constructor(port = 8081) {
this.port = port;
this.server = null;
}
start() {
const http = require('http');
this.server = http.createServer((req, res) => {
if (req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
status: 'healthy',
service: 'ente-otp-email-monitor',
timestamp: new Date().toISOString(),
processedOTPs: monitor ? monitor.processedOTPs.size : 0
}));
} else {
res.writeHead(404);
res.end('Not Found');
}
});
this.server.listen(this.port, () => {
Logger.info(`Health server listening on port ${this.port}`);
});
}
stop() {
if (this.server) {
this.server.close();
Logger.info('Health server stopped');
}
}
}
// Main application
let monitor = null;
let healthServer = null;
async function main() {
try {
Logger.info('Starting Ente OTP Email Monitor');
// Validate environment
if (!process.env.CLOUDRON_EMAIL_SMTP_SERVER) {
throw new Error('CLOUDRON_EMAIL_SMTP_SERVER not found. Email addon may not be configured.');
}
// Initialize components
const emailTemplate = new EmailTemplate(CONFIG.EMAIL_TEMPLATES_DIR);
const emailSender = new EmailSender(CONFIG);
// Test email connectivity
Logger.info('Testing email connectivity...');
await emailSender.transporter.verify();
Logger.info('Email connectivity verified');
// Start log monitor
monitor = new LogMonitor(CONFIG.LOG_FILE, emailSender, emailTemplate);
monitor.start();
// Start health server
healthServer = new HealthServer();
healthServer.start();
Logger.info('Ente OTP Email Monitor started successfully');
// Handle graceful shutdown
process.on('SIGINT', () => {
Logger.info('Received SIGINT, shutting down gracefully...');
if (monitor) monitor.stop();
if (healthServer) healthServer.stop();
process.exit(0);
});
process.on('SIGTERM', () => {
Logger.info('Received SIGTERM, shutting down gracefully...');
if (monitor) monitor.stop();
if (healthServer) healthServer.stop();
process.exit(0);
});
} catch (err) {
Logger.error('Failed to start OTP Email Monitor:', err.message);
process.exit(1);
}
}
// Start the application
if (require.main === module) {
main();
}
module.exports = {
LogMonitor,
EmailSender,
EmailTemplate,
Logger
};

22
package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "ente-otp-email-monitor",
"version": "1.0.0",
"description": "OTP email monitoring service for Ente Cloudron app",
"main": "otp-email-monitor.js",
"scripts": {
"start": "node otp-email-monitor.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"nodemailer": "^6.9.0"
},
"keywords": [
"ente",
"otp",
"email",
"cloudron",
"monitoring"
],
"author": "Ente Cloudron Integration",
"license": "Apache-2.0"
}

528
start.sh
View File

@@ -47,7 +47,6 @@ log "INFO" "Creating necessary directories"
mkdir -p /app/data/ente/server mkdir -p /app/data/ente/server
mkdir -p /app/data/ente/web mkdir -p /app/data/ente/web
mkdir -p /app/data/tmp mkdir -p /app/data/tmp
mkdir -p /app/data/web/{photos,accounts,auth,cast}
# =============================================== # ===============================================
# Repository setup # Repository setup
@@ -78,55 +77,23 @@ fi
# =============================================== # ===============================================
log "INFO" "Setting up configuration" log "INFO" "Setting up configuration"
# S3 configuration # S3 configuration - HARDCODED VALUES
S3_CONFIG="/app/data/s3.env" S3_ACCESS_KEY="QZ5M3VMBUHDTIFDFCD8E"
if [ ! -f "$S3_CONFIG" ]; then S3_SECRET_KEY="pz1eHYjU1NwAbbruedc7swzCuszd57p1rGSFVzjv"
log "INFO" "Creating default S3 configuration file" S3_ENDPOINT="https://s3.eu-central-2.wasabisys.com"
cat > "$S3_CONFIG" << EOF S3_REGION="eu-central-2"
# Ente S3 Storage Configuration S3_BUCKET="ente-due-ren"
# Edit these values with your S3-compatible storage credentials
# Required settings log "INFO" "Using hardcoded S3 configuration"
S3_ACCESS_KEY=your-access-key log "INFO" "S3 Endpoint: $S3_ENDPOINT"
S3_SECRET_KEY=your-secret-key log "INFO" "S3 Region: $S3_REGION"
S3_ENDPOINT=your-s3-endpoint # e.g., s3.amazonaws.com log "INFO" "S3 Bucket: $S3_BUCKET"
S3_REGION=your-region # e.g., us-east-1
S3_BUCKET=your-bucket-name
# Optional settings # Museum server configuration - create configurations directory structure
# S3_PREFIX=ente/ # Optional prefix for all objects MUSEUM_CONFIG_DIR="/app/data/ente/server/configurations"
# S3_PUBLIC_URL= # Optional public URL for the bucket (if different from endpoint) MUSEUM_CONFIG="$MUSEUM_CONFIG_DIR/local.yaml"
EOF mkdir -p "$MUSEUM_CONFIG_DIR"
chmod 600 "$S3_CONFIG"
log "INFO" "Created S3 config template at ${S3_CONFIG}"
log "WARN" "⚠️ YOU MUST EDIT /app/data/s3.env WITH YOUR ACTUAL S3 CREDENTIALS ⚠️"
else
log "INFO" "S3 configuration file already exists"
fi
# Load S3 configuration
if [ -f "$S3_CONFIG" ]; then
source "$S3_CONFIG"
log "INFO" "Loaded S3 configuration"
# Validate S3 configuration
if [ -z "$S3_ENDPOINT" ]; then
log "ERROR" "S3_ENDPOINT is not set. S3 storage will not work."
fi
if [ -z "$S3_ACCESS_KEY" ]; then
log "ERROR" "S3_ACCESS_KEY is not set. S3 storage will not work."
fi
if [ -z "$S3_SECRET_KEY" ]; then
log "ERROR" "S3_SECRET_KEY is not set. S3 storage will not work."
fi
if [ -z "$S3_BUCKET" ]; then
log "ERROR" "S3_BUCKET is not set. S3 storage will not work."
fi
else
log "WARN" "S3 configuration file not found at $S3_CONFIG. S3 storage may not be configured."
fi
# Museum server configuration
MUSEUM_CONFIG="/app/data/ente/server/museum.yaml"
if [ ! -f "$MUSEUM_CONFIG" ]; then if [ ! -f "$MUSEUM_CONFIG" ]; then
log "INFO" "Creating Museum server configuration" log "INFO" "Creating Museum server configuration"
cat > "$MUSEUM_CONFIG" << EOF cat > "$MUSEUM_CONFIG" << EOF
@@ -139,10 +106,12 @@ log_level: info
# Database configuration # Database configuration
db: db:
driver: postgres host: ${CLOUDRON_POSTGRESQL_HOST}
source: "postgres://${CLOUDRON_POSTGRESQL_USERNAME}:${CLOUDRON_POSTGRESQL_PASSWORD}@${CLOUDRON_POSTGRESQL_HOST}:${CLOUDRON_POSTGRESQL_PORT}/${CLOUDRON_POSTGRESQL_DATABASE}?sslmode=disable" port: ${CLOUDRON_POSTGRESQL_PORT}
max_conns: 10 name: ${CLOUDRON_POSTGRESQL_DATABASE}
max_idle: 5 user: ${CLOUDRON_POSTGRESQL_USERNAME}
password: ${CLOUDRON_POSTGRESQL_PASSWORD}
sslmode: disable
# CORS settings # CORS settings
cors: cors:
@@ -151,21 +120,29 @@ cors:
# S3 storage configuration # S3 storage configuration
s3: s3:
endpoint: "${S3_ENDPOINT:-s3.amazonaws.com}" endpoint: "${S3_ENDPOINT}"
region: "${S3_REGION:-us-east-1}" region: "${S3_REGION}"
access_key: "${S3_ACCESS_KEY}" access_key: "${S3_ACCESS_KEY}"
secret_key: "${S3_SECRET_KEY}" secret_key: "${S3_SECRET_KEY}"
bucket: "${S3_BUCKET}" bucket: "${S3_BUCKET}"
public_url: "https://${CLOUDRON_APP_FQDN}/photos" # For Wasabi, we need path style URLs
use_path_style_urls: true
are_local_buckets: false
# Email settings # Email settings
email: email:
enabled: true enabled: true
host: "${CLOUDRON_SMTP_SERVER:-localhost}" host: "${CLOUDRON_MAIL_SMTP_SERVER:-localhost}"
port: ${CLOUDRON_SMTP_PORT:-25} port: ${CLOUDRON_MAIL_SMTP_PORT:-25}
username: "${CLOUDRON_SMTP_USERNAME:-""}" username: "${CLOUDRON_MAIL_SMTP_USERNAME:-}"
password: "${CLOUDRON_SMTP_PASSWORD:-""}" password: "${CLOUDRON_MAIL_SMTP_PASSWORD:-}"
from: "Ente <${CLOUDRON_MAIL_FROM:-no-reply@${CLOUDRON_APP_DOMAIN}}>" from: "${CLOUDRON_MAIL_FROM:-no-reply@${CLOUDRON_APP_FQDN:-localhost}}"
# WebAuthn configuration for passkey support
webauthn:
rpid: "${CLOUDRON_APP_FQDN:-localhost}"
rporigins:
- "https://${CLOUDRON_APP_FQDN:-localhost}"
EOF EOF
chmod 600 "$MUSEUM_CONFIG" chmod 600 "$MUSEUM_CONFIG"
log "INFO" "Created Museum configuration at ${MUSEUM_CONFIG}" log "INFO" "Created Museum configuration at ${MUSEUM_CONFIG}"
@@ -201,259 +178,86 @@ USE_PLACEHOLDER=false
log "INFO" "Setting up Museum server binary" log "INFO" "Setting up Museum server binary"
# Function to validate a binary # Copy Museum binary from build location to data directory
validate_binary() { MUSEUM_BUILD_BIN="/app/museum-bin/museum"
local bin_path="$1" log "INFO" "Checking for pre-built Museum binary at: $MUSEUM_BUILD_BIN"
if [ -f "$MUSEUM_BUILD_BIN" ]; then
# Basic file existence check log "INFO" "Found pre-built Museum binary, copying to data directory"
if [ ! -f "$bin_path" ]; then cp "$MUSEUM_BUILD_BIN" "$MUSEUM_BIN"
return 1 chmod +x "$MUSEUM_BIN"
fi log "INFO" "Copied Museum binary to $MUSEUM_BIN"
else
# Check if file is executable log "WARN" "Pre-built Museum binary not found at $MUSEUM_BUILD_BIN"
if [ ! -x "$bin_path" ]; then
chmod +x "$bin_path" || return 1
fi
# Check if it's a text file (most likely an error message)
if file "$bin_path" | grep -q "text"; then
return 1
fi
# Check if it's a valid binary type
if ! file "$bin_path" | grep -q -E "ELF|Mach-O|PE32"; then
return 1
fi
return 0
}
# Check and remove invalid binary
if [ -f "$MUSEUM_BIN" ]; then
if ! validate_binary "$MUSEUM_BIN"; then
log "WARN" "Found invalid Museum binary, removing"
rm -f "$MUSEUM_BIN"
else
log "INFO" "Found valid Museum binary"
fi
fi fi
# Build or download if needed # Copy migration files to Museum working directory
if [ ! -f "$MUSEUM_BIN" ]; then MUSEUM_MIGRATIONS_DIR="/app/data/ente/server/migrations"
# Try building first if Go is available REPO_MIGRATIONS_DIR="/app/data/ente/repository/server/migrations"
if command -v go >/dev/null 2>&1; then if [ ! -d "$MUSEUM_MIGRATIONS_DIR" ] && [ -d "$REPO_MIGRATIONS_DIR" ]; then
log "INFO" "Go is available, attempting to build Museum server" log "INFO" "Copying database migration files"
cp -r "$REPO_MIGRATIONS_DIR" "$MUSEUM_MIGRATIONS_DIR"
cd "$ENTE_REPO_DIR/server" log "INFO" "Copied migration files to $MUSEUM_MIGRATIONS_DIR"
export GOPATH="/app/data/go" else
export PATH="$GOPATH/bin:$PATH" log "INFO" "Migration files already exist or source not available"
mkdir -p "$GOPATH/src" "$GOPATH/bin" "$GOPATH/pkg" fi
# Install dependencies if needed # Copy web templates to Museum working directory
if command -v apt-get >/dev/null 2>&1; then MUSEUM_WEB_TEMPLATES_DIR="/app/data/ente/server/web-templates"
log "INFO" "Installing build dependencies" REPO_WEB_TEMPLATES_DIR="/app/data/ente/repository/server/web-templates"
apt-get update -y && apt-get install -y gcc libsodium-dev pkg-config if [ ! -d "$MUSEUM_WEB_TEMPLATES_DIR" ] && [ -d "$REPO_WEB_TEMPLATES_DIR" ]; then
fi log "INFO" "Copying web templates"
cp -r "$REPO_WEB_TEMPLATES_DIR" "$MUSEUM_WEB_TEMPLATES_DIR"
log "INFO" "Building Museum server..." log "INFO" "Copied web templates to $MUSEUM_WEB_TEMPLATES_DIR"
if go build -o "$MUSEUM_BIN" ./cmd/museum; then else
if validate_binary "$MUSEUM_BIN"; then log "INFO" "Web templates already exist or source not available"
log "INFO" "Successfully built Museum server" fi
else
log "ERROR" "Build completed but resulted in an invalid binary" # Check if Museum binary exists and is valid
rm -f "$MUSEUM_BIN" log "INFO" "Checking for Museum binary at: $MUSEUM_BIN"
fi if [ -f "$MUSEUM_BIN" ]; then
else log "INFO" "Museum binary file exists"
log "ERROR" "Failed to build Museum server" if [ -x "$MUSEUM_BIN" ]; then
fi log "INFO" "Museum binary is executable"
# Since Museum's --help and --version commands trigger full startup (including DB migration),
# we'll trust that an existing executable binary should work
log "INFO" "Museum binary is ready to use"
else else
log "INFO" "Go is not available, skipping build attempt" log "INFO" "Museum binary exists but is not executable, fixing permissions"
fi chmod +x "$MUSEUM_BIN"
if [ -x "$MUSEUM_BIN" ]; then
# If build failed or wasn't attempted, try downloading log "INFO" "Fixed permissions, Museum binary is ready to use"
if [ ! -f "$MUSEUM_BIN" ] || ! validate_binary "$MUSEUM_BIN"; then else
log "INFO" "Attempting to download pre-built Museum server binary" log "WARN" "Failed to fix permissions, using placeholder"
# Determine architecture
ARCH=$(uname -m)
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
# Map architecture to standard names
if [ "$ARCH" = "x86_64" ]; then ARCH="amd64"; fi
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then ARCH="arm64"; fi
log "INFO" "Detected system: $OS-$ARCH"
# Define possible download URLs
DOWNLOAD_URLS=(
"https://github.com/ente-io/ente/releases/latest/download/museum-${OS}-${ARCH}"
"https://github.com/ente-io/ente/releases/download/latest/museum-${OS}-${ARCH}"
"https://github.com/ente-io/museum/releases/latest/download/museum-${OS}-${ARCH}"
"https://github.com/ente-io/museum/releases/download/latest/museum-${OS}-${ARCH}"
"https://github.com/ente-io/ente/releases/download/v0.9.0/museum-${OS}-${ARCH}"
"https://github.com/ente-io/museum/releases/download/v0.9.0/museum-${OS}-${ARCH}"
)
# Try each URL
SUCCESS=false
for URL in "${DOWNLOAD_URLS[@]}"; do
log "INFO" "Attempting download from $URL"
if curl -L -f -s -o "$MUSEUM_BIN.tmp" "$URL"; then
chmod +x "$MUSEUM_BIN.tmp"
if validate_binary "$MUSEUM_BIN.tmp"; then
mv "$MUSEUM_BIN.tmp" "$MUSEUM_BIN"
log "INFO" "Successfully downloaded Museum server binary"
SUCCESS=true
break
else
log "WARN" "Downloaded file is not a valid binary"
rm -f "$MUSEUM_BIN.tmp"
fi
else
log "WARN" "Failed to download from $URL"
fi
done
if [ "$SUCCESS" = false ]; then
log "ERROR" "All download attempts failed"
USE_PLACEHOLDER=true USE_PLACEHOLDER=true
fi fi
fi fi
fi else
log "WARN" "Museum binary file not found at $MUSEUM_BIN"
# Final check for Museum binary log "INFO" "Checking directory contents: $(ls -la $(dirname $MUSEUM_BIN) 2>/dev/null || echo 'Directory not found')"
if [ ! -f "$MUSEUM_BIN" ] || ! validate_binary "$MUSEUM_BIN"; then
log "WARN" "No valid Museum binary available"
USE_PLACEHOLDER=true USE_PLACEHOLDER=true
fi fi
# =============================================== # ===============================================
# Web Application Setup # Web Application Setup
# =============================================== # ===============================================
log "INFO" "Setting up web applications" log "INFO" "Web applications are pre-built and available in /app/web/"
# Function to create a placeholder page # Fix API endpoint configuration in built JavaScript files
create_placeholder_page() { log "INFO" "Updating API endpoint configuration in web apps"
local app_name="$1" ACTUAL_ENDPOINT="https://${CLOUDRON_APP_DOMAIN}/api"
local app_dir="/app/data/web/$app_name" log "INFO" "Setting API endpoint to: $ACTUAL_ENDPOINT"
mkdir -p "$app_dir"
cat > "$app_dir/index.html" << EOF
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ente $app_name</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 650px;
margin: 0 auto;
padding: 20px;
}
.container {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 30px;
margin-top: 50px;
}
h1 {
color: #000;
margin-top: 0;
font-weight: 600;
}
.logo {
text-align: center;
margin-bottom: 20px;
}
.logo img {
max-width: 140px;
}
.alert {
background-color: #f8f9fa;
border-left: 4px solid #2196F3;
padding: 15px;
margin-bottom: 20px;
}
.alert-warn {
border-color: #ff9800;
}
.setup-box {
background-color: #f5f5f5;
padding: 20px;
border-radius: 5px;
margin-top: 20px;
}
code {
background-color: #f1f1f1;
padding: 2px 5px;
border-radius: 3px;
font-family: monospace;
font-size: 0.9em;
}
a {
color: #2196F3;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<div class="logo">
<img src="https://raw.githubusercontent.com/ente-io/ente/main/web/packages/photos/public/images/logo_vertical.svg" alt="Ente Logo">
</div>
<h1>Ente $app_name</h1>
<div class="alert">
<strong>Status:</strong> The Ente server is being configured.
</div>
<p> # Replace placeholder endpoint in all JavaScript files
This is the Ente $app_name application running on your Cloudron. To complete the setup: for webapp in photos accounts auth cast; do
</p> WEB_DIR="/app/web/${webapp}"
if [ -d "$WEB_DIR" ]; then
<div class="setup-box"> log "INFO" "Processing ${webapp} app"
<ol> # Find and replace the placeholder endpoint in all JS files
<li>Configure your S3 storage in <code>/app/data/s3.env</code></li> find "$WEB_DIR" -name "*.js" -type f -exec sed -i "s|https://example.com/api|${ACTUAL_ENDPOINT}|g" {} \;
<li>Ensure the Museum server is properly running</li> log "INFO" "Updated endpoint configuration for ${webapp}"
<li>You might need to restart the app after configuration changes</li> else
</ol> log "WARN" "Web directory not found for ${webapp}"
</div>
<p style="margin-top: 30px; text-align: center;">
<a href="https://github.com/ente-io/ente" target="_blank">GitHub Repository</a> &middot;
<a href="https://help.ente.io" target="_blank">Documentation</a>
</p>
</div>
</body>
</html>
EOF
# Create runtime config
cat > "$app_dir/runtime-config.js" << EOF
window.RUNTIME_CONFIG = {
API_URL: "/api",
PUBLIC_ALBUMS_URL: "/public",
DEBUG: true
};
console.log("Loaded Ente runtime config:", window.RUNTIME_CONFIG);
EOF
log "INFO" "Created placeholder for $app_name app"
}
# Create placeholder pages for each app if they don't exist
for APP in photos accounts auth cast; do
if [ ! -f "/app/data/web/$APP/index.html" ]; then
create_placeholder_page "$APP"
fi fi
done done
@@ -777,7 +581,7 @@ if [ "$USE_PLACEHOLDER" = true ]; then
else else
log "INFO" "Starting actual Museum server" log "INFO" "Starting actual Museum server"
cd /app/data/ente/server cd /app/data/ente/server
"$MUSEUM_BIN" --config "$MUSEUM_CONFIG" > "$MUSEUM_LOG" 2>&1 & "$MUSEUM_BIN" > "$MUSEUM_LOG" 2>&1 &
MUSEUM_PID=$! MUSEUM_PID=$!
log "INFO" "Started Museum server with PID: $MUSEUM_PID" log "INFO" "Started Museum server with PID: $MUSEUM_PID"
@@ -821,60 +625,130 @@ cat > "$CADDY_CONFIG" << EOF
:3080 { :3080 {
log { log {
output file /app/data/logs/caddy.log output file /app/data/logs/caddy.log
level INFO
} }
# Static web apps # Enable compression
handle_path /photos/* { encode gzip
root * /app/data/web/photos
try_files {path} /index.html # CORS preflight handling
file_server @options {
method OPTIONS
} }
handle @options {
handle_path /accounts/* { header {
root * /app/data/web/accounts Access-Control-Allow-Origin "*"
try_files {path} /index.html Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
file_server Access-Control-Allow-Headers "*"
} Access-Control-Max-Age "3600"
}
handle_path /auth/* { respond 204
root * /app/data/web/auth
try_files {path} /index.html
file_server
}
handle_path /cast/* {
root * /app/data/web/cast
try_files {path} /index.html
file_server
} }
# API endpoints # API endpoints with CORS
handle /api/* { handle /api/* {
reverse_proxy localhost:8080 reverse_proxy localhost:8080 {
header_up Host {http.request.host}
header_up X-Real-IP {http.request.remote}
header_up X-Forwarded-For {http.request.remote}
header_up X-Forwarded-Proto {http.request.scheme}
}
header {
Access-Control-Allow-Origin "*"
Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Access-Control-Allow-Headers "*"
Access-Control-Allow-Credentials "true"
}
} }
# Public albums endpoint # Public albums endpoint
handle /public/* { handle /public/* {
reverse_proxy localhost:8080 reverse_proxy localhost:8080
header {
Access-Control-Allow-Origin "*"
}
} }
# Health check endpoint # Health check endpoint
handle /health { handle /health {
reverse_proxy localhost:8080 reverse_proxy localhost:8080
} }
# Static files for Next.js assets from all apps
handle /_next/* {
@photosNext path /_next/*
handle @photosNext {
root * /app/web/photos
file_server
}
header {
Cache-Control "public, max-age=31536000"
Access-Control-Allow-Origin "*"
}
}
# Photos app
handle_path /photos/* {
root * /app/web/photos
try_files {path} /index.html
file_server
}
# Redirect root to photos # Accounts app
handle { handle_path /accounts/* {
redir / /photos/ root * /app/web/accounts
try_files {path} /index.html
file_server
}
# Auth app
handle_path /auth/* {
root * /app/web/auth
try_files {path} /index.html
file_server
}
# Cast app
handle_path /cast/* {
root * /app/web/cast
try_files {path} /index.html
file_server
}
# Root redirect
handle / {
redir /photos/ permanent
} }
} }
EOF EOF
log "INFO" "Validating Caddy configuration"
if caddy validate --config "$CADDY_CONFIG" 2>&1 | tee -a "$LOG_FILE"; then
log "INFO" "Caddy configuration is valid"
else
log "ERROR" "Caddy configuration validation failed!"
log "ERROR" "Caddyfile contents:"
cat "$CADDY_CONFIG" | while read -r line; do
log "ERROR" " $line"
done
fi
log "INFO" "Starting Caddy web server" log "INFO" "Starting Caddy web server"
caddy run --config "$CADDY_CONFIG" > /app/data/logs/caddy.log 2>&1 & caddy run --config "$CADDY_CONFIG" > /app/data/logs/caddy.log 2>&1 &
CADDY_PID=$! CADDY_PID=$!
log "INFO" "Caddy web server started with PID: $CADDY_PID" log "INFO" "Caddy web server started with PID: $CADDY_PID"
# Wait a moment to see if Caddy stays running
sleep 2
if ps -p $CADDY_PID > /dev/null; then
log "INFO" "Caddy is still running after 2 seconds"
else
log "ERROR" "Caddy has crashed! Last 20 lines of Caddy log:"
tail -n 20 /app/data/logs/caddy.log | while read -r line; do
log "ERROR" " $line"
done
fi
# =============================================== # ===============================================
# Finalization and monitoring # Finalization and monitoring
# =============================================== # ===============================================