Compare commits

...

3 Commits

Author SHA1 Message Date
seb
0303f8121c u 2026-10-30 02:04:54 +01:00
seb
d151bb5fe3 u 2026-10-30 01:42:19 +01:00
seb
676a40d4fe u 2026-10-29 16:27:02 +01:00
9 changed files with 62 additions and 18 deletions

View File

@@ -235,7 +235,7 @@ _none_
- Returns: `200 { message: str }`
#### Current user — `GET /auth/me`
_none_
- Returns: `200 { user: T32, googleClientId: str }`
- Returns: `200 { user: T32, googleClientId: str, schemaVersion: int }`
#### Set selected company — `PUT /auth/selected-company`
- Params: `body: { companyId: int }`
- Returns: `200 { user: T32 }`

View File

@@ -6,7 +6,7 @@ Current user
Returns the authenticated user from the session cookie and the Google client ID.
`user.picture` is the stored profile image as a base64 string when present.
Unauthenticated calls still receive `googleClientId` with 401.
Unauthenticated calls still receive `googleClientId` and `schemaVersion` with 401.
## Response `200`
@@ -21,7 +21,8 @@ Authenticated
"selectedCompanyId": 1,
"picture": "…"
},
"googleClientId": "…"
"googleClientId": "…",
"schemaVersion": 1
}
```
@@ -29,7 +30,7 @@ Authenticated
| Status | Description |
|--------|-------------|
| 401 | Unauthorized — still includes `googleClientId` for the frontend |
| 401 | Unauthorized — still includes `googleClientId` and `schemaVersion` for the frontend |
---

View File

@@ -40,7 +40,7 @@ emits `{ sender, brandId }`.
| `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 |
| `stock` | `{}` | `POST /stock/adjust` |
| `stock` | `{}` | `POST /stock/adjust`, `POST /company/import` (also `category`, `articles`, `brand`, `attributename`, `warehouse`, `carrier`, `supplier`, `order`, `shipment`, `ledger`) |
| `ledger` | `{}` | `POST /stock/adjust` |
| `attributename` | `{}` | `POST /attributename`, `PUT /attributename/order`, `PATCH /attributename/{id}`, `DELETE /attributename/{id}` |
| `propertyname` | `{}` | `POST /propertyname`, `PUT /propertyname/order`, `PATCH /propertyname/{id}`, `DELETE /propertyname/{id}` |

View File

