reset password form 1.0

This commit is contained in:
2025-09-25 17:54:34 +03:00
parent 7023854723
commit c4fc002124
11 changed files with 303 additions and 12 deletions

7
.env
View File

@@ -10,4 +10,9 @@ ACCESS_TOKEN_EXPIRE_MINUTES=600
ALLOW_ORIGINS=* ALLOW_ORIGINS=*
ALLOW_CREDENTIALS=True ALLOW_CREDENTIALS=True
ALLOW_METHODS=* ALLOW_METHODS=*
ALLOW_HEADERS=* ALLOW_HEADERS=*
#Почта
SMTP_DOMAIN=domain.example.com
SMTP_PORT=465
MAIL_LOGIN=login.example.com
MAIL_PASSWORD=password.example.com

View File

@@ -4,5 +4,5 @@ pytest == 8.4.1
aiosqlite == 0.21.0 aiosqlite == 0.21.0
greenlet == 3.2.4 greenlet == 3.2.4
passlib == 1.7.4 passlib == 1.7.4
bcrypt == 4.3.0 bcrypt == 4.0.1
python-jose[cryptography] == 3.5.0 python-jose[cryptography] == 3.5.0

5
run.py
View File

@@ -4,4 +4,7 @@ import asyncio
from server.database import db from server.database import db
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(db.main()) asyncio.run(db.main())
uvicorn.run("server.backend.endpoints:api", host="127.0.0.1", port=8000, reload=True) uvicorn.run("server.backend.endpoints:api", host="127.0.0.1", port=8000, reload=True)
#ps aux | grep uvicorn
# kill -9 pid

View File

@@ -2,7 +2,7 @@ from fastapi import FastAPI, HTTPException, status, Depends
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from . import pydentic, JWT from . import pydentic, JWT, password
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pydantic import EmailStr from pydantic import EmailStr
from server.database import db from server.database import db
@@ -37,25 +37,25 @@ async def get_all_rows(current_user: str = Depends(JWT.current_user)):
else: else:
raise HTTPException(status_code=404, detail="The user isn't found") raise HTTPException(status_code=404, detail="The user isn't found")
@api.get("/get_user_by_email/{email}", response_model=pydentic.CreateUser) @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) user = await db.get_user_by_email(email)
if user: if user:
return user return user
else: else:
raise HTTPException(status_code=404, detail="The user isn't found") 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): async def create_user(row:pydentic.CreateUser):
new_row = pydentic.CreateUser(email=row.email, description=row.description, activated = row.activated, password = row.password) new_row = pydentic.CreateUser(email=row.email, description=row.description, activated = row.activated, password = row.password)
await db.create_user(new_row) await db.create_user(new_row)
return 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)): async def delete_user(email:str,current_user: str = Depends(JWT.current_user)):
user = await db.get_user_by_email(email) user = await db.get_user_by_email(email)
if not user: if not user:
raise HTTPException(status_code=404, detail="The user isn't found") raise HTTPException(status_code=404, detail="The user isn't found")
await db.delete_user(email) await db.delete_user(email)
return user 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)): 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) user = await db.get_user_by_email(email)
if not user: if not user:
@@ -89,4 +89,15 @@ async def login_user(form_data: OAuth2PasswordRequestForm = Depends()):
{"sub": user.email}, {"sub": user.email},
timedelta(minutes=JWT.ACCESS_TOKEN_EXPIRE_MINUTES) timedelta(minutes=JWT.ACCESS_TOKEN_EXPIRE_MINUTES)
) )
return {"access_token": access_token, "token_type": "bearer"} 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

View File

@@ -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

View File

@@ -36,5 +36,9 @@ class UserUpdate(BaseModel):
class UserLogin(BaseModel): class UserLogin(BaseModel):
email:EmailStr = Field(..., min_length=6, max_length=254, description="user's email") email:EmailStr = Field(..., min_length=6, max_length=254, description="user's email")
password:str = Field(..., description="Password") password:str = Field(..., description="Password")
class UserLogout(BaseModel): class UserReset(BaseModel):
email:EmailStr = Field(..., min_length=6, max_length=254, description="user's email") 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)

View File

@@ -77,5 +77,14 @@ async def login_user(user_info):
if user and verify_password(user_info.password, user.password): if user and verify_password(user_info.password, user.password):
return user return user
return None 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(): async def main():
await init_db() await init_db()

View File

@@ -16,7 +16,7 @@
<div class="options"> <div class="options">
<input type="checkbox" id="remember" name="remember"> <input type="checkbox" id="remember" name="remember">
<label for="remember"> Remember me</label> <label for="remember"> Remember me</label>
<a href="#">Forgot Password?</a> <a href="./../reset/index.html" id="reset">Forgot Password?</a>
</div> </div>
<button type="submit">Login</button> <button type="submit">Login</button>
<p>Don't have an account? <a href="./../register/index.html" id="register">Register</a></p> <p>Don't have an account? <a href="./../register/index.html" id="register">Register</a></p>

View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div class="glass-container">
<div class="login-box">
<h2>Login</h2>
<form id="loginForm">
<input type="text" id="email" name="email" required placeholder="Email">
<button type="submit">Reset</button>
<p>Have an account? <a href="./../login/" id="login">Login</a></p>
</form>
</div>
</div>
<script src="js.js"></script>
</body>
</html>

66
server/front/reset/js.js Normal file
View File

@@ -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);
});
}

View File

@@ -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;
}