Mbonea commited on
Commit
9e798a1
·
1 Parent(s): 27be269

testing deployment

Browse files
.gitignore CHANGED
@@ -1,2 +1,3 @@
1
  *.pyc
2
- *sqlite3*
 
 
1
  *.pyc
2
+ *sqlite3*
3
+ .vercel
App/.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ .vercel
App/Payments/Model.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from tortoise import fields
2
+ from tortoise.models import Model
3
+ import datetime
4
+ from decimal import Decimal
5
+ from App.Users.Model import User
6
+ from App.Plans.Model import Plan
7
+ from App.Subscriptions.Model import Subscription
8
+ from .Schema import PaymentMethod
9
+
10
+
11
+ class Payment(Model):
12
+ id = fields.UUIDField(pk=True)
13
+ user = fields.ForeignKeyField("models.User", related_name="payments", null=True)
14
+ plan = fields.ForeignKeyField(
15
+ "models.Plan",
16
+ related_name="payments",
17
+ null=True,
18
+ description="Plan associated with the payment",
19
+ )
20
+ amount = fields.DecimalField(
21
+ max_digits=10, decimal_places=2, description="Payment amount"
22
+ )
23
+ status = fields.CharField(
24
+ max_length=50,
25
+ default="pending",
26
+ description="Payment status (e.g., pending, completed, failed, balance-assigned)",
27
+ )
28
+ payment_method = fields.CharField(
29
+ max_length=50, choices=PaymentMethod.CHOICES, description="Payment method"
30
+ )
31
+ transaction_id = fields.CharField(
32
+ max_length=100,
33
+ unique=True,
34
+ null=True,
35
+ description="Unique transaction ID for payment (for methods like Lipa Number)",
36
+ )
37
+ created_time = fields.DatetimeField(auto_now_add=True)
38
+ updated_time = fields.DatetimeField(auto_now=True)
39
+
40
+ class Meta:
41
+ table = "payments"
42
+
43
+ async def create_subscription_if_cash(self):
44
+ """
45
+ Creates a subscription for the user if the payment method is 'cash'
46
+ and the user has enough balance for the specified plan.
47
+ """
48
+ if self.payment_method == PaymentMethod.CASH and self.user and self.plan:
49
+ if self.amount >= self.plan.amount:
50
+ expiration_time = datetime.datetime.now() + datetime.timedelta(
51
+ hours=self.plan.duration
52
+ )
53
+
54
+ # Create the subscription
55
+ await Subscription.create(
56
+ user=self.user,
57
+ duration=self.plan.duration,
58
+ download_mb=self.plan.download_speed
59
+ * 1024, # Converting Mbps to MB
60
+ upload_mb=self.plan.upload_speed * 1024, # Converting Mbps to MB
61
+ created_time=datetime.datetime.now(),
62
+ expiration_time=expiration_time,
63
+ active=True,
64
+ )
65
+ self.status = "subscription-created"
66
+ else:
67
+ self.status = "insufficient-funds"
68
+ await self.save()
App/Payments/PaymentsRoutes.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
6
+ from .Schema import (
7
+ CreatePaymentRequest,
8
+ UpdatePaymentStatusRequest,
9
+ PaymentResponse,
10
+ UpdatePaymentUserRequest,
11
+ BaseResponse,
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):
21
+ # If payment method is "Lipa Number", transaction_id is required
22
+ if (
23
+ request.payment_method == PaymentMethod.LIPA_NUMBER
24
+ and not request.transaction_id
25
+ ):
26
+ raise HTTPException(
27
+ status_code=status.HTTP_400_BAD_REQUEST,
28
+ detail="Transaction ID is required for Lipa Number payments",
29
+ )
30
+
31
+ # Check if plan exists
32
+ plan = await Plan.get_or_none(id=request.plan_id)
33
+ if request.plan_id and not plan:
34
+ raise HTTPException(
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,
41
+ plan=plan,
42
+ amount=request.amount,
43
+ payment_method=request.payment_method,
44
+ transaction_id=request.transaction_id,
45
+ status="pending", # Default status
46
+ )
47
+
48
+ # If payment method is "cash", attempt to create a subscription
49
+ if payment.payment_method == PaymentMethod.CASH:
50
+ await payment.create_subscription_if_cash()
51
+
52
+ await payment.save()
53
+
54
+ return BaseResponse(
55
+ code=200,
56
+ message="Payment created successfully",
57
+ payload={"payment_id": str(payment.id)},
58
+ )
59
+
60
+
61
+ @payment_router.get("/payment/{payment_id}", response_model=PaymentResponse)
62
+ async def get_payment(payment_id: str):
63
+ payment = await Payment.get_or_none(id=payment_id)
64
+ if not payment:
65
+ raise HTTPException(
66
+ status_code=status.HTTP_404_NOT_FOUND, detail="Payment not found"
67
+ )
68
+
69
+ return PaymentResponse(
70
+ id=str(payment.id),
71
+ user_id=payment.user_id,
72
+ plan_id=payment.plan_id,
73
+ amount=payment.amount,
74
+ payment_method=payment.payment_method,
75
+ status=payment.status,
76
+ transaction_id=payment.transaction_id,
77
+ created_time=payment.created_time,
78
+ updated_time=payment.updated_time,
79
+ )
80
+
81
+
82
+ @payment_router.get("/payments/unlinked", response_model=PaymentListResponse)
83
+ async def get_unlinked_payments():
84
+ unlinked_payments = await Payment.filter(user_id=None)
85
+ result = [
86
+ PaymentResponse(
87
+ id=str(payment.id),
88
+ user_id=payment.user_id,
89
+ plan_id=payment.plan_id,
90
+ amount=payment.amount,
91
+ payment_method=payment.payment_method,
92
+ status=payment.status,
93
+ transaction_id=payment.transaction_id,
94
+ created_time=payment.created_time,
95
+ updated_time=payment.updated_time,
96
+ )
97
+ for payment in unlinked_payments
98
+ ]
99
+
100
+ return PaymentListResponse(payments=result, total_count=len(result))
101
+
102
+
103
+ @payment_router.put("/payment/{payment_id}/link-user", response_model=BaseResponse)
104
+ async def link_user_to_payment(payment_id: str, request: UpdatePaymentUserRequest):
105
+ payment = await Payment.get_or_none(id=payment_id)
106
+ if not payment:
107
+ raise HTTPException(
108
+ status_code=status.HTTP_404_NOT_FOUND, detail="Payment not found"
109
+ )
110
+
111
+ user = await User.get_or_none(id=request.user_id)
112
+ if not user:
113
+ raise HTTPException(
114
+ status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
115
+ )
116
+
117
+ # Link payment to the user
118
+ payment.user_id = request.user_id
119
+ await payment.save()
120
+
121
+ # If the payment method is "Lipa Number" or "M-Pesa", add to user's balance directly
122
+ if (
123
+ payment.payment_method in [PaymentMethod.LIPA_NUMBER, PaymentMethod.MPESA]
124
+ and not payment.plan
125
+ ):
126
+ user.balance += payment.amount
127
+ await user.save()
128
+
129
+ # Update payment status to indicate it was assigned to balance
130
+ payment.status = "balance-assigned"
131
+ await payment.save()
132
+
133
+ return BaseResponse(
134
+ code=200,
135
+ message="Payment linked to user and amount added to balance successfully",
136
+ payload={
137
+ "payment_id": str(payment.id),
138
+ "user_id": request.user_id,
139
+ "new_balance": user.balance,
140
+ },
141
+ )
142
+
143
+ return BaseResponse(
144
+ code=200,
145
+ message="Payment linked to user successfully",
146
+ payload={"payment_id": str(payment.id), "user_id": request.user_id},
147
+ )
App/Payments/Schema.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import Optional, List
3
+ from decimal import Decimal
4
+ from datetime import datetime
5
+
6
+
7
+ class BaseResponse(BaseModel):
8
+ code: int
9
+ message: str
10
+ payload: Optional[dict] = None
11
+
12
+
13
+ class PaymentMethod:
14
+ CASH = "cash"
15
+ MPESA = "mpesa"
16
+ LIPA_NUMBER = "lipa_number"
17
+ CREDIT_CARD = "credit_card"
18
+
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"
25
+ )
26
+ plan_id: Optional[str] = Field(
27
+ None, description="ID of the plan associated with the payment"
28
+ )
29
+ amount: Decimal = Field(..., description="Payment amount")
30
+ payment_method: str = Field(
31
+ ..., description="Method of payment", example=PaymentMethod.CASH
32
+ )
33
+ transaction_id: Optional[str] = Field(
34
+ None, description="Unique transaction ID (for methods like Lipa Number)"
35
+ )
36
+
37
+
38
+ class UpdatePaymentStatusRequest(BaseModel):
39
+ status: str = Field(
40
+ ..., description="Status of the payment (e.g., pending, completed, failed)"
41
+ )
42
+
43
+
44
+ class PaymentResponse(BaseModel):
45
+ id: str
46
+ user_id: Optional[str]
47
+ plan_id: Optional[str]
48
+ amount: Decimal
49
+ payment_method: str
50
+ status: str
51
+ transaction_id: Optional[str]
52
+ created_time: datetime
53
+ updated_time: datetime
54
+
55
+ class Config:
56
+ from_attributes = True
57
+
58
+
59
+ class PaymentListResponse(BaseModel):
60
+ payments: List[PaymentResponse]
61
+ total_count: int
62
+
63
+ class Config:
64
+ from_attributes = True
65
+
66
+
67
+ class UpdatePaymentUserRequest(BaseModel):
68
+ user_id: str = Field(
69
+ ..., description="ID of the user to link or update for this payment"
70
+ )
App/Plans/Model.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from tortoise import fields
2
+ from tortoise.models import Model
3
+
4
+
5
+ class Plan(Model):
6
+ id = fields.UUIDField(pk=True)
7
+ name = fields.CharField(
8
+ max_length=100, unique=True, description="Name of the subscription plan"
9
+ )
10
+ amount = fields.DecimalField(
11
+ max_digits=10, decimal_places=2, description="Cost of the plan"
12
+ )
13
+ duration = fields.IntField(description="Duration of the subscription in hours")
14
+ download_speed = fields.FloatField(description="Download speed in Mbps")
15
+ upload_speed = fields.FloatField(description="Upload speed in Mbps")
16
+
17
+ class Meta:
18
+ table = "plans"
App/Plans/PlanRoutes.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, status, Depends
2
+ from typing import List
3
+ from .Model import Plan
4
+ from .Schema import (
5
+ CreatePlanRequest,
6
+ UpdatePlanRequest,
7
+ PlanResponse,
8
+ PlanListResponse,
9
+ BaseResponse,
10
+ )
11
+
12
+ plan_router = APIRouter(tags=["Plans"])
13
+
14
+
15
+ @plan_router.post("/plan/create", response_model=BaseResponse)
16
+ async def create_plan(request: CreatePlanRequest):
17
+ # Check if a plan with the same name already exists
18
+ existing_plan = await Plan.get_or_none(name=request.name)
19
+ if existing_plan:
20
+ raise HTTPException(
21
+ status_code=status.HTTP_400_BAD_REQUEST,
22
+ detail="A plan with this name already exists",
23
+ )
24
+
25
+ # Create a new plan
26
+ plan = await Plan.create(
27
+ name=request.name,
28
+ amount=request.amount,
29
+ duration=request.duration,
30
+ download_speed=request.download_speed,
31
+ upload_speed=request.upload_speed,
32
+ )
33
+ await plan.save()
34
+
35
+ return BaseResponse(
36
+ code=200, message="Plan created successfully", payload={"plan_id": str(plan.id)}
37
+ )
38
+
39
+
40
+ @plan_router.put("/plan/{plan_id}/update", response_model=BaseResponse)
41
+ async def update_plan(plan_id: str, request: UpdatePlanRequest):
42
+ # Find the plan by ID
43
+ plan = await Plan.get_or_none(id=plan_id)
44
+ if not plan:
45
+ raise HTTPException(
46
+ status_code=status.HTTP_404_NOT_FOUND, detail="Plan not found"
47
+ )
48
+
49
+ # Update the plan fields if provided
50
+ if request.name is not None:
51
+ plan.name = request.name
52
+ if request.amount is not None:
53
+ plan.amount = request.amount
54
+ if request.duration is not None:
55
+ plan.duration = request.duration
56
+ if request.download_speed is not None:
57
+ plan.download_speed = request.download_speed
58
+ if request.upload_speed is not None:
59
+ plan.upload_speed = request.upload_speed
60
+
61
+ await plan.save()
62
+
63
+ return BaseResponse(
64
+ code=200, message="Plan updated successfully", payload={"plan_id": str(plan.id)}
65
+ )
66
+
67
+
68
+ @plan_router.delete("/plan/{plan_id}/delete", response_model=BaseResponse)
69
+ async def delete_plan(plan_id: str):
70
+ # Find the plan by ID
71
+ plan = await Plan.get_or_none(id=plan_id)
72
+ if not plan:
73
+ raise HTTPException(
74
+ status_code=status.HTTP_404_NOT_FOUND, detail="Plan not found"
75
+ )
76
+
77
+ await plan.delete()
78
+
79
+ return BaseResponse(code=200, message="Plan deleted successfully")
80
+
81
+
82
+ @plan_router.get("/plans", response_model=PlanListResponse)
83
+ async def list_plans():
84
+ plans = await Plan.all()
85
+ total_count = await Plan.all().count()
86
+
87
+ result = [
88
+ PlanResponse(
89
+ id=str(plan.id),
90
+ name=plan.name,
91
+ amount=plan.amount,
92
+ duration=plan.duration,
93
+ download_speed=plan.download_speed,
94
+ upload_speed=plan.upload_speed,
95
+ )
96
+ for plan in plans
97
+ ]
98
+
99
+ return PlanListResponse(plans=result, total_count=total_count)
App/Plans/Schema.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import Optional, List
3
+ from decimal import Decimal
4
+
5
+
6
+ class BaseResponse(BaseModel):
7
+ code: int
8
+ message: str
9
+ payload: Optional[dict] = None
10
+
11
+
12
+ class CreatePlanRequest(BaseModel):
13
+ name: str = Field(..., description="Name of the subscription plan")
14
+ amount: Decimal = Field(..., description="Cost of the plan")
15
+ duration: int = Field(..., description="Duration of the subscription in hours")
16
+ download_speed: float = Field(..., description="Download speed in Mbps")
17
+ upload_speed: float = Field(..., description="Upload speed in Mbps")
18
+
19
+
20
+ class UpdatePlanRequest(BaseModel):
21
+ name: Optional[str] = Field(None, description="Name of the subscription plan")
22
+ amount: Optional[Decimal] = Field(None, description="Cost of the plan")
23
+ duration: Optional[int] = Field(
24
+ None, description="Duration of the subscription in hours"
25
+ )
26
+ download_speed: Optional[float] = Field(None, description="Download speed in Mbps")
27
+ upload_speed: Optional[float] = Field(None, description="Upload speed in Mbps")
28
+
29
+
30
+ class PlanResponse(BaseModel):
31
+ id: str
32
+ name: str
33
+ amount: Decimal
34
+ duration: int
35
+ download_speed: float
36
+ upload_speed: float
37
+
38
+ class Config:
39
+ from_attributes = True
40
+
41
+
42
+ class PlanListResponse(BaseModel):
43
+ plans: List[PlanResponse]
44
+ total_count: int
45
+
46
+ class Config:
47
+ from_attributes = True
App/Portals/Model.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from tortoise import fields, models
2
+
3
+
4
+ class Portal(models.Model):
5
+ id = fields.IntField(pk=True)
6
+ name = fields.CharField(
7
+ max_length=50,
8
+ unique=True,
9
+ description="Name of the portal, e.g., Android or MikroTik",
10
+ )
11
+ description = fields.CharField(
12
+ max_length=255,
13
+ description="Description of the portal",
14
+ )
15
+ url = fields.CharField(
16
+ max_length=255,
17
+ description="URL of the portal, must start with http or https",
18
+ )
19
+
20
+ class Meta:
21
+ table = "portals"
22
+
23
+ def __str__(self):
24
+ return f"{self.name} - {self.url}"
App/Portals/PortalRoutes.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, status
2
+ from .Schema import (
3
+ CreatePortalRequest,
4
+ UpdatePortalRequest,
5
+ PortalResponse,
6
+ BaseResponse,
7
+ )
8
+ from .Model import Portal
9
+
10
+ portal_router = APIRouter(tags=["Portals"])
11
+
12
+ # Constants for portal names
13
+ ANDROID = "Android"
14
+ MIKROTIK = "MikroTik"
15
+
16
+
17
+ # Get Portal by Name (Android or MikroTik)
18
+ @portal_router.get(
19
+ "/portals/{portal_name}",
20
+ response_model=PortalResponse,
21
+ status_code=status.HTTP_200_OK,
22
+ )
23
+ async def get_portal_by_name(portal_name: str):
24
+ portal = await Portal.get_or_none(name=portal_name)
25
+ if not portal:
26
+ raise HTTPException(
27
+ status_code=status.HTTP_404_NOT_FOUND,
28
+ detail=f"Portal '{portal_name}' not found.",
29
+ )
30
+
31
+ return PortalResponse.from_orm(portal)
32
+
33
+
34
+ # Assign or Update Android Portal
35
+ @portal_router.put(
36
+ "/portals/android", response_model=BaseResponse, status_code=status.HTTP_200_OK
37
+ )
38
+ async def update_android_portal(request: UpdatePortalRequest):
39
+
40
+ try:
41
+ portal, created = await Portal.get_or_create(name=ANDROID)
42
+ except:
43
+ portal, created = await Portal.get_or_create(
44
+ name=ANDROID, description="Android-Link", url="https://example.com"
45
+ )
46
+
47
+ portal_data = request.dict(exclude_unset=True)
48
+ for key, value in portal_data.items():
49
+ setattr(portal, key, value)
50
+
51
+ await portal.save()
52
+ message = (
53
+ "Android portal created successfully"
54
+ if created
55
+ else "Android portal updated successfully"
56
+ )
57
+ return BaseResponse(code=200, message=message, payload={"portal_name": ANDROID})
58
+
59
+
60
+ # Assign or Update MikroTik Portal
61
+ @portal_router.put(
62
+ "/portals/mikrotik", response_model=BaseResponse, status_code=status.HTTP_200_OK
63
+ )
64
+ async def update_mikrotik_portal(request: UpdatePortalRequest):
65
+ try:
66
+ portal, created = await Portal.get_or_create(name=MIKROTIK)
67
+ except:
68
+ portal, created = await Portal.get_or_create(
69
+ name=MIKROTIK, description="Android-Link", url="https://example.com"
70
+ )
71
+
72
+ portal_data = request.dict(exclude_unset=True)
73
+ for key, value in portal_data.items():
74
+ setattr(portal, key, value)
75
+
76
+ await portal.save()
77
+ message = (
78
+ "MikroTik portal created successfully"
79
+ if created
80
+ else "MikroTik portal updated successfully"
81
+ )
82
+ return BaseResponse(code=200, message=message, payload={"portal_name": MIKROTIK})
App/Portals/Schema.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field, HttpUrl
2
+ from typing import List, Optional
3
+
4
+
5
+ class BaseResponse(BaseModel):
6
+ code: int
7
+ message: str
8
+ payload: Optional[dict] = None
9
+
10
+
11
+ # Create Portal Request
12
+ class CreatePortalRequest(BaseModel):
13
+ name: str = Field(
14
+ ..., max_length=50, description="Name of the portal, e.g., Android or MikroTik"
15
+ )
16
+ description: str = Field(
17
+ ..., max_length=255, description="Description of the portal"
18
+ )
19
+ url: HttpUrl = Field(..., description="URL of the portal")
20
+
21
+ class Config:
22
+ schema_extra = {
23
+ "example": {
24
+ "name": "Android",
25
+ "description": "Official Android developer portal",
26
+ "url": "https://developer.android.com",
27
+ }
28
+ }
29
+
30
+
31
+ # Update Portal Request
32
+ class UpdatePortalRequest(BaseModel):
33
+ description: Optional[str] = Field(
34
+ None, max_length=255, description="Updated description of the portal"
35
+ )
36
+ url: Optional[HttpUrl] = Field(None, description="Updated URL of the portal")
37
+
38
+
39
+ # Portal Response
40
+ class PortalResponse(BaseModel):
41
+ id: int
42
+ name: str
43
+ description: str
44
+ url: HttpUrl
45
+
46
+ class Config:
47
+ orm_mode = True
48
+
49
+
50
+ # List of Portals Response
51
+ class PortalListResponse(BaseModel):
52
+ portals: List[PortalResponse]
53
+ total_count: int
54
+
55
+ class Config:
56
+ orm_mode = True
App/Subscriptions/BackgroundTask.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from datetime import datetime
3
+ from .Model import Subscription
4
+
5
+
6
+ async def deactivate_expired_subscriptions():
7
+ while True:
8
+ # Find active subscriptions that have expired
9
+ expired_subscriptions = await Subscription.filter(
10
+ active=True, expiration_time__lt=datetime.now()
11
+ )
12
+
13
+ # Deactivate all expired subscriptions
14
+ for subscription in expired_subscriptions:
15
+ subscription.active = False
16
+ await subscription.save()
17
+
18
+ # Deactivate the user if he has no plans
19
+
20
+ # Run this check every hour (or adjust as needed)
21
+ await asyncio.sleep(60) # 1 hour
App/Subscriptions/Model.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from tortoise import fields
2
+ from tortoise.models import Model
3
+ import datetime
4
+ from App.Users.Model import User
5
+ from App.Plans.Model import Plan
6
+
7
+
8
+ class Subscription(Model):
9
+ id = fields.UUIDField(pk=True)
10
+ user = fields.ForeignKeyField("models.User", related_name="subscriptions")
11
+ plan = fields.ForeignKeyField(
12
+ "models.Plan",
13
+ related_name="subscriptions",
14
+ null=True,
15
+ description="Plan associated with the subscription",
16
+ )
17
+ active = fields.BooleanField(default=True)
18
+ duration = fields.IntField(description="Duration in hours")
19
+ download_mb = fields.FloatField(
20
+ default=0.0, description="Download usage in megabytes"
21
+ )
22
+ upload_mb = fields.FloatField(default=0.0, description="Upload usage in megabytes")
23
+ created_time = fields.DatetimeField(auto_now_add=True)
24
+ expiration_time = fields.DatetimeField(null=True)
25
+
26
+ class Meta:
27
+ table = "subscriptions"
28
+
29
+ async def save(self, *args, **kwargs):
30
+ # Calculate expiration_time if it's not set and we have created_time and duration
31
+ if not self.expiration_time and self.created_time and self.duration:
32
+ self.expiration_time = self.created_time + datetime.timedelta(
33
+ hours=self.duration
34
+ )
35
+ await super().save(*args, **kwargs)
36
+
37
+ async def time_remaining(self) -> float:
38
+ """
39
+ Calculate the remaining time for the subscription in hours.
40
+ Returns:
41
+ float: Number of hours remaining. Returns 0 if subscription has expired.
42
+ """
43
+ if not self.active:
44
+ return 0.0
45
+
46
+ current_time = datetime.datetime.now(self.created_time.tzinfo)
47
+
48
+ # If subscription has expired, deactivate it
49
+ if current_time >= self.expiration_time:
50
+ self.active = False
51
+ await self.save()
52
+ return 0.0
53
+
54
+ # Calculate remaining time in hours
55
+ time_diff = self.expiration_time - current_time
56
+ remaining_hours = time_diff.total_seconds() / 3600
57
+
58
+ return round(remaining_hours, 2)
59
+
60
+ async def is_valid(self) -> bool:
61
+ """
62
+ Check if the subscription is still valid (active and not expired).
63
+ Returns:
64
+ bool: True if subscription is valid, False otherwise.
65
+ """
66
+ remaining_time = await self.time_remaining()
67
+ return self.active and remaining_time > 0
App/Subscriptions/Schema.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import Optional, List
3
+ from datetime import datetime
4
+
5
+
6
+ class BaseResponse(BaseModel):
7
+ code: int
8
+ message: str
9
+ payload: Optional[dict] = None
10
+
11
+
12
+ class CreateSubscriptionRequest(BaseModel):
13
+ user_id: str = Field(..., description="ID of the user to create subscription for")
14
+ plan_id: str = Field(..., description="ID of the plan to base the subscription on")
15
+
16
+
17
+ class UpdateUsageRequest(BaseModel):
18
+ download_mb: float = Field(0.0, ge=0, description="Download usage in megabytes")
19
+ upload_mb: float = Field(0.0, ge=0, description="Upload usage in megabytes")
20
+
21
+
22
+ class SubscriptionResponse(BaseModel):
23
+ id: str
24
+ user_id: str
25
+ plan_id: Optional[str]
26
+ active: bool
27
+ duration: int
28
+ download_mb: float
29
+ upload_mb: float
30
+ remaining_hours: float
31
+ created_time: datetime
32
+ expiration_time: datetime
33
+
34
+ class Config:
35
+ from_attributes = True
36
+
37
+
38
+ class SubscriptionListResponse(BaseModel):
39
+ subscriptions: List[SubscriptionResponse]
40
+ total_count: int
41
+
42
+ class Config:
43
+ from_attributes = True
App/Subscriptions/SubscriptionRoutes.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, status, Depends
2
+ from typing import List
3
+ from datetime import datetime
4
+ from .Model import Subscription
5
+ from App.Users.Model import User
6
+ from App.Plans.Model import Plan
7
+ from .Schema import (
8
+ CreateSubscriptionRequest,
9
+ SubscriptionResponse,
10
+ BaseResponse,
11
+ UpdateUsageRequest,
12
+ SubscriptionListResponse,
13
+ )
14
+
15
+ subscription_router = APIRouter(tags=["Subscriptions"])
16
+
17
+
18
+ @subscription_router.post("/subscription/create", response_model=BaseResponse)
19
+ async def create_subscription(request: CreateSubscriptionRequest):
20
+ # Check if user exists
21
+ user = await User.get_or_none(id=request.user_id)
22
+ if not user:
23
+ raise HTTPException(
24
+ status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
25
+ )
26
+
27
+ # Check if plan exists
28
+ plan = await Plan.get_or_none(id=request.plan_id)
29
+ if not plan:
30
+ raise HTTPException(
31
+ status_code=status.HTTP_404_NOT_FOUND, detail="Plan not found"
32
+ )
33
+
34
+ # Create new subscription based on plan details
35
+ expiration_time = datetime.datetime.now() + datetime.timedelta(hours=plan.duration)
36
+ subscription = await Subscription.create(
37
+ user=user,
38
+ plan=plan,
39
+ duration=plan.duration,
40
+ download_mb=plan.download_speed * 1024, # Converting Mbps to MB
41
+ upload_mb=plan.upload_speed * 1024, # Converting Mbps to MB
42
+ expiration_time=expiration_time,
43
+ active=True,
44
+ )
45
+ await subscription.save()
46
+
47
+ return BaseResponse(
48
+ code=200,
49
+ message="Subscription created successfully",
50
+ payload={"subscription_id": str(subscription.id)},
51
+ )
52
+
53
+
54
+ @subscription_router.get(
55
+ "/subscription/{subscription_id}", response_model=SubscriptionResponse
56
+ )
57
+ async def get_subscription(subscription_id: str):
58
+ subscription = await Subscription.get_or_none(id=subscription_id)
59
+ if not subscription:
60
+ raise HTTPException(
61
+ status_code=status.HTTP_404_NOT_FOUND, detail="Subscription not found"
62
+ )
63
+
64
+ remaining_hours = await subscription.time_remaining()
65
+
66
+ return SubscriptionResponse(
67
+ id=str(subscription.id),
68
+ user_id=subscription.user_id,
69
+ plan_id=subscription.plan_id,
70
+ active=subscription.active,
71
+ duration=subscription.duration,
72
+ download_mb=subscription.download_mb,
73
+ upload_mb=subscription.upload_mb,
74
+ remaining_hours=remaining_hours,
75
+ created_time=subscription.created_time,
76
+ expiration_time=subscription.expiration_time,
77
+ )
78
+
79
+
80
+ @subscription_router.get(
81
+ "/subscription/user/{user_id}", response_model=List[SubscriptionResponse]
82
+ )
83
+ async def get_user_subscriptions(user_id: str):
84
+ user = await User.get_or_none(id=user_id)
85
+ if not user:
86
+ raise HTTPException(
87
+ status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
88
+ )
89
+
90
+ subscriptions = await Subscription.filter(user_id=user_id)
91
+ result = []
92
+
93
+ for subscription in subscriptions:
94
+ remaining_hours = await subscription.time_remaining()
95
+ result.append(
96
+ SubscriptionResponse(
97
+ id=str(subscription.id),
98
+ user_id=subscription.user_id,
99
+ plan_id=subscription.plan_id,
100
+ active=subscription.active,
101
+ duration=subscription.duration,
102
+ download_mb=subscription.download_mb,
103
+ upload_mb=subscription.upload_mb,
104
+ remaining_hours=remaining_hours,
105
+ created_time=subscription.created_time,
106
+ expiration_time=subscription.expiration_time,
107
+ )
108
+ )
109
+
110
+ return result
111
+
112
+
113
+ @subscription_router.put(
114
+ "/subscription/{subscription_id}/update-usage", response_model=BaseResponse
115
+ )
116
+ async def update_usage(subscription_id: str, request: UpdateUsageRequest):
117
+ subscription = await Subscription.get_or_none(id=subscription_id)
118
+ if not subscription:
119
+ raise HTTPException(
120
+ status_code=status.HTTP_404_NOT_FOUND, detail="Subscription not found"
121
+ )
122
+
123
+ if not await subscription.is_valid():
124
+ raise HTTPException(
125
+ status_code=status.HTTP_400_BAD_REQUEST,
126
+ detail="Subscription is not active or has expired",
127
+ )
128
+
129
+ subscription.download_mb += request.download_mb
130
+ subscription.upload_mb += request.upload_mb
131
+ await subscription.save()
132
+
133
+ return BaseResponse(
134
+ code=200,
135
+ message="Usage updated successfully",
136
+ payload={
137
+ "total_download_mb": subscription.download_mb,
138
+ "total_upload_mb": subscription.upload_mb,
139
+ },
140
+ )
141
+
142
+
143
+ @subscription_router.post(
144
+ "/subscription/{subscription_id}/deactivate", response_model=BaseResponse
145
+ )
146
+ async def deactivate_subscription(subscription_id: str):
147
+ subscription = await Subscription.get_or_none(id=subscription_id)
148
+ if not subscription:
149
+ raise HTTPException(
150
+ status_code=status.HTTP_404_NOT_FOUND, detail="Subscription not found"
151
+ )
152
+
153
+ subscription.active = False
154
+ await subscription.save()
155
+
156
+ return BaseResponse(code=200, message="Subscription deactivated successfully")
App/Users/Model.py CHANGED
@@ -1,76 +1,75 @@
1
- import asyncio
2
- import uuid
3
- from tortoise import fields, Tortoise
4
- from tortoise.models import Model
5
  from passlib.context import CryptContext
