2025-05-23 18:51:19 [::1][-][-][trace][app\controllers\ApiSecurityController::actionCsrfToken] CSRF Token from getCsrfToken(): RpddH38dvW6myQxuKQHP18KW1nadtYYhQLClywmDdIQF7nBIJ1ftA8zxRV1bOLWzrKKDF9X0y1Jz9_yAOOwlvA==
2025-05-23 18:51:19 [::1][-][-][info][app\controllers\ApiSecurityController::actionCsrfToken] CSRF token to send to client: RpddH38dvW6myQxuKQHP18KW1nadtYYhQLClywmDdIQF7nBIJ1ftA8zxRV1bOLWzrKKDF9X0y1Jz9_yAOOwlvA==
2025-05-23 18:51:19 [::1][-][-][info][app\controllers\ApiSecurityController::actionCsrfToken] Sending CSRF cookie with params: []

	public function actionCsrfToken(): array
	{
		$request = \Yii::$app->request;
		
		if (!$request->isGet)
		{
			throw new BadRequestHttpException('Only GET requests are allowed.');
		}
		
		$token = $request->getCsrfToken();
		
		\Yii::debug('CSRF Token from getCsrfToken(): ' . $token, __METHOD__);
		
		/**
		* Explicitly add CSRF cookie to response (forces Set-Cookie header)
		* Note: No need to set cookie manually here
		*/
		$response = \Yii::$app->response;
		
		/*$response->cookies->add(
			new Cookie([
				'name' => $request->csrfParam,
				'value' => $token,
				'httpOnly' => false,// Always false for csrf token
				'secure' => YII_ENV_PROD ? true : false,// True if you use HTTPS in production
				'sameSite' => YII_ENV_PROD ? Cookie::SAME_SITE_NONE : Cookie::SAME_SITE_LAX,
				'path' => '/',// Ensure cookie always overrides previous cookies on all paths
			])
		);//*/
		
		/**
		* Cookie check
		*
		foreach ($response->cookies as $cookie)
		{
			\Yii::info("Set-Cookie: {$cookie->name} = {$cookie->value}", __METHOD__);
		}//*/
		\Yii::info("CSRF token to send to client: $token", __METHOD__);
		\Yii::info('Sending CSRF cookie with params: ' . json_encode($response->cookies->toArray()), __METHOD__);
		
		// No need to set format here explicitly if contentNegotiator behavior is active,
		//\Yii::$app->response->format = Response::FORMAT_JSON;
		
		\Yii::$app->response->statusCode = 200;
		
		return [
			'csrfParam' => $request->csrfParam,
			'csrfToken' => $token,
		];
	}
	
(2025-05-23T18:51:19.365Z) [All cookies via js-cookie:] 
{_csrf: 'cf311952abef22898e59561b0f730a118d4aae4d893228c819…rf";i:1;s:32:"Cy-WXJPmj8I3r9zdn4UaHAMs3GYK1oQ8";}'}
_csrf
: 
"cf311952abef22898e59561b0f730a118d4aae4d893228c8192edc58863ba9aea:2:{i:0;s:5:\"_csrf\";i:1;s:32:\"Cy-WXJPmj8I3r9zdn4UaHAMs3GYK1oQ8\";}"
[[Prototype]]
: 
Object
loggerUtils.ts:235 (2025-05-23T18:51:19.365Z) [csrfServices.ts] Could not fetch CSRF token from backend Error: CSRF token cookie missing.
    at fetchCsrfToken (csrfServices.ts:62:9)
    at async csrfServices.ts:110:38
loggerUtils.ts:151 (2025-05-23T18:51:19.366Z) [All cookies via js-cookie:] 
{_csrf: 'cf311952abef22898e59561b0f730a118d4aae4d893228c819…rf";i:1;s:32:"Cy-WXJPmj8I3r9zdn4UaHAMs3GYK1oQ8";}'}
_csrf
: 
"cf311952abef22898e59561b0f730a118d4aae4d893228c8192edc58863ba9aea:2:{i:0;s:5:\"_csrf\";i:1;s:32:\"Cy-WXJPmj8I3r9zdn4UaHAMs3GYK1oQ8\";}"
[[Prototype]]
: 
Object

