liquid_feedback_frontend

view model/member.lua @ 1231:e7fc3fed1593

Refactored password reset functionality
author bsw
date Tue Dec 01 18:12:00 2015 +0100 (2015-12-01)
parents c80ac323dee7
children 77dc363fa0be
line source
1 Member = mondelefant.new_class()
2 Member.table = 'member'
4 Member:add_reference{
5 mode = "1m",
6 to = "MemberHistory",
7 this_key = 'id',
8 that_key = 'member_id',
9 ref = 'history_entries',
10 back_ref = 'member'
11 }
13 Member:add_reference{
14 mode = '1m',
15 to = "MemberImage",
16 this_key = 'id',
17 that_key = 'member_id',
18 ref = 'images',
19 back_ref = 'member'
20 }
22 Member:add_reference{
23 mode = '1m',
24 to = "Contact",
25 this_key = 'id',
26 that_key = 'member_id',
27 ref = 'contacts',
28 back_ref = 'member',
29 default_order = '"other_member_id"'
30 }
32 Member:add_reference{
33 mode = '1m',
34 to = "Contact",
35 this_key = 'id',
36 that_key = 'member_id',
37 ref = 'foreign_contacts',
38 back_ref = 'other_member',
39 default_order = '"member_id"'
40 }
42 Member:add_reference{
43 mode = '1m',
44 to = "Session",
45 this_key = 'id',
46 that_key = 'member_id',
47 ref = 'sessions',
48 back_ref = 'member',
49 default_order = '"ident"'
50 }
52 Member:add_reference{
53 mode = '1m',
54 to = "Draft",
55 this_key = 'id',
56 that_key = 'author_id',
57 ref = 'drafts',
58 back_ref = 'author',
59 default_order = '"id"'
60 }
62 Member:add_reference{
63 mode = '1m',
64 to = "Suggestion",
65 this_key = 'id',
66 that_key = 'author_id',
67 ref = 'suggestions',
68 back_ref = 'author',
69 default_order = '"id"'
70 }
72 Member:add_reference{
73 mode = '1m',
74 to = "Membership",
75 this_key = 'id',
76 that_key = 'member_id',
77 ref = 'memberships',
78 back_ref = 'member',
79 default_order = '"area_id"'
80 }
82 Member:add_reference{
83 mode = '1m',
84 to = "Interest",
85 this_key = 'id',
86 that_key = 'member_id',
87 ref = 'interests',
88 back_ref = 'member',
89 default_order = '"id"'
90 }
92 Member:add_reference{
93 mode = '1m',
94 to = "Initiator",
95 this_key = 'id',
96 that_key = 'member_id',
97 ref = 'initiators',
98 back_ref = 'member'
99 }
101 Member:add_reference{
102 mode = '1m',
103 to = "Supporter",
104 this_key = 'id',
105 that_key = 'member_id',
106 ref = 'supporters',
107 back_ref = 'member'
108 }
110 Member:add_reference{
111 mode = '1m',
112 to = "Opinion",
113 this_key = 'id',
114 that_key = 'member_id',
115 ref = 'opinions',
116 back_ref = 'member',
117 default_order = '"id"'
118 }
120 Member:add_reference{
121 mode = '1m',
122 to = "Delegation",
123 this_key = 'id',
124 that_key = 'truster_id',
125 ref = 'outgoing_delegations',
126 back_ref = 'truster',
127 -- default_order = '"id"'
128 }
130 Member:add_reference{
131 mode = '1m',
132 to = "Delegation",
133 this_key = 'id',
134 that_key = 'trustee_id',
135 ref = 'incoming_delegations',
136 back_ref = 'trustee',
137 -- default_order = '"id"'
138 }
140 Member:add_reference{
141 mode = '1m',
142 to = "DirectVoter",
143 this_key = 'id',
144 that_key = 'member_id',
145 ref = 'direct_voter',
146 back_ref = 'member',
147 default_order = '"issue_id"'
148 }
150 Member:add_reference{
151 mode = '1m',
152 to = "Vote",
153 this_key = 'id',
154 that_key = 'member_id',
155 ref = 'vote',
156 back_ref = 'member',
157 default_order = '"issue_id", "initiative_id"'
158 }
160 Member:add_reference{
161 mode = 'mm',
162 to = "Member",
163 this_key = 'id',
164 that_key = 'id',
165 connected_by_table = 'contact',
166 connected_by_this_key = 'member_id',
167 connected_by_that_key = 'other_member_id',
168 ref = 'saved_members',
169 }
171 Member:add_reference{
172 mode = 'mm',
173 to = "Member",
174 this_key = 'id',
175 that_key = 'id',
176 connected_by_table = 'contact',
177 connected_by_this_key = 'other_member_id',
178 connected_by_that_key = 'member_id',
179 ref = 'saved_by_members',
180 }
182 Member:add_reference{
183 mode = 'mm',
184 to = "Unit",
185 this_key = 'id',
186 that_key = 'id',
187 connected_by_table = 'privilege',
188 connected_by_this_key = 'member_id',
189 connected_by_that_key = 'unit_id',
190 ref = 'units'
191 }
193 Member:add_reference{
194 mode = 'mm',
195 to = "Area",
196 this_key = 'id',
197 that_key = 'id',
198 connected_by_table = 'membership',
199 connected_by_this_key = 'member_id',
200 connected_by_that_key = 'area_id',
201 ref = 'areas'
202 }
204 Member:add_reference{
205 mode = 'mm',
206 to = "Issue",
207 this_key = 'id',
208 that_key = 'id',
209 connected_by_table = 'interest',
210 connected_by_this_key = 'member_id',
211 connected_by_that_key = 'issue_id',
212 ref = 'issues'
213 }
215 Member:add_reference{
216 mode = 'mm',
217 to = "Initiative",
218 this_key = 'id',
219 that_key = 'id',
220 connected_by_table = 'initiator',
221 connected_by_this_key = 'member_id',
222 connected_by_that_key = 'initiative_id',
223 ref = 'initiated_initiatives'
224 }
226 Member:add_reference{
227 mode = 'mm',
228 to = "Initiative",
229 this_key = 'id',
230 that_key = 'id',
231 connected_by_table = 'supporter',
232 connected_by_this_key = 'member_id',
233 connected_by_that_key = 'initiative_id',
234 ref = 'supported_initiatives'
235 }
237 model.has_rendered_content(Member, RenderedMemberStatement, "statement")
239 function Member:build_selector(args)
240 local selector = self:new_selector()
241 if args.active ~= nil then
242 selector:add_where{ "member.active = ?", args.active }
243 end
244 if args.locked ~= nil then
245 selector:add_where{ "member.locked = ?", args.locked }
246 end
247 if args.is_contact_of_member_id then
248 selector:join("contact", "__model_member__contact", "member.id = __model_member__contact.other_member_id")
249 selector:add_where{ "__model_member__contact.member_id = ?", args.is_contact_of_member_id }
250 end
251 if args.voting_right_for_unit_id then
252 selector:join("privilege", "__model_member__privilege", { "member.id = __model_member__privilege.member_id AND __model_member__privilege.voting_right AND __model_member__privilege.unit_id = ?", args.voting_right_for_unit_id })
253 end
254 if args.admin_search then
255 local search_string = "%" .. args.admin_search .. "%"
256 selector:add_where{ "member.identification ILIKE ? OR member.name ILIKE ?", search_string, search_string }
257 end
258 if args.order then
259 if args.order == "id" then
260 selector:add_order_by("id")
261 elseif args.order == "identification" then
262 selector:add_order_by("identification")
263 elseif args.order == "name" then
264 selector:add_order_by("name")
265 else
266 error("invalid order")
267 end
268 end
269 return selector
270 end
272 function Member:lockForReference()
273 self.get_db_conn().query("LOCK TABLE " .. self:get_qualified_table() .. " IN ROW SHARE MODE")
274 end
277 function Member:get_all_by_authority(authority)
279 local members = Member:new_selector()
280 :add_where{ "authority = ?", authority }
281 :add_field("authority_uid")
282 :exec()
284 return members
285 end
287 function Member.object:set_password(password)
288 trace.disable()
290 local hash_prefix
291 local salt_length
293 local function rounds()
294 return multirand.integer(
295 config.password_hash_min_rounds,
296 config.password_hash_max_rounds
297 )
298 end
300 if config.password_hash_algorithm == "crypt_md5" then
301 hash_prefix = "$1$"
302 salt_length = 8
304 elseif config.password_hash_algorithm == "crypt_sha256" then
305 hash_prefix = "$5$rounds=" .. rounds() .. "$"
306 salt_length = 16
308 elseif config.password_hash_algorithm == "crypt_sha512" then
309 hash_prefix = "$6$rounds=" .. rounds() .. "$"
310 salt_length = 16
312 else
313 error("Unknown hash algorithm selected in configuration")
315 end
317 hash_prefix = hash_prefix .. multirand.string(
318 salt_length,
319 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz./"
320 )
322 local hash = extos.crypt(password, hash_prefix)
324 if not hash or hash:sub(1, #hash_prefix) ~= hash_prefix then
325 error("Password hashing algorithm failed")
326 end
328 self.password = hash
329 self.password_reset_secret = nil
330 self.password_reset_secret_expiry = nil
331 end
333 function Member.object:check_password(password)
334 if type(password) == "string" and type(self.password) == "string" then
335 return extos.crypt(password, self.password) == self.password
336 else
337 return false
338 end
339 end
341 function Member.object_get:password_hash_needs_update()
343 if self.password == nil then
344 return nil
345 end
347 local function check_rounds(rounds)
348 if rounds then
349 rounds = tonumber(rounds)
350 if
351 rounds >= config.password_hash_min_rounds and
352 rounds <= config.password_hash_max_rounds
353 then
354 return false
355 end
356 end
357 return true
358 end
360 if config.password_hash_algorithm == "crypt_md5" then
362 return self.password:sub(1,3) ~= "$1$"
364 elseif config.password_hash_algorithm == "crypt_sha256" then
366 return check_rounds(self.password:match("^%$5%$rounds=([1-9][0-9]*)%$"))
368 elseif config.password_hash_algorithm == "crypt_sha512" then
370 return check_rounds(self.password:match("^%$6%$rounds=([1-9][0-9]*)%$"))
372 else
373 error("Unknown hash algorithm selected in configuration")
375 end
377 end
379 function Member.object_get:published_contacts()
380 return Member:new_selector()
381 :join('"contact"', nil, '"contact"."other_member_id" = "member"."id"')
382 :add_where{ '"contact"."member_id" = ?', self.id }
383 :add_where("public")
384 :exec()
385 end
387 function Member:by_login_and_password(login, password)
389 local function prepare_login_selector()
390 local selector = self:new_selector()
391 selector:add_field({ "now() > COALESCE(last_delegation_check, activated) + ?::interval", config.check_delegations_interval_hard }, "needs_delegation_check_hard")
392 selector:add_where('NOT "locked"')
393 selector:optional_object_mode()
394 return selector
395 end
397 local function do_local_login()
398 local selector = prepare_login_selector()
399 selector:add_where{'"login" = ?', login }
400 local member = selector:exec()
401 if member and member:check_password(password) then
402 return member
403 else
404 return nil
405 end
406 end
408 if config.ldap.member then
410 -- Let's check the users credentials against the LDAP
411 local ldap_entry, ldap_err = ldap.check_credentials(login, password)
413 -- Is the user already registered as member?
414 local uid
415 local selector = prepare_login_selector()
417 -- Get login name from LDAP entry
418 if ldap_entry then
419 uid = config.ldap.member.uid_map(ldap_entry)
420 selector:add_where{'"authority" = ? AND "authority_uid" = ?', "ldap", uid }
422 -- or build it from the login
423 else
424 login = config.ldap.member.login_normalizer(login)
425 selector:add_where{'"authority" = ? AND "authority_uid" = ?', "ldap", login }
426 end
428 local member = selector:exec()
429 -- The member is already registered
430 if member then
432 -- The credentials entered by the user are invalid
433 if ldap_err == "invalid_credentials" then
435 -- Check if the user tried a cached password (which is invalid now)
436 if config.ldap.member.cache_passwords and member:check_password(password) then
437 member.password = nil
438 member:save()
439 end
441 -- Try a regular login
442 return do_local_login()
444 end
446 -- The credentials were accepted by the LDAP server and no error occured
447 if ldap_entry and not ldap_err then
449 -- Cache the password (if feature enabled)
450 if config.ldap.member.cache_passwords and not member:check_password(password) then
451 member:set_password(password)
452 end
454 -- update the member attributes and privileges from LDAP
455 local ldap_conn, ldap_err, err, err2 = ldap.update_member_attr(member, nil, uid)
456 if not err then
457 local err = member:try_save()
458 if err then
459 return nil, "member_save_error", err
460 end
461 local succes, err, err2 = ldap.update_member_privileges(member, ldap_entry)
462 if err then
463 return nil, "update_member_privileges_error", err, err2
464 end
465 return member
466 end
468 end
470 -- Some kind of LDAP error happened, if cached password are enabled,
471 -- check user credentials against the cache
472 if config.ldap.member.cache_passwords and member:check_password(password) then
474 -- return the successfully logged in member
475 return member
477 end
479 -- The member is not registered
480 elseif config.ldap.member.registration and ldap_entry and not ldap_err then
481 -- Automatic registration ("auto")
482 if config.ldap.member.registration == "auto" then
483 member = Member:new()
484 member.authority = "ldap"
485 local ldap_login
486 if config.ldap.member.cache_passwords then
487 if config.ldap.member.login_normalizer then
488 ldap_login = config.ldap.member.login_normalizer(login)
489 else
490 ldap_login = login
491 end
492 end
493 -- TODO change this when SQL layers supports hstore
494 member.authority_uid = uid
495 member.authority_login = ldap_login
496 member.activated = "now"
497 member.last_activity = "now"
498 if config.ldap.member.cache_passwords then
499 member:set_password(password)
500 end
501 local ldap_conn, ldap_err, err, err2 = ldap.update_member_attr(member, nil, uid)
502 if not err then
503 local err = member:try_save()
504 if err then
505 return nil, "member_save_error", err
506 end
507 local success, err, err2 = ldap.update_member_privileges(member, ldap_entry)
508 if err then
509 return nil, "update_member_privileges_error", err, err2
510 end
511 return member
512 end
514 -- No automatic registration
515 else
516 return nil, "ldap_credentials_valid_but_no_member", uid
517 end
518 end
520 end
522 return do_local_login()
524 end
526 function Member:by_login(login)
527 local selector = self:new_selector()
528 selector:add_where{'"login" = ?', login }
529 selector:optional_object_mode()
530 return selector:exec()
531 end
533 function Member:by_name(name)
534 local selector = self:new_selector()
535 selector:add_where{'"name" = ?', name }
536 selector:optional_object_mode()
537 return selector:exec()
538 end
540 function Member:get_search_selector(search_string)
541 return self:new_selector()
542 :add_field( {'"highlight"("member"."name", ?)', search_string }, "name_highlighted")
543 :add_where{ '"member"."text_search_data" @@ "text_search_query"(?)', search_string }
544 :add_where("activated NOTNULL AND active")
545 end
547 function Member.object:send_password_reset_mail()
548 trace.disable()
549 if not self.notify_email then
550 return false
551 end
552 self.password_reset_secret = multirand.string( 24, "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" )
553 local expiry = db:query("SELECT now() + '1 days'::interval as expiry", "object").expiry
554 self.password_reset_secret_expiry = expiry
555 self:save()
556 local content = slot.use_temporary(function()
557 slot.put(_"Hello " .. self.name .. ",\n\n")
558 slot.put(_"to reset your password please click on the following link:\n\n")
559 slot.put(request.get_absolute_baseurl() .. "index/reset_password.html?secret=" .. self.password_reset_secret .. "\n\n")
560 slot.put(_"If this link is not working, please open following url in your web browser:\n\n")
561 slot.put(request.get_absolute_baseurl() .. "index/reset_password.html\n\n")
562 slot.put(_"On that page please enter the reset code:\n\n")
563 slot.put(self.password_reset_secret .. "\n\n")
564 end)
565 local success = net.send_mail{
566 envelope_from = config.mail_envelope_from,
567 from = config.mail_from,
568 reply_to = config.mail_reply_to,
569 to = self.notify_email,
570 subject = config.mail_subject_prefix .. _"Password reset request",
571 content_type = "text/plain; charset=UTF-8",
572 content = content
573 }
574 return success
575 end
577 function Member.object:send_invitation(template_file, subject)
578 trace.disable()
579 self.invite_code = multirand.string( 24, "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" )
580 self:save()
582 local subject = subject
583 local content
585 if template_file then
586 local fh = io.open(template_file, "r")
587 content = fh:read("*a")
588 content = (content:gsub("#{invite_code}", self.invite_code))
589 else
590 subject = config.mail_subject_prefix .. _"Invitation to LiquidFeedback"
591 content = slot.use_temporary(function()
592 slot.put(_"Hello\n\n")
593 slot.put(_"You are invited to LiquidFeedback. To register please click the following link:\n\n")
594 slot.put(request.get_absolute_baseurl() .. "index/register.html?invite=" .. self.invite_code .. "\n\n")
595 slot.put(_"If this link is not working, please open following url in your web browser:\n\n")
596 slot.put(request.get_absolute_baseurl() .. "index/register.html\n\n")
597 slot.put(_"On that page please enter the invite key:\n\n")
598 slot.put(self.invite_code .. "\n\n")
599 end)
600 end
602 local success = net.send_mail{
603 envelope_from = config.mail_envelope_from,
604 from = config.mail_from,
605 reply_to = config.mail_reply_to,
606 to = self.notify_email_unconfirmed or self.notify_email,
607 subject = subject,
608 content_type = "text/plain; charset=UTF-8",
609 content = content
610 }
611 return success
612 end
614 function Member.object:set_notify_email(notify_email)
615 trace.disable()
616 local expiry = db:query("SELECT now() + '7 days'::interval as expiry", "object").expiry
617 self.notify_email_unconfirmed = notify_email
618 self.notify_email_secret = multirand.string( 24, "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" )
619 self.notify_email_secret_expiry = expiry
620 local content = slot.use_temporary(function()
621 slot.put(_"Hello " .. self.name .. ",\n\n")
622 slot.put(_"Please confirm your email address by clicking the following link:\n\n")
623 slot.put(request.get_absolute_baseurl() .. "index/confirm_notify_email.html?secret=" .. self.notify_email_secret .. "\n\n")
624 slot.put(_"If this link is not working, please open following url in your web browser:\n\n")
625 slot.put(request.get_absolute_baseurl() .. "index/confirm_notify_email.html\n\n")
626 slot.put(_"On that page please enter the confirmation code:\n\n")
627 slot.put(self.notify_email_secret .. "\n\n")
628 end)
629 local success = net.send_mail{
630 envelope_from = config.mail_envelope_from,
631 from = config.mail_from,
632 reply_to = config.mail_reply_to,
633 to = self.notify_email_unconfirmed,
634 subject = config.mail_subject_prefix .. _"Email confirmation request",
635 content_type = "text/plain; charset=UTF-8",
636 content = content
637 }
638 if success then
639 local lock_expiry = db:query("SELECT now() + '1 hour'::interval AS lock_expiry", "object").lock_expiry
640 self.notify_email_lock_expiry = lock_expiry
641 end
642 self:save()
643 return success
644 end
646 function Member.object:get_setting(key)
647 return Setting:by_pk(self.id, key)
648 end
650 function Member.object:get_setting_value(key)
651 local setting = Setting:by_pk(self.id, key)
652 if setting then
653 return setting.value
654 end
655 end
657 function Member.object:set_setting(key, value)
658 local setting = self:get_setting(key)
659 if not setting then
660 setting = Setting:new()
661 setting.member_id = self.id
662 setting.key = key
663 end
664 setting.value = value
665 setting:save()
666 end
668 function Member.object:get_setting_maps_by_key(key)
669 return SettingMap:new_selector()
670 :add_where{ "member_id = ?", self.id }
671 :add_where{ "key = ?", key }
672 :add_order_by("subkey")
673 :exec()
674 end
676 function Member.object:get_setting_map_by_key_and_subkey(key, subkey)
677 return SettingMap:new_selector()
678 :add_where{ "member_id = ?", self.id }
679 :add_where{ "key = ?", key }
680 :add_where{ "subkey = ?", subkey }
681 :add_order_by("subkey")
682 :optional_object_mode()
683 :exec()
684 end
686 function Member.object:set_setting_map(key, subkey, value)
687 setting_map = self:get_setting_map_by_key_and_subkey(key, subkey)
688 if not setting_map then
689 setting_map = SettingMap:new()
690 setting_map.member_id = self.id
691 setting_map.key = key
692 setting_map.subkey = subkey
693 end
694 setting_map.value = value
695 setting_map:save()
696 end
698 function Member.object_get:notify_email_locked()
699 return(
700 Member:new_selector()
701 :add_where{ "id = ?", app.session.member.id }
702 :add_where("notify_email_lock_expiry > now()")
703 :count() == 1
704 )
705 end
707 function Member.object_get:units_with_voting_right()
708 return(Unit:new_selector()
709 :join("privilege", nil, { "privilege.unit_id = unit.id AND privilege.member_id = ? AND privilege.voting_right", self.id })
710 :exec()
711 )
712 end
714 function Member.object:ui_field_text(args)
715 args = args or {}
716 if app.session:has_access("authors_pseudonymous") then
717 -- ugly workaround for getting html into a replaced string and to the user
718 ui.container{label = args.label, label_attr={class="ui_field_label"}, content = function()
719 slot.put(string.format('<span><a href="%s">%s</a></span>',
720 encode.url{
721 module = "member",
722 view = "show",
723 id = self.id,
724 },
725 encode.html(self.name)))
726 end
727 }
728 else
729 ui.field.text{ label = args.label, value = _"[not displayed public]" }
730 end
731 end
733 function Member.object:has_voting_right_for_unit_id(unit_id)
734 if not self.__units_with_voting_right_hash then
735 local privileges = Privilege:new_selector()
736 :add_where{ "member_id = ?", self.id }
737 :add_where("voting_right")
738 :exec()
739 self.__units_with_voting_right_hash = {}
740 for i, privilege in ipairs(privileges) do
741 self.__units_with_voting_right_hash[privilege.unit_id] = true
742 end
743 end
744 return self.__units_with_voting_right_hash[unit_id] and true or false
745 end
747 function Member.object:has_polling_right_for_unit_id(unit_id)
748 if not self.__units_with_polling_right_hash then
749 local privileges = Privilege:new_selector()
750 :add_where{ "member_id = ?", self.id }
751 :add_where("polling_right")
752 :exec()
753 self.__units_with_polling_right_hash = {}
754 for i, privilege in ipairs(privileges) do
755 self.__units_with_polling_right_hash[privilege.unit_id] = true
756 end
757 end
758 return self.__units_with_polling_right_hash[unit_id] and true or false
759 end
761 function Member.object:get_delegatee_member(unit_id, area_id, issue_id)
762 local selector = Member:new_selector()
763 if unit_id then
764 selector:join("delegation", nil, { "delegation.trustee_id = member.id AND delegation.scope = 'unit' AND delegation.unit_id = ? AND delegation.truster_id = ?", unit_id, self.id })
765 end
766 selector:optional_object_mode()
767 return selector:exec()
768 end
770 function Member.object:delete()
771 db:query{ "SELECT delete_member(?)", self.id }
772 end

Impressum / About Us