Skip to content

Commit 45b9857

Browse files
committed
feat(portaswitch): add on-demand session migration support
1 parent 6943f44 commit 45b9857

File tree

4 files changed

+52
-13
lines changed

4 files changed

+52
-13
lines changed

app/bss/adapters/portaswitch/adapter.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
session_close_error,
5757
refresh_token_invalid_error,
5858
addon_required_error,
59+
session_upgrade_needed_error,
5960
)
6061
from .serializer import Serializer
6162
from .types import (
@@ -66,7 +67,7 @@
6667
PortaSwitchMailboxMessageFlagAction,
6768
PortaSwitchMailboxMessageAttachmentFormat,
6869
)
69-
from .utils import generate_otp_id, extract_fault_code
70+
from .utils import generate_otp_id, extract_fault_code, generate_hash_dictionary
7071

7172

7273
class PortaSwitchAdapter(BSSAdapter):
@@ -111,6 +112,7 @@ def __init__(self, config: AppConfig):
111112

112113
self._cached_otp_ids = TiedKeyValue()
113114
self._cached_capabilities = self.calculate_capabilities()
115+
self._hash_dictionary = generate_hash_dictionary() if self._settings.ENABLE_ON_DEMAND_SESSION_MIGRATION else {}
114116

115117
@classmethod
116118
def name(cls) -> str:
@@ -246,12 +248,11 @@ def validate_otp(self, otp: OTPVerifyRequest) -> SessionInfo:
246248

247249
self._cached_otp_ids.pop(otp_id)
248250

249-
# Emulate account login.
250-
account_info: dict = self._admin_api.get_account_info(i_account=i_account)["account_info"]
251-
session_data: dict = self._account_api.login(account_info["login"], account_info["password"])
251+
i_account = str(i_account)
252+
session_data = self._emulate_account_login(i_account)
252253

