diff --git a/app/ApiKey.php b/app/ApiKey.php new file mode 100644 index 0000000..6a97f85 --- /dev/null +++ b/app/ApiKey.php @@ -0,0 +1,25 @@ +belongsTo('App\User', 'id'); + } +} diff --git a/app/Http/Controllers/ApiKeyController.php b/app/Http/Controllers/ApiKeyController.php new file mode 100644 index 0000000..0975df9 --- /dev/null +++ b/app/Http/Controllers/ApiKeyController.php @@ -0,0 +1,103 @@ +with('keys', Auth::user()->keys); + } + + /** + * Store a newly created resource in storage. + * + * @param \Illuminate\Http\Request $request + */ + public function store(CreateApiKeyRequest $request) + { + $discriminator = "#" . bin2hex(openssl_random_pseudo_bytes(7)); + $secret = bin2hex(openssl_random_pseudo_bytes(32)); + + $key = ApiKey::create([ + 'name' => $request->keyName, + 'discriminator' => $discriminator, + 'secret' => Hash::make($secret), + 'status' => 'active', + 'owner_user_id' => Auth::user()->id + ]); + + if ($key) + { + $request->session()->flash('success', 'Key successfully registered!'); + $request->session()->flash('finalKey', $discriminator . '.' . $secret); + + return redirect() + ->back(); + } + + return redirect() + ->back() + ->with('error', 'An error occurred whilst trying to create an API key.'); + } + + + public function revokeKey(Request $request, ApiKey $key) + { + if (Auth::user()->is($key->user) || Auth::user()->hasRole('admin')) + { + if ($key->status == 'active') + { + $key->status = 'disabled'; + $key->save(); + } + else + { + return redirect() + ->back() + ->with('error', 'Key already revoked.'); + } + + return redirect() + ->back() + ->with('success', 'Key revoked. Apps using this key will stop working.'); + } + + return redirect() + ->back() + ->with('error', 'You do not have permission to modify this key.'); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy($id) + { + $key = ApiKey::findOrFail($id); + + if (Auth::user()->is($key->user) || Auth::user()->hasRole('admin')) + { + $key->delete(); + + return redirect() + ->back() + ->with('success', 'Key deleted successfully. Apps using this key will stop working.'); + } + + return redirect() + ->back() + ->with('error', 'You do not have permission to modify this key.'); + } +} diff --git a/app/Http/Requests/CreateApiKeyRequest.php b/app/Http/Requests/CreateApiKeyRequest.php new file mode 100644 index 0000000..d7bf22c --- /dev/null +++ b/app/Http/Requests/CreateApiKeyRequest.php @@ -0,0 +1,30 @@ + 'required|string' + ]; + } +} diff --git a/app/Providers/JSONProvider.php b/app/Providers/JSONProvider.php index 6403201..b5ef403 100644 --- a/app/Providers/JSONProvider.php +++ b/app/Providers/JSONProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use App; use App\Helpers\JSON; use Illuminate\Support\ServiceProvider; diff --git a/app/User.php b/app/User.php index a39fb5e..e3907ba 100755 --- a/app/User.php +++ b/app/User.php @@ -92,6 +92,11 @@ class User extends Authenticatable implements MustVerifyEmail return $this->hasMany('App\TeamFile', 'uploaded_by'); } + public function keys() + { + return $this->hasMany('App\ApiKey', 'owner_user_id'); + } + // UTILITY LOGIC public function isBanned() diff --git a/config/adminlte.php b/config/adminlte.php index af5ad9e..35c930a 100755 --- a/config/adminlte.php +++ b/config/adminlte.php @@ -268,6 +268,11 @@ return [ 'icon' => 'fas fa-user-circle', 'url' => '/profile/settings/account', ], + [ + 'text' => 'Programmatic Access', + 'icon' => 'fas fa-wrench', + 'route' => 'keys.index' + ], [ 'header' => 'h_app_management', 'can' => ['applications.view.all', 'applications.vote'], diff --git a/database/migrations/2021_03_29_224932_api_keys.php b/database/migrations/2021_03_29_224932_api_keys.php new file mode 100644 index 0000000..43a990d --- /dev/null +++ b/database/migrations/2021_03_29_224932_api_keys.php @@ -0,0 +1,42 @@ +id(); + $table->string('discriminator'); + $table->string('secret'); + $table->enum('status', ['disabled', 'active']); + $table->bigInteger('owner_user_id')->unsigned(); + + $table->foreign('owner_user_id') + ->references('id') + ->on('users') + ->cascadeOnDelete() + ->cascadeOnUpdate(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/database/migrations/2021_03_29_231944_add_name_to_api_keys.php b/database/migrations/2021_03_29_231944_add_name_to_api_keys.php new file mode 100644 index 0000000..3861e08 --- /dev/null +++ b/database/migrations/2021_03_29_231944_add_name_to_api_keys.php @@ -0,0 +1,32 @@ +string('name')->after('secret'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('api_keys', function (Blueprint $table) { + $table->dropColumn('name'); + }); + } +} diff --git a/database/migrations/2021_03_29_232842_add_last_used_to_api_keys.php b/database/migrations/2021_03_29_232842_add_last_used_to_api_keys.php new file mode 100644 index 0000000..9feba69 --- /dev/null +++ b/database/migrations/2021_03_29_232842_add_last_used_to_api_keys.php @@ -0,0 +1,34 @@ +dateTime('last_used')->after('owner_user_id')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('api_keys', function (Blueprint $table) { + $table->dropColumn('last_used'); + $table->dropTimestamps(); + }); + } +} diff --git a/resources/views/dashboard/user/api/index.blade.php b/resources/views/dashboard/user/api/index.blade.php new file mode 100644 index 0000000..4c2fe06 --- /dev/null +++ b/resources/views/dashboard/user/api/index.blade.php @@ -0,0 +1,118 @@ +@extends('adminlte::page') + +@section('title', config('app.name') . ' | ' . __('API Key Management')) + +@section('content_header') + +
Friendly reminder: API keys can access your whole account and the resources it has access to. Please treat them like a password. If they are leaked, please revoke them.
+This is your API key: {{ session('finalKey') }}
+Please copy it now as it'll only appear once.
+Key name | +Status | +Last Used | +Last Modified | +Actions | +
---|---|---|---|---|
{{ $key->name }} | +{{ ($key->status == 'disabled') ? 'Revoked' : 'Active' }} | +{{ ($key->last_used == null) ? 'No recent activity' : $key->last_used }} | +{{ $key->updated_at }} | ++ @if ($key->status == 'active') + + @endif + + | +
You don't have any API keys yet.
+