Beginning of email verification. Just need to clean up, add field validation, and allow users to set {primary, bugzilla, ...} emails to stuff from the person_email table.
This commit is contained in:
parent
ea0b7949b8
commit
3d687e8e54
7 changed files with 191 additions and 29 deletions
12
fas/TODO
12
fas/TODO
|
@ -1,10 +1,4 @@
|
||||||
Things to Fix in FAS2 before declaring it done
|
Things to Fix in FAS2 before declaring it done:
|
||||||
|
|
||||||
|
Nice-to-have things:
|
||||||
safasprovider.py
|
* Easy searching within groups (and sponsor/admin interface)
|
||||||
----------------
|
|
||||||
validate_password():
|
|
||||||
We'll want to change this to something that allows for longer passwords
|
|
||||||
(like md5). The one thing about that is we have to figure out how system
|
|
||||||
passwords use salt, etc. That way we'll be able to use this with
|
|
||||||
make_shell_accounts.
|
|
||||||
|
|
|
@ -54,7 +54,7 @@ tg.strict_parameters = True
|
||||||
|
|
||||||
server.webpath='/accounts'
|
server.webpath='/accounts'
|
||||||
base_url_filter.on=True
|
base_url_filter.on=True
|
||||||
base_url_filter.base_url = "https://publictest10.fedoraproject.org/accounts"
|
base_url_filter.base_url = "https://publictest10.fedoraproject.org"
|
||||||
|
|
||||||
# Make the session cookie only return to the host over an SSL link
|
# Make the session cookie only return to the host over an SSL link
|
||||||
# Disabled for testing.
|
# Disabled for testing.
|
||||||
|
|
|
@ -423,15 +423,15 @@ mapper(People, PeopleTable, properties = {
|
||||||
'email_purposes': relation(EmailPurposes, backref = 'person',
|
'email_purposes': relation(EmailPurposes, backref = 'person',
|
||||||
collection_class = column_mapped_collection(
|
collection_class = column_mapped_collection(
|
||||||
EmailPurposesTable.c.purpose)),
|
EmailPurposesTable.c.purpose)),
|
||||||
|
'person_emails': relation(PersonEmails, backref = 'person',
|
||||||
|
collection_class = column_mapped_collection(
|
||||||
|
PersonEmailsTable.c.email)),
|
||||||
'approved_roles': relation(ApprovedRoles, backref='member',
|
'approved_roles': relation(ApprovedRoles, backref='member',
|
||||||
primaryjoin = PeopleTable.c.id==ApprovedRoles.c.person_id),
|
primaryjoin = PeopleTable.c.id==ApprovedRoles.c.person_id),
|
||||||
'unapproved_roles': relation(UnApprovedRoles, backref='member',
|
'unapproved_roles': relation(UnApprovedRoles, backref='member',
|
||||||
primaryjoin = PeopleTable.c.id==UnApprovedRoles.c.person_id)
|
primaryjoin = PeopleTable.c.id==UnApprovedRoles.c.person_id)
|
||||||
})
|
})
|
||||||
mapper(PersonEmails, PersonEmailsTable, properties = {
|
mapper(PersonEmails, PersonEmailsTable)
|
||||||
'person': relation(People, backref = 'person_emails', uselist = False,
|
|
||||||
primaryjoin = PeopleTable.c.id==PersonEmailsTable.c.person_id)
|
|
||||||
})
|
|
||||||
mapper(EmailPurposes, EmailPurposesTable, properties = {
|
mapper(EmailPurposes, EmailPurposesTable, properties = {
|
||||||
'person_email': relation(PersonEmails, uselist = False,
|
'person_email': relation(PersonEmails, uselist = False,
|
||||||
primaryjoin = PersonEmailsTable.c.id==EmailPurposesTable.c.email_id)
|
primaryjoin = PersonEmailsTable.c.id==EmailPurposesTable.c.email_id)
|
||||||
|
|
30
fas/fas/templates/user/email/add.html
Normal file
30
fas/fas/templates/user/email/add.html
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||||
|
<xi:include href="../../master.html" />
|
||||||
|
<head>
|
||||||
|
<title>${_('Add Email')}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>${_('Add Email')}</h2>
|
||||||
|
<form action="${tg.url('/user/email/save/%s' % target.username)}" method="post" enctype="multipart/form-data">
|
||||||
|
<div class="field">
|
||||||
|
<label for="email">${_('Email')}:</label>
|
||||||
|
<input type="text" id="email" name="email" />
|
||||||
|
<!-- TODO: More generic documentation for adding an email -->
|
||||||
|
<script type="text/javascript">var hb1 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/user_primary_email')}'});</script>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="description">${_('Description')}:</label>
|
||||||
|
<input type="text" id="description" name="description" />
|
||||||
|
<!-- TODO: Correct documentation for this -->
|
||||||
|
<script type="text/javascript">var hb1 = new HelpBalloon({dataURL: '${tg.url('/help/get_help/user_primary_email')}'});</script>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<input type="submit" value="${_('Save!')}" />
|
||||||
|
<a href="${tg.url('/user/email/manage')}">${_('Cancel')}</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -7,7 +7,7 @@
|
||||||
<title>${_('Manage Emails')}</title>
|
<title>${_('Manage Emails')}</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h2>${_('Managing Emails for %s') % person.username}</h2>
|
<h2>${_('Managing Emails for %s') % target.username}</h2>
|
||||||
<h3>Available Emails</h3>
|
<h3>Available Emails</h3>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr py:for="email in person.person_emails">
|
<tr py:for="email in target.person_emails.values()">
|
||||||
<td><a href="mailto:${email.email}">${email.email}</a></td>
|
<td><a href="mailto:${email.email}">${email.email}</a></td>
|
||||||
<td>${email.description}</td>
|
<td>${email.description}</td>
|
||||||
<td py:if="email.verified"><span class="approved">${_('Verified')}</span></td>
|
<td py:if="email.verified"><span class="approved">${_('Verified')}</span></td>
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr py:for="purpose in person.email_purposes.values()">
|
<tr py:for="purpose in target.email_purposes.values()">
|
||||||
<td><a href="mailto:${purpose.email}">${purpose.email}</a></td>
|
<td><a href="mailto:${purpose.email}">${purpose.email}</a></td>
|
||||||
<td>${purpose.person_email.description}</td>
|
<td>${purpose.person_email.description}</td>
|
||||||
<td>${purpose.purpose}</td>
|
<td>${purpose.purpose}</td>
|
||||||
|
|
|
@ -3,6 +3,8 @@ from turbogears import controllers, expose, paginate, identity, redirect, widget
|
||||||
from turbogears.database import session
|
from turbogears.database import session
|
||||||
import cherrypy
|
import cherrypy
|
||||||
|
|
||||||
|
import turbomail
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import gpgme
|
import gpgme
|
||||||
|
@ -221,7 +223,8 @@ class User(controllers.Controller):
|
||||||
target = person
|
target = person
|
||||||
if not canEditUser(person, target):
|
if not canEditUser(person, target):
|
||||||
turbogears.flash(_('You cannot edit %s') % target.username )
|
turbogears.flash(_('You cannot edit %s') % target.username )
|
||||||
username = turbogears.identity.current.username
|
turbogears.redirect('/user/view/%s', target.username)
|
||||||
|
return dict()
|
||||||
return dict(target=target)
|
return dict(target=target)
|
||||||
|
|
||||||
@identity.require(turbogears.identity.not_anonymous())
|
@identity.require(turbogears.identity.not_anonymous())
|
||||||
|
@ -236,7 +239,7 @@ class User(controllers.Controller):
|
||||||
|
|
||||||
if not canEditUser(person, target):
|
if not canEditUser(person, target):
|
||||||
turbogears.flash(_("You do not have permission to edit '%s'") % target.username)
|
turbogears.flash(_("You do not have permission to edit '%s'") % target.username)
|
||||||
turbogears.redirect('/user/edit/%s', target.username)
|
turbogears.redirect('/user/view/%s', target.username)
|
||||||
return dict()
|
return dict()
|
||||||
try:
|
try:
|
||||||
target.human_name = human_name
|
target.human_name = human_name
|
||||||
|
@ -286,7 +289,6 @@ class User(controllers.Controller):
|
||||||
# Also, perhaps implement a timeout- delete account
|
# Also, perhaps implement a timeout- delete account
|
||||||
# if the e-mail is not verified (i.e. the person changes
|
# if the e-mail is not verified (i.e. the person changes
|
||||||
# their password) withing X days.
|
# their password) withing X days.
|
||||||
import turbomail
|
|
||||||
try:
|
try:
|
||||||
person = People()
|
person = People()
|
||||||
person.username = username
|
person.username = username
|
||||||
|
@ -296,6 +298,7 @@ class User(controllers.Controller):
|
||||||
person.status = 'active'
|
person.status = 'active'
|
||||||
session.flush()
|
session.flush()
|
||||||
|
|
||||||
|
# TODO: Handle properly if email has already been used. This might be painful, since the person already exists, at this point.
|
||||||
person_email = PersonEmails()
|
person_email = PersonEmails()
|
||||||
person_email.email = email
|
person_email.email = email
|
||||||
person_email.person = person
|
person_email.person = person
|
||||||
|
|
|
@ -3,11 +3,16 @@ from turbogears import controllers, expose, paginate, identity, redirect, widget
|
||||||
from turbogears.database import session
|
from turbogears.database import session
|
||||||
import cherrypy
|
import cherrypy
|
||||||
|
|
||||||
|
import turbomail
|
||||||
|
import random
|
||||||
|
|
||||||
from fas.model import People
|
from fas.model import People
|
||||||
from fas.model import PersonEmails
|
from fas.model import PersonEmails
|
||||||
from fas.model import EmailPurposes
|
from fas.model import EmailPurposes
|
||||||
from fas.model import Log
|
from fas.model import Log
|
||||||
|
|
||||||
|
from fas.auth import *
|
||||||
|
|
||||||
class NonFedoraEmail(validators.FancyValidator):
|
class NonFedoraEmail(validators.FancyValidator):
|
||||||
'''Make sure that an email address is not @fedoraproject.org'''
|
'''Make sure that an email address is not @fedoraproject.org'''
|
||||||
def _to_python(self, value, state):
|
def _to_python(self, value, state):
|
||||||
|
@ -16,13 +21,20 @@ class NonFedoraEmail(validators.FancyValidator):
|
||||||
if value.endswith('@fedoraproject.org'):
|
if value.endswith('@fedoraproject.org'):
|
||||||
raise validators.Invalid(_("To prevent email loops, your email address cannot be @fedoraproject.org."), value, state)
|
raise validators.Invalid(_("To prevent email loops, your email address cannot be @fedoraproject.org."), value, state)
|
||||||
|
|
||||||
class EmailCreate(validators.Schema):
|
class EmailSave(validators.Schema):
|
||||||
email = validators.All(
|
email = validators.All(
|
||||||
validators.Email(not_empty=True, strip=True),
|
validators.Email(not_empty=True, strip=True),
|
||||||
NonFedoraEmail(not_empty=True, strip=True),
|
NonFedoraEmail(not_empty=True, strip=True),
|
||||||
)
|
)
|
||||||
#fedoraPersonBugzillaMail = validators.Email(strip=True)
|
description = validators.String(not_empty=True, max=512)
|
||||||
postal_address = validators.String(max=512)
|
|
||||||
|
def generate_validtoken(length=32):
|
||||||
|
''' Generate Validation Token '''
|
||||||
|
chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||||
|
token = ''
|
||||||
|
for i in xrange(length):
|
||||||
|
token += random.choice(chars)
|
||||||
|
return token
|
||||||
|
|
||||||
class Email(controllers.Controller):
|
class Email(controllers.Controller):
|
||||||
|
|
||||||
|
@ -34,7 +46,7 @@ class Email(controllers.Controller):
|
||||||
def index(self):
|
def index(self):
|
||||||
'''Redirect to manage
|
'''Redirect to manage
|
||||||
'''
|
'''
|
||||||
turbogears.redirect('/user/email/manage/%s' % turbogears.identity.current.user_name)
|
turbogears.redirect('/user/email/manage')
|
||||||
|
|
||||||
|
|
||||||
@expose(template="fas.templates.error")
|
@expose(template="fas.templates.error")
|
||||||
|
@ -48,12 +60,135 @@ class Email(controllers.Controller):
|
||||||
#@validate(validators=UserView())
|
#@validate(validators=UserView())
|
||||||
@error_handler(error)
|
@error_handler(error)
|
||||||
@expose(template="fas.templates.user.email.manage", allow_json=True)
|
@expose(template="fas.templates.user.email.manage", allow_json=True)
|
||||||
def manage(self, username=None):
|
def manage(self, targetname=None):
|
||||||
'''
|
'''
|
||||||
Manage a person's emails.
|
Manage a person's emails
|
||||||
'''
|
'''
|
||||||
if not username:
|
# TODO: Some sort of auth checking - other people should
|
||||||
username = turbogears.identity.current.user_name
|
# probably be limited to looking at a person's email through
|
||||||
|
# /user/view, although admins should probably be able to set
|
||||||
|
# emails (with/without verification?)
|
||||||
|
username = turbogears.identity.current.user_name
|
||||||
person = People.by_username(username)
|
person = People.by_username(username)
|
||||||
return dict(person=person)
|
|
||||||
|
if targetname:
|
||||||
|
target = People.by_username(targetname)
|
||||||
|
else:
|
||||||
|
target = person
|
||||||
|
|
||||||
|
return dict(target=target)
|
||||||
|
|
||||||
|
@identity.require(turbogears.identity.not_anonymous())
|
||||||
|
#@validate(validators=UserView())
|
||||||
|
@error_handler(error)
|
||||||
|
@expose(template="fas.templates.user.email.add", allow_json=True)
|
||||||
|
def add(self, targetname=None):
|
||||||
|
'''
|
||||||
|
Display the form to add an email
|
||||||
|
'''
|
||||||
|
username = turbogears.identity.current.user_name
|
||||||
|
person = People.by_username(username)
|
||||||
|
|
||||||
|
if targetname:
|
||||||
|
target = People.by_username(targetname)
|
||||||
|
else:
|
||||||
|
target = person
|
||||||
|
|
||||||
|
if not canEditUser(person, target):
|
||||||
|
turbogears.flash(_('You cannot edit %s') % target.username )
|
||||||
|
turbogears.redirect('/user/email/manage')
|
||||||
|
return dict()
|
||||||
|
|
||||||
|
return dict(target=target)
|
||||||
|
|
||||||
|
@identity.require(turbogears.identity.not_anonymous())
|
||||||
|
@validate(validators=EmailSave())
|
||||||
|
@error_handler(error)
|
||||||
|
@expose(template="fas.templates.user.email.add", allow_json=True)
|
||||||
|
def save(self, targetname, email, description):
|
||||||
|
'''
|
||||||
|
Display the form to add an email
|
||||||
|
'''
|
||||||
|
username = turbogears.identity.current.user_name
|
||||||
|
person = People.by_username(username)
|
||||||
|
|
||||||
|
if targetname:
|
||||||
|
target = People.by_username(targetname)
|
||||||
|
else:
|
||||||
|
target = person
|
||||||
|
|
||||||
|
if not canEditUser(person, target):
|
||||||
|
turbogears.flash(_('You cannot edit %s') % target.username )
|
||||||
|
turbogears.redirect('/user/email/manage')
|
||||||
|
return dict()
|
||||||
|
|
||||||
|
validtoken = generate_validtoken()
|
||||||
|
|
||||||
|
try:
|
||||||
|
person_email = PersonEmails()
|
||||||
|
person_email.email = email
|
||||||
|
person_email.person = target
|
||||||
|
person_email.description = description
|
||||||
|
person_email.validtoken = validtoken
|
||||||
|
session.flush()
|
||||||
|
# Hmm, should this be checked in the validator or here?
|
||||||
|
except IntegrityError:
|
||||||
|
turbogears.flash(_('The email \'%s\' is already in used.') % email)
|
||||||
|
return dict(target=target)
|
||||||
|
else:
|
||||||
|
# TODO: Make this email more friendly. Maybe escape the @ in email too?
|
||||||
|
validurl = config.get('base_url_filter.base_url') + turbogears.url('/user/email/verify/%s/%s/%s') % (target.username, email, validtoken)
|
||||||
|
message = turbomail.Message(config.get('accounts_mail'), email, _('Confirm this email address'))
|
||||||
|
message.plain = _('''
|
||||||
|
Go to this URL to verify that you own this email address: %s
|
||||||
|
''') % validurl
|
||||||
|
turbomail.enqueue(message)
|
||||||
|
turbogears.flash(_('Your email has been added. Before you can use this email, you must verify it. The email you added should receive a message with instructions shortly.'))
|
||||||
|
|
||||||
|
return dict(target=target)
|
||||||
|
|
||||||
|
return dict(target=target)
|
||||||
|
|
||||||
|
@identity.require(turbogears.identity.not_anonymous())
|
||||||
|
# TODO: Validation!
|
||||||
|
#@validate(validators=UserView())
|
||||||
|
@error_handler(error)
|
||||||
|
@expose(allow_json=True)
|
||||||
|
def verify(self, targetname, email, validtoken):
|
||||||
|
'''
|
||||||
|
Verify an email
|
||||||
|
'''
|
||||||
|
username = turbogears.identity.current.user_name
|
||||||
|
person = People.by_username(username)
|
||||||
|
|
||||||
|
if targetname:
|
||||||
|
target = People.by_username(targetname)
|
||||||
|
else:
|
||||||
|
target = person
|
||||||
|
|
||||||
|
if not canEditUser(person, target):
|
||||||
|
turbogears.flash(_('You cannot edit %s') % target.username )
|
||||||
|
turbogears.redirect('/user/email/manage')
|
||||||
|
return dict()
|
||||||
|
|
||||||
|
if target.person_emails[email].verified:
|
||||||
|
turbogears.flash(_('The email provided has already been verified.'))
|
||||||
|
turbogears.redirect('/user/email/manage')
|
||||||
|
return dict()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if target.person_emails[email].validtoken == validtoken:
|
||||||
|
target.person_emails[email].validtoken = ''
|
||||||
|
target.person_emails[email].verified = True
|
||||||
|
turbogears.flash(_('Your email has been successfully verified.'))
|
||||||
|
turbogears.redirect('/user/email/manage')
|
||||||
|
return dict()
|
||||||
|
else:
|
||||||
|
turbogears.flash(_('The verification string did not match.'))
|
||||||
|
turbogears.redirect('/user/email/manage')
|
||||||
|
return dict()
|
||||||
|
except KeyError:
|
||||||
|
turbogears.flash(_('No such email is associated with your user.'))
|
||||||
|
turbogears.redirect('/user/email/manage')
|
||||||
|
return dict()
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue