API key management interface
This commit is contained in:
parent
6d94263ede
commit
99779c9053
25
app/ApiKey.php
Normal file
25
app/ApiKey.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ApiKey extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'status',
|
||||
'discriminator',
|
||||
'last_used',
|
||||
'secret',
|
||||
'owner_user_id'
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo('App\User', 'id');
|
||||
}
|
||||
}
|
103
app/Http/Controllers/ApiKeyController.php
Normal file
103
app/Http/Controllers/ApiKeyController.php
Normal file
@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\ApiKey;
|
||||
use App\Http\Requests\CreateApiKeyRequest;
|
||||
use App\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class ApiKeyController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return view('dashboard.user.api.index')
|
||||
->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.');
|
||||
}
|
||||
}
|
30
app/Http/Requests/CreateApiKeyRequest.php
Normal file
30
app/Http/Requests/CreateApiKeyRequest.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CreateApiKeyRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'keyName' => 'required|string'
|
||||
];
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App;
|
||||
use App\Helpers\JSON;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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'],
|
||||
|
42
database/migrations/2021_03_29_224932_api_keys.php
Normal file
42
database/migrations/2021_03_29_224932_api_keys.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class ApiKeys extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
// API keys can only access resources the owner's account can access
|
||||
Schema::create('api_keys', function (Blueprint $table) {
|
||||
|
||||
$table->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()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddNameToApiKeys extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('api_keys', function (Blueprint $table) {
|
||||
$table->string('name')->after('secret');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('api_keys', function (Blueprint $table) {
|
||||
$table->dropColumn('name');
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddLastUsedToApiKeys extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('api_keys', function (Blueprint $table) {
|
||||
$table->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();
|
||||
});
|
||||
}
|
||||
}
|
118
resources/views/dashboard/user/api/index.blade.php
Normal file
118
resources/views/dashboard/user/api/index.blade.php
Normal file
@ -0,0 +1,118 @@
|
||||
@extends('adminlte::page')
|
||||
|
||||
@section('title', config('app.name') . ' | ' . __('API Key Management'))
|
||||
|
||||
@section('content_header')
|
||||
|
||||
<h4>Profile / Settings / API Keys</h4>
|
||||
|
||||
@stop
|
||||
|
||||
@section('js')
|
||||
<x-global-errors></x-global-errors>
|
||||
@stop
|
||||
|
||||
@section('content')
|
||||
|
||||
<x-modal id="createKeyModal" modal-label="createKeyModalLabel" modal-title="New API Key" include-close-button="true">
|
||||
|
||||
<form id="createKey" method="post" action="{{ route('keys.store') }}">
|
||||
@csrf
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">Give your new API key a name to easily identify it.</label>
|
||||
<input type="text" name="keyName" class="form-control" id="name" required>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<x-slot name="modalFooter">
|
||||
<button onclick="$('#createKey').submit()" type="button" class="btn btn-success"><i class="fas fa-key"></i> Register new key</button>
|
||||
</x-slot>
|
||||
|
||||
</x-modal>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="alert alert-warning">
|
||||
<p><i class="fas fa-exclamation-triangle"></i> <b>Friendly reminder: </b> 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.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (session()->has('finalKey'))
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="alert alert-success">
|
||||
<p><i class="fas fa-key"></i> This is your API key: {{ session('finalKey') }}</p>
|
||||
<p>Please copy it <b>now</b> as it'll only appear once.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<x-card id="keyListing" card-title="Manage API Keys" footer-style="text-center">
|
||||
|
||||
<x-slot name="cardHeader"></x-slot>
|
||||
|
||||
@if(!$keys->isEmpty())
|
||||
<table class="table table-borderless">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key name</th>
|
||||
<th>Status</th>
|
||||
<th>Last Used</th>
|
||||
<th>Last Modified</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
|
||||
@foreach($keys as $key)
|
||||
<tr>
|
||||
<td>{{ $key->name }}</td>
|
||||
<td><span class="badge badge-{{ ($key->status == 'disabled') ? 'danger' : 'primary' }}">{{ ($key->status == 'disabled') ? 'Revoked' : 'Active' }}</span></td>
|
||||
<td><span class="badge badge-{{ ($key->last_used == null) ? 'danger' : 'primary' }}">{{ ($key->last_used == null) ? 'No recent activity' : $key->last_used }}</span></td>
|
||||
<td><span class="badge badge-primary">{{ $key->updated_at }}</span></td>
|
||||
<td>
|
||||
@if ($key->status == 'active')
|
||||
<form class="d-inline-block" action="{{ route('revokeKey', ['key' => $key->id]) }}" method="post">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
<button type="submit" class="btn btn-danger btn-sm ml-2"><i class="fas fa-lock"></i> Revoke</button>
|
||||
</form>
|
||||
@endif
|
||||
<form class="d-inline-block" action="{{ route('keys.destroy', ['key' => $key->id]) }}" method="post">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-danger btn-sm ml-2"><i class="fas fa-trash"></i> Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
@else
|
||||
<div class="alert alert-success">
|
||||
<p><i class="fa fa-info-circle"></i> You don't have any API keys yet.</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<x-slot name="cardFooter">
|
||||
<button onclick="$('#createKeyModal').modal('show')" type="button" class="btn btn-secondary"><i class="fas fa-plus"></i> New API Key</button>
|
||||
</x-slot>
|
||||
|
||||
</x-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@stop
|
||||
|
||||
|
||||
@section('footer')
|
||||
@include('breadcrumbs.dashboard.footer')
|
||||
@stop
|
@ -19,6 +19,7 @@
|
||||
* along with Raspberry Staff Manager. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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');
|
||||
|
Loading…
Reference in New Issue
Block a user