6
  import datetime
 
7
  import random
8
  import string
 
9
  pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
10
 
 
11
  def generate_short_uuid() -> str:
12
- """Generate a random 5-character alphanumeric string."""
13
- return ''.join(random.choices(string.ascii_letters + string.digits, k=5))
14
 
15
 
16
- class User(Model):
17
  id = fields.CharField(primary_key=True, max_length=5, default=generate_short_uuid)
18
- name = fields.CharField(max_length=100, db_index=True)
19
- email = fields.CharField(max_length=100, db_index=True, unique=True)
20
  password = fields.CharField(max_length=100)
21
- phoneNumber = fields.CharField(max_length=100, null=True)
22
- account_type = fields.IntField(default=1)
23
  balance = fields.DecimalField(max_digits=10, decimal_places=2, default=0.00)
24
- ip_address = fields.CharField(max_length=45, null=True)
25
- mac_address = fields.CharField(max_length=17, null=True)
26
- createdAt = fields.DatetimeField(default=datetime.datetime.now)
27
- updatedAt = fields.DatetimeField(default=datetime.datetime.now)
28
  lastLogin = fields.DatetimeField(default=datetime.datetime.now)
29
  failed_attempts = fields.IntField(default=0)
30
  account_locked = fields.BooleanField(default=False)
