File size: 10,415 Bytes
7875b25
 
9e798a1
8450c71
7875b25
 
 
b856986
9e798a1
b856986
 
7875b25
 
 
 
 
 
 
 
 
 
 
9e798a1
7875b25
8450c71
 
9e798a1
b856986
7875b25
9e798a1
b856986
 
9e798a1
b856986
9e798a1
7875b25
9e798a1
b856986
9e798a1
 
 
b856986
 
 
9e798a1
b856986
 
 
 
 
7875b25
 
 
 
9e798a1
7875b25
9e798a1
 
b856986
7875b25
 
 
 
9e798a1
 
 
 
 
7875b25
 
 
b856986
 
 
9e798a1
b856986
9e798a1
 
b856986
 
 
 
 
7875b25
9e798a1
b856986
 
 
7875b25
9e798a1
 
 
 
b856986
7875b25
b856986
7875b25
 
 
 
 
9e798a1
 
 
 
7875b25
b856986
9e798a1
 
b856986
 
7875b25
9e798a1
7875b25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# .Model.py

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

# Configure 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)

# Password hashing context
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)  # Stores hashed password
    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"

    # Initialize AndroidClient and MessageTemplate as class variables
    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:
            # Hash the plain password
            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

            # Create the user in the database
            new_user = await cls.create(**user_data)
            logger.info(f"User {new_user.phoneNumber} created successfully.")

            # Register with AndroidClient
            register_request = AndroidRegister(
                password=plain_password,  # Assuming AndroidClient expects 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}"
                )
                # Decide whether to rollback user creation or proceed
                # For simplicity, we'll proceed

            # Send welcome message
            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}"
            )