Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117764532
D5622.1775227338.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
48 KB
Referenced Files
None
Subscribers
None
D5622.1775227338.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D5622: External email verification
Attached
Detach File
Event Timeline