Ejemplo n.º 1
0
class ModBase(CopyDiff):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(80), nullable=False)
    desc = db.Column(db.Text, nullable=False, default="")
    website = db.Column(db.String(120), nullable=False, default="")

    def vsns_by_game_vsn(self):
        """
        Returns a dict mapping supported Minecraft versions to a list of
        versions of this mod that support that Minecraft version.
        """
        vsns = OrderedDict()
        for v in self.mod_vsns:
            if len(v.game_vsns) > 0:
                vsns.setdefault(v.game_vsns[0].name, []).append(v)
            else:
                vsns.setdefault('Unknown', []).append(v)
        return vsns

    # Methods for CopyDiff
    def copydiff_fields(self):
        return ['name', 'desc', 'website', 'authors']

    def get_children(self):
        return self.mod_vsns

    def add_child(self, ch):
        self.mod_vsns.append(ch)

    def rm_child(self, ch):
        self.mod_vsns.remove(ch)
Ejemplo n.º 2
0
class ModFileBase(CopyDiff):
    id = db.Column(db.Integer, primary_key=True)
    desc = db.Column(db.Text, nullable=False, default="")

    @declared_attr
    def stored_id(cls):
        return db.Column(db.Integer,
                         db.ForeignKey('stored_file.id'),
                         nullable=True)

    @declared_attr
    def stored(cls):
        return db.relation('StoredFile')

    # Link to official web page with download links.
    page_url = db.Column(db.String(500), nullable=False, default="")
    # Official download link through redirect such as adfly.
    redirect_url = db.Column(db.String(500), nullable=False, default="")
    # Official direct file download link.
    direct_url = db.Column(db.String(500), nullable=False, default="")

    def copydiff_fields(self):
        return ['stored', 'page_url', 'redirect_url', 'direct_url']

    def get_children(self):
        return []
Ejemplo n.º 3
0
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)
Ejemplo n.º 4
0
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)
Ejemplo n.º 5
0
class ModBase(CopyDiff):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(80), nullable=False)
    desc = db.Column(db.Text, nullable=False, default="")
    website = db.Column(db.String(2048), nullable=False, default="")

    uuid = db.Column(GUID(), nullable=False, default=uuid.uuid4)

    def vsns_by_game_vsn(self):
        """
        Returns a dict mapping supported Minecraft versions to a list of
        versions of this mod that support that Minecraft version.
        """
        vsns = OrderedDict()
        for v in self.mod_vsns:
            if len(v.game_vsns) > 0:
                vsns.setdefault(v.game_vsns[0].name, []).append(v)
            else:
                vsns.setdefault('Unknown', []).append(v)
        # Sort the list of versions by Minecraft version
        lst = sorted(vsns.items(),
                     key=lambda vsn: key_mc_version(vsn[0]),
                     reverse=True)
        # Sort each list of mod versions per Minecraft version
        return OrderedDict([(gvsn,
                             sorted(modvsns,
                                    key=lambda vsn: key_mod_version(vsn.name),
                                    reverse=True)) for gvsn, modvsns in lst])

    # Methods for CopyDiff
    def copydiff_fields(self):
        return ['name', 'desc', 'website', 'authors']

    def get_children(self):
        return self.mod_vsns

    def add_child(self, ch):
        self.mod_vsns.append(ch)

    def rm_child(self, ch):
        self.mod_vsns.remove(ch)
Ejemplo n.º 6
0
class ModVersionBase(CopyDiff):
    id = db.Column(db.Integer, primary_key=True)

    name = db.Column(db.String(40), nullable=False)
    desc = db.Column(db.Text, nullable=False, default="")
    url = db.Column(db.String(120), nullable=False, default="")

    def game_versions_str(self):
        """Returns a comma separated string listing the supported game versions for this mod."""
        return ", ".join(map(lambda v: v.name, self.game_vsns))

    def copydiff_fields(self):
        return ['name', 'desc', 'url', 'game_vsns']

    def get_children(self):
        return self.files

    def add_child(self, ch):
        self.files.append(ch)

    def rm_child(self, ch):
        self.files.remove(ch)
Ejemplo n.º 7
0
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')
Ejemplo n.º 8
0
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
Ejemplo n.º 9
0
class GameVersion(db.Model):
    __tablename__ = "game_version"
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(40), nullable=False, unique=True)
Ejemplo n.º 10
0
class ModAuthor(db.Model):
    __tablename__ = "author"
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(80), nullable=False, unique=True)
    desc = db.Column(db.Text, nullable=False, default="")
    website = db.Column(db.String(120), nullable=False, default="")
