diff --git a/INSTALLATION.md b/INSTALLATION.md new file mode 100644 index 0000000..560abd6 --- /dev/null +++ b/INSTALLATION.md @@ -0,0 +1,171 @@ +# Installation Guide - Ubuntu + +Panduan instalasi `doclink_web` di Ubuntu dengan Apache reverse proxy untuk URL publik seperti: + +```text +https://domain.com/docklink-desktop +``` + +## Prerequisites + +- Ubuntu Server +- Node.js 18+ atau yang kompatibel +- Apache2 +- SSL certificate jika ingin HTTPS + +## 1. Copy Project + +Masuk ke folder project: + +```bash +cd /path/to/doclink_web +``` + +Pastikan file berikut ada: + +- `server.js` +- `styles.css` +- `logo.png` +- `start.sh` +- `stop.sh` + +## 2. Jalankan App + +Set base path sesuai URL publik: + +```bash +export DOCLINK_BASE_PATH=/docklink-desktop +export PORT=5173 +node server.js +``` + +Kalau mau jalan di background, pakai script: + +```bash +DOCLINK_BASE_PATH=/docklink-desktop PORT=5173 ./start.sh +``` + +### Catatan + +- `DOCLINK_BASE_PATH` harus sama dengan path publik di browser. +- Nama folder project di disk tidak harus sama. +- Aplikasi tetap bisa diakses lewat port lokal `5173`, lalu Apache yang meneruskan request ke sana. + +## 3. Install Dependencies Ubuntu + +Update paket dan install Apache: + +```bash +sudo apt update +sudo apt install -y apache2 +``` + +Kalau Node.js belum ada, install sesuai versi yang kamu pakai di server. + +## 4. Aktifkan Module Apache + +Di Ubuntu / Debian: + +```bash +sudo a2enmod proxy proxy_http ssl +sudo systemctl restart apache2 +``` + +## 5. Buat Virtual Host + +Edit konfigurasi Apache di Ubuntu: + +```bash +sudo nano /etc/apache2/sites-available/domain.conf +``` + +Contoh konfigurasi: + +```apache + + ServerName domain.com + Redirect permanent / https://domain.com/ + + + + ServerName domain.com + + SSLEngine on + SSLCertificateFile /path/to/fullchain.pem + SSLCertificateKeyFile /path/to/privkey.pem + + ProxyPreserveHost On + ProxyRequests Off + + RedirectMatch 301 ^/docklink-desktop$ /docklink-desktop/ + + ProxyPass /docklink-desktop/ http://127.0.0.1:5173/ + ProxyPassReverse /docklink-desktop/ http://127.0.0.1:5173/ + +``` + +## 6. Enable Site + +Aktifkan site dan reload Apache: + +```bash +sudo a2ensite domain.conf +sudo systemctl reload apache2 +``` + +## 7. Test + +Buka URL berikut: + +```text +https://domain.com/docklink-desktop/ +``` + +Kalau benar, aplikasi akan tampil tanpa expose port `5173` ke publik. + +## 8. Stop App + +Kalau pakai `start.sh`: + +```bash +./stop.sh +``` + +Kalau mau stop port tertentu: + +```bash +./stop.sh 5173 +``` + +## Troubleshooting + +### Halaman tidak muncul + +- Pastikan Node.js app masih jalan di `127.0.0.1:5173` +- Pastikan Apache module `proxy` dan `proxy_http` aktif +- Pastikan `DOCLINK_BASE_PATH=/docklink-desktop` + +## Ubuntu Notes + +- File konfigurasi Apache biasanya ada di: + - `/etc/apache2/sites-available/` + - `/etc/apache2/sites-enabled/` +- Log Apache biasanya ada di: + - `/var/log/apache2/access.log` + - `/var/log/apache2/error.log` +- Service Apache dikelola dengan: + ```bash + sudo systemctl status apache2 + sudo systemctl restart apache2 + sudo systemctl reload apache2 + ``` + +### CSS atau logo tidak loading + +- Cek apakah request asset ikut lewat path `/docklink-desktop/...` +- Pastikan Apache proxy tidak memotong base path secara salah + +### Redirect balik ke root + +- Pastikan semua request ke app memakai base path yang sama +- Gunakan `DOCLINK_BASE_PATH=/docklink-desktop` diff --git a/INSTALLATION_UBUNTU.md b/INSTALLATION_UBUNTU.md new file mode 100644 index 0000000..fba016a --- /dev/null +++ b/INSTALLATION_UBUNTU.md @@ -0,0 +1,246 @@ +# DocLink Web Installation Guide for Ubuntu + +This guide explains how to run `doclink_web` on Ubuntu behind Apache, using a subfolder URL such as: + +```text +https://domain.com/docklink-desktop +``` + +## 1. Requirements + +- Ubuntu Server +- Apache 2 +- Node.js 18+ or compatible +- A valid SSL certificate if you want HTTPS + +## 2. Copy the Application + +Place the project on the server and enter the project directory: + +```bash +cd /path/to/doclink_web +``` + +Make sure these files exist: + +- `server.js` +- `styles.css` +- `logo.png` +- `start.sh` +- `stop.sh` + +## 3. Install Apache on Ubuntu + +Update package lists and install Apache: + +```bash +sudo apt update +sudo apt install -y apache2 +``` + +If Node.js is not installed yet, install the version you want to use on the server. + +## 4. Create the systemd Service + +The app should run as a service so it starts automatically and keeps running in the background. + +Create an environment file, for example: + +```bash +sudo nano /etc/doclink-web.env +``` + +Example content: + +```bash +DOCLINK_BASE_PATH=/docklink-desktop +PORT=5173 +DOCLINK_API_BASE=https://devbandungraya.aplikasi.web.id/one-api-doctor/doctor_mitra +``` + +Then create the service file: + +```bash +sudo nano /etc/systemd/system/doclink-web.service +``` + +Example content: + +```ini +[Unit] +Description=DocLink Web +After=network.target + +[Service] +Type=simple +WorkingDirectory=/path/to/doclink_web +EnvironmentFile=/etc/doclink-web.env +ExecStart=/usr/bin/node /path/to/doclink_web/server.js +Restart=on-failure +RestartSec=3 +User=www-data +Group=www-data + +[Install] +WantedBy=multi-user.target +``` + +Reload systemd and enable the service: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable doclink-web +sudo systemctl start doclink-web +``` + +Check status: + +```bash +sudo systemctl status doclink-web +``` + +## 5. Run the Application Manually + +This step is optional. Use it only if you want to test the app without systemd. + +Run it directly: + +```bash +export DOCLINK_BASE_PATH=/docklink-desktop +export PORT=5173 +node server.js +``` + +If you prefer the helper script: + +```bash +DOCLINK_BASE_PATH=/docklink-desktop PORT=5173 ./start.sh +``` + +Important notes: + +- `DOCLINK_BASE_PATH` must match the public URL path. +- The local project folder name can be anything. +- Apache will forward requests to the Node.js app on `127.0.0.1:5173`. + +## 6. Enable Apache Modules + +Enable the required proxy and SSL modules: + +```bash +sudo a2enmod proxy proxy_http ssl +sudo systemctl restart apache2 +``` + +## 7. Configure Apache Virtual Host + +Create or edit your site config in: + +```bash +sudo nano /etc/apache2/sites-available/domain.conf +``` + +Example configuration: + +```apache + + ServerName domain.com + Redirect permanent / https://domain.com/ + + + + ServerName domain.com + + SSLEngine on + SSLCertificateFile /path/to/fullchain.pem + SSLCertificateKeyFile /path/to/privkey.pem + + ProxyPreserveHost On + ProxyRequests Off + + RedirectMatch 301 ^/docklink-desktop$ /docklink-desktop/ + + ProxyPass /docklink-desktop/ http://127.0.0.1:5173/ + ProxyPassReverse /docklink-desktop/ http://127.0.0.1:5173/ + +``` + +If you want a reusable deployment template, start from: + +- `deploy/apache-vhost.template.conf` +- `deploy/env.template` + +Then replace: + +- `__SERVER_NAME__` +- `__BASE_PATH__` +- `__APP_PORT__` +- `__SSL_CERT_FILE__` +- `__SSL_KEY_FILE__` + +## 8. Enable the Site + +Enable the site and reload Apache: + +```bash +sudo a2ensite domain.conf +sudo systemctl reload apache2 +``` + +## 9. Verify the Deployment + +Open the application in the browser: + +```text +https://domain.com/docklink-desktop/ +``` + +If everything is configured correctly, the application should load without exposing port `5173` publicly. + +## 10. Stop the Application + +If you started the app with the helper script: + +```bash +./stop.sh +``` + +Or stop a specific port: + +```bash +./stop.sh 5173 +``` + +## 11. Ubuntu-Specific Locations + +- Apache site configs: + - `/etc/apache2/sites-available/` + - `/etc/apache2/sites-enabled/` +- Apache logs: + - `/var/log/apache2/access.log` + - `/var/log/apache2/error.log` +- Apache service commands: + ```bash + sudo systemctl status apache2 + sudo systemctl restart apache2 + sudo systemctl reload apache2 + ``` + +## 12. Troubleshooting + +### The page does not open + +- Check that the `doclink-web` service is running +- Check that the Node.js app is listening on `127.0.0.1:5173` +- Check that Apache modules `proxy` and `proxy_http` are enabled +- Confirm that `DOCLINK_BASE_PATH=/docklink-desktop` + +### CSS or logo does not load + +- Confirm requests are going through `/docklink-desktop/...` +- Check the Apache proxy configuration for the correct trailing slash + +### Redirects go back to `/` + +- Make sure every public request uses the same base path +- Keep `DOCLINK_BASE_PATH` aligned with the URL folder diff --git a/README.md b/README.md index 950853f..e7e5227 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,14 @@ Open: http://localhost:5173 ``` +To serve under a subfolder like `https://domain.com/folder`, set: + +```bash +DOCLINK_BASE_PATH=/folder npm start +``` + +That makes internal links, HTMX requests, and redirects stay under the same folder. + ## Click To Run If you want a one-click launcher on macOS/Linux: @@ -55,6 +63,7 @@ If you run the server in the foreground, stop it with `Ctrl+C`. - If the upstream login rejects the credentials or is unavailable, demo mode creates a local session so the UI can still be exercised. - Order search, order detail helpers, FPP loading, password change, and special message save are wired through the API adapter with mock fallback for local preview. - `start.sh` writes a pid file named `.doclink-web.pid` and a log file named `doclink-web.log`. +- Deployment templates are in `deploy/`. ## Main Files diff --git a/deploy/apache-vhost.template.conf b/deploy/apache-vhost.template.conf new file mode 100644 index 0000000..1ee27c5 --- /dev/null +++ b/deploy/apache-vhost.template.conf @@ -0,0 +1,20 @@ + + ServerName __SERVER_NAME__ + Redirect permanent / https://__SERVER_NAME__/ + + + + ServerName __SERVER_NAME__ + + SSLEngine on + SSLCertificateFile __SSL_CERT_FILE__ + SSLCertificateKeyFile __SSL_KEY_FILE__ + + ProxyPreserveHost On + ProxyRequests Off + + RedirectMatch 301 ^/__BASE_PATH__$ /__BASE_PATH__/ + + ProxyPass /__BASE_PATH__/ http://127.0.0.1:__APP_PORT__/ + ProxyPassReverse /__BASE_PATH__/ http://127.0.0.1:__APP_PORT__/ + diff --git a/deploy/doclink-web.service.template b/deploy/doclink-web.service.template new file mode 100644 index 0000000..d5511bf --- /dev/null +++ b/deploy/doclink-web.service.template @@ -0,0 +1,16 @@ +[Unit] +Description=DocLink Web +After=network.target + +[Service] +Type=simple +WorkingDirectory=__WORKING_DIRECTORY__ +EnvironmentFile=__ENV_FILE__ +ExecStart=/usr/bin/node __WORKING_DIRECTORY__/server.js +Restart=on-failure +RestartSec=3 +User=__RUN_USER__ +Group=__RUN_GROUP__ + +[Install] +WantedBy=multi-user.target diff --git a/deploy/env.template b/deploy/env.template new file mode 100644 index 0000000..bfe5e0d --- /dev/null +++ b/deploy/env.template @@ -0,0 +1,3 @@ +DOCLINK_BASE_PATH=/__BASE_PATH__ +PORT=__APP_PORT__ +DOCLINK_API_BASE=https://devbandungraya.aplikasi.web.id/one-api-doctor/doctor_mitra diff --git a/server.js b/server.js index 8a7f399..1656925 100644 --- a/server.js +++ b/server.js @@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises"; import { extname } from "node:path"; const PORT = Number(process.env.PORT || 5173); +const BASE_PATH = normalizeBasePath(process.env.DOCLINK_BASE_PATH || ""); const API_BASE = process.env.DOCLINK_API_BASE || "https://devbandungraya.aplikasi.web.id/one-api-doctor/doctor_mitra"; @@ -23,6 +24,36 @@ function escapeHtml(value) { .replaceAll('"', """); } +function normalizeBasePath(value) { + const raw = String(value || "").trim(); + if (!raw || raw === "/") return ""; + const withLeadingSlash = raw.startsWith("/") ? raw : `/${raw}`; + return withLeadingSlash.replace(/\/+$/, ""); +} + +function appPath(pathname = "/") { + const text = String(pathname || "/"); + if (/^https?:\/\//i.test(text) || text.startsWith("//")) return text; + if (!BASE_PATH) return text.startsWith("/") ? text : `/${text}`; + if (text === BASE_PATH || text.startsWith(`${BASE_PATH}/`)) return text; + if (text === "/" || text === "") return BASE_PATH; + if (text.startsWith("/")) return `${BASE_PATH}${text}`; + return `${BASE_PATH}/${text}`; +} + +function stripBasePath(pathname) { + const path = String(pathname || "/"); + if (!BASE_PATH) return path || "/"; + if (path === BASE_PATH) return "/"; + if (path.startsWith(`${BASE_PATH}/`)) return path.slice(BASE_PATH.length) || "/"; + return path; +} + +function rewriteHtmlPaths(html) { + if (!BASE_PATH || typeof html !== "string") return html; + return html.replace(/\b(href|src|action|hx-get|hx-post)="\/(?!\/)/g, `$1="${BASE_PATH}/`); +} + function statusClass(status) { const mapping = { Processing: "warning", @@ -1998,13 +2029,15 @@ function json(res, status, payload) { } function redirect(res, location, headers = {}) { - res.writeHead(302, { Location: location, ...headers }); + const resolvedLocation = typeof location === "string" && location.startsWith("/") ? appPath(location) : location; + res.writeHead(302, { Location: resolvedLocation, ...headers }); res.end(); } function html(res, status, body, headers = {}) { + const output = rewriteHtmlPaths(body); res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", ...headers }); - res.end(body); + res.end(output); } function isHtmx(req) { @@ -2204,7 +2237,7 @@ async function fragmentResultDetail(session, resultId) { } async function renderRoute(req, res, url) { - const path = url.pathname; + const path = stripBasePath(url.pathname); const query = Object.fromEntries(url.searchParams.entries()); const session = readSession(req); const authed = Boolean(session); @@ -2572,7 +2605,7 @@ async function renderRoute(req, res, url) { return; } - html(res, 404, emptyRoute(path)); + html(res, 404, emptyRoute(url.pathname)); } const server = http.createServer(async (req, res) => {