// src/services/csrfServices.ts
import Cookies from 'js-cookie';
import apiClient from './apiClientServices';
import { ENDPOINTS } from '../constants/apiConstants';
import logger from '../utils/loggerUtils';


/**
* CSRF Token manager class.
* Handles fetching token from backend, reading from cookie,
* caching token internally, and providing easy access.
*
* On app startup, call:
* 	
* 	await csrfTokenManager.init();
* 
* In your axios interceptor (or wherever you send API requests):
* 
* 	import { csrfTokenManager } from './csrfServices';
*
* 	apiClient.interceptors.request.use(config =>
* 	{
* 		const token = csrfTokenManager.getToken();
* 		if (token)
* 		{
* 			config.headers['X-CSRF-Token'] = token;
* 		}
* 		
* 		return config;
* 	});
*/
export const CSRF_COOKIE_NAME = 'csrfToken';

export type CsrfData =
{
	csrfParam: string;
	csrfToken: string;
};

/**
* Fetch the CSRF token and param from the backend endpoint.
* This triggers the backend to set the CSRF cookie.
* Throws an error if token or param are missing.
*/
export async function fetchCsrfToken(): Promise<CsrfData>
{
	const response = await apiClient.get(ENDPOINTS.security.csrfToken);
	const { csrfParam, csrfToken } = response.data;
	
	if (!csrfParam || !csrfToken)
	{
		throw new Error('Failed to fetch CSRF token');
	}
	
	// wait 50-100ms to allow cookie to become available
	await new Promise(resolve => setTimeout(resolve, 50));
	
	const cookieToken = getCsrfTokenFromCookie();
	
	if (!cookieToken)
	{
		throw new Error('CSRF token cookie missing.');
	}
	
	logger.log('csrfServices.ts', 'Token from backend response:', csrfToken);
	
	if (cookieToken.trim() !== csrfToken.trim())
	{
		throw new Error(`CSRF tokens mismatch. Cookie: ${cookieToken}, Backend: ${csrfToken}`);
	}
	
	logger.log('csrfServices.ts', 'Token from cookie:', cookieToken);
	
	return { csrfParam, csrfToken };
}

/**
* Read the CSRF token directly from the CSRF cookie.
* Returns undefined if cookie is missing.
*/
export function getCsrfTokenFromCookie(): string | undefined
{
	logger.log('All cookies via js-cookie:', Cookies.get());

	return Cookies.get(CSRF_COOKIE_NAME);
}

class CsrfTokenManager
{
	private csrfParam?: string;
	private csrfToken?: string;
	private initPromise?: Promise<void>;
	
	/**
	* Initialize the token manager by fetching token from backend,
	* and then synchronizing with the token in cookie.
	* Call once on app startup.
	*/
	async init(): Promise<void>
	{
		if (this.initPromise)
		{
			return this.initPromise;
		}
		
		this.initPromise = (async () =>
		{
			try
			{
				const { csrfParam, csrfToken } = await fetchCsrfToken();
			
				this.csrfParam = csrfParam;
				this.csrfToken = csrfToken;
			}
			catch (error)
			{
				logger.warn('csrfServices.ts', 'Could not fetch CSRF token from backend', error);
			}
			
			// Override with cookie token if available (more reliable/up-to-date)
			const cookieToken = getCsrfTokenFromCookie();
			
			if (cookieToken)
			{
				this.csrfToken = cookieToken;
			}
		})();
		
		return this.initPromise;
	}
	
	/**
	* Get the current CSRF token to be used for headers.
	* Prefers cookie token if it exists, else falls back to cached token.
	*/
	getToken(): string | undefined
	{
		// Prefer cookie token if available (more up to date)
		const cookieToken = getCsrfTokenFromCookie();
		
		if (cookieToken)
		{
			this.csrfToken = cookieToken;
			
			return cookieToken;
		}
		
		// Fall back to fetched token
		return this.csrfToken;
	}
	
	/**
	* Get the CSRF param name (cookie name / form param name).
	*/
	getParam(): string | undefined
	{
		return this.csrfParam;
	}
	
	/* Optional setter if you want to manually update tokens
	setToken(token: string, param?: string): void
	{
		this.csrfToken = token;
		
		if (param)
		{
			this.csrfParam = param;
		}
	}//*/
}

// Export a singleton CSRF token manager instance to be used app-wide.
export const csrfTokenManager = new CsrfTokenManager();