mirror of
https://github.com/fastapi-practices/fastapi_best_architecture.git
synced 2025-08-20 00:03:31 +08:00
324 lines
16 KiB
Python
324 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
import os
|
|
from hashlib import sha256
|
|
|
|
from email_validator import validate_email, EmailNotValidError
|
|
from fast_captcha import text_captcha
|
|
from fastapi import Request, HTTPException, Response, UploadFile
|
|
from fastapi.security import OAuth2PasswordRequestForm
|
|
from fastapi_pagination.ext.async_sqlalchemy import paginate
|
|
|
|
from backend.app.api import jwt
|
|
from backend.app.common.exception import errors
|
|
from backend.app.common.log import log
|
|
from backend.app.common.redis import redis_client
|
|
from backend.app.common.response.response_code import CodeEnum
|
|
from backend.app.core.conf import settings
|
|
from backend.app.core.path_conf import AvatarPath
|
|
from backend.app.crud.crud_user import UserDao
|
|
from backend.app.database.db_mysql import async_db_session
|
|
from backend.app.models import User
|
|
from backend.app.schemas.user import CreateUser, ResetPassword, UpdateUser, ELCode, Auth2
|
|
from backend.app.utils import re_verify
|
|
from backend.app.utils.format_string import cut_path
|
|
from backend.app.utils.generate_string import get_current_timestamp, get_uuid
|
|
from backend.app.utils.send_email import send_verification_code_email, SEND_EMAIL_LOGIN_TEXT
|
|
|
|
|
|
class UserService:
|
|
|
|
@staticmethod
|
|
async def login(form_data: OAuth2PasswordRequestForm):
|
|
async with async_db_session() as db:
|
|
current_user = await UserDao.get_user_by_username(db, form_data.username)
|
|
if not current_user:
|
|
raise errors.NotFoundError(msg='用户名不存在')
|
|
elif not jwt.password_verify(form_data.password, current_user.password):
|
|
raise errors.AuthorizationError(msg='密码错误')
|
|
elif not current_user.is_active:
|
|
raise errors.AuthorizationError(msg='该用户已被锁定,无法登录')
|
|
# 更新登陆时间
|
|
await UserDao.update_user_login_time(db, form_data.username)
|
|
# 创建token
|
|
access_token = jwt.create_access_token(current_user.id)
|
|
return access_token, current_user.is_superuser
|
|
|
|
# @staticmethod
|
|
# async def login(obj: Auth):
|
|
# async with async_db_session() as db:
|
|
# current_user = await UserDao.get_user_by_username(db, obj.username)
|
|
# if not current_user:
|
|
# raise errors.NotFoundError(msg='用户名不存在')
|
|
# elif not jwt.password_verify(obj.password, current_user.password):
|
|
# raise errors.AuthorizationError(msg='密码错误')
|
|
# elif not current_user.is_active:
|
|
# raise errors.AuthorizationError(msg='该用户已被锁定,无法登录')
|
|
# # 更新登陆时间
|
|
# await UserDao.update_user_login_time(db, obj.username)
|
|
# # 创建token
|
|
# access_token = jwt.create_access_token(current_user.id)
|
|
# return access_token, current_user.is_superuser
|
|
|
|
@staticmethod
|
|
async def login_email(*, request: Request, obj: Auth2):
|
|
async with async_db_session() as db:
|
|
current_email = await UserDao.check_email(db, obj.email)
|
|
if not current_email:
|
|
raise errors.NotFoundError(msg='邮箱不存在')
|
|
username = await UserDao.get_username_by_email(db, obj.email)
|
|
current_user = await UserDao.get_user_by_username(db, username)
|
|
if not current_user.is_active:
|
|
raise errors.AuthorizationError(msg='该用户已被锁定,无法登录')
|
|
try:
|
|
uid = request.app.state.email_login_code
|
|
except Exception:
|
|
raise errors.ForbiddenError(msg='请先获取邮箱验证码再登陆')
|
|
r_code = await redis_client.get(f'{uid}')
|
|
if not r_code:
|
|
raise errors.NotFoundError(msg='验证码失效,请重新获取')
|
|
if r_code != obj.code:
|
|
raise errors.CodeError(error=CodeEnum.CAPTCHA_ERROR)
|
|
await UserDao.update_user_login_time(db, username)
|
|
access_token = jwt.create_access_token(current_user.id)
|
|
return access_token, current_user.is_superuser
|
|
|
|
@staticmethod
|
|
async def send_login_email_captcha(request: Request, obj: ELCode):
|
|
async with async_db_session() as db:
|
|
if not await UserDao.check_email(db, obj.email):
|
|
raise errors.NotFoundError(msg='邮箱不存在')
|
|
username = await UserDao.get_username_by_email(db, obj.email)
|
|
current_user = await UserDao.get_user_by_username(db, username)
|
|
if not current_user.is_active:
|
|
raise errors.ForbiddenError(msg='该用户已被锁定,无法登录,发送验证码失败')
|
|
try:
|
|
code = text_captcha()
|
|
await send_verification_code_email(obj.email, code, SEND_EMAIL_LOGIN_TEXT)
|
|
except Exception as e:
|
|
log.error('验证码发送失败 {}', e)
|
|
raise errors.ServerError(msg=f'验证码发送失败: {e}')
|
|
else:
|
|
uid = get_uuid()
|
|
await redis_client.set(uid, code, settings.EMAIL_LOGIN_CODE_MAX_AGE)
|
|
request.app.state.email_login_code = uid
|
|
|
|
@staticmethod
|
|
async def register(obj: CreateUser):
|
|
async with async_db_session.begin() as db:
|
|
username = await UserDao.get_user_by_username(db, obj.username)
|
|
if username:
|
|
raise errors.ForbiddenError(msg='该用户名已注册')
|
|
email = await UserDao.check_email(db, obj.email)
|
|
if email:
|
|
raise errors.ForbiddenError(msg='该邮箱已注册')
|
|
try:
|
|
validate_email(obj.email, check_deliverability=False).email
|
|
except EmailNotValidError:
|
|
raise errors.ForbiddenError(msg='邮箱格式错误')
|
|
await UserDao.create_user(db, obj)
|
|
|
|
@staticmethod
|
|
async def get_pwd_rest_captcha(*, username_or_email: str, response: Response):
|
|
async with async_db_session() as db:
|
|
code = text_captcha()
|
|
if await UserDao.get_user_by_username(db, username_or_email):
|
|
try:
|
|
response.delete_cookie(key='fastapi_reset_pwd_code')
|
|
response.delete_cookie(key='fastapi_reset_pwd_username')
|
|
response.set_cookie(
|
|
key='fastapi_reset_pwd_code',
|
|
value=sha256(code.encode('utf-8')).hexdigest(),
|
|
max_age=settings.COOKIES_MAX_AGE
|
|
)
|
|
response.set_cookie(
|
|
key='fastapi_reset_pwd_username',
|
|
value=username_or_email,
|
|
max_age=settings.COOKIES_MAX_AGE
|
|
)
|
|
except Exception as e:
|
|
log.exception('无法发送验证码 {}', e)
|
|
raise e
|
|
current_user_email = await UserDao.get_email_by_username(db, username_or_email)
|
|
await send_verification_code_email(current_user_email, code)
|
|
else:
|
|
try:
|
|
validate_email(username_or_email, check_deliverability=False)
|
|
except EmailNotValidError:
|
|
raise HTTPException(status_code=404, detail='用户名不存在')
|
|
email_result = await UserDao.check_email(db, username_or_email)
|
|
if not email_result:
|
|
raise HTTPException(status_code=404, detail='邮箱不存在')
|
|
try:
|
|
response.delete_cookie(key='fastapi_reset_pwd_code')
|
|
response.delete_cookie(key='fastapi_reset_pwd_username')
|
|
response.set_cookie(
|
|
key='fastapi_reset_pwd_code',
|
|
value=sha256(code.encode('utf-8')).hexdigest(),
|
|
max_age=settings.COOKIES_MAX_AGE
|
|
)
|
|
username = await UserDao.get_username_by_email(db, username_or_email)
|
|
response.set_cookie(
|
|
key='fastapi_reset_pwd_username',
|
|
value=username,
|
|
max_age=settings.COOKIES_MAX_AGE
|
|
)
|
|
except Exception as e:
|
|
log.exception('无法发送验证码 {}', e)
|
|
raise e
|
|
await send_verification_code_email(username_or_email, code)
|
|
|
|
@staticmethod
|
|
async def pwd_reset(*, obj: ResetPassword, request: Request, response: Response):
|
|
async with async_db_session.begin() as db:
|
|
pwd1 = obj.password1
|
|
pwd2 = obj.password2
|
|
cookie_reset_pwd_code = request.cookies.get('fastapi_reset_pwd_code')
|
|
cookie_reset_pwd_username = request.cookies.get('fastapi_reset_pwd_username')
|
|
if pwd1 != pwd2:
|
|
raise errors.ForbiddenError(msg='两次密码输入不一致')
|
|
if cookie_reset_pwd_username is None or cookie_reset_pwd_code is None:
|
|
raise errors.NotFoundError(msg='验证码已失效,请重新获取验证码')
|
|
if cookie_reset_pwd_code != sha256(obj.code.encode('utf-8')).hexdigest():
|
|
raise errors.ForbiddenError(msg='验证码错误')
|
|
await UserDao.reset_password(db, cookie_reset_pwd_username, obj.password2)
|
|
response.delete_cookie(key='fastapi_reset_pwd_code')
|
|
response.delete_cookie(key='fastapi_reset_pwd_username')
|
|
|
|
@staticmethod
|
|
async def get_user_info(username: str):
|
|
async with async_db_session() as db:
|
|
user = await UserDao.get_user_by_username(db, username)
|
|
if not user:
|
|
raise errors.NotFoundError(msg='用户不存在')
|
|
if user.avatar is not None:
|
|
user.avatar = cut_path(AvatarPath + user.avatar)[1]
|
|
return user
|
|
|
|
@staticmethod
|
|
async def update(*, username: str, current_user: User, obj: UpdateUser):
|
|
async with async_db_session.begin() as db:
|
|
if not current_user.is_superuser:
|
|
if not username == current_user.username:
|
|
raise errors.AuthorizationError
|
|
input_user = await UserDao.get_user_by_username(db, username)
|
|
if not input_user:
|
|
raise errors.NotFoundError(msg='用户不存在')
|
|
if input_user.username != obj.username:
|
|
username = await UserDao.get_user_by_username(db, obj.username)
|
|
if username:
|
|
raise errors.ForbiddenError(msg='该用户名已存在')
|
|
if input_user.email != obj.email:
|
|
_email = await UserDao.check_email(db, obj.email)
|
|
if _email:
|
|
raise errors.ForbiddenError(msg='该邮箱已注册')
|
|
try:
|
|
validate_email(obj.email, check_deliverability=False).email
|
|
except EmailNotValidError:
|
|
raise errors.ForbiddenError(msg='邮箱格式错误')
|
|
if obj.mobile_number is not None:
|
|
if not re_verify.is_mobile(obj.mobile_number):
|
|
raise errors.ForbiddenError(msg='手机号码输入有误')
|
|
if obj.wechat is not None:
|
|
if not re_verify.is_wechat(obj.wechat):
|
|
raise errors.ForbiddenError(msg='微信号码输入有误')
|
|
if obj.qq is not None:
|
|
if not re_verify.is_qq(obj.qq):
|
|
raise errors.ForbiddenError(msg='QQ号码输入有误')
|
|
count = await UserDao.update_userinfo(db, input_user, obj)
|
|
return count
|
|
|
|
@staticmethod
|
|
async def update_avatar(*, username: str, current_user: User, avatar: UploadFile):
|
|
async with async_db_session.begin() as db:
|
|
if not current_user.is_superuser:
|
|
if not username == current_user.username:
|
|
raise errors.AuthorizationError
|
|
input_user = await UserDao.get_user_by_username(db, username)
|
|
if not input_user:
|
|
raise errors.NotFoundError(msg='用户不存在')
|
|
input_user_avatar = input_user.avatar
|
|
if avatar is not None:
|
|
if input_user_avatar is not None:
|
|
try:
|
|
os.remove(AvatarPath + input_user_avatar)
|
|
except Exception as e:
|
|
log.error('用户 {} 更新头像时,原头像文件 {} 删除失败\n{}', current_user.username,
|
|
input_user_avatar,
|
|
e)
|
|
new_file = await avatar.read()
|
|
if 'image' not in avatar.content_type:
|
|
raise errors.ForbiddenError(msg='图片格式错误,请重新选择图片')
|
|
file_name = str(get_current_timestamp()) + '_' + avatar.filename
|
|
if not os.path.exists(AvatarPath):
|
|
os.makedirs(AvatarPath)
|
|
with open(AvatarPath + f'{file_name}', 'wb') as f:
|
|
f.write(new_file)
|
|
else:
|
|
file_name = input_user_avatar
|
|
count = await UserDao.update_avatar(db, input_user, file_name)
|
|
return count
|
|
|
|
@staticmethod
|
|
async def delete_avatar(*, username: str, current_user: User):
|
|
async with async_db_session.begin() as db:
|
|
if not current_user.is_superuser:
|
|
if not username == current_user.username:
|
|
raise errors.AuthorizationError
|
|
input_user = await UserDao.get_user_by_username(db, username)
|
|
if not input_user:
|
|
raise errors.NotFoundError(msg='用户不存在')
|
|
input_user_avatar = input_user.avatar
|
|
if input_user_avatar is not None:
|
|
try:
|
|
os.remove(AvatarPath + input_user_avatar)
|
|
except Exception as e:
|
|
log.error('用户 {} 删除头像文件 {} 失败\n{}', input_user.username, input_user_avatar, e)
|
|
else:
|
|
raise errors.NotFoundError(msg='用户没有头像文件,请上传头像文件后再执行此操作')
|
|
count = await UserDao.delete_avatar(db, input_user.id)
|
|
return count
|
|
|
|
@staticmethod
|
|
async def get_user_list():
|
|
async with async_db_session() as db:
|
|
user_select = UserDao.get_users()
|
|
return await paginate(db, user_select)
|
|
|
|
@staticmethod
|
|
async def update_permission(pk: int):
|
|
async with async_db_session.begin() as db:
|
|
if await UserDao.get_user_by_id(db, pk):
|
|
count = await UserDao.super_set(db, pk)
|
|
return count
|
|
else:
|
|
raise errors.NotFoundError(msg='用户不存在')
|
|
|
|
@staticmethod
|
|
async def update_active(pk: int):
|
|
async with async_db_session.begin() as db:
|
|
if await UserDao.get_user_by_id(db, pk):
|
|
count = await UserDao.active_set(db, pk)
|
|
return count
|
|
else:
|
|
raise errors.NotFoundError(msg='用户不存在')
|
|
|
|
@staticmethod
|
|
async def delete(*, username: str, current_user: User):
|
|
async with async_db_session.begin() as db:
|
|
if not current_user.is_superuser:
|
|
if not username == current_user.username:
|
|
raise errors.AuthorizationError
|
|
input_user = await UserDao.get_user_by_username(db, username)
|
|
if not input_user:
|
|
raise errors.NotFoundError(msg='用户不存在')
|
|
input_user_avatar = input_user.avatar
|
|
try:
|
|
if input_user_avatar is not None:
|
|
os.remove(AvatarPath + input_user_avatar)
|
|
except Exception as e:
|
|
log.error(f'删除用户 {input_user.username} 头像文件:{input_user_avatar} 失败\n{e}')
|
|
finally:
|
|
count = await UserDao.delete_user(db, input_user.id)
|
|
return count
|