@@ -1,5 +1,16 @@
import env from '../../config/env.js';
import { getSchemaVersion } from '../../db/store.js';
export function respondWithAuthenticatedUser(res, status, body) {
return res.status(status).json({ ...body, googleClientId: env.googleClientId });
export async function respondWithAuthenticatedUser(res, status, body) {
let schemaVersion = null;
try {
schemaVersion = await getSchemaVersion();
} catch {
// ignore if database not accessible
}
return res.status(status).json({
...body,
googleClientId: env.googleClientId,
schemaVersion,
});
}

View File

@@ -47,7 +47,7 @@ export function clearCachedCompanyUsers(companyId) {
}
}
function respondWithAuthPayload(res, req, status, body) {
async function respondWithAuthPayload(res, req, status, body) {
if (req.path === '/api/auth/me') {
return respondWithAuthenticatedUser(res, status, body);
}
@@ -85,7 +85,7 @@ export const authMiddleware = async (req, res, next) => {
const isDocs = req.path.startsWith('/docs') || req.path.startsWith('/api/docs');
if (!token) {
if (isDocs || isOAuthAuthorize) return next();
return respondWithAuthPayload(res, req, 401, { error: 'Unauthorized' });
return await respondWithAuthPayload(res, req, 401, { error: 'Unauthorized' });
}
try {
@@ -94,7 +94,7 @@ export const authMiddleware = async (req, res, next) => {
if (!Number.isInteger(authId) || authId <= 0) {
if (req.cookies.token) clearTokenCookie(res);
if (isDocs) return next();
return respondWithAuthPayload(res, req, 401, { error: 'Invalid token' });
return await respondWithAuthPayload(res, req, 401, { error: 'Invalid token' });
}
let user = getCachedUser(authId);
@@ -111,7 +111,7 @@ export const authMiddleware = async (req, res, next) => {
clearCachedUser(authId);
if (req.cookies.token) clearTokenCookie(res);
if (isDocs) return next();
return respondWithAuthPayload(res, req, 403, { error: 'User is no longer authorized' });
return await respondWithAuthPayload(res, req, 403, { error: 'User is no longer authorized' });
}
req.user = {
@@ -133,7 +133,7 @@ export const authMiddleware = async (req, res, next) => {
} catch {
if (req.cookies.token) clearTokenCookie(res);
if (isDocs || isOAuthAuthorize) return next();
return respondWithAuthPayload(res, req, 401, { error: 'Invalid token' });
return await respondWithAuthPayload(res, req, 401, { error: 'Invalid token' });
}
};

View File

@@ -17,7 +17,7 @@ export function register(app, basePath = '') {
* description: |
* Returns the authenticated user from the session cookie and the Google client ID.
* `user.picture` is the stored profile image as a base64 string when present.
* Unauthenticated calls still receive `googleClientId` with 401.
* Unauthenticated calls still receive `googleClientId` and `schemaVersion` with 401.
* security:
* - cookieAuth: []
* x-markdown: auth/me.md
@@ -33,8 +33,11 @@ export function register(app, basePath = '') {
* $ref: '#/components/schemas/User'
* googleClientId:
* type: string
* schemaVersion:
* type: integer
* description: Current database schema version
* 401:
* description: Unauthorized — still includes `googleClientId` for the frontend
* description: Unauthorized — still includes `googleClientId` and `schemaVersion` for the frontend
* content:
* application/json:
* schema:
@@ -44,12 +47,15 @@ export function register(app, basePath = '') {
* type: string
* googleClientId:
* type: string
* schemaVersion:
* type: integer
* description: Current database schema version
*/
app.get(`${basePath}/auth/me`, async (req, res) => {
try {
const row = await db.getById(req.user.id);
if (!row) {
return respondWithAuthenticatedUser(res, 403, { error: 'User is no longer authorized' });
return await respondWithAuthenticatedUser(res, 403, { error: 'User is no longer authorized' });
}
const pictureBinary = toPictureBuffer(row.pictureBinary);
@@ -59,10 +65,10 @@ export function register(app, basePath = '') {
selectedCompanyId: row.selectedCompanyId,
...(pictureBinary && { picture: pictureBinary.toString('base64') }),
};
respondWithAuthenticatedUser(res, 200, { user });
await respondWithAuthenticatedUser(res, 200, { user });
} catch (error) {
console.error('Get current user failed:', error);
respondWithAuthenticatedUser(res, 500, { error: 'Failed to load user' });
await respondWithAuthenticatedUser(res, 500, { error: 'Failed to load user' });
}
});
}

View File

@@ -466,6 +466,15 @@ export function register(app, basePath = '') {
// #region POST /company/import
const activeImportsByCompany = new Set();
/** Signals for data the catalog import may create/modify (emitted even after a failed, partially applied run). */
const IMPORT_SIGNALS = ['category', 'articles', 'brand', 'attributename', 'warehouse', 'carrier', 'supplier', 'order', 'shipment', 'stock', 'ledger'];
const emitImportSignals = (companyId) => {
for (const signal of IMPORT_SIGNALS) {
emit(signal, signal === 'brand'
? { companyId, data: { sender: '', brandId: null } }
: { companyId });
}
};
app.post(
`${basePath}/company/import`,
@@ -637,6 +646,7 @@ export function register(app, basePath = '') {
}
} finally {
activeImportsByCompany.delete(companyId);
emitImportSignals(companyId);
}
}
);

View File

@@ -49,7 +49,7 @@ export function register(app, basePath = '') {
* | `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 |
* | `stock` | `{}` | `POST /stock/adjust` |
* | `stock` | `{}` | `POST /stock/adjust`, `POST /company/import` (also `category`, `articles`, `brand`, `attributename`, `warehouse`, `carrier`, `supplier`, `order`, `shipment`, `ledger`) |
* | `ledger` | `{}` | `POST /stock/adjust` |
* | `attributename` | `{}` | `POST /attributename`, `PUT /attributename/order`, `PATCH /attributename/{id}`, `DELETE /attributename/{id}` |
* | `propertyname` | `{}` | `POST /propertyname`, `PUT /propertyname/order`, `PATCH /propertyname/{id}`, `DELETE /propertyname/{id}` |

View File

@@ -10,6 +10,7 @@ const schemaLog = createLogger('schema');
let provider;
let store;
let connecting;
let schemaVersion;
const STORE_NAMESPACES = [
'companyuser',
@@ -149,6 +150,7 @@ export async function getStore() {
provider = createProvider();
try {
await provider.initialize((msg) => schemaLog.info(msg));
schemaVersion = await provider.getSchemaVersion();
} catch (err) {
schemaLog.error('Database schema initialization failed:', err);
throw err;
@@ -168,6 +170,19 @@ export async function getProvider() {
return provider;
}
/**
* Returns the current database schema version.
* @returns {Promise<number>}
*/
export async function getSchemaVersion() {
if (schemaVersion != null) {
return schemaVersion;
}
const prov = await getProvider();
schemaVersion = await prov.getSchemaVersion();
return schemaVersion;
}
export async function ensureConnection() {
await getStore();
}
@@ -177,6 +192,7 @@ export async function closeStore() {
await provider.close();
provider = undefined;
store = undefined;
schemaVersion = undefined;
}
}