keka intregation

This commit is contained in:
subhajit 2026-06-15 13:25:39 +05:30
parent cb86f8e362
commit 5ebda68b68
3 changed files with 69 additions and 45 deletions

View File

@ -10,16 +10,19 @@
final class KekaOutlookController extends Controller final class KekaOutlookController extends Controller
{ {
public function __construct(private KekaOutlookService $kekaOutlook) {} public function __construct(private readonly KekaOutlookService $kekaOutlook) {}
/** /**
* GET /keka/connect * GET /keka/connect
* *
* If the user already has a stored refresh token, we don't show * FIRST TIME:
* Microsoft's login screen at all just confirm/refresh silently * Your Microsoft OAuth (stores token in oauth_tokens)
* and send them straight back to Keka. * callback stores token redirect to sentientgeeks.keka.com
* (Keka's SSO kicks in automatically since Microsoft session exists)
* *
* If not connected yet, redirect to Microsoft login (one-time). * ALREADY CONNECTED:
* Silent token refresh if near expiry
* Straight to sentientgeeks.keka.com
*/ */
public function connect(Request $request): RedirectResponse public function connect(Request $request): RedirectResponse
{ {
@ -27,75 +30,64 @@ public function connect(Request $request): RedirectResponse
if ($this->kekaOutlook->isConnected($user)) { if ($this->kekaOutlook->isConnected($user)) {
try { try {
// Ensures token is fresh; throws if refresh token is invalid/revoked.
$this->kekaOutlook->getValidToken($user); $this->kekaOutlook->getValidToken($user);
} catch (\RuntimeException) {
return redirect()->away($this->kekaWebsiteUrl()); // Token fully expired/revoked → re-authenticate
} catch (\RuntimeException $e) { return redirect()->away($this->kekaOutlook->getAuthUrl());
// Refresh failed -> fall through to re-login below.
} }
// Already connected and token valid → go to Keka
return redirect()->away((string) config('microsoft.keka_dashboard_url'));
} }
// Not connected yet → your Microsoft OAuth first
return redirect()->away($this->kekaOutlook->getAuthUrl()); return redirect()->away($this->kekaOutlook->getAuthUrl());
} }
/** /**
* GET /keka/callback * GET /keka/callback
* *
* Microsoft redirects here after login with ?code=... or ?error=... * Microsoft posts auth code here store token redirect to Keka.
* Keka's own SSO will auto-login since Microsoft session is now active.
*/ */
public function callback(Request $request): RedirectResponse public function callback(Request $request): RedirectResponse
{ {
if ($request->filled('error')) { if ($request->filled('error')) {
return redirect() return redirect()->route('dashboard')
->route('dashboard') ->with('error', 'Microsoft login failed: ' . $request->get('error_description', $request->get('error')));
->with('error', 'Microsoft login was cancelled or failed: '.$request->get('error_description', $request->get('error')));
} }
if (! $this->kekaOutlook->isValidState($request->get('state'))) { if (! $this->kekaOutlook->isValidState($request->get('state'))) {
return redirect() return redirect()->route('dashboard')
->route('dashboard') ->with('error', 'Invalid session state. Please try connecting again.');
->with('error', 'Invalid login session. Please try connecting again.');
} }
$code = $request->get('code'); $code = $request->get('code');
if (! $code) { if (! $code) {
return redirect() return redirect()->route('dashboard')
->route('dashboard') ->with('error', 'No authorization code received.');
->with('error', 'No authorization code received from Microsoft.');
} }
$result = $this->kekaOutlook->handleCallback($request->user(), $code); $result = $this->kekaOutlook->handleCallback($request->user(), $code);
if (! $result['success']) { if (! $result['success']) {
return redirect() return redirect()->route('dashboard')
->route('dashboard') ->with('error', 'Connection failed: ' . ($result['message'] ?? 'Unknown error'));
->with('error', 'Failed to connect: '.($result['message'] ?? 'Unknown error'));
} }
return redirect()->away($this->kekaWebsiteUrl()); // Token stored ✓ → Keka SSO will auto-login via active Microsoft session
return redirect()->away((string) config('microsoft.keka_dashboard_url'));
} }
/** /**
* POST /keka/disconnect * POST /keka/disconnect
*
* Wipes stored tokens. User must go through Microsoft login again next time.
*/ */
public function disconnect(Request $request): RedirectResponse public function disconnect(Request $request): RedirectResponse
{ {
$this->kekaOutlook->disconnect($request->user()); $this->kekaOutlook->disconnect($request->user());
return redirect() return redirect()->route('dashboard')
->route('dashboard')
->with('success', 'Keka has been disconnected.'); ->with('success', 'Keka has been disconnected.');
} }
/**
* The Keka website URL the user lands on after a successful (silent) connection.
*/
private function kekaWebsiteUrl(): string
{
return (string) config('microsoft.keka_website_url', 'https://app.keka.com');
}
} }

View File

@ -4,13 +4,23 @@
'client_id' => env('MICROSOFT_GRAPH_CLIENT_ID'), 'client_id' => env('MICROSOFT_GRAPH_CLIENT_ID'),
'client_secret' => env('MICROSOFT_GRAPH_CLIENT_SECRET'), 'client_secret' => env('MICROSOFT_GRAPH_CLIENT_SECRET'),
'tenant_id' => env('MICROSOFT_GRAPH_TENANT_ID'), 'tenant_id' => env('MICROSOFT_GRAPH_TENANT_ID'),
// Your app's redirect URI for Keka OAuth flow
'keka_redirect_uri' => env('KEKA_MS_REDIRECT_URI', 'http://localhost:8000/keka/callback'), 'keka_redirect_uri' => env('KEKA_MS_REDIRECT_URI', 'http://localhost:8000/keka/callback'),
'keka_scopes' => ['openid',
'profile', // After first-time login: Keka's own Microsoft SSO URL
'email', // After already connected: Keka dashboard
'offline_access', 'keka_login_url' => env('KEKA_LOGIN_URL', 'https://sentientgeeks.keka.com/'),
'User.Read', 'keka_dashboard_url'=> env('KEKA_DASHBOARD_URL', 'https://sentientgeeks.keka.com/'),
'Mail.Read',
'Mail.ReadWrite', 'keka_scopes' => [
'Mail.Send'] 'openid',
'profile',
'email',
'offline_access',
'User.Read',
'Mail.Read',
'Mail.ReadWrite',
'Mail.Send',
],
]; ];

View File

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('keka_connected')->default(false);
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('keka_connected');
});
}
};