card-mod-account 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 446ac1cbd57b9473bffdf6fdb91303bc86cbcdcf584e2fafe48fc306da050580
4
+ data.tar.gz: eecb9da6a9312154ec015ac20353061b8d2839a6c61d2dacabe9c2d3377603b9
5
+ SHA512:
6
+ metadata.gz: 9472b1ed791997464de8a57adad65d83b9021df74750f76edaab8fddfb51df493801f659c861a870ae29994e806add8add7e8aa8f07c73954b159f9e288ed050
7
+ data.tar.gz: 8525e80cacd92f2d5c7ce960bbdda0be8fc63441fb3cb408080bbf03a0d9fe2c302da16ab60b117c923d5141bd1a62611e3bc48ad61cc677c6ac62729b1bce36
@@ -0,0 +1,6 @@
1
+ <!--
2
+ # @title README: account mod
3
+ -->
4
+ # account
5
+
6
+ Create and manage accounts with cards.
@@ -0,0 +1,17 @@
1
+
2
+ # allow account owner to update account field content
3
+ def ok_to_update
4
+ return true if own_account? && !name_changed? && !type_id_changed?
5
+
6
+ super
7
+ end
8
+
9
+ # force inherit permission on create
10
+ # (cannot be done with rule, because sets are not addressable)
11
+ def permission_rule_id action
12
+ if action == :create
13
+ left_permission_rule_id action
14
+ else
15
+ super
16
+ end
17
+ end
@@ -0,0 +1,49 @@
1
+ def account
2
+ fetch :account, new: {}
3
+ end
4
+
5
+ def default_account_status
6
+ "active"
7
+ end
8
+
9
+ def current_account?
10
+ id && Auth.current_id == id
11
+ end
12
+
13
+ format :html do
14
+ def default_bridge_tab
15
+ card.current_account? ? :account_tab : super
16
+ end
17
+
18
+ view :account_tab do
19
+ bridge_pill_sections "Account" do
20
+ [["Settings", account_details_items],
21
+ ["Content", account_content_items]]
22
+ end
23
+ end
24
+
25
+ def show_account_tab?
26
+ card.account.real?
27
+ end
28
+
29
+ def account_formgroups
30
+ Auth.as_bot do
31
+ subformat(card.account)._render :content_formgroups, structure: true
32
+ end
33
+ end
34
+
35
+ def account_details_items
36
+ [
37
+ ["Email and Password", :account,
38
+ { path: { slot: { hide: %i[help_link bridge_link] } } }],
39
+ ["Roles", :roles,
40
+ { path: { view: :content_with_edit_button } }],
41
+ ["Notifications", :follow]
42
+ ]
43
+ end
44
+
45
+ def account_content_items
46
+ [["Created", :created],
47
+ ["Edited", :edited]]
48
+ end
49
+ end
@@ -0,0 +1,97 @@
1
+ module ClassMethods
2
+ def default_accounted_type_id
3
+ UserID
4
+ end
5
+ end
6
+
7
+ def account
8
+ fetch :account
9
+ end
10
+
11
+ def parties
12
+ @parties ||= (all_enabled_roles << id).flatten.reject(&:blank?)
13
+ end
14
+
15
+ def among? ok_ids
16
+ ok_ids.any? do |ok_id|
17
+ ok_id == AnyoneID ||
18
+ (ok_id == AnyoneWithRoleID && all_enabled_roles.size > 1) ||
19
+ parties.member?(ok_id)
20
+ end
21
+ end
22
+
23
+ def own_account?
24
+ # card is +*account card of signed_in user.
25
+ name.part_names[0].key == Auth.as_card.key &&
26
+ name.part_names[1].key == Card[:account].key
27
+ end
28
+
29
+ def read_rules
30
+ @read_rules ||= fetch_read_rules
31
+ end
32
+
33
+ def read_rules_hash
34
+ @read_rules_hash ||= read_rules.each_with_object({}) { |id, h| h[id] = true }
35
+ end
36
+
37
+ def fetch_read_rules
38
+ return [] if id == WagnBotID # always_ok, so not needed
39
+
40
+ ([AnyoneID] + parties).each_with_object([]) do |party_id, rule_ids|
41
+ next unless (cache = Card::Rule.read_rule_cache[party_id])
42
+ rule_ids.concat cache
43
+ end
44
+ end
45
+
46
+ def clear_roles
47
+ @parties = @all_roles = @all_active_roles = @read_rules = nil
48
+ end
49
+
50
+ def with_clear_roles
51
+ a, b, c, d = @parties, @all_roles, @all_active_roles, @read_rules
52
+ yield
53
+ ensure
54
+ @parties, @all_roles, @all_active_roles, @read_rules = a, b, c, d
55
+ end
56
+
57
+ def all_enabled_roles
58
+ @all_active_roles ||= (id == AnonymousID ? [] : enabled_role_ids)
59
+ end
60
+
61
+ def all_roles
62
+ @all_roles ||= (id == AnonymousID ? [] : fetch_roles)
63
+ end
64
+
65
+ def enabled_role_ids
66
+ Auth.as_bot do
67
+ # workaround for broken migrations
68
+ return fetch_roles unless Card::Codename.exists? :enabled_roles
69
+
70
+ enabled = enabled_roles_card
71
+ enabled.virtual? ? enabled.item_ids : fetch_roles
72
+ end
73
+ end
74
+
75
+ def enabled_roles_card
76
+ fetch :enabled_roles, eager_cache: true, new: { type_id: SessionID }
77
+ end
78
+
79
+ def fetch_roles
80
+ [AnyoneSignedInID] + role_ids_from_roles_trait
81
+ end
82
+
83
+ def role_ids_from_roles_trait
84
+ Auth.as_bot do
85
+ role_trait = fetch :roles
86
+ role_trait ? role_trait.item_ids : []
87
+ end
88
+ end
89
+
90
+ event :generate_token do
91
+ Digest::SHA1.hexdigest "--#{Time.zone.now.to_f}--#{rand 10}--"
92
+ end
93
+
94
+ event :set_stamper, :prepare_to_validate do
95
+ self.updater_id = Auth.current_id
96
+ self.creator_id = updater_id if new_card?
97
+ end
@@ -0,0 +1,58 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ card_accessor :email
4
+ card_accessor :password
5
+ card_accessor :salt
6
+ card_accessor :status
7
+ card_accessor :api_key
8
+
9
+ require_field :email
10
+
11
+ def accounted
12
+ left
13
+ end
14
+
15
+ def accounted_id
16
+ left_id
17
+ end
18
+
19
+ def ok_to_read
20
+ own_account? ? true : super
21
+ end
22
+
23
+ # allow account owner to update account field content
24
+ def ok_to_update
25
+ return true if own_account? && !name_changed? && !type_id_changed?
26
+
27
+ super
28
+ end
29
+
30
+ def changes_visible? act
31
+ act.actions_affecting(act.card).each do |action|
32
+ return true if action.card.ok? :read
33
+ end
34
+ false
35
+ end
36
+
37
+ def send_account_email email_template
38
+ ecard = Card[email_template]
39
+ unless ecard&.type_id == EmailTemplateID
40
+ raise Card::Error, "invalid email template: #{email_template}"
41
+ end
42
+
43
+ ecard.deliver self, to: email
44
+ end
45
+
46
+ def validate_api_key! api_key
47
+ api_key_card.validate! api_key
48
+ end
49
+
50
+ def method_missing method, *args
51
+ super unless args.empty? && (matches = method.match(/^(?<status>.*)\?$/))
52
+
53
+ status == matches[:status]
54
+ end
55
+
56
+ def respond_to_missing? method, _include_private=false
57
+ method.match?(/\?/) ? true : super
58
+ end
@@ -0,0 +1,99 @@
1
+ #### ON CREATE
2
+
3
+ event :set_default_salt, :prepare_to_validate, on: :create do
4
+ add_subfield(:salt).generate
5
+ end
6
+
7
+ event :set_default_status, :prepare_to_validate, on: :create do
8
+ add_subfield :status, content: (accounted&.try(:default_account_status) || "active")
9
+ end
10
+
11
+ # ON UPDATE
12
+
13
+ # reset password emails contain a link to update the +*account card
14
+ # and trigger this event
15
+ event :reset_password, :prepare_to_validate, on: :update, trigger: :required do
16
+ verifying_token :reset_password_success, :reset_password_failure
17
+ end
18
+
19
+ event :verify_and_activate, :prepare_to_validate, on: :update, trigger: :required do
20
+ activatable do
21
+ verifying_token :verify_and_activate_success, :verify_and_activate_failure
22
+ add_subcard(accounted)&.try :activate_accounted
23
+ end
24
+ end
25
+
26
+ event :password_redirect, :finalize, on: :update, when: :password_redirect? do
27
+ success << { id: name, view: "edit" }
28
+ end
29
+
30
+ # INTEGRATION
31
+
32
+ %i[password_reset_email verification_email welcome_email].each do |email|
33
+ event "send_#{email}".to_sym, :integrate, trigger: :required do
34
+ send_account_email email
35
+ end
36
+ end
37
+
38
+ ## EVENT HELPERS
39
+
40
+ def activatable
41
+ abort :failure, "no field manipulation mid-activation" if subcards.present?
42
+ # above is necessary because activation uses super user (Decko Bot),
43
+ # so allowing subcards would be unsafe
44
+ yield
45
+ end
46
+
47
+ # note: this only works in the context of an action.
48
+ # if run independently, it will not activate an account
49
+ event :activate_account do
50
+ add_subfield :status, content: "active"
51
+ trigger_event! :send_welcome_email
52
+ end
53
+
54
+ def verifying_token success, failure
55
+ requiring_token do |token|
56
+ result = Auth::Token.decode token
57
+ if result.is_a?(String)
58
+ send failure, result
59
+ else
60
+ send success
61
+ end
62
+ end
63
+ end
64
+
65
+ def requiring_token
66
+ if !(token = Env.params[:token])
67
+ errors.add :token, "is required"
68
+ else
69
+ yield token
70
+ end
71
+ end
72
+
73
+ def password_redirect?
74
+ Auth.current_id == accounted_id && password.blank?
75
+ end
76
+
77
+ def verify_and_activate_success
78
+ Auth.signin accounted_id
79
+ Auth.as_bot # use admin permissions for rest of action
80
+ activate_account
81
+ success << ""
82
+ end
83
+
84
+ def verify_and_activate_failure error_message
85
+ send_verification_email
86
+ errors.add :content,
87
+ "Sorry, #{error_message}. Please check your email for a new activation link."
88
+ end
89
+
90
+ def reset_password_success
91
+ Auth.signin accounted_id
92
+ success << { id: name, view: :edit }
93
+ abort :success
94
+ end
95
+
96
+ def reset_password_failure error_message
97
+ Auth.as_bot { send_password_reset_email }
98
+ errors.add :content, tr(:sorry_email_reset, error_msg: error_message)
99
+ end
@@ -0,0 +1,65 @@
1
+ format do
2
+ view :verify_url, cache: :never do
3
+ raise Error::PermissionDenied unless card.ok?(:create) || card.action
4
+
5
+ token_url :verify_and_activate, anonymous: true
6
+ end
7
+
8
+ view :reset_password_url do
9
+ raise Error::PermissionDenied unless card.password_card.ok? :update
10
+
11
+ token_url :reset_password
12
+ end
13
+
14
+ view :token_expiry do
15
+ "(#{token_expiry_sentence}"
16
+ end
17
+
18
+ view :token_days do
19
+ Card.config.token_expiry / 1.day
20
+ end
21
+
22
+ # DEPRECATED
23
+ view :verify_days, :token_days
24
+ view :reset_password_days, :token_days
25
+
26
+ def token_url trigger, extra_payload={}
27
+ card_url path(action: :update,
28
+ card: { trigger: trigger },
29
+ token: new_token(extra_payload))
30
+ end
31
+
32
+ def token_expiry_sentence
33
+ "Link will expire in #{render_token_days} days"
34
+ end
35
+
36
+ def new_token extra_payload
37
+ Auth::Token.encode card.accounted_id, extra_payload
38
+ end
39
+ end
40
+
41
+ format :html do
42
+ view :core do
43
+ [account_field_nest(:email, "email"),
44
+ account_field_nest(:password, "password")]
45
+ end
46
+
47
+ def account_field_nest field, title
48
+ field_nest field, title: title, view: :labeled
49
+ # edit: :inline, hide: [:help_link, :bridge_link]
50
+ end
51
+
52
+ before :content_formgroups do
53
+ voo.edit_structure = [[:email, "email"], [:password, "password"]]
54
+ end
55
+
56
+ view :token_expiry do
57
+ "<p><em>#{token_expiry_sentence}</em></p>"
58
+ end
59
+ end
60
+
61
+ format :email_html do
62
+ def mail context, fields
63
+ super context, fields.reverse_merge(to: card.email)
64
+ end
65
+ end
@@ -0,0 +1,48 @@
1
+ include_set Abstract::AccountField
2
+
3
+ # DURATIONS = "second|minute|hour|day|week|month|year".freeze
4
+
5
+ def history?
6
+ false
7
+ end
8
+
9
+ view :raw do
10
+ tr :private_data
11
+ end
12
+
13
+ def validate! api_key
14
+ error =
15
+ case
16
+ when !real? then [:token_not_found, tr(:error_token_not_found)]
17
+ # when expired? then [:token_expired, tr(:error_token_expired)]
18
+ when content != api_key then [:incorrect_token, tr(:error_incorrect_token)]
19
+ end
20
+ errors.add(*error) if error
21
+ error.nil?
22
+ end
23
+
24
+ # def expired?
25
+ # !permanent? && updated_at <= term.ago
26
+ # end
27
+ #
28
+ # def permanent?
29
+ # term == "permanent"
30
+ # end
31
+
32
+ # def term
33
+ # @term ||=
34
+ # if expiration.present?
35
+ # term_from_string expiration
36
+ # else
37
+ # Card.config.token_expiry
38
+ # end
39
+ # end
40
+
41
+ # def term_from_string string
42
+ # string.strip!
43
+ # return "permanent" if string == "none"
44
+ # re_match = /^(\d+)[\.\s]*(#{DURATIONS})s?$/.match(string)
45
+ # number, unit = re_match.captures if re_match
46
+ # raise Card::Open::Error, tr(:exception_bad_expiration, example: '2 days') unless unit
47
+ # number.to_i.send unit
48
+ # end
@@ -0,0 +1,46 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ include_set Abstract::AccountField
4
+
5
+ event :validate_email, :validate, on: :save do
6
+ return unless content?
7
+
8
+ self.content = content.strip
9
+ return if content.match?(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i)
10
+
11
+ errors.add :content, tr(:error_invalid_address)
12
+ end
13
+
14
+ event :validate_unique_email, after: :validate_email, on: :save do
15
+ if content.present?
16
+ Auth.as_bot do
17
+ cql = { right_id: EmailID, eq: content, return: :id }
18
+ cql[:not] = { id: id } if id
19
+ cql_comment = tr(:search_email_duplicate, content: content)
20
+ if Card.search(cql, cql_comment).first
21
+ errors.add :content, tr(:error_not_unique)
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ event :downcase_email, :prepare_to_validate, on: :save do
28
+ return if !content || content == content.downcase
29
+ self.content = content.downcase
30
+ end
31
+
32
+ def email_required?
33
+ !left.system?
34
+ end
35
+
36
+ def ok_to_read
37
+ if own_email? || Auth.always_ok?
38
+ true
39
+ else
40
+ deny_because tr(:deny_email_restricted)
41
+ end
42
+ end
43
+
44
+ def own_email?
45
+ name.part_names[0].key == Auth.as_card.key
46
+ end
@@ -0,0 +1,51 @@
1
+ include_set Abstract::AccountField
2
+
3
+ def history?
4
+ false
5
+ end
6
+
7
+ def ok_to_read
8
+ own_account? ? true : super
9
+ end
10
+
11
+ event :encrypt_password, :store,
12
+ on: :save, changed: :content,
13
+ when: proc { !Card::Env[:no_password_encryptions] } do
14
+ # no_password_encryptions = hack for import - fix with api for ignoring events
15
+ salt = left&.salt
16
+ self.content = Auth.encrypt content, salt
17
+
18
+ # errors.add :password, 'need a valid salt'
19
+ # turns out we have a lot of existing account without a salt.
20
+ # not sure when that broke??
21
+ end
22
+
23
+ event :validate_password, :validate, on: :save do
24
+ return if content.length > 3
25
+
26
+ errors.add :password, tr(:password_length)
27
+ end
28
+
29
+ event :validate_password_present, :prepare_to_validate, on: :update do
30
+ abort :success if content.blank?
31
+ end
32
+
33
+ view :raw do
34
+ tr :encrypted
35
+ end
36
+
37
+ format :html do
38
+ view :core, wrap: :em do
39
+ render_raw
40
+ end
41
+
42
+ view :input do
43
+ card.content = ""
44
+ password_field :content, class: "d0-card-content", autocomplete: autocomplete?
45
+ end
46
+
47
+ def autocomplete?
48
+ return "on" if @parent && @parent.card.name == "*signin+*account" # HACK
49
+ "off"
50
+ end
51
+ end
@@ -0,0 +1,27 @@
1
+ event :validate_permission_to_assign_roles, :validate, on: :save do
2
+ return unless (fr = forbidden_roles).present?
3
+
4
+ errors.add :permission_denied,
5
+ "You don't have permission to assign the role#{'s' if fr.size > 1} "\
6
+ "#{fr.map(&:name).to_sentence}" # LOCALIZE
7
+ end
8
+
9
+ def forbidden_roles
10
+ # restore old roles for permission check
11
+ with_old_role_permissions do |new_roles|
12
+ new_roles.select do |card|
13
+ !Card.fetch(card, "*members").ok? :update
14
+ end
15
+ end
16
+ end
17
+
18
+ def with_old_role_permissions
19
+ new_roles = item_cards
20
+ new_content = content
21
+ left.clear_roles
22
+ Auth.update_always_cache Card::Auth.as_id, nil
23
+ self.content = db_content_before_act
24
+ yield new_roles
25
+ ensure
26
+ self.content = new_content
27
+ end
@@ -0,0 +1,13 @@
1
+ include_set Abstract::AccountField
2
+
3
+ def generate
4
+ self.content = Digest::SHA1.hexdigest "--#{Time.zone.now}--"
5
+ end
6
+
7
+ def history?
8
+ false
9
+ end
10
+
11
+ view :raw do
12
+ tr :private_data
13
+ end
@@ -0,0 +1,18 @@
1
+ include_set Abstract::AccountField
2
+ include_set Abstract::Pointer
3
+
4
+ def input_type
5
+ :radio
6
+ end
7
+
8
+ def option_names
9
+ %w[unapproved unverified active blocked system]
10
+ end
11
+
12
+ def ok_to_update
13
+ if own_account? && !Auth.always_ok?
14
+ deny_because you_cant(tr(:deny_not_change_own_account))
15
+ else
16
+ super
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ setting_opts group: :permission,
2
+ position: 5,
3
+ help_text: "Anti-spam setting. Requires non-signed-in users to complete a "\
4
+ "[[http://decko.org/captcha|captcha]] before adding or editing "\
5
+ "cards (where permitted)."
@@ -0,0 +1,226 @@
1
+ # The Sign In card manages logging in and out of the site.
2
+ #
3
+ # /:signin (core view) gives the login ui
4
+ # /:signin?view=edit gives the forgot password ui
5
+
6
+ # /update/:signin is the login action
7
+ # /delete/:signin is the logout action
8
+
9
+ # authentication event
10
+ event :signin, :validate, on: :update do
11
+ email = subfield :email
12
+ email &&= email.content
13
+ pword = subfield :password
14
+ pword &&= pword.content
15
+
16
+ authenticate_or_abort email, pword
17
+ end
18
+
19
+ # abort after successful signin (do not save card)
20
+ event :signin_success, after: :signin do
21
+ abort :success
22
+ end
23
+
24
+ event :signout, :validate, on: :delete do
25
+ Env.reset_session
26
+ Auth.signin AnonymousID
27
+ abort :success
28
+ end
29
+
30
+ # triggered by clicking "Reset my Password", this sends out the verification password
31
+ # and aborts (does not sign in)
32
+ event :send_reset_password_token, before: :signin, on: :update, trigger: :required do
33
+ email = subfield(:email)&.content
34
+ send_reset_password_email_or_fail email
35
+ end
36
+
37
+ def ok_to_read
38
+ true
39
+ end
40
+
41
+ def recaptcha_on?
42
+ false
43
+ end
44
+
45
+ def i18n_signin key
46
+ I18n.t key, scope: "mod.card-mod-account.set.self.signin"
47
+ end
48
+
49
+ def authenticate_or_abort email, pword
50
+ abort_unless email, :email_missing
51
+ abort_unless pword, :password_missing
52
+ authenticate_and_signin(email, pword) || failed_signin(email)
53
+ end
54
+
55
+ def authenticate_and_signin email, pword
56
+ return unless (account = Auth.authenticate email, pword)
57
+
58
+ Auth.signin account.left_id
59
+ end
60
+
61
+ def failed_signin email
62
+ errors.add :signin, signin_error_message(account_for(email))
63
+ abort :failure
64
+ end
65
+
66
+ def abort_unless value, error_key
67
+ abort :failure, i18n_signin(error_key) unless value
68
+ end
69
+
70
+ def signin_error_message account
71
+ case
72
+ when account.nil? then i18n_signin(:error_unknown_email)
73
+ when !account.active? then i18n_signin(:error_not_active)
74
+ else i18n_signin(:error_wrong_password)
75
+ end
76
+ end
77
+
78
+ def error_on field, error_key
79
+ errors.add field, i18n_signin(error_key)
80
+ end
81
+
82
+ def account_for email
83
+ Auth.find_account_by_email email
84
+ end
85
+
86
+ def send_reset_password_email_or_fail email
87
+ aborting do
88
+ break errors.add :email, i18n_signin(:error_blank) if email.blank?
89
+
90
+ if (account = Auth.find_account_by_email(email))&.active?
91
+ Auth.as_bot { account.send_password_reset_email }
92
+ elsif account
93
+ errors.add :account, i18n_signin(:error_not_active)
94
+ else
95
+ errors.add :email, i18n_signin(:error_not_recognized)
96
+ end
97
+ end
98
+ end
99
+
100
+ def send_reset_password_email_or_fail email
101
+ aborting do
102
+ break if blank_email? email
103
+
104
+ if (account = account_for email)&.active?
105
+ send_reset_password_email account
106
+ else
107
+ reset_password_fail account
108
+ end
109
+ end
110
+ end
111
+
112
+ def blank_email? email
113
+ return false if email.present?
114
+
115
+ error_on :email, :error_blank
116
+ end
117
+
118
+ def send_reset_password_email account
119
+ Auth.as_bot { account.send_password_reset_email }
120
+ end
121
+
122
+ def reset_password_fail account
123
+ if account
124
+ error_on :account, :error_not_active
125
+ else
126
+ error_on :email, :error_not_recognized
127
+ end
128
+ end
129
+
130
+ format :html do
131
+ view :core, cache: :never do
132
+ voo.edit_structure = [signin_field(:email), signin_field(:password)]
133
+ with_nest_mode :edit do
134
+ card_form :update, recaptcha: :off, success: signin_success do
135
+ [
136
+ _render_content_formgroups,
137
+ _render_signin_buttons
138
+ ]
139
+ end
140
+ end
141
+ end
142
+
143
+ view :open do
144
+ voo.show :help
145
+ voo.hide :menu
146
+ super()
147
+ end
148
+
149
+ # FIXME: need a generic solution for this
150
+ view :title do
151
+ voo.title ||= I18n.t(:sign_in_title, scope: "mod.card-mod-account.set.self.signin")
152
+ super()
153
+ end
154
+
155
+ view :open_content do
156
+ # annoying step designed to avoid table of contents. sigh
157
+ _render_core
158
+ end
159
+
160
+ view :one_line_content do
161
+ ""
162
+ end
163
+
164
+ view :reset_password_success do
165
+ # 'Check your email for a link to reset your password'
166
+ frame { I18n.t(:check_email, scope: "mod.card-mod-account.set.self.signin") }
167
+ end
168
+
169
+ view :signin_buttons do
170
+ button_formgroup do
171
+ [signin_button, signup_link, reset_password_link]
172
+ end
173
+ end
174
+
175
+ # FORGOT PASSWORD
176
+ view :edit do
177
+ reset_password_voo
178
+ Auth.as_bot { super() }
179
+ end
180
+
181
+ def reset_password_voo
182
+ voo.title ||= card.i18n_signin(:forgot_password)
183
+ voo.edit_structure = [signin_field(:email)]
184
+ voo.hide :help
185
+ end
186
+
187
+ view :edit_buttons do
188
+ text = I18n.t :reset_my_password, scope: "mod.card-mod-account.set.self.signin"
189
+ button_tag text, situation: "primary", class: "_close-modal-on-success"
190
+ end
191
+
192
+ def signin_success
193
+ "REDIRECT: #{Env.interrupted_action || '*previous'}"
194
+ end
195
+
196
+ def signin_button
197
+ text = I18n.t :sign_in, scope: "mod.card-mod-account.set.self.signin"
198
+ button_tag text, situation: "primary"
199
+ end
200
+
201
+ def signup_link
202
+ text = I18n.t :or_sign_up, scope: "mod.card-mod-account.set.self.signin"
203
+ subformat(Card[:account_links]).render! :sign_up, title: text
204
+ end
205
+
206
+ def reset_password_link
207
+ text = I18n.t :reset_password, scope: "mod.card-mod-account.set.self.signin"
208
+ link = link_to_view :edit, text, path: { slot: { hide: :bridge_link } }
209
+ # FIXME: inline styling
210
+ raw("<div style='float:right'>#{link}</div>")
211
+ end
212
+
213
+ def edit_view_hidden
214
+ hidden_tags card: { trigger: :send_reset_password_token }
215
+ end
216
+
217
+ def edit_success
218
+ { view: :reset_password_success }
219
+ end
220
+
221
+ def signin_field name
222
+ nest_name = "".to_name.trait(name)
223
+ [nest_name, { title: name.to_s, view: "titled",
224
+ nest_name: nest_name, skip_perms: true }]
225
+ end
226
+ end
@@ -0,0 +1,21 @@
1
+ def disabled?
2
+ Auth.current&.fetch(:disabled_roles)&.item_ids&.include? id
3
+ end
4
+
5
+ format :html do
6
+ view :link_with_checkbox, cache: :never do
7
+ role_checkbox
8
+ end
9
+
10
+ def role_checkbox
11
+ name = card.disabled? ? "add_item" : "drop_item"
12
+ subformat(Auth.current.field(:disabled_roles, new: {})).card_form :update do
13
+ [check_box_tag(name, card.id, !card.disabled?, class: "_edit-item"),
14
+ render_link]
15
+ end
16
+ end
17
+
18
+ def related_by_content_items
19
+ super.unshift ["members", :members]
20
+ end
21
+ end
@@ -0,0 +1,54 @@
1
+ include_set Abstract::Accountable
2
+
3
+ require_field :account
4
+
5
+ def default_account_status
6
+ can_approve? ? "unverified" : "unapproved"
7
+ end
8
+
9
+ def can_approve?
10
+ Card.new(type_id: Card.default_accounted_type_id).ok? :create
11
+ end
12
+
13
+ def activate_accounted
14
+ self.type_id = Card.default_accounted_type_id
15
+ end
16
+
17
+ event :auto_approve_with_verification, :validate, on: :create, when: :can_approve? do
18
+ request_verification
19
+ end
20
+
21
+ event :approve_with_verification, :validate, on: :update, trigger: :required do
22
+ approvable { request_verification }
23
+ end
24
+
25
+ event :approve_without_verification, :validate, on: :update, trigger: :required do
26
+ # TODO: if validated here, add trigger and activate in storage phase
27
+ approvable do
28
+ activate_accounted
29
+ account_subfield.activate_account
30
+ end
31
+ end
32
+
33
+ event :act_as_current_for_integrate_stage, :integrate, on: :create do
34
+ # needs justification!
35
+ Auth.current_id = id
36
+ end
37
+
38
+ def account_subfield
39
+ subfield(:account) || add_subfield(:account)
40
+ end
41
+
42
+ def request_verification
43
+ acct = account_subfield
44
+ acct.add_subfield :status, content: "unverified"
45
+ acct.trigger_event! :send_verification_email
46
+ end
47
+
48
+ def approvable
49
+ if can_approve?
50
+ yield
51
+ else
52
+ abort :failure, "illegal approval" # raise permission denied?
53
+ end
54
+ end
@@ -0,0 +1,4 @@
1
+ - unless card.new_card? # necessary?
2
+ .invite-links
3
+ = lines.map { |h| "<div>#{h}</div>" }.join "\n"
4
+ = body
@@ -0,0 +1,93 @@
1
+ format :html do
2
+ def invitation?
3
+ Auth.signed_in? && card.can_approve?
4
+ end
5
+
6
+ view :new do
7
+ voo.title = invitation? ? tr(:invite) : tr(:sign_up)
8
+ super()
9
+ end
10
+
11
+ view :content_formgroups do
12
+ [account_formgroups, (card.structure ? edit_slot : "")].join
13
+ end
14
+
15
+ view :new_buttons do
16
+ button_formgroup do
17
+ [standard_create_button, invite_button].compact
18
+ end
19
+ end
20
+
21
+ def invite_button
22
+ return unless invitation?
23
+ button_tag "Send Invitation", situation: "primary"
24
+ end
25
+
26
+ view :core, template: :haml do
27
+ @lines = [signup_line] + account_lines
28
+ @body = process_content _render_raw
29
+ end
30
+
31
+ def signup_line
32
+ ["<strong>#{safe_name}</strong>",
33
+ ("was" if invited?),
34
+ "signed up on #{format_date card.created_at}"].compact.join " "
35
+ end
36
+
37
+ def invited?
38
+ !self_signup?
39
+ end
40
+
41
+ def self_signup?
42
+ card.creator_id == AnonymousID
43
+ end
44
+
45
+ def account_lines
46
+ if card.account
47
+ verification_lines
48
+ else
49
+ [tr(:missing_account)]
50
+ end
51
+ end
52
+
53
+ def verification_lines
54
+ [verification_sent_line, verification_link_line].compact
55
+ end
56
+
57
+ def verification_sent_line
58
+ account = card.account
59
+ return unless account.email_card.ok?(:read)
60
+ "A verification email has been sent to #{account.email}"
61
+ end
62
+
63
+ def verification_link_line
64
+ links = verification_links
65
+ return if links.empty?
66
+ links.join " "
67
+ end
68
+
69
+ def verification_links
70
+ [approve_with_token_link, approve_without_token_link, deny_link].compact
71
+ end
72
+
73
+ def approve_with_token_link
74
+ action = card.account.status == "unverified" ? "Resend" : "Send"
75
+ approval_link "#{action} verification email", :with
76
+ end
77
+
78
+ def approve_without_token_link
79
+ approval_link "Approve without verification", :without
80
+ end
81
+
82
+ def approval_link text, with_or_without
83
+ return unless card.can_approve?
84
+ link_to_card card, text,
85
+ path: { action: :update,
86
+ card: { trigger: "approve_#{with_or_without}_verification" } }
87
+ end
88
+
89
+ def deny_link
90
+ return unless card.ok? :delete
91
+ link_to_card card, "Deny and delete", path: { action: :delete }
92
+ end
93
+ end
@@ -0,0 +1,67 @@
1
+ include_set Abstract::Accountable
2
+
3
+ attr_accessor :email
4
+
5
+ format :html do
6
+ view :setup, unknown: true, perms: ->(_fmt) { Auth.needs_setup? } do
7
+ with_nest_mode :edit do
8
+ voo.title = "Your deck is ready to go!" # LOCALIZE
9
+ voo.show! :help
10
+ voo.hide! :menu
11
+ voo.help = haml :setup_help
12
+ Auth.as_bot { setup_form }
13
+ end
14
+ end
15
+
16
+ def setup_form
17
+ frame_and_form :create do
18
+ [
19
+ setup_hidden_fields,
20
+ _render_name_formgroup,
21
+ account_formgroups,
22
+ setup_form_buttons
23
+ ]
24
+ end
25
+ end
26
+
27
+ def setup_form_buttons
28
+ button_formgroup { setup_button }
29
+ end
30
+
31
+ def setup_button
32
+ submit_button text: "Set up", disable_with: "Setting up"
33
+ end
34
+
35
+ def setup_hidden_fields
36
+ hidden_tags(
37
+ setup: true,
38
+ success: "REDIRECT: #{path mark: ''}",
39
+ "card[type_id]" => Card.default_accounted_type_id
40
+ )
41
+ end
42
+ end
43
+
44
+ def setup?
45
+ Card::Env.params[:setup]
46
+ end
47
+
48
+ event :setup_as_bot, before: :check_permissions, on: :create, when: :setup? do
49
+ abort :failure unless Auth.needs_setup?
50
+ Auth.as_bot
51
+ # we need bot authority to set the initial administrator roles
52
+ # this is granted and inspected here as a separate event for
53
+ # flexibility and security when configuring initial setups
54
+ end
55
+
56
+ event :setup_first_user, :prepare_to_store, on: :create, when: :setup? do
57
+ add_subcard "signup alert email+*to", content: name
58
+ add_subfield :roles, content: roles_for_first_user
59
+ end
60
+
61
+ def roles_for_first_user
62
+ %i[help_desk shark administrator].map(&:cardname)
63
+ end
64
+
65
+ event :signin_after_setup, :integrate, on: :create, when: :setup? do
66
+ Auth.signin id
67
+ end
@@ -0,0 +1,10 @@
1
+ %h3 First, set up an account.
2
+ .pb-2
3
+ As the first account holder for this deck, you will automatically have several
4
+ permissioned roles. You can configure these roles at any time.
5
+
6
+
7
+ - if Card.config.action_mailer.perform_deliveries == false
8
+ .bg-warning.p-3
9
+ WARNING: Email delivery is turned off.
10
+ Change settings in config/application.rb to send sign up notifications.
@@ -0,0 +1,13 @@
1
+ # supports legacy references to <User>+*email
2
+ # (standard representation is now <User>+*account+*email)
3
+ view :raw do
4
+ card.content_email || card.account_email || ""
5
+ end
6
+
7
+ def content_email
8
+ content if real?
9
+ end
10
+
11
+ def account_email
12
+ left&.account&.email
13
+ end
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: card-mod-account
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.11.0
5
+ platform: ruby
6
+ authors:
7
+ - Ethan McCutchen
8
+ - Philipp Kühl
9
+ - Gerry Gleason
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2020-12-24 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: card
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - '='
20
+ - !ruby/object:Gem::Version
21
+ version: 1.101.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - '='
27
+ - !ruby/object:Gem::Version
28
+ version: 1.101.0
29
+ - !ruby/object:Gem::Dependency
30
+ name: card-mod-email
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - '='
34
+ - !ruby/object:Gem::Version
35
+ version: 0.11.0
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - '='
41
+ - !ruby/object:Gem::Version
42
+ version: 0.11.0
43
+ - !ruby/object:Gem::Dependency
44
+ name: card-mod-permissions
45
+ requirement: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - '='
48
+ - !ruby/object:Gem::Version
49
+ version: 0.11.0
50
+ type: :runtime
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - '='
55
+ - !ruby/object:Gem::Version
56
+ version: 0.11.0
57
+ - !ruby/object:Gem::Dependency
58
+ name: card-mod-list
59
+ requirement: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - '='
62
+ - !ruby/object:Gem::Version
63
+ version: 0.11.0
64
+ type: :runtime
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - '='
69
+ - !ruby/object:Gem::Version
70
+ version: 0.11.0
71
+ description: ''
72
+ email:
73
+ - info@decko.org
74
+ executables: []
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - README.md
79
+ - set/abstract/account_field.rb
80
+ - set/abstract/accountable.rb
81
+ - set/all/account.rb
82
+ - set/right/account.rb
83
+ - set/right/account/events.rb
84
+ - set/right/account/views.rb
85
+ - set/right/api_key.rb
86
+ - set/right/email.rb
87
+ - set/right/password.rb
88
+ - set/right/roles.rb
89
+ - set/right/salt.rb
90
+ - set/right/status.rb
91
+ - set/self/captcha.rb
92
+ - set/self/signin.rb
93
+ - set/type/role.rb
94
+ - set/type/signup.rb
95
+ - set/type/signup/core.haml
96
+ - set/type/signup/views.rb
97
+ - set/type/user.rb
98
+ - set/type/user/setup_help.haml
99
+ - set/type_plus_right/user/email.rb
100
+ homepage: http://decko.org
101
+ licenses:
102
+ - GPL-3.0
103
+ metadata:
104
+ card-mod: account
105
+ post_install_message:
106
+ rdoc_options: []
107
+ require_paths:
108
+ - lib
109
+ required_ruby_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '2.5'
114
+ required_rubygems_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ requirements: []
120
+ rubygems_version: 3.0.3
121
+ signing_key:
122
+ specification_version: 4
123
+ summary: Email-based account handling for decko cards
124
+ test_files: []