|
|
|
|
|
from tortoise import fields, models |
|
from passlib.context import CryptContext |
|
from App.Android.Android import AndroidClient |
|
from App.Android.Schema import RegisterUserRequest as AndroidRegister |
|
from App.Templates.Templates import MessageTemplate |
|
import datetime |
|
import uuid |
|
import random |
|
import string |
|
from typing import Optional |
|
from decimal import Decimal |
|
import logging |
|
|
|
|
|
logger = logging.getLogger(__name__) |
|
logger.setLevel(logging.INFO) |
|
handler = logging.StreamHandler() |
|
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") |
|
handler.setFormatter(formatter) |
|
logger.addHandler(handler) |
|
|
|
|
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") |
|
|
|
|
|
def generate_short_uuid() -> str: |
|
"""Generates a random 5-character alphanumeric ID.""" |
|
return "".join(random.choices(string.ascii_letters + string.digits, k=5)) |
|
|
|
|
|
class User(models.Model): |
|
id = fields.CharField(primary_key=True, max_length=5, default=generate_short_uuid) |
|
name = fields.CharField(max_length=100) |
|
password = fields.CharField(max_length=100) |
|
phoneNumber = fields.CharField(max_length=15, unique=True) |
|
balance = fields.DecimalField(max_digits=10, decimal_places=2, default=0.00) |
|
mac_address = fields.CharField(max_length=17) |
|
createdAt = fields.DatetimeField(auto_now_add=True) |
|
updatedAt = fields.DatetimeField(auto_now=True) |
|
lastLogin = fields.DatetimeField(default=datetime.datetime.now) |
|
failed_attempts = fields.IntField(default=0) |
|
account_locked = fields.BooleanField(default=False) |
|
reset_token = fields.CharField(max_length=6, null=True, unique=True) |
|
reset_token_expiration = fields.DatetimeField(null=True) |
|
|
|
class Meta: |
|
table = "users" |
|
|
|
|
|
android_client = AndroidClient() |
|
message_templates = MessageTemplate() |
|
|
|
def hash_password(self, plain_password: str) -> str: |
|
"""Hashes a plain password.""" |
|
return pwd_context.hash(plain_password) |
|
|
|
def verify_password(self, plain_password: str) -> bool: |
|
""" |
|
Verifies a plain password against the hashed password. |
|
Handles account locking after multiple failed attempts. |
|
""" |
|
if ( |
|
self.account_locked |
|
and datetime.datetime.now() |
|
< self.lastLogin + datetime.timedelta(minutes=15) |
|
): |
|
logger.warning( |
|
f"Account {self.phoneNumber} is locked due to too many failed attempts." |
|
) |
|
return False |
|
|
|
if pwd_context.verify(plain_password, self.password): |
|
self.failed_attempts = 0 |
|
self.account_locked = False |
|
self.lastLogin = datetime.datetime.now() |
|
self.save() |
|
return True |
|
else: |
|
self.failed_attempts += 1 |
|
if self.failed_attempts >= 5: |
|
self.account_locked = True |
|
logger.warning(f"Account {self.phoneNumber} has been locked.") |
|
self.save() |
|
return False |
|
|
|
async def initiate_password_reset(self): |
|
"""Generates a reset token and sends it to the user via message.""" |
|
self.reset_token = f"{random.randint(100000, 999999)}" |
|
self.reset_token_expiration = datetime.datetime.now() + datetime.timedelta( |
|
minutes=15 |
|
) |
|
await self.save() |
|
await self.send_reset_token_message() |
|
|
|
async def reset_password(self, reset_token: str, new_password: str) -> bool: |
|
""" |
|
Resets the user's password if the provided token is valid and not expired. |
|
Sends a confirmation message upon successful reset. |
|
""" |
|
if ( |
|
self.reset_token != reset_token |
|
or datetime.datetime.now() > self.reset_token_expiration |
|
): |
|
logger.error(f"Invalid or expired reset token for user {self.phoneNumber}.") |
|
return False |
|
self.password = self.hash_password(new_password) |
|
self.reset_token = None |
|
self.reset_token_expiration = None |
|
await self.save() |
|
await self.send_password_reset_confirmation() |
|
return True |
|
|
|
@classmethod |
|
async def create_user(cls, user_data: dict) -> models.Model: |
|
""" |
|
Creates a new user, registers with AndroidClient, and sends a welcome message. |
|
""" |
|
try: |
|
|
|
plain_password = user_data.get("password") |
|
if not plain_password: |
|
logger.error("Password not provided during user creation.") |
|
raise ValueError("Password is required.") |
|
hashed_password = pwd_context.hash(plain_password) |
|
user_data["password"] = hashed_password |
|
|
|
|
|
new_user = await cls.create(**user_data) |
|
logger.info(f"User {new_user.phoneNumber} created successfully.") |
|
|
|
|
|
register_request = AndroidRegister( |
|
password=plain_password, |
|
phoneNumber=new_user.phoneNumber, |
|
) |
|
try: |
|
response = await cls.android_client.register_user( |
|
request=register_request |
|
) |
|
logger.info( |
|
f"AndroidClient register_user response for {new_user.phoneNumber}: {response}" |
|
) |
|
except Exception as e: |
|
logger.error( |
|
f"Failed to register user with AndroidClient for {new_user.phoneNumber}: {e}" |
|
) |
|
|
|
|
|
|
|
|
|
await new_user.send_welcome_message() |
|
|
|
return new_user |
|
except Exception as e: |
|
logger.error(f"Error creating user: {e}") |
|
raise e |
|
|
|
async def send_welcome_message(self): |
|
"""Sends a welcome message to the user.""" |
|
message = self.message_templates.registration_message(user_name=self.name) |
|
try: |
|
await self.android_client.send_message( |
|
phone_numbers=self.phoneNumber, message=message |
|
) |
|
logger.info(f"Welcome message sent to {self.phoneNumber}.") |
|
except Exception as e: |
|
logger.error(f"Failed to send welcome message to {self.phoneNumber}: {e}") |
|
|
|
async def send_reset_token_message(self): |
|
"""Sends the reset token to the user's phone number.""" |
|
message = self.message_templates.reset_token_message(token=self.reset_token) |
|
try: |
|
await self.android_client.send_message( |
|
phone_numbers=self.phoneNumber, message=message |
|
) |
|
logger.info(f"Reset token sent to {self.phoneNumber}.") |
|
except Exception as e: |
|
logger.error(f"Failed to send reset token to {self.phoneNumber}: {e}") |
|
|
|
async def send_password_reset_confirmation(self): |
|
"""Sends a confirmation message after a successful password reset.""" |
|
message = self.message_templates.password_reset_confirmation_message() |
|
try: |
|
await self.android_client.send_message( |
|
phone_numbers=self.phoneNumber, message=message |
|
) |
|
logger.info(f"Password reset confirmation sent to {self.phoneNumber}.") |
|
except Exception as e: |
|
logger.error( |
|
f"Failed to send password reset confirmation to {self.phoneNumber}: {e}" |
|
) |
|
|
|
async def toggle_account_status(self): |
|
"""Toggles the user's account status and sends a notification message.""" |
|
self.account_locked = not self.account_locked |
|
await self.save() |
|
if self.account_locked: |
|
message = self.message_templates.account_disabled_message() |
|
logger.info(f"Account {self.phoneNumber} has been locked.") |
|
else: |
|
message = self.message_templates.account_enabled_message() |
|
logger.info(f"Account {self.phoneNumber} has been unlocked.") |
|
|
|
try: |
|
await self.android_client.send_message( |
|
phone_numbers=self.phoneNumber, message=message |
|
) |
|
logger.info(f"Account status message sent to {self.phoneNumber}.") |
|
except Exception as e: |
|
logger.error( |
|
f"Failed to send account status message to {self.phoneNumber}: {e}" |
|
) |
|
|
|
async def send_payment_success_message( |
|
self, amount: Decimal, plan_name: Optional[str] = None |
|
): |
|
""" |
|
Sends a payment success message to the user. |
|
If a plan is associated, it includes subscription details. |
|
""" |
|
if plan_name: |
|
message = self.message_templates.payment_success_subscription_message( |
|
user_name=self.name, amount=amount, plan_name=plan_name |
|
) |
|
else: |
|
message = self.message_templates.payment_success_balance_message( |
|
user_name=self.name, amount=amount |
|
) |
|
try: |
|
await self.android_client.send_message( |
|
phone_numbers=self.phoneNumber, message=message |
|
) |
|
logger.info(f"Payment success message sent to {self.phoneNumber}.") |
|
except Exception as e: |
|
logger.error( |
|
f"Failed to send payment success message to {self.phoneNumber}: {e}" |
|
) |
|
|
|
async def send_insufficient_funds_message( |
|
self, required_amount: Decimal, attempted_amount: Decimal |
|
): |
|
""" |
|
Sends a message to the user indicating insufficient funds for the attempted payment. |
|
""" |
|
message = self.message_templates.insufficient_funds_message( |
|
user_name=self.name, |
|
required_amount=required_amount, |
|
attempted_amount=attempted_amount, |
|
) |
|
try: |
|
await self.android_client.send_message( |
|
phone_numbers=self.phoneNumber, message=message |
|
) |
|
logger.info(f"Insufficient funds message sent to {self.phoneNumber}.") |
|
except Exception as e: |
|
logger.error( |
|
f"Failed to send insufficient funds message to {self.phoneNumber}: {e}" |
|
) |
|
|