2 Commits

Author SHA1 Message Date
Your Name
fc82e988e9 Update CloudronManifest version to 1.0.1
Increment version after multiple iterations of S3 configuration fixes and port conflict resolution.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-21 20:37:07 -06:00
Your Name
5068e12025 Fix port conflict between Museum server and Caddy
- Changed Museum server to run on port 8080 instead of 3080
- Updated all health check URLs to use port 8080
- Updated Caddy reverse proxy to forward API requests to port 8080
- Added clarifying comment about port usage

This resolves the circular reference where both Caddy and Museum were trying to use port 3080.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-21 17:18:51 -06:00
21 changed files with 1061 additions and 2300 deletions

View File

@@ -1,34 +0,0 @@
# Ente Cloudron Quick Guide
## Build
```bash
git clone https://github.com/andreasdueren/ente-cloudron.git
cd ente-cloudron
cloudron build \
--set-build-service builder.docker.due.ren \
--build-service-token e3265de06b1d0e7bb38400539012a8433a74c2c96a17955e \
--set-repository andreasdueren/ente-cloudron \
--tag 0.4.5
```
## Install
```bash
cloudron install \
--location ente.due.ren \
--image andreasdueren/ente-cloudron:0.4.5
```
## After Install
1. **S3** In Cloudron File Manager open `/app/data/config/s3.env`, fill in your endpoint/region/bucket/access/secret, then restart the app from the dashboard.
2. **Subdomains** In the apps *Domains* tab add aliases for `accounts`, `auth`, `cast`, `albums`, `family`. Create matching DNS records pointing at the primary domain (e.g. if the app is `ente.due.ren`, add `accounts.due.ren → ente.due.ren`, etc.).
Once DNS propagates, use the dedicated hosts:
- `https://<app-host>` (the hostname you chose during install, main UI & uploads)
- `https://accounts.<domain>`
- `https://auth.<domain>`
- `https://cast.<domain>`
- `https://albums.<domain>`
- `https://family.<domain>`
Check `cloudron logs --app ente.due.ren -f` or `/app/data/logs/startup.log` if anything looks off.

View File

@@ -1,37 +1,5 @@
# Changelog # Changelog
## 0.4.5 (2025-10-30)
* Serve photos UI on the primary hostname and mount other apps on `accounts/auth/cast/albums/family.<root-domain>`
* Enable multiDomain in the manifest so aliases can be set in Cloudron UI
* Simplified documentation for S3 setup and alias domains
* Fix CORS responses for auth subdomains and forward real client IPs from Cloudron proxy
* Remove unsupported Caddy `trusted_proxies` stanza while continuing to trust Cloudron-provided `X-Forwarded-For` headers for accurate logging
## 0.4.4 (2025-10-30)
* Restore Cloudflare R2 path-style URLs and simplify to a single hot-storage data center
* Serve the frontend apps on dedicated subdomains (photos/accounts/auth/cast/albums/family)
* Startup script now regenerates Caddy and Museum configs for the new host layout
* Added post-install checklist entries and updated docs for required DNS records
## 0.4.3 (2025-10-29)
* Always regenerate Museum configuration on startup to pick up S3 credential changes
* Enables seamless workflow: add S3 credentials to /app/data/config/s3.env and restart
* Fixes issue where S3 configuration changes required manual intervention
## 0.4.2 (2025-10-29)
* Use SMTPS (port 2465) with TLS encryption for email delivery
* Fixes email sending with requiresValidCertificate flag on Cloudron 9
## 0.4.1 (2025-10-23)
* Fix email sending for user registration by enabling TLS certificate validation in sendmail addon
* Add requiresValidCertificate flag to sendmail configuration to ensure proper SMTP authentication with Go applications
* Note: Requires Cloudron 9 or later for requiresValidCertificate support
## 1.0.0 (2024-06-01) ## 1.0.0 (2024-06-01)
* Initial release of Ente for Cloudron * Initial release of Ente for Cloudron

158
CLAUDE.md
View File

