Compare commits
3 Commits
005b48ff66
...
0303f8121c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0303f8121c | ||
|
|
d151bb5fe3 | ||
|
|
676a40d4fe |
@@ -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 }`
|
||||
|
||||
@@ -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 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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}` |
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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}` |
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user