182 lines
4.3 KiB
Python
182 lines
4.3 KiB
Python
# api_code/api/models.py
|
|
import enum
|
|
import hashlib
|
|
import os
|
|
import secrets
|
|
|
|
from sqlalchemy import (
|
|
Column,
|
|
DateTime,
|
|
Enum,
|
|
Float,
|
|
ForeignKey,
|
|
Integer,
|
|
Unicode,
|
|
)
|
|
from sqlalchemy.orm import relationship
|
|
|
|
from .database import Base
|
|
|
|
UNICODE_LEN = 128
|
|
SALT_LEN = 64
|
|
|
|
# Enums
|
|
|
|
|
|
class Classes(str, enum.Enum):
|
|
first = "first"
|
|
second = "second"
|
|
|
|
|
|
class Roles(str, enum.Enum):
|
|
admin = "admin"
|
|
passenger = "passenger"
|
|
|
|
|
|
# Models
|
|
|
|
|
|
class Station(Base):
|
|
__tablename__ = "station"
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
code = Column(
|
|
Unicode(UNICODE_LEN), nullable=False, unique=True
|
|
)
|
|
country = Column(Unicode(UNICODE_LEN), nullable=False)
|
|
city = Column(Unicode(UNICODE_LEN), nullable=False)
|
|
|
|
departures = relationship(
|
|
"Train",
|
|
foreign_keys="[Train.station_from_id]",
|
|
back_populates="station_from",
|
|
)
|
|
arrivals = relationship(
|
|
"Train",
|
|
foreign_keys="[Train.station_to_id]",
|
|
back_populates="station_to",
|
|
)
|
|
|
|
def __repr__(self):
|
|
return f"<{self.code}: id={self.id} city={self.city}>"
|
|
|
|
__str__ = __repr__
|
|
|
|
|
|
class Train(Base):
|
|
__tablename__ = "train"
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
name = Column(Unicode(UNICODE_LEN), nullable=False)
|
|
|
|
station_from_id = Column(
|
|
ForeignKey("station.id"), nullable=False
|
|
)
|
|
station_from = relationship(
|
|
"Station",
|
|
foreign_keys=[station_from_id],
|
|
back_populates="departures",
|
|
)
|
|
|
|
station_to_id = Column(
|
|
ForeignKey("station.id"), nullable=False
|
|
)
|
|
station_to = relationship(
|
|
"Station",
|
|
foreign_keys=[station_to_id],
|
|
back_populates="arrivals",
|
|
)
|
|
|
|
departs_at = Column(DateTime(timezone=True), nullable=False)
|
|
arrives_at = Column(DateTime(timezone=True), nullable=False)
|
|
|
|
first_class = Column(Integer, default=0, nullable=False)
|
|
second_class = Column(Integer, default=0, nullable=False)
|
|
seats_per_car = Column(Integer, default=0, nullable=False)
|
|
|
|
tickets = relationship("Ticket", back_populates="train")
|
|
|
|
def __repr__(self):
|
|
return f"<{self.name}: id={self.id}>"
|
|
|
|
__str__ = __repr__
|
|
|
|
|
|
class Ticket(Base):
|
|
__tablename__ = "ticket"
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
created_at = Column(DateTime(timezone=True), nullable=False)
|
|
user_id = Column(ForeignKey("user.id"), nullable=False)
|
|
user = relationship(
|
|
"User", foreign_keys=[user_id], back_populates="tickets"
|
|
)
|
|
|
|
train_id = Column(ForeignKey("train.id"), nullable=False)
|
|
train = relationship(
|
|
"Train", foreign_keys=[train_id], back_populates="tickets"
|
|
)
|
|
|
|
price = Column(Float, default=0, nullable=False)
|
|
car_class = Column(Enum(Classes), nullable=False)
|
|
|
|
def __repr__(self):
|
|
return (
|
|
f"<id={self.id} user={self.user} train={self.train}>"
|
|
)
|
|
|
|
__str__ = __repr__
|
|
|
|
|
|
class User(Base):
|
|
__tablename__ = "user"
|
|
|
|
pwd_separator = "#"
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
full_name = Column(Unicode(UNICODE_LEN), nullable=False)
|
|
email = Column(Unicode(256), nullable=False, unique=True)
|
|
password = Column(Unicode(256), nullable=False)
|
|
role = Column(Enum(Roles), nullable=False)
|
|
|
|
tickets = relationship("Ticket", back_populates="user")
|
|
|
|
def is_valid_password(self, password: str):
|
|
"""Tell if password matches the one stored in DB."""
|
|
salt, stored_hash = self.password.split(
|
|
self.pwd_separator
|
|
)
|
|
_, computed_hash = _hash(
|
|
password=password, salt=bytes.fromhex(salt)
|
|
)
|
|
return secrets.compare_digest(stored_hash, computed_hash)
|
|
|
|
@classmethod
|
|
def hash_password(cls, password: str, salt: bytes = None):
|
|
salt, hashed = _hash(password=password, salt=salt)
|
|
return f"{salt}{cls.pwd_separator}{hashed}"
|
|
|
|
def __repr__(self):
|
|
return (
|
|
f"<{self.full_name}: id={self.id} "
|
|
f"role={self.role.name}>"
|
|
)
|
|
|
|
__str__ = __repr__
|
|
|
|
|
|
def _hash(password: str, salt: bytes = None):
|
|
if salt is None:
|
|
salt = os.urandom(SALT_LEN)
|
|
iterations = 100 # should be at least 100k for SHA-256
|
|
|
|
hashed = hashlib.pbkdf2_hmac(
|
|
"sha256",
|
|
password.encode("utf-8"),
|
|
salt,
|
|
iterations,
|
|
dklen=128,
|
|
)
|
|
|
|
return salt.hex(), hashed.hex()
|