Built out interfaces & endpoints for API token managment
This commit is contained in:
parent
d336ba6874
commit
dccb279c84
14 changed files with 325 additions and 6 deletions
|
@ -5,5 +5,7 @@ use Illuminate\Database\Eloquent\Model;
|
|||
class ApiToken extends Model
|
||||
{
|
||||
protected $fillable = ['name', 'expires_at'];
|
||||
|
||||
protected $casts = [
|
||||
'expires_at' => 'datetime:Y-m-d'
|
||||
];
|
||||
}
|
||||
|
|
|
@ -194,6 +194,7 @@ class UserRepo
|
|||
public function destroy(User $user)
|
||||
{
|
||||
$user->socialAccounts()->delete();
|
||||
$user->apiTokens()->delete();
|
||||
$user->delete();
|
||||
|
||||
// Delete user profile images
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Api\ApiToken;
|
||||
use BookStack\Auth\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class UserApiTokenController extends Controller
|
||||
{
|
||||
|
@ -10,11 +15,121 @@ class UserApiTokenController extends Controller
|
|||
*/
|
||||
public function create(int $userId)
|
||||
{
|
||||
// Ensure user is has access-api permission and is the current user or has permission to manage the current user.
|
||||
$this->checkPermission('access-api');
|
||||
$this->checkPermissionOrCurrentUser('manage-users', $userId);
|
||||
|
||||
// TODO - Form
|
||||
return 'test';
|
||||
$user = User::query()->findOrFail($userId);
|
||||
return view('users.api-tokens.create', [
|
||||
'user' => $user,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new API token in the system.
|
||||
*/
|
||||
public function store(Request $request, int $userId)
|
||||
{
|
||||
$this->checkPermission('access-api');
|
||||
$this->checkPermissionOrCurrentUser('manage-users', $userId);
|
||||
|
||||
$this->validate($request, [
|
||||
'name' => 'required|max:250',
|
||||
'expires_at' => 'date_format:Y-m-d',
|
||||
]);
|
||||
|
||||
$user = User::query()->findOrFail($userId);
|
||||
$secret = Str::random(32);
|
||||
$expiry = $request->get('expires_at', (Carbon::now()->addYears(100))->format('Y-m-d'));
|
||||
|
||||
$token = (new ApiToken())->forceFill([
|
||||
'name' => $request->get('name'),
|
||||
'client_id' => Str::random(32),
|
||||
'client_secret' => Hash::make($secret),
|
||||
'user_id' => $user->id,
|
||||
'expires_at' => $expiry
|
||||
]);
|
||||
|
||||
while (ApiToken::query()->where('client_id', '=', $token->client_id)->exists()) {
|
||||
$token->client_id = Str::random(32);
|
||||
}
|
||||
|
||||
$token->save();
|
||||
// TODO - Notification and activity?
|
||||
session()->flash('api-token-secret:' . $token->id, $secret);
|
||||
return redirect($user->getEditUrl('/api-tokens/' . $token->id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the details for a user API token, with access to edit.
|
||||
*/
|
||||
public function edit(int $userId, int $tokenId)
|
||||
{
|
||||
[$user, $token] = $this->checkPermissionAndFetchUserToken($userId, $tokenId);
|
||||
$secret = session()->pull('api-token-secret:' . $token->id, null);
|
||||
|
||||
return view('users.api-tokens.edit', [
|
||||
'user' => $user,
|
||||
'token' => $token,
|
||||
'model' => $token,
|
||||
'secret' => $secret,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the API token.
|
||||
*/
|
||||
public function update(Request $request, int $userId, int $tokenId)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'name' => 'required|max:250',
|
||||
'expires_at' => 'date_format:Y-m-d',
|
||||
]);
|
||||
|
||||
[$user, $token] = $this->checkPermissionAndFetchUserToken($userId, $tokenId);
|
||||
|
||||
$token->fill($request->all())->save();
|
||||
// TODO - Notification and activity?
|
||||
return redirect($user->getEditUrl('/api-tokens/' . $token->id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the delete view for this token.
|
||||
*/
|
||||
public function delete(int $userId, int $tokenId)
|
||||
{
|
||||
[$user, $token] = $this->checkPermissionAndFetchUserToken($userId, $tokenId);
|
||||
return view('users.api-tokens.delete', [
|
||||
'user' => $user,
|
||||
'token' => $token,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a token from the system.
|
||||
*/
|
||||
public function destroy(int $userId, int $tokenId)
|
||||
{
|
||||
[$user, $token] = $this->checkPermissionAndFetchUserToken($userId, $tokenId);
|
||||
$token->delete();
|
||||
|
||||
// TODO - Notification and activity?, Might have text in translations already (user_api_token_delete_success)
|
||||
return redirect($user->getEditUrl('#api_tokens'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the permission for the current user and return an array
|
||||
* where the first item is the user in context and the second item is their
|
||||
* API token in context.
|
||||
*/
|
||||
protected function checkPermissionAndFetchUserToken(int $userId, int $tokenId): array
|
||||
{
|
||||
$this->checkPermission('access-api');
|
||||
$this->checkPermissionOrCurrentUser('manage-users', $userId);
|
||||
|
||||
$user = User::query()->findOrFail($userId);
|
||||
$token = ApiToken::query()->where('user_id', '=', $user->id)->where('id', '=', $tokenId)->firstOrFail();
|
||||
return [$user, $token];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,7 +18,8 @@ class AddApiAuth extends Migration
|
|||
// Add API tokens table
|
||||
Schema::create('api_tokens', function(Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('client_id')->index();
|
||||
$table->string('name');
|
||||
$table->string('client_id')->unique();
|
||||
$table->string('client_secret');
|
||||
$table->integer('user_id')->unsigned()->index();
|
||||
$table->timestamp('expires_at')->index();
|
||||
|
|
|
@ -155,8 +155,26 @@ return [
|
|||
'users_api_tokens' => 'API Tokens',
|
||||
'users_api_tokens_none' => 'No API tokens have been created for this user',
|
||||
'users_api_tokens_create' => 'Create Token',
|
||||
'users_api_tokens_expires' => 'Expires',
|
||||
|
||||
// API Tokens
|
||||
'user_api_token_create' => 'Create API Token',
|
||||
'user_api_token_name' => 'Name',
|
||||
'user_api_token_name_desc' => 'Give your token a readable name as a future reminder of its intended purpose.',
|
||||
'user_api_token_expiry' => 'Expiry Date',
|
||||
'user_api_token_expiry_desc' => 'Set a date at which this token expires. After this date, requests made using this token will no longer work. Leaving this field blank will set an expiry 100 years into the future.',
|
||||
'user_api_token_create_secret_message' => 'Immediately after creating this token a "client id"" & "client secret" will be generated and displayed. The client secret will only be shown a single time so be sure to copy the value to somewhere safe and secure before proceeding.',
|
||||
'user_api_token' => 'API Token',
|
||||
'user_api_token_client_id' => 'Client ID',
|
||||
'user_api_token_client_id_desc' => 'This is a non-editable system generated identifier for this token which will need to be provided in API requests.',
|
||||
'user_api_token_client_secret' => 'Client Secret',
|
||||
'user_api_token_client_secret_desc' => 'This is a system generated secret for this token which will need to be provided in API requests. This will only be displayed this one time so copy this value to somewhere safe and secure.',
|
||||
'user_api_token_created' => 'Token Created :timeAgo',
|
||||
'user_api_token_updated' => 'Token Updated :timeAgo',
|
||||
'user_api_token_delete' => 'Delete Token',
|
||||
'user_api_token_delete_warning' => 'This will fully delete this API token with the name \':tokenName\' from the system.',
|
||||
'user_api_token_delete_confirm' => 'Are you sure you want to delete this API token?',
|
||||
'user_api_token_delete_success' => 'API token successfully deleted',
|
||||
|
||||
//! If editing translations files directly please ignore this in all
|
||||
//! languages apart from en. Content will be auto-copied from en.
|
||||
|
|
|
@ -19,6 +19,9 @@
|
|||
&.disabled, &[disabled] {
|
||||
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAMUlEQVQIW2NkwAGuXbv2nxGbHEhCS0uLEUMSJgHShCKJLIEiiS4Bl8QmAZbEJQGSBAC62BuJ+tt7zgAAAABJRU5ErkJggg==);
|
||||
}
|
||||
&[readonly] {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
&:focus {
|
||||
border-color: var(--color-primary);
|
||||
outline: 1px solid var(--color-primary);
|
||||
|
|
9
resources/views/form/date.blade.php
Normal file
9
resources/views/form/date.blade.php
Normal file
|
@ -0,0 +1,9 @@
|
|||
<input type="date" id="{{ $name }}" name="{{ $name }}"
|
||||
@if($errors->has($name)) class="text-neg" @endif
|
||||
placeholder="{{ $placeholder ?? 'YYYY-MM-DD' }}"
|
||||
@if($autofocus ?? false) autofocus @endif
|
||||
@if($disabled ?? false) disabled="disabled" @endif
|
||||
@if(isset($model) || old($name)) value="{{ old($name) ?? $model->$name->format('Y-m-d') ?? ''}}" @endif>
|
||||
@if($errors->has($name))
|
||||
<div class="text-neg text-small">{{ $errors->first($name) }}</div>
|
||||
@endif
|
|
@ -3,6 +3,7 @@
|
|||
@if(isset($placeholder)) placeholder="{{$placeholder}}" @endif
|
||||
@if($autofocus ?? false) autofocus @endif
|
||||
@if($disabled ?? false) disabled="disabled" @endif
|
||||
@if($readonly ?? false) readonly="readonly" @endif
|
||||
@if(isset($model) || old($name)) value="{{ old($name) ? old($name) : $model->$name}}" @endif>
|
||||
@if($errors->has($name))
|
||||
<div class="text-neg text-small">{{ $errors->first($name) }}</div>
|
||||
|
|
33
resources/views/users/api-tokens/create.blade.php
Normal file
33
resources/views/users/api-tokens/create.blade.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
@extends('simple-layout')
|
||||
|
||||
@section('body')
|
||||
|
||||
<div class="container small pt-xl">
|
||||
|
||||
<main class="card content-wrap auto-height">
|
||||
<h1 class="list-heading">{{ trans('settings.user_api_token_create') }}</h1>
|
||||
|
||||
<form action="{{ $user->getEditUrl('/create-api-token') }}" method="post">
|
||||
{!! csrf_field() !!}
|
||||
|
||||
<div class="setting-list">
|
||||
@include('users.api-tokens.form')
|
||||
|
||||
<div>
|
||||
<p class="text-warn italic">
|
||||
{{ trans('settings.user_api_token_create_secret_message') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group text-right">
|
||||
<a href="{{ $user->getEditUrl('#api_tokens') }}" class="button outline">{{ trans('common.cancel') }}</a>
|
||||
<button class="button" type="submit">{{ trans('common.save') }}</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@stop
|
26
resources/views/users/api-tokens/delete.blade.php
Normal file
26
resources/views/users/api-tokens/delete.blade.php
Normal file
|
@ -0,0 +1,26 @@
|
|||
@extends('simple-layout')
|
||||
|
||||
@section('body')
|
||||
<div class="container small pt-xl">
|
||||
|
||||
<div class="card content-wrap auto-height">
|
||||
<h1 class="list-heading">{{ trans('settings.user_api_token_delete') }}</h1>
|
||||
|
||||
<p>{{ trans('settings.user_api_token_delete_warning', ['tokenName' => $token->name]) }}</p>
|
||||
|
||||
<div class="grid half">
|
||||
<p class="text-neg"><strong>{{ trans('settings.user_api_token_delete_confirm') }}</strong></p>
|
||||
<div>
|
||||
<form action="{{ $user->getEditUrl('/api-tokens/' . $token->id) }}" method="POST" class="text-right">
|
||||
{!! csrf_field() !!}
|
||||
{!! method_field('delete') !!}
|
||||
|
||||
<a href="{{ $user->getEditUrl('/api-tokens/' . $token->id) }}" class="button outline">{{ trans('common.cancel') }}</a>
|
||||
<button type="submit" class="button">{{ trans('common.confirm') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@stop
|
66
resources/views/users/api-tokens/edit.blade.php
Normal file
66
resources/views/users/api-tokens/edit.blade.php
Normal file
|
@ -0,0 +1,66 @@
|
|||
@extends('simple-layout')
|
||||
|
||||
@section('body')
|
||||
|
||||
<div class="container small pt-xl">
|
||||
|
||||
<main class="card content-wrap auto-height">
|
||||
<h1 class="list-heading">{{ trans('settings.user_api_token') }}</h1>
|
||||
|
||||
<form action="{{ $user->getEditUrl('/api-tokens/' . $token->id) }}" method="post">
|
||||
{!! method_field('put') !!}
|
||||
{!! csrf_field() !!}
|
||||
|
||||
<div class="setting-list">
|
||||
|
||||
<div class="grid half gap-xl v-center">
|
||||
<div>
|
||||
<label class="setting-list-label">{{ trans('settings.user_api_token_client_id') }}</label>
|
||||
<p class="small">{{ trans('settings.user_api_token_client_id_desc') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
@include('form.text', ['name' => 'client_id', 'readonly' => true])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@if( $secret )
|
||||
<div class="grid half gap-xl v-center">
|
||||
<div>
|
||||
<label class="setting-list-label">{{ trans('settings.user_api_token_client_secret') }}</label>
|
||||
<p class="small text-warn">{{ trans('settings.user_api_token_client_secret_desc') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<input type="text" readonly="readonly" value="{{ $secret }}">
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@include('users.api-tokens.form', ['model' => $token])
|
||||
</div>
|
||||
|
||||
<div class="grid half gap-xl v-center">
|
||||
|
||||
<div class="text-muted text-small">
|
||||
<span title="{{ $token->created_at }}">
|
||||
{{ trans('settings.user_api_token_created', ['timeAgo' => $token->created_at->diffForHumans()]) }}
|
||||
</span>
|
||||
<br>
|
||||
<span title="{{ $token->updated_at }}">
|
||||
{{ trans('settings.user_api_token_updated', ['timeAgo' => $token->created_at->diffForHumans()]) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group text-right">
|
||||
<a href="{{ $user->getEditUrl('#api_tokens') }}" class="button outline">{{ trans('common.back') }}</a>
|
||||
<a href="{{ $user->getEditUrl('/api-tokens/' . $token->id . '/delete') }}" class="button outline">{{ trans('settings.user_api_token_delete') }}</a>
|
||||
<button class="button" type="submit">{{ trans('common.save') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@stop
|
21
resources/views/users/api-tokens/form.blade.php
Normal file
21
resources/views/users/api-tokens/form.blade.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
|
||||
|
||||
<div class="grid half gap-xl v-center">
|
||||
<div>
|
||||
<label class="setting-list-label">{{ trans('settings.user_api_token_name') }}</label>
|
||||
<p class="small">{{ trans('settings.user_api_token_name_desc') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
@include('form.text', ['name' => 'name'])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid half gap-xl v-center">
|
||||
<div>
|
||||
<label class="setting-list-label">{{ trans('settings.user_api_token_expiry') }}</label>
|
||||
<p class="small">{{ trans('settings.user_api_token_expiry_desc') }}</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
@include('form.date', ['name' => 'expires_at'])
|
||||
</div>
|
||||
</div>
|
|
@ -90,7 +90,7 @@
|
|||
|
||||
{{-- TODO - Review Control--}}
|
||||
@if(($currentUser->id === $user->id && userCan('access-api')) || userCan('manage-users'))
|
||||
<section class="card content-wrap auto-height">
|
||||
<section class="card content-wrap auto-height" id="api_tokens">
|
||||
<div class="grid half">
|
||||
<div><h2 class="list-heading">{{ trans('settings.users_api_tokens') }}</h2></div>
|
||||
<div class="text-right pt-xs">
|
||||
|
@ -100,7 +100,25 @@
|
|||
</div>
|
||||
</div>
|
||||
@if (count($user->apiTokens) > 0)
|
||||
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>{{ trans('common.name') }}</th>
|
||||
<th>{{ trans('settings.users_api_tokens_expires') }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
@foreach($user->apiTokens as $token)
|
||||
<tr>
|
||||
<td>
|
||||
{{ $token->name }} <br>
|
||||
<span class="small text-muted italic">{{ $token->client_id }}</span>
|
||||
</td>
|
||||
<td>{{ $token->expires_at->format('Y-m-d') ?? '' }}</td>
|
||||
<td class="text-right">
|
||||
<a class="button outline small" href="{{ $user->getEditUrl('/api-tokens/' . $token->id) }}">{{ trans('common.edit') }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</table>
|
||||
@else
|
||||
<p class="text-muted italic py-m">{{ trans('settings.users_api_tokens_none') }}</p>
|
||||
@endif
|
||||
|
|
|
@ -189,6 +189,11 @@ Route::group(['middleware' => 'auth'], function () {
|
|||
|
||||
// User API Tokens
|
||||
Route::get('/users/{userId}/create-api-token', 'UserApiTokenController@create');
|
||||
Route::post('/users/{userId}/create-api-token', 'UserApiTokenController@store');
|
||||
Route::get('/users/{userId}/api-tokens/{tokenId}', 'UserApiTokenController@edit');
|
||||
Route::put('/users/{userId}/api-tokens/{tokenId}', 'UserApiTokenController@update');
|
||||
Route::get('/users/{userId}/api-tokens/{tokenId}/delete', 'UserApiTokenController@delete');
|
||||
Route::delete('/users/{userId}/api-tokens/{tokenId}', 'UserApiTokenController@destroy');
|
||||
|
||||
// Roles
|
||||
Route::get('/roles', 'PermissionController@listRoles');
|
||||
|
|
Loading…
Reference in a new issue