changes
Browse files- App/Android/Android.py +8 -4
- App/Android/Schema.py +2 -1
- App/Messages/MessagesRoute.py +27 -4
- App/Payments/PaymentsRoutes.py +77 -2
- App/Payments/Schema.py +7 -0
- App/Users/Constants.py +11 -0
- App/Users/Model.py +13 -2
- App/Users/Schema.py +2 -1
- App/Users/UserRoutes.py +89 -18
- App/Users/dependencies.py +13 -1
- migrations/models/0_20241204103359_init.py +105 -0
- migrations/models/1_20241204103440_add_user_type_field.py +11 -0
App/Android/Android.py
CHANGED
@@ -75,14 +75,18 @@ class AndroidClient:
|
|
75 |
async def remove_session(self, phone_number) -> APIResponse:
|
76 |
"""Enable or disable a user."""
|
77 |
path = f"/users/{phone_number}/remove-session"
|
78 |
-
|
79 |
-
|
|
|
|
|
|
|
80 |
|
81 |
@require_base_url
|
82 |
-
async def
|
83 |
"""disable a user."""
|
84 |
path = f"/users/{request.phone}/disable"
|
85 |
response = await self.client.post(path, json=None)
|
|
|
86 |
return APIResponse(**response.json())
|
87 |
|
88 |
@require_base_url
|
@@ -143,6 +147,6 @@ class AndroidClient:
|
|
143 |
|
144 |
async def deactivate_user(self, phone_number: str):
|
145 |
request = SetUserStatusRequest(phone=phone_number, disabled=False)
|
146 |
-
await self.
|
147 |
# Replace this with actual API call logic
|
148 |
return {"status": "success", "message": "User activated."}
|
|
|
75 |
async def remove_session(self, phone_number) -> APIResponse:
|
76 |
"""Enable or disable a user."""
|
77 |
path = f"/users/{phone_number}/remove-session"
|
78 |
+
try:
|
79 |
+
response = await self.client.post(path, json=None)
|
80 |
+
return APIResponse(**response.json())
|
81 |
+
except:
|
82 |
+
pass
|
83 |
|
84 |
@require_base_url
|
85 |
+
async def _disable_request(self, request: SetUserStatusRequest) -> APIResponse:
|
86 |
"""disable a user."""
|
87 |
path = f"/users/{request.phone}/disable"
|
88 |
response = await self.client.post(path, json=None)
|
89 |
+
|
90 |
return APIResponse(**response.json())
|
91 |
|
92 |
@require_base_url
|
|
|
147 |
|
148 |
async def deactivate_user(self, phone_number: str):
|
149 |
request = SetUserStatusRequest(phone=phone_number, disabled=False)
|
150 |
+
await self._disable_request(request=request)
|
151 |
# Replace this with actual API call logic
|
152 |
return {"status": "success", "message": "User activated."}
|
App/Android/Schema.py
CHANGED
@@ -66,8 +66,9 @@ class UserListItem(BaseModel):
|
|
66 |
class APIResponse(BaseModel):
|
67 |
success: bool
|
68 |
message: str
|
69 |
-
data: Optional[
|
70 |
error: Optional[str] = None
|
|
|
71 |
|
72 |
|
73 |
# Specific Response Schemas
|
|
|
66 |
class APIResponse(BaseModel):
|
67 |
success: bool
|
68 |
message: str
|
69 |
+
data: Optional[Any] = None
|
70 |
error: Optional[str] = None
|
71 |
+
active_users: Optional[Any] = None
|
72 |
|
73 |
|
74 |
# Specific Response Schemas
|
App/Messages/MessagesRoute.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1 |
# App/Messages/Routes.py
|
2 |
-
from fastapi import APIRouter, HTTPException
|
|
|
3 |
from .Model import Message
|
4 |
from .Schema import MessageCreate, MessageResponse
|
5 |
from typing import Optional
|
@@ -16,6 +17,30 @@ from tortoise.contrib.pydantic import pydantic_model_creator
|
|
16 |
import traceback
|
17 |
import logging
|
18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
message_router = APIRouter(tags=["Messages"], prefix="/messages")
|
20 |
logging.basicConfig(level=logging.WARNING)
|
21 |
|
@@ -57,9 +82,7 @@ async def receive_message(message_data: MessageCreate):
|
|
57 |
message = await Message.get_or_none(message_id=message_data.id)
|
58 |
if not message:
|
59 |
# Process Mpesa Payments
|
60 |
-
user: User = await User.get_or_none(
|
61 |
-
phoneNumber="+" + parsed_data["phone_number"]
|
62 |
-
)
|
63 |
data_plan: Plan = await Plan.get_or_none(
|
64 |
amount=Decimal(parsed_data["amount_received"])
|
65 |
)
|
|
|
1 |
# App/Messages/Routes.py
|
2 |
+
from fastapi import APIRouter, HTTPException, Depends
|
3 |
+
from typing import List
|
4 |
from .Model import Message
|
5 |
from .Schema import MessageCreate, MessageResponse
|
6 |
from typing import Optional
|
|
|
17 |
import traceback
|
18 |
import logging
|
19 |
|
20 |
+
from App.Users.dependencies import (
|
21 |
+
get_current_active_user,
|
22 |
+
UserType,
|
23 |
+
) # Assuming you have a dependency to get the current user
|
24 |
+
|
25 |
+
message_router = APIRouter(tags=["Messages"])
|
26 |
+
|
27 |
+
|
28 |
+
@message_router.get("/messages", response_model=List[MessageResponse])
|
29 |
+
async def get_all_messages(current_user: User = Depends(get_current_active_user)):
|
30 |
+
# Check if the current user is an admin
|
31 |
+
if current_user.user_type != UserType.ADMIN:
|
32 |
+
raise HTTPException(
|
33 |
+
status_code=403,
|
34 |
+
detail="User does not have permission to access this resource",
|
35 |
+
)
|
36 |
+
|
37 |
+
# Fetch all messages from the database
|
38 |
+
messages = await Message.all()
|
39 |
+
|
40 |
+
# Serialize the messages
|
41 |
+
return [MessageResponse.from_orm(message) for message in messages]
|
42 |
+
|
43 |
+
|
44 |
message_router = APIRouter(tags=["Messages"], prefix="/messages")
|
45 |
logging.basicConfig(level=logging.WARNING)
|
46 |
|
|
|
82 |
message = await Message.get_or_none(message_id=message_data.id)
|
83 |
if not message:
|
84 |
# Process Mpesa Payments
|
85 |
+
user: User = await User.get_or_none(phoneNumber=parsed_data["phone_number"])
|
|
|
|
|
86 |
data_plan: Plan = await Plan.get_or_none(
|
87 |
amount=Decimal(parsed_data["amount_received"])
|
88 |
)
|
App/Payments/PaymentsRoutes.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1 |
-
from fastapi import APIRouter, HTTPException, status
|
2 |
from typing import List
|
|
|
3 |
from .Model import Payment
|
4 |
from App.Users.Model import User
|
5 |
from App.Plans.Model import Plan
|
@@ -12,12 +13,17 @@ from .Schema import (
|
|
12 |
PaymentListResponse,
|
13 |
)
|
14 |
from .Schema import PaymentMethod
|
|
|
|
|
|
|
|
|
|
|
15 |
|
16 |
payment_router = APIRouter(tags=["Payments"])
|
17 |
|
18 |
|
19 |
@payment_router.post("/payment/create", response_model=BaseResponse)
|
20 |
-
async def create_payment(request: CreatePaymentRequest, internal=False):
|
21 |
# If payment method is "Lipa Number", transaction_id is required
|
22 |
if (
|
23 |
request.payment_method == PaymentMethod.LIPA_NUMBER
|
@@ -35,6 +41,14 @@ async def create_payment(request: CreatePaymentRequest, internal=False):
|
|
35 |
status_code=status.HTTP_404_NOT_FOUND, detail="Plan not found"
|
36 |
)
|
37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
38 |
# Create new payment without linking a user initially
|
39 |
payment = await Payment.create(
|
40 |
user_id=request.user_id,
|
@@ -148,3 +162,64 @@ async def link_user_to_payment(payment_id: str, request: UpdatePaymentUserReques
|
|
148 |
message="Payment linked to user successfully",
|
149 |
payload={"payment_id": str(payment.id), "user_id": request.user_id},
|
150 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, HTTPException, status, Query, Depends
|
2 |
from typing import List
|
3 |
+
from datetime import datetime
|
4 |
from .Model import Payment
|
5 |
from App.Users.Model import User
|
6 |
from App.Plans.Model import Plan
|
|
|
13 |
PaymentListResponse,
|
14 |
)
|
15 |
from .Schema import PaymentMethod
|
16 |
+
from App.Users.dependencies import (
|
17 |
+
get_current_active_user,
|
18 |
+
UserType,
|
19 |
+
) # Assuming you have a dependency to get the current user
|
20 |
+
|
21 |
|
22 |
payment_router = APIRouter(tags=["Payments"])
|
23 |
|
24 |
|
25 |
@payment_router.post("/payment/create", response_model=BaseResponse)
|
26 |
+
async def create_payment(request: CreatePaymentRequest, internal: bool = False):
|
27 |
# If payment method is "Lipa Number", transaction_id is required
|
28 |
if (
|
29 |
request.payment_method == PaymentMethod.LIPA_NUMBER
|
|
|
41 |
status_code=status.HTTP_404_NOT_FOUND, detail="Plan not found"
|
42 |
)
|
43 |
|
44 |
+
payment = await Payment.get_or_none(transaction_id=request.transaction_id)
|
45 |
+
|
46 |
+
if payment:
|
47 |
+
return BaseResponse(
|
48 |
+
code=404,
|
49 |
+
message="Payment already exists",
|
50 |
+
payload={"payment_id": str(payment.id)},
|
51 |
+
)
|
52 |
# Create new payment without linking a user initially
|
53 |
payment = await Payment.create(
|
54 |
user_id=request.user_id,
|
|
|
162 |
message="Payment linked to user successfully",
|
163 |
payload={"payment_id": str(payment.id), "user_id": request.user_id},
|
164 |
)
|
165 |
+
|
166 |
+
|
167 |
+
@payment_router.get("/payments/date-range", response_model=PaymentListResponse)
|
168 |
+
async def get_payments_by_date_range(
|
169 |
+
start_date: datetime = Query(..., description="Start date in ISO format"),
|
170 |
+
end_date: datetime = Query(..., description="End date in ISO format"),
|
171 |
+
):
|
172 |
+
if start_date > end_date:
|
173 |
+
raise HTTPException(
|
174 |
+
status_code=400, detail="Start date must be less than or equal to end date"
|
175 |
+
)
|
176 |
+
|
177 |
+
payments = await Payment.filter(created_time__range=(start_date, end_date))
|
178 |
+
|
179 |
+
result = [
|
180 |
+
PaymentResponse(
|
181 |
+
id=str(payment.id),
|
182 |
+
user_id=payment.user_id,
|
183 |
+
plan_id=payment.plan_id,
|
184 |
+
amount=payment.amount,
|
185 |
+
payment_method=payment.payment_method,
|
186 |
+
status=payment.status,
|
187 |
+
transaction_id=payment.transaction_id,
|
188 |
+
created_time=payment.created_time,
|
189 |
+
updated_time=payment.updated_time,
|
190 |
+
)
|
191 |
+
for payment in payments
|
192 |
+
]
|
193 |
+
|
194 |
+
return PaymentListResponse(payments=result, total_count=len(result))
|
195 |
+
|
196 |
+
|
197 |
+
@payment_router.get("/payments/user/{user_id}", response_model=PaymentListResponse)
|
198 |
+
async def get_user_payment_history(
|
199 |
+
user_id: str, current_user: User = Depends(get_current_active_user)
|
200 |
+
):
|
201 |
+
# Optionally, you can check if the current user has permission to view this user's payment history
|
202 |
+
if current_user.user_type != UserType.ADMIN and current_user.id != user_id:
|
203 |
+
raise HTTPException(
|
204 |
+
status_code=403,
|
205 |
+
detail="User does not have permission to view this payment history",
|
206 |
+
)
|
207 |
+
|
208 |
+
payments = await Payment.filter(user_id=user_id)
|
209 |
+
|
210 |
+
result = [
|
211 |
+
PaymentResponse(
|
212 |
+
id=str(payment.id),
|
213 |
+
user_id=payment.user_id,
|
214 |
+
plan_id=payment.plan_id,
|
215 |
+
amount=payment.amount,
|
216 |
+
payment_method=payment.payment_method,
|
217 |
+
status=payment.status,
|
218 |
+
transaction_id=payment.transaction_id,
|
219 |
+
created_time=payment.created_time,
|
220 |
+
updated_time=payment.updated_time,
|
221 |
+
)
|
222 |
+
for payment in payments
|
223 |
+
]
|
224 |
+
|
225 |
+
return PaymentListResponse(payments=result, total_count=len(result))
|
App/Payments/Schema.py
CHANGED
@@ -10,6 +10,7 @@ class BaseResponse(BaseModel):
|
|
10 |
payload: Optional[dict] = None
|
11 |
|
12 |
|
|
|
13 |
class PaymentMethod:
|
14 |
CASH = "cash"
|
15 |
MPESA = "mpesa"
|
@@ -19,6 +20,12 @@ class PaymentMethod:
|
|
19 |
CHOICES = [CASH, MPESA, LIPA_NUMBER, CREDIT_CARD]
|
20 |
|
21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
class CreatePaymentRequest(BaseModel):
|
23 |
user_id: Optional[str] = Field(
|
24 |
None, description="ID of the user making the payment"
|
|
|
10 |
payload: Optional[dict] = None
|
11 |
|
12 |
|
13 |
+
### Constants
|
14 |
class PaymentMethod:
|
15 |
CASH = "cash"
|
16 |
MPESA = "mpesa"
|
|
|
20 |
CHOICES = [CASH, MPESA, LIPA_NUMBER, CREDIT_CARD]
|
21 |
|
22 |
|
23 |
+
class PaymentStatus:
|
24 |
+
ADDED_TO_BALANCE = "Added to balance"
|
25 |
+
PURCHASED_PLAN = "Purchased plan"
|
26 |
+
PENDING = "PENDING"
|
27 |
+
|
28 |
+
|
29 |
class CreatePaymentRequest(BaseModel):
|
30 |
user_id: Optional[str] = Field(
|
31 |
None, description="ID of the user making the payment"
|
App/Users/Constants.py
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
class UserType:
|
2 |
+
ADMIN = 1
|
3 |
+
MODERATOR = 2
|
4 |
+
USER = 4
|
5 |
+
|
6 |
+
|
7 |
+
class ErrorCodes:
|
8 |
+
NOT_FOUND = 1
|
9 |
+
MIKROTIK = 2
|
10 |
+
INVALID_CREDENTIALS = 3
|
11 |
+
SERVER_ERROR = 0
|
App/Users/Model.py
CHANGED
@@ -7,6 +7,7 @@ from App.Android.Schema import RegisterUserRequest as AndroidRegister
|
|
7 |
from App.Templates.Templates import MessageTemplate
|
8 |
from App.Subscriptions.Model import Subscription
|
9 |
from App.Plans.Model import Plan
|
|
|
10 |
import datetime
|
11 |
import uuid
|
12 |
import random
|
@@ -36,6 +37,7 @@ class User(models.Model):
|
|
36 |
id = fields.CharField(primary_key=True, max_length=5, default=generate_short_uuid)
|
37 |
name = fields.CharField(max_length=100)
|
38 |
password = fields.CharField(max_length=100) # Stores hashed password
|
|
|
39 |
phoneNumber = fields.CharField(max_length=15, unique=True)
|
40 |
balance = fields.DecimalField(max_digits=10, decimal_places=2, default=0.00)
|
41 |
mac_address = fields.CharField(max_length=17)
|
@@ -43,6 +45,7 @@ class User(models.Model):
|
|
43 |
updatedAt = fields.DatetimeField(auto_now=True)
|
44 |
lastLogin = fields.DatetimeField(default=datetime.datetime.now)
|
45 |
failed_attempts = fields.IntField(default=0)
|
|
|
46 |
account_locked = fields.BooleanField(default=False)
|
47 |
reset_token = fields.CharField(max_length=6, null=True, unique=True)
|
48 |
reset_token_expiration = fields.DatetimeField(null=True)
|
@@ -96,6 +99,16 @@ class User(models.Model):
|
|
96 |
await self.save()
|
97 |
return False
|
98 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
99 |
async def initiate_password_reset(self):
|
100 |
"""Generates a reset token and sends it to the user via message."""
|
101 |
self.reset_token = f"{random.randint(100000, 999999)}"
|
@@ -321,8 +334,6 @@ class User(models.Model):
|
|
321 |
|
322 |
async def deactivate_user(self):
|
323 |
|
324 |
-
self.account_locked = True
|
325 |
-
await self.save()
|
326 |
await self.remover_user_session()
|
327 |
|
328 |
try:
|
|
|
7 |
from App.Templates.Templates import MessageTemplate
|
8 |
from App.Subscriptions.Model import Subscription
|
9 |
from App.Plans.Model import Plan
|
10 |
+
from .Constants import UserType
|
11 |
import datetime
|
12 |
import uuid
|
13 |
import random
|
|
|
37 |
id = fields.CharField(primary_key=True, max_length=5, default=generate_short_uuid)
|
38 |
name = fields.CharField(max_length=100)
|
39 |
password = fields.CharField(max_length=100) # Stores hashed password
|
40 |
+
user_type = fields.IntField(default=UserType.USER)
|
41 |
phoneNumber = fields.CharField(max_length=15, unique=True)
|
42 |
balance = fields.DecimalField(max_digits=10, decimal_places=2, default=0.00)
|
43 |
mac_address = fields.CharField(max_length=17)
|
|
|
45 |
updatedAt = fields.DatetimeField(auto_now=True)
|
46 |
lastLogin = fields.DatetimeField(default=datetime.datetime.now)
|
47 |
failed_attempts = fields.IntField(default=0)
|
48 |
+
|
49 |
account_locked = fields.BooleanField(default=False)
|
50 |
reset_token = fields.CharField(max_length=6, null=True, unique=True)
|
51 |
reset_token_expiration = fields.DatetimeField(null=True)
|
|
|
99 |
await self.save()
|
100 |
return False
|
101 |
|
102 |
+
async def toggle_status(self):
|
103 |
+
if self.account_locked:
|
104 |
+
await self.activate_user()
|
105 |
+
self.account_locked = False
|
106 |
+
else:
|
107 |
+
await self.deactivate_user()
|
108 |
+
self.account_locked = True
|
109 |
+
# self.account_locked = not self.account_locked
|
110 |
+
await self.save()
|
111 |
+
|
112 |
async def initiate_password_reset(self):
|
113 |
"""Generates a reset token and sends it to the user via message."""
|
114 |
self.reset_token = f"{random.randint(100000, 999999)}"
|
|
|
334 |
|
335 |
async def deactivate_user(self):
|
336 |
|
|
|
|
|
337 |
await self.remover_user_session()
|
338 |
|
339 |
try:
|
App/Users/Schema.py
CHANGED
@@ -54,7 +54,8 @@ class RegisterUserRequest(BaseModel):
|
|
54 |
class LoginUserRequest(BaseModel):
|
55 |
phoneNumber: str = Field(..., pattern=PHONE_PATTERN)
|
56 |
password: str
|
57 |
-
|
|
|
58 |
|
59 |
|
60 |
# Access Token Response
|
|
|
54 |
class LoginUserRequest(BaseModel):
|
55 |
phoneNumber: str = Field(..., pattern=PHONE_PATTERN)
|
56 |
password: str
|
57 |
+
# grant_type: Optional[str] = None
|
58 |
+
mac_address: Optional[str] = Field(..., pattern=MAC_PATTERN)
|
59 |
|
60 |
|
61 |
# Access Token Response
|
App/Users/UserRoutes.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1 |
-
from fastapi import APIRouter, HTTPException, status, Depends
|
|
|
2 |
from .Schema import (
|
3 |
RegisterUserRequest,
|
4 |
LoginUserRequest,
|
@@ -17,12 +18,14 @@ from App.Subscriptions.Model import Subscription
|
|
17 |
from App.Android.Android import AndroidClient
|
18 |
from App.Android.Schema import RegisterUserRequest as AndroidRegister
|
19 |
from App.Templates.Templates import MessageTemplate
|
20 |
-
from .dependencies import get_current_active_user
|
|
|
|
|
21 |
|
22 |
# JWT Configurations
|
23 |
SECRET_KEY = "your_secret_key_here"
|
24 |
ALGORITHM = "HS256"
|
25 |
-
ACCESS_TOKEN_EXPIRE_MINUTES =
|
26 |
user_router = APIRouter(tags=["User"])
|
27 |
client = AndroidClient()
|
28 |
templates = MessageTemplate()
|
@@ -64,6 +67,13 @@ async def register_user(request: RegisterUserRequest):
|
|
64 |
async def login_user(request: LoginUserRequest):
|
65 |
# Find user by phone number
|
66 |
db_user: User = await User.filter(phoneNumber=request.phoneNumber).first()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
67 |
valid_password = await db_user.verify_password(request.password)
|
68 |
# Check if user exists and password is correct
|
69 |
if db_user and valid_password:
|
@@ -81,6 +91,7 @@ async def login_user(request: LoginUserRequest):
|
|
81 |
await db_user.remover_user_session()
|
82 |
access_token = create_access_token(
|
83 |
data={
|
|
|
84 |
"user_name": db_user.name,
|
85 |
"sub": db_user.phoneNumber,
|
86 |
"locked": db_user.account_locked,
|
@@ -142,24 +153,26 @@ async def reset_password(request: ResetPasswordRequest):
|
|
142 |
|
143 |
|
144 |
@user_router.get(
|
145 |
-
"/user/all",
|
|
|
|
|
146 |
)
|
147 |
-
async def get_all_users():
|
148 |
users = await User.all()
|
149 |
return [UserResponse.from_orm(user) for user in users]
|
150 |
|
151 |
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
|
164 |
|
165 |
@user_router.put(
|
@@ -173,8 +186,7 @@ async def toggle_user_status(user_id: str):
|
|
173 |
raise HTTPException(
|
174 |
status_code=status.HTTP_404_NOT_FOUND, detail="User not found."
|
175 |
)
|
176 |
-
|
177 |
-
await user.save()
|
178 |
message = (
|
179 |
"User disabled successfully."
|
180 |
if user.account_locked
|
@@ -207,3 +219,62 @@ async def activate_user(user_id: str):
|
|
207 |
raise HTTPException(
|
208 |
status_code=status.HTTP_400_BAD_REQUEST, detail="Failed to activate user."
|
209 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, HTTPException, status, Depends, Query
|
2 |
+
from fastapi.responses import JSONResponse
|
3 |
from .Schema import (
|
4 |
RegisterUserRequest,
|
5 |
LoginUserRequest,
|
|
|
18 |
from App.Android.Android import AndroidClient
|
19 |
from App.Android.Schema import RegisterUserRequest as AndroidRegister
|
20 |
from App.Templates.Templates import MessageTemplate
|
21 |
+
from .dependencies import get_current_active_user, get_admin_user
|
22 |
+
from App.Android.Schema import APIResponse
|
23 |
+
|
24 |
|
25 |
# JWT Configurations
|
26 |
SECRET_KEY = "your_secret_key_here"
|
27 |
ALGORITHM = "HS256"
|
28 |
+
ACCESS_TOKEN_EXPIRE_MINUTES = 300
|
29 |
user_router = APIRouter(tags=["User"])
|
30 |
client = AndroidClient()
|
31 |
templates = MessageTemplate()
|
|
|
67 |
async def login_user(request: LoginUserRequest):
|
68 |
# Find user by phone number
|
69 |
db_user: User = await User.filter(phoneNumber=request.phoneNumber).first()
|
70 |
+
|
71 |
+
# Raise an error if the user does not exist
|
72 |
+
if db_user is None:
|
73 |
+
raise HTTPException(
|
74 |
+
status_code=status.HTTP_404_NOT_FOUND, detail="User not found."
|
75 |
+
)
|
76 |
+
|
77 |
valid_password = await db_user.verify_password(request.password)
|
78 |
# Check if user exists and password is correct
|
79 |
if db_user and valid_password:
|
|
|
91 |
await db_user.remover_user_session()
|
92 |
access_token = create_access_token(
|
93 |
data={
|
94 |
+
"user_type": db_user.user_type,
|
95 |
"user_name": db_user.name,
|
96 |
"sub": db_user.phoneNumber,
|
97 |
"locked": db_user.account_locked,
|
|
|
153 |
|
154 |
|
155 |
@user_router.get(
|
156 |
+
"/user/all",
|
157 |
+
response_model=List[UserResponse],
|
158 |
+
status_code=status.HTTP_200_OK,
|
159 |
)
|
160 |
+
async def get_all_users(admin: User = Depends(get_admin_user)):
|
161 |
users = await User.all()
|
162 |
return [UserResponse.from_orm(user) for user in users]
|
163 |
|
164 |
|
165 |
+
@user_router.delete(
|
166 |
+
"/user/{user_id}", response_model=BaseResponse, status_code=status.HTTP_200_OK
|
167 |
+
)
|
168 |
+
async def delete_user(user_id: str, admin: User = Depends(get_current_active_user)):
|
169 |
+
user = await User.filter(id=user_id).first()
|
170 |
+
if not user:
|
171 |
+
raise HTTPException(
|
172 |
+
status_code=status.HTTP_404_NOT_FOUND, detail="User not found."
|
173 |
+
)
|
174 |
+
await user.delete()
|
175 |
+
return BaseResponse(code=200, message="User deleted successfully.")
|
176 |
|
177 |
|
178 |
@user_router.put(
|
|
|
186 |
raise HTTPException(
|
187 |
status_code=status.HTTP_404_NOT_FOUND, detail="User not found."
|
188 |
)
|
189 |
+
await user.toggle_status()
|
|
|
190 |
message = (
|
191 |
"User disabled successfully."
|
192 |
if user.account_locked
|
|
|
219 |
raise HTTPException(
|
220 |
status_code=status.HTTP_400_BAD_REQUEST, detail="Failed to activate user."
|
221 |
)
|
222 |
+
|
223 |
+
|
224 |
+
@user_router.get(
|
225 |
+
"/users/active", response_model=List[str]
|
226 |
+
) # Change response model to List[str]
|
227 |
+
async def get_active_users():
|
228 |
+
# Fetch all active users using the Android client
|
229 |
+
api_response: APIResponse = await client.get_active_users()
|
230 |
+
active_users = api_response.active_users
|
231 |
+
|
232 |
+
if not api_response or not active_users:
|
233 |
+
return JSONResponse(content={"status": 200, "message": "no users online"})
|
234 |
+
|
235 |
+
# Collect phone numbers from active users
|
236 |
+
active_phone_numbers = [user["phone"] for user in active_users]
|
237 |
+
|
238 |
+
# Filter users from the database using the collected phone numbers
|
239 |
+
users_in_db = await User.filter(phoneNumber__in=active_phone_numbers).all()
|
240 |
+
|
241 |
+
# Extract usernames from the matched users
|
242 |
+
usernames = [
|
243 |
+
{"name": user.name, "phone": user.phoneNumber} for user in users_in_db
|
244 |
+
] # Assuming 'name' is the field for username in the User model
|
245 |
+
|
246 |
+
if not usernames:
|
247 |
+
return JSONResponse(
|
248 |
+
content={"status": 200, "message": "No matching active users found."}
|
249 |
+
)
|
250 |
+
|
251 |
+
return JSONResponse(
|
252 |
+
content={
|
253 |
+
"status": 200,
|
254 |
+
"message": f"Found {len(usernames)} active",
|
255 |
+
"payload": usernames,
|
256 |
+
}
|
257 |
+
)
|
258 |
+
|
259 |
+
|
260 |
+
@user_router.get("/user/active", response_model=List[UserResponse])
|
261 |
+
async def get_active_user(phone_numbers: List[str] = Query(...)):
|
262 |
+
# Fetch all active users using the Android client
|
263 |
+
api_response: APIResponse = await client.get_active_users()
|
264 |
+
active_users = api_response.active_users
|
265 |
+
if not api_response or not api_response.active_users:
|
266 |
+
return JSONResponse(content={"status": 200, "message": "no users online"})
|
267 |
+
|
268 |
+
# Filter active users by matching phone numbers
|
269 |
+
matched_users = [
|
270 |
+
UserResponse.from_orm(user)
|
271 |
+
for user in active_users
|
272 |
+
if user["phone"] in phone_numbers
|
273 |
+
]
|
274 |
+
|
275 |
+
if not matched_users:
|
276 |
+
return JSONResponse(
|
277 |
+
content={"status": 200, "message": "No matching active users found."}
|
278 |
+
)
|
279 |
+
|
280 |
+
return matched_users
|
App/Users/dependencies.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
from fastapi import Depends, HTTPException, status
|
2 |
from jose import jwt
|
3 |
-
from
|
|
|
4 |
from fastapi.security import OAuth2PasswordBearer
|
5 |
|
6 |
SECRET_KEY = "your_secret_key_here"
|
@@ -26,3 +27,14 @@ async def get_current_user(token: str = Depends(oauth2_scheme)):
|
|
26 |
|
27 |
async def get_current_active_user(current_user: User = Depends(get_current_user)):
|
28 |
return current_user
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
from fastapi import Depends, HTTPException, status
|
2 |
from jose import jwt
|
3 |
+
from .Model import User
|
4 |
+
from .Constants import UserType
|
5 |
from fastapi.security import OAuth2PasswordBearer
|
6 |
|
7 |
SECRET_KEY = "your_secret_key_here"
|
|
|
27 |
|
28 |
async def get_current_active_user(current_user: User = Depends(get_current_user)):
|
29 |
return current_user
|
30 |
+
|
31 |
+
|
32 |
+
async def get_admin_user(current_user: User = Depends(get_current_active_user)):
|
33 |
+
print(current_user.user_type, current_user.name)
|
34 |
+
if current_user.user_type != UserType.ADMIN:
|
35 |
+
raise HTTPException(
|
36 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
37 |
+
detail="User does not have permission",
|
38 |
+
headers={"WWW-Authenticate": "Bearer"},
|
39 |
+
)
|
40 |
+
return current_user
|
migrations/models/0_20241204103359_init.py
ADDED
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from tortoise import BaseDBAsyncClient
|
2 |
+
|
3 |
+
|
4 |
+
async def upgrade(db: BaseDBAsyncClient) -> str:
|
5 |
+
return """
|
6 |
+
CREATE TABLE IF NOT EXISTS "messages" (
|
7 |
+
"id" UUID NOT NULL PRIMARY KEY,
|
8 |
+
"device_id" VARCHAR(100),
|
9 |
+
"event" VARCHAR(100),
|
10 |
+
"message_id" VARCHAR(100),
|
11 |
+
"webhook_id" VARCHAR(100),
|
12 |
+
"message_content" TEXT NOT NULL,
|
13 |
+
"phone_number" VARCHAR(20) NOT NULL,
|
14 |
+
"received_at" TIMESTAMPTZ NOT NULL,
|
15 |
+
"sim_number" INT,
|
16 |
+
"parsed_data" JSONB,
|
17 |
+
"created_time" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
18 |
+
);
|
19 |
+
CREATE TABLE IF NOT EXISTS "plans" (
|
20 |
+
"id" UUID NOT NULL PRIMARY KEY,
|
21 |
+
"name" VARCHAR(100) NOT NULL UNIQUE,
|
22 |
+
"amount" DECIMAL(10,2) NOT NULL,
|
23 |
+
"duration" INT NOT NULL,
|
24 |
+
"download_speed" DOUBLE PRECISION NOT NULL,
|
25 |
+
"upload_speed" DOUBLE PRECISION NOT NULL,
|
26 |
+
"expire_date" TIMESTAMPTZ,
|
27 |
+
"is_promo" BOOL NOT NULL DEFAULT False,
|
28 |
+
"promo_duration_days" INT,
|
29 |
+
"is_valid" BOOL NOT NULL DEFAULT True
|
30 |
+
);
|
31 |
+
COMMENT ON COLUMN "plans"."name" IS 'Name of the subscription plan';
|
32 |
+
COMMENT ON COLUMN "plans"."amount" IS 'Cost of the plan';
|
33 |
+
COMMENT ON COLUMN "plans"."duration" IS 'Duration of the subscription in hours';
|
34 |
+
COMMENT ON COLUMN "plans"."download_speed" IS 'Download speed in Mbps';
|
35 |
+
COMMENT ON COLUMN "plans"."upload_speed" IS 'Upload speed in Mbps';
|
36 |
+
COMMENT ON COLUMN "plans"."expire_date" IS 'Expiration date of the plan';
|
37 |
+
COMMENT ON COLUMN "plans"."is_promo" IS 'Indicates if the plan is a promotional plan';
|
38 |
+
COMMENT ON COLUMN "plans"."promo_duration_days" IS 'Number of days the promotion is valid';
|
39 |
+
COMMENT ON COLUMN "plans"."is_valid" IS 'Indicates if the plan is valid';
|
40 |
+
CREATE TABLE IF NOT EXISTS "portals" (
|
41 |
+
"id" SERIAL NOT NULL PRIMARY KEY,
|
42 |
+
"name" VARCHAR(50) NOT NULL UNIQUE,
|
43 |
+
"description" VARCHAR(255) NOT NULL,
|
44 |
+
"url" VARCHAR(255) NOT NULL
|
45 |
+
);
|
46 |
+
COMMENT ON COLUMN "portals"."name" IS 'Name of the portal, e.g., Android or MikroTik';
|
47 |
+
COMMENT ON COLUMN "portals"."description" IS 'Description of the portal';
|
48 |
+
COMMENT ON COLUMN "portals"."url" IS 'URL of the portal, must start with http or https';
|
49 |
+
CREATE TABLE IF NOT EXISTS "users" (
|
50 |
+
"id" VARCHAR(5) NOT NULL PRIMARY KEY,
|
51 |
+
"name" VARCHAR(100) NOT NULL,
|
52 |
+
"password" VARCHAR(100) NOT NULL,
|
53 |
+
"phoneNumber" VARCHAR(15) NOT NULL UNIQUE,
|
54 |
+
"balance" DECIMAL(10,2) NOT NULL DEFAULT 0,
|
55 |
+
"mac_address" VARCHAR(17) NOT NULL,
|
56 |
+
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
57 |
+
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
58 |
+
"lastLogin" TIMESTAMPTZ NOT NULL,
|
59 |
+
"failed_attempts" INT NOT NULL DEFAULT 0,
|
60 |
+
"account_locked" BOOL NOT NULL DEFAULT False,
|
61 |
+
"reset_token" VARCHAR(6) UNIQUE,
|
62 |
+
"reset_token_expiration" TIMESTAMPTZ
|
63 |
+
);
|
64 |
+
CREATE TABLE IF NOT EXISTS "payments" (
|
65 |
+
"id" UUID NOT NULL PRIMARY KEY,
|
66 |
+
"amount" DECIMAL(10,2) NOT NULL,
|
67 |
+
"status" VARCHAR(50) NOT NULL DEFAULT 'pending',
|
68 |
+
"payment_method" VARCHAR(50) NOT NULL,
|
69 |
+
"transaction_id" VARCHAR(100) UNIQUE,
|
70 |
+
"created_time" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
71 |
+
"updated_time" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
72 |
+
"plan_id" UUID REFERENCES "plans" ("id") ON DELETE CASCADE,
|
73 |
+
"user_id" VARCHAR(5) REFERENCES "users" ("id") ON DELETE CASCADE
|
74 |
+
);
|
75 |
+
COMMENT ON COLUMN "payments"."amount" IS 'Payment amount';
|
76 |
+
COMMENT ON COLUMN "payments"."status" IS 'Payment status (e.g., pending, completed, failed, balance-assigned)';
|
77 |
+
COMMENT ON COLUMN "payments"."payment_method" IS 'Payment method';
|
78 |
+
COMMENT ON COLUMN "payments"."transaction_id" IS 'Unique transaction ID for payment (for methods like Lipa Number)';
|
79 |
+
COMMENT ON COLUMN "payments"."plan_id" IS 'Plan associated with the payment';
|
80 |
+
CREATE TABLE IF NOT EXISTS "subscriptions" (
|
81 |
+
"id" UUID NOT NULL PRIMARY KEY,
|
82 |
+
"active" BOOL NOT NULL DEFAULT True,
|
83 |
+
"duration" INT NOT NULL,
|
84 |
+
"download_mb" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
85 |
+
"upload_mb" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
86 |
+
"created_time" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
87 |
+
"expiration_time" TIMESTAMPTZ,
|
88 |
+
"plan_id" UUID REFERENCES "plans" ("id") ON DELETE CASCADE,
|
89 |
+
"user_id" VARCHAR(5) NOT NULL REFERENCES "users" ("id") ON DELETE CASCADE
|
90 |
+
);
|
91 |
+
COMMENT ON COLUMN "subscriptions"."duration" IS 'Duration in hours';
|
92 |
+
COMMENT ON COLUMN "subscriptions"."download_mb" IS 'Download usage in megabytes';
|
93 |
+
COMMENT ON COLUMN "subscriptions"."upload_mb" IS 'Upload usage in megabytes';
|
94 |
+
COMMENT ON COLUMN "subscriptions"."plan_id" IS 'Plan associated with the subscription';
|
95 |
+
CREATE TABLE IF NOT EXISTS "aerich" (
|
96 |
+
"id" SERIAL NOT NULL PRIMARY KEY,
|
97 |
+
"version" VARCHAR(255) NOT NULL,
|
98 |
+
"app" VARCHAR(100) NOT NULL,
|
99 |
+
"content" JSONB NOT NULL
|
100 |
+
);"""
|
101 |
+
|
102 |
+
|
103 |
+
async def downgrade(db: BaseDBAsyncClient) -> str:
|
104 |
+
return """
|
105 |
+
"""
|
migrations/models/1_20241204103440_add_user_type_field.py
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from tortoise import BaseDBAsyncClient
|
2 |
+
|
3 |
+
|
4 |
+
async def upgrade(db: BaseDBAsyncClient) -> str:
|
5 |
+
return """
|
6 |
+
ALTER TABLE "users" ADD "user_type" INT NOT NULL DEFAULT 4;"""
|
7 |
+
|
8 |
+
|
9 |
+
async def downgrade(db: BaseDBAsyncClient) -> str:
|
10 |
+
return """
|
11 |
+
ALTER TABLE "users" DROP COLUMN "user_type";"""
|