Models

Permission class

GENERAL = 0x01
ADMINISTER = 0xff

Okay so here is a seemingly simple piece of code I really think is really cool! First of all we are setting up two enums here. But they are set to weird hexadecimal numbers 0x01 and 0xff. If you stick these into a hexadecimal -> decimal converter you'll find that they represent 1 and 255 respectively. But in binary they come out to 00000001 and 11111111 (8 ones). If we do a binary and (&) on these two numbers, we can actually get some unique properties from these. So if we do GENERAL & ADMINSTER, it will come out to the following

  00000001
& 11111111
----------
  00000001

We get back the exact same value as GENERAL! Similarly if we do ADMINSTER & GENERAL we get back GENERAL. This is useful for checking user roles and who is exactly who in this system. So we can create a method 'check(input, checker)' that will take an input hex to test and one to text against. We only need to do '(input & checker) == checker'. But there are some more interesting applications for this. Let us define, for example, a set of enums CAN_LIKE = 0x01, CAN_POST = 0x02, CAN_EDIT = 0x04 and CAN_REMOVE = 0x08. These are respectively in binary 00000001, 00000010, 00000100, 00001000. We can use binary OR (|) to create composite user permissions e.g. CAN_LIKE | CAN_POST | CAN_EDIT = 0x07 = 00000111 -> NEW_ROLE. We can run 'check(NEW_ROLE, CAN_LIKE)' or 'check(NEW_ROLE, CAN_POST)' or 'check(NEW_ROLE, CAN_EDIT)' and all of these will return True. For example NEW_ROLE & CAN_EDIT

  00000111
& 00000001
----------
  00000001 <- equivalent to CAN_EDIT enum

A function similar to the check described above can be found in as the 'can' method below in the User class. Moving on!

Role class

The Role class instatiates Role model. This is used for the creation of users such as a general user and an administrator

COLUMN DEFINITIONS:

id serves as the primary key (expects int).

name is the name of the role itself (expects unique String len 64)

index is the name of the index route for the route

default is a T/F value that determines whether a new user created has that permission or note (ref insert_roles()). This is indexed meaning that a separate table has been created with default as the first column and id as the second column. Default in this table is sorted and a query for default performs a binary search rather than a linear search (reduces search time complexity from O(N) to O(log n)

permissions contains the permissions enum (see Permissions class)

users is not a column but it sets up a database relation. This case is a one-to-many relationship in that for ONE Role record, there are MANY associated User objects. The backref param specifies a bi-directional relationship between the two tables in that there is a new property on both a given Role and User object. E.g. Role.users will refer to the User object (i.e. the user table). and User.role (role being the string specified with backref) will refer to the Role object. Lazy = dynamic specifies to return a Query object instead of actually asking the relationship to load all of its child elements upon creating the relationship. It is best practice to include lazy=dynamic upon the establishment of a relationship.

Sub-note on lazy-dynamic and backref:

Currently, lazy-dynamic will make the User collection to be loaded in as a Query object (so not everything is loaded at once). Simiarly (as mentioned above), the User object can reference the Role object by doing User.role however, this uses the default collection loading behavior (i.e. load the entire collection at once). It is fine in this case since the amount of Roles in the Role collection will be much less than the amount of entries in the User collection. However, we can specify that User.role uses the lazy-dynamic loading scheme. Simply redefine users here to

users = db.relationship('User', backref=db.backref('role',
                                      lazy='dynamic'), lazy='dynamic')

insert_roles() and SQLAlchemy Sessions

The staticmethod decorator specifies that insert_roles() must be be called with a instance of the Role class. E.g. role_obj.insert_roles() This method is fairly self-explanatory. It specifies a 'roles' dict This is then iterated through and foreach role in the 'roles' dict we check to see if it already exists (by name) in the Role object i.e. the Roles table. If not, then a new Role object is instantiated After that, the perms, index, default props are set and the the role object is now added to the db session and then committed.

A note about sqlalchemy if you haven't noticed already: All changes are added to a Session object (handled by SQLAlchemy). Unless specified otherwise, the session object has a merge operation that finds the difs between the new object (that was created and added to the session object) and the currently existing (corresponding) object existing in the table right now. Then a commit() propegates these changes into the database making as little changes as possible (i.e. every time we update a record, the record's attribute is changed 'in place' rather than being deleted and then replaced. Neat :)

__repr__

