Compare commits
2 Commits
d957d5ef39
...
dbda44112e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbda44112e | ||
|
|
ab4d56bfef |
171
INSTALLATION.md
Normal file
171
INSTALLATION.md
Normal file
@@ -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
|
||||
<VirtualHost *:80>
|
||||
ServerName domain.com
|
||||
Redirect permanent / https://domain.com/
|
||||
</VirtualHost>
|
||||
|
||||
<VirtualHost *:443>
|
||||
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/
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
## 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`
|
||||
246
INSTALLATION_UBUNTU.md
Normal file
246
INSTALLATION_UBUNTU.md
Normal file
@@ -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
|
||||
<VirtualHost *:80>
|
||||
ServerName domain.com
|
||||
Redirect permanent / https://domain.com/
|
||||
</VirtualHost>
|
||||
|
||||
<VirtualHost *:443>
|
||||
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/
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
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
|
||||
@@ -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
|
||||
|
||||
|
||||
20
deploy/apache-vhost.template.conf
Normal file
20
deploy/apache-vhost.template.conf
Normal file
@@ -0,0 +1,20 @@
|
||||
<VirtualHost *:80>
|
||||
ServerName __SERVER_NAME__
|
||||
Redirect permanent / https://__SERVER_NAME__/
|
||||
</VirtualHost>
|
||||
|
||||
<VirtualHost *:443>
|
||||
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__/
|
||||
</VirtualHost>
|
||||
16
deploy/doclink-web.service.template
Normal file
16
deploy/doclink-web.service.template
Normal file
@@ -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
|
||||
3
deploy/env.template
Normal file
3
deploy/env.template
Normal file
@@ -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
|
||||
84
server.js
84
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",
|
||||
@@ -359,7 +390,7 @@ function layout(title, body, { authenticated = false, activePath = "/", subtitle
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function topbar(activePath, subtitle, accountName = "", accountMeta = "") {
|
||||
function topbar(activePath, subtitle, accountName = "") {
|
||||
const titleMap = {
|
||||
"/": ["Dashboard", "Overview of orders, results, and work queues."],
|
||||
"/orders": ["Orders", "Search, review, and create patient orders."],
|
||||
@@ -370,7 +401,6 @@ function topbar(activePath, subtitle, accountName = "", accountMeta = "") {
|
||||
};
|
||||
const [title, fallbackSubtitle] = titleMap[activePath] || ["DocLink Web", "Responsive clinical workflow shell."];
|
||||
const displayName = accountName || sampleLogin.username;
|
||||
const displayMeta = accountMeta || `Doctor ID ${sampleLogin.doctorId}`;
|
||||
return `
|
||||
<header class="topbar">
|
||||
<div class="topbar-main">
|
||||
@@ -384,7 +414,6 @@ function topbar(activePath, subtitle, accountName = "", accountMeta = "") {
|
||||
<span class="account-avatar" aria-hidden="true">DL</span>
|
||||
<span class="account-copy">
|
||||
<strong>${escapeHtml(displayName)}</strong>
|
||||
<small>${escapeHtml(displayMeta)}</small>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
@@ -483,8 +512,7 @@ function resolveFppRouteIds(session) {
|
||||
|
||||
function accountLayoutOptions(session = {}) {
|
||||
return {
|
||||
accountName: session?.username || sampleLogin.username,
|
||||
accountMeta: session?.doctorId ? `Doctor ID ${session.doctorId}` : `Doctor ID ${sampleLogin.doctorId}`,
|
||||
accountName: session?.displayName || session?.username || sampleLogin.username,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1287,10 +1315,10 @@ function renderSettings(session) {
|
||||
<section class="panel">
|
||||
${panelHeader("Account", "Profile data and security entry points.")}
|
||||
<div class="grid grid-2">
|
||||
<div class="card"><span class="muted">Name</span><strong style="display:block; margin-top:8px">${escapeHtml(session?.username || "-")}</strong></div>
|
||||
<div class="card"><span class="muted">Name</span><strong style="display:block; margin-top:8px">${escapeHtml(session?.displayName || session?.username || "-")}</strong></div>
|
||||
<div class="card"><span class="muted">Username</span><strong style="display:block; margin-top:8px">${escapeHtml(session?.loginUsername || "-")}</strong></div>
|
||||
<div class="card"><span class="muted">Doctor ID</span><strong style="display:block; margin-top:8px">${escapeHtml(session?.doctorCode || session?.doctorId || "-")}</strong></div>
|
||||
<div class="card"><span class="muted">Role</span><strong style="display:block; margin-top:8px">Doctor</strong></div>
|
||||
<div class="card"><span class="muted">Internal ID</span><strong style="display:block; margin-top:8px">${escapeHtml(session?.doctorId || "-")}</strong></div>
|
||||
</div>
|
||||
</section>
|
||||
<aside class="panel">
|
||||
@@ -1312,10 +1340,11 @@ function renderSettings(session) {
|
||||
`;
|
||||
}
|
||||
|
||||
function renderChangePassword(session) {
|
||||
function renderChangePassword(session, error = "") {
|
||||
return `
|
||||
<section class="panel">
|
||||
${panelHeader("Change password", "Inline validation and a straightforward submit path.")}
|
||||
${error ? `<div class="note-box" style="margin-bottom:16px">${escapeHtml(error)}</div>` : ""}
|
||||
<form class="form" action="/settings/change-password" method="post">
|
||||
<div class="field-inline">
|
||||
<label class="field"><span>Current password</span><input type="password" name="current_password" placeholder="••••••••" required /></label>
|
||||
@@ -1323,9 +1352,12 @@ function renderChangePassword(session) {
|
||||
</div>
|
||||
<div class="field-inline">
|
||||
<label class="field"><span>Confirm password</span><input type="password" name="confirm_password" placeholder="Repeat new password" required /></label>
|
||||
<input type="hidden" name="login_username" value="${escapeHtml(session?.loginUsername || "")}" />
|
||||
<label class="field"><span>Username</span><input value="${escapeHtml(session?.loginUsername || "-")}" readonly /></label>
|
||||
<label class="field"><span>Doctor ID</span><input name="doctor_id" value="${escapeHtml(session?.doctorCode || session?.doctorId || "")}" readonly /></label>
|
||||
</div>
|
||||
<div class="topbar-actions" style="justify-content:flex-start">
|
||||
<div class="note-box" style="margin-right:auto">Password minimal 8 digit, 1 huruf kecil, 1 huruf besar, dan 1 angka.</div>
|
||||
<button class="btn btn-primary" type="submit">Save password</button>
|
||||
<a class="btn btn-secondary" href="/settings">Cancel</a>
|
||||
</div>
|
||||
@@ -1348,7 +1380,7 @@ function loginPage({ error = "" } = {}) {
|
||||
<form class="form" action="/login" method="post">
|
||||
<label class="field"><span>Username</span><input name="username" value="${escapeHtml(sampleLogin.username)}" required /></label>
|
||||
<label class="field"><span>Doctor ID</span><input name="doctor_id" value="${escapeHtml(sampleLogin.doctorId)}" required /></label>
|
||||
<label class="field"><span>Password</span><input type="password" name="password" placeholder="${escapeHtml(sampleLogin.password)}" required /></label>
|
||||
<label class="field"><span>Password</span><input type="password" name="password" required /></label>
|
||||
<button class="btn btn-primary" type="submit">${icon("login")} Login</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -1595,14 +1627,17 @@ function normalizeSession(payload) {
|
||||
const data = payload?.data || payload?.result || payload;
|
||||
const user = data?.user || data?.data || {};
|
||||
const token = data?.token || data?.access_token || data?.accessToken || payload?.token || "";
|
||||
const username = user?.M_UserUsername || data?.username || data?.M_UserUsername || data?.name || "";
|
||||
const displayName = user?.M_UserUsername || data?.M_UserUsername || data?.name || data?.display_name || "";
|
||||
const loginUsername = data?.username || user?.username || data?.login_username || data?.user_name || "";
|
||||
const doctorId = user?.M_UserM_DoctorID || data?.doctor_id || data?.doctorId || "";
|
||||
const doctorCode = user?.M_UserM_DoctorCode || data?.doctor_code || "";
|
||||
const mouId = user?.M_UserM_MouID || data?.M_UserM_MouID || data?.mou_id || "";
|
||||
const userId = user?.M_UserID || data?.M_UserID || data?.user_id || "";
|
||||
return {
|
||||
token,
|
||||
username,
|
||||
displayName,
|
||||
loginUsername,
|
||||
username: displayName || loginUsername,
|
||||
doctorId,
|
||||
doctorCode,
|
||||
mouId,
|
||||
@@ -1994,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) {
|
||||
@@ -2200,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);
|
||||
@@ -2246,7 +2283,9 @@ async function renderRoute(req, res, url) {
|
||||
}
|
||||
const sessionValue = JSON.stringify({
|
||||
token: normalized.token,
|
||||
username: normalized.username || body.username || "",
|
||||
displayName: normalized.displayName || normalized.username || body.username || "",
|
||||
loginUsername: normalized.loginUsername || body.username || "",
|
||||
username: normalized.displayName || normalized.username || body.username || "",
|
||||
doctorId: normalized.doctorId || body.doctor_id || "",
|
||||
doctorCode: normalized.doctorCode || body.doctor_id || "",
|
||||
mouId: normalized.mouId || body.M_MouID || "",
|
||||
@@ -2270,7 +2309,7 @@ async function renderRoute(req, res, url) {
|
||||
"/auth/logout",
|
||||
{
|
||||
M_UserID: sessionData.userId || "",
|
||||
M_UserUsername: sessionData.username || sampleLogin.username,
|
||||
M_UserUsername: sessionData.displayName || sessionData.username || sampleLogin.username,
|
||||
},
|
||||
sessionData.token,
|
||||
);
|
||||
@@ -2287,26 +2326,29 @@ async function renderRoute(req, res, url) {
|
||||
if (!sessionData) return;
|
||||
const body = await readBody(req);
|
||||
try {
|
||||
await apiPost(
|
||||
const payload = await apiPost(
|
||||
"/auth/change_password",
|
||||
{
|
||||
token: sessionData.token,
|
||||
M_UserID: sessionData.userId || "",
|
||||
username: sessionData.username || sampleLogin.username,
|
||||
username: body.login_username || sessionData.loginUsername || "",
|
||||
doctor_id: sessionData.doctorCode || sessionData.doctorId || sampleLogin.doctorId,
|
||||
new_password: body.new_password,
|
||||
confirm_password: body.confirm_password,
|
||||
},
|
||||
sessionData.token,
|
||||
);
|
||||
redirect(res, "/settings");
|
||||
if (payload?.status && payload.status !== "OK") {
|
||||
throw new Error(payload?.message || "Change password failed");
|
||||
}
|
||||
redirect(res, "/login", { "Set-Cookie": deleteCookie(sessionKey) });
|
||||
} catch (error) {
|
||||
html(
|
||||
res,
|
||||
200,
|
||||
layout(
|
||||
"Change Password",
|
||||
`<section class="panel">${panelHeader("Change password", "Inline validation and a straightforward submit path.")}<div class="note-box">Upstream change password call failed. Check API availability.</div></section>`,
|
||||
renderChangePassword(sessionData, error?.message || "Upstream change password call failed. Check API availability."),
|
||||
{ authenticated: true, activePath: "/settings/change-password", ...accountLayoutOptions(sessionData) },
|
||||
),
|
||||
);
|
||||
@@ -2563,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) => {
|
||||
|
||||
Reference in New Issue
Block a user