commit 816b0b41152e067a43c2e034f15a5d9757b8e310 Author: Andreas Dueren Date: Mon Jul 14 21:01:40 2025 -0600 Initial Docmost Cloudron package diff --git a/CloudronManifest.json b/CloudronManifest.json new file mode 100644 index 0000000..d1111f4 --- /dev/null +++ b/CloudronManifest.json @@ -0,0 +1,36 @@ +{ + "id": "com.docmost.app", + "title": "Docmost", + "author": "Docmost Team", + "description": "Open-source collaborative wiki and documentation platform. A modern alternative to Confluence and Notion with real-time collaboration, diagram support, and multilingual capabilities.", + "tagline": "Collaborative wiki and documentation platform", + "version": "1.0.0", + "healthCheckPath": "/api/health", + "httpPort": 3000, + "addons": { + "postgresql": {}, + "localstorage": {}, + "sendmail": {}, + "redis": {} + }, + "manifestVersion": 2, + "website": "https://docmost.com", + "contactEmail": "support@docmost.com", + "icon": "logo.png", + "tags": [ + "productivity", + "collaboration", + "documentation", + "wiki", + "knowledge-base" + ], + "postInstallMessage": "Docmost has been installed successfully!\n\nTo get started:\n1. Access your Docmost instance at https://%s\n2. Create your first workspace and admin account\n3. Start collaborating on documentation\n\nDefault features include:\n- Real-time collaborative editing\n- Diagram support (Draw.io, Excalidraw, Mermaid)\n- Page history and permissions\n- Multilingual support\n\nFor more information, visit: https://docmost.com/docs", + "minBoxVersion": "7.0.0", + "maxBoxVersion": "8.0.0", + "memoryLimit": 512000000, + "optionalSso": true, + "mediaLinks": [ + "https://docmost.com", + "https://github.com/docmost/docmost" + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..28ec087 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +FROM cloudron/base:5.0.0 + +MAINTAINER Cloudron Support + +# Install Node.js 20 and pnpm +RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ + apt-get install -y nodejs && \ + npm install -g pnpm@latest + +# Set up application directory +WORKDIR /app/code + +# Clone and build Docmost +RUN git clone https://github.com/docmost/docmost.git . && \ + pnpm install && \ + pnpm build + +# Create necessary directories +RUN mkdir -p /tmp/data /app/data && \ + chown -R cloudron:cloudron /app/data /tmp/data + +# Copy startup scripts +COPY start.sh /app/code/ +COPY nginx.conf /etc/nginx/sites-available/default + +# Make scripts executable +RUN chmod +x /app/code/start.sh + +# Install supervisord for process management +RUN apt-get update && \ + apt-get install -y supervisor && \ + rm -rf /var/lib/apt/lists/* + +COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf + +EXPOSE 3000 + +CMD [ "/app/code/start.sh" ] \ No newline at end of file diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..421297e --- /dev/null +++ b/nginx.conf @@ -0,0 +1,82 @@ +server { + listen 3000 default_server; + listen [::]:3000 default_server; + + root /app/code; + index index.html; + + client_max_body_size 100m; + + # Enable compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied expired no-cache no-store private auth; + gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss; + + # Proxy to Docmost application + location / { + proxy_pass http://127.0.0.1:3001; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + proxy_read_timeout 86400; + } + + # WebSocket support for real-time collaboration + location /socket.io/ { + proxy_pass http://127.0.0.1:3001; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 86400; + } + + # OIDC callback endpoint + location /api/v1/session/callback { + proxy_pass http://127.0.0.1:3001; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # API endpoints + location /api/ { + proxy_pass http://127.0.0.1:3001; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 300; + } + + # Health check endpoint + location /api/health { + proxy_pass http://127.0.0.1:3001; + proxy_set_header Host $host; + access_log off; + } + + # Static files (if served by nginx) + location /uploads/ { + alias /app/data/uploads/; + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; +} \ No newline at end of file diff --git a/oidc-middleware.js b/oidc-middleware.js new file mode 100644 index 0000000..e5ad4ea --- /dev/null +++ b/oidc-middleware.js @@ -0,0 +1,145 @@ +const express = require('express'); +const jwt = require('jsonwebtoken'); +const axios = require('axios'); + +class CloudronOIDCMiddleware { + constructor(options = {}) { + this.clientId = process.env.CLOUDRON_OIDC_CLIENT_ID; + this.clientSecret = process.env.CLOUDRON_OIDC_CLIENT_SECRET; + this.issuer = process.env.CLOUDRON_OIDC_ISSUER; + this.redirectUri = process.env.OIDC_REDIRECT_URI; + this.appOrigin = process.env.CLOUDRON_APP_ORIGIN; + } + + // Middleware to check authentication + authenticate() { + return async (req, res, next) => { + try { + // Check for existing session/token + const token = req.headers.authorization?.replace('Bearer ', '') || + req.cookies?.authToken || + req.session?.token; + + if (token && this.verifyToken(token)) { + req.user = jwt.decode(token); + return next(); + } + + // If no valid token, redirect to OIDC login + if (req.path.startsWith('/api/')) { + return res.status(401).json({ error: 'Authentication required' }); + } + + // Redirect to OIDC authorization + const authUrl = this.buildAuthUrl(); + res.redirect(authUrl); + } catch (error) { + console.error('Authentication error:', error); + res.status(500).json({ error: 'Authentication failed' }); + } + }; + } + + // Build OIDC authorization URL + buildAuthUrl() { + const params = new URLSearchParams({ + response_type: 'code', + client_id: this.clientId, + redirect_uri: this.redirectUri, + scope: 'openid profile email', + state: this.generateState() + }); + + return `${this.issuer}/auth?${params.toString()}`; + } + + // Handle OIDC callback + async handleCallback(req, res) { + try { + const { code, state } = req.query; + + if (!code) { + return res.status(400).json({ error: 'Authorization code required' }); + } + + // Exchange code for tokens + const tokenResponse = await this.exchangeCodeForTokens(code); + const { access_token, id_token } = tokenResponse.data; + + // Verify and decode the ID token + const userInfo = jwt.decode(id_token); + + // Create user session + const sessionToken = this.createSessionToken(userInfo); + + // Set cookie and redirect + res.cookie('authToken', sessionToken, { + httpOnly: true, + secure: true, + sameSite: 'lax', + maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days + }); + + res.redirect('/'); + } catch (error) { + console.error('OIDC callback error:', error); + res.status(500).json({ error: 'Authentication callback failed' }); + } + } + + // Exchange authorization code for tokens + async exchangeCodeForTokens(code) { + const params = new URLSearchParams({ + grant_type: 'authorization_code', + code, + redirect_uri: this.redirectUri, + client_id: this.clientId, + client_secret: this.clientSecret + }); + + return axios.post(`${this.issuer}/token`, params, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }); + } + + // Create session token + createSessionToken(userInfo) { + const payload = { + sub: userInfo.sub, + email: userInfo.email, + name: userInfo.name, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60) // 30 days + }; + + return jwt.sign(payload, process.env.APP_SECRET); + } + + // Verify JWT token + verifyToken(token) { + try { + const decoded = jwt.verify(token, process.env.APP_SECRET); + return decoded.exp > Math.floor(Date.now() / 1000); + } catch (error) { + return false; + } + } + + // Generate random state for CSRF protection + generateState() { + return Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15); + } + + // Logout handler + logout() { + return (req, res) => { + res.clearCookie('authToken'); + res.redirect('/'); + }; + } +} + +module.exports = CloudronOIDCMiddleware; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..f8c65f2 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "docmost-cloudron", + "version": "1.0.0", + "description": "Cloudron package for Docmost", + "scripts": { + "start": "./start.sh" + }, + "dependencies": { + "express": "^4.18.2", + "jsonwebtoken": "^9.0.2", + "axios": "^1.6.0", + "cookie-parser": "^1.4.6" + } +} \ No newline at end of file diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..c73dd47 --- /dev/null +++ b/start.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +set -eu + +echo "=> Starting Docmost setup" + +# Initialize /app/data if it's empty (first run) +if [ ! -f /app/data/.initialized ]; then + echo "=> Initializing data directory" + mkdir -p /app/data/uploads /app/data/logs + chown -R cloudron:cloudron /app/data + touch /app/data/.initialized +fi + +# Generate APP_SECRET if not exists +if [ ! -f /app/data/app_secret ]; then + echo "=> Generating APP_SECRET" + openssl rand -hex 32 > /app/data/app_secret + chown cloudron:cloudron /app/data/app_secret + chmod 600 /app/data/app_secret +fi + +APP_SECRET=$(cat /app/data/app_secret) + +# Set up environment variables +export NODE_ENV=production +export APP_URL="${CLOUDRON_APP_ORIGIN}" +export APP_SECRET="${APP_SECRET}" +export PORT=3000 + +# Database configuration +export DATABASE_URL="${CLOUDRON_POSTGRESQL_URL}" + +# Redis configuration +export REDIS_URL="${CLOUDRON_REDIS_URL}" + +# Email configuration +export MAIL_DRIVER=smtp +export SMTP_HOST="${CLOUDRON_MAIL_SMTP_SERVER}" +export SMTP_PORT="${CLOUDRON_MAIL_SMTP_PORT}" +export SMTP_USERNAME="${CLOUDRON_MAIL_SMTP_USERNAME}" +export SMTP_PASSWORD="${CLOUDRON_MAIL_SMTP_PASSWORD}" +export MAIL_FROM_ADDRESS="${CLOUDRON_MAIL_FROM}" + +# Storage configuration (using local storage) +export STORAGE_DRIVER=local +export STORAGE_LOCAL_PATH="/app/data/uploads" + +# OIDC configuration for Cloudron authentication +export OIDC_CLIENT_ID="${CLOUDRON_OIDC_CLIENT_ID}" +export OIDC_CLIENT_SECRET="${CLOUDRON_OIDC_CLIENT_SECRET}" +export OIDC_ISSUER="${CLOUDRON_OIDC_ISSUER}" +export OIDC_REDIRECT_URI="${CLOUDRON_APP_ORIGIN}/api/v1/session/callback" + +# File upload configuration +export FILE_UPLOAD_SIZE_LIMIT="50mb" + +# JWT configuration +export JWT_TOKEN_EXPIRES_IN="30d" + +echo "=> Running database migrations" +cd /app/code +chown -R cloudron:cloudron /app/data +sudo -u cloudron pnpm prisma migrate deploy || true + +echo "=> Starting services with supervisor" +exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf \ No newline at end of file diff --git a/supervisord.conf b/supervisord.conf new file mode 100644 index 0000000..f59f3b3 --- /dev/null +++ b/supervisord.conf @@ -0,0 +1,26 @@ +[supervisord] +nodaemon=true +user=root +logfile=/dev/stdout +logfile_maxbytes=0 +pidfile=/run/supervisord.pid + +[program:nginx] +command=/usr/sbin/nginx -g "daemon off;" +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autorestart=true +priority=100 + +[program:docmost] +command=/usr/bin/sudo -u cloudron /usr/bin/node /app/code/apps/server/dist/main.js +directory=/app/code +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autorestart=true +priority=200 +environment=NODE_ENV=production \ No newline at end of file