keka intregation
This commit is contained in:
parent
cb86f8e362
commit
5ebda68b68
@ -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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -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',
|
|
||||||
|
// After first-time login: Keka's own Microsoft SSO URL
|
||||||
|
// After already connected: Keka dashboard
|
||||||
|
'keka_login_url' => env('KEKA_LOGIN_URL', 'https://sentientgeeks.keka.com/'),
|
||||||
|
'keka_dashboard_url'=> env('KEKA_DASHBOARD_URL', 'https://sentientgeeks.keka.com/'),
|
||||||
|
|
||||||
|
'keka_scopes' => [
|
||||||
|
'openid',
|
||||||
'profile',
|
'profile',
|
||||||
'email',
|
'email',
|
||||||
'offline_access',
|
'offline_access',
|
||||||
'User.Read',
|
'User.Read',
|
||||||
'Mail.Read',
|
'Mail.Read',
|
||||||
'Mail.ReadWrite',
|
'Mail.ReadWrite',
|
||||||
'Mail.Send']
|
'Mail.Send',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user