Add access control (#65)

This commit is contained in:
Niklas Fondberg
2021-10-11 16:53:18 +02:00
committed by GitHub
parent 1d5d75d354
commit 640849de45
12 changed files with 382 additions and 35 deletions
+65
View File
@@ -0,0 +1,65 @@
import axios from 'axios';
interface TokenResponse {
access_token: string;
expires_in: number;
token_type: string;
}
export default class CognitoClient {
tokenEndpont: string;
clientId: string;
clientSecret: string;
scope: string;
expiresAt: number;
accessToken: string;
constructor(
tokenEndpoint: string,
clientId: string,
clientSecret: string,
scope: string,
) {
this.tokenEndpont = `${tokenEndpoint}/oauth2/token`;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.scope = scope;
this.accessToken = '';
this.expiresAt = 0;
}
private getPostData(): URLSearchParams {
const params = new URLSearchParams();
params.append('grant_type', 'client_credentials');
params.append('client_id', this.clientId);
params.append('scope', this.scope);
return params;
}
public async getToken(): Promise<string> {
const now = Date.now();
if (this.expiresAt < now) {
await this.getNewToken();
}
console.log('Getting cached token');
return this.accessToken;
}
private async getNewToken(): Promise<void> {
console.log('Getting new token');
const authString =
'Basic ' +
Buffer.from(this.clientId + ':' + this.clientSecret).toString('base64');
const res: TokenResponse = await axios
.post(this.tokenEndpont, this.getPostData(), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: authString,
},
})
.then((r) => r.data);
this.accessToken = res.access_token;
this.expiresAt = Date.now() + res.expires_in * 1e3;
}
}
+95
View File
@@ -0,0 +1,95 @@
import axios from 'axios';
import CognitoClient from './cognito-client';
const config = {
PW_GRAPHQL_URL: 'http://docker.for.mac.localhost:4000',
API_TOKEN_ENDPOINT:
'https://photowall-test-admin.auth.eu-west-1.amazoncognito.com',
API_CLIENT_ID: '',
API_CLIENT_SECRET: '',
};
async function doQuery(
query: string,
variables: any,
scopes: string[],
): Promise<any> {
const cognitoClient = new CognitoClient(
config.API_TOKEN_ENDPOINT,
config.API_CLIENT_ID,
config.API_CLIENT_SECRET,
scopes.join(' '),
);
let token = '';
token = await cognitoClient.getToken();
const url = config.PW_GRAPHQL_URL;
const res = await axios({
url: url,
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `Bearer ${token}`,
},
data: JSON.stringify({
query,
variables,
}),
});
return res;
}
async function testGetOrder() {
const query = `
query GetOrder($id: ID!) {
order(id:$id){
id
klarnaOrderId
# paypalTransactionId
customerType
countryCode
}
}
`;
const variables = {
id: '414246',
};
const res = await doQuery(query, variables, [
'https://graphql.photowall.com/orders.read',
]);
console.log(JSON.stringify(res.data, null, 2));
}
async function testGetProduct() {
const query = `
query GetProduct($id: Int!) {
product(id:$id){
id
fields {
artNo
name
}
}
}
`;
const variables = {
id: 52924,
};
const res = await doQuery(query, variables, [
'https://graphql.photowall.com/products.read',
]);
console.log(JSON.stringify(res.data, null, 2));
}
async function main() {
console.log('Starting');
try {
await testGetOrder();
await testGetProduct();
} catch (e) {
console.log(e?.response.data.errors);
}
}
main();
+3 -1
View File
@@ -6,7 +6,9 @@
"scripts": { "scripts": {
"exportSQL": "cd src/__test__/db_generator/ && npm install && node index.js && cd ../../../", "exportSQL": "cd src/__test__/db_generator/ && npm install && node index.js && cd ../../../",
"test": "npm run exportSQL && jest --detectOpenHandles", "test": "npm run exportSQL && jest --detectOpenHandles",
"test:dev": "npm run exportSQL && jest --watch" "test:dev": "npm run exportSQL && jest --watch",
"unit:dev": "jest --watch src/__test__/unit.test.ts",
"test:manual": "cd manual-tests && ts-node test-scopes.ts"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
+98
View File
@@ -0,0 +1,98 @@
import { checkAccess } from '../cognito/access-control';
import {
AccessTokenClaim,
ClaimVerifyResult,
IdTokenClaim,
} from '../cognito/cognito-client';
const idTokenAuth = {
userName: 'niklas.fondberg@photowall.se',
isValid: true,
idToken: {
'custom:adgroups':
'[Domain Users, Images_Write, VPN_Users, WebAdmin, WebAdminSuperUser]',
'cognito:username': 'photowalladfs_photowall\\niklas.fondberg',
token_use: 'id',
email_verified: false,
nonce: 'Qc9kHCXrN----4st19sM',
at_hash: '9x_RMAO3_2NW1cQYkhWEtg',
email: 'niklas.fondberg@photowall.se',
aud: '53og32ofjne7e6i4uf10r3bg2b',
sub: '2646dd8b-f914-40eb-895b-c652976a65e1',
identities: [null],
} as IdTokenClaim,
accessToken: null,
} as ClaimVerifyResult;
const accessTokenAuth = {
isValid: true,
idToken: null,
accessToken: {
token_use: 'access',
username: '',
scope:
'https://graphql.photowall.com/orders.write https://graphql.photowall.com/orders.read',
client_id: '5vftcl43n2jkcdvd9imc270mtu',
} as AccessTokenClaim,
} as ClaimVerifyResult;
describe('GraphQL Apollo unit tests', () => {
describe('Test auth', () => {
it('user test is accepted', () => {
const auth: ClaimVerifyResult = {
userName: 'testuser',
idToken: null,
accessToken: null,
isValid: true,
};
expect(() => checkAccess(['orders.read'], auth)).not.toThrow();
});
});
describe('Test auth not ok for test', () => {
it('user bla is accepted', () => {
const auth: ClaimVerifyResult = {
userName: 'bla',
idToken: null,
accessToken: null,
isValid: true,
};
expect(() => checkAccess(['orders.read'], auth)).toThrow();
});
});
describe('Test access auth', () => {
it('user test is accepted', () => {
const auth = accessTokenAuth;
expect(() =>
checkAccess(['orders.read', 'orders.write'], auth),
).not.toThrow();
});
});
describe('Test access auth not enough scope', () => {
it('user test is accepted', () => {
const auth = accessTokenAuth;
expect(() =>
checkAccess(['products.read', 'orders.read'], auth),
).toThrow();
});
});
describe('Test id auth', () => {
it('user test is accepted', () => {
const auth = idTokenAuth;
expect(() =>
checkAccess(['orders.read', 'orders.write'], auth),
).not.toThrow();
});
});
describe('Test id auth no enough', () => {
it('user test is accepted', () => {
const auth = JSON.parse(JSON.stringify(idTokenAuth)) as ClaimVerifyResult;
auth.idToken['custom:adgroups'] = '[]';
expect(() =>
checkAccess(['orders.read', 'orders.write'], auth),
).toThrow();
});
});
});
+35
View File
@@ -0,0 +1,35 @@
import { AuthenticationError } from 'apollo-server';
import { ClaimVerifyResult } from './cognito-client';
export const RESOURCE = 'https://graphql.photowall.com';
export enum SCOPES {
ORDERS_READ = 'orders.read',
ORDERS_WRITE = 'orders.write',
DESIGNERS_READ = 'designers.read',
DESIGNERS_WRITE = 'designers.write',
PRODUCTS_READ = 'products.read',
PRODUCTS_WRITE = 'products.write',
}
export function checkAccess(scopes: string[], auth: ClaimVerifyResult): void {
if (auth.userName === 'testuser') {
return;
}
if (auth.idToken) {
if (auth.idToken['custom:adgroups'].includes('WebAdmin')) {
return;
}
}
if (auth.accessToken) {
const authScopes = auth.accessToken.scope.split(' ');
const requiredScoped = scopes.map((s) => `${RESOURCE}/${s}`);
const checker = (arr: string[], target: string[]) =>
target.every((v) => arr.includes(v));
if (checker(authScopes, requiredScoped)) {
return;
}
}
throw new AuthenticationError('Not authorized for this endpoint');
}
+2 -2
View File
@@ -49,7 +49,7 @@ interface Claim {
iss: string; iss: string;
} }
interface AccessTokenClaim extends Claim { export interface AccessTokenClaim extends Claim {
token_use: string; token_use: string;
username: string; username: string;
client_id: string; client_id: string;
@@ -57,7 +57,7 @@ interface AccessTokenClaim extends Claim {
scope: string; scope: string;
} }
interface IdTokenClaim extends Claim { export interface IdTokenClaim extends Claim {
'custom:adgroups': string; // 'custom:adgroups': '[Domain Users, Images_Write, VPN_Users, WebAdmin, WebAdminSuperUser]', 'custom:adgroups': string; // 'custom:adgroups': '[Domain Users, Images_Write, VPN_Users, WebAdmin, WebAdminSuperUser]',
'cognito:username': string; 'cognito:username': string;
token_use: string; token_use: string;
+10 -3
View File
@@ -15,7 +15,7 @@ import { MarketLocaleAPI } from './datasources/market-locale-api';
import { ImageServerApi } from './datasources/imageserver-api'; import { ImageServerApi } from './datasources/imageserver-api';
import { InteriorAPI } from './datasources/interior-api'; import { InteriorAPI } from './datasources/interior-api';
import { verifyToken } from './cognito/cognito-client'; import { ClaimVerifyResult, verifyToken } from './cognito/cognito-client';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { InteriorsLambdaAPI } from './datasources/interiors-lambda-api'; import { InteriorsLambdaAPI } from './datasources/interiors-lambda-api';
const path = require('path'); const path = require('path');
@@ -51,7 +51,7 @@ const dataSources = () => ({
const context = async ({ req }) => { const context = async ({ req }) => {
if (process.env.NODE_ENV == 'test') { if (process.env.NODE_ENV == 'test') {
return { auth: 'testuser' }; return { auth: { userName: 'testuser' } };
} }
const authHeader = req.headers.authorization || ''; const authHeader = req.headers.authorization || '';
@@ -65,7 +65,14 @@ const context = async ({ req }) => {
process.env.NODE_ENV === 'development' && process.env.NODE_ENV === 'development' &&
authHeader == 'Basic ZGV2OnB2N1VmYSFiZVAzeGk=' authHeader == 'Basic ZGV2OnB2N1VmYSFiZVAzeGk='
) { ) {
return { auth: 'during development' }; return {
auth: {
userName: 'testuser',
isValid: true,
accessToken: null,
idToken: null,
},
};
} }
throw new AuthenticationError('Not authorized'); throw new AuthenticationError('Not authorized');
}; };
+3 -1
View File
@@ -1,8 +1,10 @@
import { checkAccess } from '../cognito/access-control';
import { CategoryAPI } from '../datasources/category-api'; import { CategoryAPI } from '../datasources/category-api';
import { ProductAPI } from '../datasources/product-api'; import { ProductAPI } from '../datasources/product-api';
const Category = { const Category = {
async products({ id }, _args, { dataSources }) { async products({ id }, _args, { dataSources, auth }) {
checkAccess(['products.read'], auth);
return (<ProductAPI>dataSources.productApi).getCategoryProducts(id); return (<ProductAPI>dataSources.productApi).getCategoryProducts(id);
}, },
}; };
+9 -4
View File
@@ -1,22 +1,27 @@
import { checkAccess } from '../cognito/access-control';
import { DesignerAPI } from '../datasources/designer-api'; import { DesignerAPI } from '../datasources/designer-api';
import { OrderAPI } from '../datasources/order-api'; import { OrderAPI } from '../datasources/order-api';
import { ProductAPI } from '../datasources/product-api'; import { ProductAPI } from '../datasources/product-api';
import { GeneralInput } from '../types/types'; import { GeneralInput } from '../types/types';
const Designer = { const Designer = {
async orderRows({ id }, input: GeneralInput, { dataSources }) { async orderRows({ id }, input: GeneralInput, { dataSources, auth }) {
checkAccess(['orders.read'], auth);
return (<OrderAPI>dataSources.orderApi).getOrderRowsByDesignerId(id, input); return (<OrderAPI>dataSources.orderApi).getOrderRowsByDesignerId(id, input);
}, },
async products({ id }, _args, { dataSources }) { async products({ id }, _args, { dataSources, auth }) {
checkAccess(['products.read'], auth);
return (<ProductAPI>dataSources.productApi).getDesignerProducts(id); return (<ProductAPI>dataSources.productApi).getDesignerProducts(id);
}, },
}; };
async function getDesigners(_, { limit }, { dataSources }) { async function getDesigners(_, { limit }, { dataSources, auth }) {
checkAccess(['designers.read'], auth);
return (<DesignerAPI>dataSources.designerApi).getDesigners(limit); return (<DesignerAPI>dataSources.designerApi).getDesigners(limit);
} }
async function getDesigner(parent, { id }, { dataSources }) { async function getDesigner(_, { id }, { dataSources, auth }) {
checkAccess(['designers.read'], auth);
return (<DesignerAPI>dataSources.designerApi).getDesignerById(id); return (<DesignerAPI>dataSources.designerApi).getDesignerById(id);
} }
+3 -1
View File
@@ -1,8 +1,10 @@
import { checkAccess } from '../cognito/access-control';
import { KeywordAPI } from '../datasources/keyword-api'; import { KeywordAPI } from '../datasources/keyword-api';
import { ProductAPI } from '../datasources/product-api'; import { ProductAPI } from '../datasources/product-api';
const Keyword = { const Keyword = {
async products({ id }, _args, { dataSources }) { async products({ id }, _args, { dataSources, auth }) {
checkAccess(['products.read'], auth);
return (<ProductAPI>dataSources.productApi).getKeywordProducts(id); return (<ProductAPI>dataSources.productApi).getKeywordProducts(id);
}, },
}; };
+7 -3
View File
@@ -1,3 +1,4 @@
import { checkAccess } from '../cognito/access-control';
import { MarketLocaleAPI } from '../datasources/market-locale-api'; import { MarketLocaleAPI } from '../datasources/market-locale-api';
import { OrderAPI } from '../datasources/order-api'; import { OrderAPI } from '../datasources/order-api';
import { OrdersResult } from '../types/order-types'; import { OrdersResult } from '../types/order-types';
@@ -27,7 +28,8 @@ const Order = {
}; };
const OrderRow = { const OrderRow = {
async order(parent, _args, { dataSources }) { async order(parent, _args, { dataSources, auth }) {
checkAccess(['orders.read'], auth);
return (<OrderAPI>dataSources.orderApi).getOrderById(parent.orderId); return (<OrderAPI>dataSources.orderApi).getOrderById(parent.orderId);
}, },
}; };
@@ -35,8 +37,9 @@ const OrderRow = {
async function getOrdersResult( async function getOrdersResult(
_, _,
input: GeneralInput, input: GeneralInput,
{ dataSources }, { dataSources, auth },
): Promise<OrdersResult> { ): Promise<OrdersResult> {
checkAccess(['orders.read'], auth);
const total = (<OrderAPI>dataSources.orderApi).getOrdersTotal(input); const total = (<OrderAPI>dataSources.orderApi).getOrdersTotal(input);
const items = (<OrderAPI>dataSources.orderApi).getOrders(input); const items = (<OrderAPI>dataSources.orderApi).getOrders(input);
return { return {
@@ -49,7 +52,8 @@ async function getOrdersResult(
}; };
} }
async function getOrder(_, { id }, { dataSources }) { async function getOrder(_, { id }, { dataSources, auth }) {
checkAccess(['orders.read'], auth);
return (<OrderAPI>dataSources.orderApi).getOrderById(id); return (<OrderAPI>dataSources.orderApi).getOrderById(id);
} }
+52 -20
View File
@@ -16,6 +16,7 @@ import { InteriorsLambdaAPI } from '../datasources/interiors-lambda-api';
import { IdNumberResult } from '../types/types'; import { IdNumberResult } from '../types/types';
import { moveS3File } from '../s3'; import { moveS3File } from '../s3';
import { DesignerAPI } from '../datasources/designer-api'; import { DesignerAPI } from '../datasources/designer-api';
import { checkAccess } from '../cognito/access-control';
const ProductBlacklist = { const ProductBlacklist = {
async market(parent, _args, { dataSources }) { async market(parent, _args, { dataSources }) {
@@ -30,7 +31,8 @@ const Product = {
async categories({ id }, _args, { dataSources }): Promise<Array<Category>> { async categories({ id }, _args, { dataSources }): Promise<Array<Category>> {
return (<CategoryAPI>dataSources.categoryApi).getProductCategories(id); return (<CategoryAPI>dataSources.categoryApi).getProductCategories(id);
}, },
async designer({ designerId }, _args, { dataSources }) { async designer({ designerId }, _args, { dataSources, auth }) {
checkAccess(['designers.read'], auth);
if (!designerId) { if (!designerId) {
return null; return null;
} }
@@ -53,8 +55,9 @@ const PrintProduct = {
async function getProductsResult( async function getProductsResult(
_, _,
input: ProductsFilterInput, input: ProductsFilterInput,
{ dataSources }, { dataSources, auth },
): Promise<ProductListResult> { ): Promise<ProductListResult> {
checkAccess(['products.read'], auth);
const total = (<ProductAPI>dataSources.productApi).getProductsTotal(input); const total = (<ProductAPI>dataSources.productApi).getProductsTotal(input);
const items = (<ProductAPI>dataSources.productApi).getProducts(input); const items = (<ProductAPI>dataSources.productApi).getProducts(input);
@@ -71,8 +74,9 @@ async function getProductsResult(
async function getProductsSearchResult( async function getProductsSearchResult(
_, _,
{ q }, { q },
{ dataSources }, { dataSources, auth },
): Promise<ProductsSearchResult> { ): Promise<ProductsSearchResult> {
checkAccess(['products.read', 'designers.read'], auth);
const prodApi = <ProductAPI>dataSources.productApi; const prodApi = <ProductAPI>dataSources.productApi;
const catApi = <CategoryAPI>dataSources.categoryApi; const catApi = <CategoryAPI>dataSources.categoryApi;
const keywApi = <KeywordAPI>dataSources.keywordApi; const keywApi = <KeywordAPI>dataSources.keywordApi;
@@ -92,7 +96,7 @@ async function getProductsSearchResult(
}; };
} }
async function getProduct(_, { id }, { dataSources }): Promise<Product> { async function getProduct(_, { id }, { dataSources, auth }): Promise<Product> {
return (<ProductAPI>dataSources.productApi).getProduct(id); return (<ProductAPI>dataSources.productApi).getProduct(id);
} }
@@ -111,14 +115,16 @@ export const productQueryTypeDefs = {
/////////////////// ///////////////////
// Mutations below // Mutations below
export const productMutationTypeDefs = { export const productMutationTypeDefs = {
async addProduct(_, { name, batch }, { dataSources }) { async addProduct(_, { name, batch }, { dataSources, auth }) {
checkAccess(['products.write'], auth);
const id = await (<ProductAPI>dataSources.productApi).addProduct( const id = await (<ProductAPI>dataSources.productApi).addProduct(
name, name,
batch, batch,
); );
return (<ProductAPI>dataSources.productApi).getProduct(id); return (<ProductAPI>dataSources.productApi).getProduct(id);
}, },
async productInfo(_, { productId, info }, { dataSources }) { async productInfo(_, { productId, info }, { dataSources, auth }) {
checkAccess(['products.write'], auth);
await (<ProductAPI>dataSources.productApi).updateProductInfo( await (<ProductAPI>dataSources.productApi).updateProductInfo(
productId, productId,
info, info,
@@ -128,8 +134,9 @@ export const productMutationTypeDefs = {
async productFocusPoint( async productFocusPoint(
_, _,
{ productId, focusXpoint2, focusYpoint2 }, { productId, focusXpoint2, focusYpoint2 },
{ dataSources }, { dataSources, auth },
) { ) {
checkAccess(['products.write'], auth);
await (<ProductAPI>dataSources.productApi).updateFocusPoint( await (<ProductAPI>dataSources.productApi).updateFocusPoint(
productId, productId,
focusXpoint2, focusXpoint2,
@@ -145,14 +152,16 @@ export const productMutationTypeDefs = {
)).generateNewInteriors(product); )).generateNewInteriors(product);
return (<ProductAPI>dataSources.productApi).getProduct(productId); return (<ProductAPI>dataSources.productApi).getProduct(productId);
}, },
async productBlacklisting(_, { productId, markets }, { dataSources }) { async productBlacklisting(_, { productId, markets }, { dataSources, auth }) {
checkAccess(['products.write'], auth);
await (<ProductAPI>dataSources.productApi).updateBlacklisting( await (<ProductAPI>dataSources.productApi).updateBlacklisting(
productId, productId,
markets, markets,
); );
return (<ProductAPI>dataSources.productApi).getProduct(productId); return (<ProductAPI>dataSources.productApi).getProduct(productId);
}, },
async productGroup(_, { productId, groupIds }, { dataSources }) { async productGroup(_, { productId, groupIds }, { dataSources, auth }) {
checkAccess(['products.write'], auth);
await (<ProductAPI>dataSources.productApi).updateGroups( await (<ProductAPI>dataSources.productApi).updateGroups(
productId, productId,
groupIds, groupIds,
@@ -168,8 +177,9 @@ export const productMutationTypeDefs = {
async productGroupInteriors( async productGroupInteriors(
_, _,
{ productId, groupId, uris }, { productId, groupId, uris },
{ dataSources }, { dataSources, auth },
) { ) {
checkAccess(['products.write'], auth);
await (<ProductAPI>dataSources.productApi).addInteriorsToProductGroup( await (<ProductAPI>dataSources.productApi).addInteriorsToProductGroup(
productId, productId,
groupId, groupId,
@@ -177,7 +187,8 @@ export const productMutationTypeDefs = {
); );
return (<ProductAPI>dataSources.productApi).getProduct(productId); return (<ProductAPI>dataSources.productApi).getProduct(productId);
}, },
async productKeywords(_, { productId, keywordIds }, { dataSources }) { async productKeywords(_, { productId, keywordIds }, { dataSources, auth }) {
checkAccess(['products.write'], auth);
await (<KeywordAPI>dataSources.keywordApi).setProductKeywords( await (<KeywordAPI>dataSources.keywordApi).setProductKeywords(
productId, productId,
keywordIds, keywordIds,
@@ -187,8 +198,9 @@ export const productMutationTypeDefs = {
async productProportionsWarning( async productProportionsWarning(
_, _,
{ productId, proportions }, { productId, proportions },
{ dataSources }, { dataSources, auth },
) { ) {
checkAccess(['products.write'], auth);
await (<ProductAPI>dataSources.productApi).setProportionsWarning( await (<ProductAPI>dataSources.productApi).setProportionsWarning(
productId, productId,
proportions, proportions,
@@ -198,8 +210,9 @@ export const productMutationTypeDefs = {
async addOwnInteriorToPrintProduct( async addOwnInteriorToPrintProduct(
_, _,
{ printId, uploadedS3Key }, { printId, uploadedS3Key },
{ dataSources }, { dataSources, auth },
): Promise<IdNumberResult> { ): Promise<IdNumberResult> {
checkAccess(['products.write'], auth);
return (<InteriorAPI>dataSources.interiorApi).addOwnUploadToPrintId( return (<InteriorAPI>dataSources.interiorApi).addOwnUploadToPrintId(
printId, printId,
uploadedS3Key, uploadedS3Key,
@@ -208,8 +221,9 @@ export const productMutationTypeDefs = {
async productWallpaperType( async productWallpaperType(
_, _,
{ productId, wallpaperTypes }, { productId, wallpaperTypes },
{ dataSources }, { dataSources, auth },
) { ) {
checkAccess(['products.write'], auth);
await (<ProductAPI>dataSources.productApi).setWallpaperTypes( await (<ProductAPI>dataSources.productApi).setWallpaperTypes(
productId, productId,
wallpaperTypes, wallpaperTypes,
@@ -219,8 +233,9 @@ export const productMutationTypeDefs = {
async updateProductImage( async updateProductImage(
_, _,
{ productId, uploadedS3Key, width, height }, { productId, uploadedS3Key, width, height },
{ dataSources }, { dataSources, auth },
) { ) {
checkAccess(['products.write'], auth);
await moveS3File( await moveS3File(
CONFIG.uploadBucket, CONFIG.uploadBucket,
uploadedS3Key, uploadedS3Key,
@@ -246,7 +261,12 @@ export const productMutationTypeDefs = {
)).generateNewInteriors(product); )).generateNewInteriors(product);
return 'success'; return 'success';
}, },
async addRelatedProducts(_, { productId, articleNumbers }, { dataSources }) { async addRelatedProducts(
_,
{ productId, articleNumbers },
{ dataSources, auth },
) {
checkAccess(['products.write'], auth);
await (<ProductAPI>dataSources.productApi).addRelatedProducts( await (<ProductAPI>dataSources.productApi).addRelatedProducts(
productId, productId,
articleNumbers, articleNumbers,
@@ -256,8 +276,9 @@ export const productMutationTypeDefs = {
async removeRelatedProducts( async removeRelatedProducts(
_, _,
{ productId, relatedProductIds }, { productId, relatedProductIds },
{ dataSources }, { dataSources, auth },
) { ) {
checkAccess(['products.write'], auth);
await (<ProductAPI>dataSources.productApi).removeRelatedProducts( await (<ProductAPI>dataSources.productApi).removeRelatedProducts(
productId, productId,
relatedProductIds, relatedProductIds,
@@ -265,7 +286,12 @@ export const productMutationTypeDefs = {
return (<ProductAPI>dataSources.productApi).getProduct(productId); return (<ProductAPI>dataSources.productApi).getProduct(productId);
}, },
async addCategoriesToProduct(_, { productId, categoryIds }, { dataSources }) { async addCategoriesToProduct(
_,
{ productId, categoryIds },
{ dataSources, auth },
) {
checkAccess(['products.write'], auth);
await (<ProductAPI>dataSources.productApi).addCategoriesToProduct( await (<ProductAPI>dataSources.productApi).addCategoriesToProduct(
productId, productId,
categoryIds, categoryIds,
@@ -276,15 +302,21 @@ export const productMutationTypeDefs = {
async removeCategoriesFromProduct( async removeCategoriesFromProduct(
_, _,
{ productId, categoryIds }, { productId, categoryIds },
{ dataSources }, { dataSources, auth },
) { ) {
checkAccess(['products.write'], auth);
await (<ProductAPI>dataSources.productApi).removeCategoriesFromProduct( await (<ProductAPI>dataSources.productApi).removeCategoriesFromProduct(
productId, productId,
categoryIds, categoryIds,
); );
return (<ProductAPI>dataSources.productApi).getProduct(productId); return (<ProductAPI>dataSources.productApi).getProduct(productId);
}, },
async updateProductComments(_, { productId, comments }, { dataSources }) { async updateProductComments(
_,
{ productId, comments },
{ dataSources, auth },
) {
checkAccess(['products.write'], auth);
await (<ProductAPI>dataSources.productApi).updateComments( await (<ProductAPI>dataSources.productApi).updateComments(
productId, productId,
comments, comments,