user(); if (! $user) { abort(403, 'Unauthorized'); } $activeServices = app(UserAppAccessService::class)->getActiveServices($user); $hasKekaAccess = $activeServices->contains(fn ($service) => 'keka-hr' === $service->provider_slug); if (! $hasKekaAccess) { abort(403, 'You do not have permission to access the required application.'); } $app = ConnectedApp::whereHas('provider', function ($query): void { $query->where('slug', 'keka-hr'); })->first(); if (! $app) { abort(404, 'Keka HR app registration not found.'); } $kekaUrl = data_get($app->settings, 'keka.keka_url') ?? data_get($app->settings, 'keka_url') ?? 'https://sentientgeeks.keka.com'; $provider = data_get($app->settings, 'keka.provider') ?? data_get($app->settings, 'provider') ?? 'Office365'; $redirectUrl = $this->getKekaExternalLoginUrl((string) $kekaUrl, (string) $provider); Log::info('Redirecting user to Keka external login page.', [ 'user_id' => $user->id, 'keka_url' => $kekaUrl, 'redirect_url' => $redirectUrl, ]); return redirect($redirectUrl); } /** * Parse the configured Keka URL to build the correct external login URL. */ private function getKekaExternalLoginUrl(string $configuredUrl, string $provider): string { $parsed = parse_url($configuredUrl); $host = $parsed['host'] ?? 'login.keka.com'; $query = []; if (isset($parsed['query'])) { parse_str($parsed['query'], $query); } $companyId = $query['companyId'] ?? $query['companyid'] ?? null; if (! $companyId && 'login.keka.com' !== $host) { $parts = explode('.', $host); if (count($parts) >= 2) { $companyId = $parts[0]; } } $buildQuery = [ 'provider' => $provider, ]; // if ($companyId) { // $buildQuery['companyId'] = $companyId; // } $queryString = http_build_query($buildQuery); return "https://login.keka.com/Account/ExternalLogin?{$queryString}"; } /** * Step 2: Handle the Microsoft callback redirection. * We record/store the tokens in the database, and then pass the code/state to Keka. */ public function callback(Request $request) { $user = auth()->user(); if (! $user) { abort(403, 'Unauthorized'); } $activeServices = app(UserAppAccessService::class)->getActiveServices($user); $hasKekaAccess = $activeServices->contains(fn ($service) => 'keka-hr' === $service->provider_slug); if (! $hasKekaAccess) { abort(403, 'You do not have permission to access the required application.'); } $code = $request->input('code'); $state = $request->input('state'); if (empty($code)) { return redirect()->route('dashboard.user')->with('keka_error', 'Keka connection failed: Missing code.'); } // Retrieve Keka App configurations $app = ConnectedApp::whereHas('provider', function ($query): void { $query->where('slug', 'keka-hr'); })->first(); if (! $app) { abort(404, 'Keka HR app registration not found.'); } $kekaUrl = data_get($app->settings, 'keka.keka_url') ?? data_get($app->settings, 'keka_url') ?? 'https://sentientgeeks.keka.com'; $callbackPath = data_get($app->settings, 'keka.callback_path') ?? 'signin-oidc'; $callbackUrl = $this->getKekaCallbackUrl((string) $kekaUrl, (string) $callbackPath); // Store token details (Option B: copy user's existing Microsoft token or save code/placeholder) $microsoftToken = $user->oauthToken('microsoft'); if ($microsoftToken) { $accessToken = $microsoftToken->access_token; $refreshToken = $microsoftToken->refresh_token; $tokenType = $microsoftToken->token_type; $scopes = $microsoftToken->scopes; $expiresAt = $microsoftToken->expires_at; } else { $accessToken = (string) $code; $refreshToken = 'keka-placeholder-refresh'; $tokenType = 'Bearer'; $scopes = 'openid profile email'; $expiresAt = now()->addHour(); } OauthToken::updateOrCreate( [ 'user_id' => $user->id, 'provider' => 'keka', ], [ 'access_token' => $accessToken, 'refresh_token' => $refreshToken, 'token_type' => $tokenType, 'scopes' => $scopes, 'expires_at' => $expiresAt, ] ); Log::info('Keka tokens stored successfully. Passing OIDC response to Keka.', [ 'user_id' => $user->id, 'callback_url' => $callbackUrl, 'method' => $request->method(), ]); if ($request->isMethod('post')) { $inputs = ''; foreach ($request->all() as $key => $value) { $safeKey = htmlspecialchars((string) $key, ENT_QUOTES, 'UTF-8'); $safeValue = htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); $inputs .= "\n"; } $html = << Forwarding to Keka...
{$inputs}
HTML; return response($html); } $queryParams = $request->query(); $queryString = http_build_query($queryParams); return redirect("{$callbackUrl}?{$queryString}"); } /** * Parse the configured Keka URL to construct the callback URL. */ private function getKekaCallbackUrl(string $configuredUrl, string $callbackPath): string { $parsed = parse_url($configuredUrl); $host = $parsed['host'] ?? 'login.keka.com'; if ('login.keka.com' !== $host) { $parts = explode('.', $host); if (count($parts) >= 2 && 'keka' === $parts[1]) { $host = 'login.keka.com'; } } $scheme = $parsed['scheme'] ?? 'https'; return "{$scheme}://{$host}/".mb_ltrim($callbackPath, '/'); } /** * Disconnect/Revoke the Keka integration for the user. */ public function disconnect(): RedirectResponse { $user = auth()->user(); if (! $user) { abort(403, 'Unauthorized'); } $activeServices = app(UserAppAccessService::class)->getActiveServices($user); $hasKekaAccess = $activeServices->contains(fn ($service) => 'keka-hr' === $service->provider_slug); if (! $hasKekaAccess) { abort(403, 'You do not have permission to access the required application.'); } $user->oauthToken('keka')?->delete(); Log::info('Keka integration disconnected for user.', [ 'user_id' => $user->id, ]); return redirect()->route('dashboard.user')->with('keka_success', 'Keka HR disconnected.'); } }