31
- reset_token = fields.CharField(max_length=100, null=True, unique=True)
32
  reset_token_expiration = fields.DatetimeField(null=True)
33
 
34
  class Meta:
35
  table = "users"
36
 
 
 
 
37
  def verify_password(self, plain_password: str) -> bool:
38
- if self.account_locked:
39
- print("Account is locked due to too many failed attempts.")
 
 
 
 
40
  return False
41
 
42
  if pwd_context.verify(plain_password, self.password):
43
- self.failed_attempts = 0 # Reset failed attempts on success
44
  self.account_locked = False
45
- self.save() # Save changes to reset the failed attempts count
 
46
  return True
47
  else:
48
  self.failed_attempts += 1
49
  if self.failed_attempts >= 5:
50
  self.account_locked = True
51
- print("Account locked due to too many failed attempts.")
52
- self.save() # Save changes to update the failed attempts count
53
  return False
54
 
55
  async def initiate_password_reset(self):
56
- # Generate a unique reset token
57
- self.reset_token = str(uuid.uuid4())
58
- self.reset_token_expiration = datetime.datetime.now() + datetime.timedelta(hours=1) # Token expires in 1 hour
 
59
  await self.save()
60
 
61
- # In a real application, send this token to the user's email or phone
62
- print(f"Password reset token for {self.email}: {self.reset_token}")
63
-
64
- async def reset_password(self, reset_token, new_password):
65
- # Check if the reset token is valid and not expired
66
- if self.reset_token != reset_token or datetime.datetime.now() > self.reset_token_expiration:
67
- print("Invalid or expired reset token.")
68
  return False
