Compare commits
2 Commits
3089cc7252
...
0e64233418
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e64233418 | ||
|
|
cf4af1480b |
@@ -3,6 +3,7 @@
|
|||||||
```
|
```
|
||||||
http://localhost:4208/api
|
http://localhost:4208/api
|
||||||
https://localhost:8443/api
|
https://localhost:8443/api
|
||||||
|
https://localhost:8443/v1 (POS API)
|
||||||
```
|
```
|
||||||
|
|
||||||
## CORS & cookies
|
## CORS & cookies
|
||||||
@@ -154,6 +155,7 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
|
|||||||
| [`routes/unit/delete.md`](routes/unit/delete.md) | `DELETE /unit` |
|
| [`routes/unit/delete.md`](routes/unit/delete.md) | `DELETE /unit` |
|
||||||
| [`routes/unit/list.md`](routes/unit/list.md) | `GET /unit` |
|
| [`routes/unit/list.md`](routes/unit/list.md) | `GET /unit` |
|
||||||
| [`routes/unit/upsert.md`](routes/unit/upsert.md) | `PUT /unit` |
|
| [`routes/unit/upsert.md`](routes/unit/upsert.md) | `PUT /unit` |
|
||||||
|
| [`routes/pos/client.md`](routes/pos/client.md) | `GET /v1/client` |
|
||||||
| [`routes/warehouse/list.md`](routes/warehouse/list.md) | `GET /warehouse` |
|
| [`routes/warehouse/list.md`](routes/warehouse/list.md) | `GET /warehouse` |
|
||||||
| [`routes/warehouse/create.md`](routes/warehouse/create.md) | `POST /warehouse` |
|
| [`routes/warehouse/create.md`](routes/warehouse/create.md) | `POST /warehouse` |
|
||||||
| [`routes/warehouse/order.md`](routes/warehouse/order.md) | `PUT /warehouse/order` |
|
| [`routes/warehouse/order.md`](routes/warehouse/order.md) | `PUT /warehouse/order` |
|
||||||
|
|||||||
@@ -8,7 +8,11 @@
|
|||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
"url": "http://localhost:4208/api",
|
"url": "http://localhost:4208/api",
|
||||||
"description": "Local default (PORT / .env)"
|
"description": "Local default HTTP (PORT / .env)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://localhost:8443",
|
||||||
|
"description": "Local default HTTPS (HTTPS_PORT / .env)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -20,6 +24,10 @@
|
|||||||
"name": "Health",
|
"name": "Health",
|
||||||
"description": "Connectivity checks"
|
"description": "Connectivity checks"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "POS",
|
||||||
|
"description": "POS bind and sync API (/v1)"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Events",
|
"name": "Events",
|
||||||
"description": "SSE update signals for live clients"
|
"description": "SSE update signals for live clients"
|
||||||
@@ -11932,6 +11940,114 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/v1/client": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"POS"
|
||||||
|
],
|
||||||
|
"summary": "POS discovery and pairing",
|
||||||
|
"description": "Special API for POS bind and sync.\n- Step 1 (Discovery): `authCode` length 1–4. Returns certificate fingerprints, serial number, and mandantId.\n- Step 2 (Pairing): `authCode` length 6 (pairingpin from posterminal). Verifies the pairingpin, sets token, paired_at, and clientserial, revokes the pairingpin, and returns device credentials and company metadata.\n",
|
||||||
|
"security": [],
|
||||||
|
"x-markdown": "pos/client.md",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "authCode",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Discovery code (1-4 digits) or pairing PIN (6 digits)",
|
||||||
|
"example": "678677"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "name",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Client device name or serial identifier (default 'JTL-POS')",
|
||||||
|
"example": "Kasse 1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Discovery or pairing successful",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"authCode": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"example": "678677"
|
||||||
|
},
|
||||||
|
"authToken": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"example": "df40ad2067954646abb0499548a52241"
|
||||||
|
},
|
||||||
|
"certificateFingerprint": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "30DD111E8048B57E6A4FB75AD7E63F7E657F1262"
|
||||||
|
},
|
||||||
|
"certificateSerialNumber": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "1BB1BD7844451E3AC6"
|
||||||
|
},
|
||||||
|
"mandantId": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "1"
|
||||||
|
},
|
||||||
|
"mandantName": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"example": "eB-Standard"
|
||||||
|
},
|
||||||
|
"mandantDatabase": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"example": "eazybusiness"
|
||||||
|
},
|
||||||
|
"serverFingerprint": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"example": "30-DD-11-1E-80-48-B5-7E-6A-4F-B7-5A-D7-E6-3F-7E-65-7F-12-62"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"serverTimestamp": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "2026-09-29 11:00:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid or incorrect authentication code",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Der Authentifizierungscode ist falsch."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
|
|||||||
46
docs/api/routes/pos/client.md
Normal file
46
docs/api/routes/pos/client.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# `GET /v1/client`
|
||||||
|
|
||||||
|
Public. No cookie required.
|
||||||
|
|
||||||
|
POS discovery and pairing
|
||||||
|
|
||||||
|
Special API for POS bind and sync.
|
||||||
|
- Step 1 (Discovery): `authCode` length 1–4. Returns certificate fingerprints, serial number, and mandantId.
|
||||||
|
- Step 2 (Pairing): `authCode` length 6 (pairingpin from posterminal). Verifies the pairingpin, sets token, paired_at, and clientserial, revokes the pairingpin, and returns device credentials and company metadata.
|
||||||
|
|
||||||
|
|
||||||
|
## Query parameters
|
||||||
|
|
||||||
|
| Param | Required | Type | Description |
|
||||||
|
|-------|----------|------|-------------|
|
||||||
|
| `authCode` | yes | string | Discovery code (1-4 digits) or pairing PIN (6 digits) |
|
||||||
|
| `name` | no | string | Client device name or serial identifier (default 'JTL-POS') |
|
||||||
|
|
||||||
|
## Response `200`
|
||||||
|
|
||||||
|
Discovery or pairing successful
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"authCode": "678677",
|
||||||
|
"authToken": "df40ad2067954646abb0499548a52241",
|
||||||
|
"certificateFingerprint": "30DD111E8048B57E6A4FB75AD7E63F7E657F1262",
|
||||||
|
"certificateSerialNumber": "1BB1BD7844451E3AC6",
|
||||||
|
"mandantId": "1",
|
||||||
|
"mandantName": "eB-Standard",
|
||||||
|
"mandantDatabase": "eazybusiness",
|
||||||
|
"serverFingerprint": "30-DD-11-1E-80-48-B5-7E-6A-4F-B7-5A-D7-E6-3F-7E-65-7F-12-62",
|
||||||
|
"name": "…",
|
||||||
|
"serverTimestamp": "2026-09-29 11:00:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| 400 | Invalid or incorrect authentication code |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
@@ -13,7 +13,7 @@ const root = path.resolve(__dirname, '..');
|
|||||||
const docsDir = path.join(root, 'docs', 'api');
|
const docsDir = path.join(root, 'docs', 'api');
|
||||||
const routesDocsDir = path.join(docsDir, 'routes');
|
const routesDocsDir = path.join(docsDir, 'routes');
|
||||||
|
|
||||||
const PUBLIC_PATHS = new Set(['/auth/login', '/auth/logout', '/health']);
|
const PUBLIC_PATHS = new Set(['/auth/login', '/auth/logout', '/health', '/v1/client']);
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
definition: {
|
definition: {
|
||||||
@@ -26,10 +26,14 @@ const options = {
|
|||||||
'Browser clients must send cookies (`credentials: \'include\'`). ' +
|
'Browser clients must send cookies (`credentials: \'include\'`). ' +
|
||||||
'Sessions use an HTTP-only `token` cookie (`Secure`, `SameSite=Strict`, 24h); HTTPS is required for login.',
|
'Sessions use an HTTP-only `token` cookie (`Secure`, `SameSite=Strict`, 24h); HTTPS is required for login.',
|
||||||
},
|
},
|
||||||
servers: [{ url: 'http://localhost:4208/api', description: 'Local default (PORT / .env)' }],
|
servers: [
|
||||||
|
{ url: 'http://localhost:4208/api', description: 'Local default HTTP (PORT / .env)' },
|
||||||
|
{ url: 'https://localhost:8443', description: 'Local default HTTPS (HTTPS_PORT / .env)' },
|
||||||
|
],
|
||||||
tags: [
|
tags: [
|
||||||
{ name: 'Auth', description: 'Google SSO and session' },
|
{ name: 'Auth', description: 'Google SSO and session' },
|
||||||
{ name: 'Health', description: 'Connectivity checks' },
|
{ name: 'Health', description: 'Connectivity checks' },
|
||||||
|
{ name: 'POS', description: 'POS bind and sync API (/v1)' },
|
||||||
{ name: 'Events', description: 'SSE update signals for live clients' },
|
{ name: 'Events', description: 'SSE update signals for live clients' },
|
||||||
{ name: 'Article', description: 'Articles under categories' },
|
{ name: 'Article', description: 'Articles under categories' },
|
||||||
{ name: 'Category', description: 'Category tree and membership' },
|
{ name: 'Category', description: 'Category tree and membership' },
|
||||||
@@ -55,6 +59,7 @@ const options = {
|
|||||||
apis: [
|
apis: [
|
||||||
path.join(root, 'src/api/openapi.js'),
|
path.join(root, 'src/api/openapi.js'),
|
||||||
path.join(root, 'src/api/routes/**/*.js'),
|
path.join(root, 'src/api/routes/**/*.js'),
|
||||||
|
path.join(root, 'src/v1/**/*.js'),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -352,6 +357,8 @@ function renderReadme(operations) {
|
|||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push('```');
|
lines.push('```');
|
||||||
lines.push('http://localhost:4208/api');
|
lines.push('http://localhost:4208/api');
|
||||||
|
lines.push('https://localhost:8443/api');
|
||||||
|
lines.push('https://localhost:8443/v1 (POS API)');
|
||||||
lines.push('```');
|
lines.push('```');
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push('## CORS & cookies');
|
lines.push('## CORS & cookies');
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { authMiddleware } from './middleware/auth.js';
|
|||||||
import { ensureConnection } from '../db/store.js';
|
import { ensureConnection } from '../db/store.js';
|
||||||
import { closeAll as closeAllSse } from './sse.js';
|
import { closeAll as closeAllSse } from './sse.js';
|
||||||
import { getOrCreateCert } from '../util/cert.js';
|
import { getOrCreateCert } from '../util/cert.js';
|
||||||
|
import v1Router from '../v1/index.js';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
@@ -85,6 +86,8 @@ async function createApi() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await loadRoutes(app, '/api');
|
await loadRoutes(app, '/api');
|
||||||
|
app.use('/v1', v1Router);
|
||||||
|
app.use('/api/v1', v1Router);
|
||||||
|
|
||||||
app.use((err, _req, res, next) => {
|
app.use((err, _req, res, next) => {
|
||||||
if (err.type === 'entity.too.large') {
|
if (err.type === 'entity.too.large') {
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ export const authMiddleware = async (req, res, next) => {
|
|||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.path.startsWith('/v1') || req.path.startsWith('/api/v1')) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
if (req.path.startsWith('/@') || req.path.startsWith('/node_modules') || req.path.startsWith('/src') || req.path === '/favicon.ico') {
|
if (req.path.startsWith('/@') || req.path.startsWith('/node_modules') || req.path.startsWith('/src') || req.path === '/favicon.ico') {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ export async function listCompanies(authId) {
|
|||||||
return store.company.list(authId);
|
return store.company.list(authId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getCompanyById(id) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.company.getById(id);
|
||||||
|
}
|
||||||
|
|
||||||
export async function createCompany(googleAuthId, data) {
|
export async function createCompany(googleAuthId, data) {
|
||||||
const store = await getStore();
|
const store = await getStore();
|
||||||
return store.company.create({ ...data, googleAuthId });
|
return store.company.create({ ...data, googleAuthId });
|
||||||
|
|||||||
@@ -34,3 +34,13 @@ export async function setOffAll(googleAuthId, posterminalId) {
|
|||||||
const store = await getStore();
|
const store = await getStore();
|
||||||
return store.posterminal.setOffAll(googleAuthId, posterminalId);
|
return store.posterminal.setOffAll(googleAuthId, posterminalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function findByPairingpin(pairingpin) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.posterminal.findByPairingpin(pairingpin);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function pair(id, data) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.posterminal.pair(id, data);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
import { X509Certificate } from 'node:crypto';
|
||||||
import selfsigned from 'selfsigned';
|
import selfsigned from 'selfsigned';
|
||||||
import env from '../config/env.js';
|
import env from '../config/env.js';
|
||||||
|
|
||||||
@@ -69,3 +70,18 @@ export async function getOrCreateCert() {
|
|||||||
cert: pems.cert,
|
cert: pems.cert,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derives pairing metadata (fingerprints and serial number) from the TLS certificate.
|
||||||
|
* @returns {Promise<{ certificateFingerprint: string, certificateSerialNumber: string, serverFingerprint: string }>}
|
||||||
|
*/
|
||||||
|
export async function getCertMetadata() {
|
||||||
|
const { cert } = await getOrCreateCert();
|
||||||
|
const x509 = new X509Certificate(cert);
|
||||||
|
const sha1 = x509.fingerprint;
|
||||||
|
return {
|
||||||
|
certificateFingerprint: sha1.replace(/:/g, ''),
|
||||||
|
certificateSerialNumber: x509.serialNumber,
|
||||||
|
serverFingerprint: sha1.replace(/:/g, '-'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
9
src/v1/index.js
Normal file
9
src/v1/index.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { handleClient } from './routes/client.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// POS discovery and pairing
|
||||||
|
router.get('/client', handleClient);
|
||||||
|
|
||||||
|
export default router;
|
||||||
204
src/v1/routes/client.js
Normal file
204
src/v1/routes/client.js
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import crypto from 'node:crypto';
|
||||||
|
import * as db from '../../db/posterminal.js';
|
||||||
|
import { getCompanyById } from '../../db/company.js';
|
||||||
|
import { getCertMetadata } from '../../util/cert.js';
|
||||||
|
|
||||||
|
export function serverTimestamp() {
|
||||||
|
const d = new Date();
|
||||||
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildClientStep1(config) {
|
||||||
|
const {
|
||||||
|
certificateFingerprint,
|
||||||
|
certificateSerialNumber,
|
||||||
|
mandantId,
|
||||||
|
serverFingerprint,
|
||||||
|
} = config;
|
||||||
|
|
||||||
|
return {
|
||||||
|
authCode: null,
|
||||||
|
authToken: null,
|
||||||
|
certificateFingerprint,
|
||||||
|
certificateSerialNumber,
|
||||||
|
mandantId,
|
||||||
|
mandantName: null,
|
||||||
|
mandantDatabase: null,
|
||||||
|
serverFingerprint,
|
||||||
|
name: null,
|
||||||
|
serverTimestamp: serverTimestamp(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildClientStep2(authCode, config) {
|
||||||
|
const {
|
||||||
|
authToken,
|
||||||
|
certificateFingerprint,
|
||||||
|
certificateSerialNumber,
|
||||||
|
mandantId,
|
||||||
|
mandantName,
|
||||||
|
mandantDatabase,
|
||||||
|
} = config;
|
||||||
|
|
||||||
|
return {
|
||||||
|
authCode,
|
||||||
|
authToken,
|
||||||
|
certificateFingerprint,
|
||||||
|
certificateSerialNumber,
|
||||||
|
mandantId,
|
||||||
|
mandantName,
|
||||||
|
mandantDatabase,
|
||||||
|
serverFingerprint: null,
|
||||||
|
name: null,
|
||||||
|
serverTimestamp: serverTimestamp(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /v1/client:
|
||||||
|
* get:
|
||||||
|
* tags: [POS]
|
||||||
|
* summary: POS discovery and pairing
|
||||||
|
* description: |
|
||||||
|
* Special API for POS bind and sync.
|
||||||
|
* - Step 1 (Discovery): `authCode` length 1–4. Returns certificate fingerprints, serial number, and mandantId.
|
||||||
|
* - Step 2 (Pairing): `authCode` length 6 (pairingpin from posterminal). Verifies the pairingpin, sets token, paired_at, and clientserial, revokes the pairingpin, and returns device credentials and company metadata.
|
||||||
|
* security: []
|
||||||
|
* x-markdown: pos/client.md
|
||||||
|
* parameters:
|
||||||
|
* - in: query
|
||||||
|
* name: authCode
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Discovery code (1-4 digits) or pairing PIN (6 digits)
|
||||||
|
* example: "678677"
|
||||||
|
* - in: query
|
||||||
|
* name: name
|
||||||
|
* required: false
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Client device name or serial identifier (default 'JTL-POS')
|
||||||
|
* example: "Kasse 1"
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Discovery or pairing successful
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* authCode:
|
||||||
|
* type: string
|
||||||
|
* nullable: true
|
||||||
|
* example: "678677"
|
||||||
|
* authToken:
|
||||||
|
* type: string
|
||||||
|
* nullable: true
|
||||||
|
* example: "df40ad2067954646abb0499548a52241"
|
||||||
|
* certificateFingerprint:
|
||||||
|
* type: string
|
||||||
|
* example: "30DD111E8048B57E6A4FB75AD7E63F7E657F1262"
|
||||||
|
* certificateSerialNumber:
|
||||||
|
* type: string
|
||||||
|
* example: "1BB1BD7844451E3AC6"
|
||||||
|
* mandantId:
|
||||||
|
* type: string
|
||||||
|
* example: "1"
|
||||||
|
* mandantName:
|
||||||
|
* type: string
|
||||||
|
* nullable: true
|
||||||
|
* example: "eB-Standard"
|
||||||
|
* mandantDatabase:
|
||||||
|
* type: string
|
||||||
|
* nullable: true
|
||||||
|
* example: "eazybusiness"
|
||||||
|
* serverFingerprint:
|
||||||
|
* type: string
|
||||||
|
* nullable: true
|
||||||
|
* example: "30-DD-11-1E-80-48-B5-7E-6A-4F-B7-5A-D7-E6-3F-7E-65-7F-12-62"
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* nullable: true
|
||||||
|
* serverTimestamp:
|
||||||
|
* type: string
|
||||||
|
* example: "2026-09-29 11:00:00"
|
||||||
|
* 400:
|
||||||
|
* description: Invalid or incorrect authentication code
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* Message:
|
||||||
|
* type: string
|
||||||
|
* example: "Der Authentifizierungscode ist falsch."
|
||||||
|
* @param {import('express').Request} req
|
||||||
|
* @param {import('express').Response} res
|
||||||
|
*/
|
||||||
|
export async function handleClient(req, res) {
|
||||||
|
const authCode = String(req.query.authCode || '').trim();
|
||||||
|
const name = String(req.query.name || 'JTL-POS').trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const certMeta = await getCertMetadata();
|
||||||
|
|
||||||
|
if (authCode.length <= 4 && authCode.length > 0) {
|
||||||
|
const config = {
|
||||||
|
...certMeta,
|
||||||
|
mandantId: process.env.MANDANT_ID || '1',
|
||||||
|
};
|
||||||
|
return res.status(200).json(buildClientStep1(config));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authCode.length === 6) {
|
||||||
|
const terminal = await db.findByPairingpin(authCode);
|
||||||
|
if (terminal) {
|
||||||
|
const token = terminal.token;
|
||||||
|
const strToken = String(token || crypto.randomBytes(16).toString('hex')).trim();
|
||||||
|
const pairedAt = serverTimestamp();
|
||||||
|
|
||||||
|
await db.pair(terminal.id, {
|
||||||
|
token: strToken,
|
||||||
|
clientserial: name,
|
||||||
|
pairedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mandantName = process.env.MANDANT_NAME || 'eB-Standard';
|
||||||
|
let mandantDatabase = process.env.MANDANT_DATABASE || 'eazybusiness';
|
||||||
|
let mandantId = String(terminal.companyId || process.env.MANDANT_ID || '1');
|
||||||
|
|
||||||
|
if (terminal.companyId) {
|
||||||
|
try {
|
||||||
|
const company = await getCompanyById(terminal.companyId);
|
||||||
|
if (company) {
|
||||||
|
mandantName = company.name;
|
||||||
|
mandantDatabase = company.name;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore lookup error and fallback to defaults
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
...certMeta,
|
||||||
|
authToken: strToken,
|
||||||
|
mandantId,
|
||||||
|
mandantName,
|
||||||
|
mandantDatabase,
|
||||||
|
};
|
||||||
|
|
||||||
|
return res.status(200).json(buildClientStep2(authCode, config));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(400).json({ Message: 'Der Authentifizierungscode ist falsch.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(400).json({ Message: 'Keinen passenden Authentifizierungscode gefunden.' });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('POS /v1/client error:', err);
|
||||||
|
return res.status(500).json({ Message: 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user