From 173e43b19262d4b3eecb47b4a74df707e9ea3bac Mon Sep 17 00:00:00 2001 From: evazion Date: Wed, 25 May 2022 17:28:39 -0500 Subject: [PATCH] user upgrades: add upgrade code system. Add a system for upgrading accounts using upgrade codes. Users purchase an upgrade code off-site then redeem it on-site to upgrade their account to Gold. Upgrade codes are randomly pre-generated and are one time use only. Codes have enough randomness that guessing a code is infeasible. --- app/controllers/application_controller.rb | 2 + app/controllers/upgrade_codes_controller.rb | 19 +++ app/controllers/webhooks_controller.rb | 3 + app/logical/payment_transaction/shopify.rb | 30 +++++ app/models/upgrade_code.rb | 62 ++++++++++ app/models/user_upgrade.rb | 6 +- app/policies/upgrade_code_policy.rb | 15 +++ app/views/static/site_map.html.erb | 2 +- app/views/upgrade_codes/index.html.erb | 29 +++++ app/views/upgrade_codes/redeem.html.erb | 28 +++++ app/views/upgrade_codes/upgrade.js.erb | 1 + app/views/user_upgrades/create.js.erb | 3 + app/views/user_upgrades/new.html.erb | 28 ++--- app/views/user_upgrades/show.html.erb | 4 - app/views/users/_upgrade_notice.html.erb | 2 +- config/danbooru_default_config.rb | 8 ++ config/routes.rb | 7 ++ .../20220525214746_create_upgrade_codes.rb | 22 ++++ db/structure.sql | 112 +++++++++++++++++- script/fixes/110_generate_upgrade_codes.rb | 8 ++ test/factories/upgrade_code.rb | 5 + .../upgrade_codes_controller_test.rb | 87 ++++++++++++++ 22 files changed, 461 insertions(+), 22 deletions(-) create mode 100644 app/controllers/upgrade_codes_controller.rb create mode 100644 app/logical/payment_transaction/shopify.rb create mode 100644 app/models/upgrade_code.rb create mode 100644 app/policies/upgrade_code_policy.rb create mode 100644 app/views/upgrade_codes/index.html.erb create mode 100644 app/views/upgrade_codes/redeem.html.erb create mode 100644 app/views/upgrade_codes/upgrade.js.erb create mode 100644 db/migrate/20220525214746_create_upgrade_codes.rb create mode 100755 script/fixes/110_generate_upgrade_codes.rb create mode 100644 test/factories/upgrade_code.rb create mode 100644 test/functional/upgrade_codes_controller_test.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a266916a4..e574a2dc5 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -136,6 +136,8 @@ class ApplicationController < ActionController::Base render_error_page(422, exception, template: "static/tag_limit_error", message: "You cannot search for more than #{CurrentUser.tag_query_limit} tags at a time.") when PostQuery::Error render_error_page(422, exception, message: exception.message) + when UpgradeCode::InvalidCodeError, UpgradeCode::RedeemedCodeError, UpgradeCode::AlreadyUpgradedError + render_error_page(422, exception, message: exception.message) when RateLimiter::RateLimitError render_error_page(429, exception, message: "Rate limit exceeded. You're doing that too fast") when PageRemovedError diff --git a/app/controllers/upgrade_codes_controller.rb b/app/controllers/upgrade_codes_controller.rb new file mode 100644 index 000000000..57b0f6ace --- /dev/null +++ b/app/controllers/upgrade_codes_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class UpgradeCodesController < ApplicationController + respond_to :js, :html, :json, :xml + + def index + @upgrade_codes = authorize UpgradeCode.visible(CurrentUser.user).paginated_search(params, count_pages: true) + respond_with(@upgrade_codes) + end + + def redeem + end + + def upgrade + @upgrade_code = UpgradeCode.redeem!(code: params.dig(:upgrade_code, :code), redeemer: CurrentUser.user) + + respond_with(@upgrade_code, location: @upgrade_code.user_upgrade) + end +end diff --git a/app/controllers/webhooks_controller.rb b/app/controllers/webhooks_controller.rb index 8f6ded08c..facd20f65 100644 --- a/app/controllers/webhooks_controller.rb +++ b/app/controllers/webhooks_controller.rb @@ -11,6 +11,9 @@ class WebhooksController < ApplicationController when "stripe" PaymentTransaction::Stripe.receive_webhook(request) head 200 + when "shopify" + PaymentTransaction::Shopify.receive_webhook(request) + head 200 when "discord" json = DiscordSlashCommand.receive_webhook(request) render json: json diff --git a/app/logical/payment_transaction/shopify.rb b/app/logical/payment_transaction/shopify.rb new file mode 100644 index 000000000..12e7a754d --- /dev/null +++ b/app/logical/payment_transaction/shopify.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class PaymentTransaction::Shopify < PaymentTransaction + class InvalidWebhookError < StandardError; end + + def create!(**params) + nil + end + + def refund!(reason = nil) + raise NotImplementedError + end + + concerning :WebhookMethods do + class_methods do + def receive_webhook(request) + verify_webhook!(request) + end + + private def verify_webhook!(request) + payload = request.body.read + actual_signature = request.headers["X-Shopify-Hmac-Sha256"].to_s + calculated_signature = Base64.strict_encode64(OpenSSL::HMAC.digest("sha256", Danbooru.config.shopify_webhook_secret, payload)) + raise InvalidWebhookError unless ActiveSupport::SecurityUtils::secure_compare(actual_signature, calculated_signature) + + request + end + end + end +end diff --git a/app/models/upgrade_code.rb b/app/models/upgrade_code.rb new file mode 100644 index 000000000..94db73d81 --- /dev/null +++ b/app/models/upgrade_code.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +# A code that can be redeemed for a Gold account. Codes are pre-generated and +# one time use only. Upgrade codes are sold in the Danbooru store. +class UpgradeCode < ApplicationRecord + class InvalidCodeError < StandardError; end + class RedeemedCodeError < StandardError; end + class AlreadyUpgradedError < StandardError; end + + UPGRADE_CODE_LENGTH = 8 + + attribute :code, default: -> { UpgradeCode.generate_code } + attribute :status, default: :unsold + + belongs_to :creator, class_name: "User" + belongs_to :redeemer, class_name: "User", optional: true + belongs_to :user_upgrade, optional: true + + enum status: { + unsold: 0, + unredeemed: 100, + redeemed: 200, + } + + def self.visible(user) + if user.is_owner? + all + else + where(redeemer: user).or(where(creator: user)) + end + end + + def self.search(params) + q = search_attributes(params, :id, :created_at, :updated_at, :code, :status, :creator, :redeemer, :user_upgrade) + q.apply_default_order(params) + end + + def self.generate_code + SecureRandom.send(:choose, [*"0".."9", *"A".."Z", *"a".."z"], UPGRADE_CODE_LENGTH) # base62 + end + + def self.redeem!(code:, redeemer:) + upgrade_code = UpgradeCode.find_by(code: code) + raise InvalidCodeError, "This upgrade code is invalid" if upgrade_code.nil? + + upgrade_code.redeem!(redeemer) + end + + def redeem!(redeemer) + transaction do + raise RedeemedCodeError, "This upgrade code has already been used" if redeemed? + raise AlreadyUpgradedError, "Your account is already Gold or higher" if redeemer.is_gold? + + user_upgrade = UserUpgrade.create!(recipient: redeemer, purchaser: redeemer, status: "processing", upgrade_type: "gold", payment_processor: "upgrade_code") + user_upgrade.process_upgrade!("paid") + + update!(status: :redeemed, redeemer: redeemer, user_upgrade: user_upgrade) + + self + end + end +end diff --git a/app/models/user_upgrade.rb b/app/models/user_upgrade.rb index 95fca14ca..8fe15b633 100644 --- a/app/models/user_upgrade.rb +++ b/app/models/user_upgrade.rb @@ -22,6 +22,8 @@ class UserUpgrade < ApplicationRecord enum payment_processor: { stripe: 0, authorize_net: 100, + shopify: 200, + upgrade_code: 300, } scope :gifted, -> { where("recipient_id != purchaser_id") } @@ -113,7 +115,7 @@ class UserUpgrade < ApplicationRecord end concerning :UpgradeMethods do - def process_upgrade!(payment_status) + def process_upgrade!(payment_status = "paid") recipient.with_lock do return unless pending? || processing? @@ -174,6 +176,8 @@ class UserUpgrade < ApplicationRecord PaymentTransaction::Stripe.new(self) in "authorize_net" PaymentTransaction::AuthorizeNet.new(self) + in "shopify" + PaymentTransaction::Shopify.new(self) end end diff --git a/app/policies/upgrade_code_policy.rb b/app/policies/upgrade_code_policy.rb new file mode 100644 index 000000000..c5887f66e --- /dev/null +++ b/app/policies/upgrade_code_policy.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class UpgradeCodePolicy < ApplicationPolicy + def index? + user.is_owner? + end + + def redeem? + true + end + + def upgrade? + unbanned? + end +end diff --git a/app/views/static/site_map.html.erb b/app/views/static/site_map.html.erb index 88c539dd7..d2c16e226 100644 --- a/app/views/static/site_map.html.erb +++ b/app/views/static/site_map.html.erb @@ -130,7 +130,7 @@
  • <%= link_to "Favorite groups", favorite_groups_path %>
  • <%= link_to "Saved searches", saved_searches_path %>
  • <% end %> -
  • <%= link_to "Upgrade information", new_user_upgrade_path %>
  • +
  • <%= link_to "Upgrade account", new_user_upgrade_path %>