Files
service-finder/backend/app/services/auth_service.py

701 lines
31 KiB
Python
Executable File

# /opt/docker/dev/service_finder/backend/app/services/auth_service.py
import logging
import uuid
import re
from datetime import datetime, timedelta, timezone
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_, update
from sqlalchemy.orm import joinedload
from fastapi.encoders import jsonable_encoder
from fastapi import HTTPException, status
from app.models.identity import User, Person, UserRole, VerificationToken, Wallet, OneTimePassword
from app.models import UserStats, LogSeverity
from app.models.marketplace import Organization, OrganizationMember, OrgType, Branch
from app.schemas.auth import UserLiteRegister, UserKYCComplete
from app.core.security import get_password_hash, verify_password, generate_secure_slug
from app.services.email_manager import email_manager
from app.core.config import settings
from app.services.config_service import config
from app.services.geo_service import GeoService
from app.services.security_service import security_service
from app.services.gamification_service import GamificationService
logger = logging.getLogger(__name__)
class AuthService:
@staticmethod
async def _validate_password_complexity(db: AsyncSession, password: str, region_code: str = None):
"""
Dinamikus jelszó komplexitás ellenőrzése az admin beállítások alapján.
"""
# Alap beállítások lekérése
min_pass = await config.get_setting(db, "auth_min_password_length", default=8)
password_strict = await config.get_setting(db, "auth_password_strict", default=False)
# Minimum hossz ellenőrzése
if len(password) < int(min_pass):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"A jelszónak legalább {min_pass} karakter hosszúnak kell lennie."
)
# Ha a strict mód be van kapcsolva, komplexitás ellenőrzés
if password_strict and str(password_strict).lower() in ("true", "1", "yes"):
# Ellenőrizzük: legalább 1 nagybetű, 1 kisbetű, 1 szám vagy speciális karakter
if not re.search(r'[A-Z]', password):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="A jelszónak tartalmaznia kell legalább egy nagybetűt."
)
if not re.search(r'[a-z]', password):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="A jelszónak tartalmaznia kell legalább egy kisbetűt."
)
if not re.search(r'[0-9!@#$%^&*()_+\-=\[\]{};:"\\|,.<>/?]', password):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="A jelszónak tartalmaznia kell legalább egy számot vagy speciális karaktert."
)
@staticmethod
async def register_lite(db: AsyncSession, user_in: UserLiteRegister):
""" 1. FÁZIS: Lite regisztráció dinamikus korlátokkal és Sentinel naplózással. """
try:
# Paraméterek lekérése az admin felületről
min_pass = await config.get_setting(db, "auth_min_password_length", default=8)
default_role_name = await config.get_setting(db, "auth_default_role", default="user")
reg_token_hours = await config.get_setting(db, "auth_registration_hours", region_code=user_in.region_code, default=48)
# Jelszó komplexitás ellenőrzése
await AuthService._validate_password_complexity(db, user_in.password, user_in.region_code)
# Check if email already exists
existing_user = await db.execute(select(User).where(User.email == user_in.email))
if existing_user.scalar_one_or_none():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Ez az email cím már regisztrálva van."
)
# Meghívó keresése referral kód alapján
referred_by_id = None
if user_in.referred_by_code:
referrer_stmt = select(User).where(User.referral_code == user_in.referred_by_code)
referrer = (await db.execute(referrer_stmt)).scalar_one_or_none()
if referrer:
referred_by_id = referrer.id
logger.info(f"User {user_in.email} referred by {referrer.email} (ID: {referrer.id})")
else:
logger.warning(f"Referral code '{user_in.referred_by_code}' not found, ignoring.")
new_person = Person(
first_name=user_in.first_name,
last_name=user_in.last_name,
is_active=False, # Email verification required before activation
identity_docs={}, # EXPLICIT BEÁLLÍTÁS A DB HIBA ELKERÜLÉSÉRE
ice_contact={}, # EXPLICIT BEÁLLÍTÁS A DB HIBA ELKERÜLÉSÉRE
lifetime_xp=0, # default -1, de explicit 0
penalty_points=0, # default -1, de explicit 0
social_reputation=1.0, # default 0.0, de explicit 1.0
is_sales_agent=False, # default True, de explicit False
is_ghost=False,
created_at=datetime.now(timezone.utc)
)
db.add(new_person)
await db.flush()
# Szerepkör dinamikus feloldása
assigned_role = UserRole[default_role_name] if default_role_name in UserRole.__members__ else UserRole.user
# Referral kód generálása
referral_code = generate_secure_slug(8).upper()
new_user = User(
email=user_in.email,
hashed_password=get_password_hash(user_in.password),
person_id=new_person.id,
role=assigned_role,
is_active=False, # Email verification required before activation
is_deleted=False,
region_code=user_in.region_code,
preferred_language=user_in.lang,
subscription_plan='FREE',
# --- EXPLICIT DEFAULT ÉRTÉKEK A DB HIBA ELKERÜLÉSÉRE ---
is_vip=True, # Changed to True to force inclusion in INSERT
preferred_currency="HUF",
scope_level="individual",
custom_permissions={},
referral_code=referral_code,
referred_by_id=referred_by_id,
created_at=datetime.now(timezone.utc)
)
db.add(new_user)
await db.flush()
# Verifikációs token
token_val = uuid.uuid4()
db.add(VerificationToken(
token=token_val,
user_id=new_user.id,
token_type="registration",
expires_at=datetime.now(timezone.utc) + timedelta(hours=int(reg_token_hours))
))
# Sentinel Audit Log
await security_service.log_event(
db, user_id=new_user.id, action="USER_REGISTER_LITE",
severity=LogSeverity.info, target_type="User", target_id=str(new_user.id),
new_data={"email": user_in.email}
)
await db.commit()
# Email küldés a beállított template alapján
verification_link = f"{settings.FRONTEND_BASE_URL}/verify?token={token_val}"
email_result = await email_manager.send_email(
recipient=user_in.email,
template_key="reg",
variables={"first_name": user_in.first_name, "link": verification_link},
lang=user_in.lang
)
# Check if email sending failed - LOG but don't crash registration
if email_result and email_result.get("status") == "error":
logger.error(f"Email sending failed for {user_in.email}: {email_result.get('message')}")
# Don't raise exception - registration should succeed even if email fails
# Just log the error and continue
else:
logger.info(f"Email sent successfully to {user_in.email}")
return new_user
except Exception as e:
await db.rollback()
logger.error(f"Lite Reg Error: {e}")
raise e
@staticmethod
async def complete_kyc(db: AsyncSession, user_id: int, kyc_in: UserKYCComplete):
""" 2. FÁZIS: Teljes profil és Gamification inicializálás. """
try:
stmt = select(User).options(joinedload(User.person)).where(User.id == user_id)
user = (await db.execute(stmt)).scalar_one_or_none()
if not user: return None
# Dinamikus beállítások (Soha ne legyen kódba vésve!)
org_tpl = await config.get_setting(db, "org_naming_template", default="{last_name} Flotta")
base_cur = await config.get_setting(db, "finance_default_currency", region_code=user.region_code, default="HUF")
kyc_reward = await config.get_setting(db, "gamification_kyc_bonus", default=500)
# Címkezelés (GeoService hívás)
addr_id = await GeoService.get_or_create_full_address(
db, zip_code=kyc_in.address_zip, city=kyc_in.address_city,
street_name=kyc_in.address_street_name, street_type=kyc_in.address_street_type,
house_number=kyc_in.address_house_number, parcel_id=kyc_in.address_hrsz
)
# Person adatok dúsítása
p = user.person
# --- SHADOW IDENTITY CHECK ---
if kyc_in.birth_date:
shadow_stmt = select(Person).where(
and_(
Person.last_name == p.last_name,
Person.first_name == p.first_name,
Person.birth_date == kyc_in.birth_date,
Person.id != p.id # Ne a jelenlegit találja meg
)
)
shadow_person = (await db.execute(shadow_stmt)).scalar_one_or_none()
else:
shadow_person = None
if shadow_person:
logger.info(f"Shadow Identity megtalálva a {user.id} userhez: Person ID {shadow_person.id}")
user.person_id = shadow_person.id
# Frissítjük a megtalált Person rekordot a KYC adatokkal
if kyc_in.first_name: shadow_person.first_name = kyc_in.first_name
if kyc_in.last_name: shadow_person.last_name = kyc_in.last_name
shadow_person.mothers_last_name = kyc_in.mothers_last_name
shadow_person.mothers_first_name = kyc_in.mothers_first_name
shadow_person.birth_place = kyc_in.birth_place
shadow_person.phone = kyc_in.phone_number
shadow_person.address_id = addr_id
shadow_person.identity_docs = jsonable_encoder(kyc_in.identity_docs)
shadow_person.is_active = True
# Inaktiváljuk/töröljük a megárvult eredeti Person-t
p.is_active = False
p.is_ghost = True
p = shadow_person
else:
if kyc_in.first_name: p.first_name = kyc_in.first_name
if kyc_in.last_name: p.last_name = kyc_in.last_name
p.mothers_last_name = kyc_in.mothers_last_name
p.mothers_first_name = kyc_in.mothers_first_name
p.birth_place = kyc_in.birth_place
p.birth_date = kyc_in.birth_date
p.phone = kyc_in.phone_number
p.address_id = addr_id
p.identity_docs = jsonable_encoder(kyc_in.identity_docs)
p.is_active = True
# --- SHADOW CHECK VÉGE ---
# Dinamikus szervezet generálás
org_full_name = org_tpl.format(last_name=p.last_name, first_name=p.first_name)
new_org = Organization(
full_name=org_full_name,
name=f"{p.last_name} Garázsa",
folder_slug=generate_secure_slug(12),
org_type=OrgType.individual,
owner_id=user.id,
is_active=True,
status="verified",
country_code=user.region_code,
# --- EXPLICIT IDŐBÉLYEGEK A DB HIBA ELKERÜLÉSÉRE ---
first_registered_at=datetime.now(timezone.utc),
current_lifecycle_started_at=datetime.now(timezone.utc),
created_at=datetime.now(timezone.utc),
subscription_plan="FREE",
base_asset_limit=1,
purchased_extra_slots=0,
notification_settings={},
external_integration_config={},
is_ownership_transferable=True
)
db.add(new_org)
await db.flush()
# Infrastruktúra elemek
db.add(Branch(organization_id=new_org.id, address_id=addr_id, name="Home Base", is_main=True))
db.add(OrganizationMember(organization_id=new_org.id, user_id=user.id, role="OWNER"))
db.add(Wallet(user_id=user.id, currency=kyc_in.preferred_currency or base_cur))
# db.add(UserStats(user_id=user.id)) # GamificationService kezeli
user.is_active = True
user.folder_slug = generate_secure_slug(12)
# Set user's scope_id to the new personal organization ID
user.scope_id = str(new_org.id)
# Update region_code from KYC form (EU country selection)
if kyc_in.region_code:
user.region_code = kyc_in.region_code
# Gamification XP jóváírás (commit=False, mert a külső metódus kezeli a tranzakciót)
await GamificationService.award_points(db, user_id=user.id, amount=int(kyc_reward), reason="KYC_VERIFICATION", commit=False)
# P2P Referral XP a meghívónak (ha van referred_by_id)
if user.referred_by_id:
p2p_xp = await config.get_setting(db, "gamification_p2p_invite_xp", default=50)
await GamificationService.award_points(
db,
user_id=user.referred_by_id,
amount=int(p2p_xp),
reason="P2P_REFERRAL_SUCCESS",
commit=False
)
logger.info(f"P2P XP ({p2p_xp}) awarded to referrer ID {user.referred_by_id} for user {user.id}")
await db.commit()
return user
except Exception as e:
await db.rollback()
logger.error(f"KYC Error: {e}")
raise e
@staticmethod
async def authenticate(db: AsyncSession, email: str, password: str):
""" Felhasználó hitelesítése. """
stmt = select(User).where(and_(User.email == email, User.is_deleted == False))
res = await db.execute(stmt)
user = res.scalar_one_or_none()
if user and verify_password(password, user.hashed_password):
return user
return None
@staticmethod
async def verify_email(db: AsyncSession, token_str: str):
"""
Email megerősítés.
Visszaadja az aktivált User objektumot sikeres verifikáció után (Magic Link támogatás).
"""
try:
token_uuid = uuid.UUID(token_str)
stmt = select(VerificationToken).where(and_(
VerificationToken.token == token_uuid,
VerificationToken.is_used == False,
VerificationToken.expires_at > datetime.now(timezone.utc)
))
token = (await db.execute(stmt)).scalar_one_or_none()
if not token: return None
token.is_used = True
# Activate user
user_stmt = select(User).where(User.id == token.user_id)
user = (await db.execute(user_stmt)).scalar_one_or_none()
if not user: return None
user.is_active = True
# Activate person - FIX: Use User.person_id instead of Person.user_id
# Person.user_id is a separate FK that may be NULL
if user.person_id:
person_stmt = select(Person).where(Person.id == user.person_id)
person = (await db.execute(person_stmt)).scalar_one_or_none()
if person:
person.is_active = True
# Also set the back-reference so Person.user_id is consistent
person.user_id = user.id
await db.commit()
return user # Return the activated User object
except Exception as e:
logger.error(f"Email verification error: {e}")
return None
@staticmethod
async def initiate_password_reset(db: AsyncSession, email: str):
""" Elfelejtett jelszó folyamat indítása. """
stmt = select(User).where(and_(User.email == email, User.is_deleted == False))
user = (await db.execute(stmt)).scalar_one_or_none()
if not user:
return "not_found"
# Ha a felhasználó létezik, de inaktív, dobjunk hibát
if not user.is_active:
logger.warning(f"Password reset requested for inactive user: {email}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="AUTH.USER_INACTIVE"
)
# Dinamikus lejárat az adminból
reset_h = await config.get_setting(db, "auth_password_reset_hours", region_code=user.region_code, default=2)
token_val = uuid.uuid4()
db.add(VerificationToken(
token=token_val, user_id=user.id, token_type="password_reset",
expires_at=datetime.now(timezone.utc) + timedelta(hours=int(reset_h))
))
link = f"{settings.FRONTEND_BASE_URL}/reset-password?token={token_val}"
# Mock email sending: log the reset link to console instead of sending real email
logger.info(f"*** MOCK EMAIL *** Password reset for {email}")
logger.info(f"*** MOCK EMAIL *** Reset link: {link}")
logger.info(f"*** MOCK EMAIL *** Token: {token_val}")
# Try to send real email, but don't crash if SMTP is unavailable
try:
result = await email_manager.send_email(
recipient=email, template_key="pwd_reset",
variables={"link": link}, lang=user.preferred_language
)
logger.info(f"Email küldés eredménye (password reset): {result}")
if result and result.get("status") == "error":
logger.warning(f"Password reset email sending failed (non-critical): {result.get('message')}")
except Exception as e:
logger.warning(f"Password reset email sending exception (non-critical): {e}")
await db.commit()
return "success"
@staticmethod
async def reset_password(db: AsyncSession, email: str, token_str: str, new_password: str):
""" Jelszó tényleges megváltoztatása token alapján. """
try:
token_uuid = uuid.UUID(token_str)
stmt = select(VerificationToken).join(User).where(and_(
User.email == email,
VerificationToken.token == token_uuid,
VerificationToken.token_type == "password_reset",
VerificationToken.is_used == False,
VerificationToken.expires_at > datetime.now(timezone.utc)
))
token_rec = (await db.execute(stmt)).scalar_one_or_none()
if not token_rec: return False
# Jelszó komplexitás ellenőrzése
user_stmt = select(User).where(User.id == token_rec.user_id)
user = (await db.execute(user_stmt)).scalar_one()
await AuthService._validate_password_complexity(db, new_password, user.region_code)
user.hashed_password = get_password_hash(new_password)
token_rec.is_used = True
await db.commit()
return True
except HTTPException:
raise
except Exception as e:
logger.error(f"Password reset error: {e}")
return False
@staticmethod
async def _generate_otp_code() -> str:
"""Generate a random 6-digit OTP code."""
import random
return f"{random.randint(0, 999999):06d}"
@staticmethod
async def request_account_restore(db: AsyncSession, email: str):
"""
30 napos fiókvisszaállítás: OTP kérés.
Megkeresi a törölt felhasználót az eredeti email alapján.
Ha 30 napon belül van a törlés, generál 6-jegyű kódot.
Ha 30 napon TÚL van, jelzi, hogy új regisztráció szükséges.
"""
try:
# Search for deleted user by checking if email exists in deleted prefix pattern
stmt = select(User).where(
User.email.like(f"%{email}"),
User.is_deleted == True
)
result = await db.execute(stmt)
user = result.scalar_one_or_none()
if not user:
return {"status": "not_found", "message": "Nem található törölt fiók ezzel az email címmel."}
# Check if within 30-day window
if user.deleted_at:
days_since_deletion = (datetime.now(timezone.utc) - user.deleted_at).days
else:
days_since_deletion = 0
if days_since_deletion > 30:
return {
"status": "expired",
"message": "A fiók véglegesen megsemmisült. Kérjük, regisztráljon újra ugyanazzal az email címmel.",
"can_re_register": True,
"original_email": email
}
# Generate 6-digit OTP code
code = await AuthService._generate_otp_code()
expires_at = datetime.now(timezone.utc) + timedelta(minutes=15)
# Store OTP
otp = OneTimePassword(
email=email,
code=code,
otp_type="restore",
extra_data={"user_id": user.id},
expires_at=expires_at
)
db.add(otp)
await db.commit()
# Mock email sending: log the OTP code to console instead of sending real email
logger.info(f"*** MOCK EMAIL *** OTP Code for {email}: {code}")
logger.info(f"*** MOCK EMAIL *** Expires at: {expires_at.isoformat()}")
return {
"status": "otp_sent",
"message": "Egy 6-jegyű visszaállítási kódot küldtünk az email címére.",
"expires_at": expires_at.isoformat()
}
except Exception as e:
await db.rollback()
logger.error(f"Account restore request error for {email}: {e}")
return {"status": "error", "message": "Hiba történt a visszaállítási kód generálása során."}
@staticmethod
async def verify_account_restore(db: AsyncSession, email: str, code: str):
"""
OTP kód ellenőrzése és fiók visszaállítása.
"""
now = datetime.now(timezone.utc)
# Find valid OTP
stmt = select(OneTimePassword).where(
OneTimePassword.email == email,
OneTimePassword.code == code,
OneTimePassword.otp_type == "restore",
OneTimePassword.is_used == False,
OneTimePassword.expires_at > now
).order_by(OneTimePassword.created_at.desc()).limit(1)
result = await db.execute(stmt)
otp = result.scalar_one_or_none()
if not otp:
return {"status": "invalid", "message": "Érvénytelen vagy lejárt kód."}
# Mark OTP as used
otp.is_used = True
# Find the deleted user
user_id = otp.extra_data.get("user_id") if otp.extra_data else None
if not user_id:
return {"status": "error", "message": "Hiba a fiók azonosításában."}
stmt = select(User).where(User.id == user_id)
result = await db.execute(stmt)
user = result.scalar_one_or_none()
if not user:
return {"status": "error", "message": "Felhasználó nem található."}
# Restore the account
user.is_deleted = False
user.is_active = True
user.deleted_at = None
# Restore original email (remove deleted_ prefix)
# The email format is: deleted_{id}_{timestamp}_{original_email}
if user.email.startswith("deleted_"):
parts = user.email.split("_", 3)
if len(parts) == 4:
user.email = parts[3]
await security_service.log_event(
db, user_id=user.id, action="USER_ACCOUNT_RESTORE",
severity=LogSeverity.info, target_type="User", target_id=str(user.id),
new_data={"restored_via": "otp", "email": user.email}
)
await db.commit()
return {"status": "success", "message": "Fiók sikeresen visszaállítva."}
@staticmethod
async def check_is_last_admin(db: AsyncSession, user_id: int) -> bool:
"""
Ellenőrzi, hogy a felhasználó az egyetlen aktív admin bármelyik szervezetében.
Visszaadja: is_last_admin: bool
"""
try:
from app.models.marketplace.organization import OrganizationMember, OrgUserRole
# Find organizations where user is ADMIN or OWNER
stmt = select(OrganizationMember.organization_id).where(
OrganizationMember.user_id == user_id,
OrganizationMember.role.in_([OrgUserRole.ADMIN, OrgUserRole.OWNER])
)
result = await db.execute(stmt)
org_ids = [row[0] for row in result.all()]
if not org_ids:
return False
# For each org, check if there are other active admins/owners
for org_id in org_ids:
stmt = select(OrganizationMember).where(
OrganizationMember.organization_id == org_id,
OrganizationMember.role.in_([OrgUserRole.ADMIN, OrgUserRole.OWNER]),
OrganizationMember.user_id != user_id
)
result = await db.execute(stmt)
other_admins = result.scalars().all()
if not other_admins:
# This org has no other admin - user is last admin here
return True
return False
except HTTPException:
raise
except Exception as e:
logger.error(f"check_is_last_admin error: {e}")
return False
@staticmethod
async def resend_verification(db: AsyncSession, email: str):
"""
Aktiváló e-mail újraküldése.
Ellenőrzi, hogy a felhasználó létezik, de még nem aktív (is_active == False),
generál egy új verifikációs tokent, és kiküldi az e-mailt.
"""
# Eager load person relationship to avoid async lazy-load issue
stmt = select(User).options(joinedload(User.person)).where(
and_(User.email == email, User.is_deleted == False)
)
user = (await db.execute(stmt)).unique().scalar_one_or_none()
if not user:
# Ne áruljuk el, hogy létezik-e az email — biztonsági okokból mindig sikert adunk
logger.info(f"Resend verification requested for non-existent email: {email}")
return "not_found"
if user.is_active:
logger.info(f"Resend verification requested for already active user: {email}")
return "already_active"
# Dinamikus lejárat az adminból
reg_token_hours = await config.get_setting(db, "auth_registration_hours", region_code=user.region_code, default=48)
token_val = uuid.uuid4()
db.add(VerificationToken(
token=token_val,
user_id=user.id,
token_type="registration",
expires_at=datetime.now(timezone.utc) + timedelta(hours=int(reg_token_hours))
))
# Email küldés — user.person már eager-loaded, így nem okoz MissingGreenlet hibát
verification_link = f"{settings.FRONTEND_BASE_URL}/verify?token={token_val}"
person = user.person
first_name = person.first_name if person else user.email
logger.info(f"Email küldés indítása (resend verification) ide: {email}...")
result = await email_manager.send_email(
recipient=email,
template_key="reg",
variables={"first_name": first_name, "link": verification_link},
lang=user.preferred_language
)
logger.info(f"Email küldés eredménye (resend verification): {result}")
await db.commit()
logger.info(f"Resend verification email sent to {email}")
return "success"
@staticmethod
async def soft_delete_user(db: AsyncSession, user_id: int, reason: str, actor_id: int):
"""
Felhasználó törlése (Soft-Delete) auditálással, Person-preserving logikával.
Üzleti szabályok:
1. User: is_active=False, is_deleted=True, deleted_at=utcnow
2. Email átírás: deleted_{id}_{timestamp}_{original_email}
3. Person rekordot NEM bántja (megőrizzük a jövőbeli újraaktiváláshoz)
4. Wallet és Gamification adatok megőrzése (inaktív user miatt freeze)
5. Refresh token-ek érvénytelenítése (JWT-alapú, így a deleted_at middleware ellenőrzi)
"""
stmt = select(User).where(User.id == user_id)
user = (await db.execute(stmt)).scalar_one_or_none()
if not user or user.is_deleted:
return False
# 1. Anonymize email with timestamp
old_email = user.email
timestamp = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
user.email = f"deleted_{user.id}_{timestamp}_{old_email}"
# 2. Set soft-delete flags
user.is_deleted = True
user.is_active = False
user.deleted_at = datetime.now(timezone.utc)
# 3. CRITICAL: Do NOT touch Person record
# Person.is_active stays as-is, Person data stays intact
# This allows future re-activation with the same identity
# 4. Security audit log with deleted_at info
await security_service.log_event(
db, user_id=actor_id, action="USER_SOFT_DELETE",
severity=LogSeverity.warning, target_type="User", target_id=str(user_id),
new_data={
"reason": reason,
"deleted_at": str(user.deleted_at),
"person_preserved": True
}
)
await db.commit()
return True