167 lines
4.6 KiB
JavaScript
167 lines
4.6 KiB
JavaScript
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
|
|
function normalizeTrimmed(value) {
|
|
return String(value || '').trim()
|
|
}
|
|
|
|
function normalizeServerNames(serverNames) {
|
|
const names = Array.isArray(serverNames)
|
|
? serverNames
|
|
: String(serverNames || '').split(',')
|
|
|
|
const normalized = names
|
|
.map((item) => normalizeTrimmed(item))
|
|
.filter(Boolean)
|
|
|
|
if (normalized.length === 0) {
|
|
throw new Error('At least one server name is required.')
|
|
}
|
|
|
|
return normalized
|
|
}
|
|
|
|
function normalizePositiveInteger(value, label) {
|
|
const parsed = Number(value)
|
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
throw new Error(`${label} must be a positive integer.`)
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
export function resolveDeploymentRenderOptions(options = {}) {
|
|
const checkoutRoot = normalizeTrimmed(options.checkoutRoot)
|
|
if (!checkoutRoot) {
|
|
throw new Error('checkoutRoot is required.')
|
|
}
|
|
|
|
const serviceUser = normalizeTrimmed(options.serviceUser || 'hypertwist')
|
|
const serviceGroup = normalizeTrimmed(options.serviceGroup || serviceUser)
|
|
const serverNames = normalizeServerNames(options.serverNames || ['hypertwist.app', 'www.hypertwist.app'])
|
|
const certificateName = normalizeTrimmed(options.certificateName || serverNames[0])
|
|
const upstreamPort = normalizePositiveInteger(options.upstreamPort || 3011, 'upstreamPort')
|
|
const upstreamName = normalizeTrimmed(options.upstreamName || 'hypertwist_website_auth_server')
|
|
|
|
const websiteRoot = normalizeTrimmed(options.websiteRoot || path.join(checkoutRoot, 'website'))
|
|
const serverRoot = normalizeTrimmed(options.serverRoot || path.join(websiteRoot, 'server'))
|
|
const environmentFile = normalizeTrimmed(options.environmentFile || path.join(serverRoot, '.env'))
|
|
const nodeEnv = normalizeTrimmed(options.nodeEnv || 'production')
|
|
|
|
return {
|
|
checkoutRoot,
|
|
websiteRoot,
|
|
serverRoot,
|
|
environmentFile,
|
|
serviceUser,
|
|
serviceGroup,
|
|
nodeEnv,
|
|
serverNames,
|
|
certificateName,
|
|
upstreamPort,
|
|
upstreamName,
|
|
}
|
|
}
|
|
|
|
export function renderSystemdService(options = {}) {
|
|
const resolved = resolveDeploymentRenderOptions(options)
|
|
|
|
return `[Unit]
|
|
Description=HyperTwist same-origin website and auth server
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=${resolved.serviceUser}
|
|
Group=${resolved.serviceGroup}
|
|
WorkingDirectory=${resolved.serverRoot}
|
|
Environment=NODE_ENV=${resolved.nodeEnv}
|
|
EnvironmentFile=${resolved.environmentFile}
|
|
ExecStart=/usr/bin/npm start
|
|
Restart=always
|
|
RestartSec=5
|
|
TimeoutStopSec=20
|
|
KillSignal=SIGTERM
|
|
StandardOutput=journal
|
|
StandardError=journal
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
`
|
|
}
|
|
|
|
export function renderNginxConfig(options = {}) {
|
|
const resolved = resolveDeploymentRenderOptions(options)
|
|
const joinedServerNames = resolved.serverNames.join(' ')
|
|
|
|
return `map $http_upgrade $connection_upgrade {
|
|
default upgrade;
|
|
'' close;
|
|
}
|
|
|
|
upstream ${resolved.upstreamName} {
|
|
server 127.0.0.1:${resolved.upstreamPort};
|
|
keepalive 32;
|
|
}
|
|
|
|
server {
|
|
listen 80;
|
|
listen [::]:80;
|
|
server_name ${joinedServerNames};
|
|
|
|
return 301 https://$host$request_uri;
|
|
}
|
|
|
|
server {
|
|
listen 443 ssl http2;
|
|
listen [::]:443 ssl http2;
|
|
server_name ${joinedServerNames};
|
|
|
|
ssl_certificate /etc/letsencrypt/live/${resolved.certificateName}/fullchain.pem;
|
|
ssl_certificate_key /etc/letsencrypt/live/${resolved.certificateName}/privkey.pem;
|
|
include /etc/letsencrypt/options-ssl-nginx.conf;
|
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
|
|
|
client_max_body_size 10m;
|
|
|
|
location / {
|
|
proxy_pass http://${resolved.upstreamName};
|
|
proxy_http_version 1.1;
|
|
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 https;
|
|
proxy_set_header X-Forwarded-Host $host;
|
|
proxy_set_header X-Forwarded-Port 443;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection $connection_upgrade;
|
|
proxy_read_timeout 60s;
|
|
proxy_send_timeout 60s;
|
|
}
|
|
}
|
|
`
|
|
}
|
|
|
|
export function writeRenderedDeploymentFiles({
|
|
systemdOut,
|
|
nginxOut,
|
|
...options
|
|
}) {
|
|
const systemdContent = renderSystemdService(options)
|
|
const nginxContent = renderNginxConfig(options)
|
|
|
|
if (systemdOut) {
|
|
fs.mkdirSync(path.dirname(systemdOut), { recursive: true })
|
|
fs.writeFileSync(systemdOut, systemdContent, 'utf8')
|
|
}
|
|
|
|
if (nginxOut) {
|
|
fs.mkdirSync(path.dirname(nginxOut), { recursive: true })
|
|
fs.writeFileSync(nginxOut, nginxContent, 'utf8')
|
|
}
|
|
|
|
return {
|
|
systemdContent,
|
|
nginxContent,
|
|
}
|
|
}
|