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
|
||||
@ -148,4 +148,4 @@ ## Key Endpoints
|
||||
| Passkey Confirm | POST | `/passkeys/confirm` |
|
||||
| Passkey Options | GET | `/user/passkeys/options` |
|
||||
| Register Passkey | POST | `/user/passkeys` |
|
||||
| Delete Passkey | DELETE | `/user/passkeys/{passkey}` |
|
||||
| Delete Passkey | DELETE | `/user/passkeys/{passkey}` |
|
||||
|
||||
@ -187,4 +187,4 @@ ## How to Apply
|
||||
|
||||
1. Identify the file type and select relevant sections (e.g., migration → §16, controller → §1, §3, §5, §6, §10)
|
||||
2. Check sibling files for existing patterns — follow those first per Consistency First
|
||||
3. Verify API syntax with `search-docs` for the installed Laravel version
|
||||
3. Verify API syntax with `search-docs` for the installed Laravel version
|
||||
|
||||
@ -103,4 +103,4 @@ ## Use Correlated Subqueries for Has-Many Ordering
|
||||
->take(1)
|
||||
);
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@ -199,4 +199,4 @@ ## Convention Over Configuration
|
||||
return $this->belongsToMany(Role::class);
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@ -33,4 +33,4 @@ ## Use Blade Fragments for Partial Re-Renders (htmx/Turbo)
|
||||
|
||||
## Use `@aware` for Deeply Nested Component Props
|
||||
|
||||
Avoids re-passing parent props through every level of nested components.
|
||||
Avoids re-passing parent props through every level of nested components.
|
||||
|
||||
@ -67,4 +67,4 @@ ## Configure Failover Cache Stores in Production
|
||||
|
||||
```php
|
||||
'failover' => ['driver' => 'failover', 'stores' => ['redis', 'database']],
|
||||
```
|
||||
```
|
||||
|
||||
@ -41,4 +41,4 @@ ## Use `#[CollectedBy]` for Custom Collection Classes
|
||||
```php
|
||||
#[CollectedBy(UserCollection::class)]
|
||||
class User extends Model {}
|
||||
```
|
||||
```
|
||||
|
||||
@ -70,4 +70,4 @@ ## Use Constants and Language Files
|
||||
```php
|
||||
// Only when lang files already exist in the project
|
||||
return back()->with('message', __('app.article_added'));
|
||||
```
|
||||
```
|
||||
|
||||
@ -189,4 +189,4 @@ ## No Queries in Blade Templates
|
||||
@foreach ($users as $user)
|
||||
{{ $user->profile->name }}
|
||||
@endforeach
|
||||
```
|
||||
```
|
||||
|
||||
@ -145,4 +145,4 @@ ## Avoid Hardcoded Table Names in Queries
|
||||
|
||||
Prefer Eloquent queries and relationships over `DB::table()` whenever possible — they already reference the model's table. When `DB::table()` or raw joins are unavoidable, always use `(new Model)->getTable()` to keep the reference traceable.
|
||||
|
||||
**Exception — migrations:** In migrations, hardcoded table names via `DB::table('settings')` are acceptable and preferred. Models change over time but migrations are frozen snapshots — referencing a model that is later renamed or deleted would break the migration.
|
||||
**Exception — migrations:** In migrations, hardcoded table names via `DB::table('settings')` are acceptable and preferred. Models change over time but migrations are frozen snapshots — referencing a model that is later renamed or deleted would break the migration.
|
||||
|
||||
@ -69,4 +69,4 @@ ## Add Context to Exception Classes
|
||||
return ['order_id' => $this->orderId];
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@ -49,4 +49,4 @@ ## Use On-Demand Notifications for Non-User Recipients
|
||||
|
||||
## Implement `HasLocalePreference` on Notifiable Models
|
||||
|
||||
Laravel automatically uses the user's preferred locale for all notifications and mailables — no per-call `locale()` needed.
|
||||
Laravel automatically uses the user's preferred locale for all notifications and mailables — no per-call `locale()` needed.
|
||||
|
||||
@ -157,4 +157,4 @@ ## Fake HTTP Calls in Tests
|
||||
Http::fake([
|
||||
'api.example.com/*' => Http::failedConnection(),
|
||||
]);
|
||||
```
|
||||
```
|
||||
|
||||
@ -24,4 +24,4 @@ ## Separate Content Tests from Sending Tests
|
||||
|
||||
Content tests: instantiate the mailable directly, call `assertSeeInHtml()`.
|
||||
Sending tests: use `Mail::fake()` and `assertSent()`/`assertQueued()`.
|
||||
Don't mix them — it conflates concerns and makes tests brittle.
|
||||
Don't mix them — it conflates concerns and makes tests brittle.
|
||||
|
||||
@ -118,4 +118,4 @@ ## Keep Migrations Focused
|
||||
|
||||
// Migration 2: seed_default_settings
|
||||
DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']);
|
||||
```
|
||||
```
|
||||
|
||||
@ -141,4 +141,4 @@ ## Use Horizon for Complex Queue Scenarios
|
||||
],
|
||||
],
|
||||
],
|
||||
```
|
||||
```
|
||||
|
||||
@ -96,4 +96,4 @@ ## Type-Hint Form Requests
|
||||
|
||||
return redirect()->route('posts.index');
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@ -36,4 +36,4 @@ ## Use Schedule Groups for Shared Configuration
|
||||
Schedule::command('emails:send --force');
|
||||
Schedule::command('emails:prune');
|
||||
});
|
||||
```
|
||||
```
|
||||
|
||||
@ -195,4 +195,4 @@ ## Encrypt Sensitive Database Fields
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@ -122,4 +122,4 @@ ## No Unnecessary Comments
|
||||
Correct:
|
||||
```php
|
||||
if ($this->hasJoins())
|
||||
```
|
||||
```
|
||||
|
||||
@ -40,4 +40,4 @@ ## Use `recycle()` to Share Relationship Instances Across Factories
|
||||
Ticket::factory()
|
||||
->recycle(Airline::factory()->create())
|
||||
->create();
|
||||
```
|
||||
```
|
||||
|
||||
@ -72,4 +72,4 @@ ## Use the `after()` Method for Custom Validation
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@ -172,4 +172,4 @@ ## Common Pitfalls
|
||||
- Expecting `wire:model` real-time → use `wire:model.live`
|
||||
- Unclosed component tags → syntax errors in v4
|
||||
- Using deprecated config keys or JS hooks
|
||||
- Including Alpine.js separately (already bundled in Livewire 4)
|
||||
- Including Alpine.js separately (already bundled in Livewire 4)
|
||||
|
||||
@ -36,4 +36,4 @@ ### Component-Scoped Interceptors
|
||||
## Magic Properties
|
||||
|
||||
- `$errors` - Access validation errors from JavaScript
|
||||
- `$intercept` - Component-scoped interceptors
|
||||
- `$intercept` - Component-scoped interceptors
|
||||
|
||||
@ -163,4 +163,4 @@ ## Common Pitfalls
|
||||
- Forgetting datasets for repetitive validation tests
|
||||
- Deleting tests without approval
|
||||
- Forgetting `assertNoJavaScriptErrors()` in browser tests
|
||||
- Prefixing `Feature/` or `Unit/` in `{name}` when using `make:test`
|
||||
- Prefixing `Feature/` or `Unit/` in `{name}` when using `make:test`
|
||||
|
||||
@ -116,4 +116,4 @@ ## Common Pitfalls
|
||||
- Using `@tailwind` directives instead of `@import "tailwindcss"`
|
||||
- Trying to use `tailwind.config.js` instead of CSS `@theme` directive
|
||||
- Using margins for spacing between siblings instead of gap utilities
|
||||
- Forgetting to add dark mode variants when the project uses dark mode
|
||||
- Forgetting to add dark mode variants when the project uses dark mode
|
||||
|
||||
@ -27,7 +27,7 @@ ## Basic Usage
|
||||
|
||||
## 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
|
||||
|
||||
@ -78,4 +78,4 @@ ## 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
|
||||
- Not following existing project patterns for Flux usage
|
||||
|
||||
@ -148,4 +148,4 @@ ## Key Endpoints
|
||||
| Passkey Confirm | POST | `/passkeys/confirm` |
|
||||
| Passkey Options | GET | `/user/passkeys/options` |
|
||||
| Register Passkey | POST | `/user/passkeys` |
|
||||
| Delete Passkey | DELETE | `/user/passkeys/{passkey}` |
|
||||
| Delete Passkey | DELETE | `/user/passkeys/{passkey}` |
|
||||
|
||||
@ -187,4 +187,4 @@ ## How to Apply
|
||||
|
||||
1. Identify the file type and select relevant sections (e.g., migration → §16, controller → §1, §3, §5, §6, §10)
|
||||
2. Check sibling files for existing patterns — follow those first per Consistency First
|
||||
3. Verify API syntax with `search-docs` for the installed Laravel version
|
||||
3. Verify API syntax with `search-docs` for the installed Laravel version
|
||||
|
||||
@ -103,4 +103,4 @@ ## Use Correlated Subqueries for Has-Many Ordering
|
||||
->take(1)
|
||||
);
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@ -199,4 +199,4 @@ ## Convention Over Configuration
|
||||
return $this->belongsToMany(Role::class);
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@ -33,4 +33,4 @@ ## Use Blade Fragments for Partial Re-Renders (htmx/Turbo)
|
||||
|
||||
## Use `@aware` for Deeply Nested Component Props
|
||||
|
||||
Avoids re-passing parent props through every level of nested components.
|
||||
Avoids re-passing parent props through every level of nested components.
|
||||
|
||||
@ -67,4 +67,4 @@ ## Configure Failover Cache Stores in Production
|
||||
|
||||
```php
|
||||
'failover' => ['driver' => 'failover', 'stores' => ['redis', 'database']],
|
||||
```
|
||||
```
|
||||
|
||||
@ -41,4 +41,4 @@ ## Use `#[CollectedBy]` for Custom Collection Classes
|
||||
```php
|
||||
#[CollectedBy(UserCollection::class)]
|
||||
class User extends Model {}
|
||||
```
|
||||
```
|
||||
|
||||
@ -70,4 +70,4 @@ ## Use Constants and Language Files
|
||||
```php
|
||||
// Only when lang files already exist in the project
|
||||
return back()->with('message', __('app.article_added'));
|
||||
```
|
||||
```
|
||||
|
||||
@ -189,4 +189,4 @@ ## No Queries in Blade Templates
|
||||
@foreach ($users as $user)
|
||||
{{ $user->profile->name }}
|
||||
@endforeach
|
||||
```
|
||||
```
|
||||
|
||||
@ -145,4 +145,4 @@ ## Avoid Hardcoded Table Names in Queries
|
||||
|
||||
Prefer Eloquent queries and relationships over `DB::table()` whenever possible — they already reference the model's table. When `DB::table()` or raw joins are unavoidable, always use `(new Model)->getTable()` to keep the reference traceable.
|
||||
|
||||
**Exception — migrations:** In migrations, hardcoded table names via `DB::table('settings')` are acceptable and preferred. Models change over time but migrations are frozen snapshots — referencing a model that is later renamed or deleted would break the migration.
|
||||
**Exception — migrations:** In migrations, hardcoded table names via `DB::table('settings')` are acceptable and preferred. Models change over time but migrations are frozen snapshots — referencing a model that is later renamed or deleted would break the migration.
|
||||
|
||||
@ -69,4 +69,4 @@ ## Add Context to Exception Classes
|
||||
return ['order_id' => $this->orderId];
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@ -49,4 +49,4 @@ ## Use On-Demand Notifications for Non-User Recipients
|
||||
|
||||
## Implement `HasLocalePreference` on Notifiable Models
|
||||
|
||||
Laravel automatically uses the user's preferred locale for all notifications and mailables — no per-call `locale()` needed.
|
||||
Laravel automatically uses the user's preferred locale for all notifications and mailables — no per-call `locale()` needed.
|
||||
|
||||
@ -157,4 +157,4 @@ ## Fake HTTP Calls in Tests
|
||||
Http::fake([
|
||||
'api.example.com/*' => Http::failedConnection(),
|
||||
]);
|
||||
```
|
||||
```
|
||||
|
||||
@ -24,4 +24,4 @@ ## Separate Content Tests from Sending Tests
|
||||
|
||||
Content tests: instantiate the mailable directly, call `assertSeeInHtml()`.
|
||||
Sending tests: use `Mail::fake()` and `assertSent()`/`assertQueued()`.
|
||||
Don't mix them — it conflates concerns and makes tests brittle.
|
||||
Don't mix them — it conflates concerns and makes tests brittle.
|
||||
|
||||
@ -118,4 +118,4 @@ ## Keep Migrations Focused
|
||||
|
||||
// Migration 2: seed_default_settings
|
||||
DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']);
|
||||
```
|
||||
```
|
||||
|
||||
@ -141,4 +141,4 @@ ## Use Horizon for Complex Queue Scenarios
|
||||
],
|
||||
],
|
||||
],
|
||||
```
|
||||
```
|
||||
|
||||
@ -96,4 +96,4 @@ ## Type-Hint Form Requests
|
||||
|
||||
return redirect()->route('posts.index');
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@ -36,4 +36,4 @@ ## Use Schedule Groups for Shared Configuration
|
||||
Schedule::command('emails:send --force');
|
||||
Schedule::command('emails:prune');
|
||||
});
|
||||
```
|
||||
```
|
||||
|
||||
@ -195,4 +195,4 @@ ## Encrypt Sensitive Database Fields
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@ -122,4 +122,4 @@ ## No Unnecessary Comments
|
||||
Correct:
|
||||
```php
|
||||
if ($this->hasJoins())
|
||||
```
|
||||
```
|
||||
|
||||
@ -40,4 +40,4 @@ ## Use `recycle()` to Share Relationship Instances Across Factories
|
||||
Ticket::factory()
|
||||
->recycle(Airline::factory()->create())
|
||||
->create();
|
||||
```
|
||||
```
|
||||
|
||||
@ -72,4 +72,4 @@ ## Use the `after()` Method for Custom Validation
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@ -172,4 +172,4 @@ ## Common Pitfalls
|
||||
- Expecting `wire:model` real-time → use `wire:model.live`
|
||||
- Unclosed component tags → syntax errors in v4
|
||||
- Using deprecated config keys or JS hooks
|
||||
- Including Alpine.js separately (already bundled in Livewire 4)
|
||||
- Including Alpine.js separately (already bundled in Livewire 4)
|
||||
|
||||
@ -36,4 +36,4 @@ ### Component-Scoped Interceptors
|
||||
## Magic Properties
|
||||
|
||||
- `$errors` - Access validation errors from JavaScript
|
||||
- `$intercept` - Component-scoped interceptors
|
||||
- `$intercept` - Component-scoped interceptors
|
||||
|
||||
@ -163,4 +163,4 @@ ## Common Pitfalls
|
||||
- Forgetting datasets for repetitive validation tests
|
||||
- Deleting tests without approval
|
||||
- Forgetting `assertNoJavaScriptErrors()` in browser tests
|
||||
- Prefixing `Feature/` or `Unit/` in `{name}` when using `make:test`
|
||||
- Prefixing `Feature/` or `Unit/` in `{name}` when using `make:test`
|
||||
|
||||
@ -116,4 +116,4 @@ ## Common Pitfalls
|
||||
- Using `@tailwind` directives instead of `@import "tailwindcss"`
|
||||
- Trying to use `tailwind.config.js` instead of CSS `@theme` directive
|
||||
- Using margins for spacing between siblings instead of gap utilities
|
||||
- Forgetting to add dark mode variants when the project uses dark mode
|
||||
- Forgetting to add dark mode variants when the project uses dark mode
|
||||
|
||||
4
.github/skills/fluxui-development/SKILL.md
vendored
4
.github/skills/fluxui-development/SKILL.md
vendored
@ -27,7 +27,7 @@ ## Basic Usage
|
||||
|
||||
## 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
|
||||
|
||||
@ -78,4 +78,4 @@ ## 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
|
||||
- Not following existing project patterns for Flux usage
|
||||
|
||||
2
.github/skills/fortify-development/SKILL.md
vendored
2
.github/skills/fortify-development/SKILL.md
vendored
@ -148,4 +148,4 @@ ## Key Endpoints
|
||||
| Passkey Confirm | POST | `/passkeys/confirm` |
|
||||
| Passkey Options | GET | `/user/passkeys/options` |
|
||||
| Register Passkey | POST | `/user/passkeys` |
|
||||
| Delete Passkey | DELETE | `/user/passkeys/{passkey}` |
|
||||
| Delete Passkey | DELETE | `/user/passkeys/{passkey}` |
|
||||
|
||||
@ -187,4 +187,4 @@ ## How to Apply
|
||||
|
||||
1. Identify the file type and select relevant sections (e.g., migration → §16, controller → §1, §3, §5, §6, §10)
|
||||
2. Check sibling files for existing patterns — follow those first per Consistency First
|
||||
3. Verify API syntax with `search-docs` for the installed Laravel version
|
||||
3. Verify API syntax with `search-docs` for the installed Laravel version
|
||||
|
||||
@ -103,4 +103,4 @@ ## Use Correlated Subqueries for Has-Many Ordering
|
||||
->take(1)
|
||||
);
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@ -199,4 +199,4 @@ ## Convention Over Configuration
|
||||
return $this->belongsToMany(Role::class);
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@ -33,4 +33,4 @@ ## Use Blade Fragments for Partial Re-Renders (htmx/Turbo)
|
||||
|
||||
## Use `@aware` for Deeply Nested Component Props
|
||||
|
||||
Avoids re-passing parent props through every level of nested components.
|
||||
Avoids re-passing parent props through every level of nested components.
|
||||
|
||||
@ -67,4 +67,4 @@ ## Configure Failover Cache Stores in Production
|
||||
|
||||
```php
|
||||
'failover' => ['driver' => 'failover', 'stores' => ['redis', 'database']],
|
||||
```
|
||||
```
|
||||
|
||||
@ -41,4 +41,4 @@ ## Use `#[CollectedBy]` for Custom Collection Classes
|
||||
```php
|
||||
#[CollectedBy(UserCollection::class)]
|
||||
class User extends Model {}
|
||||
```
|
||||
```
|
||||
|
||||
@ -70,4 +70,4 @@ ## Use Constants and Language Files
|
||||
```php
|
||||
// Only when lang files already exist in the project
|
||||
return back()->with('message', __('app.article_added'));
|
||||
```
|
||||
```
|
||||
|
||||
@ -189,4 +189,4 @@ ## No Queries in Blade Templates
|
||||
@foreach ($users as $user)
|
||||
{{ $user->profile->name }}
|
||||
@endforeach
|
||||
```
|
||||
```
|
||||
|
||||
@ -145,4 +145,4 @@ ## Avoid Hardcoded Table Names in Queries
|
||||
|
||||
Prefer Eloquent queries and relationships over `DB::table()` whenever possible — they already reference the model's table. When `DB::table()` or raw joins are unavoidable, always use `(new Model)->getTable()` to keep the reference traceable.
|
||||
|
||||
**Exception — migrations:** In migrations, hardcoded table names via `DB::table('settings')` are acceptable and preferred. Models change over time but migrations are frozen snapshots — referencing a model that is later renamed or deleted would break the migration.
|
||||
**Exception — migrations:** In migrations, hardcoded table names via `DB::table('settings')` are acceptable and preferred. Models change over time but migrations are frozen snapshots — referencing a model that is later renamed or deleted would break the migration.
|
||||
|
||||
@ -69,4 +69,4 @@ ## Add Context to Exception Classes
|
||||
return ['order_id' => $this->orderId];
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@ -49,4 +49,4 @@ ## Use On-Demand Notifications for Non-User Recipients
|
||||
|
||||
## Implement `HasLocalePreference` on Notifiable Models
|
||||
|
||||
Laravel automatically uses the user's preferred locale for all notifications and mailables — no per-call `locale()` needed.
|
||||
Laravel automatically uses the user's preferred locale for all notifications and mailables — no per-call `locale()` needed.
|
||||
|
||||
@ -157,4 +157,4 @@ ## Fake HTTP Calls in Tests
|
||||
Http::fake([
|
||||
'api.example.com/*' => Http::failedConnection(),
|
||||
]);
|
||||
```
|
||||
```
|
||||
|
||||
@ -24,4 +24,4 @@ ## Separate Content Tests from Sending Tests
|
||||
|
||||
Content tests: instantiate the mailable directly, call `assertSeeInHtml()`.
|
||||
Sending tests: use `Mail::fake()` and `assertSent()`/`assertQueued()`.
|
||||
Don't mix them — it conflates concerns and makes tests brittle.
|
||||
Don't mix them — it conflates concerns and makes tests brittle.
|
||||
|
||||
@ -118,4 +118,4 @@ ## Keep Migrations Focused
|
||||
|
||||
// Migration 2: seed_default_settings
|
||||
DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']);
|
||||
```
|
||||
```
|
||||
|
||||
@ -141,4 +141,4 @@ ## Use Horizon for Complex Queue Scenarios
|
||||
],
|
||||
],
|
||||
],
|
||||
```
|
||||
```
|
||||
|
||||
@ -96,4 +96,4 @@ ## Type-Hint Form Requests
|
||||
|
||||
return redirect()->route('posts.index');
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@ -36,4 +36,4 @@ ## Use Schedule Groups for Shared Configuration
|
||||
Schedule::command('emails:send --force');
|
||||
Schedule::command('emails:prune');
|
||||
});
|
||||
```
|
||||
```
|
||||
|
||||
@ -195,4 +195,4 @@ ## Encrypt Sensitive Database Fields
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@ -122,4 +122,4 @@ ## No Unnecessary Comments
|
||||
Correct:
|
||||
```php
|
||||
if ($this->hasJoins())
|
||||
```
|
||||
```
|
||||
|
||||
@ -40,4 +40,4 @@ ## Use `recycle()` to Share Relationship Instances Across Factories
|
||||
Ticket::factory()
|
||||
->recycle(Airline::factory()->create())
|
||||
->create();
|
||||
```
|
||||
```
|
||||
|
||||
@ -72,4 +72,4 @@ ## Use the `after()` Method for Custom Validation
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
2
.github/skills/livewire-development/SKILL.md
vendored
2
.github/skills/livewire-development/SKILL.md
vendored
@ -172,4 +172,4 @@ ## Common Pitfalls
|
||||
- Expecting `wire:model` real-time → use `wire:model.live`
|
||||
- Unclosed component tags → syntax errors in v4
|
||||
- Using deprecated config keys or JS hooks
|
||||
- Including Alpine.js separately (already bundled in Livewire 4)
|
||||
- Including Alpine.js separately (already bundled in Livewire 4)
|
||||
|
||||
@ -36,4 +36,4 @@ ### Component-Scoped Interceptors
|
||||
## Magic Properties
|
||||
|
||||
- `$errors` - Access validation errors from JavaScript
|
||||
- `$intercept` - Component-scoped interceptors
|
||||
- `$intercept` - Component-scoped interceptors
|
||||
|
||||
2
.github/skills/pest-testing/SKILL.md
vendored
2
.github/skills/pest-testing/SKILL.md
vendored
@ -163,4 +163,4 @@ ## Common Pitfalls
|
||||
- Forgetting datasets for repetitive validation tests
|
||||
- Deleting tests without approval
|
||||
- Forgetting `assertNoJavaScriptErrors()` in browser tests
|
||||
- Prefixing `Feature/` or `Unit/` in `{name}` when using `make:test`
|
||||
- Prefixing `Feature/` or `Unit/` in `{name}` when using `make:test`
|
||||
|
||||
@ -116,4 +116,4 @@ ## Common Pitfalls
|
||||
- Using `@tailwind` directives instead of `@import "tailwindcss"`
|
||||
- Trying to use `tailwind.config.js` instead of CSS `@theme` directive
|
||||
- Using margins for spacing between siblings instead of gap utilities
|
||||
- Forgetting to add dark mode variants when the project uses dark mode
|
||||
- Forgetting to add dark mode variants when the project uses dark mode
|
||||
|
||||
@ -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.
|
||||
|
||||
- php - 8.5
|
||||
- php - 8.4
|
||||
- laravel/fortify (FORTIFY) - v1
|
||||
- laravel/framework (LARAVEL) - v13
|
||||
- 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.
|
||||
- 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.
|
||||
- To check environment variables, read the `.env` file directly.
|
||||
|
||||
## 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.
|
||||
|
||||
- php - 8.5
|
||||
- php - 8.4
|
||||
- laravel/fortify (FORTIFY) - v1
|
||||
- laravel/framework (LARAVEL) - v13
|
||||
- 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.
|
||||
- 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.
|
||||
- To check environment variables, read the `.env` file directly.
|
||||
|
||||
## 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 [
|
||||
'email_verified_at' => 'datetime',
|
||||
'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,
|
||||
"guidelines": true,
|
||||
"mcp": true,
|
||||
@ -8,6 +12,7 @@
|
||||
"skills": [
|
||||
"fortify-development",
|
||||
"laravel-best-practices",
|
||||
"fluxui-development",
|
||||
"livewire-development",
|
||||
"pest-testing",
|
||||
"tailwindcss-development"
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.5",
|
||||
"php": "^8.3",
|
||||
"gehrisandro/tailwind-merge-laravel": "^1.4",
|
||||
"laravel/fortify": "^1.34",
|
||||
"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'),
|
||||
],
|
||||
|
||||
// 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 Illuminate\Support\Facades\Route;
|
||||
use Spatie\Permission\Middleware\{PermissionMiddleware, RoleMiddleware};
|
||||
use App\Http\Controllers\KekaOutlookController;
|
||||
|
||||
Route::redirect('/', '/dashboard')->name('home');
|
||||
|
||||
@ -94,6 +95,14 @@
|
||||
|
||||
// Disconnect / revoke saved token
|
||||
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';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user