Mbonea commited on
Commit
afbb33a
·
1 Parent(s): ffcfb71

vpn ready?

Browse files
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