Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2528239
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
62 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/.env.example b/src/.env.example
index cadae2fd..c8b9aced 100644
--- a/src/.env.example
+++ b/src/.env.example
@@ -1,167 +1,170 @@
APP_NAME=Kolab
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://127.0.0.1:8000
#APP_PASSPHRASE=
APP_PUBLIC_URL=
APP_DOMAIN=kolabnow.com
APP_WEBSITE_DOMAIN=kolabnow.com
APP_THEME=default
APP_TENANT_ID=5
APP_LOCALE=en
APP_LOCALES=
APP_WITH_ADMIN=1
APP_WITH_RESELLER=1
APP_WITH_SERVICES=1
+SIGNUP_LIMIT_EMAIL=0
+SIGNUP_LIMIT_IP=0
+
ASSET_URL=http://127.0.0.1:8000
WEBMAIL_URL=/apps
SUPPORT_URL=/support
SUPPORT_EMAIL=
LOG_CHANNEL=stack
LOG_SLOW_REQUESTS=5
DB_CONNECTION=mysql
DB_DATABASE=kolabdev
DB_HOST=127.0.0.1
DB_PASSWORD=kolab
DB_PORT=3306
DB_USERNAME=kolabdev
BROADCAST_DRIVER=redis
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=file
SESSION_LIFETIME=120
OPENEXCHANGERATES_API_KEY="from openexchangerates.org"
MFA_DSN=mysql://roundcube:Welcome2KolabSystems@127.0.0.1/roundcube
MFA_TOTP_DIGITS=6
MFA_TOTP_INTERVAL=30
MFA_TOTP_DIGEST=sha1
IMAP_URI=ssl://127.0.0.1:11993
IMAP_ADMIN_LOGIN=cyrus-admin
IMAP_ADMIN_PASSWORD=Welcome2KolabSystems
IMAP_VERIFY_HOST=false
IMAP_VERIFY_PEER=false
LDAP_BASE_DN="dc=mgmt,dc=com"
LDAP_DOMAIN_BASE_DN="ou=Domains,dc=mgmt,dc=com"
LDAP_HOSTS=127.0.0.1
LDAP_PORT=389
LDAP_SERVICE_BIND_DN="uid=kolab-service,ou=Special Users,dc=mgmt,dc=com"
LDAP_SERVICE_BIND_PW="Welcome2KolabSystems"
LDAP_USE_SSL=false
LDAP_USE_TLS=false
# Administrative
LDAP_ADMIN_BIND_DN="cn=Directory Manager"
LDAP_ADMIN_BIND_PW="Welcome2KolabSystems"
LDAP_ADMIN_ROOT_DN="dc=mgmt,dc=com"
# Hosted (public registration)
LDAP_HOSTED_BIND_DN="uid=hosted-kolab-service,ou=Special Users,dc=mgmt,dc=com"
LDAP_HOSTED_BIND_PW="Welcome2KolabSystems"
LDAP_HOSTED_ROOT_DN="dc=hosted,dc=com"
OPENVIDU_API_PASSWORD=MY_SECRET
OPENVIDU_API_URL=http://localhost:8080/api/
OPENVIDU_API_USERNAME=OPENVIDUAPP
OPENVIDU_API_VERIFY_TLS=true
OPENVIDU_COTURN_IP=127.0.0.1
OPENVIDU_COTURN_REDIS_DATABASE=2
OPENVIDU_COTURN_REDIS_IP=127.0.0.1
OPENVIDU_COTURN_REDIS_PASSWORD=turn
# Used as COTURN_IP, TURN_PUBLIC_IP, for KMS_TURN_URL
OPENVIDU_PUBLIC_IP=127.0.0.1
OPENVIDU_PUBLIC_PORT=3478
OPENVIDU_SERVER_PORT=8080
OPENVIDU_WEBHOOK=true
OPENVIDU_WEBHOOK_ENDPOINT=http://127.0.0.1:8000/webhooks/meet/openvidu
# "CDR" events, see https://docs.openvidu.io/en/2.13.0/reference-docs/openvidu-server-cdr/
#OPENVIDU_WEBHOOK_EVENTS=[sessionCreated,sessionDestroyed,participantJoined,participantLeft,webrtcConnectionCreated,webrtcConnectionDestroyed,recordingStatusChanged,filterEventDispatched,mediaNodeStatusChanged]
#OPENVIDU_WEBHOOK_HEADERS=[\"Authorization:\ Basic\ SOMETHING\"]
PGP_ENABLED=
PGP_BINARY=
PGP_AGENT=
PGP_GPGCONF=
PGP_LENGTH=
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
SWOOLE_HOT_RELOAD_ENABLE=true
SWOOLE_HTTP_ACCESS_LOG=true
SWOOLE_HTTP_HOST=127.0.0.1
SWOOLE_HTTP_PORT=8000
SWOOLE_HTTP_REACTOR_NUM=1
SWOOLE_HTTP_WEBSOCKET=true
SWOOLE_HTTP_WORKER_NUM=1
SWOOLE_OB_OUTPUT=true
PAYMENT_PROVIDER=
MOLLIE_KEY=
STRIPE_KEY=
STRIPE_PUBLIC_KEY=
STRIPE_WEBHOOK_SECRET=
MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="noreply@example.com"
MAIL_FROM_NAME="Example.com"
MAIL_REPLYTO_ADDRESS="replyto@example.com"
MAIL_REPLYTO_NAME=null
DNS_TTL=3600
DNS_SPF="v=spf1 mx -all"
DNS_STATIC="%s. MX 10 ext-mx01.mykolab.com."
DNS_COPY_FROM=null
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
MIX_ASSET_PATH='/'
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
# Generate with ./artisan passport:client --password
#PASSPORT_PROXY_OAUTH_CLIENT_ID=
#PASSPORT_PROXY_OAUTH_CLIENT_SECRET=
PASSPORT_PRIVATE_KEY=
PASSPORT_PUBLIC_KEY=
COMPANY_NAME=
COMPANY_ADDRESS=
COMPANY_DETAILS=
COMPANY_EMAIL=
COMPANY_LOGO=
COMPANY_FOOTER=
VAT_COUNTRIES=CH,LI
VAT_RATE=7.7
KB_ACCOUNT_DELETE=
KB_ACCOUNT_SUSPENDED=
diff --git a/src/app/Http/Controllers/API/SignupController.php b/src/app/Http/Controllers/API/SignupController.php
index 0983f03c..8a84238f 100644
--- a/src/app/Http/Controllers/API/SignupController.php
+++ b/src/app/Http/Controllers/API/SignupController.php
@@ -1,448 +1,448 @@
<?php
namespace App\Http\Controllers\API;
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\ExternalEmail;
+use App\Rules\SignupExternalEmail;
use App\Rules\UserEmailDomain;
use App\Rules\UserEmailLocal;
use App\SignupCode;
use App\SignupInvitation;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
/**
* Signup process API
*/
class SignupController extends Controller
{
/** @var ?\App\SignupCode A verification code object */
protected $code;
/** @var ?\App\Plan Signup plan object */
protected $plan;
/**
* Returns plans definitions for signup.
*
* @param \Illuminate\Http\Request $request HTTP request
*
* @return \Illuminate\Http\JsonResponse JSON response
*/
public function plans(Request $request)
{
$plans = [];
// Use reverse order just to have individual on left, group on right ;)
Plan::withEnvTenantContext()->orderByDesc('title')->get()
->map(function ($plan) use (&$plans) {
$plans[] = [
'title' => $plan->title,
'name' => $plan->name,
'button' => \trans('app.planbutton', ['plan' => $plan->name]),
'description' => $plan->description,
];
});
return response()->json(['status' => 'success', 'plans' => $plans]);
}
/**
* Starts signup process.
*
* Verifies user name and email/phone, sends verification email/sms message.
* Returns the verification code.
*
* @param \Illuminate\Http\Request $request HTTP request
*
* @return \Illuminate\Http\JsonResponse JSON response
*/
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',
]
);
$is_phone = false;
$errors = $v->fails() ? $v->errors()->toArray() : [];
// Validate user email (or phone)
if (empty($errors['email'])) {
if ($error = $this->validatePhoneOrEmail($request->email, $is_phone)) {
$errors['email'] = $error;
}
}
if (!empty($errors)) {
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
// Generate the verification code
$code = SignupCode::create([
'email' => $request->email,
'first_name' => $request->first_name,
'last_name' => $request->last_name,
'plan' => $request->plan,
'voucher' => $request->voucher,
]);
// Send email/sms message
if ($is_phone) {
SignupVerificationSMS::dispatch($code);
} else {
SignupVerificationEmail::dispatch($code);
}
return response()->json(['status' => 'success', 'code' => $code->code]);
}
/**
* Returns signup invitation information.
*
* @param string $id Signup invitation identifier
*
* @return \Illuminate\Http\JsonResponse|void
*/
public function invitation($id)
{
$invitation = SignupInvitation::withEnvTenantContext()->find($id);
if (empty($invitation) || $invitation->isCompleted()) {
return $this->errorResponse(404);
}
$has_domain = $this->getPlan()->hasDomain();
$result = [
'id' => $id,
'is_domain' => $has_domain,
'domains' => $has_domain ? [] : Domain::getPublicDomains(),
];
return response()->json($result);
}
/**
* Validation of the verification code.
*
* @param \Illuminate\Http\Request $request HTTP request
*
* @return \Illuminate\Http\JsonResponse JSON response
*/
public function verify(Request $request)
{
// Validate the request args
$v = Validator::make(
$request->all(),
[
'code' => 'required',
'short_code' => 'required',
]
);
if ($v->fails()) {
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
}
// Validate the verification code
$code = SignupCode::find($request->code);
if (
empty($code)
|| $code->isExpired()
|| Str::upper($request->short_code) !== Str::upper($code->short_code)
) {
$errors = ['short_code' => "The code is invalid or expired."];
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 (::destroy())
$this->code = $code;
$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([
'status' => 'success',
'email' => $code->email,
'first_name' => $code->first_name,
'last_name' => $code->last_name,
'voucher' => $code->voucher,
'is_domain' => $has_domain,
'domains' => $has_domain ? [] : Domain::getPublicDomains(),
]);
}
/**
* Finishes the signup process by creating the user account.
*
* @param \Illuminate\Http\Request $request HTTP request
*
* @return \Illuminate\Http\JsonResponse JSON response
*/
public function signup(Request $request)
{
// Validate input
$v = Validator::make(
$request->all(),
[
'login' => 'required|min:2',
'password' => 'required|min:4|confirmed',
'domain' => 'required',
'voucher' => 'max:32',
]
);
if ($v->fails()) {
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
}
// Signup via invitation
if ($request->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',
'voucher' => 'max:32',
]
);
$errors = $v->fails() ? $v->errors()->toArray() : [];
if (!empty($errors)) {
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
$settings = [
'external_email' => $invitation->email,
'first_name' => $request->first_name,
'last_name' => $request->last_name,
];
} else {
// Validate verification codes (again)
$v = $this->verify($request);
if ($v->status() !== 200) {
return $v;
}
// Get user name/email from the verification code database
$code_data = $v->getData();
$settings = [
'external_email' => $code_data->email,
'first_name' => $code_data->first_name,
'last_name' => $code_data->last_name,
];
}
// Find the voucher discount
if ($request->voucher) {
$discount = Discount::where('code', \strtoupper($request->voucher))
->where('active', true)->first();
if (!$discount) {
$errors = ['voucher' => \trans('validation.voucherinvalid')];
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
}
// Get the plan
$plan = $this->getPlan();
$is_domain = $plan->hasDomain();
$login = $request->login;
$domain_name = $request->domain;
// Validate login
if ($errors = self::validateLogin($login, $domain_name, $is_domain)) {
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
// We allow only ASCII, so we can safely lower-case the email address
$login = Str::lower($login);
$domain_name = Str::lower($domain_name);
$domain = null;
DB::beginTransaction();
// Create domain record
if ($is_domain) {
$domain = Domain::create([
'namespace' => $domain_name,
'status' => Domain::STATUS_NEW,
'type' => Domain::TYPE_EXTERNAL,
]);
}
// Create user record
$user = User::create([
'email' => $login . '@' . $domain_name,
'password' => $request->password,
]);
if (!empty($discount)) {
$wallet = $user->wallets()->first();
$wallet->discount()->associate($discount);
$wallet->save();
}
$user->assignPlan($plan, $domain);
// Save the external email and plan in user settings
$user->setSettings($settings);
// Update the invitation
if (!empty($invitation)) {
$invitation->status = SignupInvitation::STATUS_COMPLETED;
$invitation->user_id = $user->id;
$invitation->save();
}
// Remove the verification code
if ($this->code) {
$this->code->delete();
}
DB::commit();
return AuthController::logonResponse($user, $request->password);
}
/**
* Returns plan for the signup process
*
* @returns \App\Plan Plan object selected for current signup process
*/
protected function getPlan()
{
if (!$this->plan) {
// Get the plan if specified and exists...
if ($this->code && $this->code->plan) {
$plan = Plan::withEnvTenantContext()->where('title', $this->code->plan)->first();
}
// ...otherwise use the default plan
if (empty($plan)) {
// TODO: Get default plan title from config
$plan = Plan::withEnvTenantContext()->where('title', 'individual')->first();
}
$this->plan = $plan;
}
return $this->plan;
}
/**
* 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 ExternalEmail()]]
+ ['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)
* @param string $domain Domain name
* @param bool $external Enables additional checks for domain part
*
* @return array Error messages on validation error
*/
protected static function validateLogin($login, $domain, $external = false): ?array
{
// Validate login part alone
$v = Validator::make(
['login' => $login],
['login' => ['required', 'string', new UserEmailLocal($external)]]
);
if ($v->fails()) {
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 = Str::lower($domain);
// Check if domain is already registered with us
if ($external) {
if (Domain::where('namespace', $domain)->first()) {
return ['domain' => \trans('validation.domainexists')];
}
}
// Check if user with specified login already exists
$email = $login . '@' . $domain;
if (User::emailExists($email) || User::aliasExists($email) || \App\Group::emailExists($email)) {
return ['login' => \trans('validation.loginexists')];
}
return null;
}
}
diff --git a/src/app/Rules/ExternalEmail.php b/src/app/Rules/ExternalEmail.php
index de112d2a..b0ee7a24 100644
--- a/src/app/Rules/ExternalEmail.php
+++ b/src/app/Rules/ExternalEmail.php
@@ -1,52 +1,52 @@
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
use Illuminate\Support\Facades\Validator;
class ExternalEmail implements Rule
{
- private $message;
+ protected $message;
/**
* Determine if the validation rule passes.
*
* Email address validation with some more strict rules
* than the default Laravel's 'email' rule
*
* @param string $attribute Attribute name
* @param mixed $email Email address input
*
* @return bool
*/
public function passes($attribute, $email): bool
{
$v = Validator::make(['email' => $email], ['email' => 'required|email']);
if ($v->fails()) {
$this->message = \trans('validation.emailinvalid');
return false;
}
list($local, $domain) = explode('@', $email);
// don't allow @localhost and other no-fqdn
if (strpos($domain, '.') === false) {
$this->message = \trans('validation.emailinvalid');
return false;
}
return true;
}
/**
* Get the validation error message.
*
* @return string
*/
public function message(): ?string
{
return $this->message;
}
}
diff --git a/src/app/Rules/SignupExternalEmail.php b/src/app/Rules/SignupExternalEmail.php
new file mode 100644
index 00000000..957dec6c
--- /dev/null
+++ b/src/app/Rules/SignupExternalEmail.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace App\Rules;
+
+use App\SignupCode;
+
+class SignupExternalEmail extends ExternalEmail
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function passes($attribute, $email): bool
+ {
+ if (!parent::passes($attribute, $email)) {
+ return false;
+ }
+
+ // Check the max length, according to the database column length
+ if (strlen($email) > 191) {
+ $this->message = \trans('validation.emailinvalid');
+ return false;
+ }
+
+ // Don't allow multiple open registrations against the same email address
+ if (($limit = \config('app.signup.email_limit')) > 0) {
+ $signups = SignupCode::where('email', $email)
+ ->whereDate('expires_at', '>', \Carbon\Carbon::now());
+
+ if ($signups->count() >= $limit) {
+ // @kanarip: this is deliberately an "email invalid" message
+ $this->message = \trans('validation.emailinvalid');
+ return false;
+ }
+ }
+
+ // Don't allow multiple open registrations against the same source ip address
+ if (($limit = \config('app.signup.ip_limit')) > 0) {
+ $signups = SignupCode::where('ip_address', request()->ip())
+ ->whereDate('expires_at', '>', \Carbon\Carbon::now());
+
+ if ($signups->count() >= $limit) {
+ // @kanarip: this is deliberately an "email invalid" message
+ $this->message = \trans('validation.emailinvalid');
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/config/app.php b/src/config/app.php
index 41755949..ed3c5ab6 100644
--- a/src/config/app.php
+++ b/src/config/app.php
@@ -1,291 +1,296 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Application Name
|--------------------------------------------------------------------------
|
| This value is the name of your application. This value is used when the
| framework needs to place the application's name in a notification or
| any other location as required by the application or its packages.
|
*/
'name' => env('APP_NAME', 'Laravel'),
/*
|--------------------------------------------------------------------------
| Application Environment
|--------------------------------------------------------------------------
|
| This value determines the "environment" your application is currently
| running in. This may determine how you prefer to configure various
| services the application utilizes. Set this in your ".env" file.
|
*/
'env' => env('APP_ENV', 'production'),
/*
|--------------------------------------------------------------------------
| Application Debug Mode
|--------------------------------------------------------------------------
|
| When your application is in debug mode, detailed error messages with
| stack traces will be shown on every error that occurs within your
| application. If disabled, a simple generic error page is shown.
|
*/
'debug' => env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
|
| This URL is used by the console to properly generate URLs when using
| the Artisan command line tool. You should set this to the root of
| your application so that it is used when running Artisan tasks.
*/
'url' => env('APP_URL', 'http://localhost'),
'passphrase' => env('APP_PASSPHRASE', null),
'public_url' => env('APP_PUBLIC_URL', env('APP_URL', 'http://localhost')),
'asset_url' => env('ASSET_URL', null),
'support_url' => env('SUPPORT_URL', null),
'support_email' => env('SUPPORT_EMAIL', null),
'webmail_url' => env('WEBMAIL_URL', null),
'theme' => env('APP_THEME', 'default'),
'tenant_id' => env('APP_TENANT_ID', null),
'currency' => \strtoupper(env('APP_CURRENCY', 'CHF')),
/*
|--------------------------------------------------------------------------
| Application Domain
|--------------------------------------------------------------------------
|
| System domain used for user signup (kolab identity)
*/
'domain' => env('APP_DOMAIN', 'domain.tld'),
'website_domain' => env('APP_WEBSITE_DOMAIN', env('APP_DOMAIN', 'domain.tld')),
/*
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. We have gone
| ahead and set this to a sensible default for you out of the box.
|
*/
'timezone' => 'UTC',
/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by the translation service provider. You are free to set this value
| to any of the locales which will be supported by the application.
|
*/
'locale' => env('APP_LOCALE', 'en'),
/*
|--------------------------------------------------------------------------
| Application Fallback Locale
|--------------------------------------------------------------------------
|
| The fallback locale determines the locale to use when the current one
| is not available. You may change the value to correspond to any of
| the language folders that are provided through your application.
|
*/
'fallback_locale' => 'en',
/*
|--------------------------------------------------------------------------
| Faker Locale
|--------------------------------------------------------------------------
|
| This locale will be used by the Faker PHP library when generating fake
| data for your database seeds. For example, this will be used to get
| localized telephone numbers, street address information and more.
|
*/
'faker_locale' => 'en_US',
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is used by the Illuminate encrypter service and should be set
| to a random, 32 character string, otherwise these encrypted strings
| will not be safe. Please do this before deploying an application!
|
*/
'key' => env('APP_KEY'),
'cipher' => 'AES-256-CBC',
/*
|--------------------------------------------------------------------------
| Autoloaded Service Providers
|--------------------------------------------------------------------------
|
| The service providers listed here will be automatically loaded on the
| request to your application. Feel free to add your own services to
| this array to grant expanded functionality to your applications.
|
*/
'providers' => [
/*
* Laravel Framework Service Providers...
*/
Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Broadcasting\BroadcastServiceProvider::class,
Illuminate\Bus\BusServiceProvider::class,
Illuminate\Cache\CacheServiceProvider::class,
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
Illuminate\Cookie\CookieServiceProvider::class,
Illuminate\Database\DatabaseServiceProvider::class,
Illuminate\Encryption\EncryptionServiceProvider::class,
Illuminate\Filesystem\FilesystemServiceProvider::class,
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
Illuminate\Hashing\HashServiceProvider::class,
Illuminate\Mail\MailServiceProvider::class,
Illuminate\Notifications\NotificationServiceProvider::class,
Illuminate\Pagination\PaginationServiceProvider::class,
Illuminate\Pipeline\PipelineServiceProvider::class,
Illuminate\Queue\QueueServiceProvider::class,
Illuminate\Redis\RedisServiceProvider::class,
Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
Illuminate\Session\SessionServiceProvider::class,
Illuminate\Translation\TranslationServiceProvider::class,
Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class,
/*
* Package Service Providers...
*/
Barryvdh\DomPDF\ServiceProvider::class,
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\HorizonServiceProvider::class,
App\Providers\PassportServiceProvider::class,
App\Providers\RouteServiceProvider::class,
],
/*
|--------------------------------------------------------------------------
| Class Aliases
|--------------------------------------------------------------------------
|
| This array of class aliases will be registered when this application
| is started. However, feel free to register as many as you wish as
| the aliases are "lazy" loaded so they don't hinder performance.
|
*/
'aliases' => [
'App' => Illuminate\Support\Facades\App::class,
'Arr' => Illuminate\Support\Arr::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
'Auth' => Illuminate\Support\Facades\Auth::class,
'Blade' => Illuminate\Support\Facades\Blade::class,
'Broadcast' => Illuminate\Support\Facades\Broadcast::class,
'Bus' => Illuminate\Support\Facades\Bus::class,
'Cache' => Illuminate\Support\Facades\Cache::class,
'Config' => Illuminate\Support\Facades\Config::class,
'Cookie' => Illuminate\Support\Facades\Cookie::class,
'Crypt' => Illuminate\Support\Facades\Crypt::class,
'DB' => Illuminate\Support\Facades\DB::class,
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
'Event' => Illuminate\Support\Facades\Event::class,
'File' => Illuminate\Support\Facades\File::class,
'Gate' => Illuminate\Support\Facades\Gate::class,
'Hash' => Illuminate\Support\Facades\Hash::class,
'Lang' => Illuminate\Support\Facades\Lang::class,
'Log' => Illuminate\Support\Facades\Log::class,
'Mail' => Illuminate\Support\Facades\Mail::class,
'Notification' => Illuminate\Support\Facades\Notification::class,
'Password' => Illuminate\Support\Facades\Password::class,
'PDF' => Barryvdh\DomPDF\Facade::class,
'Queue' => Illuminate\Support\Facades\Queue::class,
'Redirect' => Illuminate\Support\Facades\Redirect::class,
'Redis' => Illuminate\Support\Facades\Redis::class,
'Request' => Illuminate\Support\Facades\Request::class,
'Response' => Illuminate\Support\Facades\Response::class,
'Route' => Illuminate\Support\Facades\Route::class,
'Schema' => Illuminate\Support\Facades\Schema::class,
'Session' => Illuminate\Support\Facades\Session::class,
'Storage' => Illuminate\Support\Facades\Storage::class,
'Str' => Illuminate\Support\Str::class,
'URL' => Illuminate\Support\Facades\URL::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
],
// Locations of knowledge base articles
'kb' => [
// An article about suspended accounts
'account_suspended' => env('KB_ACCOUNT_SUSPENDED'),
// An article about a way to delete an owned account
'account_delete' => env('KB_ACCOUNT_DELETE'),
],
'company' => [
'name' => env('COMPANY_NAME'),
'address' => env('COMPANY_ADDRESS'),
'details' => env('COMPANY_DETAILS'),
'email' => env('COMPANY_EMAIL'),
'logo' => env('COMPANY_LOGO'),
'footer' => env('COMPANY_FOOTER', env('COMPANY_DETAILS')),
],
'storage' => [
'min_qty' => (int) env('STORAGE_MIN_QTY', 5), // in GB
],
'vat' => [
'countries' => env('VAT_COUNTRIES'),
'rate' => (float) env('VAT_RATE'),
],
'payment' => [
'methods_oneoff' => env('PAYMENT_METHODS_ONEOFF', "creditcard,paypal,banktransfer"),
'methods_recurring' => env('PAYMENT_METHODS_RECURRING', "creditcard"),
],
'with_admin' => (bool) env('APP_WITH_ADMIN', false),
'with_reseller' => (bool) env('APP_WITH_RESELLER', false),
'with_services' => (bool) env('APP_WITH_SERVICES', false),
+
+ 'signup' => [
+ 'email_limit' => (int) env('SIGNUP_LIMIT_EMAIL', 0),
+ 'ip_limit' => (int) env('SIGNUP_LIMIT_IP', 0),
+ ],
];
diff --git a/src/tests/Feature/Controller/SignupTest.php b/src/tests/Feature/Controller/SignupTest.php
index b67ceb35..306070f9 100644
--- a/src/tests/Feature/Controller/SignupTest.php
+++ b/src/tests/Feature/Controller/SignupTest.php
@@ -1,782 +1,852 @@
<?php
namespace Tests\Feature\Controller;
use App\Http\Controllers\API\SignupController;
use App\Discount;
use App\Domain;
use App\SignupCode;
use App\SignupInvitation as SI;
use App\User;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class SignupTest extends TestCase
{
private $domain;
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
// TODO: Some tests depend on existence of individual and group plans,
// we should probably create plans here to not depend on that
$this->domain = $this->getPublicDomain();
$this->deleteTestUser("SignupControllerTest1@$this->domain");
$this->deleteTestUser("signuplogin@$this->domain");
$this->deleteTestUser("admin@external.com");
$this->deleteTestUser("test-inv@kolabnow.com");
$this->deleteTestDomain('external.com');
$this->deleteTestDomain('signup-domain.com');
$this->deleteTestGroup('group-test@kolabnow.com');
SI::truncate();
}
/**
* {@inheritDoc}
*/
public function tearDown(): void
{
$this->deleteTestUser("SignupControllerTest1@$this->domain");
$this->deleteTestUser("signuplogin@$this->domain");
$this->deleteTestUser("admin@external.com");
$this->deleteTestUser("test-inv@kolabnow.com");
$this->deleteTestDomain('external.com');
$this->deleteTestDomain('signup-domain.com');
$this->deleteTestGroup('group-test@kolabnow.com');
SI::truncate();
parent::tearDown();
}
/**
* Return a public domain for signup tests
*/
private function getPublicDomain(): string
{
if (!$this->domain) {
$this->refreshApplication();
$public_domains = Domain::getPublicDomains();
$this->domain = reset($public_domains);
if (empty($this->domain)) {
$this->domain = 'signup-domain.com';
Domain::create([
'namespace' => $this->domain,
'status' => Domain::STATUS_ACTIVE,
'type' => Domain::TYPE_PUBLIC,
]);
}
}
return $this->domain;
}
/**
* Test fetching plans for signup
*/
public function testSignupPlans(): void
{
$response = $this->get('/api/auth/signup/plans');
$json = $response->json();
$response->assertStatus(200);
$this->assertSame('success', $json['status']);
$this->assertCount(2, $json['plans']);
$this->assertArrayHasKey('title', $json['plans'][0]);
$this->assertArrayHasKey('name', $json['plans'][0]);
$this->assertArrayHasKey('description', $json['plans'][0]);
$this->assertArrayHasKey('button', $json['plans'][0]);
}
/**
* Test fetching invitation
*/
public function testSignupInvitations(): void
{
Queue::fake();
$invitation = SI::create(['email' => 'email1@ext.com']);
// Test existing invitation
$response = $this->get("/api/auth/signup/invitations/{$invitation->id}");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame($invitation->id, $json['id']);
// Test non-existing invitation
$response = $this->get("/api/auth/signup/invitations/abc");
$response->assertStatus(404);
// Test completed invitation
SI::where('id', $invitation->id)->update(['status' => SI::STATUS_COMPLETED]);
$response = $this->get("/api/auth/signup/invitations/{$invitation->id}");
$response->assertStatus(404);
}
/**
* Test signup initialization with invalid input
*/
public function testSignupInitInvalidInput(): void
{
// Empty input data
$data = [];
$response = $this->post('/api/auth/signup/init', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertArrayHasKey('email', $json['errors']);
// Data with missing name
$data = [
'email' => 'UsersApiControllerTest1@UsersApiControllerTest.com',
'first_name' => str_repeat('a', 250),
'last_name' => str_repeat('a', 250),
];
$response = $this->post('/api/auth/signup/init', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json['errors']);
$this->assertArrayHasKey('first_name', $json['errors']);
$this->assertArrayHasKey('last_name', $json['errors']);
// Data with invalid email (but not phone number)
$data = [
'email' => '@example.org',
'first_name' => 'Signup',
'last_name' => 'User',
];
$response = $this->post('/api/auth/signup/init', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertArrayHasKey('email', $json['errors']);
// Sanity check on voucher code, last/first name is optional
$data = [
'voucher' => '123456789012345678901234567890123',
'email' => 'valid@email.com',
];
$response = $this->post('/api/auth/signup/init', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertArrayHasKey('voucher', $json['errors']);
+ // Email address too long
+ $data = [
+ 'email' => str_repeat('a', 190) . '@example.org',
+ 'first_name' => 'Signup',
+ 'last_name' => 'User',
+ ];
+
+ $response = $this->post('/api/auth/signup/init', $data);
+ $json = $response->json();
+
+ $response->assertStatus(422);
+
+ $this->assertSame('error', $json['status']);
+ $this->assertCount(1, $json['errors']);
+ $this->assertSame("The specified email address is invalid.", $json['errors']['email']);
+
+ SignupCode::truncate();
+
+ // Email address limit check
+ $data = [
+ 'email' => 'test@example.org',
+ 'first_name' => 'Signup',
+ 'last_name' => 'User',
+ ];
+
+ \config(['app.signup.email_limit' => 0]);
+
+ $response = $this->post('/api/auth/signup/init', $data);
+ $json = $response->json();
+
+ $response->assertStatus(200);
+
+ \config(['app.signup.email_limit' => 1]);
+
+ $response = $this->post('/api/auth/signup/init', $data);
+ $json = $response->json();
+
+ $response->assertStatus(422);
+ $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']);
+
+ // IP address limit check
+ $data = [
+ 'email' => 'ip@example.org',
+ 'first_name' => 'Signup',
+ 'last_name' => 'User',
+ ];
+
+ \config(['app.signup.email_limit' => 0]);
+ \config(['app.signup.ip_limit' => 0]);
+
+ $response = $this->post('/api/auth/signup/init', $data, ['REMOTE_ADDR' => '10.1.1.1']);
+ $json = $response->json();
+
+ $response->assertStatus(200);
+
+ \config(['app.signup.ip_limit' => 1]);
+
+ $response = $this->post('/api/auth/signup/init', $data, ['REMOTE_ADDR' => '10.1.1.1']);
+ $json = $response->json();
+
+ $response->assertStatus(422);
+
+ $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']);
+
// TODO: Test phone validation
}
/**
* Test signup initialization with valid input
*/
public function testSignupInitValidInput(): array
{
Queue::fake();
// Assert that no jobs were pushed...
Queue::assertNothingPushed();
$data = [
'email' => 'testuser@external.com',
'first_name' => 'Signup',
'last_name' => 'User',
'plan' => 'individual',
];
$response = $this->post('/api/auth/signup/init', $data);
$json = $response->json();
$response->assertStatus(200);
$this->assertCount(2, $json);
$this->assertSame('success', $json['status']);
$this->assertNotEmpty($json['code']);
// Assert the email sending job was pushed once
Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, 1);
// Assert the job has proper data assigned
Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, function ($job) use ($data, $json) {
$code = TestCase::getObjectProperty($job, 'code');
return $code->code === $json['code']
&& $code->plan === $data['plan']
&& $code->email === $data['email']
&& $code->first_name === $data['first_name']
&& $code->last_name === $data['last_name'];
});
// Try the same with voucher
$data['voucher'] = 'TEST';
$response = $this->post('/api/auth/signup/init', $data);
$json = $response->json();
$response->assertStatus(200);
$this->assertCount(2, $json);
$this->assertSame('success', $json['status']);
$this->assertNotEmpty($json['code']);
// Assert the job has proper data assigned
Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, function ($job) use ($data, $json) {
$code = TestCase::getObjectProperty($job, 'code');
return $code->code === $json['code']
&& $code->plan === $data['plan']
&& $code->email === $data['email']
&& $code->voucher === $data['voucher']
&& $code->first_name === $data['first_name']
&& $code->last_name === $data['last_name'];
});
return [
'code' => $json['code'],
'email' => $data['email'],
'first_name' => $data['first_name'],
'last_name' => $data['last_name'],
'plan' => $data['plan'],
'voucher' => $data['voucher']
];
}
/**
* Test signup code verification with invalid input
*
* @depends testSignupInitValidInput
*/
public function testSignupVerifyInvalidInput(array $result): void
{
// Empty data
$data = [];
$response = $this->post('/api/auth/signup/verify', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json['errors']);
$this->assertArrayHasKey('code', $json['errors']);
$this->assertArrayHasKey('short_code', $json['errors']);
// Data with existing code but missing short_code
$data = [
'code' => $result['code'],
];
$response = $this->post('/api/auth/signup/verify', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertArrayHasKey('short_code', $json['errors']);
// Data with invalid short_code
$data = [
'code' => $result['code'],
'short_code' => 'XXXX',
];
$response = $this->post('/api/auth/signup/verify', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertArrayHasKey('short_code', $json['errors']);
// TODO: Test expired code
}
/**
* Test signup code verification with valid input
*
* @depends testSignupInitValidInput
*/
public function testSignupVerifyValidInput(array $result): array
{
$code = SignupCode::find($result['code']);
$data = [
'code' => $code->code,
'short_code' => $code->short_code,
];
$response = $this->post('/api/auth/signup/verify', $data);
$json = $response->json();
$response->assertStatus(200);
$this->assertCount(7, $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']));
return $result;
}
/**
* Test last signup step with invalid input
*
* @depends testSignupVerifyValidInput
*/
public function testSignupInvalidInput(array $result): void
{
// Empty data
$data = [];
$response = $this->post('/api/auth/signup', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(3, $json['errors']);
$this->assertArrayHasKey('login', $json['errors']);
$this->assertArrayHasKey('password', $json['errors']);
$this->assertArrayHasKey('domain', $json['errors']);
$domain = $this->getPublicDomain();
// Passwords do not match and missing domain
$data = [
'login' => 'test',
'password' => 'test',
'password_confirmation' => 'test2',
];
$response = $this->post('/api/auth/signup', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json['errors']);
$this->assertArrayHasKey('password', $json['errors']);
$this->assertArrayHasKey('domain', $json['errors']);
$domain = $this->getPublicDomain();
// Login too short
$data = [
'login' => '1',
'domain' => $domain,
'password' => 'test',
'password_confirmation' => 'test',
];
$response = $this->post('/api/auth/signup', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertArrayHasKey('login', $json['errors']);
// Missing codes
$data = [
'login' => 'login-valid',
'domain' => $domain,
'password' => 'test',
'password_confirmation' => 'test',
];
$response = $this->post('/api/auth/signup', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json['errors']);
$this->assertArrayHasKey('code', $json['errors']);
$this->assertArrayHasKey('short_code', $json['errors']);
// Data with invalid short_code
$data = [
'login' => 'TestLogin',
'domain' => $domain,
'password' => 'test',
'password_confirmation' => 'test',
'code' => $result['code'],
'short_code' => 'XXXX',
];
$response = $this->post('/api/auth/signup', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertArrayHasKey('short_code', $json['errors']);
$code = SignupCode::find($result['code']);
// Data with invalid voucher
$data = [
'login' => 'TestLogin',
'domain' => $domain,
'password' => 'test',
'password_confirmation' => 'test',
'code' => $result['code'],
'short_code' => $code->short_code,
'voucher' => 'XXX',
];
$response = $this->post('/api/auth/signup', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertArrayHasKey('voucher', $json['errors']);
// Valid code, invalid login
$data = [
'login' => 'żżżżżż',
'domain' => $domain,
'password' => 'test',
'password_confirmation' => 'test',
'code' => $result['code'],
'short_code' => $code->short_code,
];
$response = $this->post('/api/auth/signup', $data);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertArrayHasKey('login', $json['errors']);
}
/**
* Test last signup step with valid input (user creation)
*
* @depends testSignupVerifyValidInput
*/
public function testSignupValidInput(array $result): void
{
$queue = Queue::fake();
$domain = $this->getPublicDomain();
$identity = \strtolower('SignupLogin@') . $domain;
$code = SignupCode::find($result['code']);
$data = [
'login' => 'SignupLogin',
'domain' => $domain,
'password' => 'test',
'password_confirmation' => 'test',
'code' => $code->code,
'short_code' => $code->short_code,
'voucher' => 'TEST',
];
$response = $this->post('/api/auth/signup', $data);
$json = $response->json();
$response->assertStatus(200);
$this->assertSame('success', $json['status']);
$this->assertSame('bearer', $json['token_type']);
$this->assertTrue(!empty($json['expires_in']) && is_int($json['expires_in']) && $json['expires_in'] > 0);
$this->assertNotEmpty($json['access_token']);
$this->assertSame($identity, $json['email']);
Queue::assertPushed(\App\Jobs\User\CreateJob::class, 1);
Queue::assertPushed(
\App\Jobs\User\CreateJob::class,
function ($job) use ($data) {
$userEmail = TestCase::getObjectProperty($job, 'userEmail');
return $userEmail === \strtolower($data['login'] . '@' . $data['domain']);
}
);
// Check if the code has been removed
$this->assertNull(SignupCode::where('code', $result['code'])->first());
// Check if the user has been created
$user = User::where('email', $identity)->first();
$this->assertNotEmpty($user);
$this->assertSame($identity, $user->email);
// Check user settings
$this->assertSame($result['first_name'], $user->getSetting('first_name'));
$this->assertSame($result['last_name'], $user->getSetting('last_name'));
$this->assertSame($result['email'], $user->getSetting('external_email'));
// Discount
$discount = Discount::where('code', 'TEST')->first();
$this->assertSame($discount->id, $user->wallets()->first()->discount_id);
// TODO: Check SKUs/Plan
// TODO: Check if the access token works
}
/**
* Test signup for a group (custom domain) account
*/
public function testSignupGroupAccount(): void
{
Queue::fake();
// Initial signup request
$user_data = $data = [
'email' => 'testuser@external.com',
'first_name' => 'Signup',
'last_name' => 'User',
'plan' => 'group',
];
$response = $this->withoutMiddleware()->post('/api/auth/signup/init', $data);
$json = $response->json();
$response->assertStatus(200);
$this->assertCount(2, $json);
$this->assertSame('success', $json['status']);
$this->assertNotEmpty($json['code']);
// Assert the email sending job was pushed once
Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, 1);
// Assert the job has proper data assigned
Queue::assertPushed(\App\Jobs\SignupVerificationEmail::class, function ($job) use ($data, $json) {
$code = TestCase::getObjectProperty($job, 'code');
return $code->code === $json['code']
&& $code->plan === $data['plan']
&& $code->email === $data['email']
&& $code->first_name === $data['first_name']
&& $code->last_name === $data['last_name'];
});
// Verify the code
$code = SignupCode::find($json['code']);
$data = [
'code' => $code->code,
'short_code' => $code->short_code,
];
$response = $this->post('/api/auth/signup/verify', $data);
$result = $response->json();
$response->assertStatus(200);
$this->assertCount(7, $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';
$domain = 'external.com';
$data = [
'login' => $login,
'domain' => $domain,
'password' => 'test',
'password_confirmation' => 'test',
'code' => $code->code,
'short_code' => $code->short_code,
];
$response = $this->post('/api/auth/signup', $data);
$result = $response->json();
$response->assertStatus(200);
$this->assertSame('success', $result['status']);
$this->assertSame('bearer', $result['token_type']);
$this->assertTrue(!empty($result['expires_in']) && is_int($result['expires_in']) && $result['expires_in'] > 0);
$this->assertNotEmpty($result['access_token']);
$this->assertSame("$login@$domain", $result['email']);
Queue::assertPushed(\App\Jobs\Domain\CreateJob::class, 1);
Queue::assertPushed(
\App\Jobs\Domain\CreateJob::class,
function ($job) use ($domain) {
$domainNamespace = TestCase::getObjectProperty($job, 'domainNamespace');
return $domainNamespace === $domain;
}
);
Queue::assertPushed(\App\Jobs\User\CreateJob::class, 1);
Queue::assertPushed(
\App\Jobs\User\CreateJob::class,
function ($job) use ($data) {
$userEmail = TestCase::getObjectProperty($job, 'userEmail');
return $userEmail === $data['login'] . '@' . $data['domain'];
}
);
// Check if the code has been removed
$this->assertNull(SignupCode::find($code->id));
// Check if the user has been created
$user = User::where('email', $login . '@' . $domain)->first();
$this->assertNotEmpty($user);
// Check user settings
$this->assertSame($user_data['email'], $user->getSetting('external_email'));
$this->assertSame($user_data['first_name'], $user->getSetting('first_name'));
$this->assertSame($user_data['last_name'], $user->getSetting('last_name'));
// TODO: Check domain record
// TODO: Check SKUs/Plan
// TODO: Check if the access token works
}
/**
* Test signup via invitation
*/
public function testSignupViaInvitation(): void
{
Queue::fake();
$invitation = SI::create(['email' => 'email1@ext.com']);
$post = [
'invitation' => 'abc',
'first_name' => 'Signup',
'last_name' => 'User',
'login' => 'test-inv',
'domain' => 'kolabnow.com',
'password' => 'test',
'password_confirmation' => 'test',
];
// Test invalid invitation identifier
$response = $this->post('/api/auth/signup', $post);
$response->assertStatus(404);
// Test valid input
$post['invitation'] = $invitation->id;
$response = $this->post('/api/auth/signup', $post);
$result = $response->json();
$response->assertStatus(200);
$this->assertSame('success', $result['status']);
$this->assertSame('bearer', $result['token_type']);
$this->assertTrue(!empty($result['expires_in']) && is_int($result['expires_in']) && $result['expires_in'] > 0);
$this->assertNotEmpty($result['access_token']);
$this->assertSame('test-inv@kolabnow.com', $result['email']);
// Check if the user has been created
$user = User::where('email', 'test-inv@kolabnow.com')->first();
$this->assertNotEmpty($user);
// Check user settings
$this->assertSame($invitation->email, $user->getSetting('external_email'));
$this->assertSame($post['first_name'], $user->getSetting('first_name'));
$this->assertSame($post['last_name'], $user->getSetting('last_name'));
$invitation->refresh();
$this->assertSame($user->id, $invitation->user_id);
$this->assertTrue($invitation->isCompleted());
// TODO: Test POST params validation
}
/**
* List of login/domain validation cases for testValidateLogin()
*
* @return array Arguments for testValidateLogin()
*/
public function dataValidateLogin(): array
{
$domain = $this->getPublicDomain();
return [
// Individual account
['', $domain, false, ['login' => 'The login field is required.']],
['test123456', 'localhost', false, ['domain' => 'The specified domain is invalid.']],
['test123456', 'unknown-domain.org', false, ['domain' => 'The specified domain is invalid.']],
['test.test', $domain, false, null],
['test_test', $domain, false, null],
['test-test', $domain, false, null],
['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.']],
['root', $domain, false, ['login' => 'The specified login is not available.']],
// TODO existing (public domain) user
// ['signuplogin', $domain, false, ['login' => 'The specified login is not available.']],
// Domain account
['admin', 'kolabsys.com', true, null],
['testnonsystemdomain', 'invalid', true, ['domain' => 'The specified domain is invalid.']],
['testnonsystemdomain', '.com', true, ['domain' => 'The specified domain is invalid.']],
// existing custom domain
['jack', 'kolab.org', true, ['domain' => 'The specified domain is not available.']],
];
}
/**
* Signup login/domain validation.
*
* Note: Technically these include unit tests, but let's keep it here for now.
* FIXME: Shall we do a http request for each case?
*
* @dataProvider dataValidateLogin
*/
public function testValidateLogin($login, $domain, $external, $expected_result): void
{
$result = $this->invokeMethod(new SignupController(), 'validateLogin', [$login, $domain, $external]);
$this->assertSame($expected_result, $result);
}
/**
* Signup login/domain validation, more cases
*
* Note: Technically these include unit tests, but let's keep it here for now.
*/
public function testValidateLoginMore(): void
{
$group = $this->getTestGroup('group-test@kolabnow.com');
$login = 'group-test';
$domain = 'kolabnow.com';
$external = false;
$result = $this->invokeMethod(new SignupController(), 'validateLogin', [$login, $domain, $external]);
$this->assertSame(['login' => 'The specified login is not available.'], $result);
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Feb 1, 5:25 AM (1 d, 14 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
426611
Default Alt Text
(62 KB)
Attached To
Mode
R2 kolab
Attached
Detach File
Event Timeline
Log In to Comment