-
Notifications
You must be signed in to change notification settings - Fork 164
feat: Laravel Sanctum API implementation #1222
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
45b6e65
aaf84ac
12b36bf
db39fe8
91c7c30
bfd18b7
27943b0
69b2987
2c79c00
8b97456
c7d51c8
ae0d50c
767c869
c1f4685
a666f47
b9b4e47
84453a1
df7209c
cb9fa33
67d8760
5ef4d28
1391234
60f5557
ae234f9
7a3fa29
44073c4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; | ||
| } | ||
| } |
| 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); | ||
| } | ||
| } | ||
| } |
| 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(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,6 @@ class VerifyCsrfToken extends Middleware { | |
| * @var array | ||
| */ | ||
| protected $except = [ | ||
| // | ||
| 'account/api/token', | ||
| ]; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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', | ||
|
|
@@ -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.'); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -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(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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!!)