Compare commits

..

23 Commits
1.5.2 ... 1.6.1

Author SHA1 Message Date
f56eb9db92 Merge pull request 'fix if' (#57) from feature/persist-token into master
Reviewed-on: #57
2025-03-06 22:13:04 +00:00
15062c029f fix if 2025-03-06 23:12:28 +01:00
6c51c7469b Merge pull request 'add persist token' (#56) from feature/persist-token into master
Reviewed-on: #56
2025-03-06 21:46:00 +00:00
952b0211ba add persist token 2025-03-06 22:42:26 +01:00
ece35338da Merge pull request 'rollback' (#55) from feature/oauth into master
Reviewed-on: #55
2025-03-06 21:01:56 +00:00
221bd1e244 rollback 2025-03-06 22:00:52 +01:00
de60dee3eb Merge pull request 'feature/oauth' (#54) from feature/oauth into master
Reviewed-on: #54
2025-03-01 20:49:13 +00:00
4669774cc3 add env facebook 2025-03-01 21:45:10 +01:00
a094b56d44 add google client id 2025-03-01 14:02:11 +01:00
3e514acb19 add oauth wip 2025-02-24 20:45:52 +01:00
a34ba04f78 Merge pull request 'fix password update' (#53) from feature/passwordForgot into master
Reviewed-on: #53
2025-02-15 11:09:12 +00:00
8f3f2d0f98 fix password update 2025-02-15 12:07:58 +01:00
9ed5a41d32 Merge pull request 'feature/passwordForgot' (#52) from feature/passwordForgot into master
Reviewed-on: #52
2025-02-15 10:59:06 +00:00
b26dcc8777 fix password reset 2025-02-15 11:56:44 +01:00
6a21fd010d debug reset password 2025-02-15 11:18:53 +01:00
e9c558abe1 Merge pull request 'remove decode' (#51) from feature/passwordForgot into master
Reviewed-on: #51
2025-02-15 10:07:56 +00:00
2bac1f4d39 remove decode 2025-02-15 00:07:33 +01:00
5304cccb0f Merge pull request 'change address' (#50) from feature/passwordForgot into master
Reviewed-on: #50
2025-02-14 22:52:38 +00:00
32a723514b change address 2025-02-14 23:42:53 +01:00
4a38d795fc Merge pull request 'feature/passwordForgot' (#49) from feature/passwordForgot into master
Reviewed-on: #49
2025-02-14 20:06:44 +00:00
9ab9058927 fix password 2025-02-13 20:41:16 +01:00
672eeea110 password forgot / reset / update 2025-02-12 22:45:59 +01:00
6f69dde171 add password 2025-02-12 22:18:57 +01:00
8 changed files with 376 additions and 3 deletions

View File

@@ -12,11 +12,20 @@ from ..dependencies import database, cookie
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = cookie.OAuth2PasswordBearerWithCookie(tokenUrl="token")
def create_access_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)

View File

@@ -1,7 +1,7 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from .routers import users, token, mail, events, tags
from .routers import users, token, mail, events, tags, password
from .dependencies import user_add
import os
@@ -30,7 +30,7 @@ app.include_router(token.router)
app.include_router(mail.router)
app.include_router(events.router)
app.include_router(tags.router)
app.include_router(password.router)
@app.on_event("startup")
async def startup_event():

View File

@@ -46,6 +46,9 @@ class UserCreate(BaseModel):
firstName: str
name: str
class UserForgotPassword(BaseModel):
email: EmailStr
class UserInDB(User):
password: str

137
app/routers/password.py Normal file
View File

@@ -0,0 +1,137 @@
from fastapi import APIRouter, HTTPException, status, Request, Form
from fastapi.templating import Jinja2Templates
from ..dependencies import users_token, database, mail
from ..models import users, email
from fastapi.responses import JSONResponse, HTMLResponse
from fastapi_mail import MessageSchema, MessageType, FastMail
import random, os, bcrypt
router = APIRouter()
# Assurer que le chemin vers "templates" est correct
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
templates = Jinja2Templates(directory=os.path.join(BASE_DIR, "templates"))
@router.post("/password/forgot", tags=["password"])
async def forgot_password(userSingle: users.UserForgotPassword):
if not userSingle.email:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email is required"
)
# Recherche de l'utilisateur dans la base de données
user_repository = users.UserRepository(database=database.database)
user = user_repository.find_one_by({"email": {"$eq": userSingle.email}})
if user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
# Génération d'un token temporaire pour réinitialisation
reset_token = str(random.randint(100000, 999999))
key_hashed = users_token.get_password_hash(reset_token)
# Créer le lien de réinitialisation
reset_link = f"https://backend.valczeryba.ovh/password/reset?key={reset_token}&email={user.email}"
# Préparer les données à envoyer au template
email_body = {
"username": user.username,
"reset_link": reset_link
}
# Créer le message à envoyer
message = MessageSchema(
subject="Password Reset Request",
recipients=[user.email],
template_body=email_body,
subtype=MessageType.html,
)
# Utilisation de FastMail pour envoyer l'email
fm = FastMail(mail.conf)
await fm.send_message(message, template_name="forgot_password_email.html")
# Stockage du token temporaire dans Redis avec une expiration d'1 heure
database.connect_redis.setex(user.email, 3600, key_hashed)
return JSONResponse(status_code=status.HTTP_200_OK, content={"message": "Password reset email has been sent"})
@router.get("/password/reset", tags=["password"])
async def reset_password(request: Request, key: str | None = None, email: str | None = None):
if not key or not email:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Parameters 'key' and 'email' are required"
)
# Récupérer la clé hachée depuis Redis
key_hashed = database.connect_redis.get(email)
if key_hashed is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid or expired reset key"
)
# Redis stocke les valeurs en `bytes`, donc il faut décoder si nécessaire
if isinstance(key_hashed, bytes):
key_hashed = key_hashed.decode()
# Vérifier que la clé en clair correspond au hash stocké
if not bcrypt.checkpw(key.encode(), key_hashed.encode()):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid reset key"
)
# Afficher la page HTML de réinitialisation du mot de passe
return templates.TemplateResponse("reset_password.html", {"request": request, "email": email, "key": key})
@router.post("/password/update", tags=["password"])
async def update_password(request: Request, email: str = Form(...), key: str = Form(...), new_password: str = Form(...)): # Vérification du token dans Redis
# Récupérer la clé hachée depuis Redis
key_hashed = database.connect_redis.get(email)
if key_hashed is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid or expired reset key"
)
# Redis stocke les valeurs en `bytes`, donc il faut décoder si nécessaire
if isinstance(key_hashed, bytes):
key_hashed = key_hashed.decode()
# Vérifier que la clé en clair correspond au hash stocké
if not bcrypt.checkpw(key.encode(), key_hashed.encode()):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid reset key"
)
# Recherche de l'utilisateur dans la base de données
user_repository = users.UserRepository(database=database.database)
user = user_repository.find_one_by({"email": {"$eq": email}})
if user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
# Mise à jour du mot de passe de l'utilisateur
user.password = users_token.get_password_hash(new_password)
user_repository.save(user)
# Suppression du token temporaire dans Redis
database.connect_redis.delete(email)
# Afficher un message de succès dans une réponse HTML
return templates.TemplateResponse("password_update_success.html", {"request": request, "email": email})

View File

@@ -15,13 +15,16 @@ ACCESS_TOKEN_EXPIRE_MINUTES = 30
async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
user = users_token.authenticate_user(form_data.username, form_data.password)
expires_access_token_time = ACCESS_TOKEN_EXPIRE_MINUTES
if form_data.remember_me.lower() in ["true", "1", "yes"]:
expires_access_token_time=120
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token_expires = timedelta(minutes=expires_access_token_time)
access_token = users_token.create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)

View File

@@ -0,0 +1,69 @@
<!-- forgot_password_email.html -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Réinitialisation de votre mot de passe</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f9;
color: #333;
margin: 0;
padding: 0;
}
.container {
width: 100%;
max-width: 600px;
margin: 50px auto;
padding: 20px;
background-color: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
h1 {
color: #4CAF50;
text-align: center;
}
p {
font-size: 16px;
line-height: 1.5;
text-align: center;
}
.cta-button {
display: inline-block;
background-color: #4CAF50;
color: white;
padding: 10px 20px;
text-decoration: none;
border-radius: 4px;
font-weight: bold;
text-align: center;
}
.cta-button:hover {
background-color: #45a049;
}
.footer {
margin-top: 20px;
font-size: 14px;
color: #888;
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<h1>Demande de réinitialisation du mot de passe</h1>
<p>Bonjour {{ username }},</p>
<p>Nous avons reçu une demande pour réinitialiser votre mot de passe sur notre site. Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer cet email.</p>
<p>Pour réinitialiser votre mot de passe, cliquez sur le lien ci-dessous :</p>
<p><a href="{{ reset_link }}" class="cta-button">Réinitialiser mon mot de passe</a></p>
<p>Le lien est valable pendant une heure. Si vous ne pouvez pas cliquer dessus, copiez et collez-le dans votre navigateur.</p>
</div>
<div class="footer">
<p>&copy; Covas - Tous droits réservés.</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,65 @@
<!-- password_update_success.html -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mot de Passe Mis à Jour</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f9;
color: #333;
margin: 0;
padding: 0;
}
.container {
width: 100%;
max-width: 600px;
margin: 50px auto;
padding: 20px;
background-color: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
h1 {
color: #4CAF50;
text-align: center;
}
p {
font-size: 16px;
line-height: 1.5;
text-align: center;
}
.cta-button {
display: inline-block;
background-color: #4CAF50;
color: white;
padding: 10px 20px;
text-decoration: none;
border-radius: 4px;
font-weight: bold;
text-align: center;
}
.cta-button:hover {
background-color: #45a049;
}
.footer {
margin-top: 20px;
font-size: 14px;
color: #888;
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<h1>Votre mot de passe a été mis à jour avec succès</h1>
<p>Votre mot de passe a été réinitialisé avec succès. Vous pouvez maintenant utiliser votre nouveau mot de passe pour vous connecter.</p>
</div>
<div class="footer">
<p>&copy; {{ current_year }} Covas - Tous droits réservés.</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Réinitialisation du mot de passe</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f7fc;
color: #333;
}
.container {
width: 100%;
max-width: 600px;
margin: 50px auto;
padding: 20px;
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
h2 {
text-align: center;
color: #2a9d8f;
}
.form-group {
margin-bottom: 20px;
}
label {
font-weight: bold;
display: block;
margin-bottom: 5px;
}
input[type="password"], input[type="email"] {
width: 100%;
padding: 10px;
margin: 5px 0 10px 0;
border: 1px solid #ddd;
border-radius: 4px;
}
input[type="submit"] {
background-color: #2a9d8f;
color: white;
border: none;
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
border-radius: 4px;
width: 100%;
}
input[type="submit"]:hover {
background-color: #1e7c68;
}
.message {
text-align: center;
margin-top: 20px;
}
.message a {
color: #2a9d8f;
text-decoration: none;
}
</style>
</head>
<body>
<div class="container">
<h2>Réinitialisation du mot de passe</h2>
<form method="post" action="/password/update">
<input type="hidden" name="email" value="{{ email }}">
<input type="hidden" name="key" value="{{ key }}">
<div class="form-group">
<label for="new_password">Nouveau mot de passe :</label>
<input type="password" id="new_password" name="new_password" required>
</div>
<input type="submit" value="Mettre à jour le mot de passe">
</form>
<div class="message">
<p>Vous avez des questions ? <a href="mailto:support@votresite.com">Contactez-nous</a></p>
</div>
</div>
</body>
</html>