u
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import * as db from '../../../db/googleAuth.js';
|
||||
import { clearCachedUser } from '../../middleware/auth.js';
|
||||
import { setUserCompany } from '../../sse.js';
|
||||
|
||||
/**
|
||||
* Registers the selected-company route (requires auth middleware).
|
||||
@@ -17,6 +18,7 @@ export function register(app, basePath = '') {
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* x-markdown: auth/selected-company.md
|
||||
* x-signals: [company]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
@@ -76,6 +78,7 @@ export function register(app, basePath = '') {
|
||||
selectedCompanyId: updated.selectedCompanyId,
|
||||
};
|
||||
|
||||
setUserCompany(req.user.id, updated.selectedCompanyId);
|
||||
res.status(200).json({ user });
|
||||
} catch (error) {
|
||||
console.error('Set selected company failed:', error);
|
||||
|
||||
@@ -5,6 +5,7 @@ import JSZip from 'jszip';
|
||||
import * as db from '../../db/company.js';
|
||||
import * as googleAuth from '../../db/googleAuth.js';
|
||||
import { clearCachedUser } from '../middleware/auth.js';
|
||||
import { clearDeletedCompany, emit, setUserCompany } from '../sse.js';
|
||||
import { initCompanyVfs } from '../../tools/vfs.js';
|
||||
import { executeCompanyImport } from '../../services/companyImport.js';
|
||||
import { executeCompanyExport } from '../../services/companyExport.js';
|
||||
@@ -137,6 +138,7 @@ export function register(app, basePath = '') {
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* x-markdown: company/create.md
|
||||
* x-signals: [company]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
@@ -215,6 +217,7 @@ export function register(app, basePath = '') {
|
||||
console.error('Failed to initialize company VFS:', vfsErr);
|
||||
}
|
||||
const authRow = await googleAuth.getAllowedEmail(req.user.email);
|
||||
setUserCompany(req.user.id, authRow.selectedCompanyId);
|
||||
res.status(201).json({ company, user: userPayload(authRow) });
|
||||
} catch (error) {
|
||||
const status = companyErrorStatus(error);
|
||||
@@ -834,6 +837,7 @@ export function register(app, basePath = '') {
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* x-markdown: company/update.md
|
||||
* x-signals: [company]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
@@ -891,6 +895,7 @@ export function register(app, basePath = '') {
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* x-markdown: company/delete.md
|
||||
* x-signals: [company]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
@@ -934,6 +939,7 @@ export function register(app, basePath = '') {
|
||||
|
||||
try {
|
||||
const company = await db.updateCompany(parsedId.companyId, body);
|
||||
emit('company', { companyId: parsedId.companyId, data: { companyId: parsedId.companyId } });
|
||||
res.status(200).json({ company });
|
||||
} catch (error) {
|
||||
const status = companyErrorStatus(error);
|
||||
@@ -954,6 +960,7 @@ export function register(app, basePath = '') {
|
||||
|
||||
try {
|
||||
await db.deleteCompany(parsedId.companyId);
|
||||
clearDeletedCompany(parsedId.companyId);
|
||||
res.sendStatus(204);
|
||||
} catch (error) {
|
||||
console.error('Delete company failed:', error);
|
||||
|
||||
@@ -13,32 +13,35 @@ export function register(app, basePath = '') {
|
||||
* tags: [Events]
|
||||
* summary: Subscribe to update signals (SSE)
|
||||
* description: |
|
||||
* Opens a Server-Sent Events stream for the authenticated user's
|
||||
* **selected company**. Clients receive named signals when another
|
||||
* request successfully mutates shared data for that company
|
||||
* (including the same browser's other tabs).
|
||||
* Opens a Server-Sent Events stream for the authenticated user.
|
||||
* Can be opened before a company is selected (`selectedCompanyId == null`);
|
||||
* when a company is selected or changed, the stream receives a `company`
|
||||
* event (`{ companyId }`) and updates its company scope in-place.
|
||||
* Clients also receive named signals when another request successfully
|
||||
* mutates shared data for the active company (including the same
|
||||
* browser's other tabs).
|
||||
*
|
||||
* Use `EventSource` with credentials so the session cookie is sent:
|
||||
*
|
||||
* ```js
|
||||
* const es = new EventSource('https://…/api/events', { withCredentials: true });
|
||||
* es.addEventListener('company', () => { refetchCompany(); });
|
||||
* es.addEventListener('category', () => { refetchCategoryTree(); });
|
||||
* es.addEventListener('articles', () => { refetchArticleIndex(); });
|
||||
* es.addEventListener('article-42000045', () => { refetchArticle('42000045'); });
|
||||
* ```
|
||||
*
|
||||
* Keep-alive comments (`: heartbeat`) are sent roughly every 20s.
|
||||
* Reconnect after changing the selected company (the stream is bound
|
||||
* to the company at connect time).
|
||||
*
|
||||
* ## Signals
|
||||
*
|
||||
* Every SSE event uses the event name as the signal id; payloads are
|
||||
* JSON and most events send an empty object `{}`. Only `brand` emits a
|
||||
* richer payload.
|
||||
* JSON and most events send an empty object `{}`. `company` emits
|
||||
* `{ companyId }` and `brand` emits `{ sender, brandId }`.
|
||||
*
|
||||
* | Signal | Payload | Emitted after successful |
|
||||
* |--------|---------|--------------------------|
|
||||
* | `company` | `{ companyId }` | `PUT /auth/selected-company`, `POST /company`, `PUT /company/{id}`, `DELETE /company/{id}` |
|
||||
* | `category` | `{}` | `POST /category`, `PATCH /category/{id}`, `PUT /category/order`, `DELETE /category/{id}`, `POST /category/{id}/image`, `DELETE /category/{id}/image`, `PUT /category/{id}/property` |
|
||||
* | `articles` | `{}` | `POST /article`, `PATCH /article/{sku}`, `DELETE /article/{sku}`, `PUT /article/{sku}/articles`-style list-level changes, `POST /category/{id}/article` and related article-assigning category mutations |
|
||||
* | `article-<sku>` | `{}` | `PUT /article/{sku}/weight`, `PUT /article/{sku}/prices`, `PUT /article/{sku}/eans`, `PUT /article/{sku}/categories`, `PUT /article/{sku}/brand`, `PUT /article/{sku}/images/order`, `POST /article/{sku}/image`, `DELETE /article/{sku}/image/{fullHash}`, supplier/article-property mutations affecting the article |
|
||||
@@ -67,6 +70,8 @@ export function register(app, basePath = '') {
|
||||
* - cookieAuth: []
|
||||
* x-markdown: events.md
|
||||
* x-sse-events:
|
||||
* - name: company
|
||||
* payload: "{ companyId }"
|
||||
* - name: category
|
||||
* payload: "{}"
|
||||
* - name: articles
|
||||
@@ -100,12 +105,16 @@ export function register(app, basePath = '') {
|
||||
* description: |
|
||||
* SSE stream (`text/event-stream`). Each signal is an SSE event
|
||||
* whose name is the signal id. Most signals send `{}` as data;
|
||||
* `brand` sends `{ "sender": "<x-client-id>", "brandId": 12 }`.
|
||||
* `company` sends `{ "companyId": 1 }` and `brand` sends
|
||||
* `{ "sender": "<x-client-id>", "brandId": 12 }`.
|
||||
* content:
|
||||
* text/event-stream:
|
||||
* schema:
|
||||
* type: string
|
||||
* example: |
|
||||
* event: company
|
||||
* data: {"companyId":1}
|
||||
*
|
||||
* event: category
|
||||
* data: {}
|
||||
*
|
||||
@@ -117,12 +126,6 @@ export function register(app, basePath = '') {
|
||||
*
|
||||
* event: brand
|
||||
* data: {"sender":"tab-1","brandId":12}
|
||||
* 400:
|
||||
* description: No company selected
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* content:
|
||||
@@ -138,10 +141,7 @@ export function register(app, basePath = '') {
|
||||
}
|
||||
|
||||
export function handleSse(req, res) {
|
||||
const companyId = req.user?.selectedCompanyId;
|
||||
if (companyId == null) {
|
||||
return res.status(400).json({ error: 'No company selected' });
|
||||
}
|
||||
const companyId = req.user?.selectedCompanyId ?? null;
|
||||
|
||||
req.socket?.setTimeout(0);
|
||||
res.setTimeout?.(0);
|
||||
|
||||
Reference in New Issue
Block a user