[
  {
    "path": ".gitignore",
    "content": "/vendor\ncomposer.phar\ncomposer.lock\n.DS_Store\n.php_cs.cache"
  },
  {
    "path": "LICENSE.md",
    "content": "The MIT License\n\nCopyright (c) Givebutter, Inc. https://givebutter.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Laravel Keyable\r\n\r\nLaravel Keyable is a package that allows you to add API Keys to any model. This allows you to associate incoming requests with their respective models. You can also use Policies to authorize requests.\r\n\r\n[![Latest Stable Version](https://poser.pugx.org/givebutter/laravel-keyable/v/stable)](https://packagist.org/packages/givebutter/laravel-keyable) [![Total Downloads](https://poser.pugx.org/givebutter/laravel-keyable/downloads)](https://packagist.org/packages/givebutter/laravel-keyable) [![License](https://poser.pugx.org/givebutter/laravel-keyable/license)](https://packagist.org/packages/givebutter/laravel-keyable)\r\n\r\n## Installation\r\n\r\nRequire the ```givebutter/laravel-keyable``` package in your ```composer.json``` and update your dependencies:\r\n\r\n```bash\r\ncomposer require givebutter/laravel-keyable\r\n```\r\n\r\nPublish the migration and config files:\r\n```bash\r\nphp artisan vendor:publish --provider=\"Givebutter\\LaravelKeyable\\KeyableServiceProvider\"\r\n```\r\n\r\nRun the migration:\r\n```bash\r\nphp artisan migrate\r\n```\r\n\r\n## Usage\r\n\r\nAdd the ```Givebutter\\LaravelKeyable\\Keyable``` trait to your model(s):\r\n\r\n```php\r\nuse Illuminate\\Database\\Eloquent\\Model;\r\nuse Givebutter\\LaravelKeyable\\Keyable;\r\n\r\nclass Account extends Model\r\n{\r\n    use Keyable;\r\n\r\n    // ...\r\n}\r\n```\r\n\r\nAdd the ```auth.apiKey``` middleware to the ```mapApiRoutes()``` function in your ```App\\Providers\\RouteServiceProvider``` file:\r\n\r\n```php\r\n// ...\r\n\r\nprotected function mapApiRoutes()\r\n{\r\n    Route::prefix('api')\r\n        ->middleware(['api', 'auth.apikey'])\r\n\t->namespace($this->namespace . '\\API')\r\n\t->group(base_path('routes/api.php'));\r\n}\r\n\r\n// ...\r\n```\r\n\r\nThe middleware will authenticate API requests, ensuring they contain an API key that is valid.\r\n\r\n### Generating API keys\r\n\r\nYou can generate new API keys by calling the `createApiKey()` method from the `Keyable` trait.\r\n\r\nWhen you do so, it returns an instance of `NewApiKey`, which is a simple class the contains the actual `ApiKey` instance that was just created, and also contains the plain text api key, which is the one you should use to authenticate requests.\r\n\r\n```php\r\n$newApiKey = $keyable->createApiKey();\r\n\r\n$newApiKey->plainTextApiKey // This is the key you should use to authenticate requests\r\n$newApiKey->apiKey // The instance of ApiKey just created\r\n```\r\n\r\nYou can also manually create API keys without using the `createApiKey` from the `Keyable` trait, in that case, the instance you get back will have a property called `plainTextApikey` populated with the plain text API key.\r\n\r\n```php\r\n$myApiKey = ApiKey::create([\r\n    'keyable_id' => $account->getKey(),\r\n    'keyable_type' => Account::class,\r\n    'name' => 'My api key',\r\n]);\r\n\r\n$myApiKey->plainTextApikey // Token to be used to authenticate requests\r\n```\r\n\r\nKeep in mind `plainTextApikey` will only be populated immediately after creating the key.\r\n\r\n### Accessing keyable models in your controllers\r\nThe model associated with the key will be attached to the incoming request as ```keyable```:\r\n\r\n```php\r\nuse App\\Http\\Controllers\\Controller;\r\n\r\nclass FooController extends Controller {\r\n\r\n    public function index(Request $request)\r\n    {\r\n        $model = $request->keyable;\r\n\r\n        // ...\r\n    }\r\n\r\n}\r\n```\r\nNow you can use the keyable model to scope your associated API resources, for example:\r\n```php\r\nreturn $model->foo()->get();\r\n```\r\n\r\n### Keys Without Models\r\n\r\nSometimes you may not want to attach a model to an API key (if you wanted to have administrative access to your API). By default this functionality is turned off:\r\n\r\n```php\r\n<?php\r\n\r\nreturn [\r\n\r\n    'allow_empty_models' => true\r\n\r\n];\r\n```\r\n\r\n## Making Requests\r\n\r\nBy default, laravel-keyable uses bearer tokens to authenticate requests. Attach the API key to the header of each request:\r\n\r\n```\r\nAuthorization: Bearer <key>\r\n```\r\n\r\nYou can change where the API key is retrieved from by altering the setting in the `keyable.php` config file. Supported options are: `bearer`, `header`, and `parameter`.\r\n```php\r\n<?php\r\n\r\nreturn [\r\n\r\n    'mode' => 'header',\r\n\r\n    'key' => 'X-Authorization',\r\n\r\n];\r\n```\r\n\r\nNeed to pass the key as a URL parameter? Set the mode to `parameter` and the key to the string you'll use in your URL:\r\n```php\r\n<?php\r\n\r\nreturn [\r\n\r\n    'mode' => 'parameter',\r\n\r\n    'key' => 'api_key'\r\n\r\n];\r\n```\r\nNow you can make requests like this:\r\n```php\r\nhttps://example.com/api/posts?api_key=<key>\r\n```\r\n\r\n## Authorizing Requests\r\n\r\nLaravel offers a great way to perform [Authorization](https://laravel.com/docs/5.8/authorization) on incoming requests using Policies. However, they are limited to authenticated users. We replicate that functionality to let you authorize requests on any incoming model.\r\n\r\nTo begin, add the `AuthorizesKeyableRequests` trait to your base `Controller.php` class:\r\n\r\n```php\r\n<?php\r\n\r\nnamespace App\\Http\\Controllers;\r\n\r\n// ...\r\n\r\nuse Givebutter\\LaravelKeyable\\Auth\\AuthorizesKeyableRequests;\r\n\r\nclass Controller extends BaseController\r\n{\r\n    use AuthorizesKeyableRequests;\r\n}\r\n```\r\n\r\nNext, create the `app/Policies/KeyablePolicies` folder and create a new policy:\r\n\r\n```php\r\n<?php\r\n\r\nnamespace App\\Policies\\KeyablePolicies;\r\n\r\nuse App\\Models\\Post;\r\nuse Illuminate\\Database\\Eloquent\\Model;\r\nuse Givebutter\\LaravelKeyable\\Models\\ApiKey;\r\n\r\nclass PostPolicy {\r\n\r\n    public function view(ApiKey $apiKey, Model $keyable, Post $post) {\r\n    \treturn !is_null($keyable->posts()->find($post->id));\r\n    }\r\n\r\n}\r\n```\r\n\r\nLastly, register your policies in `AuthServiceProvider.php`:\r\n\r\n```php\r\n<?php\r\n\r\nnamespace App\\Providers;\r\n\r\n// ...\r\n\r\nuse App\\Models\\Post;\r\nuse App\\Policies\\KeyablePolicies\\PostPolicy;\r\nuse Givebutter\\LaravelKeyable\\Facades\\Keyable;\r\n\r\nclass AuthServiceProvider extends ServiceProvider\r\n{\r\n\r\n    // ...\r\n\r\n    protected $keyablePolicies = [\r\n        Post::class => PostPolicy::class\r\n    ];\r\n\r\n    public function boot(GateContract $gate)\r\n    {\r\n        // ...\r\n        Keyable::registerKeyablePolicies($this->keyablePolicies);\r\n    }\r\n\r\n}\r\n```\r\n\r\nIn your controller, you can now authorize the request using the policy by calling `$this->authorizeKeyable(<ability>, <model>)`:\r\n\r\n```php\r\n<?php\r\n\r\nnamespace App\\Http\\Controllers\\PostController;\r\n\r\nuse App\\Models\\Post;\r\nuse Illuminate\\Http\\Request;\r\nuse App\\Http\\Controllers\\Controller;\r\n\r\nclass PostController extends Controller {\r\n\r\n    public function show(Post $post) {\r\n        $this->authorizeKeyable('view', $post);\r\n        // ...\r\n    }\r\n\r\n}\r\n```\r\n\r\n## Keyable Model Scoping\r\n\r\nWhen using implicit model binding, you may wish to scope the first model such that it must be a child of the keyable model. Consider an example where we have a post resource:\r\n\r\n```php\r\nuse App\\Models\\Post;\r\n\r\nRoute::get('/posts/{post}', function (Post $post) {\r\n    return $post;\r\n});\r\n```\r\n\r\nYou may instruct the package to apply the scope by invoking the `keyableScoped` method when defining your route:\r\n\r\n```php\r\nuse App\\Models\\Post;\r\n\r\nRoute::get('/posts/{post}', function (Post $post) {\r\n    return $post;\r\n})->keyableScoped();\r\n```\r\n\r\nThe benefits of applying this scope are two-fold. First, models not belonging to the keyable model are caught before the controller. That means you don't have to handle this repeatedly in the controller methods. Second, models that don't belong to the keyable model will trigger a 404 response instead of a 403, keeping information hidden about other users.\r\n\r\nYou may use this in tandem with Laravel's scoping to ensure the entire heirarchy has a parent-child relationship starting with the keyable model:\r\n\r\n```php\r\nuse App\\Models\\Post;\r\nuse App\\Models\\User;\r\n\r\nRoute::get('/users/{user}/posts/{post}', function (User $user, Post $post) {\r\n    return $post;\r\n})->scopeBindings()->keyableScoped();\r\n```\r\n\r\n## Artisan Commands\r\n\r\nGenerate an API key:\r\n\r\n```bash\r\nphp artisan api-key:generate --id=1 --type=\"App\\Models\\Account\" --name=\"My api key\"\r\n```\r\n\r\nDelete an API key:\r\n```bash\r\nphp artisan api-key:delete --id=12345\r\n```\r\n\r\n## Upgrading\r\n\r\nPlease see [UPGRADING](UPGRADING.md) for details.\r\n\r\n## Security\r\n\r\nIf you discover any security related issues, please email [liran@givebutter.com](mailto:liran@givebutter.com).\r\n\r\n## License\r\nReleased under the [MIT](https://choosealicense.com/licenses/mit/) license. See [LICENSE](LICENSE.md) for more information.\r\n"
  },
  {
    "path": "UPGRADING.md",
    "content": "## Upgrade guide\r\n\r\n### From 2.1.1 to 3.0.0\r\n\r\nATTENTION: It is highly recommended that you generate a backup of your database before going through the steps below, just to be safe in case something goes wrong.\r\n\r\n#### Step 1: `api_keys` table updates\r\n\r\nImplement the following changes on your `api_keys` table.\r\n\r\n- Add a new nullable string column called `name`.\r\n- Modify the existing `key` column to increase its length from 40 to 64.\r\n\r\n#### Step 2: Update the package to version 3.0.0\r\n\r\n```bash\r\ncomposer require givebutter/laravel-keyable:3.0.0\r\n```\r\n\r\n#### Step 3. Turn on `compatibility_mode`\r\n\r\nA new configuration flag was introduced in the `keyable.php` config file on version `3.0.0`, it is called `compatibility_mode`, make sure to publish the package's config file to be able to access it.\r\n\r\nBy default it is set to `false`, but when it is set to `true` the package will handle both hashed and non hashed API keys, which should keep your application running smoothly while you complete all upgrade steps.\r\n\r\nIt is specially useful if you have a very large `api_keys` table, which could take a while to hash all existing API keys.\r\n\r\nIt points to an environment variable called `KEYABLE_COMPATIBILITY_MODE`, but you can update it to whatever you need of course.\r\n\r\nMake sure to update `KEYABLE_COMPATIBILITY_MODE` to `true` if you want to make use of that feature.\r\n\r\n#### Step 4. Hash existing API keys\r\n\r\nA command was added to hash existing API keys that are not currently hashed, it will ensure existing API keys will continue working properly once you finish all upgrade steps.\r\n\r\n```bash\r\nphp artisan api-key:hash\r\n```\r\n\r\nIt is also possible to hash a single API key at a time, by passing an `--id` option.\r\n\r\n```bash\r\nphp artisan api-key:hash --id=API_KEY_ID\r\n```\r\n\r\nBe very careful with this option, as each API key should be hashed only once.\r\n\r\nIdeally you should only use it for testing and on your own API keys.\r\n\r\nThe command tries to avoid hashing an API key twice by comparing the length of the `key` column, if it is already 64 then the command understands the key is already hashed and won't do it again.\r\n\r\n#### Step 5. Turn off compatibility mode\r\n\r\nIf you are making use of the compatibility mode, it can now be turned off by setting `KEYABLE_COMPATIBILITY_MODE` to `false`, it is not needed anymore.\r\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"givebutter/laravel-keyable\",\n    \"description\": \"Add API keys to your Laravel models\",\n    \"license\": \"MIT\",\n    \"keywords\": [\n    \t\"laravel\",\n    \t\"php\",\n    \t\"api\",\n    \t\"rest\",\n    \t\"json\",\n    \t\"api keys\",\n    \t\"api authentication\"\n    ],\n    \"homepage\": \"https://github.com/givebutter/laravel-keyable\",\n    \"authors\": [\n        {\n            \"name\": \"Liran Cohen\",\n            \"email\": \"liran@givebutter.com\"\n        }\n    ],\n    \"minimum-stability\": \"dev\",\n    \"prefer-stable\": true,\n    \"require\": {\n        \"php\": \"^7.0|^8.0\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"Givebutter\\\\LaravelKeyable\\\\\": \"src/\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Givebutter\\\\Tests\\\\\": \"tests/\"\n        }\n    },\n    \"extra\": {\n\t    \"laravel\": {\n\t        \"providers\": [\n\t            \"Givebutter\\\\LaravelKeyable\\\\KeyableServiceProvider\"\n\t        ]\n\t    }\n\t},\n    \"require-dev\": {\n        \"phpunit/phpunit\": \"^9.5\",\n        \"orchestra/testbench\": \"^8.0\"\n    }\n}\n"
  },
  {
    "path": "config/keyable.php",
    "content": "<?php\n\nreturn [\n\n    /*\n    |--------------------------------------------------------------------------\n    | Authentication Mode\n    |--------------------------------------------------------------------------\n    |\n    | Supported modes: header, bearer, parameter\n    |\n    | When using header or parameter, set a key value.\n    |\n    */\n\n    'mode' => 'bearer',\n\n    'key' => null,\n\n    /*\n    |--------------------------------------------------------------------------\n    | Empty Models\n    |--------------------------------------------------------------------------\n    |\n    | Set this to true to allow API keys without an associated model.\n    |\n    */\n\n    'allow_empty_models' => false,\n\n    /*\n    |--------------------------------------------------------------------------\n    | Compatibility mode\n    |--------------------------------------------------------------------------\n    |\n    | Set this to true to instruct this package to accept both hashed and non\n    | hashed API keys.\n    |\n    | This is useful to keep your app running smoothly while you are going\n    | throught the upgrade steps for version 2.1.1 to 3.0.0, especially if you\n    | have a very large api_keys table, which can take a while to hash all\n    | existing API keys.\n    |\n    | Once the new database changes are in place and all existing keys are\n    | hashed, you should set this flag to false to instruct this package to\n    | only look for hashed API keys.\n    |\n    */\n\n    'compatibility_mode' => env('KEYABLE_COMPATIBILITY_MODE', false),\n\n];\n"
  },
  {
    "path": "database/migrations/2019_04_09_225232_create_api_keys_table.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nclass CreateApiKeysTable extends Migration\n{\n    /**\n     * Run the migrations.\n     *\n     * @return void\n     */\n    public function up()\n    {\n        Schema::create('api_keys', function (Blueprint $table) {\n            $table->increments('id');\n            $table->nullableMorphs('keyable');\n            $table->string('name')->nullable();\n            $table->string('key', 64);\n            $table->dateTime('last_used_at')->nullable();\n            $table->timestamps();\n            $table->softDeletes();\n            $table->index('key');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     *\n     * @return void\n     */\n    public function down()\n    {\n        Schema::dropIfExists('api_keys');\n    }\n}\n"
  },
  {
    "path": "src/Auth/AuthorizesKeyableRequests.php",
    "content": "<?php\n\nnamespace Givebutter\\LaravelKeyable\\Auth;\n\nuse Givebutter\\LaravelKeyable\\Facades\\Keyable;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Auth\\Access\\Response;\n\ntrait AuthorizesKeyableRequests\n{\n    /**\n     * Authorize a request.\n     *\n     * @return Response or throw exception\n     */\n    public function authorizeKeyable($ability, $object)\n    {\n        $apiKey = request()->apiKey;\n        $keyable = request()->keyable;\n\n        if ($policy = $this->getKeyablePolicy($object)) {\n            $policyClass = (new $policy());\n\n            if (method_exists($policyClass, 'before')) {\n                $before = $policyClass->before($apiKey, $keyable, $object);\n                if (! is_null($before) && $before) {\n                    return new Response('');\n                }\n            }\n\n            if ($policyClass->$ability($apiKey, $keyable, $object)) {\n                return new Response('');\n            }\n        }\n\n        //Throw exception\n        throw new AuthorizationException('This action is unauthorized.');\n    }\n\n    /**\n     * Get the associated policy.\n     *\n     * @return policy\n     */\n    public function getKeyablePolicy($object)\n    {\n        return Keyable::getKeyablePolicies()[get_class($object)] ?? null;\n    }\n}\n"
  },
  {
    "path": "src/Auth/Keyable.php",
    "content": "<?php\n\nnamespace Givebutter\\LaravelKeyable\\Auth;\n\nclass Keyable\n{\n    protected $policies;\n\n    public function registerKeyablePolicies($policies)\n    {\n        return $this->policies = $policies;\n    }\n\n    public function getKeyablePolicies()\n    {\n        return $this->policies;\n    }\n}\n"
  },
  {
    "path": "src/Console/Commands/DeleteApiKey.php",
    "content": "<?php\n\nnamespace Givebutter\\LaravelKeyable\\Console\\Commands;\n\nuse Givebutter\\LaravelKeyable\\Models\\ApiKey;\nuse Illuminate\\Console\\Command;\n\nclass DeleteApiKey extends Command\n{\n    /**\n     * The name and signature of the console command.\n     *\n     * @var string\n     */\n    protected $signature = 'api-key:delete {--id= : ID of the API key you want to delete.}';\n\n    /**\n     * The console command description.\n     *\n     * @var string\n     */\n    protected $description = 'Delete API key';\n\n    /**\n     * Create a new command instance.\n     */\n    public function __construct()\n    {\n        parent::__construct();\n    }\n\n    /**\n     * Execute the console command.\n     *\n     * @return mixed\n     */\n    public function handle()\n    {\n        $key = ApiKey::findOrFail($this->option('id'));\n        \n        $key->delete();\n        \n        $this->info('API key successfully deleted.');\n    }\n}\n"
  },
  {
    "path": "src/Console/Commands/GenerateApiKey.php",
    "content": "<?php\n\nnamespace Givebutter\\LaravelKeyable\\Console\\Commands;\n\nuse Givebutter\\LaravelKeyable\\Models\\ApiKey;\nuse Illuminate\\Console\\Command;\n\nclass GenerateApiKey extends Command\n{\n    /**\n     * The name and signature of the console command.\n     *\n     * @var string\n     */\n    protected $signature = 'api-key:generate\n                            {--id= : ID of the model you want to bind to this API key}\n                            {--type= : The class name of the model you want to bind to this API key}\n                            {--name= : The name you want to give to this API key}';\n\n    /**\n     * The console command description.\n     *\n     * @var string\n     */\n    protected $description = 'Generate API key';\n\n    /**\n     * Create a new command instance.\n     */\n    public function __construct()\n    {\n        parent::__construct();\n    }\n\n    /**\n     * Execute the console command.\n     *\n     * @return mixed\n     */\n    public function handle()\n    {\n        $apiKey = (new ApiKey)->create([\n            'keyable_id' => $this->option('id'),\n            'keyable_type' => $this->option('type'),\n            'name' => $this->option('name'),\n        ]);\n\n        $this->info('The following API key was created: ' . \"{$apiKey->getKey()}|{$apiKey->plainTextApiKey}\");\n    }\n}\n"
  },
  {
    "path": "src/Console/Commands/HashApiKeys.php",
    "content": "<?php\n\nnamespace Givebutter\\LaravelKeyable\\Console\\Commands;\n\nuse Givebutter\\LaravelKeyable\\Models\\ApiKey;\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Facades\\DB;\n\nclass HashApiKeys extends Command\n{\n    /**\n     * The name and signature of the console command.\n     *\n     * @var string\n     */\n    protected $signature = 'api-key:hash {--id= : ID of the API key you want to hash}';\n\n    /**\n     * The console command description.\n     *\n     * @var string\n     */\n    protected $description = 'Hash existing API keys';\n\n    /**\n     * Execute the console command.\n     *\n     * @return mixed\n     */\n    public function handle()\n    {\n        DB::transaction(function () {\n            ApiKey::query()\n                ->withTrashed()\n                ->when($this->option('id'), function (Builder $query, int $id) {\n                    $query->where('id', $id);\n                })\n                ->whereRaw('LENGTH(api_keys.key) != 64')\n                ->eachById(function (ApiKey $apiKey) {\n                    $apiKey->update([\n                        'key' => hash('sha256', $apiKey->key),\n                    ]);\n\n                    $this->info(\"API key #{$apiKey->getKey()} successfully hashed.\");\n                }, 250);\n\n            $this->info('All API keys were successfully hashed.');\n        });\n    }\n}\n"
  },
  {
    "path": "src/Events/KeyableAuthenticated.php",
    "content": "<?php\n\nnamespace Givebutter\\LaravelKeyable\\Events;\n\nuse Givebutter\\LaravelKeyable\\Models\\ApiKey;\n\nclass KeyableAuthenticated\n{\n    public function __construct(public ApiKey $apiKey)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Facades/Keyable.php",
    "content": "<?php\n\nnamespace Givebutter\\LaravelKeyable\\Facades;\n\nuse Givebutter\\LaravelKeyable\\Auth\\Keyable as KeyableAuth;\nuse Illuminate\\Support\\Facades\\Facade;\n\nclass Keyable extends Facade\n{\n    /**\n     * Get the registered name of the component.\n     *\n     * @return string\n     */\n    protected static function getFacadeAccessor(): string\n    {\n        return KeyableAuth::class;\n    }\n}\n"
  },
  {
    "path": "src/Http/Middleware/AuthenticateApiKey.php",
    "content": "<?php\n\nnamespace Givebutter\\LaravelKeyable\\Http\\Middleware;\n\nuse Closure;\nuse Givebutter\\LaravelKeyable\\Models\\ApiKey;\nuse Givebutter\\LaravelKeyable\\Events\\KeyableAuthenticated;\n\nclass AuthenticateApiKey\n{\n    /**\n     * Handle an incoming request.\n     *\n     * @param \\Illuminate\\Http\\Request $request\n     * @param Closure                  $next\n     * @param string|null              $guard\n     *\n     * @return mixed\n     */\n    public function handle($request, Closure $next, $guard = null)\n    {\n        $forbidenRequestParams = ['apiKey', 'keyable'];\n\n        // Check if request has forbidden params\n        foreach ($forbidenRequestParams as $param) {\n            if ($request->missing($param)) {\n                continue;\n            }\n\n            $message = \"Request param '{$param}' is not allowed.\";\n\n            if ($request->wantsJson()) {\n                return response()->json(['message' => $message], 400);\n            }\n\n            return response($message, 400);\n        }\n\n        //Get API token from request\n        $token = $this->getKeyFromRequest($request);\n\n        //Check for presence of key\n        if (! $token) {\n            return $this->unauthorizedResponse();\n        }\n\n        //Get API key\n        $apiKey = ApiKey::getByKey($token);\n\n        //Validate key\n        if (! ($apiKey instanceof ApiKey)) {\n            return $this->unauthorizedResponse();\n        }\n\n        //Get the model\n        $keyable = $apiKey->keyable;\n\n        //Validate model\n        if (config('keyable.allow_empty_models', false)) {\n            if (! $keyable && (! is_null($apiKey->keyable_type) || ! is_null($apiKey->keyable_id))) {\n                return $this->unauthorizedResponse();\n            }\n        } else {\n            if (! $keyable) {\n                return $this->unauthorizedResponse();\n            }\n        }\n\n        //Attach the apikey object to the request\n        $request->merge(['apiKey' => $apiKey]);\n        if ($keyable) {\n            $request->merge(['keyable' => $keyable]);\n        }\n\n        //Update last_used_at\n        $apiKey->markAsUsed();\n\n        event(new KeyableAuthenticated($apiKey));\n\n        //Return\n        return $next($request);\n    }\n\n    protected function getKeyFromRequest($request)\n    {\n        $mode = config('keyable.mode', 'bearer');\n\n        switch ($mode) {\n            case 'bearer':\n                return $request->bearerToken();\n                break;\n            case 'header':\n                return $request->header(config('keyable.key', 'X-Authorization'));\n                break;\n            case 'parameter':\n                return $request->input(config('keyable.key', 'api_key'));\n                break;\n        }\n    }\n\n    protected function unauthorizedResponse()\n    {\n        return response([\n            'error' => [\n                'message' => 'Unauthorized',\n            ],\n        ], 401);\n    }\n}\n"
  },
  {
    "path": "src/Http/Middleware/EnforceKeyableScope.php",
    "content": "<?php\n\nnamespace Givebutter\\LaravelKeyable\\Http\\Middleware;\n\nuse Closure;\nuse Illuminate\\Support\\Arr;\nuse Illuminate\\Support\\Reflector;\nuse Illuminate\\Contracts\\Routing\\UrlRoutable;\nuse Illuminate\\Database\\Eloquent\\ModelNotFoundException;\n\nclass EnforceKeyableScope\n{\n    /**\n     * Handle an incoming request.\n     *\n     * @param \\Illuminate\\Http\\Request $request\n     * @param Closure                  $next\n     * @param string|null              $guard\n     *\n     * @return mixed\n     */\n    public function handle($request, Closure $next, $guard = null)\n    {\n        $route = $request->route();\n\n        if (empty($route->parameterNames())) {\n            return $next($request);\n        }\n\n        $parameterName = $route->parameterNames()[0];\n        $parameterValue = $route->originalParameters()[$parameterName];\n        $parameter = Arr::first($route->signatureParameters(UrlRoutable::class));\n        $instance = app(Reflector::getParameterClassName($parameter));\n\n        $childRouteBindingMethod = $route->allowsTrashedBindings()\n                ? 'resolveSoftDeletableChildRouteBinding'\n                : 'resolveChildRouteBinding';\n\n        if (! $request->keyable->{$childRouteBindingMethod}(\n            $parameterName,\n            $parameterValue,\n            $route->bindingFieldFor($parameterName)\n        )) {\n            throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]);\n        }\n\n        return $next($request);\n    }\n\n    protected static function getParameterName($name, $parameters)\n    {\n        if (array_key_exists($name, $parameters)) {\n            return $name;\n        }\n\n        if (array_key_exists($snakedName = Str::snake($name), $parameters)) {\n            return $snakedName;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Keyable.php",
    "content": "<?php\n\nnamespace Givebutter\\LaravelKeyable;\n\nuse Givebutter\\LaravelKeyable\\Models\\ApiKey;\nuse Illuminate\\Database\\Eloquent\\Model;\n\ntrait Keyable\n{\n    public function apiKeys()\n    {\n        return $this->morphMany(ApiKey::class, 'keyable');\n    }\n\n    public function createApiKey(array $attributes = []): NewApiKey\n    {\n        $planTextApiKey = ApiKey::generate();\n\n        $apiKey = Model::withoutEvents(function () use ($planTextApiKey, $attributes) {\n            return $this->apiKeys()->create([\n                'key' => hash('sha256', $planTextApiKey),\n                'name' => $attributes['name'] ?? null,\n            ]);\n        });\n\n        return new NewApiKey($apiKey, \"{$apiKey->getKey()}|{$planTextApiKey}\");\n    }\n}\n"
  },
  {
    "path": "src/KeyableServiceProvider.php",
    "content": "<?php\n\nnamespace Givebutter\\LaravelKeyable;\n\nuse Illuminate\\Routing\\Route;\nuse Illuminate\\Routing\\Router;\nuse Illuminate\\Support\\ServiceProvider;\nuse Illuminate\\Routing\\PendingResourceRegistration;\nuse Givebutter\\LaravelKeyable\\Console\\Commands\\DeleteApiKey;\nuse Givebutter\\LaravelKeyable\\Console\\Commands\\GenerateApiKey;\nuse Givebutter\\LaravelKeyable\\Console\\Commands\\HashApiKeys;\nuse Givebutter\\LaravelKeyable\\Http\\Middleware\\AuthenticateApiKey;\nuse Givebutter\\LaravelKeyable\\Http\\Middleware\\EnforceKeyableScope;\n\nclass KeyableServiceProvider extends ServiceProvider\n{\n    /**\n     * Bootstrap any package services.\n     *\n     * @return void\n     */\n    public function boot(Router $router)\n    {\n        $this->publishes([\n            __DIR__ . '/../config/keyable.php' => config_path('keyable.php'),\n        ]);\n\n        $this->loadMigrationsFrom(__DIR__ . '/../database/migrations');\n\n        $this->registerMiddleware($router);\n\n        $this->registerCommands();\n\n        $this->registerMacros();\n    }\n\n    /**\n     * Register services.\n     *\n     * @return void\n     */\n    public function register()\n    {\n        //\n    }\n\n    protected function registerCommands()\n    {\n        if ($this->app->runningInConsole()) {\n            $this->commands([\n                GenerateApiKey::class,\n                DeleteApiKey::class,\n                HashApiKeys::class,\n            ]);\n        }\n    }\n\n    /**\n     * Register middleware.\n     *\n     * Support added for different Laravel versions\n     *\n     * @param Router $router\n     */\n    protected function registerMiddleware(Router $router)\n    {\n        $versionComparison = version_compare(app()->version(), '5.4.0');\n        if ($versionComparison >= 0) {\n            $router->aliasMiddleware('auth.apikey', AuthenticateApiKey::class);\n            $router->aliasMiddleware('keyableScoped', EnforceKeyableScope::class);\n        } else {\n            $router->middleware('auth.apikey', AuthenticateApiKey::class);\n            $router->middleware('keyableScoped', EnforceKeyableScope::class);\n        }\n    }\n\n    protected function registerMacros()\n    {\n        PendingResourceRegistration::macro('keyableScoped', function () {\n            $this->middleware('keyableScoped');\n\n            return $this;\n        });\n\n        Route::macro('keyableScoped', function () {\n            $this->middleware('keyableScoped');\n\n            return $this;\n        });\n    }\n}\n"
  },
  {
    "path": "src/Models/ApiKey.php",
    "content": "<?php\n\nnamespace Givebutter\\LaravelKeyable\\Models;\n\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Str;\n\nclass ApiKey extends Model\n{\n    use SoftDeletes;\n\n    public ?string $plainTextApiKey = null;\n\n    protected $table = 'api_keys';\n\n    protected $fillable = [\n        'key',\n        'keyable_id',\n        'keyable_type',\n        'name',\n        'last_used_at',\n    ];\n\n    protected $casts = [\n        'last_used_at' => 'datetime',\n    ];\n\n    public static function boot()\n    {\n        parent::boot();\n\n        static::creating(function (ApiKey $apiKey) {\n            if (is_null($apiKey->key)) {\n                $apiKey->plainTextApiKey = self::generate();\n                $apiKey->key = hash('sha256', $apiKey->plainTextApiKey);\n            }\n        });\n    }\n\n    /**\n     * @return \\Illuminate\\Database\\Eloquent\\Relations\\MorphTo\n     */\n    public function keyable()\n    {\n        return $this->morphTo();\n    }\n\n    /**\n     * Generate a secure unique API key.\n     *\n     * @return string\n     */\n    public static function generate()\n    {\n        do {\n            $key = Str::random(40);\n        } while (self::keyExists($key));\n\n        return $key;\n    }\n\n    /**\n     * Get ApiKey record by key value.\n     *\n     * @param string $key\n     *\n     * @return bool\n     */\n    public static function getByKey($key)\n    {\n        return self::ofKey($key)->first();\n    }\n\n    /**\n     * Check if a key already exists.\n     *\n     * Includes soft deleted records\n     *\n     * @param string $key\n     *\n     * @return bool\n     */\n    public static function keyExists($key)\n    {\n        return self::ofKey($key)\n            ->withTrashed()\n            ->first() instanceof self;\n    }\n\n    /**\n     * Mark key as used.\n     */\n    public function markAsUsed()\n    {\n        return $this->forceFill([\n            'last_used_at' => $this->freshTimestamp()\n        ])->save();\n    }\n\n    public function scopeOfKey(Builder $query, string $key): Builder\n    {\n        $compatibilityMode = config('keyable.compatibility_mode', false);\n\n        if ($compatibilityMode) {\n            return $query->where(function (Builder $query) use ($key) {\n                if (! str_contains($key, '|')) {\n                    return $query->where('key', $key)\n                        ->orWhere('key', hash('sha256', $key));\n                }\n\n                [$id, $key] = explode('|', $key, 2);\n\n                return $query\n                    ->where(function (Builder $query) use ($key, $id) {\n                        return $query->where('key', $key)\n                            ->where('id', $id);\n                    })\n                    ->orWhere(function (Builder $query) use ($key, $id) {\n                        return $query->where('key', hash('sha256', $key))\n                            ->where('id', $id);\n                    });\n            });\n        }\n\n        if (! str_contains($key, '|')) {\n            return $query->where('key', hash('sha256', $key));\n        }\n\n        [$id, $key] = explode('|', $key, 2);\n\n        return $query->where('id', $id)\n            ->where('key', hash('sha256', $key));\n    }\n}\n"
  },
  {
    "path": "src/NewApiKey.php",
    "content": "<?php\n\nnamespace Givebutter\\LaravelKeyable;\n\nuse Givebutter\\LaravelKeyable\\Models\\ApiKey;\n\nclass NewApiKey\n{\n    public function __construct(\n        public ApiKey $apiKey,\n        public string $plainTextApiKey,\n    ) {\n        //\n    }\n}\n"
  },
  {
    "path": "tests/Feature/AuthenticateApiKey.php",
    "content": "<?php\n\nnamespace Givebutter\\Tests\\Feature;\n\nuse Givebutter\\LaravelKeyable\\Exceptions\\ForbidenRequestParamException;\nuse Givebutter\\Tests\\TestCase;\nuse Givebutter\\Tests\\Support\\Account;\nuse Illuminate\\Support\\Facades\\Route;\n\nclass AuthenticateApiKey extends TestCase\n{\n    /** @test */\n    public function request_with_api_key_responds_ok()\n    {\n        Route::get(\"/api/posts\", function () {\n            return response('All good', 200);\n        })->middleware(['api', 'auth.apikey']);\n\n        $account = Account::create();\n\n        $this->withHeaders([\n            'Authorization' => 'Bearer ' . $account->createApiKey()->plainTextApiKey,\n        ])->get(\"/api/posts\")->assertOk();\n    }\n\n    /** @test */\n    public function request_with_valid_api_key_without_id_prefix_responds_ok()\n    {\n        Route::get(\"/api/posts\", function () {\n            return response('All good', 200);\n        })->middleware(['api', 'auth.apikey']);\n\n        $account = Account::create();\n        $plainTextApiKey = $account->createApiKey()->plainTextApiKey;\n        [$id, $apiKeyWithoutIdPrefix] = explode('|', $plainTextApiKey);\n\n        $this->assertEquals(\"{$id}|{$apiKeyWithoutIdPrefix}\", $plainTextApiKey);\n\n        $this->withHeaders([\n            'Authorization' => 'Bearer ' . $apiKeyWithoutIdPrefix,\n        ])->get(\"/api/posts\")->assertOk();\n    }\n\n    /** @test */\n    public function request_having_api_key_with_valid_but_mismatched_id_and_key_responds_unauthorized()\n    {\n        Route::get(\"/api/posts\", function () {\n            return response('All good', 200);\n        })->middleware(['api', 'auth.apikey']);\n\n        $account = Account::create();\n        $apiKey1 = $account->createApiKey();\n        $apiKey2 = $account->createApiKey();\n\n        $this->assertDatabaseHas('api_keys', [\n            'id' => $apiKey1->apiKey->id,\n        ]);\n\n        $this->assertDatabaseHas('api_keys', [\n            'id' => $apiKey2->apiKey->id,\n        ]);\n\n        $idFromApiKey1 = explode('|', $apiKey1->plainTextApiKey)[0];\n        $keyFromApiKey2 = explode('|', $apiKey2->plainTextApiKey)[1];\n\n        $mismatchedApiKey = \"{$idFromApiKey1}|{$keyFromApiKey2}\";\n\n        $this->assertNotEquals($mismatchedApiKey, $apiKey1->plainTextApiKey);\n        $this->assertNotEquals($mismatchedApiKey, $apiKey2->plainTextApiKey);\n\n        $this->withHeaders([\n            'Authorization' => 'Bearer ' . $mismatchedApiKey,\n        ])->get(\"/api/posts\")->assertUnauthorized();\n    }\n\n    /** @test */\n    public function request_without_api_key_responds_unauthorized()\n    {\n        Route::get(\"/api/posts\", function () {\n            return response('All good', 200);\n        })->middleware(['api', 'auth.apikey']);\n\n        $this->get(\"/api/posts\")->assertUnauthorized();\n    }\n\n    /**\n     * @test\n     * @dataProvider forbiddenRequestParams\n     */\n    public function throw_exception_if_unauthorized_get_request_has_forbidden_request_query_params(string $queryParam): void\n    {\n        Route::get('/api/posts', function () {\n            return response('All good', 200);\n        })->middleware(['api', 'auth.apikey']);\n\n        $this->get(\"/api/posts?{$queryParam}=value\")\n            ->assertBadRequest()\n            ->assertContent(\"Request param '{$queryParam}' is not allowed.\");\n    }\n\n    /**\n     * @test\n     * @dataProvider forbiddenRequestParams\n     */\n    public function throw_exception_if_unauthorized_post_request_has_forbidden_request_body_params(string $bodyParam): void\n    {\n        Route::post('/api/posts', function () {\n            return response('All good', 200);\n        })->middleware(['api', 'auth.apikey']);\n\n        $this->post('/api/posts', [$bodyParam => 'value'])\n            ->assertBadRequest()\n            ->assertContent(\"Request param '{$bodyParam}' is not allowed.\");\n    }\n\n    /**\n     * @test\n     * @dataProvider forbiddenRequestParams\n     */\n    public function throw_exception_if_unauthorized_json_get_request_has_forbidden_request_query_params(string $queryParam): void\n    {\n        Route::get('/api/posts', function () {\n            return response('All good', 200);\n        })->middleware(['api', 'auth.apikey']);\n\n        $this->getJson(\"/api/posts?{$queryParam}=value\")\n            ->assertBadRequest()\n            ->assertJson(['message' => \"Request param '{$queryParam}' is not allowed.\"]);\n    }\n\n    /**\n     * @test\n     * @dataProvider forbiddenRequestParams\n     */\n    public function throw_exception_if_unauthorized_json_post_request_has_forbidden_request_body_params(string $bodyParam): void\n    {\n        Route::post('/api/posts', function () {\n            return response('All good', 200);\n        })->middleware(['api', 'auth.apikey']);\n\n        $this->postJson('/api/posts', [$bodyParam => 'value'])\n            ->assertBadRequest()\n            ->assertJson(['message' => \"Request param '{$bodyParam}' is not allowed.\"]);\n    }\n\n    public function forbiddenRequestParams(): array\n    {\n        return [\n            ['keyable'],\n            ['apiKey'],\n        ];\n    }\n}\n"
  },
  {
    "path": "tests/Feature/CompatibilityMode.php",
    "content": "<?php\n\nnamespace Givebutter\\Tests\\Feature;\n\nuse Givebutter\\LaravelKeyable\\Models\\ApiKey;\nuse Givebutter\\Tests\\Support\\Account;\nuse Givebutter\\Tests\\Support\\Post;\nuse Givebutter\\Tests\\TestCase;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\Config;\nuse Illuminate\\Support\\Facades\\Route;\n\nclass CompatibilityMode extends TestCase\n{\n    /** @test */\n    public function accepts_both_hashed_and_non_hashed_api_keys_when_compatibility_mode_is_on()\n    {\n        Route::get(\"/api/posts/{post}\", function (Request $request, Post $post) {\n            return response('All good', 200);\n        })->middleware(['api', 'auth.apikey'])->keyableScoped();\n\n        $account = Account::create();\n        $post = $account->posts()->create();\n\n        // Store the first api key as non hashed\n        $plainTextApiKey1 = ApiKey::generate();\n        $apiKey1 = Model::withoutEvents(function () use ($plainTextApiKey1, $account) {\n            return ApiKey::create([\n                'keyable_id' => $account->getKey(),\n                'keyable_type' => Account::class,\n                'key' => $plainTextApiKey1,\n            ]);\n        });\n\n        // Store the second api key as non hashed\n        $plainTextApiKey2 = ApiKey::generate();\n        $apiKey2 = Model::withoutEvents(function () use ($plainTextApiKey2, $account) {\n            return ApiKey::create([\n                'keyable_id' => $account->getKey(),\n                'keyable_type' => Account::class,\n                'key' => $plainTextApiKey2,\n            ]);\n        });\n\n        $this->assertDatabaseCount('api_keys', 2);\n        $this->assertDatabaseHas('api_keys', [\n            'id' => $apiKey1->getKey(),\n            'key' => $plainTextApiKey1,\n        ]);\n        $this->assertDatabaseHas('api_keys', [\n            'id' => $apiKey2->getKey(),\n            'key' => $plainTextApiKey2,\n        ]);\n\n        // Ensure compatibility mode is on\n        Config::set('keyable.compatibility_mode', true);\n\n        // Hash only the second api key\n        $this->artisan('api-key:hash', [\n            '--id' => $apiKey2->getKey(),\n        ]);\n\n        $this->assertDatabaseCount('api_keys', 2);\n        $this->assertDatabaseHas('api_keys', [\n            'id' => $apiKey1->getKey(),\n            'key' => $plainTextApiKey1,\n        ]);\n        $this->assertDatabaseHas('api_keys', [\n            'id' => $apiKey2->getKey(),\n            'key' => $apiKey2->fresh()->key,\n        ]);\n\n        // Assert that non hashed api keys works\n        $this->withHeaders([\n            'Authorization' => \"Bearer {$plainTextApiKey1}\",\n        ])->get(\"/api/posts/{$post->id}\")->assertOk();\n\n        // Assert that non hashed api keys with ID prefix works\n        $this->withHeaders([\n            'Authorization' => \"Bearer {$apiKey1->id}|{$plainTextApiKey1}\",\n        ])->get(\"/api/posts/{$post->id}\")->assertOk();\n\n        // Assert that hashed api keys works\n        $this->withHeaders([\n            'Authorization' => \"Bearer {$plainTextApiKey2}\",\n        ])->get(\"/api/posts/{$post->id}\")->assertOk();\n\n        // Assert that hashed api keys with ID prefix works\n        $this->withHeaders([\n            'Authorization' => \"Bearer {$apiKey2->id}|{$plainTextApiKey2}\",\n        ])->get(\"/api/posts/{$post->id}\")->assertOk();\n    }\n}\n"
  },
  {
    "path": "tests/Feature/EnforceKeyableScope.php",
    "content": "<?php\n\nnamespace Givebutter\\Tests\\Feature;\n\nuse Illuminate\\Http\\Request;\nuse Givebutter\\Tests\\TestCase;\nuse Givebutter\\Tests\\Support\\Post;\nuse Givebutter\\Tests\\Support\\Account;\nuse Illuminate\\Support\\Facades\\Route;\nuse Givebutter\\Tests\\Support\\PostsController;\nuse Givebutter\\Tests\\Support\\CommentsController;\n\nclass EnforceKeyableScope extends TestCase\n{\n    /** @test */\n    public function request_with_parameter_must_be_owned_by_keyable()\n    {\n        Route::get(\"/api/posts/{post}\", function (Request $request, Post $post) {\n            return response('All good', 200);\n        })->middleware(['api', 'auth.apikey'])->keyableScoped();\n\n        $account = Account::create();\n        $post = $account->posts()->create();\n\n        $this->withHeaders([\n            'Authorization' => 'Bearer ' . $account->createApiKey()->plainTextApiKey,\n        ])->get(\"/api/posts/{$post->id}\")->assertOk();\n    }\n\n    /** @test */\n    public function request_with_model_not_owned_by_keyable_throws_model_not_found()\n    {\n        Route::get(\"/api/posts/{post}\", function (Request $request, Post $post) {\n            return response('All good', 200);\n        })->middleware([ 'api', 'auth.apikey'])->keyableScoped();\n\n        $account = Account::create();\n        $account2 = Account::create();\n        $post = $account2->posts()->create();\n\n        $this->withHeaders([\n            'Authorization' => 'Bearer ' . $account->createApiKey()->plainTextApiKey,\n        ])->get(\"/api/posts/{$post->id}\")->assertNotFound();\n    }\n\n    /** @test */\n    public function works_with_resource_routes()\n    {\n        Route::prefix('api')->middleware(['api', 'auth.apikey'])->group(function () {\n            Route::apiResource('posts', PostsController::class)\n                ->only('show')\n                ->keyableScoped();\n        });\n\n        /*\n        | --------------------------------\n        | PASSING\n        | --------------------------------\n        */\n        $account = Account::create();\n        $post = $account->posts()->create();\n\n        $this->withHeaders([\n            'Authorization' => 'Bearer ' . $account->createApiKey()->plainTextApiKey,\n        ])->get(\"/api/posts/{$post->id}\")->assertOk();\n\n        /*\n        | --------------------------------\n        | FAILING\n        | --------------------------------\n        */\n        $account2 = Account::create();\n        $post = $account2->posts()->create();\n\n        $this->withHeaders([\n            'Authorization' => 'Bearer ' . $account->createApiKey()->plainTextApiKey,\n        ])->get(\"/api/posts/{$post->id}\")->assertNotFound();\n    }\n\n    /** @test */\n    public function can_use_scoped_with_keyableScoped()\n    {\n        Route::middleware(['api', 'auth.apikey'])->group(function () {\n            Route::apiResource('posts.comments', CommentsController::class)\n                ->only('show')\n                ->scoped()\n                ->keyableScoped();\n        });\n\n        /*\n        | --------------------------------\n        | PASSING\n        | --------------------------------\n        */\n        $account = Account::create();\n        $post = $account->posts()->create();\n        $comment = $post->comments()->create();\n\n        $this->withHeaders([\n            'Authorization' => 'Bearer ' . $account->createApiKey()->plainTextApiKey,\n        ])->get(\"posts/{$post->id}/comments/{$comment->id}\")->assertOk();\n\n        /*\n        | --------------------------------\n        | FAILING\n        | --------------------------------\n        */\n        $account2 = Account::create();\n        $post2 = $account2->posts()->create();\n        $comment2 = $post2->comments()->create();\n\n        $this->withHeaders([\n            'Authorization' => 'Bearer ' . $account->createApiKey()->plainTextApiKey,\n        ])->get(\"posts/{$post->id}/comments/{$comment2->id}\")->assertNotFound();\n    }\n}\n"
  },
  {
    "path": "tests/Support/Account.php",
    "content": "<?php\n\nnamespace Givebutter\\Tests\\Support;\n\nuse Givebutter\\LaravelKeyable\\Keyable;\nuse Illuminate\\Database\\Eloquent\\Model;\n\nclass Account extends Model\n{\n    use Keyable;\n\n    public function posts()\n    {\n        return $this->hasMany(Post::class);\n    }\n}\n"
  },
  {
    "path": "tests/Support/Comment.php",
    "content": "<?php\n\nnamespace Givebutter\\Tests\\Support;\n\nuse Givebutter\\Tests\\Support\\Post;\nuse Illuminate\\Database\\Eloquent\\Model;\n\nclass Comment extends Model\n{\n    public function post()\n    {\n        return $this->belongsTo(Post::class);\n    }\n}\n"
  },
  {
    "path": "tests/Support/CommentsController.php",
    "content": "<?php\n\nnamespace Givebutter\\Tests\\Support;\n\nuse Illuminate\\Http\\Request;\nuse Givebutter\\Tests\\Support\\Post;\n\nclass CommentsController\n{\n    public function show(Request $request, Post $post, Comment $comment)\n    {\n        return response('All good', 200);\n    }\n}\n"
  },
  {
    "path": "tests/Support/Migrations/create_test_tables.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Migrations\\Migration;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nclass CreateTestTables extends Migration\n{\n    public function up()\n    {\n        Schema::create('accounts', function (Blueprint $table) {\n            $table->increments('id');\n            $table->timestamps();\n        });\n\n        Schema::create('posts', function (Blueprint $table) {\n            $table->increments('id');\n            $table->foreignId('account_id')->constrained();\n            $table->timestamps();\n        });\n\n        Schema::create('comments', function (Blueprint $table) {\n            $table->increments('id');\n            $table->foreignId('post_id')->constrained();\n            $table->timestamps();\n        });\n    }\n\n    public function down()\n    {\n        Schema::dropIfExists('accounts');\n        Schema::dropIfExists('posts');\n        Schema::dropIfExists('comments');\n    }\n}\n"
  },
  {
    "path": "tests/Support/Post.php",
    "content": "<?php\n\nnamespace Givebutter\\Tests\\Support;\n\nuse Givebutter\\Tests\\Support\\Account;\nuse Givebutter\\Tests\\Support\\Comment;\nuse Illuminate\\Database\\Eloquent\\Model;\n\nclass Post extends Model\n{\n    public function account()\n    {\n        return $this->belongsTo(Account::class);\n    }\n\n    public function comments()\n    {\n        return $this->hasMany(Comment::class);\n    }\n}\n"
  },
  {
    "path": "tests/Support/PostsController.php",
    "content": "<?php\n\nnamespace Givebutter\\Tests\\Support;\n\nuse Illuminate\\Http\\Request;\nuse Givebutter\\Tests\\Support\\Post;\n\nclass PostsController\n{\n    public function show(Request $request, Post $post)\n    {\n        return response('All good', 200);\n    }\n}\n"
  },
  {
    "path": "tests/TestCase.php",
    "content": "<?php\n\nnamespace Givebutter\\Tests;\n\nuse Illuminate\\Support\\Str;\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Givebutter\\LaravelKeyable\\KeyableServiceProvider;\nuse Orchestra\\Testbench\\TestCase as OrchestraTestCase;\n\nclass TestCase extends OrchestraTestCase\n{\n    public function setUp(): void\n    {\n        parent::setUp();\n\n        Factory::guessFactoryNamesUsing(function (string $modelName) {\n            $namespace = 'Database\\\\Factories\\\\';\n\n            $modelName = Str::afterLast($modelName, '\\\\');\n\n            return $namespace.$modelName.'Factory';\n        });\n\n        $this->setUpDatabase($this->app);\n    }\n\n    protected function getPackageProviders($app)\n    {\n        return [\n            KeyableServiceProvider::class,\n        ];\n    }\n\n    protected function getEnvironmentSetUp($app)\n    {\n        // Setup default database to use sqlite :memory:\n        $app['config']->set('database.default', 'testbench');\n        $app['config']->set('database.connections.testbench', [\n            'driver'   => 'sqlite',\n            'database' => ':memory:',\n            'prefix'   => '',\n        ]);\n    }\n\n    protected function setUpDatabase($app)\n    {\n        $app['db']->connection()->getSchemaBuilder()->create('test_models', function (Blueprint $table) {\n            $table->increments('id');\n            $table->timestamps();\n        });\n\n        $this->prepareDatabaseForHasCustomFieldsModel();\n        $this->runMigrationStub();\n    }\n\n    protected function runMigrationStub()\n    {\n        include_once __DIR__ . '/../database/migrations/2019_04_09_225232_create_api_keys_table.php';\n        (new \\CreateApiKeysTable())->up();\n    }\n\n    protected function prepareDatabaseForHasCustomFieldsModel()\n    {\n        include_once __DIR__ . '/../tests/Support/Migrations/create_test_tables.php';\n        (new \\CreateTestTables())->up();\n    }\n\n    protected function resetDatabase()\n    {\n        $this->artisan('migrate:fresh');\n        $this->runMigrationStub();\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Console/Commands/DeleteApiKey.php",
    "content": "<?php\n\nnamespace Givebutter\\Tests\\Unit\\Console\\Commands;\n\nuse Givebutter\\Tests\\Support\\Account;\nuse Givebutter\\Tests\\TestCase;\n\nclass DeleteApiKey extends TestCase\n{\n    /** @test */\n    public function delete_api_key(): void\n    {\n        $account = Account::create();\n        $apiKey = $account->apiKeys()->create();\n\n        $this->assertNotSoftDeleted($apiKey);\n\n        $this->artisan('api-key:delete', [\n            '--id' => $apiKey->getKey()\n        ]);\n\n        $this->assertSoftDeleted($apiKey);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Console/Commands/GenerateApiKey.php",
    "content": "<?php\n\nnamespace Givebutter\\Tests\\Unit\\Console\\Commands;\n\nuse Givebutter\\Tests\\Support\\Account;\nuse Givebutter\\Tests\\TestCase;\nuse Illuminate\\Support\\Facades\\Artisan;\n\nclass GenerateApiKey extends TestCase\n{\n    /** @test */\n    public function generate_api_key(): void\n    {\n        // Arrange\n        $account = Account::create();\n\n        $this->assertDatabaseEmpty('api_keys');\n\n        // Act\n        $this->withoutMockingConsoleOutput()\n            ->artisan('api-key:generate', [\n                '--id' => $account->getKey(),\n                '--type' => Account::class,\n                '--name' => 'my api key',\n            ]);\n\n        // Assert\n        $output = Artisan::output();\n        $generatedKey = explode('|', $output, 2)[1];\n        $generatedKey = str_replace(\"\\n\", '', $generatedKey);\n\n        $this->assertDatabaseHas('api_keys', [\n            'key' => hash('sha256', $generatedKey),\n            'keyable_id' => $account->getKey(),\n            'keyable_type' => Account::class,\n            'name' => 'my api key',\n        ]);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Console/Commands/HashApiKeys.php",
    "content": "<?php\n\nnamespace Givebutter\\Tests\\Unit\\Console\\Commands;\n\nuse Givebutter\\LaravelKeyable\\Models\\ApiKey;\nuse Givebutter\\Tests\\Support\\Account;\nuse Givebutter\\Tests\\TestCase;\nuse Illuminate\\Database\\Eloquent\\Model;\n\nclass HashApiKeys extends TestCase\n{\n    /** @test */\n    public function hash_api_keys(): void\n    {\n        // Arrange\n        $account = Account::create();\n\n        $plainTextApiKey1 = ApiKey::generate();\n        $apiKeyNotHashed1 = Model::withoutEvents(function () use ($plainTextApiKey1, $account) {\n            return ApiKey::create([\n                'keyable_id' => $account->getKey(),\n                'keyable_type' => Account::class,\n                'key' => $plainTextApiKey1,\n            ]);\n        });\n\n        $plainTextApiKey2 = ApiKey::generate();\n        $apiKeyNotHashed2 = Model::withoutEvents(function () use ($plainTextApiKey2, $account) {\n            return ApiKey::create([\n                'keyable_id' => $account->getKey(),\n                'keyable_type' => Account::class,\n                'key' => $plainTextApiKey2,\n            ]);\n        });\n\n        $this->assertDatabaseCount('api_keys', 2);\n\n        $this->assertEquals($plainTextApiKey1, $apiKeyNotHashed1->key);\n        $this->assertEquals($plainTextApiKey2, $apiKeyNotHashed2->key);\n\n        $this->assertDatabaseHas('api_keys', [\n            'id' => $apiKeyNotHashed1->id,\n            'key' => $plainTextApiKey1,\n        ]);\n\n        $this->assertDatabaseHas('api_keys', [\n            'id' => $apiKeyNotHashed2->id,\n            'key' => $plainTextApiKey2,\n        ]);\n\n        // Act\n        $this->artisan('api-key:hash');\n\n        // Assert\n        $this->assertDatabaseCount('api_keys', 2);\n\n        $this->assertDatabaseHas('api_keys', [\n            'id' => $apiKeyNotHashed1->id,\n            'key' => hash('sha256', $plainTextApiKey1),\n        ]);\n\n        $this->assertDatabaseHas('api_keys', [\n            'id' => $apiKeyNotHashed2->id,\n            'key' => hash('sha256', $plainTextApiKey2),\n        ]);\n    }\n\n    /** @test */\n    public function hash_one_api_key_at_a_time(): void\n    {\n        // Arrange\n        $account = Account::create();\n\n        $plainTextApiKey1 = ApiKey::generate();\n        $apiKeyNotHashed1 = Model::withoutEvents(function () use ($plainTextApiKey1, $account) {\n            return ApiKey::create([\n                'keyable_id' => $account->getKey(),\n                'keyable_type' => Account::class,\n                'key' => $plainTextApiKey1,\n            ]);\n        });\n\n        $plainTextApiKey2 = ApiKey::generate();\n        $apiKeyNotHashed2 = Model::withoutEvents(function () use ($plainTextApiKey2, $account) {\n            return ApiKey::create([\n                'keyable_id' => $account->getKey(),\n                'keyable_type' => Account::class,\n                'key' => $plainTextApiKey2,\n            ]);\n        });\n\n        $this->assertDatabaseCount('api_keys', 2);\n\n        $this->assertEquals($plainTextApiKey1, $apiKeyNotHashed1->key);\n        $this->assertEquals($plainTextApiKey2, $apiKeyNotHashed2->key);\n\n        $this->assertDatabaseHas('api_keys', [\n            'id' => $apiKeyNotHashed1->id,\n            'key' => $plainTextApiKey1,\n        ]);\n\n        $this->assertDatabaseHas('api_keys', [\n            'id' => $apiKeyNotHashed2->id,\n            'key' => $plainTextApiKey2,\n        ]);\n\n        // Act\n        $this->artisan('api-key:hash', [\n            '--id' => $apiKeyNotHashed1->id,\n        ]);\n\n        // Assert\n        $this->assertDatabaseCount('api_keys', 2);\n\n        $this->assertDatabaseHas('api_keys', [\n            'id' => $apiKeyNotHashed1->id,\n            'key' => hash('sha256', $plainTextApiKey1),\n        ]);\n\n        $this->assertDatabaseHas('api_keys', [\n            'id' => $apiKeyNotHashed2->id,\n            'key' => $plainTextApiKey2,\n        ]);\n    }\n\n    /** @test */\n    public function api_key_is_not_hashed_more_than_once(): void\n    {\n        // Arrange\n        $account = Account::create();\n\n        $plainTextApiKey = ApiKey::generate();\n        $apiKey = Model::withoutEvents(function () use ($plainTextApiKey, $account) {\n            return ApiKey::create([\n                'keyable_id' => $account->getKey(),\n                'keyable_type' => Account::class,\n                'key' => $plainTextApiKey,\n            ]);\n        });\n\n        $this->assertDatabaseHas('api_keys', [\n            'id' => $apiKey->id,\n            'key' => $plainTextApiKey,\n        ]);\n\n        // Act 1\n        $this->artisan('api-key:hash', [\n            '--id' => $apiKey->id,\n        ]);\n\n        // Assert 1\n        $this->assertDatabaseHas('api_keys', [\n            'id' => $apiKey->id,\n            'key' => hash('sha256', $plainTextApiKey),\n        ]);\n\n        // Act 2\n        $this->artisan('api-key:hash', [\n            '--id' => $apiKey->id,\n        ]);\n\n        // Assert 2\n        $this->assertDatabaseHas('api_keys', [\n            'id' => $apiKey->id,\n            'key' => hash('sha256', $plainTextApiKey),\n        ]);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Models/ApiKeyTest.php",
    "content": "<?php\n\nnamespace Givebutter\\Tests\\Unit\\Models;\n\nuse Givebutter\\LaravelKeyable\\Models\\ApiKey;\nuse Givebutter\\Tests\\Support\\Account;\nuse Givebutter\\Tests\\TestCase;\n\nclass ApiKeyTest extends TestCase\n{\n    /** @test */\n    public function create_new_api_key(): void\n    {\n        $account = Account::create();\n\n        $apiKey = ApiKey::create([\n            'keyable_id' => $account->getKey(),\n            'keyable_type' => Account::class,\n            'name' => 'my api key',\n        ]);\n\n        $this->assertDatabaseHas('api_keys', [\n            'key' => hash('sha256', $apiKey->plainTextApiKey),\n            'keyable_id' => $account->getKey(),\n            'keyable_type' => Account::class,\n            'name' => 'my api key',\n        ]);\n    }\n}\n"
  }
]