Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: CI

on:
push:
branches:
- main
- fix/backend
pull_request:
branches:
- main

permissions:
contents: read

jobs:
test:
name: Backend tests
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Install dependencies
run: npm install

- name: Syntax check
run: npm run check

- name: Run tests
run: npm test
36 changes: 36 additions & 0 deletions BACKEND.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,39 @@ Open <http://localhost:3000/admin.html> in the browser. The server also serves t
Queue and notification data is stored in `data/store.json`. The admin page uses the API when served by Node and falls back to its demo behavior when opened directly as a file.

The current delivery channel is browser notifications through the live event stream. Email, WhatsApp, SMS, or push-provider delivery requires provider credentials and can be added inside `createNotification` in `server.js`.


## Environment configuration

For production, configure:

```text
PORT=3000
JWT_SECRET=<long-random-secret>
ADMIN_USERNAME=<admin-username>
ADMIN_PASSWORD=<strong-admin-password>
```

Admin-protected endpoints require a Bearer JWT obtained from `POST /api/auth/login`:

- `PATCH /api/tokens/:id/status`
- `POST /api/notifications`

Public endpoints include health, readiness, queue reads, token creation, and SSE subscription.

## Testing and CI

Run locally:

```sh
npm run check
npm test
```

GitHub Actions runs the same checks on pushes to `main` and `fix/backend`, and on pull requests targeting `main`.

## Persistence roadmap

The prototype currently uses `data/store.json`. Writes are performed through a temporary file and atomic rename to reduce partial-write corruption.

For production migration, use PostgreSQL with transactions and database constraints. The target schema is in `docs/postgresql-schema.sql`. A database sequence/transaction should replace application-side token-number generation to prevent concurrent requests from producing the same number.
7 changes: 6 additions & 1 deletion assets/js/admin/queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ export async function updateStatus(id, newStatus) {
if (state.backendAvailable) {
const response = await fetch(`/api/tokens/${encodeURIComponent(id)}/status`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
...(sessionStorage.getItem('digitoken_admin_token')
? { Authorization: `Bearer ${sessionStorage.getItem('digitoken_admin_token')}` }
: {})
},
body: JSON.stringify({ status: newStatus, channel: 'browser' })
});
if (!response.ok) return alert('Unable to update this token.');
Expand Down
28 changes: 24 additions & 4 deletions auth.html
Original file line number Diff line number Diff line change
Expand Up @@ -186,13 +186,33 @@ <h3>Verify OTP</h3>
googleBtn.style.display = role === 'admin' ? 'none' : 'block';
}

function handleLogin(e) {
async function handleLogin(e) {
e.preventDefault();
const role = document.querySelector('input[name="role"]:checked').value;
if (role === 'admin') {
window.location.href = 'admin.html';
} else {
if (role !== 'admin') {
window.location.href = 'dashboard.html';
return;
}

const inputs = document.querySelectorAll('#view-login input');
const username = inputs[1]?.value?.trim();
const password = inputs[2]?.value || '';

try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (!response.ok) {
alert(data.error || 'Admin login failed.');
return;
}
sessionStorage.setItem('digitoken_admin_token', data.token);
window.location.href = 'admin.html';
} catch {
alert('Unable to reach the DigiToken API.');
}
}

Expand Down
22 changes: 22 additions & 0 deletions docs/postgresql-schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
CREATE TABLE tokens (
id BIGSERIAL PRIMARY KEY,
public_id VARCHAR(20) NOT NULL UNIQUE,
type VARCHAR(10) NOT NULL CHECK (type IN ('walkin', 'online')),
service VARCHAR(100) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'waiting'
CHECK (status IN ('waiting', 'serving', 'completed', 'no-show')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_tokens_status_created_at ON tokens(status, created_at);

CREATE TABLE notifications (
id UUID PRIMARY KEY,
token_id BIGINT REFERENCES tokens(id) ON DELETE SET NULL,
channel VARCHAR(20) NOT NULL DEFAULT 'browser',
message VARCHAR(500) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'delivered',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_notifications_created_at ON notifications(created_at DESC);
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"description": "DigiToken queue maintenance app",
"scripts": {
"start": "node server.js",
"check": "node --check server.js"
"check": "node --check server.js",
"test": "node --test"
}
}
Loading
Loading