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 @@ -7,6 +7,9 @@ use App\Jobs\SignupVerificationSMS; use App\Domain; use App\Plan; +use App\Rules\ExternalEmail; +use App\Rules\UserEmailDomain; +use App\Rules\UserEmailLocal; use App\SignupCode; use App\User; use Illuminate\Http\Request; @@ -77,7 +80,7 @@ // Validate user email (or phone) if ($error = $this->validatePhoneOrEmail($request->email, $is_phone)) { - return response()->json(['status' => 'error', 'errors' => ['email' => __($error)]], 422); + return response()->json(['status' => 'error', 'errors' => ['email' => $error]], 422); } // Generate the verification code @@ -187,8 +190,7 @@ $domain = $request->domain; // Validate login - if ($errors = $this->validateLogin($login, $domain, $is_domain)) { - $errors = $this->resolveErrors($errors); + if ($errors = self::validateLogin($login, $domain, $is_domain)) { return response()->json(['status' => 'error', 'errors' => $errors], 422); } @@ -234,56 +236,62 @@ } /** - * 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 + * Returns plan for the signup process * - * @return string Error message label on validation error + * @returns \App\Plan Plan object selected for current signup process */ - protected function validatePhoneOrEmail($input, &$is_phone = false) + protected function getPlan() { - $is_phone = false; - - return $this->validateEmail($input); - - // TODO: Phone number support -/* - if (strpos($input, '@')) { - return $this->validateEmail($input); - } + if (!$this->plan) { + // Get the plan if specified and exists... + if ($this->code && $this->code->data['plan']) { + $plan = Plan::where('title', $this->code->data['plan'])->first(); + } - $input = str_replace(array('-', ' '), '', $input); + // ...otherwise use the default plan + if (empty($plan)) { + // TODO: Get default plan title from config + $plan = Plan::where('title', 'individual')->first(); + } - if (!preg_match('/^\+?[0-9]{9,12}$/', $input)) { - return 'validation.noemailorphone'; + $this->plan = $plan; } - $is_phone = true; -*/ + return $this->plan; } /** - * Email address validation + * Checks if the input string is a valid email address or a phone number * - * @param string $email Email address + * @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 label on validation error + * @return string Error message on validation error */ - protected function validateEmail($email) + protected static function validatePhoneOrEmail($input, &$is_phone = false): ?string { - $v = Validator::make(['email' => $email], ['email' => 'required|email']); + $is_phone = false; + + $v = Validator::make( + ['email' => $input], + ['email' => ['required', 'string', new ExternalEmail()]] + ); if ($v->fails()) { - return 'validation.emailinvalid'; + return $v->errors()->toArray()['email'][0]; } - list($local, $domain) = explode('@', $email); + // TODO: Phone number support +/* + $input = str_replace(array('-', ' '), '', $input); - // don't allow @localhost and other no-fqdn - if (strpos($domain, '.') === false) { - return 'validation.emailinvalid'; + if (!preg_match('/^\+?[0-9]{9,12}$/', $input)) { + return \trans('validation.noemailorphone'); } + + $is_phone = true; +*/ + return null; } /** @@ -295,90 +303,45 @@ * * @return array Error messages on validation error */ - protected function validateLogin($login, $domain, $external = false) + protected static function validateLogin($login, $domain, $external = false): ?array { - // don't allow @localhost and other no-fqdn - if (empty($domain) || strpos($domain, '.') === false || stripos($domain, 'www.') === 0) { - return ['domain' => 'validation.domaininvalid']; - } + // Validate login part alone + $v = Validator::make( + ['login' => $login], + ['login' => ['required', 'string', new UserEmailLocal($external)]] + ); - // Local part validation - if (!preg_match('/^[A-Za-z0-9_.-]+$/', $login)) { - return ['login' => 'validation.logininvalid']; + if ($v->fails()) { + return ['login' => $v->errors()->toArray()['login'][0]]; } - $domain = Str::lower($domain); + $domains = $external ? null : Domain::getPublicDomains(); - if (!$external) { - // Check if the local part is not one of exceptions - $exceptions = '/^(admin|administrator|sales|root)$/i'; - if (preg_match($exceptions, $login)) { - return ['login' => 'validation.loginexists']; - } + // Validate the domain + $v = Validator::make( + ['domain' => $domain], + ['domain' => ['required', 'string', new UserEmailDomain($domains)]] + ); - // Check if specified domain is allowed for signup - if (!in_array($domain, Domain::getPublicDomains())) { - return ['domain' => 'validation.domaininvalid']; - } - } else { - // Use email validator to validate the domain part - $v = Validator::make(['email' => 'user@' . $domain], ['email' => 'required|email']); - if ($v->fails()) { - return ['domain' => 'validation.domaininvalid']; - } + if ($v->fails()) { + return ['domain' => $v->errors()->toArray()['domain'][0]]; + } - // TODO: DNS registration check - maybe after signup? + $domain = Str::lower($domain); - // Check if domain is already registered with us + // Check if domain is already registered with us + if ($external) { if (Domain::where('namespace', $domain)->first()) { - return ['domain' => 'validation.domainexists']; + return ['domain' => \trans('validation.domainexists')]; } } // Check if user with specified login already exists - // TODO: Aliases $email = $login . '@' . $domain; if (User::findByEmail($email)) { - return ['login' => 'validation.loginexists']; - } - } - - /** - * 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->data['plan']) { - $plan = Plan::where('title', $this->code->data['plan'])->first(); - } - - // ...otherwise use the default plan - if (empty($plan)) { - // TODO: Get default plan title from config - $plan = Plan::where('title', 'individual')->first(); - } - - $this->plan = $plan; - } - - return $this->plan; - } - - /** - * Convert error labels to actual (localized) text - */ - protected function resolveErrors(array $errors): array - { - $result = []; - - foreach ($errors as $idx => $label) { - $result[$idx] = __($label); + return ['login' => \trans('validation.loginexists')]; } - return $result; + return null; } } diff --git a/src/app/Http/Controllers/API/UsersController.php b/src/app/Http/Controllers/API/UsersController.php --- a/src/app/Http/Controllers/API/UsersController.php +++ b/src/app/Http/Controllers/API/UsersController.php @@ -4,10 +4,14 @@ use App\Http\Controllers\Controller; use App\Domain; +use App\Rules\UserEmailDomain; +use App\Rules\UserEmailLocal; use App\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Validator; +use Illuminate\Support\Str; class UsersController extends Controller { @@ -75,18 +79,7 @@ public function info() { $user = $this->guard()->user(); - $response = $user->toArray(); - - // Settings - // TODO: It might be reasonable to limit the list of settings here to these - // that are safe and are used in the UI - $response['settings'] = []; - foreach ($user->settings as $item) { - $response['settings'][$item->key] = $item->value; - } - - // Status info - $response['statusInfo'] = self::statusInfo($user); + $response = $this->userResponse($user); return response()->json($response); } @@ -112,7 +105,6 @@ return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); } - $credentials = $request->only('email', 'password'); if ($token = $this->guard()->attempt($credentials)) { @@ -181,10 +173,12 @@ $user = User::find($id); if (empty($user)) { - return $this->errorResponse(404); + return $this->errorResponse(404); } - return response()->json($user); + $response = $this->userResponse($user); + + return response()->json($response); } /** @@ -216,7 +210,7 @@ $domain = Domain::where('namespace', $domain)->first(); // If that is not a public domain, add domain specific steps - if (!$domain->isPublic()) { + if ($domain && !$domain->isPublic()) { $steps['domain-new'] = true; $steps['domain-ldap-ready'] = 'isLdapReady'; $steps['domain-verified'] = 'isVerified'; @@ -255,7 +249,45 @@ */ public function store(Request $request) { - // TODO + if ($this->guard()->user()->controller()->id !== $this->guard()->user()->id) { + return $this->errorResponse(403); + } + + if ($error_response = $this->validateUserRequest($request, null, $settings)) { + return $error_response; + } + + $user_name = !empty($settings['first_name']) ? $settings['first_name'] : ''; + if (!empty($settings['last_name'])) { + $user_name .= ' ' . $settings['last_name']; + } + + DB::beginTransaction(); + + // Create user record + $user = User::create([ + 'name' => $user_name, + 'email' => $request->email, + 'password' => $request->password, + ]); + + if (!empty($settings)) { + $user->setSettings($settings); + } + + // TODO: Assign package + + // Add aliases + if (!empty($request->aliases)) { + $user->setAliases($request->aliases); + } + + DB::commit(); + + return response()->json([ + 'status' => 'success', + 'message' => __('app.user-create-success'), + ]); } /** @@ -278,41 +310,29 @@ return $this->errorResponse(404); } - $rules = [ - 'external_email' => 'nullable|email', - 'phone' => 'string|nullable|max:64|regex:/^[0-9+() -]+$/', - 'first_name' => 'string|nullable|max:512', - 'last_name' => 'string|nullable|max:512', - 'billing_address' => 'string|nullable|max:1024', - 'country' => 'string|nullable|alpha|size:2', - 'currency' => 'string|nullable|alpha|size:3', - ]; - - if (!empty($request->password) || !empty($request->password_confirmation)) { - $rules['password'] = 'required|min:4|max:2048|confirmed'; + if ($error_response = $this->validateUserRequest($request, $user, $settings)) { + return $error_response; } - // Validate input - $v = Validator::make($request->all(), $rules); - - if ($v->fails()) { - return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); - } - - // Update user settings - $settings = $request->only(array_keys($rules)); - unset($settings['password']); + DB::beginTransaction(); if (!empty($settings)) { $user->setSettings($settings); } // Update user password - if (!empty($rules['password'])) { + if (!empty($request->password)) { $user->password = $request->password; $user->save(); } + // Update aliases + if (isset($request->aliases)) { + $user->setAliases($request->aliases); + } + + DB::commit(); + return response()->json([ 'status' => 'success', 'message' => __('app.user-update-success'), @@ -345,4 +365,181 @@ return $current_user->id == $user_id; } + + /** + * Create a response data array for specified user. + * + * @param \App\User $user User object + * + * @return array Response data + */ + protected function userResponse(User $user): array + { + $response = $user->toArray(); + + // Settings + // TODO: It might be reasonable to limit the list of settings here to these + // that are safe and are used in the UI + $response['settings'] = []; + foreach ($user->settings as $item) { + $response['settings'][$item->key] = $item->value; + } + + // Aliases + $response['aliases'] = []; + foreach ($user->aliases as $item) { + $response['aliases'][] = $item->alias; + } + + // Status info + $response['statusInfo'] = self::statusInfo($user); + + return $response; + } + + /** + * Validate user input + * + * @param \Illuminate\Http\Request $request The API request. + * @param \App\User|null $user User identifier + * @param array $settings User settings (from the request) + * + * @return \Illuminate\Http\JsonResponse The response on error + */ + protected function validateUserRequest(Request $request, $user, &$settings = []) + { + $rules = [ + 'external_email' => 'nullable|email', + 'phone' => 'string|nullable|max:64|regex:/^[0-9+() -]+$/', + 'first_name' => 'string|nullable|max:512', + 'last_name' => 'string|nullable|max:512', + 'billing_address' => 'string|nullable|max:1024', + 'country' => 'string|nullable|alpha|size:2', + 'currency' => 'string|nullable|alpha|size:3', + 'aliases' => 'array|nullable', + ]; + + if (empty($user) || !empty($request->password) || !empty($request->password_confirmation)) { + $rules['password'] = 'required|min:4|max:2048|confirmed'; + } + + $errors = []; + + // Validate input + $v = Validator::make($request->all(), $rules); + + if ($v->fails()) { + $errors = $v->errors()->toArray(); + } + + $controller = $user ? $user->controller() : $this->guard()->user(); + + // For new user validate email address + if (empty($user)) { + $email = $request->email; + + if (empty($email)) { + $errors['email'] = \trans('validation.required', ['attribute' => 'email']); + } elseif ($error = self::validateEmail($email, $controller, false)) { + $errors['email'] = $error; + } + } + + // Validate aliases input + if (isset($request->aliases)) { + $aliases = []; + $existing_aliases = $user ? $user->aliases()->get()->pluck('alias')->toArray() : []; + + foreach ($request->aliases as $idx => $alias) { + if (is_string($alias) && !empty($alias)) { + // Alias cannot be the same as the email address (new user) + if (!empty($email) && Str::lower($alias) == Str::lower($email)) { + continue; + } + + // validate new aliases + if ( + !in_array($alias, $existing_aliases) + && ($error = self::validateEmail($alias, $controller, true)) + ) { + if (!isset($errors['aliases'])) { + $errors['aliases'] = []; + } + $errors['aliases'][$idx] = $error; + continue; + } + + $aliases[] = $alias; + } + } + + $request->aliases = $aliases; + } + + if (!empty($errors)) { + return response()->json(['status' => 'error', 'errors' => $errors], 422); + } + + // Update user settings + $settings = $request->only(array_keys($rules)); + unset($settings['password'], $settings['aliases'], $settings['email']); + } + + /** + * Email address (login or alias) validation + * + * @param string $email Email address + * @param \App\User $user The account owner + * @param bool $is_alias The email is an alias + * + * @return string Error message on validation error + */ + protected static function validateEmail(string $email, User $user, bool $is_alias = false): ?string + { + $attribute = $is_alias ? 'alias' : 'email'; + + if (strpos($email, '@') === false) { + return \trans('validation.entryinvalid', ['attribute' => $attribute]); + } + + list($login, $domain) = explode('@', $email); + + // Check if domain exists + $domain = Domain::where('namespace', Str::lower($domain))->first(); + + if (empty($domain)) { + return \trans('validation.domaininvalid'); + } + + // Validate login part alone + $v = Validator::make( + [$attribute => $login], + [$attribute => ['required', new UserEmailLocal(!$domain->isPublic())]] + ); + + if ($v->fails()) { + return $v->errors()->toArray()[$attribute][0]; + } + + // Check if it is one of domains available to the user + // TODO: We should have a helper that returns "flat" array with domain names + // I guess we could use pluck() somehow + $domains = array_map( + function ($domain) { + return $domain->namespace; + }, + $user->domains() + ); + + if (!in_array($domain->namespace, $domains)) { + return \trans('validation.entryexists', ['attribute' => 'domain']); + } + + // Check if user with specified address already exists + if (User::findByEmail($email)) { + return \trans('validation.entryexists', ['attribute' => $attribute]); + } + + return null; + } } diff --git a/src/app/Observers/UserAliasObserver.php b/src/app/Observers/UserAliasObserver.php new file mode 100644 --- /dev/null +++ b/src/app/Observers/UserAliasObserver.php @@ -0,0 +1,88 @@ +alias = \strtolower($alias->alias); + + if (User::where('email', $alias->alias)->first()) { + \Log::error("Failed creating alias {$alias->alias}. User exists."); + return false; + } + } + + /** + * Handle the user alias "created" event. + * + * @param \App\UserAlias $alias User email alias + * + * @return void + */ + public function created(UserAlias $alias) + { + \App\Jobs\UserUpdate::dispatch($alias->user); + } + + /** + * Handle the user setting "updated" event. + * + * @param \App\UserAlias $alias User email alias + * + * @return void + */ + public function updated(UserAlias $alias) + { + \App\Jobs\UserUpdate::dispatch($alias->user); + } + + /** + * Handle the user setting "deleted" event. + * + * @param \App\UserAlias $alias User email alias + * + * @return void + */ + public function deleted(UserAlias $alias) + { + \App\Jobs\UserUpdate::dispatch($alias->user); + } + + /** + * Handle the user alias "restored" event. + * + * @param \App\UserAlias $alias User email alias + * + * @return void + */ + public function restored(UserAlias $alias) + { + // not used + } + + /** + * Handle the user alias "force deleted" event. + * + * @param \App\UserAlias $alias User email alias + * + * @return void + */ + public function forceDeleted(UserAlias $alias) + { + // not used + } +} diff --git a/src/app/Providers/AppServiceProvider.php b/src/app/Providers/AppServiceProvider.php --- a/src/app/Providers/AppServiceProvider.php +++ b/src/app/Providers/AppServiceProvider.php @@ -32,6 +32,7 @@ \App\SignupCode::observe(\App\Observers\SignupCodeObserver::class); \App\Sku::observe(\App\Observers\SkuObserver::class); \App\User::observe(\App\Observers\UserObserver::class); + \App\UserAlias::observe(\App\Observers\UserAliasObserver::class); \App\UserSetting::observe(\App\Observers\UserSettingObserver::class); \App\VerificationCode::observe(\App\Observers\VerificationCodeObserver::class); \App\Wallet::observe(\App\Observers\WalletObserver::class); diff --git a/src/app/Rules/ExternalEmail.php b/src/app/Rules/ExternalEmail.php new file mode 100644 --- /dev/null +++ b/src/app/Rules/ExternalEmail.php @@ -0,0 +1,52 @@ + $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/UserEmailDomain.php b/src/app/Rules/UserEmailDomain.php new file mode 100644 --- /dev/null +++ b/src/app/Rules/UserEmailDomain.php @@ -0,0 +1,70 @@ +domains = $domains; + } + + /** + * Determine if the validation rule passes. + * + * Validation of local part of an email address that's + * going to be user's login. + * + * @param string $attribute Attribute name + * @param mixed $domain Domain part of email address + * + * @return bool + */ + public function passes($attribute, $domain): bool + { + // don't allow @localhost and other no-fqdn + if (empty($domain) || strpos($domain, '.') === false || stripos($domain, 'www.') === 0) { + $this->message = \trans('validation.domaininvalid'); + return false; + } + + $domain = Str::lower($domain); + + // Use email validator to validate the domain part + $v = Validator::make(['email' => 'user@' . $domain], ['email' => 'required|email']); + if ($v->fails()) { + $this->message = \trans('validation.domaininvalid'); + return false; + } + + // Check if specified domain is allowed for signup + if (is_array($this->domains) && !in_array($domain, $this->domains)) { + $this->message = \trans('validation.domaininvalid'); + return false; + } + + return true; + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message(): ?string + { + return $this->message; + } +} diff --git a/src/app/Rules/UserEmailLocal.php b/src/app/Rules/UserEmailLocal.php new file mode 100644 --- /dev/null +++ b/src/app/Rules/UserEmailLocal.php @@ -0,0 +1,72 @@ +external = $external; + } + + /** + * Determine if the validation rule passes. + * + * Validation of local part of an email address that's + * going to be user's login. + * + * @param string $attribute Attribute name + * @param mixed $login Local part of email address + * + * @return bool + */ + public function passes($attribute, $login): bool + { + // Strict validation + if (!preg_match('/^[A-Za-z0-9_.-]+$/', $login)) { + $this->message = \trans('validation.entryinvalid', ['attribute' => $attribute]); + return false; + } + + // Standard email address validation + $v = Validator::make([$attribute => $login . '@test.com'], [$attribute => 'required|email']); + if ($v->fails()) { + $this->message = \trans('validation.entryinvalid', ['attribute' => $attribute]); + return false; + } + + // Check if the local part is not one of exceptions + // (when signing up for an account in public domain + if (!$this->external) { + $exceptions = '/^(admin|administrator|sales|root)$/i'; + + if (preg_match($exceptions, $login)) { + $this->message = \trans('validation.entryexists', ['attribute' => $attribute]); + return false; + } + } + + return true; + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message(): ?string + { + return $this->message; + } +} diff --git a/src/app/Traits/UserAliasesTrait.php b/src/app/Traits/UserAliasesTrait.php new file mode 100644 --- /dev/null +++ b/src/app/Traits/UserAliasesTrait.php @@ -0,0 +1,41 @@ + 'some@other.erg']); + * $user->setAliases(['alias1@other.org', 'alias2@other.org']); + * ``` + * + * @param array $aliases An array of email addresses + * + * @return void + */ + public function setAliases(array $aliases): void + { + $existing_aliases = $this->aliases()->get()->map(function ($alias) { + return $alias->alias; + })->toArray(); + + $aliases = array_map('strtolower', $aliases); + $aliases = array_unique($aliases); + + foreach (array_diff($aliases, $existing_aliases) as $alias) { + $this->aliases()->create(['alias' => $alias]); + } + + foreach (array_diff($existing_aliases, $aliases) as $alias) { + $this->aliases()->where('alias', $alias)->delete(); + } + } +} diff --git a/src/app/User.php b/src/app/User.php --- a/src/app/User.php +++ b/src/app/User.php @@ -2,24 +2,30 @@ namespace App; +use App\UserAlias; +use App\Traits\UserAliasesTrait; +use App\Traits\UserSettingsTrait; use Illuminate\Notifications\Notifiable; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\User as Authenticatable; use Iatstuti\Database\Support\NullableFields; use Tymon\JWTAuth\Contracts\JWTSubject; -use App\Traits\UserSettingsTrait; /** * The eloquent definition of a User. * - * @property integer $id - * @property integer $status + * @property string $email + * @property int $id + * @property string $name + * @property string $password + * @property int $status */ class User extends Authenticatable implements JWTSubject { use Notifiable; use NullableFields; + use UserAliasesTrait; use UserSettingsTrait; use SoftDeletes; @@ -96,6 +102,16 @@ } /** + * Email aliases of this user. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function aliases() + { + return $this->hasMany('App\UserAlias', 'user_id'); + } + + /** * Assign a package to a user. The user should not have any existing entitlements. * * @param \App\Package $package The package to assign. @@ -128,6 +144,26 @@ return $user; } + /** + * Returns user controlling the current user (or self when it's the account owner) + * + * @return \App\User A user object + */ + public function controller(): User + { + // FIXME: This is most likely not the best way to do this + $entitlement = \App\Entitlement::where([ + 'entitleable_id' => $this->id, + 'entitleable_type' => User::class + ])->first(); + + if ($entitlement && $entitlement->owner_id != $this->id) { + return $entitlement->owner; + } + + return $this; + } + public function assignPlan($plan, $domain = null) { $this->setSetting('plan_id', $plan->id); @@ -216,7 +252,7 @@ * * @param string $email Email address * - * @return \App\User User model object + * @return \App\User User model object if found */ public static function findByEmail(string $email): ?User { @@ -224,11 +260,23 @@ return null; } + $email = \strtolower($email); + $user = self::where('email', $email)->first(); - // TODO: Aliases, External email + if ($user) { + return $user; + } - return $user; + $alias = UserAlias::where('alias', $email)->first(); + + if ($alias) { + return $alias->user; + } + + // TODO: External email + + return null; } public function getJWTIdentifier() diff --git a/src/app/UserAlias.php b/src/app/UserAlias.php new file mode 100644 --- /dev/null +++ b/src/app/UserAlias.php @@ -0,0 +1,33 @@ +belongsTo( + '\App\User', + 'user_id', /* local */ + 'id' /* remote */ + ); + } +} diff --git a/src/database/migrations/2020_02_26_000000_create_user_aliases_table.php b/src/database/migrations/2020_02_26_000000_create_user_aliases_table.php new file mode 100644 --- /dev/null +++ b/src/database/migrations/2020_02_26_000000_create_user_aliases_table.php @@ -0,0 +1,39 @@ +bigIncrements('id'); + $table->bigInteger('user_id'); + $table->string('alias')->unique(); + $table->timestamps(); + + $table->foreign('user_id')->references('id')->on('users') + ->onDelete('cascade')->onUpdate('cascade'); + } + ); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('user_aliases'); + } +} diff --git a/src/database/seeds/UserSeeder.php b/src/database/seeds/UserSeeder.php --- a/src/database/seeds/UserSeeder.php +++ b/src/database/seeds/UserSeeder.php @@ -45,6 +45,8 @@ ] ); + $john->setAliases(['john.doe@kolab.org']); + $user_wallets = $john->wallets()->get(); $package_domain = \App\Package::where('title', 'domain-hosting')->first(); @@ -71,6 +73,8 @@ ] ); + $jack->setAliases(['jack.daniels@kolab.org']); + $john->assignPackage($package_kolab, $jack); factory(User::class, 10)->create(); 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 @@ -35,17 +35,46 @@ const input = $('#' + input_name) if (input.length) { - input.addClass('is-invalid') - input.parent().find('.invalid-feedback').remove() - input.parent().append($('