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,