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.post.md`](routes/account.post.md) | `POST /account` |
|
||||
| [`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/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` |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Dash API Routes Overview (All)
|
||||
|
||||
Overview of all 288 API endpoints across all modules.
|
||||
Overview of all 292 API endpoints across all modules.
|
||||
|
||||
### Notation
|
||||
|
||||
@@ -73,6 +73,21 @@ _none_
|
||||
- Params: `id: int`
|
||||
- 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
|
||||
|
||||
#### Create article in category — `POST /article`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Dash API Routes Overview
|
||||
|
||||
Overview of all 208 API endpoints across all modules.
|
||||
Overview of all 212 API endpoints across all modules.
|
||||
|
||||
### Notation
|
||||
|
||||
@@ -42,6 +42,21 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
|
||||
### 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
|
||||
|
||||
#### 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 { createLogger } from '../util/logger.js';
|
||||
import v1Router from '../v1/index.js';
|
||||
import { agentRouter } from './agent/index.js';
|
||||
import { startBackendMonitor, stopBackendMonitor, isDevMode } from '../backendmonitor/server.js';
|
||||
import { startBackupService, stopBackupService } from '../services/backupService.js';
|
||||
import { startHealthMonitorService, stopHealthMonitorService } from '../services/healthMonitorService.js';
|
||||
@@ -100,6 +101,20 @@ function corsMiddleware(req, res, 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 (origin) {
|
||||
res.setHeader('Access-Control-Allow-Origin', origin);
|
||||
@@ -144,7 +159,7 @@ export async function buildApiApp() {
|
||||
|
||||
app.use((req, res, next) => {
|
||||
// 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');
|
||||
}
|
||||
next();
|
||||
@@ -164,6 +179,7 @@ export async function buildApiApp() {
|
||||
}
|
||||
|
||||
app.routeCount = await registerRoutes(app, '/api');
|
||||
app.use('/api/agent', agentRouter);
|
||||
app.use('/api/v1', v1Router);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
if (req.path.startsWith('/api/agent')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (req.path.startsWith('/api/public') || req.path.startsWith('/public')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ export const openApiOptions = {
|
||||
{ name: 'Posterminal', description: 'Company-scoped POS terminals' },
|
||||
{ name: 'Printer', description: 'Company-scoped receipt and label printers' },
|
||||
{ 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: 'Stock', description: 'Manual stock additions and removals' },
|
||||
{ name: 'Warehouse', description: 'Company-scoped warehouses' },
|
||||
@@ -56,6 +57,7 @@ export const openApiOptions = {
|
||||
apis: [
|
||||
path.join(root, 'src/api/openapi.js'),
|
||||
path.join(root, 'src/api/routes/**/*.js'),
|
||||
path.join(root, 'src/api/agent/**/*.js'),
|
||||
path.join(root, 'src/v1/**/*.js'),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -19,3 +19,28 @@ export async function remove(googleAuthId, id) {
|
||||
const store = await getStore();
|
||||
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