vpn ready?
Browse files- App/Mikrotik/mikrotikRoutes.py +133 -0
- App/Mikrotik/schema.py +246 -0
- App/Mikrotik/utils/Config.py +10 -0
- App/Mikrotik/utils/api.py +426 -0
- App/Mikrotik/utils/client1-working.ovpn +152 -0
- App/Mikrotik/utils/helperfx.py +75 -0
- App/app.py +15 -0
- Dockerfile +2 -0
App/Mikrotik/mikrotikRoutes.py
ADDED
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, HTTPException, status
|
2 |
+
from typing import List, Optional
|
3 |
+
from fastapi import FastAPI, HTTPException, Depends, Body, Path
|
4 |
+
from fastapi.responses import JSONResponse
|
5 |
+
from .utils.helperfx import get_mikrotik, MikrotikAPI
|
6 |
+
from .schema import (
|
7 |
+
UserCreate,
|
8 |
+
RegisterResponse,
|
9 |
+
LogoutResponse,
|
10 |
+
UserStatsResponse,
|
11 |
+
ActiveUsersResponse,
|
12 |
+
UserStatusResponse,
|
13 |
+
)
|
14 |
+
|
15 |
+
mikrotik_router = APIRouter(tags=["Mikrotik"])
|
16 |
+
|
17 |
+
|
18 |
+
# New route to remove a user's active session
|
19 |
+
@mikrotik_router.post("/users/{phone}/remove-session")
|
20 |
+
async def remove_active_session(
|
21 |
+
phone: str = Path(..., description="User phone number"),
|
22 |
+
mikrotik: MikrotikAPI = Depends(get_mikrotik),
|
23 |
+
):
|
24 |
+
"""Remove an active session for a hotspot user"""
|
25 |
+
response = mikrotik.remove_active_session(phone)
|
26 |
+
if not response["success"]:
|
27 |
+
return JSONResponse(
|
28 |
+
status_code=400,
|
29 |
+
content=response,
|
30 |
+
)
|
31 |
+
return JSONResponse(content={"message": f"User {phone} session removed"})
|
32 |
+
|
33 |
+
|
34 |
+
@mikrotik_router.post("/users/register", response_model=RegisterResponse)
|
35 |
+
async def register_user(
|
36 |
+
user: UserCreate = Body(...), mikrotik: MikrotikAPI = Depends(get_mikrotik)
|
37 |
+
):
|
38 |
+
"""Register a new hotspot user"""
|
39 |
+
response = mikrotik.add_hotspot_user(
|
40 |
+
phone=user.phoneNumber, password=user.password, profile=user.profile
|
41 |
+
)
|
42 |
+
print(response)
|
43 |
+
if not response.code == 200:
|
44 |
+
return JSONResponse(
|
45 |
+
status_code=400,
|
46 |
+
content=response.__dict__,
|
47 |
+
)
|
48 |
+
return response.__dict__
|
49 |
+
|
50 |
+
|
51 |
+
@mikrotik_router.post("/users/logout/{phone}", response_model=LogoutResponse)
|
52 |
+
async def logout_user(
|
53 |
+
phone: str = Path(..., description="User phone number"),
|
54 |
+
mikrotik: MikrotikAPI = Depends(get_mikrotik),
|
55 |
+
):
|
56 |
+
"""Logout a hotspot user"""
|
57 |
+
response = mikrotik.logout_hotspot_user(phone)
|
58 |
+
if not response.code == 200:
|
59 |
+
return JSONResponse(
|
60 |
+
status_code=400,
|
61 |
+
content=response.__dict__,
|
62 |
+
)
|
63 |
+
return response.__dict__
|
64 |
+
|
65 |
+
|
66 |
+
@mikrotik_router.post("/users/{phone}/disable", response_model=UserStatusResponse)
|
67 |
+
async def disable_user(
|
68 |
+
phone: str = Path(..., description="User phone number"),
|
69 |
+
mikrotik: MikrotikAPI = Depends(get_mikrotik),
|
70 |
+
):
|
71 |
+
"""Disable a hotspot user"""
|
72 |
+
response = mikrotik.set_user_status(phone, disabled=True)
|
73 |
+
print(response)
|
74 |
+
if not response.code == 200:
|
75 |
+
return JSONResponse(
|
76 |
+
status_code=400,
|
77 |
+
content=response,
|
78 |
+
)
|
79 |
+
return response.__dict__
|
80 |
+
|
81 |
+
|
82 |
+
@mikrotik_router.post("/users/{phone}/enable", response_model=UserStatusResponse)
|
83 |
+
async def enable_user(
|
84 |
+
phone: str = Path(..., description="User phone number"),
|
85 |
+
mikrotik: MikrotikAPI = Depends(get_mikrotik),
|
86 |
+
):
|
87 |
+
"""Enable a hotspot user"""
|
88 |
+
response = mikrotik.set_user_status(phone, disabled=False)
|
89 |
+
if not response.code == 200:
|
90 |
+
return JSONResponse(
|
91 |
+
status_code=400,
|
92 |
+
content=response.__dict__,
|
93 |
+
)
|
94 |
+
return response.__dict__
|
95 |
+
|
96 |
+
|
97 |
+
@mikrotik_router.get("/users/active", response_model=ActiveUsersResponse)
|
98 |
+
async def get_active_users(mikrotik: MikrotikAPI = Depends(get_mikrotik)):
|
99 |
+
"""Get list of active hotspot users"""
|
100 |
+
response = mikrotik.get_active_users()
|
101 |
+
print(response)
|
102 |
+
if not response.code == 200:
|
103 |
+
return JSONResponse(
|
104 |
+
status_code=400,
|
105 |
+
content=response.__dict__,
|
106 |
+
)
|
107 |
+
return response
|
108 |
+
|
109 |
+
|
110 |
+
@mikrotik_router.get("/users/stats", response_model=UserStatsResponse)
|
111 |
+
async def get_users_stats(
|
112 |
+
phone: Optional[str] = None, mikrotik: MikrotikAPI = Depends(get_mikrotik)
|
113 |
+
):
|
114 |
+
"""Get user statistics"""
|
115 |
+
response = mikrotik.get_user_stats(phone)
|
116 |
+
if not response.code == 200:
|
117 |
+
return JSONResponse(
|
118 |
+
status_code=400,
|
119 |
+
content=response.__dict__,
|
120 |
+
)
|
121 |
+
return response.__dict__
|
122 |
+
|
123 |
+
|
124 |
+
@mikrotik_router.get("/users")
|
125 |
+
async def get_users_list(mikrotik: MikrotikAPI = Depends(get_mikrotik)):
|
126 |
+
"""Get list of all hotspot users"""
|
127 |
+
response = mikrotik.get_users_list()
|
128 |
+
if not response.code == 200:
|
129 |
+
return JSONResponse(
|
130 |
+
status_code=400,
|
131 |
+
content=response.__dict__,
|
132 |
+
)
|
133 |
+
return response
|
App/Mikrotik/schema.py
ADDED
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pydantic import BaseModel, Field, constr
|
2 |
+
from typing import Optional, List
|
3 |
+
from datetime import datetime
|
4 |
+
from pydantic import BaseModel, Field, validator
|
5 |
+
from typing import Optional, List, Any, Dict
|
6 |
+
from .utils.helperfx import PhoneValidator
|
7 |
+
|
8 |
+
# Extend the existing schema
|
9 |
+
|
10 |
+
# Constants for phone and MAC address patterns
|
11 |
+
PHONE_PATTERN = r"^(?:\+255|0)\d{9}$"
|
12 |
+
MAC_PATTERN = r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$"
|
13 |
+
|
14 |
+
|
15 |
+
# Register User Request
|
16 |
+
class RegisterUserRequest(BaseModel):
|
17 |
+
name: str = Field(..., max_length=100)
|
18 |
+
password: str = Field(..., max_length=100)
|
19 |
+
phoneNumber: str = Field(
|
20 |
+
...,
|
21 |
+
pattern=PHONE_PATTERN,
|
22 |
+
description="Tanzanian phone number starting with +255 or 0 followed by 9 digits",
|
23 |
+
)
|
24 |
+
mac_address: str = Field(
|
25 |
+
..., pattern=MAC_PATTERN, description="MAC address in standard format"
|
26 |
+
)
|
27 |
+
|
28 |
+
def hash_password(self):
|
29 |
+
self.password = pwd_context.hash(self.password)
|
30 |
+
|
31 |
+
class Config:
|
32 |
+
schema_extra = {
|
33 |
+
"example": {
|
34 |
+
"name": "John Doe",
|
35 |
+
"password": "StrongPassword1!",
|
36 |
+
"phoneNumber": "+255123456789",
|
37 |
+
"mac_address": "00:1A:2B:3C:4D:5E",
|
38 |
+
}
|
39 |
+
}
|
40 |
+
|
41 |
+
|
42 |
+
# Login User Request
|
43 |
+
class LoginUserRequest(BaseModel):
|
44 |
+
phoneNumber: str = Field(..., pattern=PHONE_PATTERN)
|
45 |
+
password: str
|
46 |
+
mac_address: str = Field(..., pattern=MAC_PATTERN)
|
47 |
+
|
48 |
+
|
49 |
+
# User Response with session data and additional information
|
50 |
+
class UserSessionData(BaseModel):
|
51 |
+
phone: str
|
52 |
+
mac_address: str
|
53 |
+
ip_address: Optional[str] = None
|
54 |
+
uptime: Optional[str] = None
|
55 |
+
bytes_in: int = 0
|
56 |
+
bytes_out: int = 0
|
57 |
+
status: str # Active, Inactive, Disabled, etc.
|
58 |
+
profile: str
|
59 |
+
login_time: Optional[datetime] = None
|
60 |
+
logout_time: Optional[datetime] = None
|
61 |
+
|
62 |
+
|
63 |
+
# Extended response for listing active users and all users with detailed session data
|
64 |
+
class ActiveUsersResponse(BaseModel):
|
65 |
+
code: int
|
66 |
+
message: str
|
67 |
+
active_users: List[UserSessionData]
|
68 |
+
|
69 |
+
|
70 |
+
class UsersListResponse(BaseModel):
|
71 |
+
code: int
|
72 |
+
message: str
|
73 |
+
data: list
|
74 |
+
|
75 |
+
|
76 |
+
# Add User Status Response to manage user enabling/disabling status
|
77 |
+
class UserStatusResponse(BaseModel):
|
78 |
+
code: int
|
79 |
+
message: str
|
80 |
+
user_status: str # Indicates whether the user is enabled or disabled
|
81 |
+
|
82 |
+
|
83 |
+
# Base Response Schema for standardized responses
|
84 |
+
class BaseResponse(BaseModel):
|
85 |
+
code: int
|
86 |
+
message: str
|
87 |
+
payload: Optional[dict] = None
|
88 |
+
|
89 |
+
|
90 |
+
# Login and Logout Responses with tracking for login/logout times
|
91 |
+
class LoginResponse(BaseResponse):
|
92 |
+
login_time: Optional[datetime] = None
|
93 |
+
|
94 |
+
|
95 |
+
class LogoutResponse(BaseResponse):
|
96 |
+
logout_time: Optional[datetime] = None
|
97 |
+
|
98 |
+
|
99 |
+
# Forgot Password and Reset Password Requests for account management
|
100 |
+
class ForgotPasswordRequest(BaseModel):
|
101 |
+
phoneNumber: str = Field(..., pattern=PHONE_PATTERN)
|
102 |
+
|
103 |
+
|
104 |
+
class ResetPasswordRequest(BaseModel):
|
105 |
+
phoneNumber: str = Field(..., pattern=PHONE_PATTERN)
|
106 |
+
new_password: str = Field(..., max_length=100)
|
107 |
+
|
108 |
+
|
109 |
+
# User Statistics Response to get detailed statistics for specific or all users
|
110 |
+
class UserStats(BaseModel):
|
111 |
+
phone: str
|
112 |
+
profile: str
|
113 |
+
status: str
|
114 |
+
uptime: str
|
115 |
+
bytes_in: int
|
116 |
+
bytes_out: int
|
117 |
+
|
118 |
+
|
119 |
+
class UserListData(BaseModel):
|
120 |
+
phoneNumber: str = Field(..., description="User phone number")
|
121 |
+
profile: str = Field(..., description="User profile, e.g., bandwidth limit profile")
|
122 |
+
status: str = Field(..., description="User status, either 'active' or 'inactive'")
|
123 |
+
disabled: bool = Field(..., description="Whether the user is disabled")
|
124 |
+
comment: Optional[str] = Field(
|
125 |
+
None, description="Additional comments or notes for the user"
|
126 |
+
)
|
127 |
+
data_limit: Optional[int] = Field(
|
128 |
+
None, description="Total data limit for the user in bytes"
|
129 |
+
)
|
130 |
+
|
131 |
+
|
132 |
+
class UserStatsResponse(BaseResponse):
|
133 |
+
user_stats: List[UserStats]
|
134 |
+
|
135 |
+
|
136 |
+
# Example of a response builder for structured success and error responses
|
137 |
+
class ResponseBuilder:
|
138 |
+
@staticmethod
|
139 |
+
def success(response_type: BaseModel, message: str, data: dict = None):
|
140 |
+
return response_type(code=200, message=message, payload=data)
|
141 |
+
|
142 |
+
@staticmethod
|
143 |
+
def error(response_type: BaseModel, message: str, error_details: str):
|
144 |
+
return response_type(
|
145 |
+
code=400, message=message, payload={"error": error_details}
|
146 |
+
)
|
147 |
+
|
148 |
+
|
149 |
+
# Shared Phone Number Validator
|
150 |
+
class UserBase(BaseModel):
|
151 |
+
phoneNumber: str = Field(..., description="User phone number")
|
152 |
+
|
153 |
+
@validator("phoneNumber")
|
154 |
+
def validate_phone(cls, phoneNumber):
|
155 |
+
is_valid, formatted_phone = PhoneValidator.validate_and_format(phoneNumber)
|
156 |
+
if not is_valid:
|
157 |
+
raise ValueError("Invalid phone number format")
|
158 |
+
return formatted_phone
|
159 |
+
|
160 |
+
|
161 |
+
# Request Models
|
162 |
+
class UserCreate(UserBase):
|
163 |
+
password: str = Field(..., min_length=4, description="User password")
|
164 |
+
profile: str = Field(default="2mbps_profile", description="User profile")
|
165 |
+
|
166 |
+
|
167 |
+
class UserLogin(UserBase):
|
168 |
+
password: str = Field(..., description="User password")
|
169 |
+
mac_address: str = Field(..., description="Device MAC address")
|
170 |
+
ip_address: str = Field(..., description="User's IP address")
|
171 |
+
|
172 |
+
|
173 |
+
# Base API Response Model
|
174 |
+
class ApiResponse(BaseModel):
|
175 |
+
success: bool = True
|
176 |
+
message: Optional[str] = Field("success", description="Response message")
|
177 |
+
error: Optional[str] = None
|
178 |
+
data: Optional[Dict[str, Any]] = None
|
179 |
+
|
180 |
+
|
181 |
+
# Detailed User Models
|
182 |
+
class UserParams(BaseModel):
|
183 |
+
name: str
|
184 |
+
password: str
|
185 |
+
profile: str
|
186 |
+
disabled: bool
|
187 |
+
|
188 |
+
|
189 |
+
class ActiveUserData(BaseModel):
|
190 |
+
phone: str
|
191 |
+
uptime: str
|
192 |
+
mac_address: str
|
193 |
+
ip_address: str
|
194 |
+
usage: UserStats
|
195 |
+
|
196 |
+
|
197 |
+
class UserStatsData(BaseModel):
|
198 |
+
phoneNumber: str
|
199 |
+
profile: str
|
200 |
+
status: str # Enabled or Disabled
|
201 |
+
uptime: str
|
202 |
+
usage: UserStats
|
203 |
+
|
204 |
+
|
205 |
+
class UserListData(BaseModel):
|
206 |
+
phoneNumber: str
|
207 |
+
profile: str
|
208 |
+
status: str # Active or Inactive
|
209 |
+
disabled: bool
|
210 |
+
comment: Optional[str] = ""
|
211 |
+
data_limit: Optional[int] = None
|
212 |
+
|
213 |
+
|
214 |
+
# Specific Response Models
|
215 |
+
class RegisterResponse(ApiResponse):
|
216 |
+
data: Optional[Dict[str, Any]] = Field(None, description="Registration details")
|
217 |
+
|
218 |
+
|
219 |
+
class LoginResponse(ApiResponse):
|
220 |
+
data: Optional[Dict[str, Any]] = Field(None, description="Login details")
|
221 |
+
|
222 |
+
|
223 |
+
class LogoutResponse(ApiResponse):
|
224 |
+
data: Optional[Dict[str, Any]] = Field(None, description="Logout details")
|
225 |
+
|
226 |
+
|
227 |
+
class UserStatusResponse(ApiResponse):
|
228 |
+
data: Optional[Dict[str, Any]] = Field(
|
229 |
+
None, description="User status change details"
|
230 |
+
)
|
231 |
+
|
232 |
+
|
233 |
+
class ActiveUsersResponse(ApiResponse):
|
234 |
+
active_users: List[UserSessionData]
|
235 |
+
|
236 |
+
|
237 |
+
class UserStatsResponse(ApiResponse):
|
238 |
+
data: Optional[Dict[str, List[UserStatsData]]] = Field(
|
239 |
+
None, description="Statistics for users"
|
240 |
+
)
|
241 |
+
|
242 |
+
|
243 |
+
class UsersListResponse(ApiResponse):
|
244 |
+
data: Optional[Dict[str, List[UserListData]]] = Field(
|
245 |
+
None, description="List of all users with details"
|
246 |
+
)
|
App/Mikrotik/utils/Config.py
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from dataclasses import dataclass
|
2 |
+
|
3 |
+
|
4 |
+
@dataclass
|
5 |
+
class RouterConfig:
|
6 |
+
"""Class to store router configuration settings"""
|
7 |
+
|
8 |
+
host: str = "10.8.0.6"
|
9 |
+
username: str = "admin"
|
10 |
+
password: str = "G4TZ7QFJTW"
|
App/Mikrotik/utils/api.py
ADDED
@@ -0,0 +1,426 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from librouteros import connect
|
2 |
+
from librouteros.query import Key
|
3 |
+
import logging
|
4 |
+
from typing import Optional
|
5 |
+
from datetime import datetime
|
6 |
+
from ..schema import *
|
7 |
+
from ..schema import UserListData
|
8 |
+
from .helperfx import PhoneValidator
|
9 |
+
from .Config import RouterConfig
|
10 |
+
from time import sleep
|
11 |
+
|
12 |
+
|
13 |
+
class MikrotikAPI:
|
14 |
+
"""Class to handle MikroTik router API operations, inherits VPNManager."""
|
15 |
+
|
16 |
+
def __init__(
|
17 |
+
self,
|
18 |
+
config: RouterConfig,
|
19 |
+
):
|
20 |
+
"""Initialize with router configuration and VPN configuration."""
|
21 |
+
self.config = config
|
22 |
+
self.api = None
|
23 |
+
self.logger = self._setup_logging()
|
24 |
+
self.phone_validator = PhoneValidator()
|
25 |
+
|
26 |
+
def connect(self) -> bool:
|
27 |
+
"""Establish connection to VPN and MikroTik router."""
|
28 |
+
|
29 |
+
try:
|
30 |
+
self.api = connect(
|
31 |
+
username=self.config.username,
|
32 |
+
password=self.config.password,
|
33 |
+
host=self.config.host,
|
34 |
+
port=8728,
|
35 |
+
)
|
36 |
+
self.logger.info("Successfully connected to MikroTik router.")
|
37 |
+
return True
|
38 |
+
except Exception as e:
|
39 |
+
self.logger.error(f"Failed to connect to MikroTik router: {e}")
|
40 |
+
return False
|
41 |
+
|
42 |
+
def close(self):
|
43 |
+
"""Close the router connection and disconnect VPN."""
|
44 |
+
if self.api:
|
45 |
+
self.api.close()
|
46 |
+
self.logger.info("Router connection closed.")
|
47 |
+
|
48 |
+
def add_hotspot_user(
|
49 |
+
self, phone: str, password: str, profile: str = "default"
|
50 |
+
) -> BaseResponse:
|
51 |
+
"""Add a hotspot user to the router"""
|
52 |
+
if not self.api:
|
53 |
+
return ResponseBuilder.error(
|
54 |
+
BaseResponse, "Registration failed", "Not connected to router"
|
55 |
+
)
|
56 |
+
|
57 |
+
# Validate phone number
|
58 |
+
is_valid, formatted_phone = self.phone_validator.validate_and_format(phone)
|
59 |
+
if not is_valid:
|
60 |
+
return ResponseBuilder.error(
|
61 |
+
BaseResponse,
|
62 |
+
"Registration failed",
|
63 |
+
f"Invalid phone number format: {phone}",
|
64 |
+
)
|
65 |
+
|
66 |
+
users = self.api.path("ip", "hotspot", "user")
|
67 |
+
existing_users = users.select(Key("name"))
|
68 |
+
|
69 |
+
# Check if user already exists
|
70 |
+
if any(str(user["name"]) == formatted_phone for user in existing_users):
|
71 |
+
return ResponseBuilder.error(
|
72 |
+
BaseResponse,
|
73 |
+
"Registration failed",
|
74 |
+
f"User {formatted_phone} already exists",
|
75 |
+
)
|
76 |
+
|
77 |
+
try:
|
78 |
+
# Add user to router
|
79 |
+
users.add(
|
80 |
+
name=formatted_phone,
|
81 |
+
password=password,
|
82 |
+
profile=profile,
|
83 |
+
disabled=True,
|
84 |
+
)
|
85 |
+
return ResponseBuilder.success(BaseResponse, "User registered successfully")
|
86 |
+
|
87 |
+
except Exception as e:
|
88 |
+
self.logger.error(f"Failed to add user {formatted_phone}: {e}")
|
89 |
+
return ResponseBuilder.error(BaseResponse, "Registration failed", str(e))
|
90 |
+
|
91 |
+
def login_hotspot_user(
|
92 |
+
self, phone: str, password: str, mac_address: str, ip_address: str
|
93 |
+
) -> LoginResponse:
|
94 |
+
"""Login a hotspot user"""
|
95 |
+
if not self.api:
|
96 |
+
return ResponseBuilder.error(
|
97 |
+
LoginResponse, "Login failed", "Not connected to router"
|
98 |
+
)
|
99 |
+
|
100 |
+
is_valid, formatted_phone = self.phone_validator.validate_and_format(phone)
|
101 |
+
if not is_valid:
|
102 |
+
return ResponseBuilder.error(
|
103 |
+
LoginResponse, "Login failed", f"Invalid phone number format: {phone}"
|
104 |
+
)
|
105 |
+
|
106 |
+
try:
|
107 |
+
users = self.api.path("ip", "hotspot", "user")
|
108 |
+
active = self.api.path("ip", "hotspot", "active")
|
109 |
+
|
110 |
+
# Verify if user exists and password is correct
|
111 |
+
existing_users = users.select(Key("name"), Key("password"), Key("profile"))
|
112 |
+
user_data = next(
|
113 |
+
(
|
114 |
+
user
|
115 |
+
for user in existing_users
|
116 |
+
if str(user["name"]) == formatted_phone
|
117 |
+
and user["password"] == password
|
118 |
+
),
|
119 |
+
None,
|
120 |
+
)
|
121 |
+
|
122 |
+
if not user_data:
|
123 |
+
return ResponseBuilder.error(
|
124 |
+
LoginResponse, "Login failed", "Invalid credentials"
|
125 |
+
)
|
126 |
+
|
127 |
+
# Check if already logged in
|
128 |
+
active_users = active.select(Key("user"))
|
129 |
+
if any(str(user["user"]) == formatted_phone for user in active_users):
|
130 |
+
return ResponseBuilder.error(
|
131 |
+
LoginResponse,
|
132 |
+
"Login failed",
|
133 |
+
f"User {formatted_phone} is already logged in",
|
134 |
+
)
|
135 |
+
|
136 |
+
# Perform the login
|
137 |
+
self.api.path("ip", "hotspot", "host").call(
|
138 |
+
"login",
|
139 |
+
user=formatted_phone,
|
140 |
+
password=password,
|
141 |
+
mac_address=mac_address,
|
142 |
+
ip_address=ip_address,
|
143 |
+
)
|
144 |
+
login_time = datetime.now()
|
145 |
+
return LoginResponse(
|
146 |
+
code=200, message="Login successful", login_time=login_time
|
147 |
+
)
|
148 |
+
|
149 |
+
except Exception as e:
|
150 |
+
self.logger.error(f"Login failed for {formatted_phone}: {e}")
|
151 |
+
return ResponseBuilder.error(LoginResponse, "Login failed", str(e))
|
152 |
+
|
153 |
+
def logout_hotspot_user(self, phone: str) -> LogoutResponse:
|
154 |
+
"""Logout a hotspot user"""
|
155 |
+
if not self.api:
|
156 |
+
return ResponseBuilder.error(
|
157 |
+
LogoutResponse, "Logout failed", "Not connected to router"
|
158 |
+
)
|
159 |
+
|
160 |
+
is_valid, formatted_phone = self.phone_validator.validate_and_format(phone)
|
161 |
+
if not is_valid:
|
162 |
+
return ResponseBuilder.error(
|
163 |
+
LogoutResponse, "Logout failed", f"Invalid phone number format: {phone}"
|
164 |
+
)
|
165 |
+
|
166 |
+
active = self.api.path("ip", "hotspot", "active")
|
167 |
+
|
168 |
+
try:
|
169 |
+
active_users = active.select(Key("user"), Key(".id"))
|
170 |
+
user_session = next(
|
171 |
+
(
|
172 |
+
session
|
173 |
+
for session in active_users
|
174 |
+
if str(session["user"]) == formatted_phone
|
175 |
+
),
|
176 |
+
None,
|
177 |
+
)
|
178 |
+
|
179 |
+
if not user_session:
|
180 |
+
return ResponseBuilder.error(
|
181 |
+
LogoutResponse,
|
182 |
+
"Logout failed",
|
183 |
+
f"User {formatted_phone} is not logged in",
|
184 |
+
)
|
185 |
+
|
186 |
+
# Logout the user
|
187 |
+
active.remove(user_session[".id"])
|
188 |
+
logout_time = datetime.now()
|
189 |
+
return LogoutResponse(
|
190 |
+
code=200, message="Logout successful", logout_time=logout_time
|
191 |
+
)
|
192 |
+
|
193 |
+
except Exception as e:
|
194 |
+
self.logger.error(f"Logout failed for user {formatted_phone}: {e}")
|
195 |
+
return ResponseBuilder.error(LogoutResponse, "Logout failed", str(e))
|
196 |
+
|
197 |
+
def get_active_users(self) -> ActiveUsersResponse:
|
198 |
+
"""Get list of active hotspot users"""
|
199 |
+
if not self.api:
|
200 |
+
return ResponseBuilder.error(
|
201 |
+
ActiveUsersResponse,
|
202 |
+
"Failed to get active users",
|
203 |
+
"Not connected to router",
|
204 |
+
)
|
205 |
+
|
206 |
+
try:
|
207 |
+
active = self.api.path("ip", "hotspot", "active")
|
208 |
+
active_users = active.select(
|
209 |
+
Key("user"),
|
210 |
+
Key("uptime"),
|
211 |
+
Key("mac-address"),
|
212 |
+
Key("address"),
|
213 |
+
Key("bytes-in"),
|
214 |
+
Key("bytes-out"),
|
215 |
+
# Key("profile"),
|
216 |
+
)
|
217 |
+
|
218 |
+
users_list = [
|
219 |
+
UserSessionData(
|
220 |
+
phone=str(user["user"]),
|
221 |
+
uptime=user["uptime"],
|
222 |
+
mac_address=user["mac-address"],
|
223 |
+
ip_address=user["address"],
|
224 |
+
bytes_in=int(user["bytes-in"]),
|
225 |
+
bytes_out=int(user["bytes-out"]),
|
226 |
+
status="active",
|
227 |
+
profile=str("2mbps_profile"),
|
228 |
+
)
|
229 |
+
for user in active_users
|
230 |
+
]
|
231 |
+
print(users_list)
|
232 |
+
return ActiveUsersResponse(
|
233 |
+
code=200,
|
234 |
+
message="Active users retrieved successfully",
|
235 |
+
active_users=users_list,
|
236 |
+
)
|
237 |
+
|
238 |
+
except Exception as e:
|
239 |
+
self.logger.error(f"Failed to get active users: {e}")
|
240 |
+
return ResponseBuilder.error(
|
241 |
+
ActiveUsersResponse, "Failed to get active users", str(e)
|
242 |
+
)
|
243 |
+
|
244 |
+
def get_user_stats(self, phone: Optional[str] = None) -> UserStatsResponse:
|
245 |
+
"""Get statistics for all users or a specific user"""
|
246 |
+
if not self.api:
|
247 |
+
return ResponseBuilder.error(
|
248 |
+
UserStatsResponse,
|
249 |
+
"Failed to get user statistics",
|
250 |
+
"Not connected to router",
|
251 |
+
)
|
252 |
+
|
253 |
+
try:
|
254 |
+
users = self.api.path("ip", "hotspot", "user")
|
255 |
+
all_users = users.select(
|
256 |
+
Key("name"),
|
257 |
+
Key("profile"),
|
258 |
+
Key("disabled"),
|
259 |
+
Key("uptime"),
|
260 |
+
Key("bytes-in"),
|
261 |
+
Key("bytes-out"),
|
262 |
+
)
|
263 |
+
|
264 |
+
user_stats = [
|
265 |
+
UserStats(
|
266 |
+
phone=str(user["name"]),
|
267 |
+
profile=user.get("profile", "default"),
|
268 |
+
status=(
|
269 |
+
"disabled"
|
270 |
+
if user.get("disabled", "false") == "true"
|
271 |
+
else "enabled"
|
272 |
+
),
|
273 |
+
uptime=user.get("uptime", "0s"),
|
274 |
+
bytes_in=int(user.get("bytes-in", 0)),
|
275 |
+
bytes_out=int(user.get("bytes-out", 0)),
|
276 |
+
)
|
277 |
+
for user in all_users
|
278 |
+
if not phone or str(user["name"]) == phone
|
279 |
+
]
|
280 |
+
|
281 |
+
if phone and not user_stats:
|
282 |
+
return ResponseBuilder.error(
|
283 |
+
UserStatsResponse, "User not found", f"User {phone} does not exist"
|
284 |
+
)
|
285 |
+
|
286 |
+
return UserStatsResponse(
|
287 |
+
code=200,
|
288 |
+
message="User statistics retrieved successfully",
|
289 |
+
user_stats=user_stats,
|
290 |
+
)
|
291 |
+
|
292 |
+
except Exception as e:
|
293 |
+
self.logger.error(f"Failed to get user statistics: {e}")
|
294 |
+
return ResponseBuilder.error(
|
295 |
+
UserStatsResponse, "Failed to get user statistics", str(e)
|
296 |
+
)
|
297 |
+
|
298 |
+
# Existing initialization, connect, and other methods...
|
299 |
+
|
300 |
+
def set_user_status(self, phone: str, disabled: bool) -> UserStatusResponse:
|
301 |
+
"""Enable or disable a hotspot user."""
|
302 |
+
if not self.api:
|
303 |
+
return ResponseBuilder.error(
|
304 |
+
UserStatusResponse, "Status update failed", "Not connected to router"
|
305 |
+
)
|
306 |
+
|
307 |
+
# Validate phone number
|
308 |
+
is_valid, formatted_phone = self.phone_validator.validate_and_format(phone)
|
309 |
+
if not is_valid:
|
310 |
+
return ResponseBuilder.error(
|
311 |
+
UserStatusResponse,
|
312 |
+
"Status update failed",
|
313 |
+
f"Invalid phone number format: {phone}",
|
314 |
+
)
|
315 |
+
users = self.api.path("ip", "hotspot", "user")
|
316 |
+
|
317 |
+
try:
|
318 |
+
# Find the user
|
319 |
+
existing_users = users.select(
|
320 |
+
Key("name"),
|
321 |
+
Key(".id"),
|
322 |
+
Key("mbonea"),
|
323 |
+
)
|
324 |
+
for user in existing_users:
|
325 |
+
if str(user["name"]) == formatted_phone:
|
326 |
+
user_data = user
|
327 |
+
break
|
328 |
+
|
329 |
+
if not user_data:
|
330 |
+
return ResponseBuilder.error(
|
331 |
+
UserStatusResponse,
|
332 |
+
"Status update failed",
|
333 |
+
f"User {formatted_phone} does not exist",
|
334 |
+
)
|
335 |
+
|
336 |
+
user_id = user_data[".id"]
|
337 |
+
status_value = "true" if disabled else "false"
|
338 |
+
users.update(**{".id": user_id, "disabled": status_value})
|
339 |
+
status = "disabled" if disabled else "enabled"
|
340 |
+
self.logger.info(f"User {formatted_phone} successfully {status}")
|
341 |
+
|
342 |
+
# If disabling the user, log them out of any active session
|
343 |
+
if disabled:
|
344 |
+
self.logout_hotspot_user(formatted_phone)
|
345 |
+
|
346 |
+
return UserStatusResponse(
|
347 |
+
code=200, message=f"User successfully {status}", user_status=status
|
348 |
+
)
|
349 |
+
|
350 |
+
except Exception as e:
|
351 |
+
print(f"Failed to update status for user {formatted_phone}: {e}")
|
352 |
+
self.logger.error(
|
353 |
+
f"Failed to update status for user {formatted_phone}: {e}"
|
354 |
+
)
|
355 |
+
return ResponseBuilder.error(
|
356 |
+
UserStatusResponse, "Status update failed", str(e)
|
357 |
+
)
|
358 |
+
|
359 |
+
def activate_user(self, phone: str) -> UserStatusResponse:
|
360 |
+
"""Activate a user (set disabled to false)."""
|
361 |
+
return self.set_user_status(phone, disabled=False)
|
362 |
+
|
363 |
+
def deactivate_user(self, phone: str) -> UserStatusResponse:
|
364 |
+
"""Deactivate a user (set disabled to true and log them out)."""
|
365 |
+
return self.set_user_status(phone, disabled=True)
|
366 |
+
|
367 |
+
def get_users_list(self) -> UsersListResponse:
|
368 |
+
"""Get list of all hotspot users (both active and inactive)"""
|
369 |
+
if not self.api:
|
370 |
+
return UsersListResponse(
|
371 |
+
success=False,
|
372 |
+
message="Failed to get users list",
|
373 |
+
error="Not connected to router",
|
374 |
+
)
|
375 |
+
|
376 |
+
try:
|
377 |
+
users = self.api.path("ip", "hotspot", "user")
|
378 |
+
active = self.api.path("ip", "hotspot", "active")
|
379 |
+
|
380 |
+
# Retrieve all users
|
381 |
+
all_users = users.select(
|
382 |
+
Key("name"),
|
383 |
+
Key("profile"),
|
384 |
+
Key("disabled"),
|
385 |
+
Key("comment"),
|
386 |
+
Key("limit-bytes-total"),
|
387 |
+
Key("bytes-in"),
|
388 |
+
Key("bytes-out"),
|
389 |
+
)
|
390 |
+
|
391 |
+
# Retrieve active users to check statuses
|
392 |
+
active_users = active.select(Key("user"))
|
393 |
+
active_phones = [str(user.get("user", "")) for user in active_users]
|
394 |
+
|
395 |
+
# Format the response
|
396 |
+
users_list = []
|
397 |
+
for user in all_users:
|
398 |
+
phone_number = str(user["name"])
|
399 |
+
bytes_in = int(user.get("bytes-in", 0))
|
400 |
+
bytes_out = int(user.get("bytes-out", 0))
|
401 |
+
data_limit = (
|
402 |
+
int(user.get("limit-bytes-total", 0))
|
403 |
+
if user.get("limit-bytes-total")
|
404 |
+
else None
|
405 |
+
)
|
406 |
+
|
407 |
+
user_data = UserListData(
|
408 |
+
phoneNumber=phone_number,
|
409 |
+
profile=user.get("profile", "default"),
|
410 |
+
status="active" if phone_number in active_phones else "inactive",
|
411 |
+
disabled=user.get("disabled", "false") == "true",
|
412 |
+
comment=user.get("comment", ""),
|
413 |
+
data_limit=data_limit,
|
414 |
+
)
|
415 |
+
users_list.append(user_data)
|
416 |
+
|
417 |
+
return UsersListResponse(
|
418 |
+
code=200,
|
419 |
+
message=f"Retrieved {len(users_list)} users",
|
420 |
+
data=users,
|
421 |
+
)
|
422 |
+
|
423 |
+
except Exception as e:
|
424 |
+
return UsersListResponse(
|
425 |
+
success=False, message="Failed to get users list", error=str(e)
|
426 |
+
)
|
App/Mikrotik/utils/client1-working.ovpn
ADDED
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
client
|
2 |
+
dev tun
|
3 |
+
proto tcp
|
4 |
+
remote 3.76.188.80 1194
|
5 |
+
resolv-retry infinite
|
6 |
+
nobind
|
7 |
+
persist-key
|
8 |
+
persist-tun
|
9 |
+
remote-cert-tls server
|
10 |
+
auth SHA1
|
11 |
+
cipher AES-256-CBC
|
12 |
+
data-ciphers AES-256-CBC
|
13 |
+
verb 3
|
14 |
+
|
15 |
+
<ca>
|
16 |
+
-----BEGIN CERTIFICATE-----
|
17 |
+
MIIDSzCCAjOgAwIBAgIURVMIu580Y2d7neve8yFnlspmVSkwDQYJKoZIhvcNAQEL
|
18 |
+
BQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMjQxMTIzMTYwODUzWhcNMzQx
|
19 |
+
MTIxMTYwODUzWjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcN
|
20 |
+
AQEBBQADggEPADCCAQoCggEBANcRjNoLIaWYkXXd1Th/24sXAkse4xK6FMUqaV6Y
|
21 |
+
gM8WPaepF4muyYrwDFgC+IloicFKqtfle5qL02MH16UqIJpNvulgMSgp4YwgzEtV
|
22 |
+
FZY4mzvE4ZjMc54tW8gf22R+iMFPumRnMtZuPq4U8i5lpKcZ1X5V7X/0Sc2aPbwO
|
23 |
+
LckbWkZhZdoVbJO8XhBo133Cur0FMnfEA/9dlrQ5ZMpj7xdO6Kpz/scmtw4JQ36q
|
24 |
+
mAF77LYYogiA/QvyxBAXv1UoElucIdIgMFQ8PiwBKi+FfShHlVr+m5OLlNN71eoC
|
25 |
+
N6TUOqtT4o3hLGGK31YWjoeSIJtvF251G+c3V99FecORdL0CAwEAAaOBkDCBjTAM
|
26 |
+
BgNVHRMEBTADAQH/MB0GA1UdDgQWBBRm5QQIqG+pv1VNdHI6tyuTz44HjzBRBgNV
|
27 |
+
HSMESjBIgBRm5QQIqG+pv1VNdHI6tyuTz44Hj6EapBgwFjEUMBIGA1UEAwwLRWFz
|
28 |
+
eS1SU0EgQ0GCFEVTCLufNGNne53r3vMhZ5bKZlUpMAsGA1UdDwQEAwIBBjANBgkq
|
29 |
+
hkiG9w0BAQsFAAOCAQEAIYaeBqeDLmdcqNZYP5Sf5h7NN32R9+MHkP2ZQ/MAgxjy
|
30 |
+
Ozr26L21t5jvuEw6qr+kix4pEnWnud9xTBeaTsO3TLjSrp0J0o5KBuwlXmFw1hr8
|
31 |
+
L8nJ52FlqpTv2FvhCLX5L+f5i6tL4Q5bC3AlD17eM1RWza6969NaAc6Gzl9I1sgj
|
32 |
+
3WHyKw6bnx4NTGyjCBnDH+gUK9htzVIpr31b3fEySg8U6eYbvDlX9jDZjV6Mnjrw
|
33 |
+
DvuiOZONMcmzlZvUTvHoOQHq5yIl8AG0CtaTd5YT2ksmMw18m6K/mMIwzzgENIiI
|
34 |
+
Zdf9tUqqzy+ckHTcCM8Bx0x0MUXsWzWJTs8pHmWYTQ==
|
35 |
+
-----END CERTIFICATE-----
|
36 |
+
</ca>
|
37 |
+
<cert>
|
38 |
+
Certificate:
|
39 |
+
Data:
|
40 |
+
Version: 3 (0x2)
|
41 |
+
Serial Number:
|
42 |
+
59:94:39:5f:89:05:2e:c7:bd:d4:8b:da:b3:2c:88:24
|
43 |
+
Signature Algorithm: sha256WithRSAEncryption
|
44 |
+
Issuer: CN=Easy-RSA CA
|
45 |
+
Validity
|
46 |
+
Not Before: Nov 23 16:09:16 2024 GMT
|
47 |
+
Not After : Feb 26 16:09:16 2027 GMT
|
48 |
+
Subject: CN=client1
|
49 |
+
Subject Public Key Info:
|
50 |
+
Public Key Algorithm: rsaEncryption
|
51 |
+
Public-Key: (2048 bit)
|
52 |
+
Modulus:
|
53 |
+
00:c7:2e:e4:e9:41:1e:2f:47:8f:3f:a5:59:d4:d2:
|
54 |
+
47:04:46:2b:fb:47:91:ce:5c:9d:94:6a:a6:99:28:
|
55 |
+
1d:73:ec:56:ad:07:7e:5c:42:ba:15:2d:f9:f9:dd:
|
56 |
+
67:d8:a4:ae:47:bb:21:db:de:9a:69:8b:fe:8d:8d:
|
57 |
+
91:dd:90:12:98:16:c6:e5:e5:36:85:4d:e0:f0:31:
|
58 |
+
07:bd:22:0b:c3:ad:07:d1:91:98:07:38:a9:b6:db:
|
59 |
+
0f:c8:a1:ed:16:2e:ff:f4:f1:d3:b8:ea:a4:44:f5:
|
60 |
+
2c:21:64:c5:b0:7c:6a:87:2d:28:3d:03:71:d1:12:
|
61 |
+
9d:be:9c:ed:e9:f8:fb:de:de:8e:d2:2d:39:de:f1:
|
62 |
+
23:36:a3:b1:74:03:97:61:db:90:bc:04:26:94:61:
|
63 |
+
d9:ee:62:7b:a7:d3:b7:f5:e8:f6:5d:e4:8b:0d:bf:
|
64 |
+
f9:cc:bb:f9:32:b2:2d:05:05:4c:29:cf:fb:89:5e:
|
65 |
+
dd:40:74:66:5d:04:16:7a:85:31:e2:ce:47:83:9c:
|
66 |
+
ff:72:8c:26:24:c2:94:9e:6c:6c:ce:fb:b2:9d:07:
|
67 |
+
dd:58:39:7b:05:fe:f5:0e:cd:98:97:9f:d9:c2:de:
|
68 |
+
a8:08:59:4a:c9:d2:c9:4f:b8:b3:69:1e:84:8c:e4:
|
69 |
+
d4:41:5a:2a:7f:43:13:04:b4:be:ae:ca:26:bb:a6:
|
70 |
+
9b:33
|
71 |
+
Exponent: 65537 (0x10001)
|
72 |
+
X509v3 extensions:
|
73 |
+
X509v3 Basic Constraints:
|
74 |
+
CA:FALSE
|
75 |
+
X509v3 Subject Key Identifier:
|
76 |
+
83:3E:E4:7C:7D:93:EE:E0:20:2F:CF:FB:E0:D4:90:D9:C5:26:5D:04
|
77 |
+
X509v3 Authority Key Identifier:
|
78 |
+
keyid:66:E5:04:08:A8:6F:A9:BF:55:4D:74:72:3A:B7:2B:93:CF:8E:07:8F
|
79 |
+
DirName:/CN=Easy-RSA CA
|
80 |
+
serial:45:53:08:BB:9F:34:63:67:7B:9D:EB:DE:F3:21:67:96:CA:66:55:29
|
81 |
+
X509v3 Extended Key Usage:
|
82 |
+
TLS Web Client Authentication
|
83 |
+
X509v3 Key Usage:
|
84 |
+
Digital Signature
|
85 |
+
Signature Algorithm: sha256WithRSAEncryption
|
86 |
+
Signature Value:
|
87 |
+
ba:36:55:f9:d6:9c:3f:81:ce:4f:27:36:8c:d6:d3:df:69:f9:
|
88 |
+
b7:e7:34:85:33:4b:c8:71:24:63:64:10:a8:69:6a:02:78:0f:
|
89 |
+
8c:c4:81:df:8b:70:c3:75:9b:77:00:48:bd:5d:0b:91:6a:bc:
|
90 |
+
13:c1:af:99:4c:ab:59:81:05:ca:89:9b:1d:1c:94:b0:3a:f6:
|
91 |
+
27:da:be:7b:f8:7f:6a:7b:37:01:f3:f6:9f:35:73:25:54:72:
|
92 |
+
b7:de:ae:a5:a5:98:0b:84:14:60:03:34:d3:d2:e6:07:8b:bb:
|
93 |
+
2c:65:50:4f:2a:07:b5:94:96:3e:58:e4:b7:c2:52:56:b6:9e:
|
94 |
+
23:e4:56:bb:e2:59:ba:38:c8:5d:f0:ac:21:ab:f0:4a:83:ef:
|
95 |
+
7f:a4:b0:4f:0a:22:fc:c7:aa:9f:47:28:2f:7f:70:e9:4e:12:
|
96 |
+
36:7d:f6:e3:97:6a:5f:90:7e:fc:91:26:8f:0f:1b:d0:85:a2:
|
97 |
+
6e:ed:b8:1b:00:8d:67:1b:0d:06:a9:a5:d1:4f:2b:4c:f5:ac:
|
98 |
+
8e:4f:6b:18:0e:7d:77:1a:45:7b:79:f0:ca:8b:e8:23:0e:aa:
|
99 |
+
e8:3e:14:3e:e5:ba:11:b7:74:a4:6c:89:af:b3:f7:1d:af:6d:
|
100 |
+
9b:69:30:97:6a:28:af:05:ae:4c:d8:d0:c3:53:cd:57:56:33:
|
101 |
+
8f:ae:e9:04
|
102 |
+
-----BEGIN CERTIFICATE-----
|
103 |
+
MIIDVTCCAj2gAwIBAgIQWZQ5X4kFLse91IvasyyIJDANBgkqhkiG9w0BAQsFADAW
|
104 |
+
MRQwEgYDVQQDDAtFYXN5LVJTQSBDQTAeFw0yNDExMjMxNjA5MTZaFw0yNzAyMjYx
|
105 |
+
NjA5MTZaMBIxEDAOBgNVBAMMB2NsaWVudDEwggEiMA0GCSqGSIb3DQEBAQUAA4IB
|
106 |
+
DwAwggEKAoIBAQDHLuTpQR4vR48/pVnU0kcERiv7R5HOXJ2UaqaZKB1z7FatB35c
|
107 |
+
QroVLfn53WfYpK5HuyHb3pppi/6NjZHdkBKYFsbl5TaFTeDwMQe9IgvDrQfRkZgH
|
108 |
+
OKm22w/Ioe0WLv/08dO46qRE9SwhZMWwfGqHLSg9A3HREp2+nO3p+Pve3o7SLTne
|
109 |
+
8SM2o7F0A5dh25C8BCaUYdnuYnun07f16PZd5IsNv/nMu/kysi0FBUwpz/uJXt1A
|
110 |
+
dGZdBBZ6hTHizkeDnP9yjCYkwpSebGzO+7KdB91YOXsF/vUOzZiXn9nC3qgIWUrJ
|
111 |
+
0slPuLNpHoSM5NRBWip/QxMEtL6uyia7ppszAgMBAAGjgaIwgZ8wCQYDVR0TBAIw
|
112 |
+
ADAdBgNVHQ4EFgQUgz7kfH2T7uAgL8/74NSQ2cUmXQQwUQYDVR0jBEowSIAUZuUE
|
113 |
+
CKhvqb9VTXRyOrcrk8+OB4+hGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghRF
|
114 |
+
Uwi7nzRjZ3ud697zIWeWymZVKTATBgNVHSUEDDAKBggrBgEFBQcDAjALBgNVHQ8E
|
115 |
+
BAMCB4AwDQYJKoZIhvcNAQELBQADggEBALo2VfnWnD+Bzk8nNozW099p+bfnNIUz
|
116 |
+
S8hxJGNkEKhpagJ4D4zEgd+LcMN1m3cASL1dC5FqvBPBr5lMq1mBBcqJmx0clLA6
|
117 |
+
9ifavnv4f2p7NwHz9p81cyVUcrferqWlmAuEFGADNNPS5geLuyxlUE8qB7WUlj5Y
|
118 |
+
5LfCUla2niPkVrviWbo4yF3wrCGr8EqD73+ksE8KIvzHqp9HKC9/cOlOEjZ99uOX
|
119 |
+
al+QfvyRJo8PG9CFom7tuBsAjWcbDQappdFPK0z1rI5PaxgOfXcaRXt58MqL6CMO
|
120 |
+
qug+FD7luhG3dKRsia+z9x2vbZtpMJdqKK8FrkzY0MNTzVdWM4+u6QQ=
|
121 |
+
-----END CERTIFICATE-----
|
122 |
+
</cert>
|
123 |
+
<key>
|
124 |
+
-----BEGIN PRIVATE KEY-----
|
125 |
+
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDHLuTpQR4vR48/
|
126 |
+
pVnU0kcERiv7R5HOXJ2UaqaZKB1z7FatB35cQroVLfn53WfYpK5HuyHb3pppi/6N
|
127 |
+
jZHdkBKYFsbl5TaFTeDwMQe9IgvDrQfRkZgHOKm22w/Ioe0WLv/08dO46qRE9Swh
|
128 |
+
ZMWwfGqHLSg9A3HREp2+nO3p+Pve3o7SLTne8SM2o7F0A5dh25C8BCaUYdnuYnun
|
129 |
+
07f16PZd5IsNv/nMu/kysi0FBUwpz/uJXt1AdGZdBBZ6hTHizkeDnP9yjCYkwpSe
|
130 |
+
bGzO+7KdB91YOXsF/vUOzZiXn9nC3qgIWUrJ0slPuLNpHoSM5NRBWip/QxMEtL6u
|
131 |
+
yia7ppszAgMBAAECggEAIF40gNs+JnzAgJ1EPdt2AvHMT+dPgHN4gBfcvuLP9nif
|
132 |
+
lTq0hBWr26k/CCW8rG4GjE2SsQI5oZFIaoRpAdJZ0zFQXSekdoEzXpT5JvkTZFcI
|
133 |
+
ADxisjm5CqgKppX5yzMUESADQfePfk1BQKP5pDZzsUfbVB7tLgaSb9lcqDr34z2J
|
134 |
+
yf2Ref6dIL5BKdjwQm/fOzeRbjio6bXHyJ6fxrPUPjGt7GXRBkOrofXJJ7bb/x4L
|
135 |
+
ND6cClKSiM+jd/i/sOapNiqceqIWvmlvLl5hLJVIQUz505io9S9f8fv1gMy3hVZ6
|
136 |
+
j7P/LgJWlDRHDLhMgqTkNr1vz5gPnjx/kiOEROe40QKBgQD4czLuB7Fm3FqsjYkD
|
137 |
+
hvgBUji9Y6GbQM91RfXlRcvhIYwFmBfnl7Z2mIUeCwLLWO5Vzc9lvkKF/27qnls4
|
138 |
+
pr+GMaVmpmlcL7DKPSrrm5K/8cPfln2TezHn64tIjbFi8xQWw1XLEv4i2ndgSEyV
|
139 |
+
MqEen77g0Hc+2pPigVQZmkzKXQKBgQDNPG1aUmpNprECA2rnCj1fXCuLqlSsbLRO
|
140 |
+
GcrGltXot7VZ/3IxW5vwjxWqzNGeJG9deAT0oC3RSOf/fv05R3FnEDRleKnxajlq
|
141 |
+
CseAQ6X7sXAMjNb1GmSzfjguXjOvQCSRBlu+Wx1r/FNe7kK82d7Hedtb1JYZh6G0
|
142 |
+
vZy98L9CzwKBgQC2QHNMzxHgvaY6S/0FPF3zQihjLZHf/JPymCaAUEn11RENDXwD
|
143 |
+
pHPx3YJQ/ozHNG5pPPd10DKmbzEjJJUQIqn+O670dQB24nkScfppKQ9mhGhGPPPT
|
144 |
+
WxzJ3yymRWKpjlzfMd1egYkxcgb99ytOivxMJaz055eB4P94uZxCx8Cq9QKBgAri
|
145 |
+
rZogzOqZcMH+lGj0rhSkutqJijwq99U8oPivf2D8fW3sko3zoe28aRXKD0QoApAe
|
146 |
+
kYS4CjYTe9qdTakAFQ+2WFEZeUoIrErnj3VKIT+cRakkvzH42GZ8x1YOQQeGi2n1
|
147 |
+
wF/0TTcxBur+ECQcGijSWcQhHmT0QKtpcyrP3hUZAoGAW8NqevJyg3PDQI9kbShn
|
148 |
+
c08sVwV6jHniAnQHTNPKQ/KR8fzO0PS8B7eaImD4ZJOKjIAp5AfmKpwM4dvWl/i4
|
149 |
+
Vs1gY9eHFkDfxDDoCbcn0R6tPMR2LeqPbqZvhEyJaDVEd6Lh/Y9X7PQhhONCityJ
|
150 |
+
DhsXoTwnE70b3QezzBi6yk0=
|
151 |
+
-----END PRIVATE KEY-----
|
152 |
+
</key>
|
App/Mikrotik/utils/helperfx.py
ADDED
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from .api import MikrotikAPI
|
2 |
+
from .Config import RouterConfig
|
3 |
+
from fastapi import HTTPException
|
4 |
+
|
5 |
+
import re
|
6 |
+
from typing import Tuple
|
7 |
+
|
8 |
+
|
9 |
+
class PhoneValidator:
|
10 |
+
"""Validator for Tanzanian phone numbers"""
|
11 |
+
|
12 |
+
# Valid Tanzanian mobile operator prefixes
|
13 |
+
VALID_PREFIXES = [
|
14 |
+
"071",
|
15 |
+
"074",
|
16 |
+
"075",
|
17 |
+
"076",
|
18 |
+
"077", # Vodacom
|
19 |
+
"068",
|
20 |
+
"069", # Airtel
|
21 |
+
"065",
|
22 |
+
"067", # Tigo
|
23 |
+
"078",
|
24 |
+
"079", # TTCL
|
25 |
+
"073", # Zantel
|
26 |
+
"061",
|
27 |
+
"062", # Halotel
|
28 |
+
]
|
29 |
+
|
30 |
+
@staticmethod
|
31 |
+
def validate_and_format(phone: str) -> Tuple[bool, str]:
|
32 |
+
"""
|
33 |
+
Validates and formats Tanzanian phone numbers.
|
34 |
+
Returns (is_valid, formatted_number)
|
35 |
+
|
36 |
+
Valid format:
|
37 |
+
- 255712345678 (12 digits starting with 255)
|
38 |
+
"""
|
39 |
+
# Remove any spaces or special characters
|
40 |
+
phone = re.sub(r"[\s\-\(\)]", "", phone)
|
41 |
+
|
42 |
+
# Convert all formats to 255 format
|
43 |
+
if phone.startswith("+255"):
|
44 |
+
phone = "255" + phone[4:]
|
45 |
+
elif phone.startswith("0"):
|
46 |
+
phone = "255" + phone[1:]
|
47 |
+
elif not phone.startswith("255"):
|
48 |
+
return False, ""
|
49 |
+
|
50 |
+
# Check if it matches the basic pattern (12 digits starting with 255)
|
51 |
+
if not re.match(r"^255\d{9}$", phone):
|
52 |
+
return False, ""
|
53 |
+
|
54 |
+
# Check if the prefix is valid (check the digits after 255)
|
55 |
+
prefix = "0" + phone[3:5]
|
56 |
+
if prefix not in PhoneValidator.VALID_PREFIXES:
|
57 |
+
return False, ""
|
58 |
+
|
59 |
+
return True, phone
|
60 |
+
|
61 |
+
|
62 |
+
# Dependency to get MikrotikAPI instance
|
63 |
+
def get_mikrotik():
|
64 |
+
config = RouterConfig()
|
65 |
+
mikrotik = MikrotikAPI(config)
|
66 |
+
try:
|
67 |
+
if mikrotik.connect():
|
68 |
+
yield mikrotik
|
69 |
+
else:
|
70 |
+
raise HTTPException(
|
71 |
+
status_code=503,
|
72 |
+
detail={"success": False, "message": "Could not connect to router"},
|
73 |
+
)
|
74 |
+
finally:
|
75 |
+
mikrotik.close()
|
App/app.py
CHANGED
@@ -14,6 +14,7 @@ from .Messages.MessagesRoute import message_router
|
|
14 |
# from .Subscriptions.background_tasks import check_expiring_subscriptions
|
15 |
import asyncio
|
16 |
import logging
|
|
|
17 |
|
18 |
logging.basicConfig(level=logging.INFO)
|
19 |
|
@@ -32,10 +33,24 @@ app.add_middleware(
|
|
32 |
)
|
33 |
|
34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
35 |
@app.on_event("startup")
|
36 |
async def startup_event():
|
37 |
await Tortoise.init(config=TORTOISE_ORM)
|
38 |
await Tortoise.generate_schemas()
|
|
|
39 |
|
40 |
|
41 |
@app.get("/")
|
|
|
14 |
# from .Subscriptions.background_tasks import check_expiring_subscriptions
|
15 |
import asyncio
|
16 |
import logging
|
17 |
+
import subprocess
|
18 |
|
19 |
logging.basicConfig(level=logging.INFO)
|
20 |
|
|
|
33 |
)
|
34 |
|
35 |
|
36 |
+
async def connect_to_vpn():
|
37 |
+
"""Connect to the VPN on startup."""
|
38 |
+
try:
|
39 |
+
# Replace 'your_vpn_command' with the actual command to connect to your VPN
|
40 |
+
# For example, if using OpenVPN:
|
41 |
+
command = ["openvpn", "--config", "App/Mikrotik/utils/client1-working.ovpn"]
|
42 |
+
process = subprocess.Popen(command)
|
43 |
+
await asyncio.sleep(5) # Wait for a few seconds to ensure the VPN connects
|
44 |
+
logging.info("VPN connected successfully.")
|
45 |
+
except Exception as e:
|
46 |
+
logging.error(f"Failed to connect to VPN: {str(e)}")
|
47 |
+
|
48 |
+
|
49 |
@app.on_event("startup")
|
50 |
async def startup_event():
|
51 |
await Tortoise.init(config=TORTOISE_ORM)
|
52 |
await Tortoise.generate_schemas()
|
53 |
+
await connect_to_vpn() # Connect to VPN on startup
|
54 |
|
55 |
|
56 |
@app.get("/")
|
Dockerfile
CHANGED
@@ -11,6 +11,8 @@ COPY . /app
|
|
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 7860
|
16 |
|
|
|
11 |
RUN pip install --no-cache-dir --upgrade pip && \
|
12 |
pip install --no-cache-dir -r requirements.txt
|
13 |
|
14 |
+
|
15 |
+
RUN apt update ** apt install -y openvpn
|
16 |
# Expose the port the app runs on
|
17 |
EXPOSE 7860
|
18 |
|