Initial commit
This commit is contained in:
1
src/presentation/schemas/__init__.py
Normal file
1
src/presentation/schemas/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from src.presentation.schemas.user import RegistrationStart, RegistrationComplete, UserLogin, LoginStart
|
||||
89
src/presentation/schemas/user.py
Normal file
89
src/presentation/schemas/user.py
Normal file
@@ -0,0 +1,89 @@
|
||||
from __future__ import annotations
|
||||
import re
|
||||
from typing import ClassVar
|
||||
from pydantic import BaseModel, EmailStr, Field, ValidationError, field_validator, model_validator
|
||||
|
||||
|
||||
|
||||
class EmailNoSubaddressing(BaseModel):
|
||||
email: EmailStr = Field(title='Email', description='Email without subaddressing')
|
||||
|
||||
@field_validator('email')
|
||||
@classmethod
|
||||
def validate_and_normalize_email(cls, v: EmailStr) -> str:
|
||||
email = str(v).strip().lower()
|
||||
local, _, domain = email.partition('@')
|
||||
if not local or not domain:
|
||||
raise ValueError('Invalid email')
|
||||
if '+' in local:
|
||||
raise ValueError('Email subaddressing is not allowed')
|
||||
if any(ord(ch) > 127 for ch in local):
|
||||
raise ValueError('Email must be ASCII')
|
||||
if local.startswith('.') or local.endswith('.') or '..' in local:
|
||||
raise ValueError('Invalid email local part')
|
||||
if not re.fullmatch(r'[A-Za-z0-9._-]+', local):
|
||||
raise ValueError('Email contains запрещенные символы')
|
||||
|
||||
return email
|
||||
|
||||
|
||||
class RegistrationStart(EmailNoSubaddressing):
|
||||
pass
|
||||
|
||||
class LoginStart(EmailNoSubaddressing):
|
||||
pass
|
||||
|
||||
|
||||
class RegistrationComplete(EmailNoSubaddressing):
|
||||
password: str = Field(min_length=12)
|
||||
confirm_password: str = Field(min_length=12)
|
||||
code: str = Field(
|
||||
min_length=6,
|
||||
max_length=6,
|
||||
pattern=r"^\d{6}$",
|
||||
)
|
||||
|
||||
_allowed_specials: ClassVar[str] = '!@#$%^&*()_+-=.,:;?/[]{}<>'
|
||||
|
||||
@field_validator('password')
|
||||
@classmethod
|
||||
def validate_password_policy(cls, v: str) -> str:
|
||||
if len(v) < 12:
|
||||
raise ValueError('Password must be at least 12 characters long')
|
||||
if not any(c.islower() for c in v):
|
||||
raise ValueError('Password must contain at least one lowercase letter')
|
||||
if not any(c.isupper() for c in v):
|
||||
raise ValueError('Password must contain at least one uppercase letter')
|
||||
if not any(c.isdigit() for c in v):
|
||||
raise ValueError('Password must contain at least one digit')
|
||||
if not any(c in cls._allowed_specials for c in v):
|
||||
raise ValueError(
|
||||
'Password must contain at least one special character '
|
||||
f'from: {cls._allowed_specials}'
|
||||
)
|
||||
if any(c.isspace() for c in v):
|
||||
raise ValueError('Password must not contain whitespace')
|
||||
return v
|
||||
|
||||
@model_validator(mode='after')
|
||||
def validate_password_confirmation(self) -> 'RegistrationComplete':
|
||||
if self.password != self.confirm_password:
|
||||
raise ValidationError.from_exception_data(
|
||||
title='Passwords do not match',
|
||||
line_errors=[{
|
||||
'type': 'value_error',
|
||||
'loc': ('confirm_password',),
|
||||
'msg': 'Passwords do not match',
|
||||
'input': self.confirm_password,
|
||||
}],
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
class UserLogin(EmailNoSubaddressing):
|
||||
password: str = Field(min_length=12)
|
||||
code: str = Field(
|
||||
min_length=6,
|
||||
max_length=6,
|
||||
pattern=r"^\d{6}$",
|
||||
)
|
||||
Reference in New Issue
Block a user