From 3a33cc2a702e85d7af9a7427097e5c1c14ffd68e Mon Sep 17 00:00:00 2001 From: Mikael Araya Date: Mon, 19 Aug 2024 23:40:17 +0300 Subject: [PATCH 01/16] Rebase user deletion branch on updated master --- changes.diff | 449 ++++++++++++++++++ examples/kitchensink/boot.ts | 3 + package-lock.json | 14 + packages/api/src/resolvers/mutations/index.ts | 2 + .../mutations/users/deleteAccount.ts | 11 + packages/api/src/schema/mutation.ts | 1 + .../module/configureOrderDeliveriesModule.ts | 7 +- .../module/configureOrderDiscountsModule.ts | 4 + .../module/configureOrderPaymentsModule.ts | 10 +- .../module/configureOrderPositionsModule.ts | 5 + .../module/configureOrdersModule-mutations.ts | 5 + .../module/configureProductReviewsModule.ts | 1 - .../core-quotations/src/quotations-index.ts | 2 + .../src/module/configureUsersModule.ts | 53 ++- packages/core-users/src/users-settings.ts | 2 + packages/core/src/core-index.ts | 1 + 16 files changed, 562 insertions(+), 8 deletions(-) create mode 100644 changes.diff create mode 100644 packages/api/src/resolvers/mutations/users/deleteAccount.ts diff --git a/changes.diff b/changes.diff new file mode 100644 index 0000000000..c79852d4d3 --- /dev/null +++ b/changes.diff @@ -0,0 +1,449 @@ +diff --git a/examples/kitchensink/boot.ts b/examples/kitchensink/boot.ts +index 4569499c1..ccb117fd8 100644 +--- a/examples/kitchensink/boot.ts ++++ b/examples/kitchensink/boot.ts +@@ -35,6 +35,9 @@ const start = async () => { + }), + ], + options: { ++ users: { ++ enableRightToBeForgotten: true, ++ }, + payment: { + filterSupportedProviders: async ({ providers }) => { + return providers.sort((left, right) => { +diff --git a/packages/api/src/resolvers/mutations/index.ts b/packages/api/src/resolvers/mutations/index.ts +index 623457583..79eea4b6b 100755 +--- a/packages/api/src/resolvers/mutations/index.ts ++++ b/packages/api/src/resolvers/mutations/index.ts +@@ -153,6 +153,7 @@ import prepareUserAvatarUpload from './users/prepareUserAvatarUpload.js'; + import rejectOrder from './orders/rejectOrder.js'; + import removePushSubscription from './users/removePushSubscription.js'; + import addPushSubscription from './users/addPushSubscription.js'; ++import deleteAccount from './users/deleteAccount.js'; + + export default { + logout: acl(actions.logout)(logout), +@@ -318,4 +319,5 @@ export default { + signPaymentProviderForCheckout: acl(actions.registerPaymentCredentials)( + signPaymentProviderForCheckout, + ), ++ deleteAccount, + }; +diff --git a/packages/api/src/resolvers/mutations/users/deleteAccount.ts b/packages/api/src/resolvers/mutations/users/deleteAccount.ts +new file mode 100644 +index 000000000..4badfe509 +--- /dev/null ++++ b/packages/api/src/resolvers/mutations/users/deleteAccount.ts +@@ -0,0 +1,11 @@ ++import { Context } from '@unchainedshop/types/api.js'; ++import { log } from '@unchainedshop/logger'; ++ ++const deleteAccount = async (_, { userId }, context: Context) => { ++ const { modules, userAgent } = context; ++ log(`mutation deleteAccount ${userId} ${userAgent}`, { userId }); ++ await modules.users.deleteAccount({ userId }, context); ++ return true; ++}; ++ ++export default deleteAccount; +diff --git a/packages/api/src/schema/mutation.ts b/packages/api/src/schema/mutation.ts +index 86555f398..2e6f27878 100644 +--- a/packages/api/src/schema/mutation.ts ++++ b/packages/api/src/schema/mutation.ts +@@ -884,6 +884,7 @@ export default [ + Remove user W3C push subscription object + """ + removePushSubscription(p256dh: String!): User! ++ deleteAccount(userId: ID): Boolean! + } + `, + ]; +diff --git a/packages/core-orders/src/module/configureOrderDeliveriesModule.ts b/packages/core-orders/src/module/configureOrderDeliveriesModule.ts +index 8c1f082b3..37630d3f8 100644 +--- a/packages/core-orders/src/module/configureOrderDeliveriesModule.ts ++++ b/packages/core-orders/src/module/configureOrderDeliveriesModule.ts +@@ -232,5 +232,12 @@ export const configureOrderDeliveriesModule = ({ + }, + ); + }, ++ deleteUserOrderDeliveriesByOrderIds: async (orderIds) => { ++ log(`OrderDelivery -> Delete User order deliveries`, { ++ orderIds, ++ }); ++ const deleteUserOrdersResult = await OrderDeliveries.deleteMany({ orderId: { $in: orderIds } }); ++ return deleteUserOrdersResult.deletedCount; ++ }, + }; + }; +diff --git a/packages/core-orders/src/module/configureOrderDiscountsModule.ts b/packages/core-orders/src/module/configureOrderDiscountsModule.ts +index a92a038a9..818d82e2d 100644 +--- a/packages/core-orders/src/module/configureOrderDiscountsModule.ts ++++ b/packages/core-orders/src/module/configureOrderDiscountsModule.ts +@@ -231,5 +231,12 @@ export const configureOrderDiscountsModule = ({ + await emit('ORDER_UPDATE_DISCOUNT', { discount }); + return discount; + }, ++ deleteUserOrderDiscountsByOrderIds: async (orderIds) => { ++ log(`OrderDiscounts -> Delete User orders discount`, { ++ orderIds, ++ }); ++ const deleteUserOrdersResult = await OrderDiscounts.deleteMany({ orderId: { $in: orderIds } }); ++ return deleteUserOrdersResult.deletedCount; ++ }, + }; + }; +diff --git a/packages/core-orders/src/module/configureOrderPaymentsModule.ts b/packages/core-orders/src/module/configureOrderPaymentsModule.ts +index 763d81ae8..33f9e40c9 100644 +--- a/packages/core-orders/src/module/configureOrderPaymentsModule.ts ++++ b/packages/core-orders/src/module/configureOrderPaymentsModule.ts +@@ -24,9 +24,9 @@ export const buildFindByContextDataSelector = (context: any): mongodb.Filter + context[key] !== undefined + ? { +- ...currentSelector, +- [`context.${key}`]: context[key], +- } ++ ...currentSelector, ++ [`context.${key}`]: context[key], ++ } + : currentSelector, + {}, + ); +@@ -352,5 +352,12 @@ export const configureOrderPaymentsModule = ({ + }, + ); + }, ++ deleteUserOrderPaymentsByOrderIds: async (orderIds) => { ++ log(`OrderPayment -> Delete User orders payment`, { ++ orderIds, ++ }); ++ const deleteUserOrdersResult = await OrderPayments.deleteMany({ orderId: { $in: orderIds } }); ++ return deleteUserOrdersResult.deletedCount; ++ }, + }; + }; +diff --git a/packages/core-orders/src/module/configureOrderPositionsModule.ts b/packages/core-orders/src/module/configureOrderPositionsModule.ts +index 6455c9336..fadbfddd7 100644 +--- a/packages/core-orders/src/module/configureOrderPositionsModule.ts ++++ b/packages/core-orders/src/module/configureOrderPositionsModule.ts +@@ -84,8 +84,7 @@ export const configureOrderPositionsModule = ({ + const originalProductId = originalProduct ? originalProduct._id : undefined; + + log( +- `Create ${quantity}x Position with Product ${productId} ${ +- quotationId ? ` (${quotationId})` : '' ++ `Create ${quantity}x Position with Product ${productId} ${quotationId ? ` (${quotationId})` : '' + }`, + { orderId, productId, originalProductId }, + ); +@@ -372,5 +371,12 @@ export const configureOrderPositionsModule = ({ + + return upsertedOrderPosition; + }, ++ deleteUserOrderPositionsByOrderIds: async (orderIds) => { ++ log(`OrderPosition -> Delete User orders`, { ++ orderIds, ++ }); ++ const deleteUserOrdersResult = await OrderPositions.deleteMany({ orderId: { $in: orderIds } }); ++ return deleteUserOrdersResult.deletedCount; ++ }, + }; + }; +diff --git a/packages/core-orders/src/module/configureOrdersModule-mutations.ts b/packages/core-orders/src/module/configureOrdersModule-mutations.ts +index 7ed47720e..acca8370e 100644 +--- a/packages/core-orders/src/module/configureOrdersModule-mutations.ts ++++ b/packages/core-orders/src/module/configureOrdersModule-mutations.ts +@@ -236,7 +236,13 @@ export const configureOrderModuleMutations = ({ + } + return null; + }, +- + updateCalculation, ++ deleteUserOrders: async (userId) => { ++ log(`OrderPosition -> Delete User orders`, { ++ userId, ++ }); ++ const deletedUserOrdersResult = await Orders.deleteMany({ userId }); ++ return deletedUserOrdersResult.deletedCount; ++ }, + }; + }; +diff --git a/packages/core-products/src/module/configureProductReviewsModule.ts b/packages/core-products/src/module/configureProductReviewsModule.ts +index 84f7e9bf3..92469c54c 100644 +--- a/packages/core-products/src/module/configureProductReviewsModule.ts ++++ b/packages/core-products/src/module/configureProductReviewsModule.ts +@@ -170,7 +170,6 @@ export const configureProductReviewsModule = async ({ + + return productReview; + }, +- + votes: { + userIdsThatVoted, + +diff --git a/packages/core-quotations/src/quotations-index.ts b/packages/core-quotations/src/quotations-index.ts +index 9a60f6a01..2a4c4aea6 100644 +--- a/packages/core-quotations/src/quotations-index.ts ++++ b/packages/core-quotations/src/quotations-index.ts +@@ -1,7 +1,6 @@ + export { configureQuotationsModule } from './module/configureQuotationsModule.js'; + + export { QuotationStatus } from './db/QuotationStatus.js'; +- + export { QuotationAdapter } from './director/QuotationAdapter.js'; + export { QuotationDirector } from './director/QuotationDirector.js'; + +diff --git a/packages/core-users/src/module/configureUsersModule.ts b/packages/core-users/src/module/configureUsersModule.ts +index 592cc96e0..f799e1dca 100644 +--- a/packages/core-users/src/module/configureUsersModule.ts ++++ b/packages/core-users/src/module/configureUsersModule.ts +@@ -1,7 +1,6 @@ + import localePkg from 'locale'; + import bcrypt from 'bcryptjs'; + import { Address, Contact } from '@unchainedshop/types/common.js'; +-import { ModuleInput, UnchainedCore } from '@unchainedshop/types/core.js'; + import { + User, + UserQuery, +@@ -10,7 +9,9 @@ import { + UserProfile, + UserSettingsOptions, + UserData, ++ UsersModule, + } from '@unchainedshop/types/user.js'; ++import { ModuleInput, UnchainedCore } from '@unchainedshop/types/core.js'; + import { emit, registerEvents } from '@unchainedshop/events'; + import { + generateDbFilterById, +@@ -19,6 +20,7 @@ import { + generateDbObjectId, + } from '@unchainedshop/mongodb'; + import { systemLocale } from '@unchainedshop/utils'; ++import crypto from 'crypto'; + import { FileDirector } from '@unchainedshop/file-upload'; + import { SortDirection, SortOption } from '@unchainedshop/types/api.js'; + import { UsersCollection } from '../db/UsersCollection.js'; +@@ -28,6 +30,38 @@ import { configureUsersWebAuthnModule } from './configureUsersWebAuthnModule.js' + import * as pbkdf2 from './pbkdf2.js'; + import * as sha256 from './sha256.js'; + ++const isDate = (value) => { ++ const date = new Date(value); ++ return !Number.isNaN(date.getTime()); ++}; ++ ++function maskString(value) { ++ if (isDate(value)) return value; ++ return crypto ++ .createHash('sha256') ++ .update(JSON.stringify([value, new Date().getTime()])) ++ .digest('hex'); ++} ++ ++const maskUserPropertyValues = (user) => { ++ if (typeof user !== 'object' || user === null) { ++ return user; ++ } ++ if (Array.isArray(user)) { ++ return user.map((item) => maskUserPropertyValues(item)); ++ } ++ const maskedUser = {}; ++ Object.keys(user).forEach((key) => { ++ if (typeof user[key] === 'string' || isDate(user[key])) { ++ maskedUser[key] = maskString(user[key]); ++ } else { ++ maskedUser[key] = maskUserPropertyValues(user[key]); ++ } ++ }); ++ ++ return maskedUser; ++}; ++ + const { Locale } = localePkg; + + const USER_EVENTS = [ +@@ -72,9 +106,7 @@ export const configureUsersModule = async ({ + db, + options, + migrationRepository, +-}: ModuleInput) => { +- userSettings.configureSettings(options || {}, db); +- ++}: ModuleInput): Promise => { + registerEvents(USER_EVENTS); + const Users = await UsersCollection(db); + +@@ -753,5 +785,16 @@ export const configureUsersModule = async ({ + {}, + ); + }, ++ deleteAccount: async ({ userId }, context) => { ++ if (!options?.enableRightToBeForgotten) throw Error('Right to be forgotten is disabled'); ++ const { modules } = context; ++ const { _id, ...user } = await modules.users.findUserById(userId); ++ delete user?.services; ++ ++ const maskedUserData = maskUserPropertyValues({ ...user, meta: null }); ++ await modules.bookmarks.deleteByUserId(userId); ++ await modules.users.updateUser({ _id }, { $set: { ...maskedUserData, deleted: new Date() } }, {}); ++ return true; ++ }, + }; + }; +diff --git a/packages/core/src/core-index.ts b/packages/core/src/core-index.ts +index c375ac78a..4211ba194 100644 +--- a/packages/core/src/core-index.ts ++++ b/packages/core/src/core-index.ts +@@ -96,6 +96,7 @@ export const initCore = async ({ + }); + const users = await configureUsersModule({ + db, ++ options: options.users, + migrationRepository, + }); + const warehousing = await configureWarehousingModule({ +diff --git a/packages/types/enrollments.ts b/packages/types/enrollments.ts +index ebbf5829a..b50e83597 100644 +--- a/packages/types/enrollments.ts ++++ b/packages/types/enrollments.ts +@@ -216,7 +216,6 @@ export type IEnrollmentDirector = IBaseDirector & { + /* + * Settings + */ +- + export interface EnrollmentsSettingsOptions { + autoSchedulingSchedule?: WorkerSchedule; + enrollmentNumberHashFn?: (enrollment: Enrollment, index: number) => string; +diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts +index b59d8d5a9..275c8811c 100644 +--- a/packages/types/index.d.ts ++++ b/packages/types/index.d.ts +@@ -88,6 +88,9 @@ import { + QuotationsSettingsOptions, + QuotationStatus as QuotationStatusType, + } from './quotations.js'; ++ ++import { UserServices, UserSettingsOptions, UsersModule } from './user.js'; ++ + import { + IWarehousingAdapter, + IWarehousingDirector, +@@ -347,6 +350,12 @@ declare module '@unchainedshop/core-quotations' { + const QuotationError: typeof QuotationErrorType; + } + ++declare module '@unchainedshop/core-users' { ++ function configureUsersModule(params: ModuleInput): Promise; ++ ++ const userServices: UserServices; ++} ++ + declare module '@unchainedshop/core-warehousing' { + function configureWarehousingModule( + params: ModuleInput, +diff --git a/packages/types/modules.ts b/packages/types/modules.ts +index 94bfb6386..db9d7d151 100644 +--- a/packages/types/modules.ts ++++ b/packages/types/modules.ts +@@ -13,7 +13,7 @@ import { OrdersModule, OrdersSettingsOptions } from './orders.js'; + import { PaymentModule, PaymentSettingsOptions } from './payments.js'; + import { ProductsModule, ProductsSettingsOptions } from './products.js'; + import { QuotationsModule, QuotationsSettingsOptions } from './quotations.js'; +-import { UsersModule, UserSettingsOptions } from './user.js'; ++import { UserSettingsOptions, UsersModule } from './user.js'; + import { WarehousingModule } from './warehousing.js'; + import { WorkerModule, WorkerSettingsOptions } from './worker.js'; + +diff --git a/packages/types/orders.deliveries.ts b/packages/types/orders.deliveries.ts +index d97a9efcc..e37e72713 100644 +--- a/packages/types/orders.deliveries.ts ++++ b/packages/types/orders.deliveries.ts +@@ -72,6 +72,7 @@ export type OrderDeliveriesModule = { + orderDelivery: OrderDelivery, + unchainedAPI: UnchainedCore, + ) => Promise; ++ deleteUserOrderDeliveriesByOrderIds: (orderIds: string[]) => Promise; + }; + + export type OrderDeliveryDiscount = Omit & { +diff --git a/packages/types/orders.discounts.ts b/packages/types/orders.discounts.ts +index 84254d316..458800db5 100644 +--- a/packages/types/orders.discounts.ts ++++ b/packages/types/orders.discounts.ts +@@ -52,4 +52,5 @@ export type OrderDiscountsModule = { + create: (doc: OrderDiscount) => Promise; + update: (orderDiscountId: string, doc: OrderDiscount) => Promise; + delete: (orderDiscountId: string, unchainedAPI: UnchainedCore) => Promise; ++ deleteUserOrderDiscountsByOrderIds: (orderIds: string[]) => Promise; + }; +diff --git a/packages/types/orders.payments.ts b/packages/types/orders.payments.ts +index c9e87dc88..0ee16eed9 100644 +--- a/packages/types/orders.payments.ts ++++ b/packages/types/orders.payments.ts +@@ -111,6 +111,7 @@ export type OrderPaymentsModule = { + ) => Promise; + + updateCalculation: (orderPayment: OrderPayment, unchainedAPI: UnchainedCore) => Promise; ++ deleteUserOrderPaymentsByOrderIds: (orderIds: string[]) => Promise; + }; + + export type OrderPaymentDiscount = Omit & { +diff --git a/packages/types/orders.positions.ts b/packages/types/orders.positions.ts +index e4331eb5a..7ef121d86 100644 +--- a/packages/types/orders.positions.ts ++++ b/packages/types/orders.positions.ts +@@ -91,6 +91,7 @@ export type OrderPositionsModule = { + params: { order: Order; product: Product }, + unchainedAPI: UnchainedCore, + ) => Promise; ++ deleteUserOrderPositionsByOrderIds: (orderIds: string[]) => Promise; + }; + + export type OrderPositionDiscount = Omit & { +diff --git a/packages/types/orders.ts b/packages/types/orders.ts +index 9502b83b7..0e67a689a 100644 +--- a/packages/types/orders.ts ++++ b/packages/types/orders.ts +@@ -147,6 +147,7 @@ export interface OrderMutations { + updateContact: (orderId: string, contact: Contact, unchainedAPI: UnchainedCore) => Promise; + updateContext: (orderId: string, context: any, unchainedAPI: UnchainedCore) => Promise; + updateCalculation: (orderId: string, unchainedAPI: UnchainedCore) => Promise; ++ deleteUserOrders: (userId: string) => Promise; + } + + export type OrdersModule = OrderQueries & +diff --git a/packages/types/quotations.ts b/packages/types/quotations.ts +index f1125d4ca..4b8b0a5a6 100644 +--- a/packages/types/quotations.ts ++++ b/packages/types/quotations.ts +@@ -154,7 +154,6 @@ export type IQuotationDirector = IBaseDirector & { + /* + * Settings + */ +- + export interface QuotationsSettingsOptions { + quotationNumberHashFn?: (quotation: Quotation, index: number) => string; + } +diff --git a/packages/types/user.ts b/packages/types/user.ts +index 3e4884c27..70a9203aa 100644 +--- a/packages/types/user.ts ++++ b/packages/types/user.ts +@@ -110,6 +110,7 @@ export interface UserSettingsOptions { + validateUsername?: (username: string) => Promise; + validateNewUser?: (user: Partial) => Promise; + validatePassword?: (password: string) => Promise; ++ enableRightToBeForgotten?: boolean; + } + export interface UserSettings { + mergeUserCartsOnLogin: boolean; +@@ -215,6 +216,10 @@ export type UsersModule = { + }, + ) => Promise; + removePushSubscription: (userId: string, p256dh: string) => Promise; ++ deleteAccount: (params: { userId?: string }, context: UnchainedCore) => Promise; ++ hashPassword(password: string): Promise<{ ++ pbkdf2: string; ++ }>; + }; + + /* diff --git a/examples/kitchensink/boot.ts b/examples/kitchensink/boot.ts index 470ce247b9..d72a873568 100644 --- a/examples/kitchensink/boot.ts +++ b/examples/kitchensink/boot.ts @@ -47,6 +47,9 @@ const start = async () => { }), ], options: { + users: { + enableRightToBeForgotten: true, + }, payment: { filterSupportedProviders: async ({ providers }) => { return providers.sort((left, right) => { diff --git a/package-lock.json b/package-lock.json index 66a40bf882..ea27838096 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8113,6 +8113,20 @@ "node": ">=10" } }, + "node_modules/googleapis/node_modules/gcp-metadata": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.1.tgz", + "integrity": "sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/googleapis/node_modules/google-auth-library": { "version": "7.14.1", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.14.1.tgz", diff --git a/packages/api/src/resolvers/mutations/index.ts b/packages/api/src/resolvers/mutations/index.ts index 4a846d6295..1049754e83 100755 --- a/packages/api/src/resolvers/mutations/index.ts +++ b/packages/api/src/resolvers/mutations/index.ts @@ -150,6 +150,7 @@ import prepareUserAvatarUpload from './users/prepareUserAvatarUpload.js'; import rejectOrder from './orders/rejectOrder.js'; import removePushSubscription from './users/removePushSubscription.js'; import addPushSubscription from './users/addPushSubscription.js'; +import deleteAccount from './users/deleteAccount.js'; export default { logout: acl(actions.logout)(logout), @@ -312,4 +313,5 @@ export default { signPaymentProviderForCheckout: acl(actions.registerPaymentCredentials)( signPaymentProviderForCheckout, ), + deleteAccount, }; diff --git a/packages/api/src/resolvers/mutations/users/deleteAccount.ts b/packages/api/src/resolvers/mutations/users/deleteAccount.ts new file mode 100644 index 0000000000..6ed42e7654 --- /dev/null +++ b/packages/api/src/resolvers/mutations/users/deleteAccount.ts @@ -0,0 +1,11 @@ +import { log } from '@unchainedshop/logger'; +import { Context } from '../../../types.js'; + +const deleteAccount = async (_, { userId }, context: Context) => { + const { modules, userAgent } = context; + log(`mutation deleteAccount ${userId} ${userAgent}`, { userId }); + await modules.users.deleteAccount({ userId }, context); + return true; +}; + +export default deleteAccount; diff --git a/packages/api/src/schema/mutation.ts b/packages/api/src/schema/mutation.ts index 35febd8c2e..231a395282 100644 --- a/packages/api/src/schema/mutation.ts +++ b/packages/api/src/schema/mutation.ts @@ -867,6 +867,7 @@ export default [ Remove user W3C push subscription object """ removePushSubscription(p256dh: String!): User! + deleteAccount(userId: ID): Boolean! } `, ]; diff --git a/packages/core-orders/src/module/configureOrderDeliveriesModule.ts b/packages/core-orders/src/module/configureOrderDeliveriesModule.ts index 7633f83913..1c3359dfe1 100644 --- a/packages/core-orders/src/module/configureOrderDeliveriesModule.ts +++ b/packages/core-orders/src/module/configureOrderDeliveriesModule.ts @@ -1,9 +1,10 @@ -import { mongodb, generateDbFilterById, generateDbObjectId } from '@unchainedshop/mongodb'; +import { mongodb, generateDbFilterById, generateDbMutations } from '@unchainedshop/mongodb'; import { emit, registerEvents } from '@unchainedshop/events'; import { Order, OrderDelivery, OrderDeliveryStatus, OrderDiscount } from '../types.js'; import { type DeliveryLocation, type IDeliveryPricingSheet } from '@unchainedshop/core-delivery'; import { DeliveryDirector } from '@unchainedshop/core-delivery'; // TODO: Important import { OrderPricingDiscount } from '../director/OrderPricingDirector.js'; +import { ModuleMutations, UnchainedCore } from '@unchainedshop/core'; export type OrderDeliveriesModule = { // Queries @@ -270,5 +271,9 @@ export const configureOrderDeliveriesModule = ({ }, ); }, + deleteUserOrderDeliveriesByOrderIds: async (orderIds) => { + const deleteUserOrdersResult = await OrderDeliveries.deleteMany({ orderId: { $in: orderIds } }); + return deleteUserOrdersResult.deletedCount; + }, }; }; diff --git a/packages/core-orders/src/module/configureOrderDiscountsModule.ts b/packages/core-orders/src/module/configureOrderDiscountsModule.ts index 76715eed5a..2ca54e302c 100644 --- a/packages/core-orders/src/module/configureOrderDiscountsModule.ts +++ b/packages/core-orders/src/module/configureOrderDiscountsModule.ts @@ -251,5 +251,9 @@ export const configureOrderDiscountsModule = ({ await emit('ORDER_UPDATE_DISCOUNT', { discount }); return discount; }, + deleteUserOrderDiscountsByOrderIds: async (orderIds) => { + const deleteUserOrdersResult = await OrderDiscounts.deleteMany({ orderId: { $in: orderIds } }); + return deleteUserOrdersResult.deletedCount; + }, }; }; diff --git a/packages/core-orders/src/module/configureOrderPaymentsModule.ts b/packages/core-orders/src/module/configureOrderPaymentsModule.ts index 5e1705f540..54cc55e0fa 100644 --- a/packages/core-orders/src/module/configureOrderPaymentsModule.ts +++ b/packages/core-orders/src/module/configureOrderPaymentsModule.ts @@ -96,9 +96,9 @@ export const buildFindByContextDataSelector = (context: any): mongodb.Filter context[key] !== undefined ? { - ...currentSelector, - [`context.${key}`]: context[key], - } + ...currentSelector, + [`context.${key}`]: context[key], + } : currentSelector, {}, ); @@ -409,5 +409,9 @@ export const configureOrderPaymentsModule = ({ }, ); }, + deleteUserOrderPaymentsByOrderIds: async (orderIds) => { + const deleteUserOrdersResult = await OrderPayments.deleteMany({ orderId: { $in: orderIds } }); + return deleteUserOrdersResult.deletedCount; + }, }; }; diff --git a/packages/core-orders/src/module/configureOrderPositionsModule.ts b/packages/core-orders/src/module/configureOrderPositionsModule.ts index 8300fdaaa6..0920de53f7 100644 --- a/packages/core-orders/src/module/configureOrderPositionsModule.ts +++ b/packages/core-orders/src/module/configureOrderPositionsModule.ts @@ -61,6 +61,7 @@ export type OrderPositionsModule = { params: { order: Order; product: Product }, unchainedAPI, ) => Promise; + deleteUserOrderPositionsByOrderIds: (orderIds: string[]) => Promise; }; const ORDER_POSITION_EVENTS: string[] = [ @@ -349,5 +350,9 @@ export const configureOrderPositionsModule = ({ return upsertedOrderPosition; }, + deleteUserOrderPositionsByOrderIds: async (orderIds) => { + const deleteUserOrdersResult = await OrderPositions.deleteMany({ orderId: { $in: orderIds } }); + return deleteUserOrdersResult.deletedCount; + }, }; }; diff --git a/packages/core-orders/src/module/configureOrdersModule-mutations.ts b/packages/core-orders/src/module/configureOrdersModule-mutations.ts index 8f86b46361..958b9406b7 100644 --- a/packages/core-orders/src/module/configureOrdersModule-mutations.ts +++ b/packages/core-orders/src/module/configureOrdersModule-mutations.ts @@ -31,6 +31,7 @@ export interface OrderMutations { updateContact: (orderId: string, contact: Contact) => Promise; updateContext: (orderId: string, context: any) => Promise; updateCalculationSheet: (orderId: string, calculation) => Promise; + deleteUserOrders: (userId: string) => Promise; } const ORDER_EVENTS: string[] = [ @@ -250,5 +251,9 @@ export const configureOrderModuleMutations = ({ } return null; }, + deleteUserOrders: async (userId) => { + const deletedUserOrdersResult = await Orders.deleteMany({ userId }); + return deletedUserOrdersResult.deletedCount; + }, }; }; diff --git a/packages/core-products/src/module/configureProductReviewsModule.ts b/packages/core-products/src/module/configureProductReviewsModule.ts index b39e8e91e6..b7154a0f26 100644 --- a/packages/core-products/src/module/configureProductReviewsModule.ts +++ b/packages/core-products/src/module/configureProductReviewsModule.ts @@ -220,7 +220,6 @@ export const configureProductReviewsModule = async ({ return productReview; }, - votes: { userIdsThatVoted, diff --git a/packages/core-quotations/src/quotations-index.ts b/packages/core-quotations/src/quotations-index.ts index 2b14d5a42f..36da78cfb6 100644 --- a/packages/core-quotations/src/quotations-index.ts +++ b/packages/core-quotations/src/quotations-index.ts @@ -8,3 +8,5 @@ export { QuotationAdapter } from './director/QuotationAdapter.js'; export { QuotationDirector } from './director/QuotationDirector.js'; export { QuotationError } from './director/QuotationError.js'; + +export { configureQuotationsModule } from './module/configureQuotationsModule.js'; diff --git a/packages/core-users/src/module/configureUsersModule.ts b/packages/core-users/src/module/configureUsersModule.ts index 346ae432cd..8992f4583e 100644 --- a/packages/core-users/src/module/configureUsersModule.ts +++ b/packages/core-users/src/module/configureUsersModule.ts @@ -15,6 +15,40 @@ import { userSettings, UserSettingsOptions } from '../users-settings.js'; import { configureUsersWebAuthnModule, UsersWebAuthnModule } from './configureUsersWebAuthnModule.js'; import * as pbkdf2 from './pbkdf2.js'; import * as sha256 from './sha256.js'; +import type { Address, Contact } from '@unchainedshop/mongodb'; +import crypto from 'crypto'; + +const isDate = (value) => { + const date = new Date(value); + return !Number.isNaN(date.getTime()); +}; + +function maskString(value) { + if (isDate(value)) return value; + return crypto + .createHash('sha256') + .update(JSON.stringify([value, new Date().getTime()])) + .digest('hex'); +} + +const maskUserPropertyValues = (user) => { + if (typeof user !== 'object' || user === null) { + return user; + } + if (Array.isArray(user)) { + return user.map((item) => maskUserPropertyValues(item)); + } + const maskedUser = {}; + Object.keys(user).forEach((key) => { + if (typeof user[key] === 'string' || isDate(user[key])) { + maskedUser[key] = maskString(user[key]); + } else { + maskedUser[key] = maskUserPropertyValues(user[key]); + } + }); + + return maskedUser; +}; export type UsersModule = { // Submodules @@ -88,6 +122,10 @@ export type UsersModule = { }, ) => Promise; removePushSubscription: (userId: string, p256dh: string) => Promise; + deleteAccount: (params: { userId?: string }, context: UnchainedCore) => Promise; + hashPassword(password: string): Promise<{ + pbkdf2: string; + }>; }; const USER_EVENTS = [ @@ -126,9 +164,7 @@ export const configureUsersModule = async ({ db, options, migrationRepository, -}: ModuleInput) => { - userSettings.configureSettings(options || {}, db); - +}: ModuleInput): Promise => { registerEvents(USER_EVENTS); const Users = await UsersCollection(db); @@ -812,5 +848,16 @@ export const configureUsersModule = async ({ {}, ); }, + deleteAccount: async ({ userId }, context) => { + if (!options?.enableRightToBeForgotten) throw Error('Right to be forgotten is disabled'); + const { modules } = context; + const { _id, ...user } = await modules.users.findUserById(userId); + delete user?.services; + + const maskedUserData = maskUserPropertyValues({ ...user, meta: null }); + await modules.bookmarks.deleteByUserId(userId); + await modules.users.updateUser({ _id }, { $set: { ...maskedUserData, deleted: new Date() } }, {}); + return true; + }, }; }; diff --git a/packages/core-users/src/users-settings.ts b/packages/core-users/src/users-settings.ts index 604b65a6c9..ddde3bc0a0 100644 --- a/packages/core-users/src/users-settings.ts +++ b/packages/core-users/src/users-settings.ts @@ -4,6 +4,7 @@ import { mongodb } from '@unchainedshop/mongodb'; export interface UserSettingsOptions { mergeUserCartsOnLogin?: boolean; autoMessagingAfterUserCreation?: boolean; + enableRightToBeForgotten?: boolean; validateEmail?: (email: string) => Promise; validateUsername?: (username: string) => Promise; validateNewUser?: (user: Partial) => Promise; @@ -12,6 +13,7 @@ export interface UserSettingsOptions { export interface UserSettings { mergeUserCartsOnLogin: boolean; autoMessagingAfterUserCreation: boolean; + enableRightToBeForgotten?: boolean; validateEmail: (email: string) => Promise; validateUsername: (username: string) => Promise; validateNewUser: (user: Partial) => Promise; diff --git a/packages/core/src/core-index.ts b/packages/core/src/core-index.ts index 3c8c888c02..7d8269fefd 100644 --- a/packages/core/src/core-index.ts +++ b/packages/core/src/core-index.ts @@ -185,6 +185,7 @@ export const initCore = async ({ }); const users = await configureUsersModule({ db, + options: options.users, migrationRepository, }); const warehousing = await configureWarehousingModule({ From 963cdeeca59e834a67c6f40c8100909ecf98031a Mon Sep 17 00:00:00 2001 From: Mikael Araya Date: Tue, 20 Aug 2024 20:58:09 +0300 Subject: [PATCH 02/16] Enable right to forget by default and allow deletion of account to self or admin --- examples/kitchensink/boot.ts | 3 --- packages/api/src/resolvers/mutations/index.ts | 2 +- packages/api/src/resolvers/mutations/users/deleteAccount.ts | 4 ++-- packages/api/src/roles/index.ts | 1 + packages/api/src/roles/loggedIn.ts | 1 + .../core-orders/src/module/configureOrderPaymentsModule.ts | 6 +++--- packages/core-users/src/module/configureUsersModule.ts | 1 - packages/core-users/src/users-settings.ts | 2 -- 8 files changed, 8 insertions(+), 12 deletions(-) diff --git a/examples/kitchensink/boot.ts b/examples/kitchensink/boot.ts index d72a873568..470ce247b9 100644 --- a/examples/kitchensink/boot.ts +++ b/examples/kitchensink/boot.ts @@ -47,9 +47,6 @@ const start = async () => { }), ], options: { - users: { - enableRightToBeForgotten: true, - }, payment: { filterSupportedProviders: async ({ providers }) => { return providers.sort((left, right) => { diff --git a/packages/api/src/resolvers/mutations/index.ts b/packages/api/src/resolvers/mutations/index.ts index 1049754e83..553df26245 100755 --- a/packages/api/src/resolvers/mutations/index.ts +++ b/packages/api/src/resolvers/mutations/index.ts @@ -313,5 +313,5 @@ export default { signPaymentProviderForCheckout: acl(actions.registerPaymentCredentials)( signPaymentProviderForCheckout, ), - deleteAccount, + deleteAccount: acl(actions.deleteAccount)(deleteAccount), }; diff --git a/packages/api/src/resolvers/mutations/users/deleteAccount.ts b/packages/api/src/resolvers/mutations/users/deleteAccount.ts index 6ed42e7654..522089640d 100644 --- a/packages/api/src/resolvers/mutations/users/deleteAccount.ts +++ b/packages/api/src/resolvers/mutations/users/deleteAccount.ts @@ -2,9 +2,9 @@ import { log } from '@unchainedshop/logger'; import { Context } from '../../../types.js'; const deleteAccount = async (_, { userId }, context: Context) => { - const { modules, userAgent } = context; + const { modules, userAgent, userId: currentUserId } = context; log(`mutation deleteAccount ${userId} ${userAgent}`, { userId }); - await modules.users.deleteAccount({ userId }, context); + await modules.users.deleteAccount({ userId: userId || currentUserId }, context); return true; }; diff --git a/packages/api/src/roles/index.ts b/packages/api/src/roles/index.ts index 4fca6b6e0d..cd83e1208e 100644 --- a/packages/api/src/roles/index.ts +++ b/packages/api/src/roles/index.ts @@ -113,6 +113,7 @@ const actions: Record = [ 'heartbeat', 'confirmMediaUpload', 'viewStatistics', + 'deleteAccount', ].reduce((oldValue, actionValue) => { const newValue = oldValue; newValue[actionValue] = actionValue; diff --git a/packages/api/src/roles/loggedIn.ts b/packages/api/src/roles/loggedIn.ts index abc5a7bb24..08c93bfb1f 100644 --- a/packages/api/src/roles/loggedIn.ts +++ b/packages/api/src/roles/loggedIn.ts @@ -202,6 +202,7 @@ export const loggedIn = (role: any, actions: Record) => { role.allow(actions.viewUserProductReviews, isMyself); role.allow(actions.viewUserTokens, isMyself); role.allow(actions.updateUser, isMyself); + role.allow(actions.deleteAccount, isMyself); role.allow(actions.sendEmail, isOwnedEmailAddress); role.allow(actions.viewOrder, isOwnedOrder); role.allow(actions.updateOrder, isOwnedOrder); diff --git a/packages/core-orders/src/module/configureOrderPaymentsModule.ts b/packages/core-orders/src/module/configureOrderPaymentsModule.ts index 54cc55e0fa..a8b64442c8 100644 --- a/packages/core-orders/src/module/configureOrderPaymentsModule.ts +++ b/packages/core-orders/src/module/configureOrderPaymentsModule.ts @@ -96,9 +96,9 @@ export const buildFindByContextDataSelector = (context: any): mongodb.Filter context[key] !== undefined ? { - ...currentSelector, - [`context.${key}`]: context[key], - } + ...currentSelector, + [`context.${key}`]: context[key], + } : currentSelector, {}, ); diff --git a/packages/core-users/src/module/configureUsersModule.ts b/packages/core-users/src/module/configureUsersModule.ts index 8992f4583e..04a100f84f 100644 --- a/packages/core-users/src/module/configureUsersModule.ts +++ b/packages/core-users/src/module/configureUsersModule.ts @@ -849,7 +849,6 @@ export const configureUsersModule = async ({ ); }, deleteAccount: async ({ userId }, context) => { - if (!options?.enableRightToBeForgotten) throw Error('Right to be forgotten is disabled'); const { modules } = context; const { _id, ...user } = await modules.users.findUserById(userId); delete user?.services; diff --git a/packages/core-users/src/users-settings.ts b/packages/core-users/src/users-settings.ts index ddde3bc0a0..604b65a6c9 100644 --- a/packages/core-users/src/users-settings.ts +++ b/packages/core-users/src/users-settings.ts @@ -4,7 +4,6 @@ import { mongodb } from '@unchainedshop/mongodb'; export interface UserSettingsOptions { mergeUserCartsOnLogin?: boolean; autoMessagingAfterUserCreation?: boolean; - enableRightToBeForgotten?: boolean; validateEmail?: (email: string) => Promise; validateUsername?: (username: string) => Promise; validateNewUser?: (user: Partial) => Promise; @@ -13,7 +12,6 @@ export interface UserSettingsOptions { export interface UserSettings { mergeUserCartsOnLogin: boolean; autoMessagingAfterUserCreation: boolean; - enableRightToBeForgotten?: boolean; validateEmail: (email: string) => Promise; validateUsername: (username: string) => Promise; validateNewUser: (user: Partial) => Promise; From 429157830289a3885228a847cd301de8547fba2f Mon Sep 17 00:00:00 2001 From: Mikael Araya Date: Tue, 20 Aug 2024 21:13:28 +0300 Subject: [PATCH 03/16] Validate user ids --- .../api/src/resolvers/mutations/users/deleteAccount.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/api/src/resolvers/mutations/users/deleteAccount.ts b/packages/api/src/resolvers/mutations/users/deleteAccount.ts index 522089640d..3b31c7705f 100644 --- a/packages/api/src/resolvers/mutations/users/deleteAccount.ts +++ b/packages/api/src/resolvers/mutations/users/deleteAccount.ts @@ -1,10 +1,16 @@ import { log } from '@unchainedshop/logger'; import { Context } from '../../../types.js'; +import { InvalidIdError, UserNotFoundError } from '../../../errors.js'; const deleteAccount = async (_, { userId }, context: Context) => { const { modules, userAgent, userId: currentUserId } = context; log(`mutation deleteAccount ${userId} ${userAgent}`, { userId }); - await modules.users.deleteAccount({ userId: userId || currentUserId }, context); + const normalizedUserId = userId || currentUserId; + if (!normalizedUserId) throw new InvalidIdError({ userId: normalizedUserId }); + if (!(await modules.users.userExists({ userId: normalizedUserId }))) + throw new UserNotFoundError({ userId: normalizedUserId }); + + await modules.users.deleteAccount({ userId: normalizedUserId }, context); return true; }; From 9de66e16922825862175f581a170604174be6acb Mon Sep 17 00:00:00 2001 From: Mikael Araya Date: Tue, 20 Aug 2024 22:28:18 +0300 Subject: [PATCH 04/16] Add missing configureUserSettingOptions call --- packages/core-users/src/module/configureUsersModule.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core-users/src/module/configureUsersModule.ts b/packages/core-users/src/module/configureUsersModule.ts index 04a100f84f..d8bf15dbc7 100644 --- a/packages/core-users/src/module/configureUsersModule.ts +++ b/packages/core-users/src/module/configureUsersModule.ts @@ -165,6 +165,7 @@ export const configureUsersModule = async ({ options, migrationRepository, }: ModuleInput): Promise => { + userSettings.configureSettings(options || {}, db); registerEvents(USER_EVENTS); const Users = await UsersCollection(db); From 0b5230af87d36e61f7cbc2cf4aba8d94278b39b0 Mon Sep 17 00:00:00 2001 From: Mikael Araya Date: Wed, 21 Aug 2024 22:25:54 +0300 Subject: [PATCH 05/16] Dont delete order data when deleting user --- packages/api/src/resolvers/mutations/index.ts | 4 ++-- .../users/{deleteAccount.ts => deleteUser.ts} | 8 ++++---- packages/api/src/roles/index.ts | 2 +- packages/api/src/roles/loggedIn.ts | 2 +- packages/api/src/schema/mutation.ts | 2 +- .../src/module/configureOrderDeliveriesModule.ts | 4 ---- .../src/module/configureOrderDiscountsModule.ts | 4 ---- .../src/module/configureOrderPaymentsModule.ts | 10 +++------- .../src/module/configureOrderPositionsModule.ts | 5 ----- .../src/module/configureOrdersModule-mutations.ts | 4 ---- packages/core-users/src/module/configureUsersModule.ts | 4 ++-- 11 files changed, 14 insertions(+), 35 deletions(-) rename packages/api/src/resolvers/mutations/users/{deleteAccount.ts => deleteUser.ts} (68%) diff --git a/packages/api/src/resolvers/mutations/index.ts b/packages/api/src/resolvers/mutations/index.ts index 553df26245..cebbc22a76 100755 --- a/packages/api/src/resolvers/mutations/index.ts +++ b/packages/api/src/resolvers/mutations/index.ts @@ -150,7 +150,7 @@ import prepareUserAvatarUpload from './users/prepareUserAvatarUpload.js'; import rejectOrder from './orders/rejectOrder.js'; import removePushSubscription from './users/removePushSubscription.js'; import addPushSubscription from './users/addPushSubscription.js'; -import deleteAccount from './users/deleteAccount.js'; +import deleteUser from './users/deleteUser.js'; export default { logout: acl(actions.logout)(logout), @@ -313,5 +313,5 @@ export default { signPaymentProviderForCheckout: acl(actions.registerPaymentCredentials)( signPaymentProviderForCheckout, ), - deleteAccount: acl(actions.deleteAccount)(deleteAccount), + deleteUser: acl(actions.deleteUser)(deleteUser), }; diff --git a/packages/api/src/resolvers/mutations/users/deleteAccount.ts b/packages/api/src/resolvers/mutations/users/deleteUser.ts similarity index 68% rename from packages/api/src/resolvers/mutations/users/deleteAccount.ts rename to packages/api/src/resolvers/mutations/users/deleteUser.ts index 3b31c7705f..99b779aabb 100644 --- a/packages/api/src/resolvers/mutations/users/deleteAccount.ts +++ b/packages/api/src/resolvers/mutations/users/deleteUser.ts @@ -2,16 +2,16 @@ import { log } from '@unchainedshop/logger'; import { Context } from '../../../types.js'; import { InvalidIdError, UserNotFoundError } from '../../../errors.js'; -const deleteAccount = async (_, { userId }, context: Context) => { +const deleteUser = async (_, { userId }, context: Context) => { const { modules, userAgent, userId: currentUserId } = context; - log(`mutation deleteAccount ${userId} ${userAgent}`, { userId }); + log(`mutation deleteUser ${userId} ${userAgent}`, { userId }); const normalizedUserId = userId || currentUserId; if (!normalizedUserId) throw new InvalidIdError({ userId: normalizedUserId }); if (!(await modules.users.userExists({ userId: normalizedUserId }))) throw new UserNotFoundError({ userId: normalizedUserId }); - await modules.users.deleteAccount({ userId: normalizedUserId }, context); + await modules.users.deleteUser({ userId: normalizedUserId }, context); return true; }; -export default deleteAccount; +export default deleteUser; diff --git a/packages/api/src/roles/index.ts b/packages/api/src/roles/index.ts index cd83e1208e..7a4616914d 100644 --- a/packages/api/src/roles/index.ts +++ b/packages/api/src/roles/index.ts @@ -113,7 +113,7 @@ const actions: Record = [ 'heartbeat', 'confirmMediaUpload', 'viewStatistics', - 'deleteAccount', + 'deleteUser', ].reduce((oldValue, actionValue) => { const newValue = oldValue; newValue[actionValue] = actionValue; diff --git a/packages/api/src/roles/loggedIn.ts b/packages/api/src/roles/loggedIn.ts index 08c93bfb1f..ba581b15a7 100644 --- a/packages/api/src/roles/loggedIn.ts +++ b/packages/api/src/roles/loggedIn.ts @@ -202,7 +202,7 @@ export const loggedIn = (role: any, actions: Record) => { role.allow(actions.viewUserProductReviews, isMyself); role.allow(actions.viewUserTokens, isMyself); role.allow(actions.updateUser, isMyself); - role.allow(actions.deleteAccount, isMyself); + role.allow(actions.deleteUser, isMyself); role.allow(actions.sendEmail, isOwnedEmailAddress); role.allow(actions.viewOrder, isOwnedOrder); role.allow(actions.updateOrder, isOwnedOrder); diff --git a/packages/api/src/schema/mutation.ts b/packages/api/src/schema/mutation.ts index 231a395282..744c35a046 100644 --- a/packages/api/src/schema/mutation.ts +++ b/packages/api/src/schema/mutation.ts @@ -867,7 +867,7 @@ export default [ Remove user W3C push subscription object """ removePushSubscription(p256dh: String!): User! - deleteAccount(userId: ID): Boolean! + deleteUser(userId: ID): Boolean! } `, ]; diff --git a/packages/core-orders/src/module/configureOrderDeliveriesModule.ts b/packages/core-orders/src/module/configureOrderDeliveriesModule.ts index 1c3359dfe1..0996002027 100644 --- a/packages/core-orders/src/module/configureOrderDeliveriesModule.ts +++ b/packages/core-orders/src/module/configureOrderDeliveriesModule.ts @@ -271,9 +271,5 @@ export const configureOrderDeliveriesModule = ({ }, ); }, - deleteUserOrderDeliveriesByOrderIds: async (orderIds) => { - const deleteUserOrdersResult = await OrderDeliveries.deleteMany({ orderId: { $in: orderIds } }); - return deleteUserOrdersResult.deletedCount; - }, }; }; diff --git a/packages/core-orders/src/module/configureOrderDiscountsModule.ts b/packages/core-orders/src/module/configureOrderDiscountsModule.ts index 2ca54e302c..76715eed5a 100644 --- a/packages/core-orders/src/module/configureOrderDiscountsModule.ts +++ b/packages/core-orders/src/module/configureOrderDiscountsModule.ts @@ -251,9 +251,5 @@ export const configureOrderDiscountsModule = ({ await emit('ORDER_UPDATE_DISCOUNT', { discount }); return discount; }, - deleteUserOrderDiscountsByOrderIds: async (orderIds) => { - const deleteUserOrdersResult = await OrderDiscounts.deleteMany({ orderId: { $in: orderIds } }); - return deleteUserOrdersResult.deletedCount; - }, }; }; diff --git a/packages/core-orders/src/module/configureOrderPaymentsModule.ts b/packages/core-orders/src/module/configureOrderPaymentsModule.ts index a8b64442c8..b2e151f033 100644 --- a/packages/core-orders/src/module/configureOrderPaymentsModule.ts +++ b/packages/core-orders/src/module/configureOrderPaymentsModule.ts @@ -96,9 +96,9 @@ export const buildFindByContextDataSelector = (context: any): mongodb.Filter context[key] !== undefined ? { - ...currentSelector, - [`context.${key}`]: context[key], - } + ...currentSelector, + [`context.${key}`]: context[key], + } : currentSelector, {}, ); @@ -409,9 +409,5 @@ export const configureOrderPaymentsModule = ({ }, ); }, - deleteUserOrderPaymentsByOrderIds: async (orderIds) => { - const deleteUserOrdersResult = await OrderPayments.deleteMany({ orderId: { $in: orderIds } }); - return deleteUserOrdersResult.deletedCount; - }, }; }; diff --git a/packages/core-orders/src/module/configureOrderPositionsModule.ts b/packages/core-orders/src/module/configureOrderPositionsModule.ts index 0920de53f7..8300fdaaa6 100644 --- a/packages/core-orders/src/module/configureOrderPositionsModule.ts +++ b/packages/core-orders/src/module/configureOrderPositionsModule.ts @@ -61,7 +61,6 @@ export type OrderPositionsModule = { params: { order: Order; product: Product }, unchainedAPI, ) => Promise; - deleteUserOrderPositionsByOrderIds: (orderIds: string[]) => Promise; }; const ORDER_POSITION_EVENTS: string[] = [ @@ -350,9 +349,5 @@ export const configureOrderPositionsModule = ({ return upsertedOrderPosition; }, - deleteUserOrderPositionsByOrderIds: async (orderIds) => { - const deleteUserOrdersResult = await OrderPositions.deleteMany({ orderId: { $in: orderIds } }); - return deleteUserOrdersResult.deletedCount; - }, }; }; diff --git a/packages/core-orders/src/module/configureOrdersModule-mutations.ts b/packages/core-orders/src/module/configureOrdersModule-mutations.ts index 958b9406b7..d0d248ff23 100644 --- a/packages/core-orders/src/module/configureOrdersModule-mutations.ts +++ b/packages/core-orders/src/module/configureOrdersModule-mutations.ts @@ -251,9 +251,5 @@ export const configureOrderModuleMutations = ({ } return null; }, - deleteUserOrders: async (userId) => { - const deletedUserOrdersResult = await Orders.deleteMany({ userId }); - return deletedUserOrdersResult.deletedCount; - }, }; }; diff --git a/packages/core-users/src/module/configureUsersModule.ts b/packages/core-users/src/module/configureUsersModule.ts index d8bf15dbc7..6387c627c0 100644 --- a/packages/core-users/src/module/configureUsersModule.ts +++ b/packages/core-users/src/module/configureUsersModule.ts @@ -122,7 +122,7 @@ export type UsersModule = { }, ) => Promise; removePushSubscription: (userId: string, p256dh: string) => Promise; - deleteAccount: (params: { userId?: string }, context: UnchainedCore) => Promise; + deleteUser: (params: { userId?: string }, context: UnchainedCore) => Promise; hashPassword(password: string): Promise<{ pbkdf2: string; }>; @@ -849,7 +849,7 @@ export const configureUsersModule = async ({ {}, ); }, - deleteAccount: async ({ userId }, context) => { + deleteUser: async ({ userId }, context) => { const { modules } = context; const { _id, ...user } = await modules.users.findUserById(userId); delete user?.services; From c9d29593778cebfc3a06fe6efb181be834db19b1 Mon Sep 17 00:00:00 2001 From: Mikael Araya Date: Wed, 21 Aug 2024 23:25:17 +0300 Subject: [PATCH 06/16] Add deleteUserProductReviews --- packages/api/src/resolvers/mutations/index.ts | 2 + .../products/deleteUserProductReviews.ts | 22 +++++++++ packages/api/src/schema/mutation.ts | 3 +- .../module/configureOrderPaymentsModule.ts | 6 +-- .../src/module/configureUsersModule.ts | 48 +++++++++---------- 5 files changed, 52 insertions(+), 29 deletions(-) create mode 100644 packages/api/src/resolvers/mutations/products/deleteUserProductReviews.ts diff --git a/packages/api/src/resolvers/mutations/index.ts b/packages/api/src/resolvers/mutations/index.ts index cebbc22a76..b5362efe73 100755 --- a/packages/api/src/resolvers/mutations/index.ts +++ b/packages/api/src/resolvers/mutations/index.ts @@ -151,6 +151,7 @@ import rejectOrder from './orders/rejectOrder.js'; import removePushSubscription from './users/removePushSubscription.js'; import addPushSubscription from './users/addPushSubscription.js'; import deleteUser from './users/deleteUser.js'; +import deleteUserProductReviews from './products/deleteUserProductReviews.js'; export default { logout: acl(actions.logout)(logout), @@ -314,4 +315,5 @@ export default { signPaymentProviderForCheckout, ), deleteUser: acl(actions.deleteUser)(deleteUser), + deleteUserProductReviews, }; diff --git a/packages/api/src/resolvers/mutations/products/deleteUserProductReviews.ts b/packages/api/src/resolvers/mutations/products/deleteUserProductReviews.ts new file mode 100644 index 0000000000..7a6069649a --- /dev/null +++ b/packages/api/src/resolvers/mutations/products/deleteUserProductReviews.ts @@ -0,0 +1,22 @@ +import { Context } from '../../../types.js'; +import { log } from '@unchainedshop/logger'; +import { InvalidIdError, UserNotFoundError } from '../../../errors.js'; + +export default async function deleteUserProductReviews( + root: never, + params: { + userId?: string; + }, + { modules, userId: currentUserId }: Context, +) { + const normalizedUserId = params?.userId || currentUserId; + log(`mutation deleteUserProductReviews ${normalizedUserId}`, { + userId: currentUserId, + }); + if (!normalizedUserId) throw new InvalidIdError({ userId: normalizedUserId }); + if (!(await modules.users.userExists({ userId: normalizedUserId }))) + throw new UserNotFoundError({ userId: normalizedUserId }); + await modules.products.reviews.deleteMany({ authorId: normalizedUserId }); + + return true; +} diff --git a/packages/api/src/schema/mutation.ts b/packages/api/src/schema/mutation.ts index 744c35a046..ed304e9713 100644 --- a/packages/api/src/schema/mutation.ts +++ b/packages/api/src/schema/mutation.ts @@ -867,7 +867,8 @@ export default [ Remove user W3C push subscription object """ removePushSubscription(p256dh: String!): User! - deleteUser(userId: ID): Boolean! + deleteUser(userId: ID): Boolean + deleteUserProductReviews(userId: ID): Boolean } `, ]; diff --git a/packages/core-orders/src/module/configureOrderPaymentsModule.ts b/packages/core-orders/src/module/configureOrderPaymentsModule.ts index b2e151f033..5e1705f540 100644 --- a/packages/core-orders/src/module/configureOrderPaymentsModule.ts +++ b/packages/core-orders/src/module/configureOrderPaymentsModule.ts @@ -96,9 +96,9 @@ export const buildFindByContextDataSelector = (context: any): mongodb.Filter context[key] !== undefined ? { - ...currentSelector, - [`context.${key}`]: context[key], - } + ...currentSelector, + [`context.${key}`]: context[key], + } : currentSelector, {}, ); diff --git a/packages/core-users/src/module/configureUsersModule.ts b/packages/core-users/src/module/configureUsersModule.ts index 6387c627c0..9d1ae4097b 100644 --- a/packages/core-users/src/module/configureUsersModule.ts +++ b/packages/core-users/src/module/configureUsersModule.ts @@ -174,6 +174,25 @@ export const configureUsersModule = async ({ const webAuthn = await configureUsersWebAuthnModule({ db, options }); + const findUserById = async (userId: string): Promise => { + if (!userId) return null; + return Users.findOne(generateDbFilterById(userId), {}); + }; + const updateUser = async ( + selector: mongodb.Filter, + modifier: mongodb.UpdateFilter, + updateOptions?: mongodb.FindOneAndUpdateOptions, + ): Promise => { + const user = await Users.findOneAndUpdate(selector, modifier, { + ...updateOptions, + returnDocument: 'after', + }); + await emit('USER_UPDATE', { + user: removeConfidentialServiceHashes(user), + }); + return user; + }; + return { // Queries webAuthn, @@ -181,12 +200,7 @@ export const configureUsersModule = async ({ const userCount = await Users.countDocuments(buildFindSelector(query)); return userCount; }, - - async findUserById(userId: string): Promise { - if (!userId) return null; - return Users.findOne(generateDbFilterById(userId), {}); - }, - + findUserById, async findUserByUsername(username: string): Promise { if (!username) return null; return Users.findOne({ username }, {}); @@ -790,22 +804,7 @@ export const configureUsersModule = async ({ }); return user; }, - - updateUser: async ( - selector: mongodb.Filter, - modifier: mongodb.UpdateFilter, - updateOptions?: mongodb.FindOneAndUpdateOptions, - ): Promise => { - const user = await Users.findOneAndUpdate(selector, modifier, { - ...updateOptions, - returnDocument: 'after', - }); - await emit('USER_UPDATE', { - user: removeConfidentialServiceHashes(user), - }); - return user; - }, - + updateUser, addPushSubscription: async ( userId: string, subscription: any, @@ -851,12 +850,11 @@ export const configureUsersModule = async ({ }, deleteUser: async ({ userId }, context) => { const { modules } = context; - const { _id, ...user } = await modules.users.findUserById(userId); + const { _id, ...user } = await findUserById(userId); delete user?.services; - const maskedUserData = maskUserPropertyValues({ ...user, meta: null }); await modules.bookmarks.deleteByUserId(userId); - await modules.users.updateUser({ _id }, { $set: { ...maskedUserData, deleted: new Date() } }, {}); + await updateUser({ _id }, { $set: { ...maskedUserData, deleted: new Date() } }, {}); return true; }, }; From c610bd2671a715d982359f16b112d63cd187e28f Mon Sep 17 00:00:00 2001 From: Mikael Araya Date: Wed, 21 Aug 2024 23:27:43 +0300 Subject: [PATCH 07/16] Lock deleteUserProductReviews --- packages/api/src/roles/index.ts | 1 + packages/api/src/roles/loggedIn.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/api/src/roles/index.ts b/packages/api/src/roles/index.ts index 7a4616914d..4583b8ebed 100644 --- a/packages/api/src/roles/index.ts +++ b/packages/api/src/roles/index.ts @@ -114,6 +114,7 @@ const actions: Record = [ 'confirmMediaUpload', 'viewStatistics', 'deleteUser', + 'deleteUserProductReviews', ].reduce((oldValue, actionValue) => { const newValue = oldValue; newValue[actionValue] = actionValue; diff --git a/packages/api/src/roles/loggedIn.ts b/packages/api/src/roles/loggedIn.ts index ba581b15a7..e5abe2b8b0 100644 --- a/packages/api/src/roles/loggedIn.ts +++ b/packages/api/src/roles/loggedIn.ts @@ -203,6 +203,7 @@ export const loggedIn = (role: any, actions: Record) => { role.allow(actions.viewUserTokens, isMyself); role.allow(actions.updateUser, isMyself); role.allow(actions.deleteUser, isMyself); + role.allow(actions.deleteUserProductReviews, isMyself); role.allow(actions.sendEmail, isOwnedEmailAddress); role.allow(actions.viewOrder, isOwnedOrder); role.allow(actions.updateOrder, isOwnedOrder); From fda7fe71dc887598f17f1f3aa6b905d66114a58f Mon Sep 17 00:00:00 2001 From: Mikael Araya Date: Sat, 2 Nov 2024 22:14:10 +0300 Subject: [PATCH 08/16] Rebase --- package-lock.json | 2 -- .../core-orders/src/module/configureOrderDeliveriesModule.ts | 4 ++-- packages/core-users/src/module/configureUsersModule.ts | 3 +++ 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index ea27838096..504a12f52d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3258,7 +3258,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dev": true, "license": "MIT", "dependencies": { "event-target-shim": "^5.0.0" @@ -6384,7 +6383,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" diff --git a/packages/core-orders/src/module/configureOrderDeliveriesModule.ts b/packages/core-orders/src/module/configureOrderDeliveriesModule.ts index 0996002027..38824bd37f 100644 --- a/packages/core-orders/src/module/configureOrderDeliveriesModule.ts +++ b/packages/core-orders/src/module/configureOrderDeliveriesModule.ts @@ -1,10 +1,10 @@ -import { mongodb, generateDbFilterById, generateDbMutations } from '@unchainedshop/mongodb'; +import { mongodb, generateDbFilterById, generateDbObjectId } from '@unchainedshop/mongodb'; import { emit, registerEvents } from '@unchainedshop/events'; import { Order, OrderDelivery, OrderDeliveryStatus, OrderDiscount } from '../types.js'; import { type DeliveryLocation, type IDeliveryPricingSheet } from '@unchainedshop/core-delivery'; import { DeliveryDirector } from '@unchainedshop/core-delivery'; // TODO: Important import { OrderPricingDiscount } from '../director/OrderPricingDirector.js'; -import { ModuleMutations, UnchainedCore } from '@unchainedshop/core'; +import { UnchainedCore } from '@unchainedshop/core'; export type OrderDeliveriesModule = { // Queries diff --git a/packages/core-users/src/module/configureUsersModule.ts b/packages/core-users/src/module/configureUsersModule.ts index 9d1ae4097b..2643e098df 100644 --- a/packages/core-users/src/module/configureUsersModule.ts +++ b/packages/core-users/src/module/configureUsersModule.ts @@ -17,6 +17,9 @@ import * as pbkdf2 from './pbkdf2.js'; import * as sha256 from './sha256.js'; import type { Address, Contact } from '@unchainedshop/mongodb'; import crypto from 'crypto'; +import { UnchainedCore } from '@unchainedshop/core'; +import { UserServices } from '../users-index.js'; +import { FileServices, FilesModule } from '@unchainedshop/core-files'; const isDate = (value) => { const date = new Date(value); From bcd0393a9ef70fac272f15def1536e2eb906af08 Mon Sep 17 00:00:00 2001 From: Mikael Araya Date: Sat, 23 Nov 2024 21:02:08 +0300 Subject: [PATCH 09/16] Use explicit field value masking on delete user --- package-lock.json | 16 +----- .../products/deleteUserProductReviews.ts | 2 +- .../resolvers/mutations/users/deleteUser.ts | 2 +- .../src/module/configureUsersModule.ts | 57 +++++++------------ packages/core-users/src/types.ts | 1 + 5 files changed, 27 insertions(+), 51 deletions(-) diff --git a/package-lock.json b/package-lock.json index 504a12f52d..66a40bf882 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3258,6 +3258,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, "license": "MIT", "dependencies": { "event-target-shim": "^5.0.0" @@ -6383,6 +6384,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -8111,20 +8113,6 @@ "node": ">=10" } }, - "node_modules/googleapis/node_modules/gcp-metadata": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.1.tgz", - "integrity": "sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^4.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/googleapis/node_modules/google-auth-library": { "version": "7.14.1", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.14.1.tgz", diff --git a/packages/api/src/resolvers/mutations/products/deleteUserProductReviews.ts b/packages/api/src/resolvers/mutations/products/deleteUserProductReviews.ts index 7a6069649a..fc30b9e85c 100644 --- a/packages/api/src/resolvers/mutations/products/deleteUserProductReviews.ts +++ b/packages/api/src/resolvers/mutations/products/deleteUserProductReviews.ts @@ -1,6 +1,6 @@ -import { Context } from '../../../types.js'; import { log } from '@unchainedshop/logger'; import { InvalidIdError, UserNotFoundError } from '../../../errors.js'; +import { Context } from '../../../context.js'; export default async function deleteUserProductReviews( root: never, diff --git a/packages/api/src/resolvers/mutations/users/deleteUser.ts b/packages/api/src/resolvers/mutations/users/deleteUser.ts index 99b779aabb..051b4775d0 100644 --- a/packages/api/src/resolvers/mutations/users/deleteUser.ts +++ b/packages/api/src/resolvers/mutations/users/deleteUser.ts @@ -1,6 +1,6 @@ import { log } from '@unchainedshop/logger'; -import { Context } from '../../../types.js'; import { InvalidIdError, UserNotFoundError } from '../../../errors.js'; +import { Context } from '../../../context.js'; const deleteUser = async (_, { userId }, context: Context) => { const { modules, userAgent, userId: currentUserId } = context; diff --git a/packages/core-users/src/module/configureUsersModule.ts b/packages/core-users/src/module/configureUsersModule.ts index 2643e098df..1a441bde29 100644 --- a/packages/core-users/src/module/configureUsersModule.ts +++ b/packages/core-users/src/module/configureUsersModule.ts @@ -15,42 +15,29 @@ import { userSettings, UserSettingsOptions } from '../users-settings.js'; import { configureUsersWebAuthnModule, UsersWebAuthnModule } from './configureUsersWebAuthnModule.js'; import * as pbkdf2 from './pbkdf2.js'; import * as sha256 from './sha256.js'; -import type { Address, Contact } from '@unchainedshop/mongodb'; import crypto from 'crypto'; import { UnchainedCore } from '@unchainedshop/core'; -import { UserServices } from '../users-index.js'; -import { FileServices, FilesModule } from '@unchainedshop/core-files'; +import { Context } from 'vm'; -const isDate = (value) => { - const date = new Date(value); - return !Number.isNaN(date.getTime()); -}; - -function maskString(value) { - if (isDate(value)) return value; - return crypto - .createHash('sha256') - .update(JSON.stringify([value, new Date().getTime()])) - .digest('hex'); -} - -const maskUserPropertyValues = (user) => { - if (typeof user !== 'object' || user === null) { - return user; +const maskUserPropertyValues = (user, deletedById: string): User => { + if (!user || typeof user !== 'object') { + throw new Error('Invalid user object'); } - if (Array.isArray(user)) { - return user.map((item) => maskUserPropertyValues(item)); - } - const maskedUser = {}; - Object.keys(user).forEach((key) => { - if (typeof user[key] === 'string' || isDate(user[key])) { - maskedUser[key] = maskString(user[key]); - } else { - maskedUser[key] = maskUserPropertyValues(user[key]); - } - }); - - return maskedUser; + return { + ...user, + username: `deleted-${Date.now()}`, + deleted: new Date(), + deletedBy: deletedById, + emails: null, + roles: null, + profile: null, + lastBillingAddress: {}, + services: {}, + pushSubscriptions: [], + avatarId: null, + initialPassword: null, + lastContact: null, + }; }; export type UsersModule = { @@ -147,6 +134,7 @@ const USER_EVENTS = [ 'USER_UPDATE_BILLING_ADDRESS', 'USER_UPDATE_LAST_CONTACT', 'USER_REMOVE', + 'USER_PURGE', ]; export const removeConfidentialServiceHashes = (rawUser: User): User => { const user = rawUser; @@ -851,11 +839,10 @@ export const configureUsersModule = async ({ {}, ); }, - deleteUser: async ({ userId }, context) => { + deleteUser: async ({ userId }, context: Context) => { const { modules } = context; const { _id, ...user } = await findUserById(userId); - delete user?.services; - const maskedUserData = maskUserPropertyValues({ ...user, meta: null }); + const maskedUserData = maskUserPropertyValues(user, context.userId); await modules.bookmarks.deleteByUserId(userId); await updateUser({ _id }, { $set: { ...maskedUserData, deleted: new Date() } }, {}); return true; diff --git a/packages/core-users/src/types.ts b/packages/core-users/src/types.ts index e45fb4ca98..c7bd08b5d9 100644 --- a/packages/core-users/src/types.ts +++ b/packages/core-users/src/types.ts @@ -71,6 +71,7 @@ export type User = { pushSubscriptions: Array; username?: string; meta?: any; + deletedBy?: string; } & TimestampFields; export type UserQuery = mongodb.Filter & { From a1261a410049dc88c91af3834ac476aa40dd92ea Mon Sep 17 00:00:00 2001 From: Mikael Araya Date: Wed, 27 Nov 2024 18:56:51 +0300 Subject: [PATCH 10/16] Remove all related open user data when removing users --- packages/api/src/resolvers/mutations/index.ts | 2 - .../resolvers/mutations/users/deleteUser.ts | 17 ------- .../resolvers/mutations/users/removeUser.ts | 9 ++-- packages/api/src/roles/index.ts | 2 +- packages/api/src/roles/loggedIn.ts | 2 +- packages/api/src/schema/mutation.ts | 3 +- .../src/module/configureEnrollmentsModule.ts | 7 ++- .../module/configureOrderDeliveriesModule.ts | 6 ++- .../module/configureOrderDiscountsModule.ts | 5 ++ .../module/configureOrderPaymentsModule.ts | 5 ++ .../module/configureOrderPositionsModule.ts | 5 ++ .../src/module/configureOrdersModule.ts | 21 +++++++++ .../src/module/configureQuotationsModule.ts | 9 +++- .../src/module/configureUsersModule.ts | 46 ++++++------------- 14 files changed, 77 insertions(+), 62 deletions(-) delete mode 100644 packages/api/src/resolvers/mutations/users/deleteUser.ts diff --git a/packages/api/src/resolvers/mutations/index.ts b/packages/api/src/resolvers/mutations/index.ts index b5362efe73..2456ce5c77 100755 --- a/packages/api/src/resolvers/mutations/index.ts +++ b/packages/api/src/resolvers/mutations/index.ts @@ -150,7 +150,6 @@ import prepareUserAvatarUpload from './users/prepareUserAvatarUpload.js'; import rejectOrder from './orders/rejectOrder.js'; import removePushSubscription from './users/removePushSubscription.js'; import addPushSubscription from './users/addPushSubscription.js'; -import deleteUser from './users/deleteUser.js'; import deleteUserProductReviews from './products/deleteUserProductReviews.js'; export default { @@ -314,6 +313,5 @@ export default { signPaymentProviderForCheckout: acl(actions.registerPaymentCredentials)( signPaymentProviderForCheckout, ), - deleteUser: acl(actions.deleteUser)(deleteUser), deleteUserProductReviews, }; diff --git a/packages/api/src/resolvers/mutations/users/deleteUser.ts b/packages/api/src/resolvers/mutations/users/deleteUser.ts deleted file mode 100644 index 051b4775d0..0000000000 --- a/packages/api/src/resolvers/mutations/users/deleteUser.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { log } from '@unchainedshop/logger'; -import { InvalidIdError, UserNotFoundError } from '../../../errors.js'; -import { Context } from '../../../context.js'; - -const deleteUser = async (_, { userId }, context: Context) => { - const { modules, userAgent, userId: currentUserId } = context; - log(`mutation deleteUser ${userId} ${userAgent}`, { userId }); - const normalizedUserId = userId || currentUserId; - if (!normalizedUserId) throw new InvalidIdError({ userId: normalizedUserId }); - if (!(await modules.users.userExists({ userId: normalizedUserId }))) - throw new UserNotFoundError({ userId: normalizedUserId }); - - await modules.users.deleteUser({ userId: normalizedUserId }, context); - return true; -}; - -export default deleteUser; diff --git a/packages/api/src/resolvers/mutations/users/removeUser.ts b/packages/api/src/resolvers/mutations/users/removeUser.ts index 83b538e70a..764b4c1dcf 100755 --- a/packages/api/src/resolvers/mutations/users/removeUser.ts +++ b/packages/api/src/resolvers/mutations/users/removeUser.ts @@ -4,10 +4,11 @@ import { UserNotFoundError } from '../../../errors.js'; export default async function removeUser( root: never, - params: { userId: string }, - { modules, userId }: Context, + params: { userId: string; removeUserReviews?: boolean }, + unchainedApi: Context, ) { - const { userId: paramUserId } = params; + const { modules, userId } = unchainedApi; + const { userId: paramUserId, removeUserReviews = false } = params; const normalizedUserId = paramUserId || userId; log(`mutation removeUser ${normalizedUserId}`, { userId }); @@ -15,5 +16,5 @@ export default async function removeUser( if (!(await modules.users.userExists({ userId: normalizedUserId }))) throw UserNotFoundError({ id: normalizedUserId }); - return modules.users.delete(normalizedUserId); + return modules.users.delete({ userId: normalizedUserId, removeUserReviews }, unchainedApi); } diff --git a/packages/api/src/roles/index.ts b/packages/api/src/roles/index.ts index 4583b8ebed..cc2c0321f6 100644 --- a/packages/api/src/roles/index.ts +++ b/packages/api/src/roles/index.ts @@ -113,7 +113,7 @@ const actions: Record = [ 'heartbeat', 'confirmMediaUpload', 'viewStatistics', - 'deleteUser', + 'removeUser', 'deleteUserProductReviews', ].reduce((oldValue, actionValue) => { const newValue = oldValue; diff --git a/packages/api/src/roles/loggedIn.ts b/packages/api/src/roles/loggedIn.ts index e5abe2b8b0..e04c1664b6 100644 --- a/packages/api/src/roles/loggedIn.ts +++ b/packages/api/src/roles/loggedIn.ts @@ -202,7 +202,7 @@ export const loggedIn = (role: any, actions: Record) => { role.allow(actions.viewUserProductReviews, isMyself); role.allow(actions.viewUserTokens, isMyself); role.allow(actions.updateUser, isMyself); - role.allow(actions.deleteUser, isMyself); + role.allow(actions.removeUser, isMyself); role.allow(actions.deleteUserProductReviews, isMyself); role.allow(actions.sendEmail, isOwnedEmailAddress); role.allow(actions.viewOrder, isOwnedOrder); diff --git a/packages/api/src/schema/mutation.ts b/packages/api/src/schema/mutation.ts index ed304e9713..39b62b90cf 100644 --- a/packages/api/src/schema/mutation.ts +++ b/packages/api/src/schema/mutation.ts @@ -304,7 +304,7 @@ export default [ """ Remove any user or logged in user if userId is not provided """ - removeUser(userId: ID): User! + removeUser(userId: ID, removeUserReviews: Boolean): User! """ Enroll a new user, setting enroll to true will let the user choose his password (e-mail gets sent) @@ -867,7 +867,6 @@ export default [ Remove user W3C push subscription object """ removePushSubscription(p256dh: String!): User! - deleteUser(userId: ID): Boolean deleteUserProductReviews(userId: ID): Boolean } `, diff --git a/packages/core-enrollments/src/module/configureEnrollmentsModule.ts b/packages/core-enrollments/src/module/configureEnrollmentsModule.ts index 3c5e754dea..8c6cbfc0ce 100644 --- a/packages/core-enrollments/src/module/configureEnrollmentsModule.ts +++ b/packages/core-enrollments/src/module/configureEnrollmentsModule.ts @@ -99,6 +99,7 @@ export interface EnrollmentMutations { params: { status: EnrollmentStatus; info?: string }, unchainedAPI, ) => Promise; + deleteOpenUserEnrollments: (userId: string) => Promise; } export type EnrollmentsModule = EnrollmentQueries & @@ -505,7 +506,7 @@ export const configureEnrollmentsModule = async ({ }, }, ); - await emit('ORDER_REMOVE', { enrollmentId }); + await emit('ENROLLMENT_REMOVE', { enrollmentId }); return deletedCount; }, @@ -554,5 +555,9 @@ export const configureEnrollmentsModule = async ({ }, updateStatus, + deleteOpenUserEnrollments: async (userId: string) => { + const { deletedCount } = await Enrollments.deleteMany({ userId }); + return deletedCount; + }, }; }; diff --git a/packages/core-orders/src/module/configureOrderDeliveriesModule.ts b/packages/core-orders/src/module/configureOrderDeliveriesModule.ts index 38824bd37f..3d557ace70 100644 --- a/packages/core-orders/src/module/configureOrderDeliveriesModule.ts +++ b/packages/core-orders/src/module/configureOrderDeliveriesModule.ts @@ -4,7 +4,6 @@ import { Order, OrderDelivery, OrderDeliveryStatus, OrderDiscount } from '../typ import { type DeliveryLocation, type IDeliveryPricingSheet } from '@unchainedshop/core-delivery'; import { DeliveryDirector } from '@unchainedshop/core-delivery'; // TODO: Important import { OrderPricingDiscount } from '../director/OrderPricingDirector.js'; -import { UnchainedCore } from '@unchainedshop/core'; export type OrderDeliveriesModule = { // Queries @@ -46,6 +45,7 @@ export type OrderDeliveriesModule = { ) => Promise; updateCalculation: (orderDelivery: OrderDelivery, unchainedAPI) => Promise; + deleteOrderDelivery: (orderId: string) => Promise; }; const ORDER_DELIVERY_EVENTS: string[] = ['ORDER_DELIVER', 'ORDER_UPDATE_DELIVERY']; @@ -271,5 +271,9 @@ export const configureOrderDeliveriesModule = ({ }, ); }, + deleteOrderDelivery: async (orderId: string) => { + const { deletedCount } = await OrderDeliveries.deleteMany({ orderId }); + return deletedCount; + }, }; }; diff --git a/packages/core-orders/src/module/configureOrderDiscountsModule.ts b/packages/core-orders/src/module/configureOrderDiscountsModule.ts index 76715eed5a..b45d124c37 100644 --- a/packages/core-orders/src/module/configureOrderDiscountsModule.ts +++ b/packages/core-orders/src/module/configureOrderDiscountsModule.ts @@ -40,6 +40,7 @@ export type OrderDiscountsModule = { create: (doc: OrderDiscount) => Promise; update: (orderDiscountId: string, doc: OrderDiscount) => Promise; delete: (orderDiscountId: string, unchainedAPI) => Promise; + deleteOrderDiscounts: (orderId: string) => Promise; }; const ORDER_DISCOUNT_EVENTS: string[] = [ @@ -251,5 +252,9 @@ export const configureOrderDiscountsModule = ({ await emit('ORDER_UPDATE_DISCOUNT', { discount }); return discount; }, + deleteOrderDiscounts: async (orderId: string) => { + const { deletedCount } = await OrderDiscounts.deleteMany({ orderId }); + return deletedCount; + }, }; }; diff --git a/packages/core-orders/src/module/configureOrderPaymentsModule.ts b/packages/core-orders/src/module/configureOrderPaymentsModule.ts index 5e1705f540..304622b7af 100644 --- a/packages/core-orders/src/module/configureOrderPaymentsModule.ts +++ b/packages/core-orders/src/module/configureOrderPaymentsModule.ts @@ -80,6 +80,7 @@ export type OrderPaymentsModule = { ) => Promise; updateCalculation: (orderPayment: OrderPayment, unchainedAPI) => Promise; + deleteOrderPayment: (orderId: string) => Promise; }; const ORDER_PAYMENT_EVENTS: string[] = ['ORDER_UPDATE_PAYMENT', 'ORDER_SIGN_PAYMENT', 'ORDER_PAY']; @@ -409,5 +410,9 @@ export const configureOrderPaymentsModule = ({ }, ); }, + deleteOrderPayment: async (orderId: string) => { + const { deletedCount } = await OrderPayments.deleteMany({ orderId }); + return deletedCount; + }, }; }; diff --git a/packages/core-orders/src/module/configureOrderPositionsModule.ts b/packages/core-orders/src/module/configureOrderPositionsModule.ts index 8300fdaaa6..d399b3b2cb 100644 --- a/packages/core-orders/src/module/configureOrderPositionsModule.ts +++ b/packages/core-orders/src/module/configureOrderPositionsModule.ts @@ -61,6 +61,7 @@ export type OrderPositionsModule = { params: { order: Order; product: Product }, unchainedAPI, ) => Promise; + deleteOrderPositions: (orderId: string) => Promise; }; const ORDER_POSITION_EVENTS: string[] = [ @@ -349,5 +350,9 @@ export const configureOrderPositionsModule = ({ return upsertedOrderPosition; }, + deleteOrderPositions: async (orderId: string) => { + const { deletedCount } = await OrderPositions.deleteMany({ orderId }); + return deletedCount; + }, }; }; diff --git a/packages/core-orders/src/module/configureOrdersModule.ts b/packages/core-orders/src/module/configureOrdersModule.ts index 6f7bfebd68..c0aadc19ad 100644 --- a/packages/core-orders/src/module/configureOrdersModule.ts +++ b/packages/core-orders/src/module/configureOrdersModule.ts @@ -25,6 +25,13 @@ export type OrdersModule = OrderQueries & OrderTransformations & OrderProcessing & OrderMutations & { + // Order context recalculations + initProviders: (order: Order, unchainedAPI) => Promise; + updateCalculation: (orderId: string, unchainedAPI) => Promise; + invalidateProviders: (unchainedAPI, maxAgeDays: number) => Promise; + deleteUserCart: (userId: string) => Promise; + + // Sub entities deliveries: OrderDeliveriesModule; discounts: OrderDiscountsModule; positions: OrderPositionsModule; @@ -90,6 +97,20 @@ export const configureOrdersModule = async ({ OrderDeliveries, }); + const deleteUserCart = async (userId: string) => { + try { + const userCart = await Orders.findOne({ status: null, userId }); + await orderMutations.delete(userCart?._id); + await orderPaymentsModule.deleteOrderPayment(userCart?._id); + await orderDeliveriesModule.deleteOrderDelivery(userCart?._id); + await orderDiscountsModule.deleteOrderDiscounts(userCart?._id); + return true; + } catch (e) { + console.error(e); + return false; + } + }; + return { ...orderQueries, ...orderTransformations, diff --git a/packages/core-quotations/src/module/configureQuotationsModule.ts b/packages/core-quotations/src/module/configureQuotationsModule.ts index 6c3c43a1e0..9afed0a874 100644 --- a/packages/core-quotations/src/module/configureQuotationsModule.ts +++ b/packages/core-quotations/src/module/configureQuotationsModule.ts @@ -30,6 +30,7 @@ export interface QuotationQueries { ) => Promise>; count: (query: QuotationQuery) => Promise; openQuotationWithProduct: (param: { productId: string }) => Promise; + deleteRequestedUserQuotations: (userId: string) => Promise; } // Transformations @@ -398,7 +399,13 @@ export const configureQuotationsModule = async ({ return quotation; }, - + deleteRequestedUserQuotations: async (userId: string) => { + const { deletedCount } = await Quotations.deleteMany({ + userId, + status: QuotationStatus.REQUESTED, + }); + return deletedCount; + }, updateContext: updateQuotationFields(['context']), updateProposal: updateQuotationFields(['price', 'expires', 'meta']), diff --git a/packages/core-users/src/module/configureUsersModule.ts b/packages/core-users/src/module/configureUsersModule.ts index 1a441bde29..e4b232403b 100644 --- a/packages/core-users/src/module/configureUsersModule.ts +++ b/packages/core-users/src/module/configureUsersModule.ts @@ -95,7 +95,7 @@ export type UsersModule = { _id: string, { profile, meta }: { profile?: UserProfile; meta?: any }, ) => Promise; - delete: (userId: string) => Promise; + delete: (params: { userId: string; removeUserReviews?: boolean }, context: Context) => Promise; updateRoles: (_id: string, roles: Array) => Promise; updateTags: (_id: string, tags: Array) => Promise; updateUser: ( @@ -112,7 +112,6 @@ export type UsersModule = { }, ) => Promise; removePushSubscription: (userId: string, p256dh: string) => Promise; - deleteUser: (params: { userId?: string }, context: UnchainedCore) => Promise; hashPassword(password: string): Promise<{ pbkdf2: string; }>; @@ -625,34 +624,25 @@ export const configureUsersModule = async ({ return user; }, - delete: async (userId: string): Promise => { + delete: async ( + params: { userId: string; removeUserReviews?: boolean }, + context: Context, + ): Promise => { + const { userId, removeUserReviews = false } = params; const userFilter = generateDbFilterById(userId); const existingUser = await Users.findOne(userFilter, { projection: { emails: true, username: true }, }); if (!existingUser) return null; - - const uuid = crypto.randomUUID(); - const obfuscatedEmails = existingUser.emails?.flatMap(({ address, verified }) => { - if (!verified) return []; - return [ - { - address: `${address}@${uuid}.unchained.local`, - verified: true, - }, - ]; - }); - - const obfuscatedUsername = existingUser.username ? `${existingUser.username}-${uuid}` : null; - - Users.updateOne(userFilter, { - $set: { - emails: obfuscatedEmails, - username: obfuscatedUsername, - services: {}, - }, - }); + const maskedUserData = maskUserPropertyValues(existingUser, context?.userId); + await context.modules.bookmarks.deleteByUserId(userId); + await updateUser({ _id: userId }, { $set: { ...maskedUserData, deleted: new Date() } }, {}); + (context as UnchainedCore).modules.orders.deleteUserCart(userId); + await (context as UnchainedCore).modules.quotations.deleteRequestedUserQuotations(userId); + await (context as UnchainedCore).modules.enrollments.deleteOpenUserEnrollments(userId); + if (removeUserReviews) + await (context as UnchainedCore).modules.products.reviews.deleteMany({ authorId: userId }); const user = await Users.findOneAndDelete(userFilter); await emit('USER_REMOVE', { @@ -839,13 +829,5 @@ export const configureUsersModule = async ({ {}, ); }, - deleteUser: async ({ userId }, context: Context) => { - const { modules } = context; - const { _id, ...user } = await findUserById(userId); - const maskedUserData = maskUserPropertyValues(user, context.userId); - await modules.bookmarks.deleteByUserId(userId); - await updateUser({ _id }, { $set: { ...maskedUserData, deleted: new Date() } }, {}); - return true; - }, }; }; From 6c355fd09d9360bed3763061518fe51d1b99a8d9 Mon Sep 17 00:00:00 2001 From: Mikael Araya Date: Wed, 27 Nov 2024 20:04:03 +0300 Subject: [PATCH 11/16] Remove user record if its not linked with any record --- .../module/configureOrdersModule-queries.ts | 1 - .../src/module/configureUsersModule.ts | 20 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/core-orders/src/module/configureOrdersModule-queries.ts b/packages/core-orders/src/module/configureOrdersModule-queries.ts index f3255b825e..bea3d794cc 100644 --- a/packages/core-orders/src/module/configureOrdersModule-queries.ts +++ b/packages/core-orders/src/module/configureOrdersModule-queries.ts @@ -43,7 +43,6 @@ export const configureOrdersModuleQueries = ({ Orders }: { Orders: mongodb.Colle const orderCount = await Orders.countDocuments(buildFindSelector(query)); return orderCount; }, - findOrder: async ( { orderId, diff --git a/packages/core-users/src/module/configureUsersModule.ts b/packages/core-users/src/module/configureUsersModule.ts index e4b232403b..f4027f3af6 100644 --- a/packages/core-users/src/module/configureUsersModule.ts +++ b/packages/core-users/src/module/configureUsersModule.ts @@ -628,6 +628,7 @@ export const configureUsersModule = async ({ params: { userId: string; removeUserReviews?: boolean }, context: Context, ): Promise => { + const { modules } = context as UnchainedCore; const { userId, removeUserReviews = false } = params; const userFilter = generateDbFilterById(userId); @@ -638,13 +639,20 @@ export const configureUsersModule = async ({ const maskedUserData = maskUserPropertyValues(existingUser, context?.userId); await context.modules.bookmarks.deleteByUserId(userId); await updateUser({ _id: userId }, { $set: { ...maskedUserData, deleted: new Date() } }, {}); - (context as UnchainedCore).modules.orders.deleteUserCart(userId); - await (context as UnchainedCore).modules.quotations.deleteRequestedUserQuotations(userId); - await (context as UnchainedCore).modules.enrollments.deleteOpenUserEnrollments(userId); - if (removeUserReviews) - await (context as UnchainedCore).modules.products.reviews.deleteMany({ authorId: userId }); - + modules.orders.deleteUserCart(userId); + await modules.quotations.deleteRequestedUserQuotations(userId); + await modules.enrollments.deleteOpenUserEnrollments(userId); + if (removeUserReviews) await modules.products.reviews.deleteMany({ authorId: userId }); const user = await Users.findOneAndDelete(userFilter); + + const ordersCount = modules.orders.count({ userId, includeCarts: true }); + const quotationsCount = modules.quotations.count({ userId }); + const reviewsCount = modules.products.reviews.count({ authorId: userId }); + const enrollmentsCount = modules.enrollments.count({ userId }); + if (!ordersCount && !reviewsCount && !enrollmentsCount && !quotationsCount) { + await Users.deleteOne({ _id: userId }); + } + await emit('USER_REMOVE', { user: removeConfidentialServiceHashes(user), }); From f62d434e6ccab679fd0adbe14bf7500ce21af761 Mon Sep 17 00:00:00 2001 From: Mikael Araya Date: Wed, 27 Nov 2024 21:30:51 +0300 Subject: [PATCH 12/16] Fix --- .../src/module/configureUsersModule.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/core-users/src/module/configureUsersModule.ts b/packages/core-users/src/module/configureUsersModule.ts index f4027f3af6..36534b647e 100644 --- a/packages/core-users/src/module/configureUsersModule.ts +++ b/packages/core-users/src/module/configureUsersModule.ts @@ -637,26 +637,26 @@ export const configureUsersModule = async ({ }); if (!existingUser) return null; const maskedUserData = maskUserPropertyValues(existingUser, context?.userId); - await context.modules.bookmarks.deleteByUserId(userId); await updateUser({ _id: userId }, { $set: { ...maskedUserData, deleted: new Date() } }, {}); - modules.orders.deleteUserCart(userId); + + await modules.bookmarks.deleteByUserId(userId); + await modules.orders.deleteUserCart(userId); await modules.quotations.deleteRequestedUserQuotations(userId); await modules.enrollments.deleteOpenUserEnrollments(userId); if (removeUserReviews) await modules.products.reviews.deleteMany({ authorId: userId }); - const user = await Users.findOneAndDelete(userFilter); - const ordersCount = modules.orders.count({ userId, includeCarts: true }); - const quotationsCount = modules.quotations.count({ userId }); - const reviewsCount = modules.products.reviews.count({ authorId: userId }); - const enrollmentsCount = modules.enrollments.count({ userId }); + const ordersCount = await modules.orders.count({ userId, includeCarts: true }); + const quotationsCount = await modules.quotations.count({ userId }); + const reviewsCount = await modules.products.reviews.count({ authorId: userId }); + const enrollmentsCount = await modules.enrollments.count({ userId }); if (!ordersCount && !reviewsCount && !enrollmentsCount && !quotationsCount) { await Users.deleteOne({ _id: userId }); } await emit('USER_REMOVE', { - user: removeConfidentialServiceHashes(user), + user: removeConfidentialServiceHashes(existingUser), }); - return user; + return existingUser; }, updateProfile: async ( From 02e5e1d73b1ca27ca3b2ff4ed65d700dd15a5160 Mon Sep 17 00:00:00 2001 From: Mikael Araya Date: Wed, 27 Nov 2024 22:52:37 +0300 Subject: [PATCH 13/16] Move deleteUserCartService to orders service following new pattern --- .../module/configureOrdersModule-mutations.ts | 1 - .../src/module/configureOrdersModule.ts | 20 ------------------- .../src/module/configureUsersModule.ts | 4 ++-- .../src/services/deleteUserCartService.ts | 17 ++++++++++++++++ packages/core/src/services/index.ts | 2 ++ 5 files changed, 21 insertions(+), 23 deletions(-) create mode 100644 packages/core/src/services/deleteUserCartService.ts diff --git a/packages/core-orders/src/module/configureOrdersModule-mutations.ts b/packages/core-orders/src/module/configureOrdersModule-mutations.ts index d0d248ff23..8f86b46361 100644 --- a/packages/core-orders/src/module/configureOrdersModule-mutations.ts +++ b/packages/core-orders/src/module/configureOrdersModule-mutations.ts @@ -31,7 +31,6 @@ export interface OrderMutations { updateContact: (orderId: string, contact: Contact) => Promise; updateContext: (orderId: string, context: any) => Promise; updateCalculationSheet: (orderId: string, calculation) => Promise; - deleteUserOrders: (userId: string) => Promise; } const ORDER_EVENTS: string[] = [ diff --git a/packages/core-orders/src/module/configureOrdersModule.ts b/packages/core-orders/src/module/configureOrdersModule.ts index c0aadc19ad..024a184147 100644 --- a/packages/core-orders/src/module/configureOrdersModule.ts +++ b/packages/core-orders/src/module/configureOrdersModule.ts @@ -25,12 +25,6 @@ export type OrdersModule = OrderQueries & OrderTransformations & OrderProcessing & OrderMutations & { - // Order context recalculations - initProviders: (order: Order, unchainedAPI) => Promise; - updateCalculation: (orderId: string, unchainedAPI) => Promise; - invalidateProviders: (unchainedAPI, maxAgeDays: number) => Promise; - deleteUserCart: (userId: string) => Promise; - // Sub entities deliveries: OrderDeliveriesModule; discounts: OrderDiscountsModule; @@ -97,20 +91,6 @@ export const configureOrdersModule = async ({ OrderDeliveries, }); - const deleteUserCart = async (userId: string) => { - try { - const userCart = await Orders.findOne({ status: null, userId }); - await orderMutations.delete(userCart?._id); - await orderPaymentsModule.deleteOrderPayment(userCart?._id); - await orderDeliveriesModule.deleteOrderDelivery(userCart?._id); - await orderDiscountsModule.deleteOrderDiscounts(userCart?._id); - return true; - } catch (e) { - console.error(e); - return false; - } - }; - return { ...orderQueries, ...orderTransformations, diff --git a/packages/core-users/src/module/configureUsersModule.ts b/packages/core-users/src/module/configureUsersModule.ts index 36534b647e..bc0ff2efbf 100644 --- a/packages/core-users/src/module/configureUsersModule.ts +++ b/packages/core-users/src/module/configureUsersModule.ts @@ -628,7 +628,7 @@ export const configureUsersModule = async ({ params: { userId: string; removeUserReviews?: boolean }, context: Context, ): Promise => { - const { modules } = context as UnchainedCore; + const { services, modules } = context as UnchainedCore; const { userId, removeUserReviews = false } = params; const userFilter = generateDbFilterById(userId); @@ -640,7 +640,7 @@ export const configureUsersModule = async ({ await updateUser({ _id: userId }, { $set: { ...maskedUserData, deleted: new Date() } }, {}); await modules.bookmarks.deleteByUserId(userId); - await modules.orders.deleteUserCart(userId); + await services.orders.deleteUserCart(userId, context as UnchainedCore); await modules.quotations.deleteRequestedUserQuotations(userId); await modules.enrollments.deleteOpenUserEnrollments(userId); if (removeUserReviews) await modules.products.reviews.deleteMany({ authorId: userId }); diff --git a/packages/core/src/services/deleteUserCartService.ts b/packages/core/src/services/deleteUserCartService.ts new file mode 100644 index 0000000000..bfc1caddd5 --- /dev/null +++ b/packages/core/src/services/deleteUserCartService.ts @@ -0,0 +1,17 @@ +import { UnchainedCore } from '../core-index.js'; + +const deleteUserCartService = async (userId: string, unchainedAPI: UnchainedCore) => { + try { + const userCart = await unchainedAPI.modules.orders.cart({ userId }); + await unchainedAPI.modules.orders.delete(userCart?._id); + await unchainedAPI.modules.orders.payments.deleteOrderPayment(userCart?._id); + await unchainedAPI.modules.orders.deliveries.deleteOrderDelivery(userCart?._id); + await unchainedAPI.modules.orders.discounts.deleteOrderDiscounts(userCart?._id); + return true; + } catch (e) { + console.error(e); + return false; + } +}; + +export default deleteUserCartService; diff --git a/packages/core/src/services/index.ts b/packages/core/src/services/index.ts index 657e1fa1c9..7d267c47ee 100644 --- a/packages/core/src/services/index.ts +++ b/packages/core/src/services/index.ts @@ -12,6 +12,7 @@ import { nextUserCartService } from './nextUserCartService.js'; import { removeProductService } from './removeProductService.js'; import { initCartProvidersService } from './initCartProviders.js'; import { updateCalculationService } from './updateCalculationService.js'; +import deleteUserCartService from './deleteUserCartService.js'; const services = { bookmarks: { @@ -30,6 +31,7 @@ const services = { nextUserCart: nextUserCartService, initCartProviders: initCartProvidersService, updateCalculation: updateCalculationService, + deleteUserCart: deleteUserCartService, }, products: { removeProduct: removeProductService, From a21706d93ab4dfed0a5327b18596f9e19847e946 Mon Sep 17 00:00:00 2001 From: Mikael Araya Date: Thu, 28 Nov 2024 16:04:40 +0300 Subject: [PATCH 14/16] Fix cart util call --- .../api/src/resolvers/mutations/users/removeUser.ts | 2 +- packages/core/src/services/deleteUserCartService.ts | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/api/src/resolvers/mutations/users/removeUser.ts b/packages/api/src/resolvers/mutations/users/removeUser.ts index 764b4c1dcf..20796ab26f 100755 --- a/packages/api/src/resolvers/mutations/users/removeUser.ts +++ b/packages/api/src/resolvers/mutations/users/removeUser.ts @@ -14,7 +14,7 @@ export default async function removeUser( log(`mutation removeUser ${normalizedUserId}`, { userId }); if (!(await modules.users.userExists({ userId: normalizedUserId }))) - throw UserNotFoundError({ id: normalizedUserId }); + throw new UserNotFoundError({ id: normalizedUserId }); return modules.users.delete({ userId: normalizedUserId, removeUserReviews }, unchainedApi); } diff --git a/packages/core/src/services/deleteUserCartService.ts b/packages/core/src/services/deleteUserCartService.ts index bfc1caddd5..b22383b801 100644 --- a/packages/core/src/services/deleteUserCartService.ts +++ b/packages/core/src/services/deleteUserCartService.ts @@ -1,8 +1,15 @@ import { UnchainedCore } from '../core-index.js'; -const deleteUserCartService = async (userId: string, unchainedAPI: UnchainedCore) => { +const deleteUserCartService = async ( + userId: string, + unchainedAPI: UnchainedCore & { countryContext?: string }, +) => { try { - const userCart = await unchainedAPI.modules.orders.cart({ userId }); + const user = await unchainedAPI.modules.users.findUserById(userId); + const userCart = await unchainedAPI.modules.orders.cart({ + userId, + countryContext: unchainedAPI?.countryContext || user?.lastLogin?.countryCode, + }); await unchainedAPI.modules.orders.delete(userCart?._id); await unchainedAPI.modules.orders.payments.deleteOrderPayment(userCart?._id); await unchainedAPI.modules.orders.deliveries.deleteOrderDelivery(userCart?._id); From e3c6189f6ecfb978e8343cfe7da695357c401220 Mon Sep 17 00:00:00 2001 From: Mikael Araya Date: Thu, 28 Nov 2024 16:44:37 +0300 Subject: [PATCH 15/16] Add check if user has tokens linked before permanently removing it --- .../src/module/configureUsersModule.ts | 3 +- .../src/services/deleteUserCartService.ts | 34 +++++++++---------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/core-users/src/module/configureUsersModule.ts b/packages/core-users/src/module/configureUsersModule.ts index bc0ff2efbf..287e33af72 100644 --- a/packages/core-users/src/module/configureUsersModule.ts +++ b/packages/core-users/src/module/configureUsersModule.ts @@ -649,7 +649,8 @@ export const configureUsersModule = async ({ const quotationsCount = await modules.quotations.count({ userId }); const reviewsCount = await modules.products.reviews.count({ authorId: userId }); const enrollmentsCount = await modules.enrollments.count({ userId }); - if (!ordersCount && !reviewsCount && !enrollmentsCount && !quotationsCount) { + const tokens = await modules.warehousing.findTokensForUser(existingUser); + if (!ordersCount && !reviewsCount && !enrollmentsCount && !quotationsCount && !tokens?.length) { await Users.deleteOne({ _id: userId }); } diff --git a/packages/core/src/services/deleteUserCartService.ts b/packages/core/src/services/deleteUserCartService.ts index b22383b801..5cafeb68e5 100644 --- a/packages/core/src/services/deleteUserCartService.ts +++ b/packages/core/src/services/deleteUserCartService.ts @@ -1,24 +1,24 @@ import { UnchainedCore } from '../core-index.js'; const deleteUserCartService = async ( - userId: string, - unchainedAPI: UnchainedCore & { countryContext?: string }, + userId: string, + unchainedAPI: UnchainedCore & { countryContext?: string }, ) => { - try { - const user = await unchainedAPI.modules.users.findUserById(userId); - const userCart = await unchainedAPI.modules.orders.cart({ - userId, - countryContext: unchainedAPI?.countryContext || user?.lastLogin?.countryCode, - }); - await unchainedAPI.modules.orders.delete(userCart?._id); - await unchainedAPI.modules.orders.payments.deleteOrderPayment(userCart?._id); - await unchainedAPI.modules.orders.deliveries.deleteOrderDelivery(userCart?._id); - await unchainedAPI.modules.orders.discounts.deleteOrderDiscounts(userCart?._id); - return true; - } catch (e) { - console.error(e); - return false; - } + try { + const user = await unchainedAPI.modules.users.findUserById(userId); + const userCart = await unchainedAPI.modules.orders.cart({ + userId, + countryContext: unchainedAPI?.countryContext || user?.lastLogin?.countryCode, + }); + await unchainedAPI.modules.orders.delete(userCart?._id); + await unchainedAPI.modules.orders.payments.deleteOrderPayment(userCart?._id); + await unchainedAPI.modules.orders.deliveries.deleteOrderDelivery(userCart?._id); + await unchainedAPI.modules.orders.discounts.deleteOrderDiscounts(userCart?._id); + return true; + } catch (e) { + console.error(e); + return false; + } }; export default deleteUserCartService; From 6a4849fa37952085f0e0db9b0554faed9e05bcb9 Mon Sep 17 00:00:00 2001 From: Pascal Kaufmann Date: Fri, 13 Dec 2024 14:03:35 +0100 Subject: [PATCH 16/16] Cleanup --- changes.diff | 449 ------------------ packages/api/src/resolvers/mutations/index.ts | 4 +- .../resolvers/mutations/users/removeUser.ts | 11 +- .../removeUserProductReviews.ts} | 11 +- .../mutations/users/updateUserProfile.ts | 2 +- packages/api/src/roles/index.ts | 1 - packages/api/src/roles/loggedIn.ts | 1 - packages/api/src/schema/mutation.ts | 6 +- .../src/module/configureEnrollmentsModule.ts | 9 +- .../module/configureOrderDeliveriesModule.ts | 4 +- .../module/configureOrderPaymentsModule.ts | 4 +- packages/core-products/src/mock/product.ts | 2 - .../src/module/configureQuotationsModule.ts | 2 +- .../src/module/configureUsersModule.ts | 118 ++--- packages/core-users/src/types.ts | 1 - packages/core-users/tests/mock/user-mock.ts | 3 +- .../src/services/deleteUserCartService.ts | 24 - .../src/services/deleteUserCartsService.ts | 22 + packages/core/src/services/index.ts | 4 +- 19 files changed, 109 insertions(+), 569 deletions(-) delete mode 100644 changes.diff rename packages/api/src/resolvers/mutations/{products/deleteUserProductReviews.ts => users/removeUserProductReviews.ts} (57%) delete mode 100644 packages/core/src/services/deleteUserCartService.ts create mode 100644 packages/core/src/services/deleteUserCartsService.ts diff --git a/changes.diff b/changes.diff deleted file mode 100644 index c79852d4d3..0000000000 --- a/changes.diff +++ /dev/null @@ -1,449 +0,0 @@ -diff --git a/examples/kitchensink/boot.ts b/examples/kitchensink/boot.ts -index 4569499c1..ccb117fd8 100644 ---- a/examples/kitchensink/boot.ts -+++ b/examples/kitchensink/boot.ts -@@ -35,6 +35,9 @@ const start = async () => { - }), - ], - options: { -+ users: { -+ enableRightToBeForgotten: true, -+ }, - payment: { - filterSupportedProviders: async ({ providers }) => { - return providers.sort((left, right) => { -diff --git a/packages/api/src/resolvers/mutations/index.ts b/packages/api/src/resolvers/mutations/index.ts -index 623457583..79eea4b6b 100755 ---- a/packages/api/src/resolvers/mutations/index.ts -+++ b/packages/api/src/resolvers/mutations/index.ts -@@ -153,6 +153,7 @@ import prepareUserAvatarUpload from './users/prepareUserAvatarUpload.js'; - import rejectOrder from './orders/rejectOrder.js'; - import removePushSubscription from './users/removePushSubscription.js'; - import addPushSubscription from './users/addPushSubscription.js'; -+import deleteAccount from './users/deleteAccount.js'; - - export default { - logout: acl(actions.logout)(logout), -@@ -318,4 +319,5 @@ export default { - signPaymentProviderForCheckout: acl(actions.registerPaymentCredentials)( - signPaymentProviderForCheckout, - ), -+ deleteAccount, - }; -diff --git a/packages/api/src/resolvers/mutations/users/deleteAccount.ts b/packages/api/src/resolvers/mutations/users/deleteAccount.ts -new file mode 100644 -index 000000000..4badfe509 ---- /dev/null -+++ b/packages/api/src/resolvers/mutations/users/deleteAccount.ts -@@ -0,0 +1,11 @@ -+import { Context } from '@unchainedshop/types/api.js'; -+import { log } from '@unchainedshop/logger'; -+ -+const deleteAccount = async (_, { userId }, context: Context) => { -+ const { modules, userAgent } = context; -+ log(`mutation deleteAccount ${userId} ${userAgent}`, { userId }); -+ await modules.users.deleteAccount({ userId }, context); -+ return true; -+}; -+ -+export default deleteAccount; -diff --git a/packages/api/src/schema/mutation.ts b/packages/api/src/schema/mutation.ts -index 86555f398..2e6f27878 100644 ---- a/packages/api/src/schema/mutation.ts -+++ b/packages/api/src/schema/mutation.ts -@@ -884,6 +884,7 @@ export default [ - Remove user W3C push subscription object - """ - removePushSubscription(p256dh: String!): User! -+ deleteAccount(userId: ID): Boolean! - } - `, - ]; -diff --git a/packages/core-orders/src/module/configureOrderDeliveriesModule.ts b/packages/core-orders/src/module/configureOrderDeliveriesModule.ts -index 8c1f082b3..37630d3f8 100644 ---- a/packages/core-orders/src/module/configureOrderDeliveriesModule.ts -+++ b/packages/core-orders/src/module/configureOrderDeliveriesModule.ts -@@ -232,5 +232,12 @@ export const configureOrderDeliveriesModule = ({ - }, - ); - }, -+ deleteUserOrderDeliveriesByOrderIds: async (orderIds) => { -+ log(`OrderDelivery -> Delete User order deliveries`, { -+ orderIds, -+ }); -+ const deleteUserOrdersResult = await OrderDeliveries.deleteMany({ orderId: { $in: orderIds } }); -+ return deleteUserOrdersResult.deletedCount; -+ }, - }; - }; -diff --git a/packages/core-orders/src/module/configureOrderDiscountsModule.ts b/packages/core-orders/src/module/configureOrderDiscountsModule.ts -index a92a038a9..818d82e2d 100644 ---- a/packages/core-orders/src/module/configureOrderDiscountsModule.ts -+++ b/packages/core-orders/src/module/configureOrderDiscountsModule.ts -@@ -231,5 +231,12 @@ export const configureOrderDiscountsModule = ({ - await emit('ORDER_UPDATE_DISCOUNT', { discount }); - return discount; - }, -+ deleteUserOrderDiscountsByOrderIds: async (orderIds) => { -+ log(`OrderDiscounts -> Delete User orders discount`, { -+ orderIds, -+ }); -+ const deleteUserOrdersResult = await OrderDiscounts.deleteMany({ orderId: { $in: orderIds } }); -+ return deleteUserOrdersResult.deletedCount; -+ }, - }; - }; -diff --git a/packages/core-orders/src/module/configureOrderPaymentsModule.ts b/packages/core-orders/src/module/configureOrderPaymentsModule.ts -index 763d81ae8..33f9e40c9 100644 ---- a/packages/core-orders/src/module/configureOrderPaymentsModule.ts -+++ b/packages/core-orders/src/module/configureOrderPaymentsModule.ts -@@ -24,9 +24,9 @@ export const buildFindByContextDataSelector = (context: any): mongodb.Filter - context[key] !== undefined - ? { -- ...currentSelector, -- [`context.${key}`]: context[key], -- } -+ ...currentSelector, -+ [`context.${key}`]: context[key], -+ } - : currentSelector, - {}, - ); -@@ -352,5 +352,12 @@ export const configureOrderPaymentsModule = ({ - }, - ); - }, -+ deleteUserOrderPaymentsByOrderIds: async (orderIds) => { -+ log(`OrderPayment -> Delete User orders payment`, { -+ orderIds, -+ }); -+ const deleteUserOrdersResult = await OrderPayments.deleteMany({ orderId: { $in: orderIds } }); -+ return deleteUserOrdersResult.deletedCount; -+ }, - }; - }; -diff --git a/packages/core-orders/src/module/configureOrderPositionsModule.ts b/packages/core-orders/src/module/configureOrderPositionsModule.ts -index 6455c9336..fadbfddd7 100644 ---- a/packages/core-orders/src/module/configureOrderPositionsModule.ts -+++ b/packages/core-orders/src/module/configureOrderPositionsModule.ts -@@ -84,8 +84,7 @@ export const configureOrderPositionsModule = ({ - const originalProductId = originalProduct ? originalProduct._id : undefined; - - log( -- `Create ${quantity}x Position with Product ${productId} ${ -- quotationId ? ` (${quotationId})` : '' -+ `Create ${quantity}x Position with Product ${productId} ${quotationId ? ` (${quotationId})` : '' - }`, - { orderId, productId, originalProductId }, - ); -@@ -372,5 +371,12 @@ export const configureOrderPositionsModule = ({ - - return upsertedOrderPosition; - }, -+ deleteUserOrderPositionsByOrderIds: async (orderIds) => { -+ log(`OrderPosition -> Delete User orders`, { -+ orderIds, -+ }); -+ const deleteUserOrdersResult = await OrderPositions.deleteMany({ orderId: { $in: orderIds } }); -+ return deleteUserOrdersResult.deletedCount; -+ }, - }; - }; -diff --git a/packages/core-orders/src/module/configureOrdersModule-mutations.ts b/packages/core-orders/src/module/configureOrdersModule-mutations.ts -index 7ed47720e..acca8370e 100644 ---- a/packages/core-orders/src/module/configureOrdersModule-mutations.ts -+++ b/packages/core-orders/src/module/configureOrdersModule-mutations.ts -@@ -236,7 +236,13 @@ export const configureOrderModuleMutations = ({ - } - return null; - }, -- - updateCalculation, -+ deleteUserOrders: async (userId) => { -+ log(`OrderPosition -> Delete User orders`, { -+ userId, -+ }); -+ const deletedUserOrdersResult = await Orders.deleteMany({ userId }); -+ return deletedUserOrdersResult.deletedCount; -+ }, - }; - }; -diff --git a/packages/core-products/src/module/configureProductReviewsModule.ts b/packages/core-products/src/module/configureProductReviewsModule.ts -index 84f7e9bf3..92469c54c 100644 ---- a/packages/core-products/src/module/configureProductReviewsModule.ts -+++ b/packages/core-products/src/module/configureProductReviewsModule.ts -@@ -170,7 +170,6 @@ export const configureProductReviewsModule = async ({ - - return productReview; - }, -- - votes: { - userIdsThatVoted, - -diff --git a/packages/core-quotations/src/quotations-index.ts b/packages/core-quotations/src/quotations-index.ts -index 9a60f6a01..2a4c4aea6 100644 ---- a/packages/core-quotations/src/quotations-index.ts -+++ b/packages/core-quotations/src/quotations-index.ts -@@ -1,7 +1,6 @@ - export { configureQuotationsModule } from './module/configureQuotationsModule.js'; - - export { QuotationStatus } from './db/QuotationStatus.js'; -- - export { QuotationAdapter } from './director/QuotationAdapter.js'; - export { QuotationDirector } from './director/QuotationDirector.js'; - -diff --git a/packages/core-users/src/module/configureUsersModule.ts b/packages/core-users/src/module/configureUsersModule.ts -index 592cc96e0..f799e1dca 100644 ---- a/packages/core-users/src/module/configureUsersModule.ts -+++ b/packages/core-users/src/module/configureUsersModule.ts -@@ -1,7 +1,6 @@ - import localePkg from 'locale'; - import bcrypt from 'bcryptjs'; - import { Address, Contact } from '@unchainedshop/types/common.js'; --import { ModuleInput, UnchainedCore } from '@unchainedshop/types/core.js'; - import { - User, - UserQuery, -@@ -10,7 +9,9 @@ import { - UserProfile, - UserSettingsOptions, - UserData, -+ UsersModule, - } from '@unchainedshop/types/user.js'; -+import { ModuleInput, UnchainedCore } from '@unchainedshop/types/core.js'; - import { emit, registerEvents } from '@unchainedshop/events'; - import { - generateDbFilterById, -@@ -19,6 +20,7 @@ import { - generateDbObjectId, - } from '@unchainedshop/mongodb'; - import { systemLocale } from '@unchainedshop/utils'; -+import crypto from 'crypto'; - import { FileDirector } from '@unchainedshop/file-upload'; - import { SortDirection, SortOption } from '@unchainedshop/types/api.js'; - import { UsersCollection } from '../db/UsersCollection.js'; -@@ -28,6 +30,38 @@ import { configureUsersWebAuthnModule } from './configureUsersWebAuthnModule.js' - import * as pbkdf2 from './pbkdf2.js'; - import * as sha256 from './sha256.js'; - -+const isDate = (value) => { -+ const date = new Date(value); -+ return !Number.isNaN(date.getTime()); -+}; -+ -+function maskString(value) { -+ if (isDate(value)) return value; -+ return crypto -+ .createHash('sha256') -+ .update(JSON.stringify([value, new Date().getTime()])) -+ .digest('hex'); -+} -+ -+const maskUserPropertyValues = (user) => { -+ if (typeof user !== 'object' || user === null) { -+ return user; -+ } -+ if (Array.isArray(user)) { -+ return user.map((item) => maskUserPropertyValues(item)); -+ } -+ const maskedUser = {}; -+ Object.keys(user).forEach((key) => { -+ if (typeof user[key] === 'string' || isDate(user[key])) { -+ maskedUser[key] = maskString(user[key]); -+ } else { -+ maskedUser[key] = maskUserPropertyValues(user[key]); -+ } -+ }); -+ -+ return maskedUser; -+}; -+ - const { Locale } = localePkg; - - const USER_EVENTS = [ -@@ -72,9 +106,7 @@ export const configureUsersModule = async ({ - db, - options, - migrationRepository, --}: ModuleInput) => { -- userSettings.configureSettings(options || {}, db); -- -+}: ModuleInput): Promise => { - registerEvents(USER_EVENTS); - const Users = await UsersCollection(db); - -@@ -753,5 +785,16 @@ export const configureUsersModule = async ({ - {}, - ); - }, -+ deleteAccount: async ({ userId }, context) => { -+ if (!options?.enableRightToBeForgotten) throw Error('Right to be forgotten is disabled'); -+ const { modules } = context; -+ const { _id, ...user } = await modules.users.findUserById(userId); -+ delete user?.services; -+ -+ const maskedUserData = maskUserPropertyValues({ ...user, meta: null }); -+ await modules.bookmarks.deleteByUserId(userId); -+ await modules.users.updateUser({ _id }, { $set: { ...maskedUserData, deleted: new Date() } }, {}); -+ return true; -+ }, - }; - }; -diff --git a/packages/core/src/core-index.ts b/packages/core/src/core-index.ts -index c375ac78a..4211ba194 100644 ---- a/packages/core/src/core-index.ts -+++ b/packages/core/src/core-index.ts -@@ -96,6 +96,7 @@ export const initCore = async ({ - }); - const users = await configureUsersModule({ - db, -+ options: options.users, - migrationRepository, - }); - const warehousing = await configureWarehousingModule({ -diff --git a/packages/types/enrollments.ts b/packages/types/enrollments.ts -index ebbf5829a..b50e83597 100644 ---- a/packages/types/enrollments.ts -+++ b/packages/types/enrollments.ts -@@ -216,7 +216,6 @@ export type IEnrollmentDirector = IBaseDirector & { - /* - * Settings - */ -- - export interface EnrollmentsSettingsOptions { - autoSchedulingSchedule?: WorkerSchedule; - enrollmentNumberHashFn?: (enrollment: Enrollment, index: number) => string; -diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts -index b59d8d5a9..275c8811c 100644 ---- a/packages/types/index.d.ts -+++ b/packages/types/index.d.ts -@@ -88,6 +88,9 @@ import { - QuotationsSettingsOptions, - QuotationStatus as QuotationStatusType, - } from './quotations.js'; -+ -+import { UserServices, UserSettingsOptions, UsersModule } from './user.js'; -+ - import { - IWarehousingAdapter, - IWarehousingDirector, -@@ -347,6 +350,12 @@ declare module '@unchainedshop/core-quotations' { - const QuotationError: typeof QuotationErrorType; - } - -+declare module '@unchainedshop/core-users' { -+ function configureUsersModule(params: ModuleInput): Promise; -+ -+ const userServices: UserServices; -+} -+ - declare module '@unchainedshop/core-warehousing' { - function configureWarehousingModule( - params: ModuleInput, -diff --git a/packages/types/modules.ts b/packages/types/modules.ts -index 94bfb6386..db9d7d151 100644 ---- a/packages/types/modules.ts -+++ b/packages/types/modules.ts -@@ -13,7 +13,7 @@ import { OrdersModule, OrdersSettingsOptions } from './orders.js'; - import { PaymentModule, PaymentSettingsOptions } from './payments.js'; - import { ProductsModule, ProductsSettingsOptions } from './products.js'; - import { QuotationsModule, QuotationsSettingsOptions } from './quotations.js'; --import { UsersModule, UserSettingsOptions } from './user.js'; -+import { UserSettingsOptions, UsersModule } from './user.js'; - import { WarehousingModule } from './warehousing.js'; - import { WorkerModule, WorkerSettingsOptions } from './worker.js'; - -diff --git a/packages/types/orders.deliveries.ts b/packages/types/orders.deliveries.ts -index d97a9efcc..e37e72713 100644 ---- a/packages/types/orders.deliveries.ts -+++ b/packages/types/orders.deliveries.ts -@@ -72,6 +72,7 @@ export type OrderDeliveriesModule = { - orderDelivery: OrderDelivery, - unchainedAPI: UnchainedCore, - ) => Promise; -+ deleteUserOrderDeliveriesByOrderIds: (orderIds: string[]) => Promise; - }; - - export type OrderDeliveryDiscount = Omit & { -diff --git a/packages/types/orders.discounts.ts b/packages/types/orders.discounts.ts -index 84254d316..458800db5 100644 ---- a/packages/types/orders.discounts.ts -+++ b/packages/types/orders.discounts.ts -@@ -52,4 +52,5 @@ export type OrderDiscountsModule = { - create: (doc: OrderDiscount) => Promise; - update: (orderDiscountId: string, doc: OrderDiscount) => Promise; - delete: (orderDiscountId: string, unchainedAPI: UnchainedCore) => Promise; -+ deleteUserOrderDiscountsByOrderIds: (orderIds: string[]) => Promise; - }; -diff --git a/packages/types/orders.payments.ts b/packages/types/orders.payments.ts -index c9e87dc88..0ee16eed9 100644 ---- a/packages/types/orders.payments.ts -+++ b/packages/types/orders.payments.ts -@@ -111,6 +111,7 @@ export type OrderPaymentsModule = { - ) => Promise; - - updateCalculation: (orderPayment: OrderPayment, unchainedAPI: UnchainedCore) => Promise; -+ deleteUserOrderPaymentsByOrderIds: (orderIds: string[]) => Promise; - }; - - export type OrderPaymentDiscount = Omit & { -diff --git a/packages/types/orders.positions.ts b/packages/types/orders.positions.ts -index e4331eb5a..7ef121d86 100644 ---- a/packages/types/orders.positions.ts -+++ b/packages/types/orders.positions.ts -@@ -91,6 +91,7 @@ export type OrderPositionsModule = { - params: { order: Order; product: Product }, - unchainedAPI: UnchainedCore, - ) => Promise; -+ deleteUserOrderPositionsByOrderIds: (orderIds: string[]) => Promise; - }; - - export type OrderPositionDiscount = Omit & { -diff --git a/packages/types/orders.ts b/packages/types/orders.ts -index 9502b83b7..0e67a689a 100644 ---- a/packages/types/orders.ts -+++ b/packages/types/orders.ts -@@ -147,6 +147,7 @@ export interface OrderMutations { - updateContact: (orderId: string, contact: Contact, unchainedAPI: UnchainedCore) => Promise; - updateContext: (orderId: string, context: any, unchainedAPI: UnchainedCore) => Promise; - updateCalculation: (orderId: string, unchainedAPI: UnchainedCore) => Promise; -+ deleteUserOrders: (userId: string) => Promise; - } - - export type OrdersModule = OrderQueries & -diff --git a/packages/types/quotations.ts b/packages/types/quotations.ts -index f1125d4ca..4b8b0a5a6 100644 ---- a/packages/types/quotations.ts -+++ b/packages/types/quotations.ts -@@ -154,7 +154,6 @@ export type IQuotationDirector = IBaseDirector & { - /* - * Settings - */ -- - export interface QuotationsSettingsOptions { - quotationNumberHashFn?: (quotation: Quotation, index: number) => string; - } -diff --git a/packages/types/user.ts b/packages/types/user.ts -index 3e4884c27..70a9203aa 100644 ---- a/packages/types/user.ts -+++ b/packages/types/user.ts -@@ -110,6 +110,7 @@ export interface UserSettingsOptions { - validateUsername?: (username: string) => Promise; - validateNewUser?: (user: Partial) => Promise; - validatePassword?: (password: string) => Promise; -+ enableRightToBeForgotten?: boolean; - } - export interface UserSettings { - mergeUserCartsOnLogin: boolean; -@@ -215,6 +216,10 @@ export type UsersModule = { - }, - ) => Promise; - removePushSubscription: (userId: string, p256dh: string) => Promise; -+ deleteAccount: (params: { userId?: string }, context: UnchainedCore) => Promise; -+ hashPassword(password: string): Promise<{ -+ pbkdf2: string; -+ }>; - }; - - /* diff --git a/packages/api/src/resolvers/mutations/index.ts b/packages/api/src/resolvers/mutations/index.ts index 2456ce5c77..13386c2be8 100755 --- a/packages/api/src/resolvers/mutations/index.ts +++ b/packages/api/src/resolvers/mutations/index.ts @@ -150,7 +150,7 @@ import prepareUserAvatarUpload from './users/prepareUserAvatarUpload.js'; import rejectOrder from './orders/rejectOrder.js'; import removePushSubscription from './users/removePushSubscription.js'; import addPushSubscription from './users/addPushSubscription.js'; -import deleteUserProductReviews from './products/deleteUserProductReviews.js'; +import removeUserProductReviews from './users/removeUserProductReviews.js'; export default { logout: acl(actions.logout)(logout), @@ -313,5 +313,5 @@ export default { signPaymentProviderForCheckout: acl(actions.registerPaymentCredentials)( signPaymentProviderForCheckout, ), - deleteUserProductReviews, + removeUserProductReviews, }; diff --git a/packages/api/src/resolvers/mutations/users/removeUser.ts b/packages/api/src/resolvers/mutations/users/removeUser.ts index 20796ab26f..445f8dd6ab 100755 --- a/packages/api/src/resolvers/mutations/users/removeUser.ts +++ b/packages/api/src/resolvers/mutations/users/removeUser.ts @@ -5,16 +5,19 @@ import { UserNotFoundError } from '../../../errors.js'; export default async function removeUser( root: never, params: { userId: string; removeUserReviews?: boolean }, - unchainedApi: Context, + unchainedAPI: Context, ) { - const { modules, userId } = unchainedApi; + const { modules, userId } = unchainedAPI; const { userId: paramUserId, removeUserReviews = false } = params; const normalizedUserId = paramUserId || userId; log(`mutation removeUser ${normalizedUserId}`, { userId }); if (!(await modules.users.userExists({ userId: normalizedUserId }))) - throw new UserNotFoundError({ id: normalizedUserId }); + throw new UserNotFoundError({ userId: normalizedUserId }); - return modules.users.delete({ userId: normalizedUserId, removeUserReviews }, unchainedApi); + if (removeUserReviews) { + await modules.products.reviews.deleteMany({ authorId: userId }); + } + return modules.users.delete({ userId: normalizedUserId }, unchainedAPI); } diff --git a/packages/api/src/resolvers/mutations/products/deleteUserProductReviews.ts b/packages/api/src/resolvers/mutations/users/removeUserProductReviews.ts similarity index 57% rename from packages/api/src/resolvers/mutations/products/deleteUserProductReviews.ts rename to packages/api/src/resolvers/mutations/users/removeUserProductReviews.ts index fc30b9e85c..ce01141772 100644 --- a/packages/api/src/resolvers/mutations/products/deleteUserProductReviews.ts +++ b/packages/api/src/resolvers/mutations/users/removeUserProductReviews.ts @@ -1,8 +1,8 @@ import { log } from '@unchainedshop/logger'; -import { InvalidIdError, UserNotFoundError } from '../../../errors.js'; +import { InvalidIdError } from '../../../errors.js'; import { Context } from '../../../context.js'; -export default async function deleteUserProductReviews( +export default async function removeUserProductReviews( root: never, params: { userId?: string; @@ -10,12 +10,13 @@ export default async function deleteUserProductReviews( { modules, userId: currentUserId }: Context, ) { const normalizedUserId = params?.userId || currentUserId; - log(`mutation deleteUserProductReviews ${normalizedUserId}`, { + log(`mutation removeUserProductReviews ${normalizedUserId}`, { userId: currentUserId, }); if (!normalizedUserId) throw new InvalidIdError({ userId: normalizedUserId }); - if (!(await modules.users.userExists({ userId: normalizedUserId }))) - throw new UserNotFoundError({ userId: normalizedUserId }); + + // Do not check for existance of user as the existance check would return false if the user is in status + // 'deleted' and we still want to remove the reviews in that case await modules.products.reviews.deleteMany({ authorId: normalizedUserId }); return true; diff --git a/packages/api/src/resolvers/mutations/users/updateUserProfile.ts b/packages/api/src/resolvers/mutations/users/updateUserProfile.ts index 2ec77ca986..f5db2a2f5c 100755 --- a/packages/api/src/resolvers/mutations/users/updateUserProfile.ts +++ b/packages/api/src/resolvers/mutations/users/updateUserProfile.ts @@ -14,7 +14,7 @@ export default async function updateUserProfile( log(`mutation updateUserProfile ${normalizedUserId}`, { userId }); if (!(await modules.users.userExists({ userId: normalizedUserId }))) - throw UserNotFoundError({ id: normalizedUserId }); + throw UserNotFoundError({ userId: normalizedUserId }); return modules.users.updateProfile(normalizedUserId, profile); } diff --git a/packages/api/src/roles/index.ts b/packages/api/src/roles/index.ts index cc2c0321f6..e127388e62 100644 --- a/packages/api/src/roles/index.ts +++ b/packages/api/src/roles/index.ts @@ -114,7 +114,6 @@ const actions: Record = [ 'confirmMediaUpload', 'viewStatistics', 'removeUser', - 'deleteUserProductReviews', ].reduce((oldValue, actionValue) => { const newValue = oldValue; newValue[actionValue] = actionValue; diff --git a/packages/api/src/roles/loggedIn.ts b/packages/api/src/roles/loggedIn.ts index e04c1664b6..55cc2bfbfb 100644 --- a/packages/api/src/roles/loggedIn.ts +++ b/packages/api/src/roles/loggedIn.ts @@ -203,7 +203,6 @@ export const loggedIn = (role: any, actions: Record) => { role.allow(actions.viewUserTokens, isMyself); role.allow(actions.updateUser, isMyself); role.allow(actions.removeUser, isMyself); - role.allow(actions.deleteUserProductReviews, isMyself); role.allow(actions.sendEmail, isOwnedEmailAddress); role.allow(actions.viewOrder, isOwnedOrder); role.allow(actions.updateOrder, isOwnedOrder); diff --git a/packages/api/src/schema/mutation.ts b/packages/api/src/schema/mutation.ts index 39b62b90cf..0b1c5cd8b0 100644 --- a/packages/api/src/schema/mutation.ts +++ b/packages/api/src/schema/mutation.ts @@ -306,6 +306,11 @@ export default [ """ removeUser(userId: ID, removeUserReviews: Boolean): User! + """ + Remove product reviews of a user + """ + removeUserProductReviews(userId: ID!): Boolean! + """ Enroll a new user, setting enroll to true will let the user choose his password (e-mail gets sent) """ @@ -867,7 +872,6 @@ export default [ Remove user W3C push subscription object """ removePushSubscription(p256dh: String!): User! - deleteUserProductReviews(userId: ID): Boolean } `, ]; diff --git a/packages/core-enrollments/src/module/configureEnrollmentsModule.ts b/packages/core-enrollments/src/module/configureEnrollmentsModule.ts index 8c6cbfc0ce..ae02512223 100644 --- a/packages/core-enrollments/src/module/configureEnrollmentsModule.ts +++ b/packages/core-enrollments/src/module/configureEnrollmentsModule.ts @@ -99,7 +99,7 @@ export interface EnrollmentMutations { params: { status: EnrollmentStatus; info?: string }, unchainedAPI, ) => Promise; - deleteOpenUserEnrollments: (userId: string) => Promise; + deleteInactiveUserEnrollments: (userId: string) => Promise; } export type EnrollmentsModule = EnrollmentQueries & @@ -555,8 +555,11 @@ export const configureEnrollmentsModule = async ({ }, updateStatus, - deleteOpenUserEnrollments: async (userId: string) => { - const { deletedCount } = await Enrollments.deleteMany({ userId }); + deleteInactiveUserEnrollments: async (userId: string) => { + const { deletedCount } = await Enrollments.deleteMany({ + userId, + status: { $in: [null, EnrollmentStatus.INITIAL, EnrollmentStatus.TERMINATED] }, + }); return deletedCount; }, }; diff --git a/packages/core-orders/src/module/configureOrderDeliveriesModule.ts b/packages/core-orders/src/module/configureOrderDeliveriesModule.ts index 3d557ace70..c543393f1b 100644 --- a/packages/core-orders/src/module/configureOrderDeliveriesModule.ts +++ b/packages/core-orders/src/module/configureOrderDeliveriesModule.ts @@ -45,7 +45,7 @@ export type OrderDeliveriesModule = { ) => Promise; updateCalculation: (orderDelivery: OrderDelivery, unchainedAPI) => Promise; - deleteOrderDelivery: (orderId: string) => Promise; + deleteOrderDeliveries: (orderId: string) => Promise; }; const ORDER_DELIVERY_EVENTS: string[] = ['ORDER_DELIVER', 'ORDER_UPDATE_DELIVERY']; @@ -271,7 +271,7 @@ export const configureOrderDeliveriesModule = ({ }, ); }, - deleteOrderDelivery: async (orderId: string) => { + deleteOrderDeliveries: async (orderId: string) => { const { deletedCount } = await OrderDeliveries.deleteMany({ orderId }); return deletedCount; }, diff --git a/packages/core-orders/src/module/configureOrderPaymentsModule.ts b/packages/core-orders/src/module/configureOrderPaymentsModule.ts index 304622b7af..f23a02a934 100644 --- a/packages/core-orders/src/module/configureOrderPaymentsModule.ts +++ b/packages/core-orders/src/module/configureOrderPaymentsModule.ts @@ -80,7 +80,7 @@ export type OrderPaymentsModule = { ) => Promise; updateCalculation: (orderPayment: OrderPayment, unchainedAPI) => Promise; - deleteOrderPayment: (orderId: string) => Promise; + deleteOrderPayments: (orderId: string) => Promise; }; const ORDER_PAYMENT_EVENTS: string[] = ['ORDER_UPDATE_PAYMENT', 'ORDER_SIGN_PAYMENT', 'ORDER_PAY']; @@ -410,7 +410,7 @@ export const configureOrderPaymentsModule = ({ }, ); }, - deleteOrderPayment: async (orderId: string) => { + deleteOrderPayments: async (orderId: string) => { const { deletedCount } = await OrderPayments.deleteMany({ orderId }); return deletedCount; }, diff --git a/packages/core-products/src/mock/product.ts b/packages/core-products/src/mock/product.ts index dbe8e816ae..4377ec2801 100644 --- a/packages/core-products/src/mock/product.ts +++ b/packages/core-products/src/mock/product.ts @@ -6,10 +6,8 @@ export default { authorId: 'PKve0k9fLCUzn2EUi', tags: [], created: new Date('2022-10-28T10:41:13.346Z'), - createdBy: 'PKve0k9fLCUzn2EUi', slugs: ['test'], updated: new Date('2022-12-04T13:50:24.245Z'), - updatedBy: 'PKve0k9fLCUzn2EUi', commerce: { pricing: [ { diff --git a/packages/core-quotations/src/module/configureQuotationsModule.ts b/packages/core-quotations/src/module/configureQuotationsModule.ts index 9afed0a874..00a42ed27c 100644 --- a/packages/core-quotations/src/module/configureQuotationsModule.ts +++ b/packages/core-quotations/src/module/configureQuotationsModule.ts @@ -402,7 +402,7 @@ export const configureQuotationsModule = async ({ deleteRequestedUserQuotations: async (userId: string) => { const { deletedCount } = await Quotations.deleteMany({ userId, - status: QuotationStatus.REQUESTED, + status: { $in: [QuotationStatus.REQUESTED, null] }, }); return deletedCount; }, diff --git a/packages/core-users/src/module/configureUsersModule.ts b/packages/core-users/src/module/configureUsersModule.ts index 287e33af72..7564820619 100644 --- a/packages/core-users/src/module/configureUsersModule.ts +++ b/packages/core-users/src/module/configureUsersModule.ts @@ -17,28 +17,6 @@ import * as pbkdf2 from './pbkdf2.js'; import * as sha256 from './sha256.js'; import crypto from 'crypto'; import { UnchainedCore } from '@unchainedshop/core'; -import { Context } from 'vm'; - -const maskUserPropertyValues = (user, deletedById: string): User => { - if (!user || typeof user !== 'object') { - throw new Error('Invalid user object'); - } - return { - ...user, - username: `deleted-${Date.now()}`, - deleted: new Date(), - deletedBy: deletedById, - emails: null, - roles: null, - profile: null, - lastBillingAddress: {}, - services: {}, - pushSubscriptions: [], - avatarId: null, - initialPassword: null, - lastContact: null, - }; -}; export type UsersModule = { // Submodules @@ -95,7 +73,7 @@ export type UsersModule = { _id: string, { profile, meta }: { profile?: UserProfile; meta?: any }, ) => Promise; - delete: (params: { userId: string; removeUserReviews?: boolean }, context: Context) => Promise; + delete: (params: { userId: string }, context: UnchainedCore) => Promise; updateRoles: (_id: string, roles: Array) => Promise; updateTags: (_id: string, tags: Array) => Promise; updateUser: ( @@ -133,7 +111,6 @@ const USER_EVENTS = [ 'USER_UPDATE_BILLING_ADDRESS', 'USER_UPDATE_LAST_CONTACT', 'USER_REMOVE', - 'USER_PURGE', ]; export const removeConfidentialServiceHashes = (rawUser: User): User => { const user = rawUser; @@ -164,25 +141,6 @@ export const configureUsersModule = async ({ const webAuthn = await configureUsersWebAuthnModule({ db, options }); - const findUserById = async (userId: string): Promise => { - if (!userId) return null; - return Users.findOne(generateDbFilterById(userId), {}); - }; - const updateUser = async ( - selector: mongodb.Filter, - modifier: mongodb.UpdateFilter, - updateOptions?: mongodb.FindOneAndUpdateOptions, - ): Promise => { - const user = await Users.findOneAndUpdate(selector, modifier, { - ...updateOptions, - returnDocument: 'after', - }); - await emit('USER_UPDATE', { - user: removeConfidentialServiceHashes(user), - }); - return user; - }; - return { // Queries webAuthn, @@ -190,7 +148,12 @@ export const configureUsersModule = async ({ const userCount = await Users.countDocuments(buildFindSelector(query)); return userCount; }, - findUserById, + + findUserById: async (userId: string): Promise => { + if (!userId) return null; + return Users.findOne(generateDbFilterById(userId), {}); + }, + async findUserByUsername(username: string): Promise { if (!username) return null; return Users.findOne({ username }, {}); @@ -317,9 +280,7 @@ export const configureUsersModule = async ({ }, async userExists({ userId }: { userId: string }): Promise { - const selector = generateDbFilterById(userId); - selector.deleted = null; // skip deleted users when checked for existance! - const userCount = await Users.countDocuments(selector, { limit: 1 }); + const userCount = await Users.countDocuments({ _id: userId, deleted: null }, { limit: 1 }); return userCount === 1; }, @@ -624,40 +585,50 @@ export const configureUsersModule = async ({ return user; }, - delete: async ( - params: { userId: string; removeUserReviews?: boolean }, - context: Context, - ): Promise => { + delete: async ({ userId }: { userId: string }, context: UnchainedCore): Promise => { const { services, modules } = context as UnchainedCore; - const { userId, removeUserReviews = false } = params; - const userFilter = generateDbFilterById(userId); - const existingUser = await Users.findOne(userFilter, { - projection: { emails: true, username: true }, - }); - if (!existingUser) return null; - const maskedUserData = maskUserPropertyValues(existingUser, context?.userId); - await updateUser({ _id: userId }, { $set: { ...maskedUserData, deleted: new Date() } }, {}); + const user = await Users.findOneAndUpdate( + { _id: userId }, + { + $set: { + username: `deleted-${Date.now()}`, + deleted: new Date(), + emails: [], + roles: [], + profile: null, + lastBillingAddress: null, + services: {}, + pushSubscriptions: [], + avatarId: null, + initialPassword: false, + lastContact: null, + lastLogin: null, + }, + }, + { returnDocument: 'after' }, + ); + + if (!user) return null; await modules.bookmarks.deleteByUserId(userId); - await services.orders.deleteUserCart(userId, context as UnchainedCore); + await services.orders.deleteUserCarts(userId, context as UnchainedCore); await modules.quotations.deleteRequestedUserQuotations(userId); - await modules.enrollments.deleteOpenUserEnrollments(userId); - if (removeUserReviews) await modules.products.reviews.deleteMany({ authorId: userId }); + await modules.enrollments.deleteInactiveUserEnrollments(userId); const ordersCount = await modules.orders.count({ userId, includeCarts: true }); const quotationsCount = await modules.quotations.count({ userId }); const reviewsCount = await modules.products.reviews.count({ authorId: userId }); const enrollmentsCount = await modules.enrollments.count({ userId }); - const tokens = await modules.warehousing.findTokensForUser(existingUser); + const tokens = await modules.warehousing.findTokensForUser(user); if (!ordersCount && !reviewsCount && !enrollmentsCount && !quotationsCount && !tokens?.length) { await Users.deleteOne({ _id: userId }); } await emit('USER_REMOVE', { - user: removeConfidentialServiceHashes(existingUser), + user, }); - return existingUser; + return user; }, updateProfile: async ( @@ -794,7 +765,22 @@ export const configureUsersModule = async ({ }); return user; }, - updateUser, + + updateUser: async ( + selector: mongodb.Filter, + modifier: mongodb.UpdateFilter, + updateOptions?: mongodb.FindOneAndUpdateOptions, + ): Promise => { + const user = await Users.findOneAndUpdate(selector, modifier, { + ...updateOptions, + returnDocument: 'after', + }); + await emit('USER_UPDATE', { + user: removeConfidentialServiceHashes(user), + }); + return user; + }, + addPushSubscription: async ( userId: string, subscription: any, diff --git a/packages/core-users/src/types.ts b/packages/core-users/src/types.ts index c7bd08b5d9..e45fb4ca98 100644 --- a/packages/core-users/src/types.ts +++ b/packages/core-users/src/types.ts @@ -71,7 +71,6 @@ export type User = { pushSubscriptions: Array; username?: string; meta?: any; - deletedBy?: string; } & TimestampFields; export type UserQuery = mongodb.Filter & { diff --git a/packages/core-users/tests/mock/user-mock.ts b/packages/core-users/tests/mock/user-mock.ts index 116015668a..dcf1848db3 100644 --- a/packages/core-users/tests/mock/user-mock.ts +++ b/packages/core-users/tests/mock/user-mock.ts @@ -2,7 +2,7 @@ export default { _id: 'PKve0k9fLCUzn2EUi', guest: false, initialPassword: false, - lastBillingAddress: {}, + lastBillingAddress: null, profile: { address: { firstName: null, @@ -69,5 +69,4 @@ export default { countryCode: 'CH', }, updated: new Date('2022-11-30T11:02:19.624Z'), - updatedBy: 'PKve0k9fLCUzn2EUi', }; diff --git a/packages/core/src/services/deleteUserCartService.ts b/packages/core/src/services/deleteUserCartService.ts deleted file mode 100644 index 5cafeb68e5..0000000000 --- a/packages/core/src/services/deleteUserCartService.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { UnchainedCore } from '../core-index.js'; - -const deleteUserCartService = async ( - userId: string, - unchainedAPI: UnchainedCore & { countryContext?: string }, -) => { - try { - const user = await unchainedAPI.modules.users.findUserById(userId); - const userCart = await unchainedAPI.modules.orders.cart({ - userId, - countryContext: unchainedAPI?.countryContext || user?.lastLogin?.countryCode, - }); - await unchainedAPI.modules.orders.delete(userCart?._id); - await unchainedAPI.modules.orders.payments.deleteOrderPayment(userCart?._id); - await unchainedAPI.modules.orders.deliveries.deleteOrderDelivery(userCart?._id); - await unchainedAPI.modules.orders.discounts.deleteOrderDiscounts(userCart?._id); - return true; - } catch (e) { - console.error(e); - return false; - } -}; - -export default deleteUserCartService; diff --git a/packages/core/src/services/deleteUserCartsService.ts b/packages/core/src/services/deleteUserCartsService.ts new file mode 100644 index 0000000000..e94711102a --- /dev/null +++ b/packages/core/src/services/deleteUserCartsService.ts @@ -0,0 +1,22 @@ +import { UnchainedCore } from '../core-index.js'; + +export const deleteUserCartsService = async ( + userId: string, + unchainedAPI: UnchainedCore & { countryContext?: string }, +) => { + try { + const carts = await unchainedAPI.modules.orders.findOrders({ userId, status: null }); + + for (const userCart of carts) { + await unchainedAPI.modules.orders.positions.deleteOrderPositions(userCart?._id); + await unchainedAPI.modules.orders.payments.deleteOrderPayments(userCart?._id); + await unchainedAPI.modules.orders.deliveries.deleteOrderDeliveries(userCart?._id); + await unchainedAPI.modules.orders.discounts.deleteOrderDiscounts(userCart?._id); + await unchainedAPI.modules.orders.delete(userCart?._id); + } + return true; + } catch (e) { + console.error(e); + return false; + } +}; diff --git a/packages/core/src/services/index.ts b/packages/core/src/services/index.ts index 7d267c47ee..181ea5e0a3 100644 --- a/packages/core/src/services/index.ts +++ b/packages/core/src/services/index.ts @@ -12,7 +12,7 @@ import { nextUserCartService } from './nextUserCartService.js'; import { removeProductService } from './removeProductService.js'; import { initCartProvidersService } from './initCartProviders.js'; import { updateCalculationService } from './updateCalculationService.js'; -import deleteUserCartService from './deleteUserCartService.js'; +import { deleteUserCartsService } from './deleteUserCartsService.js'; const services = { bookmarks: { @@ -31,7 +31,7 @@ const services = { nextUserCart: nextUserCartService, initCartProviders: initCartProvidersService, updateCalculation: updateCalculationService, - deleteUserCart: deleteUserCartService, + deleteUserCarts: deleteUserCartsService, }, products: { removeProduct: removeProductService,