card-mod-account 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []