Skip to main content

Runbook: Version Switching Mechanism (CM V1 ↔ CM V2)

Table of Contents

  1. Overview & Architecture
  2. Upgrade Workflow: CM V1 to CM V2
  3. Downgrade Workflow: CM V2 to CM V1 (Switch Back)

1. Overview & Architecture

This runbook documents the authentication, authorization, and page routing workflow between the legacy system (CM V1 - PHP/CodeIgniter) and the new system (CM V2 - Go/Fiber & Single Page Application/SPA).

The system supports a seamless transition between versions, enabling phased feature rollouts and allowing users to toggle back to the legacy version if necessary.

Core Components

  • CM V1 (Legacy): Built on PHP (CodeIgniter). It manages the initial user session and verifies if the property/user is whitelisted for the new version.
  • CM V2 (New Version): Built on Go (Fiber framework) for the backend and a modern frontend SPA. It handles session tokens validated via SSO or a fallback legacy login mechanism.

2. Upgrade Workflow: CM V1 to CM V2

Step 1: Feature Flag & Version Detection (CM V1)

When a user accesses an optimized page in V1, the system checks whether the user/property is whitelisted for V2 and ensures they haven't explicitly forced the legacy version via a cookie setting.

Core Controller Logic Reference (V1 PHP):

// Main Controller Interception
if((featureFlag(['BNL', 'BNLTEST'], 'white_user') || featureFlag([14620,64])) && $isPayku == false) {
if(isNewVersion('dashboard')) {
header("Location: ".menuData('dashboard')["link"]);
exit();
}

$this->load->view('components/switch_version');
}

Code Explanation: Checks if the user/property is whitelisted. If isNewVersion() returns true, it immediately redirects the traffic to the CM V2 endpoint.

Cookie Validation Logic (isNewVersion):

// Version Detection via Cookie
function isNewVersion($key) {
$versions = isset($_COOKIE["legacy_features_versions"]) ? json_decode($_COOKIE["legacy_features_versions"], true) : [];
foreach ($versions as $version) {
if ($version["key"] == $key) return false; // Stay on V1
}
return true; // Upgrade to V2
}

Code Explanation: Reads the legacy_features_versions cookie. If the current page key matches an entry in the cookie, it returns false, forcing the user to stay on V1..

If the user qualifies for V2, menuData retrieves the targeted modern route and calls redirectToV2 to convert the current session into secure, query-string parameters.

Route Mapping & Redirection Wrapper:

function menuData($key){
$v2Menus = [...];

foreach($v2Menus as $menu) {
if($menu["key"] == $key) {
$menu["link"] = redirectToV2($menu["link"], @$menu["brand"]);
return $menu;
}
}
return null;
}

Code Explanation: > Acts as a routing dictionary mapping V1 keys to V2 URLs. When a matching key is requested, it passes the target V2 URL and the brand identifier to the redirectToV2 generator function.

SSO Callback URL & Fallback Token Construction:

// SSO Callback URL Construction (Simplified)
function redirectToV2($url, $brand) {
$token = $ci->session->userdata('sso_token') ?? cm_v2_token($email);
$isLegacy = empty($ci->session->userdata('sso_token'));
$propertyId = $ci->session->userdata('propvalue');

return cmV2BaseUrl() . "/sso/callback?token=" . $token . "&page=" . ltrim($url, '/') . "&property_id=" . $propertyId . "&lgc=" . $isLegacy;
}

Code Explanation: Gathers the active session token (or generates a fallback token for legacy direct logins) and generates the V2 handshake URL with query parameters.

The /sso/callback endpoint in CM V2 intercepts payload variables from V1, reconciles token credentials, flags impersonated administrator states in Redis, and sets security cookies.

Endpoint Handler Implementation (Go):

func (s *sssController) Callback(c *fiber.Ctx) error {
token := c.Query("token")
isLegacy := c.QueryBool("lgc")

// ... (PropertyUID to PropertyID conversion logic) ...

// Handle legacy administrative impersonation sessions via Redis cache
if isLegacy && isImpersonateLegacyLogin {
redis.SetWithExpired(context.Background(), fmt.Sprintf("lgc_impersonate:%s", userIdLegacyLogin), true, 15*time.Minute)
}

// Execute Token Authentication Core Flow
res := s.ssoService.VerifyUserByToken(token, domain, deviceID, isLegacy, userIdLegacyLogin)

if res.GetCode() == 200 {
data := res.GetData().(fiber.Map)
cookieSetting, _ := s.configCookie(c)

// ... (Set login cookie) ...

// Final handshake redirection to the Frontend SPA
return c.Redirect(fmt.Sprintf("%s/redirect?page=%s&property_id=%s&wd=%s&lang=%s&lgc=%t", domainData.FeUrl, page, propertyId, domainData.BeUrl, lang, isLegacy))
}
return c.Redirect(fmt.Sprintf("%s/401", domainData.FeUrl))
}

Code Explanation: > The Go backend entry point for switching version requests. It reads the parameters generated by V1, logs impersonation activity to Redis if needed, triggers the token validation service, and sets HTTP-only session cookies (CMT, USR, PTKN) before performing the final redirect to the Frontend SPA.

Token Authentication Service Core (VerifyUserByToken):

func (s *SSOService) VerifyUserByToken(token string, domain string, deviceId string, isLegacy bool, userIdLegacyLogin string) response.DataApi {
if isLegacy {
return s.LegacyLogin(token, userIdLegacyLogin, deviceId)
}

// Connect with standard external SSO endpoint under normal parameters
ssoApiUrl := fmt.Sprintf("%s/api/user/verify-by-product", env.String("SSO_API_URL"))
// ... (Inbound/Outbound HTTP Request handling) ...

if resp.StatusCode == 200 {
// Issue an authorized internal application token
cmToken, devId, _ := s.tokenService.Create(models.User{ID: extIdInt, Email: userLogin.Email}, deviceId, userLogin.AccessRules, propertyAccess)
return response.Api(response.SetCode(200), response.SetData(fiber.Map{
"token": cmToken,
"device_id": devId,
"external_id": encrypt.Encrypt(externalId),
}))
}
return response.Api(response.SetCode(resp.StatusCode))
}

Code Explanation: > Internal validation service. If isLegacy is true, it authenticates via local database structures (LegacyLogin). Otherwise, it communicates directly with the centralized SSO_API_URL service. Upon a successful verification, it generates and responds with a fresh, secure CM V2 token structure.

3. Downgrade Workflow: CM V2 to CM V1 (Switch Back)

When a user disables the "New Version" top header toggle switch container, the downgrade loop is executed.

Step 1: Frontend Client Interaction

Toggling the version switcher creates or changes a specific browser cookie named legacy_features_versions via Frontend action script or API response.

Cookie Structure Payload (JSON format):

[
{
"key": "dashboard"
},
{
"key": "rate_availability"
}
]

Step 2: Legacy Route Interception (CM V1)

When the client redirects back to the original dashboard route (/dashboard or /deals/monthly_deals), the V1 application evaluates the payload context:

// Interpret forced downgrade instructions inside the client cookie storage
$versions = json_decode($_COOKIE["legacy_features_versions"], true);

// Loop through array elements to parse matching page keys
foreach($versions as $version) {
if($version["key"] == $key) {
return false; // Break verification loop, forcing the app to reject V2 routing!
}
}

Code Explanation: > This is a breakdown snippet of the isNewVersion logic execution back in V1. It unpacks the client cookie, reads the JSON configuration array, and triggers return false if the active module name matches, preventing V1 from routing the request back to the Go V2 API.

Because isNewVersion() returns false, the upgrade block execution terminates immediately. The application skips header("Location: ...") and safely serves the standard legacy PHP view layout code.