Добавлена смена пароля и восстановление пароля по коду из почты
This commit is contained in:
parent
826f6a957a
commit
46a7f90e86
10 changed files with 214 additions and 69 deletions
|
@ -26,8 +26,8 @@ class IncorrectAuthDataException(BlackPhoenixException):
|
||||||
|
|
||||||
|
|
||||||
class IncorrectPasswordException(BlackPhoenixException):
|
class IncorrectPasswordException(BlackPhoenixException):
|
||||||
status_code = status.HTTP_401_UNAUTHORIZED
|
status_code = status.HTTP_409_CONFLICT
|
||||||
detail = "Введён не верные пароль"
|
detail = "Введён не верный пароль"
|
||||||
|
|
||||||
|
|
||||||
class IncorrectTokenFormatException(BlackPhoenixException):
|
class IncorrectTokenFormatException(BlackPhoenixException):
|
||||||
|
@ -59,11 +59,6 @@ class MessageNotFoundException(BlackPhoenixException):
|
||||||
detail = "Сообщение не найдено"
|
detail = "Сообщение не найдено"
|
||||||
|
|
||||||
|
|
||||||
class IncorrectPasswordException(BlackPhoenixException):
|
|
||||||
status_code = status.HTTP_409_CONFLICT
|
|
||||||
detail = "Введён не верный пароль"
|
|
||||||
|
|
||||||
|
|
||||||
class PasswordsМismatchException(BlackPhoenixException):
|
class PasswordsМismatchException(BlackPhoenixException):
|
||||||
status_code = status.HTTP_409_CONFLICT
|
status_code = status.HTTP_409_CONFLICT
|
||||||
detail = "Пароли не совпадают"
|
detail = "Пароли не совпадают"
|
||||||
|
|
|
@ -24,3 +24,46 @@ def create_registration_confirmation_template(
|
||||||
subtype="html"
|
subtype="html"
|
||||||
)
|
)
|
||||||
return email
|
return email
|
||||||
|
|
||||||
|
|
||||||
|
def create_password_change_confirmation_template(
|
||||||
|
username: str,
|
||||||
|
email_to: EmailStr,
|
||||||
|
):
|
||||||
|
email = EmailMessage()
|
||||||
|
|
||||||
|
email["Subject"] = "Смена пароля к аккаунту"
|
||||||
|
email["From"] = settings.SMTP_USER
|
||||||
|
email["To"] = email_to
|
||||||
|
|
||||||
|
email.set_content(
|
||||||
|
f"""
|
||||||
|
<h1>{username}, ты менял пароль?</h1>
|
||||||
|
<h2>Если нет, то пидора ответ</h2>
|
||||||
|
""",
|
||||||
|
subtype="html"
|
||||||
|
)
|
||||||
|
return email
|
||||||
|
|
||||||
|
|
||||||
|
def create_password_recover_template(
|
||||||
|
username: str,
|
||||||
|
email_to: EmailStr,
|
||||||
|
confirmation_code: str,
|
||||||
|
):
|
||||||
|
email = EmailMessage()
|
||||||
|
|
||||||
|
email["Subject"] = "Восстановление пароля"
|
||||||
|
email["From"] = settings.SMTP_USER
|
||||||
|
email["To"] = email_to
|
||||||
|
|
||||||
|
email.set_content(
|
||||||
|
f"""
|
||||||
|
<h1>{username}, ты тут хотел восстановить пароль?</h1>
|
||||||
|
<h2>{confirmation_code}</h2>
|
||||||
|
""",
|
||||||
|
subtype="html"
|
||||||
|
)
|
||||||
|
return email
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,8 @@ from pydantic import EmailStr
|
||||||
|
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from app.tasks.celery import celery
|
from app.tasks.celery import celery
|
||||||
from app.tasks.email_templates import create_registration_confirmation_template
|
from app.tasks.email_templates import create_registration_confirmation_template, \
|
||||||
|
create_password_change_confirmation_template, create_password_recover_template
|
||||||
|
|
||||||
|
|
||||||
def generate_confirmation_code(length=6):
|
def generate_confirmation_code(length=6):
|
||||||
|
@ -34,3 +35,36 @@ def send_registration_confirmation_email(
|
||||||
|
|
||||||
return confirmation_code
|
return confirmation_code
|
||||||
|
|
||||||
|
|
||||||
|
@celery.task
|
||||||
|
def send_password_change_email(
|
||||||
|
username: str,
|
||||||
|
email_to: EmailStr,
|
||||||
|
):
|
||||||
|
msg_content = create_password_change_confirmation_template(
|
||||||
|
username=username, email_to=email_to
|
||||||
|
)
|
||||||
|
|
||||||
|
with smtplib.SMTP_SSL(settings.SMTP_HOST, settings.SMTP_PORT) as server:
|
||||||
|
server.login(settings.SMTP_USER, settings.SMTP_PASS)
|
||||||
|
server.send_message(msg_content)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@celery.task
|
||||||
|
def send_password_recover_email(
|
||||||
|
username: str,
|
||||||
|
email_to: EmailStr,
|
||||||
|
):
|
||||||
|
confirmation_code = generate_confirmation_code()
|
||||||
|
|
||||||
|
msg_content = create_password_recover_template(
|
||||||
|
username=username, email_to=email_to, confirmation_code=confirmation_code
|
||||||
|
)
|
||||||
|
|
||||||
|
with smtplib.SMTP_SSL(settings.SMTP_HOST, settings.SMTP_PORT) as server:
|
||||||
|
server.login(settings.SMTP_USER, settings.SMTP_PASS)
|
||||||
|
server.send_message(msg_content)
|
||||||
|
|
||||||
|
return confirmation_code
|
||||||
|
|
|
@ -48,6 +48,12 @@ async def test_login_user(email_or_username: str, password: str, status_code: in
|
||||||
assert "black_phoenix_access_token" in response.cookies
|
assert "black_phoenix_access_token" in response.cookies
|
||||||
|
|
||||||
|
|
||||||
|
async def test_logout_user(ac: AsyncClient):
|
||||||
|
response = await ac.post("/users/logout")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "black_phoenix_access_token" not in response.cookies
|
||||||
|
|
||||||
|
|
||||||
async def test_get_user(ac: AsyncClient):
|
async def test_get_user(ac: AsyncClient):
|
||||||
await ac.post("/users/login", json={
|
await ac.post("/users/login", json={
|
||||||
"email_or_username": "urec@urec.com",
|
"email_or_username": "urec@urec.com",
|
||||||
|
@ -59,7 +65,40 @@ async def test_get_user(ac: AsyncClient):
|
||||||
assert response.json()["black_phoenix"] == False
|
assert response.json()["black_phoenix"] == False
|
||||||
|
|
||||||
|
|
||||||
async def test_logout_user(ac: AsyncClient):
|
@pytest.mark.parametrize("username,password,statuscode", [
|
||||||
response = await ac.post("/users/logout")
|
("urec", "12311231", 409),
|
||||||
assert response.status_code == 200
|
("neurec", "12311231", 200),
|
||||||
assert "black_phoenix_access_token" not in response.cookies
|
("urec", "adw", 409),
|
||||||
|
("urec", "12311231", 200),
|
||||||
|
])
|
||||||
|
async def test_rename_user(username: str, password: str, statuscode: int, ac: AsyncClient):
|
||||||
|
await ac.post("/users/login", json={
|
||||||
|
"email_or_username": "urec@urec.com",
|
||||||
|
"password": "12311231"
|
||||||
|
})
|
||||||
|
response = await ac.patch("/users/rename", json={
|
||||||
|
"username": username,
|
||||||
|
"password": password
|
||||||
|
})
|
||||||
|
assert response.status_code == statuscode
|
||||||
|
if response.status_code == 200:
|
||||||
|
assert response.json() == username
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("avatar_url,password,statuscode", [
|
||||||
|
("https://images.black-phoenix.ru/static/images/avatars/v6BtxTxfCFi2dBAl_avatar.png", "12311231", 200),
|
||||||
|
("https://images.black-phoenix.ru/static/images/%D1%82%D1%8B%20%D1%83%D0%B6%D0%B5%20%D0%BF%D0%B5%D1%88%D0%BA%D0%B0%20BP.png", "adw", 409),
|
||||||
|
("https://images.black-phoenix.ru/static/images/%D1%82%D1%8B%20%D1%83%D0%B6%D0%B5%20%D0%BF%D0%B5%D1%88%D0%BA%D0%B0%20BP.png", "12311231", 200),
|
||||||
|
])
|
||||||
|
async def test_change_avatar(avatar_url: str, password: str, statuscode: int, ac: AsyncClient):
|
||||||
|
await ac.post("/users/login", json={
|
||||||
|
"email_or_username": "urec@urec.com",
|
||||||
|
"password": "12311231"
|
||||||
|
})
|
||||||
|
response = await ac.patch("/users/change_avatar", json={
|
||||||
|
"new_avatar_image": avatar_url,
|
||||||
|
"password": password
|
||||||
|
})
|
||||||
|
assert response.status_code == statuscode
|
||||||
|
if response.status_code == 200:
|
||||||
|
assert response.json() == avatar_url
|
||||||
|
|
|
@ -99,6 +99,4 @@ async def validate_user_admin(user_id: int):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_user_codes_list(user_codes: list[dict], sort_description: str) -> list[str]:
|
|
||||||
user_codes_list = [user_code['code'] for user_code in user_codes if user_code['description'] == sort_description]
|
|
||||||
return user_codes_list
|
|
||||||
|
|
|
@ -1,13 +1,7 @@
|
||||||
import json
|
from fastapi import WebSocket, WebSocketDisconnect
|
||||||
from typing import Dict, List
|
|
||||||
|
|
||||||
from fastapi import WebSocket, Depends, WebSocketDisconnect
|
|
||||||
from pydantic import parse_obj_as
|
|
||||||
|
|
||||||
from app.users.chat.dao import ChatDAO
|
from app.users.chat.dao import ChatDAO
|
||||||
from app.users.auth import validate_user_access_to_chat
|
from app.users.auth import validate_user_access_to_chat
|
||||||
from app.users.chat.shemas import SMessageSchema, SMessage
|
|
||||||
from app.users.models import Users
|
|
||||||
from app.users.chat.router import router
|
from app.users.chat.router import router
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -70,8 +70,8 @@ class UserDAO(BaseDAO):
|
||||||
chats_with_avatars.c.chat_name,
|
chats_with_avatars.c.chat_name,
|
||||||
chats_with_avatars.c.avatar_image
|
chats_with_avatars.c.avatar_image
|
||||||
)
|
)
|
||||||
.select_from(chats_with_avatars)
|
.select_from(chats_with_avatars)
|
||||||
.where(
|
.where(
|
||||||
and_(
|
and_(
|
||||||
chats_with_avatars.c.id == user_id,
|
chats_with_avatars.c.id == user_id,
|
||||||
chats_with_avatars.c.visibility == True
|
chats_with_avatars.c.visibility == True
|
||||||
|
@ -93,9 +93,9 @@ class UserCodesDAO(BaseDAO):
|
||||||
model = UsersVerificationCodes
|
model = UsersVerificationCodes
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def set_user_codes(cls, user_id: int, code: str):
|
async def set_user_codes(cls, user_id: int, code: str, description: str):
|
||||||
query = (insert(UsersVerificationCodes)
|
query = (insert(UsersVerificationCodes)
|
||||||
.values(user_id=user_id, code=code, description="Код подтверждения почты")
|
.values(user_id=user_id, code=code, description=description)
|
||||||
.returning(cls.model.code))
|
.returning(cls.model.code))
|
||||||
async with async_session_maker() as session:
|
async with async_session_maker() as session:
|
||||||
result = await session.execute(query)
|
result = await session.execute(query)
|
||||||
|
@ -103,7 +103,7 @@ class UserCodesDAO(BaseDAO):
|
||||||
return result.scalar()
|
return result.scalar()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_user_codes(cls, user_id: int) -> list[dict[str, str | int | datetime]]:
|
async def get_user_codes(cls, **filter_by) -> list[dict | None]:
|
||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
usersverificationcodes.id,
|
usersverificationcodes.id,
|
||||||
|
@ -116,13 +116,12 @@ class UserCodesDAO(BaseDAO):
|
||||||
usersverificationcodes.user_id = 20
|
usersverificationcodes.user_id = 20
|
||||||
AND now() - usersverificationcodes.date_of_creation < INTERVAL '30 minutes'
|
AND now() - usersverificationcodes.date_of_creation < INTERVAL '30 minutes'
|
||||||
"""
|
"""
|
||||||
query = select(UsersVerificationCodes.__table__.columns).where(
|
query = (select(UsersVerificationCodes.__table__.columns)
|
||||||
and_(
|
.where((func.now() - UsersVerificationCodes.date_of_creation) < text("INTERVAL '30 minutes'"))
|
||||||
UsersVerificationCodes.user_id == user_id,
|
.filter_by(**filter_by))
|
||||||
(func.now() - UsersVerificationCodes.date_of_creation) < text("INTERVAL '30 minutes'")
|
|
||||||
))
|
|
||||||
async with async_session_maker() as session:
|
async with async_session_maker() as session:
|
||||||
# print(query.compile(engine, compile_kwargs={"literal_binds": True})) # Проверка SQL запроса
|
# print(query.compile(engine, compile_kwargs={"literal_binds": True})) # Проверка SQL запроса
|
||||||
result = await session.execute(query)
|
result = await session.execute(query)
|
||||||
result = result.mappings().all()
|
result = result.mappings().all()
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -17,7 +17,8 @@ class Users(Base):
|
||||||
role: Mapped[int] = mapped_column(server_default=f'0')
|
role: Mapped[int] = mapped_column(server_default=f'0')
|
||||||
black_phoenix: Mapped[bool] = mapped_column(server_default='false')
|
black_phoenix: Mapped[bool] = mapped_column(server_default='false')
|
||||||
avatar_image: Mapped[Optional[str]] = mapped_column(
|
avatar_image: Mapped[Optional[str]] = mapped_column(
|
||||||
server_default='https://images.black-phoenix.ru/static/images/%D1%82%D1%8B%20%D1%83%D0%B6%D0%B5%20%D0%BF%D0%B5%D1%88%D0%BA%D0%B0%20BP.png')
|
server_default='https://images.black-phoenix.ru/static/images/%D1%82%D1%8B%20%D1%83%D0%B6%D0%B5%20%D0%BF%D0%B5%D1%88%D0%BA%D0%B0%20BP.png'
|
||||||
|
)
|
||||||
date_of_birth: Mapped[date]
|
date_of_birth: Mapped[date]
|
||||||
date_of_registration: Mapped[date] = mapped_column(server_default=func.now())
|
date_of_registration: Mapped[date] = mapped_column(server_default=func.now())
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,20 @@
|
||||||
from typing import Annotated
|
from fastapi import APIRouter, Response, Depends
|
||||||
|
|
||||||
from fastapi import APIRouter, Response, Depends, UploadFile, File, Body, Form
|
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
|
from sqlalchemy.sql.functions import current_user
|
||||||
from starlette import status
|
from starlette import status
|
||||||
|
|
||||||
from app.config import settings
|
from app.exceptions import UsernameAlreadyInUseException, \
|
||||||
from app.exceptions import UserAlreadyExistsException, UsernameAlreadyInUseException, \
|
|
||||||
IncorrectPasswordException, PasswordsМismatchException, WrongCodeException, UserNotFoundException
|
IncorrectPasswordException, PasswordsМismatchException, WrongCodeException, UserNotFoundException
|
||||||
from app.images.router import upload_file, upload_file_returning_str
|
|
||||||
from app.users.auth import get_password_hash, authenticate_user_by_email, \
|
from app.users.auth import get_password_hash, authenticate_user_by_email, \
|
||||||
create_access_token, verify_password, REGISTRATED_USER, get_user_codes_list, VERIFICATED_USER, authenticate_user, \
|
create_access_token, verify_password, VERIFICATED_USER, authenticate_user, \
|
||||||
check_existing_user
|
check_existing_user
|
||||||
from app.users.dao import UserDAO, UserCodesDAO
|
from app.users.dao import UserDAO, UserCodesDAO
|
||||||
from app.users.dependencies import get_current_user
|
from app.users.dependencies import get_current_user
|
||||||
from app.users.models import Users
|
from app.users.models import Users
|
||||||
from app.users.schemas import SUserLogin, SUserRegister, SUser, SUserPassword, SUserRename, SUserAvatar
|
from app.users.schemas import SUserLogin, SUserRegister, SUser, SUserPassword, SUserRename, SUserAvatar, \
|
||||||
from app.tasks.tasks import send_registration_confirmation_email
|
SUserPasswordRecover, SUserCode, SUserPasswordChange
|
||||||
|
from app.tasks.tasks import send_registration_confirmation_email, send_password_change_email, \
|
||||||
|
send_password_recover_email
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
prefix="/users",
|
prefix="/users",
|
||||||
|
@ -48,21 +47,23 @@ async def register_user(response: Response, user_data: SUserRegister):
|
||||||
result = send_registration_confirmation_email.delay(username=user_data.username, email_to=user_data.email)
|
result = send_registration_confirmation_email.delay(username=user_data.username, email_to=user_data.email)
|
||||||
result = result.get()
|
result = result.get()
|
||||||
|
|
||||||
if await UserCodesDAO.set_user_codes(user_id=user_id, code=result) == result:
|
if await UserCodesDAO.set_user_codes(
|
||||||
|
user_id=user_id, code=result, description="Код подтверждения почты"
|
||||||
|
) == result:
|
||||||
user = await authenticate_user_by_email(user_data.email, user_data.password)
|
user = await authenticate_user_by_email(user_data.email, user_data.password)
|
||||||
access_token = create_access_token({"sub": str(user.id)})
|
access_token = create_access_token({"sub": str(user.id)})
|
||||||
response.set_cookie(key="black_phoenix_access_token", value=access_token, httponly=True)
|
response.set_cookie(key="black_phoenix_access_token", value=access_token, httponly=True)
|
||||||
return {"access_token": access_token}
|
return {"access_token": access_token}
|
||||||
|
|
||||||
|
|
||||||
@router.post('/email_verification', response_model=bool)
|
@router.post('/email_verification', status_code=status.HTTP_200_OK, response_model=bool)
|
||||||
async def email_verification(user_code: str, user: Users = Depends(get_current_user)):
|
async def email_verification(user_code: SUserCode, user: Users = Depends(get_current_user)):
|
||||||
user_codes = await UserCodesDAO.get_user_codes(user_id=user.id)
|
user_codes = await UserCodesDAO.get_user_codes(
|
||||||
user_codes = get_user_codes_list(user_codes=user_codes, sort_description="Код подтверждения почты")
|
user_id=user.id, description="Код подтверждения почты", code=user_code.user_code
|
||||||
if user_code in user_codes:
|
)
|
||||||
if await UserDAO.change_data(user_id=user.id, role=VERIFICATED_USER):
|
if not user_codes or not await UserDAO.change_data(user_id=user.id, role=VERIFICATED_USER):
|
||||||
return True
|
raise WrongCodeException
|
||||||
raise WrongCodeException
|
return True
|
||||||
|
|
||||||
|
|
||||||
@router.post("/login", response_model=dict[str, str])
|
@router.post("/login", response_model=dict[str, str])
|
||||||
|
@ -95,17 +96,12 @@ async def rename_user(new_username: SUserRename, current_user: Users = Depends(g
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/change_avatar", response_model=str)
|
@router.patch("/change_avatar", response_model=str)
|
||||||
async def change_avatar(
|
async def change_avatar(user_data: SUserAvatar, current_user: Users = Depends(get_current_user)):
|
||||||
new_avatar: Annotated[UploadFile, File()],
|
if not verify_password(user_data.password, current_user.hashed_password):
|
||||||
password: Annotated[SUserAvatar, Body()],
|
|
||||||
current_user: Users = Depends(get_current_user)
|
|
||||||
):
|
|
||||||
if not verify_password(password.password, current_user.hashed_password):
|
|
||||||
raise IncorrectPasswordException
|
raise IncorrectPasswordException
|
||||||
new_avatar_url = await upload_file_returning_str(new_avatar, "upload_avatar")
|
if await UserDAO.change_data(current_user.id, avatar_image=user_data.new_avatar_image):
|
||||||
if await UserDAO.change_data(current_user.id, avatar_image=new_avatar_url):
|
return user_data.new_avatar_image
|
||||||
return new_avatar_url
|
raise UserNotFoundException
|
||||||
raise UserNotFoundException # Надо подумать
|
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/change_password", status_code=status.HTTP_200_OK)
|
@router.patch("/change_password", status_code=status.HTTP_200_OK)
|
||||||
|
@ -117,3 +113,41 @@ async def change_password(new_password: SUserPassword, current_user: Users = Dep
|
||||||
raise PasswordsМismatchException
|
raise PasswordsМismatchException
|
||||||
hashed_password = get_password_hash(new_password.new_password)
|
hashed_password = get_password_hash(new_password.new_password)
|
||||||
await UserDAO.change_data(current_user.id, hashed_password=hashed_password)
|
await UserDAO.change_data(current_user.id, hashed_password=hashed_password)
|
||||||
|
send_password_change_email.delay(current_user.username, current_user.email)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/send_recovery_email", status_code=status.HTTP_200_OK)
|
||||||
|
async def send_recovery_email(email: SUserPasswordRecover):
|
||||||
|
existing_user = await UserDAO.find_one_or_none(email=email.email)
|
||||||
|
if not existing_user:
|
||||||
|
raise UserNotFoundException
|
||||||
|
result = send_password_recover_email.delay(existing_user.username, existing_user.email)
|
||||||
|
result = result.get()
|
||||||
|
|
||||||
|
if await UserCodesDAO.set_user_codes(
|
||||||
|
user_id=existing_user.user_id, code=result, description="Код восстановления пароля"
|
||||||
|
) == result:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/confirm_password_recovery", status_code=status.HTTP_200_OK, response_model=int)
|
||||||
|
async def confirm_password_recovery(user_code: SUserCode):
|
||||||
|
user_codes = await UserCodesDAO.get_user_codes(
|
||||||
|
description="Код восстановления пароля", code=user_code.user_code
|
||||||
|
)
|
||||||
|
if not user_codes:
|
||||||
|
raise WrongCodeException
|
||||||
|
return user_codes[0]['user_id']
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/password_recovery", status_code=status.HTTP_200_OK, response_model=str)
|
||||||
|
async def password_recovery(passwords: SUserPasswordChange):
|
||||||
|
if passwords.password1 != passwords.password2:
|
||||||
|
raise PasswordsМismatchException
|
||||||
|
hashed_password = get_password_hash(passwords.password1)
|
||||||
|
username = await UserDAO.change_data(passwords.user_id, hashed_password=hashed_password)
|
||||||
|
user = await UserDAO.find_one_or_none(username=username, id=passwords.user_id)
|
||||||
|
if not user:
|
||||||
|
raise UserNotFoundException
|
||||||
|
send_password_change_email.delay(user.username, user.email)
|
||||||
|
return username
|
||||||
|
|
|
@ -49,16 +49,24 @@ class SUserRename(BaseModel):
|
||||||
|
|
||||||
class SUserAvatar(BaseModel):
|
class SUserAvatar(BaseModel):
|
||||||
password: str
|
password: str
|
||||||
|
new_avatar_image: str
|
||||||
@model_validator(mode="before")
|
|
||||||
@classmethod
|
|
||||||
def auto_loads_json_string(cls, data):
|
|
||||||
if isinstance(data, str) and data.startswith("{"):
|
|
||||||
data = json.loads(data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class SUserPassword(BaseModel):
|
class SUserPassword(BaseModel):
|
||||||
password: str = Query(None, min_length=8)
|
password: str = Query(None, min_length=8)
|
||||||
new_password: str = Query(None, min_length=8)
|
new_password: str = Query(None, min_length=8)
|
||||||
new_password2: str = Query(None, min_length=8)
|
new_password2: str = Query(None, min_length=8)
|
||||||
|
|
||||||
|
|
||||||
|
class SUserPasswordRecover(BaseModel):
|
||||||
|
email: EmailStr
|
||||||
|
|
||||||
|
|
||||||
|
class SUserCode(BaseModel):
|
||||||
|
user_code: str
|
||||||
|
|
||||||
|
|
||||||
|
class SUserPasswordChange(BaseModel):
|
||||||
|
user_id: int
|
||||||
|
password1: str
|
||||||
|
password2: str
|
||||||
|
|
Loading…
Add table
Reference in a new issue