sync: migrate secure-online-shop to Gitea (2026-08-10)

This commit is contained in:
konturai-ops
2026-08-10 15:26:59 +00:00
commit 2022c8890d
44 changed files with 3196 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Database package."""
+101
View File
@@ -0,0 +1,101 @@
"""
Script to create a shop account with an operator-supplied strong password.
Usage:
python -m app.db.create_shop_account
Provide SHOP_ACCOUNT_PASSWORD for non-interactive execution,
or enter the password securely via getpass().
"""
from __future__ import annotations
import os
import sys
from getpass import getpass
from sqlalchemy import select
from app.core.security import get_password_hash
from app.db.session import SessionLocal, engine
from app.models import Base, RoleEnum, User
SHOP_USERNAME = "shop"
MIN_PASSWORD_LENGTH = 12
def _validate_password_strength(password: str) -> None:
if len(password) < MIN_PASSWORD_LENGTH or len(password) > 72:
raise ValueError("Password must be between 12 and 72 characters long.")
has_upper = any(char.isupper() for char in password)
has_lower = any(char.islower() for char in password)
has_digit = any(char.isdigit() for char in password)
has_special = any(not char.isalnum() for char in password)
if not all((has_upper, has_lower, has_digit, has_special)):
raise ValueError(
"Password must include upper, lower, digit, and special characters."
)
def _get_shop_password() -> str:
env_password = os.getenv("SHOP_ACCOUNT_PASSWORD", "").strip()
if env_password:
_validate_password_strength(env_password)
return env_password
if not sys.stdin.isatty():
raise RuntimeError(
"Interactive password input is unavailable. Set SHOP_ACCOUNT_PASSWORD to create the account safely."
)
while True:
password = getpass("Enter a strong password for the shop account: ")
confirm_password = getpass("Confirm the password: ")
if password != confirm_password:
print("Passwords do not match. Please try again.", file=sys.stderr)
continue
_validate_password_strength(password)
return password
def create_shop_account() -> None:
"""Create a shop account with a strong password if it doesn't exist."""
Base.metadata.create_all(bind=engine)
with SessionLocal() as db:
existing = db.scalar(
select(User).where(User.username == SHOP_USERNAME)
)
if existing is not None:
print(f"Shop account '{SHOP_USERNAME}' already exists (id={existing.id}).")
print("No new account was created.")
return
password = _get_shop_password()
password_hash = get_password_hash(password)
shop = User(
username=SHOP_USERNAME,
password_hash=password_hash,
role=RoleEnum.shop,
is_active=True,
)
db.add(shop)
db.commit()
db.refresh(shop)
print("=" * 60)
print("SHOP ACCOUNT CREATED SUCCESSFULLY")
print("=" * 60)
print(f"Username: {SHOP_USERNAME}")
print("=" * 60)
print("Password was accepted and hashed without being printed to stdout.")
print("=" * 60)
if __name__ == "__main__":
try:
create_shop_account()
except Exception as exc:
print(f"Error: {exc}", file=sys.stderr)
sys.exit(1)
+27
View File
@@ -0,0 +1,27 @@
from __future__ import annotations
import logging
from app.core.config import get_settings
from app.db.seed_demo import seed_demo_products
from app.db.session import engine
from app.models import Base
logger = logging.getLogger("app.db")
def initialize_database() -> None:
Base.metadata.create_all(bind=engine)
logger.info("Database tables created successfully")
settings = get_settings()
if settings.demo_seed_products:
from app.db.session import SessionLocal
with SessionLocal() as db:
seed_demo_products(db, settings.demo_product_count)
if __name__ == "__main__":
initialize_database()
print("Database initialized successfully.")
print("To create a shop account, run: python -m app.db.create_shop_account")
+61
View File
@@ -0,0 +1,61 @@
from __future__ import annotations
import logging
from decimal import Decimal
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from app.models.product import Product
logger = logging.getLogger("app.db")
PRODUCT_TEMPLATES = (
("Aurora Headphones", "Wireless headphones with active noise reduction", "189.90", 42),
("Nimbus Laptop", "Thin laptop for study, work, and secure online payments", "1299.00", 18),
("Pulse Smartwatch", "Fitness smartwatch with long battery life", "249.50", 55),
("Volt Power Bank", "Compact 20000 mAh power bank with fast charging", "79.99", 90),
("Axis Keyboard", "Mechanical keyboard with quiet tactile switches", "139.00", 36),
("Orbit Backpack", "Water-resistant backpack with laptop compartment", "89.90", 64),
("Brew Coffee Maker", "Programmable drip coffee maker for home offices", "119.95", 31),
("Frame Desk Lamp", "Adjustable LED lamp with warm and cold light modes", "54.40", 83),
("Focus Webcam", "Full HD webcam with privacy shutter", "74.99", 71),
("Studio Speaker", "Bluetooth speaker with balanced stereo sound", "159.00", 27),
("Terra Sneakers", "Lightweight everyday sneakers with cushioned soles", "109.90", 49),
("Slate Tablet", "Portable tablet for browsing, media, and field work", "399.00", 22),
)
def seed_demo_products(db: Session, target_count: int) -> None:
"""Populate the catalog with deterministic demo products for MVP deployment."""
if target_count <= 0:
return
existing_count = db.scalar(select(func.count(Product.id))) or 0
products_to_create = target_count - existing_count
if products_to_create <= 0:
logger.info("Demo product seed skipped existing_count=%s", existing_count)
return
products: list[Product] = []
for offset in range(products_to_create):
sequence = existing_count + offset + 1
name, description, price, base_stock = PRODUCT_TEMPLATES[offset % len(PRODUCT_TEMPLATES)]
batch = (offset // len(PRODUCT_TEMPLATES)) + 1
products.append(
Product(
name=f"{name} {batch:02d}",
description=description,
price=Decimal(price),
stock=base_stock + (sequence % 17),
is_active=True,
)
)
db.add_all(products)
db.commit()
logger.info(
"Demo product seed completed created=%s target_count=%s",
len(products),
target_count,
)
+27
View File
@@ -0,0 +1,27 @@
from __future__ import annotations
from collections.abc import Generator
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from app.core.config import get_settings
settings = get_settings()
sqlite_connect_args = {"check_same_thread": False} if settings.database_url.startswith("sqlite") else {}
engine = create_engine(
settings.database_url,
connect_args=sqlite_connect_args,
pool_pre_ping=True,
)
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, expire_on_commit=False)
def get_db() -> Generator[Session, None, None]:
db = SessionLocal()
try:
yield db
finally:
db.close()