mirror of https://github.com/Bubka/2FAuth.git
Compare commits
10 Commits
a3060a9ada
...
c6f8317d81
Author | SHA1 | Date |
---|---|---|
Bubka | c6f8317d81 | |
Bubka | bc89760e74 | |
Bubka | 6b96fced3d | |
Bubka | 7a033409a5 | |
Bubka | 8734e9c807 | |
Bubka | 99bf9d7d80 | |
Bubka | 4987e060c4 | |
Bubka | 76c3b6fe0c | |
Bubka | e498350f62 | |
Bubka | e75589526b |
14
.env.example
14
.env.example
|
@ -9,6 +9,14 @@ APP_NAME=2FAuth
|
|||
APP_ENV=local
|
||||
|
||||
|
||||
# The timezone for your application, which is used to record dates and times to database. This global setting can be
|
||||
# overridden by users via in-app settings for a personalised dates and times display.
|
||||
|
||||
# If this setting is changed while the application is already running, existing records in the database won't be updated.
|
||||
|
||||
APP_TIMEZONE=UTC
|
||||
|
||||
|
||||
# Set to true if you want to see debug information in error screens.
|
||||
|
||||
APP_DEBUG=false
|
||||
|
@ -179,6 +187,12 @@ LOGIN_THROTTLE=5
|
|||
AUTHENTICATION_GUARD=web-guard
|
||||
|
||||
|
||||
# Authentication log retention time, in days.
|
||||
# Log entries older than that are automatically deleted.
|
||||
|
||||
AUTHENTICATION_LOG_RETENTION=365
|
||||
|
||||
|
||||
# Name of the HTTP headers sent by the reverse proxy that identifies the authenticated user at proxy level.
|
||||
# Check your proxy documentation to find out how these headers are named (i.e 'REMOTE_USER', 'REMOTE_EMAIL', etc...)
|
||||
# (only relevant when AUTHENTICATION_GUARD is set to 'reverse-proxy-guard')
|
||||
|
|
|
@ -12,4 +12,5 @@ BCRYPT_ROUNDS=4
|
|||
BROADCAST_DRIVER=log
|
||||
CACHE_DRIVER=array
|
||||
SESSION_DRIVER=array
|
||||
QUEUE_DRIVER=sync
|
||||
QUEUE_DRIVER=sync
|
||||
MAIL_MAILER=array
|
|
@ -114,6 +114,10 @@ ENV \
|
|||
# You can leave this on "local". If you change it to production most console commands will ask for extra confirmation.
|
||||
# Never set it to "testing".
|
||||
APP_ENV=local \
|
||||
# The timezone for your application, which is used to record dates and times to database. This global setting can be
|
||||
# overridden by users via in-app settings for a personalised dates and times display.
|
||||
# If this setting is changed while the application is already running, existing records in the database won't be updated.
|
||||
APP_TIMEZONE=UTC \
|
||||
# Set to true if you want to see debug information in error screens.
|
||||
APP_DEBUG=false \
|
||||
# This should be your email address
|
||||
|
@ -186,6 +190,9 @@ ENV \
|
|||
# authentication checks. That means your proxy is fully responsible of the authentication process, 2FAuth will
|
||||
# trust him as long as headers are presents.
|
||||
AUTHENTICATION_GUARD=web-guard \
|
||||
# Authentication log retention time, in days.
|
||||
# Log entries older than that are automatically deleted.
|
||||
AUTHENTICATION_LOG_RETENTION=365 \
|
||||
# Name of the HTTP headers sent by the reverse proxy that identifies the authenticated user at proxy level.
|
||||
# Check your proxy documentation to find out how these headers are named (i.e 'REMOTE_USER', 'REMOTE_EMAIL', etc...)
|
||||
# (only relevant when AUTHENTICATION_GUARD is set to 'reverse-proxy-guard')
|
||||
|
|
442
_ide_helper.php
442
_ide_helper.php
|
@ -5,7 +5,7 @@
|
|||
|
||||
/**
|
||||
* A helper file for Laravel, to provide autocomplete information to your IDE
|
||||
* Generated for Laravel 10.48.4.
|
||||
* Generated for Laravel 10.48.8.
|
||||
*
|
||||
* This file should not be included in your code, only analyzed by your IDE!
|
||||
*
|
||||
|
@ -1640,36 +1640,6 @@ namespace Illuminate\Support\Facades {
|
|||
/**
|
||||
*
|
||||
*
|
||||
* @method static bool attempt(array $credentials = [], bool $remember = false)
|
||||
* @method static bool once(array $credentials = [])
|
||||
* @method static void login(\Illuminate\Contracts\Auth\Authenticatable $user, bool $remember = false)
|
||||
* @method static \Illuminate\Contracts\Auth\Authenticatable|bool loginUsingId(mixed $id, bool $remember = false)
|
||||
* @method static \Illuminate\Contracts\Auth\Authenticatable|bool onceUsingId(mixed $id)
|
||||
* @method static bool viaRemember()
|
||||
* @method static void logout()
|
||||
* @method static \Symfony\Component\HttpFoundation\Response|null basic(string $field = 'email', array $extraConditions = [])
|
||||
* @method static \Symfony\Component\HttpFoundation\Response|null onceBasic(string $field = 'email', array $extraConditions = [])
|
||||
* @method static bool attemptWhen(array $credentials = [], array|callable|null $callbacks = null, bool $remember = false)
|
||||
* @method static void logoutCurrentDevice()
|
||||
* @method static \Illuminate\Contracts\Auth\Authenticatable|null logoutOtherDevices(string $password, string $attribute = 'password')
|
||||
* @method static void attempting(mixed $callback)
|
||||
* @method static \Illuminate\Contracts\Auth\Authenticatable getLastAttempted()
|
||||
* @method static string getName()
|
||||
* @method static string getRecallerName()
|
||||
* @method static \Illuminate\Auth\SessionGuard setRememberDuration(int $minutes)
|
||||
* @method static \Illuminate\Contracts\Cookie\QueueingFactory getCookieJar()
|
||||
* @method static void setCookieJar(\Illuminate\Contracts\Cookie\QueueingFactory $cookie)
|
||||
* @method static \Illuminate\Contracts\Events\Dispatcher getDispatcher()
|
||||
* @method static void setDispatcher(\Illuminate\Contracts\Events\Dispatcher $events)
|
||||
* @method static \Illuminate\Contracts\Session\Session getSession()
|
||||
* @method static \Illuminate\Contracts\Auth\Authenticatable|null getUser()
|
||||
* @method static \Symfony\Component\HttpFoundation\Request getRequest()
|
||||
* @method static \Illuminate\Auth\SessionGuard setRequest(\Symfony\Component\HttpFoundation\Request $request)
|
||||
* @method static \Illuminate\Support\Timebox getTimebox()
|
||||
* @method static void macro(string $name, object|callable $macro)
|
||||
* @method static void mixin(object $mixin, bool $replace = true)
|
||||
* @method static bool hasMacro(string $name)
|
||||
* @method static void flushMacros()
|
||||
* @see \Illuminate\Auth\AuthManager
|
||||
* @see \Illuminate\Auth\SessionGuard
|
||||
*/ class Auth {
|
||||
|
@ -1851,24 +1821,336 @@ namespace Illuminate\Support\Facades {
|
|||
return $instance->getDefaultUserProvider();
|
||||
}
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* Get the currently authenticated user.
|
||||
*
|
||||
* @return \App\Models\User|null
|
||||
* @static
|
||||
*/ public static function user()
|
||||
{
|
||||
/** @var \App\Services\Auth\ReverseProxyGuard $instance */
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->user();
|
||||
}
|
||||
/**
|
||||
* Get the ID for the currently authenticated user.
|
||||
*
|
||||
* @return int|string|null
|
||||
* @static
|
||||
*/ public static function id()
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->id();
|
||||
}
|
||||
/**
|
||||
* Log a user into the application without sessions or cookies.
|
||||
*
|
||||
* @param array $credentials
|
||||
* @return bool
|
||||
* @static
|
||||
*/ public static function once($credentials = [])
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->once($credentials);
|
||||
}
|
||||
/**
|
||||
* Log the given user ID into the application without sessions or cookies.
|
||||
*
|
||||
* @param mixed $id
|
||||
* @return \App\Models\User|false
|
||||
* @static
|
||||
*/ public static function onceUsingId($id)
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->onceUsingId($id);
|
||||
}
|
||||
/**
|
||||
* Validate a user's credentials.
|
||||
*
|
||||
* @param array $credentials
|
||||
* @return bool
|
||||
* @codeCoverageIgnore
|
||||
* @static
|
||||
*/ public static function validate($credentials = [])
|
||||
{
|
||||
/** @var \App\Services\Auth\ReverseProxyGuard $instance */
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->validate($credentials);
|
||||
}
|
||||
/**
|
||||
* Attempt to authenticate using HTTP Basic Auth.
|
||||
*
|
||||
* @param string $field
|
||||
* @param array $extraConditions
|
||||
* @return \Symfony\Component\HttpFoundation\Response|null
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException
|
||||
* @static
|
||||
*/ public static function basic($field = 'email', $extraConditions = [])
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->basic($field, $extraConditions);
|
||||
}
|
||||
/**
|
||||
* Perform a stateless HTTP Basic login attempt.
|
||||
*
|
||||
* @param string $field
|
||||
* @param array $extraConditions
|
||||
* @return \Symfony\Component\HttpFoundation\Response|null
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException
|
||||
* @static
|
||||
*/ public static function onceBasic($field = 'email', $extraConditions = [])
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->onceBasic($field, $extraConditions);
|
||||
}
|
||||
/**
|
||||
* Attempt to authenticate a user using the given credentials.
|
||||
*
|
||||
* @param array $credentials
|
||||
* @param bool $remember
|
||||
* @return bool
|
||||
* @static
|
||||
*/ public static function attempt($credentials = [], $remember = false)
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->attempt($credentials, $remember);
|
||||
}
|
||||
/**
|
||||
* Attempt to authenticate a user with credentials and additional callbacks.
|
||||
*
|
||||
* @param array $credentials
|
||||
* @param array|callable|null $callbacks
|
||||
* @param bool $remember
|
||||
* @return bool
|
||||
* @static
|
||||
*/ public static function attemptWhen($credentials = [], $callbacks = null, $remember = false)
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->attemptWhen($credentials, $callbacks, $remember);
|
||||
}
|
||||
/**
|
||||
* Log the given user ID into the application.
|
||||
*
|
||||
* @param mixed $id
|
||||
* @param bool $remember
|
||||
* @return \App\Models\User|false
|
||||
* @static
|
||||
*/ public static function loginUsingId($id, $remember = false)
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->loginUsingId($id, $remember);
|
||||
}
|
||||
/**
|
||||
* Log a user into the application.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||
* @param bool $remember
|
||||
* @return void
|
||||
* @static
|
||||
*/ public static function login($user, $remember = false)
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
$instance->login($user, $remember);
|
||||
}
|
||||
/**
|
||||
* Log the user out of the application.
|
||||
*
|
||||
* @return void
|
||||
* @static
|
||||
*/ public static function logout()
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
$instance->logout();
|
||||
}
|
||||
/**
|
||||
* Log the user out of the application on their current device only.
|
||||
*
|
||||
* This method does not cycle the "remember" token.
|
||||
*
|
||||
* @return void
|
||||
* @static
|
||||
*/ public static function logoutCurrentDevice()
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
$instance->logoutCurrentDevice();
|
||||
}
|
||||
/**
|
||||
* Invalidate other sessions for the current user.
|
||||
*
|
||||
* The application must be using the AuthenticateSession middleware.
|
||||
*
|
||||
* @param string $password
|
||||
* @param string $attribute
|
||||
* @return \App\Models\User|null
|
||||
* @throws \Illuminate\Auth\AuthenticationException
|
||||
* @static
|
||||
*/ public static function logoutOtherDevices($password, $attribute = 'password')
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->logoutOtherDevices($password, $attribute);
|
||||
}
|
||||
/**
|
||||
* Register an authentication attempt event listener.
|
||||
*
|
||||
* @param mixed $callback
|
||||
* @return void
|
||||
* @static
|
||||
*/ public static function attempting($callback)
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
$instance->attempting($callback);
|
||||
}
|
||||
/**
|
||||
* Get the last user we attempted to authenticate.
|
||||
*
|
||||
* @return \App\Models\User
|
||||
* @static
|
||||
*/ public static function getLastAttempted()
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->getLastAttempted();
|
||||
}
|
||||
/**
|
||||
* Get a unique identifier for the auth session value.
|
||||
*
|
||||
* @return string
|
||||
* @static
|
||||
*/ public static function getName()
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->getName();
|
||||
}
|
||||
/**
|
||||
* Get the name of the cookie used to store the "recaller".
|
||||
*
|
||||
* @return string
|
||||
* @static
|
||||
*/ public static function getRecallerName()
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->getRecallerName();
|
||||
}
|
||||
/**
|
||||
* Determine if the user was authenticated via "remember me" cookie.
|
||||
*
|
||||
* @return bool
|
||||
* @static
|
||||
*/ public static function viaRemember()
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->viaRemember();
|
||||
}
|
||||
/**
|
||||
* Set the number of minutes the remember me cookie should be valid for.
|
||||
*
|
||||
* @param int $minutes
|
||||
* @return \Illuminate\Auth\SessionGuard
|
||||
* @static
|
||||
*/ public static function setRememberDuration($minutes)
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->setRememberDuration($minutes);
|
||||
}
|
||||
/**
|
||||
* Get the cookie creator instance used by the guard.
|
||||
*
|
||||
* @return \Illuminate\Contracts\Cookie\QueueingFactory
|
||||
* @throws \RuntimeException
|
||||
* @static
|
||||
*/ public static function getCookieJar()
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->getCookieJar();
|
||||
}
|
||||
/**
|
||||
* Set the cookie creator instance used by the guard.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Cookie\QueueingFactory $cookie
|
||||
* @return void
|
||||
* @static
|
||||
*/ public static function setCookieJar($cookie)
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
$instance->setCookieJar($cookie);
|
||||
}
|
||||
/**
|
||||
* Get the event dispatcher instance.
|
||||
*
|
||||
* @return \Illuminate\Contracts\Events\Dispatcher
|
||||
* @static
|
||||
*/ public static function getDispatcher()
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->getDispatcher();
|
||||
}
|
||||
/**
|
||||
* Set the event dispatcher instance.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Events\Dispatcher $events
|
||||
* @return void
|
||||
* @static
|
||||
*/ public static function setDispatcher($events)
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
$instance->setDispatcher($events);
|
||||
}
|
||||
/**
|
||||
* Get the session store used by the guard.
|
||||
*
|
||||
* @return \Illuminate\Contracts\Session\Session
|
||||
* @static
|
||||
*/ public static function getSession()
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->getSession();
|
||||
}
|
||||
/**
|
||||
* Return the currently cached user.
|
||||
*
|
||||
* @return \App\Models\User|null
|
||||
* @static
|
||||
*/ public static function getUser()
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->getUser();
|
||||
}
|
||||
/**
|
||||
* Set the current user.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||
* @return \Illuminate\Auth\SessionGuard
|
||||
* @static
|
||||
*/ public static function setUser($user)
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->setUser($user);
|
||||
}
|
||||
/**
|
||||
* Get the current request instance.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Request
|
||||
* @static
|
||||
*/ public static function getRequest()
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->getRequest();
|
||||
}
|
||||
/**
|
||||
* Set the current request instance.
|
||||
*
|
||||
* @param \Symfony\Component\HttpFoundation\Request $request
|
||||
* @return \Illuminate\Auth\SessionGuard
|
||||
* @static
|
||||
*/ public static function setRequest($request)
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->setRequest($request);
|
||||
}
|
||||
/**
|
||||
* Get the timebox instance used by the guard.
|
||||
*
|
||||
* @return \Illuminate\Support\Timebox
|
||||
* @static
|
||||
*/ public static function getTimebox()
|
||||
{
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->getTimebox();
|
||||
}
|
||||
/**
|
||||
* Determine if the current user is authenticated. If not, throw an exception.
|
||||
|
@ -1878,7 +2160,7 @@ namespace Illuminate\Support\Facades {
|
|||
* @static
|
||||
*/ public static function authenticate()
|
||||
{
|
||||
/** @var \App\Services\Auth\ReverseProxyGuard $instance */
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->authenticate();
|
||||
}
|
||||
/**
|
||||
|
@ -1888,7 +2170,7 @@ namespace Illuminate\Support\Facades {
|
|||
* @static
|
||||
*/ public static function hasUser()
|
||||
{
|
||||
/** @var \App\Services\Auth\ReverseProxyGuard $instance */
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->hasUser();
|
||||
}
|
||||
/**
|
||||
|
@ -1898,7 +2180,7 @@ namespace Illuminate\Support\Facades {
|
|||
* @static
|
||||
*/ public static function check()
|
||||
{
|
||||
/** @var \App\Services\Auth\ReverseProxyGuard $instance */
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->check();
|
||||
}
|
||||
/**
|
||||
|
@ -1908,38 +2190,17 @@ namespace Illuminate\Support\Facades {
|
|||
* @static
|
||||
*/ public static function guest()
|
||||
{
|
||||
/** @var \App\Services\Auth\ReverseProxyGuard $instance */
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->guest();
|
||||
}
|
||||
/**
|
||||
* Get the ID for the currently authenticated user.
|
||||
*
|
||||
* @return int|string|null
|
||||
* @static
|
||||
*/ public static function id()
|
||||
{
|
||||
/** @var \App\Services\Auth\ReverseProxyGuard $instance */
|
||||
return $instance->id();
|
||||
}
|
||||
/**
|
||||
* Set the current user.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||
* @return \App\Services\Auth\ReverseProxyGuard
|
||||
* @static
|
||||
*/ public static function setUser($user)
|
||||
{
|
||||
/** @var \App\Services\Auth\ReverseProxyGuard $instance */
|
||||
return $instance->setUser($user);
|
||||
}
|
||||
/**
|
||||
* Forget the current user.
|
||||
*
|
||||
* @return \App\Services\Auth\ReverseProxyGuard
|
||||
* @return \Illuminate\Auth\SessionGuard
|
||||
* @static
|
||||
*/ public static function forgetUser()
|
||||
{
|
||||
/** @var \App\Services\Auth\ReverseProxyGuard $instance */
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->forgetUser();
|
||||
}
|
||||
/**
|
||||
|
@ -1949,7 +2210,7 @@ namespace Illuminate\Support\Facades {
|
|||
* @static
|
||||
*/ public static function getProvider()
|
||||
{
|
||||
/** @var \App\Services\Auth\ReverseProxyGuard $instance */
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
return $instance->getProvider();
|
||||
}
|
||||
/**
|
||||
|
@ -1960,8 +2221,50 @@ namespace Illuminate\Support\Facades {
|
|||
* @static
|
||||
*/ public static function setProvider($provider)
|
||||
{
|
||||
/** @var \App\Services\Auth\ReverseProxyGuard $instance */
|
||||
/** @var \Illuminate\Auth\SessionGuard $instance */
|
||||
$instance->setProvider($provider);
|
||||
}
|
||||
/**
|
||||
* Register a custom macro.
|
||||
*
|
||||
* @param string $name
|
||||
* @param object|callable $macro
|
||||
* @return void
|
||||
* @static
|
||||
*/ public static function macro($name, $macro)
|
||||
{
|
||||
\Illuminate\Auth\SessionGuard::macro($name, $macro);
|
||||
}
|
||||
/**
|
||||
* Mix another object into the class.
|
||||
*
|
||||
* @param object $mixin
|
||||
* @param bool $replace
|
||||
* @return void
|
||||
* @throws \ReflectionException
|
||||
* @static
|
||||
*/ public static function mixin($mixin, $replace = true)
|
||||
{
|
||||
\Illuminate\Auth\SessionGuard::mixin($mixin, $replace);
|
||||
}
|
||||
/**
|
||||
* Checks if macro is registered.
|
||||
*
|
||||
* @param string $name
|
||||
* @return bool
|
||||
* @static
|
||||
*/ public static function hasMacro($name)
|
||||
{
|
||||
return \Illuminate\Auth\SessionGuard::hasMacro($name);
|
||||
}
|
||||
/**
|
||||
* Flush the existing macros.
|
||||
*
|
||||
* @return void
|
||||
* @static
|
||||
*/ public static function flushMacros()
|
||||
{
|
||||
\Illuminate\Auth\SessionGuard::flushMacros();
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
@ -8569,6 +8872,17 @@ namespace Illuminate\Support\Facades {
|
|||
{
|
||||
/** @var \Illuminate\Support\Testing\Fakes\NotificationFake $instance */
|
||||
return $instance->hasSent($notifiable, $notification);
|
||||
}
|
||||
/**
|
||||
* Specify if notification should be serialized and restored when being "pushed" to the queue.
|
||||
*
|
||||
* @param bool $serializeAndRestore
|
||||
* @return \Illuminate\Support\Testing\Fakes\NotificationFake
|
||||
* @static
|
||||
*/ public static function serializeAndRestore($serializeAndRestore = true)
|
||||
{
|
||||
/** @var \Illuminate\Support\Testing\Fakes\NotificationFake $instance */
|
||||
return $instance->serializeAndRestore($serializeAndRestore);
|
||||
}
|
||||
/**
|
||||
* Get the notifications that have been sent.
|
||||
|
|
|
@ -23,7 +23,7 @@ class UserManagerController extends Controller
|
|||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
return UserManagerResource::collection(User::all());
|
||||
return UserManagerResource::collection(User::withCount('twofaccounts')->get());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -18,6 +18,7 @@ use Jenssegers\Agent\Agent;
|
|||
* @property Carbon|null $logout_at
|
||||
* @property bool $login_successful
|
||||
* @property string|null $duration
|
||||
* @property string|null $login_method
|
||||
*/
|
||||
class UserAuthentication extends JsonResource
|
||||
{
|
||||
|
@ -50,6 +51,8 @@ class UserAuthentication extends JsonResource
|
|||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
$tz = $request->user()->preferences['timezone'];
|
||||
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'ip_address' => $this->ip_address,
|
||||
|
@ -58,15 +61,16 @@ class UserAuthentication extends JsonResource
|
|||
'platform' => $this->agent->platform(),
|
||||
'device' => $this->agent->deviceType(),
|
||||
'login_at' => $this->login_at
|
||||
? Carbon::parse($this->login_at)->toDayDateTimeString()
|
||||
? Carbon::parse($this->login_at,)->tz($tz)->toDayDateTimeString()
|
||||
: null,
|
||||
'logout_at' => $this->logout_at
|
||||
? Carbon::parse($this->logout_at)->toDayDateTimeString()
|
||||
? Carbon::parse($this->logout_at)->tz($tz)->toDayDateTimeString()
|
||||
: null,
|
||||
'login_successful' => $this->login_successful,
|
||||
'duration' => $this->logout_at
|
||||
? Carbon::parse($this->logout_at)->diffForHumans(Carbon::parse($this->login_at), ['syntax' => CarbonInterface::DIFF_ABSOLUTE])
|
||||
: null,
|
||||
'login_method' => $this->login_method,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,12 +85,14 @@ class UserManagerResource extends UserResource
|
|||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
$tz = $request->user()?->preferences['timezone'];
|
||||
|
||||
return array_merge(
|
||||
parent::toArray($request),
|
||||
[
|
||||
'twofaccounts_count' => is_null($this->twofaccounts_count) ? 0 : $this->twofaccounts_count,
|
||||
'last_seen_at' => Carbon::parse($this->last_seen_at)->locale(App::getLocale())->diffForHumans(),
|
||||
'created_at' => Carbon::parse($this->created_at)->locale(App::getLocale())->diffForHumans(),
|
||||
'last_seen_at' => Carbon::parse($this->last_seen_at)->tz($tz)->locale(App::getLocale())->diffForHumans(),
|
||||
'created_at' => Carbon::parse($this->created_at)->tz($tz)->locale(App::getLocale())->diffForHumans(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
* Copyright (c) 2024 Bubka
|
||||
* Copyright (c) 2024 Anthony Rappa
|
||||
* Copyright (c) 2017 Yaakov Dahan
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
* associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
* including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
* portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\AuthLog;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class PurgeAuthLog extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $signature = '2fauth:purge-log';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $description = 'Purge all authentication logs older than the configurable amount of days.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle() : void
|
||||
{
|
||||
$this->comment('Clearing authentication log...');
|
||||
|
||||
$deleted = AuthLog::where('login_at', '<', now()->subDays(config('2fauth.authLogRetentionTime'))->format('Y-m-d H:i:s'))->delete();
|
||||
|
||||
$this->info($deleted . ' authentication logs cleared.');
|
||||
}
|
||||
}
|
|
@ -70,7 +70,7 @@ trait ResetTrait
|
|||
DB::table('groups')->delete();
|
||||
DB::table('users')->delete();
|
||||
DB::table('options')->delete();
|
||||
DB::table(config('authentication-log.table_name'))->delete();
|
||||
DB::table('auth_logs')->delete();
|
||||
|
||||
$this->line('Database cleaned');
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class VisitedByProxyUser
|
||||
{
|
||||
/**
|
||||
* The authenticated user.
|
||||
*
|
||||
* @var User|Authenticatable
|
||||
*/
|
||||
public $user;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(User|Authenticatable $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
}
|
|
@ -16,6 +16,8 @@ use Illuminate\Validation\ValidationException;
|
|||
|
||||
class RemoteUserProvider implements UserProvider
|
||||
{
|
||||
const FAKE_REMOTE_DOMAIN = '@remote';
|
||||
|
||||
/**
|
||||
* The currently authenticated user.
|
||||
*
|
||||
|
@ -85,7 +87,7 @@ class RemoteUserProvider implements UserProvider
|
|||
*/
|
||||
protected function fakeRemoteEmail(mixed $id)
|
||||
{
|
||||
return substr($id, 0, 184) . '@remote';
|
||||
return substr($id, 0, 184) . self::FAKE_REMOTE_DOMAIN;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,6 +5,9 @@ namespace App\Facades;
|
|||
use App\Services\QrCodeService;
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
/**
|
||||
* @see \App\Services\QrCodeService
|
||||
*/
|
||||
class QrCode extends Facade
|
||||
{
|
||||
protected static function getFacadeAccessor()
|
||||
|
|
|
@ -5,6 +5,9 @@ namespace App\Facades;
|
|||
use App\Services\SettingService;
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
/**
|
||||
* @see \App\Services\SettingService
|
||||
*/
|
||||
class Settings extends Facade
|
||||
{
|
||||
protected static function getFacadeAccessor()
|
||||
|
|
|
@ -23,8 +23,7 @@ class LogUserLastSeen
|
|||
// We do not track activity of:
|
||||
// - Guest
|
||||
// - User authenticated against a bearer token
|
||||
// - User authenticated via a reverse-proxy
|
||||
if (Auth::guard($guard)->check() && ! $request->bearerToken() && config('auth.defaults.guard') !== 'reverse-proxy-guard') {
|
||||
if (Auth::guard($guard)->check() && ! $request->bearerToken()) {
|
||||
Auth::guard($guard)->user()->last_seen_at = Carbon::now()->format('Y-m-d H:i:s');
|
||||
Auth::guard($guard)->user()->save();
|
||||
break;
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace App\Listeners\Authentication;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
abstract class AbstractAccessListener
|
||||
{
|
||||
/**
|
||||
* The current request
|
||||
*/
|
||||
public Request $request;
|
||||
|
||||
/**
|
||||
* Create the event listener.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Request $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function handle(mixed $event);
|
||||
|
||||
/**
|
||||
* Get the login method based on the request input parameters
|
||||
*/
|
||||
public function loginMethod() : ?string
|
||||
{
|
||||
if ($this->request->has('response.authenticatorData')) {
|
||||
return 'webauthn';
|
||||
} elseif ($this->request->has('password')) {
|
||||
return 'password';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
* Copyright (c) 2024 Bubka
|
||||
* Copyright (c) 2024 Anthony Rappa
|
||||
* Copyright (c) 2017 Yaakov Dahan
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
* associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
* including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
* portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
namespace App\Listeners\Authentication;
|
||||
|
||||
use App\Notifications\FailedLogin;
|
||||
use Illuminate\Auth\Events\Failed;
|
||||
|
||||
class FailedLoginListener extends AbstractAccessListener
|
||||
{
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle(mixed $event) : void
|
||||
{
|
||||
if (! $event instanceof Failed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($event->user) {
|
||||
/**
|
||||
* @var \App\Models\User
|
||||
*/
|
||||
$user = $event->user;
|
||||
$guard = $event->guard;
|
||||
$ip = config('2fauth.proxy_headers.forIp') ?? $this->request->ip();
|
||||
|
||||
$log = $user->authentications()->create([
|
||||
'ip_address' => $ip,
|
||||
'user_agent' => $this->request->userAgent(),
|
||||
'login_at' => now(),
|
||||
'login_successful' => false,
|
||||
'guard' => $guard,
|
||||
'login_method' => $this->loginMethod(),
|
||||
]);
|
||||
|
||||
if ($user->preferences['notifyOnFailedLogin'] == true) {
|
||||
$user->notify(new FailedLogin($log));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
* Copyright (c) 2024 Bubka
|
||||
* Copyright (c) 2024 Anthony Rappa
|
||||
* Copyright (c) 2017 Yaakov Dahan
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
* associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
* including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
* portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
namespace App\Listeners\Authentication;
|
||||
|
||||
use App\Notifications\SignedInWithNewDevice;
|
||||
use Illuminate\Auth\Events\Login;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class LoginListener extends AbstractAccessListener
|
||||
{
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle(mixed $event) : void
|
||||
{
|
||||
if (! $event instanceof Login) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var \App\Models\User
|
||||
*/
|
||||
$user = $event->user;
|
||||
$ip = config('2fauth.proxy_headers.forIp') ?? $this->request->ip();
|
||||
$userAgent = $this->request->userAgent();
|
||||
$known = $user->authentications()->whereIpAddress($ip)->whereUserAgent($userAgent)->whereLoginSuccessful(true)->first();
|
||||
$newUser = Carbon::parse($user->{$user->getCreatedAtColumn()})->diffInMinutes(Carbon::now()) < 1;
|
||||
$guard = $event->guard;
|
||||
|
||||
$log = $user->authentications()->create([
|
||||
'ip_address' => $ip,
|
||||
'user_agent' => $userAgent,
|
||||
'login_at' => now(),
|
||||
'login_successful' => true,
|
||||
'guard' => $guard,
|
||||
'login_method' => $this->loginMethod(),
|
||||
]);
|
||||
|
||||
if (! $known && ! $newUser && $user->preferences['notifyOnNewAuthDevice'] == true) {
|
||||
$user->notify(new SignedInWithNewDevice($log));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
* Copyright (c) 2024 Bubka
|
||||
* Copyright (c) 2024 Anthony Rappa
|
||||
* Copyright (c) 2017 Yaakov Dahan
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
* associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
* including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
* portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
namespace App\Listeners\Authentication;
|
||||
|
||||
use App\Models\AuthLog;
|
||||
use Illuminate\Auth\Events\Logout;
|
||||
|
||||
class LogoutListener extends AbstractAccessListener
|
||||
{
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle(mixed $event) : void
|
||||
{
|
||||
if (! $event instanceof Logout) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var \App\Models\User
|
||||
*/
|
||||
$user = $event->user;
|
||||
$ip = config('2fauth.proxy_headers.forIp') ?? $this->request->ip();
|
||||
$userAgent = $this->request->userAgent();
|
||||
$log = $user->authentications()->whereIpAddress($ip)->whereUserAgent($userAgent)->whereGuard($event->guard)->orderByDesc('login_at')->first();
|
||||
$guard = $event->guard;
|
||||
|
||||
if (! $log) {
|
||||
$log = new AuthLog([
|
||||
'ip_address' => $ip,
|
||||
'user_agent' => $userAgent,
|
||||
'guard' => $guard,
|
||||
]);
|
||||
}
|
||||
|
||||
$log->logout_at = now();
|
||||
$user->authentications()->save($log);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
* Copyright (c) 2024 Bubka
|
||||
* Copyright (c) 2024 Anthony Rappa
|
||||
* Copyright (c) 2017 Yaakov Dahan
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
* associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
* including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
* portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
namespace App\Listeners\Authentication;
|
||||
|
||||
use App\Models\AuthLog;
|
||||
use Illuminate\Auth\Events\OtherDeviceLogout;
|
||||
|
||||
class OtherDeviceLogoutListener extends AbstractAccessListener
|
||||
{
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle(mixed $event) : void
|
||||
{
|
||||
if (! $event instanceof OtherDeviceLogout) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var \App\Models\User
|
||||
*/
|
||||
$user = $event->user;
|
||||
$ip = config('2fauth.proxy_headers.forIp') ?? $this->request->ip();
|
||||
$userAgent = $this->request->userAgent();
|
||||
$authLog = $user->authentications()->whereIpAddress($ip)->whereUserAgent($userAgent)->first();
|
||||
$guard = $event->guard;
|
||||
|
||||
if (! $authLog) {
|
||||
$authLog = new AuthLog([
|
||||
'ip_address' => $ip,
|
||||
'user_agent' => $userAgent,
|
||||
'guard' => $guard,
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($user->authentications()->whereLoginSuccessful(true)->whereNull('logout_at')->get() as $log) {
|
||||
if ($log->id !== $authLog->id) {
|
||||
$log->update([
|
||||
'cleared_by_user' => true,
|
||||
'logout_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
namespace App\Listeners\Authentication;
|
||||
|
||||
use App\Events\VisitedByProxyUser;
|
||||
use App\Extensions\RemoteUserProvider;
|
||||
use App\Listeners\Authentication\AbstractAccessListener;
|
||||
use App\Notifications\SignedInWithNewDevice;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class VisitedByProxyUserListener extends AbstractAccessListener
|
||||
{
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle(mixed $event): void
|
||||
{
|
||||
if (! $event instanceof VisitedByProxyUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var \App\Models\User
|
||||
*/
|
||||
$user = $event->user;
|
||||
$ip = config('2fauth.proxy_headers.forIp') ?? $this->request->ip();
|
||||
$userAgent = $this->request->userAgent();
|
||||
$known = $user->authentications()->whereIpAddress($ip)->whereUserAgent($userAgent)->whereLoginSuccessful(true)->first();
|
||||
$newUser = Carbon::parse($user->{$user->getCreatedAtColumn()})->diffInMinutes(Carbon::now()) < 1;
|
||||
$guard = config('auth.defaults.guard');
|
||||
|
||||
$log = $user->authentications()->create([
|
||||
'ip_address' => $ip,
|
||||
'user_agent' => $userAgent,
|
||||
'login_at' => now(),
|
||||
'login_successful' => true,
|
||||
'guard' => $guard,
|
||||
]);
|
||||
|
||||
if (! $known && ! $newUser && ! str_ends_with($user->email, RemoteUserProvider::FAKE_REMOTE_DOMAIN) && $user->preferences['notifyOnNewAuthDevice']) {
|
||||
$user->notify(new SignedInWithNewDevice($log));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
* Copyright (c) 2024 Bubka
|
||||
* Copyright (c) 2024 Anthony Rappa
|
||||
* Copyright (c) 2017 Yaakov Dahan
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
* associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
* including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
* portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $authenticatable_type
|
||||
* @property int $authenticatable_id
|
||||
* @property string|null $ip_address
|
||||
* @property string|null $user_agent
|
||||
* @property \Illuminate\Support\Carbon|null $login_at
|
||||
* @property bool $login_successful
|
||||
* @property \Illuminate\Support\Carbon|null $logout_at
|
||||
* @property bool $cleared_by_user
|
||||
* @property string|null $guard
|
||||
* @property string|null $method
|
||||
*/
|
||||
class AuthLog extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*/
|
||||
protected $fillable = [
|
||||
'ip_address',
|
||||
'user_agent',
|
||||
'login_at',
|
||||
'login_successful',
|
||||
'logout_at',
|
||||
'cleared_by_user',
|
||||
'guard',
|
||||
'login_method',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*/
|
||||
protected $casts = [
|
||||
'cleared_by_user' => 'boolean',
|
||||
'login_successful' => 'boolean',
|
||||
'login_at' => 'datetime',
|
||||
'logout_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* MorphTo relation to get the associated authenticatable user
|
||||
*
|
||||
* @return MorphTo<\Illuminate\Database\Eloquent\Model, AuthLog>
|
||||
*/
|
||||
public function authenticatable()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
* Copyright (c) 2024 Bubka
|
||||
* Copyright (c) 2024 Anthony Rappa
|
||||
* Copyright (c) 2017 Yaakov Dahan
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
* associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
* including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
* portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
namespace App\Models\Traits;
|
||||
|
||||
use App\Models\AuthLog;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
trait HasAuthenticationLog
|
||||
{
|
||||
/**
|
||||
* Get all user's authentications from the auth log
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphMany<AuthLog>
|
||||
*/
|
||||
public function authentications()
|
||||
{
|
||||
return $this->morphMany(AuthLog::class, 'authenticatable')->latest('id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authentications for the provided timespan (in month)
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, AuthLog>
|
||||
*/
|
||||
public function authenticationsByPeriod(int $period = 1)
|
||||
{
|
||||
$from = Carbon::now()->subMonths($period);
|
||||
|
||||
return $this->authentications->filter(function (AuthLog $authentication) use ($from) {
|
||||
return $authentication->login_at >= $from || $authentication->logout_at >= $from;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's latest authentication
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphOne<AuthLog>
|
||||
*/
|
||||
public function latestAuthentication()
|
||||
{
|
||||
return $this->morphOne(AuthLog::class, 'authenticatable')->latestOfMany('login_at');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's latest authentication datetime
|
||||
*/
|
||||
public function lastLoginAt() : ?Carbon
|
||||
{
|
||||
return $this->authentications()->first()?->login_at;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's latest successful login datetime
|
||||
*/
|
||||
public function lastSuccessfulLoginAt() : ?Carbon
|
||||
{
|
||||
return $this->authentications()->whereLoginSuccessful(true)->first()?->login_at;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ip address of user's latest login
|
||||
*/
|
||||
public function lastLoginIp() : ?string
|
||||
{
|
||||
return $this->authentications()->first()?->ip_address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ip address of user's latest successful login
|
||||
*/
|
||||
public function lastSuccessfulLoginIp() : ?string
|
||||
{
|
||||
return $this->authentications()->whereLoginSuccessful(true)->first()?->ip_address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's previous login datetime
|
||||
*/
|
||||
public function previousLoginAt() : ?Carbon
|
||||
{
|
||||
return $this->authentications()->skip(1)->first()?->login_at;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ip address of user's previous login
|
||||
*/
|
||||
public function previousLoginIp() : ?string
|
||||
{
|
||||
return $this->authentications()->skip(1)->first()?->ip_address;
|
||||
}
|
||||
|
||||
/**
|
||||
* The notification channels to be used for notifications
|
||||
*/
|
||||
public function notifyAuthLogVia() : array
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
namespace App\Models;
|
||||
|
||||
use App\Models\Traits\WebAuthnManageCredentials;
|
||||
use Bubka\LaravelAuthenticationLog\Traits\AuthenticationLoggable;
|
||||
use App\Models\Traits\HasAuthenticationLog;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Auth\Notifications\ResetPassword;
|
||||
use Illuminate\Contracts\Translation\HasLocalePreference;
|
||||
|
@ -43,9 +43,9 @@ use Laravel\Passport\HasApiTokens;
|
|||
* @property-read int|null $web_authn_credentials_count
|
||||
* @property string|null $oauth_id
|
||||
* @property string|null $oauth_provider
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Bubka\LaravelAuthenticationLog\Models\AuthenticationLog> $authentications
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\AuthLog> $authentications
|
||||
* @property-read int|null $authentications_count
|
||||
* @property-read \Bubka\LaravelAuthenticationLog\Models\AuthenticationLog|null $latestAuthentication
|
||||
* @property-read \App\Models\AuthLog|null $latestAuthentication
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User admins()
|
||||
*
|
||||
|
@ -53,7 +53,7 @@ use Laravel\Passport\HasApiTokens;
|
|||
*/
|
||||
class User extends Authenticatable implements HasLocalePreference, WebAuthnAuthenticatable
|
||||
{
|
||||
use AuthenticationLoggable;
|
||||
use HasAuthenticationLog;
|
||||
use HasApiTokens, HasFactory, Notifiable;
|
||||
use WebAuthnAuthentication, WebAuthnManageCredentials;
|
||||
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\AuthLog;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Jenssegers\Agent\Agent;
|
||||
|
||||
class FailedLogin extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* A user agent parser instance.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
protected $agent;
|
||||
|
||||
/**
|
||||
* The AuthLog model instance
|
||||
*/
|
||||
public AuthLog $authLog;
|
||||
|
||||
/**
|
||||
* Create a new FailedLogin instance
|
||||
*/
|
||||
public function __construct(AuthLog $authLog)
|
||||
{
|
||||
$this->authLog = $authLog;
|
||||
$this->agent = new Agent();
|
||||
$this->agent->setUserAgent($authLog->user_agent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's channels.
|
||||
*/
|
||||
public function via(mixed $notifiable) : array|string
|
||||
{
|
||||
return $notifiable->notifyAuthLogVia();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the mail representation of the notification.
|
||||
*/
|
||||
public function toMail(mixed $notifiable) : MailMessage
|
||||
{
|
||||
return (new MailMessage())
|
||||
->subject(__('notifications.failed_login.subject'))
|
||||
->markdown('emails.failedLogin', [
|
||||
'account' => $notifiable,
|
||||
'time' => $this->authLog->login_at,
|
||||
'ipAddress' => $this->authLog->ip_address,
|
||||
'browser' => $this->authLog->user_agent,
|
||||
'platform' => $this->agent->platform(),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Notifications;
|
||||
|
||||
use Bubka\LaravelAuthenticationLog\Models\AuthenticationLog;
|
||||
use App\Models\AuthLog;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
@ -12,8 +12,11 @@ use Jenssegers\Agent\Agent;
|
|||
class SignedInWithNewDevice extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public AuthenticationLog $authenticationLog;
|
||||
|
||||
/**
|
||||
* The AuthLog model instance
|
||||
*/
|
||||
public AuthLog $authLog;
|
||||
|
||||
/**
|
||||
* A user agent parser instance.
|
||||
|
@ -25,16 +28,16 @@ class SignedInWithNewDevice extends Notification implements ShouldQueue
|
|||
/**
|
||||
* Create a new SignedInWithNewDevice instance
|
||||
*/
|
||||
public function __construct(AuthenticationLog $authenticationLog)
|
||||
public function __construct(AuthLog $authLog)
|
||||
{
|
||||
$this->authenticationLog = $authenticationLog;
|
||||
$this->agent = new Agent();
|
||||
$this->agent->setUserAgent($authenticationLog->user_agent);
|
||||
$this->authLog = $authLog;
|
||||
$this->agent = new Agent();
|
||||
$this->agent->setUserAgent($authLog->user_agent);
|
||||
}
|
||||
|
||||
public function via(mixed $notifiable) : array|string
|
||||
{
|
||||
return $notifiable->notifyAuthenticationLogVia();
|
||||
return $notifiable->notifyAuthLogVia();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -44,10 +47,10 @@ class SignedInWithNewDevice extends Notification implements ShouldQueue
|
|||
{
|
||||
return (new MailMessage())
|
||||
->subject(__('notifications.new_device.subject'))
|
||||
->markdown('emails.newDevice', [
|
||||
->markdown('emails.SignedInWithNewDevice', [
|
||||
'account' => $notifiable,
|
||||
'time' => $this->authenticationLog->login_at,
|
||||
'ipAddress' => $this->authenticationLog->ip_address,
|
||||
'time' => $this->authLog->login_at,
|
||||
'ipAddress' => $this->authLog->ip_address,
|
||||
'browser' => $this->agent->browser(),
|
||||
'platform' => $this->agent->platform(),
|
||||
]);
|
||||
|
|
|
@ -2,10 +2,15 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Listeners\Authentication\VisitedByProxyUserListener;
|
||||
use App\Events\GroupDeleted;
|
||||
use App\Events\GroupDeleting;
|
||||
use App\Events\VisitedByProxyUser;
|
||||
use App\Events\ScanForNewReleaseCalled;
|
||||
use App\Events\TwoFAccountDeleted;
|
||||
use App\Listeners\Authentication\FailedLoginListener;
|
||||
use App\Listeners\Authentication\LoginListener;
|
||||
use App\Listeners\Authentication\LogoutListener;
|
||||
use App\Listeners\CleanIconStorage;
|
||||
use App\Listeners\DissociateTwofaccountFromGroup;
|
||||
use App\Listeners\LogNotification;
|
||||
|
@ -14,6 +19,9 @@ use App\Listeners\ReleaseRadar;
|
|||
use App\Listeners\ResetUsersPreference;
|
||||
use App\Models\User;
|
||||
use App\Observers\UserObserver;
|
||||
use Illuminate\Auth\Events\Failed;
|
||||
use Illuminate\Auth\Events\Login;
|
||||
use Illuminate\Auth\Events\Logout;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
|
@ -49,6 +57,18 @@ class EventServiceProvider extends ServiceProvider
|
|||
NotificationSent::class => [
|
||||
LogNotification::class,
|
||||
],
|
||||
Login::class => [
|
||||
LoginListener::class,
|
||||
],
|
||||
Failed::class => [
|
||||
FailedLoginListener::class,
|
||||
],
|
||||
Logout::class => [
|
||||
LogoutListener::class,
|
||||
],
|
||||
VisitedByProxyUser::class => [
|
||||
VisitedByProxyUserListener::class,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
namespace App\Services\Auth;
|
||||
|
||||
use App\Events\VisitedByProxyUser;
|
||||
use Illuminate\Auth\GuardHelpers;
|
||||
use Illuminate\Contracts\Auth\Guard;
|
||||
use Illuminate\Contracts\Auth\UserProvider;
|
||||
|
@ -17,14 +18,12 @@ class ReverseProxyGuard implements Guard
|
|||
/**
|
||||
* The currently authenticated user.
|
||||
*
|
||||
* @var \Illuminate\Contracts\Auth\Authenticatable|null
|
||||
* @var \Illuminate\Contracts\Auth\Authenticatable|\App\Models\User|null
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* Create a new authentication guard.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(UserProvider $provider)
|
||||
{
|
||||
|
@ -76,7 +75,13 @@ class ReverseProxyGuard implements Guard
|
|||
}
|
||||
}
|
||||
|
||||
return $this->user = $this->provider->retrieveById($identifier);
|
||||
if ($this->user = $this->provider->retrieveById($identifier)) {
|
||||
if ($this->user->lastLoginAt() < now()->subMinutes(15)) {
|
||||
event(new VisitedByProxyUser($this->user));
|
||||
}
|
||||
}
|
||||
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -37,18 +37,8 @@
|
|||
"paragonie/constant_time_encoding": "^2.6",
|
||||
"socialiteproviders/manager": "^4.4",
|
||||
"spatie/eloquent-sortable": "^4.0.1",
|
||||
"spomky-labs/otphp": "^11.0",
|
||||
"bubka/laravel-authentication-log": "@dev"
|
||||
"spomky-labs/otphp": "^11.0"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "path",
|
||||
"url": "../packages/bubka/laravel-authentication-log",
|
||||
"options": {
|
||||
"symlink": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-ide-helper": "^2.13",
|
||||
"brianium/paratest": "^7.3",
|
||||
|
|
|
@ -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": "3d355aab37adc72f3901f200edcf2445",
|
||||
"content-hash": "93e717902e3c1435d0768115a200b000",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
|
@ -61,59 +61,6 @@
|
|||
],
|
||||
"time": "2023-01-15T23:15:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "bubka/laravel-authentication-log",
|
||||
"version": "dev-main",
|
||||
"dist": {
|
||||
"type": "path",
|
||||
"url": "../packages/bubka/laravel-authentication-log",
|
||||
"reference": "a916caaa979b1d18d679d8b063325fe547f691c6"
|
||||
},
|
||||
"require": {
|
||||
"illuminate/contracts": "^10.0|^11.0",
|
||||
"laravel/pint": "^1.15",
|
||||
"php": "^8.1",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"spatie/laravel-package-tools": "^1.4.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"larastan/larastan": "^2.9",
|
||||
"nunomaduro/collision": "^6.0",
|
||||
"orchestra/testbench": "^8.22"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Bubka\\LaravelAuthenticationLog\\LaravelAuthenticationLogServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Bubka\\LaravelAuthenticationLog\\": "src",
|
||||
"Bubka\\LaravelAuthenticationLog\\Database\\Factories\\": "database/factories"
|
||||
}
|
||||
},
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Bubka"
|
||||
}
|
||||
],
|
||||
"description": "Log user authentication details and send new device notifications.",
|
||||
"homepage": "https://github.com/bubka/laravel-authentication-log",
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"laravel-authentication-log"
|
||||
],
|
||||
"transport-options": {
|
||||
"symlink": true,
|
||||
"relative": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "carbonphp/carbon-doctrine-types",
|
||||
"version": "2.1.0",
|
||||
|
@ -2543,72 +2490,6 @@
|
|||
},
|
||||
"time": "2024-03-01T11:11:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/pint",
|
||||
"version": "v1.15.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/pint.git",
|
||||
"reference": "5f288b5e79938cc72f5c298d384e639de87507c6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/pint/zipball/5f288b5e79938cc72f5c298d384e639de87507c6",
|
||||
"reference": "5f288b5e79938cc72f5c298d384e639de87507c6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-tokenizer": "*",
|
||||
"ext-xml": "*",
|
||||
"php": "^8.1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.52.1",
|
||||
"illuminate/view": "^10.48.4",
|
||||
"larastan/larastan": "^2.9.2",
|
||||
"laravel-zero/framework": "^10.3.0",
|
||||
"mockery/mockery": "^1.6.11",
|
||||
"nunomaduro/termwind": "^1.15.1",
|
||||
"pestphp/pest": "^2.34.5"
|
||||
},
|
||||
"bin": [
|
||||
"builds/pint"
|
||||
],
|
||||
"type": "project",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Seeders\\": "database/seeders/",
|
||||
"Database\\Factories\\": "database/factories/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nuno Maduro",
|
||||
"email": "enunomaduro@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "An opinionated code formatter for PHP.",
|
||||
"homepage": "https://laravel.com",
|
||||
"keywords": [
|
||||
"format",
|
||||
"formatter",
|
||||
"lint",
|
||||
"linter",
|
||||
"php"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/pint/issues",
|
||||
"source": "https://github.com/laravel/pint"
|
||||
},
|
||||
"time": "2024-04-02T14:28:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/prompts",
|
||||
"version": "v0.1.19",
|
||||
|
@ -4791,64 +4672,6 @@
|
|||
],
|
||||
"time": "2024-03-03T02:14:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "1.10.67",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan.git",
|
||||
"reference": "16ddbe776f10da6a95ebd25de7c1dbed397dc493"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/16ddbe776f10da6a95ebd25de7c1dbed397dc493",
|
||||
"reference": "16ddbe776f10da6a95ebd25de7c1dbed397dc493",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2|^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"phpstan/phpstan-shim": "*"
|
||||
},
|
||||
"bin": [
|
||||
"phpstan",
|
||||
"phpstan.phar"
|
||||
],
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "PHPStan - PHP Static Analysis Tool",
|
||||
"keywords": [
|
||||
"dev",
|
||||
"static analysis"
|
||||
],
|
||||
"support": {
|
||||
"docs": "https://phpstan.org/user-guide/getting-started",
|
||||
"forum": "https://github.com/phpstan/phpstan/discussions",
|
||||
"issues": "https://github.com/phpstan/phpstan/issues",
|
||||
"security": "https://github.com/phpstan/phpstan/security/policy",
|
||||
"source": "https://github.com/phpstan/phpstan-src"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/ondrejmirtes",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/phpstan",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-04-16T07:22:02+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/cache",
|
||||
"version": "3.0.0",
|
||||
|
@ -9285,6 +9108,72 @@
|
|||
],
|
||||
"time": "2024-04-16T19:13:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/pint",
|
||||
"version": "v1.15.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/pint.git",
|
||||
"reference": "5f288b5e79938cc72f5c298d384e639de87507c6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/pint/zipball/5f288b5e79938cc72f5c298d384e639de87507c6",
|
||||
"reference": "5f288b5e79938cc72f5c298d384e639de87507c6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-tokenizer": "*",
|
||||
"ext-xml": "*",
|
||||
"php": "^8.1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.52.1",
|
||||
"illuminate/view": "^10.48.4",
|
||||
"larastan/larastan": "^2.9.2",
|
||||
"laravel-zero/framework": "^10.3.0",
|
||||
"mockery/mockery": "^1.6.11",
|
||||
"nunomaduro/termwind": "^1.15.1",
|
||||
"pestphp/pest": "^2.34.5"
|
||||
},
|
||||
"bin": [
|
||||
"builds/pint"
|
||||
],
|
||||
"type": "project",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Seeders\\": "database/seeders/",
|
||||
"Database\\Factories\\": "database/factories/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nuno Maduro",
|
||||
"email": "enunomaduro@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "An opinionated code formatter for PHP.",
|
||||
"homepage": "https://laravel.com",
|
||||
"keywords": [
|
||||
"format",
|
||||
"formatter",
|
||||
"lint",
|
||||
"linter",
|
||||
"php"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/pint/issues",
|
||||
"source": "https://github.com/laravel/pint"
|
||||
},
|
||||
"time": "2024-04-02T14:28:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "mockery/mockery",
|
||||
"version": "1.6.11",
|
||||
|
@ -9887,6 +9776,64 @@
|
|||
},
|
||||
"time": "2024-04-03T18:51:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "1.10.67",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan.git",
|
||||
"reference": "16ddbe776f10da6a95ebd25de7c1dbed397dc493"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/16ddbe776f10da6a95ebd25de7c1dbed397dc493",
|
||||
"reference": "16ddbe776f10da6a95ebd25de7c1dbed397dc493",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2|^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"phpstan/phpstan-shim": "*"
|
||||
},
|
||||
"bin": [
|
||||
"phpstan",
|
||||
"phpstan.phar"
|
||||
],
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "PHPStan - PHP Static Analysis Tool",
|
||||
"keywords": [
|
||||
"dev",
|
||||
"static analysis"
|
||||
],
|
||||
"support": {
|
||||
"docs": "https://phpstan.org/user-guide/getting-started",
|
||||
"forum": "https://github.com/phpstan/phpstan/discussions",
|
||||
"issues": "https://github.com/phpstan/phpstan/issues",
|
||||
"security": "https://github.com/phpstan/phpstan/security/policy",
|
||||
"source": "https://github.com/phpstan/phpstan-src"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/ondrejmirtes",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/phpstan",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-04-16T07:22:02+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
"version": "10.1.14",
|
||||
|
@ -11584,9 +11531,7 @@
|
|||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {
|
||||
"bubka/laravel-authentication-log": 20
|
||||
},
|
||||
"stability-flags": [],
|
||||
"prefer-stable": true,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
|
|
|
@ -28,6 +28,18 @@ return [
|
|||
'outgoingProxy' => env('PROXY_FOR_OUTGOING_REQUESTS', ''),
|
||||
'proxyLogoutUrl' => env('PROXY_LOGOUT_URL', null),
|
||||
'appSubdirectory' => env('APP_SUBDIRECTORY', ''),
|
||||
'authLogRetentionTime' => env('AUTHENTICATION_LOG_RETENTION', 365),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Proxy headers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
*/
|
||||
|
||||
'proxy_headers' => [
|
||||
'forIp' => env('PROXY_HEADER_FOR_IP', null),
|
||||
],
|
||||
|
||||
/*
|
||||
|
@ -110,6 +122,9 @@ return [
|
|||
'formatPasswordBy' => 0.5,
|
||||
'lang' => 'browser',
|
||||
'getOtpOnRequest' => true,
|
||||
'notifyOnNewAuthDevice' => true,
|
||||
'notifyOnFailedLogin' => true,
|
||||
'timezone' => env('APP_TIMEZONE', 'UTC'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
@ -72,7 +72,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'timezone' => 'UTC',
|
||||
'timezone' => env('APP_TIMEZONE', 'UTC'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
// The database table name
|
||||
// You can change this if the database keys get too long for your driver
|
||||
'table_name' => 'authentication_log',
|
||||
|
||||
// The database connection where the authentication_log table resides. Leave empty to use the default
|
||||
'db_connection' => null,
|
||||
|
||||
// The events the package listens for to log
|
||||
'events' => [
|
||||
'login' => \Illuminate\Auth\Events\Login::class,
|
||||
'failed' => \Illuminate\Auth\Events\Failed::class,
|
||||
'logout' => \Illuminate\Auth\Events\Logout::class,
|
||||
// 'logout-other-devices' => \Illuminate\Auth\Events\OtherDeviceLogout::class,
|
||||
// 'proxyUserAccess' => \App\Events\VisitedByProxyUser::class,
|
||||
],
|
||||
|
||||
'listeners' => [
|
||||
'login' => \Bubka\LaravelAuthenticationLog\Listeners\LoginListener::class,
|
||||
'failed' => \Bubka\LaravelAuthenticationLog\Listeners\FailedLoginListener::class,
|
||||
'logout' => \Bubka\LaravelAuthenticationLog\Listeners\LogoutListener::class,
|
||||
// 'logout-other-devices' => \Bubka\LaravelAuthenticationLog\Listeners\OtherDeviceLogoutListener::class,
|
||||
// 'proxyUserAccess' => \App\Listeners\VisitedByProxyUserListener::class,
|
||||
],
|
||||
|
||||
'notifications' => [
|
||||
'new-device' => [
|
||||
// Send the NewDevice notification
|
||||
'enabled' => env('NEW_DEVICE_NOTIFICATION', true),
|
||||
|
||||
// Use torann/geoip to attempt to get a location
|
||||
'location' => false,
|
||||
|
||||
// The Notification class to send
|
||||
'template' => \App\Notifications\SignedInWithNewDevice::class,
|
||||
],
|
||||
'failed-login' => [
|
||||
// Send the FailedLogin notification
|
||||
'enabled' => env('FAILED_LOGIN_NOTIFICATION', false),
|
||||
|
||||
// Use torann/geoip to attempt to get a location
|
||||
'location' => false,
|
||||
|
||||
// The Notification class to send
|
||||
'template' => \Bubka\LaravelAuthenticationLog\Notifications\FailedLogin::class,
|
||||
],
|
||||
],
|
||||
|
||||
// When the clean-up command is run, delete old logs greater than `purge` days
|
||||
// Don't schedule the clean-up command if you want to keep logs forever.
|
||||
'purge' => 365,
|
||||
|
||||
// If you are behind an CDN proxy, set 'behind_cdn.http_header_field' to the corresponding http header field of your cdn
|
||||
// For cloudflare you can have look at: https://developers.cloudflare.com/fundamentals/get-started/reference/http-request-headers/
|
||||
// 'behind_cdn' => [
|
||||
// 'http_header_field' => 'HTTP_CF_CONNECTING_IP' // used by Cloudflare
|
||||
// ],
|
||||
|
||||
// If you are not a cdn user, use false
|
||||
'behind_cdn' => false,
|
||||
];
|
|
@ -0,0 +1,164 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use ParagonIE\ConstantTime\Base32;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AuthLog>
|
||||
*/
|
||||
class AuthLogFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function definition()
|
||||
{
|
||||
return [
|
||||
'ip_address' => '127.0.0.1',
|
||||
'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
|
||||
'login_at' => now(),
|
||||
'login_successful' => true,
|
||||
'logout_at' => null,
|
||||
'guard' => 'web-guard',
|
||||
'login_method' => 'password',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model is a failed login.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AuthLog>
|
||||
*/
|
||||
public function failedLogin()
|
||||
{
|
||||
return $this->state(function (array $attributes) {
|
||||
return [
|
||||
'login_successful' => false,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model has a logout date too.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AuthLog>
|
||||
*/
|
||||
public function withLogout()
|
||||
{
|
||||
return $this->state(function (array $attributes) {
|
||||
return [
|
||||
'logout_at' => now(),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model has a logout date only, without login date.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AuthLog>
|
||||
*/
|
||||
public function logoutOnly()
|
||||
{
|
||||
return $this->state(function (array $attributes) {
|
||||
return [
|
||||
'login_at' => null,
|
||||
'login_successful' => false,
|
||||
'logout_at' => now(),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model has login during last month.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AuthLog>
|
||||
*/
|
||||
public function duringLastMonth()
|
||||
{
|
||||
return $this->state(function (array $attributes) {
|
||||
$loginDate = now()->subDays(15);
|
||||
$logoutDate = $loginDate->addHours(1);
|
||||
|
||||
return [
|
||||
'login_at' => $loginDate,
|
||||
'logout_at' => $logoutDate,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model has login during last 3 months.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AuthLog>
|
||||
*/
|
||||
public function duringLastThreeMonth()
|
||||
{
|
||||
return $this->state(function (array $attributes) {
|
||||
$loginDate = now()->subMonths(2);
|
||||
$logoutDate = $loginDate->addHours(1);
|
||||
|
||||
return [
|
||||
'login_at' => $loginDate,
|
||||
'logout_at' => $logoutDate,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model has login during last 6 month.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AuthLog>
|
||||
*/
|
||||
public function duringLastSixMonth()
|
||||
{
|
||||
return $this->state(function (array $attributes) {
|
||||
$loginDate = now()->subMonths(4);
|
||||
$logoutDate = $loginDate->addHours(1);
|
||||
|
||||
return [
|
||||
'login_at' => $loginDate,
|
||||
'logout_at' => $logoutDate,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model has login during last year.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AuthLog>
|
||||
*/
|
||||
public function duringLastYear()
|
||||
{
|
||||
return $this->state(function (array $attributes) {
|
||||
$loginDate = now()->subMonths(10);
|
||||
$logoutDate = $loginDate->addHours(1);
|
||||
|
||||
return [
|
||||
'login_at' => $loginDate,
|
||||
'logout_at' => $logoutDate,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model has login before last year.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AuthLog>
|
||||
*/
|
||||
public function beforeLastYear()
|
||||
{
|
||||
return $this->state(function (array $attributes) {
|
||||
$loginDate = now()->subYears(2);
|
||||
$logoutDate = $loginDate->addHours(1);
|
||||
|
||||
return [
|
||||
'login_at' => $loginDate,
|
||||
'logout_at' => $logoutDate,
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
* Copyright (c) 2024 Bubka
|
||||
* Copyright (c) 2024 Anthony Rappa
|
||||
* Copyright (c) 2017 Yaakov Dahan
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
* associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
* including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
* portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('auth_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->morphs('authenticatable');
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->timestamp('login_at')->nullable();
|
||||
$table->boolean('login_successful')->default(false);
|
||||
$table->timestamp('logout_at')->nullable();
|
||||
$table->boolean('cleared_by_user')->default(false);
|
||||
$table->string('guard', 40)->nullable();
|
||||
$table->string('login_method', 40)->nullable();
|
||||
|
||||
$table->foreign('authenticatable_id')->references('id')->on('users')->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::whenTableHasColumn('auth_logs', 'authenticatable_id', function (Blueprint $table) {
|
||||
// cannot drop foreign keys in SQLite:
|
||||
if (DB::getDriverName() !== 'sqlite') {
|
||||
$table->dropForeign(['authenticatable_id']);
|
||||
}
|
||||
});
|
||||
|
||||
Schema::dropIfExists('auth_logs');
|
||||
}
|
||||
};
|
|
@ -1,30 +0,0 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create(config('authentication-log.table_name'), function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->morphs('authenticatable');
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->timestamp('login_at')->nullable();
|
||||
$table->boolean('login_successful')->default(false);
|
||||
$table->timestamp('logout_at')->nullable();
|
||||
$table->boolean('cleared_by_user')->default(false);
|
||||
$table->json('location')->nullable();
|
||||
$table->string('guard', 40)->nullable();
|
||||
$table->string('login_method', 40)->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists(config('authentication-log.table_name'));
|
||||
}
|
||||
};
|
|
@ -13,6 +13,10 @@ services:
|
|||
# You can leave this on "local". If you change it to production most console commands will ask for extra confirmation.
|
||||
# Never set it to "testing".
|
||||
- APP_ENV=local
|
||||
# The timezone for your application, which is used to record dates and times to database. This global setting can be
|
||||
# overridden by users via in-app settings for a personalised dates and times display.
|
||||
# If this setting is changed while the application is already running, existing records in the database won't be updated
|
||||
- APP_TIMEZONE=UTC
|
||||
# Set to true if you want to see debug information in error screens.
|
||||
- APP_DEBUG=false
|
||||
# This should be your email address
|
||||
|
@ -83,6 +87,9 @@ services:
|
|||
# authentication checks. That means your proxy is fully responsible of the authentication process, 2FAuth will
|
||||
# trust him as long as headers are presents.
|
||||
- AUTHENTICATION_GUARD=web-guard
|
||||
# Authentication log retention time, in days.
|
||||
# Log entries older than that are automatically deleted.
|
||||
- AUTHENTICATION_LOG_RETENTION=365
|
||||
# Name of the HTTP headers sent by the reverse proxy that identifies the authenticated user at proxy level.
|
||||
# Check your proxy documentation to find out how these headers are named (i.e 'REMOTE_USER', 'REMOTE_EMAIL', etc...)
|
||||
# (only relevant when AUTHENTICATION_GUARD is set to 'reverse-proxy-guard')
|
||||
|
|
|
@ -11,6 +11,4 @@ parameters:
|
|||
analyse:
|
||||
- app/Protobuf/*
|
||||
ignoreErrors:
|
||||
-
|
||||
message: '#.*geoip.*#'
|
||||
checkMissingIterableValueType: false
|
|
@ -7,6 +7,7 @@
|
|||
import { UseColorMode } from '@vueuse/components'
|
||||
|
||||
const notify = useNotifyStore()
|
||||
const $2fauth = inject('2fauth')
|
||||
|
||||
const props = defineProps({
|
||||
userId: [Number, String],
|
||||
|
@ -171,6 +172,7 @@
|
|||
<div>
|
||||
<span v-if="isFailedEntry(authentication)" v-html="$t('admin.failed_login_on', { login_at: authentication.login_at })" />
|
||||
<span v-else-if="isSuccessfulLogout(authentication)" v-html="$t('admin.successful_logout_on', { login_at: authentication.logout_at })" />
|
||||
<span v-else-if="$2fauth.config.proxyAuth" v-html="$t('admin.viewed_on', { login_at: authentication.login_at })" />
|
||||
<span v-else v-html="$t('admin.successful_login_on', { login_at: authentication.login_at })" />
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
const router = useRouter()
|
||||
const user = useUserStore()
|
||||
const bus = useBusStore()
|
||||
const $2fauth = inject('2fauth')
|
||||
|
||||
const isFetching = ref(false)
|
||||
const managedUser = ref(null)
|
||||
|
@ -212,12 +213,14 @@
|
|||
<div class="block is-size-6 is-size-7-mobile has-text-grey">
|
||||
{{ $t('admin.registered_on_date', { date: managedUser.info.created_at }) }} - {{ $t('admin.last_seen_on_date', { date: managedUser.info.last_seen_at }) }}
|
||||
</div>
|
||||
<!-- isAdmin option -->
|
||||
<div class="block">
|
||||
<!-- otp as dot -->
|
||||
<FormCheckbox v-model="managedUser.info.is_admin" @update:model-value="val => saveAdminRole(val === true)" fieldName="is_admin" label="admin.forms.is_admin.label" help="admin.forms.is_admin.help" />
|
||||
</div>
|
||||
<h2 class="title is-4 has-text-grey-light">{{ $t('admin.access') }}</h2>
|
||||
<div class="block">
|
||||
<h2 v-if="!$2fauth.config.proxyAuth" class="title is-4 has-text-grey-light">{{ $t('admin.access') }}</h2>
|
||||
<!-- access -->
|
||||
<div v-if="!$2fauth.config.proxyAuth" class="block">
|
||||
<!-- reset password -->
|
||||
<div class="list-item is-size-6 is-size-6-mobile has-text-grey">
|
||||
<div class="mb-3 is-flex is-justify-content-space-between">
|
||||
<div>
|
||||
|
@ -245,6 +248,7 @@
|
|||
<span v-html="$t('admin.reset_password_help')" class="is-block block"></span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- personal access tokens -->
|
||||
<div class="list-item is-size-6 is-size-6-mobile has-text-grey is-flex is-justify-content-space-between">
|
||||
<div>
|
||||
<span class="has-text-weight-bold">{{ $t('settings.personal_access_tokens') }}</span>
|
||||
|
@ -263,6 +267,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- webauthn devices -->
|
||||
<div class="list-item is-size-6 is-size-6-mobile has-text-grey is-flex is-justify-content-space-between">
|
||||
<div>
|
||||
<span class="has-text-weight-bold">{{ $t('auth.webauthn.security_devices') }}</span>
|
||||
|
@ -282,6 +287,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- last access -->
|
||||
<div class="block">
|
||||
<h3 class="title is-5 has-text-grey-light mb-2">{{ $t('admin.last_accesses') }}</h3>
|
||||
<AccessLogViewer :userId="props.userId" :lastOnly="true" @has-more-entries="showFullLogLink = true"/>
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import { useUserStore } from '@/stores/user'
|
||||
import { useGroups } from '@/stores/groups'
|
||||
import { useNotifyStore } from '@/stores/notify'
|
||||
import { timezones } from './timezones'
|
||||
|
||||
const $2fauth = inject('2fauth')
|
||||
const user = useUserStore()
|
||||
|
@ -123,6 +124,8 @@
|
|||
<FontAwesomeIcon :icon="['fas', 'external-link-alt']" />
|
||||
</a>
|
||||
</div>
|
||||
<!-- timezone -->
|
||||
<FormSelect v-model="user.preferences.timezone" @update:model-value="val => savePreference('timezone', val)" :options="timezones" fieldName="timezone" label="settings.forms.timezone.label" help="settings.forms.timezone.help" />
|
||||
<!-- display mode -->
|
||||
<FormToggle v-model="user.preferences.displayMode" @update:model-value="val => savePreference('displayMode', val)" :choices="layouts" fieldName="displayMode" label="settings.forms.display_mode.label" help="settings.forms.display_mode.help"/>
|
||||
<!-- theme -->
|
||||
|
|
|
@ -0,0 +1,537 @@
|
|||
export let timezones = [
|
||||
{ text: 'Africa/Abidjan', value: 'Africa/Abidjan' },
|
||||
{ text: 'Africa/Accra', value: 'Africa/Accra' },
|
||||
{ text: 'Africa/Addis_Ababa', value: 'Africa/Addis_Ababa' },
|
||||
{ text: 'Africa/Algiers', value: 'Africa/Algiers' },
|
||||
{ text: 'Africa/Asmara', value: 'Africa/Asmara' },
|
||||
{ text: 'Africa/Asmera', value: 'Africa/Asmera' },
|
||||
{ text: 'Africa/Bamako', value: 'Africa/Bamako' },
|
||||
{ text: 'Africa/Bangui', value: 'Africa/Bangui' },
|
||||
{ text: 'Africa/Banjul', value: 'Africa/Banjul' },
|
||||
{ text: 'Africa/Bissau', value: 'Africa/Bissau' },
|
||||
{ text: 'Africa/Blantyre', value: 'Africa/Blantyre' },
|
||||
{ text: 'Africa/Brazzaville', value: 'Africa/Brazzaville' },
|
||||
{ text: 'Africa/Bujumbura', value: 'Africa/Bujumbura' },
|
||||
{ text: 'Africa/Cairo', value: 'Africa/Cairo' },
|
||||
{ text: 'Africa/Casablanca', value: 'Africa/Casablanca' },
|
||||
{ text: 'Africa/Ceuta', value: 'Africa/Ceuta' },
|
||||
{ text: 'Africa/Conakry', value: 'Africa/Conakry' },
|
||||
{ text: 'Africa/Dakar', value: 'Africa/Dakar' },
|
||||
{ text: 'Africa/Dar_es_Salaam', value: 'Africa/Dar_es_Salaam' },
|
||||
{ text: 'Africa/Djibouti', value: 'Africa/Djibouti' },
|
||||
{ text: 'Africa/Douala', value: 'Africa/Douala' },
|
||||
{ text: 'Africa/El_Aaiun', value: 'Africa/El_Aaiun' },
|
||||
{ text: 'Africa/Freetown', value: 'Africa/Freetown' },
|
||||
{ text: 'Africa/Gaborone', value: 'Africa/Gaborone' },
|
||||
{ text: 'Africa/Harare', value: 'Africa/Harare' },
|
||||
{ text: 'Africa/Johannesburg', value: 'Africa/Johannesburg' },
|
||||
{ text: 'Africa/Juba', value: 'Africa/Juba' },
|
||||
{ text: 'Africa/Kampala', value: 'Africa/Kampala' },
|
||||
{ text: 'Africa/Khartoum', value: 'Africa/Khartoum' },
|
||||
{ text: 'Africa/Kigali', value: 'Africa/Kigali' },
|
||||
{ text: 'Africa/Kinshasa', value: 'Africa/Kinshasa' },
|
||||
{ text: 'Africa/Lagos', value: 'Africa/Lagos' },
|
||||
{ text: 'Africa/Libreville', value: 'Africa/Libreville' },
|
||||
{ text: 'Africa/Lome', value: 'Africa/Lome' },
|
||||
{ text: 'Africa/Luanda', value: 'Africa/Luanda' },
|
||||
{ text: 'Africa/Lubumbashi', value: 'Africa/Lubumbashi' },
|
||||
{ text: 'Africa/Lusaka', value: 'Africa/Lusaka' },
|
||||
{ text: 'Africa/Malabo', value: 'Africa/Malabo' },
|
||||
{ text: 'Africa/Maputo', value: 'Africa/Maputo' },
|
||||
{ text: 'Africa/Maseru', value: 'Africa/Maseru' },
|
||||
{ text: 'Africa/Mbabane', value: 'Africa/Mbabane' },
|
||||
{ text: 'Africa/Mogadishu', value: 'Africa/Mogadishu' },
|
||||
{ text: 'Africa/Monrovia', value: 'Africa/Monrovia' },
|
||||
{ text: 'Africa/Nairobi', value: 'Africa/Nairobi' },
|
||||
{ text: 'Africa/Ndjamena', value: 'Africa/Ndjamena' },
|
||||
{ text: 'Africa/Niamey', value: 'Africa/Niamey' },
|
||||
{ text: 'Africa/Nouakchott', value: 'Africa/Nouakchott' },
|
||||
{ text: 'Africa/Ouagadougou', value: 'Africa/Ouagadougou' },
|
||||
{ text: 'Africa/Porto-Novo', value: 'Africa/Porto-Novo' },
|
||||
{ text: 'Africa/Sao_Tome', value: 'Africa/Sao_Tome' },
|
||||
{ text: 'Africa/Timbuktu', value: 'Africa/Timbuktu' },
|
||||
{ text: 'Africa/Tripoli', value: 'Africa/Tripoli' },
|
||||
{ text: 'Africa/Tunis', value: 'Africa/Tunis' },
|
||||
{ text: 'Africa/Windhoek', value: 'Africa/Windhoek' },
|
||||
{ text: 'America/Adak', value: 'America/Adak' },
|
||||
{ text: 'America/Anchorage', value: 'America/Anchorage' },
|
||||
{ text: 'America/Anguilla', value: 'America/Anguilla' },
|
||||
{ text: 'America/Antigua', value: 'America/Antigua' },
|
||||
{ text: 'America/Araguaina', value: 'America/Araguaina' },
|
||||
{ text: 'America/Argentina/Buenos_Aires', value: 'America/Argentina/Buenos_Aires' },
|
||||
{ text: 'America/Argentina/Catamarca', value: 'America/Argentina/Catamarca' },
|
||||
{ text: 'America/Argentina/ComodRivadavia', value: 'America/Argentina/ComodRivadavia' },
|
||||
{ text: 'America/Argentina/Cordoba', value: 'America/Argentina/Cordoba' },
|
||||
{ text: 'America/Argentina/Jujuy', value: 'America/Argentina/Jujuy' },
|
||||
{ text: 'America/Argentina/La_Rioja', value: 'America/Argentina/La_Rioja' },
|
||||
{ text: 'America/Argentina/Mendoza', value: 'America/Argentina/Mendoza' },
|
||||
{ text: 'America/Argentina/Rio_Gallegos', value: 'America/Argentina/Rio_Gallegos' },
|
||||
{ text: 'America/Argentina/Salta', value: 'America/Argentina/Salta' },
|
||||
{ text: 'America/Argentina/San_Juan', value: 'America/Argentina/San_Juan' },
|
||||
{ text: 'America/Argentina/San_Luis', value: 'America/Argentina/San_Luis' },
|
||||
{ text: 'America/Argentina/Tucuman', value: 'America/Argentina/Tucuman' },
|
||||
{ text: 'America/Argentina/Ushuaia', value: 'America/Argentina/Ushuaia' },
|
||||
{ text: 'America/Aruba', value: 'America/Aruba' },
|
||||
{ text: 'America/Asuncion', value: 'America/Asuncion' },
|
||||
{ text: 'America/Atikokan', value: 'America/Atikokan' },
|
||||
{ text: 'America/Atka', value: 'America/Atka' },
|
||||
{ text: 'America/Bahia', value: 'America/Bahia' },
|
||||
{ text: 'America/Bahia_Banderas', value: 'America/Bahia_Banderas' },
|
||||
{ text: 'America/Barbados', value: 'America/Barbados' },
|
||||
{ text: 'America/Belem', value: 'America/Belem' },
|
||||
{ text: 'America/Belize', value: 'America/Belize' },
|
||||
{ text: 'America/Blanc-Sablon', value: 'America/Blanc-Sablon' },
|
||||
{ text: 'America/Boa_Vista', value: 'America/Boa_Vista' },
|
||||
{ text: 'America/Bogota', value: 'America/Bogota' },
|
||||
{ text: 'America/Boise', value: 'America/Boise' },
|
||||
{ text: 'America/Buenos_Aires', value: 'America/Buenos_Aires' },
|
||||
{ text: 'America/Cambridge_Bay', value: 'America/Cambridge_Bay' },
|
||||
{ text: 'America/Campo_Grande', value: 'America/Campo_Grande' },
|
||||
{ text: 'America/Cancun', value: 'America/Cancun' },
|
||||
{ text: 'America/Caracas', value: 'America/Caracas' },
|
||||
{ text: 'America/Catamarca', value: 'America/Catamarca' },
|
||||
{ text: 'America/Cayenne', value: 'America/Cayenne' },
|
||||
{ text: 'America/Cayman', value: 'America/Cayman' },
|
||||
{ text: 'America/Chicago', value: 'America/Chicago' },
|
||||
{ text: 'America/Chihuahua', value: 'America/Chihuahua' },
|
||||
{ text: 'America/Ciudad_Juarez', value: 'America/Ciudad_Juarez' },
|
||||
{ text: 'America/Coral_Harbour', value: 'America/Coral_Harbour' },
|
||||
{ text: 'America/Cordoba', value: 'America/Cordoba' },
|
||||
{ text: 'America/Costa_Rica', value: 'America/Costa_Rica' },
|
||||
{ text: 'America/Creston', value: 'America/Creston' },
|
||||
{ text: 'America/Cuiaba', value: 'America/Cuiaba' },
|
||||
{ text: 'America/Curacao', value: 'America/Curacao' },
|
||||
{ text: 'America/Danmarkshavn', value: 'America/Danmarkshavn' },
|
||||
{ text: 'America/Dawson', value: 'America/Dawson' },
|
||||
{ text: 'America/Dawson_Creek', value: 'America/Dawson_Creek' },
|
||||
{ text: 'America/Denver', value: 'America/Denver' },
|
||||
{ text: 'America/Detroit', value: 'America/Detroit' },
|
||||
{ text: 'America/Dominica', value: 'America/Dominica' },
|
||||
{ text: 'America/Edmonton', value: 'America/Edmonton' },
|
||||
{ text: 'America/Eirunepe', value: 'America/Eirunepe' },
|
||||
{ text: 'America/El_Salvador', value: 'America/El_Salvador' },
|
||||
{ text: 'America/Ensenada', value: 'America/Ensenada' },
|
||||
{ text: 'America/Fort_Nelson', value: 'America/Fort_Nelson' },
|
||||
{ text: 'America/Fort_Wayne', value: 'America/Fort_Wayne' },
|
||||
{ text: 'America/Fortaleza', value: 'America/Fortaleza' },
|
||||
{ text: 'America/Glace_Bay', value: 'America/Glace_Bay' },
|
||||
{ text: 'America/Godthab', value: 'America/Godthab' },
|
||||
{ text: 'America/Goose_Bay', value: 'America/Goose_Bay' },
|
||||
{ text: 'America/Grand_Turk', value: 'America/Grand_Turk' },
|
||||
{ text: 'America/Grenada', value: 'America/Grenada' },
|
||||
{ text: 'America/Guadeloupe', value: 'America/Guadeloupe' },
|
||||
{ text: 'America/Guatemala', value: 'America/Guatemala' },
|
||||
{ text: 'America/Guayaquil', value: 'America/Guayaquil' },
|
||||
{ text: 'America/Guyana', value: 'America/Guyana' },
|
||||
{ text: 'America/Halifax', value: 'America/Halifax' },
|
||||
{ text: 'America/Havana', value: 'America/Havana' },
|
||||
{ text: 'America/Hermosillo', value: 'America/Hermosillo' },
|
||||
{ text: 'America/Indiana/Indianapolis', value: 'America/Indiana/Indianapolis' },
|
||||
{ text: 'America/Indiana/Knox', value: 'America/Indiana/Knox' },
|
||||
{ text: 'America/Indiana/Marengo', value: 'America/Indiana/Marengo' },
|
||||
{ text: 'America/Indiana/Petersburg', value: 'America/Indiana/Petersburg' },
|
||||
{ text: 'America/Indiana/Tell_City', value: 'America/Indiana/Tell_City' },
|
||||
{ text: 'America/Indiana/Vevay', value: 'America/Indiana/Vevay' },
|
||||
{ text: 'America/Indiana/Vincennes', value: 'America/Indiana/Vincennes' },
|
||||
{ text: 'America/Indiana/Winamac', value: 'America/Indiana/Winamac' },
|
||||
{ text: 'America/Indianapolis', value: 'America/Indianapolis' },
|
||||
{ text: 'America/Inuvik', value: 'America/Inuvik' },
|
||||
{ text: 'America/Iqaluit', value: 'America/Iqaluit' },
|
||||
{ text: 'America/Jamaica', value: 'America/Jamaica' },
|
||||
{ text: 'America/Jujuy', value: 'America/Jujuy' },
|
||||
{ text: 'America/Juneau', value: 'America/Juneau' },
|
||||
{ text: 'America/Kentucky/Louisville', value: 'America/Kentucky/Louisville' },
|
||||
{ text: 'America/Kentucky/Monticello', value: 'America/Kentucky/Monticello' },
|
||||
{ text: 'America/Knox_IN', value: 'America/Knox_IN' },
|
||||
{ text: 'America/Kralendijk', value: 'America/Kralendijk' },
|
||||
{ text: 'America/La_Paz', value: 'America/La_Paz' },
|
||||
{ text: 'America/Lima', value: 'America/Lima' },
|
||||
{ text: 'America/Los_Angeles', value: 'America/Los_Angeles' },
|
||||
{ text: 'America/Louisville', value: 'America/Louisville' },
|
||||
{ text: 'America/Lower_Princes', value: 'America/Lower_Princes' },
|
||||
{ text: 'America/Maceio', value: 'America/Maceio' },
|
||||
{ text: 'America/Managua', value: 'America/Managua' },
|
||||
{ text: 'America/Manaus', value: 'America/Manaus' },
|
||||
{ text: 'America/Marigot', value: 'America/Marigot' },
|
||||
{ text: 'America/Martinique', value: 'America/Martinique' },
|
||||
{ text: 'America/Matamoros', value: 'America/Matamoros' },
|
||||
{ text: 'America/Mazatlan', value: 'America/Mazatlan' },
|
||||
{ text: 'America/Mendoza', value: 'America/Mendoza' },
|
||||
{ text: 'America/Menominee', value: 'America/Menominee' },
|
||||
{ text: 'America/Merida', value: 'America/Merida' },
|
||||
{ text: 'America/Metlakatla', value: 'America/Metlakatla' },
|
||||
{ text: 'America/Mexico_City', value: 'America/Mexico_City' },
|
||||
{ text: 'America/Miquelon', value: 'America/Miquelon' },
|
||||
{ text: 'America/Moncton', value: 'America/Moncton' },
|
||||
{ text: 'America/Monterrey', value: 'America/Monterrey' },
|
||||
{ text: 'America/Montevideo', value: 'America/Montevideo' },
|
||||
{ text: 'America/Montreal', value: 'America/Montreal' },
|
||||
{ text: 'America/Montserrat', value: 'America/Montserrat' },
|
||||
{ text: 'America/Nassau', value: 'America/Nassau' },
|
||||
{ text: 'America/New_York', value: 'America/New_York' },
|
||||
{ text: 'America/Nipigon', value: 'America/Nipigon' },
|
||||
{ text: 'America/Nome', value: 'America/Nome' },
|
||||
{ text: 'America/Noronha', value: 'America/Noronha' },
|
||||
{ text: 'America/North_Dakota/Beulah', value: 'America/North_Dakota/Beulah' },
|
||||
{ text: 'America/North_Dakota/Center', value: 'America/North_Dakota/Center' },
|
||||
{ text: 'America/North_Dakota/New_Salem', value: 'America/North_Dakota/New_Salem' },
|
||||
{ text: 'America/Nuuk', value: 'America/Nuuk' },
|
||||
{ text: 'America/Ojinaga', value: 'America/Ojinaga' },
|
||||
{ text: 'America/Panama', value: 'America/Panama' },
|
||||
{ text: 'America/Pangnirtung', value: 'America/Pangnirtung' },
|
||||
{ text: 'America/Paramaribo', value: 'America/Paramaribo' },
|
||||
{ text: 'America/Phoenix', value: 'America/Phoenix' },
|
||||
{ text: 'America/Port-au-Prince', value: 'America/Port-au-Prince' },
|
||||
{ text: 'America/Port_of_Spain', value: 'America/Port_of_Spain' },
|
||||
{ text: 'America/Porto_Acre', value: 'America/Porto_Acre' },
|
||||
{ text: 'America/Porto_Velho', value: 'America/Porto_Velho' },
|
||||
{ text: 'America/Puerto_Rico', value: 'America/Puerto_Rico' },
|
||||
{ text: 'America/Punta_Arenas', value: 'America/Punta_Arenas' },
|
||||
{ text: 'America/Rainy_River', value: 'America/Rainy_River' },
|
||||
{ text: 'America/Rankin_Inlet', value: 'America/Rankin_Inlet' },
|
||||
{ text: 'America/Recife', value: 'America/Recife' },
|
||||
{ text: 'America/Regina', value: 'America/Regina' },
|
||||
{ text: 'America/Resolute', value: 'America/Resolute' },
|
||||
{ text: 'America/Rio_Branco', value: 'America/Rio_Branco' },
|
||||
{ text: 'America/Rosario', value: 'America/Rosario' },
|
||||
{ text: 'America/Santa_Isabel', value: 'America/Santa_Isabel' },
|
||||
{ text: 'America/Santarem', value: 'America/Santarem' },
|
||||
{ text: 'America/Santiago', value: 'America/Santiago' },
|
||||
{ text: 'America/Santo_Domingo', value: 'America/Santo_Domingo' },
|
||||
{ text: 'America/Sao_Paulo', value: 'America/Sao_Paulo' },
|
||||
{ text: 'America/Scoresbysund', value: 'America/Scoresbysund' },
|
||||
{ text: 'America/Shiprock', value: 'America/Shiprock' },
|
||||
{ text: 'America/Sitka', value: 'America/Sitka' },
|
||||
{ text: 'America/St_Barthelemy', value: 'America/St_Barthelemy' },
|
||||
{ text: 'America/St_Johns', value: 'America/St_Johns' },
|
||||
{ text: 'America/St_Kitts', value: 'America/St_Kitts' },
|
||||
{ text: 'America/St_Lucia', value: 'America/St_Lucia' },
|
||||
{ text: 'America/St_Thomas', value: 'America/St_Thomas' },
|
||||
{ text: 'America/St_Vincent', value: 'America/St_Vincent' },
|
||||
{ text: 'America/Swift_Current', value: 'America/Swift_Current' },
|
||||
{ text: 'America/Tegucigalpa', value: 'America/Tegucigalpa' },
|
||||
{ text: 'America/Thule', value: 'America/Thule' },
|
||||
{ text: 'America/Thunder_Bay', value: 'America/Thunder_Bay' },
|
||||
{ text: 'America/Tijuana', value: 'America/Tijuana' },
|
||||
{ text: 'America/Toronto', value: 'America/Toronto' },
|
||||
{ text: 'America/Tortola', value: 'America/Tortola' },
|
||||
{ text: 'America/Vancouver', value: 'America/Vancouver' },
|
||||
{ text: 'America/Virgin', value: 'America/Virgin' },
|
||||
{ text: 'America/Whitehorse', value: 'America/Whitehorse' },
|
||||
{ text: 'America/Winnipeg', value: 'America/Winnipeg' },
|
||||
{ text: 'America/Yakutat', value: 'America/Yakutat' },
|
||||
{ text: 'America/Yellowknife', value: 'America/Yellowknife' },
|
||||
{ text: 'Antarctica/Casey', value: 'Antarctica/Casey' },
|
||||
{ text: 'Antarctica/Davis', value: 'Antarctica/Davis' },
|
||||
{ text: 'Antarctica/DumontDUrville', value: 'Antarctica/DumontDUrville' },
|
||||
{ text: 'Antarctica/Macquarie', value: 'Antarctica/Macquarie' },
|
||||
{ text: 'Antarctica/Mawson', value: 'Antarctica/Mawson' },
|
||||
{ text: 'Antarctica/McMurdo', value: 'Antarctica/McMurdo' },
|
||||
{ text: 'Antarctica/Palmer', value: 'Antarctica/Palmer' },
|
||||
{ text: 'Antarctica/Rothera', value: 'Antarctica/Rothera' },
|
||||
{ text: 'Antarctica/South_Pole', value: 'Antarctica/South_Pole' },
|
||||
{ text: 'Antarctica/Syowa', value: 'Antarctica/Syowa' },
|
||||
{ text: 'Antarctica/Troll', value: 'Antarctica/Troll' },
|
||||
{ text: 'Antarctica/Vostok', value: 'Antarctica/Vostok' },
|
||||
{ text: 'Arctic/Longyearbyen', value: 'Arctic/Longyearbyen' },
|
||||
{ text: 'Asia/Aden', value: 'Asia/Aden' },
|
||||
{ text: 'Asia/Almaty', value: 'Asia/Almaty' },
|
||||
{ text: 'Asia/Amman', value: 'Asia/Amman' },
|
||||
{ text: 'Asia/Anadyr', value: 'Asia/Anadyr' },
|
||||
{ text: 'Asia/Aqtau', value: 'Asia/Aqtau' },
|
||||
{ text: 'Asia/Aqtobe', value: 'Asia/Aqtobe' },
|
||||
{ text: 'Asia/Ashgabat', value: 'Asia/Ashgabat' },
|
||||
{ text: 'Asia/Ashkhabad', value: 'Asia/Ashkhabad' },
|
||||
{ text: 'Asia/Atyrau', value: 'Asia/Atyrau' },
|
||||
{ text: 'Asia/Baghdad', value: 'Asia/Baghdad' },
|
||||
{ text: 'Asia/Bahrain', value: 'Asia/Bahrain' },
|
||||
{ text: 'Asia/Baku', value: 'Asia/Baku' },
|
||||
{ text: 'Asia/Bangkok', value: 'Asia/Bangkok' },
|
||||
{ text: 'Asia/Barnaul', value: 'Asia/Barnaul' },
|
||||
{ text: 'Asia/Beirut', value: 'Asia/Beirut' },
|
||||
{ text: 'Asia/Bishkek', value: 'Asia/Bishkek' },
|
||||
{ text: 'Asia/Brunei', value: 'Asia/Brunei' },
|
||||
{ text: 'Asia/Calcutta', value: 'Asia/Calcutta' },
|
||||
{ text: 'Asia/Chita', value: 'Asia/Chita' },
|
||||
{ text: 'Asia/Choibalsan', value: 'Asia/Choibalsan' },
|
||||
{ text: 'Asia/Chongqing', value: 'Asia/Chongqing' },
|
||||
{ text: 'Asia/Chungking', value: 'Asia/Chungking' },
|
||||
{ text: 'Asia/Colombo', value: 'Asia/Colombo' },
|
||||
{ text: 'Asia/Dacca', value: 'Asia/Dacca' },
|
||||
{ text: 'Asia/Damascus', value: 'Asia/Damascus' },
|
||||
{ text: 'Asia/Dhaka', value: 'Asia/Dhaka' },
|
||||
{ text: 'Asia/Dili', value: 'Asia/Dili' },
|
||||
{ text: 'Asia/Dubai', value: 'Asia/Dubai' },
|
||||
{ text: 'Asia/Dushanbe', value: 'Asia/Dushanbe' },
|
||||
{ text: 'Asia/Famagusta', value: 'Asia/Famagusta' },
|
||||
{ text: 'Asia/Gaza', value: 'Asia/Gaza' },
|
||||
{ text: 'Asia/Harbin', value: 'Asia/Harbin' },
|
||||
{ text: 'Asia/Hebron', value: 'Asia/Hebron' },
|
||||
{ text: 'Asia/Ho_Chi_Minh', value: 'Asia/Ho_Chi_Minh' },
|
||||
{ text: 'Asia/Hong_Kong', value: 'Asia/Hong_Kong' },
|
||||
{ text: 'Asia/Hovd', value: 'Asia/Hovd' },
|
||||
{ text: 'Asia/Irkutsk', value: 'Asia/Irkutsk' },
|
||||
{ text: 'Asia/Istanbul', value: 'Asia/Istanbul' },
|
||||
{ text: 'Asia/Jakarta', value: 'Asia/Jakarta' },
|
||||
{ text: 'Asia/Jayapura', value: 'Asia/Jayapura' },
|
||||
{ text: 'Asia/Jerusalem', value: 'Asia/Jerusalem' },
|
||||
{ text: 'Asia/Kabul', value: 'Asia/Kabul' },
|
||||
{ text: 'Asia/Kamchatka', value: 'Asia/Kamchatka' },
|
||||
{ text: 'Asia/Karachi', value: 'Asia/Karachi' },
|
||||
{ text: 'Asia/Kashgar', value: 'Asia/Kashgar' },
|
||||
{ text: 'Asia/Kathmandu', value: 'Asia/Kathmandu' },
|
||||
{ text: 'Asia/Katmandu', value: 'Asia/Katmandu' },
|
||||
{ text: 'Asia/Khandyga', value: 'Asia/Khandyga' },
|
||||
{ text: 'Asia/Kolkata', value: 'Asia/Kolkata' },
|
||||
{ text: 'Asia/Krasnoyarsk', value: 'Asia/Krasnoyarsk' },
|
||||
{ text: 'Asia/Kuala_Lumpur', value: 'Asia/Kuala_Lumpur' },
|
||||
{ text: 'Asia/Kuching', value: 'Asia/Kuching' },
|
||||
{ text: 'Asia/Kuwait', value: 'Asia/Kuwait' },
|
||||
{ text: 'Asia/Macao', value: 'Asia/Macao' },
|
||||
{ text: 'Asia/Macau', value: 'Asia/Macau' },
|
||||
{ text: 'Asia/Magadan', value: 'Asia/Magadan' },
|
||||
{ text: 'Asia/Makassar', value: 'Asia/Makassar' },
|
||||
{ text: 'Asia/Manila', value: 'Asia/Manila' },
|
||||
{ text: 'Asia/Muscat', value: 'Asia/Muscat' },
|
||||
{ text: 'Asia/Nicosia', value: 'Asia/Nicosia' },
|
||||
{ text: 'Asia/Novokuznetsk', value: 'Asia/Novokuznetsk' },
|
||||
{ text: 'Asia/Novosibirsk', value: 'Asia/Novosibirsk' },
|
||||
{ text: 'Asia/Omsk', value: 'Asia/Omsk' },
|
||||
{ text: 'Asia/Oral', value: 'Asia/Oral' },
|
||||
{ text: 'Asia/Phnom_Penh', value: 'Asia/Phnom_Penh' },
|
||||
{ text: 'Asia/Pontianak', value: 'Asia/Pontianak' },
|
||||
{ text: 'Asia/Pyongyang', value: 'Asia/Pyongyang' },
|
||||
{ text: 'Asia/Qatar', value: 'Asia/Qatar' },
|
||||
{ text: 'Asia/Qostanay', value: 'Asia/Qostanay' },
|
||||
{ text: 'Asia/Qyzylorda', value: 'Asia/Qyzylorda' },
|
||||
{ text: 'Asia/Rangoon', value: 'Asia/Rangoon' },
|
||||
{ text: 'Asia/Riyadh', value: 'Asia/Riyadh' },
|
||||
{ text: 'Asia/Saigon', value: 'Asia/Saigon' },
|
||||
{ text: 'Asia/Sakhalin', value: 'Asia/Sakhalin' },
|
||||
{ text: 'Asia/Samarkand', value: 'Asia/Samarkand' },
|
||||
{ text: 'Asia/Seoul', value: 'Asia/Seoul' },
|
||||
{ text: 'Asia/Shanghai', value: 'Asia/Shanghai' },
|
||||
{ text: 'Asia/Singapore', value: 'Asia/Singapore' },
|
||||
{ text: 'Asia/Srednekolymsk', value: 'Asia/Srednekolymsk' },
|
||||
{ text: 'Asia/Taipei', value: 'Asia/Taipei' },
|
||||
{ text: 'Asia/Tashkent', value: 'Asia/Tashkent' },
|
||||
{ text: 'Asia/Tbilisi', value: 'Asia/Tbilisi' },
|
||||
{ text: 'Asia/Tehran', value: 'Asia/Tehran' },
|
||||
{ text: 'Asia/Tel_Aviv', value: 'Asia/Tel_Aviv' },
|
||||
{ text: 'Asia/Thimbu', value: 'Asia/Thimbu' },
|
||||
{ text: 'Asia/Thimphu', value: 'Asia/Thimphu' },
|
||||
{ text: 'Asia/Tokyo', value: 'Asia/Tokyo' },
|
||||
{ text: 'Asia/Tomsk', value: 'Asia/Tomsk' },
|
||||
{ text: 'Asia/Ujung_Pandang', value: 'Asia/Ujung_Pandang' },
|
||||
{ text: 'Asia/Ulaanbaatar', value: 'Asia/Ulaanbaatar' },
|
||||
{ text: 'Asia/Ulan_Bator', value: 'Asia/Ulan_Bator' },
|
||||
{ text: 'Asia/Urumqi', value: 'Asia/Urumqi' },
|
||||
{ text: 'Asia/Ust-Nera', value: 'Asia/Ust-Nera' },
|
||||
{ text: 'Asia/Vientiane', value: 'Asia/Vientiane' },
|
||||
{ text: 'Asia/Vladivostok', value: 'Asia/Vladivostok' },
|
||||
{ text: 'Asia/Yakutsk', value: 'Asia/Yakutsk' },
|
||||
{ text: 'Asia/Yangon', value: 'Asia/Yangon' },
|
||||
{ text: 'Asia/Yekaterinburg', value: 'Asia/Yekaterinburg' },
|
||||
{ text: 'Asia/Yerevan', value: 'Asia/Yerevan' },
|
||||
{ text: 'Atlantic/Azores', value: 'Atlantic/Azores' },
|
||||
{ text: 'Atlantic/Bermuda', value: 'Atlantic/Bermuda' },
|
||||
{ text: 'Atlantic/Canary', value: 'Atlantic/Canary' },
|
||||
{ text: 'Atlantic/Cape_Verde', value: 'Atlantic/Cape_Verde' },
|
||||
{ text: 'Atlantic/Faeroe', value: 'Atlantic/Faeroe' },
|
||||
{ text: 'Atlantic/Faroe', value: 'Atlantic/Faroe' },
|
||||
{ text: 'Atlantic/Jan_Mayen', value: 'Atlantic/Jan_Mayen' },
|
||||
{ text: 'Atlantic/Madeira', value: 'Atlantic/Madeira' },
|
||||
{ text: 'Atlantic/Reykjavik', value: 'Atlantic/Reykjavik' },
|
||||
{ text: 'Atlantic/South_Georgia', value: 'Atlantic/South_Georgia' },
|
||||
{ text: 'Atlantic/St_Helena', value: 'Atlantic/St_Helena' },
|
||||
{ text: 'Atlantic/Stanley', value: 'Atlantic/Stanley' },
|
||||
{ text: 'Australia/ACT', value: 'Australia/ACT' },
|
||||
{ text: 'Australia/Adelaide', value: 'Australia/Adelaide' },
|
||||
{ text: 'Australia/Brisbane', value: 'Australia/Brisbane' },
|
||||
{ text: 'Australia/Broken_Hill', value: 'Australia/Broken_Hill' },
|
||||
{ text: 'Australia/Canberra', value: 'Australia/Canberra' },
|
||||
{ text: 'Australia/Currie', value: 'Australia/Currie' },
|
||||
{ text: 'Australia/Darwin', value: 'Australia/Darwin' },
|
||||
{ text: 'Australia/Eucla', value: 'Australia/Eucla' },
|
||||
{ text: 'Australia/Hobart', value: 'Australia/Hobart' },
|
||||
{ text: 'Australia/LHI', value: 'Australia/LHI' },
|
||||
{ text: 'Australia/Lindeman', value: 'Australia/Lindeman' },
|
||||
{ text: 'Australia/Lord_Howe', value: 'Australia/Lord_Howe' },
|
||||
{ text: 'Australia/Melbourne', value: 'Australia/Melbourne' },
|
||||
{ text: 'Australia/NSW', value: 'Australia/NSW' },
|
||||
{ text: 'Australia/North', value: 'Australia/North' },
|
||||
{ text: 'Australia/Perth', value: 'Australia/Perth' },
|
||||
{ text: 'Australia/Queensland', value: 'Australia/Queensland' },
|
||||
{ text: 'Australia/South', value: 'Australia/South' },
|
||||
{ text: 'Australia/Sydney', value: 'Australia/Sydney' },
|
||||
{ text: 'Australia/Tasmania', value: 'Australia/Tasmania' },
|
||||
{ text: 'Australia/Victoria', value: 'Australia/Victoria' },
|
||||
{ text: 'Australia/West', value: 'Australia/West' },
|
||||
{ text: 'Australia/Yancowinna', value: 'Australia/Yancowinna' },
|
||||
{ text: 'Brazil/Acre', value: 'Brazil/Acre' },
|
||||
{ text: 'Brazil/DeNoronha', value: 'Brazil/DeNoronha' },
|
||||
{ text: 'Brazil/East', value: 'Brazil/East' },
|
||||
{ text: 'Brazil/West', value: 'Brazil/West' },
|
||||
{ text: 'Canada/Atlantic', value: 'Canada/Atlantic' },
|
||||
{ text: 'Canada/Central', value: 'Canada/Central' },
|
||||
{ text: 'Canada/Eastern', value: 'Canada/Eastern' },
|
||||
{ text: 'Canada/Mountain', value: 'Canada/Mountain' },
|
||||
{ text: 'Canada/Newfoundland', value: 'Canada/Newfoundland' },
|
||||
{ text: 'Canada/Pacific', value: 'Canada/Pacific' },
|
||||
{ text: 'Canada/Saskatchewan', value: 'Canada/Saskatchewan' },
|
||||
{ text: 'Canada/Yukon', value: 'Canada/Yukon' },
|
||||
{ text: 'Chile/Continental', value: 'Chile/Continental' },
|
||||
{ text: 'Chile/EasterIsland', value: 'Chile/EasterIsland' },
|
||||
{ text: 'Cuba', value: 'Cuba' },
|
||||
{ text: 'Egypt', value: 'Egypt' },
|
||||
{ text: 'Eire', value: 'Eire' },
|
||||
{ text: 'Europe/Amsterdam', value: 'Europe/Amsterdam' },
|
||||
{ text: 'Europe/Andorra', value: 'Europe/Andorra' },
|
||||
{ text: 'Europe/Astrakhan', value: 'Europe/Astrakhan' },
|
||||
{ text: 'Europe/Athens', value: 'Europe/Athens' },
|
||||
{ text: 'Europe/Belfast', value: 'Europe/Belfast' },
|
||||
{ text: 'Europe/Belgrade', value: 'Europe/Belgrade' },
|
||||
{ text: 'Europe/Berlin', value: 'Europe/Berlin' },
|
||||
{ text: 'Europe/Bratislava', value: 'Europe/Bratislava' },
|
||||
{ text: 'Europe/Brussels', value: 'Europe/Brussels' },
|
||||
{ text: 'Europe/Bucharest', value: 'Europe/Bucharest' },
|
||||
{ text: 'Europe/Budapest', value: 'Europe/Budapest' },
|
||||
{ text: 'Europe/Busingen', value: 'Europe/Busingen' },
|
||||
{ text: 'Europe/Chisinau', value: 'Europe/Chisinau' },
|
||||
{ text: 'Europe/Copenhagen', value: 'Europe/Copenhagen' },
|
||||
{ text: 'Europe/Dublin', value: 'Europe/Dublin' },
|
||||
{ text: 'Europe/Gibraltar', value: 'Europe/Gibraltar' },
|
||||
{ text: 'Europe/Guernsey', value: 'Europe/Guernsey' },
|
||||
{ text: 'Europe/Helsinki', value: 'Europe/Helsinki' },
|
||||
{ text: 'Europe/Isle_of_Man', value: 'Europe/Isle_of_Man' },
|
||||
{ text: 'Europe/Istanbul', value: 'Europe/Istanbul' },
|
||||
{ text: 'Europe/Jersey', value: 'Europe/Jersey' },
|
||||
{ text: 'Europe/Kaliningrad', value: 'Europe/Kaliningrad' },
|
||||
{ text: 'Europe/Kiev', value: 'Europe/Kiev' },
|
||||
{ text: 'Europe/Kirov', value: 'Europe/Kirov' },
|
||||
{ text: 'Europe/Kyiv', value: 'Europe/Kyiv' },
|
||||
{ text: 'Europe/Lisbon', value: 'Europe/Lisbon' },
|
||||
{ text: 'Europe/Ljubljana', value: 'Europe/Ljubljana' },
|
||||
{ text: 'Europe/London', value: 'Europe/London' },
|
||||
{ text: 'Europe/Luxembourg', value: 'Europe/Luxembourg' },
|
||||
{ text: 'Europe/Madrid', value: 'Europe/Madrid' },
|
||||
{ text: 'Europe/Malta', value: 'Europe/Malta' },
|
||||
{ text: 'Europe/Mariehamn', value: 'Europe/Mariehamn' },
|
||||
{ text: 'Europe/Minsk', value: 'Europe/Minsk' },
|
||||
{ text: 'Europe/Monaco', value: 'Europe/Monaco' },
|
||||
{ text: 'Europe/Moscow', value: 'Europe/Moscow' },
|
||||
{ text: 'Europe/Nicosia', value: 'Europe/Nicosia' },
|
||||
{ text: 'Europe/Oslo', value: 'Europe/Oslo' },
|
||||
{ text: 'Europe/Paris', value: 'Europe/Paris' },
|
||||
{ text: 'Europe/Podgorica', value: 'Europe/Podgorica' },
|
||||
{ text: 'Europe/Prague', value: 'Europe/Prague' },
|
||||
{ text: 'Europe/Riga', value: 'Europe/Riga' },
|
||||
{ text: 'Europe/Rome', value: 'Europe/Rome' },
|
||||
{ text: 'Europe/Samara', value: 'Europe/Samara' },
|
||||
{ text: 'Europe/San_Marino', value: 'Europe/San_Marino' },
|
||||
{ text: 'Europe/Sarajevo', value: 'Europe/Sarajevo' },
|
||||
{ text: 'Europe/Saratov', value: 'Europe/Saratov' },
|
||||
{ text: 'Europe/Simferopol', value: 'Europe/Simferopol' },
|
||||
{ text: 'Europe/Skopje', value: 'Europe/Skopje' },
|
||||
{ text: 'Europe/Sofia', value: 'Europe/Sofia' },
|
||||
{ text: 'Europe/Stockholm', value: 'Europe/Stockholm' },
|
||||
{ text: 'Europe/Tallinn', value: 'Europe/Tallinn' },
|
||||
{ text: 'Europe/Tirane', value: 'Europe/Tirane' },
|
||||
{ text: 'Europe/Tiraspol', value: 'Europe/Tiraspol' },
|
||||
{ text: 'Europe/Ulyanovsk', value: 'Europe/Ulyanovsk' },
|
||||
{ text: 'Europe/Uzhgorod', value: 'Europe/Uzhgorod' },
|
||||
{ text: 'Europe/Vaduz', value: 'Europe/Vaduz' },
|
||||
{ text: 'Europe/Vatican', value: 'Europe/Vatican' },
|
||||
{ text: 'Europe/Vienna', value: 'Europe/Vienna' },
|
||||
{ text: 'Europe/Vilnius', value: 'Europe/Vilnius' },
|
||||
{ text: 'Europe/Volgograd', value: 'Europe/Volgograd' },
|
||||
{ text: 'Europe/Warsaw', value: 'Europe/Warsaw' },
|
||||
{ text: 'Europe/Zagreb', value: 'Europe/Zagreb' },
|
||||
{ text: 'Europe/Zaporozhye', value: 'Europe/Zaporozhye' },
|
||||
{ text: 'Europe/Zurich', value: 'Europe/Zurich' },
|
||||
{ text: 'Hongkong', value: 'Hongkong' },
|
||||
{ text: 'Iceland', value: 'Iceland' },
|
||||
{ text: 'Indian/Antananarivo', value: 'Indian/Antananarivo' },
|
||||
{ text: 'Indian/Chagos', value: 'Indian/Chagos' },
|
||||
{ text: 'Indian/Christmas', value: 'Indian/Christmas' },
|
||||
{ text: 'Indian/Cocos', value: 'Indian/Cocos' },
|
||||
{ text: 'Indian/Comoro', value: 'Indian/Comoro' },
|
||||
{ text: 'Indian/Kerguelen', value: 'Indian/Kerguelen' },
|
||||
{ text: 'Indian/Mahe', value: 'Indian/Mahe' },
|
||||
{ text: 'Indian/Maldives', value: 'Indian/Maldives' },
|
||||
{ text: 'Indian/Mauritius', value: 'Indian/Mauritius' },
|
||||
{ text: 'Indian/Mayotte', value: 'Indian/Mayotte' },
|
||||
{ text: 'Indian/Reunion', value: 'Indian/Reunion' },
|
||||
{ text: 'Iran', value: 'Iran' },
|
||||
{ text: 'Israel', value: 'Israel' },
|
||||
{ text: 'Jamaica', value: 'Jamaica' },
|
||||
{ text: 'Japan', value: 'Japan' },
|
||||
{ text: 'Kwajalein', value: 'Kwajalein' },
|
||||
{ text: 'Libya', value: 'Libya' },
|
||||
{ text: 'Mexico/BajaNorte', value: 'Mexico/BajaNorte' },
|
||||
{ text: 'Mexico/BajaSur', value: 'Mexico/BajaSur' },
|
||||
{ text: 'Mexico/General', value: 'Mexico/General' },
|
||||
{ text: 'Navajo', value: 'Navajo' },
|
||||
{ text: 'Pacific/Apia', value: 'Pacific/Apia' },
|
||||
{ text: 'Pacific/Auckland', value: 'Pacific/Auckland' },
|
||||
{ text: 'Pacific/Bougainville', value: 'Pacific/Bougainville' },
|
||||
{ text: 'Pacific/Chatham', value: 'Pacific/Chatham' },
|
||||
{ text: 'Pacific/Chuuk', value: 'Pacific/Chuuk' },
|
||||
{ text: 'Pacific/Easter', value: 'Pacific/Easter' },
|
||||
{ text: 'Pacific/Efate', value: 'Pacific/Efate' },
|
||||
{ text: 'Pacific/Enderbury', value: 'Pacific/Enderbury' },
|
||||
{ text: 'Pacific/Fakaofo', value: 'Pacific/Fakaofo' },
|
||||
{ text: 'Pacific/Fiji', value: 'Pacific/Fiji' },
|
||||
{ text: 'Pacific/Funafuti', value: 'Pacific/Funafuti' },
|
||||
{ text: 'Pacific/Galapagos', value: 'Pacific/Galapagos' },
|
||||
{ text: 'Pacific/Gambier', value: 'Pacific/Gambier' },
|
||||
{ text: 'Pacific/Guadalcanal', value: 'Pacific/Guadalcanal' },
|
||||
{ text: 'Pacific/Guam', value: 'Pacific/Guam' },
|
||||
{ text: 'Pacific/Honolulu', value: 'Pacific/Honolulu' },
|
||||
{ text: 'Pacific/Johnston', value: 'Pacific/Johnston' },
|
||||
{ text: 'Pacific/Kanton', value: 'Pacific/Kanton' },
|
||||
{ text: 'Pacific/Kiritimati', value: 'Pacific/Kiritimati' },
|
||||
{ text: 'Pacific/Kosrae', value: 'Pacific/Kosrae' },
|
||||
{ text: 'Pacific/Kwajalein', value: 'Pacific/Kwajalein' },
|
||||
{ text: 'Pacific/Majuro', value: 'Pacific/Majuro' },
|
||||
{ text: 'Pacific/Marquesas', value: 'Pacific/Marquesas' },
|
||||
{ text: 'Pacific/Midway', value: 'Pacific/Midway' },
|
||||
{ text: 'Pacific/Nauru', value: 'Pacific/Nauru' },
|
||||
{ text: 'Pacific/Niue', value: 'Pacific/Niue' },
|
||||
{ text: 'Pacific/Norfolk', value: 'Pacific/Norfolk' },
|
||||
{ text: 'Pacific/Noumea', value: 'Pacific/Noumea' },
|
||||
{ text: 'Pacific/Pago_Pago', value: 'Pacific/Pago_Pago' },
|
||||
{ text: 'Pacific/Palau', value: 'Pacific/Palau' },
|
||||
{ text: 'Pacific/Pitcairn', value: 'Pacific/Pitcairn' },
|
||||
{ text: 'Pacific/Pohnpei', value: 'Pacific/Pohnpei' },
|
||||
{ text: 'Pacific/Ponape', value: 'Pacific/Ponape' },
|
||||
{ text: 'Pacific/Port_Moresby', value: 'Pacific/Port_Moresby' },
|
||||
{ text: 'Pacific/Rarotonga', value: 'Pacific/Rarotonga' },
|
||||
{ text: 'Pacific/Saipan', value: 'Pacific/Saipan' },
|
||||
{ text: 'Pacific/Samoa', value: 'Pacific/Samoa' },
|
||||
{ text: 'Pacific/Tahiti', value: 'Pacific/Tahiti' },
|
||||
{ text: 'Pacific/Tarawa', value: 'Pacific/Tarawa' },
|
||||
{ text: 'Pacific/Tongatapu', value: 'Pacific/Tongatapu' },
|
||||
{ text: 'Pacific/Truk', value: 'Pacific/Truk' },
|
||||
{ text: 'Pacific/Wake', value: 'Pacific/Wake' },
|
||||
{ text: 'Pacific/Wallis', value: 'Pacific/Wallis' },
|
||||
{ text: 'Pacific/Yap', value: 'Pacific/Yap' },
|
||||
{ text: 'Poland', value: 'Poland' },
|
||||
{ text: 'Portugal', value: 'Portugal' },
|
||||
{ text: 'Singapore', value: 'Singapore' },
|
||||
{ text: 'Turkey', value: 'Turkey' },
|
||||
{ text: 'US/Alaska', value: 'US/Alaska' },
|
||||
{ text: 'US/Aleutian', value: 'US/Aleutian' },
|
||||
{ text: 'US/Arizona', value: 'US/Arizona' },
|
||||
{ text: 'US/Central', value: 'US/Central' },
|
||||
{ text: 'US/East-Indiana', value: 'US/East-Indiana' },
|
||||
{ text: 'US/Eastern', value: 'US/Eastern' },
|
||||
{ text: 'US/Hawaii', value: 'US/Hawaii' },
|
||||
{ text: 'US/Indiana-Starke', value: 'US/Indiana-Starke' },
|
||||
{ text: 'US/Michigan', value: 'US/Michigan' },
|
||||
{ text: 'US/Mountain', value: 'US/Mountain' },
|
||||
{ text: 'US/Pacific', value: 'US/Pacific' },
|
||||
{ text: 'US/Samoa', value: 'US/Samoa' },
|
||||
{ text: 'UTC', value: 'UTC' },
|
||||
{ text: 'Zulu', value: 'Zulu' }
|
||||
]
|
|
@ -71,6 +71,7 @@ return [
|
|||
'successful_login_on' => 'Successful login on <span class="light-or-darker">:login_at</span>',
|
||||
'successful_logout_on' => 'Successful logout on <span class="light-or-darker">:login_at</span>',
|
||||
'failed_login_on' => 'Failed login on <span class="light-or-darker">:login_at</span>',
|
||||
'viewed_on' => 'Viewed on <span class="light-or-darker">:login_at</span>',
|
||||
'last_accesses' => 'Last accesses',
|
||||
'see_full_log' => 'See full log',
|
||||
'browser_on_platform' => ':browser on :platform',
|
||||
|
|
|
@ -27,4 +27,10 @@ return [
|
|||
'connection_details' => 'Here are the details of this connection',
|
||||
'recommandations' => 'If this was you, you can ignore this alert. If you suspect any suspicious activity on your account, please change your password.'
|
||||
],
|
||||
'failed_login' => [
|
||||
'subject' => 'Failed login to 2FAuth',
|
||||
'resume' => 'There has been a failed login attempt to your 2FAuth account.',
|
||||
'connection_details' => 'Here are the details of this connection attempt',
|
||||
'recommandations' => 'If this was you, you can ignore this alert. If further attempts fail, you should contact the 2FAuth administrator to review security settings and take action against this attacker.'
|
||||
],
|
||||
];
|
|
@ -52,6 +52,10 @@ return [
|
|||
'label' => 'Language',
|
||||
'help' => 'Language used to translate the 2FAuth user interface. Named languages are complete, set the one of your choice to override your browser preference.'
|
||||
],
|
||||
'timezone' => [
|
||||
'label' => 'Time zone',
|
||||
'help' => 'The time zone applied to all dates and times displayed in the application'
|
||||
],
|
||||
'show_otp_as_dot' => [
|
||||
'label' => 'Show generated <abbr title="One-Time Password">OTP</abbr> as dot',
|
||||
'help' => 'Replace generated password caracters with *** to ensure confidentiality. Do not affect the copy/paste feature'
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
|
||||
@component('mail::message')
|
||||
@lang('notifications.hello_user', ['username' => $account->name])
|
||||
<br/><br/>
|
||||
**@lang('notifications.failed_login.resume')**<br/>
|
||||
@lang('notifications.failed_login.connection_details'):
|
||||
|
||||
<x-mail::panel>
|
||||
@lang('commons.time'): **{{ $time->toCookieString() }}**<br/>
|
||||
@lang('commons.ip_address'): **{{ $ipAddress }}**<br/>
|
||||
@lang('commons.device'): **@lang('admin.browser_on_platform', ['browser' => $browser, 'platform' => $platform])**<br/>
|
||||
</x-mail::panel>
|
||||
|
||||
@lang('notifications.failed_login.recommandations')<br/>
|
||||
|
||||
@lang('notifications.regards'),<br/>
|
||||
{{ config('app.name') }}
|
||||
@endcomponent
|
|
@ -19,7 +19,7 @@ use Laravel\Passport\Http\Controllers\PersonalAccessTokenController;
|
|||
|
||||
// use App\Models\User;
|
||||
// use App\Notifications\SignedInWithNewDevice;
|
||||
// use Bubka\LaravelAuthenticationLog\Models\AuthenticationLog;
|
||||
// use App\Models\AuthLog;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -95,7 +95,7 @@ Route::get('refresh-csrf', function () {
|
|||
|
||||
// Route::get('/notification', function () {
|
||||
// $user = User::find(1);
|
||||
// return (new SignedInWithNewDevice(AuthenticationLog::find(9)))
|
||||
// return (new SignedInWithNewDevice(AuthLog::find(9)))
|
||||
// ->toMail($user);
|
||||
// });
|
||||
|
||||
|
|
|
@ -89,47 +89,22 @@ class UserControllerTest extends FeatureTestCase
|
|||
*/
|
||||
public function test_allPreferences_returns_preferences_with_user_values()
|
||||
{
|
||||
$userPrefs = [
|
||||
'showOtpAsDot' => true,
|
||||
'closeOtpOnCopy' => true,
|
||||
'copyOtpOnDisplay' => true,
|
||||
'useBasicQrcodeReader' => true,
|
||||
'displayMode' => 'grid',
|
||||
'showAccountsIcons' => false,
|
||||
'kickUserAfter' => 5,
|
||||
'activeGroup' => 1,
|
||||
'rememberActiveGroup' => false,
|
||||
'defaultGroup' => 1,
|
||||
'defaultCaptureMode' => 'advancedForm',
|
||||
'useDirectCapture' => true,
|
||||
'useWebauthnOnly' => true,
|
||||
'getOfficialIcons' => false,
|
||||
'theme' => 'dark',
|
||||
'formatPassword' => false,
|
||||
'formatPasswordBy' => 1,
|
||||
'lang' => 'fr',
|
||||
];
|
||||
$userPrefs = [];
|
||||
|
||||
$this->user['preferences->showOtpAsDot'] = $userPrefs['showOtpAsDot'];
|
||||
$this->user['preferences->closeOtpOnCopy'] = $userPrefs['closeOtpOnCopy'];
|
||||
$this->user['preferences->copyOtpOnDisplay'] = $userPrefs['copyOtpOnDisplay'];
|
||||
$this->user['preferences->useBasicQrcodeReader'] = $userPrefs['useBasicQrcodeReader'];
|
||||
$this->user['preferences->displayMode'] = $userPrefs['displayMode'];
|
||||
$this->user['preferences->showAccountsIcons'] = $userPrefs['showAccountsIcons'];
|
||||
$this->user['preferences->kickUserAfter'] = $userPrefs['kickUserAfter'];
|
||||
$this->user['preferences->activeGroup'] = $userPrefs['activeGroup'];
|
||||
$this->user['preferences->rememberActiveGroup'] = $userPrefs['rememberActiveGroup'];
|
||||
$this->user['preferences->defaultGroup'] = $userPrefs['defaultGroup'];
|
||||
$this->user['preferences->defaultCaptureMode'] = $userPrefs['defaultCaptureMode'];
|
||||
$this->user['preferences->useDirectCapture'] = $userPrefs['useDirectCapture'];
|
||||
$this->user['preferences->useWebauthnOnly'] = $userPrefs['useWebauthnOnly'];
|
||||
$this->user['preferences->getOfficialIcons'] = $userPrefs['getOfficialIcons'];
|
||||
$this->user['preferences->theme'] = $userPrefs['theme'];
|
||||
$this->user['preferences->formatPassword'] = $userPrefs['formatPassword'];
|
||||
$this->user['preferences->formatPasswordBy'] = $userPrefs['formatPasswordBy'];
|
||||
$this->user['preferences->lang'] = $userPrefs['lang'];
|
||||
foreach (config('2fauth.preferences') as $pref => $value) {
|
||||
if (is_numeric($value)) {
|
||||
$userPrefs[$pref] = $value + 1;
|
||||
} else if (is_string($value)) {
|
||||
$userPrefs[$pref] = $value . '_';
|
||||
} else if (is_bool($value)) {
|
||||
$userPrefs[$pref] = ! $value;
|
||||
}
|
||||
|
||||
$this->user['preferences->' . $pref] = $userPrefs[$pref];
|
||||
}
|
||||
|
||||
$this->user->save();
|
||||
|
||||
|
||||
$response = $this->actingAs($this->user, 'api-guard')
|
||||
->json('GET', '/api/v1/user/preferences')
|
||||
->assertJsonCount(count(config('2fauth.preferences')), $key = null);
|
||||
|
|
|
@ -4,6 +4,8 @@ namespace Tests\Api\v1\Controllers;
|
|||
|
||||
use App\Api\v1\Controllers\UserManagerController;
|
||||
use App\Api\v1\Resources\UserManagerResource;
|
||||
use App\Models\AuthLog;
|
||||
use App\Models\TwoFAccount;
|
||||
use App\Models\User;
|
||||
use App\Policies\UserPolicy;
|
||||
use Database\Factories\UserFactory;
|
||||
|
@ -12,7 +14,6 @@ use Illuminate\Http\Request;
|
|||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
|
@ -22,7 +23,6 @@ use Laravel\Passport\TokenRepository;
|
|||
use Mockery\MockInterface;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Tests\Data\AuthenticationLogData;
|
||||
use Tests\FeatureTestCase;
|
||||
|
||||
#[CoversClass(UserManagerController::class)]
|
||||
|
@ -40,6 +40,11 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
|
||||
protected $anotherUser;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultPreferences;
|
||||
|
||||
private const USERNAME = 'john doe';
|
||||
|
||||
private const EMAIL = 'johndoe@example.org';
|
||||
|
@ -56,6 +61,11 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
$this->admin = User::factory()->administrator()->create();
|
||||
$this->user = User::factory()->create();
|
||||
$this->anotherUser = User::factory()->create();
|
||||
TwoFAccount::factory()->for($this->anotherUser)->create();
|
||||
|
||||
foreach (config('2fauth.preferences') as $pref => $value) {
|
||||
$this->defaultPreferences[$pref] = config('2fauth.preferences.' . $pref);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -79,63 +89,71 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_index_returns_all_users()
|
||||
public function test_index_returns_all_users_with_expected_UserManagerResources() : void
|
||||
{
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
$response = $this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users')
|
||||
->assertOk()
|
||||
->assertJsonCount(3)
|
||||
->assertJsonFragment([
|
||||
'email' => $this->admin->email,
|
||||
->assertJsonStructure([
|
||||
'*' => [
|
||||
"last_seen_at",
|
||||
"created_at",
|
||||
]
|
||||
])
|
||||
->assertJsonFragment([
|
||||
'email' => $this->user->email,
|
||||
"id" => $this->user->id,
|
||||
"name" => $this->user->name,
|
||||
"email" => $this->user->email,
|
||||
"oauth_provider" => null,
|
||||
"preferences" => $this->defaultPreferences,
|
||||
"is_admin" => false,
|
||||
"twofaccounts_count" => 0,
|
||||
])
|
||||
->assertJsonFragment([
|
||||
'email' => $this->anotherUser->email,
|
||||
"id" => $this->admin->id,
|
||||
"name" => $this->admin->name,
|
||||
"email" => $this->admin->email,
|
||||
"oauth_provider" => null,
|
||||
"preferences" => $this->defaultPreferences,
|
||||
"is_admin" => true,
|
||||
"twofaccounts_count" => 0,
|
||||
])
|
||||
->assertJsonFragment([
|
||||
"id" => $this->anotherUser->id,
|
||||
"name" => $this->anotherUser->name,
|
||||
"email" => $this->anotherUser->email,
|
||||
"oauth_provider" => null,
|
||||
"preferences" => $this->defaultPreferences,
|
||||
"is_admin" => false,
|
||||
"twofaccounts_count" => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_index_succeeds_and_returns_UserManagerResource() : void
|
||||
{
|
||||
$path = '/api/v1/users';
|
||||
$resources = UserManagerResource::collection(User::all());
|
||||
$request = Request::create($path, 'GET');
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', $path)
|
||||
->assertExactJson($resources->response($request)->getData(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_show_returns_the_correct_user()
|
||||
public function test_show_returns_the_expected_UserManagerResource() : void
|
||||
{
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id)
|
||||
->assertJsonFragment([
|
||||
'email' => $this->user->email,
|
||||
->assertJson([
|
||||
"info" => [
|
||||
"id" => $this->user->id,
|
||||
"name" => $this->user->name,
|
||||
"email" => $this->user->email,
|
||||
"oauth_provider" => null,
|
||||
"preferences" => $this->defaultPreferences,
|
||||
"is_admin" => false,
|
||||
"twofaccounts_count" => 0,
|
||||
"last_seen_at" => "1 second ago",
|
||||
"created_at" => "1 second ago"
|
||||
],
|
||||
"password_reset" => null,
|
||||
"valid_personal_access_tokens" => 0,
|
||||
"webauthn_credentials" => 0
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_show_returns_UserManagerResource() : void
|
||||
{
|
||||
$path = '/api/v1/users/' . $this->user->id;
|
||||
$resources = UserManagerResource::make($this->user);
|
||||
$request = Request::create($path, 'GET');
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', $path)
|
||||
->assertExactJson($resources->response($request)->getData(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
|
@ -526,46 +544,36 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_authLog_events_are_listened_by_authLog_listeners()
|
||||
public function test_authentications_returns_all_entries() : void
|
||||
{
|
||||
Event::fake();
|
||||
AuthLog::factory()->for($this->user, 'authenticatable')->beforeLastYear()->create();
|
||||
AuthLog::factory()->for($this->user, 'authenticatable')->duringLastYear()->create();
|
||||
AuthLog::factory()->for($this->user, 'authenticatable')->duringLastSixMonth()->create();
|
||||
AuthLog::factory()->for($this->user, 'authenticatable')->duringLastThreeMonth()->create();
|
||||
AuthLog::factory()->for($this->user, 'authenticatable')->duringLastMonth()->create();
|
||||
AuthLog::factory()->for($this->user, 'authenticatable')->logoutOnly()->create();
|
||||
AuthLog::factory()->for($this->user, 'authenticatable')->failedLogin()->create();
|
||||
AuthLog::factory()->for($this->user, 'authenticatable')->create();
|
||||
|
||||
foreach (config('authentication-log.listeners') as $type => $listenerClass) {
|
||||
Event::assertListening(
|
||||
config('authentication-log.events.' . $type),
|
||||
$listenerClass
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Local feeder because Factory cannot be used here
|
||||
*/
|
||||
protected function feedAuthenticationLog() : int
|
||||
{
|
||||
// Do not change creation order
|
||||
$this->user->authentications()->create(AuthenticationLogData::beforeLastYear());
|
||||
$this->user->authentications()->create(AuthenticationLogData::duringLastYear());
|
||||
$this->user->authentications()->create(AuthenticationLogData::duringLastSixMonth());
|
||||
$this->user->authentications()->create(AuthenticationLogData::duringLastThreeMonth());
|
||||
$this->user->authentications()->create(AuthenticationLogData::duringLastMonth());
|
||||
$this->user->authentications()->create(AuthenticationLogData::noLogin());
|
||||
$this->user->authentications()->create(AuthenticationLogData::noLogout());
|
||||
|
||||
return 7;
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications')
|
||||
->assertOk()
|
||||
->assertJsonCount(8);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_authentications_returns_all_entries() : void
|
||||
public function test_authentications_returns_user_entries_only() : void
|
||||
{
|
||||
$created = $this->feedAuthenticationLog();
|
||||
AuthLog::factory()->for($this->admin, 'authenticatable')->create();
|
||||
AuthLog::factory()->for($this->user, 'authenticatable')->create();
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
$response = $this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications')
|
||||
->assertOk()
|
||||
->assertJsonCount($created);
|
||||
->assertJsonCount(1);
|
||||
|
||||
$this->assertEquals($response->getData()[0]->id, $this->user->id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -573,7 +581,7 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
*/
|
||||
public function test_authentications_returns_expected_resource() : void
|
||||
{
|
||||
$this->user->authentications()->create(AuthenticationLogData::duringLastMonth());
|
||||
AuthLog::factory()->for($this->user, 'authenticatable')->create();
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications')
|
||||
|
@ -589,6 +597,7 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
'logout_at',
|
||||
'login_successful',
|
||||
'duration',
|
||||
'login_method',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
@ -596,12 +605,41 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_authentications_returns_no_login_entry() : void
|
||||
public function test_authentications_returns_resource_with_timezoned_dates() : void
|
||||
{
|
||||
$this->user->authentications()->create(AuthenticationLogData::noLogin());
|
||||
$timezone = 'Europe/Paris';
|
||||
$this->admin['preferences->timezone'] = $timezone;
|
||||
$this->admin->save();
|
||||
|
||||
$now = now();
|
||||
$timezonedNow = now($timezone);
|
||||
|
||||
AuthLog::factory()->for($this->user, 'authenticatable')->create([
|
||||
'login_at' => $now,
|
||||
'logout_at' => $now,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications');
|
||||
|
||||
$login_at = Carbon::parse($response->getData()[0]->login_at);
|
||||
$logout_at = Carbon::parse($response->getData()[0]->logout_at);
|
||||
|
||||
$this->assertTrue($login_at->isSameHour($timezonedNow));
|
||||
$this->assertTrue($login_at->isSameMinute($timezonedNow));
|
||||
$this->assertTrue($logout_at->isSameHour($timezonedNow));
|
||||
$this->assertTrue($logout_at->isSameMinute($timezonedNow));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_authentications_returns_loginless_entries() : void
|
||||
{
|
||||
$this->logUserOut();
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=1')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications')
|
||||
->assertJsonCount(1)
|
||||
->assertJsonFragment([
|
||||
'login_at' => null,
|
||||
|
@ -611,12 +649,12 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_authentications_returns_no_logout_entry() : void
|
||||
public function test_authentications_returns_logoutless_entries() : void
|
||||
{
|
||||
$this->user->authentications()->create(AuthenticationLogData::noLogout());
|
||||
$this->logUserIn();
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=1')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications')
|
||||
->assertJsonCount(1)
|
||||
->assertJsonFragment([
|
||||
'logout_at' => null,
|
||||
|
@ -628,14 +666,15 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
*/
|
||||
public function test_authentications_returns_failed_entry() : void
|
||||
{
|
||||
$this->user->authentications()->create(AuthenticationLogData::failedLogin());
|
||||
$expected = Carbon::parse(AuthenticationLogData::failedLogin()['login_at'])->toDayDateTimeString();
|
||||
$this->json('POST', '/user/login', [
|
||||
'email' => $this->user->email,
|
||||
'password' => 'wrong_password',
|
||||
]);
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=1')
|
||||
->assertJsonCount(1)
|
||||
->assertJsonFragment([
|
||||
'login_at' => $expected,
|
||||
'login_successful' => false,
|
||||
]);
|
||||
}
|
||||
|
@ -645,15 +684,16 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
*/
|
||||
public function test_authentications_returns_last_month_entries() : void
|
||||
{
|
||||
$this->feedAuthenticationLog();
|
||||
$expected = Carbon::parse(AuthenticationLogData::duringLastMonth()['login_at'])->toDayDateTimeString();
|
||||
$this->travel(-2)->months();
|
||||
$this->logUserInAndOut();
|
||||
$this->travelBack();
|
||||
$this->logUserIn();
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
$response = $this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=1')
|
||||
->assertJsonCount(3)
|
||||
->assertJsonFragment([
|
||||
'login_at' => $expected,
|
||||
]);
|
||||
->assertJsonCount(1);
|
||||
|
||||
$this->assertTrue(Carbon::parse($response->getData()[0]->login_at)->isSameDay(now()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -661,19 +701,18 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
*/
|
||||
public function test_authentications_returns_last_three_months_entries() : void
|
||||
{
|
||||
$this->feedAuthenticationLog();
|
||||
$expectedOneMonth = Carbon::parse(AuthenticationLogData::duringLastMonth()['login_at'])->toDayDateTimeString();
|
||||
$expectedThreeMonth = Carbon::parse(AuthenticationLogData::duringLastThreeMonth()['login_at'])->toDayDateTimeString();
|
||||
$this->travel(-100)->days();
|
||||
$this->logUserInAndOut();
|
||||
$this->travelBack();
|
||||
$this->travel(-80)->days();
|
||||
$this->logUserIn();
|
||||
$this->travelBack();
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
$response = $this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=3')
|
||||
->assertJsonCount(4)
|
||||
->assertJsonFragment([
|
||||
'login_at' => $expectedOneMonth,
|
||||
])
|
||||
->assertJsonFragment([
|
||||
'login_at' => $expectedThreeMonth,
|
||||
]);
|
||||
->assertJsonCount(1);
|
||||
|
||||
$this->assertTrue(Carbon::parse($response->getData()[0]->login_at)->isSameDay(now()->subDays(80)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -681,23 +720,18 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
*/
|
||||
public function test_authentications_returns_last_six_months_entries() : void
|
||||
{
|
||||
$this->feedAuthenticationLog();
|
||||
$expectedOneMonth = Carbon::parse(AuthenticationLogData::duringLastMonth()['login_at'])->toDayDateTimeString();
|
||||
$expectedThreeMonth = Carbon::parse(AuthenticationLogData::duringLastThreeMonth()['login_at'])->toDayDateTimeString();
|
||||
$expectedSixMonth = Carbon::parse(AuthenticationLogData::duringLastSixMonth()['login_at'])->toDayDateTimeString();
|
||||
$this->travel(-7)->months();
|
||||
$this->logUserInAndOut();
|
||||
$this->travelBack();
|
||||
$this->travel(-5)->months();
|
||||
$this->logUserIn();
|
||||
$this->travelBack();
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
$response = $this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=6')
|
||||
->assertJsonCount(5)
|
||||
->assertJsonFragment([
|
||||
'login_at' => $expectedOneMonth,
|
||||
])
|
||||
->assertJsonFragment([
|
||||
'login_at' => $expectedThreeMonth,
|
||||
])
|
||||
->assertJsonFragment([
|
||||
'login_at' => $expectedSixMonth,
|
||||
]);
|
||||
->assertJsonCount(1);
|
||||
|
||||
$this->assertTrue(Carbon::parse($response->getData()[0]->login_at)->isSameDay(now()->subMonths(5)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -705,27 +739,18 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
*/
|
||||
public function test_authentications_returns_last_year_entries() : void
|
||||
{
|
||||
$this->feedAuthenticationLog();
|
||||
$expectedOneMonth = Carbon::parse(AuthenticationLogData::duringLastMonth()['login_at'])->toDayDateTimeString();
|
||||
$expectedThreeMonth = Carbon::parse(AuthenticationLogData::duringLastThreeMonth()['login_at'])->toDayDateTimeString();
|
||||
$expectedSixMonth = Carbon::parse(AuthenticationLogData::duringLastSixMonth()['login_at'])->toDayDateTimeString();
|
||||
$expectedYear = Carbon::parse(AuthenticationLogData::duringLastYear()['login_at'])->toDayDateTimeString();
|
||||
$this->travel(-13)->months();
|
||||
$this->logUserInAndOut();
|
||||
$this->travelBack();
|
||||
$this->travel(-11)->months();
|
||||
$this->logUserIn();
|
||||
$this->travelBack();
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
$response = $this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=12')
|
||||
->assertJsonCount(6)
|
||||
->assertJsonFragment([
|
||||
'login_at' => $expectedOneMonth,
|
||||
])
|
||||
->assertJsonFragment([
|
||||
'login_at' => $expectedThreeMonth,
|
||||
])
|
||||
->assertJsonFragment([
|
||||
'login_at' => $expectedSixMonth,
|
||||
])
|
||||
->assertJsonFragment([
|
||||
'login_at' => $expectedYear,
|
||||
]);
|
||||
->assertJsonCount(1);
|
||||
|
||||
$this->assertTrue(Carbon::parse($response->getData()[0]->login_at)->isSameDay(now()->subMonths(11)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -734,7 +759,10 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
#[DataProvider('LimitProvider')]
|
||||
public function test_authentications_returns_limited_entries($limit) : void
|
||||
{
|
||||
$this->feedAuthenticationLog();
|
||||
AuthLog::factory()->for($this->user, 'authenticatable')->duringLastYear()->create();
|
||||
AuthLog::factory()->for($this->user, 'authenticatable')->duringLastSixMonth()->create();
|
||||
AuthLog::factory()->for($this->user, 'authenticatable')->duringLastThreeMonth()->create();
|
||||
AuthLog::factory()->for($this->user, 'authenticatable')->duringLastMonth()->create();
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?limit=' . $limit)
|
||||
|
@ -743,7 +771,7 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
}
|
||||
|
||||
/**
|
||||
* Provide various limit
|
||||
* Provide various limits
|
||||
*/
|
||||
public static function LimitProvider()
|
||||
{
|
||||
|
@ -751,6 +779,7 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
'limited to 1' => [1],
|
||||
'limited to 2' => [2],
|
||||
'limited to 3' => [3],
|
||||
'limited to 4' => [4],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -759,13 +788,9 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
*/
|
||||
public function test_authentications_returns_expected_ip_and_useragent_chunks() : void
|
||||
{
|
||||
$this->user->authentications()->create([
|
||||
AuthLog::factory()->for($this->user, 'authenticatable')->create([
|
||||
'ip_address' => '127.0.0.1',
|
||||
'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
|
||||
'login_at' => now(),
|
||||
'login_successful' => true,
|
||||
'logout_at' => null,
|
||||
'location' => null,
|
||||
]);
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
|
@ -786,7 +811,7 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
{
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?limit=' . $limit)
|
||||
->assertStatus(422);
|
||||
->assertInvalid(['limit']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -797,7 +822,7 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
{
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', '/api/v1/users/' . $this->user->id . '/authentications?period=' . $period)
|
||||
->assertStatus(422);
|
||||
->assertInvalid(['period']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -813,4 +838,33 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
'array' => ['[]'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a request to login the user in
|
||||
*/
|
||||
protected function logUserIn() : void
|
||||
{
|
||||
$this->json('POST', '/user/login', [
|
||||
'email' => $this->user->email,
|
||||
'password' => self::PASSWORD,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a request to login the user out
|
||||
*/
|
||||
protected function logUserOut() : void
|
||||
{
|
||||
$this->actingAs($this->user, 'web-guard')
|
||||
->json('GET', '/user/logout');
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a request to login the user out
|
||||
*/
|
||||
protected function logUserInAndOut() : void
|
||||
{
|
||||
$this->logUserIn();
|
||||
$this->logUserOut();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,159 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Data;
|
||||
|
||||
class AuthenticationLogData
|
||||
{
|
||||
/**
|
||||
* Indicate that the model should have login date.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function failedLogin()
|
||||
{
|
||||
$loginDate = now()->subDays(15);
|
||||
|
||||
return [
|
||||
'ip_address' => fake()->ipv4(),
|
||||
'user_agent' => fake()->userAgent(),
|
||||
'login_at' => $loginDate,
|
||||
'login_successful' => false,
|
||||
'logout_at' => null,
|
||||
'location' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model should have no login date
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function noLogin()
|
||||
{
|
||||
return [
|
||||
'ip_address' => fake()->ipv4(),
|
||||
'user_agent' => fake()->userAgent(),
|
||||
'login_at' => null,
|
||||
'login_successful' => false,
|
||||
'logout_at' => now(),
|
||||
'location' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model should have no logout date
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function noLogout()
|
||||
{
|
||||
return [
|
||||
'ip_address' => fake()->ipv4(),
|
||||
'user_agent' => fake()->userAgent(),
|
||||
'login_at' => now(),
|
||||
'login_successful' => true,
|
||||
'logout_at' => null,
|
||||
'location' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model should have login during last month
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function duringLastMonth()
|
||||
{
|
||||
$loginDate = now()->subDays(15);
|
||||
$logoutDate = $loginDate->addHours(1);
|
||||
|
||||
return [
|
||||
'ip_address' => '127.0.0.1',
|
||||
'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
|
||||
'login_at' => $loginDate,
|
||||
'login_successful' => true,
|
||||
'logout_at' => $logoutDate,
|
||||
'location' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model should have login during last 3 month
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function duringLastThreeMonth()
|
||||
{
|
||||
$loginDate = now()->subMonths(2);
|
||||
$logoutDate = $loginDate->addHours(1);
|
||||
|
||||
return [
|
||||
'ip_address' => fake()->ipv4(),
|
||||
'user_agent' => fake()->userAgent(),
|
||||
'login_at' => $loginDate,
|
||||
'login_successful' => true,
|
||||
'logout_at' => $logoutDate,
|
||||
'location' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model should have login during last 6 month
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function duringLastSixMonth()
|
||||
{
|
||||
$loginDate = now()->subMonths(4);
|
||||
$logoutDate = $loginDate->addHours(1);
|
||||
|
||||
return [
|
||||
'ip_address' => fake()->ipv4(),
|
||||
'user_agent' => fake()->userAgent(),
|
||||
'login_at' => $loginDate,
|
||||
'login_successful' => true,
|
||||
'logout_at' => $logoutDate,
|
||||
'location' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model should have login during last month
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function duringLastYear()
|
||||
{
|
||||
$loginDate = now()->subMonths(10);
|
||||
$logoutDate = $loginDate->addHours(1);
|
||||
|
||||
return [
|
||||
'ip_address' => fake()->ipv4(),
|
||||
'user_agent' => fake()->userAgent(),
|
||||
'login_at' => $loginDate,
|
||||
'login_successful' => true,
|
||||
'logout_at' => $logoutDate,
|
||||
'location' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model should have login during last month
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function beforeLastYear()
|
||||
{
|
||||
$loginDate = now()->subYears(2);
|
||||
$logoutDate = $loginDate->addHours(1);
|
||||
|
||||
return [
|
||||
'ip_address' => fake()->ipv4(),
|
||||
'user_agent' => fake()->userAgent(),
|
||||
'login_at' => $loginDate,
|
||||
'login_successful' => true,
|
||||
'logout_at' => $logoutDate,
|
||||
'location' => null,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -7,9 +7,14 @@ use App\Http\Middleware\RejectIfAuthenticated;
|
|||
use App\Http\Middleware\RejectIfDemoMode;
|
||||
use App\Http\Middleware\RejectIfReverseProxy;
|
||||
use App\Http\Middleware\SkipIfAuthenticated;
|
||||
use App\Listeners\Authentication\FailedLoginListener;
|
||||
use App\Listeners\Authentication\LoginListener;
|
||||
use App\Models\User;
|
||||
use App\Notifications\FailedLogin;
|
||||
use App\Notifications\SignedInWithNewDevice;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Tests\FeatureTestCase;
|
||||
|
||||
|
@ -21,6 +26,8 @@ use Tests\FeatureTestCase;
|
|||
#[CoversClass(RejectIfReverseProxy::class)]
|
||||
#[CoversClass(RejectIfDemoMode::class)]
|
||||
#[CoversClass(SkipIfAuthenticated::class)]
|
||||
#[CoversClass(LoginListener::class)]
|
||||
#[CoversClass(FailedLoginListener::class)]
|
||||
class LoginTest extends FeatureTestCase
|
||||
{
|
||||
/**
|
||||
|
@ -70,6 +77,63 @@ class LoginTest extends FeatureTestCase
|
|||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_login_send_new_device_notification()
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
$this->json('POST', '/user/login', [
|
||||
'email' => $this->user->email,
|
||||
'password' => self::PASSWORD,
|
||||
])->assertOk();
|
||||
|
||||
$this->actingAs($this->user, 'web-guard')
|
||||
->json('GET', '/user/logout');
|
||||
|
||||
$this->travel(1)->minute();
|
||||
|
||||
$this->json('POST', '/user/login', [
|
||||
'email' => $this->user->email,
|
||||
'password' => self::PASSWORD,
|
||||
], [
|
||||
'HTTP_USER_AGENT' => 'NotSymfony'
|
||||
])->assertOk();
|
||||
|
||||
Notification::assertSentTo($this->user, SignedInWithNewDevice::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_login_does_not_send_new_device_notification()
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
$this->user['preferences->notifyOnNewAuthDevice'] = 0;
|
||||
$this->user->save();
|
||||
|
||||
$this->json('POST', '/user/login', [
|
||||
'email' => $this->user->email,
|
||||
'password' => self::PASSWORD,
|
||||
])->assertOk();
|
||||
|
||||
$this->actingAs($this->user, 'web-guard')
|
||||
->json('GET', '/user/logout');
|
||||
|
||||
$this->travel(1)->minute();
|
||||
|
||||
$this->json('POST', '/user/login', [
|
||||
'email' => $this->user->email,
|
||||
'password' => self::PASSWORD,
|
||||
], [
|
||||
'HTTP_USER_AGENT' => 'NotSymfony'
|
||||
])->assertOk();
|
||||
|
||||
Notification::assertNothingSentTo($this->user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
|
@ -164,6 +228,39 @@ class LoginTest extends FeatureTestCase
|
|||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_login_with_invalid_credentials_send_failed_login_notification()
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
$this->json('POST', '/user/login', [
|
||||
'email' => $this->user->email,
|
||||
'password' => self::WRONG_PASSWORD,
|
||||
])->assertStatus(401);
|
||||
|
||||
Notification::assertSentTo($this->user, FailedLogin::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_login_with_invalid_credentials_does_not_send_new_device_notification()
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
$this->user['preferences->notifyOnFailedLogin'] = 0;
|
||||
$this->user->save();
|
||||
|
||||
$this->json('POST', '/user/login', [
|
||||
'email' => $this->user->email,
|
||||
'password' => self::WRONG_PASSWORD,
|
||||
])->assertStatus(401);
|
||||
|
||||
Notification::assertNothingSentTo($this->user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Tests\Feature\Models;
|
||||
|
||||
use App\Models\AuthLog;
|
||||
use App\Models\Group;
|
||||
use App\Models\TwoFAccount;
|
||||
use App\Models\User;
|
||||
|
@ -113,6 +114,7 @@ class UserModelTest extends FeatureTestCase
|
|||
{
|
||||
$user = User::factory()->create();
|
||||
TwoFAccount::factory()->for($user)->create();
|
||||
AuthLog::factory()->for($user, 'authenticatable')->create();
|
||||
Group::factory()->for($user)->create();
|
||||
|
||||
DB::table('webauthn_credentials')->insert([
|
||||
|
@ -154,6 +156,9 @@ class UserModelTest extends FeatureTestCase
|
|||
$this->assertDatabaseMissing(config('auth.passwords.users.table'), [
|
||||
'email' => $user->email,
|
||||
]);
|
||||
$this->assertDatabaseMissing('auth_logs', [
|
||||
'authenticatable_id' => $user->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit\Listeners\Authentication;
|
||||
|
||||
use App\Listeners\Authentication\FailedLoginListener;
|
||||
use Illuminate\Auth\Events\Failed;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* FailedLoginListenerTest test class
|
||||
*/
|
||||
#[CoversClass(FailedLoginListener::class)]
|
||||
class FailedLoginListenerTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_FailedLoginListener_listen_to_Failed_event()
|
||||
{
|
||||
Event::fake();
|
||||
|
||||
Event::assertListening(
|
||||
Failed::class,
|
||||
FailedLoginListener::class
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit\Listeners\Authentication;
|
||||
|
||||
use App\Listeners\Authentication\LoginListener;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Login;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Mockery;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* LoginListenerTest test class
|
||||
*/
|
||||
#[CoversClass(LoginListener::class)]
|
||||
class LoginListenerTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_LoginListener_listen_to_Login_event()
|
||||
{
|
||||
Event::fake();
|
||||
|
||||
Event::assertListening(
|
||||
Login::class,
|
||||
LoginListener::class
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit\Listeners\Authentication;
|
||||
|
||||
use App\Listeners\Authentication\LogoutListener;
|
||||
use Illuminate\Auth\Events\Logout;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* LogoutListenerTest test class
|
||||
*/
|
||||
#[CoversClass(LogoutListener::class)]
|
||||
class LogoutListenerTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_LogoutListener_listen_to_Logout_event()
|
||||
{
|
||||
Event::fake();
|
||||
|
||||
Event::assertListening(
|
||||
Logout::class,
|
||||
LogoutListener::class
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit\Listeners\Authentication;
|
||||
|
||||
use App\Events\VisitedByProxyUser;
|
||||
use App\Listeners\Authentication\VisitedByProxyUserListener;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* VisitedByProxyUserListenerTest test class
|
||||
*/
|
||||
#[CoversClass(VisitedByProxyUserListener::class)]
|
||||
class VisitedByProxyUserListenerTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_VisitedByProxyUserListener_listen_to_VisitedByProxyUser_event()
|
||||
{
|
||||
Event::fake();
|
||||
|
||||
Event::assertListening(
|
||||
VisitedByProxyUser::class,
|
||||
VisitedByProxyUserListener::class
|
||||
);
|
||||
}
|
||||
}
|
|
@ -78,9 +78,9 @@ export default defineConfig({
|
|||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
watch: {
|
||||
followSymlinks: false,
|
||||
}
|
||||
}
|
||||
// server: {
|
||||
// watch: {
|
||||
// followSymlinks: false,
|
||||
// }
|
||||
// }
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue