From 45b6e658e071089803e5f416a2441250704da383 Mon Sep 17 00:00:00 2001 From: perappu Date: Sat, 9 Nov 2024 12:07:04 -0600 Subject: [PATCH 01/21] feat: generate personal access tokens with laravel sanctum --- app/Http/Controllers/Api/AuthController.php | 30 +++++++ app/Http/Kernel.php | 7 +- app/Models/User/User.php | 3 +- composer.json | 1 + composer.lock | 68 ++++++++++++++- config/sanctum.php | 83 +++++++++++++++++++ ...01_create_personal_access_tokens_table.php | 33 ++++++++ routes/api.php | 9 +- 8 files changed, 227 insertions(+), 7 deletions(-) create mode 100644 app/Http/Controllers/Api/AuthController.php create mode 100644 config/sanctum.php create mode 100644 database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php diff --git a/app/Http/Controllers/Api/AuthController.php b/app/Http/Controllers/Api/AuthController.php new file mode 100644 index 0000000000..c6ca6e8bed --- /dev/null +++ b/app/Http/Controllers/Api/AuthController.php @@ -0,0 +1,30 @@ +validate([ + 'email' => 'required|email', + 'password' => 'required', + 'token_name' => 'required', + ]); + + $user = User::where('email', $request->email)->first(); + + if (! $user || ! Hash::check($request->password, $user->password)) { + throw ValidationException::withMessages([ + 'email' => ['The provided credentials are incorrect.'], + ]); + } + + return $user->createToken($request->token_name)->plainTextToken; + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index f8f41cfba4..d4d56e4681 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -38,8 +38,9 @@ class Kernel extends HttpKernel { ], 'api' => [ - 'throttle:60,1', - 'bindings', + \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, + 'throttle:api', + \Illuminate\Routing\Middleware\SubstituteBindings::class, ], ]; @@ -65,6 +66,8 @@ class Kernel extends HttpKernel { 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'post.throttle' => Middleware\PostRequestThrottleMiddleware::class, + 'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class, + 'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class, ]; /** diff --git a/app/Models/User/User.php b/app/Models/User/User.php index 4c7432a347..328f75dbaf 100644 --- a/app/Models/User/User.php +++ b/app/Models/User/User.php @@ -25,9 +25,10 @@ use Illuminate\Notifications\Notifiable; use Illuminate\Support\Facades\Auth; use Laravel\Fortify\TwoFactorAuthenticatable; +use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable implements MustVerifyEmail { - use Commenter, Notifiable, TwoFactorAuthenticatable; + use Commenter, Notifiable, TwoFactorAuthenticatable, HasApiTokens; /** * The attributes that are mass assignable. diff --git a/composer.json b/composer.json index 80150b404f..10295c3e41 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ "laravel/fortify": "^1.7", "laravel/framework": "^10.0", "laravel/helpers": "^1.4", + "laravel/sanctum": "^3.3", "laravel/socialite": "^5.2", "laravel/tinker": "^2.0", "laravelcollective/html": "^6.0", diff --git a/composer.lock b/composer.lock index 2293e7e0e2..353ab94edb 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "600614d9a8d933ab7520bec6c1c22baa", + "content-hash": "d25afebd70606201e6b385871080e8a5", "packages": [ { "name": "bacon/bacon-qr-code", @@ -2270,6 +2270,72 @@ }, "time": "2024-08-12T22:06:33+00:00" }, + { + "name": "laravel/sanctum", + "version": "v3.3.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/sanctum.git", + "reference": "8c104366459739f3ada0e994bcd3e6fd681ce3d5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/8c104366459739f3ada0e994bcd3e6fd681ce3d5", + "reference": "8c104366459739f3ada0e994bcd3e6fd681ce3d5", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^9.21|^10.0", + "illuminate/contracts": "^9.21|^10.0", + "illuminate/database": "^9.21|^10.0", + "illuminate/support": "^9.21|^10.0", + "php": "^8.0.2" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^7.28.2|^8.8.3", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Sanctum\\SanctumServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sanctum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.", + "keywords": [ + "auth", + "laravel", + "sanctum" + ], + "support": { + "issues": "https://github.com/laravel/sanctum/issues", + "source": "https://github.com/laravel/sanctum" + }, + "time": "2023-12-19T18:44:48+00:00" + }, { "name": "laravel/serializable-closure", "version": "v1.3.5", diff --git a/config/sanctum.php b/config/sanctum.php new file mode 100644 index 0000000000..35d75b31e4 --- /dev/null +++ b/config/sanctum.php @@ -0,0 +1,83 @@ + explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( + '%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + Sanctum::currentApplicationUrlWithPort() + ))), + + /* + |-------------------------------------------------------------------------- + | Sanctum Guards + |-------------------------------------------------------------------------- + | + | This array contains the authentication guards that will be checked when + | Sanctum is trying to authenticate a request. If none of these guards + | are able to authenticate the request, Sanctum will use the bearer + | token that's present on an incoming request for authentication. + | + */ + + 'guard' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Expiration Minutes + |-------------------------------------------------------------------------- + | + | This value controls the number of minutes until an issued token will be + | considered expired. This will override any values set in the token's + | "expires_at" attribute, but first-party sessions are not affected. + | + */ + + 'expiration' => null, + + /* + |-------------------------------------------------------------------------- + | Token Prefix + |-------------------------------------------------------------------------- + | + | Sanctum can prefix new tokens in order to take advantage of numerous + | security scanning initiatives maintained by open source platforms + | that notify developers if they commit tokens into repositories. + | + | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning + | + */ + + 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Sanctum Middleware + |-------------------------------------------------------------------------- + | + | When authenticating your first-party SPA with Sanctum you may need to + | customize some of the middleware Sanctum uses while processing the + | request. You may change the middleware listed below as required. + | + */ + + 'middleware' => [ + 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, + 'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class, + 'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class, + ], + +]; diff --git a/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php b/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php new file mode 100644 index 0000000000..e828ad8189 --- /dev/null +++ b/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->morphs('tokenable'); + $table->string('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +}; diff --git a/routes/api.php b/routes/api.php index c641ca5e5b..5a457e3043 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,6 +1,9 @@ get('/user', function (Request $request) { - return $request->user(); -}); +Route::group(['namespace' => 'Api'], function () { + Route::post('/token', 'AuthController@postGenerateToken'); +}); \ No newline at end of file From aaf84acf517e5228e5571693ee5cd7e030927f48 Mon Sep 17 00:00:00 2001 From: perappu Date: Sat, 9 Nov 2024 18:10:12 +0000 Subject: [PATCH 02/21] refactor: fix PHP styling --- app/Http/Controllers/Api/AuthController.php | 15 +++++++-------- app/Models/User/User.php | 2 +- config/sanctum.php | 4 ++-- ...000001_create_personal_access_tokens_table.php | 9 +++------ routes/api.php | 7 +------ 5 files changed, 14 insertions(+), 23 deletions(-) diff --git a/app/Http/Controllers/Api/AuthController.php b/app/Http/Controllers/Api/AuthController.php index c6ca6e8bed..aca6eb57f2 100644 --- a/app/Http/Controllers/Api/AuthController.php +++ b/app/Http/Controllers/Api/AuthController.php @@ -8,23 +8,22 @@ use Illuminate\Support\Facades\Hash; use Illuminate\Validation\ValidationException; -class AuthController extends Controller -{ +class AuthController extends Controller { public function postGenerateToken(Request $request) { $request->validate([ - 'email' => 'required|email', - 'password' => 'required', + 'email' => 'required|email', + 'password' => 'required', 'token_name' => 'required', ]); - + $user = User::where('email', $request->email)->first(); - - if (! $user || ! Hash::check($request->password, $user->password)) { + + if (!$user || !Hash::check($request->password, $user->password)) { throw ValidationException::withMessages([ 'email' => ['The provided credentials are incorrect.'], ]); } - + return $user->createToken($request->token_name)->plainTextToken; } } diff --git a/app/Models/User/User.php b/app/Models/User/User.php index 328f75dbaf..1857ca01cb 100644 --- a/app/Models/User/User.php +++ b/app/Models/User/User.php @@ -28,7 +28,7 @@ use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable implements MustVerifyEmail { - use Commenter, Notifiable, TwoFactorAuthenticatable, HasApiTokens; + use Commenter, HasApiTokens, Notifiable, TwoFactorAuthenticatable; /** * The attributes that are mass assignable. diff --git a/config/sanctum.php b/config/sanctum.php index 35d75b31e4..ae76c432e8 100644 --- a/config/sanctum.php +++ b/config/sanctum.php @@ -76,8 +76,8 @@ 'middleware' => [ 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, - 'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class, - 'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class, + 'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class, + 'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class, ], ]; diff --git a/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php b/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php index e828ad8189..73b0c178b7 100644 --- a/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php +++ b/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php @@ -4,13 +4,11 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. */ - public function up(): void - { + public function up(): void { Schema::create('personal_access_tokens', function (Blueprint $table) { $table->id(); $table->morphs('tokenable'); @@ -26,8 +24,7 @@ public function up(): void /** * Reverse the migrations. */ - public function down(): void - { + public function down(): void { Schema::dropIfExists('personal_access_tokens'); } }; diff --git a/routes/api.php b/routes/api.php index 5a457e3043..5122e97296 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,10 +1,5 @@ 'Api'], function () { Route::post('/token', 'AuthController@postGenerateToken'); -}); \ No newline at end of file +}); From 12b36bf720daff8929193d28237816bf654a1197 Mon Sep 17 00:00:00 2001 From: perappu Date: Sat, 9 Nov 2024 14:22:30 -0600 Subject: [PATCH 03/21] feat: proof-of-concept to get user and character info if user has api_access power --- app/Http/Controllers/Api/AuthController.php | 19 ++++++++++-------- app/Http/Controllers/Api/InfoController.php | 22 +++++++++++++++++++++ app/Models/User/User.php | 2 +- config/lorekeeper/powers.php | 4 ++++ routes/api.php | 15 +++++++++----- 5 files changed, 48 insertions(+), 14 deletions(-) create mode 100644 app/Http/Controllers/Api/InfoController.php diff --git a/app/Http/Controllers/Api/AuthController.php b/app/Http/Controllers/Api/AuthController.php index c6ca6e8bed..ee9df6b5bf 100644 --- a/app/Http/Controllers/Api/AuthController.php +++ b/app/Http/Controllers/Api/AuthController.php @@ -8,23 +8,26 @@ use Illuminate\Support\Facades\Hash; use Illuminate\Validation\ValidationException; -class AuthController extends Controller -{ +class AuthController extends Controller { public function postGenerateToken(Request $request) { + $request->validate([ - 'email' => 'required|email', - 'password' => 'required', + 'email' => 'required|email', + 'password' => 'required', 'token_name' => 'required', ]); - + $user = User::where('email', $request->email)->first(); - - if (! $user || ! Hash::check($request->password, $user->password)) { + + if (!$user || !Hash::check($request->password, $user->password)) { throw ValidationException::withMessages([ 'email' => ['The provided credentials are incorrect.'], ]); } - + + // Delete any pre-existing tokens + $user->tokens()->delete(); + return $user->createToken($request->token_name)->plainTextToken; } } diff --git a/app/Http/Controllers/Api/InfoController.php b/app/Http/Controllers/Api/InfoController.php new file mode 100644 index 0000000000..f291baadc2 --- /dev/null +++ b/app/Http/Controllers/Api/InfoController.php @@ -0,0 +1,22 @@ +id); + + return $user; + } + + public function getCharacter(Request $request) { + $character = Character::findOrFail($request->id); + + return $character; + } +} diff --git a/app/Models/User/User.php b/app/Models/User/User.php index 328f75dbaf..1857ca01cb 100644 --- a/app/Models/User/User.php +++ b/app/Models/User/User.php @@ -28,7 +28,7 @@ use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable implements MustVerifyEmail { - use Commenter, Notifiable, TwoFactorAuthenticatable, HasApiTokens; + use Commenter, HasApiTokens, Notifiable, TwoFactorAuthenticatable; /** * The attributes that are mass assignable. diff --git a/config/lorekeeper/powers.php b/config/lorekeeper/powers.php index 0a7519d794..8b38627e45 100644 --- a/config/lorekeeper/powers.php +++ b/config/lorekeeper/powers.php @@ -67,4 +67,8 @@ 'name' => 'Comment on Sales', 'description' => 'Allow rank to comment on sales in preview mode.', ], + 'api_access' => [ + 'name' => 'Has API Access', + 'description' => 'Can utilize the website\'s API.', + ], ]; diff --git a/routes/api.php b/routes/api.php index 5a457e3043..4e890e46c4 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,10 +1,5 @@ 'Api'], function () { + Route::post('/token', 'AuthController@postGenerateToken'); + + // TODO: Failing the power:api_access returns the HTML for the front page, would rather 401 + Route::group(['middleware' => ['auth:sanctum', 'power:api_access']], function() { + + Route::get('/user', 'InfoController@getUser'); + Route::get('/character', 'InfoController@getCharacter'); + + }); + }); \ No newline at end of file From 91c7c3044def8f51e63492cba2355d46f414c6ee Mon Sep 17 00:00:00 2001 From: perappu Date: Sat, 9 Nov 2024 20:26:38 +0000 Subject: [PATCH 04/21] refactor: fix PHP styling --- app/Http/Controllers/Api/AuthController.php | 1 - routes/api.php | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/app/Http/Controllers/Api/AuthController.php b/app/Http/Controllers/Api/AuthController.php index ee9df6b5bf..a043595da4 100644 --- a/app/Http/Controllers/Api/AuthController.php +++ b/app/Http/Controllers/Api/AuthController.php @@ -10,7 +10,6 @@ class AuthController extends Controller { public function postGenerateToken(Request $request) { - $request->validate([ 'email' => 'required|email', 'password' => 'required', diff --git a/routes/api.php b/routes/api.php index e85ce24f2c..fceaa8afb7 100644 --- a/routes/api.php +++ b/routes/api.php @@ -12,15 +12,11 @@ */ Route::group(['namespace' => 'Api'], function () { - Route::post('/token', 'AuthController@postGenerateToken'); // TODO: Failing the power:api_access returns the HTML for the front page, would rather 401 - Route::group(['middleware' => ['auth:sanctum', 'power:api_access']], function() { - + Route::group(['middleware' => ['auth:sanctum', 'power:api_access']], function () { Route::get('/user', 'InfoController@getUser'); Route::get('/character', 'InfoController@getCharacter'); - }); - }); From bfd18b7b4266708e24c1543df7f61bfc5bc22759 Mon Sep 17 00:00:00 2001 From: perappu Date: Sat, 1 Feb 2025 18:16:17 -0600 Subject: [PATCH 05/21] feat: revoke a user's current tokens from the Edit User interface --- .../Admin/Users/UserController.php | 39 ++++++ app/Services/UserService.php | 116 ++++++++++++------ .../_user_revoke_token_confirmation.blade.php | 4 + resources/views/admin/users/user.blade.php | 20 +++ routes/lorekeeper/admin.php | 3 + 5 files changed, 145 insertions(+), 37 deletions(-) create mode 100644 resources/views/admin/users/_user_revoke_token_confirmation.blade.php diff --git a/app/Http/Controllers/Admin/Users/UserController.php b/app/Http/Controllers/Admin/Users/UserController.php index 6ee7507116..2a706845d1 100644 --- a/app/Http/Controllers/Admin/Users/UserController.php +++ b/app/Http/Controllers/Admin/Users/UserController.php @@ -421,4 +421,43 @@ public function postReactivate(Request $request, UserService $service, $name) { return redirect()->back(); } + + /** + * Show a user's token revokation page. + * + * @param mixed $name + * + * @return \Illuminate\Contracts\Support\Renderable + */ + public function getRevokeTokensConfirmation($name) { + $user = User::where('name', $name)->with('settings')->first(); + + if (!$user) { + abort(404); + } + + return view('admin.users._user_revoke_token_confirmation', [ + 'user' => $user, + ]); + } + + public function postRevokeTokens(Request $request, UserService $service, $name) { + + $user = User::where('name', $name)->first(); + + if (!$user) { + flash('Invalid user.')->error(); + } elseif (!Auth::user()->canEditRank($user->rank)) { + flash('You cannot edit the information of a user that has a higher rank than yourself.')->error(); + } elseif ($service->revokeTokens($user, Auth::user())) { + flash('Revoked all user API tokens successfully.')->success(); + } else { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + } + + return redirect()->back(); + + } } diff --git a/app/Services/UserService.php b/app/Services/UserService.php index 46d8a9d93a..002cee21f5 100644 --- a/app/Services/UserService.php +++ b/app/Services/UserService.php @@ -21,7 +21,8 @@ use Intervention\Image\Facades\Image; use Laravel\Fortify\Contracts\TwoFactorAuthenticationProvider; -class UserService extends Service { +class UserService extends Service +{ /* |-------------------------------------------------------------------------- | User Service @@ -38,7 +39,8 @@ class UserService extends Service { * * @return User */ - public function createUser($data) { + public function createUser($data) + { // If the rank is not given, create a user with the lowest existing rank. if (!isset($data['rank_id'])) { $data['rank_id'] = Rank::orderBy('sort')->first()->id; @@ -74,14 +76,16 @@ public function createUser($data) { * * @return \Illuminate\Contracts\Validation\Validator */ - public function validator(array $data, $socialite = false) { + public function validator(array $data, $socialite = false) + { return Validator::make($data, [ 'name' => ['required', 'string', 'min:3', 'max:25', 'alpha_dash', 'unique:users'], 'email' => ($socialite ? [] : ['required']) + ['string', 'email', 'max:255', 'unique:users'], 'agreement' => ['required', 'accepted'], 'password' => ($socialite ? [] : ['required']) + ['string', 'min:8', 'confirmed'], 'dob' => [ - 'required', function ($attribute, $value, $fail) { + 'required', + function ($attribute, $value, $fail) { $formatDate = Carbon::createFromFormat('Y-m-d', $value); $now = Carbon::now(); if ($formatDate->diffInYears($now) < 13) { @@ -89,17 +93,19 @@ public function validator(array $data, $socialite = false) { } }, ], - 'code' => ['string', function ($attribute, $value, $fail) { - if (!Settings::get('is_registration_open')) { - if (!$value) { - $fail('An invitation code is required to register an account.'); + 'code' => [ + 'string', + function ($attribute, $value, $fail) { + if (!Settings::get('is_registration_open')) { + if (!$value) { + $fail('An invitation code is required to register an account.'); + } + $invitation = Invitation::where('code', $value)->whereNull('recipient_id')->first(); + if (!$invitation) { + $fail('Invalid code entered.'); + } } - $invitation = Invitation::where('code', $value)->whereNull('recipient_id')->first(); - if (!$invitation) { - $fail('Invalid code entered.'); - } - } - }, + }, ], ] + (config('app.env') == 'production' && config('lorekeeper.extensions.use_recaptcha') ? [ 'g-recaptcha-response' => 'required|recaptchav3:register,0.5', @@ -113,7 +119,8 @@ public function validator(array $data, $socialite = false) { * * @return User */ - public function updateUser($data) { + public function updateUser($data) + { $user = User::find($data['id']); if (isset($data['password'])) { $data['password'] = Hash::make($data['password']); @@ -133,7 +140,8 @@ public function updateUser($data) { * * @return bool */ - public function updatePassword($data, $user) { + public function updatePassword($data, $user) + { DB::beginTransaction(); try { @@ -163,7 +171,8 @@ public function updatePassword($data, $user) { * * @return bool */ - public function updateEmail($data, $user) { + public function updateEmail($data, $user) + { $user->email = $data['email']; $user->email_verified_at = null; $user->save(); @@ -179,7 +188,8 @@ public function updateEmail($data, $user) { * @param mixed $data * @param mixed $user */ - public function updateBirthday($data, $user) { + public function updateBirthday($data, $user) + { DB::beginTransaction(); try { @@ -200,7 +210,8 @@ public function updateBirthday($data, $user) { * @param mixed $data * @param mixed $user */ - public function updateBirthdayVisibilitySetting($data, $user) { + public function updateBirthdayVisibilitySetting($data, $user) + { DB::beginTransaction(); try { @@ -224,7 +235,8 @@ public function updateBirthdayVisibilitySetting($data, $user) { * * @return bool */ - public function confirmTwoFactor($code, $data, $user) { + public function confirmTwoFactor($code, $data, $user) + { DB::beginTransaction(); try { @@ -253,7 +265,8 @@ public function confirmTwoFactor($code, $data, $user) { * * @return bool */ - public function disableTwoFactor($code, $user) { + public function disableTwoFactor($code, $user) + { DB::beginTransaction(); try { @@ -280,7 +293,8 @@ public function disableTwoFactor($code, $user) { * @param mixed $data * @param mixed $user */ - public function updateContentWarningVisibility($data, $user) { + public function updateContentWarningVisibility($data, $user) + { DB::beginTransaction(); try { @@ -303,17 +317,18 @@ public function updateContentWarningVisibility($data, $user) { * * @return bool */ - public function updateAvatar($avatar, $user) { + public function updateAvatar($avatar, $user) + { DB::beginTransaction(); try { if (!$avatar) { throw new \Exception('Please upload a file.'); } - $filename = $user->id.'.'.$avatar->getClientOriginalExtension(); + $filename = $user->id . '.' . $avatar->getClientOriginalExtension(); if ($user->avatar != 'default.jpg') { - $file = 'images/avatars/'.$user->avatar; + $file = 'images/avatars/' . $user->avatar; //$destinationPath = 'uploads/' . $id . '/'; if (File::exists($file)) { @@ -329,7 +344,7 @@ public function updateAvatar($avatar, $user) { throw new \Exception('Failed to move file.'); } } else { - if (!Image::make($avatar)->resize(150, 150)->save(public_path('images/avatars/'.$filename))) { + if (!Image::make($avatar)->resize(150, 150)->save(public_path('images/avatars/' . $filename))) { throw new \Exception('Failed to process avatar.'); } } @@ -353,7 +368,8 @@ public function updateAvatar($avatar, $user) { * * @return bool */ - public function updateUsername($username, $user) { + public function updateUsername($username, $user) + { DB::beginTransaction(); try { @@ -382,8 +398,8 @@ public function updateUsername($username, $user) { $last_change = UserUpdateLog::where('user_id', $user->id)->where('type', 'Username Change')->orderBy('created_at', 'desc')->first(); if ($last_change && $last_change->created_at->diffInDays(Carbon::now()) < config('lorekeeper.settings.username_change_cooldown')) { throw new \Exception('You must wait ' - .config('lorekeeper.settings.username_change_cooldown') - $last_change->created_at->diffInDays(Carbon::now()). - ' days before changing your username again.'); + . config('lorekeeper.settings.username_change_cooldown') - $last_change->created_at->diffInDays(Carbon::now()) . + ' days before changing your username again.'); } } @@ -415,13 +431,14 @@ public function updateUsername($username, $user) { * * @return bool */ - public function ban($data, $user, $staff) { + public function ban($data, $user, $staff) + { DB::beginTransaction(); try { if (!$user->is_banned) { // New ban (not just editing the reason), clear all their engagements - if (!$this->logAdminAction($staff, 'Banned User', 'Banned '.$user->displayname)) { + if (!$this->logAdminAction($staff, 'Banned User', 'Banned ' . $user->displayname)) { throw new \Exception('Failed to log admin action.'); } @@ -431,7 +448,7 @@ public function ban($data, $user, $staff) { $query->where('sender_id', $user->id)->orWhere('recipient_id', $user->id); })->where('status', 'Pending')->get(); foreach ($transfers as $transfer) { - $characterManager->processTransferQueue(['transfer' => $transfer, 'action' => 'Reject', 'reason' => ($transfer->sender_id == $user->id ? 'Sender' : 'Recipient').' has been banned from site activity.'], $staff); + $characterManager->processTransferQueue(['transfer' => $transfer, 'action' => 'Reject', 'reason' => ($transfer->sender_id == $user->id ? 'Sender' : 'Recipient') . ' has been banned from site activity.'], $staff); } // 2. Submissions and claims @@ -502,11 +519,12 @@ public function ban($data, $user, $staff) { * * @return bool */ - public function unban($user, $staff) { + public function unban($user, $staff) + { DB::beginTransaction(); try { - if (!$this->logAdminAction($staff, 'Unbanned User', 'Unbanned '.$user->displayname)) { + if (!$this->logAdminAction($staff, 'Unbanned User', 'Unbanned ' . $user->displayname)) { throw new \Exception('Failed to log admin action.'); } @@ -537,7 +555,8 @@ public function unban($user, $staff) { * * @return bool */ - public function deactivate($data, $user, $staff = null) { + public function deactivate($data, $user, $staff = null) + { DB::beginTransaction(); try { @@ -553,7 +572,7 @@ public function deactivate($data, $user, $staff = null) { $query->where('sender_id', $user->id)->orWhere('recipient_id', $user->id); })->where('status', 'Pending')->get(); foreach ($transfers as $transfer) { - $characterManager->processTransferQueue(['transfer' => $transfer, 'action' => 'Reject', 'reason' => ($transfer->sender_id == $user->id ? 'Sender' : 'Recipient').'\'s account was deactivated.'], ($staff ? $staff : $user)); + $characterManager->processTransferQueue(['transfer' => $transfer, 'action' => 'Reject', 'reason' => ($transfer->sender_id == $user->id ? 'Sender' : 'Recipient') . '\'s account was deactivated.'], ($staff ? $staff : $user)); } // 2. Submissions and claims @@ -632,7 +651,8 @@ public function deactivate($data, $user, $staff = null) { * * @return bool */ - public function reactivate($user, $staff = null) { + public function reactivate($user, $staff = null) + { DB::beginTransaction(); try { @@ -664,4 +684,26 @@ public function reactivate($user, $staff = null) { return $this->rollbackReturn(false); } + + /** + * Revokes all of a user's API tokens. + * + * @param User $user + * @param User $staff + * + * @return bool + */ + public function revokeTokens($user, $staff = null) { + try { + $user->tokens()->delete(); + + UserUpdateLog::create(['staff_id' => $staff ? $staff->id : $user->id, 'user_id' => $user->id, 'data' => json_encode([]), 'type' => 'Tokens Revoked']); + + return true; + + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + return false; + } } diff --git a/resources/views/admin/users/_user_revoke_token_confirmation.blade.php b/resources/views/admin/users/_user_revoke_token_confirmation.blade.php new file mode 100644 index 0000000000..6c129de140 --- /dev/null +++ b/resources/views/admin/users/_user_revoke_token_confirmation.blade.php @@ -0,0 +1,4 @@ +

Are you sure you want to revoke all of {!! $user->displayName !!}'s API tokens?

+{!! Form::open(['url' => 'admin/users/' . $user->name . '/revoke']) !!} + {!! Form::submit('Revoke Tokens', ['class' => 'btn btn-danger']) !!} +{!! Form::close() !!} \ No newline at end of file diff --git a/resources/views/admin/users/user.blade.php b/resources/views/admin/users/user.blade.php index 957d405985..5254f57c75 100644 --- a/resources/views/admin/users/user.blade.php +++ b/resources/views/admin/users/user.blade.php @@ -132,4 +132,24 @@

No aliases found.

@endif + +
+

API Tokens

+

As an admin, you are able to revoke a user's API tokens at any time.

+

This user currently has {{ $user->tokens()->count() }} active API token(s).

+ {!! Form::open(['url' => 'admin/users/' . $user->name . '/revoke']) !!} + {!! Form::submit('Revoke Tokens', ['class' => 'btn btn-danger revoke-button']) !!} + {!! Form::close() !!} +
@endsection + +@section('scripts') + +@endsection \ No newline at end of file diff --git a/routes/lorekeeper/admin.php b/routes/lorekeeper/admin.php index 906913a53c..7b76fe5139 100644 --- a/routes/lorekeeper/admin.php +++ b/routes/lorekeeper/admin.php @@ -40,6 +40,9 @@ Route::post('{name}/deactivate', 'UserController@postDeactivate'); Route::get('{name}/reactivate-confirm', 'UserController@getReactivateConfirmation'); Route::post('{name}/reactivate', 'UserController@postReactivate'); + + Route::get('{name}/revoke-confirm', 'UserController@getRevokeTokensConfirmation'); + Route::post('{name}/revoke', 'UserController@postRevokeTokens'); }); // RANKS From 27943b0177d2f14cb99f0ba7febd97f6a9809cc6 Mon Sep 17 00:00:00 2001 From: perappu Date: Sun, 2 Feb 2025 10:31:01 -0600 Subject: [PATCH 06/21] feat: UI interface for creating and revoking tokens --- app/Http/Controllers/Admin/ApiController.php | 63 ++++++++++++++++++++ app/Http/Controllers/Api/AuthController.php | 4 ++ app/Http/Controllers/Api/InfoController.php | 8 +-- app/Http/Kernel.php | 2 +- app/Services/UserService.php | 30 +++++++++- config/lorekeeper/admin_sidebar.php | 9 +++ resources/views/admin/api/api.blade.php | 41 +++++++++++++ routes/lorekeeper/admin.php | 7 +++ 8 files changed, 155 insertions(+), 9 deletions(-) create mode 100644 app/Http/Controllers/Admin/ApiController.php create mode 100644 resources/views/admin/api/api.blade.php diff --git a/app/Http/Controllers/Admin/ApiController.php b/app/Http/Controllers/Admin/ApiController.php new file mode 100644 index 0000000000..94ca0cf1aa --- /dev/null +++ b/app/Http/Controllers/Admin/ApiController.php @@ -0,0 +1,63 @@ +generateToken(Auth::user())) { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + } + + return redirect()->back()->withInput(); + } + + /** + * Generates (or re-generates) an API token + * + * @return \Illuminate\Contracts\Support\Renderable + */ + public function postRevokeToken(Request $request, UserService $service) { + + if (!$service->revokeTokens(Auth::user())) { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + } else { + flash('Token(s) revoked successfully.')->success(); + } + + return redirect()->back()->withInput(); + } + +} \ No newline at end of file diff --git a/app/Http/Controllers/Api/AuthController.php b/app/Http/Controllers/Api/AuthController.php index a043595da4..59dde7f037 100644 --- a/app/Http/Controllers/Api/AuthController.php +++ b/app/Http/Controllers/Api/AuthController.php @@ -9,6 +9,10 @@ use Illuminate\Validation\ValidationException; class AuthController extends Controller { + + /** + * Authenticate with email/password and receive a PAT through the API + */ public function postGenerateToken(Request $request) { $request->validate([ 'email' => 'required|email', diff --git a/app/Http/Controllers/Api/InfoController.php b/app/Http/Controllers/Api/InfoController.php index f291baadc2..024d065ddb 100644 --- a/app/Http/Controllers/Api/InfoController.php +++ b/app/Http/Controllers/Api/InfoController.php @@ -8,15 +8,9 @@ use Illuminate\Http\Request; class InfoController extends Controller { - public function getUser(Request $request) { - $user = User::findOrFail($request->id); - - return $user; - } - public function getCharacter(Request $request) { $character = Character::findOrFail($request->id); - return $character; + return response()->json($character); } } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index d4d56e4681..aa3eb75e83 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -39,7 +39,7 @@ class Kernel extends HttpKernel { 'api' => [ \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, - 'throttle:api', + 'throttle:60,1', \Illuminate\Routing\Middleware\SubstituteBindings::class, ], ]; diff --git a/app/Services/UserService.php b/app/Services/UserService.php index 002cee21f5..3554b9f8dd 100644 --- a/app/Services/UserService.php +++ b/app/Services/UserService.php @@ -685,6 +685,34 @@ public function reactivate($user, $staff = null) return $this->rollbackReturn(false); } + /** + * Generate an API token for the user. + * + * @param User $user + * @param User $staff + * + * @return bool + */ + public function generateToken($user) { + try { + + $token = $user->createToken("token")->plainTextToken; + + UserUpdateLog::create(['staff_id' => $user->id, 'user_id' => $user->id, 'data' => json_encode([]), 'type' => 'Generated API Token']); + + flash('Token created successfully:')->success(); + flash($token)->success(); + flash('Copy this down! It will NOT be shown again.')->warning(); + + return true; + + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + return false; + } + + /** * Revokes all of a user's API tokens. * @@ -698,7 +726,7 @@ public function revokeTokens($user, $staff = null) { $user->tokens()->delete(); UserUpdateLog::create(['staff_id' => $staff ? $staff->id : $user->id, 'user_id' => $user->id, 'data' => json_encode([]), 'type' => 'Tokens Revoked']); - + return true; } catch (\Exception $e) { diff --git a/config/lorekeeper/admin_sidebar.php b/config/lorekeeper/admin_sidebar.php index c1deabf75d..e1f4830b73 100644 --- a/config/lorekeeper/admin_sidebar.php +++ b/config/lorekeeper/admin_sidebar.php @@ -31,6 +31,15 @@ ], ], ], + 'API' => [ + 'power' => 'api_access', + 'links' => [ + [ + 'name' => 'API Tokens', + 'url' => 'admin/api', + ] + ], + ], 'Reports' => [ 'power' => 'manage_reports', 'links' => [ diff --git a/resources/views/admin/api/api.blade.php b/resources/views/admin/api/api.blade.php new file mode 100644 index 0000000000..ae421ddb8b --- /dev/null +++ b/resources/views/admin/api/api.blade.php @@ -0,0 +1,41 @@ +@extends('admin.layout') + +@section('admin-title') + API Tokens +@endsection + +@section('admin-content') + {!! breadcrumbs(['Admin Panel' => 'admin', 'API Tokens' => 'admin/api']) !!} + +

API Tokens

+ +

Here you may generate personal access tokens for your account. Be warned: giving away this token is like giving away your password!

+ + @if(Auth::user()->tokens->count()) +
+

Regenerate Token

+

You already have an API token generated. You may regenerate this token with the button below. It will create a new token, not show you your current one.

+ {!! Form::open(['url' => 'admin/api/token']) !!} + {!! Form::submit('Regenerate Token', ['class' => 'btn btn-warning']) !!} + {!! Form::close() !!} +
+
+

Revoke Token

+

Click the below button to revoke (delete) your current API token. This can not be undone!

+ {!! Form::open(['url' => 'admin/api/revoke']) !!} + {!! Form::submit('Revoke Token', ['class' => 'btn btn-danger']) !!} + {!! Form::close() !!} + +
+ @else +
+

Generate Token

+

Clicking the below button will generate a token for your account. This token will only display ONCE. Be sure to copy it down!

+ {!! Form::open(['url' => 'admin/api/token']) !!} + {!! Form::submit('Generate Token', ['class' => 'btn btn-primary']) !!} + {!! Form::close() !!} +
+ @endif + + +@endsection diff --git a/routes/lorekeeper/admin.php b/routes/lorekeeper/admin.php index 7b76fe5139..7ee9f548b8 100644 --- a/routes/lorekeeper/admin.php +++ b/routes/lorekeeper/admin.php @@ -17,6 +17,13 @@ Route::post('staff-reward-settings/{key}', 'HomeController@postEditStaffRewardSetting'); }); +Route::group(['prefix' => 'api','middleware' => 'power:api_access'], function () { + Route::get('/', 'ApiController@getIndex'); + + Route::post('/token', 'ApiController@postGenerateToken'); + Route::post('/revoke', 'ApiController@postRevokeToken'); +}); + Route::group(['prefix' => 'users', 'namespace' => 'Users'], function () { // USER LIST Route::group(['middleware' => 'power:edit_user_info'], function () { From 69b298794282da219d14f54857cff4bb0b82e74b Mon Sep 17 00:00:00 2001 From: perappu Date: Sun, 2 Feb 2025 16:33:21 +0000 Subject: [PATCH 07/21] refactor: fix blade formatting --- resources/views/admin/api/api.blade.php | 38 +++++++++---------- .../_user_revoke_token_confirmation.blade.php | 4 +- resources/views/admin/users/user.blade.php | 12 +++--- 3 files changed, 26 insertions(+), 28 deletions(-) diff --git a/resources/views/admin/api/api.blade.php b/resources/views/admin/api/api.blade.php index ae421ddb8b..8f6ec42150 100644 --- a/resources/views/admin/api/api.blade.php +++ b/resources/views/admin/api/api.blade.php @@ -11,31 +11,29 @@

Here you may generate personal access tokens for your account. Be warned: giving away this token is like giving away your password!

- @if(Auth::user()->tokens->count()) -
-

Regenerate Token

+ @if (Auth::user()->tokens->count()) +
+

Regenerate Token

You already have an API token generated. You may regenerate this token with the button below. It will create a new token, not show you your current one.

{!! Form::open(['url' => 'admin/api/token']) !!} {!! Form::submit('Regenerate Token', ['class' => 'btn btn-warning']) !!} {!! Form::close() !!} -
-
-

Revoke Token

+
+
+

Revoke Token

Click the below button to revoke (delete) your current API token. This can not be undone!

- {!! Form::open(['url' => 'admin/api/revoke']) !!} - {!! Form::submit('Revoke Token', ['class' => 'btn btn-danger']) !!} - {!! Form::close() !!} - -
+ {!! Form::open(['url' => 'admin/api/revoke']) !!} + {!! Form::submit('Revoke Token', ['class' => 'btn btn-danger']) !!} + {!! Form::close() !!} + +
@else -
-

Generate Token

-

Clicking the below button will generate a token for your account. This token will only display ONCE. Be sure to copy it down!

- {!! Form::open(['url' => 'admin/api/token']) !!} - {!! Form::submit('Generate Token', ['class' => 'btn btn-primary']) !!} - {!! Form::close() !!} -
+
+

Generate Token

+

Clicking the below button will generate a token for your account. This token will only display ONCE. Be sure to copy it down!

+ {!! Form::open(['url' => 'admin/api/token']) !!} + {!! Form::submit('Generate Token', ['class' => 'btn btn-primary']) !!} + {!! Form::close() !!} +
@endif - - @endsection diff --git a/resources/views/admin/users/_user_revoke_token_confirmation.blade.php b/resources/views/admin/users/_user_revoke_token_confirmation.blade.php index 6c129de140..67c0f2fb82 100644 --- a/resources/views/admin/users/_user_revoke_token_confirmation.blade.php +++ b/resources/views/admin/users/_user_revoke_token_confirmation.blade.php @@ -1,4 +1,4 @@

Are you sure you want to revoke all of {!! $user->displayName !!}'s API tokens?

{!! Form::open(['url' => 'admin/users/' . $user->name . '/revoke']) !!} - {!! Form::submit('Revoke Tokens', ['class' => 'btn btn-danger']) !!} -{!! Form::close() !!} \ No newline at end of file +{!! Form::submit('Revoke Tokens', ['class' => 'btn btn-danger']) !!} +{!! Form::close() !!} diff --git a/resources/views/admin/users/user.blade.php b/resources/views/admin/users/user.blade.php index 5254f57c75..b82f4af223 100644 --- a/resources/views/admin/users/user.blade.php +++ b/resources/views/admin/users/user.blade.php @@ -138,7 +138,7 @@

As an admin, you are able to revoke a user's API tokens at any time.

This user currently has {{ $user->tokens()->count() }} active API token(s).

{!! Form::open(['url' => 'admin/users/' . $user->name . '/revoke']) !!} - {!! Form::submit('Revoke Tokens', ['class' => 'btn btn-danger revoke-button']) !!} + {!! Form::submit('Revoke Tokens', ['class' => 'btn btn-danger revoke-button']) !!} {!! Form::close() !!} @endsection @@ -146,10 +146,10 @@ @section('scripts') -@endsection \ No newline at end of file +@endsection From 2c79c00244b493bbd8f3fce2f53643cc1dff1186 Mon Sep 17 00:00:00 2001 From: perappu Date: Sun, 2 Feb 2025 16:35:53 +0000 Subject: [PATCH 08/21] refactor: fix PHP styling --- app/Http/Controllers/Admin/ApiController.php | 9 +- .../Admin/Users/UserController.php | 4 +- app/Http/Controllers/Api/AuthController.php | 3 +- app/Http/Controllers/Api/InfoController.php | 1 - app/Services/UserService.php | 82 +++++++------------ config/lorekeeper/admin_sidebar.php | 2 +- routes/lorekeeper/admin.php | 2 +- 7 files changed, 38 insertions(+), 65 deletions(-) diff --git a/app/Http/Controllers/Admin/ApiController.php b/app/Http/Controllers/Admin/ApiController.php index 94ca0cf1aa..8073c06e91 100644 --- a/app/Http/Controllers/Admin/ApiController.php +++ b/app/Http/Controllers/Admin/ApiController.php @@ -27,12 +27,11 @@ public function getIndex() { } /** - * Generates (or re-generates) an API token + * Generates (or re-generates) an API token. * * @return \Illuminate\Contracts\Support\Renderable */ public function postGenerateToken(Request $request, UserService $service) { - if (!$service->generateToken(Auth::user())) { foreach ($service->errors()->getMessages()['error'] as $error) { flash($error)->error(); @@ -43,12 +42,11 @@ public function postGenerateToken(Request $request, UserService $service) { } /** - * Generates (or re-generates) an API token + * Generates (or re-generates) an API token. * * @return \Illuminate\Contracts\Support\Renderable */ public function postRevokeToken(Request $request, UserService $service) { - if (!$service->revokeTokens(Auth::user())) { foreach ($service->errors()->getMessages()['error'] as $error) { flash($error)->error(); @@ -59,5 +57,4 @@ public function postRevokeToken(Request $request, UserService $service) { return redirect()->back()->withInput(); } - -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Admin/Users/UserController.php b/app/Http/Controllers/Admin/Users/UserController.php index 2a706845d1..0928b1986d 100644 --- a/app/Http/Controllers/Admin/Users/UserController.php +++ b/app/Http/Controllers/Admin/Users/UserController.php @@ -422,7 +422,7 @@ public function postReactivate(Request $request, UserService $service, $name) { return redirect()->back(); } - /** + /** * Show a user's token revokation page. * * @param mixed $name @@ -442,7 +442,6 @@ public function getRevokeTokensConfirmation($name) { } public function postRevokeTokens(Request $request, UserService $service, $name) { - $user = User::where('name', $name)->first(); if (!$user) { @@ -458,6 +457,5 @@ public function postRevokeTokens(Request $request, UserService $service, $name) } return redirect()->back(); - } } diff --git a/app/Http/Controllers/Api/AuthController.php b/app/Http/Controllers/Api/AuthController.php index 59dde7f037..064ee3a6d0 100644 --- a/app/Http/Controllers/Api/AuthController.php +++ b/app/Http/Controllers/Api/AuthController.php @@ -9,9 +9,8 @@ use Illuminate\Validation\ValidationException; class AuthController extends Controller { - /** - * Authenticate with email/password and receive a PAT through the API + * Authenticate with email/password and receive a PAT through the API. */ public function postGenerateToken(Request $request) { $request->validate([ diff --git a/app/Http/Controllers/Api/InfoController.php b/app/Http/Controllers/Api/InfoController.php index 024d065ddb..2567ed09c3 100644 --- a/app/Http/Controllers/Api/InfoController.php +++ b/app/Http/Controllers/Api/InfoController.php @@ -4,7 +4,6 @@ use App\Http\Controllers\Controller; use App\Models\Character\Character; -use App\Models\User\User; use Illuminate\Http\Request; class InfoController extends Controller { diff --git a/app/Services/UserService.php b/app/Services/UserService.php index 3554b9f8dd..1fcd09bb17 100644 --- a/app/Services/UserService.php +++ b/app/Services/UserService.php @@ -21,8 +21,7 @@ use Intervention\Image\Facades\Image; use Laravel\Fortify\Contracts\TwoFactorAuthenticationProvider; -class UserService extends Service -{ +class UserService extends Service { /* |-------------------------------------------------------------------------- | User Service @@ -39,8 +38,7 @@ class UserService extends Service * * @return User */ - public function createUser($data) - { + public function createUser($data) { // If the rank is not given, create a user with the lowest existing rank. if (!isset($data['rank_id'])) { $data['rank_id'] = Rank::orderBy('sort')->first()->id; @@ -76,8 +74,7 @@ public function createUser($data) * * @return \Illuminate\Contracts\Validation\Validator */ - public function validator(array $data, $socialite = false) - { + public function validator(array $data, $socialite = false) { return Validator::make($data, [ 'name' => ['required', 'string', 'min:3', 'max:25', 'alpha_dash', 'unique:users'], 'email' => ($socialite ? [] : ['required']) + ['string', 'email', 'max:255', 'unique:users'], @@ -119,8 +116,7 @@ function ($attribute, $value, $fail) { * * @return User */ - public function updateUser($data) - { + public function updateUser($data) { $user = User::find($data['id']); if (isset($data['password'])) { $data['password'] = Hash::make($data['password']); @@ -140,8 +136,7 @@ public function updateUser($data) * * @return bool */ - public function updatePassword($data, $user) - { + public function updatePassword($data, $user) { DB::beginTransaction(); try { @@ -171,8 +166,7 @@ public function updatePassword($data, $user) * * @return bool */ - public function updateEmail($data, $user) - { + public function updateEmail($data, $user) { $user->email = $data['email']; $user->email_verified_at = null; $user->save(); @@ -188,8 +182,7 @@ public function updateEmail($data, $user) * @param mixed $data * @param mixed $user */ - public function updateBirthday($data, $user) - { + public function updateBirthday($data, $user) { DB::beginTransaction(); try { @@ -210,8 +203,7 @@ public function updateBirthday($data, $user) * @param mixed $data * @param mixed $user */ - public function updateBirthdayVisibilitySetting($data, $user) - { + public function updateBirthdayVisibilitySetting($data, $user) { DB::beginTransaction(); try { @@ -235,8 +227,7 @@ public function updateBirthdayVisibilitySetting($data, $user) * * @return bool */ - public function confirmTwoFactor($code, $data, $user) - { + public function confirmTwoFactor($code, $data, $user) { DB::beginTransaction(); try { @@ -265,8 +256,7 @@ public function confirmTwoFactor($code, $data, $user) * * @return bool */ - public function disableTwoFactor($code, $user) - { + public function disableTwoFactor($code, $user) { DB::beginTransaction(); try { @@ -293,8 +283,7 @@ public function disableTwoFactor($code, $user) * @param mixed $data * @param mixed $user */ - public function updateContentWarningVisibility($data, $user) - { + public function updateContentWarningVisibility($data, $user) { DB::beginTransaction(); try { @@ -317,18 +306,17 @@ public function updateContentWarningVisibility($data, $user) * * @return bool */ - public function updateAvatar($avatar, $user) - { + public function updateAvatar($avatar, $user) { DB::beginTransaction(); try { if (!$avatar) { throw new \Exception('Please upload a file.'); } - $filename = $user->id . '.' . $avatar->getClientOriginalExtension(); + $filename = $user->id.'.'.$avatar->getClientOriginalExtension(); if ($user->avatar != 'default.jpg') { - $file = 'images/avatars/' . $user->avatar; + $file = 'images/avatars/'.$user->avatar; //$destinationPath = 'uploads/' . $id . '/'; if (File::exists($file)) { @@ -344,7 +332,7 @@ public function updateAvatar($avatar, $user) throw new \Exception('Failed to move file.'); } } else { - if (!Image::make($avatar)->resize(150, 150)->save(public_path('images/avatars/' . $filename))) { + if (!Image::make($avatar)->resize(150, 150)->save(public_path('images/avatars/'.$filename))) { throw new \Exception('Failed to process avatar.'); } } @@ -368,8 +356,7 @@ public function updateAvatar($avatar, $user) * * @return bool */ - public function updateUsername($username, $user) - { + public function updateUsername($username, $user) { DB::beginTransaction(); try { @@ -398,7 +385,7 @@ public function updateUsername($username, $user) $last_change = UserUpdateLog::where('user_id', $user->id)->where('type', 'Username Change')->orderBy('created_at', 'desc')->first(); if ($last_change && $last_change->created_at->diffInDays(Carbon::now()) < config('lorekeeper.settings.username_change_cooldown')) { throw new \Exception('You must wait ' - . config('lorekeeper.settings.username_change_cooldown') - $last_change->created_at->diffInDays(Carbon::now()) . + .config('lorekeeper.settings.username_change_cooldown') - $last_change->created_at->diffInDays(Carbon::now()). ' days before changing your username again.'); } } @@ -431,14 +418,13 @@ public function updateUsername($username, $user) * * @return bool */ - public function ban($data, $user, $staff) - { + public function ban($data, $user, $staff) { DB::beginTransaction(); try { if (!$user->is_banned) { // New ban (not just editing the reason), clear all their engagements - if (!$this->logAdminAction($staff, 'Banned User', 'Banned ' . $user->displayname)) { + if (!$this->logAdminAction($staff, 'Banned User', 'Banned '.$user->displayname)) { throw new \Exception('Failed to log admin action.'); } @@ -448,7 +434,7 @@ public function ban($data, $user, $staff) $query->where('sender_id', $user->id)->orWhere('recipient_id', $user->id); })->where('status', 'Pending')->get(); foreach ($transfers as $transfer) { - $characterManager->processTransferQueue(['transfer' => $transfer, 'action' => 'Reject', 'reason' => ($transfer->sender_id == $user->id ? 'Sender' : 'Recipient') . ' has been banned from site activity.'], $staff); + $characterManager->processTransferQueue(['transfer' => $transfer, 'action' => 'Reject', 'reason' => ($transfer->sender_id == $user->id ? 'Sender' : 'Recipient').' has been banned from site activity.'], $staff); } // 2. Submissions and claims @@ -519,12 +505,11 @@ public function ban($data, $user, $staff) * * @return bool */ - public function unban($user, $staff) - { + public function unban($user, $staff) { DB::beginTransaction(); try { - if (!$this->logAdminAction($staff, 'Unbanned User', 'Unbanned ' . $user->displayname)) { + if (!$this->logAdminAction($staff, 'Unbanned User', 'Unbanned '.$user->displayname)) { throw new \Exception('Failed to log admin action.'); } @@ -555,8 +540,7 @@ public function unban($user, $staff) * * @return bool */ - public function deactivate($data, $user, $staff = null) - { + public function deactivate($data, $user, $staff = null) { DB::beginTransaction(); try { @@ -572,7 +556,7 @@ public function deactivate($data, $user, $staff = null) $query->where('sender_id', $user->id)->orWhere('recipient_id', $user->id); })->where('status', 'Pending')->get(); foreach ($transfers as $transfer) { - $characterManager->processTransferQueue(['transfer' => $transfer, 'action' => 'Reject', 'reason' => ($transfer->sender_id == $user->id ? 'Sender' : 'Recipient') . '\'s account was deactivated.'], ($staff ? $staff : $user)); + $characterManager->processTransferQueue(['transfer' => $transfer, 'action' => 'Reject', 'reason' => ($transfer->sender_id == $user->id ? 'Sender' : 'Recipient').'\'s account was deactivated.'], ($staff ? $staff : $user)); } // 2. Submissions and claims @@ -651,8 +635,7 @@ public function deactivate($data, $user, $staff = null) * * @return bool */ - public function reactivate($user, $staff = null) - { + public function reactivate($user, $staff = null) { DB::beginTransaction(); try { @@ -689,30 +672,27 @@ public function reactivate($user, $staff = null) * Generate an API token for the user. * * @param User $user - * @param User $staff * * @return bool */ public function generateToken($user) { try { - - $token = $user->createToken("token")->plainTextToken; - + $token = $user->createToken('token')->plainTextToken; + UserUpdateLog::create(['staff_id' => $user->id, 'user_id' => $user->id, 'data' => json_encode([]), 'type' => 'Generated API Token']); - + flash('Token created successfully:')->success(); flash($token)->success(); flash('Copy this down! It will NOT be shown again.')->warning(); return true; - } catch (\Exception $e) { $this->setError('error', $e->getMessage()); } + return false; } - /** * Revokes all of a user's API tokens. * @@ -724,14 +704,14 @@ public function generateToken($user) { public function revokeTokens($user, $staff = null) { try { $user->tokens()->delete(); - + UserUpdateLog::create(['staff_id' => $staff ? $staff->id : $user->id, 'user_id' => $user->id, 'data' => json_encode([]), 'type' => 'Tokens Revoked']); return true; - } catch (\Exception $e) { $this->setError('error', $e->getMessage()); } + return false; } } diff --git a/config/lorekeeper/admin_sidebar.php b/config/lorekeeper/admin_sidebar.php index e1f4830b73..772ef004f5 100644 --- a/config/lorekeeper/admin_sidebar.php +++ b/config/lorekeeper/admin_sidebar.php @@ -37,7 +37,7 @@ [ 'name' => 'API Tokens', 'url' => 'admin/api', - ] + ], ], ], 'Reports' => [ diff --git a/routes/lorekeeper/admin.php b/routes/lorekeeper/admin.php index 7ee9f548b8..dffd01ecbd 100644 --- a/routes/lorekeeper/admin.php +++ b/routes/lorekeeper/admin.php @@ -17,7 +17,7 @@ Route::post('staff-reward-settings/{key}', 'HomeController@postEditStaffRewardSetting'); }); -Route::group(['prefix' => 'api','middleware' => 'power:api_access'], function () { +Route::group(['prefix' => 'api', 'middleware' => 'power:api_access'], function () { Route::get('/', 'ApiController@getIndex'); Route::post('/token', 'ApiController@postGenerateToken'); From ae0d50c8f089645797cd71bbc70965a6061e7b9e Mon Sep 17 00:00:00 2001 From: perappu Date: Sun, 2 Feb 2025 10:54:02 -0600 Subject: [PATCH 09/21] fix: only allow PAT generation via API if the user has the correct permission --- app/Http/Controllers/Api/AuthController.php | 29 ++++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/app/Http/Controllers/Api/AuthController.php b/app/Http/Controllers/Api/AuthController.php index 064ee3a6d0..032b9a034b 100644 --- a/app/Http/Controllers/Api/AuthController.php +++ b/app/Http/Controllers/Api/AuthController.php @@ -8,11 +8,13 @@ use Illuminate\Support\Facades\Hash; use Illuminate\Validation\ValidationException; -class AuthController extends Controller { +class AuthController extends Controller +{ /** * Authenticate with email/password and receive a PAT through the API. */ - public function postGenerateToken(Request $request) { + public function postGenerateToken(Request $request) + { $request->validate([ 'email' => 'required|email', 'password' => 'required', @@ -21,15 +23,22 @@ public function postGenerateToken(Request $request) { $user = User::where('email', $request->email)->first(); - if (!$user || !Hash::check($request->password, $user->password)) { - throw ValidationException::withMessages([ - 'email' => ['The provided credentials are incorrect.'], - ]); - } + if ($user->hasPower('api_access')) { + if (!$user || !Hash::check($request->password, $user->password)) { + throw ValidationException::withMessages([ + 'email' => ['The provided credentials are incorrect.'], + ]); + } + - // Delete any pre-existing tokens - $user->tokens()->delete(); + // Delete any pre-existing tokens + $user->tokens()->delete(); - return $user->createToken($request->token_name)->plainTextToken; + return $user->createToken($request->token_name)->plainTextToken; + } else { + return response()->json([ + 'message' => 'You do not have API access permissions.' + ], 404); + } } } From 767c869322cf7b6d2be6ae9b449c1183c512df9f Mon Sep 17 00:00:00 2001 From: perappu Date: Sun, 2 Feb 2025 16:57:39 +0000 Subject: [PATCH 10/21] refactor: fix PHP styling --- app/Http/Controllers/Api/AuthController.php | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/Api/AuthController.php b/app/Http/Controllers/Api/AuthController.php index 032b9a034b..bde37c89a3 100644 --- a/app/Http/Controllers/Api/AuthController.php +++ b/app/Http/Controllers/Api/AuthController.php @@ -8,13 +8,11 @@ use Illuminate\Support\Facades\Hash; use Illuminate\Validation\ValidationException; -class AuthController extends Controller -{ +class AuthController extends Controller { /** * Authenticate with email/password and receive a PAT through the API. */ - public function postGenerateToken(Request $request) - { + public function postGenerateToken(Request $request) { $request->validate([ 'email' => 'required|email', 'password' => 'required', @@ -30,14 +28,13 @@ public function postGenerateToken(Request $request) ]); } - // Delete any pre-existing tokens $user->tokens()->delete(); return $user->createToken($request->token_name)->plainTextToken; } else { return response()->json([ - 'message' => 'You do not have API access permissions.' + 'message' => 'You do not have API access permissions.', ], 404); } } From c1f46855129a1920945b834367efc1b2fdbb46e6 Mon Sep 17 00:00:00 2001 From: perappu Date: Sun, 2 Feb 2025 11:11:45 -0600 Subject: [PATCH 11/21] chore: remove irrelevant todo comment lmao --- routes/api.php | 1 - 1 file changed, 1 deletion(-) diff --git a/routes/api.php b/routes/api.php index fceaa8afb7..93bde0b927 100644 --- a/routes/api.php +++ b/routes/api.php @@ -14,7 +14,6 @@ Route::group(['namespace' => 'Api'], function () { Route::post('/token', 'AuthController@postGenerateToken'); - // TODO: Failing the power:api_access returns the HTML for the front page, would rather 401 Route::group(['middleware' => ['auth:sanctum', 'power:api_access']], function () { Route::get('/user', 'InfoController@getUser'); Route::get('/character', 'InfoController@getCharacter'); From a666f47c297035914f6326d04f8c0453b2ec142a Mon Sep 17 00:00:00 2001 From: perappu Date: Sun, 9 Feb 2025 10:55:22 -0600 Subject: [PATCH 12/21] feat: users can generate their own token, non-csrf route for generating currently authenticated user's token --- app/Http/Controllers/Api/AuthController.php | 22 ++++------- app/Http/Controllers/Api/InfoController.php | 16 ++++++-- .../{Admin => Users}/ApiController.php | 21 +++++----- app/Services/UserService.php | 24 +++++++++++- config/lorekeeper/powers.php | 6 +-- resources/views/account/settings.blade.php | 33 ++++++++++++++++ resources/views/admin/api/api.blade.php | 39 ------------------- routes/api.php | 3 +- routes/lorekeeper/admin.php | 7 ---- routes/lorekeeper/members.php | 6 +++ 10 files changed, 95 insertions(+), 82 deletions(-) rename app/Http/Controllers/{Admin => Users}/ApiController.php (81%) delete mode 100644 resources/views/admin/api/api.blade.php diff --git a/app/Http/Controllers/Api/AuthController.php b/app/Http/Controllers/Api/AuthController.php index bde37c89a3..064ee3a6d0 100644 --- a/app/Http/Controllers/Api/AuthController.php +++ b/app/Http/Controllers/Api/AuthController.php @@ -21,21 +21,15 @@ public function postGenerateToken(Request $request) { $user = User::where('email', $request->email)->first(); - if ($user->hasPower('api_access')) { - if (!$user || !Hash::check($request->password, $user->password)) { - throw ValidationException::withMessages([ - 'email' => ['The provided credentials are incorrect.'], - ]); - } + if (!$user || !Hash::check($request->password, $user->password)) { + throw ValidationException::withMessages([ + 'email' => ['The provided credentials are incorrect.'], + ]); + } - // Delete any pre-existing tokens - $user->tokens()->delete(); + // Delete any pre-existing tokens + $user->tokens()->delete(); - return $user->createToken($request->token_name)->plainTextToken; - } else { - return response()->json([ - 'message' => 'You do not have API access permissions.', - ], 404); - } + return $user->createToken($request->token_name)->plainTextToken; } } diff --git a/app/Http/Controllers/Api/InfoController.php b/app/Http/Controllers/Api/InfoController.php index 2567ed09c3..f6126ac7d8 100644 --- a/app/Http/Controllers/Api/InfoController.php +++ b/app/Http/Controllers/Api/InfoController.php @@ -6,10 +6,18 @@ use App\Models\Character\Character; use Illuminate\Http\Request; -class InfoController extends Controller { - public function getCharacter(Request $request) { - $character = Character::findOrFail($request->id); +class InfoController extends Controller +{ + public function getCharacter(Request $request) + { + $character = Character::find($request->id); - return response()->json($character); + if ($character) { + return response()->json($character); + } else { + return response()->json([ + 'message' => 'No character found.', + ], 404); + } } } diff --git a/app/Http/Controllers/Admin/ApiController.php b/app/Http/Controllers/Users/ApiController.php similarity index 81% rename from app/Http/Controllers/Admin/ApiController.php rename to app/Http/Controllers/Users/ApiController.php index 8073c06e91..1eb93d3162 100644 --- a/app/Http/Controllers/Admin/ApiController.php +++ b/app/Http/Controllers/Users/ApiController.php @@ -1,6 +1,6 @@ back()->withInput(); } + + /** + * Generates (or re-generates) an API token without CSRF protection. + * This token is different from the one provided to the user via settings. + * + * @return mixed + */ + public function getGenerateToken(Request $request, UserService $service) { + return $service->generateTokenAPI(); + } } diff --git a/app/Services/UserService.php b/app/Services/UserService.php index 2ece2eb319..df116135c6 100644 --- a/app/Services/UserService.php +++ b/app/Services/UserService.php @@ -14,6 +14,7 @@ use App\Models\User\User; use App\Models\User\UserUpdateLog; use Carbon\Carbon; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Hash; @@ -701,7 +702,7 @@ public function reactivate($user, $staff = null) { } /** - * Generate an API token for the user. + * Generate a "public" API token for the user. * * @param User $user * @@ -709,6 +710,8 @@ public function reactivate($user, $staff = null) { */ public function generateToken($user) { try { + $user->tokens()->where('personal_access_tokens.name','token')->delete(); + $token = $user->createToken('token')->plainTextToken; UserUpdateLog::create(['staff_id' => $user->id, 'user_id' => $user->id, 'data' => json_encode([]), 'type' => 'Generated API Token']); @@ -746,4 +749,23 @@ public function revokeTokens($user, $staff = null) { return false; } + + /** + * Generate a "hidden" API token for the user and return it as plain text. + * + * + * @return mixed + */ + public function generateTokenAPI() { + try { + Auth::user()->tokens()->where('personal_access_tokens.name','hidden')->delete(); + $token = Auth::user()->createToken('hidden')->plainTextToken; + + UserUpdateLog::create(['staff_id' => Auth::user()->id, 'user_id' => Auth::user()->id, 'data' => json_encode([]), 'type' => 'Generated API Token w/o CSRF']); + + return $token; + } catch (\Exception $e) { + return response()->json($e); + } + } } diff --git a/config/lorekeeper/powers.php b/config/lorekeeper/powers.php index 8b38627e45..6c43a69f19 100644 --- a/config/lorekeeper/powers.php +++ b/config/lorekeeper/powers.php @@ -66,9 +66,5 @@ 'comment_on_sales' => [ 'name' => 'Comment on Sales', 'description' => 'Allow rank to comment on sales in preview mode.', - ], - 'api_access' => [ - 'name' => 'Has API Access', - 'description' => 'Can utilize the website\'s API.', - ], + ] ]; diff --git a/resources/views/account/settings.blade.php b/resources/views/account/settings.blade.php index ae2477da5a..bf817db335 100644 --- a/resources/views/account/settings.blade.php +++ b/resources/views/account/settings.blade.php @@ -220,6 +220,39 @@ {!! Form::close() !!} @endif + +
+

API Token

+ +

Here you may generate personal access (API) tokens for your account. It will have the same site permissions as you.

+
Be warned: giving away this token is like giving away your password!
+ + @if (Auth::user()->tokens->count()) +
+

Regenerate Token

+

You already have an API token generated. You may regenerate this token with the button below. It will create a new token, not show you your current one.

+ {!! Form::open(['url' => 'account/api/token']) !!} + {!! Form::submit('Regenerate Token', ['class' => 'btn btn-warning']) !!} + {!! Form::close() !!} +
+
+

Revoke Token

+

Click the below button to revoke (delete) your current API token. This can not be undone!

+ {!! Form::open(['url' => 'account/api/revoke']) !!} + {!! Form::submit('Revoke Token', ['class' => 'btn btn-danger']) !!} + {!! Form::close() !!} + +
+ @else +
+

Generate Token

+

Clicking the below button will generate a token for your account. This token will only display ONCE. Be sure to copy it down!

+ {!! Form::open(['url' => 'account/api/token']) !!} + {!! Form::submit('Generate Token', ['class' => 'btn btn-primary']) !!} + {!! Form::close() !!} +
+ @endif +
@endsection @section('scripts')