Skip to content

Commit c0e24db

Browse files
wu-clanRanY-Luck
authored andcommitted
Add a standalone email sending plugin (fastapi-practices#769)
1 parent d25a456 commit c0e24db

File tree

15 files changed

+324
-3
lines changed

15 files changed

+324
-3
lines changed

backend/app/admin/api/v1/sys/user.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,18 @@ async def update_user_avatar(
133133
return response_base.fail()
134134

135135

136+
@router.put('/me/email', summary='更新当前用户邮箱', dependencies=[DependsJwtAuth])
137+
async def update_user_email(
138+
request: Request,
139+
captcha: Annotated[str, Body(embed=True, description='邮箱验证码')],
140+
email: Annotated[str, Body(embed=True, description='用户邮箱')],
141+
) -> ResponseModel:
142+
count = await user_service.update_email(request=request, captcha=captcha, email=email)
143+
if count > 0:
144+
return response_base.success()
145+
return response_base.fail()
146+
147+
136148
@router.delete(
137149
path='/{pk}',
138150
summary='删除用户',

backend/app/admin/crud/crud_user.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,17 @@ async def update_avatar(self, db: AsyncSession, user_id: int, avatar: str) -> in
151151
"""
152152
return await self.update_model(db, user_id, {'avatar': avatar})
153153

154+
async def update_email(self, db: AsyncSession, user_id: int, email: str) -> int:
155+
"""
156+
更新用户邮箱
157+
158+
:param db: 数据库会话
159+
:param user_id: 用户 ID
160+
:param email: 邮箱
161+
:return:
162+
"""
163+
return await self.update_model(db, user_id, {'email': email})
164+
154165
async def delete(self, db: AsyncSession, user_id: int) -> int:
155166
"""
156167
删除用户

backend/app/admin/service/user_service.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
)
1919
from backend.common.enums import UserPermissionType
2020
from backend.common.exception import errors
21+
from backend.common.response.response_code import CustomErrorCode
2122
from backend.common.security.jwt import get_token, jwt_decode, password_verify, superuser_verify
2223
from backend.core.conf import settings
2324
from backend.database.db import async_db_session
@@ -206,7 +207,7 @@ async def reset_password(*, request: Request, pk: int, password: str) -> int:
206207
@staticmethod
207208
async def update_nickname(*, request: Request, nickname: str) -> int:
208209
"""
209-
更新用户昵称
210+
更新当前用户昵称
210211
211212
:param request: FastAPI 请求对象
212213
:param nickname: 用户昵称
@@ -225,7 +226,7 @@ async def update_nickname(*, request: Request, nickname: str) -> int:
225226
@staticmethod
226227
async def update_avatar(*, request: Request, avatar: str) -> int:
227228
"""
228-
更新用户头像
229+
更新当前用户头像
229230
230231
:param request: FastAPI 请求对象
231232
:param avatar: 头像地址
@@ -241,10 +242,36 @@ async def update_avatar(*, request: Request, avatar: str) -> int:
241242
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
242243
return count
243244

245+
@staticmethod
246+
async def update_email(*, request: Request, captcha: str, email: str) -> int:
247+
"""
248+
更新当前用户邮箱
249+
250+
:param request: FastAPI 请求对象
251+
:param captcha: 邮箱验证码
252+
:param email: 邮箱
253+
:return:
254+
"""
255+
async with async_db_session.begin() as db:
256+
token = get_token(request)
257+
token_payload = jwt_decode(token)
258+
user = await user_dao.get(db, token_payload.id)
259+
if not user:
260+
raise errors.NotFoundError(msg='用户不存在')
261+
captcha_code = await redis_client.get(f'{settings.EMAIL_CAPTCHA_REDIS_PREFIX}:{request.state.ip}')
262+
if not captcha_code:
263+
raise errors.RequestError(msg='验证码已失效,请重新获取')
264+
if captcha != captcha_code:
265+
raise errors.CustomError(error=CustomErrorCode.CAPTCHA_ERROR)
266+
await redis_client.delete(f'{settings.EMAIL_CAPTCHA_REDIS_PREFIX}:{request.state.ip}')
267+
count = await user_dao.update_email(db, token_payload.id, email)
268+
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
269+
return count
270+
244271
@staticmethod
245272
async def update_password(*, request: Request, obj: ResetPasswordParam) -> int:
246273
"""
247-
更新用户密码
274+
更新当前用户密码
248275
249276
:param request: FastAPI 请求对象
250277
:param obj: 密码重置参数

backend/plugin/config/crud/crud_config.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env python3
22
# -*- coding: utf-8 -*-
3+
from typing import Sequence
34

45
from sqlalchemy import Select
56
from sqlalchemy.ext.asyncio import AsyncSession
@@ -22,6 +23,16 @@ async def get(self, db: AsyncSession, pk: int) -> Config | None:
2223
"""
2324
return await self.select_model_by_column(db, id=pk)
2425

26+
async def get_by_type(self, db: AsyncSession, type: str) -> Sequence[Config | None]:
27+
"""
28+
通过键名获取参数配置
29+
30+
:param db: 数据库会话
31+
:param type: 参数配置类型
32+
:return:
33+
"""
34+
return await self.select_models(db, type=type)
35+
2536
async def get_by_key(self, db: AsyncSession, key: str) -> Config | None:
2637
"""
2738
通过键名获取参数配置

backend/plugin/email/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
## 参数配置
2+
3+
默认使用本地电子邮件配置
4+
5+
支持通过 `config 插件` 动态配置电子邮件参数,当动态配置 `EMAIL_STATUS``1` 时,将自动应用动态配置

backend/plugin/email/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-

backend/plugin/email/api/router.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
from fastapi import APIRouter
4+
5+
from backend.core.conf import settings
6+
from backend.plugin.email.api.v1.email import router as email_router
7+
8+
v1 = APIRouter(prefix=f'{settings.FASTAPI_API_V1_PATH}/emails', tags=['电子邮件'])
9+
10+
v1.include_router(email_router)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
import random
4+
5+
from typing import Annotated
6+
7+
from fastapi import APIRouter, Body, Request
8+
9+
from backend.common.response.response_schema import response_base
10+
from backend.common.security.jwt import DependsJwtAuth
11+
from backend.core.conf import settings
12+
from backend.database.db import CurrentSession
13+
from backend.database.redis import redis_client
14+
from backend.plugin.email.utils.send import send_email
15+
16+
router = APIRouter()
17+
18+
19+
@router.post('/captcha', summary='发送电子邮件验证码', dependencies=[DependsJwtAuth])
20+
async def send_email_captcha(
21+
request: Request,
22+
db: CurrentSession,
23+
recipients: Annotated[str | list[str], Body(embed=True, description='邮件接收者')],
24+
):
25+
code = ''.join([str(random.randint(1, 9)) for _ in range(6)])
26+
ip = request.state.ip
27+
await redis_client.set(
28+
f'{settings.EMAIL_CAPTCHA_REDIS_PREFIX}:{ip}',
29+
code,
30+
ex=settings.EMAIL_CAPTCHA_EXPIRE_SECONDS,
31+
)
32+
content = {'code': code, 'expired': int(settings.EMAIL_CAPTCHA_EXPIRE_SECONDS / 60)}
33+
await send_email(db, recipients, 'FBA 验证码', content, 'captcha.html')
34+
return response_base.success()

0 commit comments

Comments
 (0)