fix: update
This commit is contained in:
@@ -1,10 +0,0 @@
|
||||
from src.presentation.schemas.avatar import SetAvatarRequest
|
||||
from src.presentation.schemas.phone import SetPhoneRequest
|
||||
from src.presentation.schemas.bank import BankUpdateRequest, BankConfirmRequest
|
||||
from src.presentation.schemas.encrypted_mnemonic import EncryptedMnemonicConfirmRequest
|
||||
from src.presentation.schemas.password import (
|
||||
ChangePasswordConfirmRequest,
|
||||
ForgotPasswordStartRequest,
|
||||
ForgotPasswordCompleteRequest,
|
||||
)
|
||||
from src.presentation.schemas.email import ChangeEmailConfirmOldRequest, ChangeEmailCompleteRequest
|
||||
@@ -1,19 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ApiErrorPayload(BaseModel):
|
||||
detail: str = Field(description='Текстовое описание ошибки для клиента')
|
||||
|
||||
|
||||
class ValidationErrorDetailItem(BaseModel):
|
||||
loc: list[str | int] = Field(description='Путь к полю, вызвавшему ошибку')
|
||||
msg: str = Field(description='Сообщение')
|
||||
type: str = Field(description='Тип ошибки валидации')
|
||||
|
||||
|
||||
class ApiValidationErrorsPayload(BaseModel):
|
||||
detail: list[ValidationErrorDetailItem] = Field(
|
||||
description='Список ошибок валидации тела или параметров запроса'
|
||||
)
|
||||
@@ -1,69 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
|
||||
AVATAR_MAX_BYTES = 10 * 1024 * 1024
|
||||
|
||||
_IMAGE_SIGNATURES = (
|
||||
b'\xff\xd8\xff',
|
||||
b'\x89PNG\r\n\x1a\n',
|
||||
b'GIF87a',
|
||||
b'GIF89a',
|
||||
)
|
||||
|
||||
|
||||
def _avatar_payload_to_bytes(photo_base64: str) -> bytes:
|
||||
s = photo_base64.strip()
|
||||
if not s:
|
||||
raise ValueError('photo_base64 must not be empty')
|
||||
if s.startswith('data:'):
|
||||
parts = s.split(',', 1)
|
||||
if len(parts) != 2:
|
||||
raise ValueError('Invalid data URL')
|
||||
s = parts[1].strip()
|
||||
try:
|
||||
data = base64.b64decode(s, validate=True)
|
||||
except binascii.Error as exc:
|
||||
raise ValueError('Invalid base64') from exc
|
||||
if len(data) > AVATAR_MAX_BYTES:
|
||||
raise ValueError(f'Photo must not exceed {AVATAR_MAX_BYTES} bytes')
|
||||
if len(data) < 12:
|
||||
raise ValueError('Photo data is too small')
|
||||
ok = False
|
||||
for sig in _IMAGE_SIGNATURES:
|
||||
if data.startswith(sig):
|
||||
ok = True
|
||||
break
|
||||
if not ok and data.startswith(b'RIFF') and len(data) > 12 and data[8:12] == b'WEBP':
|
||||
ok = True
|
||||
if not ok:
|
||||
raise ValueError('Photo must be JPEG, PNG, GIF or WebP')
|
||||
return data
|
||||
|
||||
|
||||
class SetAvatarRequest(BaseModel):
|
||||
photo_base64: str = Field(
|
||||
...,
|
||||
description='Изображение JPEG, PNG, GIF или WebP в Base64; допустим data URL (data:image/...;base64,...). Максимум 10 МБ после декодирования.',
|
||||
)
|
||||
decoded_bytes: bytes = Field(exclude=True)
|
||||
|
||||
@model_validator(mode='before')
|
||||
@classmethod
|
||||
def _decode_input(cls, data: Any):
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
raw = data.get('photo_base64')
|
||||
if raw is None:
|
||||
return data
|
||||
if not isinstance(raw, str):
|
||||
raise ValueError('photo_base64 must be a string')
|
||||
stripped = raw.strip()
|
||||
decoded = _avatar_payload_to_bytes(stripped)
|
||||
return {'photo_base64': stripped, 'decoded_bytes': decoded}
|
||||
@@ -1,116 +0,0 @@
|
||||
import re
|
||||
from typing import Self
|
||||
from pydantic import BaseModel, field_validator, model_validator
|
||||
|
||||
|
||||
class BankUpdateRequest(BaseModel):
|
||||
passport_data: str | None = None
|
||||
inn: str | None = None
|
||||
erc20: str | None = None
|
||||
|
||||
@model_validator(mode='after')
|
||||
def at_least_one(self) -> Self:
|
||||
if not any([self.passport_data, self.inn, self.erc20]):
|
||||
raise ValueError('At least one field is required')
|
||||
return self
|
||||
|
||||
@field_validator('passport_data', 'inn', 'erc20')
|
||||
@classmethod
|
||||
def strip_optional(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return None
|
||||
s = v.strip()
|
||||
return s or None
|
||||
|
||||
@field_validator('inn')
|
||||
@classmethod
|
||||
def validate_inn(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return None
|
||||
v = v.strip()
|
||||
if not re.match(r'^\d{10}(\d{2})?$', v):
|
||||
raise ValueError('INN must be 10 or 12 digits')
|
||||
if len(v) > 12:
|
||||
raise ValueError('INN is too long')
|
||||
return v
|
||||
|
||||
@field_validator('erc20')
|
||||
@classmethod
|
||||
def validate_erc20(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return None
|
||||
v = v.strip()
|
||||
if not re.match(r'^0x[a-fA-F0-9]{40}$', v):
|
||||
raise ValueError('ERC20 address must be 0x followed by 40 hex characters')
|
||||
return v
|
||||
|
||||
@field_validator('passport_data')
|
||||
@classmethod
|
||||
def validate_passport_data(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return None
|
||||
v = v.strip()
|
||||
if len(v) > 255:
|
||||
raise ValueError('Passport data is too long')
|
||||
return v
|
||||
|
||||
|
||||
class BankConfirmRequest(BaseModel):
|
||||
code: str
|
||||
passport_data: str | None = None
|
||||
inn: str | None = None
|
||||
erc20: str | None = None
|
||||
|
||||
@model_validator(mode='after')
|
||||
def at_least_one_field(self) -> Self:
|
||||
if not any([self.passport_data, self.inn, self.erc20]):
|
||||
raise ValueError('At least one field is required')
|
||||
return self
|
||||
|
||||
@field_validator('code')
|
||||
@classmethod
|
||||
def validate_code(cls, v: str) -> str:
|
||||
v = v.strip()
|
||||
if not re.match(r'^\d{6}$', v):
|
||||
raise ValueError('Code must be exactly 6 digits')
|
||||
return v
|
||||
|
||||
@field_validator('passport_data', 'inn', 'erc20')
|
||||
@classmethod
|
||||
def strip_optional(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return None
|
||||
s = v.strip()
|
||||
return s or None
|
||||
|
||||
@field_validator('inn')
|
||||
@classmethod
|
||||
def validate_inn(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return None
|
||||
v = v.strip()
|
||||
if not re.match(r'^\d{10}(\d{2})?$', v):
|
||||
raise ValueError('INN must be 10 or 12 digits')
|
||||
if len(v) > 12:
|
||||
raise ValueError('INN is too long')
|
||||
return v
|
||||
|
||||
@field_validator('erc20')
|
||||
@classmethod
|
||||
def validate_erc20(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return None
|
||||
v = v.strip()
|
||||
if not re.match(r'^0x[a-fA-F0-9]{40}$', v):
|
||||
raise ValueError('ERC20 address must be 0x followed by 40 hex characters')
|
||||
return v
|
||||
|
||||
@field_validator('passport_data')
|
||||
@classmethod
|
||||
def validate_passport_data(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return None
|
||||
v = v.strip()
|
||||
if len(v) > 255:
|
||||
raise ValueError('Passport data is too long')
|
||||
return v
|
||||
@@ -1,35 +0,0 @@
|
||||
import re
|
||||
from pydantic import BaseModel, field_validator
|
||||
|
||||
|
||||
class ChangeEmailConfirmOldRequest(BaseModel):
|
||||
code: str
|
||||
new_email: str
|
||||
|
||||
@field_validator('code')
|
||||
@classmethod
|
||||
def validate_code(cls, v: str) -> str:
|
||||
v = v.strip()
|
||||
if not re.match(r'^\d{6}$', v):
|
||||
raise ValueError('Code must be exactly 6 digits')
|
||||
return v
|
||||
|
||||
@field_validator('new_email')
|
||||
@classmethod
|
||||
def validate_new_email(cls, v: str) -> str:
|
||||
v = v.strip().lower()
|
||||
if not re.match(r'^[^@\s]+@[^@\s]+\.[^@\s]+$', v):
|
||||
raise ValueError('Invalid email address')
|
||||
return v
|
||||
|
||||
|
||||
class ChangeEmailCompleteRequest(BaseModel):
|
||||
code: str
|
||||
|
||||
@field_validator('code')
|
||||
@classmethod
|
||||
def validate_code(cls, v: str) -> str:
|
||||
v = v.strip()
|
||||
if not re.match(r'^\d{6}$', v):
|
||||
raise ValueError('Code must be exactly 6 digits')
|
||||
return v
|
||||
@@ -1,23 +0,0 @@
|
||||
import re
|
||||
from pydantic import BaseModel, field_validator
|
||||
|
||||
|
||||
class EncryptedMnemonicConfirmRequest(BaseModel):
|
||||
code: str
|
||||
encrypted_mnemonic: str
|
||||
|
||||
@field_validator('code')
|
||||
@classmethod
|
||||
def validate_code(cls, v: str) -> str:
|
||||
v = v.strip()
|
||||
if not re.match(r'^\d{6}$', v):
|
||||
raise ValueError('Code must be exactly 6 digits')
|
||||
return v
|
||||
|
||||
@field_validator('encrypted_mnemonic')
|
||||
@classmethod
|
||||
def validate_encrypted_mnemonic(cls, v: str) -> str:
|
||||
v = v.strip()
|
||||
if not v:
|
||||
raise ValueError('encrypted_mnemonic must not be empty')
|
||||
return v
|
||||
@@ -1,59 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from src.application.domain.entities import UserEntity
|
||||
|
||||
|
||||
class MeUserPublicResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=False)
|
||||
|
||||
id: str | None = Field(None, description='Идентификатор пользователя')
|
||||
email: str | None = Field(None, description='Email')
|
||||
first_name: str | None = Field(None, description='Имя')
|
||||
middle_name: str | None = Field(None, description='Отчество')
|
||||
last_name: str | None = Field(None, description='Фамилия')
|
||||
birth_date: date | None = Field(None, description='Дата рождения')
|
||||
encrypted_mnemonic: str | None = Field(None, description='Шифрованная мнемоника')
|
||||
phone: str | None = Field(None, description='Телефон')
|
||||
passport_data: str | None = Field(None, description='Паспортные данные')
|
||||
inn: str | None = Field(None, description='ИНН')
|
||||
erc20: str | None = Field(None, description='ERC-20 адрес')
|
||||
avatar_link: str | None = Field(None, description='HTTPS-ссылка на текущий аватар в хранилище')
|
||||
kyc_verified: bool | None = Field(None, description='Признак пройденного KYC')
|
||||
is_deleted: bool | None = Field(None, description='Удалён ли аккаунт')
|
||||
created_at: datetime | None = Field(None, description='Время создания записи')
|
||||
updated_at: datetime | None = Field(None, description='Время последнего обновления')
|
||||
kyc_verified_at: datetime | None = Field(None, description='Время подтверждения KYC')
|
||||
|
||||
@classmethod
|
||||
def from_user(cls, user: UserEntity) -> MeUserPublicResponse:
|
||||
return cls(
|
||||
id=user.id,
|
||||
email=user.email,
|
||||
first_name=user.first_name,
|
||||
middle_name=user.middle_name,
|
||||
last_name=user.last_name,
|
||||
birth_date=user.birth_date,
|
||||
encrypted_mnemonic=user.encrypted_mnemonic,
|
||||
phone=user.phone,
|
||||
passport_data=user.passport_data,
|
||||
inn=user.inn,
|
||||
erc20=user.erc20,
|
||||
avatar_link=user.avatar_link,
|
||||
kyc_verified=user.kyc_verified,
|
||||
is_deleted=user.is_deleted,
|
||||
created_at=user.created_at,
|
||||
updated_at=user.updated_at,
|
||||
kyc_verified_at=user.kyc_verified_at,
|
||||
)
|
||||
|
||||
|
||||
class SetAvatarPublicResponse(MeUserPublicResponse):
|
||||
webp_size_bytes: int = Field(
|
||||
...,
|
||||
ge=0,
|
||||
description='Размер сохранённого файла аватара в формате WebP, байты',
|
||||
)
|
||||
@@ -1,75 +0,0 @@
|
||||
import re
|
||||
from typing import Self
|
||||
from pydantic import BaseModel, field_validator, model_validator
|
||||
from src.application.domain.password_policy import validate_password_strength
|
||||
|
||||
|
||||
class ForgotPasswordStartRequest(BaseModel):
|
||||
email: str
|
||||
|
||||
@field_validator('email')
|
||||
@classmethod
|
||||
def validate_email(cls, v: str) -> str:
|
||||
v = v.strip().lower()
|
||||
if not re.match(r'^[^@\s]+@[^@\s]+\.[^@\s]+$', v):
|
||||
raise ValueError('Invalid email address')
|
||||
return v
|
||||
|
||||
|
||||
class ForgotPasswordCompleteRequest(BaseModel):
|
||||
email: str
|
||||
code: str
|
||||
new_password: str
|
||||
confirm_password: str
|
||||
|
||||
@field_validator('email')
|
||||
@classmethod
|
||||
def validate_email(cls, v: str) -> str:
|
||||
v = v.strip().lower()
|
||||
if not re.match(r'^[^@\s]+@[^@\s]+\.[^@\s]+$', v):
|
||||
raise ValueError('Invalid email address')
|
||||
return v
|
||||
|
||||
@model_validator(mode='after')
|
||||
def passwords_match(self) -> Self:
|
||||
if self.new_password != self.confirm_password:
|
||||
raise ValueError('Passwords do not match')
|
||||
return self
|
||||
|
||||
@field_validator('code')
|
||||
@classmethod
|
||||
def validate_code(cls, v: str) -> str:
|
||||
v = v.strip()
|
||||
if not re.match(r'^\d{6}$', v):
|
||||
raise ValueError('Code must be exactly 6 digits')
|
||||
return v
|
||||
|
||||
@field_validator('new_password')
|
||||
@classmethod
|
||||
def validate_new_password(cls, v: str) -> str:
|
||||
return validate_password_strength(v)
|
||||
|
||||
|
||||
class ChangePasswordConfirmRequest(BaseModel):
|
||||
code: str
|
||||
new_password: str
|
||||
confirm_password: str
|
||||
|
||||
@model_validator(mode='after')
|
||||
def passwords_match(self) -> Self:
|
||||
if self.new_password != self.confirm_password:
|
||||
raise ValueError('Passwords do not match')
|
||||
return self
|
||||
|
||||
@field_validator('code')
|
||||
@classmethod
|
||||
def validate_code(cls, v: str) -> str:
|
||||
v = v.strip()
|
||||
if not re.match(r'^\d{6}$', v):
|
||||
raise ValueError('Code must be exactly 6 digits')
|
||||
return v
|
||||
|
||||
@field_validator('new_password')
|
||||
@classmethod
|
||||
def validate_new_password(cls, v: str) -> str:
|
||||
return validate_password_strength(v)
|
||||
@@ -1,16 +0,0 @@
|
||||
import re
|
||||
from pydantic import BaseModel, field_validator
|
||||
|
||||
|
||||
class SetPhoneRequest(BaseModel):
|
||||
phone: str
|
||||
|
||||
@field_validator('phone')
|
||||
@classmethod
|
||||
def validate_russian_phone(cls, v: str) -> str:
|
||||
cleaned = re.sub(r'[\s\-\(\)]', '', v)
|
||||
pattern = r'^(\+7|8)\d{10}$'
|
||||
if not re.match(pattern, cleaned):
|
||||
raise ValueError('Invalid Russian phone number')
|
||||
normalized = '+7' + cleaned[-10:]
|
||||
return normalized
|
||||
Reference in New Issue
Block a user