69
-
70
- # Set the new password
71
- self.password = pwd_context.hash(new_password)
72
- self.reset_token = None # Clear the token after successful reset
73
  self.reset_token_expiration = None
74
  await self.save()
75
- print("Password has been reset successfully.")
76
- return True
 
1
+ from tortoise import fields, models
 
 
 
2
  from passlib.context import CryptContext
3
  import datetime
4
+ import uuid
5
  import random
6
  import string
7
+
8
  pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
9
 
10
+
11
  def generate_short_uuid() -> str:
12
+ return "".join(random.choices(string.ascii_letters + string.digits, k=5))
 
13
 
14
 
15
+ class User(models.Model):
16
  id = fields.CharField(primary_key=True, max_length=5, default=generate_short_uuid)
17
+ name = fields.CharField(max_length=100)
 
18
  password = fields.CharField(max_length=100)
19
+ phoneNumber = fields.CharField(max_length=15, unique=True)
 
20
  balance = fields.DecimalField(max_digits=10, decimal_places=2, default=0.00)
21
+ mac_address = fields.CharField(max_length=17)
22
+ createdAt = fields.DatetimeField(auto_now_add=True)
23
+ updatedAt = fields.DatetimeField(auto_now=True)
 
24
  lastLogin = fields.DatetimeField(default=datetime.datetime.now)
