diff --git a/.env b/.env index 97092e1..c28ea21 100644 --- a/.env +++ b/.env @@ -10,4 +10,9 @@ ACCESS_TOKEN_EXPIRE_MINUTES=600 ALLOW_ORIGINS=* ALLOW_CREDENTIALS=True ALLOW_METHODS=* -ALLOW_HEADERS=* \ No newline at end of file +ALLOW_HEADERS=* +#Почта +SMTP_DOMAIN=domain.example.com +SMTP_PORT=465 +MAIL_LOGIN=login.example.com +MAIL_PASSWORD=password.example.com diff --git a/requirements b/requirements index fa64fe4..ac96079 100644 --- a/requirements +++ b/requirements @@ -4,5 +4,5 @@ pytest == 8.4.1 aiosqlite == 0.21.0 greenlet == 3.2.4 passlib == 1.7.4 -bcrypt == 4.3.0 +bcrypt == 4.0.1 python-jose[cryptography] == 3.5.0 \ No newline at end of file diff --git a/run.py b/run.py index 882519a..b1a20ea 100644 --- a/run.py +++ b/run.py @@ -4,4 +4,7 @@ import asyncio from server.database import db if __name__ == "__main__": asyncio.run(db.main()) - uvicorn.run("server.backend.endpoints:api", host="127.0.0.1", port=8000, reload=True) \ No newline at end of file + uvicorn.run("server.backend.endpoints:api", host="127.0.0.1", port=8000, reload=True) + +#ps aux | grep uvicorn +# kill -9 pid \ No newline at end of file diff --git a/server/backend/endpoints.py b/server/backend/endpoints.py index 0faf14e..dd73dc2 100644 --- a/server/backend/endpoints.py +++ b/server/backend/endpoints.py @@ -2,7 +2,7 @@ from fastapi import FastAPI, HTTPException, status, Depends from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from fastapi.security import OAuth2PasswordRequestForm -from . import pydentic, JWT +from . import pydentic, JWT, password from datetime import datetime, timedelta from pydantic import EmailStr from server.database import db @@ -37,25 +37,25 @@ async def get_all_rows(current_user: str = Depends(JWT.current_user)): else: raise HTTPException(status_code=404, detail="The user isn't found") @api.get("/get_user_by_email/{email}", response_model=pydentic.CreateUser) -async def GetUserbyEmail(email:str, current_user: str = Depends(JWT.current_user)): +async def get_user_by_email(email:str, current_user: str = Depends(JWT.current_user)): user = await db.get_user_by_email(email) if user: return user else: raise HTTPException(status_code=404, detail="The user isn't found") -@api.post("/user_create", response_model=pydentic.CreateUser) +@api.post("/user_create", response_model=pydentic.UsersInfo) async def create_user(row:pydentic.CreateUser): new_row = pydentic.CreateUser(email=row.email, description=row.description, activated = row.activated, password = row.password) await db.create_user(new_row) return new_row -@api.delete("/user_delete/{email}", response_model=pydentic.CreateUser) +@api.delete("/user_delete/{email}", response_model=pydentic.UsersInfo) async def delete_user(email:str,current_user: str = Depends(JWT.current_user)): user = await db.get_user_by_email(email) if not user: raise HTTPException(status_code=404, detail="The user isn't found") await db.delete_user(email) return user -@api.put("/user_update/{email}", response_model=pydentic.CreateUser) +@api.put("/user_update/{email}", response_model=pydentic.UsersInfo) async def update_user(email:str, updated_row: pydentic.UserUpdate, current_user: str = Depends(JWT.current_user)): user = await db.get_user_by_email(email) if not user: @@ -89,4 +89,15 @@ async def login_user(form_data: OAuth2PasswordRequestForm = Depends()): {"sub": user.email}, timedelta(minutes=JWT.ACCESS_TOKEN_EXPIRE_MINUTES) ) - return {"access_token": access_token, "token_type": "bearer"} \ No newline at end of file + return {"access_token": access_token, "token_type": "bearer"} +@api.post("/reset", response_model=pydentic.UsersInfo) +async def reset_user(row:pydentic.UserReset): + user = await db.get_user_by_email(row.email) + if not user: + raise HTTPException(status_code=401, detail="The user isn't found") + new_password = password.generate_password() + new_row = pydentic.UserReset(email=row.email, new_password=new_password) + password.send_password(new_row) + user = await db.reset_user(new_row) + return user + diff --git a/server/backend/password.py b/server/backend/password.py new file mode 100644 index 0000000..c0c347b --- /dev/null +++ b/server/backend/password.py @@ -0,0 +1,69 @@ +import os +import string +import secrets +import smtplib +from email.message import EmailMessage +from dotenv import load_dotenv + + +def generate_password(length: int = 12) -> str: + """Генерация пароля: минимум 1 буква, 1 цифра и 1 спецсимвол""" + if length < 3: + raise ValueError("Длина пароля должна быть минимум 3 символа") + + # обязательные категории + password = [ + secrets.choice(string.ascii_letters), + secrets.choice(string.digits), + secrets.choice(string.punctuation), + ] + + # остальные символы + all_chars = string.ascii_letters + string.digits + string.punctuation + password += [secrets.choice(all_chars) for _ in range(length - 3)] + + # перемешиваем + secrets.SystemRandom().shuffle(password) + return "".join(password) + + +load_dotenv() + + +def send_password(user_info): + smtp_domain = os.getenv("SMTP_DOMAIN") + smtp_port = int(os.getenv("SMTP_PORT", "587")) + mail_login = os.getenv("MAIL_LOGIN") + mail_password = os.getenv("MAIL_PASSWORD") + + msg = EmailMessage() + msg["From"] = mail_login + msg["To"] = user_info.email + msg["Subject"] = "Ваш новый пароль" + msg.set_content( + f"Здравствуйте!\n\n" + f"Ваш новый пароль: {user_info.new_password}\n\n" + "Рекомендуем сразу его сменить." + ) + + try: + if smtp_port == 465: + # SSL-соединение сразу + with smtplib.SMTP_SSL(smtp_domain, smtp_port, timeout=10) as smtp: + smtp.login(mail_login, mail_password) + smtp.send_message(msg) + else: + # STARTTLS (обычно порт 587) + with smtplib.SMTP(smtp_domain, smtp_port, timeout=10) as smtp: + smtp.ehlo() + smtp.starttls() + smtp.ehlo() + smtp.login(mail_login, mail_password) + smtp.send_message(msg) + + print(f"Пароль отправлен на {user_info.email}") + + except Exception as e: + # Логируй ошибку, но не пались паролем в логах! + print(f"Ошибка при отправке письма: {e}") + raise \ No newline at end of file diff --git a/server/backend/pydentic.py b/server/backend/pydentic.py index 9284e3b..3490aaa 100644 --- a/server/backend/pydentic.py +++ b/server/backend/pydentic.py @@ -36,5 +36,9 @@ class UserUpdate(BaseModel): class UserLogin(BaseModel): email:EmailStr = Field(..., min_length=6, max_length=254, description="user's email") password:str = Field(..., description="Password") -class UserLogout(BaseModel): - email:EmailStr = Field(..., min_length=6, max_length=254, description="user's email") \ No newline at end of file +class UserReset(BaseModel): + email:EmailStr = Field(..., min_length=6, max_length=254, description="user's email") + new_password:constr(min_length=8) = Field(None,description="New_password") + @validator('new_password') + def password_validator(cls, new_password): + return check_password_complexity(cls, new_password) diff --git a/server/database/db.py b/server/database/db.py index cfd4cc7..7f700bd 100644 --- a/server/database/db.py +++ b/server/database/db.py @@ -77,5 +77,14 @@ async def login_user(user_info): if user and verify_password(user_info.password, user.password): return user return None +async def reset_user(user_info): + async with AsyncSessionLocal() as session: + result = await session.execute(select(User).where(User.email == user_info.email)) + user = result.scalar_one_or_none() + if user: + user.password = hash_password(user_info.new_password) + await session.commit() + return user + return None async def main(): await init_db() \ No newline at end of file diff --git a/server/front/login/index.html b/server/front/login/index.html index f202c7c..989ff9f 100644 --- a/server/front/login/index.html +++ b/server/front/login/index.html @@ -16,7 +16,7 @@
Don't have an account? Register
diff --git a/server/front/reset/index.html b/server/front/reset/index.html new file mode 100644 index 0000000..b292c95 --- /dev/null +++ b/server/front/reset/index.html @@ -0,0 +1,22 @@ + + + + + +