def __repr__(self):
    return '<Role \'%s\'>' % self.name

this repr method is pretty much optional, but it is helpful in that it will allow the program to pretty print the user object when you come across an error

User Model

The class User represents users... it extends db.Model and UserMixin. Per the flask-login documentation, the User class needs to implement is_authenticated (returns True if the user is authenticated and in turn fulfill login_required), is_active (returns True if the user has been activated i.e. confirmed by email in our case), is_anonymous (returns if a user is Anonymous i.e. is_active = is_authenticated = False, is_anonymous = True, and get_id() = None), get_id() (returns a UNICODE that has the id of the user NOT an int).

Column Descriptions:

id - primary key for the table. Id of the user. i.e. the unique identifier for the collection

confirmed - boolean val (default value = False) that is an indication of whether the user has confirmed their account via email.

first_name - ... string self explanatory

last_name - ... string self explanatory

email - string self explanatory. But we impose the uniqueness constraint on this column. It is necessary to check for this on the backend before entering an email into the table, else there will be some nasty errors produced when the user tries to add an existing email into the table.

Note: first_name, last_name, email form an index table for easy lookup. See Role for more info

password_hash is a 128 char long string containing the hashed password. As always, it is best practice to never include the plaintext password on the server. This hashed password is checked against when authenticating users.

role_id is the id of the role the user is. It is a foreign key and relates to the id's in the Role collection. By default the general user is role.id = 1, and role.id = 2 is the admin. Also note that we refer to the Role collection with 'roles' rather than the assigned backref 'role' since we are referring to an individual column.

Other User Class Variables and Methods

Note that the following methods are actually available in your Jinja templates since they are attached to the user instance.

full_name provides the full name of the user given a first and last name

can provides a really cool way of determining whether a user has given permissions. See the Permissions class for more info. is_admin is an implementation of can to test a user against admin permissions.

password This does not give a password if a user just calls the method and throws an AttributeError. However if someone chooses to set a password e.g. u = User( password = test ) the second definition of password method is run, taking the keyword arg (kwarg) as the password to then call the generate_password_hash method and set the password_hash property of the user to the generated password.

verify_password well...verifies a provided user plaintext password against the password_hash in the user record. Uses the check_password_hash method.

generate_confirmation_token returns a cryptographically signed string with encrypted user id under key confirm. This will expire in 7 days. Note that Serializer is actually TimedJSONWebSerializer when looking for documentation.

generate_changed_email_token also returns a cryptographically signed string with encrypted user id under key change_email and a encrypted new_email parameter password into the method containing the desired new email the user wants to replace the old email with.

generate_password_reset_token operates similarly to generate_ confirmation_token. Generates token for password reset

NOTE: For context, the generate_..._token methods are used to create a random string that will be later added to an email (usually) to the requesting user.

confirm_account

The confirm_account method will take in a token (which was presumably generated from the generate_confirmation_token method) and then return True if the provided token is valid (and can be decrypted with the SECRET_KEY and has not expired) AND the decrypted token has the key 'confirm' with the id of the requesting user. If so, it flips the 'confirmed' attribute of the requesting user to True. Will throw BadSignature of the token is invalid, will throw SignatureExpired if the token is past the expiration time.

change_email

The change_email method will take in a token (which was presumably generated from the generate_email_token method) and then return True True if the token is valid (see above method for explanation of 'valid') and contains the key 'change_email' with value = user id in addition to the key 'new_email' with the new email address the user wants to change their email to. Before the new_email is committed to the session, a query is performed on the User collection on all the emails to maintain the unique constraint on the email columns. Then the user's 'email' attribute is set to the 'new_email' specified in the decrypted token. will throw BadSignature if invalid token and SignatureExpired if the token is expired.

AnonymousUser

We define a custom AnonymousUser class that represents a non-logged user. It extends the AnonymousUserMixing provided by flask-loginmanager we deny all permissions and affirm that this user is not an admin

class AnonymousUser(AnonymousUserMixin):
    def can(self, _):
        return False

    def is_admin(self):
        return False
login_manager.anonymous_user = AnonymousUser

We then register our custom AnonymousUser class as the default login_manager anonymous user class

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

This is the default user_loader method for login_manager. This method defines how to query for a user given a user_id from the user SESSION object. It is pretty straightforward, it will query the User table and find the user with ID equal to the user_id provided in the user SESSION