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