25
  failed_attempts = fields.IntField(default=0)
26
  account_locked = fields.BooleanField(default=False)
27
+ reset_token = fields.CharField(max_length=6, null=True, unique=True)
28
  reset_token_expiration = fields.DatetimeField(null=True)
29
 
30
  class Meta:
31
  table = "users"
32
 
33
+ def hash_password(self, plain_password: str) -> str:
34
+ return pwd_context.hash(plain_password)
35
+
36
  def verify_password(self, plain_password: str) -> bool:
37
+ if (
38
+ self.account_locked
39
+ and datetime.datetime.now()
40
+ < self.lastLogin + datetime.timedelta(minutes=15)
41
+ ):
42
+ print("Account is locked due to too many failed attempts. Try again later.")
43
  return False
44
 
45
  if pwd_context.verify(plain_password, self.password):
46
+ self.failed_attempts = 0
47
  self.account_locked = False
48
+ self.lastLogin = datetime.datetime.now()
49
+ self.save()
50
  return True
51
  else:
52
  self.failed_attempts += 1
53
  if self.failed_attempts >= 5:
54
  self.account_locked = True
55
+ self.save()
 
56
  return False
57
 
58
  async def initiate_password_reset(self):
59
+ self.reset_token = f"{random.randint(100000, 999999)}"
60
+ self.reset_token_expiration = datetime.datetime.now() + datetime.timedelta(
61
+ minutes=15
62
+ )
63
  await self.save()
64
 
65
+ async def reset_password(self, reset_token: str, new_password: str):
66
+ if (
67
+ self.reset_token != reset_token
68
+ or datetime.datetime.now() > self.reset_token_expiration
69
+ ):
 
 
70
  return False
71
+ self.password = self.hash_password(new_password)
72
+ self.reset_token = None
 
 
73
  self.reset_token_expiration = None
74
  await self.save()
75
+ return True
 
App/Users/{Schemas.py → Schema.py} RENAMED
@@ -5,45 +5,46 @@ from passlib.context import CryptContext
5
 
6
  pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
7
 
 
 
 
 
8
 
9
  # Register User Request
10
  class RegisterUserRequest(BaseModel):
11
  name: str = Field(..., max_length=100)
12
- email: str = Field(..., max_length=100)
13
- password: constr(min_length=8)
14
- phoneNumber: str = Field(..., max_length=15)
15
- mac_address: str = Field(..., max_length=17) # Standard MAC address format
 
 
 
 
 
 
16
  def hash_password(self):