253254
return SessionInfo(
254-
user_id=UserId(str(account_info["i_account"])),
255+
user_id=UserId(i_account),
255256
access_token=session_data["access_token"],
256257
refresh_token=session_data["refresh_token"],
257258
expires_at=datetime.now() + timedelta(seconds=session_data["expires_in"]),
@@ -284,20 +285,29 @@ def validate_session(self, access_token: str) -> SessionInfo:
284285
except ExpiredSignatureError:
285286
raise access_token_expired_error()
286287
except JWTError:
288+
if self._settings.ENABLE_ON_DEMAND_SESSION_MIGRATION:
289+
raise session_upgrade_needed_error()
287290
raise access_token_invalid_error()
288291

289292
def refresh_session(self, refresh_token: str) -> SessionInfo:
290293
"""Refreshes the PortaSwitch account session.
291294
292295
Parameters:
293-
refresh_token (str): The token used to refresh the session.
296+
refresh_token (str): The token used to refresh the session or hashed i_account in case of on-demand session migration
294297
295298
Returns:
296299
(SessionInfo): The object with the obtained session tokens.
297300
298301
"""
299302
try:
300-
session_data: dict = self._account_api.refresh(refresh_token=refresh_token)
303+
304+
if self._settings.ENABLE_ON_DEMAND_SESSION_MIGRATION:
305+
# On-demand session migration is enabled. We need to emulate account login to get a new access token
306+
i_account = self._hash_dictionary.get(refresh_token, refresh_token)
307+
session_data = self._emulate_account_login(str(i_account))
308+
else:
309+
session_data = self._account_api.refresh(refresh_token)
310+
301311
access_token: str = session_data["access_token"]
302312
account_info: dict = self._account_api.get_account_info(access_token=access_token)["account_info"]
303313

@@ -1242,8 +1252,7 @@ def custom_method_private(
12421252

12431253
def _custom_pages(self, user_id: str, data: CustomRequest, lang: str = None) -> CustomResponse:
12441254
_ = get_translation_func(lang)
1245-
account_info = self._admin_api.get_account_info(i_account=user_id).get("account_info")
1246-
session_data = self._account_api.login(account_info["login"], account_info["password"])
1255+
session_data = self._emulate_account_login(user_id)
12471256

12481257
pages = []
12491258
if self._portaswitch_settings.SELF_CONFIG_PORTAL_URL:
@@ -1260,8 +1269,7 @@ def _custom_pages(self, user_id: str, data: CustomRequest, lang: str = None) ->
12601269
return CustomResponse(pages=pages)
12611270

12621271
def _external_page_access_token(self, user_id: str, data: CustomRequest, lang: str = None) -> CustomResponse:
1263-
account_info = self._admin_api.get_account_info(i_account=user_id).get("account_info")
1264-
session_data = self._account_api.login(account_info["login"], account_info["password"])
1272+
session_data = self._emulate_account_login(user_id)
12651273

12661274
return CustomResponse(
12671275
access_token=AccessToken(session_data['access_token']),
@@ -1393,3 +1401,9 @@ def _get_all_accounts_by_customer(self, i_customer: int) -> list[dict]:
13931401
offset += limit
13941402

13951403
return all_accounts
1404+
1405+
def _emulate_account_login(self, i_account: str) -> dict:
1406+
"""Emulate a login for a PortaSwitch account."""
1407+
account_info = self._admin_api.get_account_info(i_account=i_account).get("account_info")
1408+
1409+
return self._account_api.login(account_info["login"], account_info["password"])

app/bss/adapters/portaswitch/config.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def decode_contacts_selecting_extension_types(cls, v: Union[List, str]) -> List[
5757
parts = [x.strip() for x in v.split(';') if x.strip()]
5858
if not parts:
5959
return list(PortaSwitchExtensionType)
60-
60+
6161
return [PortaSwitchExtensionType(x) for x in parts]
6262

6363
@field_validator("CONTACTS_SELECTING_CUSTOMER_IDS", mode='before')
@@ -87,7 +87,7 @@ def decode_contacts_custom(cls, v: Union[List, str]) -> List[dict]:
8787
parts = [x.strip() for x in v.split(';') if x.strip()]
8888
if not parts:
8989
return []
90-
90+
9191
return [json.loads(x) for x in parts]
9292

9393
@field_validator("ALLOWED_ADDONS", mode='before')
@@ -119,6 +119,7 @@ def decode_ignore_accounts(cls, v: Union[List, str, int, None]) -> List[str]:
119119

120120
class Settings(BaseSettings):
121121
JANUS_SIP_FORCE_TCP: bool = False
122+
ENABLE_ON_DEMAND_SESSION_MIGRATION: bool = False
122123

123124
PORTASWITCH_SETTINGS: PortaSwitchSettings = PortaSwitchSettings()
124125
OTP_SETTINGS: OTPSettings = OTPSettings()

app/bss/adapters/portaswitch/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ def access_token_expired_error():
99
return WebTritErrorException(401, "Access token expired", "access_token_expired")
1010

1111

12+
def session_upgrade_needed_error():
13+
return WebTritErrorException(401, "Token format is outdated. Migration required.", "session_upgrade_needed")
14+
15+
1216
def user_authentication_error():
1317
return WebTritErrorException(401, "User authentication error")
1418

app/bss/adapters/portaswitch/utils.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import base64
2+
import hashlib
13
import uuid
24

35
from report_error import WebTritErrorException
@@ -34,3 +36,21 @@ def extract_fault_code(error: WebTritErrorException) -> str:
3436
def generate_otp_id() -> str:
3537
"""Generate a new unique ID for the session"""
3638
return str(uuid.uuid1()).replace("-", "") + str(uuid.uuid4()).replace("-", "")
39+
40+
41+
def generate_hash(value) -> str:
42+
"""Generates a SHA256 hash encoded in Base64 without padding."""
43+
hash_bytes = hashlib.sha256(str(value).encode("utf-8")).digest()
44+
45+
return base64.b64encode(hash_bytes).decode("utf-8").rstrip("=")
46+
47+
48+
def generate_hash_dictionary(max_value: int = 1_000_000) -> dict[str, int]:
49+
"""Generates a hash dictionary for a specific range."""
50+
hash_dict = {}
51+
52+
for user_id in range(1, max_value + 1):
53+
hash_value = generate_hash(user_id)
54+
hash_dict[hash_value] = user_id
55+
56+
return hash_dict

0 commit comments

Comments
 (0)