|
|
|
|
|
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 |
|
from App.Subscriptions.Model import Subscription |
|
from App.Plans.Model import Plan |
|
from .Constants import UserType |
|
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) |
|
user_type = fields.IntField(default=UserType.USER) |
|
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) |
|
|
|
async def send_message(self, message: str): |
|
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 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() |
|
await 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.") |
|
await self.save() |
|
return False |
|
|
|
async def toggle_status(self): |
|
if self.account_locked: |
|
await self.activate_user() |
|
self.account_locked = False |
|
else: |
|
await self.deactivate_user() |
|
self.account_locked = True |
|
|
|
await self.save() |
|
|
|
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 |
|
|
|
|
|
register_request = AndroidRegister( |
|
password=plain_password, |
|
phoneNumber=user_data["phoneNumber"], |
|
) |
|
try: |
|
response = await cls.android_client.register_user( |
|
request=register_request |
|
) |
|
logger.info( |
|
f"AndroidClient register_user response for {user_data['phoneNumber']}: {response}" |
|
) |
|
except Exception as e: |
|
logger.error( |
|
f"Failed to register user with AndroidClient for {user_data['phoneNumber']}: {e}" |
|
) |
|
logger.error(f"Error creating user: {e}") |
|
raise e |
|
new_user = await cls.create(**user_data) |
|
logger.info(f"User {new_user.phoneNumber} created successfully.") |
|
await new_user.send_welcome_message() |
|
|
|
promo_plans = await Plan.filter(is_promo=True).all() |
|
for promo_plan in promo_plans: |
|
plan_valid = await promo_plan.is_promo_valid() |
|
if plan_valid: |
|
await new_user.create_subscription( |
|
plan=promo_plan, send_message=True |
|
) |
|
await new_user.activate_user() |
|
break |
|
|
|
|
|
|
|
|
|
|
|
|
|
return new_user |
|
except Exception as e: |
|
logger.error(f"Error creating user: {e}") |
|
raise e |
|
|
|
async def send_plan_subscription_message(self, plan: Plan, expiration_time): |
|
message = self.message_templates.subscription_created_message( |
|
expiration_time=expiration_time, plan_name=plan.name |
|
) |
|
await self.send_message(message=message) |
|
|
|
async def create_subscription(self, plan: Plan, send_message=False): |
|
expiration_time = datetime.datetime.now() + datetime.timedelta( |
|
hours=plan.duration |
|
) |
|
|
|
await Subscription.create( |
|
user=self, |
|
duration=plan.duration, |
|
download_mb=plan.download_speed * 1024, |
|
upload_mb=plan.upload_speed * 1024, |
|
created_time=datetime.datetime.now(), |
|
expiration_time=expiration_time, |
|
active=True, |
|
) |
|
await self.activate_user() |
|
if send_message: |
|
await self.send_plan_subscription_message( |
|
plan=plan, expiration_time=expiration_time |
|
) |
|
|
|
async def send_welcome_message(self): |
|
"""Sends a welcome message to the user.""" |
|
message = self.message_templates.registration_message(user_name=self.name) |
|
await self.send_message(message=message) |
|
|
|
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.balance_assigned_message( |
|
amount=amount, new_balance=self.balance |
|
) |
|
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}" |
|
) |
|
|
|
async def activate_user(self): |
|
"""Activate the user using the Android client.""" |
|
try: |
|
response = await self.android_client.activate_user(self.phoneNumber) |
|
if response.get("status") == "success": |
|
logger.info(f"User {self.phoneNumber} activated successfully.") |
|
self.account_locked = False |
|
await self.save() |
|
return True |
|
else: |
|
logger.error( |
|
f"Failed to activate user {self.phoneNumber}: {response.get('message')}" |
|
) |
|
return False |
|
except Exception as e: |
|
logger.error(f"Error activating user {self.phoneNumber}: {str(e)}") |
|
return False |
|
|
|
async def deactivate_user(self): |
|
|
|
await self.remover_user_session() |
|
|
|
try: |
|
await self.android_client.deactivate_user(self.phoneNumber) |
|
except Exception as e: |
|
logger.error(f"Failed to deactivate user {self.phoneNumber}: {e}") |
|
raise e |
|
|
|
async def send_subcription_expired_message(self, plan: Plan): |
|
try: |
|
message = self.message_templates.subscription_expired_message( |
|
user_name=self.name, plan_name=plan.name |
|
) |
|
await self.send_message(message=message) |
|
logger.info(f"Subscription expired message sent to {self.phoneNumber}.") |
|
except Exception as e: |
|
logger.error( |
|
f"Failed to send subscription expired message to {self.phoneNumber}: {e}" |
|
) |
|
raise e |
|
|
|
async def is_active(self): |
|
try: |
|
return await self.android_client.is_user_active(self.phoneNumber) |
|
except Exception as e: |
|
raise e |
|
|
|
async def remover_user_session(self): |
|
try: |
|
await self.android_client.remove_session(self.phoneNumber) |
|
except Exception as e: |
|
logger.error(f"Failed to remove session for {self.phoneNumber}: {e}") |
|
raise e |
|
|