17
  self.password = pwd_context.hash(self.password)
 
18
  class Config:
19
  schema_extra = {
20
  "example": {
21
  "name": "John Doe",
22
- "email": "[email protected]",
23
- "password": "strongpassword123",
24
- "phoneNumber": "1234567890",
25
- "mac_address": "00:1A:2B:3C:4D:5E"
26
  }
27
  }
28
 
29
 
30
- # Login User Request (using phone number and MAC address)
31
  class LoginUserRequest(BaseModel):
32
- phoneNumber: str = Field(..., max_length=15)
33
- password: constr(min_length=8)
34
- mac_address: str = Field(..., max_length=17) # Required for login
35
 
36
- class Config:
37
- schema_extra = {
38
- "example": {
39
- "phoneNumber": "1234567890",
40
- "password": "strongpassword123",
41
- "mac_address": "00:1A:2B:3C:4D:5E"
42
- }
43
- }
44
 
45
-
46
- # Access Token Response (used for Login)
47
  class AccessTokenResponse(BaseModel):
48
  access_token: str
49
  token_type: str = "bearer"
@@ -55,45 +56,19 @@ class BaseResponse(BaseModel):
55
  message: str
56
  payload: Optional[dict] = None
57
 
58
- # Schemas.py
59
-
60
 
61
-
62
- # Step 1: Forgot Password Request (using phone number)
63
  class ForgotPasswordRequest(BaseModel):
64
- phoneNumber: str = Field(..., max_length=15)
65
-
66
- class Config:
67
- schema_extra = {
68
- "example": {
69
- "phoneNumber": "1234567890"
70
- }
71
- }
72
 
73
 
74
- # Step 2a: Verify Reset Token Request
75
  class VerifyResetTokenRequest(BaseModel):
76
- phoneNumber: str = Field(..., max_length=15)
77
- reset_token: str = Field(..., max_length=6) # Short token
78
-
79
- class Config:
80
- schema_extra = {
81
- "example": {
82
- "phoneNumber": "1234567890",
83
- "reset_token": "123456"
84
- }
85
- }
86
 
87
 
88
- # Step 2b: Reset Password Request (after token verification)
89
  class ResetPasswordRequest(BaseModel):
90
- phoneNumber: str = Field(..., max_length=15)
91
- new_password: constr(min_length=8)
92
-
93
- class Config:
94
- schema_extra = {
95
- "example": {
96
- "phoneNumber": "1234567890",
97
- "new_password": "newstrongpassword123"
98
- }
99
- }
 
5
 
6
  pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
7
 
8
+ # Constants for phone and MAC address patterns
9
+ PHONE_PATTERN = r"^(?:\+255|0)\d{9}$"
10
+ MAC_PATTERN = r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$"
11
+
12
 
13
  # Register User Request
14
  class RegisterUserRequest(BaseModel):
15
  name: str = Field(..., max_length=100)
16
+ password: str = Field(..., max_length=100)
17
+ phoneNumber: str = Field(
18
+ ...,
19
+ pattern=PHONE_PATTERN,
20
+ description="Tanzanian phone number starting with +255 or 0 followed by 9 digits",
21
+ )
22
+ mac_address: str = Field(
23
+ ..., pattern=MAC_PATTERN, description="MAC address in standard format"
24
+ )
25
+
26
  def hash_password(self):
27
  self.password = pwd_context.hash(self.password)
28
+
29
  class Config:
30
  schema_extra = {
31
  "example": {
32
  "name": "John Doe",
33
+ "password": "StrongPassword1!",
34
+ "phoneNumber": "+255123456789",
35
+ "mac_address": "00:1A:2B:3C:4D:5E",
 
36
  }
37
  }
38
 
39
 
40
+ # Login User Request
41
  class LoginUserRequest(BaseModel):
42
+ phoneNumber: str = Field(..., pattern=PHONE_PATTERN)
43
+ password: str
44
+ mac_address: str = Field(..., pattern=MAC_PATTERN)
45
 
 
 
 
 
 
 
 
 
46
 
47
+ # Access Token Response
 
48
  class AccessTokenResponse(BaseModel):
49
  access_token: str
50
  token_type: str = "bearer"
 
56
  message: str
57
  payload: Optional[dict] = None
58
 
 
 
59
 
60
+ # Forgot Password Request
 
61
  class ForgotPasswordRequest(BaseModel):
62
+ phoneNumber: str = Field(..., pattern=PHONE_PATTERN)
 
 
 
 
 
 
 
63
 
64
 
65
+ # Verify Reset Token Request
66
  class VerifyResetTokenRequest(BaseModel):
67
+ phoneNumber: str = Field(..., pattern=PHONE_PATTERN)
68
+ reset_token: str = Field(..., max_length=6)
 
 
 
 
 
 
 
 
69
 
70
 
71
+ # Reset Password Request
72
  class ResetPasswordRequest(BaseModel):
73
+ phoneNumber: str = Field(..., pattern=PHONE_PATTERN)
74
+ new_password: str = Field(..., max_length=100)
 
 
 
 
 
 
 
 
App/Users/UserRoutes.py CHANGED
@@ -1,105 +1,102 @@
1
  from fastapi import APIRouter, HTTPException, status
2
- from .Schemas import (
3
- RegisterUserRequest, ResetPasswordRequest, LoginUserRequest,
4
- AccessTokenResponse, BaseResponse, ForgotPasswordRequest, VerifyResetTokenRequest
 
 
 
 
 
5
  )
6
  from .Model import User
7
  from jose import jwt
8
- from datetime import datetime, timedelta,timezone
9
- import random
10
- from passlib.context import CryptContext
11
 
12
- # Configurations for JWT and Password Reset
13
  SECRET_KEY = "your_secret_key_here"
14
  ALGORITHM = "HS256"
15
  ACCESS_TOKEN_EXPIRE_MINUTES = 30
16
- PASSWORD_RESET_EXPIRE_MINUTES = 15 # Reset token valid for 15 minutes
17
-
18
- pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
19
  user_router = APIRouter(tags=["User"])
20
 
21
- # Utility function to create JWT token
22
  def create_access_token(data: dict, expires_delta: timedelta = None):
23
  to_encode = data.copy()
24
- expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
 
 
25
  to_encode.update({"exp": expire})
26
  return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
27
 
28
- # Register route
29
- @user_router.post("/user/register", response_model=BaseResponse, status_code=status.HTTP_201_CREATED)
30
- async def register_user(user: RegisterUserRequest):
31
- existing_user = await User.filter(phoneNumber=user.phoneNumber).first()
 
 
32
  if existing_user:
33
- raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="User already exists.")
34
- user.hash_password()
35
-
36
- new_user = await User.create(**user.dict())
37
- return BaseResponse(code=200, message="User created successfully", payload={"user_id": new_user.id})
 
 
 
38
 
39
- # Login route (using phone number)
40
- @user_router.post("/user/login", response_model=AccessTokenResponse, status_code=status.HTTP_200_OK)
41
- async def login_user(user: LoginUserRequest):
42
- db_user = await User.filter(phoneNumber=user.phoneNumber).first()
43
- if db_user and db_user.verify_password(user.password):
 
 
44
  access_token = create_access_token(data={"sub": db_user.phoneNumber})
45
  return AccessTokenResponse(access_token=access_token, token_type="bearer")
46
-
47
  raise HTTPException(
48
- status_code=status.HTTP_401_UNAUTHORIZED,
49
- detail="Invalid credentials",
50
- headers={"WWW-Authenticate": "Bearer"},
51
  )
52
 
