Link articlehistory to companyuser; store Google picture and sub on google_auth
This commit is contained in:
15
README.md
15
README.md
@@ -71,14 +71,17 @@ For `PATCH /api/company/:id`:
|
|||||||
| Method | Signature | Returns |
|
| Method | Signature | Returns |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `add` | `(email)` | `id` |
|
| `add` | `(email)` | `id` |
|
||||||
| `getByEmail` | `(email)` | `{ id, email, createdAt, selectedCompanyId } \| undefined` |
|
| `getByEmail` | `(email)` | `{ id, email, picture, sub, createdAt, selectedCompanyId } \| undefined` |
|
||||||
| `listAllowedEmails` | `()` | `{ id, email, createdAt, selectedCompanyId }[]` |
|
| `listAllowedEmails` | `()` | `{ id, email, picture, sub, createdAt, selectedCompanyId }[]` |
|
||||||
| `removeAllowedEmail` | `(email)` | `void` |
|
| `removeAllowedEmail` | `(email)` | `void` |
|
||||||
| `setSelectedCompany` | `(googleAuthId, companyId)` | `void` |
|
| `setSelectedCompany` | `(googleAuthId, companyId)` | `void` |
|
||||||
|
| `setProfile` | `(googleAuthId, { picture?: string \| null, sub?: string \| null })` | `void` |
|
||||||
|
|
||||||
|
`picture` / `sub` come from the Google ID token (`payload.picture`, `payload.sub`). `sub` is Google’s stable subject id (unique when set). `setProfile` overwrites both columns (pass `null` to clear). Errors: `User not found`.
|
||||||
|
|
||||||
Backend mapping:
|
Backend mapping:
|
||||||
|
|
||||||
- Login: `getByEmail(email)`
|
- Login: `getByEmail(email)` then `setProfile(id, { picture, sub })` from the verified token
|
||||||
- CLI user manager: `listAllowedEmails()` / `removeAllowedEmail(email)` / `add(email)`
|
- CLI user manager: `listAllowedEmails()` / `removeAllowedEmail(email)` / `add(email)`
|
||||||
- `PUT /api/auth/selected-company`: `setSelectedCompany(googleAuthId, companyId)`
|
- `PUT /api/auth/selected-company`: `setSelectedCompany(googleAuthId, companyId)`
|
||||||
|
|
||||||
@@ -267,7 +270,7 @@ Company-scoped POS terminals. Auth via `resolveCompanyAccess`.
|
|||||||
| `listExport` | `(googleAuthId: string, companyId: number, categoryId?: number)` | `ArticleExportEntry[]` (see below) |
|
| `listExport` | `(googleAuthId: string, companyId: number, categoryId?: number)` | `ArticleExportEntry[]` (see below) |
|
||||||
| `createUnderParent` | `({ googleAuthId: string, parentId: number, name: string })` | `{ id: number, parentId: number, name: string, sku: string }` |
|
| `createUnderParent` | `({ googleAuthId: string, parentId: number, name: string })` | `{ id: number, parentId: number, name: string, sku: string }` |
|
||||||
| `getBySku` | `(googleAuthId: string, sku: string)` | `ArticleDetail` (see below) |
|
| `getBySku` | `(googleAuthId: string, sku: string)` | `ArticleDetail` (see below) |
|
||||||
| `getHistoryBySku` | `(googleAuthId: string, sku: string)` | `{ id: number, date: Date, text: string }[]` |
|
| `getHistoryBySku` | `(googleAuthId: string, sku: string)` | `{ id: number, companyuserId: number \| null, date: Date, text: string }[]` |
|
||||||
| `updateBySku` | `(googleAuthId: string, sku: string, { name?: string, unitamount?: number, unitSi?: string })` | `ArticleDetail` |
|
| `updateBySku` | `(googleAuthId: string, sku: string, { name?: string, unitamount?: number, unitSi?: string })` | `ArticleDetail` |
|
||||||
| `setPricesBySku` | `(googleAuthId: string, sku: string, PriceInput[])` | `ArticleDetail` |
|
| `setPricesBySku` | `(googleAuthId: string, sku: string, PriceInput[])` | `ArticleDetail` |
|
||||||
| `setEansBySku` | `(googleAuthId: string, sku: string, string[])` | `ArticleDetail` |
|
| `setEansBySku` | `(googleAuthId: string, sku: string, string[])` | `ArticleDetail` |
|
||||||
@@ -381,9 +384,9 @@ type PriceEntry = {
|
|||||||
|
|
||||||
Auth via the article's `categoryarticle` category + `resolveCategoryAccess`. Errors: `Article not found`, `Not authorized`.
|
Auth via the article's `categoryarticle` category + `resolveCategoryAccess`. Errors: `Article not found`, `Not authorized`.
|
||||||
|
|
||||||
`updateBySku` patches article fields in a transaction. At least one of `name`, `unitamount`, `unitSi` required. `name` → `articletext` (slugified `slug`); empty name → `Name required`. `unitSi` → existing `unit.si` lookup (no silent ensure); unknown → `Unit not found`. `unitamount` must be finite and `> 0` → `Invalid unitamount`. When `name` and/or unit (`unitamount`/`unitSi`) actually change, inserts one `articlehistory` row in the same transaction (`text` lines joined by newline), e.g. `Name geändert von "Alt" auf "Neu"` / `Einheit geändert von "500g" auf "1kg"` (label = `unitamount` + `unittext.symbol`, fallback `unit.si`). Returns `ArticleDetail`. Errors: `Article not found`, `Not authorized`, `Name required`, `Unit not found`, `Invalid unitamount`.
|
`updateBySku` patches article fields in a transaction. At least one of `name`, `unitamount`, `unitSi` required. `name` → `articletext` (slugified `slug`); empty name → `Name required`. `unitSi` → existing `unit.si` lookup (no silent ensure); unknown → `Unit not found`. `unitamount` must be finite and `> 0` → `Invalid unitamount`. When `name` and/or unit (`unitamount`/`unitSi`) actually change, inserts one `articlehistory` row in the same transaction with `companyuser_id` set to the auth user (`text` lines joined by newline), e.g. `Name geändert von "Alt" auf "Neu"` / `Einheit geändert von "500g" auf "1kg"` (label = `unitamount` + `unittext.symbol`, fallback `unit.si`). Returns `ArticleDetail`. Errors: `Article not found`, `Not authorized`, `Name required`, `Unit not found`, `Invalid unitamount`.
|
||||||
|
|
||||||
`getHistoryBySku` returns `articlehistory` for the article, newest first (`date` DESC, `id` DESC). Auth like `getBySku`. Errors: `Article not found`, `Not authorized`.
|
`getHistoryBySku` returns `articlehistory` for the article, newest first (`date` DESC, `id` DESC). `companyuserId` is null when the user was deleted (`ON DELETE SET NULL`). Auth like `getBySku`. Errors: `Article not found`, `Not authorized`.
|
||||||
|
|
||||||
`setPricesBySku` replaces all `price` rows for the article’s company `poscustomergroup_id` in one transaction. Each entry: `price` → `net`, `tax` → tax rate (ensured via `tax.ensure`), `minmultiplier` defaults to `1`. Empty array clears all POS prices. Returns `ArticleDetail`. Errors: `Article not found`, `Not authorized`, `POS customer group required`, `Invalid prices`.
|
`setPricesBySku` replaces all `price` rows for the article’s company `poscustomergroup_id` in one transaction. Each entry: `price` → `net`, `tax` → tax rate (ensured via `tax.ensure`), `minmultiplier` defaults to `1`. Empty array clears all POS prices. Returns `ArticleDetail`. Errors: `Article not found`, `Not authorized`, `POS customer group required`, `Invalid prices`.
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
2302
schema-graph.svg
2302
schema-graph.svg
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 160 KiB |
@@ -271,6 +271,7 @@ const MENU = {
|
|||||||
{ value: 'google_auth.listAllowedEmails', label: 'listAllowedEmails', hint: '→ rows' },
|
{ value: 'google_auth.listAllowedEmails', label: 'listAllowedEmails', hint: '→ rows' },
|
||||||
{ value: 'google_auth.removeAllowedEmail', label: 'removeAllowedEmail', hint: 'email' },
|
{ value: 'google_auth.removeAllowedEmail', label: 'removeAllowedEmail', hint: 'email' },
|
||||||
{ value: 'google_auth.setSelectedCompany', label: 'setSelectedCompany', hint: 'googleAuthId, companyId' },
|
{ value: 'google_auth.setSelectedCompany', label: 'setSelectedCompany', hint: 'googleAuthId, companyId' },
|
||||||
|
{ value: 'google_auth.setProfile', label: 'setProfile', hint: 'googleAuthId, picture, sub' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
@@ -1125,6 +1126,19 @@ async function runMethod(client, action) {
|
|||||||
await client.google_auth.setSelectedCompany(Number(id), Number(companyId));
|
await client.google_auth.setSelectedCompany(Number(id), Number(companyId));
|
||||||
return ['ok', true];
|
return ['ok', true];
|
||||||
}
|
}
|
||||||
|
case 'google_auth.setProfile': {
|
||||||
|
const id = await askInt('Auth id');
|
||||||
|
if (id == null) return;
|
||||||
|
const picture = await askText('picture URL', { placeholder: '(empty = null)' });
|
||||||
|
if (picture == null) return;
|
||||||
|
const sub = await askText('Google sub', { placeholder: '(empty = null)' });
|
||||||
|
if (sub == null) return;
|
||||||
|
await client.google_auth.setProfile(Number(id), {
|
||||||
|
picture: picture.trim() || null,
|
||||||
|
sub: sub.trim() || null,
|
||||||
|
});
|
||||||
|
return ['ok', true];
|
||||||
|
}
|
||||||
case 'initialize': {
|
case 'initialize': {
|
||||||
await client.initialize();
|
await client.initialize();
|
||||||
return ['ok', true];
|
return ['ok', true];
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ import googleAuthGetByEmail from './store/google_auth/getByEmail.js';
|
|||||||
import googleAuthListAllowedEmails from './store/google_auth/listAllowedEmails.js';
|
import googleAuthListAllowedEmails from './store/google_auth/listAllowedEmails.js';
|
||||||
import googleAuthRemoveAllowedEmail from './store/google_auth/removeAllowedEmail.js';
|
import googleAuthRemoveAllowedEmail from './store/google_auth/removeAllowedEmail.js';
|
||||||
import googleAuthSetSelectedCompany from './store/google_auth/setSelectedCompany.js';
|
import googleAuthSetSelectedCompany from './store/google_auth/setSelectedCompany.js';
|
||||||
|
import googleAuthSetProfile from './store/google_auth/setProfile.js';
|
||||||
import picGetImageByHash from './store/pic/getImageByHash.js';
|
import picGetImageByHash from './store/pic/getImageByHash.js';
|
||||||
import picGetThumbByHash from './store/pic/getThumbByHash.js';
|
import picGetThumbByHash from './store/pic/getThumbByHash.js';
|
||||||
import storePurge from './store/purge.js';
|
import storePurge from './store/purge.js';
|
||||||
@@ -164,6 +165,7 @@ export default class LibDashStore {
|
|||||||
listAllowedEmails: googleAuthListAllowedEmails(pool),
|
listAllowedEmails: googleAuthListAllowedEmails(pool),
|
||||||
removeAllowedEmail: googleAuthRemoveAllowedEmail(pool),
|
removeAllowedEmail: googleAuthRemoveAllowedEmail(pool),
|
||||||
setSelectedCompany: googleAuthSetSelectedCompany(pool),
|
setSelectedCompany: googleAuthSetSelectedCompany(pool),
|
||||||
|
setProfile: googleAuthSetProfile(pool),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.pic = {
|
this.pic = {
|
||||||
|
|||||||
@@ -21,11 +21,14 @@ BEGIN
|
|||||||
CREATE TABLE google_auth (
|
CREATE TABLE google_auth (
|
||||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
email NVARCHAR(320) NOT NULL,
|
email NVARCHAR(320) NOT NULL,
|
||||||
|
picture NVARCHAR(2048) NULL,
|
||||||
|
sub NVARCHAR(255) NULL,
|
||||||
created_at DATETIME2 NOT NULL CONSTRAINT DF_google_auth_created_at DEFAULT SYSUTCDATETIME(),
|
created_at DATETIME2 NOT NULL CONSTRAINT DF_google_auth_created_at DEFAULT SYSUTCDATETIME(),
|
||||||
selected_company INT NULL,
|
selected_company INT NULL,
|
||||||
CONSTRAINT UQ_google_auth_email UNIQUE (email),
|
CONSTRAINT UQ_google_auth_email UNIQUE (email),
|
||||||
CONSTRAINT FK_google_auth_selected_company FOREIGN KEY (selected_company) REFERENCES company(id) ON DELETE SET NULL
|
CONSTRAINT FK_google_auth_selected_company FOREIGN KEY (selected_company) REFERENCES company(id) ON DELETE SET NULL
|
||||||
);
|
);
|
||||||
|
CREATE UNIQUE INDEX UQ_google_auth_sub ON google_auth(sub) WHERE sub IS NOT NULL;
|
||||||
END
|
END
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -260,9 +263,11 @@ BEGIN
|
|||||||
CREATE TABLE articlehistory (
|
CREATE TABLE articlehistory (
|
||||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
article_id INT NOT NULL,
|
article_id INT NOT NULL,
|
||||||
|
companyuser_id INT NULL,
|
||||||
[date] DATETIME2 NOT NULL CONSTRAINT DF_articlehistory_date DEFAULT SYSUTCDATETIME(),
|
[date] DATETIME2 NOT NULL CONSTRAINT DF_articlehistory_date DEFAULT SYSUTCDATETIME(),
|
||||||
[text] NVARCHAR(MAX) NOT NULL,
|
[text] NVARCHAR(MAX) NOT NULL,
|
||||||
CONSTRAINT FK_articlehistory_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE
|
CONSTRAINT FK_articlehistory_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT FK_articlehistory_companyuser FOREIGN KEY (companyuser_id) REFERENCES companyuser(id) ON DELETE SET NULL
|
||||||
);
|
);
|
||||||
END
|
END
|
||||||
`;
|
`;
|
||||||
@@ -615,7 +620,6 @@ export const SCHEMA_SCRIPTS = [
|
|||||||
INIT_ARTICLELIST,
|
INIT_ARTICLELIST,
|
||||||
INIT_BRAND,
|
INIT_BRAND,
|
||||||
INIT_ARTICLE,
|
INIT_ARTICLE,
|
||||||
INIT_ARTICLEHISTORY,
|
|
||||||
INIT_ARTICLEEAN,
|
INIT_ARTICLEEAN,
|
||||||
INIT_ARTICLEPIC,
|
INIT_ARTICLEPIC,
|
||||||
INIT_BRANDPIC,
|
INIT_BRANDPIC,
|
||||||
@@ -631,6 +635,7 @@ export const SCHEMA_SCRIPTS = [
|
|||||||
INIT_NUMBERS,
|
INIT_NUMBERS,
|
||||||
INIT_GET_NEXT_NUMBER_WITH_PREFIX_AND_PADDING,
|
INIT_GET_NEXT_NUMBER_WITH_PREFIX_AND_PADDING,
|
||||||
INIT_COMPANYUSER,
|
INIT_COMPANYUSER,
|
||||||
|
INIT_ARTICLEHISTORY,
|
||||||
INIT_SYNCPOS,
|
INIT_SYNCPOS,
|
||||||
INIT_COMPANYSETTING,
|
INIT_COMPANYSETTING,
|
||||||
INIT_POSTERMINAL,
|
INIT_POSTERMINAL,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
export const INSERT = `
|
export const INSERT = `
|
||||||
INSERT INTO articlehistory (article_id, [text])
|
INSERT INTO articlehistory (article_id, companyuser_id, [text])
|
||||||
VALUES (@articleId, @text)
|
VALUES (@articleId, @companyuserId, @text)
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const LIST_BY_ARTICLE_ID = `
|
export const LIST_BY_ARTICLE_ID = `
|
||||||
SELECT id, [date], [text]
|
SELECT id, companyuser_id AS companyuserId, [date], [text]
|
||||||
FROM articlehistory
|
FROM articlehistory
|
||||||
WHERE article_id = @articleId
|
WHERE article_id = @articleId
|
||||||
ORDER BY [date] DESC, id DESC
|
ORDER BY [date] DESC, id DESC
|
||||||
|
|||||||
@@ -5,13 +5,25 @@ VALUES (@email)
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const GET_BY_EMAIL = `
|
export const GET_BY_EMAIL = `
|
||||||
SELECT id, email, created_at AS createdAt, selected_company AS selectedCompanyId
|
SELECT
|
||||||
|
id,
|
||||||
|
email,
|
||||||
|
picture,
|
||||||
|
sub,
|
||||||
|
created_at AS createdAt,
|
||||||
|
selected_company AS selectedCompanyId
|
||||||
FROM google_auth
|
FROM google_auth
|
||||||
WHERE email = @email
|
WHERE email = @email
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const LIST_ALLOWED_EMAILS = `
|
export const LIST_ALLOWED_EMAILS = `
|
||||||
SELECT id, email, created_at AS createdAt, selected_company AS selectedCompanyId
|
SELECT
|
||||||
|
id,
|
||||||
|
email,
|
||||||
|
picture,
|
||||||
|
sub,
|
||||||
|
created_at AS createdAt,
|
||||||
|
selected_company AS selectedCompanyId
|
||||||
FROM google_auth
|
FROM google_auth
|
||||||
ORDER BY email
|
ORDER BY email
|
||||||
`;
|
`;
|
||||||
@@ -26,3 +38,9 @@ UPDATE google_auth
|
|||||||
SET selected_company = @companyId
|
SET selected_company = @companyId
|
||||||
WHERE id = @googleAuthId
|
WHERE id = @googleAuthId
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const SET_PROFILE = `
|
||||||
|
UPDATE google_auth
|
||||||
|
SET picture = @picture, sub = @sub
|
||||||
|
WHERE id = @googleAuthId
|
||||||
|
`;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export default function getHistoryBySku(pool) {
|
|||||||
|
|
||||||
return result.recordset.map((row) => ({
|
return result.recordset.map((row) => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
|
companyuserId: row.companyuserId ?? null,
|
||||||
date: row.date,
|
date: row.date,
|
||||||
text: row.text,
|
text: row.text,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export default function resolveArticleBySku(pool) {
|
|||||||
companyId: row.companyId,
|
companyId: row.companyId,
|
||||||
rootCategoryId: row.rootCategoryId,
|
rootCategoryId: row.rootCategoryId,
|
||||||
posCustomerGroupId: row.posCustomerGroupId ?? null,
|
posCustomerGroupId: row.posCustomerGroupId ?? null,
|
||||||
|
companyuserId: row.companyuserId,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ export default function updateBySku(pool) {
|
|||||||
throw new Error('Invalid unitamount');
|
throw new Error('Invalid unitamount');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { articleId, languageId, companyId } = await resolve(googleAuthId, sku);
|
const { articleId, languageId, companyId, companyuserId } = await resolve(
|
||||||
|
googleAuthId,
|
||||||
|
sku,
|
||||||
|
);
|
||||||
|
|
||||||
const currentResult = await pool
|
const currentResult = await pool
|
||||||
.request()
|
.request()
|
||||||
@@ -134,6 +137,7 @@ export default function updateBySku(pool) {
|
|||||||
if (historyLines.length) {
|
if (historyLines.length) {
|
||||||
await request(pool, tx)
|
await request(pool, tx)
|
||||||
.input('articleId', articleId)
|
.input('articleId', articleId)
|
||||||
|
.input('companyuserId', companyuserId)
|
||||||
.input('text', historyLines.join('\n'))
|
.input('text', historyLines.join('\n'))
|
||||||
.query(INSERT_HISTORY);
|
.query(INSERT_HISTORY);
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/store/google_auth/setProfile.js
Normal file
16
src/store/google_auth/setProfile.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { SET_PROFILE } from '../../sql/google_auth.js';
|
||||||
|
|
||||||
|
export default function setProfile(pool) {
|
||||||
|
return async (googleAuthId, { picture = null, sub = null } = {}) => {
|
||||||
|
const result = await pool
|
||||||
|
.request()
|
||||||
|
.input('googleAuthId', googleAuthId)
|
||||||
|
.input('picture', picture)
|
||||||
|
.input('sub', sub)
|
||||||
|
.query(SET_PROFILE);
|
||||||
|
|
||||||
|
if (!result.rowsAffected[0]) {
|
||||||
|
throw new Error('User not found');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user