@@ -1,158 +0,0 @@
Cloudron Application Packaging System Prompt
You are a Cloudron packaging expert specializing in creating complete, production-ready Cloudron packages. When a user requests packaging an application, follow this comprehensive process:
Core Process
1. Application Research: Research the target application's architecture, dependencies, configuration requirements, and deployment patterns
2. Package Generation: Create all required Cloudron packaging files
3. Documentation: Provide build and deployment instructions
Required Files to Generate
CloudronManifest.json
- Use reverse-domain notation for app ID (e.g., io.example.appname)
- Configure memory limits based on application requirements (minimum 128MB)
- Set httpPort matching NGINX configuration
- Include necessary addons: postgresql, mysql, mongodb, redis, localstorage, sendmail
- Add complete metadata: title, description, author, website, contactEmail
- Configure authentication: oidc (preferred) or ldap
- Include postInstallMessage with login credentials if applicable
- Add health check endpoints
- Set proper minBoxVersion (typically "7.0.0")
Dockerfile
- Base image: FROM cloudron/base:5.0.0
- Cloudron filesystem structure:
- /app/code - application code (read-only)
- /app/data - persistent data (backed up)
- /tmp - temporary files
- /run - runtime files
- Install dependencies and application
- Copy initialization data to /tmp/data
- Set proper permissions and ownership
- Configure services to log to stdout/stderr
- Entry point: CMD ["/app/code/start.sh"]
start.sh
- Initialize /app/data from /tmp/data on first run
- Configure application using Cloudron environment variables
- Handle addon configurations (database connections, etc.)
- Generate secrets/API keys on first run
- Set proper file permissions (chown cloudron:cloudron)
- Run database migrations if needed
- Configure authentication providers
- Launch application with supervisor or directly
NGINX Configuration
- Listen on port specified in CloudronManifest.json
- Handle proxy headers properly:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
- Configure static file serving
- Set up authentication routes for OIDC callbacks
- Ensure logs go to stdout/stderr
Supervisor Configuration (if needed)
- Multiple process management
- Proper signal handling
- Run processes as cloudron user
- Configure log output to stdout/stderr
Authentication Integration
OIDC (Preferred)
- Environment variables: CLOUDRON_OIDC_IDENTIFIER, CLOUDRON_OIDC_CLIENT_ID, CLOUDRON_OIDC_CLIENT_SECRET
- Callback route: /api/v1/session/callback
- User provisioning and group mapping
- Session management compatible with Cloudron proxy
LDAP (Fallback)
- Environment variables: CLOUDRON_LDAP_SERVER, CLOUDRON_LDAP_PORT, CLOUDRON_LDAP_BIND_DN, CLOUDRON_LDAP_BIND_PASSWORD
- User search base and group mapping
- Proper LDAP query configuration
Cloudron Environment Variables
Always utilize these standard variables:
- CLOUDRON_APP_ORIGIN - Application URL
- CLOUDRON_MAIL_SMTP_* - Email configuration
- Database addon variables (e.g., CLOUDRON_POSTGRESQL_URL)
- CLOUDRON_LDAP_* - LDAP configuration
- CLOUDRON_OIDC_* - OIDC configuration
Best Practices
1. Security: Never expose secrets, use environment variables
2. Persistence: Store data in /app/data, initialize from /tmp/data
3. Updates: Handle schema migrations and configuration updates
4. Logging: All logs to stdout/stderr for Cloudron log aggregation
5. Health Checks: Implement endpoints for monitoring
6. Process Management: Use supervisor for multi-process applications
7. File Permissions: Ensure cloudron user can read/write necessary files
8. Building: use the cloudron build service under builder.docker.due.ren
9. Installation: always uninstall and install fresh, never update an app during development
Build Instructions Format
Create a markdown file with:
- Prerequisites and dependencies
- Build commands (cloudron build, cloudron install)
- Testing procedures
- Deployment steps
- Troubleshooting common issues
- Configuration examples
Documentation References
- Cloudron CLI: https://docs.cloudron.io/packaging/cli/
- Packaging Tutorial: https://docs.cloudron.io/packaging/tutorial/
- Manifest Reference: https://docs.cloudron.io/packaging/manifest/
- Addons Guide: https://docs.cloudron.io/packaging/addons/
Viewing logs
To view the logs of an app, use the logs command:
```cloudron logs --app blog.example.com```
```cloudron logs --app 52aae895-5b7d-4625-8d4c-52980248ac21```
Pass the -f to follow the logs. Note that not all apps log to stdout/stderr. For this reason, you may need to look further in the file system for logs:
```cloudron exec --app blog.example.com # shell into the app's file system```
``# tail -f /run/wordpress/wp-debug.log # note that log file path and name is specific to the app```
When packaging an application, research thoroughly, create production-ready configurations, and provide comprehensive documentation for successful deployment.
Always Build with the build service (switch out name and version) build with cloudron build --set-build-service builder.docker.due.ren --build-service-token
e3265de06b1d0e7bb38400539012a8433a74c2c96a17955e --set-repository andreasdueren/ente-cloudron --tag 0.1.0
cloudron install --location ente.due.ren --image andreasdueren/ente-cloudron:0.1.0
After install and build, dont wait more than 30 seconds for feedback. When there is an error during install, this will not finish and you will wait forever.
Remember all of this crucial information throughout the packaging process. Create a file for persistency if necessary to poll from later. Fix this packaging of ente for cloudron:
https://github.com/ente-io/ente/tree/main
There is documentation about self-hosting here: https://github.com/ente-io/ente/tree/main/docs/docs/self-hosting and here https://github.com/ente-io/ente/tree/main/server
Use Caddy as a reverse proxy. More info on setting it up: https://help.ente.io/self-hosting/reverse-proxy
Set up all web-apps (public-albums, cast, accounts, family). Use a path (/albums, /cast…) and not sub domains.: https://help.ente.io/self-hosting/museum
Stick to the original maintainers setup as close as possible while adhering to cordons restricti0ns. Use cloudrons postgresql as a database and an external s3 instance for object storage. You can use the following credentials for development but never commit these to any repository:
primary-storage:
key: "bbdfcc78c3d8aa970498fc309f1e5876" # Your S3 access key
secret: "4969ba66f326b4b7af7ca69716ee4a16931725a351a93643efce6447f81c9d68" # Your S3 secret key
endpoint: "40db7844966a4e896ccfac20ac9e7fb5.r2.cloudflarestorage.com" # S3 endpoint URL
region: "wnam" # S3 region (e.g. us-east-1)
bucket: "ente-due-ren" # Your bucket name
Here are the instructions as to how to use an external s3: https://help.ente.io/self-hosting/guides/external-s3

View File

@@ -1,21 +0,0 @@
{
admin off
auto_https off
}
:3080 {
log {
output stdout
level DEBUG
}
# Simple health check that always works
handle /health {
respond "{\"status\": \"OK\"}" 200
}
# Catch-all for debugging
handle {
respond "Caddy is running on port 3080" 200
}
}

View File

@@ -1,42 +1,37 @@
{ {
"id": "io.ente.cloudronapp", "id": "io.ente.cloudronapp",
"title": "Ente", "title": "Ente",
"author": "Ente Development Team", "author": "Ente Authors",
"description": "file://DESCRIPTION.md", "description": "file://DESCRIPTION.md",
"changelog": "file://CHANGELOG.md", "changelog": "file://CHANGELOG.md",
"contactEmail": "contact@ente.io", "contactEmail": "contact@ente.io",
"website": "https://ente.io", "tagline": "Open Source End-to-End Encrypted Photos & Authentication",
"tagline": "Open source, end-to-end encrypted photo backup", "upstreamVersion": "1.0.0",
"version": "0.4.5", "version": "1.0.1",
"upstreamVersion": "git-main",
"healthCheckPath": "/health", "healthCheckPath": "/health",
"httpPort": 3080, "httpPort": 3080,
"memoryLimit": 1610612736, "memoryLimit": 1073741824,
"postInstallMessage": "file://POSTINSTALL.md",
"multiDomain": true,
"addons": { "addons": {
"localstorage": {}, "localstorage": {},
"postgresql": {}, "postgresql": {},
"sendmail": { "sendmail": {
"supportsDisplayName": true, "supportsDisplayName": true
"requiresValidCertificate": true
} }
}, },
"checklist": { "checklist": {
"configure-object-storage": { "create-permanent-admin": {
"message": "Configure your S3-compatible storage in /app/data/config/s3.env before first use." "message": "Required: S3 Storage Configuration!"
},
"configure-subdomains": {
"message": "Create DNS records and add Cloudron aliases for accounts., auth., cast., albums. and family. (using the base domain of this app)."
} }
}, },
"icon": "file://logo.png", "icon": "file://logo.png",
"tags": [ "tags": [
"photos", "photos",
"encryption", "authentication",
"backup", "e2ee",
"self-hosting" "encryption"
], ],
"manifestVersion": 2, "manifestVersion": 2,
"minBoxVersion": "8.1.0" "minBoxVersion": "8.1.0",
"website": "https://ente.io"
} }

