Public Access
Compare commits
3
Commits
5afccaffa8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28b52145eb | ||
|
|
511929d579 | ||
|
|
fc2898083c |
@@ -179,10 +179,16 @@ the widest roll it will accept — the console warns you if you enter anything w
|
|||||||
| DK-11208 | 38 × 90 mm die-cut | Narrower; turn the photo off |
|
| DK-11208 | 38 × 90 mm die-cut | Narrower; turn the photo off |
|
||||||
| DK-11209 | 29 × 62 mm die-cut | Name and host only |
|
| DK-11209 | 29 × 62 mm die-cut | Name and host only |
|
||||||
|
|
||||||
|
**Tell it which roll is loaded.** *Roll loaded in the printer* under **Sites → Edit → Printer**
|
||||||
|
must match what is physically in the machine. A two-colour job sent to a plain roll is refused
|
||||||
|
outright: the printer shows **Wrong Roll Type** and nothing comes out. The setting is separate
|
||||||
|
from the red styling option on purpose — a design choice should not silently change the media
|
||||||
|
type the printer is told to expect.
|
||||||
|
|
||||||
**The red option.** Tick *Print the heading and the no-check warning in red* and the site name
|
**The red option.** Tick *Print the heading and the no-check warning in red* and the site name
|
||||||
and the **No WWCC / VIT** box print red instead of black, which makes a visitor without a check
|
and the **No WWCC / VIT** box print red instead of black, which makes a visitor without a check
|
||||||
obvious across a room. It only works on a DK-22251 roll — on any other roll the printer renders
|
obvious across a room. It needs the roll setting above to be the DK-22251; with a plain roll
|
||||||
it as grey. Two-colour printing is also far slower than black alone (Brother rate it at roughly
|
selected the badge is drawn in black instead and the console says so. Two-colour printing is also far slower than black alone (Brother rate it at roughly
|
||||||
15 labels a minute against 110), which is irrelevant for one badge at a time but worth knowing.
|
15 labels a minute against 110), which is irrelevant for one badge at a time but worth knowing.
|
||||||
|
|
||||||
### Printing from the server
|
### Printing from the server
|
||||||
@@ -271,9 +277,25 @@ HOST_PORT=8443
|
|||||||
HTTPS_PUBLIC_PORT=8443
|
HTTPS_PUBLIC_PORT=8443
|
||||||
```
|
```
|
||||||
|
|
||||||
An address that isn't listed produces a browser warning. Change the list and restart; the
|
An address that isn't listed produces a browser warning.
|
||||||
certificate reissues itself automatically, and devices that already trust the authority accept
|
|
||||||
it without any further work.
|
After changing anything in `.env`, bring the container back with **`docker compose up -d`**, not
|
||||||
|
`docker compose restart`. Restart reuses the running container along with the environment it
|
||||||
|
started with, so the edit appears to do nothing; `up -d` recreates it and picks the new values
|
||||||
|
up. Confirm with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec visitor-signin printenv HTTPS_HOSTNAMES
|
||||||
|
docker compose logs --tail=20 visitor-signin | grep tls
|
||||||
|
```
|
||||||
|
|
||||||
|
The log should say `renewing the server certificate: HTTPS_HOSTNAMES changed` and then list every
|
||||||
|
name it now covers. If it lists only `localhost`, `visitors.local` and a `172.x` address, the
|
||||||
|
variable never reached the container — those are the defaults plus the container's own docker
|
||||||
|
bridge address.
|
||||||
|
|
||||||
|
Reissuing does **not** touch the certificate authority, so devices that already trust it keep
|
||||||
|
working and no MDM profile needs redeploying.
|
||||||
|
|
||||||
The kiosk is then at `https://visitors.local:8443`, admin at `https://visitors.local:8443/admin`.
|
The kiosk is then at `https://visitors.local:8443`, admin at `https://visitors.local:8443/admin`.
|
||||||
|
|
||||||
|
|||||||
+44
-6
@@ -702,9 +702,9 @@ async function loadSites() {
|
|||||||
<dt>Printer</dt>
|
<dt>Printer</dt>
|
||||||
<dd>${
|
<dd>${
|
||||||
s.printer.enabled && s.printer.host
|
s.printer.enabled && s.printer.host
|
||||||
? `${esc(s.printer.model)} at ${esc(s.printer.host)}:${s.printer.port}${
|
? `${esc(s.printer.model)} at ${esc(s.printer.host)}:${s.printer.port}, ${
|
||||||
s.printer.rotate ? `, rotated ${s.printer.rotate}°` : ''
|
s.printer.label === '62red' ? 'black and red roll' : 'black roll'
|
||||||
}${
|
}${s.printer.rotate ? `, rotated ${s.printer.rotate}°` : ''}${
|
||||||
s.printerStatus
|
s.printerStatus
|
||||||
? s.printerStatus.ok
|
? s.printerStatus.ok
|
||||||
? ` <span class="pill">last print ok, ${stamp(s.printerStatus.at)}</span>`
|
? ` <span class="pill">last print ok, ${stamp(s.printerStatus.at)}</span>`
|
||||||
@@ -799,14 +799,21 @@ function openSiteModal(site) {
|
|||||||
<p class="hint" id="badge-warning" hidden></p>
|
<p class="hint" id="badge-warning" hidden></p>
|
||||||
<label class="inline"><input type="checkbox" name="showPhoto" id="badge-photo" ${site.badge.showPhoto ? 'checked' : ''}> Include the visitor's photo</label>
|
<label class="inline"><input type="checkbox" name="showPhoto" id="badge-photo" ${site.badge.showPhoto ? 'checked' : ''}> Include the visitor's photo</label>
|
||||||
<label class="inline"><input type="checkbox" name="accent" id="badge-accent" ${site.badge.accent ? 'checked' : ''}> Print the heading and the no-check warning in red</label>
|
<label class="inline"><input type="checkbox" name="accent" id="badge-accent" ${site.badge.accent ? 'checked' : ''}> Print the heading and the no-check warning in red</label>
|
||||||
<p class="hint">Red needs a two-colour roll such as the Brother DK-22251. On any other
|
<p class="hint" id="accent-note"></p>
|
||||||
roll it prints as grey. Two-colour printing is also much slower than black alone.</p>
|
<p class="hint">Two-colour printing is much slower than black alone.</p>
|
||||||
<h4 class="modal-section">Printer</h4>
|
<h4 class="modal-section">Printer</h4>
|
||||||
<label class="inline"><input type="checkbox" name="printerEnabled" ${site.printer.enabled ? 'checked' : ''}> Print from the server, straight to a network printer</label>
|
<label class="inline"><input type="checkbox" name="printerEnabled" ${site.printer.enabled ? 'checked' : ''}> Print from the server, straight to a network printer</label>
|
||||||
<div class="modal-row">
|
<div class="modal-row">
|
||||||
${field('Printer IP address', 'printerHost', site.printer.host, 'text')}
|
${field('Printer IP address', 'printerHost', site.printer.host, 'text')}
|
||||||
${field('Port', 'printerPort', site.printer.port, 'number')}
|
${field('Port', 'printerPort', site.printer.port, 'number')}
|
||||||
</div>
|
</div>
|
||||||
|
<label class="modal-field"><span>Roll loaded in the printer</span>
|
||||||
|
<select name="printerLabel">
|
||||||
|
<option value="62" ${site.printer.label !== '62red' ? 'selected' : ''}>62 mm continuous, black only</option>
|
||||||
|
<option value="62red" ${site.printer.label === '62red' ? 'selected' : ''}>62 mm continuous, black and red (DK-22251)</option>
|
||||||
|
</select></label>
|
||||||
|
<p class="hint">This must match the roll actually in the machine. Send a two-colour job to a
|
||||||
|
plain roll and the printer answers <em>Wrong Roll Type</em> and prints nothing.</p>
|
||||||
<div class="modal-row">
|
<div class="modal-row">
|
||||||
${field('Model', 'printerModel', site.printer.model)}
|
${field('Model', 'printerModel', site.printer.model)}
|
||||||
<label class="modal-field"><span>Rotation</span>
|
<label class="modal-field"><span>Rotation</span>
|
||||||
@@ -883,6 +890,7 @@ function openSiteModal(site) {
|
|||||||
port: Number(data.printerPort) || 9100,
|
port: Number(data.printerPort) || 9100,
|
||||||
model: data.printerModel,
|
model: data.printerModel,
|
||||||
rotate: Number(data.printerRotate) || 0,
|
rotate: Number(data.printerRotate) || 0,
|
||||||
|
label: data.printerLabel,
|
||||||
},
|
},
|
||||||
branding: {
|
branding: {
|
||||||
brand: data.brand || null,
|
brand: data.brand || null,
|
||||||
@@ -992,6 +1000,35 @@ function wireBannerEditor() {
|
|||||||
[pageInput, textInput].forEach((el) => el.addEventListener('input', showContrast));
|
[pageInput, textInput].forEach((el) => el.addEventListener('input', showContrast));
|
||||||
showContrast();
|
showContrast();
|
||||||
|
|
||||||
|
// Red is only possible on the two-colour roll, so say so as the two settings change.
|
||||||
|
const accentBox = $('#badge-accent');
|
||||||
|
const rollSelect = $('#modal-form [name="printerLabel"]');
|
||||||
|
const accentNote = $('#accent-note');
|
||||||
|
|
||||||
|
const showAccentNote = () => {
|
||||||
|
if (!accentNote) return;
|
||||||
|
if (!accentBox?.checked) {
|
||||||
|
accentNote.hidden = false;
|
||||||
|
accentNote.className = 'hint';
|
||||||
|
accentNote.textContent = 'Everything prints black.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rollSelect?.value === '62red') {
|
||||||
|
accentNote.hidden = false;
|
||||||
|
accentNote.className = 'hint';
|
||||||
|
accentNote.textContent = 'The heading and the no-check warning will print red.';
|
||||||
|
} else {
|
||||||
|
accentNote.hidden = false;
|
||||||
|
accentNote.className = 'hint warn';
|
||||||
|
accentNote.textContent =
|
||||||
|
'The roll selected below cannot print red, so these will come out black. Load a DK-22251 and change the roll setting to use colour.';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
accentBox?.addEventListener('change', showAccentNote);
|
||||||
|
rollSelect?.addEventListener('change', showAccentNote);
|
||||||
|
showAccentNote();
|
||||||
|
|
||||||
$('#banner-file').addEventListener('change', (event) => {
|
$('#banner-file').addEventListener('change', (event) => {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
@@ -1354,7 +1391,8 @@ function renderTls(tls) {
|
|||||||
itself before it lapses, and devices that trust the authority keep working without being
|
itself before it lapses, and devices that trust the authority keep working without being
|
||||||
touched again.</p>
|
touched again.</p>
|
||||||
<div class="sys-actions">
|
<div class="sys-actions">
|
||||||
<a class="ghost" href="/admin/api/tls/ca.crt" download>Download the CA certificate</a>
|
<a class="ghost" href="/admin/api/tls/ca.crt" download>CA certificate (.crt)</a>
|
||||||
|
<a class="ghost" href="/admin/api/tls/ca.cer" download>CA certificate (.cer, for Jamf and Apple)</a>
|
||||||
<button class="ghost owner-only" id="renew-cert">Renew the server certificate</button>
|
<button class="ghost owner-only" id="renew-cert">Renew the server certificate</button>
|
||||||
<button class="ghost danger owner-only" id="new-ca">Start a new authority</button>
|
<button class="ghost danger owner-only" id="new-ca">Start a new authority</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|||||||
@@ -41,6 +41,30 @@ if ((git remote) -match '^origin$') {
|
|||||||
git remote add origin $Remote
|
git remote add origin $Remote
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------- finish what was started
|
||||||
|
|
||||||
|
# A rebase or merge left half-done blocks everything that follows, and the error
|
||||||
|
# git gives is easy to mistake for a push problem. Catch it here and say plainly
|
||||||
|
# what to do.
|
||||||
|
$gitDir = (git rev-parse --git-dir 2>$null)
|
||||||
|
if ($gitDir) {
|
||||||
|
$stuck = @('rebase-merge', 'rebase-apply', 'MERGE_HEAD', 'CHERRY_PICK_HEAD') |
|
||||||
|
Where-Object { Test-Path (Join-Path $gitDir $_) }
|
||||||
|
if ($stuck) {
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host 'There is an unfinished rebase or merge in this folder.' -ForegroundColor Red
|
||||||
|
Write-Host 'Nothing else can happen until it is settled. Your options:' -ForegroundColor Yellow
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host ' git rebase --abort throw the attempt away and go back to how things were'
|
||||||
|
Write-Host ' git status see which files still need attention'
|
||||||
|
Write-Host ' git rebase --continue after fixing the files git listed'
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host 'If you are unsure, "git rebase --abort" is the safe one. It puts the' -ForegroundColor Yellow
|
||||||
|
Write-Host 'folder back exactly as it was before the rebase started.' -ForegroundColor Yellow
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------- commit
|
# ---------------------------------------------------------------- commit
|
||||||
|
|
||||||
git add -A
|
git add -A
|
||||||
|
|||||||
@@ -19,6 +19,26 @@ else
|
|||||||
git remote add origin "$REMOTE"
|
git remote add origin "$REMOTE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# An unfinished rebase or merge blocks everything below, and git's own error is
|
||||||
|
# easy to mistake for a push problem.
|
||||||
|
GIT_DIR_PATH=$(git rev-parse --git-dir 2>/dev/null || echo .git)
|
||||||
|
for marker in rebase-merge rebase-apply MERGE_HEAD CHERRY_PICK_HEAD; do
|
||||||
|
if [ -e "$GIT_DIR_PATH/$marker" ]; then
|
||||||
|
cat >&2 <<'MSG'
|
||||||
|
|
||||||
|
There is an unfinished rebase or merge in this folder.
|
||||||
|
Nothing else can happen until it is settled:
|
||||||
|
|
||||||
|
git rebase --abort throw the attempt away, back to how things were
|
||||||
|
git status see which files still need attention
|
||||||
|
git rebase --continue after fixing the files git listed
|
||||||
|
|
||||||
|
If unsure, "git rebase --abort" is the safe one.
|
||||||
|
MSG
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
git add -A
|
git add -A
|
||||||
if [ -n "$(git status --porcelain)" ]; then
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
read -r -p "Describe this change (enter for a dated default): " MSG
|
read -r -p "Describe this change (enter for a dated default): " MSG
|
||||||
|
|||||||
+4
-2
@@ -25,8 +25,10 @@ fi
|
|||||||
|
|
||||||
if docker compose ps --status running 2>/dev/null | grep -q visitor-signin; then
|
if docker compose ps --status running 2>/dev/null | grep -q visitor-signin; then
|
||||||
docker compose exec -T visitor-signin node scripts/make-cert.mjs $FORCE
|
docker compose exec -T visitor-signin node scripts/make-cert.mjs $FORCE
|
||||||
echo "Restarting so the new certificate is served..."
|
echo "Recreating the container so the new certificate is served..."
|
||||||
docker compose restart visitor-signin
|
# up -d rather than restart: restart keeps the environment the container was
|
||||||
|
# started with, so an edited .env would be ignored.
|
||||||
|
docker compose up -d visitor-signin
|
||||||
else
|
else
|
||||||
node scripts/make-cert.mjs $FORCE
|
node scripts/make-cert.mjs $FORCE
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ CREATE TABLE IF NOT EXISTS sites (
|
|||||||
printer_port INTEGER NOT NULL DEFAULT 9100,
|
printer_port INTEGER NOT NULL DEFAULT 9100,
|
||||||
printer_model TEXT NOT NULL DEFAULT 'QL-820NWB',
|
printer_model TEXT NOT NULL DEFAULT 'QL-820NWB',
|
||||||
printer_rotate INTEGER NOT NULL DEFAULT 0,
|
printer_rotate INTEGER NOT NULL DEFAULT 0,
|
||||||
|
printer_label TEXT NOT NULL DEFAULT '62',
|
||||||
banner_path TEXT,
|
banner_path TEXT,
|
||||||
banner_height INTEGER NOT NULL DEFAULT 64,
|
banner_height INTEGER NOT NULL DEFAULT 64,
|
||||||
banner_align TEXT NOT NULL DEFAULT 'left',
|
banner_align TEXT NOT NULL DEFAULT 'left',
|
||||||
@@ -183,6 +184,10 @@ addColumn('sites', 'printer_host', 'TEXT');
|
|||||||
addColumn('sites', 'printer_port', 'INTEGER NOT NULL DEFAULT 9100');
|
addColumn('sites', 'printer_port', 'INTEGER NOT NULL DEFAULT 9100');
|
||||||
addColumn('sites', 'printer_model', "TEXT NOT NULL DEFAULT 'QL-820NWB'");
|
addColumn('sites', 'printer_model', "TEXT NOT NULL DEFAULT 'QL-820NWB'");
|
||||||
addColumn('sites', 'printer_rotate', 'INTEGER NOT NULL DEFAULT 0');
|
addColumn('sites', 'printer_rotate', 'INTEGER NOT NULL DEFAULT 0');
|
||||||
|
// Which roll is physically loaded. Kept separate from the red styling option:
|
||||||
|
// the printer refuses a two-colour job on a plain roll, so guessing the media
|
||||||
|
// from a design setting means a wrong-roll error nobody can explain.
|
||||||
|
addColumn('sites', 'printer_label', "TEXT NOT NULL DEFAULT '62'");
|
||||||
addColumn('frequent_visitors', 'company', 'TEXT');
|
addColumn('frequent_visitors', 'company', 'TEXT');
|
||||||
|
|
||||||
db.exec('CREATE INDEX IF NOT EXISTS idx_visits_site ON visits(site_id, signed_out_at)');
|
db.exec('CREATE INDEX IF NOT EXISTS idx_visits_site ON visits(site_id, signed_out_at)');
|
||||||
|
|||||||
+24
-3
@@ -41,9 +41,26 @@ export function isConfigured(site) {
|
|||||||
return Boolean(site?.printer_enabled && site?.printer_host);
|
return Boolean(site?.printer_enabled && site?.printer_host);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** brother_ql's label id. The two-colour roll is a different label to the plain one. */
|
/**
|
||||||
|
* brother_ql's media id for the roll that is actually loaded.
|
||||||
|
*
|
||||||
|
* This is set explicitly rather than inferred from the red styling option. A
|
||||||
|
* two-colour job sent to a plain roll is rejected by the printer with "Wrong
|
||||||
|
* Roll Type", which gives no clue that a colour checkbox caused it.
|
||||||
|
*/
|
||||||
|
export const ROLL_TYPES = {
|
||||||
|
'62': '62 mm continuous, black only',
|
||||||
|
'62red': '62 mm continuous, black and red (DK-22251)',
|
||||||
|
};
|
||||||
|
|
||||||
export function labelFor(site) {
|
export function labelFor(site) {
|
||||||
return site?.badge_accent ? '62red' : '62';
|
const label = site?.printer_label;
|
||||||
|
return Object.hasOwn(ROLL_TYPES, label) ? label : '62';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Red ink only exists on the two-colour roll; anywhere else it prints dark grey. */
|
||||||
|
export function accentWillPrintRed(site) {
|
||||||
|
return Boolean(site?.badge_accent) && labelFor(site) === '62red';
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------ rendering */
|
/* ------------------------------------------------------------ rendering */
|
||||||
@@ -223,7 +240,8 @@ export async function renderBadgePng(visit, site) {
|
|||||||
|
|
||||||
const design = createCanvas(designW, designH);
|
const design = createCanvas(designW, designH);
|
||||||
const ctx = design.getContext('2d');
|
const ctx = design.getContext('2d');
|
||||||
const accent = Boolean(site.badge_accent);
|
// Only paint red when the loaded roll can actually print it.
|
||||||
|
const accent = accentWillPrintRed(site);
|
||||||
|
|
||||||
// Measure first, then draw the block centred down the label. Without this the
|
// Measure first, then draw the block centred down the label. Without this the
|
||||||
// content hugs the top and leaves a wide blank strip at the bottom of every badge.
|
// content hugs the top and leaves a wide blank strip at the bottom of every badge.
|
||||||
@@ -273,6 +291,9 @@ function explainPrintError(output, host) {
|
|||||||
if (/Unknown label|label/i.test(last) && /identifier/i.test(last)) {
|
if (/Unknown label|label/i.test(last) && /identifier/i.test(last)) {
|
||||||
return 'The printer rejected the label size. Check the roll loaded matches the badge settings.';
|
return 'The printer rejected the label size. Check the roll loaded matches the badge settings.';
|
||||||
}
|
}
|
||||||
|
if (/wrong roll|WrongMedia|media/i.test(last)) {
|
||||||
|
return 'The printer says the roll is wrong. Check "Roll loaded in the printer" matches what is actually in the machine — a black and red job is refused on a plain roll.';
|
||||||
|
}
|
||||||
return last || 'The printer did not accept the job.';
|
return last || 'The printer did not accept the job.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+23
-2
@@ -397,7 +397,7 @@ router.patch('/sites/:id', (req, res) => {
|
|||||||
badge_height_mm = ?, badge_show_photo = ?, badge_accent = ?, badge_note = ?,
|
badge_height_mm = ?, badge_show_photo = ?, badge_accent = ?, badge_note = ?,
|
||||||
colour_brand = ?, colour_signout = ?, colour_page = ?, colour_text = ?,
|
colour_brand = ?, colour_signout = ?, colour_page = ?, colour_text = ?,
|
||||||
banner_height = ?, banner_align = ?, printer_enabled = ?, printer_host = ?,
|
banner_height = ?, banner_align = ?, printer_enabled = ?, printer_host = ?,
|
||||||
printer_port = ?, printer_model = ?, printer_rotate = ? WHERE id = ?`
|
printer_port = ?, printer_model = ?, printer_rotate = ?, printer_label = ? WHERE id = ?`
|
||||||
).run(
|
).run(
|
||||||
clean(req.body?.name ?? site.name, 100) || site.name,
|
clean(req.body?.name ?? site.name, 100) || site.name,
|
||||||
req.body?.slug ? uniqueSlug(req.body.slug, site.id) : site.slug,
|
req.body?.slug ? uniqueSlug(req.body.slug, site.id) : site.slug,
|
||||||
@@ -432,7 +432,10 @@ router.patch('/sites/:id', (req, res) => {
|
|||||||
printerCfg.model !== undefined ? clean(printerCfg.model, 40) || 'QL-820NWB' : site.printer_model,
|
printerCfg.model !== undefined ? clean(printerCfg.model, 40) || 'QL-820NWB' : site.printer_model,
|
||||||
printerCfg.rotate !== undefined
|
printerCfg.rotate !== undefined
|
||||||
? ([0, 90, 180, 270].includes(Number(printerCfg.rotate)) ? Number(printerCfg.rotate) : 0)
|
? ([0, 90, 180, 270].includes(Number(printerCfg.rotate)) ? Number(printerCfg.rotate) : 0)
|
||||||
: site.printer_rotate
|
: site.printer_rotate,
|
||||||
|
printerCfg.label !== undefined
|
||||||
|
? (Object.hasOwn(printer.ROLL_TYPES, printerCfg.label) ? printerCfg.label : '62')
|
||||||
|
: site.printer_label
|
||||||
, site.id);
|
, site.id);
|
||||||
|
|
||||||
res.json(shapeSite(db.prepare('SELECT * FROM sites WHERE id = ?').get(site.id)));
|
res.json(shapeSite(db.prepare('SELECT * FROM sites WHERE id = ?').get(site.id)));
|
||||||
@@ -1224,6 +1227,24 @@ router.get('/tls/ca.crt', (req, res) => {
|
|||||||
res.send(ca);
|
res.send(ca);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The same authority in DER form, for Jamf Pro and anything else built on Apple's
|
||||||
|
* tooling. Offered as .cer and .der because different consoles insist on
|
||||||
|
* different extensions for the identical bytes.
|
||||||
|
*/
|
||||||
|
router.get(['/tls/ca.cer', '/tls/ca.der'], (req, res) => {
|
||||||
|
try {
|
||||||
|
const der = tls.caCertificateDer();
|
||||||
|
if (!der) return res.status(404).send('No certificate authority has been generated yet.');
|
||||||
|
const ext = req.path.endsWith('.der') ? 'der' : 'cer';
|
||||||
|
res.setHeader('Content-Type', 'application/pkix-cert');
|
||||||
|
res.setHeader('Content-Disposition', `attachment; filename="visitor-signin-ca.${ext}"`);
|
||||||
|
res.send(der);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).send(`Could not convert the certificate: ${err.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.post('/tls/renew', requireOwner, (req, res) => {
|
router.post('/tls/renew', requireOwner, (req, res) => {
|
||||||
try {
|
try {
|
||||||
// A brand new CA means every kiosk device has to trust it again, so it is
|
// A brand new CA means every kiosk device has to trust it again, so it is
|
||||||
|
|||||||
@@ -121,6 +121,25 @@ function startRedirectServer() {
|
|||||||
|
|
||||||
http
|
http
|
||||||
.createServer((req, res) => {
|
.createServer((req, res) => {
|
||||||
|
// DER for Apple tooling, PEM for everything else.
|
||||||
|
if (req.url === '/ca.cer' || req.url === '/ca.der') {
|
||||||
|
try {
|
||||||
|
const der = tls.caCertificateDer();
|
||||||
|
if (!der) {
|
||||||
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||||
|
return res.end('No certificate authority has been generated yet.');
|
||||||
|
}
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'application/pkix-cert',
|
||||||
|
'Content-Disposition': 'attachment; filename="visitor-signin-ca.cer"',
|
||||||
|
});
|
||||||
|
return res.end(der);
|
||||||
|
} catch (err) {
|
||||||
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
||||||
|
return res.end(`Could not convert the certificate: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (req.url === '/ca.crt' || req.url === '/ca.pem') {
|
if (req.url === '/ca.crt' || req.url === '/ca.pem') {
|
||||||
const ca = tls.caCertificate();
|
const ca = tls.caCertificate();
|
||||||
if (!ca) {
|
if (!ca) {
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export function shapeSite(site) {
|
|||||||
port: site.printer_port || 9100,
|
port: site.printer_port || 9100,
|
||||||
model: site.printer_model || 'QL-820NWB',
|
model: site.printer_model || 'QL-820NWB',
|
||||||
rotate: site.printer_rotate || 0,
|
rotate: site.printer_rotate || 0,
|
||||||
|
label: site.printer_label || '62',
|
||||||
},
|
},
|
||||||
badge: {
|
badge: {
|
||||||
enabled: Boolean(site.badge_enabled),
|
enabled: Boolean(site.badge_enabled),
|
||||||
|
|||||||
+25
@@ -194,6 +194,18 @@ export function ensureCertificates({ force = false } = {}) {
|
|||||||
fs.writeFileSync(p.names, JSON.stringify(config.https.hostnames));
|
fs.writeFileSync(p.names, JSON.stringify(config.https.hostnames));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A very common mistake is editing .env and then using `docker compose restart`,
|
||||||
|
// which reuses the old environment. The symptom is a certificate covering only
|
||||||
|
// the defaults, so say so rather than letting it fail later in a browser.
|
||||||
|
const configured = config.https.hostnames;
|
||||||
|
if (configured.length === 1 && configured[0] === 'visitors.local') {
|
||||||
|
console.warn(
|
||||||
|
'[tls] HTTPS_HOSTNAMES is at its default. If you set it in .env, bring the container\n' +
|
||||||
|
' back with "docker compose up -d" rather than "docker compose restart" — restart\n' +
|
||||||
|
' keeps the environment the container started with.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: fs.readFileSync(p.key),
|
key: fs.readFileSync(p.key),
|
||||||
cert: fs.readFileSync(p.cert),
|
cert: fs.readFileSync(p.cert),
|
||||||
@@ -225,6 +237,19 @@ export function describe() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The CA in DER form.
|
||||||
|
*
|
||||||
|
* The .crt on disk is PEM: base64 text between BEGIN/END lines. Apple's tooling,
|
||||||
|
* and therefore Jamf Pro's certificate payload, wants the raw binary DER instead
|
||||||
|
* and rejects the file on its extension. Same certificate, different wrapper.
|
||||||
|
*/
|
||||||
|
export function caCertificateDer() {
|
||||||
|
const p = paths();
|
||||||
|
if (!fs.existsSync(p.caCert)) return null;
|
||||||
|
return openssl(['x509', '-in', p.caCert, '-outform', 'der']);
|
||||||
|
}
|
||||||
|
|
||||||
export function caCertificate() {
|
export function caCertificate() {
|
||||||
const p = paths();
|
const p = paths();
|
||||||
return fs.existsSync(p.caCert) ? fs.readFileSync(p.caCert) : null;
|
return fs.existsSync(p.caCert) ? fs.readFileSync(p.caCert) : null;
|
||||||
|
|||||||
Reference in New Issue
Block a user