用户接口相关邮箱验证功能接口
This commit is contained in:
parent
fc053c239d
commit
340b2a7b6b
|
|
@ -4,6 +4,7 @@
|
||||||
<file url="file://$PROJECT_DIR$/migrations/models/11_20250803092810_update.py" dialect="MySQL" />
|
<file url="file://$PROJECT_DIR$/migrations/models/11_20250803092810_update.py" dialect="MySQL" />
|
||||||
<file url="file://$PROJECT_DIR$/migrations/models/16_20250806104900_update.py" dialect="GenericSQL" />
|
<file url="file://$PROJECT_DIR$/migrations/models/16_20250806104900_update.py" dialect="GenericSQL" />
|
||||||
<file url="file://$PROJECT_DIR$/migrations/models/21_20250827100315_update.py" dialect="MySQL" />
|
<file url="file://$PROJECT_DIR$/migrations/models/21_20250827100315_update.py" dialect="MySQL" />
|
||||||
|
<file url="file://$PROJECT_DIR$/migrations/models/33_20250929111212_update.py" dialect="MySQL" />
|
||||||
<file url="file://$PROJECT_DIR$/scripts/update_fr.py" dialect="MySQL" />
|
<file url="file://$PROJECT_DIR$/scripts/update_fr.py" dialect="MySQL" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
103
README.md
103
README.md
|
|
@ -24,6 +24,7 @@ Authorization: Bearer <your_jwt_token>
|
||||||
### 1. 用户认证模块 (`/users`)
|
### 1. 用户认证模块 (`/users`)
|
||||||
|
|
||||||
#### 1.1 用户注册
|
#### 1.1 用户注册
|
||||||
|
##### 1.1.1 注册主接口
|
||||||
|
|
||||||
- **接口**: `POST /users/register`
|
- **接口**: `POST /users/register`
|
||||||
- **描述**: 新用户注册
|
- **描述**: 新用户注册
|
||||||
|
|
@ -33,8 +34,11 @@ Authorization: Bearer <your_jwt_token>
|
||||||
{
|
{
|
||||||
"username": "string",
|
"username": "string",
|
||||||
"password": "string",
|
"password": "string",
|
||||||
|
"email": "EmailFields[string]",
|
||||||
|
"phone": "PhoneFields",
|
||||||
"lang_pref": "jp" | "fr" | "private",
|
"lang_pref": "jp" | "fr" | "private",
|
||||||
"portrait": "string"
|
"portrait": "string",
|
||||||
|
"code": "string"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -51,6 +55,30 @@ Authorization: Bearer <your_jwt_token>
|
||||||
- `200`: 注册成功
|
- `200`: 注册成功
|
||||||
- `400`: 参数验证失败
|
- `400`: 参数验证失败
|
||||||
|
|
||||||
|
##### 1.1.2 邮箱验证
|
||||||
|
|
||||||
|
- **接口**: `POST /users/register/email_verify`
|
||||||
|
- **描述**: 新用户注册时的邮箱验证
|
||||||
|
- **请求体**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user_email" : "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **响应**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "验证码已发送"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **状态码**:
|
||||||
|
- `200`: 邮件发送成功
|
||||||
|
- `400`: 邮箱已被使用
|
||||||
|
|
||||||
#### 1.2 用户登录
|
#### 1.2 用户登录
|
||||||
|
|
||||||
- **接口**: `POST /users/login`
|
- **接口**: `POST /users/login`
|
||||||
|
|
@ -120,6 +148,79 @@ Authorization: Bearer <your_jwt_token>
|
||||||
- `200`: 更新成功
|
- `200`: 更新成功
|
||||||
- `400`: 原密码错误或用户名为保留词
|
- `400`: 原密码错误或用户名为保留词
|
||||||
|
|
||||||
|
#### 1.5 邮箱找回密码(发送验证码)
|
||||||
|
|
||||||
|
- **接口**: `POST /users/auth/forget-password/email`
|
||||||
|
- **描述**: 用户请求通过邮箱找回密码时,向注册邮箱发送验证码
|
||||||
|
- **请求体**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
- **响应**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "验证码已发送"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
- **状态码**
|
||||||
|
- `200`: 更新成功
|
||||||
|
- `404`: 用户不存在
|
||||||
|
|
||||||
|
#### 1.6 邮箱验证码验证
|
||||||
|
|
||||||
|
- **接口**: `POST /users/auth/varify_code/email`
|
||||||
|
- **描述**: 用户输入邮箱验证码后,验证验证码是否有效,返回重置令牌
|
||||||
|
- **请求体**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": "string",
|
||||||
|
"code": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **响应**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"reset_token": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **状态码**
|
||||||
|
- `200`: 验证码验证成功
|
||||||
|
- `400`: 验证码错误或已过期
|
||||||
|
|
||||||
|
#### 1.7 重置密码
|
||||||
|
|
||||||
|
- **接口**: `POST /users/auth/reset-password`
|
||||||
|
- **描述**: 用户通过邮箱验证码获得的重置令牌来设置新密码
|
||||||
|
- **请求头**:
|
||||||
|
- `x-reset-token`: 重置令牌
|
||||||
|
- **请求体**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"password": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **响应**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"massage": "密码重置成功"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **状态码**
|
||||||
|
- `200`: 密码重置成功
|
||||||
|
- `400`: 密码不合法或令牌无效
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2. 词典搜索模块
|
### 2. 词典搜索模块
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
import redis
|
import redis
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from app.core.redis import redis_client, get_redis
|
|
||||||
|
from app.core.redis import get_redis
|
||||||
|
|
||||||
redis_test_router = APIRouter()
|
redis_test_router = APIRouter()
|
||||||
|
|
||||||
@redis_test_router.get("/ping-redis")
|
@redis_test_router.get("/ping-redis")
|
||||||
async def ping_redis(r: redis.Redis = Depends(get_redis)):
|
async def ping_redis(r: redis.Redis = Depends(get_redis)):
|
||||||
return {"pong": await r.ping()}
|
return {
|
||||||
|
"pong": await r.ping(),
|
||||||
|
"redis": r.connection_pool.connection_kwargs,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,41 +6,70 @@ from fastapi import APIRouter, HTTPException, Depends, Request
|
||||||
from jose import jwt
|
from jose import jwt
|
||||||
|
|
||||||
from app.api.user.user_schemas import UserIn, UpdateUserRequest, UserLoginRequest, UserResetPhoneRequest, \
|
from app.api.user.user_schemas import UserIn, UpdateUserRequest, UserLoginRequest, UserResetPhoneRequest, \
|
||||||
UserDoesNotExistsError, VerifyCodeRequest, UserResetEmailRequest, UserResetPasswordRequest, VerifyEmailRequest
|
VerifyPhoneCodeRequest, UserResetEmailRequest, UserResetPasswordRequest, VerifyEmailRequest
|
||||||
from app.core.redis import get_redis
|
from app.core.redis import get_redis
|
||||||
from app.models.base import ReservedWords, User, Language
|
from app.models.base import ReservedWords, User, Language
|
||||||
from app.utils.security import get_current_user
|
from app.utils.security import get_current_user
|
||||||
from settings import settings
|
from settings import settings
|
||||||
from . import service
|
from . import service
|
||||||
from .service import hash_password, send_email_code
|
|
||||||
|
|
||||||
users_router = APIRouter()
|
users_router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@users_router.post("/register")
|
@users_router.post("/register")
|
||||||
async def register(user_in: UserIn):
|
async def register(req: Request, user_in: UserIn):
|
||||||
await service.validate_username(user_in.username)
|
await service.validate_username(user_in.username)
|
||||||
await service.validate_password(user_in.password)
|
await service.validate_password(user_in.password)
|
||||||
|
# await service.validate_email_exists(user_in.email)
|
||||||
|
result = await service.verify_email_code(
|
||||||
|
redis=req.app.state.redis,
|
||||||
|
email=user_in.email,
|
||||||
|
input_code=user_in.code
|
||||||
|
)
|
||||||
|
if not result:
|
||||||
|
raise HTTPException(status_code=400, detail="验证码错误或已过期")
|
||||||
|
|
||||||
hashed_pwd = service.hash_password(user_in.password)
|
hashed_pwd = service.hash_password(user_in.password)
|
||||||
|
|
||||||
lang_pref = await Language.get(code=user_in.lang_pref)
|
lang_pref = await Language.get(code=user_in.lang_pref)
|
||||||
|
|
||||||
new_user, created = await User.get_or_create(
|
encrypted_phone = (
|
||||||
|
req.app.state.phone_encrypto.encrypt(user_in.phone)) \
|
||||||
|
if user_in.phone else None
|
||||||
|
|
||||||
|
new_user = await User.create(
|
||||||
name=user_in.username,
|
name=user_in.username,
|
||||||
defaults={
|
email=user_in.email,
|
||||||
"pwd_hashed": hashed_pwd,
|
pwd_hashed=hashed_pwd,
|
||||||
"language": lang_pref,
|
language=lang_pref,
|
||||||
"portrait": user_in.portrait, },
|
encrypted_phone=encrypted_phone,
|
||||||
)
|
)
|
||||||
if not created:
|
|
||||||
raise HTTPException(status_code=400, detail="Username already exists")
|
|
||||||
return {
|
return {
|
||||||
"id": new_user.id,
|
"id": new_user.id,
|
||||||
"message": "register success",
|
"message": "register success",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@users_router.post("/register/email_verify")
|
||||||
|
async def register_email_verify(req: Request, user_email: UserResetEmailRequest):
|
||||||
|
await service.validate_email_exists(user_email.email)
|
||||||
|
|
||||||
|
code = service.generate_code()
|
||||||
|
redis = req.app.state.redis
|
||||||
|
|
||||||
|
await service.save_email_code(redis, email=user_email.email, code=code)
|
||||||
|
await service.send_email_code(
|
||||||
|
redis=redis,
|
||||||
|
email=user_email.email,
|
||||||
|
code=code,
|
||||||
|
ops_type="reg"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"[DEBUG] 给 {user_email.email} 发送验证码:{code}")
|
||||||
|
|
||||||
|
return {"message": "验证码已发送"}
|
||||||
|
|
||||||
|
|
||||||
@users_router.put("/update", deprecated=False)
|
@users_router.put("/update", deprecated=False)
|
||||||
async def user_modification(updated_user: UpdateUserRequest, current_user: User = Depends(get_current_user)):
|
async def user_modification(updated_user: UpdateUserRequest, current_user: User = Depends(get_current_user)):
|
||||||
"""
|
"""
|
||||||
|
|
@ -62,7 +91,7 @@ async def user_modification(updated_user: UpdateUserRequest, current_user: User
|
||||||
|
|
||||||
# 修改密码(如果提供)
|
# 修改密码(如果提供)
|
||||||
if updated_user.new_password:
|
if updated_user.new_password:
|
||||||
current_user.password_hash = hash_password(updated_user.new_password)
|
current_user.password_hash = service.hash_password(updated_user.new_password)
|
||||||
|
|
||||||
|
|
||||||
@users_router.post("/login")
|
@users_router.post("/login")
|
||||||
|
|
@ -110,20 +139,11 @@ async def user_logout(request: Request,
|
||||||
now = datetime.now(timezone.utc).timestamp()
|
now = datetime.now(timezone.utc).timestamp()
|
||||||
ttl = max(int(exp - now), 1) if exp else 7200
|
ttl = max(int(exp - now), 1) if exp else 7200
|
||||||
|
|
||||||
# try:
|
|
||||||
# payload = jwt.decode(raw_token, SECRET_KEY, algorithms=["HS256"])
|
|
||||||
# exp = payload.get("exp")
|
|
||||||
# now = datetime.now(timezone.utc).timestamp()
|
|
||||||
# ttl = int(exp - now) if exp else 7200 # Time To Live: 黑名单生效时长
|
|
||||||
# except ExpiredSignatureError:
|
|
||||||
# raise HTTPException(status_code=401, detail="登录信息已过期")
|
|
||||||
# except JWTError:
|
|
||||||
# raise HTTPException(status_code=401, detail="无效 token")
|
|
||||||
|
|
||||||
await redis_client.setex(f"blacklist:{raw_token}", ttl, "true")
|
await redis_client.setex(f"blacklist:{raw_token}", ttl, "true")
|
||||||
|
|
||||||
return {"message": "logout ok"}
|
return {"message": "logout ok"}
|
||||||
|
|
||||||
|
|
||||||
# 后续通过参数合并
|
# 后续通过参数合并
|
||||||
@users_router.post("/auth/forget-password/phone", deprecated=True)
|
@users_router.post("/auth/forget-password/phone", deprecated=True)
|
||||||
async def forget_password(request: Request, user_request: UserResetPhoneRequest):
|
async def forget_password(request: Request, user_request: UserResetPhoneRequest):
|
||||||
|
|
@ -131,7 +151,7 @@ async def forget_password(request: Request, user_request: UserResetPhoneRequest)
|
||||||
user = await User.get_or_none(encrypted_phone=encrypted_phone)
|
user = await User.get_or_none(encrypted_phone=encrypted_phone)
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
raise UserDoesNotExistsError()
|
raise HTTPException(status_code=404, detail="User does not exists")
|
||||||
|
|
||||||
redis = request.app.state.Redis
|
redis = request.app.state.Redis
|
||||||
code = service.generate_code()
|
code = service.generate_code()
|
||||||
|
|
@ -147,7 +167,7 @@ async def forget_password(request: Request, user_request: UserResetPhoneRequest)
|
||||||
# TODO 后续升级为防止爆破测试手机号的
|
# TODO 后续升级为防止爆破测试手机号的
|
||||||
|
|
||||||
@users_router.post("/auth/varify_code", deprecated=True)
|
@users_router.post("/auth/varify_code", deprecated=True)
|
||||||
async def varify_code(data: VerifyCodeRequest, request: Request):
|
async def varify_code(data: VerifyPhoneCodeRequest, request: Request):
|
||||||
redis = request.app.state.redis
|
redis = request.app.state.redis
|
||||||
if not await service.varify_code(redis=redis, phone=data.phone, input_code=data.code):
|
if not await service.varify_code(redis=redis, phone=data.phone, input_code=data.code):
|
||||||
raise HTTPException(status_code=400, detail="验证码错误或已过期")
|
raise HTTPException(status_code=400, detail="验证码错误或已过期")
|
||||||
|
|
@ -165,14 +185,14 @@ async def email_forget_password(request: Request, user_request: UserResetEmailRe
|
||||||
user_email = user_request.email
|
user_email = user_request.email
|
||||||
user = await User.get_or_none(email=user_email)
|
user = await User.get_or_none(email=user_email)
|
||||||
if not user:
|
if not user:
|
||||||
raise UserDoesNotExistsError("User does not exists")
|
raise HTTPException(status_code=404, detail="User does not exists")
|
||||||
|
|
||||||
redis = request.app.state.redis
|
redis = request.app.state.redis
|
||||||
code = service.generate_code()
|
code = service.generate_code()
|
||||||
await service.save_email_code(redis, email=user_request.email, code=code)
|
# await service.save_email_code(redis, email=user_request.email, code=code)
|
||||||
|
|
||||||
# 邮箱服务
|
# 邮箱服务
|
||||||
await send_email_code(redis, user_request.email, code)
|
await service.send_email_code(redis, user_request.email, code, ops_type="reset")
|
||||||
|
|
||||||
print(f"[DEBUG] 给 {user_request.email} 发送验证码:{code}")
|
print(f"[DEBUG] 给 {user_request.email} 发送验证码:{code}")
|
||||||
|
|
||||||
|
|
@ -190,6 +210,7 @@ async def email_varify_code(request: Request, data: VerifyEmailRequest):
|
||||||
"reset_token": reset_token,
|
"reset_token": reset_token,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@users_router.post("/auth/reset-password", deprecated=False)
|
@users_router.post("/auth/reset-password", deprecated=False)
|
||||||
async def reset_password(request: Request, reset_request: UserResetPasswordRequest):
|
async def reset_password(request: Request, reset_request: UserResetPasswordRequest):
|
||||||
# 校验密码是否合法
|
# 校验密码是否合法
|
||||||
|
|
@ -199,9 +220,8 @@ async def reset_password(request: Request, reset_request: UserResetPasswordReque
|
||||||
reset_token = request.headers.get("x-reset-token")
|
reset_token = request.headers.get("x-reset-token")
|
||||||
user_id = await service.is_reset_password(redis=redis, token=reset_token)
|
user_id = await service.is_reset_password(redis=redis, token=reset_token)
|
||||||
|
|
||||||
new_password = hash_password(raw_password=reset_request.password)
|
new_password = service.hash_password(raw_password=reset_request.password)
|
||||||
|
|
||||||
await User.filter(id=user_id).update(pwd_hashed=new_password)
|
await User.filter(id=user_id).update(pwd_hashed=new_password)
|
||||||
|
|
||||||
return {"massage": "密码重置成功"}
|
return {"massage": "密码重置成功"}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
import bcrypt
|
import bcrypt
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
|
@ -38,6 +39,11 @@ async def validate_password(password: str):
|
||||||
detail=f"密码只能包含字母、数字和常见特殊字符 {allowed_specials}"
|
detail=f"密码只能包含字母、数字和常见特殊字符 {allowed_specials}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def validate_email_exists(email: str):
|
||||||
|
user = await User.get_or_none(email=email)
|
||||||
|
if user:
|
||||||
|
raise HTTPException(status_code=400, detail="邮箱已经被使用,请更换其他邮箱后重试")
|
||||||
|
|
||||||
|
|
||||||
# 登陆校验
|
# 登陆校验
|
||||||
async def verify_password(raw_password: str, hashed_password: str) -> bool:
|
async def verify_password(raw_password: str, hashed_password: str) -> bool:
|
||||||
|
|
@ -79,7 +85,7 @@ async def save_code_redis(redis: Redis, phone: str, code: str, expire: int = 300
|
||||||
await redis.setex(f"sms:{phone}", expire, code)
|
await redis.setex(f"sms:{phone}", expire, code)
|
||||||
|
|
||||||
|
|
||||||
async def varify_code(redis: Redis, phone: str, input_code: str):
|
async def varify_phone_code(redis: Redis, phone: str, input_code: str):
|
||||||
stored = await redis.get(f"sms:{phone}")
|
stored = await redis.get(f"sms:{phone}")
|
||||||
return stored is not None and stored.decode() == input_code
|
return stored is not None and stored.decode() == input_code
|
||||||
|
|
||||||
|
|
@ -89,16 +95,20 @@ async def save_email_code(redis: Redis, email: str, code: str, expire: int = 300
|
||||||
await redis.setex(f"email:{email}", expire, code)
|
await redis.setex(f"email:{email}", expire, code)
|
||||||
|
|
||||||
|
|
||||||
async def send_email_code(redis: Redis, email: str, code: str):
|
async def send_email_code(redis: Redis, email: str, code: str, ops_type: Literal["reg", "reset"]):
|
||||||
await save_email_code(redis, email, code)
|
await save_email_code(redis, email, code)
|
||||||
|
|
||||||
|
ops_dict = {
|
||||||
|
"reg" : "用户注册",
|
||||||
|
"reset" : "密码重置",
|
||||||
|
}
|
||||||
subject = "Lexiverse 用户邮箱验证码"
|
subject = "Lexiverse 用户邮箱验证码"
|
||||||
content = f"""
|
content = f"""
|
||||||
<html>
|
<html>
|
||||||
<body style="font-family: Arial, sans-serif; line-height:1.6;">
|
<body style="font-family: Arial, sans-serif; line-height:1.6;">
|
||||||
<h2 style="color:#4CAF50;">Lexiverse 验证码</h2>
|
<h2 style="color:#4CAF50;">Lexiverse 验证码</h2>
|
||||||
<p>您好,</p>
|
<p>您好,</p>
|
||||||
<p>您正在进行 <b>密码重置</b> 操作。</p>
|
<p>您正在进行 <b>{ops_dict[ops_type]}</b> 操作。</p>
|
||||||
<p>
|
<p>
|
||||||
您的验证码是:
|
您的验证码是:
|
||||||
<span style="font-size: 24px; font-weight: bold; color: #d9534f;">{code}</span>
|
<span style="font-size: 24px; font-weight: bold; color: #d9534f;">{code}</span>
|
||||||
|
|
@ -115,7 +125,7 @@ async def send_email_code(redis: Redis, email: str, code: str):
|
||||||
send_email(email, subject, content)
|
send_email(email, subject, content)
|
||||||
|
|
||||||
|
|
||||||
async def __verify_email_code(redis: Redis, email: str, input_code: str) -> bool:
|
async def verify_email_code(redis: Redis, email: str, input_code: str) -> bool:
|
||||||
stored = await redis.getdel(f"email:{email}")
|
stored = await redis.getdel(f"email:{email}")
|
||||||
if stored is None or stored != input_code:
|
if stored is None or stored != input_code:
|
||||||
return False
|
return False
|
||||||
|
|
@ -135,7 +145,7 @@ async def __get_reset_token(redis: Redis, email: str):
|
||||||
|
|
||||||
|
|
||||||
async def verify_and_get_reset_token(redis: Redis, email: str, input_code: str):
|
async def verify_and_get_reset_token(redis: Redis, email: str, input_code: str):
|
||||||
ok = await __verify_email_code(redis, email, input_code)
|
ok = await verify_email_code(redis, email, input_code)
|
||||||
if not ok:
|
if not ok:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
|
import re
|
||||||
from typing import Literal, Optional, Annotated
|
from typing import Literal, Optional, Annotated
|
||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field, field_validator
|
||||||
|
|
||||||
default_portrait_url = '#'
|
default_portrait_url = '#'
|
||||||
|
|
||||||
|
|
@ -12,11 +13,26 @@ EmailField = Annotated[str, Field(pattern=r"^[\w\.-]+@[\w\.-]+\.\w+$")]
|
||||||
class UserIn(BaseModel):
|
class UserIn(BaseModel):
|
||||||
username: str
|
username: str
|
||||||
password: str
|
password: str
|
||||||
email: Optional[EmailField] = None
|
email: str
|
||||||
phone: ChinaPhone
|
phone: Optional[str] = None
|
||||||
lang_pref: Literal['jp', 'fr', 'private'] = "private"
|
lang_pref: Literal['jp', 'fr', 'private'] = "private"
|
||||||
portrait: str = default_portrait_url
|
portrait: str = default_portrait_url
|
||||||
|
|
||||||
|
code: str
|
||||||
|
|
||||||
|
@field_validator('email')
|
||||||
|
@classmethod
|
||||||
|
def validate_email(cls, v):
|
||||||
|
if not re.match(pattern=r"^[\w\.-]+@[\w\.-]+\.\w+$", string=v):
|
||||||
|
raise HTTPException(status_code=400, detail="邮箱格式错误")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_validator('phone')
|
||||||
|
@classmethod
|
||||||
|
def validate_phone(cls, v):
|
||||||
|
if not re.match(pattern=r"^1[3-9]\d{9}$", string=v):
|
||||||
|
raise HTTPException(status_code=400, detail="手机号格式错误")
|
||||||
|
return v
|
||||||
# @field_validator('username')
|
# @field_validator('username')
|
||||||
# @classmethod
|
# @classmethod
|
||||||
# def validate_username(cls, v):
|
# def validate_username(cls, v):
|
||||||
|
|
@ -75,12 +91,7 @@ class UserResetPhoneRequest(BaseModel):
|
||||||
phone_number: ChinaPhone
|
phone_number: ChinaPhone
|
||||||
|
|
||||||
|
|
||||||
class UserDoesNotExistsError(HTTPException):
|
class VerifyPhoneCodeRequest(BaseModel):
|
||||||
def __init__(self, message: str):
|
|
||||||
super().__init__(status_code=404, detail=message)
|
|
||||||
|
|
||||||
|
|
||||||
class VerifyCodeRequest(BaseModel):
|
|
||||||
code: str
|
code: str
|
||||||
phone: ChinaPhone
|
phone: ChinaPhone
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ redis_client: Optional[redis.Redis] = None
|
||||||
async def init_redis():
|
async def init_redis():
|
||||||
global redis_client
|
global redis_client
|
||||||
if redis_client is None:
|
if redis_client is None:
|
||||||
redis_client = await redis.Redis(
|
redis_client = redis.Redis(
|
||||||
host="127.0.0.1",
|
host="127.0.0.1",
|
||||||
port=6379,
|
port=6379,
|
||||||
decode_responses=True, # 返回 str 而不是 Bytes
|
decode_responses=True, # 返回 str 而不是 Bytes
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@ class User(Model):
|
||||||
name = fields.CharField(max_length=20, unique=True, description="用户名")
|
name = fields.CharField(max_length=20, unique=True, description="用户名")
|
||||||
pwd_hashed = fields.CharField(max_length=60, description="密码")
|
pwd_hashed = fields.CharField(max_length=60, description="密码")
|
||||||
portrait = fields.CharField(max_length=120, default='#', description="用户头像")
|
portrait = fields.CharField(max_length=120, default='#', description="用户头像")
|
||||||
email = fields.CharField(max_length=120, null=True, description="e-mail")
|
email = fields.CharField(max_length=120, description="e-mail")
|
||||||
encrypted_phone = fields.CharField(max_length=11, description="用户手机号")
|
encrypted_phone = fields.CharField(max_length=11, description="用户手机号", null=True)
|
||||||
language = fields.ForeignKeyField("models.Language", related_name="users", on_delete=fields.CASCADE)
|
language = fields.ForeignKeyField("models.Language", related_name="users", on_delete=fields.CASCADE)
|
||||||
is_admin = fields.BooleanField(default=False, description="管理员权限")
|
is_admin = fields.BooleanField(default=False, description="管理员权限")
|
||||||
|
|
||||||
|
|
|
||||||
4
main.py
4
main.py
|
|
@ -13,7 +13,7 @@ from app.api.translator import translator_router
|
||||||
from app.api.user.routes import users_router
|
from app.api.user.routes import users_router
|
||||||
from app.core.redis import init_redis, close_redis
|
from app.core.redis import init_redis, close_redis
|
||||||
from app.utils.phone_encrypt import PhoneEncrypt
|
from app.utils.phone_encrypt import PhoneEncrypt
|
||||||
from settings import ONLINE_SETTINGS
|
from settings import TORTOISE_ORM
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
|
|
@ -41,7 +41,7 @@ app.add_middleware(
|
||||||
|
|
||||||
register_tortoise(
|
register_tortoise(
|
||||||
app=app,
|
app=app,
|
||||||
config=ONLINE_SETTINGS,
|
config=TORTOISE_ORM,
|
||||||
)
|
)
|
||||||
|
|
||||||
app.include_router(users_router, tags=["User API"], prefix="/users")
|
app.include_router(users_router, tags=["User API"], prefix="/users")
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,13 @@ dependencies = [
|
||||||
"aiosqlite==0.21.0",
|
"aiosqlite==0.21.0",
|
||||||
"annotated-types==0.7.0",
|
"annotated-types==0.7.0",
|
||||||
"anyio==4.10.0",
|
"anyio==4.10.0",
|
||||||
|
"async-timeout==5.0.1",
|
||||||
"asyncclick==8.2.2.2",
|
"asyncclick==8.2.2.2",
|
||||||
"asyncmy==0.2.10",
|
"asyncmy==0.2.10",
|
||||||
"bcrypt==4.3.0",
|
"bcrypt==4.3.0",
|
||||||
|
"certifi==2025.8.3",
|
||||||
"cffi==1.17.1",
|
"cffi==1.17.1",
|
||||||
|
"charset-normalizer==3.4.3",
|
||||||
"click==8.2.1",
|
"click==8.2.1",
|
||||||
"colorama==0.4.6",
|
"colorama==0.4.6",
|
||||||
"cryptography==45.0.6",
|
"cryptography==45.0.6",
|
||||||
|
|
@ -23,22 +26,28 @@ dependencies = [
|
||||||
"fastapi==0.116.1",
|
"fastapi==0.116.1",
|
||||||
"fugashi==1.5.1",
|
"fugashi==1.5.1",
|
||||||
"h11==0.16.0",
|
"h11==0.16.0",
|
||||||
|
"httpcore==1.0.9",
|
||||||
|
"httpx==0.28.1",
|
||||||
"idna==3.10",
|
"idna==3.10",
|
||||||
"iso8601==2.1.0",
|
"iso8601==2.1.0",
|
||||||
"jaconv==0.4.0",
|
"jaconv==0.4.0",
|
||||||
"numpy==2.3.2",
|
"numpy==2.3.2",
|
||||||
"openpyxl==3.1.5",
|
"openpyxl==3.1.5",
|
||||||
"pandas==2.3.1",
|
"pandas==2.3.1",
|
||||||
|
"pandas-stubs==2.3.2.250827",
|
||||||
"pyasn1==0.6.1",
|
"pyasn1==0.6.1",
|
||||||
"pycparser==2.22",
|
"pycparser==2.22",
|
||||||
|
"pycryptodome==3.23.0",
|
||||||
"pydantic==2.11.7",
|
"pydantic==2.11.7",
|
||||||
"pydantic-core==2.33.2",
|
"pydantic-core==2.33.2",
|
||||||
"pykakasi==2.3.0",
|
"pykakasi==2.3.0",
|
||||||
"pypika-tortoise==0.6.1",
|
"pypika-tortoise==0.6.1",
|
||||||
"python-dateutil==2.9.0.post0",
|
"python-dateutil==2.9.0.post0",
|
||||||
|
"python-dotenv==1.1.1",
|
||||||
"python-jose==3.5.0",
|
"python-jose==3.5.0",
|
||||||
"pytz==2025.2",
|
"pytz==2025.2",
|
||||||
"redis==6.4.0",
|
"redis==6.4.0",
|
||||||
|
"requests==2.32.5",
|
||||||
"rsa==4.9.1",
|
"rsa==4.9.1",
|
||||||
"six==1.17.0",
|
"six==1.17.0",
|
||||||
"sniffio==1.3.1",
|
"sniffio==1.3.1",
|
||||||
|
|
@ -47,7 +56,8 @@ dependencies = [
|
||||||
"typing-extensions==4.14.1",
|
"typing-extensions==4.14.1",
|
||||||
"typing-inspection==0.4.1",
|
"typing-inspection==0.4.1",
|
||||||
"tzdata==2025.2",
|
"tzdata==2025.2",
|
||||||
"unidic-lite>=1.0.8",
|
"unidic-lite==1.0.8",
|
||||||
|
"urllib3==2.5.0",
|
||||||
"uvicorn==0.35.0",
|
"uvicorn==0.35.0",
|
||||||
"wrapt==1.17.3",
|
"wrapt==1.17.3",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ ONLINE_SETTINGS = {
|
||||||
'app.models.base',
|
'app.models.base',
|
||||||
'app.models.fr',
|
'app.models.fr',
|
||||||
'app.models.jp',
|
'app.models.jp',
|
||||||
'app.models.comments'
|
'app.models.comments',
|
||||||
'aerich.models' # aerich自带模型类(必须填入)
|
'aerich.models' # aerich自带模型类(必须填入)
|
||||||
],
|
],
|
||||||
'default_connection': 'default',
|
'default_connection': 'default',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue