diff --git a/app/Api/ApiToken.php b/app/Api/ApiToken.php index 838e70abb..e7101387f 100644 --- a/app/Api/ApiToken.php +++ b/app/Api/ApiToken.php @@ -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' + ]; } diff --git a/app/Auth/UserRepo.php b/app/Auth/UserRepo.php index a903e2c38..e082b2dd5 100644 --- a/app/Auth/UserRepo.php +++ b/app/Auth/UserRepo.php @@ -194,6 +194,7 @@ class UserRepo public function destroy(User $user) { $user->socialAccounts()->delete(); + $user->apiTokens()->delete(); $user->delete(); // Delete user profile images diff --git a/app/Http/Controllers/UserApiTokenController.php b/app/Http/Controllers/UserApiTokenController.php index 385352011..3bfb0175e 100644 --- a/app/Http/Controllers/UserApiTokenController.php +++ b/app/Http/Controllers/UserApiTokenController.php @@ -1,6 +1,11 @@ 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]; + } } diff --git a/database/migrations/2019_12_29_120917_add_api_auth.php b/database/migrations/2019_12_29_120917_add_api_auth.php index e80fe3ae4..2af0b292e 100644 --- a/database/migrations/2019_12_29_120917_add_api_auth.php +++ b/database/migrations/2019_12_29_120917_add_api_auth.php @@ -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(); diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php index bb750a780..a2148361a 100755 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -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. diff --git a/resources/sass/_forms.scss b/resources/sass/_forms.scss index 3e7ff60f3..da0f7ef4c 100644 --- a/resources/sass/_forms.scss +++ b/resources/sass/_forms.scss @@ -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); diff --git a/resources/views/form/date.blade.php b/resources/views/form/date.blade.php new file mode 100644 index 000000000..c2e70b9e3 --- /dev/null +++ b/resources/views/form/date.blade.php @@ -0,0 +1,9 @@ +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)) +
{{ $errors->first($name) }}
+@endif diff --git a/resources/views/form/text.blade.php b/resources/views/form/text.blade.php index 4b3631a06..fabfab451 100644 --- a/resources/views/form/text.blade.php +++ b/resources/views/form/text.blade.php @@ -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))
{{ $errors->first($name) }}
diff --git a/resources/views/users/api-tokens/create.blade.php b/resources/views/users/api-tokens/create.blade.php new file mode 100644 index 000000000..46c3e0b8a --- /dev/null +++ b/resources/views/users/api-tokens/create.blade.php @@ -0,0 +1,33 @@ +@extends('simple-layout') + +@section('body') + +
+ +
+

{{ trans('settings.user_api_token_create') }}

+ +
+ {!! csrf_field() !!} + +
+ @include('users.api-tokens.form') + +
+

+ {{ trans('settings.user_api_token_create_secret_message') }} +

+
+
+ +
+ {{ trans('common.cancel') }} + +
+ +
+ +
+
+ +@stop diff --git a/resources/views/users/api-tokens/delete.blade.php b/resources/views/users/api-tokens/delete.blade.php new file mode 100644 index 000000000..8fcfcda95 --- /dev/null +++ b/resources/views/users/api-tokens/delete.blade.php @@ -0,0 +1,26 @@ +@extends('simple-layout') + +@section('body') +
+ +
+

{{ trans('settings.user_api_token_delete') }}

+ +

{{ trans('settings.user_api_token_delete_warning', ['tokenName' => $token->name]) }}

+ +
+

{{ trans('settings.user_api_token_delete_confirm') }}

+
+
+ {!! csrf_field() !!} + {!! method_field('delete') !!} + + {{ trans('common.cancel') }} + +
+
+
+ +
+
+@stop diff --git a/resources/views/users/api-tokens/edit.blade.php b/resources/views/users/api-tokens/edit.blade.php new file mode 100644 index 000000000..0ec9adbe6 --- /dev/null +++ b/resources/views/users/api-tokens/edit.blade.php @@ -0,0 +1,66 @@ +@extends('simple-layout') + +@section('body') + +
+ +
+

{{ trans('settings.user_api_token') }}

+ +
+ {!! method_field('put') !!} + {!! csrf_field() !!} + +
+ +
+
+ +

{{ trans('settings.user_api_token_client_id_desc') }}

+
+
+ @include('form.text', ['name' => 'client_id', 'readonly' => true]) +
+
+ + + @if( $secret ) +
+
+ +

{{ trans('settings.user_api_token_client_secret_desc') }}

+
+
+ +
+
+ @endif + + @include('users.api-tokens.form', ['model' => $token]) +
+ +
+ +
+ + {{ trans('settings.user_api_token_created', ['timeAgo' => $token->created_at->diffForHumans()]) }} + +
+ + {{ trans('settings.user_api_token_updated', ['timeAgo' => $token->created_at->diffForHumans()]) }} + +
+ + +
+ +
+ +
+
+ +@stop diff --git a/resources/views/users/api-tokens/form.blade.php b/resources/views/users/api-tokens/form.blade.php new file mode 100644 index 000000000..d81a330d5 --- /dev/null +++ b/resources/views/users/api-tokens/form.blade.php @@ -0,0 +1,21 @@ + + +
+
+ +

{{ trans('settings.user_api_token_name_desc') }}

+
+
+ @include('form.text', ['name' => 'name']) +
+
+ +
+
+ +

{{ trans('settings.user_api_token_expiry_desc') }}

+
+
+ @include('form.date', ['name' => 'expires_at']) +
+
\ No newline at end of file diff --git a/resources/views/users/edit.blade.php b/resources/views/users/edit.blade.php index b3f73773b..54e0ee21a 100644 --- a/resources/views/users/edit.blade.php +++ b/resources/views/users/edit.blade.php @@ -90,7 +90,7 @@ {{-- TODO - Review Control--}} @if(($currentUser->id === $user->id && userCan('access-api')) || userCan('manage-users')) -
+

{{ trans('settings.users_api_tokens') }}

@@ -100,7 +100,25 @@
@if (count($user->apiTokens) > 0) - + + + + + + + @foreach($user->apiTokens as $token) + + + + + + @endforeach +
{{ trans('common.name') }}{{ trans('settings.users_api_tokens_expires') }}
+ {{ $token->name }}
+ {{ $token->client_id }} +
{{ $token->expires_at->format('Y-m-d') ?? '' }} + {{ trans('common.edit') }} +
@else

{{ trans('settings.users_api_tokens_none') }}

@endif diff --git a/routes/web.php b/routes/web.php index 2a0e85dfe..f38575b79 100644 --- a/routes/web.php +++ b/routes/web.php @@ -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');