Initial Docmost Cloudron package
This commit is contained in:
36
CloudronManifest.json
Normal file
36
CloudronManifest.json
Normal file
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
38
Dockerfile
Normal file
38
Dockerfile
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
FROM cloudron/base:5.0.0
|
||||||
|
|
||||||
|
MAINTAINER Cloudron Support <support@cloudron.io>
|
||||||
|
|
||||||
|
# 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" ]
|
82
nginx.conf
Normal file
82
nginx.conf
Normal file
@@ -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;
|
||||||
|
}
|
145
oidc-middleware.js
Normal file
145
oidc-middleware.js
Normal file
@@ -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;
|
14
package.json
Normal file
14
package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
67
start.sh
Normal file
67
start.sh
Normal file
@@ -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
|
26
supervisord.conf
Normal file
26
supervisord.conf
Normal file
@@ -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
|
Reference in New Issue
Block a user