Spaces:
Sleeping
Sleeping
Fix password hashing compatibility
Browse files- App/routers/admin/routes.py +2 -3
- App/routers/admin/seed.py +2 -3
- App/routers/users/routes.py +7 -6
- App/routers/users/utils.py +15 -0
- requirements.txt +2 -2
App/routers/admin/routes.py
CHANGED
|
@@ -2,7 +2,6 @@ from datetime import date
|
|
| 2 |
from typing import Optional
|
| 3 |
|
| 4 |
from fastapi import APIRouter, BackgroundTasks, Depends
|
| 5 |
-
from passlib.hash import bcrypt
|
| 6 |
from pydantic import BaseModel
|
| 7 |
|
| 8 |
from App.routers.admin.models import (
|
|
@@ -16,7 +15,7 @@ from App.routers.funds.models import FundFAQ, FundInfo, MutualFund
|
|
| 16 |
from App.routers.funds.runner import run_import
|
| 17 |
from App.routers.stocks.models import CorporateAction, Stock
|
| 18 |
from App.routers.users.models import User
|
| 19 |
-
from App.routers.users.utils import get_current_user
|
| 20 |
from App.schemas import AppException, ResponseModel
|
| 21 |
|
| 22 |
router = APIRouter(prefix="/admin", tags=["Admin"])
|
|
@@ -250,7 +249,7 @@ async def temporary_password(user_id: str, payload: PasswordResetPayload, admin:
|
|
| 250 |
user = await User.get_or_none(id=user_id)
|
| 251 |
if not user:
|
| 252 |
raise AppException(status_code=404, message="User not found")
|
| 253 |
-
user.hashed_password =
|
| 254 |
user.must_change_password = payload.must_change_password
|
| 255 |
await user.save()
|
| 256 |
await audit(admin, "temporary_password_reset", "user", user_id)
|
|
|
|
| 2 |
from typing import Optional
|
| 3 |
|
| 4 |
from fastapi import APIRouter, BackgroundTasks, Depends
|
|
|
|
| 5 |
from pydantic import BaseModel
|
| 6 |
|
| 7 |
from App.routers.admin.models import (
|
|
|
|
| 15 |
from App.routers.funds.runner import run_import
|
| 16 |
from App.routers.stocks.models import CorporateAction, Stock
|
| 17 |
from App.routers.users.models import User
|
| 18 |
+
from App.routers.users.utils import get_current_user, hash_password
|
| 19 |
from App.schemas import AppException, ResponseModel
|
| 20 |
|
| 21 |
router = APIRouter(prefix="/admin", tags=["Admin"])
|
|
|
|
| 249 |
user = await User.get_or_none(id=user_id)
|
| 250 |
if not user:
|
| 251 |
raise AppException(status_code=404, message="User not found")
|
| 252 |
+
user.hashed_password = hash_password(payload.temporary_password)
|
| 253 |
user.must_change_password = payload.must_change_password
|
| 254 |
await user.save()
|
| 255 |
await audit(admin, "temporary_password_reset", "user", user_id)
|
App/routers/admin/seed.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
-
from passlib.hash import bcrypt
|
| 2 |
-
|
| 3 |
from App.routers.users.models import User
|
|
|
|
| 4 |
|
| 5 |
DEFAULT_ADMIN_EMAIL = "MboneaMjema@gmail.com"
|
| 6 |
DEFAULT_ADMIN_PASSWORD = "123456789"
|
|
@@ -12,7 +11,7 @@ async def ensure_default_admin() -> User:
|
|
| 12 |
user = await User.create(
|
| 13 |
username="admin",
|
| 14 |
email=DEFAULT_ADMIN_EMAIL,
|
| 15 |
-
hashed_password=
|
| 16 |
is_admin=True,
|
| 17 |
is_active=True,
|
| 18 |
must_change_password=True,
|
|
|
|
|
|
|
|
|
|
| 1 |
from App.routers.users.models import User
|
| 2 |
+
from App.routers.users.utils import hash_password
|
| 3 |
|
| 4 |
DEFAULT_ADMIN_EMAIL = "MboneaMjema@gmail.com"
|
| 5 |
DEFAULT_ADMIN_PASSWORD = "123456789"
|
|
|
|
| 11 |
user = await User.create(
|
| 12 |
username="admin",
|
| 13 |
email=DEFAULT_ADMIN_EMAIL,
|
| 14 |
+
hashed_password=hash_password(DEFAULT_ADMIN_PASSWORD),
|
| 15 |
is_admin=True,
|
| 16 |
is_active=True,
|
| 17 |
must_change_password=True,
|
App/routers/users/routes.py
CHANGED
|
@@ -6,7 +6,6 @@ from datetime import datetime, timedelta, timezone
|
|
| 6 |
|
| 7 |
from fastapi import APIRouter, Depends
|
| 8 |
import httpx
|
| 9 |
-
from passlib.hash import bcrypt
|
| 10 |
|
| 11 |
from App.schemas import ResponseModel, AppException
|
| 12 |
from .models import User, Watchlist, PasswordResetCode
|
|
@@ -29,6 +28,8 @@ from .utils import (
|
|
| 29 |
build_totp_uri,
|
| 30 |
generate_totp_secret,
|
| 31 |
get_current_user,
|
|
|
|
|
|
|
| 32 |
verify_totp_code,
|
| 33 |
SECRET_KEY,
|
| 34 |
ALGORITHM,
|
|
@@ -95,7 +96,7 @@ async def register(payload: UserCreate):
|
|
| 95 |
user = await User.create(
|
| 96 |
username=payload.username,
|
| 97 |
email=payload.email,
|
| 98 |
-
hashed_password=
|
| 99 |
)
|
| 100 |
|
| 101 |
await Portfolio.create(user=user, name="Default Portfolio")
|
|
@@ -115,7 +116,7 @@ async def register(payload: UserCreate):
|
|
| 115 |
async def login(payload: UserLogin):
|
| 116 |
user = await User.get_or_none(email=payload.email)
|
| 117 |
|
| 118 |
-
if not user or not
|
| 119 |
raise AppException(status_code=400, message="Invalid email or password")
|
| 120 |
if not user.is_active:
|
| 121 |
raise AppException(status_code=403, message="User account is disabled")
|
|
@@ -196,7 +197,7 @@ async def request_password_reset(payload: ForgotPasswordRequest):
|
|
| 196 |
expires_at = datetime.now(timezone.utc) + timedelta(minutes=PASSWORD_RESET_CODE_EXPIRE_MINUTES)
|
| 197 |
await PasswordResetCode.create(
|
| 198 |
user=user,
|
| 199 |
-
code_hash=
|
| 200 |
expires_at=expires_at,
|
| 201 |
)
|
| 202 |
|
|
@@ -224,14 +225,14 @@ async def reset_password(payload: ResetPasswordRequest):
|
|
| 224 |
expires_at = reset_code.expires_at
|
| 225 |
if expires_at.tzinfo is None:
|
| 226 |
expires_at = expires_at.replace(tzinfo=timezone.utc)
|
| 227 |
-
if expires_at >= now and
|
| 228 |
matching_code = reset_code
|
| 229 |
break
|
| 230 |
|
| 231 |
if not matching_code:
|
| 232 |
raise AppException(status_code=400, message="Invalid or expired reset code")
|
| 233 |
|
| 234 |
-
user.hashed_password =
|
| 235 |
matching_code.used_at = now
|
| 236 |
await user.save()
|
| 237 |
await matching_code.save()
|
|
|
|
| 6 |
|
| 7 |
from fastapi import APIRouter, Depends
|
| 8 |
import httpx
|
|
|
|
| 9 |
|
| 10 |
from App.schemas import ResponseModel, AppException
|
| 11 |
from .models import User, Watchlist, PasswordResetCode
|
|
|
|
| 28 |
build_totp_uri,
|
| 29 |
generate_totp_secret,
|
| 30 |
get_current_user,
|
| 31 |
+
hash_password,
|
| 32 |
+
verify_password,
|
| 33 |
verify_totp_code,
|
| 34 |
SECRET_KEY,
|
| 35 |
ALGORITHM,
|
|
|
|
| 96 |
user = await User.create(
|
| 97 |
username=payload.username,
|
| 98 |
email=payload.email,
|
| 99 |
+
hashed_password=hash_password(payload.password),
|
| 100 |
)
|
| 101 |
|
| 102 |
await Portfolio.create(user=user, name="Default Portfolio")
|
|
|
|
| 116 |
async def login(payload: UserLogin):
|
| 117 |
user = await User.get_or_none(email=payload.email)
|
| 118 |
|
| 119 |
+
if not user or not verify_password(payload.password, user.hashed_password):
|
| 120 |
raise AppException(status_code=400, message="Invalid email or password")
|
| 121 |
if not user.is_active:
|
| 122 |
raise AppException(status_code=403, message="User account is disabled")
|
|
|
|
| 197 |
expires_at = datetime.now(timezone.utc) + timedelta(minutes=PASSWORD_RESET_CODE_EXPIRE_MINUTES)
|
| 198 |
await PasswordResetCode.create(
|
| 199 |
user=user,
|
| 200 |
+
code_hash=hash_password(code),
|
| 201 |
expires_at=expires_at,
|
| 202 |
)
|
| 203 |
|
|
|
|
| 225 |
expires_at = reset_code.expires_at
|
| 226 |
if expires_at.tzinfo is None:
|
| 227 |
expires_at = expires_at.replace(tzinfo=timezone.utc)
|
| 228 |
+
if expires_at >= now and verify_password(code, reset_code.code_hash):
|
| 229 |
matching_code = reset_code
|
| 230 |
break
|
| 231 |
|
| 232 |
if not matching_code:
|
| 233 |
raise AppException(status_code=400, message="Invalid or expired reset code")
|
| 234 |
|
| 235 |
+
user.hashed_password = hash_password(payload.new_password)
|
| 236 |
matching_code.used_at = now
|
| 237 |
await user.save()
|
| 238 |
await matching_code.save()
|
App/routers/users/utils.py
CHANGED
|
@@ -10,6 +10,7 @@ from fastapi import Depends
|
|
| 10 |
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
| 11 |
from App.schemas import AppException
|
| 12 |
from .models import User
|
|
|
|
| 13 |
|
| 14 |
SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret-key-change-in-production")
|
| 15 |
ALGORITHM = "HS256"
|
|
@@ -18,6 +19,20 @@ APP_ISSUER = os.getenv("APP_ISSUER", "Uwekezaji")
|
|
| 18 |
_security = HTTPBearer()
|
| 19 |
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
async def get_current_user(
|
| 22 |
credentials: HTTPAuthorizationCredentials = Depends(_security),
|
| 23 |
) -> User:
|
|
|
|
| 10 |
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
| 11 |
from App.schemas import AppException
|
| 12 |
from .models import User
|
| 13 |
+
from passlib.hash import bcrypt, bcrypt_sha256
|
| 14 |
|
| 15 |
SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret-key-change-in-production")
|
| 16 |
ALGORITHM = "HS256"
|
|
|
|
| 19 |
_security = HTTPBearer()
|
| 20 |
|
| 21 |
|
| 22 |
+
def hash_password(password: str) -> str:
|
| 23 |
+
"""Hash passwords without bcrypt's 72-byte input limit."""
|
| 24 |
+
return bcrypt_sha256.hash(password)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def verify_password(password: str, password_hash: str) -> bool:
|
| 28 |
+
"""Verify both new bcrypt-sha256 hashes and legacy bcrypt hashes."""
|
| 29 |
+
if not password_hash:
|
| 30 |
+
return False
|
| 31 |
+
if password_hash.startswith("$bcrypt-sha256$"):
|
| 32 |
+
return bcrypt_sha256.verify(password, password_hash)
|
| 33 |
+
return bcrypt.verify(password[:72], password_hash)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
async def get_current_user(
|
| 37 |
credentials: HTTPAuthorizationCredentials = Depends(_security),
|
| 38 |
) -> User:
|
requirements.txt
CHANGED
|
@@ -27,6 +27,6 @@ typing_extensions==4.12.2
|
|
| 27 |
#
|
| 28 |
httpx
|
| 29 |
bs4
|
| 30 |
-
passlib
|
| 31 |
PyJWT
|
| 32 |
-
bcrypt
|
|
|
|
| 27 |
#
|
| 28 |
httpx
|
| 29 |
bs4
|
| 30 |
+
passlib==1.7.4
|
| 31 |
PyJWT
|
| 32 |
+
bcrypt==4.0.1
|