u
This commit is contained in:
@@ -37,6 +37,10 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
|
|||||||
| [`routes/account.get.md`](routes/account.get.md) | `GET /account` |
|
| [`routes/account.get.md`](routes/account.get.md) | `GET /account` |
|
||||||
| [`routes/account.post.md`](routes/account.post.md) | `POST /account` |
|
| [`routes/account.post.md`](routes/account.post.md) | `POST /account` |
|
||||||
| [`routes/account/_id.md`](routes/account/_id.md) | `DELETE /account/{id}` |
|
| [`routes/account/_id.md`](routes/account/_id.md) | `DELETE /account/{id}` |
|
||||||
|
| [`routes/agent/events.md`](routes/agent/events.md) | `GET /agent/events` |
|
||||||
|
| [`routes/agent/pair.get.md`](routes/agent/pair.get.md) | `GET /agent/pair` |
|
||||||
|
| [`routes/agent/pair.post.md`](routes/agent/pair.post.md) | `POST /agent/pair` |
|
||||||
|
| [`routes/agent/state.md`](routes/agent/state.md) | `POST /agent/state` |
|
||||||
| [`routes/article/create.md`](routes/article/create.md) | `POST /article` |
|
| [`routes/article/create.md`](routes/article/create.md) | `POST /article` |
|
||||||
| [`routes/article/export.zip.md`](routes/article/export.zip.md) | `GET /article/export.zip` |
|
| [`routes/article/export.zip.md`](routes/article/export.zip.md) | `GET /article/export.zip` |
|
||||||
| [`routes/article/history-list.md`](routes/article/history-list.md) | `GET /article/history` |
|
| [`routes/article/history-list.md`](routes/article/history-list.md) | `GET /article/history` |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Dash API Routes Overview (All)
|
# Dash API Routes Overview (All)
|
||||||
|
|
||||||
Overview of all 288 API endpoints across all modules.
|
Overview of all 292 API endpoints across all modules.
|
||||||
|
|
||||||
### Notation
|
### Notation
|
||||||
|
|
||||||
@@ -73,6 +73,21 @@ _none_
|
|||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `204 void`
|
- Returns: `204 void`
|
||||||
|
|
||||||
|
### Section: /agent
|
||||||
|
|
||||||
|
#### Printer agent SSE stream — `GET /agent/events`
|
||||||
|
- Params: `query: token?: str, agentversion?: str`
|
||||||
|
- Returns: `200 SSE`
|
||||||
|
#### Pair printer agent via query parameters — `GET /agent/pair`
|
||||||
|
- Params: `query: authCode?: str, pairingpin?: str, serial?: str, model?: str, version?: str`
|
||||||
|
- Returns: `200`
|
||||||
|
#### Pair printer agent — `POST /agent/pair`
|
||||||
|
- Params: `body: { pairingpin: str, clientserial?: str, clientmodel?: str, agentversion?: str }`
|
||||||
|
- Returns: `200 { id: int, name: str, companyId: int, token: str, pairedAt: str, clientserial: str|null, clientmodel: str|null, agentversion: str|null, sort: int }`
|
||||||
|
#### Post agent state — `POST /agent/state`
|
||||||
|
- Params: `body: { state?: any }`
|
||||||
|
- Returns: `200 { success: bool, agent: T8 }`
|
||||||
|
|
||||||
### Section: /article
|
### Section: /article
|
||||||
|
|
||||||
#### Create article in category — `POST /article`
|
#### Create article in category — `POST /article`
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Dash API Routes Overview
|
# Dash API Routes Overview
|
||||||
|
|
||||||
Overview of all 208 API endpoints across all modules.
|
Overview of all 212 API endpoints across all modules.
|
||||||
|
|
||||||
### Notation
|
### Notation
|
||||||
|
|
||||||
@@ -42,6 +42,21 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
|
|||||||
### Routes
|
### Routes
|
||||||
|
|
||||||
|
|
||||||
|
### Section: /agent
|
||||||
|
|
||||||
|
#### Printer agent SSE stream — `GET /agent/events`
|
||||||
|
- Params: `query: token?: str, agentversion?: str`
|
||||||
|
- Returns: `200 SSE`
|
||||||
|
#### Pair printer agent via query parameters — `GET /agent/pair`
|
||||||
|
- Params: `query: authCode?: str, pairingpin?: str, serial?: str, model?: str, version?: str`
|
||||||
|
- Returns: `200`
|
||||||
|
#### Pair printer agent — `POST /agent/pair`
|
||||||
|
- Params: `body: { pairingpin: str, clientserial?: str, clientmodel?: str, agentversion?: str }`
|
||||||
|
- Returns: `200 { id: int, name: str, companyId: int, token: str, pairedAt: str, clientserial: str|null, clientmodel: str|null, agentversion: str|null, sort: int }`
|
||||||
|
#### Post agent state — `POST /agent/state`
|
||||||
|
- Params: `body: { state?: any }`
|
||||||
|
- Returns: `200 { success: bool, agent: T5 }`
|
||||||
|
|
||||||
### Section: /article
|
### Section: /article
|
||||||
|
|
||||||
#### Create article in category — `POST /article`
|
#### Create article in category — `POST /article`
|
||||||
|
|||||||
30
docs/api/routes/agent/events.md
Normal file
30
docs/api/routes/agent/events.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# `GET /agent/events`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
Printer agent SSE stream
|
||||||
|
|
||||||
|
Authorized Server-Sent Events stream for connected printer agents.
|
||||||
|
Maintains a persistent connection with regular heartbeats (`: heartbeat\n\n`) and updates `lastseen_at` and `agentversion`.
|
||||||
|
|
||||||
|
|
||||||
|
## Query parameters
|
||||||
|
|
||||||
|
| Param | Required | Type | Description |
|
||||||
|
|-------|----------|------|-------------|
|
||||||
|
| `token` | no | string | Agent auth token if not provided via Authorization header |
|
||||||
|
| `agentversion` | no | string | Current agent version |
|
||||||
|
|
||||||
|
## Response `200`
|
||||||
|
|
||||||
|
Event stream established
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| 401 | Unauthorized |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
31
docs/api/routes/agent/pair.get.md
Normal file
31
docs/api/routes/agent/pair.get.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# `GET /agent/pair`
|
||||||
|
|
||||||
|
Public. No cookie required.
|
||||||
|
|
||||||
|
Pair printer agent via query parameters
|
||||||
|
|
||||||
|
Alternative GET endpoint for devices that pair via query parameters.
|
||||||
|
|
||||||
|
## Query parameters
|
||||||
|
|
||||||
|
| Param | Required | Type | Description |
|
||||||
|
|-------|----------|------|-------------|
|
||||||
|
| `authCode` | no | string | Pairing PIN (or pairingpin) |
|
||||||
|
| `pairingpin` | no | string | |
|
||||||
|
| `serial` | no | string | |
|
||||||
|
| `model` | no | string | |
|
||||||
|
| `version` | no | string | |
|
||||||
|
|
||||||
|
## Response `200`
|
||||||
|
|
||||||
|
Pairing successful
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| 400 | Missing or invalid pairing PIN |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
55
docs/api/routes/agent/pair.post.md
Normal file
55
docs/api/routes/agent/pair.post.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# `POST /agent/pair`
|
||||||
|
|
||||||
|
Public. No cookie required.
|
||||||
|
|
||||||
|
Pair printer agent
|
||||||
|
|
||||||
|
Public, unauthorized endpoint used by the printer agent to complete pairing using a 6-digit PIN.
|
||||||
|
Updates `clientserial`, `clientmodel`, and `agentversion`, clears the pairing PIN, and returns a device authentication token.
|
||||||
|
|
||||||
|
|
||||||
|
## Body (JSON)
|
||||||
|
|
||||||
|
| Field | Required | Type | Description |
|
||||||
|
|-------|----------|------|-------------|
|
||||||
|
| `pairingpin` | yes | string | |
|
||||||
|
| `clientserial` | no | string | |
|
||||||
|
| `clientmodel` | no | string | |
|
||||||
|
| `agentversion` | no | string | |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"pairingpin": "123456",
|
||||||
|
"clientserial": "SN-987654321",
|
||||||
|
"clientmodel": "Epson TM-T88VI",
|
||||||
|
"agentversion": "1.0.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response `200`
|
||||||
|
|
||||||
|
Pairing successful
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "…",
|
||||||
|
"companyId": 1,
|
||||||
|
"token": "…",
|
||||||
|
"pairedAt": "…",
|
||||||
|
"clientserial": "…",
|
||||||
|
"clientmodel": "…",
|
||||||
|
"agentversion": "…",
|
||||||
|
"sort": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| 400 | Missing or invalid pairing PIN — `{"error":"…"}` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
55
docs/api/routes/agent/state.md
Normal file
55
docs/api/routes/agent/state.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# `POST /agent/state`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
Post agent state
|
||||||
|
|
||||||
|
Authorized endpoint for the printer agent to report its operational status/telemetry.
|
||||||
|
|
||||||
|
|
||||||
|
## Body (JSON)
|
||||||
|
|
||||||
|
| Field | Required | Type | Description |
|
||||||
|
|-------|----------|------|-------------|
|
||||||
|
| `state` | no | | Status text or status object |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": "…"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response `200`
|
||||||
|
|
||||||
|
State recorded
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"agent": {
|
||||||
|
"id": 1,
|
||||||
|
"companyId": 1,
|
||||||
|
"name": "…",
|
||||||
|
"token": "…",
|
||||||
|
"pairingpin": "…",
|
||||||
|
"pairedAt": "…",
|
||||||
|
"clientserial": "…",
|
||||||
|
"clientmodel": "…",
|
||||||
|
"clientstate": "…",
|
||||||
|
"agentversion": "…",
|
||||||
|
"lastseenAt": "…",
|
||||||
|
"updatedAt": "…",
|
||||||
|
"sort": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| 401 | Unauthorized |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
315
src/api/agent/index.js
Normal file
315
src/api/agent/index.js
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import crypto from 'node:crypto';
|
||||||
|
import * as db from '../../db/printeragent.js';
|
||||||
|
import { agentAuthMiddleware, extractAgentVersion } from '../middleware/agentAuth.js';
|
||||||
|
import { emit } from '../sse.js';
|
||||||
|
|
||||||
|
export const agentRouter = express.Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to extract pairing parameters from body or query.
|
||||||
|
*/
|
||||||
|
function extractPairParams(req) {
|
||||||
|
const rawPin = req.body?.pairingpin ||
|
||||||
|
req.body?.authCode ||
|
||||||
|
req.body?.pin ||
|
||||||
|
req.body?.code ||
|
||||||
|
req.query?.pairingpin ||
|
||||||
|
req.query?.authCode ||
|
||||||
|
req.query?.pin ||
|
||||||
|
req.query?.code ||
|
||||||
|
'';
|
||||||
|
const pairingpin = String(rawPin).trim();
|
||||||
|
|
||||||
|
const rawSerial = req.body?.clientserial ||
|
||||||
|
req.body?.serial ||
|
||||||
|
req.body?.name ||
|
||||||
|
req.query?.clientserial ||
|
||||||
|
req.query?.serial ||
|
||||||
|
req.query?.name ||
|
||||||
|
null;
|
||||||
|
const clientserial = rawSerial ? String(rawSerial).trim() : null;
|
||||||
|
|
||||||
|
const rawModel = req.body?.clientmodel ||
|
||||||
|
req.body?.model ||
|
||||||
|
req.query?.clientmodel ||
|
||||||
|
req.query?.model ||
|
||||||
|
null;
|
||||||
|
const clientmodel = rawModel ? String(rawModel).trim() : null;
|
||||||
|
|
||||||
|
const agentversion = extractAgentVersion(req);
|
||||||
|
|
||||||
|
return { pairingpin, clientserial, clientmodel, agentversion };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePair(req, res) {
|
||||||
|
const { pairingpin, clientserial, clientmodel, agentversion } = extractPairParams(req);
|
||||||
|
|
||||||
|
if (!pairingpin) {
|
||||||
|
return res.status(400).json({ error: 'pairingpin is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const agent = await db.findByPairingpin(pairingpin);
|
||||||
|
if (!agent) {
|
||||||
|
return res.status(400).json({ error: 'Invalid pairing PIN' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = crypto.randomBytes(32).toString('hex');
|
||||||
|
const pairedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
const paired = await db.pair(agent.id, {
|
||||||
|
token,
|
||||||
|
clientserial,
|
||||||
|
clientmodel,
|
||||||
|
agentversion,
|
||||||
|
pairedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (paired.companyId) {
|
||||||
|
emit('printeragent', { companyId: paired.companyId });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
id: paired.id,
|
||||||
|
name: paired.name,
|
||||||
|
companyId: paired.companyId,
|
||||||
|
token,
|
||||||
|
pairedAt: paired.pairedAt,
|
||||||
|
clientserial: paired.clientserial,
|
||||||
|
clientmodel: paired.clientmodel,
|
||||||
|
agentversion: paired.agentversion,
|
||||||
|
sort: paired.sort,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Agent pairing failed:', err);
|
||||||
|
return res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /agent/pair:
|
||||||
|
* post:
|
||||||
|
* tags: [Agent]
|
||||||
|
* summary: Pair printer agent
|
||||||
|
* description: |
|
||||||
|
* Public, unauthorized endpoint used by the printer agent to complete pairing using a 6-digit PIN.
|
||||||
|
* Updates `clientserial`, `clientmodel`, and `agentversion`, clears the pairing PIN, and returns a device authentication token.
|
||||||
|
* security: []
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required: [pairingpin]
|
||||||
|
* properties:
|
||||||
|
* pairingpin:
|
||||||
|
* type: string
|
||||||
|
* example: "123456"
|
||||||
|
* clientserial:
|
||||||
|
* type: string
|
||||||
|
* example: "SN-987654321"
|
||||||
|
* clientmodel:
|
||||||
|
* type: string
|
||||||
|
* example: "Epson TM-T88VI"
|
||||||
|
* agentversion:
|
||||||
|
* type: string
|
||||||
|
* example: "1.0.0"
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Pairing successful
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: integer
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* companyId:
|
||||||
|
* type: integer
|
||||||
|
* token:
|
||||||
|
* type: string
|
||||||
|
* pairedAt:
|
||||||
|
* type: string
|
||||||
|
* clientserial:
|
||||||
|
* type: string
|
||||||
|
* nullable: true
|
||||||
|
* clientmodel:
|
||||||
|
* type: string
|
||||||
|
* nullable: true
|
||||||
|
* agentversion:
|
||||||
|
* type: string
|
||||||
|
* nullable: true
|
||||||
|
* sort:
|
||||||
|
* type: integer
|
||||||
|
* 400:
|
||||||
|
* description: Missing or invalid pairing PIN
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* get:
|
||||||
|
* tags: [Agent]
|
||||||
|
* summary: Pair printer agent via query parameters
|
||||||
|
* description: Alternative GET endpoint for devices that pair via query parameters.
|
||||||
|
* security: []
|
||||||
|
* parameters:
|
||||||
|
* - in: query
|
||||||
|
* name: authCode
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Pairing PIN (or pairingpin)
|
||||||
|
* - in: query
|
||||||
|
* name: pairingpin
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* - in: query
|
||||||
|
* name: serial
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* - in: query
|
||||||
|
* name: model
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* - in: query
|
||||||
|
* name: version
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Pairing successful
|
||||||
|
* 400:
|
||||||
|
* description: Missing or invalid pairing PIN
|
||||||
|
*/
|
||||||
|
agentRouter.post('/pair', handlePair);
|
||||||
|
agentRouter.get('/pair', handlePair);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /agent/events:
|
||||||
|
* get:
|
||||||
|
* tags: [Agent]
|
||||||
|
* summary: Printer agent SSE stream
|
||||||
|
* description: |
|
||||||
|
* Authorized Server-Sent Events stream for connected printer agents.
|
||||||
|
* Maintains a persistent connection with regular heartbeats (`: heartbeat\n\n`) and updates `lastseen_at` and `agentversion`.
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* - apiKeyAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: query
|
||||||
|
* name: token
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Agent auth token if not provided via Authorization header
|
||||||
|
* - in: query
|
||||||
|
* name: agentversion
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Current agent version
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Event stream established
|
||||||
|
* content:
|
||||||
|
* text/event-stream:
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized
|
||||||
|
*/
|
||||||
|
agentRouter.get('/events', agentAuthMiddleware, async (req, res) => {
|
||||||
|
const version = extractAgentVersion(req);
|
||||||
|
if (version) {
|
||||||
|
db.touchLastseen(req.agent.id, version).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'text/event-stream');
|
||||||
|
res.setHeader('Cache-Control', 'no-cache');
|
||||||
|
res.setHeader('Connection', 'keep-alive');
|
||||||
|
res.setHeader('X-Accel-Buffering', 'no');
|
||||||
|
res.flushHeaders?.();
|
||||||
|
|
||||||
|
res.write(`event: connected\ndata: ${JSON.stringify({ agentId: req.agent.id, name: req.agent.name })}\n\n`);
|
||||||
|
|
||||||
|
const keepAlive = setInterval(() => {
|
||||||
|
if (res.writableEnded) return;
|
||||||
|
res.write(': heartbeat\n\n');
|
||||||
|
db.touchLastseen(req.agent.id).catch(() => {});
|
||||||
|
}, 15000);
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
clearInterval(keepAlive);
|
||||||
|
};
|
||||||
|
|
||||||
|
req.on('close', cleanup);
|
||||||
|
res.on('close', cleanup);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /agent/state:
|
||||||
|
* post:
|
||||||
|
* tags: [Agent]
|
||||||
|
* summary: Post agent state
|
||||||
|
* description: |
|
||||||
|
* Authorized endpoint for the printer agent to report its operational status/telemetry.
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* - apiKeyAuth: []
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* state:
|
||||||
|
* description: Status text or status object
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: State recorded
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* agent:
|
||||||
|
* $ref: '#/components/schemas/Printeragent'
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized
|
||||||
|
*/
|
||||||
|
async function handleState(req, res) {
|
||||||
|
try {
|
||||||
|
const rawState = req.body?.state !== undefined ? req.body.state : (req.body?.clientstate !== undefined ? req.body.clientstate : req.body);
|
||||||
|
const stateStr = rawState == null ? null : (typeof rawState === 'string' ? rawState : JSON.stringify(rawState));
|
||||||
|
|
||||||
|
const updated = await db.updateState(req.agent.id, stateStr);
|
||||||
|
|
||||||
|
const version = extractAgentVersion(req);
|
||||||
|
if (version) {
|
||||||
|
await db.touchLastseen(req.agent.id, version);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated.companyId) {
|
||||||
|
emit('printeragent', { companyId: updated.companyId });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
agent: updated,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Agent post state failed:', err);
|
||||||
|
return res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
agentRouter.post('/state', agentAuthMiddleware, handleState);
|
||||||
|
agentRouter.put('/state', agentAuthMiddleware, handleState);
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ import { shutdownJavascriptSandboxWorkers } from '../services/javascriptSandbox.
|
|||||||
import { getOrCreateCert } from '../util/cert.js';
|
import { getOrCreateCert } from '../util/cert.js';
|
||||||
import { createLogger } from '../util/logger.js';
|
import { createLogger } from '../util/logger.js';
|
||||||
import v1Router from '../v1/index.js';
|
import v1Router from '../v1/index.js';
|
||||||
|
import { agentRouter } from './agent/index.js';
|
||||||
import { startBackendMonitor, stopBackendMonitor, isDevMode } from '../backendmonitor/server.js';
|
import { startBackendMonitor, stopBackendMonitor, isDevMode } from '../backendmonitor/server.js';
|
||||||
import { startBackupService, stopBackupService } from '../services/backupService.js';
|
import { startBackupService, stopBackupService } from '../services/backupService.js';
|
||||||
import { startHealthMonitorService, stopHealthMonitorService } from '../services/healthMonitorService.js';
|
import { startHealthMonitorService, stopHealthMonitorService } from '../services/healthMonitorService.js';
|
||||||
@@ -100,6 +101,20 @@ function corsMiddleware(req, res, next) {
|
|||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.path.startsWith('/api/agent')) {
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', origin || '*');
|
||||||
|
const reqHeaders = req.headers['access-control-request-headers'];
|
||||||
|
res.setHeader(
|
||||||
|
'Access-Control-Allow-Headers',
|
||||||
|
reqHeaders || 'Content-Type, Authorization, x-agent-token, x-auth-token, authtoken, auth-token, token, x-token, agentversion, clientversion, version'
|
||||||
|
);
|
||||||
|
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
return res.sendStatus(204);
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
if (req.path.startsWith('/v1')) {
|
if (req.path.startsWith('/v1')) {
|
||||||
if (origin) {
|
if (origin) {
|
||||||
res.setHeader('Access-Control-Allow-Origin', origin);
|
res.setHeader('Access-Control-Allow-Origin', origin);
|
||||||
@@ -144,7 +159,7 @@ export async function buildApiApp() {
|
|||||||
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
// Do not attach no-store to SSE or public endpoints
|
// Do not attach no-store to SSE or public endpoints
|
||||||
if (req.path !== '/api/events' && !req.path.startsWith('/api/public') && !req.path.startsWith('/public')) {
|
if (req.path !== '/api/events' && !req.path.endsWith('/events') && !req.path.startsWith('/api/public') && !req.path.startsWith('/public')) {
|
||||||
res.setHeader('Cache-Control', 'no-store');
|
res.setHeader('Cache-Control', 'no-store');
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
@@ -164,6 +179,7 @@ export async function buildApiApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.routeCount = await registerRoutes(app, '/api');
|
app.routeCount = await registerRoutes(app, '/api');
|
||||||
|
app.use('/api/agent', agentRouter);
|
||||||
app.use('/api/v1', v1Router);
|
app.use('/api/v1', v1Router);
|
||||||
|
|
||||||
app.use((_req, res) => {
|
app.use((_req, res) => {
|
||||||
|
|||||||
72
src/api/middleware/agentAuth.js
Normal file
72
src/api/middleware/agentAuth.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import * as db from '../../db/printeragent.js';
|
||||||
|
|
||||||
|
export function extractAgentToken(req) {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (typeof authHeader === 'string' && authHeader.trim()) {
|
||||||
|
const trimmed = authHeader.trim();
|
||||||
|
if (/^Bearer\s+/i.test(trimmed)) {
|
||||||
|
return trimmed.replace(/^Bearer\s+/i, '').trim();
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of ['x-agent-token', 'x-auth-token', 'authtoken', 'auth-token', 'token', 'x-token']) {
|
||||||
|
const val = req.headers[key];
|
||||||
|
if (typeof val === 'string' && val.trim()) {
|
||||||
|
return val.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryToken = req.query?.authToken || req.query?.token;
|
||||||
|
if (typeof queryToken === 'string' && queryToken.trim()) {
|
||||||
|
return queryToken.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractAgentVersion(req) {
|
||||||
|
const raw = req.headers['agentversion'] ||
|
||||||
|
req.headers['agent-version'] ||
|
||||||
|
req.headers['clientversion'] ||
|
||||||
|
req.headers['version'] ||
|
||||||
|
req.query?.agentversion ||
|
||||||
|
req.query?.version ||
|
||||||
|
req.body?.agentversion ||
|
||||||
|
req.body?.version ||
|
||||||
|
null;
|
||||||
|
return raw ? String(raw).trim() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticates the printer agent via token and attaches `req.agent`.
|
||||||
|
* @param {import('express').Request} req
|
||||||
|
* @param {import('express').Response} res
|
||||||
|
* @param {import('express').NextFunction} next
|
||||||
|
*/
|
||||||
|
export async function agentAuthMiddleware(req, res, next) {
|
||||||
|
const token = extractAgentToken(req);
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const agent = await db.findByToken(token);
|
||||||
|
if (!agent) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
req.agent = agent;
|
||||||
|
|
||||||
|
const version = extractAgentVersion(req);
|
||||||
|
db.touchLastseen(agent.id, version).catch((err) => {
|
||||||
|
console.error('Failed to touch lastseen for printeragent:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Agent auth error:', err);
|
||||||
|
return res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -63,6 +63,10 @@ export const authMiddleware = async (req, res, next) => {
|
|||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.path.startsWith('/api/agent')) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
if (req.path.startsWith('/api/public') || req.path.startsWith('/public')) {
|
if (req.path.startsWith('/api/public') || req.path.startsWith('/public')) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export const openApiOptions = {
|
|||||||
{ name: 'Posterminal', description: 'Company-scoped POS terminals' },
|
{ name: 'Posterminal', description: 'Company-scoped POS terminals' },
|
||||||
{ name: 'Printer', description: 'Company-scoped receipt and label printers' },
|
{ name: 'Printer', description: 'Company-scoped receipt and label printers' },
|
||||||
{ name: 'Printeragent', description: 'Company-scoped printer agents (create, rename, delete)' },
|
{ name: 'Printeragent', description: 'Company-scoped printer agents (create, rename, delete)' },
|
||||||
|
{ name: 'Agent', description: 'Printer agent device endpoints (pairing, SSE events, state)' },
|
||||||
{ name: 'Ledger', description: 'Append-only ledger transactions' },
|
{ name: 'Ledger', description: 'Append-only ledger transactions' },
|
||||||
{ name: 'Stock', description: 'Manual stock additions and removals' },
|
{ name: 'Stock', description: 'Manual stock additions and removals' },
|
||||||
{ name: 'Warehouse', description: 'Company-scoped warehouses' },
|
{ name: 'Warehouse', description: 'Company-scoped warehouses' },
|
||||||
@@ -56,6 +57,7 @@ export const openApiOptions = {
|
|||||||
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/api/agent/**/*.js'),
|
||||||
path.join(root, 'src/v1/**/*.js'),
|
path.join(root, 'src/v1/**/*.js'),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,3 +19,28 @@ export async function remove(googleAuthId, id) {
|
|||||||
const store = await getStore();
|
const store = await getStore();
|
||||||
return store.printeragent.delete(googleAuthId, id);
|
return store.printeragent.delete(googleAuthId, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function findByPairingpin(pairingpin) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.printeragent.findByPairingpin(pairingpin);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findByToken(token) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.printeragent.findByToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function pair(id, data) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.printeragent.pair(id, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateState(id, clientstate) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.printeragent.updateState(id, clientstate);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function touchLastseen(id, agentversion) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.printeragent.touchLastseen(id, agentversion);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user