Skip to content

Commit

Permalink
Improve authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
jlbelanger committed Oct 9, 2023
1 parent 2cbb6ab commit e279eca
Show file tree
Hide file tree
Showing 25 changed files with 507 additions and 110 deletions.
5 changes: 4 additions & 1 deletion app/Console/Commands/ResetAuth.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Hash;

class ResetAuth extends Command
{
Expand Down Expand Up @@ -39,8 +41,9 @@ public function handle()
$user = new User();
$user->username = 'test';
$user->email = 'test@example.com';
$user->email_verified_at = Carbon::now();
}
$user->password = bcrypt('test');
$user->password = Hash::make('test');
$user->save();
echo "Success!\n";
}
Expand Down
28 changes: 20 additions & 8 deletions app/Exceptions/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\Exceptions\ThrottleRequestsException;
use Illuminate\Routing\Exceptions\InvalidSignatureException;
use Jlbelanger\Tapioca\Exceptions\JsonApiException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;

class Handler extends ExceptionHandler
Expand All @@ -16,9 +18,7 @@ class Handler extends ExceptionHandler
*
* @var array<int, class-string<Throwable>>
*/
protected $dontReport = [
JsonApiException::class,
];
protected $dontReport = [JsonApiException::class];

/**
* A list of the inputs that are never flashed for validation exceptions.
Expand All @@ -40,21 +40,33 @@ class Handler extends ExceptionHandler
*/
public function register()
{
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
$this->renderable(function (InvalidSignatureException $e) {
if (!url()->signatureHasNotExpired(request())) {
return response()->json(['errors' => [['title' => __('passwords.expired'), 'status' => '403']]], 403);
}
return response()->json(['errors' => [['title' => __('passwords.token'), 'status' => '403']]], 403);
});

// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
$this->renderable(function (MethodNotAllowedHttpException $e) {
return response()->json(['errors' => [['title' => 'URL does not exist.', 'status' => '404', 'detail' => 'Method not allowed.']]], 404);
});

$this->renderable(function (JsonApiException $e) {
return response()->json(['errors' => $e->getErrors()], $e->getCode());
$this->renderable(function (NotFoundHttpException $e) {
return response()->json(['errors' => [['title' => $e->getMessage() ? $e->getMessage() : 'URL does not exist.', 'status' => '404']]], 404);
});

$this->renderable(function (ThrottleRequestsException $e) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
return response()->json(['errors' => [['title' => 'Please wait before retrying.', 'status' => '429']]], 429);
});

$this->renderable(function (HttpException $e) {
return response()->json(['errors' => [['title' => $e->getMessage(), 'status' => $e->getStatusCode()]]], $e->getStatusCode());
return response()->json(['errors' => [['title' => $e->getMessage(), 'status' => (string) $e->getStatusCode()]]], $e->getStatusCode());
});

$this->renderable(function (ThrottleRequestsException $e) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
return response()->json(['errors' => [['title' => 'Please wait before retrying.', 'status' => '429']]], 429);
$this->renderable(function (JsonApiException $e) {
return response()->json(['errors' => $e->getErrors()], $e->getCode());
});

$this->renderable(function (Throwable $e) {
Expand Down
72 changes: 58 additions & 14 deletions app/Http/Controllers/AuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@
use App\Http\Controllers\Controller;
use App\Models\User;
use DB;
use Exception;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Events\Verified;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
Expand Down Expand Up @@ -48,10 +53,12 @@ public function login(Request $request) : JsonResponse
}

$user = User::where('username', '=', $credentials['username'])->first();
$token = $user->createToken('api');
if ($user instanceof MustVerifyEmail && !$user->email_verified_at) {
return response()->json(['errors' => [['title' => __('auth.unverified'), 'status' => '401', 'code' => 'auth.unverified']]], 401);
}

return response()->json([
'token' => $token->plainTextToken,
'token' => $user->createToken('api')->plainTextToken,
'user' => $user->getAuthInfo($remember),
]);
}
Expand Down Expand Up @@ -79,7 +86,7 @@ public function register(Request $request) : JsonResponse
{
$data = $request->input('data');
$rules = [
'attributes.username' => ['required', 'max:255', 'unique:users,username'],
'attributes.username' => ['required', 'alpha_num', 'max:255', 'unique:users,username'],
'attributes.email' => ['required', 'email', 'max:255', 'unique:users,email'],
'attributes.password' => ['required', 'confirmed', Rules\Password::defaults()],
'attributes.password_confirmation' => ['required'],
Expand All @@ -96,11 +103,15 @@ public function register(Request $request) : JsonResponse
'password' => Hash::make($data['attributes']['password']),
])->save();
$user->save();
$token = $user->createToken('api');
event(new Registered($user));
DB::commit();

if ($user instanceof MustVerifyEmail) {
return response()->json(null, 204);
}

return response()->json([
'token' => $token->plainTextToken,
'token' => $user->createToken('api')->plainTextToken,
'user' => $user->getAuthInfo(false),
]);
}
Expand All @@ -124,7 +135,7 @@ public function forgotPassword(Request $request) : JsonResponse
try {
Password::sendResetLink(['email' => $data['attributes']['email']]);
} catch (Exception $e) {
return response()->json(['errors' => [['title' => 'We were unable to send a password reset email. Please try again later.', 'status' => '500']]], 500);
return response()->json(['errors' => [['title' => __('passwords.send_error'), 'status' => '500']]], 500);
}

return response()->json(null, 204);
Expand All @@ -141,15 +152,15 @@ public function resetPassword(Request $request, string $token) : JsonResponse
$rules = [
'attributes.email' => ['required', 'email'],
'attributes.new_password' => ['required', 'confirmed', Rules\Password::defaults()],
'attributes.new_password_confirmation' => ['required'],
];
$validator = Validator::make($data, $rules, [], Utilities::prettyAttributeNames($rules));
if ($validator->fails()) {
$errors = ValidationException::formatErrors($validator->errors()->toArray());
return response()->json(['errors' => $errors], 422);
}
if (!empty($errors)) {
return response()->json(['errors' => $errors], 422);

if ($request->query('expires') < Carbon::now()->timestamp) {
return response()->json(['errors' => [['title' => __('passwords.expired'), 'status' => '403']]], 403);
}

$status = Password::reset(
Expand All @@ -159,21 +170,54 @@ public function resetPassword(Request $request, string $token) : JsonResponse
'password_confirmation' => $data['attributes']['new_password_confirmation'],
'token' => $token,
],
function ($user, $password) use ($request) {
$user->forceFill([
function ($user, $password) {
$userData = [
'password' => Hash::make($password),
])->save();

$user->setRememberToken(Str::random(60));
'remember_token' => Str::random(60),
];
if ($user instanceof MustVerifyEmail && !$user->email_verified_at) {
$userData['email_verified_at'] = Carbon::now();
}
$user->forceFill($userData)->save();

event(new PasswordReset($user));
}
);

if ($status !== Password::PASSWORD_RESET) {
if ($status === 'passwords.user') {
$status = 'passwords.token';
}
return response()->json(['errors' => [['title' => __($status), 'status' => '422']]], 422);
}

return response()->json(null, 204);
}

/**
* @param Request $request
* @return Response
*/
public function verifyEmail(Request $request) : JsonResponse
{
$user = User::find($request->query('id'));
if (!$user->hasVerifiedEmail()) {
$user->markEmailAsVerified();
event(new Verified($user));
}
return response()->json(null, 204);
}

/**
* @param Request $request
* @return Response
*/
public function resendVerification(Request $request) : JsonResponse
{
$user = User::where('username', '=', $request->input('username'))->first();
if ($user) {
$user->sendEmailVerificationNotification();
}
return response()->json(null, 204);
}
}
4 changes: 1 addition & 3 deletions app/Http/Controllers/FoodController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Jlbelanger\Tapioca\Controllers\AuthorizedResourceController;
use Jlbelanger\Tapioca\Exceptions\NotFoundException;

class FoodController extends AuthorizedResourceController
{
Expand All @@ -23,7 +21,7 @@ public function favourite(Request $request, string $id) : JsonResponse // phpcs:
$food = Food::find($id);
$user = Auth::guard('sanctum')->user();
if (!$food || !$user) {
throw NotFoundException::generate();
abort(404);
}

$favourite = DB::table('food_user')
Expand Down
3 changes: 1 addition & 2 deletions app/Http/Controllers/MealController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Jlbelanger\Tapioca\Controllers\AuthorizedResourceController;
use Jlbelanger\Tapioca\Exceptions\NotFoundException;

class MealController extends AuthorizedResourceController
{
Expand All @@ -24,7 +23,7 @@ public function add(Request $request, string $id) : JsonResponse // phpcs:ignore
$user = Auth::guard('sanctum')->user();
$date = $request->input('date');
if (!$meal || !$user || !$date) {
throw NotFoundException::generate();
abort(404);
}

$foods = $meal->foods()->get();
Expand Down
12 changes: 7 additions & 5 deletions app/Http/Controllers/UserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use App\Models\Weight;
use DB;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
Expand All @@ -17,7 +18,6 @@
use Illuminate\Validation\Rules;
use Jlbelanger\Tapioca\Controllers\AuthorizedResourceController;
use Jlbelanger\Tapioca\Exceptions\JsonApiException;
use Jlbelanger\Tapioca\Exceptions\NotFoundException;
use Jlbelanger\Tapioca\Exceptions\ValidationException;
use Jlbelanger\Tapioca\Helpers\Utilities;
use Validator;
Expand All @@ -36,7 +36,7 @@ public function changeEmail(Request $request, string $id) : JsonResponse
throw JsonApiException::generate([['title' => 'You do not have permission to update this record.', 'status' => '403']], 403);
}
if (!$user || !Auth::guard('sanctum')->user()->can('update', $user)) {
throw NotFoundException::generate();
abort(404);
}

$data = $request->input('data');
Expand Down Expand Up @@ -64,6 +64,9 @@ public function changeEmail(Request $request, string $id) : JsonResponse
}

$user->email = $data['attributes']['email'];
if ($user instanceof MustVerifyEmail && !$user->email_verified_at) {
$user->email_verified_at = null;
}
$user->save();

return response()->json(null, 204);
Expand All @@ -81,14 +84,13 @@ public function changePassword(Request $request, string $id) : JsonResponse
throw JsonApiException::generate([['title' => 'You do not have permission to update this record.', 'status' => '403']], 403);
}
if (!$user || !Auth::guard('sanctum')->user()->can('update', $user)) {
throw NotFoundException::generate();
abort(404);
}