View File

@@ -1,112 +1,152 @@
# syntax=docker/dockerfile:1 FROM node:20-bookworm-slim as web-builder
ARG ENTE_GIT_REF=main WORKDIR /ente
FROM debian:bookworm AS ente-source # Clone the repository for web app building
ARG ENTE_GIT_REF RUN apt-get update && apt-get install -y git && \
RUN apt-get update && \ git clone --depth=1 https://github.com/ente-io/ente.git . && \
apt-get install -y --no-install-recommends ca-certificates git && \ apt-get clean && apt-get autoremove && \
git clone --depth=1 --branch "${ENTE_GIT_REF}" https://github.com/ente-io/ente.git /src && \ rm -rf /var/cache/apt /var/lib/apt/lists
rm -rf /var/lib/apt/lists/*
FROM golang:1.24-bookworm AS museum-builder # Will help default to yarn version 1.22.22
COPY --from=ente-source /src /ente
WORKDIR /ente/server
RUN apt-get update && \
apt-get install -y --no-install-recommends build-essential pkg-config libsodium-dev && \
rm -rf /var/lib/apt/lists/*
RUN mkdir -p /build/museum && \
CGO_ENABLED=1 GOOS=linux go build -o /build/museum/museum ./cmd/museum && \
for dir in migrations web-templates mail-templates assets; do \
rm -rf "/build/museum/$dir"; \
if [ -d "$dir" ]; then \
cp -r "$dir" "/build/museum/$dir"; \
else \
mkdir -p "/build/museum/$dir"; \
fi; \
done
FROM golang:1.24-bookworm AS cli-builder
COPY --from=ente-source /src /ente
WORKDIR /ente/cli
RUN go build -o /build/ente .
FROM node:20-bookworm-slim AS web-builder
ENV NEXT_PUBLIC_ENTE_ENDPOINT=ENTE_API_ORIGIN_PLACEHOLDER
ENV NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT=https://albums.localhost.invalid
COPY --from=ente-source /src /ente
WORKDIR /ente/web
RUN apt-get update && \
apt-get install -y --no-install-recommends build-essential python3 && \
rm -rf /var/lib/apt/lists/*
RUN corepack enable RUN corepack enable
RUN yarn install --network-timeout 1000000
RUN mkdir -p /build/web/photos /build/web/accounts /build/web/auth /build/web/cast /build/web/albums /build/web/family # Set environment variables for web app build
RUN set -e; \ # Use "/api" as the endpoint which will be replaced at runtime with the full URL
yarn build:photos; \ ENV NEXT_PUBLIC_ENTE_ENDPOINT="/api"
yarn build:accounts; \ # Add a note for clarity
yarn build:auth; \ RUN echo "Building with NEXT_PUBLIC_ENTE_ENDPOINT=/api, will be replaced at runtime with full URL"
yarn build:cast
RUN if [ -d "apps" ]; then \ # Debugging the repository structure
for app in photos accounts auth cast; do \ RUN find . -type d -maxdepth 3 | sort
if [ -d "apps/${app}/out" ]; then \
rm -rf "/build/web/${app}"; \ # Check if web directory exists with apps subdirectory
mkdir -p "/build/web/${app}"; \ RUN mkdir -p /build/web/photos /build/web/accounts /build/web/auth /build/web/cast && \
cp -r "apps/${app}/out/." "/build/web/${app}/"; \ if [ -d "web" ] && [ -d "web/apps" ]; then \
else \ echo "Found web/apps directory, building web apps"; \
printf 'Missing build output for %s\n' "${app}"; \ cd web && \
printf '<html><body><h1>Ente %s</h1><p>Build output missing.</p></body></html>\n' "${app}" > "/build/web/${app}/index.html"; \ yarn cache clean && \
fi; \ yarn install --network-timeout 1000000000 && \
done; \ yarn build:photos && \
else \ yarn build:accounts && \
for app in photos accounts auth cast; do \ yarn build:auth && \
printf '<html><body><h1>Ente %s</h1><p>Build output missing.</p></body></html>\n' "${app}" > "/build/web/${app}/index.html"; \ yarn build:cast && \
done; \ if [ -d "apps/photos/out" ]; then \
cp -r apps/photos/out/* /build/web/photos/; \
fi && \ fi && \
rm -rf /build/web/albums /build/web/family && \ if [ -d "apps/accounts/out" ]; then \
cp -r /build/web/photos /build/web/albums && \ cp -r apps/accounts/out/* /build/web/accounts/; \
cp -r /build/web/photos /build/web/family fi && \
if [ -d "apps/auth/out" ]; then \
cp -r apps/auth/out/* /build/web/auth/; \
fi && \
if [ -d "apps/cast/out" ]; then \
cp -r apps/cast/out/* /build/web/cast/; \
fi; \
elif [ -d "web" ]; then \
echo "Found web directory, looking for alternative structure"; \
find web -type d | grep -v node_modules | sort; \
if [ -d "web/photos" ]; then \
echo "Building photos app"; \
cd web/photos && yarn install && yarn build && \
if [ -d "out" ]; then cp -r out/* /build/web/photos/; fi; \
fi; \
if [ -d "web/accounts" ]; then \
echo "Building accounts app"; \
cd web/accounts && yarn install && yarn build && \
if [ -d "out" ]; then cp -r out/* /build/web/accounts/; fi; \
fi; \
if [ -d "web/auth" ]; then \
echo "Building auth app"; \
cd web/auth && yarn install && yarn build && \
if [ -d "out" ]; then cp -r out/* /build/web/auth/; fi; \
fi; \
if [ -d "web/cast" ]; then \
echo "Building cast app"; \
cd web/cast && yarn install && yarn build && \
if [ -d "out" ]; then cp -r out/* /build/web/cast/; fi; \
fi; \
else \
echo "Web directory not found, creating placeholder web pages"; \
# Create placeholder HTML files for each app \
mkdir -p /build/web/photos /build/web/accounts /build/web/auth /build/web/cast; \
echo "<html><body><h1>Ente Photos</h1><p>Web app not available. Please check the build logs.</p></body></html>" > /build/web/photos/index.html; \
echo "<html><body><h1>Ente Accounts</h1><p>Web app not available. Please check the build logs.</p></body></html>" > /build/web/accounts/index.html; \
echo "<html><body><h1>Ente Auth</h1><p>Web app not available. Please check the build logs.</p></body></html>" > /build/web/auth/index.html; \
echo "<html><body><h1>Ente Cast</h1><p>Web app not available. Please check the build logs.</p></body></html>" > /build/web/cast/index.html; \
fi
FROM cloudron/base:5.0.0@sha256:04fd70dbd8ad6149c19de39e35718e024417c3e01dc9c6637eaf4a41ec4e596c FROM cloudron/base:5.0.0@sha256:04fd70dbd8ad6149c19de39e35718e024417c3e01dc9c6637eaf4a41ec4e596c
ENV APP_DIR=/app/code \ # Install necessary packages and Caddy webserver
DATA_DIR=/app/data \
HOME=/app/data/home
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends ca-certificates curl jq libsodium23 pkg-config postgresql-client caddy openssl && \ apt-get install -y curl git nodejs npm libsodium23 libsodium-dev pkg-config postgresql-client && \
rm -rf /var/lib/apt/lists/* npm install -g yarn serve && \
# Install Caddy for web server
apt-get install -y debian-keyring debian-archive-keyring apt-transport-https && \
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg && \
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list && \
apt-get update && \
apt-get install -y caddy && \
apt-get clean && apt-get autoremove && \
rm -rf /var/cache/apt /var/lib/apt/lists
RUN mkdir -p /app/pkg /app/web "$HOME" && chown -R cloudron:cloudron /app /app/web "$HOME" # Install Go 1.24.1
RUN curl -L https://go.dev/dl/go1.24.1.linux-amd64.tar.gz -o go.tar.gz && \
rm -rf /usr/local/go && \
tar -C /usr/local -xzf go.tar.gz && \
rm go.tar.gz && \
ln -sf /usr/local/go/bin/go /usr/local/bin/go && \
ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt
COPY --from=ente-source /src ${APP_DIR} # Set up directory structure
RUN rm -rf ${APP_DIR}/.git RUN mkdir -p /app/code /app/data/config /app/data/caddy /app/web
RUN mkdir -p /app/museum-bin WORKDIR /app/code
COPY --from=museum-builder /build/museum/museum /app/museum-bin/museum
COPY --from=museum-builder /build/museum/migrations ${APP_DIR}/server/migrations
COPY --from=museum-builder /build/museum/web-templates ${APP_DIR}/server/web-templates
COPY --from=museum-builder /build/museum/mail-templates ${APP_DIR}/server/mail-templates
COPY --from=museum-builder /build/museum/assets ${APP_DIR}/server/assets
RUN chmod +x /app/museum-bin/museum
COPY --from=cli-builder /build/ente /app/code/ente # Clone the ente repository during build (for the Museum server)
RUN ln -sf /app/code/ente /usr/local/bin/ente && chmod +x /app/code/ente RUN git clone --depth=1 https://github.com/ente-io/ente.git . && \
sed -i 's/go 1.23/go 1.24.1/' server/go.mod && \
mkdir -p /app/data/go && \
cp -r server/go.mod server/go.sum /app/data/go/ && \
chmod 777 /app/data/go/go.mod /app/data/go/go.sum
# Pre-download Go dependencies
RUN cd server && \
export GOMODCACHE="/app/data/go/pkg/mod" && \
export GOFLAGS="-modfile=/app/data/go/go.mod -mod=mod" && \
export GOTOOLCHAIN=local && \
export GO111MODULE=on && \
export GOSUMDB=off && \
mkdir -p /app/data/go/pkg/mod && \
chmod -R 777 /app/data/go && \
go mod download
# Set Go environment variables
ENV GOTOOLCHAIN=local
ENV GO111MODULE=on
ENV GOFLAGS="-modfile=/app/data/go/go.mod -mod=mod"
ENV PATH="/usr/local/go/bin:${PATH}"
ENV GOSUMDB=off
ENV GOMODCACHE="/app/data/go/pkg/mod"
# Copy the web app built files from the first stage
COPY --from=web-builder /build/web/photos /app/web/photos COPY --from=web-builder /build/web/photos /app/web/photos
COPY --from=web-builder /build/web/accounts /app/web/accounts 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 --from=web-builder /build/web/albums /app/web/albums
COPY --from=web-builder /build/web/family /app/web/family
COPY start.sh /app/pkg/start.sh # Copy configuration and startup scripts
COPY admin-helper.sh /app/pkg/admin-helper.sh ADD start.sh /app/pkg/
COPY admin-helper-direct.sh /app/pkg/admin-helper-direct.sh ADD config.template.yaml /app/pkg/
RUN chmod +x /app/pkg/start.sh /app/pkg/admin-helper.sh /app/pkg/admin-helper-direct.sh # Set proper permissions
RUN ln -s /app/data/cli-data /cli-data RUN chmod +x /app/pkg/start.sh
EXPOSE 3080 8080 # Expose the web port (Cloudron expects port 3080)
EXPOSE 3080
# Also expose API port
EXPOSE 8080
# Start the application
CMD ["/app/pkg/start.sh"] CMD ["/app/pkg/start.sh"]

View File

@@ -1,56 +1,25 @@
Your Ente installation is almost ready! Your Ente installation is almost ready!
## Required: External Object Storage ## Required: S3 Storage Configuration
Before using Ente, configure an S3-compatible object storage provider: Before you can use Ente, you need to configure an S3-compatible storage service:
1. Open the Cloudron dashboard and select your Ente app. 1. Go to your Cloudron dashboard
2. Launch the web terminal. 2. Click on your Ente app
3. Edit `/app/data/config/s3.env` and provide values for **all** required keys: 3. Click on "Terminal"
```bash 4. Edit the S3 configuration template:
nano /app/data/config/s3.env
``` ```
4. Save the file and restart the app from the Cloudron dashboard. nano /app/data/config/s3.env.template
```
Supported variables: 5. Fill in your S3 credentials (AWS S3, MinIO, DigitalOcean Spaces, etc.)
- `S3_ENDPOINT` (e.g. `https://<account>.r2.cloudflarestorage.com`) 6. Save the file and rename it:
- `S3_REGION` ```
- `S3_BUCKET` mv /app/data/config/s3.env.template /app/data/config/s3.env
- `S3_ACCESS_KEY` ```
- `S3_SECRET_KEY` 7. Restart your Ente app from the Cloudron dashboard
- `S3_PREFIX` (optional path prefix)
## Required: DNS Subdomains
Ente now serves supporting apps on dedicated hosts. Create DNS records (CNAME or A) for:
- `accounts.<root-domain>`
- `auth.<root-domain>`
- `cast.<root-domain>`
- `albums.<root-domain>`
- `family.<root-domain>`
For example, if you installed the app at `ente.due.ren`, create records for `accounts.due.ren`, `auth.due.ren`, etc., all pointing to `ente.due.ren`. After adding the DNS records, open the Cloudron dashboard → Ente app → Domains tab and add each hostname as an alias. DNS propagation must complete before the `/accounts` and `/auth` apps will accept sessions.
## Next Steps ## Next Steps
- Visit the app URL and create the first administrator account. 1. Once S3 is configured, visit your app URL to create an admin account
- Configure the Ente mobile apps to use your custom server (`Settings → Advanced → Custom Server`). 2. Configure your mobile apps to use your custom self-hosted server (Settings → Advanced → Custom Server)
- Optional: set the environment variables `CLOUDRON_OIDC_IDENTIFIER`, `CLOUDRON_OIDC_CLIENT_ID`, and `CLOUDRON_OIDC_CLIENT_SECRET` to enable Cloudron SSO in the generated Museum config. 3. Enjoy your private, end-to-end encrypted photo storage!
- Administrative CLI tooling is available inside the container. Open a terminal and run:
```bash
cloudron exec --app ente.due.ren -- sudo -u cloudron ente --help
cloudron exec --app ente.due.ren -- sudo -u cloudron ente admin --help
```
The CLI stores its state at `/app/data/cli-data` (inside the container it is available at `/cli-data`). Consult the upstream CLI guide for per-command usage, including storage quota adjustments.
## Administration Helpers
- The Ente CLI binary is shipped at `/app/code/ente`. Run it via the Cloudron web terminal.
- CLI configuration lives at `/app/data/home/.ente/config.yaml` and already points to `https://<your-domain>/api`.
- The main Museum configuration is generated at `/app/data/museum/configurations/local.yaml`. Delete this file to regenerate it with updated environment variables.
Logs are streamed to the Cloudron dashboard. For deeper inspection use:
```bash
cloudron logs --app <location> -f
```

View File

@@ -81,15 +81,9 @@ The package includes several enhancements to ensure proper functionality:
2. **API Configuration**: Dynamic runtime configuration to ensure the frontend connects to the correct API endpoint 2. **API Configuration**: Dynamic runtime configuration to ensure the frontend connects to the correct API endpoint
3. **CORS Headers**: Proper CORS configuration for API access 3. **CORS Headers**: Proper CORS configuration for API access
### Cloudron Admin Notes You need to manually set up and configure:
After installing on Cloudron remember to: - S3-compatible object storage
1. Open the File Manager for the app, edit `/app/data/config/s3.env` with your object storage endpoint/keys, and restart the app.
2. Add alias domains for `accounts`, `auth`, `cast`, `albums`, and `family` in the apps **Domains** tab (create matching DNS records pointing to the primary hostname).
3. Use the bundled Ente CLI for admin tasks via `cloudron exec --app <location> -- sudo -u cloudron ente --help`. The CLI stores its state in `/app/data/cli-data` (exposed inside the container at `/cli-data`) and already trusts your apps API endpoint.
The main photos UI continues to live on the hostname you selected during installation.
## Usage ## Usage

View File

@@ -1,133 +0,0 @@
#!/bin/bash
# Direct Database Admin Helper for Ente Cloudron
# This script directly updates the database for admin operations
# Function to update user subscription directly in database
update_subscription() {
local user_email="$1"
local storage_gb="$2"
local valid_days="$3"
if [ -z "$user_email" ] || [ -z "$storage_gb" ] || [ -z "$valid_days" ]; then
echo "Usage: $0 update-subscription <user-email> <storage-gb> <valid-days>"
echo "Example: $0 update-subscription user@example.com 100 365"
return 1
fi
echo "Updating subscription for: $user_email"
echo "Storage: ${storage_gb}GB"
echo "Valid for: ${valid_days} days"
# Convert GB to bytes (1 GB = 1073741824 bytes)
local storage_bytes=$((storage_gb * 1073741824))
# Calculate expiry timestamp (current time + valid_days)
local current_timestamp=$(date +%s)
local expiry_timestamp=$((current_timestamp + (valid_days * 86400)))
# Convert to microseconds for the database
local expiry_microseconds="${expiry_timestamp}000000"
# Update the database directly
PGPASSWORD="$CLOUDRON_POSTGRESQL_PASSWORD" psql \
-h "$CLOUDRON_POSTGRESQL_HOST" \
-p "$CLOUDRON_POSTGRESQL_PORT" \
-U "$CLOUDRON_POSTGRESQL_USERNAME" \
-d "$CLOUDRON_POSTGRESQL_DATABASE" << EOF
-- Update user's storage and subscription
UPDATE users
SET
storage_bonus = $storage_bytes,
subscription_expiry = $expiry_microseconds
WHERE email = '$user_email';
-- Show the updated values
SELECT
email,
storage_bonus / 1073741824.0 as storage_gb,
to_timestamp(subscription_expiry / 1000000) as subscription_expires
FROM users
WHERE email = '$user_email';
EOF
if [ $? -eq 0 ]; then
echo "✓ Subscription updated successfully"
else
echo "✗ Failed to update subscription"
return 1
fi
}
# Function to get user details
get_user_details() {
local user_email="$1"
if [ -z "$user_email" ]; then
echo "Usage: $0 get-user <user-email>"
return 1
fi
PGPASSWORD="$CLOUDRON_POSTGRESQL_PASSWORD" psql \
-h "$CLOUDRON_POSTGRESQL_HOST" \
-p "$CLOUDRON_POSTGRESQL_PORT" \
-U "$CLOUDRON_POSTGRESQL_USERNAME" \
-d "$CLOUDRON_POSTGRESQL_DATABASE" << EOF
SELECT
email,
storage_bonus / 1073741824.0 as storage_gb,
storage_consumed / 1073741824.0 as used_gb,
to_timestamp(subscription_expiry / 1000000) as subscription_expires,
CASE
WHEN subscription_expiry > (EXTRACT(EPOCH FROM NOW()) * 1000000) THEN 'Active'
ELSE 'Expired'
END as status
FROM users
WHERE email = '$user_email';
EOF
}
# Function to list all users
list_users() {
PGPASSWORD="$CLOUDRON_POSTGRESQL_PASSWORD" psql \
-h "$CLOUDRON_POSTGRESQL_HOST" \
-p "$CLOUDRON_POSTGRESQL_PORT" \
-U "$CLOUDRON_POSTGRESQL_USERNAME" \
-d "$CLOUDRON_POSTGRESQL_DATABASE" << EOF
SELECT
email,
storage_bonus / 1073741824.0 as storage_gb,
storage_consumed / 1073741824.0 as used_gb,
to_timestamp(subscription_expiry / 1000000) as expires,
CASE
WHEN subscription_expiry > (EXTRACT(EPOCH FROM NOW()) * 1000000) THEN 'Active'
ELSE 'Expired'
END as status
FROM users
ORDER BY email;
EOF
}
# Main command handler
case "$1" in
"update-subscription")
update_subscription "$2" "$3" "$4"
;;
"get-user")
get_user_details "$2"
;;
"list-users")
list_users
;;
*)
echo "Ente Direct Admin Helper"
echo ""
echo "Usage:"
echo " $0 update-subscription <user-email> <storage-gb> <valid-days>"
echo " $0 get-user <user-email>"
echo " $0 list-users"
echo ""
echo "Examples:"
echo " $0 update-subscription user@example.com 100 365"
echo " $0 get-user user@example.com"
echo " $0 list-users"
;;
esac

View File

@@ -1,93 +0,0 @@
#!/bin/bash
# Ente Admin Helper Script for Cloudron
# This script simplifies admin operations in the Cloudron terminal
MUSEUM_BIN="/app/museum-bin/museum"
# Check if museum binary exists
if [ ! -f "$MUSEUM_BIN" ]; then
echo "Error: Museum binary not found at $MUSEUM_BIN"
exit 1
fi
# Function to update user subscription
update_subscription() {
local user_email="$1"
local storage_gb="$2"
local valid_days="$3"
if [ -z "$user_email" ] || [ -z "$storage_gb" ] || [ -z "$valid_days" ]; then
echo "Usage: $0 update-subscription <user-email> <storage-gb> <valid-days>"
echo "Example: $0 update-subscription user@example.com 100 365"
return 1
fi
echo "Updating subscription for: $user_email"
echo "Storage: ${storage_gb}GB"
echo "Valid for: ${valid_days} days"
cd /app/data/museum
# Use environment variables for database connection
export DB_HOST="$CLOUDRON_POSTGRESQL_HOST"
export DB_PORT="$CLOUDRON_POSTGRESQL_PORT"
export DB_NAME="$CLOUDRON_POSTGRESQL_DATABASE"
export DB_USERNAME="$CLOUDRON_POSTGRESQL_USERNAME"
export DB_PASSWORD="$CLOUDRON_POSTGRESQL_PASSWORD"
# Museum admin commands need specific syntax
"$MUSEUM_BIN" admin update-subscription "$user_email" "$storage_gb" "$valid_days"
}
# Function to get user details
get_user_details() {
local user_email="$1"
if [ -z "$user_email" ]; then
echo "Usage: $0 get-user <user-email>"
return 1
fi
cd /app/data/museum
"$MUSEUM_BIN" admin get-user-details --user "$user_email"
}
# Function to list all users
list_users() {
cd /app/data/museum
# Connect to PostgreSQL and list users
PGPASSWORD="$CLOUDRON_POSTGRESQL_PASSWORD" psql \
-h "$CLOUDRON_POSTGRESQL_HOST" \
-p "$CLOUDRON_POSTGRESQL_PORT" \
-U "$CLOUDRON_POSTGRESQL_USERNAME" \
-d "$CLOUDRON_POSTGRESQL_DATABASE" \
-c "SELECT email, storage_bonus, subscription_expiry FROM users ORDER BY email;"
}
# Main command handler
case "$1" in
"update-subscription")
update_subscription "$2" "$3" "$4"
;;
"get-user")
get_user_details "$2"
;;
"list-users")
list_users
;;
*)
echo "Ente Admin Helper"
echo ""
echo "Usage:"
echo " $0 update-subscription <user-email> <storage-gb> <valid-days>"
echo " $0 get-user <user-email>"
echo " $0 list-users"
echo ""
echo "Examples:"
echo " $0 update-subscription user@example.com 100 365"
echo " $0 get-user user@example.com"
echo " $0 list-users"
;;
esac

View File

@@ -18,20 +18,16 @@ database:
maxIdleConns: 25 maxIdleConns: 25
connMaxLifetime: "1h" connMaxLifetime: "1h"
s3: storage:
are_local_buckets: false type: "s3"
use_path_style_urls: true s3:
hot_storage:
primary: b2-eu-cen
secondary: b2-eu-cen
derived-storage: b2-eu-cen
b2-eu-cen:
endpoint: "%%S3_ENDPOINT%%" endpoint: "%%S3_ENDPOINT%%"
region: "%%S3_REGION%%" region: "%%S3_REGION%%"
bucket: "%%S3_BUCKET%%" bucket: "%%S3_BUCKET%%"
key: "%%S3_ACCESS_KEY%%" accessKey: "%%S3_ACCESS_KEY%%"
secret: "%%S3_SECRET_KEY%%" secretKey: "%%S3_SECRET_KEY%%"
path_prefix: "%%S3_PREFIX%%" prefix: "%%S3_PREFIX%%"
forcePathStyle: true
email: email:
smtp: smtp:

View File

@@ -1,35 +0,0 @@
#!/bin/bash
echo "==> Debugging Caddy MIME type headers"
echo "==> Testing various file types..."
BASE_URL="${1:-https://ente.due.ren}"
echo
echo "Testing HTML files:"
curl -I "$BASE_URL/" 2>/dev/null | grep -i content-type || echo "No Content-Type header found"
curl -I "$BASE_URL/index.html" 2>/dev/null | grep -i content-type || echo "No Content-Type header found"
echo
echo "Testing JavaScript files:"
curl -I "$BASE_URL/config.js" 2>/dev/null | grep -i content-type || echo "No Content-Type header found"
echo
echo "Testing CSS files (if any):"
curl -I "$BASE_URL/styles.css" 2>/dev/null | grep -i content-type || echo "File not found or no Content-Type header"
echo
echo "Testing JSON files (if any):"
curl -I "$BASE_URL/manifest.json" 2>/dev/null | grep -i content-type || echo "File not found or no Content-Type header"
echo
echo "==> Full response headers for main page:"
curl -I "$BASE_URL/" 2>/dev/null || echo "Failed to connect to $BASE_URL"
echo
echo "==> To test from inside a container:"
echo "docker exec -it <container-name> curl -I http://localhost:3080/"
echo
echo "==> To view Caddy logs:"
echo "docker exec -it <container-name> tail -f /app/data/logs/caddy.log"

View File

@@ -1,38 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Debug Ente Auth Network Calls</title>
</head>
<body>
<h1>Debug Ente Auth Network Calls</h1>
<div id="output"></div>
<script>
// Override fetch to log all network requests
const originalFetch = window.fetch;
window.fetch = function(...args) {
console.log('FETCH REQUEST:', args[0], args[1]);
const output = document.getElementById('output');
output.innerHTML += '<p>FETCH: ' + args[0] + '</p>';
return originalFetch.apply(this, args)
.then(response => {
console.log('FETCH RESPONSE:', response.status, response.url);
output.innerHTML += '<p>RESPONSE: ' + response.status + ' ' + response.url + '</p>';
return response;
})
.catch(error => {
console.log('FETCH ERROR:', error);
output.innerHTML += '<p>ERROR: ' + error.message + '</p>';
throw error;
});
};
// Load the Ente Auth app in an iframe to see what happens
const iframe = document.createElement('iframe');
iframe.src = 'https://ente.due.ren/auth/';
iframe.style.width = '100%';
iframe.style.height = '400px';
document.body.appendChild(iframe);
</script>
</body>
</html>

View File

@@ -1,32 +0,0 @@
#!/bin/bash
# Add this debugging section to your start.sh after line 350
# Start Caddy with more verbose logging
echo "==> Starting Caddy web server with debug logging"
echo "==> Validating Caddyfile first..."
caddy validate --config /app/data/Caddyfile --adapter caddyfile || {
echo "==> ERROR: Caddyfile validation failed!"
cat /app/data/Caddyfile
exit 1
}
echo "==> Starting Caddy..."
# Run Caddy in foreground first to see errors
timeout 10 caddy run --config /app/data/Caddyfile --adapter caddyfile 2>&1 | tee /app/data/logs/caddy-debug.log || {
echo "==> ERROR: Caddy failed to start"
echo "==> Last 50 lines of Caddy debug log:"
tail -50 /app/data/logs/caddy-debug.log
}
# Check if port is actually listening
echo "==> Checking if port 3080 is listening..."
netstat -tlnp | grep 3080 || lsof -i :3080 || {
echo "==> ERROR: Nothing listening on port 3080"
}
# Test the health endpoint
echo "==> Testing health endpoint..."
curl -v http://localhost:3080/health || {
echo "==> ERROR: Health check failed"
}

View File

@@ -1,64 +0,0 @@
# Ente CLI Configuration for Custom Server
The Ente CLI expects configuration in `~/.ente/config.yaml`. Here's how to set it up:
## Method 1: Direct Configuration
1. Create the config file:
```bash
mkdir -p ~/.ente
cat > ~/.ente/config.yaml << EOF
api:
url: https://ente.due.ren
EOF
```
2. Add your account interactively:
```bash
ente account add
# It will ask for:
# - Export directory: /tmp/ente-export (or any directory)
# - Email: your-admin@email.com
# - Password: your-password
```
## Method 2: Using the Admin Commands Directly
If the interactive setup is problematic, you can use the admin commands with explicit parameters:
```bash
# Set the API endpoint
export ENTE_API_URL="https://ente.due.ren"
# Or pass it directly in the command
ente admin update-subscription \
--api-url https://ente.due.ren \
--admin-user admin@due.ren \
--user user@example.com \
--storage 1000 \
--valid-for 365
```
## Method 3: Direct Database Update (Fallback)
Since the CLI setup seems problematic, you can update the database directly in the Cloudron terminal:
```bash
# In Cloudron terminal
PGPASSWORD="$CLOUDRON_POSTGRESQL_PASSWORD" psql \
-h "$CLOUDRON_POSTGRESQL_HOST" \
-U "$CLOUDRON_POSTGRESQL_USERNAME" \
-d "$CLOUDRON_POSTGRESQL_DATABASE" << EOF
-- Update user to 1TB for 1 year
UPDATE users
SET storage_bonus = 1073741824000, -- 1000 GB in bytes
subscription_expiry = EXTRACT(EPOCH FROM NOW() + INTERVAL '365 days') * 1000000
WHERE email = 'andreas@due.ren';
-- Show the result
SELECT email,
storage_bonus / 1073741824.0 as storage_gb,
to_timestamp(subscription_expiry / 1000000) as expires
FROM users WHERE email = 'andreas@due.ren';
EOF
```

View File

@@ -1,389 +0,0 @@
#!/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
};

View File

@@ -1,22 +0,0 @@
{
"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"
}

View File

@@ -1,20 +0,0 @@
#!/bin/bash
# Setup Ente CLI for custom server
echo "Setting up Ente CLI for custom server..."
# Create config directory
mkdir -p ~/.ente
# Create the CLI config with custom endpoint
cat > ~/.ente/config.yaml << EOF
host: https://ente.due.ren
EOF
echo "Configuration created at ~/.ente/config.yaml"
echo ""
echo "Now you can add your account:"
echo " ente account add"
echo ""
echo "Then use admin commands:"
echo " ente admin update-subscription --admin-user admin@due.ren --user user@example.com --storage 1000 --valid-for 365"

View File

@@ -1,150 +0,0 @@
#!/bin/bash
# Better signal handling - forward signals to child processes
trap 'kill -TERM $SERVER_PID; kill -TERM $CADDY_PID; exit' TERM INT
set -eu
echo "==> Starting Ente Cloudron app (DEBUG MODE)..."
# Create necessary directories
mkdir -p /app/data/config /app/data/logs /app/data/caddy
# Check if web directories exist
echo "==> Checking web app directories:"
for app in photos accounts auth cast; do
if [ -d "/app/web/$app" ]; then
echo "==> Found: /app/web/$app"
ls -la "/app/web/$app" | head -5
else
echo "==> WARNING: Missing /app/web/$app - creating placeholder"
mkdir -p "/app/web/$app"
echo "<html><body><h1>$app app placeholder</h1></body></html>" > "/app/web/$app/index.html"
fi
done
# Create a simple test Caddyfile first
echo "==> Creating simple test Caddyfile"
cat > /app/data/Caddyfile <<'EOT'
{
admin off
auto_https off
}
:3080 {
log {
output stdout
format console
level DEBUG
}
# Health check endpoint
handle /health {
header Content-Type "application/json"
respond "{\"status\": \"OK\", \"timestamp\": \"{{now | date \"2006-01-02T15:04:05Z07:00\"}}\"}" 200
}
# Test endpoint
handle /test {
respond "Caddy is working on port 3080!" 200
}
# API proxy to Museum server
handle /api/* {
uri strip_prefix /api
reverse_proxy localhost:8080 {
transport http {
read_timeout 60s
write_timeout 60s
}
# Add error handling
handle_errors {
respond "{\"error\": \"Museum server not available\"}" 503
}
}
}
# Serve web apps with fallback
handle {
root * /app/web/photos
try_files {path} {path}/ /index.html
file_server {
browse
}
}
}
EOT
# Start a simple Museum mock server for testing
echo "==> Starting mock Museum server on port 8080"
cat > /tmp/museum-mock.js <<'EOF'
const http = require('http');
const server = http.createServer((req, res) => {
console.log(`Museum mock: ${req.method} ${req.url}`);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok', path: req.url, timestamp: new Date().toISOString() }));
});
server.listen(8080, '127.0.0.1', () => {
console.log('Museum mock server running on http://127.0.0.1:8080');
});
EOF
node /tmp/museum-mock.js > /app/data/logs/museum-mock.log 2>&1 &
SERVER_PID=$!
echo "==> Mock Museum server started (PID: $SERVER_PID)"
# Wait for Museum mock to be ready
sleep 2
# Test Museum mock
echo "==> Testing Museum mock server..."
curl -s http://localhost:8080/test || echo "WARNING: Museum mock not responding"
# Validate Caddyfile
echo "==> Validating Caddyfile..."
caddy validate --config /app/data/Caddyfile --adapter caddyfile || {
echo "==> ERROR: Caddyfile validation failed!"
exit 1
}
# Start Caddy with explicit environment
echo "==> Starting Caddy web server..."
CADDY_FORMAT=console caddy run --config /app/data/Caddyfile --adapter caddyfile 2>&1 | tee /app/data/logs/caddy-combined.log &
CADDY_PID=$!
echo "==> Caddy started (PID: $CADDY_PID)"
# Wait for Caddy to start
echo "==> Waiting for Caddy to start..."
for i in {1..30}; do
if curl -s http://localhost:3080/health > /dev/null; then
echo "==> Caddy is responding!"
break
fi
echo -n "."
sleep 1
done
echo
# Check process status
echo "==> Process status:"
ps aux | grep -E "(caddy|node)" | grep -v grep || echo "No processes found"
# Check port status
echo "==> Port status:"
netstat -tlnp 2>/dev/null | grep -E "(3080|8080)" || lsof -i :3080 -i :8080 2>/dev/null || echo "Cannot check port status"
# Test endpoints
echo "==> Testing endpoints:"
echo "Health check:"
curl -s http://localhost:3080/health | jq . || echo "Failed"
echo -e "\nTest endpoint:"
curl -s http://localhost:3080/test || echo "Failed"
echo -e "\nAPI proxy:"
curl -s http://localhost:3080/api/status | jq . || echo "Failed"
echo "==> Startup complete. Services:"
echo " - Caddy PID: $CADDY_PID"
echo " - Museum Mock PID: $SERVER_PID"
echo "==> Logs: /app/data/logs/"
# Keep running
wait $SERVER_PID $CADDY_PID

1697
start.sh Executable file → Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,57 +0,0 @@
#!/bin/bash
# Script to update Ente user storage using the Ente CLI
# Run this from your local machine (not inside Cloudron)
# Check if ente CLI is installed
if ! command -v ente &> /dev/null; then
echo "Ente CLI is not installed. Please install it first:"
echo ""
echo "For macOS:"
echo " brew tap ente-io/ente"
echo " brew install ente-cli"
echo ""
echo "For other systems, download from:"
echo " https://github.com/ente-io/ente/releases"
exit 1
fi
# Your Ente instance
ENTE_ENDPOINT="https://ente.due.ren"
# Function to update subscription
update_subscription() {
local admin_email="$1"
local user_email="$2"
local storage_gb="$3"
local valid_days="$4"
echo "Updating subscription for: $user_email"
echo "Storage: ${storage_gb}GB"
echo "Valid for: ${valid_days} days"
echo "Using admin account: $admin_email"
echo ""
# Run the ente CLI command
ente admin update-subscription \
--host "$ENTE_ENDPOINT" \
--admin-user "$admin_email" \
--user "$user_email" \
--storage "$storage_gb" \
--valid-for "$valid_days"
}
# Check arguments
if [ $# -lt 4 ]; then
echo "Usage: $0 <admin-email> <user-email> <storage-gb> <valid-days>"
echo ""
echo "Example:"
echo " $0 admin@due.ren andreas@due.ren 1000 365"
echo ""
echo "Make sure you're logged in to the Ente CLI first:"
echo " ente account add"
echo " API endpoint: $ENTE_ENDPOINT"
exit 1
fi
# Run the update
update_subscription "$1" "$2" "$3" "$4"