Skip to content

Commit

Permalink
User auth using form (#10)
Browse files Browse the repository at this point in the history
* - add password to users table

* - ask for password when creating user through cli

* - ask for password when creating user

* - allow user login using password

* - fix google auth

* - fix showing correct login form
  • Loading branch information
xterm-inator authored Nov 11, 2023
1 parent 3c6350d commit 7d52dba
Show file tree
Hide file tree
Showing 36 changed files with 582 additions and 135 deletions.
132 changes: 132 additions & 0 deletions api/app/Auth/ThrottlesLogins.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php

namespace App\Auth;

use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Cache\RateLimiter;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Support\Facades\Lang;
use Illuminate\Validation\ValidationException;

/**
* @author Taylor Otwell
* @link https://github.com/laravel/ui/blob/2.x/auth-backend/ThrottlesLogins.php
*/
trait ThrottlesLogins
{
/**
* Determine if the user has too many failed login attempts.
*
* @param Request $request
* @return bool
*/
protected function hasTooManyLoginAttempts(Request $request): bool
{
return $this->limiter()->tooManyAttempts(
$this->throttleKey($request),
$this->maxAttempts()
);
}

/**
* Increment the login attempts for the user.
*
* @param Request $request
* @return void
*/
protected function incrementLoginAttempts(Request $request): void
{
$this->limiter()->hit(
$this->throttleKey($request),
$this->decayMinutes() * 60
);
}

/**
* Redirect the user after determining they are locked out.
*
* @param Request $request
* @return void
*
* @throws ValidationException
*/
protected function sendLockoutResponse(Request $request): void
{
$seconds = $this->limiter()->availableIn(
$this->throttleKey($request)
);

throw ValidationException::withMessages([
'email' => [Lang::get('auth.throttle', [
'attempts' => $this->maxAttempts(),
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
])],
])->status(Response::HTTP_TOO_MANY_REQUESTS);
}

/**
* Clear the login locks for the given user credentials.
*
* @param Request $request
* @return void
*/
protected function clearLoginAttempts(Request $request): void
{
$this->limiter()->clear($this->throttleKey($request));
}

/**
* Fire an event when a lockout occurs.
*
* @param Request $request
* @return void
*/
protected function fireLockoutEvent(Request $request): void
{
event(new Lockout($request));
}

/**
* Get the throttle key for the given request.
*
* @param Request $request
* @return string
*/
protected function throttleKey(Request $request): string
{
return Str::lower($request->input('email')).'|'.$request->ip();
}

/**
* Get the rate limiter instance.
*
* @return RateLimiter
*/
protected function limiter(): RateLimiter
{
return app(RateLimiter::class);
}

/**
* Get the maximum number of attempts to allow.
*
* @return int
*/
public function maxAttempts(): int
{
return property_exists($this, 'maxAttempts') ? $this->maxAttempts : 5;
}

/**
* Get the number of minutes to throttle for.
*
* @return int
*/
public function decayMinutes(): int
{
return property_exists($this, 'decayMinutes') ? $this->decayMinutes : 1;
}
}
17 changes: 14 additions & 3 deletions api/app/Console/Commands/CreateUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
namespace App\Console\Commands;

use App\Models\User;
use App\Support\Enums\Auth;
use App\Support\Enums\Role;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rules\Enum;
use Illuminate\Validation\Rules\RequiredIf;
use Illuminate\Validation\Rules\Unique;
use Illuminate\Validation\ValidationException;
use function Laravel\Prompts\password;

class CreateUser extends Command
{
Expand All @@ -33,9 +37,16 @@ class CreateUser extends Command
*/
public function handle(): int
{
$validator = Validator::make($this->arguments(), [
'email' => ['email'],
'role' => [new Enum(Role::class)]
$password = null;

if (auth_type() == Auth::Form) {
$password = password('User password', required: true);
}

$validator = Validator::make([...$this->arguments(), 'password' => $password], [
'email' => ['email', new Unique('users', 'email')],
'role' => [new Enum(Role::class)],
'password' => ['nullable', new RequiredIf(auth_type() == Auth::Form), 'min:8'],
]);

if ($validator->errors()->count()) {
Expand Down
60 changes: 60 additions & 0 deletions api/app/Http/Controllers/Auth/LoginController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace App\Http\Controllers\Auth;

use Illuminate\Http\Exceptions\HttpResponseException;
use App\Auth\ThrottlesLogins;
use App\Http\Requests\Auth\Login;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Lang;
use App\Http\Resources\User as UserResource;
use Illuminate\Contracts\Auth\Factory as Auth;
use Illuminate\Validation\ValidationException;

class LoginController extends Controller
{
use ThrottlesLogins;

/**
* Max number of login attempts allowed.
*
* @var integer
*/
protected int $maxAttempts = 5;

/**
* Number of minutes login attempts are throttled for.
*
* @var integer
*/
protected int $decayMinutes = 5;

/**
* Handle an authentication attempt.
*
* @param Login $request
* @param Auth $auth
* @return UserResource
*@throws HttpResponseException
*/
public function __invoke(Login $request, Auth $auth)
{
if ($this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);

$this->sendLockoutResponse($request);
}

if ($auth->attempt($request->only('email', 'password'))) {
$request->session()->regenerate();

$this->clearLoginAttempts($request);

return new UserResource($auth->user());
}

$this->incrementLoginAttempts($request);

throw ValidationException::withMessages(['email' => [Lang::get('auth.failed')]])->status(422);
}
}
15 changes: 15 additions & 0 deletions api/app/Http/Controllers/SystemConfigController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace App\Http\Controllers;

use Illuminate\Http\JsonResponse;

class SystemConfigController extends Controller
{
public function __invoke()
{
return new JsonResponse([
'auth_type' => auth_type()
]);
}
}
31 changes: 31 additions & 0 deletions api/app/Http/Requests/Auth/Login.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace App\Http\Requests\Auth;

use Illuminate\Foundation\Http\FormRequest;

class Login extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return true;
}

/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
'email' => ['required'],
'password' => ['required'],
];
}
}
7 changes: 7 additions & 0 deletions api/app/Http/Requests/StoreUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
namespace App\Http\Requests;

use App\Models\User;
use App\Support\Enums\Auth;
use App\Support\Enums\Role;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Enum;
use Illuminate\Validation\Rules\RequiredIf;

class StoreUser extends FormRequest
{
Expand Down Expand Up @@ -38,6 +40,11 @@ public function rules()
'bail',
'required',
new Enum(Role::class),
],
'password' => [
'nullable',
new RequiredIf(auth_type() == Auth::Form),
'confirmed'
]
];
}
Expand Down
6 changes: 6 additions & 0 deletions api/app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use App\Support\Enums\Role;
use App\Support\Traits\Uuids;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
Expand Down Expand Up @@ -43,6 +44,11 @@ class User extends Authenticatable
'role' => Role::class,
];

