""" Copyright 2019-2021 by J. Christopher Wagner (jwag). All rights reserved. :license: MIT, see LICENSE for more details. Complete models for all features when using Flask-SqlAlchemy. You can change the table names by passing them in to the set_db_info() method. BE AWARE: Once any version of this is shipped no changes can be made - instead a new version needs to be created. """ import datetime from typing import cast from sqlalchemy import ( Boolean, DateTime, Column, Integer, String, ForeignKey, ) from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.mutable import MutableList from sqlalchemy.sql import func from flask_security import AsaList, RoleMixin, UserMixin class FsModels: """ Helper class for model mixins. This records the ``db`` (which is a Flask-SqlAlchemy object) for use in mixins. """ roles_users = None db = None fs_model_version = 1 user_table_name = "user" role_table_name = "role" webauthn_table_name = "webauthn" @classmethod def set_db_info( cls, appdb, user_table_name="user", role_table_name="role", webauthn_table_name="webauthn", ): """Initialize Model. This needs to be called after the DB object has been created (e.g. db = Sqlalchemy()) """ cls.db = appdb cls.user_table_name = user_table_name cls.role_table_name = role_table_name cls.webauthn_table_name = webauthn_table_name cls.roles_users = appdb.Table( "roles_users", Column("user_id", Integer(), ForeignKey(f"{cls.user_table_name}.id")), Column("role_id", Integer(), ForeignKey(f"{cls.role_table_name}.id")), ) class FsRoleMixin(RoleMixin): id = Column(Integer(), primary_key=True) name = Column(String(80), unique=True, nullable=False) description = Column(String(255)) # A comma separated list of strings permissions = Column( MutableList.as_mutable(AsaList()), nullable=True # type: ignore ) update_datetime = Column( type_=DateTime, nullable=False, server_default=func.now(), onupdate=datetime.datetime.utcnow, ) class FsUserMixin(UserMixin): """User information""" # flask_security basic fields id = Column(Integer, primary_key=True) email = Column(String(255), unique=True, nullable=False) # Username is important since shouldn't expose email to other users in most cases. username = Column(String(255)) password = Column(String(255), nullable=False) active = cast(bool, Column(Boolean(), nullable=False)) # Flask-Security user identifier fs_uniquifier = Column(String(64), unique=True, nullable=False) # confirmable confirmed_at = Column(DateTime()) # trackable last_login_at = Column(DateTime()) current_login_at = Column(DateTime()) last_login_ip = Column(String(64)) current_login_ip = Column(String(64)) login_count = Column(Integer) # 2FA tf_primary_method = Column(String(64), nullable=True) tf_totp_secret = Column(String(255), nullable=True) tf_phone_number = Column(String(128), nullable=True) @declared_attr def roles(cls): # The first arg is a class name, the backref is a column name return FsModels.db.relationship( "Role", secondary=FsModels.roles_users, backref=FsModels.db.backref( "users", lazy="dynamic", cascade_backrefs=False ), ) create_datetime = Column(type_=DateTime, nullable=False, server_default=func.now()) update_datetime = Column( type_=DateTime, nullable=False, server_default=func.now(), onupdate=datetime.datetime.utcnow, )