Compare commits

...

12 Commits

Author SHA1 Message Date
Bubka a61f5f4702 Add shortcuts definition to the webapp manifest 2024-04-02 09:35:08 +02:00
Bubka 5fb1d60636 Update npm dependencies 2024-03-31 16:06:46 +02:00
Bubka f43fc97523 Complete tests 2024-03-30 15:42:54 +01:00
Bubka eb3e38f4a6 Add user policy checking 2024-03-30 15:42:34 +01:00
Bubka fdccbbcc55 Fix typo in class name 2024-03-30 15:40:42 +01:00
Bubka 53eef9e018 Fix migration not being supported by sqlite 2024-03-29 19:26:34 +01:00
Bubka 55192fe89f Enable parallel testing 2024-03-29 09:57:46 +01:00
Bubka 4bb1cf89ab Update composer dependencies 2024-03-29 09:47:26 +01:00
Bubka 49fddfd331 Apply Pint fixes 2024-03-29 09:42:54 +01:00
Bubka ca903b6fc0 Upgrade to laragear/webauthn v2 - Fixes #255 2024-03-29 09:21:00 +01:00
Bubka 4a8db39ab0 Update ide helpers 2024-03-28 18:38:54 +01:00
Bubka 2e8a9f75b5 Fix translation sources 2024-03-28 09:25:25 +01:00
50 changed files with 2594 additions and 1608 deletions

View File

@ -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

8
Dockerfile vendored
View File

@ -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.
#

File diff suppressed because it is too large Load Diff

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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(),
]
);
}

View File

@ -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
{

View File

@ -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.

View File

@ -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;

View File

@ -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);
}

View File

@ -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;
}

View File

@ -109,6 +109,4 @@ class SystemController extends Controller
return response()->json(['exit-code' => $exitCode], 200);
}
}

View File

@ -3,7 +3,6 @@
namespace App\Listeners;
use Illuminate\Notifications\Events\NotificationSent;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class LogNotification

View File

@ -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.
*/

View File

@ -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)

View File

@ -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.
*/

View File

@ -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.
// *

View File

@ -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
{
//
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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],
];

View File

@ -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();
}

View File

@ -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);

View File

@ -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/"
]
}
}
}

1485
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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');
});
}

View File

@ -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.
#

1274
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -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"
}
]
}

View File

@ -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>

View File

@ -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);

View File

@ -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)
})

View File

@ -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>

View File

@ -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)',

View File

@ -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');

View File

@ -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');
});

View File

@ -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));
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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]);

View File

@ -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']);
}

View File

@ -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);

View File

@ -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
*/

View File

@ -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,
]);

View File

@ -30,7 +30,7 @@ class GroupModelTest extends ModelTestCase
[
'id' => 'int',
'twofaccounts_count' => 'integer',
'user_id' => 'integer'
'user_id' => 'integer',
],
[
'deleting' => GroupDeleting::class,

View File

@ -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;

View File

@ -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',
]],
];
}
}

View File

@ -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'],

2
vite.config.js vendored
View File

@ -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 */',
},
},
},