Page MenuHomePhorge

D5122.1775299130.diff
No OneTemporary

Authored By
Unknown
Size
56 KB
Referenced Files
None
Subscribers
None

D5122.1775299130.diff

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
@@ -9,14 +9,16 @@
use App\Plan;
use App\Providers\PaymentProvider;
use App\ReferralCode;
-use App\Rules\SignupExternalEmail;
-use App\Rules\SignupToken as SignupTokenRule;
use App\Rules\Password;
use App\Rules\ReferralCode as ReferralCodeRule;
+use App\Rules\SignupExternalEmail;
+use App\Rules\SignupToken as SignupTokenRule;
use App\Rules\UserEmailDomain;
use App\Rules\UserEmailLocal;
use App\SignupCode;
use App\SignupInvitation;
+use App\SignupToken;
+use App\Tenant;
use App\User;
use App\Utils;
use App\VatRate;
@@ -92,11 +94,12 @@
'first_name' => 'max:128',
'last_name' => 'max:128',
'voucher' => 'max:32',
+ 'plan' => 'required',
];
- $plan = $this->getPlan();
+ $plan = $this->getPlan($request);
- if ($plan->mode == Plan::MODE_TOKEN) {
+ if ($plan?->mode == Plan::MODE_TOKEN) {
$rules['token'] = ['required', 'string', new SignupTokenRule($plan)];
} else {
$rules['email'] = ['required', 'string', new SignupExternalEmail()];
@@ -124,15 +127,13 @@
'status' => 'success',
'code' => $code->code,
'mode' => $plan->mode ?: 'email',
+ 'domains' => Domain::getPublicDomains(),
+ 'is_domain' => $plan->hasDomain(),
];
if ($plan->mode == Plan::MODE_TOKEN) {
// Token verification, jump to the last step
- $has_domain = $plan->hasDomain();
-
$response['short_code'] = $code->short_code;
- $response['is_domain'] = $has_domain;
- $response['domains'] = $has_domain ? [] : Domain::getPublicDomains();
} else {
// External email verification, send an email message
SignupVerificationJob::dispatch($code);
@@ -156,18 +157,11 @@
return $this->errorResponse(404);
}
- $has_domain = $this->getPlan()->hasDomain();
-
- $result = [
- 'id' => $id,
- 'is_domain' => $has_domain,
- 'domains' => $has_domain ? [] : Domain::getPublicDomains(),
- ];
+ $result = ['id' => $id];
return response()->json($result);
}
-
/**
* Validation of the verification code.
*
@@ -199,21 +193,26 @@
|| $code->isExpired()
|| 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.signupcodeinvalid')];
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
// For signup last-step mode remember the code object, so we can delete it
// with single SQL query (->delete()) instead of two
- $request->code = $code;
+ $request->merge(['code' => $code]);
+
+ $plan = $this->getPlan($request);
+
+ if (!$plan) {
+ $errors = ['short_code' => self::trans('validation.signupcodeinvalid')];
+ return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ }
if ($update) {
$code->verify_ip_address = $request->ip();
$code->save();
}
- $has_domain = $this->getPlan()->hasDomain();
-
// Return user name and email/phone/voucher from the codes database,
// domains list for selection and "plan type" flag
return response()->json([
@@ -222,8 +221,7 @@
'first_name' => $code->first_name,
'last_name' => $code->last_name,
'voucher' => $code->voucher,
- 'is_domain' => $has_domain,
- 'domains' => $has_domain ? [] : Domain::getPublicDomains(),
+ 'is_domain' => $plan->hasDomain(),
]);
}
@@ -243,10 +241,28 @@
'voucher' => 'max:32',
];
+ if ($request->invitation) {
+ // Signup via invitation
+ $invitation = SignupInvitation::withEnvTenantContext()->find($request->invitation);
+
+ if (empty($invitation) || $invitation->isCompleted()) {
+ return $this->errorResponse(404);
+ }
+
+ // Check optional fields
+ $rules['first_name'] = 'max:128';
+ $rules['last_name'] = 'max:128';
+ }
+
+ if (!$request->code) {
+ $rules['plan'] = 'required';
+ }
+
+ $plan = $this->getPlan($request);
+
// Direct signup by token
if ($request->token) {
// This will validate the token and the plan mode
- $plan = $request->plan ? Plan::withEnvTenantContext()->where('title', $request->plan)->first() : null;
$rules['token'] = ['required', 'string', new SignupTokenRule($plan)];
}
@@ -259,44 +275,16 @@
$settings = [];
- if (!empty($request->token)) {
+ if ($request->token) {
$settings = ['signup_token' => strtoupper($request->token)];
- } elseif (!empty($request->plan) && empty($request->code) && empty($request->invitation)) {
- // Plan parameter is required/allowed in mandate mode
- $plan = Plan::withEnvTenantContext()->where('title', $request->plan)->first();
-
- if (!$plan || $plan->mode != Plan::MODE_MANDATE) {
- $msg = self::trans('validation.exists', ['attribute' => 'plan']);
- return response()->json(['status' => 'error', 'errors' => ['plan' => $msg]], 422);
- }
- } elseif ($request->invitation) {
- // Signup via invitation
- $invitation = SignupInvitation::withEnvTenantContext()->find($request->invitation);
-
- if (empty($invitation) || $invitation->isCompleted()) {
- return $this->errorResponse(404);
- }
-
- // Check required fields
- $v = Validator::make(
- $request->all(),
- [
- 'first_name' => 'max:128',
- 'last_name' => 'max:128',
- ]
- );
-
- $errors = $v->fails() ? $v->errors()->toArray() : [];
-
- if (!empty($errors)) {
- return response()->json(['status' => 'error', 'errors' => $errors], 422);
- }
-
+ } elseif (!empty($invitation)) {
$settings = [
'external_email' => $invitation->email,
'first_name' => $request->first_name,
'last_name' => $request->last_name,
];
+ } elseif (!$request->code && $plan?->mode == Plan::MODE_MANDATE) {
+ // mandate mode
} else {
// Validate verification codes (again)
$v = $this->verify($request, false);
@@ -304,8 +292,6 @@
return $v;
}
- $plan = $this->getPlan();
-
// Get user name/email from the verification code database
$code_data = $v->getData();
@@ -314,6 +300,8 @@
'last_name' => $code_data->last_name,
];
+ $plan = $this->getPlan($request);
+
if ($plan->mode == Plan::MODE_TOKEN) {
$settings['signup_token'] = strtoupper($code_data->email);
} else {
@@ -332,10 +320,6 @@
}
}
- if (empty($plan)) {
- $plan = $this->getPlan();
- }
-
$is_domain = $plan->hasDomain();
// Validate login
@@ -345,7 +329,6 @@
// Set some properties for signup() method
$request->settings = $settings;
- $request->plan = $plan;
$request->discount = $discount ?? null;
$request->invitation = $invitation ?? null;
@@ -391,7 +374,7 @@
DB::beginTransaction();
// Create domain record
- if ($is_domain) {
+ if ($is_domain && !Domain::withTrashed()->where('namespace', $domain_name)->exists()) {
$domain = Domain::create([
'namespace' => $domain_name,
'type' => Domain::TYPE_EXTERNAL,
@@ -452,7 +435,7 @@
// Bump up counter on the signup token
if (!empty($request->settings['signup_token'])) {
- \App\SignupToken::where('id', $request->settings['signup_token'])->increment('counter');
+ SignupToken::where('id', $request->settings['signup_token'])->increment('counter');
}
DB::commit();
@@ -508,7 +491,7 @@
$mandate = [
'currency' => $wallet->currency,
- 'description' => \App\Tenant::getConfig($user->tenant_id, 'app.name')
+ 'description' => Tenant::getConfig($user->tenant_id, 'app.name')
. ' ' . self::trans('app.mandate-description-suffix'),
'methodId' => PaymentProvider::METHOD_CREDITCARD,
@@ -576,13 +559,15 @@
/**
* Returns plan for the signup process
*
+ * @param \Illuminate\Http\Request $request HTTP request
+ *
* @returns \App\Plan Plan object selected for current signup process
*/
- protected function getPlan()
+ protected function getPlan(Request $request)
{
- $request = request();
+ if (!$request->plan instanceof Plan) {
+ $plan = null;
- if (!$request->plan || !$request->plan instanceof Plan) {
// Get the plan if specified and exists...
if (($request->code instanceof SignupCode) && $request->code->plan) {
$plan = Plan::withEnvTenantContext()->where('title', $request->code->plan)->first();
@@ -590,13 +575,7 @@
$plan = Plan::withEnvTenantContext()->where('title', $request->plan)->first();
}
- // ...otherwise use the default plan
- if (empty($plan)) {
- // TODO: Get default plan title from config
- $plan = Plan::withEnvTenantContext()->where('title', 'individual')->first();
- }
-
- $request->plan = $plan;
+ $request->merge(['plan' => $plan]);
}
return $request->plan;
@@ -605,13 +584,13 @@
/**
* Login (kolab identity) validation
*
- * @param string $login Login (local part of an email address)
- * @param string $domain Domain name
- * @param bool $external Enables additional checks for domain part
+ * @param string $login Login (local part of an email address)
+ * @param string $namespace Domain name
+ * @param bool $external Enable signup for a custom domain
*
* @return array Error messages on validation error
*/
- protected static function validateLogin($login, $domain, $external = false): ?array
+ protected static function validateLogin($login, $namespace, $external = false): ?array
{
// Validate login part alone
$v = Validator::make(
@@ -623,29 +602,35 @@
return ['login' => $v->errors()->toArray()['login'][0]];
}
- $domains = $external ? null : Domain::getPublicDomains();
-
- // Validate the domain
- $v = Validator::make(
- ['domain' => $domain],
- ['domain' => ['required', 'string', new UserEmailDomain($domains)]]
- );
-
- if ($v->fails()) {
- return ['domain' => $v->errors()->toArray()['domain'][0]];
+ $domain = null;
+ if (is_string($namespace)) {
+ $namespace = Str::lower($namespace);
+ $domain = Domain::withTrashed()->where('namespace', $namespace)->first();
}
- $domain = Str::lower($domain);
+ if ($domain && $domain->isPublic() && !$domain->trashed()) {
+ // no error, everyone can signup for an existing public domain
+ } elseif ($domain) {
+ // domain exists and is not public (or is deleted)
+ return ['domain' => self::trans('validation.domainnotavailable')];
+ } else {
+ // non-existing custom domain
+ if (!$external) {
+ return ['domain' => self::trans('validation.domaininvalid')];
+ }
+
+ $v = Validator::make(
+ ['domain' => $namespace],
+ ['domain' => ['required', 'string', new UserEmailDomain()]]
+ );
- // Check if domain is already registered with us
- if ($external) {
- if (Domain::withTrashed()->where('namespace', $domain)->exists()) {
- return ['domain' => self::trans('validation.domainexists')];
+ if ($v->fails()) {
+ return ['domain' => $v->errors()->toArray()['domain'][0]];
}
}
// Check if user with specified login already exists
- $email = $login . '@' . $domain;
+ $email = $login . '@' . $namespace;
if (User::emailExists($email) || User::aliasExists($email) || \App\Group::emailExists($email)) {
return ['login' => self::trans('validation.loginexists')];
}
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
@@ -7,7 +7,6 @@
use App\License;
use App\Plan;
use App\Rules\Password;
-use App\Rules\UserEmailDomain;
use App\Rules\UserEmailLocal;
use App\Sku;
use App\User;
@@ -248,8 +247,7 @@
$result = [
'skus' => $skus,
'enableBeta' => $hasBeta,
- // TODO: This will change when we enable all users to create domains
- 'enableDomains' => $isController && $hasCustomDomain,
+ 'enableDomains' => $isController && ($hasCustomDomain || $plan?->hasDomain()),
'enableDistlists' => $isController && $hasCustomDomain && \config('app.with_distlists'),
'enableFiles' => !$isDegraded && $hasBeta && \config('app.with_files'),
'enableFolders' => $isController && $hasCustomDomain && \config('app.with_shared_folders'),
@@ -260,7 +258,7 @@
'enableUsers' => $isController,
'enableWallets' => $isController && \config('app.with_wallet'),
'enableWalletMandates' => $isController,
- 'enableWalletPayments' => $isController && (!$plan || $plan->mode != Plan::MODE_MANDATE),
+ 'enableWalletPayments' => $isController && $plan?->mode != Plan::MODE_MANDATE,
'enableCompanionapps' => $hasBeta && \config('app.with_companion_app'),
];
diff --git a/src/app/Observers/EntitlementObserver.php b/src/app/Observers/EntitlementObserver.php
--- a/src/app/Observers/EntitlementObserver.php
+++ b/src/app/Observers/EntitlementObserver.php
@@ -33,13 +33,11 @@
return false;
}
- $sku = \App\Sku::find($entitlement->sku_id);
-
- if (!$sku) {
+ if (empty($entitlement->sku)) {
return false;
}
- $result = $sku->handler_class::preReq($entitlement, $wallet->owner);
+ $result = $entitlement->sku->handler_class::preReq($entitlement, $wallet->owner);
if (!$result) {
return false;
diff --git a/src/app/Plan.php b/src/app/Plan.php
--- a/src/app/Plan.php
+++ b/src/app/Plan.php
@@ -23,7 +23,6 @@
* @property string $id
* @property string $mode Plan signup mode (Plan::MODE_*)
* @property string $name
- * @property \App\Package[] $packages
* @property datetime $promo_from
* @property datetime $promo_to
* @property ?int $tenant_id
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -150,22 +150,32 @@
* @param \App\Domain $domain Optional domain object
*
* @return \App\User Self
+ * @throws \Exception
*/
public function assignPlan($plan, $domain = null): User
{
- $this->setSetting('plan_id', $plan->id);
+ $domain_packages = $plan->packages->filter(function ($package) {
+ return $package->isDomain();
+ });
- foreach ($plan->packages as $package) {
- if ($package->isDomain()) {
- if (!$domain) {
- throw new \Exception("Attempted to assign a domain package without passing a domain.");
- }
+ // Before we do anything let's make sure that a custom domain can be assigned only
+ // to a plan with a domain package
+ if ($domain && $domain_packages->isEmpty()) {
+ throw new \Exception("Custom domain requires a plan with a domain SKU");
+ }
+
+ foreach ($plan->packages->diff($domain_packages) as $package) {
+ $this->assignPackage($package);
+ }
+
+ if ($domain) {
+ foreach ($domain_packages as $package) {
$domain->assignPackage($package, $this);
- } else {
- $this->assignPackage($package);
}
}
+ $this->setSetting('plan_id', $plan->id);
+
return $this;
}
diff --git a/src/resources/js/app.js b/src/resources/js/app.js
--- a/src/resources/js/app.js
+++ b/src/resources/js/app.js
@@ -356,6 +356,12 @@
// Add 'class' attribute to the body, different for each page
// so, we can apply page-specific styles
document.body.className = 'page-' + (name || this.pageName()).replace(/\/.*$/, '').toLowerCase()
+ },
+ updateStatusInfo() {
+ axios.get('/api/v4/users/' + this.authInfo.id + '/status')
+ .then(response => {
+ this.authInfo.statusInfo = response.data
+ })
}
}
})
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
@@ -436,6 +436,7 @@
'step2' => "We sent out a confirmation code to your email address. Enter the code we sent you, or click the link in the message.",
'step3' => "Create your {app} identity (you can choose additional addresses later).",
'created' => "The account is about to be created!",
+ 'owndomain' => "Use your own domain",
'token' => "Signup authorization token",
'voucher' => "Voucher Code",
],
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
@@ -176,6 +176,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.',
/*
|--------------------------------------------------------------------------
diff --git a/src/resources/vue/Domain/Info.vue b/src/resources/vue/Domain/Info.vue
--- a/src/resources/vue/Domain/Info.vue
+++ b/src/resources/vue/Domain/Info.vue
@@ -162,6 +162,7 @@
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
this.$router.push({ name: 'domains' })
+ this.$root.updateStatusInfo()
}
})
},
@@ -179,6 +180,7 @@
.then(response => {
this.$toast.success(response.data.message)
this.$router.push({ name: 'domains' })
+ this.$root.updateStatusInfo()
})
},
submitSettings() {
diff --git a/src/resources/vue/Signup.vue b/src/resources/vue/Signup.vue
--- a/src/resources/vue/Signup.vue
+++ b/src/resources/vue/Signup.vue
@@ -1,6 +1,6 @@
<template>
<div class="container">
- <div id="step0" v-if="!invitation">
+ <div id="step0">
<div class="plan-selector row row-cols-sm-2 g-3">
<div v-for="item in plans" :key="item.id" :id="'plan-' + item.title">
<div :class="'card bg-light plan-' + item.title">
@@ -39,7 +39,7 @@
<label for="signup_email" class="visually-hidden">{{ $t('signup.email') }}</label>
<input type="text" class="form-control" id="signup_email" :placeholder="$t('signup.email')" required v-model="email">
</div>
- <btn class="btn-secondary" @click="stepBack">{{ $t('btn.back') }}</btn>
+ <btn class="btn-secondary" @click="stepBack" v-if="plans.length > 1">{{ $t('btn.back') }}</btn>
<btn class="btn-primary ms-2" type="submit" icon="check">{{ $t('btn.continue') }}</btn>
</form>
</div>
@@ -78,10 +78,14 @@
</div>
<div class="mb-3">
<label for="signup_login" class="visually-hidden"></label>
+ <div v-if="is_domain" class="form-check float-end text-secondary">
+ <input class="form-check-input" type="checkbox" value="1" id="custom_domain" @change="useCustomDomain">
+ <label class="form-check-label" for="custom_domain">{{ $t('signup.owndomain') }}</label>
+ </div>
<div class="input-group">
<input type="text" class="form-control" id="signup_login" required v-model="login" :placeholder="$t('signup.login')">
<span class="input-group-text">@</span>
- <input v-if="is_domain" type="text" class="form-control rounded-end" id="signup_domain" required v-model="domain" :placeholder="$t('form.domain')">
+ <input v-if="use_custom" type="text" class="form-control rounded-end" id="signup_domain" required :placeholder="$t('form.domain')" v-model="domain">
<select v-else class="form-select rounded-end" id="signup_domain" required v-model="domain">
<option v-for="_domain in domains" :key="_domain" :value="_domain">{{ _domain }}</option>
</select>
@@ -92,7 +96,7 @@
<label for="signup_voucher" class="visually-hidden">{{ $t('signup.voucher') }}</label>
<input type="text" class="form-control" id="signup_voucher" :placeholder="$t('signup.voucher')" v-model="voucher">
</div>
- <btn v-if="!invitation" class="btn-secondary me-2" @click="stepBack">{{ $t('btn.back') }}</btn>
+ <btn v-if="!invitation || plans.length > 1" class="btn-secondary me-2" @click="stepBack">{{ $t('btn.back') }}</btn>
<btn class="btn-primary" type="submit" icon="check">
<span v-if="invitation">{{ $t('btn.signup') }}</span>
<span v-else>{{ $t('btn.submit') }}</span>
@@ -176,7 +180,9 @@
},
plans: [],
referral: '',
+ selectedDomain: '',
token: '',
+ use_custom: false,
voucher: ''
}
},
@@ -201,14 +207,7 @@
axios.get('/api/auth/signup/invitations/' + params[1], { loader: true })
.then(response => {
this.invitation = response.data
- this.login = response.data.login
- this.voucher = response.data.voucher
- this.first_name = response.data.first_name
- this.last_name = response.data.last_name
- this.plan = response.data.plan
- this.is_domain = response.data.is_domain
- this.setDomain(response.data)
- this.displayForm(3, true)
+ this.displayForm(0)
})
.catch(error => {
this.$root.errorHandler(error)
@@ -247,29 +246,20 @@
this.plan = title
this.mode = plan.mode
this.is_domain = plan.isDomain
- this.domain = ''
-
- let step = 1
-
- if (plan.mode == 'mandate') {
- step = 3
- if (!plan.isDomain || !this.domains.length) {
- axios.get('/api/auth/signup/domains')
- .then(response => {
- this.displayForm(step, true)
- this.setDomain(response.data)
- })
- return
- }
- }
- this.displayForm(step, true)
+ this.displayForm(plan.mode == 'mandate' || this.invitation ? 3 : 1, true)
}
},
step0(plan) {
if (!this.plans.length) {
axios.get('/api/auth/signup/plans', { loader: true }).then(response => {
this.plans = response.data.plans
+ if (this.plans.length == 1) {
+ if (!plan || plan != this.plans[0].title) {
+ plan = this.plans[0].title
+ }
+ }
+
this.selectPlanByTitle(plan)
})
.catch(error => {
@@ -291,12 +281,8 @@
this.short_code = response.data.short_code
this.mode = response.data.mode
this.is_domain = response.data.is_domain
+ this.setDomain(response.data)
this.displayForm(this.mode == 'token' ? 3 : 2, true)
-
- // Fill the domain selector with available domains
- if (!this.is_domain) {
- this.setDomain(response.data)
- }
})
},
// Submits the code to the API for verification
@@ -311,19 +297,13 @@
axios.post('/api/auth/signup/verify', post)
.then(response => {
- this.displayForm(3, true)
// Reset user name/email/plan, we don't have them if user used a verification link
this.first_name = response.data.first_name
this.last_name = response.data.last_name
this.email = response.data.email
this.is_domain = response.data.is_domain
this.voucher = response.data.voucher
- this.domain = ''
-
- // Fill the domain selector with available domains
- if (!this.is_domain) {
- this.setDomain(response.data)
- }
+ this.displayForm(3, true)
})
.catch(error => {
if (bylink === true) {
@@ -366,7 +346,7 @@
const card = $(e.target).closest('.card[id^="step"]')
let step = card.attr('id').replace('step', '')
- card.addClass('d-none').find('form')[0].reset()
+ card.addClass('d-none')
step -= 1
@@ -374,7 +354,7 @@
step = 1
}
- if (this.mode == 'mandate' && step < 3) {
+ if ((this.invitation || this.mode == 'mandate') && step < 3) {
step = 0
}
@@ -394,10 +374,19 @@
return this.step0()
}
- $('#step' + step).removeClass('d-none').find('form')[0].reset()
+ if (step > 2 && !this.domains.length) {
+ axios.get('/api/auth/signup/domains')
+ .then(response => {
+ this.setDomain(response.data)
+ this.displayForm(step, focus)
+ })
+ return
+ }
+
+ $('#step' + step).removeClass('d-none')
if (focus) {
- $('#step' + step).find('input').first().focus()
+ $('#step' + step).find('input:not([type=checkbox])').first().focus()
}
},
lastStepPostData() {
@@ -430,6 +419,12 @@
this.domain = this.domains[0]
}
}
+
+ this.selectedDomain = this.domain
+ },
+ useCustomDomain(event) {
+ this.use_custom = event.target.checked
+ this.domain = this.use_custom ? '' : this.selectedDomain
}
}
}
diff --git a/src/tests/Browser/DomainTest.php b/src/tests/Browser/DomainTest.php
--- a/src/tests/Browser/DomainTest.php
+++ b/src/tests/Browser/DomainTest.php
@@ -3,6 +3,7 @@
namespace Tests\Browser;
use App\Domain;
+use App\Plan;
use App\User;
use Tests\Browser;
use Tests\Browser\Components\Dialog;
@@ -22,7 +23,9 @@
public function setUp(): void
{
parent::setUp();
+
$this->deleteTestDomain('testdomain.com');
+ $this->deleteTestUser('testuserdomain@' . \config('app.domain'));
}
/**
@@ -31,6 +34,8 @@
public function tearDown(): void
{
$this->deleteTestDomain('testdomain.com');
+ $this->deleteTestUser('testuserdomain@' . \config('app.domain'));
+
parent::tearDown();
}
@@ -217,8 +222,8 @@
*/
public function testDomainListEmpty(): void
{
+ // User is not a wallet controller
$this->browse(function ($browser) {
- // Login the user
$browser->visit('/login')
->on(new Home())
->submitLogon('jack@kolab.org', 'simple123', true)
@@ -227,15 +232,51 @@
->assertMissing('@links a.link-domains')
->assertMissing('@links a.link-users')
->assertMissing('@links a.link-wallet');
-/*
- // On dashboard click the "Domains" link
- ->assertSeeIn('@links a.link-domains', 'Domains')
+ });
+
+ // A group account without a custom domain
+ $user = $this->getTestUser('testuserdomain@' . \config('app.domain'), ['password' => 'simple123']);
+ $plan = Plan::withObjectTenantContext($user)->where('title', 'group')->first();
+ $user->setSetting('plan_id', $plan->id);
+
+ $this->browse(function ($browser) use ($user) {
+ $browser->visit('/login')
+ ->on(new Home())
+ ->submitLogon($user->email, 'simple123', true)
+ ->on(new Dashboard())
+ ->assertVisible('@links a.link-settings')
+ ->assertVisible('@links a.link-domains')
+ ->assertMissing('@links a.link-shared-folders')
->click('@links a.link-domains')
- // On Domains List page click the domain entry
+ // We'll create/delete a domain and make sure new Dashboard items appear after that.
+ // More precise tests on the domain creation/deletion and list pages are in another place
->on(new DomainList())
- ->assertMissing('@table tbody')
- ->assertSeeIn('tfoot td', 'There are no domains in this account.');
-*/
+ ->click('.card-title button.btn-success')
+ ->on(new DomainInfo())
+ ->type('@general #namespace', 'testdomain.com')
+ ->click('button[type=submit]')
+ ->on(new DomainList())
+ ->assertSeeIn('@table tr:nth-child(1) a', 'testdomain.com')
+ ->click('a.link-dashboard')
+ ->on(new Dashboard())
+ ->assertVisible('@links a.link-domains')
+ ->assertVisible('@links a.link-shared-folders')
+ ->click('@links a.link-domains')
+ ->on(new DomainList())
+ ->click('@table tbody tr:nth-child(1) a')
+ ->on(new DomainInfo())
+ ->waitFor('button.button-delete')
+ ->click('button.button-delete')
+ ->with(new Dialog('#delete-warning'), function ($browser) {
+ $browser->click('@button-action');
+ })
+ ->waitUntilMissing('#delete-warning')
+ ->on(new DomainList())
+ ->assertElementsCount('@table tbody tr', 0)
+ ->click('a.link-dashboard')
+ ->on(new Dashboard())
+ ->assertVisible('@links a.link-domains')
+ ->assertMissing('@links a.link-shared-folders');
});
}
diff --git a/src/tests/Browser/Pages/DomainList.php b/src/tests/Browser/Pages/DomainList.php
--- a/src/tests/Browser/Pages/DomainList.php
+++ b/src/tests/Browser/Pages/DomainList.php
@@ -25,7 +25,7 @@
*/
public function assert($browser)
{
- $browser->assertPathIs($this->url())
+ $browser->waitForLocation($this->url())
->waitUntilMissing('@app .app-loader')
->assertSeeIn('@list .card-title', 'Domains');
}
diff --git a/src/tests/Browser/SignupTest.php b/src/tests/Browser/SignupTest.php
--- a/src/tests/Browser/SignupTest.php
+++ b/src/tests/Browser/SignupTest.php
@@ -34,6 +34,7 @@
$this->deleteTestDomain('user-domain-signup.com');
Plan::whereNot('mode', Plan::MODE_EMAIL)->update(['mode' => Plan::MODE_EMAIL]);
+ Plan::withEnvTenantContext()->where('title', 'individual')->update(['hidden' => false]);
SignupToken::truncate();
ReferralProgram::query()->delete();
}
@@ -49,6 +50,7 @@
SignupInvitation::truncate();
Plan::whereNot('mode', Plan::MODE_EMAIL)->update(['mode' => Plan::MODE_EMAIL]);
+ Plan::withEnvTenantContext()->where('title', 'individual')->update(['hidden' => false]);
SignupToken::truncate();
ReferralProgram::query()->delete();
@@ -323,6 +325,8 @@
->assertVisible('#signup_password')
->assertVisible('#signup_password_confirmation')
->assertVisible('select#signup_domain')
+ ->assertMissing('#custom_domain')
+ ->assertMissing('input#signup_domain')
->assertElementsCount('select#signup_domain option', $domains_count, false)
->assertText('select#signup_domain option:nth-child(1)', $domains[0])
->assertValue('select#signup_domain option:nth-child(1)', $domains[0])
@@ -456,15 +460,28 @@
->click('[type=submit]');
});
- // Here we expect 4 text inputs, Back and Continue buttons
+ // Here we expect text inputs, as well as domain selector, and buttons
$browser->whenAvailable('@step3', function ($step) {
+ $domains = Domain::getPublicDomains();
+ $domains_count = count($domains);
+
$step->assertVisible('#signup_login')
->assertVisible('#signup_password')
->assertVisible('#signup_password_confirmation')
- ->assertVisible('input#signup_domain')
+ ->assertVisible('select#signup_domain')
+ ->assertMissing('input#signup_domain')
+ ->assertVisible('#custom_domain')
+ ->assertElementsCount('select#signup_domain option', $domains_count, false)
+ ->assertText('select#signup_domain option:nth-child(1)', $domains[0])
+ ->assertValue('select#signup_domain option:nth-child(1)', $domains[0])
+ ->assertText('select#signup_domain option:nth-child(2)', $domains[1])
+ ->assertValue('select#signup_domain option:nth-child(2)', $domains[1])
->assertVisible('[type=button]')
->assertVisible('[type=submit]')
->assertFocused('#signup_login')
+ ->click('#custom_domain + label')
+ ->assertVisible('input#signup_domain')
+ ->assertMissing('select#signup_domain')
->assertValue('input#signup_domain', '')
->assertValue('#signup_login', '')
->assertValue('#signup_password', '')
@@ -473,8 +490,7 @@
// Submit invalid login and password data
$browser->with('@step3', function ($step) {
- $step->assertFocused('#signup_login')
- ->type('#signup_login', '*')
+ $step->type('#signup_login', '*')
->type('#signup_domain', 'test.com')
->type('#signup_password', '12345678')
->type('#signup_password_confirmation', '123456789')
@@ -524,6 +540,71 @@
});
}
+ /**
+ * Test more signup cases
+ */
+ public function testSignupGroupWithPublicDomain(): void
+ {
+ // We also test skipping plan selection if there's only one plan active
+ $plan = Plan::withEnvTenantContext()->where('title', 'individual')->first();
+ $plan->hidden = true;
+ $plan->save();
+
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new Signup());
+
+ // Submit valid data
+ $browser->whenAvailable('@step1', function ($step) {
+ $step->type('#signup_first_name', 'Test')
+ ->type('#signup_last_name', 'User')
+ ->type('#signup_email', 'BrowserSignupTestUser1@kolab.org')
+ ->click('[type=submit]');
+ });
+
+ // Submit valid code
+ $browser->whenAvailable('@step2', function ($step) {
+ // Get the code and short_code from database
+ $code = $step->value('#signup_code');
+ $code = SignupCode::find($code);
+
+ $step->type('#signup_short_code', $code->short_code)
+ ->click('[type=submit]');
+ });
+
+ // Here we expect text inputs, as well as domain selector, and buttons
+ $browser->whenAvailable('@step3', function ($step) {
+ $step->assertVisible('#signup_login')
+ ->assertVisible('#signup_password')
+ ->assertVisible('#signup_password_confirmation')
+ ->assertVisible('select#signup_domain')
+ ->assertMissing('input#signup_domain')
+ ->assertVisible('#custom_domain')
+ ->assertValue('select#signup_domain option:checked', \config('app.domain'))
+ ->type('#signup_login', 'signuptestdusk')
+ ->type('#signup_password', '12345678')
+ ->type('#signup_password_confirmation', '12345678')
+ ->click('[type=submit]');
+ });
+
+ // At this point we should be auto-logged-in to dashboard
+ $browser->waitUntilMissing('@step3')
+ ->waitUntilMissing('.app-loader')
+ ->on(new Dashboard())
+ ->assertUser('signuptestdusk@' . \config('app.domain'))
+ ->assertVisible('@links a.link-settings')
+ ->assertVisible('@links a.link-domains')
+ ->assertVisible('@links a.link-users')
+ ->assertVisible('@links a.link-wallet')
+ ->assertMissing('@links a.link-distlists')
+ ->assertMissing('@links a.link-shared-folders')
+ ->assertMissing('@links a.link-resources');
+
+ $browser->within(new Menu(), function ($browser) {
+ $browser->clickMenuItem('logout');
+ });
+ });
+ }
+
/**
* Test signup with a mandate plan, also the UI lock
*
@@ -558,6 +639,7 @@
$domains_count = count($domains);
$browser->assertMissing('.card-title')
+ ->waitFor('select#signup_domain option:checked')
->assertElementsCount('select#signup_domain option', $domains_count, false)
->assertText('select#signup_domain option:nth-child(1)', $domains[0])
->assertValue('select#signup_domain option:nth-child(1)', $domains[0])
@@ -585,7 +667,6 @@
})
->on(new PaymentMollie())
->assertSeeIn('@title', 'Auto-Payment Setup')
- ->assertMissing('@amount')
->submitPayment('open')
->on(new PaymentStatus())
->assertSeeIn('@lock-alert', 'The account is locked')
@@ -755,6 +836,7 @@
// Step 2
->whenAvailable('@step3', function ($browser) {
$browser->assertSeeIn('.card-title', 'Sign Up - Step 2/2')
+ ->click('#custom_domain')
->type('input#signup_domain', 'user-domain-signup.com')
->type('#signup_login', 'admin')
->type('#signup_password', '12345678')
@@ -902,11 +984,13 @@
$browser->visit('/signup/invite/' . $invitation->id)
->onWithoutAssert(new Signup())
->waitUntilMissing('.app-loader')
+ ->waitFor('@step0')
+ ->click('.plan-individual button')
->with('@step3', function ($step) {
$domains_count = count(Domain::getPublicDomains());
$step->assertMissing('.card-title')
- ->assertVisible('#signup_last_name')
+ ->waitFor('#signup_last_name')
->assertVisible('#signup_first_name')
->assertVisible('#signup_login')
->assertVisible('#signup_password')
@@ -914,7 +998,7 @@
->assertVisible('select#signup_domain')
->assertElementsCount('select#signup_domain option', $domains_count, false)
->assertVisible('[type=submit]')
- ->assertMissing('[type=button]') // Back button
+ ->assertVisible('[type=button]') // Back button
->assertSeeIn('[type=submit]', 'Sign Up')
->assertFocused('#signup_first_name')
->assertValue('select#signup_domain', \config('app.domain'))
diff --git a/src/tests/Feature/Controller/SignupTest.php b/src/tests/Feature/Controller/SignupTest.php
--- a/src/tests/Feature/Controller/SignupTest.php
+++ b/src/tests/Feature/Controller/SignupTest.php
@@ -188,14 +188,16 @@
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
- $this->assertCount(1, $json['errors']);
+ $this->assertCount(2, $json['errors']);
$this->assertArrayHasKey('email', $json['errors']);
+ $this->assertArrayHasKey('plan', $json['errors']);
// Data with missing name
$data = [
'email' => 'UsersApiControllerTest1@UsersApiControllerTest.com',
'first_name' => str_repeat('a', 250),
'last_name' => str_repeat('a', 250),
+ 'plan' => 'individual',
];
$response = $this->post('/api/auth/signup/init', $data);
@@ -208,11 +210,12 @@
$this->assertArrayHasKey('first_name', $json['errors']);
$this->assertArrayHasKey('last_name', $json['errors']);
- // Data with invalid email (but not phone number)
+ // Data with invalid email (but not phone number), and invalid plan
$data = [
'email' => '@example.org',
'first_name' => 'Signup',
'last_name' => 'User',
+ 'plan' => 'invalid',
];
$response = $this->post('/api/auth/signup/init', $data);
@@ -221,13 +224,15 @@
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
- $this->assertCount(1, $json['errors']);
+ $this->assertCount(2, $json['errors']);
$this->assertArrayHasKey('email', $json['errors']);
+ $this->assertArrayHasKey('plan', $json['errors']);
// Sanity check on voucher code, last/first name is optional
$data = [
'voucher' => '123456789012345678901234567890123',
'email' => 'valid@email.com',
+ 'plan' => 'individual',
];
$response = $this->post('/api/auth/signup/init', $data);
@@ -244,6 +249,7 @@
'email' => str_repeat('a', 190) . '@example.org',
'first_name' => 'Signup',
'last_name' => 'User',
+ 'plan' => 'individual',
];
$response = $this->post('/api/auth/signup/init', $data);
@@ -262,6 +268,7 @@
'email' => 'test@example.org',
'first_name' => 'Signup',
'last_name' => 'User',
+ 'plan' => 'individual',
];
\config(['app.signup.email_limit' => 0]);
@@ -287,6 +294,7 @@
'email' => 'ip@example.org',
'first_name' => 'Signup',
'last_name' => 'User',
+ 'plan' => 'individual',
];
\config(['app.signup.email_limit' => 0]);
@@ -326,7 +334,7 @@
'email' => 'testuser@external.com',
'first_name' => 'Signup',
'last_name' => 'User',
- 'plan' => 'individual',
+ 'plan' => 'group',
];
$response = $this->post('/api/auth/signup/init', $data, ['REMOTE_ADDR' => '10.1.1.2']);
@@ -334,10 +342,12 @@
$response->assertStatus(200);
- $this->assertCount(3, $json);
+ $this->assertCount(5, $json);
$this->assertSame('success', $json['status']);
$this->assertSame('email', $json['mode']);
+ $this->assertSame(true, $json['is_domain']);
$this->assertNotEmpty($json['code']);
+ $this->assertSame($all_domains = Domain::getPublicDomains(), $json['domains']);
$code = SignupCode::find($json['code']);
@@ -361,15 +371,18 @@
// Try the same with voucher
$data['voucher'] = 'TEST';
+ $data['plan'] = 'individual';
$response = $this->post('/api/auth/signup/init', $data);
$json = $response->json();
$response->assertStatus(200);
- $this->assertCount(3, $json);
+ $this->assertCount(5, $json);
$this->assertSame('success', $json['status']);
$this->assertSame('email', $json['mode']);
+ $this->assertSame(false, $json['is_domain']);
$this->assertNotEmpty($json['code']);
+ $this->assertSame($all_domains, $json['domains']);
// Assert the job has proper data assigned
Queue::assertPushed(\App\Jobs\Mail\SignupVerificationJob::class, function ($job) use ($data, $json) {
@@ -461,14 +474,14 @@
$json = $response->json();
$response->assertStatus(200);
- $this->assertCount(7, $json);
+ $this->assertCount(6, $json);
$this->assertSame('success', $json['status']);
$this->assertSame($result['email'], $json['email']);
$this->assertSame($result['first_name'], $json['first_name']);
$this->assertSame($result['last_name'], $json['last_name']);
$this->assertSame($result['voucher'], $json['voucher']);
$this->assertSame(false, $json['is_domain']);
- $this->assertTrue(is_array($json['domains']) && !empty($json['domains']));
+ //$this->assertTrue(is_array($json['domains']) && !empty($json['domains']));
$code->refresh();
@@ -494,10 +507,11 @@
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
- $this->assertCount(3, $json['errors']);
+ $this->assertCount(4, $json['errors']);
$this->assertArrayHasKey('login', $json['errors']);
$this->assertArrayHasKey('password', $json['errors']);
$this->assertArrayHasKey('domain', $json['errors']);
+ $this->assertArrayHasKey('plan', $json['errors']);
$domain = $this->getPublicDomain();
@@ -506,6 +520,7 @@
'login' => 'test',
'password' => 'test',
'password_confirmation' => 'test2',
+ 'plan' => 'individual',
];
$response = $this->post('/api/auth/signup', $data);
@@ -525,6 +540,7 @@
'domain' => $domain,
'password' => 'test',
'password_confirmation' => 'test',
+ 'plan' => 'indvalid',
];
$response = $this->post('/api/auth/signup', $data);
@@ -532,9 +548,10 @@
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
- $this->assertCount(2, $json['errors']);
+ $this->assertCount(3, $json['errors']);
$this->assertArrayHasKey('login', $json['errors']);
$this->assertArrayHasKey('password', $json['errors']);
+ $this->assertArrayHasKey('plan', $json['errors']);
// Missing codes
$data = [
@@ -542,6 +559,7 @@
'domain' => $domain,
'password' => 'testtest',
'password_confirmation' => 'testtest',
+ 'plan' => 'individual',
];
$response = $this->post('/api/auth/signup', $data);
@@ -719,10 +737,12 @@
$json = $response->json();
$response->assertStatus(200);
- $this->assertCount(3, $json);
+ $this->assertCount(5, $json);
$this->assertSame('success', $json['status']);
$this->assertSame('email', $json['mode']);
+ $this->assertSame(true, $json['is_domain']);
$this->assertNotEmpty($json['code']);
+ $this->assertSame(Domain::getPublicDomains(), $json['domains']);
// Assert the email sending job was pushed once
Queue::assertPushed(\App\Jobs\Mail\SignupVerificationJob::class, 1);
@@ -749,14 +769,13 @@
$result = $response->json();
$response->assertStatus(200);
- $this->assertCount(7, $result);
+ $this->assertCount(6, $result);
$this->assertSame('success', $result['status']);
$this->assertSame($user_data['email'], $result['email']);
$this->assertSame($user_data['first_name'], $result['first_name']);
$this->assertSame($user_data['last_name'], $result['last_name']);
$this->assertSame(null, $result['voucher']);
$this->assertSame(true, $result['is_domain']);
- $this->assertSame([], $result['domains']);
// Final signup request
$login = 'admin';
@@ -871,7 +890,7 @@
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
- $this->assertSame("The selected plan is invalid.", $json['errors']['plan']);
+ $this->assertSame(["The plan field is required."], $json['errors']['plan']);
// Test valid input
$post['plan'] = $plan->title;
@@ -907,14 +926,21 @@
'domain' => 'kolabnow.com',
'password' => 'testtest',
'password_confirmation' => 'testtest',
+ 'plan' => 'individual',
];
// Test invalid invitation identifier
$response = $this->post('/api/auth/signup', $post);
$response->assertStatus(404);
- // Test valid input
+ // Test invalid plan
$post['invitation'] = $invitation->id;
+ $post['plan'] = 'invalid';
+ $response = $this->post('/api/auth/signup', $post);
+ $response->assertStatus(422);
+
+ // Test valid input
+ $post['plan'] = 'individual';
$response = $this->post('/api/auth/signup', $post);
$result = $response->json();
@@ -1034,10 +1060,13 @@
$json = $response->json();
$this->assertSame('error', $json['status']);
- $this->assertSame(['referral' => ["The referral program code is invalid."]], $json['errors']);
+ $this->assertCount(2, $json['errors']);
+ $this->assertSame(['The referral program code is invalid.'], $json['errors']['referral']);
+ $this->assertSame(['The plan field is required.'], $json['errors']['plan']);
// Test valid code
$post['referral'] = $referral_code->code;
+ $post['plan'] = 'individual';
$response = $this->post('/api/auth/signup/init', $post);
$json = $response->json();
@@ -1098,11 +1127,12 @@
$json = $response->json();
$this->assertSame('error', $json['status']);
- $this->assertCount(4, $json['errors']);
+ $this->assertCount(5, $json['errors']);
$this->assertSame(["The login must be at least 2 characters."], $json['errors']['login']);
$this->assertSame(["The password confirmation does not match."], $json['errors']['password']);
$this->assertSame(["The domain field is required."], $json['errors']['domain']);
$this->assertSame(["The voucher may not be greater than 32 characters."], $json['errors']['voucher']);
+ $this->assertSame(["The plan field is required."], $json['errors']['plan']);
// Test with mode=mandate plan, but invalid voucher code
$post = [
@@ -1182,6 +1212,7 @@
['test.test', $domain, false, null],
['test_test', $domain, false, null],
['test-test', $domain, false, null],
+ ['test-test', 'kolab.org', false, ['domain' => 'The specified domain is not available.']],
['admin', $domain, false, ['login' => 'The specified login is not available.']],
['administrator', $domain, false, ['login' => 'The specified login is not available.']],
['sales', $domain, false, ['login' => 'The specified login is not available.']],
@@ -1189,6 +1220,7 @@
// Domain account
['admin', 'kolabsys.com', true, null],
+ ['testsystemdomain', $domain, true, null],
['testnonsystemdomain', 'invalid', true, ['domain' => 'The specified domain is invalid.']],
['testnonsystemdomain', '.com', true, ['domain' => 'The specified domain is invalid.']],
];
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
@@ -674,6 +674,17 @@
$result = UsersController::statusInfo($user);
$this->assertTrue($result['enableBeta']);
$this->assertFalse($result['enableRooms']);
+
+ // Test user in a group account without a custom domain
+ $user = $this->getTestUser('UsersControllerTest2@userscontroller.com');
+ $plan = Plan::withObjectTenantContext($user)->where('title', 'group')->first();
+ $user->setSetting('plan_id', $plan->id);
+
+ $result = UsersController::statusInfo($user);
+ $this->assertTrue($result['enableDomains']);
+ $this->assertFalse($result['enableFolders']);
+ $this->assertFalse($result['enableDistlists']);
+ $this->assertFalse($result['enableResources']);
}
/**
diff --git a/src/tests/Feature/UserTest.php b/src/tests/Feature/UserTest.php
--- a/src/tests/Feature/UserTest.php
+++ b/src/tests/Feature/UserTest.php
@@ -7,6 +7,7 @@
use App\Group;
use App\Package;
use App\PackageSku;
+use App\Plan;
use App\Sku;
use App\User;
use App\Auth\Utils as AuthUtils;
@@ -115,7 +116,37 @@
*/
public function testAssignPlan(): void
{
- $this->markTestIncomplete();
+ $domain = $this->getTestDomain('useraccount.com', [
+ 'status' => Domain::STATUS_NEW | Domain::STATUS_ACTIVE,
+ 'type' => Domain::TYPE_EXTERNAL,
+ ]);
+ $user = $this->getTestUser('useraccounta@' . $domain->namespace);
+ $plan = Plan::withObjectTenantContext($user)->where('title', 'group')->first();
+
+ // Group plan without domain
+ $user->assignPlan($plan);
+
+ $this->assertSame((string) $plan->id, $user->getSetting('plan_id'));
+ $this->assertSame(7, $user->entitlements()->count()); // 5 storage + 1 mailbox + 1 groupware
+
+ $user = $this->getTestUser('useraccountb@' . $domain->namespace);
+
+ // Group plan with a domain
+ $user->assignPlan($plan, $domain);
+
+ $this->assertSame((string) $plan->id, $user->getSetting('plan_id'));
+ $this->assertSame(7, $user->entitlements()->count()); // 5 storage + 1 mailbox + 1 groupware
+ $this->assertSame(1, $domain->entitlements()->count());
+
+ // Individual plan (domain is not allowed)
+ $user = $this->getTestUser('user-test@' . \config('app.domain'));
+ $plan = Plan::withObjectTenantContext($user)->where('title', 'individual')->first();
+
+ $this->expectException(\Exception::class);
+ $user->assignPlan($plan, $domain);
+
+ $this->assertNull($user->getSetting('plan_id'));
+ $this->assertSame(0, $user->entitlements()->count());
}
/**

File Metadata

Mime Type
text/plain
Expires
Sat, Apr 4, 10:38 AM (20 h, 18 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18795918
Default Alt Text
D5122.1775299130.diff (56 KB)

Event Timeline