53
- # Forgot Password route (using phone number only)
54
- @user_router.post("/user/forgot-password", response_model=BaseResponse, status_code=status.HTTP_200_OK)
 
 
55
  async def forgot_password(request: ForgotPasswordRequest):
56
  user = await User.filter(phoneNumber=request.phoneNumber).first()
57
  if not user:
58
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found.")
59
-
60
- # Generate a short reset token (6-digit code)
61
- reset_token = f"{random.randint(100000, 999999)}"
62
- reset_token_expiration = datetime.utcnow() + timedelta(minutes=PASSWORD_RESET_EXPIRE_MINUTES)
63
-
64
- # Store reset token and expiration in the user record
65
- user.reset_token = reset_token
66
- user.reset_token_expiration = reset_token_expiration
67
- await user.save()
68
 
69
- # In production, send this token via SMS to the user's phone number
70
- print(f"Password reset token for {request.phoneNumber}: {reset_token}")
71
 
72
- return BaseResponse(code=200, message="Password reset token sent. Check your phone for further instructions.")
73
-
74
- # Verify Reset Token route
75
- @user_router.post("/user/verify-reset-token", response_model=BaseResponse, status_code=status.HTTP_200_OK)
 
76
  async def verify_reset_token(request: VerifyResetTokenRequest):
77
- user = await User.filter(phoneNumber=request.phoneNumber, reset_token=request.reset_token).first()
78
- if not user:
79
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Invalid token or phone number.")
80
-
81
- # Check if the reset token has expired
82
- if datetime.utcnow().replace(tzinfo=timezone.utc) > user.reset_token_expiration:
83
- raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Reset token has expired.")
84
-
85
- return BaseResponse(code=200, message="Token verified. You may now reset your password.")
86
-
87
- # Reset Password route (After Token Verification)
88
- @user_router.post("/user/reset-password", response_model=BaseResponse, status_code=status.HTTP_200_OK)
 
89
  async def reset_password(request: ResetPasswordRequest):
90
  user = await User.filter(phoneNumber=request.phoneNumber).first()
91
  if not user:
92
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found.")
93
-
94
- # Check if the reset token is present and not expired
95
-
96
- if not user.reset_token or datetime.utcnow().replace(tzinfo=timezone.utc) > user.reset_token_expiration:
97
- raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Reset token invalid or expired.")
98
-
99
- # Update the user's password and clear the reset token
100
- user.password = pwd_context.hash(request.new_password)
101
- user.reset_token = None
102
- user.reset_token_expiration = None
103
- await user.save()
104
-
105
  return BaseResponse(code=200, message="Password has been reset successfully.")
 
1
  from fastapi import APIRouter, HTTPException, status
2
+ from .Schema import (
3
+ RegisterUserRequest,
4
+ LoginUserRequest,
5
+ AccessTokenResponse,
6
+ ForgotPasswordRequest,
7
+ VerifyResetTokenRequest,
8
+ ResetPasswordRequest,
9
+ BaseResponse,
10
  )
11
  from .Model import User
12
  from jose import jwt
13
+ from datetime import datetime, timedelta
 
 
14
 
15
+ # JWT Configurations
16
  SECRET_KEY = "your_secret_key_here"
17
  ALGORITHM = "HS256"
18
  ACCESS_TOKEN_EXPIRE_MINUTES = 30
 
 
 
19
  user_router = APIRouter(tags=["User"])
20
 
21
+
22
  def create_access_token(data: dict, expires_delta: timedelta = None):
23
  to_encode = data.copy()
24
+ expire = datetime.utcnow() + (
25
+ expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
26
+ )
27
  to_encode.update({"exp": expire})
28
  return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
29
 
30
+
31
+ @user_router.post(
32
+ "/user/register", response_model=BaseResponse, status_code=status.HTTP_201_CREATED
33
+ )
34
+ async def register_user(request: RegisterUserRequest):
35
+ existing_user = await User.filter(phoneNumber=request.phoneNumber).first()
36
  if existing_user:
37
+ raise HTTPException(
38
+ status_code=status.HTTP_400_BAD_REQUEST, detail="User already exists."
39
+ )
40
+ request.hash_password()
41
+ new_user = await User.create(**request.dict())
42
+ return BaseResponse(
43
+ code=200, message="User created successfully", payload={"user_id": new_user.id}
44
+ )
45
 
46
+
47
+ @user_router.post(
48
+ "/user/login", response_model=AccessTokenResponse, status_code=status.HTTP_200_OK
49
+ )
50
+ async def login_user(request: LoginUserRequest):
51
+ db_user = await User.filter(phoneNumber=request.phoneNumber).first()
52
+ if db_user and db_user.verify_password(request.password):
53
  access_token = create_access_token(data={"sub": db_user.phoneNumber})
54
  return AccessTokenResponse(access_token=access_token, token_type="bearer")
 
55
  raise HTTPException(
56
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials"
 
 
57
  )
58
 
59
+
60
+ @user_router.post(
61
+ "/user/forgot-password", response_model=BaseResponse, status_code=status.HTTP_200_OK
62
+ )
63
  async def forgot_password(request: ForgotPasswordRequest):
64
  user = await User.filter(phoneNumber=request.phoneNumber).first()
65
  if not user:
66
+ raise HTTPException(
67
+ status_code=status.HTTP_404_NOT_FOUND, detail="User not found."
68
+ )
69
+ await user.initiate_password_reset()
70
+ return BaseResponse(code=200, message="Password reset token sent to your phone.")
 
 
 
 
 
71
 
 
 
72
 
73
+ @user_router.post(
74
+ "/user/verify-reset-token",
75
+ response_model=BaseResponse,
76
+ status_code=status.HTTP_200_OK,
77
+ )
78
  async def verify_reset_token(request: VerifyResetTokenRequest):
79
+ user = await User.filter(
80
+ phoneNumber=request.phoneNumber, reset_token=request.reset_token
81
+ ).first()
82
+ if not user or datetime.utcnow() > user.reset_token_expiration:
83
+ raise HTTPException(
84
+ status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid or expired token."
85
+ )
86
+ return BaseResponse(code=200, message="Token verified. Proceed to reset password.")
87
+
88
+
89
+ @user_router.post(
90
+ "/user/reset-password", response_model=BaseResponse, status_code=status.HTTP_200_OK
91
+ )
92
  async def reset_password(request: ResetPasswordRequest):
93
  user = await User.filter(phoneNumber=request.phoneNumber).first()
94
  if not user:
95
+ raise HTTPException(
96
+ status_code=status.HTTP_404_NOT_FOUND, detail="User not found."
97
+ )
98
+ if not await user.reset_password(request.reset_token, request.new_password):
99
+ raise HTTPException(
100
+ status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid or expired token."
101
+ )
 
 
 
 
 
 
102
  return BaseResponse(code=200, message="Password has been reset successfully.")
App/app.py CHANGED
@@ -2,7 +2,11 @@ from fastapi import FastAPI
2
  from tortoise import Tortoise, run_async
3
  from .Users.UserRoutes import user_router
4
  from .Webhooks.webhookRoute import webhook_router
 
5
  from .modelInit import TORTOISE_ORM
 
 
 
6
 
7
  app = FastAPI()
8
 
@@ -20,10 +24,17 @@ async def landing_page():
20
 
21
  app.include_router(user_router)
22
  app.include_router(webhook_router)
 
 
 
 
23
 
24
 
25
- async def main():
26
- pass
 
 
 
27
 
28
 
29
  # if __name__ == "__main__":
 
2
  from tortoise import Tortoise, run_async
3
  from .Users.UserRoutes import user_router
4
  from .Webhooks.webhookRoute import webhook_router
5
+ from .Subscriptions.SubscriptionRoutes import subscription_router
6
  from .modelInit import TORTOISE_ORM
