1
0
Fork 0

Improve PRO feature approach

Dieser Commit ist enthalten in:
angusmcleod 2021-09-01 10:19:00 +08:00
Ursprung a810155f91
Commit 6b1e7568c1
22 geänderte Dateien mit 331 neuen und 314 gelöschten Zeilen

Datei anzeigen

@ -6,7 +6,8 @@ import discourseComputed from "discourse-common/utils/decorators";
import I18n from "I18n";
export default Component.extend({
classNames: ["realtime-validations"],
classNames: ["realtime-validations", "setting", "full", "pro"],
@discourseComputed
timeUnits() {
return ["days", "weeks", "months", "years"].map((unit) => {

Datei anzeigen

@ -77,7 +77,12 @@ export default Controller.extend({
wizard
.save(opts)
.then((result) => {
console.log(result)
if (result.wizard_id) {
this.send("afterSave", result.wizard_id);
} else if (result.errors) {
this.set('error', result.errors.join(', '));
}
})
.catch((result) => {
let errorType = "failed";

Datei anzeigen

@ -220,17 +220,11 @@
options=fieldConditionOptions}}
</div>
</div>
{{/if}}
{{#if showAdvanced}}
{{wizard-advanced-toggle showAdvanced=field.showAdvanced}}
{{#if field.showAdvanced}}
<div class="advanced-settings">
<div class="setting full field-mapper-setting">
<div class="setting full field-mapper-setting pro">
<div class="setting-label">
<label>{{i18n "admin.wizard.index"}}</label>
<span class="pro-label">{{i18n "admin.wizard.pro.label"}}</span>
</div>
<div class="setting-value">
@ -241,9 +235,10 @@
</div>
{{#if isCategory}}
<div class="setting">
<div class="setting pro">
<div class="setting-label">
<label>{{i18n "admin.wizard.field.property"}}</label>
<span class="pro-label">{{i18n "admin.wizard.pro.label"}}</span>
</div>
<div class="setting-value">
@ -258,9 +253,10 @@
</div>
{{/if}}
<div class="setting">
<div class="setting pro">
<div class="setting-label">
<label>{{i18n "admin.wizard.translation"}}</label>
<span class="pro-label">{{i18n "admin.wizard.pro.label"}}</span>
</div>
<div class="setting-value medium">
{{input
@ -274,6 +270,4 @@
{{#if validations}}
{{wizard-realtime-validations field=field validations=validations}}
{{/if}}
</div>
{{/if}}
{{/if}}

Datei anzeigen

@ -21,8 +21,11 @@
{{#if subscribed}}
<div class="detail-container">
<div class="subscription-state {{stateClass}}" title={{stateLabel}}>{{stateLabel}}</div>
{{#if subscription.updated_at}}
<div class="subscription-updated-at" title={{subscription.updated_at}}>
{{{i18n 'admin.wizard.pro.subscription.last_updated' updated_at=(format-date subscription.updated_at leaveAgo="true")}}}
</div>
{{/if}}
</div>
{{/if}}

Datei anzeigen

@ -1,5 +1,8 @@
<h3>{{i18n "admin.wizard.field.validations.header"}}</h3>
<div class="setting-label">
<label>{{i18n "admin.wizard.field.validations.header"}}</label>
<span class="pro-label">{{i18n "admin.wizard.pro.label"}}</span>
</div>
<div class="setting-value full">
<ul>
{{#each-in field.validations as |type props|}}
<li>
@ -39,12 +42,13 @@
</div>
<div class="setting-value">
{{radio-button name=(concat type field.id) value="above" selection=props.position}}
{{i18n "admin.wizard.field.validations.above"}}
<span>{{i18n "admin.wizard.field.validations.above"}}</span>
{{radio-button name=(concat type field.id) value="below" selection=props.position}}
{{i18n "admin.wizard.field.validations.below"}}
<span>{{i18n "admin.wizard.field.validations.below"}}</span>
</div>
</div>
</div>
</li>
{{/each-in}}
</ul>
</div>

Datei anzeigen

@ -694,27 +694,59 @@
}
}
.realtime-validations > ul {
.admin-wizard-container.settings .realtime-validations .setting-value > ul {
list-style: none;
margin: 0;
width: 100%;
display: flex;
flex-wrap: wrap;
> li {
background-color: var(--primary-low);
padding: 1em;
margin: 0 0 1em 0;
input {
margin-bottom: 0;
.setting-title {
display: flex;
align-items: center;
h4 {
margin: 0 15px 0 0;
}
input[type="checkbox"] {
margin: 0 5px 0 0;
}
}
.setting-label {
width: 100px;
}
.setting-value {
display: flex;
align-items: center;
.input .select-kit,
> .select-kit {
max-width: unset !important;
}
> span {
margin-right: 1em;
}
}
}
}
.validation-container {
display: flex;
flex-direction: column;
padding: 1em 0;
.validation-section {
width: 250px;
min-width: 250px;
margin: .5em 0;
}
}

Datei anzeigen

@ -192,7 +192,7 @@ en:
label: "Format"
instructions: "<a href='https://momentjs.com/docs/#/displaying/format/' target='_blank'>Moment.js format</a>"
validations:
header: "Realtime Validations"
header: "Validations"
enabled: "Enabled"
similar_topics: "Similar Topics"
position: "Position"

Datei anzeigen

@ -17,7 +17,7 @@ en:
name_too_short: "'%{name}' is too short for a custom field name (min length is #{min_length})"
name_already_taken: "'%{name}' is already taken as a custom field name"
save_default: "Failed to save custom field '%{name}'"
pro_required: "PRO Actions require a PRO Subscription"
pro_type: "%{type} custom fields require PRO Subscription"
field:
too_short: "%{label} must be at least %{min} characters"
@ -50,7 +50,7 @@ en:
required: "%{property} is required"
conflict: "Wizard with id '%{wizard_id}' already exists"
after_time: "After time setting is invalid"
pro: "%{property} is PRO only"
pro: "%{type} %{property} is PRO only"
site_settings:
custom_wizard_enabled: "Enable custom wizards."

Datei anzeigen

@ -47,7 +47,7 @@ Discourse::Application.routes.append do
get 'admin/wizards/pro' => 'admin_pro#index'
get 'admin/wizards/pro/authorize' => 'admin_pro#authorize'
get 'admin/wizards/pro/authorize/callback' => 'admin_pro#authorize_callback'
delete 'admin/wizards/pro/authorize' => 'admin_pro#destroy'
delete 'admin/wizards/pro/authorize' => 'admin_pro#destroy_authentication'
post 'admin/wizards/pro/subscription' => 'admin_pro#update_subscription'
end
end

Datei anzeigen

@ -4,27 +4,27 @@ class CustomWizard::AdminProController < CustomWizard::AdminController
skip_before_action :check_xhr, :preload_json, :verify_authenticity_token, only: [:authorize, :authorize_callback]
def index
render_serialized(CustomWizard::Pro.new, CustomWizard::ProSerializer, root: false)
render_serialized(pro, CustomWizard::ProSerializer, root: false)
end
def authorize
request_id = SecureRandom.hex(32)
cookies[:user_api_request_id] = request_id
redirect_to CustomWizard::Pro.auth_request(current_user.id, request_id).to_s
redirect_to pro.authentication_request(current_user.id, request_id).to_s
end
def authorize_callback
payload = params[:payload]
request_id = cookies[:user_api_request_id]
CustomWizard::Pro.auth_response(request_id, payload)
CustomWizard::Pro.update_subscription
pro.authentication_response(request_id, payload)
pro.update_subscription
redirect_to '/admin/wizards/pro'
end
def destroy
if CustomWizard::ProAuthentication.destroy
def destroy_authentication
if pro.destroy_authentication
render json: success_json
else
render json: failed_json
@ -32,15 +32,17 @@ class CustomWizard::AdminProController < CustomWizard::AdminController
end
def update_subscription
if CustomWizard::Pro.update
render json: success_json.merge(
subscription: CustomWizard::ProSubscriptionSerializer.new(
CustomWizard::ProSubscription.new,
root: false
)
)
if pro.update_subscription
subscription = CustomWizard::ProSubscriptionSerializer.new(pro.subscription, root: false)
render json: success_json.merge(subscription: subscription)
else
render json: failed_json
end
end
protected
def pro
@pro ||= CustomWizard::Pro.new
end
end

Datei anzeigen

@ -5,6 +5,7 @@ class CustomWizard::WizardController < ::ApplicationController
layout 'wizard'
before_action :ensure_plugin_enabled
before_action :update_pro_subscription
helper_method :wizard_page_title
helper_method :wizard_theme_id
helper_method :wizard_theme_lookup
@ -81,4 +82,8 @@ class CustomWizard::WizardController < ::ApplicationController
redirect_to path("/")
end
end
def update_pro_subscription
CustomWizard::Pro.update_subscription
end
end

Datei anzeigen

@ -1,7 +1,7 @@
# frozen_string_literal: true
class CustomWizard::UpdateProSubscription < ::Jobs::Scheduled
every 10.minutes
every 1.hour
def execute(args)
CustomWizard::Pro.update_subscription

Datei anzeigen

@ -14,15 +14,9 @@ class CustomWizard::Action
@submission = opts[:submission]
@log = []
@result = CustomWizard::ActionResult.new
@pro = CustomWizard::Pro.new
end
def perform
if pro_actions.include?(action['type']) && !@pro.subscribed?
log_error(I18n.t("wizard.custom_field.error.pro_required"))
return
end
ActiveRecord::Base.transaction do
self.send(action['type'].to_sym)
end

Datei anzeigen

@ -6,7 +6,6 @@ class CustomWizard::Builder
@template = CustomWizard::Template.create(wizard_id)
return nil if @template.nil?
@wizard = CustomWizard::Wizard.new(template.data, user)
@pro = CustomWizard::Pro.new
end
def self.sorted_handlers
@ -226,11 +225,6 @@ class CustomWizard::Builder
end
def check_condition(template)
unless @pro.subscribed?
CustomWizard::Log.create(I18n.t("wizard.custom_field.error.pro_required"))
return false
end
if template['condition'].present?
result = CustomWizard::Mapper.new(
inputs: template['condition'],

Datei anzeigen

@ -37,6 +37,8 @@ class ::CustomWizard::CustomField
send("#{attr}=", value)
end
end
@pro = CustomWizard::Pro.new
end
def save
@ -92,6 +94,10 @@ class ::CustomWizard::CustomField
add_error(I18n.t("#{i18n_key}.unsupported_type", type: value))
end
if attr == 'type' && value == 'json' && !@pro.subscribed?
add_error(I18n.t("wizard.custom_field.error.pro_type", type: value))
end
if attr == 'name'
unless value.is_a?(String)
add_error(I18n.t("#{i18n_key}.name_invalid", name: value))

Datei anzeigen

@ -47,6 +47,7 @@ class CustomWizard::Mapper
@data = params[:data] || {}
@user = params[:user]
@opts = params[:opts] || {}
@pro = CustomWizard::Pro.new
end
def perform
@ -251,7 +252,7 @@ class CustomWizard::Mapper
end
end
if opts[:template]
if @pro.subscribed? && opts[:template]
template = Liquid::Template.parse(string)
string = template.render(data)
end

Datei anzeigen

@ -1,12 +1,22 @@
# frozen_string_literal: true
class CustomWizard::Pro
attr_reader :authentication,
include ActiveModel::Serialization
attr_accessor :authentication,
:subscription
def initialize
@authentication = CustomWizard::ProAuthentication.new
@subscription = CustomWizard::ProSubscription.new
@authentication = CustomWizard::ProAuthentication.new(get_authentication)
@subscription = CustomWizard::ProSubscription.new(get_subscription)
end
def authorized?
@authentication.active?
end
def subscribed?
@subscription.active?
end
def server
@ -21,12 +31,8 @@ class CustomWizard::Pro
"custom-wizard"
end
def authorized?
@authentication.active?
end
def subscribed?
@subscription.active?
def scope
"discourse-subscription-server:user_subscription"
end
def update_subscription
@ -45,28 +51,35 @@ class CustomWizard::Pro
return false
end
return @subscription.update(data)
return false unless data && data.is_a?(Hash)
subscriptions = data[:subscriptions]
if subscriptions.present?
subscription = subscriptions.first
type = subscription[:price_nickname]
@subscription = set_subscription(type)
return true
end
end
end
@subscription.destroy
false
remove_subscription
end
def destroy
@authentication.destroy
def destroy_subscription
remove_subscription
end
def auth_request(user_id, request_id)
def authentication_request(user_id, request_id)
keys = @authentication.generate_keys(user_id, request_id)
params = {
public_key: keys.public_key,
nonce: keys.nonce,
client_id: @authentication.client_id,
auth_redirect: "#{Discourse.base_url}/admin/wizards/pro/authorize/callback",
application_name: SiteSetting.title,
scopes: "discourse-subscription-server:user_subscription"
scopes: scope
}
uri = URI.parse("https://#{server}/user-api-key/new")
@ -74,26 +87,24 @@ class CustomWizard::Pro
uri.to_s
end
def auth_response(request_id, payload)
def authentication_response(request_id, payload)
data = @authentication.decrypt_payload(request_id, payload)
return unless data.is_a?(Hash) && data[:key] && data[:user_id]
@authentication.update(data)
api_key = data[:key]
user_id = data[:user_id]
user = User.find(user_id)
if user&.admin
@authentication = set_authentication(api_key, user.id)
true
else
false
end
end
def self.update
self.new.update
end
def self.destroy
self.new.destroy
end
def self.generate_request
self.new.generate_request
end
def self.handle_response
self.new.handle_response
def destroy_authentication
remove_authentication
end
def self.subscribed?
@ -103,4 +114,56 @@ class CustomWizard::Pro
def self.namespace
"custom_wizard_pro"
end
private
def subscription_db_key
"subscription"
end
def authentication_db_key
"authentication"
end
def get_subscription
raw = PluginStore.get(self.class.namespace, subscription_db_key)
if raw.present?
OpenStruct.new(
type: raw['type'],
updated_at: raw['updated_at']
)
end
end
def remove_subscription
PluginStore.remove(self.class.namespace, subscription_db_key)
end
def set_subscription(type)
PluginStore.set(CustomWizard::Pro.namespace, subscription_db_key, type: type, updated_at: Time.now)
CustomWizard::ProSubscription.new(get_subscription)
end
def get_authentication
raw = PluginStore.get(self.class.namespace, authentication_db_key)
OpenStruct.new(
key: raw && raw['key'],
auth_by: raw && raw['auth_by'],
auth_at: raw && raw['auth_at']
)
end
def set_authentication(key, user_id)
PluginStore.set(self.class.namespace, authentication_db_key,
key: key,
auth_by: user_id,
auth_at: Time.now
)
CustomWizard::ProAuthentication.new(get_authentication)
end
def remove_authentication
PluginStore.remove(self.class.namespace, authentication_db_key)
end
end

Datei anzeigen

@ -6,12 +6,13 @@ class CustomWizard::ProAuthentication
:auth_at,
:api_key
def initialize
api = get_api_key
def initialize(auth)
if auth
@api_key = auth.key
@auth_at = auth.auth_at
@auth_by = auth.auth_by
end
@api_key = api.key
@auth_at = api.auth_at
@auth_by = api.auth_by
@client_id = get_client_id || set_client_id
end
@ -19,22 +20,6 @@ class CustomWizard::ProAuthentication
@api_key.present?
end
def update(data)
api_key = data[:key]
user_id = data[:user_id]
user = User.find(user_id)
if user&.admin
set_api_key(api_key, user.id)
else
false
end
end
def destroy
remove
end
def generate_keys(user_id, request_id)
rsa = OpenSSL::PKey::RSA.generate(2048)
nonce = SecureRandom.hex(32)
@ -66,47 +51,12 @@ class CustomWizard::ProAuthentication
private
def api_key_db_key
"api_key"
end
def api_client_id_db_key
"api_client_id"
end
def keys_db_key
"keys"
end
def get_api_key
raw = PluginStore.get(CustomWizard::Pro.namespace, api_key_db_key)
OpenStruct.new(
key: raw && raw['key'],
auth_by: raw && raw['auth_by'],
auth_at: raw && raw['auth_at']
)
end
def set_api_key(key, user_id)
PluginStore.set(CustomWizard::Pro.namespace, api_key_db_key,
key: key,
auth_by: user_id,
auth_at: Time.now
)
end
def remove
PluginStore.remove(CustomWizard::Pro.namespace, api_key_db_key)
end
def get_client_id
PluginStore.get(CustomWizard::Pro.namespace, api_client_id_db_key)
end
def set_client_id
client_id = SecureRandom.hex(32)
PluginStore.set(CustomWizard::Pro.namespace, api_client_id_db_key, client_id)
client_id
def client_id_db_key
"client_id"
end
def set_keys(request_id, user_id, rsa, nonce)
@ -129,4 +79,14 @@ class CustomWizard::ProAuthentication
def delete_keys(request_id)
PluginStore.remove(CustomWizard::Pro.namespace, "#{keys_db_key}_#{request_id}")
end
def get_client_id
PluginStore.get(CustomWizard::Pro.namespace, client_id_db_key)
end
def set_client_id
client_id = SecureRandom.hex(32)
PluginStore.set(CustomWizard::Pro.namespace, client_id_db_key, client_id)
client_id
end
end

Datei anzeigen

@ -4,12 +4,10 @@ class CustomWizard::ProSubscription
attr_reader :type,
:updated_at
def initialize
raw = get
if raw
@type = raw['type']
@updated_at = raw['updated_at']
def initialize(subscription)
if subscription
@type = subscription.type
@updated_at = subscription.updated_at
end
end
@ -18,40 +16,6 @@ class CustomWizard::ProSubscription
end
def active?
types.include?(type) && updated_at.to_datetime > (Date.today - 15.minutes).to_datetime
end
def update(data)
return false unless data && data.is_a?(Hash)
subscriptions = data[:subscriptions]
if subscriptions.present?
subscription = subscriptions.first
type = subscription[:price_nickname]
set(type)
end
end
def destroy
remove
end
private
def key
"custom_wizard_pro_subscription"
end
def set(type)
PluginStore.set(CustomWizard::Pro.namespace, key, type: type, updated_at: Time.now)
end
def get
PluginStore.get(CustomWizard::Pro.namespace, key)
end
def remove
PluginStore.remove(CustomWizard::Pro.namespace, key)
types.include?(type) && updated_at.to_datetime > (Time.zone.now - 2.hours).to_datetime
end
end

Datei anzeigen

@ -31,6 +31,7 @@ class CustomWizard::TemplateValidator
if data[:actions].present?
data[:actions].each do |action|
validate_pro_action(action)
check_required(action, :action)
end
end
@ -53,15 +54,31 @@ class CustomWizard::TemplateValidator
def self.pro
{
step: ['condition'],
field: ['conition']
wizard: {},
step: {
condition: 'present',
index: 'conditional'
},
field: {
condition: 'present',
index: 'conditional'
},
action: {
type: %w[
send_message
create_category
create_group
watch_categories
send_to_api
]
}
}
end
private
def check_required(object, type)
CustomWizard::TemplateValidator.required[type].each do |property|
self.class.required[type].each do |property|
if object[property].blank?
errors.add :base, I18n.t("wizard.validation.required", property: property)
end
@ -69,9 +86,13 @@ class CustomWizard::TemplateValidator
end
def validate_pro(object, type)
CustomWizard::TemplateValidator.required[type].each do |property|
if object[property].present? && !@pro.subscribed?
errors.add :base, I18n.t("wizard.validation.pro", property: property)
self.class.pro[type].each do |property, pro_type|
is_pro = (pro_type === 'present' && object[property].present?) ||
(pro_type === 'conditional' && object[property].is_a?(Hash)) ||
(pro_type.is_a?(Array) && pro_type.includes?(object[property]))
if is_pro && @pro.subscribed?
errors.add :base, I18n.t("wizard.validation.pro", type: type.to_s, property: property)
end
end
end

Datei anzeigen

@ -69,7 +69,7 @@ after_initialize do
../controllers/custom_wizard/realtime_validations.rb
../jobs/regular/refresh_api_access_token.rb
../jobs/regular/set_after_time_wizard.rb
../jobs/scheduled/update_pro_status.rb
../jobs/scheduled/update_pro_subscription.rb
../lib/custom_wizard/validators/template.rb
../lib/custom_wizard/validators/update.rb
../lib/custom_wizard/action_result.rb
@ -111,9 +111,9 @@ after_initialize do
../serializers/custom_wizard/log_serializer.rb
../serializers/custom_wizard/submission_serializer.rb
../serializers/custom_wizard/realtime_validation/similar_topics_serializer.rb
../serializers/custom_wizard/pro_serializer.rb
../serializers/custom_wizard/pro/authentication_serializer.rb
../serializers/custom_wizard/pro/subscription_serializer.rb
../serializers/custom_wizard/pro_serializer.rb
../extensions/extra_locales_controller.rb
../extensions/invites_controller.rb
../extensions/users_controller.rb
@ -126,18 +126,6 @@ after_initialize do
Liquid::Template.register_filter(::CustomWizard::LiquidFilter::FirstNonEmpty)
class CustomWizard::UnpermittedOverride < StandardError; end
CustomWizard.constants.each do |class_name|
klass = CustomWizard.const_get(class_name)
next if !klass.is_a?(Class) || klass.superclass.name.to_s.split("::").first == 'CustomWizard'
klass.define_singleton_method(:prepend) { |klass| raise CustomWizard::UnpermittedOverride.new }
klass.define_singleton_method(:include) { |klass| raise CustomWizard::UnpermittedOverride.new }
klass.define_singleton_method(:define_method) { |name, &block| raise CustomWizard::UnpermittedOverride.new }
klass.define_singleton_method(:define_singleton_method) { |name, &block| raise CustomWizard::UnpermittedOverride.new }
end
add_class_method(:wizard, :user_requires_completion?) do |user|
wizard_result = self.new(user).requires_completion?
return wizard_result if wizard_result

Datei anzeigen

@ -1,26 +1,6 @@
# frozen_string_literal: true
class CustomWizard::ProSerializer < ApplicationSerializer
attributes :server,
:authentication,
:subscription
def server
CustomWizard::ProSubscription::SUBSCRIPTION_SERVER
end
def authentication
if object.authentication
CustomWizard::ProAuthenticationSerializer.new(object.authentication, root: false)
else
nil
end
end
def subscription
if object.subscription
CustomWizard::ProSubscriptionSerializer.new(object.subscription, root: false)
else
nil
end
end
attributes :server
has_one :authentication, serializer: CustomWizard::ProAuthenticationSerializer, embed: :objects
has_one :subscription, serializer: CustomWizard::ProSubscriptionSerializer, embed: :objects
end