Files
team-tryouts/app/models/user_model/user.py
T
GGThedandClaude Opus 5 2c37c05c8f fix(auth): rendre effectives l'expiration de session et la desactivation de compte
Deux protections etaient configurees sans avoir d'effet.

Expiration de session
  app.py:73 definit PERMANENT_SESSION_LIFETIME = 3600, mais Flask
  n'applique cette duree qu'aux sessions marquees permanentes. Aucune
  occurrence de session.permanent n'existait dans app/. Le cookie emis
  etait donc un cookie de session navigateur, sans expiration, et le
  serveur ne verifiait aucune anciennete. Ajout de session.permanent
  juste avant login_user, apres la rotation anti-fixation.

Desactivation de compte
  is_active_account n'etait consulte qu'au moment du login (auth.py:141).
  User n'ayant pas surcharge is_active, UserMixin renvoyait True en
  permanence. Desactiver un compte empechait donc la reconnexion mais
  laissait vivre la session en cours.

  La propriete is_active seule ne suffit pas : Flask-Login ne la consulte
  qu'a l'appel de login_user, jamais lors de la restauration d'une session
  depuis le cookie. Le verrou effectif est donc dans load_user, qui renvoie
  desormais None pour un compte desactive. La propriete est ajoutee malgre
  tout pour que login_user soit coherent avec le chargeur.

load_user passe au passage de Query.get() (API heritee, avertie en
SQLAlchemy 2.0) a db.session.get(), et tolere un identifiant non entier
sans lever.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-07 19:16:37 -04:00

105 lines
3.9 KiB
Python

"""Base User model — shared fields and polymorphic configuration."""
from app.extensions import db
from flask_login import UserMixin
from datetime import datetime
class User(UserMixin, db.Model):
"""Base user model — shared fields for every role.
Do not instantiate this class directly; use Admin, Manager, Coach, Player,
or Scout so that `polymorphic_identity` is set correctly.
"""
__tablename__ = 'users'
# --- columns -----------------------------------------------------------
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password_hash = db.Column(db.String(256), nullable=False)
role = db.Column(db.String(20), nullable=False, default='player') # polymorphic discriminator
full_name = db.Column(db.String(100), nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
phone = db.Column(db.String(20), nullable=True)
is_active_account = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
failed_login_attempts = db.Column(db.Integer, default=0)
locked_until = db.Column(db.DateTime, nullable=True)
# E-Sports fields
games = db.Column(db.Text, nullable=True) # comma-separated (only meaningful for Player)
discord_username = db.Column(db.String(128), nullable=True)
discord_user_id = db.Column(db.String(64), nullable=True)
league_os_profile = db.Column(db.String(256), nullable=True)
# --- polymorphic configuration -----------------------------------------
__mapper_args__ = {
'polymorphic_identity': 'user',
'polymorphic_on': role,
}
# --- relationships (defined once on the base) --------------------------
evaluations_given = db.relationship(
'Evaluation', foreign_keys='Evaluation.evaluator_id',
backref='evaluator', lazy='dynamic')
evaluations_received = db.relationship(
'Evaluation', foreign_keys='Evaluation.player_id',
backref='player', lazy='dynamic')
tryout_registrations = db.relationship(
'TryoutRegistration', backref='player', lazy='dynamic')
team_assignments = db.relationship(
'TeamMember', foreign_keys='TeamMember.player_id',
backref='player_ref', lazy='dynamic')
# --- Flask-Login integration -------------------------------------------
@property
def is_active(self):
"""Whether Flask-Login should accept this account.
UserMixin returns True unconditionally, which meant a deactivated
account kept any session it already held. Binding this to
is_active_account makes deactivation take effect on the next request.
"""
return bool(self.is_active_account)
# --- shared helper methods ---------------------------------------------
def get_games_list(self):
"""Return the user's games as a list."""
if self.games:
return [g.strip() for g in self.games.split(',') if g.strip()]
return []
def get_gamertags(self):
"""Return gamertags as a dict keyed by game."""
return {gt.game: {'gamertag': gt.gamertag, 'platform': gt.platform}
for gt in self.gamertags}
def get_org_teams(self):
"""Return all OrgTeams this player belongs to."""
return [tp.org_team for tp in self.team_placements]
# --- stubs (overridden in subclasses) ----------------------------------
def can_evaluate(self):
return False
def can_manage_users(self):
return False
def can_manage_teams(self):
return False
def can_manage_tryouts(self):
return False
def can_schedule_matches(self):
return False
def can_manage_this_tryout(self, tryout):
return False
def can_manage_this_org_team(self, org_team):
return False
def get_visible_tryouts(self):
return []