Ejemplo n.º 11
0
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)
Ejemplo n.º 12
0
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(80), unique=True, nullable=False)
    password = db.Column(db.Binary(60), nullable=True)
    email = db.Column(db.String(120), unique=True, nullable=False)
    totp_secret = db.Column(db.String(16))
    totp_last_auth = db.Column(db.String(6))  # To avoid replay attacks
    role = db.Column(db.Enum(UserRole), nullable=False)
    last_seen = db.Column(db.DateTime)
    disabled = db.Column(db.Boolean)

    _settings = None

    def __init__(self, *args, password=None, passhash=None, **kwargs):
        """
        Creates a new `User` object. The given password will be hashed automatically.

        The `passhash` parameter specifies an already hashed password, and can
        be used to speed up tests, as bcrypt is intentionally slow.
        """
        pwd = None
        if passhash: pwd = passhash
        elif password: pwd = bcrypt.generate_password_hash(password)

        super(User, self).__init__(*args, password=pwd, **kwargs)

    @property
    def settings(self):
        """
        Returns a `UserSettings` object which provides access to the user's settings.
        """
        if not self._settings:
            self._settings = UserSettings(self)
        return self._settings

    def has_role(self, role):
        """Returns True if the user's role is equal or greater than the given role.
        If the given role is None, returns True."""
        if role is None: return True
        return self.role >= role

    def avatar_url(self):
        """Returns the Gravatar URL for this user based on their email."""
        # See https://en.gravatar.com/site/implement/hash/
        # Trim whitespace, convert to lowercase, md5 hash
        email = self.email.encode("utf-8")
        gravatar_hash = hashlib.md5(email.strip().lower()).hexdigest()
        return "https://gravatar.com/avatar/{}?d=identicon".format(
            gravatar_hash)

    def set_password(self, password):
        """Sets a new password. The given password will be hashed."""
        self.password = bcrypt.generate_password_hash(password)

    def clear_password(self):
        """Clears this user's password, preventing them from logging in anymore.
        Also clears all of the user's sessions, logging them out."""
        self.clear_sessions()
        self.password = None

    def reset_2fa_secret(self):
        """Generates a new TOTP secret for this user."""
        self.clear_sessions()
        self.totp_secret = pyotp.random_base32()

    def clear_sessions(self):
        """Logs this user out by clearing all of their login sessions."""
        for sess in self.sessions:
            sess.disable()

    def gen_passwd_reset_token(self):
        """Creates a reset token for this user and returns the string token."""
        self.reset_token = ResetToken(kind=reset_type.password)
        token = self.reset_token.token
        return str(token)

    def gen_2fa_reset_token(self):
        """Creates a reset token for this user's 2-factor authentication secret."""
        self.reset_token = ResetToken(kind=reset_type.otp)
        token = self.reset_token.token
        return str(token)

    def totp_uri(self):
        return pyotp.TOTP(self.totp_secret).provisioning_uri(
            self.email, "MCArchive")

    def validate_otp(self, code):
        if self.totp_last_auth == code:
            # Reject repeated codes to prevent a replay attack.
            return False
        else:
            totp = pyotp.TOTP(self.totp_secret)
            if totp.verify(code, valid_window=1):
                self.totp_last_auth = code
                return True
            else:
                return False

    def disable(self):
        for sess in self.sessions:
            sess.disable()
        self.disabled = True

    def enable(self):
        self.disabled = False

    def __repr__(self):
        return '<User %r>' % self.name
Ejemplo n.º 13
0
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
Ejemplo n.º 14
0
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(80), unique=True, nullable=False)
    password = db.Column(db.Text, nullable=True)
    email = db.Column(db.String(120), unique=True, nullable=False)
    role = db.Column(db.Enum(UserRole), nullable=False)
    last_seen = db.Column(db.DateTime)
    disabled = db.Column(db.Boolean)

    def __init__(self, *args, password, passhash=None, **kwargs):
        """
        Creates a new `User` object. The given password will be hashed automatically.

        The `passhash` parameter specifies an already hashed password, and can
        be used to speed up tests, as bcrypt is intentionally slow.
        """
        pwd = None
        if passhash is not None: pwd = passhash
        else: pwd = bcrypt.generate_password_hash(password)
        super(User, self).__init__(*args, password=pwd, **kwargs)

    def has_role(self, role):
        """Returns True if the user's role is equal or greater than the given role.
        If the given role is None, returns True."""
        if role is None: return True
        return self.role >= role

    def avatar_url(self):
        """Returns the Gravatar URL for this user based on their email."""
        # See https://en.gravatar.com/site/implement/hash/
        # Trim whitespace, convert to lowercase, md5 hash
        email = self.email.encode("utf-8")
        gravatar_hash = hashlib.md5(email.strip().lower()).hexdigest()
        return "https://gravatar.com/avatar/{}?d=identicon".format(
            gravatar_hash)

    def set_password(self, password):
        """Sets a new password. The given password will be hashed."""
        self.password = bcrypt.generate_password_hash(password)

    def clear_password(self):
        """Clears this user's password, preventing them from logging in anymore.
        Also clears all of the user's sessions, logging them out."""
        for sess in self.sessions:
            sess.disable()
        self.password = None

    def gen_passwd_reset_token(self):
        """Creates a reset token for this user and returns the string token."""
        self.reset_token = ResetToken()
        token = self.reset_token.token
        return str(token)

    def disable(self):
        for sess in self.sessions:
            sess.disable()
        self.disabled = True

    def enable(self):
        self.disabled = False

    def __repr__(self):
        return '<User %r>' % self.name