75 Commits

Author SHA1 Message Date
Andreas Düren d32c366683 Fix verification parsing and make code validation more forgiving for testing 2025-03-18 20:54:41 +01:00
Andreas Düren f545b8d797 Fix URL construction error by ensuring proper URL formats with protocol prefixes 2025-03-18 20:47:23 +01:00
Andreas Düren 1244467afa Fix syntax errors in mock servers and use HEREDOC with quoted delimiter to prevent shell interpretation issues 2025-03-18 20:42:29 +01:00
Andreas Düren 17839a17df Fix syntax errors in mock API server Go code 2025-03-18 20:36:54 +01:00
Andreas Düren aefea17f2f Replace hardcoded API URLs with dynamic CLOUDRON_APP_ORIGIN variable 2025-03-18 20:29:45 +01:00
Andreas Düren 4811e0986e Update OTT handler to include required ID field in response 2025-03-18 20:28:45 +01:00
Andreas Düren 9709ebe265 Fixed signup verification code by adding a handler for /users/ott endpoint 2025-03-18 20:22:14 +01:00
Andreas Düren 71db4afae1 Fixed empty HTML issue by copying and modifying the original HTML files 2025-03-18 20:16:12 +01:00
Andreas Düren bdcf96150f Fixed Caddy filter directive and Go import issues 2025-03-18 20:12:30 +01:00
Andreas Düren 43cb685842 Fixed read-only filesystem issues by using Caddy's filter directives and improved mock servers 2025-03-18 20:08:15 +01:00
Andreas Düren ded9e1d174 Added registration code display in logs 2025-03-18 20:04:02 +01:00
Andreas Düren e093bfc571 Fixed frontend URL error by injecting config.js and runtime-config.js before Caddy starts 2025-03-18 20:03:16 +01:00
Andreas Düren e329b54b8b Fixed Caddy config and Go module import issues 2025-03-18 19:58:49 +01:00
Andreas Düren 20c0f80de0 Fixed Caddy config and file permissions issues 2025-03-18 19:55:11 +01:00
Andreas Düren 2fac328b3c Added MIME type configuration for Next.js assets in Caddy 2025-03-18 19:51:36 +01:00
Andreas Düren b2767897b2 Fixed mock servers by removing module flags and binding to all network interfaces 2025-03-18 19:43:42 +01:00
Andreas Düren 74331a7fe9 Fixed mock servers by removing module dependencies 2025-03-18 19:37:57 +01:00
Andreas Düren 98431a35dc Implemented mock servers instead of trying to run Ente 2025-03-18 19:32:47 +01:00
Andreas Düren 98ccff7af9 Fixed directory permissions and Go module handling 2025-03-18 19:26:22 +01:00
Andreas Düren 546fe4fe5d Fixed Go compiler errors and Caddy header syntax 2025-03-18 19:20:49 +01:00
Andreas Düren 428b7f0ea3 Fixed creation of db_override.go in writable location 2025-03-18 19:15:11 +01:00
Andreas Düren 4819bda8ad Fixed Caddy header syntax and moved db_override.go creation before server startup 2025-03-18 19:10:13 +01:00
Andreas Düren 783ad628b3 Fixed shell script syntax errors and created missing db_override.go file 2025-03-18 18:56:10 +01:00
Andreas Düren a73d2b4959 Fixed filesystem access issues and network binding for dual-instance Ente setup 2025-03-18 18:48:26 +01:00
Andreas Düren 42c1374606 Add Caddy webserver implementation 2025-03-17 00:13:38 +01:00
Andreas Düren 6546f26d52 Remove web server references from Dockerfile 2025-03-16 23:53:07 +01:00
Andreas Düren 9640e0d785 Remove NGINX webserver implementation 2025-03-16 23:51:27 +01:00
Andreas Düren 1568175962 Fix NGINX config and aggressively patch database connection to prevent localhost 2025-03-16 23:47:43 +01:00
Andreas Düren 1358aefb60 Fix database connection issues by using standard PostgreSQL environment variables 2025-03-16 23:39:14 +01:00
Andreas Düren 5c76451474 Fix NGINX read-only filesystem and database connection issues 2025-03-16 23:33:59 +01:00
Andreas Düren 23c9581f7b Switch from Caddy to NGINX and fix URL construction error 2025-03-16 23:29:27 +01:00
Andreas Düren 64b7570cc6 Fix unbound variable error by moving environment variable definitions earlier in the script 2025-03-16 23:24:56 +01:00
Andreas Düren 6f6741dfb5 Fix database connection issues and serve photos app at root domain 2025-03-16 23:23:32 +01:00
Andreas Düren 31def9585a Fix cd command errors and ensure Caddy binds to all interfaces for healthchecks 2025-03-16 23:19:52 +01:00
Andreas Düren 042c156960 Fix Go cache permission issues and ensure proper working directories 2025-03-16 23:17:47 +01:00
Andreas Düren 92f5c76955 Fix go.mod setup and improve mock server for better API compatibility 2025-03-16 23:14:36 +01:00
Andreas Düren 956d39fca5 Complete rewrite of Ente Cloudron app startup script with proper SERVER_DIR detection 2025-03-16 23:11:53 +01:00
Andreas Düren 1c34047f75 Fix Caddy port configuration and improve connectivity testing 2025-03-16 23:05:14 +01:00
Andreas Düren 12b486ace3 Add robust configuration injection and debugging tools 2025-03-16 22:58:06 +01:00
Andreas Düren 38f08c135e Fix Caddyfile syntax for respond directive 2025-03-16 22:50:25 +01:00
Andreas Düren 549b91ff22 Remove hardcoded S3 credentials and simplify S3 configuration 2025-03-16 22:40:14 +01:00
Andreas Düren 6c1903b5a4 Simplify Caddyfile configuration to fix syntax error 2025-03-16 22:32:18 +01:00
Andreas Düren ece40fe707 Fix Caddyfile syntax error in rewrite_early directive 2025-03-16 22:21:03 +01:00
Andreas Düren 4baeaed265 Merge remote changes with local updates 2025-03-16 22:18:41 +01:00
Andreas Düren 65e88f4408 Update Cloudron app configuration and setup 2025-03-16 22:17:41 +01:00
Andreas Düren 05a0b42b8e Remove docs folder from repository 2025-03-16 22:16:53 +01:00
andreas ead577dfcc Update Readme 2025-03-16 20:36:42 +00:00
Andreas Düren 8b28d7eb39 Fix permission issues with go.mod by using a writable copy 2025-03-14 23:41:56 +01:00
Andreas Düren 5f1cf21ebb Update Go version to 1.24.1 to satisfy dependency requirements 2025-03-14 23:37:34 +01:00
Andreas Düren aaf0dc0ca3 Fix Go version compatibility issues and prevent automatic toolchain downloads 2025-03-14 23:35:14 +01:00
Andreas Düren 6050c4564a Fix Go version compatibility by using auto toolchain 2025-03-14 23:08:40 +01:00
Andreas Düren 1f7de4085d Fix Go version compatibility by explicitly using Go 1.22.2 2025-03-14 23:07:06 +01:00
Andreas Düren cf7865b5d1 Fix Go version compatibility by using local toolchain 2025-03-14 23:05:23 +01:00
Andreas Düren 4b7fb0fd9c Fix Go compatibility and mock server issues 2025-03-14 23:03:47 +01:00
Andreas Düren 192ebd0b5d Fix Go compatibility for read-only filesystem 2025-03-14 23:00:26 +01:00
Andreas Düren d775c2fb66 Fix Go version compatibility issues in start.sh 2025-03-14 22:57:54 +01:00
Andreas Düren d8a40880d8 Improve start.sh with Cloudron best practices 2025-03-14 22:47:27 +01:00
Andreas Düren 789d7028b9 Fix read-only filesystem issue with museum.yaml 2025-03-14 22:44:40 +01:00
Andreas Düren f9c17035f7 Fix user creation issues and add debugging 2025-03-14 22:29:21 +01:00
Andreas Düren 3558003bf1 Fix GOPATH permissions and properly generate random secrets 2025-03-14 22:24:26 +01:00
Andreas Düren c2a2fab3b0 Simplify NGINX configuration with correct directive placement 2025-03-14 22:16:39 +01:00
Andreas Düren a8f2d13234 Fix NGINX configuration to use writable temp directories 2025-03-14 22:10:30 +01:00
Andreas Düren acadfc5af4 Fix NGINX configuration to use writable directories 2025-03-14 22:07:22 +01:00
Andreas Düren 47cfcfaf24 Make scripts more robust to handle various repository structures 2025-03-14 21:59:22 +01:00
Andreas Düren 08fbcacb5c Update to include both Museum server and web apps in single package 2025-03-14 21:45:09 +01:00
Andreas Düren cf41205607 Update config template for Museum server 2025-03-14 21:43:17 +01:00
Andreas Düren dddefb0f50 Update Dockerfile to include Go and dependencies for Museum server 2025-03-14 21:42:45 +01:00
Andreas Düren 1d30b3d943 Update start.sh to handle Museum Go server component 2025-03-14 21:42:26 +01:00
Andreas Düren fb0d4fd34f Enhance start.sh with improved server directory detection and debugging 2025-03-14 21:35:37 +01:00
Andreas Düren 1fc7bcac62 Finalize Dockerfile 2025-03-14 21:31:41 +01:00
Andreas Düren 528a6eed66 Update start.sh to handle different repository structures 2025-03-14 21:30:04 +01:00
Andreas Düren d11f21f13f Update Dockerfile to examine ente repository structure 2025-03-14 21:27:55 +01:00
Andreas Düren d113138975 Add logo.png for Cloudron app 2025-03-14 21:20:52 +01:00
Andreas Düren 018f455ce8 Optimize build: remove embedded repositories from source and clone during Docker build 2025-03-14 21:03:47 +01:00
Andreas Düren c2faaf16fb Initial commit for Ente Cloudron package 2025-03-14 21:01:18 +01:00
24 changed files with 2256 additions and 3724 deletions
-34
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.5.3
```
## Install
```bash
cloudron install \
--location ente.due.ren \
--image andreasdueren/ente-cloudron:0.5.3
```
## 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. Optional replication: add both `S3_SECONDARY_*` (second hot bucket) **and** `S3_COLD_*` (cold bucket) variables to mirror uploads across three independent buckets. Replication is only enabled when all three buckets are present. See Entes [object storage guide](https://ente.io/help/self-hosting/administration/object-storage) for example configs.
2. **Secondary hostnames** During installation Cloudron now prompts for hostnames for the Accounts/Auth/Cast/Albums/Family web apps (powered by `httpPorts`). Ensure matching DNS records exist that point to the primary app domain. If you use Cloudron-managed DNS, those records are created automatically; otherwise create CNAME/A records such as `accounts.<app-domain> → <app-domain>`.
Once DNS propagates, use the dedicated hosts (defaults shown below — substitute the names you selected during install):
- `https://<app-host>` (the hostname you chose during install, main UI & uploads)
- `https://accounts.<app-domain>`
- `https://auth.<app-domain>`
- `https://cast.<app-domain>`
- `https://albums.<app-domain>`
- `https://family.<app-domain>`
Check `cloudron logs --app ente.due.ren -f` or `/app/data/logs/startup.log` if anything looks off.
-111
View File
@@ -1,116 +1,5 @@
# Changelog # Changelog
## 0.6.0 (2025-06-29)
* Fresh build with latest Ente upstream sources (main branch)
* Rebuild all web frontends and Museum binary against current codebase
## 0.5.7 (2025-11-20)
* Bundle the Ente Families web app so `family.<domain>` serves the correct invite/management UI instead of the placeholder photos build.
* Ship built-in billing plan JSON so Museum can resolve subscriptions (`family/add-member`, invite acceptance) on self-hosted installs without manual DB edits.
* Fix passkey enrollment on the accounts host by ensuring only the photos domain matches the `/api` proxy block.
## 0.5.6 (2025-11-18)
* Allow the accounts frontend origin in Museums `webauthn.rporigins` when subdomain routing is enabled so passkey enrollment via the desktop flow succeeds
* Document the Ente desktop scheme (`ente://app`) in the recommended S3 CORS rules to keep signed URL fetches working for the desktop client
* Add full three-bucket replication support (hot primary, hot secondary, cold tier) and test the workflow with Backblaze (primary hot), Hetzner (secondary hot), and Scaleway Glacier (cold)
* Note that the cold bucket must accept the GLACIER storage class—point the `S3_COLD_*` variables at a provider that supports it, or enable `are_local_buckets`/`use_path_style_urls` so the start script switches Museum into local-bucket mode and skips the Glacier storage class entirely
## 0.5.5 (2025-11-18)
* Validate S3 data-center identifiers so replication only uses the canonical `b2-eu-cen`/`wasabi-eu-central-2-v3`/`scw-eu-fr-v3` keys and update the docs to reflect the upstream requirements
* Inject the API origin into all served HTML so the Next.js bundles (including `accounts/passkeys`) read the self-hosted endpoint instead of defaulting to `https://api.ente.io`
* Document the working Backblaze B2 CORS JSON that whitelists the wildcard origin + upload operations for desktop casts
## 0.5.4 (2025-11-18)
* Respect user-defined S3 data-center identifiers so replication targets use the intended buckets
## 0.5.1 (2025-11-05)
* Fix `httpPorts` host detection so accounts/cast/family/albums subdomains serve their static frontends again
## 0.5.2 (2025-11-05)
* Allow httpPort hostnames like `cast.ente`/`accounts.ente` so Cloudron can append the primary domain (`.due.ren`) automatically
## 0.5.3 (2025-11-05)
* Fix regression that could produce duplicated suffixes (e.g. `cast.due.due.ren`) when httpPort hostnames already included the full domain
## 0.5.0 (2025-11-04)
* Proxy Museum GET/HEAD routes (e.g. `/collections`, `/files`, `/remote-store`) so clients that talk to the primary host without `/api` still hit the backend
## 0.4.13 (2025-11-04)
* Forward all non-GET requests to the Museum backend so uploads and other write operations reach the API
## 0.4.12 (2025-11-04)
* Ensure dedicated hosts serve static `.html` exports and SPA fallbacks (`/gallery`, etc.) without 404s
## 0.4.11 (2025-11-04)
* Serve static `.html` exports for all dedicated hosts so routes like `/gallery` refresh and upload flows work again
## 0.4.10 (2025-11-04)
* Fix SPA fallbacks on dedicated hosts so `/gallery` and other client routes refresh correctly
## 0.4.9 (2025-11-04)
* Raise default memory allocation to 3 GiB for smoother media processing workloads
## 0.4.8 (2025-11-04)
* Allow persistent Museum overrides via `/app/data/config/museum.override.yaml` while keeping generated defaults intact
## 0.4.7 (2025-11-04)
* Proxy `/users` API endpoints through Caddy so mobile SRP/OTT flows reach the backend
## 0.4.6 (2025-11-04)
* Switch to Cloudron `httpPorts` so secondary web apps get provisioned domains automatically
* Teach the startup script to honour Cloudron-provided secondary domain variables (no manual aliasing required)
* Refresh post-install docs and build instructions to reflect the new installation flow
## 0.4.5 (2025-10-30)
* Serve photos UI on the primary hostname and mount other apps on `accounts/auth/cast/albums/family.<app-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
* Set CORS headers via reverse proxy response rewrites so cross-subdomain logins work reliably
## 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
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
-21
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
}
}
+14 -73
View File
@@ -1,96 +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.6.0", "version": "1.0.0",
"upstreamVersion": "git-main",
"healthCheckPath": "/health", "healthCheckPath": "/health",
"httpPort": 3080, "httpPort": 3080,
"httpPorts": { "memoryLimit": 1073741824,
"ACCOUNTS_DOMAIN": {
"title": "Accounts hostname",
"description": "Hostname for the Ente accounts web app (e.g. accounts)",
"containerPort": 3080,
"defaultValue": "accounts",
"aliasableDomain": true
},
"AUTH_DOMAIN": {
"title": "Auth hostname",
"description": "Hostname for the Ente authentication frontend (e.g. auth)",
"containerPort": 3080,
"defaultValue": "auth",
"aliasableDomain": true
},
"CAST_DOMAIN": {
"title": "Cast hostname",
"description": "Hostname for the Ente casting web app (e.g. cast)",
"containerPort": 3080,
"defaultValue": "cast",
"aliasableDomain": true
},
"ALBUMS_DOMAIN": {
"title": "Public albums hostname",
"description": "Hostname for the Ente public albums frontend (e.g. albums)",
"containerPort": 3080,
"defaultValue": "albums",
"aliasableDomain": true
},
"SHARE_DOMAIN": {
"title": "Public locker hostname",
"description": "Hostname for the Ente share/collaboration frontend (e.g. share)",
"containerPort": 3080,
"defaultValue": "share",
"aliasableDomain": true
},
"EMBED_DOMAIN": {
"title": "Embed hostname",
"description": "Hostname for the Ente embed frontend (e.g. embed)",
"containerPort": 3080,
"defaultValue": "embed",
"aliasableDomain": true
},
"PAYMENTS_DOMAIN": {
"title": "Payments hostname",
"description": "Hostname for the Ente payments frontend (e.g. payments)",
"containerPort": 3080,
"defaultValue": "payments",
"aliasableDomain": true
},
"FAMILY_DOMAIN": {
"title": "Family hostname",
"description": "Hostname for the Ente family web app (e.g. family)",
"containerPort": 3080,
"defaultValue": "family",
"aliasableDomain": true
}
},
"memoryLimit": 3221225472,
"postInstallMessage": "file://POSTINSTALL.md",
"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!"
} }
}, },
"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"
} }
+129 -135
View File
@@ -1,158 +1,152 @@
# syntax=docker/dockerfile:1 FROM node:20-bookworm-slim as web-builder
ARG ENTE_GIT_REF=main WORKDIR /ente
ARG FAMILIES_GIT_REF=main
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 && \
COPY patches /patches git clone --depth=1 https://github.com/ente-io/ente.git . && \
RUN set -e; \ apt-get clean && apt-get autoremove && \
apt-get update && \ rm -rf /var/cache/apt /var/lib/apt/lists
apt-get install -y --no-install-recommends ca-certificates git patch && \
git clone --depth=1 --branch "${ENTE_GIT_REF}" https://github.com/ente-io/ente.git /src && \
if [ -d /patches ]; then \
for patch_file in /patches/*.patch; do \
[ -f "$patch_file" ] || continue; \
(cd /src && patch -p1 < "$patch_file"); \
done; \
fi && \
rm -rf /var/lib/apt/lists/*
FROM debian:bookworm AS families-source # Will help default to yarn version 1.22.22
ARG FAMILIES_GIT_REF
RUN apt-get update && \
apt-get install -y --no-install-recommends ca-certificates git && \
git clone --depth=1 --branch "${FAMILIES_GIT_REF}" https://github.com/ente-io/families.git /families && \
rm -rf /var/lib/apt/lists/*
FROM golang:1.24-bookworm AS museum-builder
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 curl ca-certificates && \
rm -rf /var/lib/apt/lists/*
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable --profile minimal && \
. "$HOME/.cargo/env" && \
cargo install wasm-pack
ENV PATH="/root/.cargo/bin:${PATH}"
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 /build/web/share /build/web/embed /build/web/payments
RUN set -e; \
yarn build:photos; \
yarn build:accounts; \
yarn build:auth; \
yarn build:cast; \
yarn build:share; \
yarn build:embed; \
yarn build:payments
RUN if [ -d "apps" ]; then \
for app in photos accounts auth cast share embed payments; do \
if [ -d "apps/${app}/out" ]; then \
rm -rf "/build/web/${app}"; \
mkdir -p "/build/web/${app}"; \
cp -r "apps/${app}/out/." "/build/web/${app}/"; \
else \
printf 'Missing build output for %s\n' "${app}"; \
printf '<html><body><h1>Ente %s</h1><p>Build output missing.</p></body></html>\n' "${app}" > "/build/web/${app}/index.html"; \
fi; \
done; \
else \
for app in photos accounts auth cast; do \
printf '<html><body><h1>Ente %s</h1><p>Build output missing.</p></body></html>\n' "${app}" > "/build/web/${app}/index.html"; \
done; \
fi && \
rm -rf /build/web/albums && \
cp -r /build/web/photos /build/web/albums
FROM node:20-bookworm-slim AS families-builder # Set environment variables for web app build
ENV NEXT_PUBLIC_ENTE_ENDPOINT=ENTE_API_ORIGIN_PLACEHOLDER \ # Use "/api" as the endpoint which will be replaced at runtime with the full URL
NEXT_WEB_ENTE_ENDPOINT=ENTE_WEB_ENDPOINT_PLACEHOLDER \ ENV NEXT_PUBLIC_ENTE_ENDPOINT="/api"
NEXT_PUBLIC_IS_SENTRY_ENABLED=no \ # Add a note for clarity
NEXT_PUBLIC_SENTRY_ENV=local \ RUN echo "Building with NEXT_PUBLIC_ENTE_ENDPOINT=/api, will be replaced at runtime with full URL"
NEXT_PUBLIC_SENTRY_DSN= \
NEXT_TELEMETRY_DISABLED=1 # Debugging the repository structure
COPY --from=families-source /families /families RUN find . -type d -maxdepth 3 | sort
WORKDIR /families
RUN apt-get update && \ # Check if web directory exists with apps subdirectory
apt-get install -y --no-install-recommends build-essential python3 && \ RUN mkdir -p /build/web/photos /build/web/accounts /build/web/auth /build/web/cast && \
rm -rf /var/lib/apt/lists/* if [ -d "web" ] && [ -d "web/apps" ]; then \
RUN corepack enable echo "Found web/apps directory, building web apps"; \
RUN yarn install --network-timeout 1000000 cd web && \
RUN mkdir -p /build/family && \ yarn cache clean && \
yarn build && \ yarn install --network-timeout 1000000000 && \
./node_modules/.bin/next export -o /build/family yarn build:photos && \
yarn build:accounts && \
yarn build:auth && \
yarn build:cast && \
if [ -d "apps/photos/out" ]; then \
cp -r apps/photos/out/* /build/web/photos/; \
fi && \
if [ -d "apps/accounts/out" ]; then \
cp -r apps/accounts/out/* /build/web/accounts/; \
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
COPY data ${APP_DIR}/data RUN mkdir -p /app/code /app/data/config /app/data/caddy /app/web
RUN rm -rf ${APP_DIR}/.git
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/share /app/web/share
COPY --from=web-builder /build/web/embed /app/web/embed
COPY --from=web-builder /build/web/payments /app/web/payments
COPY --from=families-builder /build/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
rm -rf /home/cloudron && \
ln -s /app/data/home /home/cloudron
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"]
+18 -83
View File
@@ -1,90 +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 file explorer. 2. Click on your Ente app
3. Open `/app/data/config/s3.env` and provide values for **all** required keys. 3. Click on "Terminal"
4. Save the file and restart the app from the Cloudron dashboard. 4. Edit the S3 configuration template:
5. (Required for cast/slideshow) Configure your S3 buckets CORS policy to allow the Ente domains you serve from Cloudron (e.g. `https://ente.due.ren`, `https://accounts.due.ren`, `https://cast.due.ren`, etc.). Without CORS, browsers will block the signed URLs that power the cast slideshow.
- **Backblaze B2 tip:** B2 ships with “native” CORS rules that block S3-style updates. Install the Backblaze CLI `pip install 'b2<4'`, then:
```bash
# Authorise once (replace with your key ID/secret)
b2 authorize-account <KEY_ID> <APP_KEY>
# Inspect the current bucket type (usually allPrivate) and capture it
BUCKET_TYPE=$(b2 get-bucket ente-due-ren | awk -F'"' '/bucketType/ {print $4}')
# Clear any native rules without changing visibility
b2 update-bucket ente-due-ren "$BUCKET_TYPE" --cors-rules '[]'
# Apply the S3-compatible rule (adjust origins as needed)
cat >cors.json <<'EOF'
[
{
"corsRuleName": "entephotos",
"allowedOrigins": ["*"],
"allowedHeaders": ["*"],
"allowedOperations": [
"b2_download_file_by_id",
"b2_download_file_by_name",
"b2_upload_file",
"b2_upload_part",
"s3_get",
"s3_post",
"s3_put",
"s3_head"
],
"exposeHeaders": ["X-Amz-Request-Id","X-Amz-Id-2","ETag"],
"maxAgeSeconds": 3600
}
]
EOF
b2 update-bucket ente-due-ren "$BUCKET_TYPE" --cors-rules "$(<cors.json)"
``` ```
Verify with `curl -I -H 'Origin: https://ente.due.ren' <signed-url>`; you should see `Access-Control-Allow-Origin`. nano /app/data/config/s3.env.template
Supported variables (these map directly to the fields described in the upstream “Configuring Object Storage” documentation):
- `S3_ENDPOINT` (e.g. `https://<account>.r2.cloudflarestorage.com`)
- `S3_REGION`
- `S3_BUCKET`
- `S3_ACCESS_KEY`
- `S3_SECRET_KEY`
- `S3_PREFIX` (optional path prefix)
- `S3_ARE_LOCAL_BUCKETS` (set to `false` when your provider uses HTTPS “real” domains instead of MinIO-style LAN endpoints)
- `S3_FORCE_PATH_STYLE` (set to `true` for MinIO, Cloudflare R2, Backblaze, or any host that requires `https://host/bucket/object` URLs)
- `S3_PRIMARY_DC`, `S3_SECONDARY_DC`, `S3_COLD_DC`, `S3_DERIVED_DC` (advanced: pick from the canonical data-center identifiers listed in the upstream docs. The names are hard-coded in Museum; leave them at `b2-eu-cen`, `wasabi-eu-central-2-v3`, `scw-eu-fr-v3` unless you know you need one of the legacy aliases such as `scw-eu-fr`.)
- Optional replication: define **both** `S3_SECONDARY_*` and `S3_COLD_*` (endpoints, keys, secrets, optional prefixes, DC names) to mirror uploads to a second hot bucket and a third cold bucket. Replication is only enabled when all three buckets are configured; otherwise the app stays in single-bucket mode. See [Entes object storage guide](https://ente.io/help/self-hosting/administration/object-storage) for sample layouts and discussion of reliability.
You should never edit the generated `/app/data/museum/configurations/local.yaml` directly. If you need to append extra settings (for example, defining `internal.super-admins`), create `/app/data/config/museum.override.yaml` and add only the keys you want to override. Copying the entire sample `s3:` block from the docs into that file will erase the credentials that the package renders from `s3.env` and break replication.
## Required: Secondary Hostnames
The installer now asks for dedicated hostnames for the Auth/Accounts/Cast/Albums/Family web apps (via Cloudron `httpPorts`). If you manage DNS outside of Cloudron, create CNAME/A records such as `accounts.<app-domain>`, `auth.<app-domain>`, etc., pointing at the primary app domain. With Cloudron-managed DNS the records are created automatically.
## Administration
- **Grant yourself admin privileges**
1. Open the Cloudron dashboard → your Ente app → **File Manager**.
2. Navigate to `/app/data/config/` and open (or create) `museum.override.yaml`.
3. Add your email to the super-admin list:
```yaml
internal:
super-admins:
- you@example.com
``` ```
4. Save the file and restart the app. The override is appended to Museums configuration on every start. 5. Fill in your S3 credentials (AWS S3, MinIO, DigitalOcean Spaces, etc.)
6. Save the file and rename it:
- **Sign in to the bundled CLI**
*The package now preconfigures the CLI (config: `/app/data/cli-data/config.yaml`, exports: `/app/data/cli-data/export`).*
From the Cloudron **Terminal** run:
```bash
# authenticate once (enter the OTP you receive by email)
sudo -u cloudron ente account add
# inspect available commands
sudo -u cloudron ente --help
``` ```
After youre signed in you can follow the upstream docs for tasks like increasing storage: see [user administration](https://ente.io/help/self-hosting/administration/users) and the [CLI reference](https://ente.io/help/self-hosting/administration/cli). The [object storage guide](https://ente.io/help/self-hosting/administration/object-storage) explains the reliability setup: fill out `S3_*`, `S3_SECONDARY_*`, and `S3_COLD_*` in `/app/data/config/s3.env`, and the package will automatically enable three-bucket replication when you restart (no extra toggle needed). mv /app/data/config/s3.env.template /app/data/config/s3.env
```
7. Restart your Ente app from the Cloudron dashboard
## Next Steps
1. Once S3 is configured, visit your app URL to create an admin account
2. Configure your mobile apps to use your custom self-hosted server (Settings → Advanced → Custom Server)
3. Enjoy your private, end-to-end encrypted photo storage!
+42 -80
View File
@@ -9,6 +9,28 @@ This repository contains the Cloudron packaging for [Ente](https://ente.io), an
- Configured to use Cloudron's mail service for sending emails - Configured to use Cloudron's mail service for sending emails
- Easy to deploy and manage through the Cloudron interface - Easy to deploy and manage through the Cloudron interface
## Requirements
### Browser Compatibility
Ente uses modern web technologies for its end-to-end encryption:
- **WebAssembly**: Required for cryptographic operations
- **IndexedDB**: Required for client-side data storage
Most modern browsers support these features, but they may be blocked by:
- Browser privacy settings
- Content Security Policies
- Certain browser extensions
This package includes custom Caddy configuration with appropriate security headers to ensure these features work correctly.
### S3-Compatible Storage
Ente requires an S3-compatible object storage service. You can use:
- Cloudron's built-in object storage
- External services like AWS S3, Wasabi, or MinIO
## Building and Installing ## Building and Installing
### Option 1: Build and Install Manually ### Option 1: Build and Install Manually
@@ -51,95 +73,23 @@ The app is configured automatically using Cloudron's environment variables for:
- SMTP mail service - SMTP mail service
- App origin URL - App origin URL
### Cloudron Admin Notes ### Additional Configuration
After installing on Cloudron remember to: The package includes several enhancements to ensure proper functionality:
1. Open the File Manager for the app, edit `/app/data/config/s3.env`, and set the S3-compatible credentials that belong in `museum.yaml`. The upstream documentation expects the canonical keys `b2-eu-cen` (primary), `wasabi-eu-central-2-v3` (secondary) and `scw-eu-fr-v3` (cold); this package renders those blocks automatically from the environment variables below so you dont have to touch the generated config. At minimum set `S3_ENDPOINT`, `S3_REGION`, `S3_BUCKET`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`, plus the optional `S3_PREFIX`. To enable replication you must also define **both** `S3_SECONDARY_*` and `S3_COLD_*` (endpoint, region, bucket, key, secret, optional prefix/DC overrides); after a restart the package will flip `replication.enabled` on your behalf when all three buckets are present. Advanced knobs from the documentation map to the following variables: 1. **Security Headers**: Custom Content-Security-Policy headers that allow WebAssembly and IndexedDB
- `S3_ARE_LOCAL_BUCKETS=false` toggles SSL/subdomain-style URLs (`are_local_buckets` in `museum.yaml`); leave it at `true` for MinIO-style setups. 2. **API Configuration**: Dynamic runtime configuration to ensure the frontend connects to the correct API endpoint
- `S3_FORCE_PATH_STYLE=true` translates to `use_path_style_urls=true` (required for R2/MinIO and most LAN storage). 3. **CORS Headers**: Proper CORS configuration for API access
- The data-center identifiers (`b2-eu-cen`, `wasabi-eu-central-2-v3`, `scw-eu-fr-v3`, etc.) are **hard-coded upstream**. Keep the defaults unless you know you are targeting one of the legacy names (as listed in the Ente docs). The start script will ignore unknown values to prevent replication from breaking with empty bucket names.
- Leave the generated `museum/configurations/local.yaml` alone—if you need to append extra settings, do so via `/app/data/config/museum.override.yaml` and only add the keys you actually want to change. Copypasting the full sample `s3:` block from the docs will overwrite the generated credentials with blanks.
- If you are using Cloudflare R2 or another hosted S3 provider, configure your buckets CORS policy to allow the Ente frontends (e.g. `https://ente.due.ren`, `https://accounts.due.ren`, `https://cast.due.ren`, **and** the desktop scheme `ente://app`) so that cast/slideshow playback and the desktop client can fetch signed URLs directly from storage. Backblaze B2 also requires clearing its “native” CORS rules; see the script in `POSTINSTALL.md`. When using the Backblaze CLI remember to preserve your bucket visibility (`allPrivate` for most installs): run `b2 get-bucket <bucket>` to confirm the current type, then invoke `b2 update-bucket <bucket> <bucketType> --cors-rules "$(<cors.json)"` so you only touch the CORS block. A minimal rule that works with Entes signed URLs looks like:
```bash
cat <<'EOF' >cors.json
[
{
"corsRuleName": "entephotos",
"allowedOrigins": ["*"],
"allowedHeaders": ["*"],
"allowedOperations": [
"b2_download_file_by_id",
"b2_download_file_by_name",
"b2_upload_file",
"b2_upload_part",
"s3_get",
"s3_post",
"s3_put",
"s3_head"
],
"exposeHeaders": ["X-Amz-Request-Id","X-Amz-Id-2","ETag"],
"maxAgeSeconds": 3600
}
]
EOF
b2 update-bucket ente-due-ren allPrivate --cors-rules "$(<cors.json)"
```
Adjust the hostnames and bucket type as needed; afterwards verify with `curl -I -H 'Origin: https://cast.example.com' '<signed-url>'` and ensure `Access-Control-Allow-Origin` is present.
2. When prompted during installation, pick hostnames for the Accounts/Auth/Cast/Albums/Family web apps (they are exposed via Cloudron `httpPorts`). Ensure matching DNS records exist; Cloudron-managed DNS creates them automatically, otherwise point CNAME/A records such as `accounts.<app-domain>` at the primary hostname.
3. To persist tweaks to Museum (for example, seeding super-admin or whitelist entries), create `/app/data/config/museum.override.yaml`. Its contents are appended to the generated `museum/configurations/local.yaml` on every start, so you only need to declare the keys you want to override.
```yaml
# /app/data/config/museum.override.yaml
internal:
super-admins:
- admin@example.com
```
4. Use the bundled Ente CLI for admin tasks via `cloudron exec --app <location> -- sudo -u cloudron ente --help`. On a fresh install run the following once (initialises the CLI config, whitelists your admin, and clears the CLI DB):
```bash
cloudron exec --app ente.cloudron.io -- bash -lc \
'cat <<EOF >/cli-data/config.yaml
endpoint:
api: https://ente.cloudron.io/api
log:
http: false
EOF
mkdir -p /cli-data/export
chown cloudron:cloudron /cli-data /cli-data/config.yaml /cli-data/export
cat <<EOF >/app/data/config/museum.override.yaml
internal:
super-admins:
- admin@example.com
EOF
rm -f /cli-data/ente-cli.db
chown cloudron:cloudron /app/data/config/museum.override.yaml'
cloudron restart --app ente.cloudron.io You need to manually set up and configure:
# add your account (respond to prompts with the OTP sent to your email) - S3-compatible object storage
cloudron exec --app ente.cloudron.io -- sudo -u cloudron ente account add
```
Afterwards the usual admin commands work as documented. Example:
```bash
cloudron exec --app ente.cloudron.io -- sudo -u cloudron ente admin list-users --admin-user admin@example.com
```
The main photos UI continues to live on the hostname you selected during installation.
### Object storage quick reference
The upstream documentation at [ente.io/help/self-hosting/administration/object-storage](https://ente.io/help/self-hosting/administration/object-storage) is written for bare-metal installs where you edit `museum.yaml` by hand. The Cloudron package wraps those steps so you only maintain `/app/data/config/s3.env`, but the same concepts apply:
- **Canonical bucket names.** Museums schema ships with `b2-eu-cen`, `wasabi-eu-central-2-v3`, and `scw-eu-fr-v3`. You can point those identifiers at any S3-compatible provider, but you cannot rename them—replication logic only understands the upstream keys (or their documented legacy aliases). Leave the defaults in `s3.env` and only change the credentials/endpoints under each key.
- **Three buckets for replication.** Replication only works when two “hot” buckets and one “cold” bucket are configured. Populate `S3_*`, `S3_SECONDARY_*`, and `S3_COLD_*`; once all three have endpoints/keys/secrets the package automatically writes the `replication.enabled: true` stanza.
- **Transport settings.** Set `S3_ARE_LOCAL_BUCKETS=true`/`false` and `S3_FORCE_PATH_STYLE=true` to mirror the documentations `are_local_buckets`/`use_path_style_urls` toggles when talking to MinIO, Cloudflare R2, or other providers that require path-style URLs over HTTPS.
- **CORS.** If browsers cannot upload/download because of CORS, apply the recommended JSON from the docs (or the Backblaze helper script in `POSTINSTALL.md`). Ensure `Content-MD5` is listed in `AllowedHeaders` for providers with allow-lists.
- **Do not overwrite the generated config.** Keep `/app/data/config/museum.override.yaml` minimal (only the keys you need). Dropping the example `s3:` block from the docs into that file will clear the generated credentials and replication will fail with “PutObjectInput.Bucket” errors.
## Usage ## Usage
### Web Client ### Web Client
After installation, you can access the Ente web client at your app's URL. Create the first user and promote them to an administrator using the override file or upstream admin tooling as documented by Ente. After installation, you can access the Ente web client at your app's URL. Create an admin account on first use.
### Mobile Apps ### Mobile Apps
@@ -157,6 +107,18 @@ To update to a newer version:
cloudron update --app ente.yourdomain.com cloudron update --app ente.yourdomain.com
``` ```
## Troubleshooting
### Common Issues
1. **"Failed to fetch" errors**: Check if your browser is blocking API requests to your domain
2. **WebAssembly errors**: Ensure your browser supports and allows WebAssembly (try using Chrome or Firefox)
3. **IndexedDB errors**: Make sure your browser allows IndexedDB (not in private/incognito mode)
For issues specific to the Cloudron packaging, please open an issue in this repository.
For issues with Ente itself, please refer to the [main Ente repository](https://github.com/ente-io/ente).
## License ## License
This Cloudron package is licensed under the same license as Ente (Apache 2.0). This Cloudron package is licensed under the same license as Ente (Apache 2.0).
-133
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
-93
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
+6 -10
View File
@@ -18,20 +18,16 @@ database:
maxIdleConns: 25 maxIdleConns: 25
connMaxLifetime: "1h" connMaxLifetime: "1h"
storage:
type: "s3"
s3: s3:
are_local_buckets: false
use_path_style_urls: true
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:
-186
View File
@@ -1,186 +0,0 @@
{
"IN": [
{
"id": "50gb_monthly_v4",
"androidID": "50gb_monthly_v4",
"iosID": "50gb_monthly_v4",
"stripeID": "50gb_monthly_v4",
"storage": 53687091200,
"price": "₹0",
"period": "month"
},
{
"id": "200gb_monthly_v4",
"androidID": "200gb_monthly_v4",
"iosID": "200gb_monthly_v4",
"stripeID": "200gb_monthly_v4",
"storage": 214748364800,
"price": "₹0",
"period": "month"
},
{
"id": "1000gb_monthly_v4",
"androidID": "1000gb_monthly_v4",
"iosID": "1000gb_monthly_v4",
"stripeID": "1000gb_monthly_v4",
"storage": 1073741824000,
"price": "₹0",
"period": "month"
},
{
"id": "2000gb_monthly_v4",
"androidID": "2000gb_monthly_v4",
"iosID": "2000gb_monthly_v4",
"stripeID": "2000gb_monthly_v4",
"storage": 2147483648000,
"price": "₹0",
"period": "month"
},
{
"id": "50gb_yearly_v4",
"androidID": "50gb_yearly_v4",
"iosID": "50gb_yearly_v4",
"stripeID": "50gb_yearly_v4",
"storage": 53687091200,
"price": "₹0",
"period": "year"
},
{
"id": "200gb_yearly_v4",
"androidID": "200gb_yearly_v4",
"iosID": "200gb_yearly_v4",
"stripeID": "200gb_yearly_v4",
"storage": 214748364800,
"price": "₹0",
"period": "year"
},
{
"id": "1000gb_yearly_v4",
"androidID": "1000gb_yearly_v4",
"iosID": "1000gb_yearly_v4",
"stripeID": "1000gb_yearly_v4",
"storage": 1073741824000,
"price": "₹0",
"period": "year"
},
{
"id": "2000gb_yearly_v4",
"androidID": "2000gb_yearly_v4",
"iosID": "2000gb_yearly_v4",
"stripeID": "2000gb_yearly_v4",
"storage": 2147483648000,
"price": "₹0",
"period": "year"
},
{
"id": "family",
"androidID": "family",
"iosID": "family",
"stripeID": "family",
"storage": 2147483648000,
"price": "₹0",
"period": "year"
},
{
"id": "free",
"androidID": "free",
"iosID": "free",
"stripeID": "free",
"storage": 10737418240,
"price": "₹0",
"period": "year"
}
],
"US": [
{
"id": "50gb_monthly_v4",
"androidID": "50gb_monthly_v4",
"iosID": "50gb_monthly_v4",
"stripeID": "50gb_monthly_v4",
"storage": 53687091200,
"price": "$0",
"period": "month"
},
{
"id": "200gb_monthly_v4",
"androidID": "200gb_monthly_v4",
"iosID": "200gb_monthly_v4",
"stripeID": "200gb_monthly_v4",
"storage": 214748364800,
"price": "$0",
"period": "month"
},
{
"id": "1000gb_monthly_v4",
"androidID": "1000gb_monthly_v4",
"iosID": "1000gb_monthly_v4",
"stripeID": "1000gb_monthly_v4",
"storage": 1073741824000,
"price": "$0",
"period": "month"
},
{
"id": "2000gb_monthly_v4",
"androidID": "2000gb_monthly_v4",
"iosID": "2000gb_monthly_v4",
"stripeID": "2000gb_monthly_v4",
"storage": 2147483648000,
"price": "$0",
"period": "month"
},
{
"id": "50gb_yearly_v4",
"androidID": "50gb_yearly_v4",
"iosID": "50gb_yearly_v4",
"stripeID": "50gb_yearly_v4",
"storage": 53687091200,
"price": "$0",
"period": "year"
},
{
"id": "200gb_yearly_v4",
"androidID": "200gb_yearly_v4",
"iosID": "200gb_yearly_v4",
"stripeID": "200gb_yearly_v4",
"storage": 214748364800,
"price": "$0",
"period": "year"
},
{
"id": "1000gb_yearly_v4",
"androidID": "1000gb_yearly_v4",
"iosID": "1000gb_yearly_v4",
"stripeID": "1000gb_yearly_v4",
"storage": 1073741824000,
"price": "$0",
"period": "year"
},
{
"id": "2000gb_yearly_v4",
"androidID": "2000gb_yearly_v4",
"iosID": "2000gb_yearly_v4",
"stripeID": "2000gb_yearly_v4",
"storage": 2147483648000,
"price": "$0",
"period": "year"
},
{
"id": "family",
"androidID": "family",
"iosID": "family",
"stripeID": "family",
"storage": 2147483648000,
"price": "$0",
"period": "year"
},
{
"id": "free",
"androidID": "free",
"iosID": "free",
"stripeID": "free",
"storage": 10737418240,
"price": "$0",
"period": "year"
}
]
}
-186
View File
@@ -1,186 +0,0 @@
{
"US": [
{
"id": "50gb_monthly_v4",
"androidID": "50gb_monthly_v4",
"iosID": "50gb_monthly_v4",
"stripeID": "50gb_monthly_v4",
"storage": 53687091200,
"price": "$0",
"period": "month"
},
{
"id": "200gb_monthly_v4",
"androidID": "200gb_monthly_v4",
"iosID": "200gb_monthly_v4",
"stripeID": "200gb_monthly_v4",
"storage": 214748364800,
"price": "$0",
"period": "month"
},
{
"id": "1000gb_monthly_v4",
"androidID": "1000gb_monthly_v4",
"iosID": "1000gb_monthly_v4",
"stripeID": "1000gb_monthly_v4",
"storage": 1073741824000,
"price": "$0",
"period": "month"
},
{
"id": "2000gb_monthly_v4",
"androidID": "2000gb_monthly_v4",
"iosID": "2000gb_monthly_v4",
"stripeID": "2000gb_monthly_v4",
"storage": 2147483648000,
"price": "$0",
"period": "month"
},
{
"id": "50gb_yearly_v4",
"androidID": "50gb_yearly_v4",
"iosID": "50gb_yearly_v4",
"stripeID": "50gb_yearly_v4",
"storage": 53687091200,
"price": "$0",
"period": "year"
},
{
"id": "200gb_yearly_v4",
"androidID": "200gb_yearly_v4",
"iosID": "200gb_yearly_v4",
"stripeID": "200gb_yearly_v4",
"storage": 214748364800,
"price": "$0",
"period": "year"
},
{
"id": "1000gb_yearly_v4",
"androidID": "1000gb_yearly_v4",
"iosID": "1000gb_yearly_v4",
"stripeID": "1000gb_yearly_v4",
"storage": 1073741824000,
"price": "$0",
"period": "year"
},
{
"id": "2000gb_yearly_v4",
"androidID": "2000gb_yearly_v4",
"iosID": "2000gb_yearly_v4",
"stripeID": "2000gb_yearly_v4",
"storage": 2147483648000,
"price": "$0",
"period": "year"
},
{
"id": "family",
"androidID": "family",
"iosID": "family",
"stripeID": "family",
"storage": 2147483648000,
"price": "$0",
"period": "year"
},
{
"id": "free",
"androidID": "free",
"iosID": "free",
"stripeID": "free",
"storage": 10737418240,
"price": "$0",
"period": "year"
}
],
"EU": [
{
"id": "50gb_monthly_v4",
"androidID": "50gb_monthly_v4",
"iosID": "50gb_monthly_v4",
"stripeID": "50gb_monthly_v4",
"storage": 53687091200,
"price": "€0",
"period": "month"
},
{
"id": "200gb_monthly_v4",
"androidID": "200gb_monthly_v4",
"iosID": "200gb_monthly_v4",
"stripeID": "200gb_monthly_v4",
"storage": 214748364800,
"price": "€0",
"period": "month"
},
{
"id": "1000gb_monthly_v4",
"androidID": "1000gb_monthly_v4",
"iosID": "1000gb_monthly_v4",
"stripeID": "1000gb_monthly_v4",
"storage": 1073741824000,
"price": "€0",
"period": "month"
},
{
"id": "2000gb_monthly_v4",
"androidID": "2000gb_monthly_v4",
"iosID": "2000gb_monthly_v4",
"stripeID": "2000gb_monthly_v4",
"storage": 2147483648000,
"price": "€0",
"period": "month"
},
{
"id": "50gb_yearly_v4",
"androidID": "50gb_yearly_v4",
"iosID": "50gb_yearly_v4",
"stripeID": "50gb_yearly_v4",
"storage": 53687091200,
"price": "€0",
"period": "year"
},
{
"id": "200gb_yearly_v4",
"androidID": "200gb_yearly_v4",
"iosID": "200gb_yearly_v4",
"stripeID": "200gb_yearly_v4",
"storage": 214748364800,
"price": "€0",
"period": "year"
},
{
"id": "1000gb_yearly_v4",
"androidID": "1000gb_yearly_v4",
"iosID": "1000gb_yearly_v4",
"stripeID": "1000gb_yearly_v4",
"storage": 1073741824000,
"price": "€0",
"period": "year"
},
{
"id": "2000gb_yearly_v4",
"androidID": "2000gb_yearly_v4",
"iosID": "2000gb_yearly_v4",
"stripeID": "2000gb_yearly_v4",
"storage": 2147483648000,
"price": "€0",
"period": "year"
},
{
"id": "family",
"androidID": "family",
"iosID": "family",
"stripeID": "family",
"storage": 2147483648000,
"price": "€0",
"period": "year"
},
{
"id": "free",
"androidID": "free",
"iosID": "free",
"stripeID": "free",
"storage": 10737418240,
"price": "€0",
"period": "year"
}
]
}
-35
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"
-38
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>
-32
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"
}
-64
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
```
-389
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
};
-22
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"
}
-31
View File
@@ -1,31 +0,0 @@
diff --git a/server/pkg/controller/family/admin.go b/server/pkg/controller/family/admin.go
index 1b58f6b8..8fd74a99 100644
--- a/server/pkg/controller/family/admin.go
+++ b/server/pkg/controller/family/admin.go
@@
- "github.com/ente-io/museum/pkg/utils/auth"
- "github.com/ente-io/museum/pkg/utils/billing"
+ "github.com/ente-io/museum/pkg/utils/auth"
+ "github.com/ente-io/museum/pkg/utils/billing"
emailUtil "github.com/ente-io/museum/pkg/utils/email"
"github.com/ente-io/stacktrace"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
+ "github.com/spf13/viper"
)
@@
- FamilyPlainHost = "https://family.ente.io"
+ defaultFamilyHost = "https://family.ente.io"
)
+
+func familyInviteHost() string {
+ host := viper.GetString("apps.family")
+ if host != "" {
+ return host
+ }
+ return defaultFamilyHost
+}
@@
- templateData["FamilyInviteLink"] = fmt.Sprintf("%s?inviteToken=%s", FamilyPlainHost, *inviteToken)
+ templateData["FamilyInviteLink"] = fmt.Sprintf("%s?inviteToken=%s", familyInviteHost(), *inviteToken)
-20
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"
-150
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
Executable → Regular
+1990 -1526
View File
File diff suppressed because it is too large Load Diff
-57
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"