rokojori-auth/workspace/add-subdomain.html

441 lines
16 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Add a Subdomain — rokojori</title>
<link rel="stylesheet" href="./_assets_/styles.css">
<link rel="stylesheet" href="./_assets_/nav.css">
<style>
.vars-card {
background: var(--surface);
border: 2px solid var(--accent);
border-radius: 8px;
padding: calc(var(--font-size) * 1.25) calc(var(--font-size) * 1.5);
margin-bottom: calc(var(--font-size) * 2);
}
.vars-card h3 { font-size: calc(var(--font-size) * 1); font-weight: 600; color: var(--text); margin-bottom: 0.25rem; }
.vars-card p { font-size: calc(var(--font-size) * 0.85); color: var(--muted); margin-bottom: calc(var(--font-size) * 0.75); }
.vars-grid { display: flex; gap: 1.5rem; flex-wrap: wrap; }
.vars-grid label { display: flex; flex-direction: column; gap: 0.3rem; font-size: calc(var(--font-size) * 0.78); color: var(--muted); }
.vars-grid input {
background: #0a0c14;
border: 1px solid var(--border);
border-radius: 4px;
color: var(--accent);
padding: 0.4rem 0.7rem;
font-family: ui-monospace, "Cascadia Code", monospace;
font-size: calc(var(--font-size) * 0.88);
width: 200px;
}
.vars-grid input:focus { outline: none; border-color: var(--accent); }
.v-sub, .v-port, .v-svc { color: #fff; font-weight: 600; }
</style>
</head>
<body>
<div class="page">
<header>
<p class="date">Guide</p>
<h1>Add a Subdomain</h1>
<p class="subtitle">
Step-by-step checklist for deploying a new Node.js service as a
<code>subdomain.rokojori.com</code> on Server A.
Fill in the variables below — all commands update automatically.
</p>
</header>
<!-- ─── Variables ────────────────────────────────────────────── -->
<div class="vars-card">
<h3>Variables</h3>
<p>Fill these in once and every command in the guide updates.</p>
<div class="vars-grid">
<label>
Subdomain
<input id="v-subdomain" type="text" placeholder="styles" spellcheck="false">
</label>
<label>
Port
<input id="v-port" type="text" placeholder="3002" spellcheck="false">
</label>
<label>
Service name (systemd)
<input id="v-service" type="text" placeholder="styles-rokojori" spellcheck="false">
</label>
</div>
</div>
<!-- ─── 0. CORS ──────────────────────────────────────────────── -->
<section>
<h2>0 — Ensure compatibility</h2>
<div class="card">
<h3>When do you need CORS?</h3>
<p>
If any other rokojori site loads resources from this service (fonts, CSS, JSON,
images) via a browser request — <code>@import</code>, <code>fetch</code>,
<code>&lt;img src&gt;</code> etc. — the browser enforces the
<strong style="color:var(--text)">same-origin policy</strong> and blocks the
response unless the server sends an <code>Access-Control-Allow-Origin</code>
header. Services that only serve their own pages and API calls to their own
frontend do not need CORS.
</p>
</div>
<div class="card">
<h3>Do not use <code>*</code></h3>
<p>
<code>Access-Control-Allow-Origin: *</code> allows any website on the internet
to load your resources. For public CDN assets that is acceptable, but for
rokojori services restrict it to known origins.
</p>
<p style="margin-top:0.75rem">
Because the header cannot carry a wildcard pattern like
<code>*.rokojori.com</code>, the server must check the incoming
<code>Origin</code> header against an allowlist and echo back the matched
origin. Unrecognised origins receive no header and the browser blocks them.
</p>
</div>
<div class="card">
<h3>Pattern — allowlist middleware</h3>
<p>Add this to <code>source/server/index.ts</code> before the routes that need it:</p>
<pre><code>const CORS_ALLOWED: Array&lt;string | RegExp&gt; = [
/^https?:\/\/([\w-]+\.)?rokojori\.com$/, // all *.rokojori.com subdomains
// 'https://someother.site', // add extra origins here
];
function allowCors(
req: express.Request,
res: express.Response,
next: express.NextFunction
): void
{
const origin = req.headers.origin;
if ( origin && CORS_ALLOWED.some( rule =>
typeof rule === 'string' ? rule === origin : rule.test( origin )
) )
{
res.set( 'Access-Control-Allow-Origin', origin );
}
next();
}
// Apply before the routes that serve cross-origin resources:
app.use( '/your-public-route', allowCors );</code></pre>
</div>
<div class="card">
<h3>What the regex matches</h3>
<p>
<code>/^https?:\/\/([\w-]+\.)?rokojori\.com$/</code> accepts:
<code>https://rokojori.com</code>,
<code>https://roject.rokojori.com</code>,
<code>https://styles.rokojori.com</code>, and so on.
HTTP is included so local dev with a mapped host still works.
Extend <code>CORS_ALLOWED</code> with additional strings or regexes for
any external sites that need access.
</p>
</div>
</section>
<!-- ─── 1. Gitea repo ─────────────────────────────────────────── -->
<section>
<h2>1 — Create the repository</h2>
<div class="card">
<h3>New repo on Gitea</h3>
<p>
Log in to <strong style="color:var(--text)">community.rokojori.com</strong>,
click <em>New Repository</em>, name it (e.g. <code><span class="v-sub">subdomain</span></code>),
set it to private, and copy the clone URL.
Do not initialise with a README — the local project is already set up.
</p>
</div>
</section>
<!-- ─── 2. .gitignore, commit, push ──────────────────────────── -->
<section>
<h2>2 — .gitignore, commit, push</h2>
<div class="card">
<h3>.gitignore</h3>
<p>Create this at the project root before the first commit:</p>
<pre><code>node_modules/
dist/
build/
.env
storage/</code></pre>
</div>
<div class="card">
<h3>Initial commit and push</h3>
<pre><code>git init
git add .
git commit -m "init"
git remote add origin https://community.rokojori.com/Rokojori/<span class="v-sub">subdomain</span>.git
git push -u origin main</code></pre>
</div>
</section>
<!-- ─── 3. DNS on IONOS ───────────────────────────────────────── -->
<section>
<h2>3 — DNS record on IONOS</h2>
<div class="card">
<h3>Add A record</h3>
<p>
Log in to IONOS → Domains &amp; SSL → <strong style="color:var(--text)">rokojori.com</strong>
→ DNS. Add a new record:
</p>
<pre><code>Type: A
Name: <span class="v-sub">subdomain</span>
Value: &lt;Server A IP&gt;
TTL: 300</code></pre>
<p style="margin-top:0.75rem">
Wait a few minutes for propagation before running certbot.
Verify with <code>ping <span class="v-sub">subdomain</span>.rokojori.com</code> from any machine.
</p>
</div>
</section>
<!-- ─── 4. Clone on server ────────────────────────────────────── -->
<section>
<h2>4 — Clone on the server</h2>
<div class="card">
<h3>SSH and clone</h3>
<pre><code>ssh root@&lt;Server A IP&gt;
cd /opt
git clone https://community.rokojori.com/Rokojori/<span class="v-sub">subdomain</span>.git <span class="v-sub">subdomain</span>
cd <span class="v-sub">subdomain</span></code></pre>
</div>
</section>
<!-- ─── 5. Install / build / configure ───────────────────────── -->
<section>
<h2>5 — Install, build, configure</h2>
<div class="card">
<h3>Install dependencies</h3>
<pre><code>npm install</code></pre>
</div>
<div class="card">
<h3>Copy pages (if the project has source/pages/)</h3>
<pre><code>node scripts/copy-pages.js</code></pre>
</div>
<div class="card">
<h3>Create .env</h3>
<p>
Copy the contents from your local <code>.env</code> and paste them on the server.
Update any values that differ in production (port, secrets, URLs).
</p>
<pre><code>nano .env</code></pre>
</div>
<div class="card">
<h3>Create storage directories (if needed)</h3>
<pre><code># example for styles.rokojori.com
mkdir -p storage/fonts</code></pre>
</div>
<div class="card">
<h3>Quick smoke test</h3>
<p>
Run the server once manually to confirm it starts. Kill it with
<code>Ctrl+C</code> before moving to the systemd step — leave it
running and the port will conflict.
</p>
<pre><code>npm run dev
# → service running on http://localhost:<span class="v-port">PORT</span>
# Ctrl+C to stop</code></pre>
</div>
</section>
<!-- ─── 6. nginx HTTP config ──────────────────────────────────── -->
<section>
<h2>6 — nginx config (HTTP only)</h2>
<div class="card">
<h3>Create the site config</h3>
<pre><code>nano /etc/nginx/sites-available/<span class="v-sub">subdomain</span>.rokojori.com</code></pre>
<pre><code>server {
listen 80;
server_name <span class="v-sub">subdomain</span>.rokojori.com;
location / {
proxy_pass http://localhost:<span class="v-port">PORT</span>;
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;
}
}</code></pre>
</div>
<div class="card">
<h3>Enable and reload</h3>
<pre><code>ln -s /etc/nginx/sites-available/<span class="v-sub">subdomain</span>.rokojori.com \
/etc/nginx/sites-enabled/<span class="v-sub">subdomain</span>.rokojori.com
nginx -t
systemctl reload nginx</code></pre>
</div>
</section>
<!-- ─── 7. Let's Encrypt ──────────────────────────────────────── -->
<section>
<h2>7 — Let's Encrypt certificate</h2>
<div class="card">
<h3>Run certbot</h3>
<p>
The <code>--nginx</code> plugin handles the ACME HTTP-01 challenge
through nginx directly — the app does not need to be running.
When prompted whether to redirect HTTP to HTTPS, choose
<strong style="color:var(--text)">Redirect</strong> (option 2).
</p>
<pre><code>certbot --nginx -d <span class="v-sub">subdomain</span>.rokojori.com</code></pre>
<p style="margin-top:0.75rem">
Certbot modifies the nginx config in-place, adding TLS directives and
an HTTP→HTTPS redirect block. It also installs a systemd timer for
automatic renewal — no extra setup needed.
</p>
</div>
</section>
<!-- ─── 8. nginx HTTPS ────────────────────────────────────────── -->
<section>
<h2>8 — Verify nginx HTTPS config</h2>
<div class="card">
<h3>Check and reload</h3>
<p>
Certbot updates the config automatically, but always verify before reloading:
</p>
<pre><code>nginx -t
systemctl reload nginx</code></pre>
</div>
<div class="card">
<h3>Confirm TLS is live</h3>
<pre><code>curl -I https://<span class="v-sub">subdomain</span>.rokojori.com</code></pre>
<p style="margin-top:0.5rem">
Expect <code>HTTP/2 200</code> (or 502 if the app is not yet running — that is fine at this stage).
</p>
</div>
</section>
<!-- ─── 9. systemd service ────────────────────────────────────── -->
<section>
<h2>9 — systemd service</h2>
<div class="card">
<h3>Create the unit file</h3>
<pre><code>nano /etc/systemd/system/<span class="v-svc">service-name</span>.service</code></pre>
<pre><code>[Unit]
Description=<span class="v-svc">service-name</span>
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/<span class="v-sub">subdomain</span>
ExecStart=/usr/bin/npx ts-node --project tsconfig.ts-node.json source/server/index.ts
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target</code></pre>
<p style="margin-top:0.75rem">
The <code>.env</code> file is loaded by <code>import 'dotenv/config'</code>
inside the app, so no <code>EnvironmentFile=</code> is needed.
</p>
</div>
<div class="card">
<h3>Enable and start</h3>
<pre><code>systemctl daemon-reload
systemctl enable <span class="v-svc">service-name</span>
systemctl start <span class="v-svc">service-name</span>
systemctl status <span class="v-svc">service-name</span></code></pre>
</div>
<div class="card">
<h3>Check logs</h3>
<pre><code>journalctl -u <span class="v-svc">service-name</span> -f</code></pre>
</div>
</section>
<!-- ─── Quick reference ───────────────────────────────────────── -->
<section>
<h2>Quick reference</h2>
<div class="card">
<h3>Deploy an update</h3>
<pre><code>cd /opt/<span class="v-sub">subdomain</span>
git pull
npm install
node scripts/copy-pages.js # if applicable
systemctl restart <span class="v-svc">service-name</span></code></pre>
</div>
<div class="card">
<h3>Common commands</h3>
<pre><code>systemctl restart <span class="v-svc">service-name</span> # restart app
systemctl status <span class="v-svc">service-name</span> # current state
journalctl -u <span class="v-svc">service-name</span> -n 100 # last 100 log lines
nginx -t # validate nginx config
systemctl reload nginx # apply nginx changes</code></pre>
</div>
<div class="card">
<h3>Certificate renewal</h3>
<p>
Certbot installs a systemd timer (<code>certbot.timer</code>) that
renews certificates automatically before expiry. Check its status with:
</p>
<pre><code>systemctl status certbot.timer</code></pre>
</div>
</section>
<footer>
rokojori-auth &mdash; add-subdomain guide
</footer>
</div>
<script>
const subInput = document.getElementById('v-subdomain');
const portInput = document.getElementById('v-port');
const svcInput = document.getElementById('v-service');
function update()
{
const sub = subInput.value.trim() || 'subdomain';
const port = portInput.value.trim() || 'PORT';
const svc = svcInput.value.trim() || 'service-name';
document.querySelectorAll('.v-sub').forEach( el => el.textContent = sub );
document.querySelectorAll('.v-port').forEach( el => el.textContent = port );
document.querySelectorAll('.v-svc').forEach( el => el.textContent = svc );
}
subInput.addEventListener( 'input', update );
portInput.addEventListener( 'input', update );
svcInput.addEventListener( 'input', update );
</script>
<script>var NAV_ROOT = './';</script>
<script src="./_assets_/nav-data.js"></script>
<script src="./_assets_/nav.js"></script>
</body>
</html>