update
This commit is contained in:
parent
688b0307b8
commit
cb86f8e362
81
.agents/skills/fluxui-development/SKILL.md
Normal file
81
.agents/skills/fluxui-development/SKILL.md
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
name: fluxui-development
|
||||||
|
description: "Use this skill for Flux UI development in Livewire applications only. Trigger when working with <flux:*> components, building or customizing Livewire component UIs, creating forms, modals, tables, or other interactive elements. Covers: flux: components (buttons, inputs, modals, forms, tables, date-pickers, kanban, badges, tooltips, etc.), component composition, Tailwind CSS styling, Heroicons/Lucide icon integration, validation patterns, responsive design, and theming. Do not use for non-Livewire frameworks or non-component styling."
|
||||||
|
license: MIT
|
||||||
|
metadata:
|
||||||
|
author: laravel
|
||||||
|
---
|
||||||
|
|
||||||
|
# Flux UI Development
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Use `search-docs` for detailed Flux UI patterns and documentation.
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
This project uses the free edition of Flux UI, which includes all free components and variants but not Pro components.
|
||||||
|
|
||||||
|
Flux UI is a component library for Livewire built with Tailwind CSS. It provides components that are easy to use and customize.
|
||||||
|
|
||||||
|
Use Flux UI components when available. Fall back to standard Blade components when no Flux component exists for your needs.
|
||||||
|
|
||||||
|
<!-- Basic Button -->
|
||||||
|
```blade
|
||||||
|
<flux:button variant="primary">Click me</flux:button>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Components (Free Edition)
|
||||||
|
|
||||||
|
Available: avatar, badge, brand, breadcrumbs, button, callout, card, checkbox, dropdown, field, heading, icon, input, modal, navbar, otp-input, pagination, profile, progress, radio, select, separator, skeleton, switch, table, text, textarea, toast, tooltip
|
||||||
|
|
||||||
|
## Icons
|
||||||
|
|
||||||
|
Flux includes [Heroicons](https://heroicons.com/) as its default icon set. Search for exact icon names on the Heroicons site - do not guess or invent icon names.
|
||||||
|
|
||||||
|
<!-- Icon Button -->
|
||||||
|
```blade
|
||||||
|
<flux:button icon="arrow-down-tray">Export</flux:button>
|
||||||
|
```
|
||||||
|
|
||||||
|
For icons not available in Heroicons, use [Lucide](https://lucide.dev/). Import the icons you need with the Artisan command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan flux:icon crown grip-vertical github
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Form Fields
|
||||||
|
|
||||||
|
<!-- Form Field -->
|
||||||
|
```blade
|
||||||
|
<flux:field>
|
||||||
|
<flux:label>Email</flux:label>
|
||||||
|
<flux:input type="email" wire:model="email" />
|
||||||
|
<flux:error name="email" />
|
||||||
|
</flux:field>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modals
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
```blade
|
||||||
|
<flux:modal wire:model="showModal">
|
||||||
|
<flux:heading>Title</flux:heading>
|
||||||
|
<p>Content</p>
|
||||||
|
</flux:modal>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
1. Check component renders correctly
|
||||||
|
2. Test interactive states
|
||||||
|
3. Verify mobile responsiveness
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
- Trying to use Pro-only components in the free edition
|
||||||
|
- Not checking if a Flux component exists before creating custom implementations
|
||||||
|
- Forgetting to use the `search-docs` tool for component-specific documentation
|
||||||
|
- Not following existing project patterns for Flux usage
|
||||||
@ -27,7 +27,7 @@ ## Basic Usage
|
|||||||
|
|
||||||
## Available Components (Free Edition)
|
## Available Components (Free Edition)
|
||||||
|
|
||||||
Available: avatar, badge, brand, breadcrumbs, button, callout, checkbox, dropdown, field, heading, icon, input, modal, navbar, otp-input, profile, radio, select, separator, skeleton, switch, text, textarea, tooltip
|
Available: avatar, badge, brand, breadcrumbs, button, callout, card, checkbox, dropdown, field, heading, icon, input, modal, navbar, otp-input, pagination, profile, progress, radio, select, separator, skeleton, switch, table, text, textarea, toast, tooltip
|
||||||
|
|
||||||
## Icons
|
## Icons
|
||||||
|
|
||||||
|
|||||||
2
.github/skills/fluxui-development/SKILL.md
vendored
2
.github/skills/fluxui-development/SKILL.md
vendored
@ -27,7 +27,7 @@ ## Basic Usage
|
|||||||
|
|
||||||
## Available Components (Free Edition)
|
## Available Components (Free Edition)
|
||||||
|
|
||||||
Available: avatar, badge, brand, breadcrumbs, button, callout, checkbox, dropdown, field, heading, icon, input, modal, navbar, otp-input, profile, radio, select, separator, skeleton, switch, text, textarea, tooltip
|
Available: avatar, badge, brand, breadcrumbs, button, callout, card, checkbox, dropdown, field, heading, icon, input, modal, navbar, otp-input, pagination, profile, progress, radio, select, separator, skeleton, switch, table, text, textarea, toast, tooltip
|
||||||
|
|
||||||
## Icons
|
## Icons
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ ## Foundational Context
|
|||||||
|
|
||||||
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
|
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
|
||||||
|
|
||||||
- php - 8.5
|
- php - 8.4
|
||||||
- laravel/fortify (FORTIFY) - v1
|
- laravel/fortify (FORTIFY) - v1
|
||||||
- laravel/framework (LARAVEL) - v13
|
- laravel/framework (LARAVEL) - v13
|
||||||
- laravel/prompts (PROMPTS) - v0
|
- laravel/prompts (PROMPTS) - v0
|
||||||
@ -87,7 +87,6 @@ ## Artisan
|
|||||||
- Run Artisan commands directly via the command line (e.g., `php artisan route:list`). Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters.
|
- Run Artisan commands directly via the command line (e.g., `php artisan route:list`). Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters.
|
||||||
- Inspect routes with `php artisan route:list`. Filter with: `--method=GET`, `--name=users`, `--path=api`, `--except-vendor`, `--only-vendor`.
|
- Inspect routes with `php artisan route:list`. Filter with: `--method=GET`, `--name=users`, `--path=api`, `--except-vendor`, `--only-vendor`.
|
||||||
- Read configuration values using dot notation: `php artisan config:show app.name`, `php artisan config:show database.default`. Or read config files directly from the `config/` directory.
|
- Read configuration values using dot notation: `php artisan config:show app.name`, `php artisan config:show database.default`. Or read config files directly from the `config/` directory.
|
||||||
- To check environment variables, read the `.env` file directly.
|
|
||||||
|
|
||||||
## Tinker
|
## Tinker
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ ## Foundational Context
|
|||||||
|
|
||||||
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
|
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
|
||||||
|
|
||||||
- php - 8.5
|
- php - 8.4
|
||||||
- laravel/fortify (FORTIFY) - v1
|
- laravel/fortify (FORTIFY) - v1
|
||||||
- laravel/framework (LARAVEL) - v13
|
- laravel/framework (LARAVEL) - v13
|
||||||
- laravel/prompts (PROMPTS) - v0
|
- laravel/prompts (PROMPTS) - v0
|
||||||
@ -87,7 +87,6 @@ ## Artisan
|
|||||||
- Run Artisan commands directly via the command line (e.g., `php artisan route:list`). Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters.
|
- Run Artisan commands directly via the command line (e.g., `php artisan route:list`). Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters.
|
||||||
- Inspect routes with `php artisan route:list`. Filter with: `--method=GET`, `--name=users`, `--path=api`, `--except-vendor`, `--only-vendor`.
|
- Inspect routes with `php artisan route:list`. Filter with: `--method=GET`, `--name=users`, `--path=api`, `--except-vendor`, `--only-vendor`.
|
||||||
- Read configuration values using dot notation: `php artisan config:show app.name`, `php artisan config:show database.default`. Or read config files directly from the `config/` directory.
|
- Read configuration values using dot notation: `php artisan config:show app.name`, `php artisan config:show database.default`. Or read config files directly from the `config/` directory.
|
||||||
- To check environment variables, read the `.env` file directly.
|
|
||||||
|
|
||||||
## Tinker
|
## Tinker
|
||||||
|
|
||||||
|
|||||||
101
app/Http/Controllers/KekaOutlookController.php
Normal file
101
app/Http/Controllers/KekaOutlookController.php
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Services\KekaOutlookService;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
final class KekaOutlookController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private KekaOutlookService $kekaOutlook) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /keka/connect
|
||||||
|
*
|
||||||
|
* If the user already has a stored refresh token, we don't show
|
||||||
|
* Microsoft's login screen at all — just confirm/refresh silently
|
||||||
|
* and send them straight back to Keka.
|
||||||
|
*
|
||||||
|
* If not connected yet, redirect to Microsoft login (one-time).
|
||||||
|
*/
|
||||||
|
public function connect(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
if ($this->kekaOutlook->isConnected($user)) {
|
||||||
|
try {
|
||||||
|
// Ensures token is fresh; throws if refresh token is invalid/revoked.
|
||||||
|
$this->kekaOutlook->getValidToken($user);
|
||||||
|
|
||||||
|
return redirect()->away($this->kekaWebsiteUrl());
|
||||||
|
} catch (\RuntimeException $e) {
|
||||||
|
// Refresh failed -> fall through to re-login below.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->away($this->kekaOutlook->getAuthUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /keka/callback
|
||||||
|
*
|
||||||
|
* Microsoft redirects here after login with ?code=... or ?error=...
|
||||||
|
*/
|
||||||
|
public function callback(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
if ($request->filled('error')) {
|
||||||
|
return redirect()
|
||||||
|
->route('dashboard')
|
||||||
|
->with('error', 'Microsoft login was cancelled or failed: '.$request->get('error_description', $request->get('error')));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $this->kekaOutlook->isValidState($request->get('state'))) {
|
||||||
|
return redirect()
|
||||||
|
->route('dashboard')
|
||||||
|
->with('error', 'Invalid login session. Please try connecting again.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = $request->get('code');
|
||||||
|
|
||||||
|
if (! $code) {
|
||||||
|
return redirect()
|
||||||
|
->route('dashboard')
|
||||||
|
->with('error', 'No authorization code received from Microsoft.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->kekaOutlook->handleCallback($request->user(), $code);
|
||||||
|
|
||||||
|
if (! $result['success']) {
|
||||||
|
return redirect()
|
||||||
|
->route('dashboard')
|
||||||
|
->with('error', 'Failed to connect: '.($result['message'] ?? 'Unknown error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->away($this->kekaWebsiteUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /keka/disconnect
|
||||||
|
*
|
||||||
|
* Wipes stored tokens. User must go through Microsoft login again next time.
|
||||||
|
*/
|
||||||
|
public function disconnect(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->kekaOutlook->disconnect($request->user());
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('dashboard')
|
||||||
|
->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -59,6 +59,7 @@ protected function casts(): array
|
|||||||
return [
|
return [
|
||||||
'email_verified_at' => 'datetime',
|
'email_verified_at' => 'datetime',
|
||||||
'password' => 'hashed',
|
'password' => 'hashed',
|
||||||
|
'keka_ms_token_expires_at' => 'datetime',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
224
app/Services/KekaOutlookService.php
Normal file
224
app/Services/KekaOutlookService.php
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles Microsoft OAuth (delegated, authorization code flow) for Keka-Outlook integration.
|
||||||
|
*
|
||||||
|
* Flow:
|
||||||
|
* - connect() -> build Microsoft login URL
|
||||||
|
* - handleCallback() -> exchange code for tokens, store on user
|
||||||
|
* - getValidToken() -> return a valid access token, refreshing silently if expired
|
||||||
|
* - disconnect() -> wipe stored tokens for the user
|
||||||
|
*/
|
||||||
|
final class KekaOutlookService
|
||||||
|
{
|
||||||
|
private string $clientId;
|
||||||
|
|
||||||
|
private string $clientSecret;
|
||||||
|
|
||||||
|
private string $tenantId;
|
||||||
|
|
||||||
|
private string $redirectUri;
|
||||||
|
|
||||||
|
private array $scopes;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->clientId = (string) config('microsoft.client_id');
|
||||||
|
$this->clientSecret = (string) config('microsoft.client_secret');
|
||||||
|
$this->tenantId = (string) config('microsoft.tenant_id');
|
||||||
|
$this->redirectUri = (string) config('microsoft.keka_redirect_uri');
|
||||||
|
$this->scopes = config('microsoft.keka_scopes', [
|
||||||
|
'openid',
|
||||||
|
'profile',
|
||||||
|
'email',
|
||||||
|
'offline_access',
|
||||||
|
'User.Read',
|
||||||
|
'Mail.Read',
|
||||||
|
'Mail.ReadWrite',
|
||||||
|
'Mail.Send',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the Microsoft login URL the user is redirected to.
|
||||||
|
* A random "state" value is generated and stored in the session to prevent CSRF.
|
||||||
|
*/
|
||||||
|
public function getAuthUrl(): string
|
||||||
|
{
|
||||||
|
$state = Str::random(40);
|
||||||
|
session(['keka_ms_oauth_state' => $state]);
|
||||||
|
|
||||||
|
$query = http_build_query([
|
||||||
|
'client_id' => $this->clientId,
|
||||||
|
'response_type' => 'code',
|
||||||
|
'redirect_uri' => $this->redirectUri,
|
||||||
|
'response_mode' => 'query',
|
||||||
|
'scope' => implode(' ', $this->scopes),
|
||||||
|
'state' => $state,
|
||||||
|
// prompt=select_account forces account chooser on first connect.
|
||||||
|
'prompt' => 'select_account',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return "https://login.microsoftonline.com/{$this->tenantId}/oauth2/v2.0/authorize?{$query}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the "state" returned by Microsoft against the one stored in session.
|
||||||
|
*/
|
||||||
|
public function isValidState(?string $state): bool
|
||||||
|
{
|
||||||
|
$expected = session('keka_ms_oauth_state');
|
||||||
|
|
||||||
|
if (! $expected || ! $state) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash_equals($expected, $state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exchange the authorization code for access + refresh tokens and store them on the user.
|
||||||
|
*
|
||||||
|
* @return array{success: bool, message?: string}
|
||||||
|
*/
|
||||||
|
public function handleCallback(User $user, string $code): array
|
||||||
|
{
|
||||||
|
$response = Http::asForm()->post(
|
||||||
|
"https://login.microsoftonline.com/{$this->tenantId}/oauth2/v2.0/token",
|
||||||
|
[
|
||||||
|
'client_id' => $this->clientId,
|
||||||
|
'client_secret' => $this->clientSecret,
|
||||||
|
'grant_type' => 'authorization_code',
|
||||||
|
'code' => $code,
|
||||||
|
'redirect_uri' => $this->redirectUri,
|
||||||
|
'scope' => implode(' ', $this->scopes),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (! $response->successful()) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => $response->json('error_description', 'Failed to exchange code for token.'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $response->json();
|
||||||
|
|
||||||
|
$this->storeTokens($user, $data);
|
||||||
|
|
||||||
|
// Fetch the connected mailbox's email address for display purposes.
|
||||||
|
$profile = Http::withToken($data['access_token'])
|
||||||
|
->get('https://graph.microsoft.com/v1.0/me');
|
||||||
|
|
||||||
|
if ($profile->successful()) {
|
||||||
|
$user->keka_ms_email = $profile->json('mail') ?? $profile->json('userPrincipalName');
|
||||||
|
$user->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
session()->forget('keka_ms_oauth_state');
|
||||||
|
|
||||||
|
return ['success' => true];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist tokens + expiry on the user model.
|
||||||
|
*/
|
||||||
|
private function storeTokens(User $user, array $tokenData): void
|
||||||
|
{
|
||||||
|
$user->keka_ms_access_token = $tokenData['access_token'];
|
||||||
|
$user->keka_ms_token_expires_at = now()->addSeconds((int) $tokenData['expires_in']);
|
||||||
|
|
||||||
|
// Microsoft only returns refresh_token if 'offline_access' scope was granted.
|
||||||
|
// If a new one isn't returned on refresh, keep the existing one.
|
||||||
|
if (! empty($tokenData['refresh_token'])) {
|
||||||
|
$user->keka_ms_refresh_token = $tokenData['refresh_token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the user has connected Keka/Outlook (has a refresh token stored).
|
||||||
|
*/
|
||||||
|
public function isConnected(User $user): bool
|
||||||
|
{
|
||||||
|
return ! empty($user->keka_ms_refresh_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a valid access token for the user.
|
||||||
|
* Silently refreshes via refresh_token if the current access token has expired.
|
||||||
|
*
|
||||||
|
* @throws \RuntimeException if the user has not connected or refresh fails.
|
||||||
|
*/
|
||||||
|
public function getValidToken(User $user): string
|
||||||
|
{
|
||||||
|
if (! $this->isConnected($user)) {
|
||||||
|
throw new \RuntimeException('User has not connected Keka/Outlook.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Still valid? (1 minute buffer)
|
||||||
|
if (
|
||||||
|
$user->keka_ms_access_token
|
||||||
|
&& $user->keka_ms_token_expires_at
|
||||||
|
&& now()->lt($user->keka_ms_token_expires_at->subMinute())
|
||||||
|
) {
|
||||||
|
return $user->keka_ms_access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->refreshToken($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the stored refresh_token to obtain a new access_token silently.
|
||||||
|
*
|
||||||
|
* @throws \RuntimeException if Microsoft rejects the refresh token (user must reconnect).
|
||||||
|
*/
|
||||||
|
private function refreshToken(User $user): string
|
||||||
|
{
|
||||||
|
$response = Http::asForm()->post(
|
||||||
|
"https://login.microsoftonline.com/{$this->tenantId}/oauth2/v2.0/token",
|
||||||
|
[
|
||||||
|
'client_id' => $this->clientId,
|
||||||
|
'client_secret' => $this->clientSecret,
|
||||||
|
'grant_type' => 'refresh_token',
|
||||||
|
'refresh_token' => $user->keka_ms_refresh_token,
|
||||||
|
'scope' => implode(' ', $this->scopes),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (! $response->successful()) {
|
||||||
|
// Refresh token expired/revoked -> force user to reconnect.
|
||||||
|
$this->disconnect($user);
|
||||||
|
|
||||||
|
throw new \RuntimeException(
|
||||||
|
$response->json('error_description', 'Session expired. Please reconnect Keka.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $response->json();
|
||||||
|
$this->storeTokens($user, $data);
|
||||||
|
|
||||||
|
return $data['access_token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect: wipe stored tokens. User must go through login flow again to reconnect.
|
||||||
|
*/
|
||||||
|
public function disconnect(User $user): void
|
||||||
|
{
|
||||||
|
$user->keka_ms_access_token = null;
|
||||||
|
$user->keka_ms_refresh_token = null;
|
||||||
|
$user->keka_ms_token_expires_at = null;
|
||||||
|
$user->keka_ms_email = null;
|
||||||
|
$user->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,9 @@
|
|||||||
{
|
{
|
||||||
"agents": ["claude_code", "copilot", "opencode"],
|
"agents": [
|
||||||
|
"claude_code",
|
||||||
|
"copilot",
|
||||||
|
"opencode"
|
||||||
|
],
|
||||||
"cloud": false,
|
"cloud": false,
|
||||||
"guidelines": true,
|
"guidelines": true,
|
||||||
"mcp": true,
|
"mcp": true,
|
||||||
@ -8,6 +12,7 @@
|
|||||||
"skills": [
|
"skills": [
|
||||||
"fortify-development",
|
"fortify-development",
|
||||||
"laravel-best-practices",
|
"laravel-best-practices",
|
||||||
|
"fluxui-development",
|
||||||
"livewire-development",
|
"livewire-development",
|
||||||
"pest-testing",
|
"pest-testing",
|
||||||
"tailwindcss-development"
|
"tailwindcss-development"
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.5",
|
"php": "^8.3",
|
||||||
"gehrisandro/tailwind-merge-laravel": "^1.4",
|
"gehrisandro/tailwind-merge-laravel": "^1.4",
|
||||||
"laravel/fortify": "^1.34",
|
"laravel/fortify": "^1.34",
|
||||||
"laravel/framework": "^13.7",
|
"laravel/framework": "^13.7",
|
||||||
|
|||||||
1131
composer.lock
generated
1131
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -125,4 +125,21 @@
|
|||||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// Redirect URI registered in Azure: http://localhost:8000/keka/callback
|
||||||
|
'keka_redirect_uri' => env('KEKA_MS_REDIRECT_URI', 'http://localhost:8000/keka/callback'),
|
||||||
|
|
||||||
|
// Where to send the user after a successful (or silent) connection.
|
||||||
|
'keka_website_url' => env('KEKA_WEBSITE_URL', 'https://app.keka.com'),
|
||||||
|
|
||||||
|
'keka_scopes' => [
|
||||||
|
'openid',
|
||||||
|
'profile',
|
||||||
|
'email',
|
||||||
|
'offline_access', // REQUIRED for refresh_token / silent reconnect
|
||||||
|
'User.Read',
|
||||||
|
'Mail.Read',
|
||||||
|
'Mail.ReadWrite',
|
||||||
|
'Mail.Send',
|
||||||
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
16
config/microsoft.php
Normal file
16
config/microsoft.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'client_id' => env('MICROSOFT_GRAPH_CLIENT_ID'),
|
||||||
|
'client_secret' => env('MICROSOFT_GRAPH_CLIENT_SECRET'),
|
||||||
|
'tenant_id' => env('MICROSOFT_GRAPH_TENANT_ID'),
|
||||||
|
'keka_redirect_uri' => env('KEKA_MS_REDIRECT_URI', 'http://localhost:8000/keka/callback'),
|
||||||
|
'keka_scopes' => ['openid',
|
||||||
|
'profile',
|
||||||
|
'email',
|
||||||
|
'offline_access',
|
||||||
|
'User.Read',
|
||||||
|
'Mail.Read',
|
||||||
|
'Mail.ReadWrite',
|
||||||
|
'Mail.Send']
|
||||||
|
];
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
<?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->text('keka_ms_access_token')->nullable();
|
||||||
|
$table->text('keka_ms_refresh_token')->nullable();
|
||||||
|
$table->timestamp('keka_ms_token_expires_at')->nullable();
|
||||||
|
$table->string('keka_ms_email')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn([
|
||||||
|
'keka_ms_access_token',
|
||||||
|
'keka_ms_refresh_token',
|
||||||
|
'keka_ms_token_expires_at',
|
||||||
|
'keka_ms_email',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
37
resources/views/test/keka.blade.php
Normal file
37
resources/views/test/keka.blade.php
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Keka</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
|
|
||||||
|
<div class="bg-white rounded-2xl shadow-lg p-10 w-full max-w-md text-center space-y-6">
|
||||||
|
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="w-14 h-14 rounded-xl flex items-center justify-center" style="background:#FF6C37">
|
||||||
|
<span class="text-white font-bold text-2xl">K</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-gray-800">Keka HR</h1>
|
||||||
|
<p class="text-gray-500 text-sm mt-1">Open Keka via Microsoft SSO</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{{ route('keka.open') }}" target="_blank" rel="noopener"
|
||||||
|
class="flex items-center justify-center gap-2 w-full text-white font-semibold py-3 px-6 rounded-xl transition duration-150"
|
||||||
|
style="background:#FF6C37">
|
||||||
|
Open Keka
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<p class="text-xs text-gray-400">
|
||||||
|
Opens Keka in a new tab. If you're already signed into Microsoft in this browser, you'll be logged in automatically.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -12,6 +12,7 @@
|
|||||||
use App\Http\Controllers\{ResolveDashboardRouteController, SamlIdpController};
|
use App\Http\Controllers\{ResolveDashboardRouteController, SamlIdpController};
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Spatie\Permission\Middleware\{PermissionMiddleware, RoleMiddleware};
|
use Spatie\Permission\Middleware\{PermissionMiddleware, RoleMiddleware};
|
||||||
|
use App\Http\Controllers\KekaOutlookController;
|
||||||
|
|
||||||
Route::redirect('/', '/dashboard')->name('home');
|
Route::redirect('/', '/dashboard')->name('home');
|
||||||
|
|
||||||
@ -94,6 +95,14 @@
|
|||||||
|
|
||||||
// Disconnect / revoke saved token
|
// Disconnect / revoke saved token
|
||||||
Route::delete('disconnect', [EntraController::class, 'disconnect'])->name('disconnect');
|
Route::delete('disconnect', [EntraController::class, 'disconnect'])->name('disconnect');
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::middleware(['auth', 'verified'])->prefix('keka')->name('keka.')->group(function (): void {
|
||||||
|
Route::get('/connect', [KekaOutlookController::class, 'connect'])->name('connect');
|
||||||
|
Route::get('/callback', [KekaOutlookController::class, 'callback'])->name('callback');
|
||||||
|
Route::post('/disconnect', [KekaOutlookController::class, 'disconnect'])->name('disconnect');
|
||||||
});
|
});
|
||||||
|
|
||||||
require __DIR__.'/settings.php';
|
require __DIR__.'/settings.php';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user