added password hashing and ensured admin user exists
This commit is contained in:
@@ -30,6 +30,50 @@ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
Base: "type[DeclarativeBase]" = declarative_base()
|
||||
|
||||
|
||||
def _ensure_admin_user() -> None:
|
||||
"""Create or update admin user from environment variables."""
|
||||
from baby_monitor.models.db.user import User
|
||||
from baby_monitor.utils.password import hash_password
|
||||
|
||||
admin_username = os.getenv("ADMIN_USERNAME")
|
||||
admin_password = os.getenv("ADMIN_PASSWORD")
|
||||
|
||||
if not admin_username:
|
||||
raise RuntimeError(
|
||||
"ADMIN_USERNAME environment variable is required but not set"
|
||||
)
|
||||
if not admin_password:
|
||||
raise RuntimeError(
|
||||
"ADMIN_PASSWORD environment variable is required but not set"
|
||||
)
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# Check if admin user exists
|
||||
admin_user = db.query(User).filter(
|
||||
User.username == admin_username
|
||||
).first()
|
||||
|
||||
hashed_pw = hash_password(admin_password)
|
||||
|
||||
if admin_user:
|
||||
# Update existing admin user's password and ensure admin flag
|
||||
admin_user.hashed_password = hashed_pw # type: ignore[assignment]
|
||||
admin_user.is_admin = True # type: ignore[assignment]
|
||||
db.commit()
|
||||
else:
|
||||
# Create new admin user
|
||||
new_admin = User(
|
||||
username=admin_username,
|
||||
hashed_password=hashed_pw,
|
||||
is_admin=True,
|
||||
)
|
||||
db.add(new_admin)
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def init_db() -> None:
|
||||
"""Initialize the database by creating all tables."""
|
||||
# Import models to register them with Base.metadata
|
||||
@@ -40,6 +84,9 @@ def init_db() -> None:
|
||||
# Ensure data directory exists
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
# Create admin user if it doesn't exist
|
||||
_ensure_admin_user()
|
||||
|
||||
|
||||
def get_database() -> Generator[Session, None, None]:
|
||||
|
||||
@@ -12,7 +12,6 @@ from baby_monitor.models.auth import (
|
||||
)
|
||||
from baby_monitor.repositories import (
|
||||
get_token_repository,
|
||||
get_credentials_repository,
|
||||
get_user_repository,
|
||||
)
|
||||
from baby_monitor.repositories.dependencies.get_invitation_repository import (
|
||||
@@ -20,10 +19,10 @@ from baby_monitor.repositories.dependencies.get_invitation_repository import (
|
||||
)
|
||||
from baby_monitor.repositories.interfaces import (
|
||||
TokenRepositoryInterface,
|
||||
CredentialsRepositoryInterface,
|
||||
UserRepositoryInterface,
|
||||
InvitationRepositoryInterface,
|
||||
)
|
||||
from baby_monitor.utils.password import hash_password, verify_password
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["authentication"])
|
||||
|
||||
@@ -55,22 +54,15 @@ def verify_admin(
|
||||
user_repo: Annotated[UserRepositoryInterface, Depends(get_user_repository)],
|
||||
) -> int:
|
||||
"""Verify the user is an admin and return user_id."""
|
||||
# First try to get user from database
|
||||
user = user_repo.get_by_id(user_id)
|
||||
|
||||
if user:
|
||||
# Database user - check is_admin field
|
||||
if not user.get("is_admin", False):
|
||||
raise HTTPException(status_code=403, detail="Admin access required")
|
||||
return user_id
|
||||
if not user:
|
||||
raise HTTPException(status_code=401, detail="User not found")
|
||||
|
||||
# If not in database but has valid token with user_id=1,
|
||||
# it's the environment-based admin (only assigned during env admin login)
|
||||
if user_id == 1:
|
||||
return user_id
|
||||
if not user.get("is_admin", False):
|
||||
raise HTTPException(status_code=403, detail="Admin access required")
|
||||
|
||||
# User not found and not environment admin
|
||||
raise HTTPException(status_code=401, detail="User not found")
|
||||
return user_id
|
||||
|
||||
|
||||
@router.post("/login", response_model=LoginResponse)
|
||||
@@ -78,9 +70,6 @@ def login(
|
||||
credentials: LoginRequest,
|
||||
token_repo: Annotated[TokenRepositoryInterface, Depends(get_token_repository)],
|
||||
user_repo: Annotated[UserRepositoryInterface, Depends(get_user_repository)],
|
||||
creds_repo: Annotated[
|
||||
CredentialsRepositoryInterface, Depends(get_credentials_repository)
|
||||
],
|
||||
) -> LoginResponse:
|
||||
"""
|
||||
API endpoint for user authentication.
|
||||
@@ -88,38 +77,29 @@ def login(
|
||||
Returns user info and access token on successful login.
|
||||
Raises 401 on invalid credentials.
|
||||
"""
|
||||
# First check if it's a database user
|
||||
# Check if user exists in database
|
||||
user = user_repo.get_by_username(credentials.username)
|
||||
if user:
|
||||
# TODO: Use proper password hashing (bcrypt/argon2)
|
||||
# For now, compare plain text (matches registration)
|
||||
if user["hashed_password"] == credentials.password:
|
||||
# Generate a secure random token
|
||||
access_token = secrets.token_urlsafe(32)
|
||||
token_repo.store(access_token, user_id=user["id"], ttl=3600)
|
||||
|
||||
return LoginResponse(
|
||||
message="Login successful",
|
||||
username=credentials.username,
|
||||
access_token=access_token,
|
||||
is_admin=user.get("is_admin", False),
|
||||
)
|
||||
|
||||
# Fallback to admin credentials from environment
|
||||
if creds_repo.verify_admin_credentials(credentials.username, credentials.password):
|
||||
# Generate a secure random token
|
||||
access_token = secrets.token_urlsafe(32)
|
||||
# Store token with user_id (hardcoded 1 for admin)
|
||||
token_repo.store(access_token, user_id=1, ttl=3600)
|
||||
|
||||
return LoginResponse(
|
||||
message="Login successful",
|
||||
username=credentials.username,
|
||||
access_token=access_token,
|
||||
is_admin=True,
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=401, detail="Invalid username or password"
|
||||
)
|
||||
|
||||
# Verify password using bcrypt
|
||||
if not verify_password(credentials.password, user["hashed_password"]):
|
||||
raise HTTPException(
|
||||
status_code=401, detail="Invalid username or password"
|
||||
)
|
||||
|
||||
# Generate a secure random token
|
||||
access_token = secrets.token_urlsafe(32)
|
||||
token_repo.store(access_token, user_id=user["id"], ttl=3600)
|
||||
|
||||
raise HTTPException(status_code=401, detail="Invalid username or password")
|
||||
return LoginResponse(
|
||||
message="Login successful",
|
||||
username=credentials.username,
|
||||
access_token=access_token,
|
||||
is_admin=user.get("is_admin", False),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
@@ -177,11 +157,11 @@ def register(
|
||||
detail="Username already exists",
|
||||
)
|
||||
|
||||
# Create the new user
|
||||
# TODO: Hash password before storing (currently plain text)
|
||||
# Create the new user with hashed password
|
||||
hashed_pw = hash_password(request.password)
|
||||
user = user_repo.create(
|
||||
username=request.username,
|
||||
hashed_password=request.password,
|
||||
hashed_password=hashed_pw,
|
||||
)
|
||||
|
||||
# Consume the invitation token
|
||||
@@ -203,21 +183,12 @@ def register(
|
||||
def get_current_user(
|
||||
user_id: Annotated[int, Depends(verify_token)],
|
||||
user_repo: Annotated[UserRepositoryInterface, Depends(get_user_repository)],
|
||||
creds_repo: Annotated[
|
||||
CredentialsRepositoryInterface, Depends(get_credentials_repository)
|
||||
],
|
||||
) -> dict:
|
||||
"""Get current user info including admin status."""
|
||||
user = user_repo.get_by_id(user_id)
|
||||
|
||||
# If user not found in DB, might be env-based admin
|
||||
if not user:
|
||||
# Return admin info from environment
|
||||
return {
|
||||
"id": user_id,
|
||||
"username": creds_repo.get_admin_username(),
|
||||
"is_admin": True, # Env-based admin is always admin
|
||||
}
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
return {
|
||||
"id": user["id"],
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Password hashing utilities using bcrypt."""
|
||||
|
||||
import bcrypt
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
"""Hash a password using bcrypt.
|
||||
|
||||
Args:
|
||||
password: Plain text password to hash
|
||||
|
||||
Returns:
|
||||
Hashed password as a string
|
||||
"""
|
||||
salt = bcrypt.gensalt()
|
||||
hashed: bytes = bcrypt.hashpw(password.encode("utf-8"), salt)
|
||||
return hashed.decode("utf-8")
|
||||
|
||||
|
||||
def verify_password(password: str, hashed_password: str) -> bool:
|
||||
"""Verify a password against a hashed password.
|
||||
|
||||
Args:
|
||||
password: Plain text password to verify
|
||||
hashed_password: Previously hashed password
|
||||
|
||||
Returns:
|
||||
True if password matches, False otherwise
|
||||
"""
|
||||
result: bool = bcrypt.checkpw(
|
||||
password.encode("utf-8"), hashed_password.encode("utf-8")
|
||||
)
|
||||
return result
|
||||
Reference in New Issue
Block a user