public function password(): Attribute
{
return new Attribute(set: fn ($value) => $value ? bcrypt($value) : null);
}

public function oauthProviders(): HasMany
{
return $this->hasMany(OAuthProvider::class);
Expand Down
17 changes: 17 additions & 0 deletions api/app/Support/Enums/Auth.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace App\Support\Enums;

enum Auth: string
{
case Form = 'form';
case Google = 'google';

public function title(): string
{
return match ($this) {
self::Form => 'Form',
self::Google => 'Google'
};
}
}
10 changes: 10 additions & 0 deletions api/app/Support/helpers.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

use Carbon\Carbon;
use App\Support\Enums\Auth;

function generate_last_handshake_date(?string $string): ?Carbon
{
Expand Down Expand Up @@ -29,3 +30,12 @@ function generate_last_handshake_date(?string $string): ?Carbon

return $currentDate;
}

function auth_type(): Auth
{
if (config('services.google.client_id')) {
return Auth::Google;
}

return Auth::Form;
}
1 change: 1 addition & 0 deletions api/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"guzzlehttp/guzzle": "^7.2",
"laravel/framework": "^10",
"laravel/octane": "^1.3",
"laravel/prompts": "^0.1.13",
"laravel/sanctum": "^3.2",
"laravel/socialite": "^5.5",
"laravel/tinker": "^2.7",
Expand Down
2 changes: 1 addition & 1 deletion api/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 7d52dba

Please sign in to comment.