Page MenuHomePhorge

D5622.1775227338.diff
No OneTemporary

Authored By
Unknown
Size
48 KB
Referenced Files
None
Subscribers
None

D5622.1775227338.diff

diff --git a/src/app/Http/Controllers/API/PasswordResetController.php b/src/app/Http/Controllers/API/PasswordResetController.php
--- a/src/app/Http/Controllers/API/PasswordResetController.php
+++ b/src/app/Http/Controllers/API/PasswordResetController.php
@@ -57,8 +57,8 @@
}
// Generate the verification code
- $code = new VerificationCode(['mode' => 'password-reset']);
- $user->verificationcodes()->save($code);
+ $code = new VerificationCode(['mode' => VerificationCode::MODE_PASSWORD]);
+ $user->verificationCodes()->save($code);
// Send email/sms message
PasswordResetJob::dispatch($code);
@@ -98,10 +98,10 @@
if (
empty($code)
|| $code->isExpired()
- || $code->mode !== 'password-reset'
+ || $code->mode !== VerificationCode::MODE_PASSWORD
|| Str::upper($request->short_code) !== Str::upper($code->short_code)
) {
- $errors = ['short_code' => "The code is invalid or expired."];
+ $errors = ['short_code' => self::trans('validation.verificationcodeinvalid')];
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
@@ -202,7 +202,7 @@
{
// Generate the verification code
$code = new VerificationCode();
- $code->mode = 'password-reset';
+ $code->mode = VerificationCode::MODE_PASSWORD;
// These codes are valid for 24 hours
$code->expires_at = now()->addHours(24);
@@ -210,7 +210,7 @@
// The code is inactive until it is submitted via a different endpoint
$code->active = false;
- $this->guard()->user()->verificationcodes()->save($code);
+ $this->guard()->user()->verificationCodes()->save($code);
return response()->json([
'status' => 'success',
diff --git a/src/app/Http/Controllers/API/SignupController.php b/src/app/Http/Controllers/API/SignupController.php
--- a/src/app/Http/Controllers/API/SignupController.php
+++ b/src/app/Http/Controllers/API/SignupController.php
@@ -208,7 +208,7 @@
|| $code->isExpired()
|| Str::upper($request->short_code) !== Str::upper($code->short_code)
) {
- $errors = ['short_code' => self::trans('validation.signupcodeinvalid')];
+ $errors = ['short_code' => self::trans('validation.verificationcodeinvalid')];
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
@@ -219,7 +219,7 @@
$plan = $this->getPlan($request);
if (!$plan) {
- $errors = ['short_code' => self::trans('validation.signupcodeinvalid')];
+ $errors = ['short_code' => self::trans('validation.verificationcodeinvalid')];
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php
--- a/src/app/Http/Controllers/API/V4/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/UsersController.php
@@ -9,6 +9,7 @@
use App\Http\Controllers\RelationController;
use App\Http\Resources\UserInfoExtendedResource;
use App\Http\Resources\UserResource;
+use App\Jobs\Mail\EmailVerificationJob;
use App\Jobs\User\CreateJob;
use App\Package;
use App\Resource;
@@ -52,6 +53,53 @@
/** @var ?VerificationCode Password reset code to activate on user create/update */
protected $passCode;
+ /**
+ * Verification code validation
+ *
+ * @param Request $request the API request
+ * @param string $id User identifier
+ * @param string $code Verification code identifier
+ */
+ public function codeValidation(Request $request, $id, $code): JsonResponse
+ {
+ // Validate the request args
+ $v = Validator::make(
+ $request->all(),
+ [
+ // Verification code secret
+ 'short_code' => 'required',
+ ]
+ );
+
+ if ($v->fails()) {
+ return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+ }
+
+ // Validate the verification code
+ $code = VerificationCode::where('code', $code)->where('active', true)->first();
+
+ if ($code && ($this->guard()->user()->id != $code->user_id || $code->user_id != $id)) {
+ return $this->errorResponse(403);
+ }
+
+ if (
+ empty($code)
+ || $code->isExpired()
+ || Str::upper($request->short_code) !== Str::upper($code->short_code)
+ || empty($message = $code->applyAction())
+ ) {
+ $errors = ['short_code' => self::trans('validation.verificationcodeinvalid')];
+ return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ }
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => $message,
+ // Verification code mode
+ 'mode' => $code->mode,
+ ]);
+ }
+
/**
* Listing of users.
*
@@ -338,11 +386,45 @@
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
+ $response = [
+ 'status' => 'success',
+ 'message' => self::trans('app.user-update-success'),
+ // @var array|null Extended status/permissions information
+ 'statusInfo' => null,
+ // @var array Extra settings that got added in this action
+ 'settings' => [],
+ ];
+
DB::beginTransaction();
SkusController::updateEntitlements($user, $request->skus);
if (!empty($settings)) {
+ if ($user->id == $current_user->id && array_key_exists('external_email', $settings)) {
+ if (!empty($settings['external_email'])) {
+ // User changes his own external email, required code verification
+ if ($settings['external_email'] != $user->getSetting('external_email')) {
+ $code = $user->verificationCodes()->create(['mode' => VerificationCode::MODE_EMAIL]);
+ $extras = [
+ 'external_email_new' => $settings['external_email'],
+ 'external_email_code' => $code->code,
+ ];
+ $response['settings'] = array_merge($response['settings'], $extras);
+ $settings = array_merge($settings, $extras);
+ unset($settings['external_email']);
+ EmailVerificationJob::dispatch($code->code)->afterCommit();
+ }
+ } else {
+ // User removes his own external email
+ $extras = [
+ 'external_email_new' => null,
+ 'external_email_code' => null,
+ ];
+ $response['settings'] = array_merge($response['settings'], $extras);
+ $settings = array_merge($settings, $extras);
+ }
+ }
+
$user->setSettings($settings);
}
@@ -359,13 +441,6 @@
DB::commit();
- $response = [
- 'status' => 'success',
- 'message' => self::trans('app.user-update-success'),
- // @var array Extended status/permissions information
- 'statusInfo' => null,
- ];
-
// For self-update refresh the statusInfo in the UI
if ($user->id == $current_user->id) {
$response['statusInfo'] = self::statusInfo($user);
@@ -406,8 +481,11 @@
$code = explode('-', $code)[1];
}
- $this->passCode = $this->guard()->user()->verificationcodes()
- ->where('code', $code)->where('active', false)->first();
+ $this->passCode = $this->guard()->user()->verificationCodes()
+ ->where('code', $code)
+ ->where('mode', VerificationCode::MODE_PASSWORD)
+ ->where('active', false)
+ ->first();
// Generate a password for a new user with password reset link
// FIXME: Should/can we have a user with no password set?
diff --git a/src/app/Http/Resources/UserInfoExtendedResource.php b/src/app/Http/Resources/UserInfoExtendedResource.php
--- a/src/app/Http/Resources/UserInfoExtendedResource.php
+++ b/src/app/Http/Resources/UserInfoExtendedResource.php
@@ -4,6 +4,7 @@
use App\Entitlement;
use App\User;
+use App\VerificationCode;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@@ -20,7 +21,9 @@
*/
public function toArray(Request $request): array
{
- $code = $this->resource->verificationcodes()->where('active', true)
+ $code = $this->resource->verificationCodes()
+ ->where('active', true)
+ ->where('mode', VerificationCode::MODE_PASSWORD)
->where('expires_at', '>', Carbon::now())
->first();
diff --git a/src/app/Http/Resources/UserInfoResource.php b/src/app/Http/Resources/UserInfoResource.php
--- a/src/app/Http/Resources/UserInfoResource.php
+++ b/src/app/Http/Resources/UserInfoResource.php
@@ -19,6 +19,8 @@
'country',
'currency',
'external_email',
+ 'external_email_new',
+ 'external_email_code',
'first_name',
'last_name',
'organization',
diff --git a/src/app/Jobs/Mail/EmailVerificationJob.php b/src/app/Jobs/Mail/EmailVerificationJob.php
new file mode 100644
--- /dev/null
+++ b/src/app/Jobs/Mail/EmailVerificationJob.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace App\Jobs\Mail;
+
+use App\Jobs\MailJob;
+use App\Mail\EmailVerification;
+use App\Mail\Helper;
+use App\VerificationCode;
+
+class EmailVerificationJob extends MailJob
+{
+ /** @var string Verification code identifier */
+ protected $code;
+
+ /**
+ * Create a new job instance.
+ *
+ * @param string $code Verification code identifier
+ */
+ public function __construct(string $code)
+ {
+ $this->code = $code;
+ }
+
+ /**
+ * Execute the job.
+ */
+ public function handle()
+ {
+ $code = VerificationCode::find($this->code);
+
+ if (empty($code) || !$code->active || $code->isExpired()) {
+ // Code does not exist or got deactivated, nothing to do here
+ return;
+ }
+
+ $settings = $code->user->getSettings(['external_email_new', 'external_email_code']);
+
+ if (empty($settings['external_email_new']) || $settings['external_email_code'] != $code->code) {
+ // Settings changed in meantime, nothing to do here
+ return;
+ }
+
+ Helper::sendMail(
+ new EmailVerification($code),
+ $code->user->tenant_id,
+ ['to' => $settings['external_email_new']]
+ );
+ }
+}
diff --git a/src/app/Mail/EmailVerification.php b/src/app/Mail/EmailVerification.php
new file mode 100644
--- /dev/null
+++ b/src/app/Mail/EmailVerification.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace App\Mail;
+
+use App\Tenant;
+use App\Utils;
+use App\VerificationCode;
+use Illuminate\Support\Str;
+
+class EmailVerification extends Mailable
+{
+ /** @var VerificationCode A verification code object */
+ protected $code;
+
+ /**
+ * Create a new message instance.
+ *
+ * @param VerificationCode $code A verification code object
+ */
+ public function __construct(VerificationCode $code)
+ {
+ $this->code = $code;
+ $this->user = $code->user;
+ }
+
+ /**
+ * Build the message.
+ *
+ * @return $this
+ */
+ public function build()
+ {
+ $appName = Tenant::getConfig($this->code->user->tenant_id, 'app.name');
+ /*
+ // TODO: Include clickable link if this is a user with no role=device?
+ $href = Utils::serviceUrl(
+ sprintf('/code/%s-%s', $this->code->short_code, $this->code->code),
+ $this->code->user->tenant_id
+ );
+ */
+
+ $vars = [
+ 'site' => $appName,
+ 'name' => $this->code->user->name(true),
+ ];
+
+ $this->view('emails.html.email_verification')
+ ->text('emails.plain.email_verification')
+ ->subject(\trans('mail.emailverification-subject', $vars))
+ ->with([
+ 'vars' => $vars,
+ // 'href' => $href,
+ 'code' => $this->code->code,
+ 'short_code' => $this->code->short_code,
+ ]);
+
+ return $this;
+ }
+
+ /**
+ * Render the mail template with fake data
+ *
+ * @param string $type Output format ('html' or 'text')
+ *
+ * @return string HTML or Plain Text output
+ */
+ public static function fakeRender(string $type = 'html'): string
+ {
+ $code = new VerificationCode([
+ 'code' => Str::random(VerificationCode::CODE_LENGTH),
+ 'short_code' => VerificationCode::generateShortCode(),
+ 'mode' => VerificationCode::MODE_EMAIL,
+ ]);
+
+ // @phpstan-ignore-next-line
+ $code->user = new User();
+ $code->user->email = 'test@' . \config('app.domain');
+
+ $mail = new self($code);
+
+ return Helper::render($mail, $type);
+ }
+}
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -691,7 +691,7 @@
*
* @return HasMany<VerificationCode, $this>
*/
- public function verificationcodes()
+ public function verificationCodes()
{
return $this->hasMany(VerificationCode::class, 'user_id', 'id');
}
diff --git a/src/app/Utils.php b/src/app/Utils.php
--- a/src/app/Utils.php
+++ b/src/app/Utils.php
@@ -353,12 +353,15 @@
* @param int $length The length of each segment
* @param int $qty The quantity of segments
* @param string $join The string to use to join the segments
+ * @param string $chars The characters to use to build the code
*
* @return string
*/
- public static function randStr($length, $qty = 1, $join = '')
+ public static function randStr($length, $qty = 1, $join = '', string $chars = '')
{
- $chars = env('SHORTCODE_CHARS', self::CHARS);
+ if (strlen($chars) == 0) {
+ $chars = env('SHORTCODE_CHARS', self::CHARS);
+ }
$randStrs = [];
diff --git a/src/app/VerificationCode.php b/src/app/VerificationCode.php
--- a/src/app/VerificationCode.php
+++ b/src/app/VerificationCode.php
@@ -21,13 +21,16 @@
use BelongsToUserTrait;
// Code expires after so many hours
- public const SHORTCODE_LENGTH = 8;
+ public const SHORTCODE_LENGTH = 6;
public const CODE_LENGTH = 32;
// Code expires after so many hours
public const CODE_EXP_HOURS = 8;
+ public const MODE_EMAIL = 'ext-email';
+ public const MODE_PASSWORD = 'password-reset';
+
/** @var string The primary key associated with the table */
protected $primaryKey = 'code';
@@ -50,13 +53,40 @@
protected $fillable = ['user_id', 'code', 'short_code', 'mode', 'expires_at', 'active'];
/**
- * Generate a short code (for human).
+ * Apply action on verified code.
+ */
+ public function applyAction(): ?string
+ {
+ switch ($this->mode) {
+ case self::MODE_EMAIL:
+ $settings = $this->user->getSettings(['external_email_new', 'external_email_code']);
+
+ if ($settings['external_email_code'] != $this->code) {
+ return null;
+ }
+
+ $this->user->setSettings([
+ 'external_email' => $settings['external_email_new'],
+ 'external_email_new' => null,
+ 'external_email_code' => null,
+ ]);
+
+ $this->delete();
+
+ return \trans('app.code-verified-email');
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Generate a short (numeric) code (for human).
*/
public static function generateShortCode(): string
{
$code_length = env('VERIFICATION_CODE_LENGTH', self::SHORTCODE_LENGTH);
- return Utils::randStr($code_length);
+ return Utils::randStr($code_length, 1, '', '1234567890');
}
/**
diff --git a/src/resources/lang/en/app.php b/src/resources/lang/en/app.php
--- a/src/resources/lang/en/app.php
+++ b/src/resources/lang/en/app.php
@@ -22,6 +22,8 @@
'chart-users' => 'Users - last 8 weeks',
'chart-users-per-country' => 'Users per country',
+ 'code-verified-email' => 'The external email address has been verified.',
+
'companion-create-success' => 'Companion app has been created.',
'companion-delete-success' => 'Companion app has been removed.',
diff --git a/src/resources/lang/en/mail.php b/src/resources/lang/en/mail.php
--- a/src/resources/lang/en/mail.php
+++ b/src/resources/lang/en/mail.php
@@ -26,6 +26,10 @@
'degradedaccountreminder-body4' => "You can also delete your account there, making sure your data disappears from our systems.",
'degradedaccountreminder-body5' => "Thank you for your consideration!",
+ 'emailverification-subject' => ":site Verification",
+ 'emailverification-body1' => "This is the verification code to validate eligibility of an email address provided in :site account settings.",
+ 'emailverification-body2' => "",
+
'itip-cancel-subject' => "\":summary\" has been canceled",
'itip-cancel-body' => "The event \":summary\" at :start has been canceled by the organizer."
. " The copy in your calendar has been removed accordingly.",
diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php
--- a/src/resources/lang/en/ui.php
+++ b/src/resources/lang/en/ui.php
@@ -540,6 +540,9 @@
'distlists' => "Distribution lists",
'domains' => "Domains",
'ext-email' => "External Email",
+ 'extemailverification' => "External email verification",
+ 'extemailverificationbody' => "A verification code has been sent in a message to {email}. Please, enter the code here:",
+ 'extemailverificationbtn' => "Verify {email}",
'email-aliases' => "Email Aliases",
'finances' => "Finances",
'geolimit' => "Geo-lockin",
diff --git a/src/resources/lang/en/validation.php b/src/resources/lang/en/validation.php
--- a/src/resources/lang/en/validation.php
+++ b/src/resources/lang/en/validation.php
@@ -179,7 +179,7 @@
'password-policy-last-error' => 'The minimum value for last N passwords is :last.',
'referralcodeinvalid' => 'The referral program code is invalid.',
'signuptokeninvalid' => 'The signup token is invalid.',
- 'signupcodeinvalid' => 'The verification code is invalid or expired.',
+ 'verificationcodeinvalid' => 'The verification code is invalid or expired.',
/*
|--------------------------------------------------------------------------
diff --git a/src/resources/views/emails/html/email_verification.blade.php b/src/resources/views/emails/html/email_verification.blade.php
new file mode 100644
--- /dev/null
+++ b/src/resources/views/emails/html/email_verification.blade.php
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <p>{{ __('mail.header', $vars) }}</p>
+
+ <p>{{ __('mail.emailverification-body1', $vars) }}
+
+ <p><strong>{!! $short_code !!}</strong></p>
+
+ <p>{{ __('mail.emailverification-body2', $vars) }}</p>
+
+ <p>{{ __('mail.footer1', $vars) }}</p>
+ <p>{{ __('mail.footer2', $vars) }}</p>
+ </body>
+</html>
diff --git a/src/resources/views/emails/plain/email_verification.blade.php b/src/resources/views/emails/plain/email_verification.blade.php
new file mode 100644
--- /dev/null
+++ b/src/resources/views/emails/plain/email_verification.blade.php
@@ -0,0 +1,11 @@
+{!! __('mail.header', $vars) !!}
+
+{!! __('mail.emailverification-body1', $vars) !!}
+
+{!! $short_code !!}
+
+{!! __('mail.emailverification-body2', $vars) !!}
+
+--
+{!! __('mail.footer1', $vars) !!}
+{!! __('mail.footer2', $vars) !!}
diff --git a/src/resources/vue/User/Info.vue b/src/resources/vue/User/Info.vue
--- a/src/resources/vue/User/Info.vue
+++ b/src/resources/vue/User/Info.vue
@@ -273,6 +273,12 @@
<div class="row form-text"><span>{{ $t('user.delegation-desc') }}</span></div>
</form>
</modal-dialog>
+ <modal-dialog id="extemailverification" ref="extEmailVerification" :buttons="['save']" @click="codeValidate()" :title="$t('user.extemailverification')">
+ <div>
+ <p>{{ $t('user.extemailverificationbody', { email: user.external_email_new }) }}</p>
+ <p><input type="text" class="form-control" id="short_code" value=""></p>
+ </div>
+ </modal-dialog>
</div>
</template>
@@ -445,6 +451,24 @@
})
},
methods: {
+ codeValidate() {
+ let post = { short_code: $('#short_code').val() }
+ axios.post('/api/v4/users/' + this.user_id + '/code/' + this.user.external_email_code, post)
+ .then(response => {
+ if (response.data.status == 'success') {
+ this.$refs.extEmailVerification.hide()
+ this.$toast.success(response.data.message)
+
+ if (this.successRoute) {
+ this.$router.push(this.successRoute)
+ } else {
+ this.user.external_email = this.user.external_email_new
+ delete this.user.external_email_new
+ delete this.user.external_email_code
+ }
+ }
+ })
+ },
passwordLinkCopy() {
navigator.clipboard.writeText($('#password-link code').text());
},
@@ -560,7 +584,11 @@
}
this.$toast.success(response.data.message)
- if (this.successRoute) {
+
+ if (this.isSelf && response.data.settings.external_email_code) {
+ this.user = { ...this.user, ...response.data.settings }
+ this.$refs.extEmailVerification.show()
+ } else if (this.successRoute) {
this.$router.push(this.successRoute)
}
})
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -242,6 +242,7 @@
Route::post('users/{id}/login-as', [API\V4\UsersController::class, 'loginAs']);
Route::get('users/{id}/skus', [API\V4\UsersController::class, 'skus']);
Route::get('users/{id}/status', [API\V4\UsersController::class, 'status']);
+ Route::post('users/{id}/code/{code}', [API\V4\UsersController::class, 'codeValidation']);
if (\config('app.with_delegation')) {
Route::get('users/{id}/delegations', [API\V4\UsersController::class, 'delegations']);
diff --git a/src/tests/Browser/PasswordResetTest.php b/src/tests/Browser/PasswordResetTest.php
--- a/src/tests/Browser/PasswordResetTest.php
+++ b/src/tests/Browser/PasswordResetTest.php
@@ -284,8 +284,8 @@
$user = $this->getTestUser('passwordresettestdusk@' . \config('app.domain'));
$user->setSetting('external_email', 'external@domain.tld');
- $code = new VerificationCode(['mode' => 'password-reset']);
- $user->verificationcodes()->save($code);
+ $code = new VerificationCode(['mode' => VerificationCode::MODE_PASSWORD]);
+ $user->verificationCodes()->save($code);
$this->browse(function (Browser $browser) use ($code) {
// Test a valid link
diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php
--- a/src/tests/Browser/UsersTest.php
+++ b/src/tests/Browser/UsersTest.php
@@ -103,8 +103,8 @@
$this->browse(function (Browser $browser) {
$john = $this->getTestUser('john@kolab.org');
$jack = $this->getTestUser('jack@kolab.org');
- $john->verificationcodes()->delete();
- $jack->verificationcodes()->delete();
+ $john->verificationCodes()->delete();
+ $jack->verificationCodes()->delete();
$john->setSetting('password_policy', 'min:10,upper,digit');
// Test that the page requires authentication
@@ -292,8 +292,8 @@
});
// Test password reset link delete and create
- $code = new VerificationCode(['mode' => 'password-reset']);
- $jack->verificationcodes()->save($code);
+ $code = new VerificationCode(['mode' => VerificationCode::MODE_PASSWORD]);
+ $jack->verificationCodes()->save($code);
$browser->visit('/user/' . $jack->id)
->on(new UserInfo())
@@ -320,7 +320,7 @@
->assertMissing('#pass-mode-input:checked')
->assertMissing('#password');
- $this->assertSame(0, $jack->verificationcodes()->count());
+ $this->assertSame(0, $jack->verificationCodes()->count());
// Test creating a password reset link
$link = preg_replace('|/[a-z0-9A-Z-]+$|', '', $link) . '/';
@@ -334,7 +334,7 @@
// Test copy to clipboard
/* TODO: Figure out how to give permission to do this operation
- $code = $john->verificationcodes()->first();
+ $code = $john->verificationCodes()->first();
$link .= $code->short_code . '-' . $code->code;
$browser->assertMissing('#password-link button.text-danger')
@@ -349,8 +349,8 @@
->click('button[type=submit]')
->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.');
- $this->assertSame(1, $jack->verificationcodes()->where('active', true)->count());
- $this->assertSame(0, $john->verificationcodes()->count());
+ $this->assertSame(1, $jack->verificationCodes()->where('active', true)->count());
+ $this->assertSame(0, $john->verificationCodes()->count());
});
});
}
diff --git a/src/tests/Feature/Controller/PasswordResetTest.php b/src/tests/Feature/Controller/PasswordResetTest.php
--- a/src/tests/Feature/Controller/PasswordResetTest.php
+++ b/src/tests/Feature/Controller/PasswordResetTest.php
@@ -35,7 +35,7 @@
$user->removeSetting('password_expired');
$user->password = \config('app.passphrase');
$user->save();
- $user->verificationcodes()->delete();
+ $user->verificationCodes()->delete();
IP4Net::where('net_number', inet_pton('128.0.0.0'))->delete();
@@ -202,8 +202,8 @@
// Add verification code and required external email address to user settings
$user = $this->getTestUser('passwordresettest@' . \config('app.domain'));
- $code = new VerificationCode(['mode' => 'password-reset']);
- $user->verificationcodes()->save($code);
+ $code = new VerificationCode(['mode' => VerificationCode::MODE_PASSWORD]);
+ $user->verificationCodes()->save($code);
// Data with existing code but missing short_code
$data = [
@@ -242,8 +242,8 @@
{
// Add verification code and required external email address to user settings
$user = $this->getTestUser('passwordresettest@' . \config('app.domain'));
- $code = new VerificationCode(['mode' => 'password-reset']);
- $user->verificationcodes()->save($code);
+ $code = new VerificationCode(['mode' => VerificationCode::MODE_PASSWORD]);
+ $user->verificationCodes()->save($code);
// Data with invalid code
$data = [
@@ -279,8 +279,8 @@
$this->assertArrayHasKey('short_code', $json['errors']);
$user = $this->getTestUser('passwordresettest@' . \config('app.domain'));
- $code = new VerificationCode(['mode' => 'password-reset']);
- $user->verificationcodes()->save($code);
+ $code = new VerificationCode(['mode' => VerificationCode::MODE_PASSWORD]);
+ $user->verificationCodes()->save($code);
// Data with existing code but missing password
$data = [
@@ -352,9 +352,9 @@
public function testPasswordResetValidInput()
{
$user = $this->getTestUser('passwordresettest@' . \config('app.domain'));
- $code = new VerificationCode(['mode' => 'password-reset']);
- $user->verificationcodes()->delete();
- $user->verificationcodes()->save($code);
+ $code = new VerificationCode(['mode' => VerificationCode::MODE_PASSWORD]);
+ $user->verificationCodes()->delete();
+ $user->verificationCodes()->save($code);
Queue::fake();
Queue::assertNothingPushed();
@@ -393,13 +393,13 @@
$this->assertTrue($user->validatePassword('testtest'));
// Check if the code has been removed
- $this->assertCount(0, $user->verificationcodes()->get());
+ $this->assertCount(0, $user->verificationCodes()->get());
// Test 2FA handling
$user = $this->getTestUser('ned@kolab.org');
- $code = new VerificationCode(['mode' => 'password-reset']);
- $user->verificationcodes()->delete();
- $user->verificationcodes()->save($code);
+ $code = new VerificationCode(['mode' => VerificationCode::MODE_PASSWORD]);
+ $user->verificationCodes()->delete();
+ $user->verificationCodes()->save($code);
$user->removeSetting('password_expired');
$data = [
@@ -421,7 +421,7 @@
// Make sure password didn't change if 2FA wasn't provided
$user->refresh();
$this->assertTrue($user->validatePassword(\config('app.passphrase')));
- $this->assertCount(1, $user->verificationcodes()->get());
+ $this->assertCount(1, $user->verificationCodes()->get());
$data['secondfactor'] = SecondFactor::code('ned@kolab.org');
$response = $this->post('/api/auth/password-reset', $data);
@@ -435,7 +435,7 @@
$user->refresh();
$this->assertTrue($user->validatePassword('ABC123456789'));
- $this->assertCount(0, $user->verificationcodes()->get());
+ $this->assertCount(0, $user->verificationCodes()->get());
}
/**
@@ -570,14 +570,14 @@
public function testCodeCreate()
{
$user = $this->getTestUser('john@kolab.org');
- $user->verificationcodes()->delete();
+ $user->verificationCodes()->delete();
$response = $this->actingAs($user)->post('/api/v4/password-reset/code', []);
$response->assertStatus(200);
$json = $response->json();
- $code = $user->verificationcodes()->first();
+ $code = $user->verificationCodes()->first();
$this->assertSame('success', $json['status']);
$this->assertSame($code->code, $json['code']);
@@ -593,15 +593,15 @@
$user = $this->getTestUser('passwordresettest@' . \config('app.domain'));
$john = $this->getTestUser('john@kolab.org');
$jack = $this->getTestUser('jack@kolab.org');
- $john->verificationcodes()->delete();
- $jack->verificationcodes()->delete();
+ $john->verificationCodes()->delete();
+ $jack->verificationCodes()->delete();
- $john_code = new VerificationCode(['mode' => 'password-reset']);
- $john->verificationcodes()->save($john_code);
- $jack_code = new VerificationCode(['mode' => 'password-reset']);
- $jack->verificationcodes()->save($jack_code);
- $user_code = new VerificationCode(['mode' => 'password-reset']);
- $user->verificationcodes()->save($user_code);
+ $john_code = new VerificationCode(['mode' => VerificationCode::MODE_PASSWORD]);
+ $john->verificationCodes()->save($john_code);
+ $jack_code = new VerificationCode(['mode' => VerificationCode::MODE_PASSWORD]);
+ $jack->verificationCodes()->save($jack_code);
+ $user_code = new VerificationCode(['mode' => VerificationCode::MODE_PASSWORD]);
+ $user->verificationCodes()->save($user_code);
// Unauth access
$response = $this->delete('/api/v4/password-reset/code/' . $user_code->code);
@@ -621,7 +621,7 @@
$json = $response->json();
- $this->assertSame(0, $john->verificationcodes()->count());
+ $this->assertSame(0, $john->verificationCodes()->count());
$this->assertSame('success', $json['status']);
$this->assertSame("Password reset code deleted successfully.", $json['message']);
@@ -633,7 +633,7 @@
$json = $response->json();
- $this->assertSame(0, $jack->verificationcodes()->count());
+ $this->assertSame(0, $jack->verificationCodes()->count());
$this->assertSame('success', $json['status']);
$this->assertSame("Password reset code deleted successfully.", $json['message']);
}
diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php
--- a/src/tests/Feature/Controller/UsersTest.php
+++ b/src/tests/Feature/Controller/UsersTest.php
@@ -7,6 +7,7 @@
use App\Enums\ProcessState;
use App\Http\Controllers\API\V4\UsersController;
use App\Http\Resources\UserInfoResource;
+use App\Jobs\Mail\EmailVerificationJob;
use App\Jobs\User\CreateJob;
use App\Package;
use App\Plan;
@@ -88,6 +89,42 @@
parent::tearDown();
}
+ /**
+ * Test validation of a verification code (POST /api/v4/users/<user>/code/<code>)
+ */
+ public function testCodeValidation(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jane = $this->getTestUser('jane@kolabnow.com');
+
+ $code = $jane->verificationCodes()->create(['mode' => VerificationCode::MODE_EMAIL]);
+ $jane->setSettings([
+ 'external_email_new' => 'test@domain.tld',
+ 'external_email_code' => $code->code,
+ ]);
+
+ // Test access by another user
+ $post = ['short_code' => $code->short_code];
+ $response = $this->actingAs($john)->post("/api/v4/users/{$jane->id}/code/{$code->code}", $post);
+ $response->assertStatus(403);
+
+ // Test access by the user
+ $response = $this->actingAs($jane)->post("/api/v4/users/{$jane->id}/code/{$code->code}", $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+ $this->assertSame('The external email address has been verified.', $json['message']);
+ $this->assertSame($code->mode, $json['mode']);
+ $this->assertSame('test@domain.tld', $jane->getSetting('external_email'));
+ $this->assertNull($jane->getSetting('external_email_new'));
+ $this->assertNull($jane->getSetting('external_email_code'));
+ $this->assertCount(0, $jane->verificationCodes);
+
+ // TODO: Test all error conditions (e.g. expired code, wrong short code)
+ }
+
/**
* Test user deleting (DELETE /api/v4/users/<id>)
*/
@@ -967,8 +1004,8 @@
'storage', 'storage', 'storage', 'storage', 'storage']);
// Test password reset link "mode"
- $code = new VerificationCode(['mode' => 'password-reset', 'active' => false]);
- $john->verificationcodes()->save($code);
+ $code = new VerificationCode(['mode' => VerificationCode::MODE_PASSWORD, 'active' => false]);
+ $john->verificationCodes()->save($code);
$post = [
'first_name' => 'John2',
@@ -1041,6 +1078,8 @@
*/
public function testUpdate(): void
{
+ Queue::fake();
+
$userA = $this->getTestUser('UsersControllerTest1@userscontroller.com');
$userA->setSetting('password_policy', 'min:8,digit');
$jack = $this->getTestUser('jack@kolab.org');
@@ -1068,7 +1107,6 @@
$this->assertSame('success', $json['status']);
$this->assertSame("User data updated successfully.", $json['message']);
$this->assertTrue(!empty($json['statusInfo']));
- $this->assertCount(3, $json);
// Test some invalid data
$post = ['password' => '1234567', 'currency' => 'invalid'];
@@ -1106,17 +1144,33 @@
$this->assertSame('success', $json['status']);
$this->assertSame("User data updated successfully.", $json['message']);
$this->assertTrue(!empty($json['statusInfo']));
- $this->assertCount(3, $json);
$this->assertTrue($userA->password != $userA->fresh()->password);
+ $code = $userA->verificationCodes()->first();
+ $this->assertSame(VerificationCode::MODE_EMAIL, $code->mode);
+ $this->assertSame($code->code, $json['settings']['external_email_code']);
+ $this->assertSame($post['external_email'], $json['settings']['external_email_new']);
+ $post['external_email_new'] = $post['external_email'];
+ $post['external_email_code'] = $code->code;
+ $post['external_email'] = null;
unset($post['password'], $post['password_confirmation'], $post['aliases']);
foreach ($post as $key => $value) {
- $this->assertSame($value, $userA->getSetting($key));
+ $this->assertSame($value, $userA->getSetting($key), "User setting key: {$key}");
}
$aliases = $userA->aliases()->orderBy('alias')->get();
$this->assertCount(2, $aliases);
$this->assertSame('useralias1@' . \config('app.domain'), $aliases[0]->alias);
$this->assertSame('useralias2@' . \config('app.domain'), $aliases[1]->alias);
+ Queue::assertPushed(EmailVerificationJob::class, 1);
+ Queue::assertPushed(
+ EmailVerificationJob::class,
+ static function ($job) use ($code) {
+ return $code->code === TestCase::getObjectProperty($job, 'code');
+ }
+ );
+
+ Queue::fake();
+
// Test unsetting values
$post = [
'first_name' => '',
@@ -1138,15 +1192,20 @@
$this->assertSame('success', $json['status']);
$this->assertSame("User data updated successfully.", $json['message']);
$this->assertTrue(!empty($json['statusInfo']));
- $this->assertCount(3, $json);
+ $this->assertNull($json['settings']['external_email_new']);
+ $this->assertNull($json['settings']['external_email_code']);
+ $post['external_email_new'] = null;
+ $post['external_email_code'] = null;
unset($post['aliases']);
foreach ($post as $key => $value) {
- $this->assertNull($userA->getSetting($key));
+ $this->assertNull($userA->getSetting($key), "User setting key: {$key}");
}
$aliases = $userA->aliases()->get();
$this->assertCount(1, $aliases);
$this->assertSame('useralias2@' . \config('app.domain'), $aliases[0]->alias);
+ Queue::assertPushed(EmailVerificationJob::class, 0);
+
// Test error on some invalid aliases missing password confirmation
$post = [
'password' => 'simple123',
@@ -1176,6 +1235,7 @@
$json = $response->json();
$this->assertTrue(empty($json['statusInfo']));
+ $this->assertTrue(empty($json['emailVerificationCode']));
// TODO: Test error on aliases with invalid/non-existing/other-user's domain
@@ -1238,8 +1298,8 @@
$this->assertTrue(empty($json['statusInfo']));
// Test password reset link "mode"
- $code = new VerificationCode(['mode' => 'password-reset', 'active' => false]);
- $owner->verificationcodes()->save($code);
+ $code = new VerificationCode(['mode' => VerificationCode::MODE_PASSWORD, 'active' => false]);
+ $owner->verificationCodes()->save($code);
$post = ['passwordLinkCode' => $code->short_code . '-' . $code->code];
diff --git a/src/tests/Feature/Jobs/Mail/PasswordResetJobTest.php b/src/tests/Feature/Jobs/Mail/EmailVerificationJobTest.php
copy from src/tests/Feature/Jobs/Mail/PasswordResetJobTest.php
copy to src/tests/Feature/Jobs/Mail/EmailVerificationJobTest.php
--- a/src/tests/Feature/Jobs/Mail/PasswordResetJobTest.php
+++ b/src/tests/Feature/Jobs/Mail/EmailVerificationJobTest.php
@@ -2,25 +2,25 @@
namespace Tests\Feature\Jobs\Mail;
-use App\Jobs\Mail\PasswordResetJob;
-use App\Mail\PasswordReset;
+use App\Jobs\Mail\EmailVerificationJob;
+use App\Mail\EmailVerification;
use App\VerificationCode;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
-class PasswordResetJobTest extends TestCase
+class EmailVerificationJobTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
- $this->deleteTestUser('PasswordReset@UserAccount.com');
+ $this->deleteTestUser('EmailVerification@UserAccount.com');
}
protected function tearDown(): void
{
- $this->deleteTestUser('PasswordReset@UserAccount.com');
+ $this->deleteTestUser('EmailVerification@UserAccount.com');
parent::tearDown();
}
@@ -30,39 +30,37 @@
*/
public function testHandle(): void
{
- $code = new VerificationCode([
- 'mode' => 'password-reset',
- ]);
+ $code = new VerificationCode(['mode' => VerificationCode::MODE_EMAIL]);
- $user = $this->getTestUser('PasswordReset@UserAccount.com');
- $user->verificationcodes()->save($code);
- $user->setSettings(['external_email' => 'etx@email.com']);
+ $user = $this->getTestUser('EmailVerification@UserAccount.com');
+ $user->verificationCodes()->save($code);
+ $user->setSettings(['external_email_new' => 'etx@email.com']);
Mail::fake();
// Assert that no jobs were pushed...
Mail::assertNothingSent();
- $job = new PasswordResetJob($code);
+ $job = new EmailVerificationJob($code);
$job->handle();
// Assert the email sending job was pushed once
- Mail::assertSent(PasswordReset::class, 1);
+ Mail::assertSent(EmailVerification::class, 1);
// Assert the mail was sent to the code's email
- Mail::assertSent(PasswordReset::class, static function ($mail) use ($code) {
- return $mail->hasTo($code->user->getSetting('external_email'));
+ Mail::assertSent(EmailVerification::class, static function ($mail) {
+ return $mail->hasTo('ext@email.com');
});
// Assert sender
- Mail::assertSent(PasswordReset::class, static function ($mail) {
+ Mail::assertSent(EmailVerification::class, static function ($mail) {
return $mail->hasFrom(\config('mail.sender.address'), \config('mail.sender.name'))
&& $mail->hasReplyTo(\config('mail.replyto.address'), \config('mail.replyto.name'));
});
// Test that the job is dispatched to the proper queue
Queue::fake();
- PasswordResetJob::dispatch($code);
- Queue::assertPushedOn(\App\Enums\Queue::Mail->value, PasswordResetJob::class);
+ EmailVerificationJob::dispatch($code);
+ Queue::assertPushedOn(\App\Enums\Queue::Mail->value, EmailVerificationJob::class);
}
}
diff --git a/src/tests/Feature/Jobs/Mail/PasswordResetJobTest.php b/src/tests/Feature/Jobs/Mail/PasswordResetJobTest.php
--- a/src/tests/Feature/Jobs/Mail/PasswordResetJobTest.php
+++ b/src/tests/Feature/Jobs/Mail/PasswordResetJobTest.php
@@ -30,12 +30,10 @@
*/
public function testHandle(): void
{
- $code = new VerificationCode([
- 'mode' => 'password-reset',
- ]);
+ $code = new VerificationCode(['mode' => VerificationCode::MODE_PASSWORD]);
$user = $this->getTestUser('PasswordReset@UserAccount.com');
- $user->verificationcodes()->save($code);
+ $user->verificationCodes()->save($code);
$user->setSettings(['external_email' => 'etx@email.com']);
Mail::fake();
diff --git a/src/tests/Feature/VerificationCodeTest.php b/src/tests/Feature/VerificationCodeTest.php
--- a/src/tests/Feature/VerificationCodeTest.php
+++ b/src/tests/Feature/VerificationCodeTest.php
@@ -32,7 +32,7 @@
$user = $this->getTestUser('UserAccountA@UserAccount.com');
$data = [
'user_id' => $user->id,
- 'mode' => 'password-reset',
+ 'mode' => VerificationCode::MODE_PASSWORD,
];
$code = VerificationCode::create($data);
diff --git a/src/tests/Unit/Mail/PasswordResetTest.php b/src/tests/Unit/Mail/EmailVerificationTest.php
copy from src/tests/Unit/Mail/PasswordResetTest.php
copy to src/tests/Unit/Mail/EmailVerificationTest.php
--- a/src/tests/Unit/Mail/PasswordResetTest.php
+++ b/src/tests/Unit/Mail/EmailVerificationTest.php
@@ -2,13 +2,13 @@
namespace Tests\Unit\Mail;
-use App\Mail\PasswordReset;
+use App\Mail\EmailVerification;
use App\User;
use App\Utils;
use App\VerificationCode;
use Tests\TestCase;
-class PasswordResetTest extends TestCase
+class EmailVerificationTest extends TestCase
{
/**
* Test email content
@@ -17,7 +17,7 @@
{
$code = new VerificationCode([
'user_id' => 123456789,
- 'mode' => 'password-reset',
+ 'mode' => VerificationCode::MODE_EMAIL,
'code' => 'code',
'short_code' => 'short-code',
]);
@@ -25,25 +25,27 @@
// @phpstan-ignore-next-line
$code->user = new User(['email' => 'test@user']);
- $mail = $this->renderMail(new PasswordReset($code));
+ $mail = $this->renderMail(new EmailVerification($code));
$html = $mail['html'];
$plain = $mail['plain'];
- $url = Utils::serviceUrl('/password-reset/' . $code->short_code . '-' . $code->code);
+ $url = Utils::serviceUrl('/code/' . $code->short_code . '-' . $code->code);
$link = "<a href=\"{$url}\">{$url}</a>";
$appName = \config('app.name');
- $this->assertSame("{$appName} Password Reset", $mail['subject']);
+ $this->assertSame("{$appName} Verification", $mail['subject']);
$this->assertStringStartsWith('<!DOCTYPE html>', $html);
- $this->assertTrue(strpos($html, $link) > 0);
$this->assertTrue(strpos($html, $code->user->name(true)) > 0);
- $this->assertTrue(strpos($html, $code->user->email) > 0);
+ $this->assertStringContainsString($code->short_code, $html);
+ $this->assertStringContainsString('This is the verification', $html);
+ // $this->assertTrue(strpos($html, $link) > 0);
$this->assertStringStartsWith("Dear " . $code->user->name(true), $plain);
- $this->assertTrue(strpos($plain, $link) > 0);
- $this->assertTrue(strpos($plain, $code->user->email) > 0);
+ $this->assertStringContainsString($code->short_code, $plain);
+ $this->assertStringContainsString('This is the verification', $plain);
+ // $this->assertTrue(strpos($plain, $link) > 0);
}
/**
@@ -54,7 +56,7 @@
$appName = \config('app.name');
$code = new VerificationCode([
'user_id' => 123456789,
- 'mode' => 'password-reset',
+ 'mode' => VerificationCode::MODE_EMAIL,
'code' => 'code',
'short_code' => 'short-code',
]);
@@ -62,9 +64,9 @@
// @phpstan-ignore-next-line
$code->user = new User();
- $mail = new PasswordReset($code);
+ $mail = new EmailVerification($code);
- $this->assertSame("{$appName} Password Reset", $mail->getSubject());
+ $this->assertSame("{$appName} Verification", $mail->getSubject());
$this->assertSame($code->user, $mail->getUser());
}
}
diff --git a/src/tests/Unit/Mail/PasswordResetTest.php b/src/tests/Unit/Mail/PasswordResetTest.php
--- a/src/tests/Unit/Mail/PasswordResetTest.php
+++ b/src/tests/Unit/Mail/PasswordResetTest.php
@@ -17,7 +17,7 @@
{
$code = new VerificationCode([
'user_id' => 123456789,
- 'mode' => 'password-reset',
+ 'mode' => VerificationCode::MODE_PASSWORD,
'code' => 'code',
'short_code' => 'short-code',
]);
@@ -54,7 +54,7 @@
$appName = \config('app.name');
$code = new VerificationCode([
'user_id' => 123456789,
- 'mode' => 'password-reset',
+ 'mode' => VerificationCode::MODE_PASSWORD,
'code' => 'code',
'short_code' => 'short-code',
]);
diff --git a/src/tests/Unit/VerificationCodeTest.php b/src/tests/Unit/VerificationCodeTest.php
--- a/src/tests/Unit/VerificationCodeTest.php
+++ b/src/tests/Unit/VerificationCodeTest.php
@@ -2,7 +2,6 @@
namespace Tests\Unit;
-use App\Utils;
use App\VerificationCode;
use Tests\TestCase;
@@ -18,6 +17,6 @@
$code_length = env('VERIFICATION_CODE_LENGTH', VerificationCode::SHORTCODE_LENGTH);
$this->assertTrue(strlen($code) === $code_length);
- $this->assertTrue(strspn($code, Utils::CHARS) === strlen($code));
+ $this->assertTrue(strspn($code, '1234567890') === strlen($code));
}
}

File Metadata

Mime Type
text/plain
Expires
Fri, Apr 3, 2:42 PM (20 h, 39 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18824334
Default Alt Text
D5622.1775227338.diff (48 KB)

Event Timeline