From c4fc002124fc953526b36fb7826d17d77a5568b0 Mon Sep 17 00:00:00 2001 From: "MH.Dmitrii" Date: Thu, 25 Sep 2025 17:54:34 +0300 Subject: [PATCH] reset password form 1.0 --- .env | 7 ++- requirements | 2 +- run.py | 5 +- server/backend/endpoints.py | 23 ++++++-- server/backend/password.py | 69 +++++++++++++++++++++++ server/backend/pydentic.py | 8 ++- server/database/db.py | 9 +++ server/front/login/index.html | 2 +- server/front/reset/index.html | 22 ++++++++ server/front/reset/js.js | 66 ++++++++++++++++++++++ server/front/reset/style.css | 102 ++++++++++++++++++++++++++++++++++ 11 files changed, 303 insertions(+), 12 deletions(-) create mode 100644 server/backend/password.py create mode 100644 server/front/reset/index.html create mode 100644 server/front/reset/js.js create mode 100644 server/front/reset/style.css 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 @@
- Forgot Password? + Forgot Password?

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 @@ + + + + + + Login + + + +
+ +
+ + + \ No newline at end of file diff --git a/server/front/reset/js.js b/server/front/reset/js.js new file mode 100644 index 0000000..d858b77 --- /dev/null +++ b/server/front/reset/js.js @@ -0,0 +1,66 @@ +function getToken() { + return localStorage.getItem("token") || sessionStorage.getItem("token"); +} +function tokenCheck(){ + const token = getToken(); + if (!token) { + window.location.href = "./../main/index.html"; + } +} +document.getElementById('loginForm').addEventListener('submit', async function (e) { + e.preventDefault(); + const email = document.getElementById('email').value; + const userData = { + email + }; + try { + const response = await fetch("http://localhost:8000/reset", { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(userData) + }); + const data = await response.json(); // читаем только один раз + if (response.ok) { // сохраняем только при успехе + window.location.href = './../login/index.html'; + } else { //парсинг и вывод ошибок, если есть + if (Array.isArray(data.detail)) { + const messages = data.detail.map(e => { + const field = e.loc.filter(locPart => locPart !== 'body').join(' -> '); + return `${field}: ${e.msg}`; + }); + showError(messages); + } else if (typeof data.detail === 'string') { + showError([data.detail]); + } else { + showError(['Unknown error']); + } + } + } catch (err) { + showError(['Connection error: ' + err.message]); + } +}); +function showError(messages){ //Добавление их на form со стилями + let errorElem = document.getElementById('formError'); + let container = document.getElementById('glass-container'); + if (!errorElem){ + errorElem = document.createElement('div'); + errorElem.style.transition="3s"; + errorElem.id = 'formError'; + errorElem.style.color = 'red'; + errorElem.style.marginTop = '20px'; + errorElem.style.fontSize = '14px'; + errorElem.style.fontWeight = '100'; + errorElem.style.marginBottom = '20px'; + errorElem.style.lineHeight="120%"; + errorElem.style.height = 'auto'; + const form = document.getElementById('loginForm'); + form.insertAdjacentElement('afterend', errorElem); + }; + errorElem.innerHTML = ''; + messages.forEach(msg => { + const li = document.createElement('li'); + li.style.listStyleType="none"; + li.textContent = msg; + errorElem.appendChild(li); + }); +} \ No newline at end of file diff --git a/server/front/reset/style.css b/server/front/reset/style.css new file mode 100644 index 0000000..6bbaba5 --- /dev/null +++ b/server/front/reset/style.css @@ -0,0 +1,102 @@ +* { + margin: 0; + box-sizing: border-box; + font-family: 'Poppins', sans-serif; +} + +body { + margin: 0; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + background-image: url('./../login/background-image.jpeg'); + background-size: cover; +} + +.glass-container { + width: 300px; + height: 350px; + position: relative; + z-index: 1; + background: rgba(255, 255, 255, 0.1); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + border-radius: 10px; + border: 1px solid #fff; +} + +.glass-container::before { + content: ''; + position: absolute; + width: 100%; + height: 100%; + border-radius: 10px; + backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); + z-index: -1; +} + +.login-box { + max-width: 250px; + margin: 0 auto; + text-align: center; +} + +h2 { + color: #fff; + margin-top: 30px; + margin-bottom: -20px; +} + +form { + display: flex; + flex-direction: column; + margin-top: 20px; +} + +input { + padding: 10px; + margin-top: 25px; + border: none; + border-radius: 10px; + background: transparent; + border: 1px solid #fff; + color: #fff; + font-size: 13px; +} + +input::placeholder { + color: #fff; +} + +input:focus { + outline: none; +} + +button { + background: #fff; + color: black; + padding: 10px; + border: none; + border-radius: 10px; + cursor: pointer; + margin-top: 15px; +} + +button:hover { + background: transparent; + color: white; + outline: 1px solid #fff; +} + +p { + font-size: 12px; + color: #fff; + margin-top: 15px; +} + +#login { + text-decoration: none; + color: #fff; + font-weight: bold; +}