Feat: Modularize Keka HR integration with services
- Introduced `KekaAccessGuard`, `KekaConfigResolver`, `KekaIntegrationService`, `KekaTokenService`, and `KekaUrlBuilder` to streamline Keka HR functionality. - Refactored `KekaController` to delegate key operations to modular services, improving maintainability. - Updated dashboard UI to ensure alignment with the new service-based architecture. - Enhanced error handling, logging, and code reusability for Keka integration workflows.
This commit is contained in:
parent
b90dd5c092
commit
c4e2cfc4bb
@ -4,239 +4,35 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\{ConnectedApp, OauthToken};
|
use App\Services\Keka\{KekaAccessGuard, KekaIntegrationService};
|
||||||
use App\Services\UserAppAccessService;
|
use Illuminate\Http\{RedirectResponse, Request, Response};
|
||||||
use Illuminate\Http\{RedirectResponse, Request};
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
class KekaController extends Controller
|
class KekaController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
public function __construct(
|
||||||
* Step 1: Redirect the user to Keka's external login page.
|
private readonly KekaAccessGuard $accessGuard,
|
||||||
*/
|
private readonly KekaIntegrationService $kekaService,
|
||||||
|
) {}
|
||||||
|
|
||||||
public function connect(): RedirectResponse
|
public function connect(): RedirectResponse
|
||||||
{
|
{
|
||||||
$user = auth()->user();
|
$user = $this->accessGuard->authorize();
|
||||||
if (! $user) {
|
|
||||||
abort(403, 'Unauthorized');
|
|
||||||
}
|
|
||||||
|
|
||||||
$activeServices = app(UserAppAccessService::class)->getActiveServices($user);
|
return redirect($this->kekaService->getExternalLoginUrl($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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function callback(Request $request): Response|RedirectResponse
|
||||||
* Parse the configured Keka URL to build the correct external login URL.
|
|
||||||
*/
|
|
||||||
private function getKekaExternalLoginUrl(string $configuredUrl, string $provider): string
|
|
||||||
{
|
{
|
||||||
$parsed = parse_url($configuredUrl);
|
$user = $this->accessGuard->authorize();
|
||||||
$host = $parsed['host'] ?? 'login.keka.com';
|
|
||||||
|
|
||||||
$query = [];
|
return $this->kekaService->handleCallback($user, $request);
|
||||||
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 .= "<input type=\"hidden\" name=\"{$safeKey}\" value=\"{$safeValue}\" />\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
$html = <<<HTML
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Forwarding to Keka...</title>
|
|
||||||
</head>
|
|
||||||
<body onload="document.forms[0].submit()">
|
|
||||||
<form method="post" action="{$callbackUrl}">
|
|
||||||
{$inputs}
|
|
||||||
<noscript>
|
|
||||||
<p>JavaScript is required to connect to Keka. Click the button below to continue.</p>
|
|
||||||
<input type="submit" value="Continue" />
|
|
||||||
</noscript>
|
|
||||||
</form>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
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
|
public function disconnect(): RedirectResponse
|
||||||
{
|
{
|
||||||
$user = auth()->user();
|
$user = $this->accessGuard->authorize();
|
||||||
if (! $user) {
|
|
||||||
abort(403, 'Unauthorized');
|
|
||||||
}
|
|
||||||
|
|
||||||
$activeServices = app(UserAppAccessService::class)->getActiveServices($user);
|
$this->kekaService->disconnect($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.');
|
return redirect()->route('dashboard.user')->with('keka_success', 'Keka HR disconnected.');
|
||||||
}
|
}
|
||||||
|
|||||||
37
app/Services/Keka/KekaAccessGuard.php
Normal file
37
app/Services/Keka/KekaAccessGuard.php
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\Keka;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\UserAppAccessService;
|
||||||
|
|
||||||
|
class KekaAccessGuard
|
||||||
|
{
|
||||||
|
private const PROVIDER_SLUG = 'keka-hr';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly UserAppAccessService $userAppAccessService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function authorize(): User
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (! $user) {
|
||||||
|
abort(403, 'Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
$activeServices = $this->userAppAccessService->getActiveServices($user);
|
||||||
|
$hasAccess = $activeServices->contains(
|
||||||
|
fn ($service) => self::PROVIDER_SLUG === $service->provider_slug
|
||||||
|
);
|
||||||
|
|
||||||
|
if (! $hasAccess) {
|
||||||
|
abort(403, 'You do not have permission to access the required application.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
}
|
||||||
58
app/Services/Keka/KekaConfigResolver.php
Normal file
58
app/Services/Keka/KekaConfigResolver.php
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\Keka;
|
||||||
|
|
||||||
|
use App\Models\ConnectedApp;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
|
||||||
|
class KekaConfigResolver
|
||||||
|
{
|
||||||
|
private const PROVIDER_SLUG = 'keka-hr';
|
||||||
|
|
||||||
|
private const DEFAULT_KEKA_URL = 'https://sentientgeeks.keka.com';
|
||||||
|
|
||||||
|
private const DEFAULT_PROVIDER = 'Office365';
|
||||||
|
|
||||||
|
private const DEFAULT_CALLBACK_PATH = 'signin-oidc';
|
||||||
|
|
||||||
|
public function getApp(): ConnectedApp
|
||||||
|
{
|
||||||
|
$app = ConnectedApp::whereHas('provider', function ($query): void {
|
||||||
|
$query->where('slug', self::PROVIDER_SLUG);
|
||||||
|
})->first();
|
||||||
|
|
||||||
|
if (! $app) {
|
||||||
|
throw new NotFoundHttpException('Keka HR app registration not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $app;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getKekaUrl(ConnectedApp $app): string
|
||||||
|
{
|
||||||
|
return (string) (
|
||||||
|
data_get($app->settings, 'keka.keka_url')
|
||||||
|
?? data_get($app->settings, 'keka_url')
|
||||||
|
?? self::DEFAULT_KEKA_URL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProvider(ConnectedApp $app): string
|
||||||
|
{
|
||||||
|
return (string) (
|
||||||
|
data_get($app->settings, 'keka.provider')
|
||||||
|
?? data_get($app->settings, 'provider')
|
||||||
|
?? self::DEFAULT_PROVIDER
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCallbackPath(ConnectedApp $app): string
|
||||||
|
{
|
||||||
|
return (string) (
|
||||||
|
data_get($app->settings, 'keka.callback_path')
|
||||||
|
?? self::DEFAULT_CALLBACK_PATH
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
105
app/Services/Keka/KekaIntegrationService.php
Normal file
105
app/Services/Keka/KekaIntegrationService.php
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\Keka;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\{RedirectResponse, Request, Response};
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class KekaIntegrationService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly KekaConfigResolver $configResolver,
|
||||||
|
private readonly KekaUrlBuilder $urlBuilder,
|
||||||
|
private readonly KekaTokenService $tokenService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getExternalLoginUrl(User $user): string
|
||||||
|
{
|
||||||
|
$app = $this->configResolver->getApp();
|
||||||
|
|
||||||
|
$kekaUrl = $this->configResolver->getKekaUrl($app);
|
||||||
|
$provider = $this->configResolver->getProvider($app);
|
||||||
|
|
||||||
|
$redirectUrl = $this->urlBuilder->buildExternalLoginUrl($kekaUrl, $provider);
|
||||||
|
|
||||||
|
Log::info('Redirecting user to Keka external login page.', [
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'keka_url' => $kekaUrl,
|
||||||
|
'redirect_url' => $redirectUrl,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $redirectUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleCallback(User $user, Request $request): Response|RedirectResponse
|
||||||
|
{
|
||||||
|
$code = $request->input('code');
|
||||||
|
|
||||||
|
if (empty($code)) {
|
||||||
|
return redirect()
|
||||||
|
->route('dashboard.user')
|
||||||
|
->with('keka_error', 'Keka connection failed: Missing code.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$app = $this->configResolver->getApp();
|
||||||
|
$kekaUrl = $this->configResolver->getKekaUrl($app);
|
||||||
|
$callbackPath = $this->configResolver->getCallbackPath($app);
|
||||||
|
$callbackUrl = $this->urlBuilder->buildCallbackUrl($kekaUrl, $callbackPath);
|
||||||
|
|
||||||
|
$this->tokenService->store($user, (string) $code);
|
||||||
|
|
||||||
|
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')) {
|
||||||
|
return response($this->renderAutoSubmitForm($callbackUrl, $request->all()));
|
||||||
|
}
|
||||||
|
|
||||||
|
$queryString = http_build_query($request->query());
|
||||||
|
|
||||||
|
return redirect("{$callbackUrl}?{$queryString}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private function renderAutoSubmitForm(string $callbackUrl, array $inputsData): string
|
||||||
|
{
|
||||||
|
$inputs = '';
|
||||||
|
foreach ($inputsData as $key => $value) {
|
||||||
|
$safeKey = htmlspecialchars((string) $key, ENT_QUOTES, 'UTF-8');
|
||||||
|
$safeValue = htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||||
|
$inputs .= "<input type=\"hidden\" name=\"{$safeKey}\" value=\"{$safeValue}\" />\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return <<<HTML
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Forwarding to Keka...</title>
|
||||||
|
</head>
|
||||||
|
<body onload="document.forms[0].submit()">
|
||||||
|
<form method="post" action="{$callbackUrl}">
|
||||||
|
{$inputs}
|
||||||
|
<noscript>
|
||||||
|
<p>JavaScript is required to connect to Keka. Click the button below to continue.</p>
|
||||||
|
<input type="submit" value="Continue" />
|
||||||
|
</noscript>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function disconnect(User $user): void
|
||||||
|
{
|
||||||
|
$this->tokenService->revoke($user);
|
||||||
|
|
||||||
|
Log::info('Keka integration disconnected for user.', [
|
||||||
|
'user_id' => $user->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
48
app/Services/Keka/KekaTokenService.php
Normal file
48
app/Services/Keka/KekaTokenService.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\Keka;
|
||||||
|
|
||||||
|
use App\Models\{OauthToken, User};
|
||||||
|
|
||||||
|
class KekaTokenService
|
||||||
|
{
|
||||||
|
public function store(User $user, string $code): void
|
||||||
|
{
|
||||||
|
$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 = $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,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function revoke(User $user): void
|
||||||
|
{
|
||||||
|
$user->oauthToken('keka')?->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
59
app/Services/Keka/KekaUrlBuilder.php
Normal file
59
app/Services/Keka/KekaUrlBuilder.php
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\Keka;
|
||||||
|
|
||||||
|
class KekaUrlBuilder
|
||||||
|
{
|
||||||
|
private const DEFAULT_HOST = 'login.keka.com';
|
||||||
|
|
||||||
|
public function buildExternalLoginUrl(string $configuredUrl, string $provider): string
|
||||||
|
{
|
||||||
|
$parsed = parse_url($configuredUrl);
|
||||||
|
$host = $parsed['host'] ?? self::DEFAULT_HOST;
|
||||||
|
|
||||||
|
$query = [];
|
||||||
|
if (isset($parsed['query'])) {
|
||||||
|
parse_str($parsed['query'], $query);
|
||||||
|
}
|
||||||
|
|
||||||
|
$companyId = $query['companyId'] ?? $query['companyid'] ?? null;
|
||||||
|
|
||||||
|
if (! $companyId && self::DEFAULT_HOST !== $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://'.self::DEFAULT_HOST."/Account/ExternalLogin?{$queryString}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildCallbackUrl(string $configuredUrl, string $callbackPath): string
|
||||||
|
{
|
||||||
|
$parsed = parse_url($configuredUrl);
|
||||||
|
$host = $parsed['host'] ?? self::DEFAULT_HOST;
|
||||||
|
|
||||||
|
if (self::DEFAULT_HOST !== $host) {
|
||||||
|
$parts = explode('.', $host);
|
||||||
|
if (count($parts) >= 2 && 'keka' === $parts[1]) {
|
||||||
|
$host = self::DEFAULT_HOST;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scheme = $parsed['scheme'] ?? 'https';
|
||||||
|
|
||||||
|
return "{$scheme}://{$host}/".mb_ltrim($callbackPath, '/');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -201,6 +201,7 @@ class=" btn-error"
|
|||||||
</form>
|
</form>
|
||||||
@else
|
@else
|
||||||
<x-mary-button
|
<x-mary-button
|
||||||
|
external
|
||||||
no-wire-navigate
|
no-wire-navigate
|
||||||
icon="lucide.plug"
|
icon="lucide.plug"
|
||||||
:link="route('entra.connect')"
|
:link="route('entra.connect')"
|
||||||
@ -233,7 +234,6 @@ class="badge-soft badge-neutral text-[10px] font-semibold px-2 py-0.5">
|
|||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@if($this->isKekaConnected)
|
@if($this->isKekaConnected)
|
||||||
<x-mary-button
|
<x-mary-button
|
||||||
external
|
|
||||||
icon="lucide.external-link"
|
icon="lucide.external-link"
|
||||||
:link="route('keka.connect')"
|
:link="route('keka.connect')"
|
||||||
class=" btn-primary flex-1"
|
class=" btn-primary flex-1"
|
||||||
@ -253,6 +253,7 @@ class=" btn-error"
|
|||||||
</form>
|
</form>
|
||||||
@else
|
@else
|
||||||
<x-mary-button
|
<x-mary-button
|
||||||
|
external
|
||||||
no-wire-navigate
|
no-wire-navigate
|
||||||
icon="lucide.plug"
|
icon="lucide.plug"
|
||||||
:link="route('keka.connect')"
|
:link="route('keka.connect')"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user