Skip to content

Commit

Permalink
Add api token handling for users (#165)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kovah committed Jul 19, 2022
1 parent cbebfa4 commit 5298fd2
Show file tree
Hide file tree
Showing 30 changed files with 443 additions and 227 deletions.
1 change: 1 addition & 0 deletions app/Enums/ActivityLog.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ class ActivityLog
public const SYSTEM_CRON_TOKEN_REGENERATED = 'system.cron_token_regenerated';

public const USER_API_TOKEN_GENERATED = 'user.api_token_regenerated';
public const USER_API_TOKEN_REVOKED = 'user.api_token_revoked';
}
9 changes: 9 additions & 0 deletions app/Enums/ApiToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace App\Enums;

class ApiToken
{
public const ABILITY_USER_ACCESS = 'user_access';
public const ABILITY_SYSTEM_ACCESS = 'system_access';
}
48 changes: 21 additions & 27 deletions app/Exceptions/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,56 +3,50 @@
namespace App\Exceptions;

use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Throwable;

class Handler extends ExceptionHandler
{
/**
* A list of exception types with their corresponding custom log levels.
*
* @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
*/
protected $levels = [
//
];

/**
* A list of the exception types that are not reported.
*
* @var array
* @var array<int, class-string<\Throwable>>
*/
protected $dontReport = [
//
];

/**
* A list of the inputs that are never flashed for validation exceptions.
* A list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];

/**
* Report or log an exception.
*
* @param Throwable $exception
* @throws Throwable
*/
public function report(Throwable $exception): void
{
if ($this->shouldReport($exception) && app()->bound('sentry')) {
app('sentry')->captureException($exception);
}

parent::report($exception);
}

/**
* Render an exception into an HTTP response.
* Register the exception handling callbacks for the application.
*
* @param Request $request
* @param Throwable $exception
* @return Response
* @throws Throwable
* @return void
*/
public function render($request, Throwable $exception): Response
public function register()
{
return parent::render($request, $exception);
$this->reportable(function (Throwable $e) {
if (app()->bound('sentry')) {
app('sentry')->captureException($e);
}
});
}
}
47 changes: 47 additions & 0 deletions app/Http/Controllers/App/ApiTokenController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace App\Http\Controllers\App;

use App\Enums\ActivityLog;
use App\Enums\ApiToken;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\CreateApiTokenRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Laravel\Sanctum\PersonalAccessToken;

class ApiTokenController extends Controller
{
public function index(Request $request)
{
return view('app.api-tokens.index', [
'tokens' => $request->user()->tokens()->get(),
]);
}

public function store(CreateApiTokenRequest $request): RedirectResponse
{
$token = $request->user()->createToken($request->validated('token_name'), [ApiToken::ABILITY_USER_ACCESS]);

activity()
->by($request->user())
->withProperty('token_id', $token->accessToken->id)
->log(ActivityLog::USER_API_TOKEN_GENERATED);

return redirect()->route('api-tokens.index')->with('new_token', $token->plainTextToken);
}

public function destroy(Request $request, PersonalAccessToken $token): RedirectResponse
{
$this->authorize('delete', $token);

$token->delete();

activity()
->by($request->user())
->log(ActivityLog::USER_API_TOKEN_REVOKED);

flash()->warning(trans('auth.api_tokens.revoke_successful'));
return redirect()->route('api-tokens.index');
}
}
20 changes: 0 additions & 20 deletions app/Http/Controllers/App/UserSettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,24 +87,4 @@ public function changeUserPassword(Request $request): RedirectResponse
flash(trans('settings.password_updated'), 'success');
return redirect()->back();
}

/**
* Generate a new API token for the current user.
*
* @param Request $request
* @return JsonResponse
*/
public function generateApiToken(Request $request): JsonResponse
{
$new_token = Str::random(32);

$request->user()->api_token = $new_token;
$request->user()->save();

activity()->by(auth()->user())->log(ActivityLog::USER_API_TOKEN_GENERATED);

return response()->json([
'new_token' => $new_token,
]);
}
}
25 changes: 25 additions & 0 deletions app/Http/Requests/Auth/CreateApiTokenRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace App\Http\Requests\Auth;

