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') + +

Profile / Settings / API Keys

+ +@stop + +@section('js') + +@stop + +@section('content') + + + +
+ @csrf + +
+ + +
+ +
+ + + + + +
+ +
+
+
+

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.

+
+
+
+ + @if (session()->has('finalKey')) +
+
+
+

This is your API key: {{ session('finalKey') }}

+

Please copy it now as it'll only appear once.

+
+
+
+ @endif + +
+
+ + + + + @if(!$keys->isEmpty()) + + + + + + + + + + + + + + @foreach($keys as $key) + + + + + + + + @endforeach + + +
Key nameStatusLast UsedLast ModifiedActions
{{ $key->name }}{{ ($key->status == 'disabled') ? 'Revoked' : 'Active' }}{{ ($key->last_used == null) ? 'No recent activity' : $key->last_used }}{{ $key->updated_at }} + @if ($key->status == 'active') +
+ @csrf + @method('PATCH') + +
+ @endif +
+ @csrf + @method('DELETE') + +
+
+ @else +
+

You don't have any API keys yet.

+
+ @endif + + + + + +
+
+
+ +@stop + + +@section('footer') + @include('breadcrumbs.dashboard.footer') +@stop diff --git a/routes/web.php b/routes/web.php index b13425b..3132a3b 100755 --- a/routes/web.php +++ b/routes/web.php @@ -19,6 +19,7 @@ * along with Raspberry Staff Manager. If not, see . */ +use App\Http\Controllers\ApiKeyController; use App\Http\Controllers\ApplicationController; use App\Http\Controllers\AppointmentController; use App\Http\Controllers\Auth\TwofaController; @@ -68,11 +69,11 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['lo ->name('processDeleteConfirmation'); Route::group(['middleware' => ['auth', 'forcelogout', 'passwordexpiration', '2fa', 'verified']], function () { - + Route::group(['middleware' => ['passwordredirect']], function(){ - + Route::get('/dashboard', [DashboardController::class, 'index']) ->name('dashboard') ->middleware('eligibility'); @@ -107,7 +108,7 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['lo Route::get('team/files/{teamFile}/download', [TeamFileController::class, 'download']) ->name('downloadTeamFile'); - + }); Route::group(['prefix' => '/applications', 'middleware' => ['passwordredirect']], function () { @@ -163,6 +164,13 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['lo ->name('showProfileSettings') ->middleware('passwordredirect'); + Route::resource('keys', ApiKeyController::class) + ->middleware('passwordredirect'); + + Route::patch('keys/revoke/{key}', [ApiKeyController::class, 'revokeKey']) + ->name('revokeKey') + ->middleware('passwordredirect'); + Route::patch('/settings/save', [ProfileController::class, 'saveProfile']) ->name('saveProfileSettings') ->middleware('passwordredirect');