diff --git a/app/core/executor.py b/app/core/executor.py
index 50619188..18f0e2db 100755
--- a/app/core/executor.py
+++ b/app/core/executor.py
@@ -666,7 +666,7 @@ async def notice(env: list, plan: PityTestPlan, project: Project, report_dict: d
for m in msg_types:
if int(m) == NoticeType.EMAIL:
render_html = Email.render_html(plan_name=plan.name, **report_dict[e])
- Email.send_msg(
+ await Email.send_msg(
f"【{report_dict[e].get('env')}】测试计划【{plan.name}】执行完毕({report_dict[e].get('plan_result')})",
render_html, None, *[r.get("email") for r in users])
if int(m) == NoticeType.DINGDING:
diff --git a/app/core/msg/mail.py b/app/core/msg/mail.py
index 70e45347..1c9dfe2f 100644
--- a/app/core/msg/mail.py
+++ b/app/core/msg/mail.py
@@ -3,6 +3,8 @@
from email.mime.text import MIMEText
from email.utils import make_msgid
+import aioify
+from awaits.awaitable import awaitable
from jinja2.environment import Template
from app.core.configuration import SystemConfiguration
@@ -24,6 +26,7 @@ class Email(Notification):
# client.send(receiver, subject=subject, contents=content, attachments=attachment)
@staticmethod
+ @awaitable
def send_msg(subject, content, attachment=None, *receiver):
configuration = SystemConfiguration.get_config()
data = configuration.get("email")
diff --git a/app/crud/auth/UserDao.py b/app/crud/auth/UserDao.py
index 2658ddb9..f3fa2f9a 100644
--- a/app/crud/auth/UserDao.py
+++ b/app/crud/auth/UserDao.py
@@ -1,4 +1,3 @@
-import asyncio
import random
import time
from datetime import datetime
@@ -9,7 +8,7 @@
from app.crud import Mapper
from app.middleware.Jwt import UserToken
from app.middleware.RedisManager import RedisHelper
-from app.models import async_session, DatabaseHelper
+from app.models import async_session
from app.models.user import User
from app.schema.user import UserUpdateForm
from app.utils.logger import Log
@@ -149,7 +148,7 @@ async def login(username, password):
async with session.begin():
# 查询用户名/密码匹配且没有被删除的用户
query = await session.execute(
- select(User).where(User.username == username, User.password == pwd,
+ select(User).where(or_(User.username == username, User.email == username), User.password == pwd,
User.deleted_at == 0))
user = query.scalars().first()
if user is None:
@@ -195,3 +194,22 @@ async def list_user_touch(*user):
except Exception as e:
UserDao.log.error(f"获取用户联系方式失败: {str(e)}")
raise Exception(f"获取用户联系方式失败: {e}")
+
+ @staticmethod
+ async def reset_password(email: str, password: str):
+ pwd = UserToken.add_salt(password)
+ try:
+ async with async_session() as session:
+ async with session.begin():
+ sql = update(User).where(User.email == email).values(password=pwd)
+ await session.execute(sql)
+ except Exception as e:
+ UserDao.log.error(f"重置用户: {email}密码失败: {str(e)}")
+ raise Exception(f"重置{email}密码失败")
+
+ @staticmethod
+ async def query_user_by_email(email: str):
+ async with async_session() as session:
+ sql = select(User).where(User.email == email, User.is_valid == True)
+ query = await session.execute(sql)
+ return query.scalars().first()
diff --git a/app/routers/auth/user.py b/app/routers/auth/user.py
index 415be87d..34273afe 100755
--- a/app/routers/auth/user.py
+++ b/app/routers/auth/user.py
@@ -1,14 +1,17 @@
+import asyncio
+
import requests
from fastapi import APIRouter, Depends
from starlette import status
+from app.core.msg.mail import Email
from app.crud.auth.UserDao import UserDao
from app.excpetions.RequestException import AuthException
from app.handler.fatcory import PityResponse
from app.middleware.Jwt import UserToken
from app.routers import Permission, FORBIDDEN
-from app.routers.auth.user_schema import UserDto, UserForm
-from app.schema.user import UserUpdateForm
+from app.schema.user import UserUpdateForm, UserForm, UserDto, ResetPwdForm
+from app.utils.des import Des
from config import Config
router = APIRouter(prefix="/auth")
@@ -106,3 +109,34 @@ async def delete_user(id: int, user=Depends(Permission(Config.ADMIN))):
return PityResponse.success(user)
except Exception as e:
return PityResponse.failed(e)
+
+
+@router.post("/reset", summary="重置用户密码")
+async def reset_user(form: ResetPwdForm):
+ email = Des.des_decrypt(form.token)
+ await UserDao.reset_password(email, form.password)
+ return PityResponse.success()
+
+
+@router.get("/reset/generate/{email}", summary="生成重置密码链接")
+async def generate_reset_url(email: str):
+ try:
+ user = await UserDao.query_user_by_email(email)
+ if user is not None:
+ # 说明邮件存在,发送邮件
+ em = Des.des_encrypt(email)
+ link = f"""https://pity.fun/#/user/resetPassword?token={em}"""
+ render_html = Email.render_html(Config.PASSWORD_HTML_PATH, link=link, name=user.name)
+ asyncio.create_task(Email.send_msg("重置你的pity密码", render_html, None, email))
+ return PityResponse.success(None)
+ except Exception as e:
+ return PityResponse.failed(str(e))
+
+
+@router.get("/reset/check/{token}", summary="检测生成的链接是否正确")
+async def check_reset_url(token: str):
+ try:
+ email = Des.des_decrypt(token)
+ return PityResponse.success(email)
+ except:
+ return PityResponse.failed("重置链接不存在, 请不要无脑尝试")
diff --git a/app/routers/auth/user_schema.py b/app/routers/auth/user_schema.py
deleted file mode 100755
index 53923e63..00000000
--- a/app/routers/auth/user_schema.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from pydantic import BaseModel, validator
-
-from app.excpetions.ParamsException import ParamsError
-
-
-class UserDto(BaseModel):
- name: str
- password: str
- username: str
- email: str
-
- @validator('name', 'password', 'username', 'email')
- def field_not_empty(cls, v):
- if isinstance(v, str) and len(v.strip()) == 0:
- raise ParamsError("不能为空")
- return v
-
-
-class UserForm(BaseModel):
- username: str
- password: str
-
- @validator('password', 'username')
- def name_not_empty(cls, v):
- if isinstance(v, str) and len(v.strip()) == 0:
- raise ParamsError("不能为空")
- return v
diff --git a/app/schema/user.py b/app/schema/user.py
index 2d77007b..9dccbd02 100644
--- a/app/schema/user.py
+++ b/app/schema/user.py
@@ -1,6 +1,7 @@
from pydantic import BaseModel, validator
# 都可以为空,为空则不进行更改
+from app.excpetions.ParamsException import ParamsError
from app.schema.base import PityModel
@@ -15,3 +16,38 @@ class UserUpdateForm(BaseModel):
@validator('id')
def id_not_empty(cls, v):
return PityModel.not_empty(v)
+
+
+class UserDto(BaseModel):
+ name: str
+ password: str
+ username: str
+ email: str
+
+ @validator('name', 'password', 'username', 'email')
+ def field_not_empty(cls, v):
+ if isinstance(v, str) and len(v.strip()) == 0:
+ raise ParamsError("不能为空")
+ return v
+
+
+class UserForm(BaseModel):
+ username: str
+ password: str
+
+ @validator('password', 'username')
+ def name_not_empty(cls, v):
+ if isinstance(v, str) and len(v.strip()) == 0:
+ raise ParamsError("不能为空")
+ return v
+
+
+class ResetPwdForm(BaseModel):
+ password: str
+ token: str
+
+ @validator('token', 'password')
+ def name_not_empty(cls, v):
+ if isinstance(v, str) and len(v.strip()) == 0:
+ raise ParamsError("不能为空")
+ return v
diff --git a/app/utils/des.py b/app/utils/des.py
new file mode 100644
index 00000000..3ea14454
--- /dev/null
+++ b/app/utils/des.py
@@ -0,0 +1,38 @@
+import binascii
+
+from pyDes import des, CBC, PAD_PKCS5
+
+# 秘钥
+KEY = 'pityspwd'
+
+
+class Des(object):
+
+ @staticmethod
+ def des_encrypt(s):
+ """
+ DES 加密
+ :param s: 原始字符串
+ :return: 加密后字符串,16进制
+ """
+ secret_key = KEY # 密码
+ iv = secret_key # 偏移
+ # secret_key:加密密钥,CBC:加密模式,iv:偏移, padmode:填充
+ des_obj = des(secret_key, CBC, iv, pad=None, padmode=PAD_PKCS5)
+ # 返回为字节
+ secret_bytes = des_obj.encrypt(s, padmode=PAD_PKCS5)
+ # 返回为16进制
+ return binascii.b2a_hex(secret_bytes).decode()
+
+ @staticmethod
+ def des_decrypt(s):
+ """
+ DES 解密
+ :param s: 加密后的字符串,16进制
+ :return: 解密后的字符串
+ """
+ secret_key = KEY
+ iv = secret_key
+ des_obj = des(secret_key, CBC, iv, pad=None, padmode=PAD_PKCS5)
+ decrypt_str = des_obj.decrypt(binascii.a2b_hex(s), padmode=PAD_PKCS5)
+ return decrypt_str.decode()
diff --git a/config.py b/config.py
index 03d53786..7450b6c1 100755
--- a/config.py
+++ b/config.py
@@ -61,6 +61,9 @@ class BaseConfig(BaseSettings):
# 测试报告路径
REPORT_PATH = os.path.join(ROOT, "templates", "report.html")
+ # 重置密码路径
+ PASSWORD_HTML_PATH = os.path.join(ROOT, "templates", "reset_password.html")
+
# APP 路径
APP_PATH = os.path.join(ROOT, "app")
diff --git a/requirements.txt b/requirements.txt
index e9ad5167..643de49e 100755
--- a/requirements.txt
+++ b/requirements.txt
@@ -33,4 +33,5 @@ jsonpath~=0.82
py-mock
starlette_context
gunicorn
-supervisor
\ No newline at end of file
+supervisor
+pyDes~=2.0.1
\ No newline at end of file
diff --git a/templates/reset_password.html b/templates/reset_password.html
new file mode 100644
index 00000000..12dae43b
--- /dev/null
+++ b/templates/reset_password.html
@@ -0,0 +1,193 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hi {{ woody }},
+
+ 这是你的密码重置相关介绍.
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+ 重置你的pity密码的链接已经生成,如果不是你本人操作请直接忽略,否则请点击下面的链接重置你的密码:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Thank you,
+
+Team pity
+
+
+
+
+
+
+ 如果按钮不能正常点击,请复制链接地址并在浏览器中打开。
+ pity官网.
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
\ No newline at end of file