from datetime import date from app import db from flask_login import UserMixin from app.models.contents import Content from app.models.privs import SpecialPrivilege, Group, GroupMember, \ GroupPrivilege from config import V5Config import werkzeug.security import re import math import app # User: Website user that performs actions on the content class User(UserMixin, db.Model): __tablename__ = 'user' # User ID, should be used to refer to any user. Thea actual user can either # be a guest (with IP as key) or a member (with this ID as key). id = db.Column(db.Integer, primary_key=True) # User type (polymorphic discriminator) type = db.Column(db.String(30)) # TODO: add good relation contents = db.relationship('Content', back_populates="author") __mapper_args__ = { 'polymorphic_identity': __tablename__, 'polymorphic_on': type } def __repr__(self): return f'' @staticmethod def valid_name(name): """ Checks whether a string is a valid user name. The criteria are: 1. At least 3 characters and no longer than 32 characters 2. No whitespace-class character 3. No special chars 4. At least one letter 5. Not in forbidden usernames Possibily other intresting criteria: 6. Unicode restriction """ # Rule 1 if type(name) != str or len(name) < 3 or len(name) > 32: return False # Rule 2 # Reject all Unicode whitespaces. This is important to avoid the most # common Unicode tricks! if re.search(r'\s', name) is not None: return False # Rule 3 if re.search(V5Config.FORBIDDEN_CHARS_USERNAMES, name) is not None: return False # Rule 4 # There must be at least one letter (avoid complete garbage) if re.search(r'\w', name) is None: return False # Rule 5 if name in V5Config.FORBIDDEN_USERNAMES: return False return True # Guest: Unregistered user with minimal privileges class Guest(User, db.Model): __tablename__ = 'guest' __mapper_args__ = {'polymorphic_identity': __tablename__} # ID of the [User] entry id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) # Reusable username, can be the name of a member (will be distinguished at # rendering time) username = db.Column(db.Unicode(64), index=True) # IP address, 47 characters is the max for an IPv6 address ip = db.Column(db.String(47)) def __repr__(self): return f'' # Member: Registered user with full access to the website's services class Member(User, db.Model): __tablename__ = 'member' __mapper_args__ = {'polymorphic_identity': __tablename__} # Id of the [User] entry id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) # Primary attributes (needed for the system to work) name = db.Column(db.Unicode(32), index=True, unique=True) email = db.Column(db.Unicode(120), index=True, unique=True) password_hash = db.Column(db.String(255)) xp = db.Column(db.Integer) innovation = db.Column(db.Integer) register_date = db.Column(db.Date, default=date.today) # Avatars # TODO: rendre ça un peu plus propre @property def avatar(self): return 'avatars/' + str(self.id) + '.png' @property def level(self): xp = self.xp + 2 * self.innovation level = math.asinh(xp / 1000) * (100 / math.asinh(10)) return int(level), int(level * 100) % 100 # Groups and related privileges groups = db.relationship('Group', secondary=GroupMember, back_populates='members') # Personal information, all optional bio = db.Column(db.UnicodeText) signature = db.Column(db.UnicodeText) birthday = db.Column(db.Date) # Settings newsletter = db.Column(db.Boolean, default=False) # Relations # trophies = db.relationship('Trophy', back_populates='member') # tests = db.relationship('Test', back_populates='author') def __init__(self, name, email, password): """Register a new user.""" if not User.valid_name(name): raise Exception(f'{name} is not a valid user name') self.name = name self.email = email self.set_password(password) self.xp = 0 self.innovation = 0 self.bio = "" self.signature = "" self.birthday = None def delete(self): """ Deletes the user and the associated information: * Special privileges """ for sp in SpecialPrivilege.query.filter_by(mid=self.id).all(): db.session.delete(sp) db.session.commit() db.session.delete(self) db.session.commit() def priv(self, priv): """Check whether the member has the specified privilege.""" if SpecialPrivilege.query.filter_by(mid=self.id, priv=priv).first(): return True return False # return db.session.query(User, Group, GroupPrivilege).filter( # Group.id.in_(User.groups), GroupPrivilege.gid==Group.id, # GroupPrivilege.priv==priv).first() is not None def special_privileges(self): """List member's special privileges.""" sp = SpecialPrivilege.query.filter_by(mid=self.id).all() return sorted(row.priv for row in sp) def update(self, **data): """ Update all or part of the user's metadata. The [data] dictionary accepts the following keys: "name" str User name "email" str User mail ddress "password" str Raw password "bio" str Biograpy "signature" str Post signature "birthday" date Birthday date "newsletter" bool Newsletter setting For future compatibility, other attributes are silently ignored. None values can be specified and are ignored. It is the caller's responsibility to check that the request sender has the right to change user names, password... otherwise this method will turn out dangerous! """ data = {key: data[key] for key in data if data[key] is not None} if "name" in data: if not User.valid_name(data["name"]): raise Exception(f'{data["name"]} is not a valid user name') self.name = data["name"] # TODO: verify good type of those args, think about the password mgt if "email" in data: self.email = data["email"] if "password" in data: self.set_password(data["password"]) if "bio" in data: self.bio = data["bio"] if "signature" in data: self.signature = data["signature"] if "birthday" in data: self.birthday = data["birthday"] if "newsletter" in data: self.newsletter = data["newsletter"] # For admins only if "xp" in data: self.xp = data["xp"] if "innovation" in data: self.innovation = data["innovation"] def get_public_data(self): """Returns the public information of the member.""" return { "name": self.name, "xp": self.xp, "innovation": self.innovation, "register_date": self.register_date, "bio": self.bio, "signature": self.signature, "birthday": self.birthday, } def add_xp(self, amount): """ Reward xp to a member. If [amount] is negative, the xp total of the member will decrease, down to zero. """ self.xp_points = max(self.xp_points + amount, 0) def add_innovation(self, amount): """ Reward innovation points to a member. If [amount] is negative, the innovation points total will decrease, down to zero. """ self.innovation = max(self.innovation + amount, 0) def set_password(self, password): """ Set the user's password. Check whether the request sender has the right to do this! """ self.password_hash = werkzeug.security.generate_password_hash(password, method='pbkdf2:sha512', salt_length=10) def check_password(self, password): """Compares password against member hash.""" return werkzeug.security.check_password_hash(self.password_hash, password) def __repr__(self): return f'' @app.login.user_loader def load_user(id): return User.query.get(int(id))