diff --git a/app/Helpers/JSON.php b/app/Helpers/JSON.php index 4ec71cb..fec7a6c 100644 --- a/app/Helpers/JSON.php +++ b/app/Helpers/JSON.php @@ -103,11 +103,13 @@ class JSON public function build($headers = []) { + // Uses the same structure as model resources, for consistency when they aren't used. $response = [ - 'status' => $this->getStatus(), - 'message' => $this->getMessage(), - 'type' => $this->getType(), - 'response' => $this->getData() + 'data' => $this->getData(), + 'meta' => [ + 'status' => $this->getStatus(), + 'message' => $this->getMessage(), + ], ]; return response($response, $this->getCode(), $headers); diff --git a/app/Http/Controllers/ApiKeyController.php b/app/Http/Controllers/ApiKeyController.php index d25602c..f959ae2 100644 --- a/app/Http/Controllers/ApiKeyController.php +++ b/app/Http/Controllers/ApiKeyController.php @@ -11,29 +11,13 @@ 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); - } + $this->authorize('viewAny', ApiKey::class); - public function adminKeys() - { - if (Auth::user()->hasRole('admin')) - { - return view('dashboard.administration.keys') - ->with('keys', ApiKey::all()); - } - else - { - return redirect() - ->back() - ->with('error', 'You do not have permission to access this page.'); - } + return view('dashboard.administration.keys') + ->with('keys', ApiKey::all()); } /** @@ -43,6 +27,8 @@ class ApiKeyController extends Controller */ public function store(CreateApiKeyRequest $request) { + $this->authorize('create', ApiKey::class); + $discriminator = "#" . bin2hex(openssl_random_pseudo_bytes(7)); $secret = bin2hex(openssl_random_pseudo_bytes(32)); @@ -71,28 +57,24 @@ class ApiKeyController extends Controller 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.'); - } + $this->authorize('update', $key); + if ($key->status == 'active') + { + $key->status = 'disabled'; + $key->save(); + } + else + { return redirect() ->back() - ->with('success', 'Key revoked. Apps using this key will stop working.'); + ->with('error', 'Key already revoked.'); } return redirect() ->back() - ->with('error', 'You do not have permission to modify this key.'); + ->with('success', 'Key revoked. Apps using this key will stop working.'); + } /** @@ -101,18 +83,13 @@ class ApiKeyController extends Controller public function destroy($id) { $key = ApiKey::findOrFail($id); + $this->authorize('delete', $key); - 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.'); - } + $key->delete(); return redirect() ->back() - ->with('error', 'You do not have permission to modify this key.'); + ->with('success', 'Key deleted successfully. Apps using this key will stop working.'); + } } diff --git a/app/Http/Controllers/ApplicationController.php b/app/Http/Controllers/ApplicationController.php index 7b74341..e3b5f4d 100755 --- a/app/Http/Controllers/ApplicationController.php +++ b/app/Http/Controllers/ApplicationController.php @@ -23,6 +23,7 @@ namespace App\Http\Controllers; use App\Application; use App\Events\ApplicationDeniedEvent; +use App\Http\Resources\ApplicationResource; use App\Notifications\ApplicationMoved; use App\Notifications\NewApplicant; use App\Response; @@ -56,39 +57,58 @@ class ApplicationController extends Controller public function showUserApp(Request $request, Application $application) { - $this->authorize('view', $application); + if (!$request->wantsJson()) + { + $this->authorize('view', $application); - if (! is_null($application)) { - return view('dashboard.user.viewapp') - ->with( - [ - 'application' => $application, - 'comments' => $application->comments, - 'structuredResponses' => json_decode($application->response->responseData, true), - 'formStructure' => $application->response->form, - 'vacancy' => $application->response->vacancy, - 'canVote' => $this->canVote($application->votes), - ] - ); - } else { - $request->session()->flash('error', 'The application you requested could not be found.'); + if (! is_null($application)) { + return view('dashboard.user.viewapp') + ->with( + [ + 'application' => $application, + 'comments' => $application->comments, + 'structuredResponses' => json_decode($application->response->responseData, true), + 'formStructure' => $application->response->form, + 'vacancy' => $application->response->vacancy, + 'canVote' => $this->canVote($application->votes), + ] + ); + } else { + $request->session()->flash('error', 'The application you requested could not be found.'); + } + + return redirect()->back(); } - return redirect()->back(); + return (new ApplicationResource($application))->additional([ + 'meta' => [ + 'code' => 200, + 'status' => 'success' + ] + ]); } - public function showAllApps() + public function showAllApps(Request $request) { - $this->authorize('viewAny', Application::class); + if (!$request->wantsJson()) + { + $this->authorize('viewAny', Application::class); - return view('dashboard.appmanagement.all') - ->with('applications', Application::paginate(6)); + return view('dashboard.appmanagement.all') + ->with('applications', Application::paginate(6)); + } + + + // todo: eager load all relationships used + return ApplicationResource::collection(Application::paginate(6))->additional([ + 'code' => '200', + 'status' => 'success', + ]); } public function renderApplicationForm(Request $request, $vacancySlug) { - // FIXME: Get rid of references to first(), this is a wonky query $vacancyWithForm = Vacancy::with('forms')->where('vacancySlug', $vacancySlug)->get(); $firstVacancy = $vacancyWithForm->first(); @@ -96,10 +116,8 @@ class ApplicationController extends Controller if (! $vacancyWithForm->isEmpty() && $firstVacancy->vacancyCount !== 0 && $firstVacancy->vacancyStatus == 'OPEN') { return view('dashboard.application-rendering.apply') ->with([ - 'vacancy' => $vacancyWithForm->first(), 'preprocessedForm' => json_decode($vacancyWithForm->first()->forms->formStructure, true), - ]); } else { abort(404, 'The application you\'re looking for could not be found or it is currently unavailable.'); diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 10fc2cf..634033c 100755 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -21,6 +21,7 @@ namespace App\Http; +use App\Http\Middleware\APIAuthenticationMiddleware; use Illuminate\Foundation\Http\Kernel as HttpKernel; class Kernel extends HttpKernel @@ -60,6 +61,7 @@ class Kernel extends HttpKernel 'api' => [ 'throttle:60,1', \Illuminate\Routing\Middleware\SubstituteBindings::class, + APIAuthenticationMiddleware::class ], ]; diff --git a/app/Http/Middleware/APIAuthenticationMiddleware.php b/app/Http/Middleware/APIAuthenticationMiddleware.php new file mode 100644 index 0000000..8797104 --- /dev/null +++ b/app/Http/Middleware/APIAuthenticationMiddleware.php @@ -0,0 +1,61 @@ +bearerToken(); + + if (!is_null($key)) + { + // we have a valid discriminator + $discriminator = Str::before($key, '.'); + $loneKey = Str::after($key, '.'); + + $keyRecord = ApiKey::where('discriminator', $discriminator)->first(); + + if ($keyRecord && Hash::check($loneKey, $keyRecord->secret) && $keyRecord->status == 'active') + { + Log::alert('API Authentication Success', [ + 'discriminator' => $discriminator + ]); + + $keyRecord->last_used = Carbon::now(); + $keyRecord->save(); + + return $next($request); + } + + return JSON::setResponseType('error') + ->setStatus('authfail') + ->setMessage('Invalid / Revoked API key.') + ->setCode(401) + ->build(); + } + + return JSON::setResponseType('error') + ->setStatus('malformed_key') + ->setMessage('Missing or malformed API key.') + ->setCode(400) + ->build(); + + } +} diff --git a/app/Http/Resources/ApplicationResource.php b/app/Http/Resources/ApplicationResource.php new file mode 100644 index 0000000..3994e00 --- /dev/null +++ b/app/Http/Resources/ApplicationResource.php @@ -0,0 +1,28 @@ + $this->id, + 'applicationStatus' => $this->applicationStatus, + 'applicant' => new UserResource(User::findOrFail($this->applicantUserID)), + 'response' => new ResponseResource(Response::findOrFail($this->applicantFormResponseID)), + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at + ]; + } +} diff --git a/app/Http/Resources/AppointmentResource.php b/app/Http/Resources/AppointmentResource.php new file mode 100644 index 0000000..4b71947 --- /dev/null +++ b/app/Http/Resources/AppointmentResource.php @@ -0,0 +1,19 @@ + $this->id, + 'formName' => $this->formName, + 'formStructure' => json_decode($this->formStructure), + 'formStatus' => $this->formStatus, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at + ]; + } +} diff --git a/app/Http/Resources/OptionResource.php b/app/Http/Resources/OptionResource.php new file mode 100644 index 0000000..0315068 --- /dev/null +++ b/app/Http/Resources/OptionResource.php @@ -0,0 +1,19 @@ + $this->id, + 'form' => new FormResource(Form::findOrFail($this->responseFormID)), + 'responseData' => json_decode($this->responseData), + 'vacancy' => new VacancyResource(Vacancy::findOrFail($this->associatedVacancyID)), + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at + ]; + } +} diff --git a/app/Http/Resources/TeamFileResource.php b/app/Http/Resources/TeamFileResource.php new file mode 100644 index 0000000..efad411 --- /dev/null +++ b/app/Http/Resources/TeamFileResource.php @@ -0,0 +1,19 @@ + $this->id, + 'uuid' => $this->uuid, + 'name' => $this->name, + 'email' => $this->email, + 'username' => $this->username, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + 'current_team_id' => $this->current_team_id + ]; + } +} diff --git a/app/Http/Resources/VacancyResource.php b/app/Http/Resources/VacancyResource.php new file mode 100644 index 0000000..dc87ae1 --- /dev/null +++ b/app/Http/Resources/VacancyResource.php @@ -0,0 +1,19 @@ +hasRole('admin')) + return true; + + return false; + } + + + /** + * Determine whether the user can create models. + * + * @param \App\User $user + * @return mixed + */ + public function create(User $user) + { + if ($user->hasRole('admin')) + return true; + + return false; + } + + /** + * Determine whether the user can update the model. + * + * @param \App\User $user + * @param \App\ApiKey $apiKey + * @return mixed + */ + public function update(User $user, ApiKey $apiKey) + { + if ($user->hasRole('admin')) + return true; + + return false; + } + + /** + * Determine whether the user can delete the model. + * + * @param \App\User $user + * @param \App\ApiKey $apiKey + * @return mixed + */ + public function delete(User $user, ApiKey $apiKey) + { + if ($user->hasRole('admin')) + return true; + + return false; + } + +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 5d33795..fd42925 100755 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -21,10 +21,12 @@ namespace App\Providers; +use App\ApiKey; use App\Application; use App\Appointment; use App\Ban; use App\Form; +use App\Policies\ApiKeyPolicy; use App\Policies\ApplicationPolicy; use App\Policies\AppointmentPolicy; use App\Policies\BanPolicy; @@ -61,7 +63,8 @@ class AuthServiceProvider extends ServiceProvider Ban::class => BanPolicy::class, Appointment::class => AppointmentPolicy::class, Team::class => TeamPolicy::class, - TeamFile::class, TeamFilePolicy::class + TeamFile::class => TeamFilePolicy::class, + ApiKey::class => ApiKeyPolicy::class ]; /** diff --git a/config/adminlte.php b/config/adminlte.php index c0b557c..9e19fb8 100755 --- a/config/adminlte.php +++ b/config/adminlte.php @@ -268,11 +268,6 @@ 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'], @@ -374,7 +369,7 @@ return [ 'text' => 'API Keys', 'icon' => 'fas fa-user-shield', 'can' => 'admin.settings.view', - 'route' => 'adminKeys' + 'route' => 'keys.index' ] ], ], diff --git a/resources/views/dashboard/administration/keys.blade.php b/resources/views/dashboard/administration/keys.blade.php index b2e5ef0..f79d132 100644 --- a/resources/views/dashboard/administration/keys.blade.php +++ b/resources/views/dashboard/administration/keys.blade.php @@ -16,6 +16,24 @@ @section('content') + + +
+ @csrf + +
+ + +
+ +
+ + + + + +
+
@@ -24,6 +42,17 @@
+ @if (session()->has('finalKey')) +
+
+
+

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

+

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

+
+
+
+ @endif +
@@ -68,6 +97,12 @@ @else @endif +
+ @csrf + @method('DELETE') + +
+ @endforeach @@ -82,8 +117,8 @@ - - + + diff --git a/resources/views/dashboard/user/api/index.blade.php b/resources/views/dashboard/user/api/index.blade.php deleted file mode 100644 index 4c2fe06..0000000 --- a/resources/views/dashboard/user/api/index.blade.php +++ /dev/null @@ -1,118 +0,0 @@ -@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/api.php b/routes/api.php index b473196..11be7cd 100755 --- a/routes/api.php +++ b/routes/api.php @@ -33,6 +33,9 @@ use Illuminate\Support\Facades\Route; | */ -Route::middleware('auth:api')->get('/user', function (Request $request) { - return $request->user(); +Route::middleware(['api'])->group(function (){ + + Route::get('applications', [\App\Http\Controllers\ApplicationController::class, 'showAllApps']); + Route::get('applications/view/{application}', [\App\Http\Controllers\ApplicationController::class, 'showUserApp']); + }); diff --git a/routes/web.php b/routes/web.php index e5d9c39..a5084db 100755 --- a/routes/web.php +++ b/routes/web.php @@ -164,13 +164,6 @@ 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'); @@ -229,8 +222,10 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['lo Route::get('settings', [OptionsController::class, 'index']) ->name('showSettings'); - Route::get('keys', [ApiKeyController::class, 'adminKeys']) - ->name('adminKeys'); + Route::resource('keys', ApiKeyController::class); + + Route::patch('keys/revoke/{key}', [ApiKeyController::class, 'revokeKey']) + ->name('revokeKey'); Route::post('settings/save', [OptionsController::class, 'saveSettings']) ->name('saveSettings');