Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
45b6e65
feat: generate personal access tokens with laravel sanctum
perappu Nov 9, 2024
aaf84ac
refactor: fix PHP styling
perappu Nov 9, 2024
12b36bf
feat: proof-of-concept to get user and character info if user has api…
perappu Nov 9, 2024
db39fe8
Merge branch 'feature/api' of https://github.com/perappu/lorekeeper i…
perappu Nov 9, 2024
91c7c30
refactor: fix PHP styling
perappu Nov 9, 2024
bfd18b7
feat: revoke a user's current tokens from the Edit User interface
perappu Feb 2, 2025
27943b0
feat: UI interface for creating and revoking tokens
perappu Feb 2, 2025
69b2987
refactor: fix blade formatting
perappu Feb 2, 2025
2c79c00
refactor: fix PHP styling
perappu Feb 2, 2025
8b97456
Merge remote-tracking branch 'lorekeeper/develop' into feature/api
perappu Feb 2, 2025
c7d51c8
Merge branch 'feature/api' of https://github.com/perappu/lorekeeper i…
perappu Feb 2, 2025
ae0d50c
fix: only allow PAT generation via API if the user has the correct pe…
perappu Feb 2, 2025
767c869
refactor: fix PHP styling
perappu Feb 2, 2025
c1f4685
chore: remove irrelevant todo comment lmao
perappu Feb 2, 2025
a666f47
feat: users can generate their own token, non-csrf route for generati…
perappu Feb 9, 2025
b9b4e47
refactor: fix PHP styling
perappu Feb 9, 2025
84453a1
refactor: fix blade formatting
perappu Feb 9, 2025
df7209c
feat: add site setting to disable user generated tokens
perappu Feb 9, 2025
cb9fa33
Merge branch 'feature/api' of https://github.com/perappu/lorekeeper i…
perappu Feb 9, 2025
67d8760
refactor: fix blade formatting
perappu Feb 9, 2025
5ef4d28
refactor: fix PHP styling
perappu Feb 9, 2025
1391234
feat: add allow_token_generation_via_api site setting
perappu Feb 11, 2025
60f5557
Merge branch 'feature/api' of https://github.com/perappu/lorekeeper i…
perappu Feb 11, 2025
ae234f9
refactor: fix PHP styling
perappu Feb 11, 2025
7a3fa29
refactor: token generation in $except instead of withoutMiddleware
perappu Feb 13, 2025
44073c4
refactor: fix PHP styling
perappu Feb 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/Console/Commands/AddSiteSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ public function handle() {

$this->addSiteSetting('allow_users_to_delete_profile_comments', 0, '0: Users cannot delete profile comments, 1: Users can delete profile comments.');

$this->addSiteSetting('allow_users_to_generate_tokens', 0, '0: Users cannot generate their own API tokens, 1: Users can generate their own API tokens.');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is better than initial, but I think it would be best if the generation of tokens is entirely outside of non-staff's hands.

When I mean generation of a token on an endpoint, I meant an automatic dispense of the token to either browser session or some other carrying method!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my other comment -- I can definitely remove this code if that's the general consensus, but I'd like to try to standardize as many API use cases as I can (since that's the main purpose of this PR, standardizing an API implementation).

(and also I replied to these in backwards order but thank you for reviewing this!!)


$this->addSiteSetting('allow_token_generation_via_api', 0, '0: Tokens can not be generated via the API, 1: Users can directly generate tokens via username/password authentication with the API.');

$this->line("\nSite settings up to date!");
}

Expand Down
37 changes: 37 additions & 0 deletions app/Http/Controllers/Admin/Users/UserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -421,4 +421,41 @@ 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();
}
}
41 changes: 41 additions & 0 deletions app/Http/Controllers/Api/AuthController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\User\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;

class AuthController extends Controller {
/**
* Authenticate with email/password and receive a PAT through the API.
*/
public function postGenerateToken(Request $request) {
if (!Settings::get('allow_token_generation_via_api')) {
return response()->json([
'message' => 'Token generation via API is not allowed.',
], 403);
}

$request->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.'],
]);
}

// Delete any pre-existing tokens
$user->tokens()->delete();

return $user->createToken($request->token_name)->plainTextToken;
}
}
21 changes: 21 additions & 0 deletions app/Http/Controllers/Api/InfoController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Character\Character;
use Illuminate\Http\Request;

class InfoController extends Controller {
public function getCharacter(Request $request) {
$character = Character::find($request->id);

if ($character) {
return response()->json($character);
} else {
return response()->json([
'message' => 'No character found.',
], 404);
}
}
}
65 changes: 65 additions & 0 deletions app/Http/Controllers/Users/ApiController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace App\Http\Controllers\Users;

use App\Http\Controllers\Controller;
use App\Services\UserService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class ApiController extends Controller {
/*
|--------------------------------------------------------------------------
| Admin / Api Controller
|--------------------------------------------------------------------------
|
| Handles creation/editing of API access.
|
*/

/**
* Generates (or re-generates) an API token.
*
* @return \Illuminate\Contracts\Support\Renderable
*/
public function postGenerateToken(Request $request, UserService $service) {
if (!Settings::get('allow_users_to_generate_tokens')) {
abort(404);
}

if (!$service->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();
}

/**
* 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();
}
}
5 changes: 4 additions & 1 deletion app/Http/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ class Kernel extends HttpKernel {
],

'api' => [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:60,1',
'bindings',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];

Expand All @@ -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,
];

/**
Expand Down
2 changes: 1 addition & 1 deletion app/Http/Middleware/VerifyCsrfToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ class VerifyCsrfToken extends Middleware {
* @var array
*/
protected $except = [
//
'account/api/token',
];
}
3 changes: 2 additions & 1 deletion app/Models/User/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,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, HasApiTokens, Notifiable, TwoFactorAuthenticatable;

/**
* The attributes that are mass assignable.
Expand Down
95 changes: 83 additions & 12 deletions app/Services/UserService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -81,25 +82,28 @@ public function validator(array $data, $socialite = false) {
'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) {
$fail('You must be 13 or older to access this site.');
}
},
],
'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',
Expand Down Expand Up @@ -415,7 +419,7 @@ public function updateUsername($username, $user) {
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.');
' days before changing your username again.');
}
}

Expand Down Expand Up @@ -696,4 +700,71 @@ public function reactivate($user, $staff = null) {

return $this->rollbackReturn(false);
}

/**
* Generate a "public" API token for the user.
*
* @param User $user
*
* @return bool
*/
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']);

flash('Token created successfully:')->success();
flash($token)->success();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah ideally we do not let the users do this at all. most members will have no idea what to do with this and ideally shouldn't

Copy link
Author

@perappu perappu Feb 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I've received conflicting feedback regarding whether or not users should have the feature. I can see circumstances where we might want it for users -- such as giving a discord bot their API key, like I've seen with bots that connect to MMO APIs.

My happy medium would be a feature that can be disabled with a site setting, but if it's strongly desired, I can revert back to the api_access solution.

Maybe I can have it be disabled in the config rather than as a site setting so it has to be enabled more "deliberately"?

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.
*
* @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;
}

/**
* 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);
}
}
}
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"laravel/fortify": "^1.25",
"laravel/framework": "^10.0",
"laravel/helpers": "^1.4",
"laravel/sanctum": "^3.3",
"laravel/socialite": "^5.2",
"laravel/tinker": "^2.0",
"laravelcollective/html": "^6.0",
Expand Down
Loading