use Illuminate\Database\Query\Builder;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class CreateApiTokenRequest extends FormRequest
{
public function rules(): array
{
return [
'token_name' => [
'required',
'alpha_dash',
'min:3',
'max:100',
Rule::unique('personal_access_tokens', 'name')->where(function (Builder $query) {
return $query->where('tokenable_id', request()->user()->id);
}),
],
];
}
}
6 changes: 4 additions & 2 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Carbon;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Sanctum\HasApiTokens;
use OwenIt\Auditing\Auditable as AuditableTrait;
use OwenIt\Auditing\Contracts\Auditable;
use Spatie\Permission\Traits\HasRoles;
Expand All @@ -30,11 +31,12 @@
class User extends Authenticatable implements Auditable
{
use AuditableTrait;
use Notifiable;
use HasApiTokens;
use HasFactory;
use HasRoles;
use TwoFactorAuthenticatable;
use Notifiable;
use SoftDeletes;
use TwoFactorAuthenticatable;

protected $fillable = [
'name',
Expand Down
47 changes: 47 additions & 0 deletions app/Policies/ApiTokenPolicy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace App\Policies;

use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
use Laravel\Sanctum\PersonalAccessToken;

class ApiTokenPolicy
{
use HandlesAuthorization;

public function viewAny(User $user): bool
{
return true;
}

public function view(User $user, PersonalAccessToken $personalAccessToken): bool
{
return true;
}

public function create(User $user): bool
{
return true;
}

public function update(User $user, PersonalAccessToken $personalAccessToken): bool
{
return false;
}

public function delete(User $user, PersonalAccessToken $personalAccessToken): bool
{
return $personalAccessToken->tokenable->is($user);
}

public function restore(User $user, PersonalAccessToken $personalAccessToken): bool
{
return false;
}

public function forceDelete(User $user, PersonalAccessToken $personalAccessToken): bool
{
return false;
}
}
6 changes: 3 additions & 3 deletions app/Policies/LinkPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,17 @@ public function update(User $user, Link $link): bool

public function delete(User $user, Link $link): bool
{
return $this->userCanAccessLink($user, $link);
return $link->user->is($user);
}

public function restore(User $user, Link $link): bool
{
return $this->userCanAccessLink($user, $link);
return $link->user->is($user);
}

public function forceDelete(User $user, Link $link): bool
{
return $this->userCanAccessLink($user, $link);
return $link->user->is($user);
}

// Link must be either owned by user, or be not private
Expand Down
3 changes: 3 additions & 0 deletions app/Providers/AuthServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
use App\Models\LinkList;
use App\Models\Note;
use App\Models\Tag;
use App\Policies\ApiTokenPolicy;
use App\Policies\LinkListPolicy;
use App\Policies\LinkPolicy;
use App\Policies\NotePolicy;
use App\Policies\TagPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Laravel\Sanctum\PersonalAccessToken;

class AuthServiceProvider extends ServiceProvider
{
Expand All @@ -24,6 +26,7 @@ class AuthServiceProvider extends ServiceProvider
LinkList::class => LinkListPolicy::class,
Note::class => NotePolicy::class,
Tag::class => TagPolicy::class,
PersonalAccessToken::class => ApiTokenPolicy::class,
];

/**
Expand Down
3 changes: 2 additions & 1 deletion app/Providers/RouteServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
use Laravel\Sanctum\PersonalAccessToken;

class RouteServiceProvider extends ServiceProvider
{
Expand All @@ -25,7 +26,7 @@ class RouteServiceProvider extends ServiceProvider
*/
public function boot(): void
{
//
Route::model('api_token', PersonalAccessToken::class);

parent::boot();
}
Expand Down
6 changes: 0 additions & 6 deletions config/auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,6 @@
'driver' => 'session',
'provider' => 'users',
],

'api' => [
'driver' => 'token',
'provider' => 'users',
'hash' => false,
],
],

/*
Expand Down
1 change: 0 additions & 1 deletion database/factories/UserFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ public function definition(): array
'name' => $this->faker->userName(),
'email' => $this->faker->unique()->safeEmail(),
'password' => '$2y$10$9.preebMjZ.8obdvk5ZVdOCw7Cq1EJm6i1B1RJevxCXYW0lUiwDJG', // secretpassword
'api_token' => Str::random(32),
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePersonalAccessTokensTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->bigIncrements('id');
$table->morphs('tokenable');
$table->string('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamps();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('personal_access_tokens');
}
}
3 changes: 2 additions & 1 deletion lang/en_US/audit.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
'cron_token_regenerated' => 'System: Cron Token was re-generated',
],
'user_settings' => [
'api_token_regenerated' => 'User: API Token was re-generated',
'api_token_generated' => 'User: API Token was generated',
'api_token_revoken' => 'User: API Token was revoked',
],
],
];

0 comments on commit 5298fd2

Please sign in to comment.