docs(api): document phase1 and phase2 device/realtime endpoints
This commit is contained in:
@@ -83,6 +83,34 @@ All authenticated endpoints expect a Better Auth session cookie sent by the clie
|
|||||||
|
|
||||||
`POST /videos/upload-url` request body requires `fileName` and `deviceId` (UUID belonging to the authenticated user), with optional `prefix`.
|
`POST /videos/upload-url` request body requires `fileName` and `deviceId` (UUID belonging to the authenticated user), with optional `prefix`.
|
||||||
|
|
||||||
|
### Device Management (Phase 1)
|
||||||
|
| Endpoint | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| `POST /devices/register` | Register a user-owned device as `camera` or `client` and issue bearer device token |
|
||||||
|
| `GET /devices` | List all devices for the authenticated user |
|
||||||
|
| `PATCH /devices/:deviceId` | Update device role/metadata/status |
|
||||||
|
| `POST /devices/:deviceId/heartbeat` | Device-token authenticated presence heartbeat |
|
||||||
|
|
||||||
|
### Camera-Client Linking (Phase 1)
|
||||||
|
| Endpoint | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| `POST /device-links` | Link one client device to one camera device |
|
||||||
|
| `GET /device-links` | List links for the authenticated user |
|
||||||
|
| `DELETE /device-links/:linkId` | Remove a camera-client link |
|
||||||
|
|
||||||
|
### Realtime Commands (Phase 2)
|
||||||
|
| Endpoint | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| `POST /commands` | Queue and dispatch command from a linked client device to camera |
|
||||||
|
| `GET /commands` | Inspect command status/history |
|
||||||
|
| `POST /commands/:commandId/ack` | Device-token ack/reject command fallback |
|
||||||
|
|
||||||
|
Socket.IO channel:
|
||||||
|
- Devices connect with bearer device token (`auth.token` or `Authorization: Bearer ...`).
|
||||||
|
- Camera receives `command:received`.
|
||||||
|
- Camera sends `command:ack` with `acknowledged` or `rejected`.
|
||||||
|
- Source client receives `command:status`.
|
||||||
|
|
||||||
### API Docs
|
### API Docs
|
||||||
OpenAPI docs are generated from Zod/OpenAPI definitions:
|
OpenAPI docs are generated from Zod/OpenAPI definitions:
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,55 @@ const DeleteVideoResponseSchema = registry.register(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const DeviceSchema = registry.register(
|
||||||
|
'Device',
|
||||||
|
z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
userId: z.string().uuid(),
|
||||||
|
name: z.string().nullable(),
|
||||||
|
role: z.enum(['camera', 'client']),
|
||||||
|
platform: z.string().nullable(),
|
||||||
|
appVersion: z.string().nullable(),
|
||||||
|
status: z.string(),
|
||||||
|
isCamera: z.boolean(),
|
||||||
|
lastSeenAt: z.string().datetime().nullable(),
|
||||||
|
createdAt: z.string().datetime(),
|
||||||
|
updatedAt: z.string().datetime(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const DeviceLinkSchema = registry.register(
|
||||||
|
'DeviceLink',
|
||||||
|
z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
ownerUserId: z.string().uuid(),
|
||||||
|
cameraDeviceId: z.string().uuid(),
|
||||||
|
clientDeviceId: z.string().uuid(),
|
||||||
|
status: z.string(),
|
||||||
|
createdAt: z.string().datetime(),
|
||||||
|
updatedAt: z.string().datetime(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const DeviceCommandSchema = registry.register(
|
||||||
|
'DeviceCommand',
|
||||||
|
z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
ownerUserId: z.string().uuid(),
|
||||||
|
sourceDeviceId: z.string().uuid(),
|
||||||
|
targetDeviceId: z.string().uuid(),
|
||||||
|
commandType: z.string(),
|
||||||
|
payload: z.record(z.string(), z.unknown()).nullable(),
|
||||||
|
status: z.string(),
|
||||||
|
retryCount: z.number().int(),
|
||||||
|
lastDispatchedAt: z.string().datetime().nullable(),
|
||||||
|
acknowledgedAt: z.string().datetime().nullable(),
|
||||||
|
error: z.string().nullable(),
|
||||||
|
createdAt: z.string().datetime(),
|
||||||
|
updatedAt: z.string().datetime(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: '/',
|
path: '/',
|
||||||
@@ -403,6 +452,162 @@ registry.registerPath({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: 'post',
|
||||||
|
path: '/devices/register',
|
||||||
|
summary: 'Register a mobile device and issue a device token',
|
||||||
|
tags: ['Devices'],
|
||||||
|
security: [{ cookieAuth: [] }],
|
||||||
|
request: {
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: z.object({
|
||||||
|
name: z.string().optional(),
|
||||||
|
role: z.enum(['camera', 'client']),
|
||||||
|
platform: z.string().optional(),
|
||||||
|
appVersion: z.string().optional(),
|
||||||
|
pushToken: z.string().optional(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
201: {
|
||||||
|
description: 'Device registered',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: z.object({
|
||||||
|
message: z.string(),
|
||||||
|
device: DeviceSchema,
|
||||||
|
deviceToken: z.string(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: 'get',
|
||||||
|
path: '/devices',
|
||||||
|
summary: 'List devices for the authenticated user',
|
||||||
|
tags: ['Devices'],
|
||||||
|
security: [{ cookieAuth: [] }],
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'User devices',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: z.object({
|
||||||
|
count: z.number().int(),
|
||||||
|
devices: z.array(DeviceSchema),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: 'post',
|
||||||
|
path: '/devices/{deviceId}/heartbeat',
|
||||||
|
summary: 'Record heartbeat for a device using bearer device token',
|
||||||
|
tags: ['Devices'],
|
||||||
|
security: [{ bearerDeviceToken: [] }],
|
||||||
|
request: {
|
||||||
|
params: z.object({ deviceId: z.string().uuid() }),
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: z.object({ status: z.enum(['online', 'offline']).optional() }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Heartbeat recorded',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: z.object({
|
||||||
|
message: z.string(),
|
||||||
|
device: z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
status: z.string(),
|
||||||
|
lastSeenAt: z.string().datetime().nullable(),
|
||||||
|
updatedAt: z.string().datetime(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: 'post',
|
||||||
|
path: '/device-links',
|
||||||
|
summary: 'Link a client device to a camera device',
|
||||||
|
tags: ['Device Links'],
|
||||||
|
security: [{ cookieAuth: [] }],
|
||||||
|
request: {
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: z.object({
|
||||||
|
cameraDeviceId: z.string().uuid(),
|
||||||
|
clientDeviceId: z.string().uuid(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
201: {
|
||||||
|
description: 'Link created',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: z.object({ message: z.string(), link: DeviceLinkSchema }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: 'post',
|
||||||
|
path: '/commands',
|
||||||
|
summary: 'Create and dispatch a realtime command from client to camera',
|
||||||
|
tags: ['Commands'],
|
||||||
|
security: [{ cookieAuth: [] }],
|
||||||
|
request: {
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: z.object({
|
||||||
|
sourceDeviceId: z.string().uuid(),
|
||||||
|
targetDeviceId: z.string().uuid(),
|
||||||
|
commandType: z.enum(['start_stream', 'stop_stream', 'ping', 'update_settings']),
|
||||||
|
payload: z.record(z.string(), z.unknown()).optional(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
201: {
|
||||||
|
description: 'Command queued',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: z.object({ message: z.string(), command: DeviceCommandSchema }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export function buildOpenApiDocument() {
|
export function buildOpenApiDocument() {
|
||||||
const generator = new OpenApiGeneratorV3(registry.definitions);
|
const generator = new OpenApiGeneratorV3(registry.definitions);
|
||||||
const document = generator.generateDocument({
|
const document = generator.generateDocument({
|
||||||
@@ -417,6 +622,9 @@ export function buildOpenApiDocument() {
|
|||||||
{ name: 'System', description: 'Service endpoints' },
|
{ name: 'System', description: 'Service endpoints' },
|
||||||
{ name: 'Videos', description: 'Authenticated video object operations' },
|
{ name: 'Videos', description: 'Authenticated video object operations' },
|
||||||
{ name: 'Admin', description: 'Basic-auth protected admin operations' },
|
{ name: 'Admin', description: 'Basic-auth protected admin operations' },
|
||||||
|
{ name: 'Devices', description: 'Device registration and heartbeat endpoints' },
|
||||||
|
{ name: 'Device Links', description: 'Client-camera authorization links' },
|
||||||
|
{ name: 'Commands', description: 'Realtime command dispatch and status' },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -433,6 +641,10 @@ export function buildOpenApiDocument() {
|
|||||||
type: 'http',
|
type: 'http',
|
||||||
scheme: 'basic',
|
scheme: 'basic',
|
||||||
},
|
},
|
||||||
|
bearerDeviceToken: {
|
||||||
|
type: 'http',
|
||||||
|
scheme: 'bearer',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user