$data = $request->input('data');
$rules = [
'attributes.password' => ['required'],
'attributes.new_password' => ['required', 'confirmed', Rules\Password::defaults()],
'attributes.new_password_confirmation' => ['required'],
];
$validator = Validator::make($data, $rules, [], Utilities::prettyAttributeNames($rules));
if ($validator->fails()) {
Expand All @@ -111,8 +113,8 @@ public function changePassword(Request $request, string $id) : JsonResponse

$user->forceFill([
'password' => Hash::make($data['attributes']['new_password']),
'remember_token' => Str::random(60),
])->save();
$user->setRememberToken(Str::random(60));
event(new PasswordReset($user));

return response()->json(null, 204);
Expand Down
3 changes: 1 addition & 2 deletions app/Models/Food.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
use App\Models\FoodMeal;
use App\Models\Meal;
use App\Models\User;
use App\Rules\CannotChange;
use DB;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
Expand Down Expand Up @@ -291,7 +290,7 @@ protected function rules(array $data, string $method) : array // phpcs:ignore Ge
$rules['attributes.slug'][] = $unique;

if (!Auth::guard('sanctum')->user()->is_admin) {
$rules['relationships.user'] = [new CannotChange()];
$rules['relationships.user'] = ['prohibited'];
}

return $rules;
Expand Down
Loading

0 comments on commit e279eca

Please sign in to comment.