mirror of https://github.com/Bubka/2FAuth.git
Compare commits
12 Commits
dd7d3d95df
...
a61f5f4702
Author | SHA1 | Date |
---|---|---|
Bubka | a61f5f4702 | |
Bubka | 5fb1d60636 | |
Bubka | f43fc97523 | |
Bubka | eb3e38f4a6 | |
Bubka | fdccbbcc55 | |
Bubka | 53eef9e018 | |
Bubka | 55192fe89f | |
Bubka | 4bb1cf89ab | |
Bubka | 49fddfd331 | |
Bubka | ca903b6fc0 | |
Bubka | 4a8db39ab0 | |
Bubka | 2e8a9f75b5 |
|
@ -200,7 +200,8 @@ PROXY_LOGOUT_URL=null
|
|||
WEBAUTHN_NAME=2FAuth
|
||||
|
||||
|
||||
# Relying Party ID. If null, the device will fill it internally.
|
||||
# Relying Party ID, should equal the site domain (i.e 2fauth.example.com).
|
||||
# If null, the device will fill it internally (recommended)
|
||||
# See https://webauthn-doc.spomky-labs.com/prerequisites/the-relying-party#how-to-determine-the-relying-party-id
|
||||
|
||||
WEBAUTHN_ID=null
|
||||
|
|
|
@ -194,14 +194,12 @@ ENV \
|
|||
# Custom logout URL to open when using an auth proxy.
|
||||
PROXY_LOGOUT_URL=null \
|
||||
# WebAuthn settings
|
||||
# Relying Party name, aka the name of the application. If null, defaults to APP_NAME
|
||||
# Relying Party name, aka the name of the application. If blank, defaults to APP_NAME. Do not set to null.
|
||||
WEBAUTHN_NAME=2FAuth \
|
||||
# Relying Party ID. If null, the device will fill it internally.
|
||||
# Relying Party ID, should equal the site domain (i.e 2fauth.example.com).
|
||||
# If null, the device will fill it internally (recommended)
|
||||
# See https://webauthn-doc.spomky-labs.com/prerequisites/the-relying-party#how-to-determine-the-relying-party-id
|
||||
WEBAUTHN_ID=null \
|
||||
# Optional image data in BASE64 (128 bytes maximum) or an image url
|
||||
# See https://webauthn-doc.spomky-labs.com/prerequisites/the-relying-party#relying-party-icon
|
||||
WEBAUTHN_ICON=null \
|
||||
# Use this setting to control how user verification behave during the
|
||||
# WebAuthn authentication flow.
|
||||
#
|
||||
|
|
812
_ide_helper.php
812
_ide_helper.php
File diff suppressed because it is too large
Load Diff
|
@ -2,8 +2,8 @@
|
|||
|
||||
namespace App\Api\v1\Controllers;
|
||||
|
||||
use App\Api\v1\Requests\UserManagerStoreRequest;
|
||||
use App\Api\v1\Requests\UserManagerPromoteRequest;
|
||||
use App\Api\v1\Requests\UserManagerStoreRequest;
|
||||
use App\Api\v1\Resources\UserManagerResource;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
|
@ -32,11 +32,13 @@ class UserManagerController extends Controller
|
|||
*/
|
||||
public function show(User $user)
|
||||
{
|
||||
$this->authorize('view', $user);
|
||||
|
||||
return new UserManagerResource($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset user's password
|
||||
* Reset user's password
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
|
@ -44,6 +46,8 @@ class UserManagerController extends Controller
|
|||
{
|
||||
Log::info(sprintf('Password reset for User ID #%s requested by User ID #%s', $user->id, $request->user()->id));
|
||||
|
||||
$this->authorize('update', $user);
|
||||
|
||||
$credentials = [
|
||||
'token' => $this->broker()->createToken($user),
|
||||
'email' => $user->email,
|
||||
|
@ -59,15 +63,14 @@ class UserManagerController extends Controller
|
|||
|
||||
if ($response == Password::PASSWORD_RESET) {
|
||||
Log::info(sprintf('Temporary password set for User ID #%s', $user->id));
|
||||
|
||||
|
||||
$response = $this->broker()->sendResetLink(
|
||||
['email' => $credentials['email']]
|
||||
);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return response()->json([
|
||||
'message' => 'bad request',
|
||||
'reason' => is_string($response) ? __($response) : __('errors.no_pwd_reset_for_this_user_type')
|
||||
'reason' => is_string($response) ? __($response) : __('errors.no_pwd_reset_for_this_user_type'),
|
||||
], 400);
|
||||
}
|
||||
|
||||
|
@ -75,7 +78,7 @@ class UserManagerController extends Controller
|
|||
? new UserManagerResource($user)
|
||||
: response()->json([
|
||||
'message' => 'bad request',
|
||||
'reason' => __($response)
|
||||
'reason' => __($response),
|
||||
], 400);
|
||||
}
|
||||
|
||||
|
@ -86,12 +89,14 @@ class UserManagerController extends Controller
|
|||
*/
|
||||
public function store(UserManagerStoreRequest $request)
|
||||
{
|
||||
$this->authorize('create', User::class);
|
||||
|
||||
$validated = $request->validated();
|
||||
|
||||
$user = User::create([
|
||||
'name' => $validated['name'],
|
||||
'email' => $validated['email'],
|
||||
'password' => Hash::make($validated['password']),
|
||||
'name' => $validated['name'],
|
||||
'email' => $validated['email'],
|
||||
'password' => Hash::make($validated['password']),
|
||||
]);
|
||||
|
||||
Log::info(sprintf('User ID #%s created by user ID #%s', $user->id, $request->user()->id));
|
||||
|
@ -118,6 +123,8 @@ class UserManagerController extends Controller
|
|||
{
|
||||
Log::info(sprintf('Deletion of all personal access tokens for User ID #%s requested by User ID #%s', $user->id, $request->user()->id));
|
||||
|
||||
$this->authorize('update', $user);
|
||||
|
||||
$tokens = $tokenRepository->forUser($user->getAuthIdentifier());
|
||||
|
||||
$tokens->load('client')->filter(function ($token) {
|
||||
|
@ -140,6 +147,8 @@ class UserManagerController extends Controller
|
|||
{
|
||||
Log::info(sprintf('Deletion of all security devices for User ID #%s requested by User ID #%s', $user->id, $request->user()->id));
|
||||
|
||||
$this->authorize('update', $user);
|
||||
|
||||
$user->flushCredentials();
|
||||
|
||||
// WebauthnOnly user options need to be reset to prevent impossible login when
|
||||
|
@ -163,6 +172,8 @@ class UserManagerController extends Controller
|
|||
*/
|
||||
public function destroy(Request $request, User $user)
|
||||
{
|
||||
$this->authorize('delete', $user);
|
||||
|
||||
// This will delete the user and all its 2FAs & Groups thanks to the onCascadeDelete constrains.
|
||||
// Deletion will not be done (and returns False) if the user is the only existing admin (see UserObserver clas)
|
||||
return $user->delete() === false
|
||||
|
@ -179,6 +190,8 @@ class UserManagerController extends Controller
|
|||
*/
|
||||
public function promote(UserManagerPromoteRequest $request, User $user)
|
||||
{
|
||||
$this->authorize('promote', $user);
|
||||
|
||||
$user->promoteToAdministrator($request->validated('is_admin'));
|
||||
$user->save();
|
||||
|
||||
|
@ -196,5 +209,4 @@ class UserManagerController extends Controller
|
|||
{
|
||||
return Password::broker();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Api\v1\Requests;
|
||||
|
||||
use App\Rules\IsValideEmailList;
|
||||
use App\Rules\IsValidEmailList;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
|
@ -28,11 +28,11 @@ class SettingUpdateRequest extends FormRequest
|
|||
$rule = [
|
||||
'value' => [
|
||||
'required',
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
if ($this->route()?->parameter('settingName') == 'restrictList') {
|
||||
$rule['value'][] = new IsValideEmailList;
|
||||
$rule['value'][] = new IsValidEmailList;
|
||||
}
|
||||
|
||||
return $rule;
|
||||
|
|
|
@ -38,7 +38,7 @@ class UserManagerResource extends UserResource
|
|||
{
|
||||
$this->resource = $resource;
|
||||
$password_reset = null;
|
||||
|
||||
|
||||
// Password reset token
|
||||
$resetToken = DB::table(config('auth.passwords.users.table'))->where(
|
||||
'email', $this->resource->getEmailForPasswordReset()
|
||||
|
@ -52,7 +52,7 @@ class UserManagerResource extends UserResource
|
|||
|
||||
// Personal Access Tokens (PATs)
|
||||
$tokenRepository = App::make(TokenRepository::class);
|
||||
$tokens = $tokenRepository->forUser($this->resource->getAuthIdentifier());
|
||||
$tokens = $tokenRepository->forUser($this->resource->getAuthIdentifier());
|
||||
|
||||
$PATs_count = $tokens->load('client')->filter(function ($token) {
|
||||
return $token->client->personal_access_client && ! $token->revoked;
|
||||
|
@ -61,10 +61,9 @@ class UserManagerResource extends UserResource
|
|||
$this->with = [
|
||||
'password_reset' => $password_reset,
|
||||
'valid_personal_access_tokens' => $PATs_count,
|
||||
'webauthn_credentials' => $this->resource->webAuthnCredentials()->count()
|
||||
'webauthn_credentials' => $this->resource->webAuthnCredentials()->count(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the token has expired.
|
||||
|
@ -75,7 +74,7 @@ class UserManagerResource extends UserResource
|
|||
protected function tokenExpired($createdAt)
|
||||
{
|
||||
// See Illuminate\Auth\Passwords\DatabaseTokenRepository
|
||||
return Carbon::parse($createdAt)->addSeconds(config('auth.passwords.users.expires', 60)*60)->isPast();
|
||||
return Carbon::parse($createdAt)->addSeconds(config('auth.passwords.users.expires', 60) * 60)->isPast();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -90,8 +89,8 @@ class UserManagerResource extends UserResource
|
|||
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)->locale(App::getLocale())->diffForHumans(),
|
||||
'created_at' => Carbon::parse($this->created_at)->locale(App::getLocale())->diffForHumans(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -137,7 +137,7 @@ class Install extends Command
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Runs the passport:install command silently
|
||||
*/
|
||||
protected function installPassport() : void
|
||||
{
|
||||
|
@ -147,7 +147,7 @@ class Install extends Command
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Runs the config:cache command silently
|
||||
*/
|
||||
protected function cacheConfig() : void
|
||||
{
|
||||
|
@ -157,11 +157,11 @@ class Install extends Command
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Runs the storage:link command silently
|
||||
*/
|
||||
protected function createStorageLink() : void
|
||||
{
|
||||
if (!file_exists(public_path('storage'))) {
|
||||
if (! file_exists(public_path('storage'))) {
|
||||
$this->components->task('Creating storage link', function () : void {
|
||||
$this->callSilently('storage:link');
|
||||
});
|
||||
|
@ -169,7 +169,7 @@ class Install extends Command
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Lets the user set the main environment variables
|
||||
*/
|
||||
protected function setMainEnvVars() : void
|
||||
{
|
||||
|
@ -177,8 +177,7 @@ class Install extends Command
|
|||
$appUrl = trim($this->ask('URL of this 2FAuth instance', config('app.url')), '/');
|
||||
if (filter_var($appUrl, FILTER_VALIDATE_URL)) {
|
||||
break;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$this->components->error('This is not a valid URL, please retry');
|
||||
}
|
||||
}
|
||||
|
@ -195,7 +194,7 @@ class Install extends Command
|
|||
}
|
||||
|
||||
/**
|
||||
* Prompt user for valid database credentials and set them to .env file.
|
||||
* Prompts user for valid database credentials and sets them to .env file.
|
||||
*/
|
||||
protected function setDbEnvVars() : void
|
||||
{
|
||||
|
@ -253,7 +252,7 @@ class Install extends Command
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Runs db migration with --force option
|
||||
*/
|
||||
protected function migrateDatabase() : mixed
|
||||
{
|
||||
|
@ -265,7 +264,7 @@ class Install extends Command
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Clears some caches
|
||||
*/
|
||||
protected function clearCaches() : void
|
||||
{
|
||||
|
@ -276,7 +275,7 @@ class Install extends Command
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Loads the existing env file or creates it
|
||||
*/
|
||||
protected function loadEnvFile() : void
|
||||
{
|
||||
|
@ -300,7 +299,7 @@ class Install extends Command
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Generates an app key if necessary
|
||||
*/
|
||||
protected function maybeGenerateAppKey() : void
|
||||
{
|
||||
|
@ -317,7 +316,7 @@ class Install extends Command
|
|||
}
|
||||
|
||||
/**
|
||||
* Generate a random key for the application.
|
||||
* Generates a random key for the application.
|
||||
*/
|
||||
protected function generateRandomKey() : string
|
||||
{
|
||||
|
|
|
@ -16,7 +16,7 @@ class WebauthnTwoFAuthUserProvider extends WebAuthnUserProvider
|
|||
public function validateCredentials($user, array $credentials) : bool
|
||||
{
|
||||
if ($user instanceof WebAuthnAuthenticatable && $this->isSignedChallenge($credentials)) {
|
||||
return $this->validateWebAuthn();
|
||||
return $this->validateWebAuthn($user);
|
||||
}
|
||||
|
||||
// If the user disabled the fallback, we will validate the credential password.
|
||||
|
|
|
@ -8,7 +8,6 @@ use App\Http\Requests\UserDeleteRequest;
|
|||
use App\Http\Requests\UserUpdateRequest;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
|
|
|
@ -10,11 +10,10 @@ use Illuminate\Contracts\Support\Responsable;
|
|||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Lang;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Laragear\WebAuthn\Enums\UserVerification;
|
||||
use Laragear\WebAuthn\Http\Requests\AssertionRequest;
|
||||
use Laragear\WebAuthn\WebAuthn;
|
||||
|
||||
class WebAuthnLoginController extends Controller
|
||||
{
|
||||
|
@ -44,10 +43,10 @@ class WebAuthnLoginController extends Controller
|
|||
public function options(AssertionRequest $request) : Responsable|JsonResponse
|
||||
{
|
||||
switch (config('webauthn.user_verification')) {
|
||||
case WebAuthn::USER_VERIFICATION_DISCOURAGED:
|
||||
case UserVerification::DISCOURAGED:
|
||||
$request = $request->fastLogin(); // Makes the authenticator to only check for user presence on registration
|
||||
break;
|
||||
case WebAuthn::USER_VERIFICATION_REQUIRED:
|
||||
case UserVerification::REQUIRED:
|
||||
$request = $request->secureLogin(); // Makes the authenticator to always verify the user thoroughly on registration
|
||||
break;
|
||||
}
|
||||
|
@ -88,17 +87,6 @@ class WebAuthnLoginController extends Controller
|
|||
return $this->sendLockoutResponse($request);
|
||||
}
|
||||
|
||||
if ($request->has('response')) {
|
||||
$response = $request->response;
|
||||
|
||||
// Some authenticators do not send a userHandle so we hack the response to be compliant
|
||||
// with Laragear\WebAuthn implementation that waits for a userHandle
|
||||
if (! Arr::exists($response, 'userHandle') || blank($response['userHandle'])) {
|
||||
$response['userHandle'] = User::getFromCredentialId($request->id)?->userHandle();
|
||||
$request->merge(['response' => $response]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->attemptLogin($request)) {
|
||||
return $this->sendLoginResponse($request);
|
||||
}
|
||||
|
|
|
@ -6,9 +6,9 @@ use App\Http\Controllers\Controller;
|
|||
use Illuminate\Contracts\Support\Responsable;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Laragear\WebAuthn\Enums\UserVerification;
|
||||
use Laragear\WebAuthn\Http\Requests\AttestationRequest;
|
||||
use Laragear\WebAuthn\Http\Requests\AttestedRequest;
|
||||
use Laragear\WebAuthn\WebAuthn;
|
||||
|
||||
class WebAuthnRegisterController extends Controller
|
||||
{
|
||||
|
@ -18,10 +18,10 @@ class WebAuthnRegisterController extends Controller
|
|||
public function options(AttestationRequest $request) : Responsable
|
||||
{
|
||||
switch (config('webauthn.user_verification')) {
|
||||
case WebAuthn::USER_VERIFICATION_DISCOURAGED:
|
||||
case UserVerification::DISCOURAGED:
|
||||
$request = $request->fastRegistration(); // Makes the authenticator to only check for user presence on registration
|
||||
break;
|
||||
case WebAuthn::USER_VERIFICATION_REQUIRED:
|
||||
case UserVerification::REQUIRED:
|
||||
$request = $request->secureRegistration(); // Makes the authenticator to always verify the user thoroughly on registration
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -109,6 +109,4 @@ class SystemController extends Controller
|
|||
|
||||
return response()->json(['exit-code' => $exitCode], 200);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
namespace App\Listeners;
|
||||
|
||||
use Illuminate\Notifications\Events\NotificationSent;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class LogNotification
|
||||
|
|
|
@ -4,7 +4,6 @@ namespace App\Models\Traits;
|
|||
|
||||
use App\Notifications\WebauthnRecoveryNotification;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @see \App\Models\WebAuthnAuthenticatable
|
||||
|
@ -12,20 +11,6 @@ use Illuminate\Support\Str;
|
|||
*/
|
||||
trait WebAuthnManageCredentials
|
||||
{
|
||||
/**
|
||||
* Return the handle used to identify his credentials.
|
||||
*/
|
||||
public function userHandle() : string
|
||||
{
|
||||
// Laragear\WebAuthn uses Ramsey\Uuid\Uuid::fromString()->getHex()->toString()
|
||||
// to obtain a UUID v4 with dashes removed and uses it as user_id (aka userHandle)
|
||||
// see https://github.com/ramsey/uuid/blob/4.x/src/Uuid.php#L379
|
||||
// and Laragear\WebAuthn\Assertion\Validator\Pipes\CheckCredentialIsForUser::validateId()
|
||||
|
||||
return $this->webAuthnCredentials()->value('user_id')
|
||||
?? str_replace('-', '', Str::uuid()->toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a new alias for a given WebAuthn credential.
|
||||
*/
|
||||
|
|
|
@ -9,7 +9,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use Laragear\WebAuthn\WebAuthnAuthentication;
|
||||
use Laravel\Passport\HasApiTokens;
|
||||
|
@ -92,7 +91,7 @@ class User extends Authenticatable implements WebAuthnAuthenticatable
|
|||
/**
|
||||
* Determine if the user is an administrator.
|
||||
*
|
||||
* @return boolean
|
||||
* @return bool
|
||||
*/
|
||||
public function isAdministrator()
|
||||
{
|
||||
|
@ -102,7 +101,6 @@ class User extends Authenticatable implements WebAuthnAuthenticatable
|
|||
/**
|
||||
* Grant administrator permissions to the user.
|
||||
*
|
||||
* @param bool $promote
|
||||
* @return void
|
||||
*/
|
||||
public function promoteToAdministrator(bool $promote = true)
|
||||
|
|
|
@ -6,11 +6,6 @@ use Laragear\WebAuthn\Contracts\WebAuthnAuthenticatable as Authenticatable;
|
|||
|
||||
interface WebAuthnAuthenticatable extends Authenticatable
|
||||
{
|
||||
/**
|
||||
* Return the handle used to identify his credentials.
|
||||
*/
|
||||
public function userHandle() : string;
|
||||
|
||||
/**
|
||||
* Saves a new alias for a given WebAuthn credential.
|
||||
*/
|
||||
|
|
|
@ -9,7 +9,6 @@ use Illuminate\Support\Facades\Lang;
|
|||
|
||||
class TestEmailSettingNotification extends Notification
|
||||
{
|
||||
|
||||
// /**
|
||||
// * The callback that should be used to create the reset password URL.
|
||||
// *
|
||||
|
|
|
@ -13,16 +13,20 @@ class UserObserver
|
|||
{
|
||||
/**
|
||||
* Handle the User "created" event.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function created(User $user): void
|
||||
public function created(User $user) : void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the User "updated" event.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function updated(User $user): void
|
||||
public function updated(User $user) : void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
@ -30,7 +34,7 @@ class UserObserver
|
|||
/**
|
||||
* Handle the User "deleting" event.
|
||||
*/
|
||||
public function deleting(User $user): bool
|
||||
public function deleting(User $user) : bool
|
||||
{
|
||||
Log::info(sprintf('Deletion of User ID #%s requested by User ID #%s', $user->id, Auth::user()->id ?? 'unknown'));
|
||||
|
||||
|
@ -39,7 +43,7 @@ class UserObserver
|
|||
|
||||
if ($isLastAdmin) {
|
||||
Log::notice(sprintf('Deletion of user ID #%s refused, cannot delete the only administrator', $user->id));
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -57,7 +61,7 @@ class UserObserver
|
|||
/**
|
||||
* Handle the User "deleted" event.
|
||||
*/
|
||||
public function deleted(User $user): void
|
||||
public function deleted(User $user) : void
|
||||
{
|
||||
// DB has cascade delete enabled to flush 2FA and Groups but,
|
||||
// for an unknown reason, SQLite refuses to delete these related.
|
||||
|
@ -81,16 +85,20 @@ class UserObserver
|
|||
|
||||
/**
|
||||
* Handle the User "restored" event.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function restored(User $user): void
|
||||
public function restored(User $user) : void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the User "force deleted" event.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function forceDeleted(User $user): void
|
||||
public function forceDeleted(User $user) : void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
|
|
@ -12,19 +12,19 @@ class UserPolicy
|
|||
/**
|
||||
* Perform pre-authorization checks.
|
||||
*/
|
||||
public function before(User $user, string $ability): bool|null
|
||||
public function before(User $user, string $ability) : ?bool
|
||||
{
|
||||
if ($user->isAdministrator()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
public function viewAny(User $user) : bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ class UserPolicy
|
|||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*/
|
||||
public function view(User $user, User $model): bool
|
||||
public function view(User $user, User $model) : bool
|
||||
{
|
||||
$can = $this->isHimself($user, $model);
|
||||
|
||||
|
@ -46,7 +46,7 @@ class UserPolicy
|
|||
/**
|
||||
* Determine whether the user can create models.
|
||||
*/
|
||||
public function create(?User $user): bool
|
||||
public function create(?User $user) : bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ class UserPolicy
|
|||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*/
|
||||
public function update(User $user, User $model): bool
|
||||
public function update(User $user, User $model) : bool
|
||||
{
|
||||
$can = $this->isHimself($user, $model);
|
||||
|
||||
|
@ -68,7 +68,7 @@ class UserPolicy
|
|||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*/
|
||||
public function delete(User $user, User $model): bool
|
||||
public function delete(User $user, User $model) : bool
|
||||
{
|
||||
$can = $this->isHimself($user, $model);
|
||||
|
||||
|
@ -78,4 +78,12 @@ class UserPolicy
|
|||
|
||||
return $can;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can promote the model.
|
||||
*/
|
||||
public function promote(User $user) : bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Laravel\Passport\Console\ClientCommand;
|
||||
|
|
|
@ -50,12 +50,12 @@ class EventServiceProvider extends ServiceProvider
|
|||
LogNotification::class,
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* The model observers for your application.
|
||||
*
|
||||
* @var array<string, string|object|array<int, string|object>>
|
||||
*/
|
||||
* The model observers for your application.
|
||||
*
|
||||
* @var array<string, string|object|array<int, string|object>>
|
||||
*/
|
||||
protected $observers = [
|
||||
User::class => [UserObserver::class],
|
||||
];
|
||||
|
|
|
@ -11,11 +11,11 @@ class ComplyWithEmailRestrictionPolicy implements ValidationRule
|
|||
/**
|
||||
* Run the validation rule.
|
||||
*/
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
public function validate(string $attribute, mixed $value, Closure $fail) : void
|
||||
{
|
||||
$list = Settings::get('restrictList');
|
||||
$regex = Settings::get('restrictRule');
|
||||
|
||||
|
||||
$validatesFilter = true;
|
||||
$validatesRegex = true;
|
||||
|
||||
|
@ -31,8 +31,7 @@ class ComplyWithEmailRestrictionPolicy implements ValidationRule
|
|||
if (! $validatesFilter && ! $validatesRegex) {
|
||||
$fail('validation.custom.email.ComplyWithEmailRestrictionPolicy')->translate();
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (! $validatesFilter || ! $validatesRegex) {
|
||||
$fail('validation.custom.email.ComplyWithEmailRestrictionPolicy')->translate();
|
||||
}
|
||||
|
|
|
@ -6,12 +6,12 @@ use Closure;
|
|||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class IsValideEmailList implements ValidationRule
|
||||
class IsValidEmailList implements ValidationRule
|
||||
{
|
||||
/**
|
||||
* Run the validation rule.
|
||||
*/
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
public function validate(string $attribute, mixed $value, Closure $fail) : void
|
||||
{
|
||||
$emails = explode('|', $value);
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
"guzzlehttp/guzzle": "^7.2",
|
||||
"jackiedo/dotenv-editor": "^2.1",
|
||||
"khanamiryan/qrcode-detector-decoder": "^2.0.2",
|
||||
"laragear/webauthn": "^1.2.0",
|
||||
"laragear/webauthn": "^2.0",
|
||||
"laravel/framework": "^10.10",
|
||||
"laravel/passport": "^11.2",
|
||||
"laravel/socialite": "^5.10",
|
||||
|
@ -40,6 +40,7 @@
|
|||
},
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-ide-helper": "^2.13",
|
||||
"brianium/paratest": "^7.3",
|
||||
"fakerphp/faker": "^1.21",
|
||||
"larastan/larastan": "^2.0",
|
||||
"laravel/pint": "^1.6",
|
||||
|
@ -89,7 +90,7 @@
|
|||
],
|
||||
"test": [
|
||||
"php artisan config:clear",
|
||||
"vendor/bin/phpunit"
|
||||
"php artisan test --parallel"
|
||||
],
|
||||
"test-mysql": [
|
||||
"php artisan config:clear",
|
||||
|
@ -100,4 +101,4 @@
|
|||
"vendor/bin/phpunit --coverage-html tests/Coverage/"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -28,6 +28,9 @@ return new class extends Migration
|
|||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('oauth_id');
|
||||
});
|
||||
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('oauth_provider');
|
||||
});
|
||||
}
|
||||
|
|
|
@ -91,14 +91,12 @@ services:
|
|||
# Custom logout URL to open when using an auth proxy.
|
||||
- PROXY_LOGOUT_URL=null
|
||||
# WebAuthn settings
|
||||
# Relying Party name, aka the name of the application. If null, defaults to APP_NAME
|
||||
# Relying Party name, aka the name of the application. If blank, defaults to APP_NAME. Do not set to null.
|
||||
- WEBAUTHN_NAME=2FAuth
|
||||
# Relying Party ID. If null, the device will fill it internally.
|
||||
# Relying Party ID, should equal the site domain (i.e 2fauth.example.com).
|
||||
# If null, the device will fill it internally (recommended)
|
||||
# See https://webauthn-doc.spomky-labs.com/prerequisites/the-relying-party#how-to-determine-the-relying-party-id
|
||||
- WEBAUTHN_ID=null
|
||||
# Optional image data in BASE64 (128 bytes maximum) or an image url
|
||||
# See https://webauthn-doc.spomky-labs.com/prerequisites/the-relying-party#relying-party-icon
|
||||
- WEBAUTHN_ICON=null
|
||||
# Use this setting to control how user verification behave during the
|
||||
# WebAuthn authentication flow.
|
||||
#
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -13,7 +13,7 @@
|
|||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.3",
|
||||
"@kyvg/vue3-notification": "^3.0.2",
|
||||
"@vitejs/plugin-vue": "^4.3.4",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vueuse/components": "^10.4.1",
|
||||
"@vueuse/core": "^10.4.1",
|
||||
"@vueuse/integrations": "^10.6.1",
|
||||
|
@ -21,14 +21,14 @@
|
|||
"bulma": "^0.9.4",
|
||||
"bulma-checkradio": "^2.1.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"laravel-vite-plugin": "^0.8.0",
|
||||
"laravel-vite-plugin": "^1.0.2",
|
||||
"laravel-vue-i18n": "^2.7.1",
|
||||
"php-parser": "^3.1.5",
|
||||
"pinia": "^2.1.6",
|
||||
"sass": "^1.67.0",
|
||||
"sortablejs": "^1.15.0",
|
||||
"unplugin-auto-import": "^0.16.6",
|
||||
"vite": "^4.4.9",
|
||||
"vite": "^5.2.7",
|
||||
"vue": "^3.3.4",
|
||||
"vue-qrcode-reader": "^5.4.0",
|
||||
"vue-router": "^4.2.4"
|
||||
|
|
|
@ -11,5 +11,22 @@
|
|||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"theme_color": "#242424"
|
||||
"theme_color": "#242424",
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "New 2FA",
|
||||
"url": "/start",
|
||||
"description": "Add a new 2FA account by flashing a QR code or filling out a form"
|
||||
},
|
||||
{
|
||||
"name": "Import 2FAs",
|
||||
"url": "/account/import",
|
||||
"description": "Import 2FA accounts previously exported from another 2FA app"
|
||||
},
|
||||
{
|
||||
"name": "Settings",
|
||||
"url": "/settings/options",
|
||||
"description": "Manage your 2FAuth user settings"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -5,8 +5,6 @@
|
|||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const { inputId } = useIdGenerator(props.inputType, props.fieldName)
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: [String, Number, Boolean],
|
||||
label: {
|
||||
|
@ -47,6 +45,8 @@
|
|||
leftIcon: '',
|
||||
rightIcon: '',
|
||||
})
|
||||
|
||||
const { inputId } = useIdGenerator(props.inputType, props.fieldName)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -5,9 +5,7 @@
|
|||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const { inputId } = useIdGenerator(props.inputType, props.fieldName)
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: String,
|
||||
modelModifiers: { default: () => ({}) },
|
||||
|
@ -55,6 +53,8 @@
|
|||
}
|
||||
})
|
||||
|
||||
const { inputId } = useIdGenerator(props.inputType, props.fieldName)
|
||||
|
||||
const fieldIsLocked = ref(props.isDisabled || props.isEditMode)
|
||||
const hasBeenTrimmed = ref(false)
|
||||
const componentKey = ref(0);
|
||||
|
|
|
@ -5,10 +5,6 @@
|
|||
inheritAttrs: true
|
||||
})
|
||||
|
||||
const { inputId } = useIdGenerator(props.inputType, props.fieldName)
|
||||
const currentType = ref(props.inputType)
|
||||
const hasCapsLockOn = ref(false)
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: [String],
|
||||
label: {
|
||||
|
@ -47,6 +43,10 @@
|
|||
},
|
||||
})
|
||||
|
||||
const { inputId } = useIdGenerator(props.inputType, props.fieldName)
|
||||
const currentType = ref(props.inputType)
|
||||
const hasCapsLockOn = ref(false)
|
||||
|
||||
const hasLowerCase = computed(() => {
|
||||
return /[a-z]/.test(props.modelValue)
|
||||
})
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const { inputId } = useIdGenerator(props.inputType, props.fieldName)
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: [String, Number, Boolean],
|
||||
label: {
|
||||
|
@ -45,6 +43,8 @@
|
|||
},
|
||||
isIndented: Boolean,
|
||||
})
|
||||
|
||||
const { inputId } = useIdGenerator(props.inputType, props.fieldName)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -42,13 +42,13 @@ return [
|
|||
'registered_on_date' => 'Registered :date',
|
||||
'updated_on_date' => 'Updated :date',
|
||||
'access' => 'Access',
|
||||
'password_requested_on_t' => 'A password reset request exists for this user (request sent at :datetime) meaning the user didn\'t change its password yet but the link he received is still valid. This could be a request from the user himself or from an administrator.',
|
||||
'password_request_expired' => 'A password reset request exists for this user but has expired, meaning the user didn\'t change its password in time. This could be a request from the user himself or from an administrator.',
|
||||
'password_requested_on_t' => 'A password reset request exists for this user (request sent at :datetime), which means that the user has not yet changed their password but the link they received is still valid. This may be a request from the user themselves or from an administrator.',
|
||||
'password_request_expired' => 'A password reset request exists for this user but has expired, meaning that the user has not changed their password in time. This may be a request from the user themselves or from an administrator.',
|
||||
'resend_email' => 'Resend email',
|
||||
'resend_email_title' => 'Resend a password reset email to the user',
|
||||
'resend_email_help' => 'Use <b>Resend email</b> to send a new password reset email to the user so he can set a new password. This will leave its current password as is and any previous request will be revoked.',
|
||||
'reset_password' => 'Reset password',
|
||||
'reset_password_help' => 'Use <b>Reset password</b> to force a password reset (this will set a temporary password) before sending a password reset email to the user so he can set a new password. Any previous request will be revoked.',
|
||||
'reset_password_help' => 'Use <b>Reset password</b> to force a password reset (this will set a temporary password) before sending a password reset email to the user so they can set a new password. Any previous request will be revoked.',
|
||||
'reset_password_title' => 'Reset the user\'s password',
|
||||
'password_successfully_reset' => 'Password successfully reset',
|
||||
'user_has_x_active_pat' => ':count active token(s)',
|
||||
|
|
|
@ -32,7 +32,7 @@ Route::group(['middleware' => 'auth:api-guard'], function () {
|
|||
Route::get('user/preferences/{preferenceName}', [UserController::class, 'showPreference'])->name('user.preferences.show');
|
||||
Route::get('user/preferences', [UserController::class, 'allPreferences'])->name('user.preferences.all');
|
||||
Route::put('user/preferences/{preferenceName}', [UserController::class, 'setPreference'])->name('user.preferences.set');
|
||||
|
||||
|
||||
Route::delete('twofaccounts', [TwoFAccountController::class, 'batchDestroy'])->name('twofaccounts.batchDestroy');
|
||||
Route::patch('twofaccounts/withdraw', [TwoFAccountController::class, 'withdraw'])->name('twofaccounts.withdraw');
|
||||
Route::post('twofaccounts/reorder', [TwoFAccountController::class, 'reorder'])->name('twofaccounts.reorder');
|
||||
|
|
|
@ -76,7 +76,7 @@ Route::group(['middleware' => ['behind-auth', 'rejectIfReverseProxy']], function
|
|||
/**
|
||||
* Routes protected by an authentication guard and restricted to administrators
|
||||
*/
|
||||
Route::group(['middleware' => ['behind-auth', 'admin']], function () {
|
||||
Route::group(['middleware' => ['behind-auth', 'admin']], function () {
|
||||
Route::get('system/infos', [SystemController::class, 'infos'])->name('system.infos');
|
||||
Route::post('system/test-email', [SystemController::class, 'testEmail'])->name('system.testEmail');
|
||||
});
|
||||
|
|
|
@ -95,11 +95,11 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_index_succeeds_and_returns_UserManagerResource(): void
|
||||
public function test_index_succeeds_and_returns_UserManagerResource() : void
|
||||
{
|
||||
$path = '/api/v1/users';
|
||||
$path = '/api/v1/users';
|
||||
$resources = UserManagerResource::collection(User::all());
|
||||
$request = Request::create($path, 'GET');
|
||||
$request = Request::create($path, 'GET');
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', $path)
|
||||
|
@ -121,11 +121,11 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_show_returns_UserManagerResource(): void
|
||||
public function test_show_returns_UserManagerResource() : void
|
||||
{
|
||||
$path = '/api/v1/users/' . $this->user->id;
|
||||
$path = '/api/v1/users/' . $this->user->id;
|
||||
$resources = UserManagerResource::make($this->user);
|
||||
$request = Request::create($path, 'GET');
|
||||
$request = Request::create($path, 'GET');
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('GET', $path)
|
||||
|
@ -140,7 +140,7 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
Notification::fake();
|
||||
|
||||
DB::table(config('auth.passwords.users.table'))->delete();
|
||||
$user = User::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
$oldPassword = $user->password;
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
|
@ -165,9 +165,9 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
{
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
$path = '/api/v1/users/' . $user->id . '/password/reset';
|
||||
$request = Request::create($path, 'PATCH');
|
||||
$user = User::factory()->create();
|
||||
$path = '/api/v1/users/' . $user->id . '/password/reset';
|
||||
$request = Request::create($path, 'PATCH');
|
||||
|
||||
$response = $this->actingAs($this->admin, 'api-guard')
|
||||
->json('PATCH', $path);
|
||||
|
@ -201,7 +201,7 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
'message',
|
||||
'reason',
|
||||
]);
|
||||
|
||||
|
||||
Notification::assertNothingSent();
|
||||
}
|
||||
|
||||
|
@ -232,7 +232,7 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
'message',
|
||||
'reason',
|
||||
]);
|
||||
|
||||
|
||||
Notification::assertNothingSent();
|
||||
}
|
||||
|
||||
|
@ -247,10 +247,10 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
'email' => self::EMAIL,
|
||||
'password' => self::PASSWORD,
|
||||
'password_confirmation' => self::PASSWORD,
|
||||
'is_admin' => false
|
||||
'is_admin' => false,
|
||||
])
|
||||
->assertCreated();
|
||||
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'name' => self::USERNAME,
|
||||
'email' => self::EMAIL,
|
||||
|
@ -260,18 +260,18 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_store_returns_UserManagerResource_of_created_user(): void
|
||||
public function test_store_returns_UserManagerResource_of_created_user() : void
|
||||
{
|
||||
$path = '/api/v1/users';
|
||||
$userDefinition = (new UserFactory)->definition();
|
||||
$path = '/api/v1/users';
|
||||
$userDefinition = (new UserFactory)->definition();
|
||||
$userDefinition['password_confirmation'] = $userDefinition['password'];
|
||||
$request = Request::create($path, 'POST');
|
||||
$request = Request::create($path, 'POST');
|
||||
|
||||
$response = $this->actingAs($this->admin, 'api-guard')
|
||||
->json('POST', $path, $userDefinition)
|
||||
->assertCreated();
|
||||
|
||||
$user = User::where('email', $userDefinition['email'])->first();
|
||||
|
||||
$user = User::where('email', $userDefinition['email'])->first();
|
||||
$resource = UserManagerResource::make($user);
|
||||
|
||||
$response->assertExactJson($resource->response($request)->getData(true));
|
||||
|
@ -280,19 +280,19 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_store_returns_UserManagerResource_of_created_admin(): void
|
||||
public function test_store_returns_UserManagerResource_of_created_admin() : void
|
||||
{
|
||||
$path = '/api/v1/users';
|
||||
$userDefinition = (new UserFactory)->definition();
|
||||
$userDefinition['is_admin'] = true;
|
||||
$path = '/api/v1/users';
|
||||
$userDefinition = (new UserFactory)->definition();
|
||||
$userDefinition['is_admin'] = true;
|
||||
$userDefinition['password_confirmation'] = $userDefinition['password'];
|
||||
$request = Request::create($path, 'POST');
|
||||
$request = Request::create($path, 'POST');
|
||||
|
||||
$response = $this->actingAs($this->admin, 'api-guard')
|
||||
->json('POST', $path, $userDefinition)
|
||||
->assertCreated();
|
||||
|
||||
$user = User::where('email', $userDefinition['email'])->first();
|
||||
|
||||
$user = User::where('email', $userDefinition['email'])->first();
|
||||
$resource = UserManagerResource::make($user);
|
||||
|
||||
$response->assertExactJson($resource->response($request)->getData(true));
|
||||
|
@ -310,10 +310,10 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
'name' => 'RandomTokenName',
|
||||
])
|
||||
->assertOk();
|
||||
|
||||
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('DELETE', '/api/v1/users/' . $this->user->id . '/pats');
|
||||
|
||||
|
||||
$tokens = $tokenRepository->forUser($this->user->getAuthIdentifier());
|
||||
$tokens = $tokens->load('client')->filter(function ($token) {
|
||||
return $token->client->personal_access_client && ! $token->revoked;
|
||||
|
@ -423,7 +423,7 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
->assertNoContent();
|
||||
|
||||
$this->user->refresh();
|
||||
|
||||
|
||||
$this->assertFalse($this->user->preferences['useWebauthnOnly']);
|
||||
}
|
||||
|
||||
|
@ -452,30 +452,30 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_promote_changes_admin_status(): void
|
||||
public function test_promote_changes_admin_status() : void
|
||||
{
|
||||
$this->actingAs($this->admin, 'api-guard')
|
||||
->json('PATCH', '/api/v1/users/' . $this->user->id . '/promote', [
|
||||
'is_admin' => true
|
||||
'is_admin' => true,
|
||||
])
|
||||
->assertOk();
|
||||
|
||||
$this->user->refresh();
|
||||
|
||||
|
||||
$this->assertTrue($this->user->isAdministrator());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_promote_returns_UserManagerResource(): void
|
||||
public function test_promote_returns_UserManagerResource() : void
|
||||
{
|
||||
$path = '/api/v1/users/' . $this->user->id . '/promote';
|
||||
$request = Request::create($path, 'PUT');
|
||||
$path = '/api/v1/users/' . $this->user->id . '/promote';
|
||||
$request = Request::create($path, 'PUT');
|
||||
|
||||
$response = $this->actingAs($this->admin, 'api-guard')
|
||||
->json('PATCH', $path, [
|
||||
'is_admin' => true
|
||||
'is_admin' => true,
|
||||
]);
|
||||
|
||||
$this->user->refresh();
|
||||
|
@ -483,6 +483,4 @@ class UserManagerControllerTest extends FeatureTestCase
|
|||
|
||||
$response->assertExactJson($resources->response($request)->getData(true));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Console;
|
||||
|
||||
use App\Console\Commands\Install;
|
||||
use Jackiedo\DotenvEditor\DotenvEditor;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Tests\FeatureTestCase;
|
||||
|
||||
/**
|
||||
* InstallTest test class
|
||||
*/
|
||||
#[CoversClass(Install::class)]
|
||||
class InstallTest extends FeatureTestCase
|
||||
{
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_install_completes()
|
||||
{
|
||||
$this->artisan('2fauth:install')
|
||||
->expectsConfirmation('Existing .env file found. Do you wish to review its vars?', 'no')
|
||||
->assertSuccessful();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_install_informs_about_no_interaction()
|
||||
{
|
||||
$this->artisan('2fauth:install', ['--no-interaction' => true])
|
||||
->expectsOutput('(Running in no-interaction mode)')
|
||||
->expectsConfirmation('Existing .env file found. Do you wish to review its vars?', 'no')
|
||||
->assertSuccessful();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_install_generates_an_app_key()
|
||||
{
|
||||
config(['app.key' => '']);
|
||||
|
||||
$this->assertEquals('', config('app.key'));
|
||||
|
||||
$this->artisan('2fauth:install')
|
||||
->expectsConfirmation('Existing .env file found. Do you wish to review its vars?', 'no')
|
||||
->assertSuccessful();
|
||||
|
||||
$this->assertNotEquals('', config('app.key'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_install_gives_2fauth_address()
|
||||
{
|
||||
$this->artisan('2fauth:install')
|
||||
->expectsConfirmation('Existing .env file found. Do you wish to review its vars?', 'no')
|
||||
->expectsOutputToContain(config('app.url'))
|
||||
->assertSuccessful();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_install_informs_about_sponsoring()
|
||||
{
|
||||
$this->artisan('2fauth:install')
|
||||
->expectsConfirmation('Existing .env file found. Do you wish to review its vars?', 'no')
|
||||
->expectsOutputToContain('https://ko-fi.com/bubka')
|
||||
->expectsOutputToContain('https://github.com/sponsors/Bubka')
|
||||
->assertSuccessful();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_install_fails_with_exception_message()
|
||||
{
|
||||
$mock = $this->mock(DotenvEditor::class);
|
||||
$mock->shouldReceive('load')
|
||||
->andThrow(new \Exception('exception message'));
|
||||
|
||||
$this->artisan('2fauth:install')
|
||||
->expectsOutputToContain('exception message')
|
||||
->assertFailed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_install_fails_with_link_to_online_help()
|
||||
{
|
||||
$mock = $this->mock(DotenvEditor::class);
|
||||
$mock->shouldReceive('load')
|
||||
->andThrow(new \Exception());
|
||||
|
||||
$this->artisan('2fauth:install')
|
||||
->expectsOutputToContain(config('2fauth.installDocUrl'))
|
||||
->assertFailed();
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ use App\Facades\Settings;
|
|||
use App\Http\Controllers\Auth\RegisterController;
|
||||
use App\Http\Requests\UserStoreRequest;
|
||||
use App\Models\User;
|
||||
use App\Rules\ComplyWithEmailRestrictionPolicy;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Tests\FeatureTestCase;
|
||||
|
@ -15,6 +16,7 @@ use Tests\FeatureTestCase;
|
|||
*/
|
||||
#[CoversClass(RegisterController::class)]
|
||||
#[CoversClass(UserStoreRequest::class)]
|
||||
#[CoversClass(ComplyWithEmailRestrictionPolicy::class)]
|
||||
class RegisterControllerTest extends FeatureTestCase
|
||||
{
|
||||
private const USERNAME = 'john doe';
|
||||
|
@ -171,7 +173,7 @@ class RegisterControllerTest extends FeatureTestCase
|
|||
'password' => self::PASSWORD,
|
||||
'password_confirmation' => self::PASSWORD,
|
||||
])
|
||||
->assertStatus(201);
|
||||
->assertStatus(201);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -189,7 +191,7 @@ class RegisterControllerTest extends FeatureTestCase
|
|||
'password' => self::PASSWORD,
|
||||
'password_confirmation' => self::PASSWORD,
|
||||
])
|
||||
->assertStatus(422);
|
||||
->assertStatus(422);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -207,7 +209,7 @@ class RegisterControllerTest extends FeatureTestCase
|
|||
'password' => self::PASSWORD,
|
||||
'password_confirmation' => self::PASSWORD,
|
||||
])
|
||||
->assertStatus(201);
|
||||
->assertStatus(201);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -225,7 +227,7 @@ class RegisterControllerTest extends FeatureTestCase
|
|||
'password' => self::PASSWORD,
|
||||
'password_confirmation' => self::PASSWORD,
|
||||
])
|
||||
->assertStatus(422);
|
||||
->assertStatus(422);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -243,7 +245,7 @@ class RegisterControllerTest extends FeatureTestCase
|
|||
'password' => self::PASSWORD,
|
||||
'password_confirmation' => self::PASSWORD,
|
||||
])
|
||||
->assertStatus(201);
|
||||
->assertStatus(201);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -261,6 +263,6 @@ class RegisterControllerTest extends FeatureTestCase
|
|||
'password' => self::PASSWORD,
|
||||
'password_confirmation' => self::PASSWORD,
|
||||
])
|
||||
->assertStatus(201);
|
||||
->assertStatus(201);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ use App\Models\User;
|
|||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laragear\WebAuthn\Assertion\Validator\AssertionValidator;
|
||||
use Laragear\WebAuthn\WebAuthn;
|
||||
use Laragear\WebAuthn\Enums\UserVerification;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Tests\FeatureTestCase;
|
||||
|
||||
|
@ -369,7 +369,7 @@ class WebAuthnLoginControllerTest extends FeatureTestCase
|
|||
*/
|
||||
public function test_get_options_returns_success()
|
||||
{
|
||||
Config::set('webauthn.user_verification', WebAuthn::USER_VERIFICATION_PREFERRED);
|
||||
Config::set('webauthn.user_verification', UserVerification::PREFERRED);
|
||||
|
||||
$this->user = User::factory()->create(['email' => self::EMAIL]);
|
||||
|
||||
|
@ -409,7 +409,7 @@ class WebAuthnLoginControllerTest extends FeatureTestCase
|
|||
*/
|
||||
public function test_get_options_for_securelogin_returns_required_userVerification()
|
||||
{
|
||||
Config::set('webauthn.user_verification', WebAuthn::USER_VERIFICATION_REQUIRED);
|
||||
Config::set('webauthn.user_verification', UserVerification::REQUIRED);
|
||||
|
||||
$this->user = User::factory()->create(['email' => self::EMAIL]);
|
||||
|
||||
|
@ -451,7 +451,7 @@ class WebAuthnLoginControllerTest extends FeatureTestCase
|
|||
*/
|
||||
public function test_get_options_for_fastlogin_returns_discouraged_userVerification()
|
||||
{
|
||||
Config::set('webauthn.user_verification', WebAuthn::USER_VERIFICATION_DISCOURAGED);
|
||||
Config::set('webauthn.user_verification', UserVerification::DISCOURAGED);
|
||||
|
||||
$this->user = User::factory()->create(['email' => self::EMAIL]);
|
||||
|
||||
|
|
|
@ -166,10 +166,10 @@ class WebAuthnRecoveryControllerTest extends FeatureTestCase
|
|||
'email' => $this->user->email,
|
||||
'password' => UserFactory::USER_PASSWORD,
|
||||
])
|
||||
->assertStatus(200);
|
||||
->assertStatus(200);
|
||||
|
||||
$this->user->refresh();
|
||||
|
||||
|
||||
$this->assertFalse($this->user->preferences['useWebauthnOnly']);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,10 +5,10 @@ namespace Tests\Feature\Http\Auth;
|
|||
use App\Http\Controllers\Auth\WebAuthnRegisterController;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Laragear\WebAuthn\Enums\UserVerification;
|
||||
use Laragear\WebAuthn\Http\Requests\AttestationRequest;
|
||||
use Laragear\WebAuthn\Http\Requests\AttestedRequest;
|
||||
use Laragear\WebAuthn\JsonTransport;
|
||||
use Laragear\WebAuthn\WebAuthn;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Tests\FeatureTestCase;
|
||||
|
||||
|
@ -38,7 +38,7 @@ class WebAuthnRegisterControllerTest extends FeatureTestCase
|
|||
*/
|
||||
public function test_uses_attestation_with_fastRegistration_request() : void
|
||||
{
|
||||
Config::set('webauthn.user_verification', WebAuthn::USER_VERIFICATION_DISCOURAGED);
|
||||
Config::set('webauthn.user_verification', UserVerification::DISCOURAGED);
|
||||
|
||||
$request = $this->mock(AttestationRequest::class);
|
||||
|
||||
|
@ -55,7 +55,7 @@ class WebAuthnRegisterControllerTest extends FeatureTestCase
|
|||
*/
|
||||
public function test_uses_attestation_with_secureRegistration_request() : void
|
||||
{
|
||||
Config::set('webauthn.user_verification', WebAuthn::USER_VERIFICATION_REQUIRED);
|
||||
Config::set('webauthn.user_verification', UserVerification::REQUIRED);
|
||||
|
||||
$request = $this->mock(AttestationRequest::class);
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ use App\Http\Controllers\SystemController;
|
|||
use App\Models\User;
|
||||
use App\Notifications\TestEmailSettingNotification;
|
||||
use App\Services\ReleaseRadarService;
|
||||
use Illuminate\Support\Facades\Lang;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Tests\FeatureTestCase;
|
||||
|
@ -14,12 +15,16 @@ use Tests\FeatureTestCase;
|
|||
* SystemControllerTest test class
|
||||
*/
|
||||
#[CoversClass(SystemController::class)]
|
||||
#[CoversClass(TestEmailSettingNotification::class)]
|
||||
|
||||
class SystemControllerTest extends FeatureTestCase
|
||||
{
|
||||
/**
|
||||
* @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable
|
||||
*/
|
||||
protected $user, $admin;
|
||||
protected $user;
|
||||
|
||||
protected $admin;
|
||||
|
||||
/**
|
||||
* @test
|
||||
|
@ -28,7 +33,7 @@ class SystemControllerTest extends FeatureTestCase
|
|||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->user = User::factory()->create();
|
||||
$this->user = User::factory()->create();
|
||||
$this->admin = User::factory()->administrator()->create();
|
||||
}
|
||||
|
||||
|
@ -77,7 +82,7 @@ class SystemControllerTest extends FeatureTestCase
|
|||
'Auth guard',
|
||||
'webauthn user verification',
|
||||
'Trusted proxies',
|
||||
'lastRadarScan'
|
||||
'lastRadarScan',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
@ -131,6 +136,19 @@ class SystemControllerTest extends FeatureTestCase
|
|||
Notification::assertSentTo($this->admin, TestEmailSettingNotification::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function test_testEmail_renders_to_email()
|
||||
{
|
||||
$mail = (new TestEmailSettingNotification('test_token'))->toMail($this->user)->render();
|
||||
|
||||
$this->assertStringContainsString(
|
||||
Lang::get('notifications.test_email_settings.reason'),
|
||||
$mail
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
|
|
|
@ -2,14 +2,10 @@
|
|||
|
||||
namespace Tests\Feature\Models;
|
||||
|
||||
use App\Extensions\WebauthnCredentialBroker;
|
||||
use App\Models\Group;
|
||||
use App\Models\TwoFAccount;
|
||||
use App\Models\User;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Http\Testing\FileFactory;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
|
@ -52,7 +48,7 @@ class UserModelTest extends FeatureTestCase
|
|||
*/
|
||||
public function test_isAdministrator_returns_correct_state()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
$admin = User::factory()->administrator()->create();
|
||||
|
||||
$this->assertEquals($user->isAdministrator(), false);
|
||||
|
@ -88,7 +84,7 @@ class UserModelTest extends FeatureTestCase
|
|||
*/
|
||||
public function test_resetPassword_resets_password_with_success()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
$oldPassword = $user->password;
|
||||
|
||||
$user->resetPassword();
|
||||
|
@ -118,7 +114,7 @@ class UserModelTest extends FeatureTestCase
|
|||
$user = User::factory()->create();
|
||||
TwoFAccount::factory()->for($user)->create();
|
||||
Group::factory()->for($user)->create();
|
||||
|
||||
|
||||
DB::table('webauthn_credentials')->insert([
|
||||
'id' => '-VOLFKPY-_FuMI_sJ7gMllK76L3VoRUINj6lL_Z3qDg',
|
||||
'authenticatable_type' => \App\Models\User::class,
|
||||
|
@ -139,7 +135,7 @@ class UserModelTest extends FeatureTestCase
|
|||
Password::broker()->createToken($user);
|
||||
|
||||
$user->delete();
|
||||
|
||||
|
||||
$this->assertDatabaseMissing('twofaccounts', [
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
|
|
|
@ -30,7 +30,7 @@ class GroupModelTest extends ModelTestCase
|
|||
[
|
||||
'id' => 'int',
|
||||
'twofaccounts_count' => 'integer',
|
||||
'user_id' => 'integer'
|
||||
'user_id' => 'integer',
|
||||
],
|
||||
[
|
||||
'deleting' => GroupDeleting::class,
|
||||
|
|
|
@ -5,8 +5,8 @@ namespace Tests\Unit\Listeners;
|
|||
use App\Listeners\RegisterOpenId;
|
||||
use App\Providers\Socialite\OpenId;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Laravel\Socialite\SocialiteManager;
|
||||
use Laravel\Socialite\Contracts\Factory as SocialiteFactory;
|
||||
use Laravel\Socialite\SocialiteManager;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||
use Tests\TestCase;
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit\Rules;
|
||||
|
||||
use App\Rules\IsValidEmailList;
|
||||
use Illuminate\Foundation\Testing\WithoutMiddleware;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* IsValidEmailListTest test class
|
||||
*/
|
||||
#[CoversClass(IsValidEmailList::class)]
|
||||
class IsValidEmailListTest extends TestCase
|
||||
{
|
||||
use WithoutMiddleware;
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
#[DataProvider('provideValidData')]
|
||||
public function test_valid_data(array $data) : void
|
||||
{
|
||||
$validator = Validator::make($data, ['value' => [new IsValidEmailList]]);
|
||||
|
||||
$this->assertFalse($validator->fails());
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide Valid data for validation test
|
||||
*/
|
||||
public static function provideValidData() : array
|
||||
{
|
||||
return [
|
||||
[[
|
||||
'value' => 'johndoe@example.com',
|
||||
]],
|
||||
[[
|
||||
'value' => 'johndoe@example.com|janedoe@example.com',
|
||||
]],
|
||||
[[
|
||||
'value' => '|johndoe@example.com|janedoe@example.com',
|
||||
]],
|
||||
[[
|
||||
'value' => 'johndoe@example.com|janedoe@example.com|',
|
||||
]],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
#[DataProvider('provideInvalidData')]
|
||||
public function test_invalid_data(array $data) : void
|
||||
{
|
||||
$validator = Validator::make($data, ['value' => [new IsValidEmailList]]);
|
||||
|
||||
$this->assertTrue($validator->fails());
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide Valid data for validation test
|
||||
*/
|
||||
public static function provideInvalidData() : array
|
||||
{
|
||||
return [
|
||||
[[
|
||||
'value' => 'johndoeexamplecom',
|
||||
]],
|
||||
[[
|
||||
'value' => 'johndoe@example.com|janedoeexamplecom',
|
||||
]],
|
||||
[[
|
||||
'value' => 'johndoe@example.com,janedoe@example.com',
|
||||
]],
|
||||
[[
|
||||
'value' => 'johndoe@example.com;janedoe@example.com|',
|
||||
]],
|
||||
[[
|
||||
'value' => 'johndoe@example.com janedoe@example.com',
|
||||
]],
|
||||
[[
|
||||
'value' => 'johndoe@example.com | janedoe@example.com',
|
||||
]],
|
||||
];
|
||||
}
|
||||
}
|
|
@ -34,8 +34,8 @@ class TwoFAccountModelTest extends ModelTestCase
|
|||
['*'],
|
||||
[],
|
||||
[
|
||||
'id' => 'int',
|
||||
'user_id' => 'integer'
|
||||
'id' => 'int',
|
||||
'user_id' => 'integer',
|
||||
],
|
||||
['deleted' => TwoFAccountDeleted::class],
|
||||
['created_at', 'updated_at'],
|
||||
|
|
|
@ -74,7 +74,7 @@ export default defineConfig({
|
|||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
banner: '/*! 2FAuth version ' + version + ' - Copyright (c) 2023 Bubka - https://github.com/Bubka/2FAuth */',
|
||||
banner: '/*! 2FAuth version ' + version + ' - Copyright (c) 2024 Bubka - https://github.com/Bubka/2FAuth */',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue