class LogMod(ModBase, db.Model): """Represents a change made to a mod.""" __tablename__ = "log_mod" # The user that made this change. user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) user = db.relationship('User', backref='changes') # Date this change was made. date = db.Column(db.DateTime, default=datetime.datetime.utcnow) cur_id = db.Column(db.Integer, db.ForeignKey('mod.id'), nullable=True) current = db.relationship("Mod", backref=backref("logs", order_by='LogMod.date')) # Index within this mod's list of versions. index = db.Column(db.Integer, nullable=False) authors = db.relationship("ModAuthor", secondary=authored_by_table) mod_vsns = db.relationship("LogModVersion", back_populates="mod") def blank(self, **kwargs): return LogMod(**kwargs) def blank_child(self, **kwargs): return LogModVersion(**kwargs) def copy_from(self, other): if hasattr(other, 'cur_id'): self.cur_id = other.cur_id self.cur_id = other.id super(ModBase, self).copy_from(other)
class Mod(ModBase, db.Model): __tablename__ = "mod" slug = db.Column(db.String(80), nullable=False, unique=True) authors = db.relationship( "ModAuthor", secondary=authored_by_table, backref="mods") mod_vsns = db.relationship("ModVersion", back_populates="mod") def game_versions(self): """Returns a list of game versions supported by all the versions of this mod.""" gvs = GameVersion.query \ .join(ModVersion, GameVersion.mod_vsns) \ .filter(ModVersion.mod_id == self.id).all() gvsns = set() for gv in gvs: gvsns.add(gv.name) return sorted(list(gvsns)) def game_versions_str(self): """Returns a comma separated string listing the supported game versions for this mod.""" return ", ".join(map(lambda v: v, self.game_versions())) def blank(self, **kwargs): return Mod(**kwargs) def blank_child(self, **kwargs): return ModVersion(**kwargs) def log_change(self, user): entry = LogMod(user=user, cur_id=self.id, index=len(self.logs)) entry.copy_from(self) db.session.add(entry) return entry @property def latest_vsn(self): # FIXME: This could probably be done faster with a DB query. return self.logs[len(self.logs)-1] def make_draft(self, user): """Creates a DraftMod based on the latest version of this mod.""" latest = self.latest_vsn draft = DraftMod(user=user) draft.copy_from(latest) return draft def revert_to(self, log): """ Takes a `ModLog` and reverts this mod to its state at the time of that log entry. Raises `ValueError` if the given log entry is not for this mod. """ if log.cur_id != self.id: raise ValueError('Log entry {} is not for mod {}'.format(log, self)) self.copy_from(log)
class ModVersion(ModVersionBase, db.Model): __tablename__ = "mod_version" mod_id = db.Column(db.Integer, db.ForeignKey('mod.id')) mod = db.relationship("Mod", back_populates="mod_vsns") game_vsns = db.relationship( "GameVersion", secondary=for_game_vsn_table, backref="mod_vsns") files = db.relationship("ModFile", back_populates="version") def blank(self, **kwargs): return ModVersion(**kwargs) def blank_child(self, **kwargs): return ModFile(**kwargs)
class LogModFile(ModFileBase, db.Model): __tablename__ = "log_mod_file" version_id = db.Column(db.Integer, db.ForeignKey('log_mod_version.id')) version = db.relationship("LogModVersion", back_populates="files") cur_id = db.Column(db.Integer, db.ForeignKey('mod_file.id'), nullable=True) current = db.relationship("ModFile") def blank(self, **kwargs): return LogModFile(**kwargs) def copy_from(self, other): if hasattr(other, 'cur_id'): self.cur_id = other.cur_id self.cur_id = other.id super(ModFileBase, self).copy_from(other)
class ModFile(ModFileBase, db.Model): __tablename__ = "mod_file" version_id = db.Column(db.Integer, db.ForeignKey('mod_version.id')) version = db.relationship("ModVersion", back_populates="files") # Whether we're providing our own download links for this file. redist = db.Column(db.Boolean) def blank(self, **kwargs): return ModFile(**kwargs)
class LogModVersion(ModVersionBase, db.Model): __tablename__ = "log_mod_version" mod_id = db.Column(db.Integer, db.ForeignKey('log_mod.id')) mod = db.relationship("LogMod", back_populates="mod_vsns") game_vsns = db.relationship("GameVersion", secondary=for_game_vsn_table) files = db.relationship("LogModFile", back_populates="version") cur_id = db.Column(db.Integer, db.ForeignKey('mod_version.id'), nullable=True) current = db.relationship("ModVersion") def blank(self, **kwargs): return LogModVersion(**kwargs) def blank_child(self, **kwargs): return LogModFile(**kwargs) def copy_from(self, other): if hasattr(other, 'cur_id'): self.cur_id = other.cur_id self.cur_id = other.id super(ModVersionBase, self).copy_from(other)
class UserSetting(db.Model): # type: ignore """ Table for storing user settings. Each setting the user has changed from the defaults is stored as a separate row containing a key, value pair in this table. This schema allows for new settings to be added without requiring a database migration. """ key = db.Column(db.String(40), nullable=False, primary_key=True) value = db.Column(db.JSON, nullable=False) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) user = db.relationship('User')
class StoredFile(db.Model): """Represents a file stored in some sort of storage medium.""" __tablename__ = 'stored_file' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(80), nullable=False) sha256 = db.Column(db.String(130), nullable=False) upload_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) upload_by = db.relationship('User') # Path to this file within the B2 bucket. Null if file is not on B2. b2_path = db.Column(db.String(300), nullable=True) def b2_download_url(self): """Gets the URL to download this file from the archive's B2 bucket.""" if self.b2_path: return urljoin(app.config['B2_PUBLIC_URL'], self.b2_path)
class ResetToken(db.Model): """Represents a one time use key that allows a user to reset (or set) their password.""" id = db.Column(db.Integer, primary_key=True) token = db.Column(GUID(), nullable=False, unique=True) created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) active = db.Column(db.Boolean, nullable=False, default=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) user = db.relationship('User', backref=db.backref('reset_token', uselist=False, cascade='all, delete-orphan')) def __init__(self, *args, token=None, **kwargs): if not token: token = uuid.uuid4() super(ResetToken, self).__init__(*args, token=token, **kwargs) def expired(self): return not self.active or \ self.created < datetime.utcnow() - app.config['PASSWD_RESET_EXPIRE_TIME']
class Session(db.Model): """Represents a user's login session.""" id = db.Column(db.Integer, primary_key=True) # Session ID stored in the user's browser cookies. sess_id = db.Column(GUID(), nullable=False, unique=True) login_ip = db.Column(db.String(128), nullable=False) login_date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) last_seen = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) # User this session is logged in as. user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) user = db.relationship('User', backref=db.backref('sessions', lazy=True, order_by=last_seen.desc())) active = db.Column(db.Boolean, nullable=False, default=True) def __init__(self, *args, sess_id=None, **kwargs): if not sess_id: sess_id = uuid.uuid4() super(Session, self).__init__(*args, sess_id=sess_id, **kwargs) def expired(self): return not self.active or \ self.last_seen < datetime.utcnow() - app.config['SERV_SESSION_EXPIRE_TIME'] def disable(self): self.active = False def touch(self): """Updates this session's last seen date.""" now = datetime.utcnow() self.last_seen = now self.user.last_seen = now
class Mod(ModBase, db.Model): __tablename__ = "mod" slug = db.Column(db.String(80), nullable=False, unique=True) authors = db.relationship( "ModAuthor", secondary=authored_by_table, backref="mods") mod_vsns = db.relationship("ModVersion", back_populates="mod") # If this is set to false, the mod will be de-listed. redist = db.Column(db.Boolean, nullable=False, default=True) @staticmethod def search_query(game_vsn=None, author=None, keyword=None, include_delisted=False): """ Returns a pre-made standard search query for listing mods. Optional parameters may be specified for filtering. """ query = Mod.query if not include_delisted: query = query.filter_by(redist=True) if author and len(author) > 0: query = query.join(ModAuthor, Mod.authors).filter(ModAuthor.name == author) if game_vsn and len(game_vsn) > 0: query = query.join(ModVersion) \ .join(GameVersion, ModVersion.game_vsns) \ .filter(GameVersion.name == game_vsn) if keyword and len(keyword) > 0: query = query.filter(Mod.name.ilike("%"+keyword+"%")) return query def game_versions(self): """Returns a list of game versions supported by all the versions of this mod.""" gvs = GameVersion.query \ .join(ModVersion, GameVersion.mod_vsns) \ .filter(ModVersion.mod_id == self.id).all() gvsns = set() for gv in gvs: gvsns.add(gv.name) return sorted(list(gvsns)) def game_versions_str(self): """Returns a comma separated string listing the supported game versions for this mod.""" return ", ".join(map(lambda v: v, self.game_versions())) def blank(self, **kwargs): return Mod(**kwargs) def blank_child(self, **kwargs): return ModVersion(**kwargs) def log_change(self, user, approved_by=None): """ Logs a new change for this mod. Assigns the change to `user`, and if `approved_by` is not `None`, assigns them as the user who approved it. """ entry = LogMod(user=user, approved_by=approved_by, cur_id=self.id, index=len(self.logs)) entry.copy_from(self) db.session.add(entry) return entry @property def latest_vsn(self): # FIXME: This could probably be done faster with a DB query. return self.logs[len(self.logs)-1] def make_draft(self, user): """Creates a DraftMod based on the latest version of this mod.""" latest = self.latest_vsn draft = DraftMod(user=user) draft.copy_from(latest) return draft def revert_to(self, log): """ Takes a `ModLog` and reverts this mod to its state at the time of that log entry. Raises `ValueError` if the given log entry is not for this mod. """ if log.cur_id != self.id: raise ValueError('Log entry {} is not for mod {}'.format(log, self)) self.copy_from(log)
class Session(db.Model): """Represents a user's login session. A session may not be considered fully authenticated if `authed_2fa` is false. """ id = db.Column(db.Integer, primary_key=True) # Session ID stored in the user's browser cookies. sess_id = db.Column(GUID(), nullable=False, unique=True) login_ip = db.Column(db.String(128), nullable=False) login_date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) last_seen = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) # User this session is logged in as. user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) user = db.relationship('User', backref=db.backref('sessions', lazy=True, order_by=last_seen.desc())) authed_2fa = db.Column(db.Boolean, nullable=False, default=False) active = db.Column(db.Boolean, nullable=False, default=True) def __init__(self, *args, sess_id=None, **kwargs): if not sess_id: sess_id = uuid.uuid4() super(Session, self).__init__(*args, sess_id=sess_id, **kwargs) def expired(self): if not self.active: return True if self.authed_2fa: return self.last_seen < datetime.utcnow( ) - app.config['SERV_SESSION_EXPIRE_TIME'] else: return self.last_seen < datetime.utcnow() - \ app.config['SERV_PARTIAL_SESSION_EXPIRE_TIME'] def auth_2fa(self, code): """Authenticates the user's second factor with the given code. Returns true on success, false on failure. If successful, this session will be marked as fully authenticated. If the user's 2 factor is disabled, this has no effect. """ if self.user.totp_secret and self.user.validate_otp(code): self.authed_2fa = True db.session.commit() return True else: return False def disable(self): self.active = False def touch(self): """Updates this session's last seen date.""" now = datetime.utcnow() self.last_seen = now self.user.last_seen = now