liquid_feedback_frontend

view model/member.lua @ 1232:77dc363fa0be

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

Impressum / About Us