You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

75 lines
2.6 KiB
Python

import re
from typing import List
from argon2 import PasswordHasher
from flask_login import UserMixin
from sqlalchemy import ForeignKey
from sqlalchemy import String
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
# See https://en.wikipedia.org/wiki/North_American_Numbering_Plan
PHONE_REGEX = "^(?:(?:\+?1\s*(?:[.-]\s*)?)?(?:\(\s*([2-9]1[02-9]|[2-9][02-8]1|[2-9][02-8][02-9])\s*\)|([2-9]1[02-9]|[2-9][02-8]1|[2-9][02-8][02-9]))\s*(?:[.-]\s*)?)?([2-9]1[02-9]|[2-9][02-9]1|[2-9][02-9]{2})\s*(?:[.-]\s*)?([0-9]{4})(?:\s*(?:#|x\.?|ext\.?|extension)\s*(\d+))?$" # noqa: E501
def _is_valid_phone_number(phone_number):
return bool(re.match(PHONE_REGEX, phone_number))
# TODO make hashing values configurable
ph = PasswordHasher()
class Base(DeclarativeBase):
pass
class User(Base, UserMixin):
__tablename__ = "user"
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(String(30), unique=True)
primary_email: Mapped[str] = mapped_column(String(120), unique=True)
password_hash: Mapped[str] = mapped_column(String())
contacts: Mapped[List["Contact"]] = relationship(back_populates="user", cascade="all, delete-orphan")
def __repr__(self) -> str:
return f"User(id={self.id!r}, username={self.username!r})" # see format specifiers for !r
@property
def password(self):
raise AttributeError('password: write-only field')
@password.setter
def password(self, password):
self.password_hash = ph.hash(password)
def check_password(self, password):
return ph.verify(self.password_hash, password)
class Contact(Base):
__tablename__ = "contact"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
first_name: Mapped[str] = mapped_column(String(30))
last_name: Mapped[str] = mapped_column(String(30))
phone_contents: Mapped[str] = mapped_column(String(30))
email: Mapped[str] = mapped_column(String(120), unique=True)
user: Mapped["User"] = relationship(back_populates="contacts")
@property
def phone(self):
return self.phone_contents
@phone.setter
def phone(self, value):
if not _is_valid_phone_number(value):
raise ValueError(f"Invalid phone number {value}")
self.phone_contents = value
def __repr__(self) -> str:
return f"Contact(id={self.id!r}, first_name={self.first_name!r}, last_name={self.last_name!r}, phone={self.phone!r}, email={self.email!r})" # noqa: E501