Add one-time password recovery flow
This commit is contained in:
@@ -25,6 +25,7 @@ from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
|
||||
from app import database
|
||||
from app import auth
|
||||
from app import password_reset_email
|
||||
from app.model_provider import get_model_provider
|
||||
from app import workspace
|
||||
from app import skills
|
||||
@@ -295,6 +296,15 @@ class PasswordCredentials(BaseModel):
|
||||
password: str = Field(min_length=12, max_length=256)
|
||||
|
||||
|
||||
class PasswordResetRequest(BaseModel):
|
||||
email: str = Field(min_length=3, max_length=254)
|
||||
|
||||
|
||||
class PasswordResetCompletion(BaseModel):
|
||||
token: str = Field(min_length=32, max_length=256)
|
||||
new_password: str = Field(min_length=12, max_length=256)
|
||||
|
||||
|
||||
class WebReadRequest(BaseModel):
|
||||
url: str = Field(min_length=8, max_length=2048, description="رابط صفحة ويب عامة تريد تحليلها")
|
||||
question: str = Field(default="لخّص محتوى الصفحة وأهم نقاطها.", min_length=1, max_length=2000)
|
||||
@@ -1209,6 +1219,49 @@ def register_account(credentials: PasswordCredentials, request: Request) -> dict
|
||||
return _auth_response(user_id, email, "account")
|
||||
|
||||
|
||||
@app.post("/v1/auth/password-reset/request", status_code=202)
|
||||
def request_password_reset(payload: PasswordResetRequest, request: Request) -> dict[str, str]:
|
||||
try:
|
||||
email = auth.normalize_email(payload.email)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||||
if not password_reset_email.smtp_configured():
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="استعادة كلمة المرور غير مهيأة؛ أعد لاحقًا بعد إعداد SMTP.",
|
||||
)
|
||||
client_host = request.client.host if request.client is not None else "unknown"
|
||||
retry_after = auth.password_reset_retry_after(email, client_host)
|
||||
if retry_after:
|
||||
raise HTTPException(
|
||||
status_code=429,
|
||||
detail="طلبات الاستعادة كثيرة؛ حاول بعد انتهاء المهلة.",
|
||||
headers={"Retry-After": str(retry_after)},
|
||||
)
|
||||
auth.record_password_reset_attempt(email, client_host)
|
||||
token = auth.issue_password_reset(email)
|
||||
try:
|
||||
password_reset_email.send_password_reset_email(email, token)
|
||||
except Exception as exc:
|
||||
# Do not log the destination address, token, SMTP transcript, or credentials.
|
||||
logger.warning("Password reset email delivery failed (%s)", type(exc).__name__)
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="تعذر إرسال رسالة الاستعادة الآن؛ حاول لاحقًا.",
|
||||
) from None
|
||||
return {
|
||||
"status": "accepted",
|
||||
"message": "إذا كان البريد مرتبطًا بحساب، فستصلك رسالة استعادة.",
|
||||
}
|
||||
|
||||
|
||||
@app.post("/v1/auth/password-reset/complete")
|
||||
def complete_password_reset(payload: PasswordResetCompletion) -> dict[str, str]:
|
||||
if not auth.reset_password(payload.token, payload.new_password):
|
||||
raise HTTPException(status_code=400, detail="رمز الاستعادة غير صالح أو منتهي الصلاحية.")
|
||||
return {"status": "password_reset"}
|
||||
|
||||
|
||||
@app.post("/v1/auth/login")
|
||||
def login_account(credentials: PasswordCredentials, request: Request) -> dict[str, Any]:
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user