diff options
author | Zachary Sloan | 2013-10-11 14:46:52 -0500 |
---|---|---|
committer | Zachary Sloan | 2013-10-11 14:46:52 -0500 |
commit | 34c6c908ac072609a2f923946d474504d3fa0331 (patch) | |
tree | 8f7635a312115419bf8ce12602fe9c46e048a92c | |
parent | 51be798ef6311a86ea1ca8c5092277d727b98a5a (diff) | |
parent | 38ae30a5fb46753a361e1c7454871430d7097c3b (diff) | |
download | genenetwork2-34c6c908ac072609a2f923946d474504d3fa0331.tar.gz |
Merge /home/sam/gene
32 files changed, 1314 insertions, 62 deletions
diff --git a/wqflask/flask_security/__init__.py b/wqflask/flask_security/__init__.py index 297033c9..81e6c89e 100644 --- a/wqflask/flask_security/__init__.py +++ b/wqflask/flask_security/__init__.py @@ -12,8 +12,6 @@ __version__ = '1.6.0' -print "using internal flask security" - from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore from .decorators import auth_token_required, http_auth_required, \ @@ -23,3 +21,5 @@ from .forms import ForgotPasswordForm, LoginForm, RegisterForm, \ from .signals import confirm_instructions_sent, password_reset, \ reset_password_instructions_sent, user_confirmed, user_registered from .utils import login_user, logout_user, url_for_security + +print "Using our own flask.ext.security"
\ No newline at end of file diff --git a/wqflask/flask_security/core.py b/wqflask/flask_security/core.py index d794fad5..0f3a231f 100644 --- a/wqflask/flask_security/core.py +++ b/wqflask/flask_security/core.py @@ -207,6 +207,7 @@ def _get_serializer(app, name): def _get_state(app, datastore, **kwargs): for key, value in get_config(app).items(): + print "in _get_state [{}]: {}".format(key, value) kwargs[key.lower()] = value kwargs.update(dict( diff --git a/wqflask/flask_security/datastore.py b/wqflask/flask_security/datastore.py index f8c7218d..634399d9 100644 --- a/wqflask/flask_security/datastore.py +++ b/wqflask/flask_security/datastore.py @@ -157,6 +157,7 @@ class UserDatastore(object): """Creates and returns a new user from the given parameters.""" user = self.user_model(**self._prepare_create_user_args(**kwargs)) + print "in abstraced create_user, user is:", user return self.put(user) def delete_user(self, user): diff --git a/wqflask/flask_security/forms.py b/wqflask/flask_security/forms.py index e64e1502..54677e77 100644 --- a/wqflask/flask_security/forms.py +++ b/wqflask/flask_security/forms.py @@ -89,6 +89,12 @@ def valid_user_email(form, field): class Form(BaseForm): def __init__(self, *args, **kwargs): + #print "importing tracer" + #from wqflask import tracer + #tracer.turn_on() + #print "imported tracer" + print "in Form, args:", args + print "in Form, kwargs:", kwargs if current_app.testing: self.TIME_LIMIT = None super(Form, self).__init__(*args, **kwargs) @@ -148,10 +154,13 @@ class RegisterFormMixin(): def to_dict(form): def is_field_and_user_attr(member): + print "in ifaua:", member return isinstance(member, Field) and \ hasattr(_datastore.user_model, member.name) + print("green:", vars(form)) fields = inspect.getmembers(form, is_field_and_user_attr) + print("fields:" ,vars(form)) return dict((key, value.data) for key, value in fields) diff --git a/wqflask/flask_security/registerable.py b/wqflask/flask_security/registerable.py index 4e9f357d..4606c7c6 100644 --- a/wqflask/flask_security/registerable.py +++ b/wqflask/flask_security/registerable.py @@ -24,6 +24,7 @@ _datastore = LocalProxy(lambda: _security.datastore) def register_user(**kwargs): + print "in register_user kwargs:", kwargs confirmation_link, token = None, None kwargs['password'] = encrypt_password(kwargs['password']) user = _datastore.create_user(**kwargs) diff --git a/wqflask/other_config/nginx_conf/sam.conf b/wqflask/other_config/nginx_conf/sam.conf new file mode 100644 index 00000000..3cc443a8 --- /dev/null +++ b/wqflask/other_config/nginx_conf/sam.conf @@ -0,0 +1,42 @@ +server { + # Modeled after http://flask.pocoo.org/docs/deploying/wsgi-standalone/ + listen 80; + + server_name sam.genenetwork.org; + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + location ^~ /css/ { + root /gene/wqflask/wqflask/static/; + } + + location ^~ /javascript/ { + root /gene/wqflask/wqflask/static/; + } + +# location ^~ /image/ { +# root /gene/wqflask/wqflask/static/; +# } + + location ^~ /images/ { + root /gene/wqflask/wqflask/static/; + } + + ### New - added by Sam + #location ^~ /static/ { + # root /gene/wqflask/wqflask/static/; + #} + + location / { + proxy_pass http://127.0.0.1:5555/; + proxy_redirect off; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_read_timeout 40m; + } +} + diff --git a/wqflask/secure_server.py b/wqflask/secure_server.py index 2abfdb05..a77abf7e 100644 --- a/wqflask/secure_server.py +++ b/wqflask/secure_server.py @@ -1,4 +1,4 @@ -from __future__ import print_function, division, absolute_import +from __future__ import absolute_import, division, print_function from wqflask import app @@ -25,6 +25,12 @@ app.logger.addHandler(file_handler) import logging_tree logging_tree.printout() +#import sys +#print("At startup, path is:", sys.path) + +from werkzeug.contrib.fixers import ProxyFix +app.wsgi_app = ProxyFix(app.wsgi_app) + #print("app.config is:", app.config) if __name__ == '__main__': diff --git a/wqflask/utility/__init__.py b/wqflask/utility/__init__.py index d0e4a3fa..d9856eed 100755 --- a/wqflask/utility/__init__.py +++ b/wqflask/utility/__init__.py @@ -1,5 +1,6 @@ from pprint import pformat as pf +# Todo: Move these out of __init__ class Bunch(object): """Like a dictionary but using object notation""" @@ -10,3 +11,25 @@ class Bunch(object): return pf(self.__dict__) +class Struct(object): + '''The recursive class for building and representing objects with. + + From http://stackoverflow.com/a/6573827/1175849 + + ''' + + def __init__(self, obj): + for k, v in obj.iteritems(): + if isinstance(v, dict): + setattr(self, k, Struct(v)) + else: + setattr(self, k, v) + + def __getitem__(self, val): + return self.__dict__[val] + + def __repr__(self): + return '{%s}' % str(', '.join('%s : %s' % (k, repr(v)) for + (k, v) in self.__dict__.iteritems())) + + diff --git a/wqflask/wqflask/database.py b/wqflask/wqflask/database.py new file mode 100644 index 00000000..e55f06a7 --- /dev/null +++ b/wqflask/wqflask/database.py @@ -0,0 +1,26 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import scoped_session, sessionmaker +from sqlalchemy.ext.declarative import declarative_base + +from wqflask import app + +#engine = create_engine('sqlite:////tmp/test.db', convert_unicode=True) +engine = create_engine(app.config['DB_URI'], convert_unicode=True) + +db_session = scoped_session(sessionmaker(autocommit=False, + autoflush=False, + bind=engine)) +Base = declarative_base() +Base.query = db_session.query_property() + +def init_db(): + # import all modules here that might define models so that + # they will be registered properly on the metadata. Otherwise + # you will have to import them first before calling init_db() + #import yourapplication.models + import wqflask.model + print("Creating all..") + Base.metadata.create_all(bind=engine) + print("Done creating all...") + +init_db()
\ No newline at end of file diff --git a/wqflask/wqflask/model.py b/wqflask/wqflask/model.py index 5beba9ff..5c514bde 100644 --- a/wqflask/wqflask/model.py +++ b/wqflask/wqflask/model.py @@ -1,12 +1,25 @@ from __future__ import print_function, division, absolute_import +import uuid +import datetime + +from flask import request from flask.ext.sqlalchemy import SQLAlchemy -from flask.ext.security import Security, SQLAlchemyUserDatastore, UserMixin, RoleMixin +#from flask.ext.security import Security, SQLAlchemyUserDatastore, UserMixin, RoleMixin + +#from flask_security.forms import TextField +#from flask_security.forms import RegisterForm from wqflask import app +from sqlalchemy import Column, Integer, String, Table, ForeignKey, Unicode, Boolean, DateTime, Text +from sqlalchemy.orm import relationship, backref + +from wqflask.database import Base, init_db + # Create database connection object -db = SQLAlchemy(app) +#db = SQLAlchemy(app) + # Is this right? -Sam #from sqlalchemy.ext.declarative import declarative_base @@ -23,45 +36,76 @@ db = SQLAlchemy(app) # """ # print("in get cls is:", cls) # print(" key is {} : {}".format(type(key), key)) -# query = db.Model.query(cls) +# query = Model.query(cls) # print("query is: ", query) # record = query.get(key) # return record # # -#print("db.Model is:", vars(db.Model)) -#db.Model.get = get +#print("Model is:", vars(Model)) +#Model.get = get # Define models -roles_users = db.Table('roles_users', - db.Column('user_id', db.Integer(), db.ForeignKey('user.id')), - db.Column('role_id', db.Integer(), db.ForeignKey('role.id'))) - -class Role(db.Model, RoleMixin): - id = db.Column(db.Integer(), primary_key=True) - name = db.Column(db.String(80), unique=True) - description = db.Column(db.String(255)) - -class User(db.Model, UserMixin): - id = db.Column(db.Integer(), primary_key=True) - email = db.Column(db.String(255), unique=True) - password = db.Column(db.String(255)) - active = db.Column(db.Boolean()) - confirmed_at = db.Column(db.DateTime()) - - last_login_at = db.Column(db.DateTime()) - current_login_at = db.Column(db.DateTime()) - last_login_ip = db.Column(db.String(39)) - current_login_ip = db.Column(db.String(39)) - login_count = db.Column(db.Integer()) - - roles = db.relationship('Role', secondary=roles_users, - backref=db.backref('users', lazy='dynamic')) +#roles_users = Table('roles_users', +# Column('user_id', Integer(), ForeignKey('user.the_id')), +# Column('role_id', Integer(), ForeignKey('role.the_id'))) + +class Role(Base): + __tablename__ = "role" + id = Column(Unicode(36), primary_key=True, default=lambda: unicode(uuid.uuid4())) + name = Column(Unicode(80), unique=True, nullable=False) + description = Column(Unicode(255)) + +class User(Base): + __tablename__ = "user" + id = Column(Unicode(36), primary_key=True, default=lambda: unicode(uuid.uuid4())) + email_address = Column(Unicode(50), unique=True, nullable=False) + + # Todo: Turn on strict mode for Mysql + password = Column(Text, nullable=False) + + full_name = Column(Unicode(50)) + organization = Column(Unicode(50)) + + active = Column(Boolean(), nullable=False, default=True) + registration_info = Column(Text) # json detailing when they were registered, etc. + + confirmed = Column(Text) # json detailing when they confirmed, etc. + + #last_login_at = Column(DateTime()) + #current_login_at = Column(DateTime()) + #last_login_ip = Column(Unicode(39)) + #current_login_ip = Column(Unicode(39)) + #login_count = Column(Integer()) + + #roles = relationship('Role', secondary=roles_users, + # backref=backref('users', lazy='dynamic')) + +class Login(Base): + __tablename__ = "login" + id = Column(Unicode(36), primary_key=True, default=lambda: unicode(uuid.uuid4())) + user = Column(Unicode(36), ForeignKey('user.id')) + timestamp = Column(DateTime(), default=lambda: datetime.datetime.utcnow()) + ip_address = Column(Unicode(39)) + successful = Column(Boolean(), nullable=False) # False if wrong password was entered + session_id = Column(Text) # Set only if successfully logged in, otherwise should be blank + + def __init__(self, user): + self.user = user.id + self.ip_address = request.remote_addr + # Setup Flask-Security -user_datastore = SQLAlchemyUserDatastore(db, User, Role) -security = Security(app, user_datastore) +#user_datastore = SQLAlchemyUserDatastore(db, User, Role) + +#class ExtendedRegisterForm(RegisterForm): +# name = TextField('name') +# #print("name is:", name['_name'], vars(name)) +# organization = TextField('organization') +# +#security = Security(app, user_datastore, register_form=ExtendedRegisterForm) + + +#user_datastore.create_role(name="Genentech", description="Genentech Beta Project(testing)") -db.metadata.create_all(db.engine) -user_datastore.create_role(name="Genentech", description="Genentech Beta Project(testing)") diff --git a/wqflask/wqflask/pbkdf2.py b/wqflask/wqflask/pbkdf2.py new file mode 100644 index 00000000..f7f61a09 --- /dev/null +++ b/wqflask/wqflask/pbkdf2.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +""" + pbkdf2 + ~~~~~~ + + This module implements pbkdf2 for Python. It also has some basic + tests that ensure that it works. The implementation is straightforward + and uses stdlib only stuff and can be easily be copy/pasted into + your favourite application. + + Use this as replacement for bcrypt that does not need a c implementation + of a modified blowfish crypto algo. + + Example usage: + + >>> pbkdf2_hex('what i want to hash', 'the random salt') + 'fa7cc8a2b0a932f8e6ea42f9787e9d36e592e0c222ada6a9' + + How to use this: + + 1. Use a constant time string compare function to compare the stored hash + with the one you're generating:: + + def safe_str_cmp(a, b): + if len(a) != len(b): + return False + rv = 0 + for x, y in izip(a, b): + rv |= ord(x) ^ ord(y) + return rv == 0 + + 2. Use `os.urandom` to generate a proper salt of at least 8 byte. + Use a unique salt per hashed password. + + 3. Store ``algorithm$salt:costfactor$hash`` in the database so that + you can upgrade later easily to a different algorithm if you need + one. For instance ``PBKDF2-256$thesalt:10000$deadbeef...``. + + + :copyright: (c) Copyright 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import hmac +import hashlib +from struct import Struct +from operator import xor +from itertools import izip, starmap + + +_pack_int = Struct('>I').pack + + +def pbkdf2_hex(data, salt, iterations=1000, keylen=24, hashfunc=None): + """Like :func:`pbkdf2_bin` but returns a hex encoded string.""" + return pbkdf2_bin(data, salt, iterations, keylen, hashfunc).encode('hex') + + +def pbkdf2_bin(data, salt, iterations=1000, keylen=24, hashfunc=None): + """Returns a binary digest for the PBKDF2 hash algorithm of `data` + with the given `salt`. It iterates `iterations` time and produces a + key of `keylen` bytes. By default SHA-1 is used as hash function, + a different hashlib `hashfunc` can be provided. + """ + hashfunc = hashfunc or hashlib.sha1 + mac = hmac.new(data, None, hashfunc) + def _pseudorandom(x, mac=mac): + h = mac.copy() + h.update(x) + return map(ord, h.digest()) + buf = [] + for block in xrange(1, -(-keylen // mac.digest_size) + 1): + rv = u = _pseudorandom(salt + _pack_int(block)) + for i in xrange(iterations - 1): + u = _pseudorandom(''.join(map(chr, u))) + rv = list(starmap(xor, izip(rv, u))) + buf.extend(rv) + return ''.join(map(chr, buf))[:keylen] + + +def safe_str_cmp(a, b): + if len(a) != len(b): + return False + rv = 0 + for x, y in izip(a, b): + rv |= ord(x) ^ ord(y) + return rv == 0 + + + +def test(): + failed = [] + def check(data, salt, iterations, keylen, expected): + rv = pbkdf2_hex(data, salt, iterations, keylen) + if rv != expected: + print 'Test failed:' + print ' Expected: %s' % expected + print ' Got: %s' % rv + print ' Parameters:' + print ' data=%s' % data + print ' salt=%s' % salt + print ' iterations=%d' % iterations + print + failed.append(1) + + # From RFC 6070 + check('password', 'salt', 1, 20, + '0c60c80f961f0e71f3a9b524af6012062fe037a6') + check('password', 'salt', 2, 20, + 'ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957') + check('password', 'salt', 4096, 20, + '4b007901b765489abead49d926f721d065a429c1') + check('passwordPASSWORDpassword', 'saltSALTsaltSALTsaltSALTsaltSALTsalt', + 4096, 25, '3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038') + check('pass\x00word', 'sa\x00lt', 4096, 16, + '56fa6aa75548099dcc37d7f03425e0c3') + # This one is from the RFC but it just takes for ages + ##check('password', 'salt', 16777216, 20, + ## 'eefe3d61cd4da4e4e9945b3d6ba2158c2634e984') + + # From Crypt-PBKDF2 + check('password', 'ATHENA.MIT.EDUraeburn', 1, 16, + 'cdedb5281bb2f801565a1122b2563515') + check('password', 'ATHENA.MIT.EDUraeburn', 1, 32, + 'cdedb5281bb2f801565a1122b25635150ad1f7a04bb9f3a333ecc0e2e1f70837') + check('password', 'ATHENA.MIT.EDUraeburn', 2, 16, + '01dbee7f4a9e243e988b62c73cda935d') + check('password', 'ATHENA.MIT.EDUraeburn', 2, 32, + '01dbee7f4a9e243e988b62c73cda935da05378b93244ec8f48a99e61ad799d86') + check('password', 'ATHENA.MIT.EDUraeburn', 1200, 32, + '5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a31e2e62b1e13') + check('X' * 64, 'pass phrase equals block size', 1200, 32, + '139c30c0966bc32ba55fdbf212530ac9c5ec59f1a452f5cc9ad940fea0598ed1') + check('X' * 65, 'pass phrase exceeds block size', 1200, 32, + '9ccad6d468770cd51b10e6a68721be611a8b4d282601db3b36be9246915ec82a') + + raise SystemExit(bool(failed)) + + +if __name__ == '__main__': + test() diff --git a/wqflask/wqflask/send_mail.py b/wqflask/wqflask/send_mail.py new file mode 100644 index 00000000..be51ad0d --- /dev/null +++ b/wqflask/wqflask/send_mail.py @@ -0,0 +1,45 @@ +from __future__ import absolute_import, division, print_function + +import datetime + +import simplejson as json + +from redis import StrictRedis +Redis = StrictRedis() + +import mailer + +def timestamp(): + ts = datetime.datetime.utcnow() + return ts.isoformat() + +def main(): + while True: + print("Waiting for message to show up in queue...") + msg = Redis.blpop("mail_queue") + + # Queue name is the first element, we want the second, which is the actual message + msg = msg[1] + + print("\nGot a msg in queue at {}: {}".format(timestamp(), msg)) + # Todo: Truncate mail_processed when it gets to long + Redis.rpush("mail_processed", msg) + process_message(msg) + + +def process_message(msg): + msg = json.loads(msg) + + message = mailer.Message() + message.From = msg['From'] + message.To = msg['To'] + message.Subject = msg['Subject'] + message.Body = msg['Body'] + + sender = mailer.Mailer('localhost') + sender.send(message) + print("Sent message at {}: {}\n".format(timestamp(), msg)) + + +if __name__ == '__main__': + main()
\ No newline at end of file diff --git a/wqflask/wqflask/static/new/css/parsley.css b/wqflask/wqflask/static/new/css/parsley.css new file mode 100644 index 00000000..7d244579 --- /dev/null +++ b/wqflask/wqflask/static/new/css/parsley.css @@ -0,0 +1,20 @@ +/* Adapted from parsleyjs.org/documentation.html#parsleyclasses */ + +input.parsley-success, textarea.parsley-success { + color: #468847 !important; + background-color: #DFF0D8 !important; + border: 1px solid #D6E9C6 !important; +} +input.parsley-error, textarea.parsley-error { + color: #B94A48 !important; + background-color: #F2DEDE !important; + border: 1px solid #EED3D7 !important; +} +ul.parsley-error-list { + font-size: 11px; + margin: 2px; + list-style-type:none; +} +ul.parsley-error-list li { + line-height: 11px; +}
\ No newline at end of file diff --git a/wqflask/wqflask/static/new/javascript/password_strength.coffee b/wqflask/wqflask/static/new/javascript/password_strength.coffee new file mode 100644 index 00000000..0bee5836 --- /dev/null +++ b/wqflask/wqflask/static/new/javascript/password_strength.coffee @@ -0,0 +1,33 @@ +$ -> + + + $("#password").keyup -> + passtext = $(this).val() + result = zxcvbn(passtext) + if passtext.length < 6 + $("#password_strength").html('') + $("#password_alert").fadeOut() + else + word = word_score(result.score) + crack_time = result.crack_time_display + if crack_time == "instant" + crack_time = "a second" + display = "This is #{word} password. It can be cracked in #{crack_time}." + $("#password_strength").html(display) + $("#password_alert").fadeIn() + + + + word_score = (num_score) -> + num_score = parseInt(num_score) + console.log("num_score is:", num_score) + mapping = + 0: "a <strong>terrible</strong>" + 1: "a <strong>bad</strong>" + 2: "a <strong>mediocre</strong>" + 3: "a <strong>good</strong>" + 4: "an <strong>excellent</strong>" + console.log("mapping is:", mapping) + result = mapping[num_score] + console.log("result is:", result) + return result
\ No newline at end of file diff --git a/wqflask/wqflask/static/new/javascript/password_strength.js b/wqflask/wqflask/static/new/javascript/password_strength.js new file mode 100644 index 00000000..166e1125 --- /dev/null +++ b/wqflask/wqflask/static/new/javascript/password_strength.js @@ -0,0 +1,42 @@ +// Generated by CoffeeScript 1.6.1 +(function() { + + $(function() { + var word_score; + $("#password").keyup(function() { + var crack_time, display, passtext, result, word; + passtext = $(this).val(); + result = zxcvbn(passtext); + if (passtext.length < 6) { + $("#password_strength").html(''); + return $("#password_alert").fadeOut(); + } else { + word = word_score(result.score); + crack_time = result.crack_time_display; + if (crack_time === "instant") { + crack_time = "a second"; + } + display = "This is " + word + " password. It can be cracked in " + crack_time + "."; + $("#password_strength").html(display); + return $("#password_alert").fadeIn(); + } + }); + return word_score = function(num_score) { + var mapping, result; + num_score = parseInt(num_score); + console.log("num_score is:", num_score); + mapping = { + 0: "a <strong>terrible</strong>", + 1: "a <strong>bad</strong>", + 2: "a <strong>mediocre</strong>", + 3: "a <strong>good</strong>", + 4: "an <strong>excellent</strong>" + }; + console.log("mapping is:", mapping); + result = mapping[num_score]; + console.log("result is:", result); + return result; + }; + }); + +}).call(this); diff --git a/wqflask/wqflask/static/new/js_external/parsley.min.js b/wqflask/wqflask/static/new/js_external/parsley.min.js new file mode 100644 index 00000000..ab85c683 --- /dev/null +++ b/wqflask/wqflask/static/new/js_external/parsley.min.js @@ -0,0 +1,35 @@ +/* Parsley dist/parsley.min.js build version 1.1.18 http://parsleyjs.org */ +!function(d){var h=function(a){this.messages={defaultMessage:"This value seems to be invalid.",type:{email:"This value should be a valid email.",url:"This value should be a valid url.",urlstrict:"This value should be a valid url.",number:"This value should be a valid number.",digits:"This value should be digits.",dateIso:"This value should be a valid date (YYYY-MM-DD).",alphanum:"This value should be alphanumeric.",phone:"This value should be a valid phone number."},notnull:"This value should not be null.", +notblank:"This value should not be blank.",required:"This value is required.",regexp:"This value seems to be invalid.",min:"This value should be greater than or equal to %s.",max:"This value should be lower than or equal to %s.",range:"This value should be between %s and %s.",minlength:"This value is too short. It should have %s characters or more.",maxlength:"This value is too long. It should have %s characters or less.",rangelength:"This value length is invalid. It should be between %s and %s characters long.", +mincheck:"You must select at least %s choices.",maxcheck:"You must select %s choices or less.",rangecheck:"You must select between %s and %s choices.",equalto:"This value should be the same."};this.init(a)};h.prototype={constructor:h,validators:{notnull:function(a){return 0<a.length},notblank:function(a){return"string"===typeof a&&""!==a.replace(/^\s+/g,"").replace(/\s+$/g,"")},required:function(a){if("object"===typeof a){for(var b in a)if(this.required(a[b]))return!0;return!1}return this.notnull(a)&& +this.notblank(a)},type:function(a,b){var c;switch(b){case "number":c=/^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/;break;case "digits":c=/^\d+$/;break;case "alphanum":c=/^\w+$/;break;case "email":c=/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))){2,6}$/i; +break;case "url":a=/(https?|s?ftp|git)/i.test(a)?a:"http://"+a;case "urlstrict":c=/^(https?|s?ftp|git):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i; +break;case "dateIso":c=/^(\d{4})\D?(0[1-9]|1[0-2])\D?([12]\d|0[1-9]|3[01])$/;break;case "phone":c=/^((\+\d{1,3}(-| )?\(?\d\)?(-| )?\d{1,5})|(\(?\d{2,6}\)?))(-| )?(\d{3,4})(-| )?(\d{4})(( x| ext)\d{1,5}){0,1}$/;break;default:return!1}return""!==a?c.test(a):!1},regexp:function(a,b,c){return RegExp(b,c.options.regexpFlag||"").test(a)},minlength:function(a,b){return a.length>=b},maxlength:function(a,b){return a.length<=b},rangelength:function(a,b){return this.minlength(a,b[0])&&this.maxlength(a,b[1])}, +min:function(a,b){return Number(a)>=b},max:function(a,b){return Number(a)<=b},range:function(a,b){return a>=b[0]&&a<=b[1]},equalto:function(a,b,c){c.options.validateIfUnchanged=!0;return a===d(b).val()},remote:function(a,b,c){var f={},g={};f[c.$element.attr("name")]=a;"undefined"!==typeof c.options.remoteDatatype&&(g={dataType:c.options.remoteDatatype});var m=function(a,b){"undefined"!==typeof b&&("undefined"!==typeof c.Validator.messages.remote&&b!==c.Validator.messages.remote)&&d(c.ulError+" .remote").remove(); +c.updtConstraint({name:"remote",valid:a},b);c.manageValidationResult()},n=function(a){if("object"===typeof a)return a;try{a=d.parseJSON(a)}catch(b){}return a},e=function(a){return"object"===typeof a&&null!==a?"undefined"!==typeof a.error?a.error:"undefined"!==typeof a.message?a.message:null:null};d.ajax(d.extend({},{url:b,data:f,type:c.options.remoteMethod||"GET",success:function(a){a=n(a);m(1===a||!0===a||"object"===typeof a&&null!==a&&"undefined"!==typeof a.success,e(a))},error:function(a){a=n(a); +m(!1,e(a))}},g));return null},mincheck:function(a,b){return this.minlength(a,b)},maxcheck:function(a,b){return this.maxlength(a,b)},rangecheck:function(a,b){return this.rangelength(a,b)}},init:function(a){var b=a.validators;a=a.messages;for(var c in b)this.addValidator(c,b[c]);for(c in a)this.addMessage(c,a[c])},formatMesssage:function(a,b){if("object"===typeof b){for(var c in b)a=this.formatMesssage(a,b[c]);return a}return"string"===typeof a?a.replace(/%s/i,b):""},addValidator:function(a,b){this.validators[a]= +b},addMessage:function(a,b,c){if("undefined"!==typeof c&&!0===c)this.messages.type[a]=b;else if("type"===a)for(var d in b)this.messages.type[d]=b[d];else this.messages[a]=b}};var j=function(a,b,c){this.options=b;this.Validator=new h(b);if("ParsleyFieldMultiple"===c)return this;this.init(a,c||"ParsleyField")};j.prototype={constructor:j,init:function(a,b){this.type=b;this.valid=!0;this.element=a;this.validatedOnce=!1;this.$element=d(a);this.val=this.$element.val();this.isRequired=!1;this.constraints= +{};"undefined"===typeof this.isRadioOrCheckbox&&(this.isRadioOrCheckbox=!1,this.hash=this.generateHash(),this.errorClassHandler=this.options.errors.classHandler(a,this.isRadioOrCheckbox)||this.$element);this.ulErrorManagement();this.bindHtml5Constraints();this.addConstraints();this.hasConstraints()&&this.bindValidationEvents()},setParent:function(a){this.$parent=d(a)},getParent:function(){return this.$parent},bindHtml5Constraints:function(){if(this.$element.hasClass("required")||this.$element.prop("required"))this.options.required= +!0;"undefined"!==typeof this.$element.attr("type")&&RegExp(this.$element.attr("type"),"i").test("email url number range")&&(this.options.type=this.$element.attr("type"),RegExp(this.options.type,"i").test("number range")&&(this.options.type="number","undefined"!==typeof this.$element.attr("min")&&this.$element.attr("min").length&&(this.options.min=this.$element.attr("min")),"undefined"!==typeof this.$element.attr("max")&&this.$element.attr("max").length&&(this.options.max=this.$element.attr("max")))); +"string"===typeof this.$element.attr("pattern")&&this.$element.attr("pattern").length&&(this.options.regexp=this.$element.attr("pattern"))},addConstraints:function(){for(var a in this.options){var b={};b[a]=this.options[a];this.addConstraint(b,!0)}},addConstraint:function(a,b){for(var c in a)c=c.toLowerCase(),"function"===typeof this.Validator.validators[c]&&(this.constraints[c]={name:c,requirements:a[c],valid:null},"required"===c&&(this.isRequired=!0),this.addCustomConstraintMessage(c));"undefined"=== +typeof b&&this.bindValidationEvents()},updateConstraint:function(a,b){for(var c in a)this.updtConstraint({name:c,requirements:a[c],valid:null},b)},updtConstraint:function(a,b){this.constraints[a.name]=d.extend(!0,this.constraints[a.name],a);"string"===typeof b&&(this.Validator.messages[a.name]=b);this.bindValidationEvents()},removeConstraint:function(a){a=a.toLowerCase();delete this.constraints[a];"required"===a&&(this.isRequired=!1);this.hasConstraints()?this.bindValidationEvents():"ParsleyForm"=== +typeof this.getParent()?this.getParent().removeItem(this.$element):this.destroy()},addCustomConstraintMessage:function(a){var b=a+("type"===a&&"undefined"!==typeof this.options[a]?this.options[a].charAt(0).toUpperCase()+this.options[a].substr(1):"")+"Message";"undefined"!==typeof this.options[b]&&this.Validator.addMessage("type"===a?this.options[a]:a,this.options[b],"type"===a)},bindValidationEvents:function(){this.valid=null;this.$element.addClass("parsley-validated");this.$element.off("."+this.type); +this.options.remote&&!/change/i.test(this.options.trigger)&&(this.options.trigger=!this.options.trigger?"change":" change");var a=(!this.options.trigger?"":this.options.trigger)+(/key/i.test(this.options.trigger)?"":" keyup");this.$element.is("select")&&(a+=/change/i.test(a)?"":" change");a=a.replace(/^\s+/g,"").replace(/\s+$/g,"");this.$element.on((a+" ").split(" ").join("."+this.type+" "),!1,d.proxy(this.eventValidation,this))},generateHash:function(){return"parsley-"+(Math.random()+"").substring(2)}, +getHash:function(){return this.hash},getVal:function(){return this.$element.data("value")||this.$element.val()},eventValidation:function(a){var b=this.getVal();if("keyup"===a.type&&!/keyup/i.test(this.options.trigger)&&!this.validatedOnce||"change"===a.type&&!/change/i.test(this.options.trigger)&&!this.validatedOnce||!this.isRadioOrCheckbox&&this.getLength(b)<this.options.validationMinlength&&!this.validatedOnce)return!0;this.validate()},getLength:function(a){return!a||!a.hasOwnProperty("length")? +0:a.length},isValid:function(){return this.validate(!1)},hasConstraints:function(){for(var a in this.constraints)return!0;return!1},validate:function(a){var b=this.getVal(),c=null;if(!this.hasConstraints())return null;if(this.options.listeners.onFieldValidate(this.element,this)||""===b&&!this.isRequired)return this.reset(),null;if(!this.needsValidation(b))return this.valid;c=this.applyValidators();("undefined"!==typeof a?a:this.options.showErrors)&&this.manageValidationResult();return c},needsValidation:function(a){if(!this.options.validateIfUnchanged&& +null!==this.valid&&this.val===a&&this.validatedOnce)return!1;this.val=a;return this.validatedOnce=!0},applyValidators:function(){var a=null,b;for(b in this.constraints){var c=this.Validator.validators[this.constraints[b].name](this.val,this.constraints[b].requirements,this);!1===c?(a=!1,this.constraints[b].valid=a,this.options.listeners.onFieldError(this.element,this.constraints,this)):!0===c&&(this.constraints[b].valid=!0,a=!1!==a,!1===this.options.listeners.onFieldSuccess(this.element,this.constraints, +this)&&(a=!1))}return a},manageValidationResult:function(){var a=null,b;for(b in this.constraints)!1===this.constraints[b].valid?(this.manageError(this.constraints[b]),a=!1):!0===this.constraints[b].valid&&(this.removeError(this.constraints[b].name),a=!1!==a);this.valid=a;if(!0===this.valid)return this.removeErrors(),this.errorClassHandler.removeClass(this.options.errorClass).addClass(this.options.successClass),!0;if(!1===this.valid)return this.errorClassHandler.removeClass(this.options.successClass).addClass(this.options.errorClass), +!1;this.ulError&&0===d(this.ulError).children().length&&this.removeErrors();return a},ulErrorManagement:function(){this.ulError="#"+this.hash;this.ulTemplate=d(this.options.errors.errorsWrapper).attr("id",this.hash).addClass("parsley-error-list")},removeError:function(a){a=this.ulError+" ."+a;var b=this;this.options.animate?d(a).fadeOut(this.options.animateDuration,function(){d(this).remove();b.ulError&&0===d(b.ulError).children().length&&b.removeErrors()}):d(a).remove()},addError:function(a){for(var b in a){var c= +d(this.options.errors.errorElem).addClass(b);d(this.ulError).append(this.options.animate?d(c).html(a[b]).hide().fadeIn(this.options.animateDuration):d(c).html(a[b]))}},removeErrors:function(){this.options.animate?d(this.ulError).fadeOut(this.options.animateDuration,function(){d(this).remove()}):d(this.ulError).remove()},reset:function(){this.valid=null;this.removeErrors();this.validatedOnce=!1;this.errorClassHandler.removeClass(this.options.successClass).removeClass(this.options.errorClass);for(var a in this.constraints)this.constraints[a].valid= +null;return this},manageError:function(a){d(this.ulError).length||this.manageErrorContainer();if(!("required"===a.name&&null!==this.getVal()&&0<this.getVal().length)&&(!this.isRequired||!("required"!==a.name&&(null===this.getVal()||0===this.getVal().length)))){var b=a.name,c=!1!==this.options.errorMessage?"custom-error-message":b,f={};a=!1!==this.options.errorMessage?this.options.errorMessage:"type"===a.name?this.Validator.messages[b][a.requirements]:"undefined"===typeof this.Validator.messages[b]? +this.Validator.messages.defaultMessage:this.Validator.formatMesssage(this.Validator.messages[b],a.requirements);d(this.ulError+" ."+c).length||(f[c]=a,this.addError(f))}},manageErrorContainer:function(){var a=this.options.errorContainer||this.options.errors.container(this.element,this.isRadioOrCheckbox),b=this.options.animate?this.ulTemplate.show():this.ulTemplate;"undefined"!==typeof a?d(a).append(b):!this.isRadioOrCheckbox?this.$element.after(b):this.$element.parent().after(b)},addListener:function(a){for(var b in a)this.options.listeners[b]= +a[b]},destroy:function(){this.$element.removeClass("parsley-validated");this.reset().$element.off("."+this.type).removeData(this.type)}};var l=function(a,b,c){this.initMultiple(a,b);this.inherit(a,b);this.Validator=new h(b);this.init(a,c||"ParsleyFieldMultiple")};l.prototype={constructor:l,initMultiple:function(a,b){this.element=a;this.$element=d(a);this.group=b.group||!1;this.hash=this.getName();this.siblings=this.group?'[data-group="'+this.group+'"]':'input[name="'+this.$element.attr("name")+'"]'; +this.isRadioOrCheckbox=!0;this.isRadio=this.$element.is("input[type=radio]");this.isCheckbox=this.$element.is("input[type=checkbox]");this.errorClassHandler=b.errors.classHandler(a,this.isRadioOrCheckbox)||this.$element.parent()},inherit:function(a,b){var c=new j(a,b,"ParsleyFieldMultiple"),d;for(d in c)"undefined"===typeof this[d]&&(this[d]=c[d])},getName:function(){if(this.group)return"parsley-"+this.group;if("undefined"===typeof this.$element.attr("name"))throw"A radio / checkbox input must have a data-group attribute or a name to be Parsley validated !"; +return"parsley-"+this.$element.attr("name").replace(/(:|\.|\[|\])/g,"")},getVal:function(){if(this.isRadio)return d(this.siblings+":checked").val()||"";if(this.isCheckbox){var a=[];d(this.siblings+":checked").each(function(){a.push(d(this).val())});return a}},bindValidationEvents:function(){this.valid=null;this.$element.addClass("parsley-validated");this.$element.off("."+this.type);var a=this,b=(!this.options.trigger?"":this.options.trigger)+(/change/i.test(this.options.trigger)?"":" change"),b=b.replace(/^\s+/g, +"").replace(/\s+$/g,"");d(this.siblings).each(function(){d(this).on(b.split(" ").join("."+a.type+" "),!1,d.proxy(a.eventValidation,a))})}};var k=function(a,b,c){this.init(a,b,c||"parsleyForm")};k.prototype={constructor:k,init:function(a,b,c){this.type=c;this.items=[];this.$element=d(a);this.options=b;var f=this;this.$element.find(b.inputs).each(function(){f.addItem(this)});this.$element.on("submit."+this.type,!1,d.proxy(this.validate,this))},addListener:function(a){for(var b in a)if(/Field/.test(b))for(var c= +0;c<this.items.length;c++)this.items[c].addListener(a);else this.options.listeners[b]=a[b]},addItem:function(a){if(d(a).is(this.options.excluded))return!1;a=d(a).parsley(this.options);a.setParent(this);this.items.push(a)},removeItem:function(a){a=d(a).parsley();for(var b=0;b<this.items.length;b++)if(this.items[b].hash===a.hash)return this.items[b].destroy(),this.items.splice(b,1),!0;return!1},validate:function(a){var b=!0;this.focusedField=!1;for(var c=0;c<this.items.length;c++)if("undefined"!==typeof this.items[c]&& +!1===this.items[c].validate()&&(b=!1,!this.focusedField&&"first"===this.options.focus||"last"===this.options.focus))this.focusedField=this.items[c].$element;this.focusedField&&!b&&this.focusedField.focus();a=this.options.listeners.onFormSubmit(b,a,this);return"undefined"!==typeof a?a:b},isValid:function(){for(var a=0;a<this.items.length;a++)if(!1===this.items[a].isValid())return!1;return!0},removeErrors:function(){for(var a=0;a<this.items.length;a++)this.items[a].parsley("reset")},destroy:function(){for(var a= +0;a<this.items.length;a++)this.items[a].destroy();this.$element.off("."+this.type).removeData(this.type)},reset:function(){for(var a=0;a<this.items.length;a++)this.items[a].reset()}};d.fn.parsley=function(a,b){function c(c,g){var e=d(c).data(g);if(!e){switch(g){case "parsleyForm":e=new k(c,f,"parsleyForm");break;case "parsleyField":e=new j(c,f,"parsleyField");break;case "parsleyFieldMultiple":e=new l(c,f,"parsleyFieldMultiple");break;default:return}d(c).data(g,e)}return"string"===typeof a&&"function"=== +typeof e[a]?(e=e[a](b),"undefined"!==typeof e?e:d(c)):e}var f=d.extend(!0,{},d.fn.parsley.defaults,"undefined"!==typeof window.ParsleyConfig?window.ParsleyConfig:{},a,this.data()),g=null;d(this).is("form")||!0===d(this).data("bind")?g=c(d(this),"parsleyForm"):d(this).is(f.inputs)&&!d(this).is(f.excluded)&&(g=c(d(this),!d(this).is("input[type=radio], input[type=checkbox]")?"parsleyField":"parsleyFieldMultiple"));return"function"===typeof b?b():g};d.fn.parsley.Constructor=k;d.fn.parsley.defaults={inputs:"input, textarea, select", +excluded:"input[type=hidden], input[type=file], :disabled",trigger:!1,animate:!0,animateDuration:300,focus:"first",validationMinlength:3,successClass:"parsley-success",errorClass:"parsley-error",errorMessage:!1,validators:{},showErrors:!0,messages:{},validateIfUnchanged:!1,errors:{classHandler:function(){},container:function(){},errorsWrapper:"<ul></ul>",errorElem:"<li></li>"},listeners:{onFieldValidate:function(){return!1},onFormSubmit:function(){},onFieldError:function(){},onFieldSuccess:function(){}}}; +d(window).on("load",function(){d('[data-validate="parsley"]').each(function(){d(this).parsley()})})}(window.jQuery||window.Zepto); diff --git a/wqflask/wqflask/static/new/js_external/zxcvbn/zxcvbn-async.js b/wqflask/wqflask/static/new/js_external/zxcvbn/zxcvbn-async.js new file mode 100644 index 00000000..404944d3 --- /dev/null +++ b/wqflask/wqflask/static/new/js_external/zxcvbn/zxcvbn-async.js @@ -0,0 +1 @@ +(function(){var a;a=function(){var a,b;b=document.createElement("script");b.src="//dl.dropbox.com/u/209/zxcvbn/zxcvbn.js";b.type="text/javascript";b.async=!0;a=document.getElementsByTagName("script")[0];return a.parentNode.insertBefore(b,a)};null!=window.attachEvent?window.attachEvent("onload",a):window.addEventListener("load",a,!1)}).call(this); diff --git a/wqflask/wqflask/templates/admin/ind_user_manager.html b/wqflask/wqflask/templates/admin/ind_user_manager.html index 9776af0b..2fe4a002 100644 --- a/wqflask/wqflask/templates/admin/ind_user_manager.html +++ b/wqflask/wqflask/templates/admin/ind_user_manager.html @@ -30,6 +30,16 @@ <th>Value</th> </tr> </thead>--> + + <tr> + <td>Name</td> + <td>{{ user.name }}</td> + </tr> + + <tr> + <td>Organization</td> + <td>{{ user.organization }}</td> + </tr> <tr> diff --git a/wqflask/wqflask/templates/admin/user_manager.html b/wqflask/wqflask/templates/admin/user_manager.html index 14cd12e0..1308ff4b 100644 --- a/wqflask/wqflask/templates/admin/user_manager.html +++ b/wqflask/wqflask/templates/admin/user_manager.html @@ -16,20 +16,20 @@ <table class="table table-hover"> <thead> <tr> - <th>ID</th> <th>Email</th> - <th>Confirmed at</th> + <th>Organization</th> <th>Active</th> + <th>Confirmed</th> </tr> </thead> {% for user in users %} <tr> - <td title="{{ user.__dict__ }}"> - <a href="{{ url_for('manage_user', user_id=user.id) }}">{{ user.id }}</a> + <td title="{{ user.id }}"> + <a href="{{ url_for('manage_user', user_id=user.id) }}">{{ user.email_address }}</a> </td> - <td>{{ user.email }}</td> - <td>{{ user.confirmed_at }}</td> - <td>{{ user.active }}</td> + <td>{{ user.organization }}</td> + <td>{{ 'Yes' if user.active else 'No' }}</td> + <td title="{{ user.confirmed }}">{{ 'True' if user.confirmed else 'False' }}</td> </tr> {% endfor %} </table> diff --git a/wqflask/wqflask/templates/base.html b/wqflask/wqflask/templates/base.html index cbed5859..8efba6a7 100644 --- a/wqflask/wqflask/templates/base.html +++ b/wqflask/wqflask/templates/base.html @@ -19,16 +19,46 @@ <link rel="stylesheet" type="text/css" href="/static/packages/colorbox/example4/colorbox.css" /> <link rel="stylesheet" type="text/css" href="/static/new/css/main.css" /> + <link rel="stylesheet" type="text/css" href="/static/new/css/parsley.css" /> {% block css %} {% endblock %} </head> +{% macro header(main, second) %} + <header class="jumbotron subhead" id="overview"> + <div class="container"> + <h1>Login</h1> + <p class="lead"> + Gain access to GeneNetwork. + </p> + </div> + </header> + + {{ flash_me() }} +{% endmacro %} + + +{% macro flash_me() -%} + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + <br /> + <div class="container"> + {% for category, message in messages %} + <div class="alert {{ category }}">{{ message }}</div> + {% endfor %} + </div> + {% endif %} + {% endwith %} +{% endmacro %} + <body data-spy="scroll" data-target=".bs-docs-sidebar"> <!-- Navbar ================================================== --> + <div class="navbar navbar-inverse navbar-fixed-top"> + <div class="navbar-inner"> <div class="container"> <button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse"> @@ -69,10 +99,10 @@ <a href="/links">Links</a> </li> <li class=""> - {% if g.identity.name=="anon" %} - <a id="login_out" class="modalize" href="/login">Sign in</a> + {% if g.user_session.logged_in %} + <a id="login_out" href="/n/logout">Sign out</a> {% else %} - <a id="login_out" href="/logout">Sign out</a> + <a id="login_in" href="/n/login">Sign in</a> {% endif %} </li> </ul> @@ -180,12 +210,13 @@ <script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.9.1/jquery-ui.min.js" type="text/javascript"></script> <script language="javascript" type="text/javascript" src="/static/packages/colorbox/jquery.colorbox.js"></script> - <script type="text/javascript" src="/static/new/javascript/login.js"></script> + <!--<script type="text/javascript" src="/static/new/javascript/login.js"></script>--> + + <script type="text/javascript" src="/static/new/js_external/parsley.min.js"></script> + {% block js %} {% endblock %} - - </body> </html> diff --git a/wqflask/wqflask/templates/email/verification.txt b/wqflask/wqflask/templates/email/verification.txt new file mode 100644 index 00000000..29229c68 --- /dev/null +++ b/wqflask/wqflask/templates/email/verification.txt @@ -0,0 +1,8 @@ +Thank you for signing up for GeneNetwork. + +We need to verify your email address. + +To do that please click the following link, or cut and paste it into your browser window: + +{{ url_for_hmac("verify", code = verification_code, _external=True )}} + diff --git a/wqflask/wqflask/templates/index_page.html b/wqflask/wqflask/templates/index_page.html index 61e34eca..98682e57 100644 --- a/wqflask/wqflask/templates/index_page.html +++ b/wqflask/wqflask/templates/index_page.html @@ -8,11 +8,14 @@ <header class="jumbotron subhead" id="overview"> <div class="container"> <h1>GeneNetwork</h1> - <p class="lead">Open source bioinformatics for systems genetics</p> + <p class="lead">Open source bioinformatics for systems genetics</p> </div> </header> <div class="container"> + + {{ flash_me() }} + <div class="row"> <div class="span3 bs-docs-sidebar"> <ul class="nav nav-list bs-docs-sidenav"> @@ -24,7 +27,7 @@ <li><a href="#websites"><i class="icon-chevron-right"></i> Affiliates and mirrors</a></li> </ul> </div> - + <div class="span9"> <section id="quick-search"> <div class="page-header"> diff --git a/wqflask/wqflask/templates/new_security/_scripts.html b/wqflask/wqflask/templates/new_security/_scripts.html new file mode 100644 index 00000000..5fefe305 --- /dev/null +++ b/wqflask/wqflask/templates/new_security/_scripts.html @@ -0,0 +1 @@ +<!--<script type="text/javascript" src="/static/new/javascript/login.js"></script>--> diff --git a/wqflask/wqflask/templates/new_security/login_user.html b/wqflask/wqflask/templates/new_security/login_user.html new file mode 100644 index 00000000..f232ccbc --- /dev/null +++ b/wqflask/wqflask/templates/new_security/login_user.html @@ -0,0 +1,83 @@ +{% extends "base.html" %} +{% block title %}Register{% endblock %} +{% block content %} + + {{ header("Login", "Gain access to GeneNetwork.") }} + + <div class="container"> + <div class="page-header"> + <h1>Login</h1> + </div> + + + <div class="security_box"> + + <h4>Don't have an account?</h4> + + + <a href="/n/register" class="btn btn-info modalize">Create a new account</a> + + + <hr /> + + <h4>Already have an account?</h4> + + <h5>Sign in here</h5> + + + + <form class="form-horizontal" action="/n/login" + method="POST" name="login_user_form"> + <fieldset> + + + <div class="control-group"> + <label class="control-label" for="email_address">Email Address</label> + <div class="controls"> + <input id="email_address" class="focused" name="email_address" type="text" value=""> + </div> + </div> + + <div class="control-group"> + <label class="control-label" for="password">Password</label> + <div class="controls"> + <input id="password" name="password" type="password" value=""> + <br /> + <a href="url_for_security('forgot_password')">Forgot your password?</a><br/> + </div> + </div> + + + <div class="control-group"> + <div class="controls"> + <label class="checkbox"> + <input id="remember" name="remember" type="checkbox" value="y"> Remember me + </label> + </div> + + + <div class="control-group"> + <div class="controls"> + <input id="next" name="next" type="hidden" value=""> + + <input class="btn btn-primary" id="submit" name="submit" type="submit" value="Sign in"> + </div> + + + </div> + </fieldset> + + </form> + </div> + </div> + </div> + + {% endblock %} + +{% block js %} + <!--<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>--> + + {% include "new_security/_scripts.html" %} + +{% endblock %} + diff --git a/wqflask/wqflask/templates/new_security/register_user.html b/wqflask/wqflask/templates/new_security/register_user.html new file mode 100644 index 00000000..2a02e7ca --- /dev/null +++ b/wqflask/wqflask/templates/new_security/register_user.html @@ -0,0 +1,113 @@ +{% extends "base.html" %} +{% block title %}Register{% endblock %} +{% block content %} + <header class="jumbotron subhead" id="overview"> + <div class="container"> + <h1>Register</h1> + <p class="lead"> + It's easy and fast to make an account. + </p> + </div> + </header> + + <div class="container"> + <div class="page-header"> + <h1>Registration</h1> + </div> + + <div class="security_box"> + <h4>Already have an account?</h4> + + + <a href="/n/login" + class="btn btn-info modalize">Sign in using existing account</a> + + + <hr /> + + <h4>Don't have an account?</h4> + + <h5>Register here</h5> + + {% if errors %} + <div class="alert alert-error"> + <strong>Please note:</strong> + <ul> + {% for error in errors %} + <li>{{error}}</li> + {% endfor %} + </ul> + </div> + {% endif %} + + <form class="form-horizontal" action="/n/register" data-validate="parsley" + method="POST" name="login_user_form"> + <fieldset> + + <div class="control-group"> + <label class="control-label" for="email_address">Email Address</label> + <div class="controls"> + <input id="email_address" name="email_address" class="focused" type="text" value="{{values.email_address}}" + data-trigger="change" data-required="true" data-type="email" data-maxlength="50"> + </div> + </div> + + <div class="control-group"> + <label class="control-label" for="full_name">Full Name</label> + <div class="controls"> + <input id="full_name" name="full_name" type="text" value="{{values.full_name}}" + data-trigger="change" data-required="true" data-minlength="5" data-maxlength="50"> + </div> + </div> + + <div class="control-group"> + <label class="control-label" for="organization">Organization</label> + <div class="controls"> + <input id="organization" name="organization" type="text" value="{{values.organization}}" data-minlength="3" data-maxlength="50"> + </div> + </div> + + <div class="control-group"> + <label class="control-label" for="password">Password</label> + <div class="controls"> + <input id="password" name="password" type="password" value="" + data-trigger="change" data-required="true" data-minlength="6"> + </div> + </div> + + <div class="control-group" style="display: none" id="password_alert"> + <div class="controls""> + <span id="password_strength" class="alert"></span> + </div> + </div> + + <div class="control-group"> + <label class="control-label" for="password_confirm">Confirm Password</label> + <div class="controls"> + <input id="password" name="password_confirm" type="password" value="" + data-trigger="change" data-required="true" data-equalto="#password"> + </div> + </div> + + <div class="control-group"> + <div class="controls""> + <input class="btn btn-primary" id="submit" name="submit" type="submit" value="Create account"> + </div> + </div> + + </fieldset> + + </form> + </div> + </div> + +{% endblock %} + +{% block js %} + <!--<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>--> + + {% include "new_security/_scripts.html" %} + <script type="text/javascript" src="/static/new/js_external/zxcvbn/zxcvbn-async.js"></script> + <script type="text/javascript" src="/static/new/javascript/password_strength.js"></script> +{% endblock %} + diff --git a/wqflask/wqflask/templates/new_security/registered.html b/wqflask/wqflask/templates/new_security/registered.html new file mode 100644 index 00000000..391a6044 --- /dev/null +++ b/wqflask/wqflask/templates/new_security/registered.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} +{% block title %}Register{% endblock %} +{% block content %} + <header class="jumbotron subhead" id="overview"> + <div class="container"> + <h1>Thank you</h1> + <p class="lead"> + Thanks for verifying. + </p> + </div> + </header> + + <div class="container"> + <div class="page-header"> + <h3>One last step</h3> + </div> + + <p>You will receive an email with the subject <strong>"GeneNetwork email verification"</strong>.</p> + + <p>You must click the link in the email to complete registration.</p> + + <p>If you don't see the email, check your spam folder.</p> + </div> + +{% endblock %} + +{% block js %} + <!--<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>--> + + {% include "new_security/_scripts.html" %} + <script type="text/javascript" src="/static/new/js_external/zxcvbn/zxcvbn-async.js"></script> + <script type="text/javascript" src="/static/new/javascript/password_strength.js"></script> +{% endblock %} + diff --git a/wqflask/wqflask/templates/new_security/thank_you.html b/wqflask/wqflask/templates/new_security/thank_you.html new file mode 100644 index 00000000..97cb7807 --- /dev/null +++ b/wqflask/wqflask/templates/new_security/thank_you.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} +{% block title %}Register{% endblock %} +{% block content %} + <header class="jumbotron subhead" id="overview"> + <div class="container"> + <h1>Thank you</h1> + <p class="lead"> + Thanks for verifying. + </p> + </div> + </header> + + <div class="container"> + <div class="page-header"> + <h3>You are done registering</h3> + </div> + + <p>Enjoy using the site.</p> + + <p>Go to the <a href="{{ url_for("index_page") }}">homepage</a></p>. + </div> + +{% endblock %} + +{% block js %} + <!--<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>--> + + {% include "new_security/_scripts.html" %} + <script type="text/javascript" src="/static/new/js_external/zxcvbn/zxcvbn-async.js"></script> + <script type="text/javascript" src="/static/new/javascript/password_strength.js"></script> +{% endblock %} + diff --git a/wqflask/wqflask/templates/new_security/verified.html b/wqflask/wqflask/templates/new_security/verified.html new file mode 100644 index 00000000..97cb7807 --- /dev/null +++ b/wqflask/wqflask/templates/new_security/verified.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} +{% block title %}Register{% endblock %} +{% block content %} + <header class="jumbotron subhead" id="overview"> + <div class="container"> + <h1>Thank you</h1> + <p class="lead"> + Thanks for verifying. + </p> + </div> + </header> + + <div class="container"> + <div class="page-header"> + <h3>You are done registering</h3> + </div> + + <p>Enjoy using the site.</p> + + <p>Go to the <a href="{{ url_for("index_page") }}">homepage</a></p>. + </div> + +{% endblock %} + +{% block js %} + <!--<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>--> + + {% include "new_security/_scripts.html" %} + <script type="text/javascript" src="/static/new/js_external/zxcvbn/zxcvbn-async.js"></script> + <script type="text/javascript" src="/static/new/javascript/password_strength.js"></script> +{% endblock %} + diff --git a/wqflask/wqflask/templates/security/register_user.html b/wqflask/wqflask/templates/security/register_user.html index 3cd021b0..8e6908ff 100644 --- a/wqflask/wqflask/templates/security/register_user.html +++ b/wqflask/wqflask/templates/security/register_user.html @@ -28,8 +28,22 @@ </div> {{ render_only_errors(register_user_form.email) }} </div> + + <div class="control-group"> + <label class="control-label" for="email">Name</label> + <div class="controls"> + <input id="name" name="name" type="text" value=""> + </div> + </div> <div class="control-group"> + <label class="control-label" for="email">Organization</label> + <div class="controls"> + <input id="organization" name="organization" type="text" value=""> + </div> + </div> + + <div class="control-group"> <label class="control-label" for="password">Password</label> <div class="controls"> <input id="password" name="password" type="password" value=""> @@ -49,7 +63,7 @@ <div class="control-group"> <div class="controls""> - <input class="btn btn-primary" id="submit" name="submit" type="submit" value="Sign in"> + <input class="btn btn-primary" id="submit" name="submit" type="submit" value="Create account"> </div> </div> diff --git a/wqflask/wqflask/tracer.py b/wqflask/wqflask/tracer.py new file mode 100644 index 00000000..43a0f15e --- /dev/null +++ b/wqflask/wqflask/tracer.py @@ -0,0 +1,41 @@ +from __future__ import absolute_import, division, print_function + +print("At top of tracer") + +import sys + +#################################################################################### + +# Originally based on http://stackoverflow.com/a/8315566 +def tracefunc(frame, event, arg, indent=[0]): + + func = dict(funcname = frame.f_code.co_name, + filename = frame.f_code.co_filename, + lineno = frame.f_lineno) + + #These are too common to bother printing... + too_common = ( + '/home/sam/ve27/local/lib/python2.7/site-packages/werkzeug/', + '/home/sam/ve27/local/lib/python2.7/site-packages/jinja2/', + ) + + + if func['filename'].startswith(too_common): + return tracefunc + + info = "{funcname} [{filename}: {lineno}]".format(**func) + + if event == "call": + indent[0] += 2 + #print("-" * indent[0] + "> call function", frame.f_code.co_name) + print("-" * indent[0] + "> call function:", info) + elif event == "return": + print("<" + "-" * indent[0], "exit function:", info) + indent[0] -= 2 + return tracefunc + +def turn_on(): + sys.settrace(tracefunc) + print("Tracing turned on!!!!") +#################################################################################### + diff --git a/wqflask/wqflask/user_manager.py b/wqflask/wqflask/user_manager.py index 9e666bbd..a2dff7f2 100644 --- a/wqflask/wqflask/user_manager.py +++ b/wqflask/wqflask/user_manager.py @@ -7,18 +7,80 @@ from __future__ import print_function, division, absolute_import """ -from wqflask import model +import os +import hashlib +import datetime +import time + +import uuid +import hashlib +import hmac +import base64 + +import simplejson as json + +from redis import StrictRedis +Redis = StrictRedis() + + +from flask import (Flask, g, render_template, url_for, request, make_response, + redirect, flash) + +from wqflask import app -from flask import Flask, g from pprint import pformat as pf +from wqflask import pbkdf2 + +from wqflask.database import db_session + +from wqflask import model + +from utility import Bunch, Struct + + + from base.data_set import create_datasets_list -#from app import db -print("globals are:", globals()) +def timestamp(): + return datetime.datetime.utcnow().isoformat() + + + + +class UserSession(object): + cookie_name = 'session_id' + + def __init__(self): + cookie = request.cookies.get(self.cookie_name) + if not cookie: + self.logged_in = False + return + else: + session_id, separator, session_id_signature = cookie.partition(':') + assert len(session_id) == 36, "Is session_id a uuid?" + assert separator == ":", "Expected a : here" + assert session_id_signature == actual_hmac_creation(session_id), "Uh-oh, someone tampering with the cookie?" + self.redis_key = "session_id:" + session_id + print("self.redis_key is:", self.redis_key) + self.session_id = session_id + self.record = Redis.hgetall(self.redis_key) + print("record is:", self.record) + self.logged_in = True + + + def delete_session(self): + # And more importantly delete the redis record + Redis.delete(self.cookie_name) + print("At end of delete_session") + +@app.before_request +def before_request(): + g.user_session = UserSession() + class UsersManager(object): def __init__(self): self.users = model.User.query.all() @@ -28,7 +90,7 @@ class UsersManager(object): class UserManager(object): def __init__(self, kw): - self.user_id = int(kw['user_id']) + self.user_id = kw['user_id'] print("In UserManager locals are:", pf(locals())) #self.user = model.User.get(user_id) #print("user is:", user) @@ -43,6 +105,260 @@ class UserManager(object): print(" ID:", dataset.id) print(" Confidential:", dataset.check_confidentiality()) #print(" ---> self.datasets:", self.datasets) + + +class RegisterUser(object): + def __init__(self, kw): + self.thank_you_mode = False + self.errors = [] + self.user = Bunch() + + self.user.email_address = kw.get('email_address', '').strip() + if not (5 <= len(self.user.email_address) <= 50): + self.errors.append('Email Address needs to be between 5 and 50 characters.') + + self.user.full_name = kw.get('full_name', '').strip() + if not (5 <= len(self.user.full_name) <= 50): + self.errors.append('Full Name needs to be between 5 and 50 characters.') + + self.user.organization = kw.get('organization', '').strip() + if self.user.organization and not (5 <= len(self.user.organization) <= 50): + self.errors.append('Organization needs to be empty or between 5 and 50 characters.') + + password = str(kw.get('password', '')) + if not (6 <= len(password)): + self.errors.append('Password needs to be at least 6 characters.') + + if kw.get('password_confirm') != password: + self.errors.append("Passwords don't match.") + + if self.errors: + return + + print("No errors!") + + self.set_password(password) + + self.user.registration_info = json.dumps(basic_info(), sort_keys=True) + + self.new_user = model.User(**self.user.__dict__) + db_session.add(self.new_user) + db_session.commit() + + print("Adding verification email to queue") + self.send_email_verification() + print("Added verification email to queue") + + self.thank_you_mode = True + + + def set_password(self, password): + pwfields = Bunch() + + pwfields.algorithm = "pbkdf2" + pwfields.hashfunc = "sha256" + #hashfunc = getattr(hashlib, pwfields.hashfunc) + + # Encoding it to base64 makes storing it in json much easier + pwfields.salt = base64.b64encode(os.urandom(32)) + + # https://forums.lastpass.com/viewtopic.php?t=84104 + pwfields.iterations = 100000 + pwfields.keylength = 32 + + pwfields.created_ts = timestamp() + # One more check on password length + assert len(password) >= 6, "Password shouldn't be so short here" + + print("pwfields:", vars(pwfields)) + print("locals:", locals()) + + enc_password = Password(password, + pwfields.salt, + pwfields.iterations, + pwfields.keylength, + pwfields.hashfunc) + + pwfields.password = enc_password.password + pwfields.encrypt_time = enc_password.encrypt_time + + self.user.password = json.dumps(pwfields.__dict__, + sort_keys=True, + ) + + def send_email_verification(self): + verification_code = str(uuid.uuid4()) + key = "verification_code:" + verification_code + + data = json.dumps(dict(id=self.new_user.id, + timestamp=timestamp()) + ) + + Redis.set(key, data) + two_days = 60 * 60 * 24 * 2 + Redis.expire(key, two_days) + to = self.user.email_address + subject = "GeneNetwork email verification" + body = render_template("email/verification.txt", + verification_code = verification_code) + send_email(to, subject, body) + +class Password(object): + def __init__(self, unencrypted_password, salt, iterations, keylength, hashfunc): + hashfunc = getattr(hashlib, hashfunc) + print("hashfunc is:", hashfunc) + # On our computer it takes around 1.4 seconds in 2013 + start_time = time.time() + salt = base64.b64decode(salt) + print("now salt is:", salt) + self.password = pbkdf2.pbkdf2_hex(str(unencrypted_password), + salt, iterations, keylength, hashfunc) + self.encrypt_time = round(time.time() - start_time, 3) + print("Creating password took:", self.encrypt_time) + + +def basic_info(): + return dict(timestamp = timestamp(), + ip_address = request.remote_addr, + user_agent = request.headers.get('User-Agent')) + +def verify_email(): + print("in verify_email request.url is:", request.url) + verify_url_hmac(request.url) + verification_code = request.args['code'] + data = Redis.get("verification_code:" + verification_code) + data = json.loads(data) + print("data is:", data) + user = model.User.query.get(data['id']) + user.confirmed = json.dumps(basic_info(), sort_keys=True) + db_session.commit() + +def login(): + params = request.form if request.form else request.args + print("in login params are:", params) + if not params: + return render_template("new_security/login_user.html") + else: + user = model.User.query.filter_by(email_address=params['email_address']).one() + submitted_password = params['password'] + pwfields = Struct(json.loads(user.password)) + encrypted = Password(submitted_password, + pwfields.salt, + pwfields.iterations, + pwfields.keylength, + pwfields.hashfunc) + print("\n\nComparing:\n{}\n{}\n".format(encrypted.password, pwfields.password)) + valid = pbkdf2.safe_str_cmp(encrypted.password, pwfields.password) + print("valid is:", valid) + + login_rec = model.Login(user) + + + if valid: + login_rec.successful = True + login_rec.session_id = str(uuid.uuid4()) + #session_id = "session_id:{}".format(login_rec.session_id) + session_id_signature = actual_hmac_creation(login_rec.session_id) + session_id_signed = login_rec.session_id + ":" + session_id_signature + print("session_id_signed:", session_id_signed) + + session = dict(login_time = time.time(), + user_id = user.id, + user_email_address = user.email_address) + + flash("Thank you for logging in.", "alert-success") + + key = "session_id:" + login_rec.session_id + print("Key when signing:", key) + Redis.hmset(key, session) + + response = make_response(redirect(url_for('index_page'))) + response.set_cookie(UserSession.cookie_name, session_id_signed) + else: + login_rec.successful = False + flash("Invalid email-address or password. Please try again.", "alert-error") + response = make_response(redirect(url_for('login'))) + db_session.add(login_rec) + db_session.commit() + return response + +@app.route("/n/logout") +def logout(): + print("Logging out...") + UserSession().delete_session() + flash("You are now logged out. We hope you come back soon!") + response = make_response(redirect(url_for('index_page'))) + # Delete the cookie + response.set_cookie(UserSession.cookie_name, '', expires=0) + return response + + +################################# Sign and unsign ##################################### + +def url_for_hmac(endpoint, **values): + """Like url_for but adds an hmac at the end to insure the url hasn't been tampered with""" + + url = url_for(endpoint, **values) + + hm = actual_hmac_creation(url) + if '?' in url: + combiner = "&" + else: + combiner = "?" + return url + combiner + "hm=" + hm + +def verify_url_hmac(url): + """Pass in a url that was created with url_hmac and this assures it hasn't been tampered with""" + print("url passed in to verify is:", url) + # Verify parts are correct at the end - we expect to see &hm= or ?hm= followed by an hmac + assert url[-23:-20] == "hm=", "Unexpected url (stage 1)" + assert url[-24] in ["?", "&"], "Unexpected url (stage 2)" + hmac = url[-20:] + url = url[:-24] # Url without any of the hmac stuff + + #print("before urlsplit, url is:", url) + #url = divide_up_url(url)[1] + #print("after urlsplit, url is:", url) + + hm = actual_hmac_creation(url) + + assert hm == hmac, "Unexpected url (stage 3)" + +def actual_hmac_creation(stringy): + """Helper function to create the actual hmac""" + + secret = app.config['SECRET_HMAC_CODE'] + + hmaced = hmac.new(secret, stringy, hashlib.sha1) + hm = hmaced.hexdigest() + # "Conventional wisdom is that you don't lose much in terms of security if you throw away up to half of the output." + # http://www.w3.org/QA/2009/07/hmac_truncation_in_xml_signatu.html + hm = hm[:20] + return hm + +app.jinja_env.globals.update(url_for_hmac=url_for_hmac) + +####################################################################################### + +def send_email(to, subject, body): + msg = json.dumps(dict(From="no-reply@genenetwork.org", + To=to, + Subject=subject, + Body=body)) + Redis.rpush("mail_queue", msg) + + +#def combined_salt(user_salt): +# """Combine the master salt with the user salt...we use two seperate salts so that if the database is compromised, the +# salts aren't immediately known""" +# secret_salt = app.confing['SECRET_SALT'] +# assert len(user_salt) == 32 +# assert len(secret_salt) == 32 +# combined = "" +# for x, y in user_salt, secret_salt: +# combined = combined + x + y +# return combined + class GroupsManager(object): @@ -54,3 +370,13 @@ class RolesManager(object): def __init__(self): self.roles = model.Role.query.all() print("Roles are:", self.roles) + + +#class Password(object): +# """To generate a master password: dd if=/dev/urandom bs=32 count=1 > master_salt""" +# +# master_salt = + + + + diff --git a/wqflask/wqflask/views.py b/wqflask/wqflask/views.py index d57bbaa7..deccf459 100644 --- a/wqflask/wqflask/views.py +++ b/wqflask/wqflask/views.py @@ -23,7 +23,8 @@ import sqlalchemy from wqflask import app -from flask import render_template, request, make_response, Response, Flask, g, config, jsonify +from flask import (render_template, request, make_response, Response, + Flask, g, config, jsonify, redirect, url_for) from wqflask import search_results from base.data_set import DataSet # Used by YAML in marker_regression @@ -50,6 +51,11 @@ from wqflask import user_manager @app.before_request def connect_db(): g.db = sqlalchemy.create_engine(app.config['DB_URI']) + +#@app.before_request +#def trace_it(): +# from wqflask import tracer +# tracer.turn_on() @app.route("/") def index_page(): @@ -261,15 +267,15 @@ def sharing_info_page(): template_vars = SharingInfoPage.SharingInfoPage(fd) return template_vars -# Take this out or secure it before going into production +# Take this out or secure it before putting into production @app.route("/get_temp_data") def get_temp_data(): temp_uuid = request.args['key'] return flask.jsonify(temp_data.TempData(temp_uuid).get_all()) -@app.route("/thank_you") -def thank_you(): - return render_template("security/thank_you.html") + +################################################################################################### + @app.route("/manage/users") def manage_users(): @@ -287,6 +293,55 @@ def manage_groups(): return render_template("admin/group_manager.html", **template_vars.__dict__) +@app.route("/n/register", methods=('GET', 'POST')) +def register(): + params = None + errors = None + + #if request.form: + # params = request.form + #else: + # params = request.args + + params = request.form if request.form else request.args + + if params: + print("Attempting to register the user...") + result = user_manager.RegisterUser(params) + errors = result.errors + + if result.thank_you_mode: + assert not errors, "Errors while in thank you mode? That seems wrong..." + return render_template("new_security/registered.html") + + return render_template("new_security/register_user.html", values=params, errors=errors) + +#@app.route("/n/register_submit", methods=('POST',)) +#def register_submit(): +# print("request.args are: ", request.args) +# result = user_manager.RegisterUser(request.form) +# if result.errors: +# print("Redirecting") +# # 307 preserves the post on the redirect (maybe) +# errors = result.errors +# #errors = json.dumps(errors) +# print("request.args are: ", request.args) +# return render_template("new_security/register_user.html", errors=errors, values=request.form) +# #return redirect(url_for('new_register', errors=errors), code=307) + + +@app.route("/n/login", methods=('GET', 'POST')) +def login(): + return user_manager.login() + +@app.route("/manage/verify") +def verify(): + user_manager.verify_email() + return render_template("new_security/verified.html") + + +########################################################################## + def json_default_handler(obj): '''Based on http://stackoverflow.com/a/2680060/1175849''' # Handle datestamps |