Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117798958
D3815.1775265280.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
33 KB
Referenced Files
None
Subscribers
None
D3815.1775265280.diff
View Options
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
@@ -4,11 +4,11 @@
use App\Http\Controllers\Controller;
use App\Jobs\SignupVerificationEmail;
-use App\Jobs\SignupVerificationSMS;
use App\Discount;
use App\Domain;
use App\Plan;
use App\Rules\SignupExternalEmail;
+use App\Rules\SignupToken;
use App\Rules\Password;
use App\Rules\UserEmailDomain;
use App\Rules\UserEmailLocal;
@@ -39,11 +39,18 @@
// Use reverse order just to have individual on left, group on right ;)
Plan::withEnvTenantContext()->orderByDesc('title')->get()
->map(function ($plan) use (&$plans) {
+ // Allow themes to set custom button label
+ $button = \trans('theme::app.planbutton-' . $plan->title);
+ if ($button == 'theme::app.planbutton-' . $plan->title) {
+ $button = \trans('app.planbutton', ['plan' => $plan->name]);
+ }
+
$plans[] = [
'title' => $plan->title,
'name' => $plan->name,
- 'button' => \trans('app.planbutton', ['plan' => $plan->name]),
+ 'button' => $button,
'description' => $plan->description,
+ 'mode' => $plan->mode ?: 'email',
];
});
@@ -62,49 +69,55 @@
*/
public function init(Request $request)
{
- // Check required fields
- $v = Validator::make(
- $request->all(),
- [
- 'email' => 'required',
- 'first_name' => 'max:128',
- 'last_name' => 'max:128',
- 'plan' => 'nullable|alpha_num|max:128',
- 'voucher' => 'max:32',
- ]
- );
+ $rules = [
+ 'first_name' => 'max:128',
+ 'last_name' => 'max:128',
+ 'voucher' => 'max:32',
+ ];
- $is_phone = false;
- $errors = $v->fails() ? $v->errors()->toArray() : [];
+ $plan = $this->getPlan();
- // Validate user email (or phone)
- if (empty($errors['email'])) {
- if ($error = $this->validatePhoneOrEmail($request->email, $is_phone)) {
- $errors['email'] = $error;
- }
+ if ($plan->mode == 'token') {
+ $rules['token'] = ['required', 'string', new SignupToken()];
+ } else {
+ $rules['email'] = ['required', 'string', new SignupExternalEmail()];
}
- if (!empty($errors)) {
- return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ // Check required fields, validate input
+ $v = Validator::make($request->all(), $rules);
+
+ if ($v->fails()) {
+ return response()->json(['status' => 'error', 'errors' => $v->errors()->toArray()], 422);
}
// Generate the verification code
$code = SignupCode::create([
- 'email' => $request->email,
+ 'email' => $plan->mode == 'token' ? $request->token : $request->email,
'first_name' => $request->first_name,
'last_name' => $request->last_name,
- 'plan' => $request->plan,
+ 'plan' => $plan->title,
'voucher' => $request->voucher,
]);
- // Send email/sms message
- if ($is_phone) {
- SignupVerificationSMS::dispatch($code);
+ $response = [
+ 'status' => 'success',
+ 'code' => $code->code,
+ 'mode' => $plan->mode ?: 'email',
+ ];
+
+ if ($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
SignupVerificationEmail::dispatch($code);
}
- return response()->json(['status' => 'success', 'code' => $code->code]);
+ return response()->json($response);
}
/**
@@ -251,10 +264,15 @@
$code_data = $v->getData();
$settings = [
- 'external_email' => $code_data->email,
'first_name' => $code_data->first_name,
'last_name' => $code_data->last_name,
];
+
+ if ($this->getPlan()->mode == 'token') {
+ $settings['signup_token'] = $code_data->email;
+ } else {
+ $settings['external_email'] = $code_data->email;
+ }
}
// Find the voucher discount
@@ -343,6 +361,8 @@
// Get the plan if specified and exists...
if ($request->code && $request->code->plan) {
$plan = Plan::withEnvTenantContext()->where('title', $request->code->plan)->first();
+ } elseif ($request->plan) {
+ $plan = Plan::withEnvTenantContext()->where('title', $request->plan)->first();
}
// ...otherwise use the default plan
@@ -358,40 +378,6 @@
}
/**
- * Checks if the input string is a valid email address or a phone number
- *
- * @param string $input Email address or phone number
- * @param bool $is_phone Will have been set to True if the string is valid phone number
- *
- * @return string Error message on validation error
- */
- protected static function validatePhoneOrEmail($input, &$is_phone = false): ?string
- {
- $is_phone = false;
-
- $v = Validator::make(
- ['email' => $input],
- ['email' => ['required', 'string', new SignupExternalEmail()]]
- );
-
- if ($v->fails()) {
- return $v->errors()->toArray()['email'][0];
- }
-
- // TODO: Phone number support
-/*
- $input = str_replace(array('-', ' '), '', $input);
-
- if (!preg_match('/^\+?[0-9]{9,12}$/', $input)) {
- return \trans('validation.noemailorphone');
- }
-
- $is_phone = true;
-*/
- return null;
- }
-
- /**
* Login (kolab identity) validation
*
* @param string $login Login (local part of an email address)
diff --git a/src/app/Http/Controllers/ContentController.php b/src/app/Http/Controllers/ContentController.php
--- a/src/app/Http/Controllers/ContentController.php
+++ b/src/app/Http/Controllers/ContentController.php
@@ -27,8 +27,6 @@
abort(404);
}
- self::loadLocale($theme);
-
return view($view)->with('env', \App\Utils::uiEnv());
}
@@ -63,8 +61,6 @@
// Localization
if (!empty($faq)) {
- self::loadLocale($theme_name);
-
foreach ($faq as $idx => $item) {
if (!empty($item['label'])) {
$faq[$idx]['title'] = \trans('theme::faq.' . $item['label']);
@@ -158,16 +154,4 @@
return $menu;
}
-
- /**
- * Register localization files from the theme.
- *
- * @param string $theme Theme name
- */
- protected static function loadLocale(string $theme): void
- {
- $path = resource_path(sprintf('themes/%s/lang', $theme));
-
- \app('translator')->addNamespace('theme', $path);
- }
}
diff --git a/src/app/Http/Middleware/Locale.php b/src/app/Http/Middleware/Locale.php
--- a/src/app/Http/Middleware/Locale.php
+++ b/src/app/Http/Middleware/Locale.php
@@ -64,6 +64,10 @@
app()->setLocale($lang);
}
+ // Allow skins to define/overwrite some localization
+ $theme = \config('app.theme');
+ \app('translator')->addNamespace('theme', \resource_path("themes/{$theme}/lang"));
+
return $next($request);
}
}
diff --git a/src/app/Observers/SignupCodeObserver.php b/src/app/Observers/SignupCodeObserver.php
--- a/src/app/Observers/SignupCodeObserver.php
+++ b/src/app/Observers/SignupCodeObserver.php
@@ -47,7 +47,7 @@
$code->expires_at = Carbon::now()->addHours($exp_hours);
$code->ip_address = request()->ip();
- if ($code->email) {
+ if ($code->email && strpos($code->email, '@')) {
$parts = explode('@', $code->email);
$code->local_part = $parts[0];
diff --git a/src/app/Plan.php b/src/app/Plan.php
--- a/src/app/Plan.php
+++ b/src/app/Plan.php
@@ -19,6 +19,7 @@
* @property int $discount_qty
* @property int $discount_rate
* @property string $id
+ * @property string $mode Plan signup mode (email|token)
* @property string $name
* @property \App\Package[] $packages
* @property datetime $promo_from
@@ -37,6 +38,7 @@
/** @var array<int, string> The attributes that are mass assignable */
protected $fillable = [
'title',
+ 'mode',
'name',
'description',
// a start and end datetime for this promotion
diff --git a/src/app/Rules/SignupToken.php b/src/app/Rules/SignupToken.php
new file mode 100644
--- /dev/null
+++ b/src/app/Rules/SignupToken.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace App\Rules;
+
+use Illuminate\Contracts\Validation\Rule;
+
+class SignupToken implements Rule
+{
+ protected $message;
+
+ /**
+ * Determine if the validation rule passes.
+ *
+ * @param string $attribute Attribute name
+ * @param mixed $token The value to validate
+ *
+ * @return bool
+ */
+ public function passes($attribute, $token): bool
+ {
+ // Check the max length, according to the database column length
+ if (!is_string($token) || strlen($token) > 191) {
+ $this->message = \trans('validation.signuptokeninvalid');
+ return false;
+ }
+
+ // Check the list of tokens for token existence
+ $file = storage_path('signup-tokens.txt');
+ $list = [];
+ $token = \strtoupper($token);
+
+ if (file_exists($file)) {
+ $list = file($file);
+ $list = array_map('trim', $list);
+ $list = array_map('strtoupper', $list);
+ } else {
+ \Log::error("Signup tokens file ({$file}) does not exist");
+ }
+
+ if (!in_array($token, $list)) {
+ $this->message = \trans('validation.signuptokeninvalid');
+ return false;
+ }
+
+ // Check if the token has been already used for registration (exclude deleted users)
+ $used = \App\User::select()
+ ->join('user_settings', 'users.id', '=', 'user_settings.user_id')
+ ->where('user_settings.key', 'signup_token')
+ ->where('user_settings.value', $token)
+ ->exists();
+
+ if ($used) {
+ $this->message = \trans('validation.signuptokeninvalid');
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the validation error message.
+ *
+ * @return string
+ */
+ public function message(): ?string
+ {
+ return $this->message;
+ }
+}
diff --git a/src/database/migrations/2022_09_01_100000_plans_mode.php b/src/database/migrations/2022_09_01_100000_plans_mode.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2022_09_01_100000_plans_mode.php
@@ -0,0 +1,38 @@
+<?php
+
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+return new class extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::table(
+ 'plans',
+ function (Blueprint $table) {
+ $table->string('mode', 32)->default('email');
+ }
+ );
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table(
+ 'plans',
+ function (Blueprint $table) {
+ $table->dropColumn('mode');
+ }
+ );
+ }
+};
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
@@ -381,6 +381,7 @@
'step1' => "Sign up to start your free month.",
'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 Kolab identity (you can choose additional addresses later).",
+ '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
@@ -170,6 +170,7 @@
'password-policy-min-len-error' => 'Minimum password length cannot be less than :min.',
'password-policy-max-len-error' => 'Maximum password length cannot be more than :max.',
'password-policy-last-error' => 'The minimum value for last N passwords is :last.',
+ 'signuptokeninvalid' => 'The signup token is invalid.',
/*
|--------------------------------------------------------------------------
diff --git a/src/resources/themes/app.scss b/src/resources/themes/app.scss
--- a/src/resources/themes/app.scss
+++ b/src/resources/themes/app.scss
@@ -229,8 +229,8 @@
.plan-ico {
margin:auto;
font-size: 3.8rem;
- color: #f1a539;
- border: 3px solid #f1a539;
+ color: $main-color;
+ border: 3px solid $main-color;
width: 6rem;
height: 6rem;
border-radius: 50%;
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
@@ -20,7 +20,7 @@
<div class="card d-none" id="step1" v-if="!invitation">
<div class="card-body">
- <h4 class="card-title">{{ $t('signup.title') }} - {{ $t('nav.step', { i: 1, n: 3 }) }}</h4>
+ <h4 class="card-title">{{ $t('signup.title') }} - {{ $t('nav.step', { i: 1, n: steps }) }}</h4>
<p class="card-text">
{{ $t('signup.step1') }}
</p>
@@ -31,7 +31,11 @@
<input type="text" class="form-control rounded-end" id="signup_last_name" :placeholder="$t('form.surname')" v-model="last_name">
</div>
</div>
- <div class="mb-3">
+ <div v-if="mode == 'token'" class="mb-3">
+ <label for="signup_token" class="visually-hidden">{{ $t('signup.token') }}</label>
+ <input type="text" class="form-control" id="signup_token" :placeholder="$t('signup.token')" required v-model="token">
+ </div>
+ <div v-else class="mb-3">
<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>
@@ -43,7 +47,7 @@
<div class="card d-none" id="step2" v-if="!invitation">
<div class="card-body">
- <h4 class="card-title">{{ $t('signup.title') }} - {{ $t('nav.step', { i: 2, n: 3 }) }}</h4>
+ <h4 class="card-title">{{ $t('signup.title') }} - {{ $t('nav.step', { i: 2, n: steps }) }}</h4>
<p class="card-text">
{{ $t('signup.step2') }}
</p>
@@ -61,7 +65,7 @@
<div class="card d-none" id="step3">
<div class="card-body">
- <h4 v-if="!invitation" class="card-title">{{ $t('signup.title') }} - {{ $t('nav.step', { i: 3, n: 3 }) }}</h4>
+ <h4 v-if="!invitation" class="card-title">{{ $t('signup.title') }} - {{ $t('nav.step', { i: steps, n: steps }) }}</h4>
<p class="card-text">
{{ $t('signup.step3') }}
</p>
@@ -105,7 +109,8 @@
import { library } from '@fortawesome/fontawesome-svg-core'
library.add(
- require('@fortawesome/free-solid-svg-icons/faUsers').definition,
+ require('@fortawesome/free-solid-svg-icons/faMobileRetro').definition,
+ require('@fortawesome/free-solid-svg-icons/faUsers').definition
)
export default {
@@ -125,15 +130,23 @@
domains: [],
invitation: null,
is_domain: false,
+ mode: 'email',
plan: null,
plan_icons: {
individual: 'user',
- group: 'users'
+ group: 'users',
+ phone: 'mobile-retro'
},
plans: [],
+ token: '',
voucher: ''
}
},
+ computed: {
+ steps() {
+ return this.mode == 'token' ? 2 : 3
+ }
+ },
mounted() {
let param = this.$route.params.param;
@@ -165,8 +178,7 @@
this.submitStep2(true)
} else if (/^([a-zA-Z_]+)$/.test(param)) {
// Plan title provided, save it and display Step 1
- this.plan = param
- this.displayForm(1, true)
+ this.step0(param)
} else {
this.$root.errorPage(404)
}
@@ -177,30 +189,48 @@
methods: {
selectPlan(plan) {
this.$router.push({path: '/signup/' + plan})
- this.plan = plan
- this.displayForm(1, true)
+ this.selectPlanByTitle(plan)
},
// Composes plan selection page
- step0() {
+ selectPlanByTitle(title) {
+ const plan = this.plans.filter(plan => plan.title == title)[0]
+ if (plan) {
+ this.plan = title
+ this.mode = plan.mode
+ this.displayForm(1, true)
+ }
+ },
+ step0(plan) {
if (!this.plans.length) {
axios.get('/api/auth/signup/plans', { loader: true }).then(response => {
this.plans = response.data.plans
+ this.selectPlanByTitle(plan)
})
.catch(error => {
this.$root.errorHandler(error)
})
+ } else {
+ this.selectPlanByTitle(plan)
}
},
// Submits data to the API, validates and gets verification code
submitStep1() {
this.$root.clearFormValidation($('#step1 form'))
- const post = this.$root.pick(this, ['email', 'last_name', 'first_name', 'plan', 'voucher'])
+ const post = this.$root.pick(this, ['email', 'last_name', 'first_name', 'plan', 'token', 'voucher'])
axios.post('/api/auth/signup/init', post)
.then(response => {
- this.displayForm(2, true)
this.code = response.data.code
+ this.short_code = response.data.short_code
+ this.mode = response.data.mode
+ this.is_domain = response.data.is_domain
+ 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
@@ -222,6 +252,7 @@
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) {
@@ -262,12 +293,20 @@
},
// Moves the user a step back in registration form
stepBack(e) {
- var card = $(e.target).closest('.card')
+ const card = $(e.target).closest('.card')
+ let step = card.attr('id').replace('step', '')
- card.prev().removeClass('d-none').find('input').first().focus()
card.addClass('d-none').find('form')[0].reset()
- if (card.attr('id') == 'step1') {
+ step -= 1
+
+ if (step == 2 && this.mode == 'token') {
+ step = 1
+ }
+
+ $('#step' + step).removeClass('d-none').find('input').first().focus()
+
+ if (!step) {
this.step0()
this.$router.replace({path: '/signup'})
}
@@ -281,7 +320,7 @@
return this.step0()
}
- $('#step' + step).removeClass('d-none')
+ $('#step' + step).removeClass('d-none').find('form')[0].reset()
if (focus) {
$('#step' + step).find('input').first().focus()
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
@@ -4,6 +4,7 @@
use App\Discount;
use App\Domain;
+use App\Plan;
use App\SignupCode;
use App\SignupInvitation;
use App\User;
@@ -27,6 +28,8 @@
$this->deleteTestUser('signuptestdusk@' . \config('app.domain'));
$this->deleteTestUser('admin@user-domain-signup.com');
$this->deleteTestDomain('user-domain-signup.com');
+
+ Plan::where('mode', 'token')->update(['mode' => 'email']);
}
/**
@@ -39,6 +42,10 @@
$this->deleteTestDomain('user-domain-signup.com');
SignupInvitation::truncate();
+ Plan::where('mode', 'token')->update(['mode' => 'email']);
+
+ @unlink(storage_path('signup-tokens.txt'));
+
parent::tearDown();
}
@@ -125,6 +132,7 @@
$browser->waitForLocation('/signup/individual')
->assertVisible('@step1')
+ ->assertSeeIn('.card-title', 'Sign Up - Step 1/3')
->assertMissing('@step0')
->assertMissing('@step2')
->assertMissing('@step3')
@@ -162,7 +170,8 @@
// Here we expect two text inputs and Back and Continue buttons
$browser->with('@step1', function ($step) {
- $step->assertVisible('#signup_last_name')
+ $step->waitFor('#signup_last_name')
+ ->assertSeeIn('.card-title', 'Sign Up - Step 1/3')
->assertVisible('#signup_first_name')
->assertFocused('#signup_first_name')
->assertVisible('#signup_email')
@@ -221,6 +230,7 @@
{
$this->browse(function (Browser $browser) {
$browser->assertVisible('@step2')
+ ->assertSeeIn('@step2 .card-title', 'Sign Up - Step 2/3')
->assertMissing('@step0')
->assertMissing('@step1')
->assertMissing('@step3');
@@ -509,6 +519,109 @@
}
/**
+ * Test signup with a token plan
+ */
+ public function testSignupToken(): void
+ {
+ // Test the individual plan
+ Plan::where('title', 'individual')->update(['mode' => 'token']);
+
+ // Register some valid tokens
+ $tokens = ['1234567890', 'abcdefghijk'];
+ file_put_contents(storage_path('signup-tokens.txt'), implode("\n", $tokens));
+
+ $this->browse(function (Browser $browser) use ($tokens) {
+ $browser->visit(new Signup())
+ ->waitFor('@step0 .plan-individual button')
+ ->click('@step0 .plan-individual button')
+ // Step 1
+ ->whenAvailable('@step1', function ($browser) use ($tokens) {
+ $browser->assertSeeIn('.card-title', 'Sign Up - Step 1/2')
+ ->type('#signup_first_name', 'Test')
+ ->type('#signup_last_name', 'User')
+ ->assertMissing('#signup_email')
+ ->type('#signup_token', '1234')
+ // invalid token
+ ->click('[type=submit]')
+ ->waitFor('#signup_token.is-invalid')
+ ->assertVisible('#signup_token + .invalid-feedback')
+ ->assertFocused('#signup_token')
+ ->assertToast(Toast::TYPE_ERROR, 'Form validation error')
+ // valid token
+ ->type('#signup_token', $tokens[0])
+ ->click('[type=submit]');
+ })
+ // Step 2
+ ->whenAvailable('@step3', function ($browser) {
+ $domains = Domain::getPublicDomains();
+ $domains_count = count($domains);
+
+ $browser->assertSeeIn('.card-title', 'Sign Up - Step 2/2')
+ ->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])
+ ->type('#signup_login', 'signuptestdusk')
+ ->type('#signup_password', '12345678')
+ ->type('#signup_password_confirmation', '12345678')
+ ->click('[type=submit]');
+ })
+ ->waitUntilMissing('@step3')
+ ->on(new Dashboard())
+ ->within(new Menu(), function ($browser) {
+ $browser->clickMenuItem('logout');
+ });
+ });
+
+ $user = User::where('email', 'signuptestdusk@' . \config('app.domain'))->first();
+ $this->assertSame($tokens[0], $user->getSetting('signup_token'));
+ $this->assertSame(null, $user->getSetting('external_email'));
+
+ // Test the group plan
+ Plan::where('title', 'group')->update(['mode' => 'token']);
+
+ $this->browse(function (Browser $browser) use ($tokens) {
+ $browser->visit(new Signup())
+ ->waitFor('@step0 .plan-group button')
+ ->click('@step0 .plan-group button')
+ // Step 1
+ ->whenAvailable('@step1', function ($browser) use ($tokens) {
+ $browser->assertSeeIn('.card-title', 'Sign Up - Step 1/2')
+ ->type('#signup_first_name', 'Test')
+ ->type('#signup_last_name', 'User')
+ ->assertMissing('#signup_email')
+ ->type('#signup_token', '1234')
+ // invalid token
+ ->click('[type=submit]')
+ ->waitFor('#signup_token.is-invalid')
+ ->assertVisible('#signup_token + .invalid-feedback')
+ ->assertFocused('#signup_token')
+ ->assertToast(Toast::TYPE_ERROR, 'Form validation error')
+ // valid token
+ ->type('#signup_token', $tokens[1])
+ ->click('[type=submit]');
+ })
+ // Step 2
+ ->whenAvailable('@step3', function ($browser) {
+ $browser->assertSeeIn('.card-title', 'Sign Up - Step 2/2')
+ ->type('input#signup_domain', 'user-domain-signup.com')
+ ->type('#signup_login', 'admin')
+ ->type('#signup_password', '12345678')
+ ->type('#signup_password_confirmation', '12345678')
+ ->click('[type=submit]');
+ })
+ ->waitUntilMissing('@step3')
+ ->on(new Dashboard())
+ ->within(new Menu(), function ($browser) {
+ $browser->clickMenuItem('logout');
+ });
+ });
+
+ $user = User::where('email', 'admin@user-domain-signup.com')->first();
+ $this->assertSame($tokens[1], $user->getSetting('signup_token'));
+ $this->assertSame(null, $user->getSetting('external_email'));
+ }
+
+ /**
* Test signup with voucher
*/
public function testSignupVoucherLink(): void
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
@@ -204,7 +204,7 @@
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
- $this->assertSame("The specified email address is invalid.", $json['errors']['email']);
+ $this->assertSame(["The specified email address is invalid."], $json['errors']['email']);
SignupCode::truncate();
@@ -231,7 +231,7 @@
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
// TODO: This probably should be a different message?
- $this->assertSame("The specified email address is invalid.", $json['errors']['email']);
+ $this->assertSame(["The specified email address is invalid."], $json['errors']['email']);
// IP address limit check
$data = [
@@ -258,7 +258,7 @@
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
// TODO: This probably should be a different message?
- $this->assertSame("The specified email address is invalid.", $json['errors']['email']);
+ $this->assertSame(["The specified email address is invalid."], $json['errors']['email']);
// TODO: Test phone validation
}
@@ -284,8 +284,10 @@
$json = $response->json();
$response->assertStatus(200);
- $this->assertCount(2, $json);
+
+ $this->assertCount(3, $json);
$this->assertSame('success', $json['status']);
+ $this->assertSame('email', $json['mode']);
$this->assertNotEmpty($json['code']);
// Assert the email sending job was pushed once
@@ -309,8 +311,9 @@
$json = $response->json();
$response->assertStatus(200);
- $this->assertCount(2, $json);
+ $this->assertCount(3, $json);
$this->assertSame('success', $json['status']);
+ $this->assertSame('email', $json['mode']);
$this->assertNotEmpty($json['code']);
// Assert the job has proper data assigned
@@ -629,8 +632,9 @@
$json = $response->json();
$response->assertStatus(200);
- $this->assertCount(2, $json);
+ $this->assertCount(3, $json);
$this->assertSame('success', $json['status']);
+ $this->assertSame('email', $json['mode']);
$this->assertNotEmpty($json['code']);
// Assert the email sending job was pushed once
diff --git a/src/tests/Feature/PlanTest.php b/src/tests/Feature/PlanTest.php
--- a/src/tests/Feature/PlanTest.php
+++ b/src/tests/Feature/PlanTest.php
@@ -107,6 +107,9 @@
$this->assertTrue($plan->cost() == $package_costs);
}
+ /**
+ * Tests for Plan::tenant()
+ */
public function testTenant(): void
{
$plan = Plan::withEnvTenantContext()->where('title', 'individual')->first();
diff --git a/src/tests/Unit/Rules/SignupTokenTest.php b/src/tests/Unit/Rules/SignupTokenTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Unit/Rules/SignupTokenTest.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Tests\Unit\Rules;
+
+use App\Rules\SignupToken;
+use Illuminate\Support\Facades\Validator;
+use Tests\TestCase;
+
+class SignupTokenTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ @unlink(storage_path('signup-tokens.txt'));
+
+ $john = $this->getTestUser('john@kolab.org');
+ $john->settings()->where('key', 'signup_token')->delete();
+
+ parent::tearDown();
+ }
+
+ /**
+ * Tests the resource name validator
+ */
+ public function testValidation(): void
+ {
+ $tokens = ['1234567890', 'abcdefghijk'];
+ file_put_contents(storage_path('signup-tokens.txt'), implode("\n", $tokens));
+
+ $rules = ['token' => [new SignupToken()]];
+
+ // Empty input
+ $v = Validator::make(['token' => null], $rules);
+ $this->assertSame(['token' => ["The signup token is invalid."]], $v->errors()->toArray());
+
+ // Length limit
+ $v = Validator::make(['token' => str_repeat('a', 192)], $rules);
+ $this->assertSame(['token' => ["The signup token is invalid."]], $v->errors()->toArray());
+
+ // Non-existing token
+ $v = Validator::make(['token' => '123'], $rules);
+ $this->assertSame(['token' => ["The signup token is invalid."]], $v->errors()->toArray());
+
+ // Valid tokens
+ $v = Validator::make(['token' => $tokens[0]], $rules);
+ $this->assertSame([], $v->errors()->toArray());
+
+ $v = Validator::make(['token' => strtoupper($tokens[1])], $rules);
+ $this->assertSame([], $v->errors()->toArray());
+
+ // Tokens already used
+ $john = $this->getTestUser('john@kolab.org');
+ $john->setSetting('signup_token', $tokens[0]);
+
+ $v = Validator::make(['token' => $tokens[0]], $rules);
+ $this->assertSame(['token' => ["The signup token is invalid."]], $v->errors()->toArray());
+ }
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Apr 4, 1:14 AM (1 d, 10 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18823604
Default Alt Text
D3815.1775265280.diff (33 KB)
Attached To
Mode
D3815: Signup by token
Attached
Detach File
Event Timeline