From 564b63181c53c2c1aedf523fa854ebb890b56a8b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Oct 2021 23:15:31 +0000 Subject: [PATCH 1/4] Bump sidekiq from 6.0.7 to 6.2.1 Bumps [sidekiq](https://github.com/mperham/sidekiq) from 6.0.7 to 6.2.1. - [Release notes](https://github.com/mperham/sidekiq/releases) - [Changelog](https://github.com/mperham/sidekiq/blob/main/Changes.md) - [Commits](https://github.com/mperham/sidekiq/compare/v6.0.7...v6.2.1) --- updated-dependencies: - dependency-name: sidekiq dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 731af7df2..493c901d3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -89,7 +89,7 @@ GEM coercible (1.0.0) descendants_tracker (~> 0.0.1) concurrent-ruby (1.1.8) - connection_pool (2.2.3) + connection_pool (2.2.5) coveralls_reborn (0.18.0) simplecov (>= 0.18.1, < 0.20.0) term-ansicolor (~> 1.6) @@ -271,8 +271,6 @@ GEM raabro (1.1.6) racc (1.5.2) rack (2.2.3) - rack-protection (2.0.8.1) - rack rack-proxy (0.6.5) rack rack-test (1.1.0) @@ -318,7 +316,7 @@ GEM recurrence (1.3.0) activesupport i18n - redis (4.1.4) + redis (4.4.0) repost (0.3.2) responders (2.4.1) actionpack (>= 4.2.0, < 6.0) @@ -348,11 +346,10 @@ GEM activesupport (>= 4) semantic_range (2.3.0) sha3 (1.0.1) - sidekiq (6.0.7) + sidekiq (6.2.1) connection_pool (>= 2.2.2) rack (~> 2.0) - rack-protection (>= 2.0.0) - redis (>= 4.1.0) + redis (>= 4.2.0) sidekiq-cron (1.1.0) fugit (~> 1.1) sidekiq (>= 4.2.1) From cf55b2eb1c0168ee9d1c776c72623cd12b01d913 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Oct 2021 18:12:38 +0000 Subject: [PATCH 2/4] Bump puma from 4.3.8 to 4.3.9 Bumps [puma](https://github.com/puma/puma) from 4.3.8 to 4.3.9. - [Release notes](https://github.com/puma/puma/releases) - [Changelog](https://github.com/puma/puma/blob/master/History.md) - [Commits](https://github.com/puma/puma/compare/v4.3.8...v4.3.9) --- updated-dependencies: - dependency-name: puma dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Gemfile | 2 +- Gemfile.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index ba51c9e79..85a7fb58f 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,7 @@ gem 'rails', '~> 5.2.4' # Used by rails 5.2 to reduce the app boot time by over 50% gem 'bootsnap' # Use Puma as web server -gem 'puma', '4.3.8' +gem 'puma', '4.3.9' gem 'webpacker', '~> 5.x' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder diff --git a/Gemfile.lock b/Gemfile.lock index 731af7df2..f1d962cc1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -216,7 +216,7 @@ GEM multi_json (1.14.1) multi_xml (0.6.0) multipart-post (2.1.1) - nio4r (2.5.7) + nio4r (2.5.8) nokogiri (1.11.4) mini_portile2 (~> 2.5.0) racc (~> 1.4) @@ -264,7 +264,7 @@ GEM prawn-table (0.2.2) prawn (>= 1.3.0, < 3.0.0) public_suffix (4.0.6) - puma (4.3.8) + puma (4.3.9) nio4r (~> 2.0) pundit (2.1.0) activesupport (>= 3.0.0) @@ -470,7 +470,7 @@ DEPENDENCIES pg_search prawn prawn-table - puma (= 4.3.8) + puma (= 4.3.9) pundit railroady rails (~> 5.2.4) From af6838a63ae4fb74da49c80bff6e799b54b207de Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 19 Oct 2021 10:25:19 +0200 Subject: [PATCH 3/4] updated changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7112194c4..69144e41f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ - Fix a security issue: updated url-parse to 1.5.3 to fix [CVE-2021-3664](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-3664) - Fix a security issue: updated axios to 0.21.2 to fix [CVE-2021-3749](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-3749) - Fix a security issue: updated nokogiri to 1.12.5 to fix [CVE-2021-41098](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-41098) +- Fix a security issue: updated puma to 4.3.9 to fix [CVE-2021-41136](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-41136) +- Fix a security issue: updated sidekiq to 6.2.1 to fix [CVE-2021-30151](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-30151) ## v5.1.10 2021 October 04 From 2d61dac9ccd1e97b15b821cf0847c62f7e17808d Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 19 Oct 2021 12:24:41 +0200 Subject: [PATCH 4/4] refactored admin takes subscription for a member --- .../subscriptions/subscribe-modal.tsx | 135 +++++++++++++----- .../javascript/controllers/admin/members.js | 102 +++---------- app/frontend/src/javascript/lib/format.ts | 9 +- .../templates/admin/members/edit.html | 9 +- .../admin/subscriptions/create_modal.html | 24 ---- config/locales/app.admin.en.yml | 7 + 6 files changed, 141 insertions(+), 145 deletions(-) delete mode 100644 app/frontend/templates/admin/subscriptions/create_modal.html diff --git a/app/frontend/src/javascript/components/subscriptions/subscribe-modal.tsx b/app/frontend/src/javascript/components/subscriptions/subscribe-modal.tsx index 3d2ba5f54..2ca4b5c5f 100644 --- a/app/frontend/src/javascript/components/subscriptions/subscribe-modal.tsx +++ b/app/frontend/src/javascript/components/subscriptions/subscribe-modal.tsx @@ -3,15 +3,21 @@ import Select from 'react-select'; import { useTranslation } from 'react-i18next'; import { Subscription } from '../../models/subscription'; import { User } from '../../models/user'; -import { PaymentMethod } from '../../models/payment'; +import { PaymentMethod, ShoppingCart } from '../../models/payment'; import { FabModal } from '../base/fab-modal'; -import LocalPaymentAPI from '../../api/local-payment'; import SubscriptionAPI from '../../api/subscription'; import { Plan } from '../../models/plan'; import PlanAPI from '../../api/plan'; import { Loader } from '../base/loader'; import { react2angular } from 'react2angular'; import { IApplication } from '../../models/application'; +import FormatLib from '../../lib/format'; +import { SelectSchedule } from '../payment-schedule/select-schedule'; +import { ComputePriceResult } from '../../models/price'; +import { PaymentScheduleSummary } from '../payment-schedule/payment-schedule-summary'; +import { PaymentSchedule } from '../../models/payment-schedule'; +import PriceAPI from '../../api/price'; +import { LocalPaymentModal } from '../payment/local-payment/local-payment-modal'; declare const Application: IApplication; @@ -19,6 +25,7 @@ interface SubscribeModalProps { isOpen: boolean, toggleModal: () => void, customer: User, + operator: User, onSuccess: (message: string, subscription: Subscription) => void, onError: (message: string) => void, } @@ -30,56 +37,90 @@ interface SubscribeModalProps { type selectOption = { value: number, label: string }; /** - * Modal dialog shown to create a subscription for teh given customer + * Modal dialog shown to create a subscription for the given customer */ -const SubscribeModal: React.FC = ({ isOpen, toggleModal, customer, onError, onSuccess }) => { +const SubscribeModal: React.FC = ({ isOpen, toggleModal, customer, operator, onError, onSuccess }) => { const { t } = useTranslation('admin'); - const [plan, setPlan] = useState(null); - const [plans, setPlans] = useState>(null); + const [selectedPlan, setSelectedPlan] = useState(null); + const [selectedSchedule, setSelectedSchedule] = useState(false); + const [allPlans, setAllPlans] = useState>(null); + const [price, setPrice] = useState(null); + const [cart, setCart] = useState(null); + const [localPaymentModal, setLocalPaymentModal] = useState(false); // fetch all plans from the API on component mount useEffect(() => { PlanAPI.index() - .then(allPlans => setPlans(allPlans)) + .then(plans => setAllPlans(plans)) .catch(error => onError(error)); }, []); - /** - * Callback triggered when the user validates the subscription - */ - const handleConfirmSubscribe = (): void => { - LocalPaymentAPI.confirmPayment({ + // when the plan is updated, update the default value for the payment schedule requirement + useEffect(() => { + if (!selectedPlan) return; + + setSelectedSchedule(selectedPlan.monthly_payment); + }, [selectedPlan]); + + // when the plan or the requirement for a payment schedule are updated, update the cart accordingly + useEffect(() => { + if (!selectedPlan) return; + + setCart({ customer_id: customer.id, - payment_method: PaymentMethod.Other, - items: [ - { - subscription: { - plan_id: plan - } + items: [{ + subscription: { + plan_id: selectedPlan.id } - ] - }).then(res => { - SubscriptionAPI.get(res.main_object.id).then(subscription => { - onSuccess(t('app.admin.subscribe_modal.subscription_success'), subscription); - toggleModal(); - }).catch(error => onError(error)); - }).catch(err => onError(err)); - }; + }], + payment_method: PaymentMethod.Other, + payment_schedule: selectedSchedule + }); + }, [selectedSchedule, selectedPlan]); + + // when the cart is updated, update the price accordingly + useEffect(() => { + if (!cart) return; + + PriceAPI.compute(cart) + .then(res => setPrice(res)) + .catch(err => onError(err)); + }, [cart]); /** * Callback triggered when the user selects a group in the dropdown list */ const handlePlanSelect = (option: selectOption): void => { - setPlan(option.value); + const plan = allPlans.find(p => p.id === option.value); + setSelectedPlan(plan); + }; + + /** + * Callback triggered when the payment of the subscription was successful + */ + const onPaymentSuccess = (res): void => { + SubscriptionAPI.get(res.main_object.id).then(subscription => { + onSuccess(t('app.admin.subscribe_modal.subscription_success'), subscription); + toggleModal(); + }).catch(error => onError(error)); + }; + + /** + * Open/closes the local payment modal + */ + const toggleLocalPaymentModal = (): void => { + setLocalPaymentModal(!localPaymentModal); }; /** * Convert all groups to the react-select format */ const buildOptions = (): Array => { - return plans.filter(p => !p.disabled).map(p => { - return { value: p.id, label: p.base_name }; + if (!allPlans) return []; + + return allPlans.filter(p => !p.disabled && p.group_id === customer.group_id).map(p => { + return { value: p.id, label: `${p.base_name} (${FormatLib.duration(p.interval, p.interval_count)})` }; }); }; @@ -89,22 +130,42 @@ const SubscribeModal: React.FC = ({ isOpen, toggleModal, cu className="subscribe-modal" title={t('app.admin.subscribe_modal.subscribe_USER', { USER: customer.name })} confirmButton={t('app.admin.subscribe_modal.subscribe')} - onConfirm={handleConfirmSubscribe} + onConfirm={toggleLocalPaymentModal} closeButton> - - + + + +
+ {price?.schedule && } + {price && !price.schedule &&
+

{t('app.admin.subscribe_modal.pay_in_one_go')}

+ {FormatLib.price(price.price)} +
} +
+ ); }; -const SubscribeModalWrapper: React.FC = ({ isOpen, toggleModal, customer, onError, onSuccess }) => { +const SubscribeModalWrapper: React.FC = ({ isOpen, toggleModal, customer, operator, onError, onSuccess }) => { return ( - + ); }; -Application.Components.component('subscribeModal', react2angular(SubscribeModalWrapper, ['toggleModal', 'isOpen', 'customer', 'onError', 'onSuccess'])); +Application.Components.component('subscribeModal', react2angular(SubscribeModalWrapper, ['toggleModal', 'isOpen', 'customer', 'operator', 'onError', 'onSuccess'])); diff --git a/app/frontend/src/javascript/controllers/admin/members.js b/app/frontend/src/javascript/controllers/admin/members.js index a9a4a682f..e2670a1d8 100644 --- a/app/frontend/src/javascript/controllers/admin/members.js +++ b/app/frontend/src/javascript/controllers/admin/members.js @@ -711,6 +711,9 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state', // modal dialog to renew the current subscription $scope.isOpenRenewModal = false; + // modal dialog to take a new subscription + $scope.isOpenSubscribeModal = false; + /** * Open a modal dialog asking for confirmation to change the role of the given user * @returns {*} @@ -778,6 +781,15 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state', }, 50); }; + /** + * Opens/closes the modal dialog to renew the subscription (with payment) + */ + $scope.toggleSubscribeModal = () => { + setTimeout(() => { + $scope.isOpenSubscribeModal = !$scope.isOpenSubscribeModal; + $scope.$apply(); + }, 50); + }; /** * Callback triggered if the subscription was successfully extended */ @@ -786,6 +798,14 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state', $scope.subscription.expired_at = newExpirationDate; }; + /** + * Callback triggered if a new subscription was successfully taken + */ + $scope.onSubscribeSuccess = (message, newSubscription) => { + growl.success(message); + $scope.subscription = newSubscription; + }; + /** * Callback triggered in case of error */ @@ -793,88 +813,6 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state', growl.error(message); }; - /** - * Open a modal dialog allowing the admin to set a subscription for the given user. - * @param user {Object} User object, user currently reviewed, as recovered from GET /api/members/:id - * @param plans {Array} List of plans, available for the currently reviewed user, as recovered from GET /api/plans - */ - $scope.createSubscriptionModal = function (user, plans) { - const modalInstance = $uibModal.open({ - animation: true, - templateUrl: '/admin/subscriptions/create_modal.html', - size: 'lg', - controller: ['$scope', '$uibModalInstance', 'Subscription', function ($scope, $uibModalInstance, Subscription) { - // selected user - $scope.user = user; - - // available plans for the selected user - $scope.plans = plans; - - // default parameters for the new subscription - $scope.subscription = { - payment_schedule: false, - payment_method: 'check' - }; - - /** - * Generate a string identifying the given plan by literal human-readable name - * @param plan {Object} Plan object, as recovered from GET /api/plan/:id - * @param groups {Array} List of Groups objects, as recovered from GET /api/groups - * @param short {boolean} If true, the generated name will contain the group slug, otherwise the group full name - * will be included. - * @returns {String} - */ - $scope.humanReadablePlanName = function (plan, groups, short) { return `${$filter('humanReadablePlanName')(plan, groups, short)}`; }; - - /** - * Check if the currently selected plan can be paid with a payment schedule or not - * @return {boolean} - */ - $scope.allowMonthlySchedule = function () { - if (!$scope.subscription) return false; - - const plan = plans.find(p => p.id === $scope.subscription.plan_id); - return plan && plan.monthly_payment; - }; - - /** - * Triggered by the component. - * We must use a setTimeout to workaround the react integration. - * @param checked {Boolean} - */ - $scope.toggleSchedule = function (checked) { - setTimeout(() => { - $scope.subscription.payment_schedule = checked; - $scope.$apply(); - }, 50); - }; - - /** - * Modal dialog validation callback - */ - $scope.ok = function () { - $scope.subscription.user_id = user.id; - return Subscription.save({ }, { subscription: $scope.subscription }, function (_subscription) { - growl.success(_t('app.admin.members_edit.subscription_successfully_purchased')); - $uibModalInstance.close(_subscription); - return $state.reload(); - } - , function (error) { - growl.error(_t('app.admin.members_edit.a_problem_occurred_while_taking_the_subscription')); - console.error(error); - }); - }; - - /** - * Modal dialog cancellation callback - */ - $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); }; - }] - }); - // once the form was validated successfully ... - return modalInstance.result.then(function (subscription) { $scope.subscription = subscription; }); - }; - $scope.createWalletCreditModal = function (user, wallet) { const modalInstance = $uibModal.open({ animation: true, diff --git a/app/frontend/src/javascript/lib/format.ts b/app/frontend/src/javascript/lib/format.ts index 8ebb31aa7..14b21ddce 100644 --- a/app/frontend/src/javascript/lib/format.ts +++ b/app/frontend/src/javascript/lib/format.ts @@ -1,4 +1,4 @@ -import moment from 'moment'; +import moment, { unitOfTime } from 'moment'; import { IFablab } from '../models/fablab'; declare let Fablab: IFablab; @@ -18,6 +18,13 @@ export default class FormatLib { return Intl.DateTimeFormat(Fablab.intl_locale, { hour: 'numeric', minute: 'numeric' }).format(moment(date).toDate()); }; + /** + * Return the formatted localized duration + */ + static duration = (interval: unitOfTime.DurationConstructor, intervalCount: number): string => { + return moment.duration(intervalCount, interval).locale(Fablab.moment_locale).humanize(); + } + /** * Return the formatted localized amount for the given price (eg. 20.5 => "20,50 €") */ diff --git a/app/frontend/templates/admin/members/edit.html b/app/frontend/templates/admin/members/edit.html index ddf2e842a..79579bd6a 100644 --- a/app/frontend/templates/admin/members/edit.html +++ b/app/frontend/templates/admin/members/edit.html @@ -105,7 +105,14 @@

{{ 'app.admin.members_edit.user_has_no_current_subscription' }}

- + + + diff --git a/app/frontend/templates/admin/subscriptions/create_modal.html b/app/frontend/templates/admin/subscriptions/create_modal.html deleted file mode 100644 index 80ba7b896..000000000 --- a/app/frontend/templates/admin/subscriptions/create_modal.html +++ /dev/null @@ -1,24 +0,0 @@ - - - diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 8122f5a0e..b9018c68f 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -925,6 +925,13 @@ en: pay_in_one_go: "Pay in one go" renew: "Renew" renew_success: "The subscription was successfully renewed" + # take a new subscription + subscribe_modal: + subscribe_USER: "Subscribe for {USER}" + subscribe: "Subscribe" + select_plan: "Please select a plan" + pay_in_one_go: "Pay in one go" + subscription_success: "" #add a new administrator to the platform admins_new: add_an_administrator: "Add an administrator"