added password hashing and ensured admin user exists #8
+2
-2
@@ -13,8 +13,8 @@ services:
|
||||
environment:
|
||||
- ENVIRONMENT=development
|
||||
# Optional: Override admin credentials for development
|
||||
# - ADMIN_USERNAME=admin
|
||||
# - ADMIN_PASSWORD=devpassword
|
||||
- ADMIN_USERNAME=admin
|
||||
- ADMIN_PASSWORD=password
|
||||
# Optional: Use Redis for token storage (uncomment redis service below)
|
||||
# - REDIS_URI=redis://redis:6379/0
|
||||
command: ["--host", "0.0.0.0", "--port", "8000", "--reload"]
|
||||
|
||||
@@ -5,6 +5,7 @@ description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"bcrypt>=4.0.0",
|
||||
"fastapi>=0.121.0",
|
||||
"sqlalchemy>=2.0.0",
|
||||
"uvicorn>=0.38.0",
|
||||
|
||||
@@ -30,6 +30,48 @@ 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
|
||||
@@ -41,6 +83,9 @@ def init_db() -> None:
|
||||
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]:
|
||||
"""
|
||||
|
||||
@@ -42,7 +42,7 @@ class InMemoryTokenRepository(TokenRepositoryInterface):
|
||||
|
||||
def cleanup_expired(self) -> None:
|
||||
"""Remove expired tokens."""
|
||||
now = datetime.utcnow()
|
||||
now = datetime.now(UTC)
|
||||
expired_tokens = [
|
||||
token for token, (_, expiry) in self._tokens.items() if now > expiry
|
||||
]
|
||||
|
||||
@@ -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,25 @@ 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)
|
||||
if not user:
|
||||
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),
|
||||
)
|
||||
# Verify password using bcrypt
|
||||
if not verify_password(credentials.password, user["hashed_password"]):
|
||||
raise HTTPException(status_code=401, detail="Invalid username or password")
|
||||
|
||||
# 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)
|
||||
# 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=True,
|
||||
)
|
||||
|
||||
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 +153,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 +179,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
|
||||
@@ -12,3 +12,9 @@ def pytest_configure(config: pytest.Config) -> None:
|
||||
tmpdir = tempfile.mkdtemp(prefix="baby_monitor_test_")
|
||||
os.environ["DATA_DIR"] = tmpdir
|
||||
os.environ["ENVIRONMENT"] = "development"
|
||||
|
||||
# Set admin credentials for tests
|
||||
if "ADMIN_USERNAME" not in os.environ:
|
||||
os.environ["ADMIN_USERNAME"] = "admin"
|
||||
if "ADMIN_PASSWORD" not in os.environ:
|
||||
os.environ["ADMIN_PASSWORD"] = "password"
|
||||
|
||||
@@ -39,6 +39,7 @@ name = "baby-monitor"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "bcrypt" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "sqlalchemy" },
|
||||
{ name = "uvicorn" },
|
||||
@@ -67,6 +68,7 @@ dev = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "bcrypt", specifier = ">=4.0.0" },
|
||||
{ name = "fastapi", specifier = ">=0.121.0" },
|
||||
{ name = "psycopg2-binary", marker = "extra == 'postgres'", specifier = ">=2.9.0" },
|
||||
{ name = "redis", marker = "extra == 'redis'", specifier = ">=5.0.0" },
|
||||
@@ -88,6 +90,72 @@ dev = [
|
||||
{ name = "types-psycopg2", specifier = ">=2.9.21.20251012" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bcrypt"
|
||||
version = "5.0.0"
|
||||
source = { registry = "http://10.0.0.2:5001/index/" }
|
||||
sdist = { url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd" }
|
||||
wheels = [
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2" },
|
||||
{ url = "http://10.0.0.2:5001/index/bcrypt/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.10.5"
|
||||
|
||||
Reference in New Issue
Block a user