7
+ from .Payments.PaymentsRoutes import payment_router
8
+ from .Plans.PlanRoutes import plan_router
9
+ from .Portals.PortalRoutes import portal_router
10
 
11
  app = FastAPI()
12
 
 
24
 
25
  app.include_router(user_router)
26
  app.include_router(webhook_router)
27
+ app.include_router(subscription_router)
28
+ app.include_router(payment_router)
29
+ app.include_router(plan_router)
30
+ app.include_router(portal_router)
31
 
32
 
33
+ if __name__ == "__main__":
34
+ import uvicorn
35
+
36
+ # Run the FastAPI app located at App.app:app
37
+ uvicorn.run(app, host="0.0.0.0", port=8000)
38
 
39
 
40
  # if __name__ == "__main__":
App/discovery.py CHANGED
@@ -1,25 +1,34 @@
1
- # discover_models.py
2
-
3
  import importlib
4
  import os
5
  from typing import List
6
 
7
- def discover_models(target_file: str,directory: str="./") -> List[str]:
 
8
  """
9
- Discover target file (e.g., models.py) in all directories and subdirectories.
10
-
11
- :param directory: The root directory to start searching from.
12
  :param target_file: The filename to look for in subdirectories (e.g., "models.py").
 
 
13
  :return: A list of module paths as strings.
14
  """
 
 
 
 
 
15
  model_modules = []
16
-
17
  # Traverse directory and subdirectories
18
  for root, _, files in os.walk(directory):
19
  if target_file in files:
20
- # Construct the module path, converting file path to dot notation
21
  relative_path = os.path.relpath(root, directory)
22
- module_name = f"{directory.replace('/', '.')}.{relative_path.replace('/', '.')}.{target_file[:-3]}"
23
- model_modules.append(module_name.replace('...',""))
24
- print(model_modules)
 
 
 
 
25
  return model_modules
 
 
 
1
  import importlib
2
  import os
3
  from typing import List
4
 
5
+
6
+ def discover_models(target_file: str, directory: str = None) -> List[str]:
7
  """
8
+ Discover and import target files (e.g., models.py) in all directories and subdirectories.
9
+
 
10
  :param target_file: The filename to look for in subdirectories (e.g., "models.py").
11
+ :param directory: The root directory to start searching from.
12
+ Defaults to the directory where this script is located.
13
  :return: A list of module paths as strings.
14
  """
15
+ if directory is None:
16
+ directory = os.path.dirname(
17
+ os.path.abspath(__file__)
18
+ ) # Use current directory of the script
19
+
20
  model_modules = []
21
+
22
  # Traverse directory and subdirectories
23
  for root, _, files in os.walk(directory):
24
  if target_file in files:
25
+ # Construct the module path relative to the given directory
26
  relative_path = os.path.relpath(root, directory)
27
+ module_name = os.path.join(relative_path, target_file).replace(
28
+ os.sep, "."
29
+ ) # Correct dot notation
30
+ module_name = module_name[:-3] # Remove '.py' extension
31
+ model_modules.append(module_name)
32
+
33
+ print("Discovered models:", model_modules)
34
  return model_modules
App/modelInit.py CHANGED
@@ -1,18 +1,39 @@
 
1
  from App.discovery import discover_models
2
- import asyncio
3
 
 
 
 
 
4
 
5
-
6
- DATABASE_URL = "sqlite://db.sqlite3" # Example: SQLite for local development
 
7
 
8
  TORTOISE_ORM = {
9
- "connections": {"default": DATABASE_URL},
 
 
 
 
 
 
 
 
 
 
 
 
10
  "apps": {
11
  "models": {
12
- "models": discover_models("Model.py"), # Automatically discover models in the "models" directory
 
 
 
 
 
 
13
  "default_connection": "default",
14
  }
15
  },
16
  }
17
-
18
-
 
1
+ import ssl
2
  from App.discovery import discover_models
 
3
 
4
+ # Set up SSL context for secure database connections
5
+ ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
6
+ ssl_context.check_hostname = True
7
+ ssl_context.verify_mode = ssl.CERT_REQUIRED
8
 
9
+ # Discover models
10
+ models = discover_models("Model.py")
11
+ print("Discovered models:", models)
12
 
13
  TORTOISE_ORM = {
14
+ "connections": {
15
+ "default": {
16
+ "engine": "tortoise.backends.asyncpg",
17
+ "credentials": {
18
+ "host": "ep-patient-darkness-a5bmmt9r.us-east-2.aws.neon.tech",
19
+ "port": "5432",
20
+ "user": "neondb_owner",
21
+ "password": "l2kE5dbMyqfx",
22
+ "database": "neondb",
23
+ "ssl": ssl_context, # Pass the SSL context here
24
+ },
25
+ }
26
+ },
27
  "apps": {
28
  "models": {
29
+ "models": [
30
+ "App.Payments.Model",
31
+ "App.Plans.Model",
32
+ "App.Portals.Model",
33
+ "App.Subscriptions.Model",
34
+ "App.Users.Model",
35
+ ],
36
  "default_connection": "default",
37
  }
38
  },
39
  }
 
 
App/requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ passlib
3
+ pydantic[email]
4
+ uvicorn
5
+ python-jose
6
+ bcrypt
7
+ httpx
8
+ pytest
9
+ tortoise-orm
App/vercel.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "builds": [
3
+ {
4
+ "src": "App/app.py",
5
+ "use": "@vercel/python",
6
+ "runtime": "python3.10"
7
+ }
8
+ ],
9
+ "routes": [
10
+ {
11
+ "src": "/(.*)",
12
+ "dest": "App/app.py"
13
+ }
14
+ ]
15
+ }
Dockerfile CHANGED
@@ -1,7 +1,18 @@
1
- FROM python:3.8.13
2
- WORKDIR /usr/src/nginx
3
- COPY . .
4
 
5
- RUN python3 -m pip install -r requirements.txt
6
- CMD uvicorn App.app:app --host 0.0.0.0 --workers 4
7
- EXPOSE 8000
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official Python image as a base
2
+ FROM python:3.10-slim
 
3
 
4
+ # Set the working directory in the container
5
+ WORKDIR /app
6
+
7
+ # Copy the application files to the container
8
+ COPY . /app
9
+
10
+ # Install dependencies
11
+ RUN pip install --no-cache-dir --upgrade pip && \
12
+ pip install --no-cache-dir -r requirements.txt
13
+
14
+ # Expose the port the app runs on
15
+ EXPOSE 8000
16
+
17
+ # Command to run the FastAPI app using Uvicorn
18
+ CMD ["uvicorn", "App.app:app", "--host", "0.0.0.0", "--port", "8000"]
call.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import uvicorn
2
+
3
+ if __name__ == "__main__":
4
+ # Run the FastAPI app located at App.app:app
5
+ uvicorn.run("App.app:app", host="0.0.0.0", port=8000, reload=True)
requirements.txt CHANGED
@@ -2,7 +2,9 @@ fastapi
2
  passlib
3
  pydantic[email]
4
  uvicorn
5
- jose
 
6
  httpx
7
  pytest
 
8
  tortoise-orm
 
2
  passlib
3
  pydantic[email]
4
  uvicorn
5
+ python-jose
6
+ bcrypt
7
  httpx
8
  pytest
9
+ asyncpg
10
  tortoise-orm
vercel.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "builds": [
3
+ {
4
+ "src": "App/app.py",
5
+ "use": "@vercel/python"
6
+ }
7
+ ],
8
+ "routes": [
9
+ {
10
+ "src": "/(.*)",
11
+ "dest": "App/app.py"
12
+ }
13
+ ]
14
+ }