diff --git a/.env.example b/.env.example
index d66a920f..a82d36de 100644
--- a/.env.example
+++ b/.env.example
@@ -85,3 +85,5 @@ ANYSTACK_FOREVER_POLICY_ID=
ANYSTACK_TRIAL_POLICY_ID=
FILAMENT_USERS=
+
+BIFROST_API_KEY=your-secure-api-key-here
diff --git a/app/Filament/Resources/PersonalAccessTokenResource.php b/app/Filament/Resources/PersonalAccessTokenResource.php
deleted file mode 100644
index 35463118..00000000
--- a/app/Filament/Resources/PersonalAccessTokenResource.php
+++ /dev/null
@@ -1,111 +0,0 @@
-schema([
- Forms\Components\TextInput::make('name')
- ->required()
- ->maxLength(255)
- ->helperText('A descriptive name for this API key'),
-
- Forms\Components\Select::make('tokenable_id')
- ->label('User')
- ->options(\App\Models\User::pluck('name', 'id'))
- ->required()
- ->searchable(),
-
- Forms\Components\Hidden::make('tokenable_type')
- ->default(\App\Models\User::class),
-
- Forms\Components\TextInput::make('abilities')
- ->default('*')
- ->helperText('Comma-separated list of abilities. Use * for all abilities.'),
-
- Forms\Components\DateTimePicker::make('expires_at')
- ->label('Expires At')
- ->nullable()
- ->helperText('Leave empty for no expiration'),
- ]);
- }
-
- public static function table(Table $table): Table
- {
- return $table
- ->columns([
- Tables\Columns\TextColumn::make('name')
- ->searchable()
- ->sortable(),
-
- Tables\Columns\TextColumn::make('tokenable.name')
- ->label('User')
- ->searchable()
- ->sortable(),
-
- Tables\Columns\TextColumn::make('abilities')
- ->formatStateUsing(fn ($state) => is_array($state) ? implode(', ', $state) : $state),
-
- Tables\Columns\TextColumn::make('last_used_at')
- ->dateTime()
- ->sortable()
- ->placeholder('Never'),
-
- Tables\Columns\TextColumn::make('expires_at')
- ->dateTime()
- ->sortable()
- ->placeholder('Never'),
-
- Tables\Columns\TextColumn::make('created_at')
- ->dateTime()
- ->sortable()
- ->toggleable(isToggledHiddenByDefault: true),
- ])
- ->filters([
- //
- ])
- ->actions([
- Tables\Actions\DeleteAction::make(),
- ])
- ->bulkActions([
- Tables\Actions\DeleteBulkAction::make(),
- ])
- ->defaultSort('created_at', 'desc');
- }
-
- public static function getRelations(): array
- {
- return [
- //
- ];
- }
-
- public static function getPages(): array
- {
- return [
- 'index' => Pages\ListPersonalAccessTokens::route('/'),
- 'create' => Pages\CreatePersonalAccessToken::route('/create'),
- ];
- }
-}
diff --git a/app/Filament/Resources/PersonalAccessTokenResource/Pages/CreatePersonalAccessToken.php b/app/Filament/Resources/PersonalAccessTokenResource/Pages/CreatePersonalAccessToken.php
deleted file mode 100644
index 9fcca8a3..00000000
--- a/app/Filament/Resources/PersonalAccessTokenResource/Pages/CreatePersonalAccessToken.php
+++ /dev/null
@@ -1,61 +0,0 @@
-createToken(
- name: $data['name'],
- abilities: $abilities,
- expiresAt: $data['expires_at'] ?? null
- );
-
- // Store the plain text token to show to user
- session(['new_api_token' => $token->plainTextToken]);
-
- // Return the token model
- return $token->accessToken;
- }
-
- protected function afterCreate(): void
- {
- $token = session('new_api_token');
-
- if ($token) {
- Notification::make()
- ->title('API Key Created Successfully')
- ->body("Your API key: {$token}")
- ->success()
- ->persistent()
- ->send();
-
- session()->forget('new_api_token');
- }
- }
-
- protected function getRedirectUrl(): string
- {
- return $this->getResource()::getUrl('index');
- }
-}
diff --git a/app/Filament/Resources/PersonalAccessTokenResource/Pages/EditPersonalAccessToken.php b/app/Filament/Resources/PersonalAccessTokenResource/Pages/EditPersonalAccessToken.php
deleted file mode 100644
index 77a2f67d..00000000
--- a/app/Filament/Resources/PersonalAccessTokenResource/Pages/EditPersonalAccessToken.php
+++ /dev/null
@@ -1,19 +0,0 @@
- \App\Http\Middleware\Authenticate::class,
+ 'auth.api_key' => \App\Http\Middleware\AuthenticateApiKey::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
diff --git a/app/Http/Middleware/AuthenticateApiKey.php b/app/Http/Middleware/AuthenticateApiKey.php
new file mode 100644
index 00000000..95189696
--- /dev/null
+++ b/app/Http/Middleware/AuthenticateApiKey.php
@@ -0,0 +1,38 @@
+json(['message' => 'API key not configured'], 500);
+ }
+
+ $authHeader = $request->header('Authorization');
+
+ if (! $authHeader || ! str_starts_with($authHeader, 'Bearer ')) {
+ return response()->json(['message' => 'Unauthorized'], 401);
+ }
+
+ $providedKey = substr($authHeader, 7); // Remove 'Bearer ' prefix
+
+ if (! hash_equals($apiKey, $providedKey)) {
+ return response()->json(['message' => 'Unauthorized'], 401);
+ }
+
+ return $next($request);
+ }
+}
diff --git a/config/services.php b/config/services.php
index 2b73a53f..238da67e 100644
--- a/config/services.php
+++ b/config/services.php
@@ -34,4 +34,8 @@
'anystack' => [
'key' => env('ANYSTACK_API_KEY'),
],
+
+ 'bifrost' => [
+ 'api_key' => env('BIFROST_API_KEY'),
+ ],
];
diff --git a/phpunit.xml b/phpunit.xml
index 368547a6..ebb932db 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -44,5 +44,6 @@
+
diff --git a/routes/api.php b/routes/api.php
index 6c7fce63..4b4018ea 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -15,7 +15,10 @@
|
*/
+Route::middleware('auth.api_key')->group(function () {
+ Route::post('/licenses', [LicenseController::class, 'store']);
+});
+
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', fn (Request $request) => $request->user());
- Route::post('/licenses', [LicenseController::class, 'store']);
});
diff --git a/tests/Feature/Api/CreateLicenseTest.php b/tests/Feature/Api/CreateLicenseTest.php
index 445f0353..fcbead06 100644
--- a/tests/Feature/Api/CreateLicenseTest.php
+++ b/tests/Feature/Api/CreateLicenseTest.php
@@ -44,8 +44,7 @@ public function test_requires_authentication()
public function test_validates_required_fields()
{
- $user = User::factory()->create();
- $token = $user->createToken('test-token')->plainTextToken;
+ $token = config('services.bifrost.api_key');
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$token,
@@ -57,8 +56,7 @@ public function test_validates_required_fields()
public function test_validates_subscription_enum()
{
- $user = User::factory()->create();
- $token = $user->createToken('test-token')->plainTextToken;
+ $token = config('services.bifrost.api_key');
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$token,
@@ -74,8 +72,7 @@ public function test_validates_subscription_enum()
public function test_creates_new_user_when_email_not_exists()
{
- $user = User::factory()->create();
- $token = $user->createToken('test-token')->plainTextToken;
+ $token = config('services.bifrost.api_key');
$this->withHeaders([
'Authorization' => 'Bearer '.$token,
@@ -101,8 +98,7 @@ public function test_finds_existing_user_when_email_exists()
'name' => 'Original Name',
]);
- $user = User::factory()->create();
- $token = $user->createToken('test-token')->plainTextToken;
+ $token = config('services.bifrost.api_key');
$this->withHeaders([
'Authorization' => 'Bearer '.$token,
@@ -121,8 +117,7 @@ public function test_finds_existing_user_when_email_exists()
public function test_creates_license_with_bifrost_source()
{
- $user = User::factory()->create();
- $token = $user->createToken('test-token')->plainTextToken;
+ $token = config('services.bifrost.api_key');
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$token,
@@ -164,8 +159,7 @@ public function test_creates_license_for_existing_user()
'name' => 'Existing User',
]);
- $authUser = User::factory()->create();
- $token = $authUser->createToken('test-token')->plainTextToken;
+ $token = config('services.bifrost.api_key');
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$token,