diff --git a/src/.env.example b/src/.env.example
--- a/src/.env.example
+++ b/src/.env.example
@@ -6,6 +6,7 @@
APP_PUBLIC_URL=
APP_DOMAIN=kolabnow.com
APP_THEME=default
+APP_TENANT_ID=1
ASSET_URL=http://127.0.0.1:8000
diff --git a/src/app/Console/Command.php b/src/app/Console/Command.php
--- a/src/app/Console/Command.php
+++ b/src/app/Console/Command.php
@@ -2,8 +2,24 @@
namespace App\Console;
-class Command extends \Illuminate\Console\Command
+abstract class Command extends \Illuminate\Console\Command
{
+ /**
+ * Annotate this command as being dangerous for any potential unintended consequences.
+ *
+ * Commands are considered dangerous if;
+ *
+ * * observers are deliberately not triggered, meaning that the deletion of an object model that requires the
+ * associated observer to clean some things up, or charge a wallet or something, are deliberately not triggered,
+ *
+ * * deletion of objects and their relations rely on database foreign keys with obscure cascading,
+ *
+ * * a command will result in the permanent, irrecoverable loss of data.
+ *
+ * @var boolean
+ */
+ protected $dangerous = false;
+
/**
* Find the domain.
*
@@ -25,7 +41,7 @@
*
* @return mixed
*/
- public function getObject($objectClass, $objectIdOrTitle, $objectTitle)
+ public function getObject($objectClass, $objectIdOrTitle, $objectTitle = null)
{
if ($this->hasOption('with-deleted') && $this->option('with-deleted')) {
$object = $objectClass::withTrashed()->find($objectIdOrTitle);
@@ -68,6 +84,26 @@
return $this->getObject(\App\Wallet::class, $wallet, null);
}
+ public function handle()
+ {
+ if ($this->dangerous) {
+ $this->warn(
+ "This command is a dangerous scalpel command with potentially significant unintended consequences"
+ );
+
+ $confirmation = $this->confirm("Are you sure you understand what's about to happen?");
+
+ if (!$confirmation) {
+ $this->info("Better safe than sorry.");
+ return false;
+ }
+
+ $this->info("VĂ¡monos!");
+ }
+
+ return true;
+ }
+
/**
* Return a string for output, with any additional attributes specified as well.
*
diff --git a/src/app/Console/Commands/Discount/MergeCommand.php b/src/app/Console/Commands/Discount/MergeCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Discount/MergeCommand.php
@@ -0,0 +1,95 @@
+ 158f660b-e992-4fb9-ac12-5173b5f33807 \
+ * > 62af659f-17d8-4527-87c1-c69eaa26653c \
+ * > --description="Employee discount"
+ * ```
+ */
+class MergeCommand extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'discount:merge {source} {target} {--description*}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'Merge one discount in to another discount, ' .
+ 'optionally set the description, and delete the source discount';
+
+ /**
+ * Create a new command instance.
+ *
+ * @return void
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $source = \App\Discount::find($this->argument('source'));
+
+ if (!$source) {
+ $this->error("No such source discount: {$source}");
+ return 1;
+ }
+
+ $target = \App\Discount::find($this->argument('target'));
+
+ if (!$target) {
+ $this->error("No such target discount: {$target}");
+ return 1;
+ }
+
+ if ($source->discount !== $target->discount) {
+ $this->error("Can't merge two discounts that have different rates");
+ return 1;
+ }
+
+ foreach ($source->wallets as $wallet) {
+ $wallet->discount_id = $target->id;
+ $wallet->timestamps = false;
+ $wallet->save();
+ }
+
+ if ($this->option('description')) {
+ $target->description = $this->option('description');
+ $target->save();
+ }
+
+ $source->delete();
+ }
+}
diff --git a/src/app/Console/Commands/DiscountList.php b/src/app/Console/Commands/DiscountList.php
deleted file mode 100644
--- a/src/app/Console/Commands/DiscountList.php
+++ /dev/null
@@ -1,61 +0,0 @@
-orderBy('discount')->get()->each(
- function ($discount) {
- $name = $discount->description;
-
- if ($discount->code) {
- $name .= " [{$discount->code}]";
- }
-
- $this->info(
- sprintf(
- "%s %3d%% %s",
- $discount->id,
- $discount->discount,
- $name
- )
- );
- }
- );
- }
-}
diff --git a/src/app/Console/Commands/DiscountsCommand.php b/src/app/Console/Commands/DiscountsCommand.php
--- a/src/app/Console/Commands/DiscountsCommand.php
+++ b/src/app/Console/Commands/DiscountsCommand.php
@@ -4,6 +4,29 @@
use App\Console\ObjectListCommand;
+/**
+ * List discounts.
+ *
+ * Example usage:
+ *
+ * ```
+ * $ ./artisan discounts
+ * 003f18e5-cbd2-4de8-9485-b0c966e4757d
+ * 00603496-5c91-4347-b341-cd5022566210
+ * 0076b174-f122-458a-8466-bd05c3cac35d
+ * (...)
+ * ```
+ *
+ * To include specific attributes, use `--attr` (allowed multiple times):
+ *
+ * ```
+ * $ ./artisan discounts --attr=discount --attr=description
+ * 003f18e5-cbd2-4de8-9485-b0c966e4757d 54 Custom volume discount
+ * 00603496-5c91-4347-b341-cd5022566210 30 Developer Discount
+ * 0076b174-f122-458a-8466-bd05c3cac35d 100 it's me
+ * (...)
+ * ```
+ */
class DiscountsCommand extends ObjectListCommand
{
protected $objectClass = \App\Discount::class;
diff --git a/src/app/Console/Commands/DomainAdd.php b/src/app/Console/Commands/Domain/CreateCommand.php
rename from src/app/Console/Commands/DomainAdd.php
rename to src/app/Console/Commands/Domain/CreateCommand.php
--- a/src/app/Console/Commands/DomainAdd.php
+++ b/src/app/Console/Commands/Domain/CreateCommand.php
@@ -1,20 +1,18 @@
argument('domain'));
// must use withTrashed(), because unique constraint
- $domain = Domain::withTrashed()->where('namespace', $namespace)->first();
+ $domain = \App\Domain::withTrashed()->where('namespace', $namespace)->first();
if ($domain && !$this->option('force')) {
$this->error("Domain {$namespace} already exists.");
return 1;
}
- Queue::fake(); // ignore LDAP for now
-
if ($domain) {
if ($domain->deleted_at) {
- // revive domain
- $domain->deleted_at = null;
- $domain->status = 0;
+ // set the status back to new
+ $domain->status = \App\Domain::STATUS_NEW;
$domain->save();
// remove existing entitlement
- $entitlement = Entitlement::withTrashed()->where(
+ $entitlement = \App\Entitlement::withTrashed()->where(
[
'entitleable_id' => $domain->id,
'entitleable_type' => \App\Domain::class
@@ -60,17 +55,36 @@
if ($entitlement) {
$entitlement->forceDelete();
}
+
+ // restore the domain to allow for the observer to handle the create job
+ $domain->restore();
+
+ $this->info(
+ sprintf(
+ "Domain %s with ID %d revived. Remember to assign it to a wallet with 'domain:set-wallet'",
+ $domain->namespace,
+ $domain->id
+ )
+ );
} else {
$this->error("Domain {$namespace} not marked as deleted... examine more closely");
return 1;
}
} else {
- $domain = Domain::create([
+ $domain = \App\Domain::create(
+ [
'namespace' => $namespace,
- 'type' => Domain::TYPE_EXTERNAL,
- ]);
- }
+ 'type' => \App\Domain::TYPE_EXTERNAL,
+ ]
+ );
- $this->info($domain->id);
+ $this->info(
+ sprintf(
+ "Domain %s created with ID %d. Remember to assign it to a wallet with 'domain:set-wallet'",
+ $domain->namespace,
+ $domain->id
+ )
+ );
+ }
}
}
diff --git a/src/app/Console/Commands/Group/CreateCommand.php b/src/app/Console/Commands/Group/CreateCommand.php
--- a/src/app/Console/Commands/Group/CreateCommand.php
+++ b/src/app/Console/Commands/Group/CreateCommand.php
@@ -9,6 +9,11 @@
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
+/**
+ * Create a (mail-enabled) distribution group.
+ *
+ * @see \App\Console\Commands\Scalpel\Group\CreateCommand
+ */
class CreateCommand extends Command
{
/**
diff --git a/src/app/Console/Commands/Scalpel/Discount/CreateCommand.php b/src/app/Console/Commands/Scalpel/Discount/CreateCommand.php
--- a/src/app/Console/Commands/Scalpel/Discount/CreateCommand.php
+++ b/src/app/Console/Commands/Scalpel/Discount/CreateCommand.php
@@ -6,6 +6,8 @@
class CreateCommand extends ObjectCreateCommand
{
+ protected $hidden = true;
+
protected $commandPrefix = 'scalpel';
protected $objectClass = \App\Discount::class;
protected $objectName = 'discount';
diff --git a/src/app/Console/Commands/Scalpel/Discount/CreateCommand.php b/src/app/Console/Commands/Scalpel/Discount/DeleteCommand.php
copy from src/app/Console/Commands/Scalpel/Discount/CreateCommand.php
copy to src/app/Console/Commands/Scalpel/Discount/DeleteCommand.php
--- a/src/app/Console/Commands/Scalpel/Discount/CreateCommand.php
+++ b/src/app/Console/Commands/Scalpel/Discount/DeleteCommand.php
@@ -2,10 +2,13 @@
namespace App\Console\Commands\Scalpel\Discount;
-use App\Console\ObjectCreateCommand;
+use App\Console\ObjectDeleteCommand;
-class CreateCommand extends ObjectCreateCommand
+class DeleteCommand extends ObjectDeleteCommand
{
+ protected $dangerous = true;
+ protected $hidden = true;
+
protected $commandPrefix = 'scalpel';
protected $objectClass = \App\Discount::class;
protected $objectName = 'discount';
diff --git a/src/app/Console/Commands/Scalpel/Discount/ReadCommand.php b/src/app/Console/Commands/Scalpel/Discount/ReadCommand.php
--- a/src/app/Console/Commands/Scalpel/Discount/ReadCommand.php
+++ b/src/app/Console/Commands/Scalpel/Discount/ReadCommand.php
@@ -6,6 +6,8 @@
class ReadCommand extends ObjectReadCommand
{
+ protected $hidden = true;
+
protected $commandPrefix = 'scalpel';
protected $objectClass = \App\Discount::class;
protected $objectName = 'discount';
diff --git a/src/app/Console/Commands/Scalpel/Discount/CreateCommand.php b/src/app/Console/Commands/Scalpel/Discount/UpdateCommand.php
copy from src/app/Console/Commands/Scalpel/Discount/CreateCommand.php
copy to src/app/Console/Commands/Scalpel/Discount/UpdateCommand.php
--- a/src/app/Console/Commands/Scalpel/Discount/CreateCommand.php
+++ b/src/app/Console/Commands/Scalpel/Discount/UpdateCommand.php
@@ -2,10 +2,12 @@
namespace App\Console\Commands\Scalpel\Discount;
-use App\Console\ObjectCreateCommand;
+use App\Console\ObjectUpdateCommand;
-class CreateCommand extends ObjectCreateCommand
+class UpdateCommand extends ObjectUpdateCommand
{
+ protected $hidden = true;
+
protected $commandPrefix = 'scalpel';
protected $objectClass = \App\Discount::class;
protected $objectName = 'discount';
diff --git a/src/app/Console/Commands/Scalpel/Domain/CreateCommand.php b/src/app/Console/Commands/Scalpel/Domain/CreateCommand.php
--- a/src/app/Console/Commands/Scalpel/Domain/CreateCommand.php
+++ b/src/app/Console/Commands/Scalpel/Domain/CreateCommand.php
@@ -6,6 +6,8 @@
class CreateCommand extends ObjectCreateCommand
{
+ protected $hidden = true;
+
protected $commandPrefix = 'scalpel';
protected $objectClass = \App\Domain::class;
protected $objectName = 'domain';
diff --git a/src/app/Console/Commands/Scalpel/Domain/ReadCommand.php b/src/app/Console/Commands/Scalpel/Domain/ReadCommand.php
--- a/src/app/Console/Commands/Scalpel/Domain/ReadCommand.php
+++ b/src/app/Console/Commands/Scalpel/Domain/ReadCommand.php
@@ -6,6 +6,8 @@
class ReadCommand extends ObjectReadCommand
{
+ protected $hidden = true;
+
protected $commandPrefix = 'scalpel';
protected $objectClass = \App\Domain::class;
protected $objectName = 'domain';
diff --git a/src/app/Console/Commands/Scalpel/Domain/UpdateCommand.php b/src/app/Console/Commands/Scalpel/Domain/UpdateCommand.php
--- a/src/app/Console/Commands/Scalpel/Domain/UpdateCommand.php
+++ b/src/app/Console/Commands/Scalpel/Domain/UpdateCommand.php
@@ -6,6 +6,8 @@
class UpdateCommand extends ObjectUpdateCommand
{
+ protected $hidden = true;
+
protected $commandPrefix = 'scalpel';
protected $objectClass = \App\Domain::class;
protected $objectName = 'domain';
diff --git a/src/app/Console/Commands/Scalpel/Entitlement/CreateCommand.php b/src/app/Console/Commands/Scalpel/Entitlement/CreateCommand.php
--- a/src/app/Console/Commands/Scalpel/Entitlement/CreateCommand.php
+++ b/src/app/Console/Commands/Scalpel/Entitlement/CreateCommand.php
@@ -6,6 +6,8 @@
class CreateCommand extends ObjectCreateCommand
{
+ protected $hidden = true;
+
protected $commandPrefix = 'scalpel';
protected $objectClass = \App\Entitlement::class;
protected $objectName = 'entitlement';
diff --git a/src/app/Console/Commands/Scalpel/Entitlement/ReadCommand.php b/src/app/Console/Commands/Scalpel/Entitlement/ReadCommand.php
--- a/src/app/Console/Commands/Scalpel/Entitlement/ReadCommand.php
+++ b/src/app/Console/Commands/Scalpel/Entitlement/ReadCommand.php
@@ -6,6 +6,8 @@
class ReadCommand extends ObjectReadCommand
{
+ protected $hidden = true;
+
protected $commandPrefix = 'scalpel';
protected $objectClass = \App\Entitlement::class;
protected $objectName = 'entitlement';
diff --git a/src/app/Console/Commands/Scalpel/Entitlement/UpdateCommand.php b/src/app/Console/Commands/Scalpel/Entitlement/UpdateCommand.php
--- a/src/app/Console/Commands/Scalpel/Entitlement/UpdateCommand.php
+++ b/src/app/Console/Commands/Scalpel/Entitlement/UpdateCommand.php
@@ -6,6 +6,8 @@
class UpdateCommand extends ObjectUpdateCommand
{
+ protected $hidden = true;
+
protected $commandPrefix = 'scalpel';
protected $objectClass = \App\Entitlement::class;
protected $objectName = 'entitlement';
diff --git a/src/app/Console/Commands/Scalpel/Group/CreateCommand.php b/src/app/Console/Commands/Scalpel/Group/CreateCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Scalpel/Group/CreateCommand.php
@@ -0,0 +1,15 @@
+owner) {
+ if (!$wallet || !$wallet->owner || $wallet->owner->tenant_id != \config('app.tenant_id')) {
return 1;
}
@@ -51,6 +51,7 @@
// Get all wallets, excluding deleted accounts
$wallets = Wallet::select('wallets.*')
->join('users', 'users.id', '=', 'wallets.user_id')
+ ->withEnvTenant('users')
->whereNull('users.deleted_at')
->get();
}
diff --git a/src/app/Console/ObjectUpdateCommand.php b/src/app/Console/ObjectDeleteCommand.php
copy from src/app/Console/ObjectUpdateCommand.php
copy to src/app/Console/ObjectDeleteCommand.php
--- a/src/app/Console/ObjectUpdateCommand.php
+++ b/src/app/Console/ObjectDeleteCommand.php
@@ -8,13 +8,13 @@
/**
* This abstract class provides a means to treat objects in our model using CRUD.
*/
-abstract class ObjectUpdateCommand extends ObjectCommand
+abstract class ObjectDeleteCommand extends ObjectCommand
{
public function __construct()
{
- $this->description = "Update a {$this->objectName}";
+ $this->description = "Delete a {$this->objectName}";
$this->signature = sprintf(
- "%s%s:update {%s}",
+ "%s%s:delete {%s}",
$this->commandPrefix ? $this->commandPrefix . ":" : "",
$this->objectName,
$this->objectName
@@ -36,10 +36,6 @@
$classes = class_uses_recursive($this->objectClass);
- if (in_array(SoftDeletes::class, $classes)) {
- $this->signature .= " {--with-deleted : Include deleted {$this->objectName}s}";
- }
-
parent::__construct();
}
@@ -73,6 +69,12 @@
*/
public function handle()
{
+ $result = parent::handle();
+
+ if (!$result) {
+ return 1;
+ }
+
$argument = $this->argument($this->objectName);
$object = $this->getObject($this->objectClass, $argument, $this->objectTitle);
@@ -82,18 +84,12 @@
return 1;
}
- foreach ($this->getProperties() as $property => $value) {
- if ($property == "deleted_at" && $value == "null") {
- $value = null;
- }
-
- $object->{$property} = $value;
+ if ($this->commandPrefix == 'scalpel') {
+ $this->objectClass::withoutEvents(
+ function () use ($object) {
+ $object->delete();
+ }
+ );
}
-
- $object->timestamps = false;
-
- $object->save(['timestamps' => false]);
-
- $this->cacheRefresh($object);
}
}
diff --git a/src/app/Console/ObjectRelationListCommand.php b/src/app/Console/ObjectRelationListCommand.php
--- a/src/app/Console/ObjectRelationListCommand.php
+++ b/src/app/Console/ObjectRelationListCommand.php
@@ -67,19 +67,16 @@
return 1;
}
- if ($result instanceof \Illuminate\Database\Eloquent\Collection) {
- $result->each(
- function ($entry) {
- $this->info($this->toString($entry));
- }
- );
- } elseif ($result instanceof \Illuminate\Database\Eloquent\Relations\Relation) {
- $result->each(
- function ($entry) {
- $this->info($this->toString($entry));
- }
- );
- } elseif (is_array($result)) {
+ // Convert query builder into a collection
+ if ($result instanceof \Illuminate\Database\Eloquent\Relations\Relation) {
+ $result = $result->get();
+ }
+
+ // Print the result
+ if (
+ ($result instanceof \Illuminate\Database\Eloquent\Collection)
+ || is_array($result)
+ ) {
foreach ($result as $entry) {
$this->info($this->toString($entry));
}
diff --git a/src/app/Console/ObjectUpdateCommand.php b/src/app/Console/ObjectUpdateCommand.php
--- a/src/app/Console/ObjectUpdateCommand.php
+++ b/src/app/Console/ObjectUpdateCommand.php
@@ -92,7 +92,15 @@
$object->timestamps = false;
- $object->save(['timestamps' => false]);
+ if ($this->commandPrefix == 'scalpel') {
+ $this->objectClass::withoutEvents(
+ function () use ($object) {
+ $object->save();
+ }
+ );
+ } else {
+ $object->save();
+ }
$this->cacheRefresh($object);
}
diff --git a/src/app/Discount.php b/src/app/Discount.php
--- a/src/app/Discount.php
+++ b/src/app/Discount.php
@@ -7,6 +7,12 @@
/**
* The eloquent definition of a Discount.
+ *
+ * @property bool $active
+ * @property string $code
+ * @property string $description
+ * @property int $discount
+ * @property int $tenant_id
*/
class Discount extends Model
{
diff --git a/src/app/Domain.php b/src/app/Domain.php
--- a/src/app/Domain.php
+++ b/src/app/Domain.php
@@ -10,6 +10,9 @@
* The eloquent definition of a Domain.
*
* @property string $namespace
+ * @property int $status
+ * @property int $tenant_id
+ * @property int $type
*/
class Domain extends Model
{
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
@@ -12,6 +12,7 @@
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;
@@ -113,6 +114,33 @@
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::withEnvTenant()->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.
*
@@ -190,10 +218,50 @@
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
}
- // Validate verification codes (again)
- $v = $this->verify($request);
- if ($v->status() !== 200) {
- return $v;
+ // Signup via invitation
+ if ($request->invitation) {
+ $invitation = SignupInvitation::withEnvTenant()->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
@@ -219,10 +287,6 @@
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
- // Get user name/email from the verification code database
- $code_data = $v->getData();
- $user_email = $code_data->email;
-
// We allow only ASCII, so we can safely lower-case the email address
$login = Str::lower($login);
$domain_name = Str::lower($domain_name);
@@ -254,14 +318,19 @@
$user->assignPlan($plan, $domain);
// Save the external email and plan in user settings
- $user->setSettings([
- 'external_email' => $user_email,
- 'first_name' => $code_data->first_name,
- 'last_name' => $code_data->last_name,
- ]);
+ $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
- $this->code->delete();
+ if ($this->code) {
+ $this->code->delete();
+ }
DB::commit();
diff --git a/src/app/Http/Controllers/API/V4/Reseller/DiscountsController.php b/src/app/Http/Controllers/API/V4/Reseller/DiscountsController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/API/V4/Reseller/DiscountsController.php
@@ -0,0 +1,45 @@
+user();
+
+ $discounts = $user->tenant->discounts()
+ ->where('active', true)
+ ->orderBy('discount')
+ ->get()
+ ->map(function ($discount) {
+ $label = $discount->discount . '% - ' . $discount->description;
+
+ if ($discount->code) {
+ $label .= " [{$discount->code}]";
+ }
+
+ return [
+ 'id' => $discount->id,
+ 'discount' => $discount->discount,
+ 'code' => $discount->code,
+ 'description' => $discount->description,
+ 'label' => $label,
+ ];
+ });
+
+ return response()->json([
+ 'status' => 'success',
+ 'list' => $discounts,
+ 'count' => count($discounts),
+ ]);
+ }
+}
diff --git a/src/app/Http/Controllers/API/V4/Reseller/DomainsController.php b/src/app/Http/Controllers/API/V4/Reseller/DomainsController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/API/V4/Reseller/DomainsController.php
@@ -0,0 +1,55 @@
+input('search'));
+ $owner = trim(request()->input('owner'));
+ $result = collect([]);
+
+ if ($owner) {
+ if ($owner = User::find($owner)) {
+ foreach ($owner->wallets as $wallet) {
+ $entitlements = $wallet->entitlements()->where('entitleable_type', Domain::class)->get();
+
+ foreach ($entitlements as $entitlement) {
+ $domain = $entitlement->entitleable;
+ $result->push($domain);
+ }
+ }
+
+ $result = $result->sortBy('namespace');
+ }
+ } elseif (!empty($search)) {
+ if ($domain = Domain::where('namespace', $search)->first()) {
+ $result->push($domain);
+ }
+ }
+
+ // Process the result
+ $result = $result->map(function ($domain) {
+ $data = $domain->toArray();
+ $data = array_merge($data, self::domainStatuses($domain));
+ return $data;
+ });
+
+ $result = [
+ 'list' => $result,
+ 'count' => count($result),
+ 'message' => \trans('app.search-foundxdomains', ['x' => count($result)]),
+ ];
+
+ return response()->json($result);
+ }
+}
diff --git a/src/app/Http/Controllers/API/V4/Reseller/EntitlementsController.php b/src/app/Http/Controllers/API/V4/Reseller/EntitlementsController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/API/V4/Reseller/EntitlementsController.php
@@ -0,0 +1,7 @@
+errorResponse(404);
+ }
+
+ /**
+ * Remove the specified invitation.
+ *
+ * @param int $id Invitation identifier
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function destroy($id)
+ {
+ $invitation = SignupInvitation::withUserTenant()->find($id);
+
+ if (empty($invitation)) {
+ return $this->errorResponse(404);
+ }
+
+ $invitation->delete();
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => trans('app.signup-invitation-delete-success'),
+ ]);
+ }
+
+ /**
+ * Show the form for editing the specified resource.
+ *
+ * @param int $id Invitation identifier
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function edit($id)
+ {
+ return $this->errorResponse(404);
+ }
+
+ /**
+ * Display a listing of the resource.
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function index()
+ {
+ $pageSize = 10;
+ $search = request()->input('search');
+ $page = intval(request()->input('page')) ?: 1;
+ $hasMore = false;
+
+ $result = SignupInvitation::withUserTenant()
+ ->latest()
+ ->limit($pageSize + 1)
+ ->offset($pageSize * ($page - 1));
+
+ if ($search) {
+ if (strpos($search, '@')) {
+ $result->where('email', $search);
+ } else {
+ $result->whereLike('email', $search);
+ }
+ }
+
+ $result = $result->get();
+
+ if (count($result) > $pageSize) {
+ $result->pop();
+ $hasMore = true;
+ }
+
+ $result = $result->map(function ($invitation) {
+ return $this->invitationToArray($invitation);
+ });
+
+ return response()->json([
+ 'status' => 'success',
+ 'list' => $result,
+ 'count' => count($result),
+ 'hasMore' => $hasMore,
+ 'page' => $page,
+ ]);
+ }
+
+ /**
+ * Resend the specified invitation.
+ *
+ * @param int $id Invitation identifier
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function resend($id)
+ {
+ $invitation = SignupInvitation::withUserTenant()->find($id);
+
+ if (empty($invitation)) {
+ return $this->errorResponse(404);
+ }
+
+ if ($invitation->isFailed() || $invitation->isSent()) {
+ // Note: The email sending job will be dispatched by the observer
+ $invitation->status = SignupInvitation::STATUS_NEW;
+ $invitation->save();
+ }
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => trans('app.signup-invitation-resend-success'),
+ 'invitation' => $this->invitationToArray($invitation),
+ ]);
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param \Illuminate\Http\Request $request
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function store(Request $request)
+ {
+ $errors = [];
+ $invitations = [];
+
+ if (!empty($request->file) && is_object($request->file)) {
+ // Expected a text/csv file with multiple email addresses
+ if (!$request->file->isValid()) {
+ $errors = ['file' => [$request->file->getErrorMessage()]];
+ } else {
+ $fh = fopen($request->file->getPathname(), 'r');
+ $line_number = 0;
+ $error = null;
+
+ while ($line = fgetcsv($fh)) {
+ $line_number++;
+
+ if (count($line) >= 1 && $line[0]) {
+ $email = trim($line[0]);
+
+ if (strpos($email, '@')) {
+ $v = Validator::make(['email' => $email], ['email' => 'email:filter|required']);
+
+ if ($v->fails()) {
+ $args = ['email' => $email, 'line' => $line_number];
+ $error = trans('app.signup-invitations-csv-invalid-email', $args);
+ break;
+ }
+
+ $invitations[] = ['email' => $email];
+ }
+ }
+ }
+
+ fclose($fh);
+
+ if ($error) {
+ $errors = ['file' => $error];
+ } elseif (empty($invitations)) {
+ $errors = ['file' => trans('app.signup-invitations-csv-empty')];
+ }
+ }
+ } else {
+ // Expected 'email' field with an email address
+ $v = Validator::make($request->all(), ['email' => 'email|required']);
+
+ if ($v->fails()) {
+ $errors = $v->errors()->toArray();
+ } else {
+ $invitations[] = ['email' => $request->email];
+ }
+ }
+
+ if (!empty($errors)) {
+ return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ }
+
+ $count = 0;
+ foreach ($invitations as $idx => $invitation) {
+ SignupInvitation::create($invitation);
+ $count++;
+ }
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => \trans_choice('app.signup-invitations-created', $count, ['count' => $count]),
+ 'count' => $count,
+ ]);
+ }
+
+ /**
+ * Display the specified resource.
+ *
+ * @param int $id Invitation identifier
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function show($id)
+ {
+ return $this->errorResponse(404);
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param \Illuminate\Http\Request $request
+ * @param int $id
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function update(Request $request, $id)
+ {
+ return $this->errorResponse(404);
+ }
+
+ /**
+ * Convert an invitation object to an array for output
+ *
+ * @param \App\SignupInvitation $invitation The signup invitation object
+ *
+ * @return array
+ */
+ protected static function invitationToArray(SignupInvitation $invitation): array
+ {
+ return [
+ 'id' => $invitation->id,
+ 'email' => $invitation->email,
+ 'isNew' => $invitation->isNew(),
+ 'isSent' => $invitation->isSent(),
+ 'isFailed' => $invitation->isFailed(),
+ 'isCompleted' => $invitation->isCompleted(),
+ 'created' => $invitation->created_at->toDateTimeString(),
+ ];
+ }
+}
diff --git a/src/app/Http/Controllers/API/V4/Reseller/PackagesController.php b/src/app/Http/Controllers/API/V4/Reseller/PackagesController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/API/V4/Reseller/PackagesController.php
@@ -0,0 +1,7 @@
+input('search'));
+ $owner = trim(request()->input('owner'));
+ $result = collect([]);
+
+ if ($owner) {
+ $owner = User::where('id', $owner)
+ ->withUserTenant()
+ ->whereNull('role')
+ ->first();
+
+ if ($owner) {
+ $result = $owner->users(false)->whereNull('role')->orderBy('email')->get();
+ }
+ } elseif (strpos($search, '@')) {
+ // Search by email
+ $result = User::withTrashed()->where('email', $search)
+ ->withUserTenant()
+ ->whereNull('role')
+ ->orderBy('email')
+ ->get();
+
+ if ($result->isEmpty()) {
+ // Search by an alias
+ $user_ids = UserAlias::where('alias', $search)->get()->pluck('user_id');
+
+ // Search by an external email
+ $ext_user_ids = UserSetting::where('key', 'external_email')
+ ->where('value', $search)
+ ->get()
+ ->pluck('user_id');
+
+ $user_ids = $user_ids->merge($ext_user_ids)->unique();
+
+ if (!$user_ids->isEmpty()) {
+ $result = User::withTrashed()->whereIn('id', $user_ids)
+ ->withUserTenant()
+ ->whereNull('role')
+ ->orderBy('email')
+ ->get();
+ }
+ }
+ } elseif (is_numeric($search)) {
+ // Search by user ID
+ $user = User::withTrashed()->where('id', $search)
+ ->withUserTenant()
+ ->whereNull('role')
+ ->first();
+
+ if ($user) {
+ $result->push($user);
+ }
+ } elseif (!empty($search)) {
+ // Search by domain
+ $domain = Domain::withTrashed()->where('namespace', $search)
+ ->withUserTenant()
+ ->first();
+
+ if ($domain) {
+ if (
+ ($wallet = $domain->wallet())
+ && ($owner = $wallet->owner()->withTrashed()->withUserTenant()->first())
+ && empty($owner->role)
+ ) {
+ $result->push($owner);
+ }
+ }
+ }
+
+ // Process the result
+ $result = $result->map(function ($user) {
+ $data = $user->toArray();
+ $data = array_merge($data, self::userStatuses($user));
+ return $data;
+ });
+
+ $result = [
+ 'list' => $result,
+ 'count' => count($result),
+ 'message' => \trans('app.search-foundxusers', ['x' => count($result)]),
+ ];
+
+ return response()->json($result);
+ }
+
+ /**
+ * Update user data.
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ * @params string $id User identifier
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ public function update(Request $request, $id)
+ {
+ $user = User::where('id', $id)->withUserTenant()->first();
+
+ if (empty($user) || $user->role == 'admin') {
+ return $this->errorResponse(404);
+ }
+
+ // For now admins can change only user external email address
+
+ $rules = [];
+
+ if (array_key_exists('external_email', $request->input())) {
+ $rules['external_email'] = 'email';
+ }
+
+ // 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));
+
+ if (!empty($settings)) {
+ $user->setSettings($settings);
+ }
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => __('app.user-update-success'),
+ ]);
+ }
+}
diff --git a/src/app/Http/Controllers/API/V4/Reseller/WalletsController.php b/src/app/Http/Controllers/API/V4/Reseller/WalletsController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/API/V4/Reseller/WalletsController.php
@@ -0,0 +1,49 @@
+errorResponse(404);
+ }
+
+ if (array_key_exists('discount', $request->input())) {
+ if (empty($request->discount)) {
+ $wallet->discount()->dissociate();
+ $wallet->save();
+ } elseif ($discount = Discount::find($request->discount)) {
+ $wallet->discount()->associate($discount);
+ $wallet->save();
+ }
+ }
+
+ $response = $wallet->toArray();
+
+ if ($wallet->discount) {
+ $response['discount'] = $wallet->discount->discount;
+ $response['discount_description'] = $wallet->discount->description;
+ }
+
+ $response['status'] = 'success';
+ $response['message'] = \trans('app.wallet-update-success');
+
+ return response()->json($response);
+ }
+}
diff --git a/src/app/Http/Kernel.php b/src/app/Http/Kernel.php
--- a/src/app/Http/Kernel.php
+++ b/src/app/Http/Kernel.php
@@ -63,6 +63,7 @@
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
+ 'reseller' => \App\Http\Middleware\AuthenticateReseller::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
@@ -79,10 +80,10 @@
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\AuthenticateAdmin::class,
+ \App\Http\Middleware\AuthenticateReseller::class,
\App\Http\Middleware\Authenticate::class,
\Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Illuminate\Auth\Middleware\Authorize::class,
- \App\Http\Middleware\AuthenticateAdmin::class,
];
}
diff --git a/src/app/Http/Middleware/AuthenticateReseller.php b/src/app/Http/Middleware/AuthenticateReseller.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Middleware/AuthenticateReseller.php
@@ -0,0 +1,34 @@
+user();
+
+ if (!$user) {
+ abort(403, "Unauthorized");
+ }
+
+ if ($user->role !== "reseller") {
+ abort(403, "Unauthorized");
+ }
+
+ if ($user->tenant_id != \config('app.tenant_id')) {
+ abort(403, "Unauthorized");
+ }
+
+ return $next($request);
+ }
+}
diff --git a/src/app/Jobs/SignupInvitationEmail.php b/src/app/Jobs/SignupInvitationEmail.php
new file mode 100644
--- /dev/null
+++ b/src/app/Jobs/SignupInvitationEmail.php
@@ -0,0 +1,75 @@
+invitation = $invitation;
+ }
+
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ Mail::to($this->invitation->email)->send(new SignupInvitationMail($this->invitation));
+
+ // Update invitation status
+ $this->invitation->status = SignupInvitation::STATUS_SENT;
+ $this->invitation->save();
+ }
+
+ /**
+ * The job failed to process.
+ *
+ * @param \Exception $exception
+ *
+ * @return void
+ */
+ public function failed(\Exception $exception)
+ {
+ if ($this->attempts() >= $this->tries) {
+ // Update invitation status
+ $this->invitation->status = SignupInvitation::STATUS_FAILED;
+ $this->invitation->save();
+ }
+ }
+}
diff --git a/src/app/Mail/SignupInvitation.php b/src/app/Mail/SignupInvitation.php
new file mode 100644
--- /dev/null
+++ b/src/app/Mail/SignupInvitation.php
@@ -0,0 +1,72 @@
+invitation = $invitation;
+ }
+
+ /**
+ * Build the message.
+ *
+ * @return $this
+ */
+ public function build()
+ {
+ $href = Utils::serviceUrl('/signup/invite/' . $this->invitation->id);
+
+ $this->view('emails.html.signup_invitation')
+ ->text('emails.plain.signup_invitation')
+ ->subject(__('mail.signupinvitation-subject', ['site' => \config('app.name')]))
+ ->with([
+ 'site' => \config('app.name'),
+ 'href' => $href,
+ ]);
+
+ return $this;
+ }
+
+ /**
+ * Render the mail template with fake data
+ *
+ * @param string $type Output format ('html' or 'text')
+ *
+ * @return string HTML or Plain Text output
+ */
+ public static function fakeRender(string $type = 'html'): string
+ {
+ $invitation = new SI([
+ 'email' => 'test@external.org',
+ ]);
+
+ $invitation->id = Utils::uuidStr();
+
+ $mail = new self($invitation);
+
+ return Helper::render($mail, $type);
+ }
+}
diff --git a/src/app/Observers/DiscountObserver.php b/src/app/Observers/DiscountObserver.php
--- a/src/app/Observers/DiscountObserver.php
+++ b/src/app/Observers/DiscountObserver.php
@@ -25,5 +25,7 @@
break;
}
}
+
+ $discount->tenant_id = \config('app.tenant_id');
}
}
diff --git a/src/app/Observers/DomainObserver.php b/src/app/Observers/DomainObserver.php
--- a/src/app/Observers/DomainObserver.php
+++ b/src/app/Observers/DomainObserver.php
@@ -27,6 +27,8 @@
$domain->namespace = \strtolower($domain->namespace);
$domain->status |= Domain::STATUS_NEW;
+
+ $domain->tenant_id = \config('app.tenant_id');
}
/**
diff --git a/src/app/Observers/SignupInvitationObserver.php b/src/app/Observers/SignupInvitationObserver.php
new file mode 100644
--- /dev/null
+++ b/src/app/Observers/SignupInvitationObserver.php
@@ -0,0 +1,65 @@
+{$invitation->getKeyName()} = $allegedly_unique;
+ break;
+ }
+ }
+
+ $invitation->status = SI::STATUS_NEW;
+
+ $invitation->tenant_id = \config('app.tenant_id');
+ }
+
+ /**
+ * Handle the invitation "created" event.
+ *
+ * @param \App\SignupInvitation $invitation The invitation object
+ *
+ * @return void
+ */
+ public function created(SI $invitation)
+ {
+ \App\Jobs\SignupInvitationEmail::dispatch($invitation);
+ }
+
+ /**
+ * Handle the invitation "updated" event.
+ *
+ * @param \App\SignupInvitation $invitation The invitation object
+ *
+ * @return void
+ */
+ public function updated(SI $invitation)
+ {
+ $oldStatus = $invitation->getOriginal('status');
+
+ // Resend the invitation
+ if (
+ $invitation->status == SI::STATUS_NEW
+ && ($oldStatus == SI::STATUS_FAILED || $oldStatus == SI::STATUS_SENT)
+ ) {
+ \App\Jobs\SignupInvitationEmail::dispatch($invitation);
+ }
+ }
+}
diff --git a/src/app/Observers/UserObserver.php b/src/app/Observers/UserObserver.php
--- a/src/app/Observers/UserObserver.php
+++ b/src/app/Observers/UserObserver.php
@@ -38,7 +38,7 @@
// only users that are not imported get the benefit of the doubt.
$user->status |= User::STATUS_NEW | User::STATUS_ACTIVE;
- // can't dispatch job here because it'll fail serialization
+ $user->tenant_id = \config('app.tenant_id');
}
/**
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
@@ -2,6 +2,7 @@
namespace App\Providers;
+use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
@@ -35,6 +36,7 @@
\App\PackageSku::observe(\App\Observers\PackageSkuObserver::class);
\App\Plan::observe(\App\Observers\PlanObserver::class);
\App\SignupCode::observe(\App\Observers\SignupCodeObserver::class);
+ \App\SignupInvitation::observe(\App\Observers\SignupInvitationObserver::class);
\App\Sku::observe(\App\Observers\SkuObserver::class);
\App\Transaction::observe(\App\Observers\TransactionObserver::class);
\App\User::observe(\App\Observers\UserObserver::class);
@@ -57,5 +59,50 @@
$path = trim($path, '/\'"');
return "";
});
+
+ // Query builder 'withEnvTenant' macro
+ Builder::macro('withEnvTenant', function (string $table = null) {
+ $tenant_id = \config('app.tenant_id');
+
+ if ($tenant_id) {
+ /** @var Builder $this */
+ return $this->where(($table ? "$table." : '') . 'tenant_id', $tenant_id);
+ }
+
+ /** @var Builder $this */
+ return $this->whereNull(($table ? "$table." : '') . 'tenant_id');
+ });
+
+ // Query builder 'withUserTenant' macro
+ Builder::macro('withUserTenant', function (string $table = null) {
+ $tenant_id = auth()->user()->tenant_id;
+
+ if ($tenant_id) {
+ /** @var Builder $this */
+ return $this->where(($table ? "$table." : '') . 'tenant_id', $tenant_id);
+ }
+
+ /** @var Builder $this */
+ return $this->whereNull(($table ? "$table." : '') . 'tenant_id');
+ });
+
+ // Query builder 'whereLike' mocro
+ Builder::macro('whereLike', function (string $column, string $search, int $mode = 0) {
+ $search = addcslashes($search, '%_');
+
+ switch ($mode) {
+ case 2:
+ $search .= '%';
+ break;
+ case 1:
+ $search = '%' . $search;
+ break;
+ default:
+ $search = '%' . $search . '%';
+ }
+
+ /** @var Builder $this */
+ return $this->where($column, 'like', $search);
+ });
}
}
diff --git a/src/app/SignupInvitation.php b/src/app/SignupInvitation.php
new file mode 100644
--- /dev/null
+++ b/src/app/SignupInvitation.php
@@ -0,0 +1,109 @@
+status & self::STATUS_COMPLETED) > 0;
+ }
+
+ /**
+ * Returns whether this invitation sending failed.
+ *
+ * @return bool
+ */
+ public function isFailed(): bool
+ {
+ return ($this->status & self::STATUS_FAILED) > 0;
+ }
+
+ /**
+ * Returns whether this invitation is new.
+ *
+ * @return bool
+ */
+ public function isNew(): bool
+ {
+ return ($this->status & self::STATUS_NEW) > 0;
+ }
+
+ /**
+ * Returns whether this invitation has been sent.
+ *
+ * @return bool
+ */
+ public function isSent(): bool
+ {
+ return ($this->status & self::STATUS_SENT) > 0;
+ }
+
+ /**
+ * The tenant for this invitation.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
+ public function tenant()
+ {
+ return $this->belongsTo('App\Tenant', 'tenant_id', 'id');
+ }
+
+ /**
+ * The account to which the invitation was used for.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
+ public function user()
+ {
+ return $this->belongsTo('App\User', 'user_id', 'id');
+ }
+}
diff --git a/src/app/Tenant.php b/src/app/Tenant.php
new file mode 100644
--- /dev/null
+++ b/src/app/Tenant.php
@@ -0,0 +1,40 @@
+hasMany('App\Discount');
+ }
+
+ /**
+ * SignupInvitations assigned to this tenant.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ */
+ public function signupInvitations()
+ {
+ return $this->hasMany('App\SignupInvitation');
+ }
+}
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -21,6 +21,7 @@
* @property int $id
* @property string $password
* @property int $status
+ * @property int $tenant_id
*/
class User extends Authenticatable implements JWTSubject
{
@@ -58,7 +59,7 @@
'email',
'password',
'password_ldap',
- 'status'
+ 'status',
];
/**
@@ -221,7 +222,7 @@
*/
public function canRead($object): bool
{
- if ($this->role == "admin") {
+ if ($this->role == 'admin') {
return true;
}
@@ -229,6 +230,18 @@
return true;
}
+ if ($this->role == 'reseller') {
+ if ($object instanceof User && $object->role == 'admin') {
+ return false;
+ }
+
+ if ($object instanceof Wallet && !empty($object->owner)) {
+ $object = $object->owner;
+ }
+
+ return isset($object->tenant_id) && $object->tenant_id == $this->tenant_id;
+ }
+
if ($object instanceof Wallet) {
return $object->user_id == $this->id || $object->controllers->contains($this);
}
@@ -239,7 +252,7 @@
$wallet = $object->wallet();
- return $this->wallets->contains($wallet) || $this->accounts->contains($wallet);
+ return $wallet && ($this->wallets->contains($wallet) || $this->accounts->contains($wallet));
}
/**
@@ -251,10 +264,6 @@
*/
public function canUpdate($object): bool
{
- if (!method_exists($object, 'wallet')) {
- return false;
- }
-
if ($object instanceof User && $this->id == $object->id) {
return true;
}
@@ -588,6 +597,16 @@
$this->save();
}
+ /**
+ * The tenant for this user account.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
+ public function tenant()
+ {
+ return $this->belongsTo('App\Tenant', 'tenant_id', 'id');
+ }
+
/**
* Unsuspend this domain.
*
diff --git a/src/app/Utils.php b/src/app/Utils.php
--- a/src/app/Utils.php
+++ b/src/app/Utils.php
@@ -384,6 +384,8 @@
if ($req_domain == "admin.$sys_domain") {
$env['jsapp'] = 'admin.js';
+ } elseif ($req_domain == "reseller.$sys_domain") {
+ $env['jsapp'] = 'reseller.js';
}
$env['paymentProvider'] = \config('services.payment_provider');
diff --git a/src/config/app.php b/src/config/app.php
--- a/src/config/app.php
+++ b/src/config/app.php
@@ -65,6 +65,8 @@
'theme' => env('APP_THEME', 'default'),
+ 'tenant_id' => env('APP_TENANT_ID', null),
+
/*
|--------------------------------------------------------------------------
| Application Domain
diff --git a/src/database/migrations/2020_05_05_095212_create_tenants_table.php b/src/database/migrations/2020_05_05_095212_create_tenants_table.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2020_05_05_095212_create_tenants_table.php
@@ -0,0 +1,53 @@
+bigIncrements('id');
+ $table->string('title', 32);
+ $table->timestamps();
+ }
+ );
+
+ Schema::table(
+ 'users',
+ function (Blueprint $table) {
+ $table->bigInteger('tenant_id')->unsigned()->nullable();
+
+ $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('set null');
+ }
+ );
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table(
+ 'users',
+ function (Blueprint $table) {
+ $table->dropForeign(['tenant_id']);
+ $table->dropColumn('tenant_id');
+ }
+ );
+
+ Schema::dropIfExists('tenants');
+ }
+}
diff --git a/src/database/migrations/2021_02_19_093832_discounts_add_tenant_id.php b/src/database/migrations/2021_02_19_093832_discounts_add_tenant_id.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2021_02_19_093832_discounts_add_tenant_id.php
@@ -0,0 +1,42 @@
+bigInteger('tenant_id')->unsigned()->default(\config('app.tenant_id'))->nullable();
+
+ $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('set null');
+ }
+ );
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table(
+ 'discounts',
+ function (Blueprint $table) {
+ $table->dropForeign(['tenant_id']);
+ $table->dropColumn('tenant_id');
+ }
+ );
+ }
+}
diff --git a/src/database/migrations/2021_02_19_093833_domains_add_tenant_id.php b/src/database/migrations/2021_02_19_093833_domains_add_tenant_id.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2021_02_19_093833_domains_add_tenant_id.php
@@ -0,0 +1,42 @@
+bigInteger('tenant_id')->unsigned()->default(\config('app.tenant_id'))->nullable();
+
+ $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('set null');
+ }
+ );
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table(
+ 'domains',
+ function (Blueprint $table) {
+ $table->dropForeign(['tenant_id']);
+ $table->dropColumn('tenant_id');
+ }
+ );
+ }
+}
diff --git a/src/database/migrations/2021_03_26_080000_create_signup_invitations_table.php b/src/database/migrations/2021_03_26_080000_create_signup_invitations_table.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2021_03_26_080000_create_signup_invitations_table.php
@@ -0,0 +1,49 @@
+string('id', 36);
+ $table->string('email');
+ $table->smallInteger('status');
+ $table->bigInteger('user_id')->nullable();
+ $table->bigInteger('tenant_id')->unsigned()->nullable();
+ $table->timestamps();
+
+ $table->primary('id');
+
+ $table->index('email');
+ $table->index('created_at');
+
+ $table->foreign('tenant_id')->references('id')->on('tenants')
+ ->onUpdate('cascade')->onDelete('set null');
+ $table->foreign('user_id')->references('id')->on('users')
+ ->onUpdate('cascade')->onDelete('set null');
+ }
+ );
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('signup_invitations');
+ }
+}
diff --git a/src/database/seeds/DatabaseSeeder.php b/src/database/seeds/DatabaseSeeder.php
--- a/src/database/seeds/DatabaseSeeder.php
+++ b/src/database/seeds/DatabaseSeeder.php
@@ -14,6 +14,7 @@
{
// Define seeders order
$seeders = [
+ 'TenantSeeder',
'DiscountSeeder',
'DomainSeeder',
'SkuSeeder',
diff --git a/src/database/seeds/local/DomainSeeder.php b/src/database/seeds/local/DomainSeeder.php
--- a/src/database/seeds/local/DomainSeeder.php
+++ b/src/database/seeds/local/DomainSeeder.php
@@ -63,5 +63,19 @@
]
);
}
+
+ // example tenant domain, note that 'tenant_id' is not a fillable.
+ $domain = Domain::create(
+ [
+ 'namespace' => 'example-tenant.dev-local',
+ 'status' => Domain::STATUS_CONFIRMED + Domain::STATUS_ACTIVE,
+ 'type' => Domain::TYPE_PUBLIC
+ ]
+ );
+
+ $tenant = \App\Tenant::where('title', 'Sample Tenant')->first();
+
+ $domain->tenant_id = $tenant->id;
+ $domain->save();
}
}
diff --git a/src/database/seeds/local/TenantSeeder.php b/src/database/seeds/local/TenantSeeder.php
new file mode 100644
--- /dev/null
+++ b/src/database/seeds/local/TenantSeeder.php
@@ -0,0 +1,29 @@
+ 'Kolab Now'
+ ]
+ );
+
+ Tenant::create(
+ [
+ 'title' => 'Sample Tenant'
+ ]
+ );
+ }
+}
diff --git a/src/database/seeds/local/UserSeeder.php b/src/database/seeds/local/UserSeeder.php
--- a/src/database/seeds/local/UserSeeder.php
+++ b/src/database/seeds/local/UserSeeder.php
@@ -145,5 +145,30 @@
$jeroen->role = 'admin';
$jeroen->save();
+
+ $tenant1 = \App\Tenant::where('title', 'Kolab Now')->first();
+ $tenant2 = \App\Tenant::where('title', 'Sample Tenant')->first();
+
+ $reseller1 = User::create(
+ [
+ 'email' => 'reseller@kolabnow.com',
+ 'password' => 'reseller',
+ ]
+ );
+
+ $reseller1->tenant_id = $tenant1->id;
+ $reseller1->role = 'reseller';
+ $reseller1->save();
+
+ $reseller2 = User::create(
+ [
+ 'email' => 'reseller@reseller.com',
+ 'password' => 'reseller',
+ ]
+ );
+
+ $reseller2->tenant_id = $tenant2->id;
+ $reseller2->role = 'reseller';
+ $reseller2->save();
}
}
diff --git a/src/package-lock.json b/src/package-lock.json
--- a/src/package-lock.json
+++ b/src/package-lock.json
@@ -5136,7 +5136,7 @@
"css": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz",
- "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==",
+ "integrity": "sha1-xkZ1XHOXHyu6amAeLPL9cbEpiSk=",
"dev": true,
"requires": {
"inherits": "^2.0.3",
@@ -5148,7 +5148,7 @@
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=",
"dev": true
}
}
diff --git a/src/phpstan.neon b/src/phpstan.neon
--- a/src/phpstan.neon
+++ b/src/phpstan.neon
@@ -7,8 +7,12 @@
- '#Access to an undefined property Illuminate\\Database\\Eloquent\\Model::\$id#'
- '#Access to an undefined property Illuminate\\Database\\Eloquent\\Model::\$created_at#'
- '#Call to an undefined method Illuminate\\Database\\Eloquent\\Model::toString\(\)#'
+ - '#Call to an undefined [a-zA-Z0-9<>\\ ]+::withEnvTenant\(\)#'
+ - '#Call to an undefined [a-zA-Z0-9<>\\ ]+::withUserTenant\(\)#'
- '#Call to an undefined method Tests\\Browser::#'
level: 4
+ parallel:
+ processTimeout: 300.0
paths:
- app/
- tests/
diff --git a/src/resources/js/admin.js b/src/resources/js/admin/app.js
rename from src/resources/js/admin.js
rename to src/resources/js/admin/app.js
--- a/src/resources/js/admin.js
+++ b/src/resources/js/admin/app.js
@@ -2,9 +2,9 @@
* Application code for the admin UI
*/
-import routes from './routes-admin.js'
+import routes from './routes.js'
window.routes = routes
window.isAdmin = true
-require('./app')
+require('../app')
diff --git a/src/resources/js/routes-admin.js b/src/resources/js/admin/routes.js
rename from src/resources/js/routes-admin.js
rename to src/resources/js/admin/routes.js
--- a/src/resources/js/routes-admin.js
+++ b/src/resources/js/admin/routes.js
@@ -1,10 +1,10 @@
-import DashboardComponent from '../vue/Admin/Dashboard'
-import DomainComponent from '../vue/Admin/Domain'
-import LoginComponent from '../vue/Login'
-import LogoutComponent from '../vue/Logout'
-import PageComponent from '../vue/Page'
-import StatsComponent from '../vue/Admin/Stats'
-import UserComponent from '../vue/Admin/User'
+import DashboardComponent from '../../vue/Admin/Dashboard'
+import DomainComponent from '../../vue/Admin/Domain'
+import LoginComponent from '../../vue/Login'
+import LogoutComponent from '../../vue/Logout'
+import PageComponent from '../../vue/Page'
+import StatsComponent from '../../vue/Admin/Stats'
+import UserComponent from '../../vue/Admin/User'
const routes = [
{
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
@@ -78,7 +78,7 @@
router: window.router,
data() {
return {
- isAdmin: window.isAdmin,
+ isUser: !window.isAdmin && !window.isReseller,
appName: window.config['app.name'],
appUrl: window.config['app.url'],
themeDir: '/themes/' + window.config['app.theme']
@@ -425,7 +425,7 @@
}
if (input.length) {
- // Create an error message\
+ // Create an error message
// API responses can use a string, array or object
let msg_text = ''
if ($.type(msg) !== 'string') {
diff --git a/src/resources/js/reseller/app.js b/src/resources/js/reseller/app.js
new file mode 100644
--- /dev/null
+++ b/src/resources/js/reseller/app.js
@@ -0,0 +1,10 @@
+/**
+ * Application code for the reseller UI
+ */
+
+import routes from './routes.js'
+
+window.routes = routes
+window.isReseller = true
+
+require('../app')
diff --git a/src/resources/js/routes-admin.js b/src/resources/js/reseller/routes.js
rename from src/resources/js/routes-admin.js
rename to src/resources/js/reseller/routes.js
--- a/src/resources/js/routes-admin.js
+++ b/src/resources/js/reseller/routes.js
@@ -1,10 +1,11 @@
-import DashboardComponent from '../vue/Admin/Dashboard'
-import DomainComponent from '../vue/Admin/Domain'
-import LoginComponent from '../vue/Login'
-import LogoutComponent from '../vue/Logout'
-import PageComponent from '../vue/Page'
-import StatsComponent from '../vue/Admin/Stats'
-import UserComponent from '../vue/Admin/User'
+import DashboardComponent from '../../vue/Reseller/Dashboard'
+import DomainComponent from '../../vue/Admin/Domain'
+import InvitationsComponent from '../../vue/Reseller/Invitations'
+import LoginComponent from '../../vue/Login'
+import LogoutComponent from '../../vue/Logout'
+import PageComponent from '../../vue/Page'
+//import StatsComponent from '../../vue/Reseller/Stats'
+import UserComponent from '../../vue/Admin/User'
const routes = [
{
@@ -33,12 +34,20 @@
name: 'logout',
component: LogoutComponent
},
+ {
+ path: '/invitations',
+ name: 'invitations',
+ component: InvitationsComponent,
+ meta: { requiresAuth: true }
+ },
+/*
{
path: '/stats',
name: 'stats',
component: StatsComponent,
meta: { requiresAuth: true }
},
+*/
{
path: '/user/:user',
name: 'user',
diff --git a/src/resources/js/user.js b/src/resources/js/user/app.js
rename from src/resources/js/user.js
rename to src/resources/js/user/app.js
--- a/src/resources/js/user.js
+++ b/src/resources/js/user/app.js
@@ -2,9 +2,10 @@
* Application code for the user UI
*/
-import routes from './routes-user.js'
+import routes from './routes.js'
window.routes = routes
window.isAdmin = false
+window.isReseller = false
-require('./app')
+require('../app')
diff --git a/src/resources/js/routes-user.js b/src/resources/js/user/routes.js
rename from src/resources/js/routes-user.js
rename to src/resources/js/user/routes.js
--- a/src/resources/js/routes-user.js
+++ b/src/resources/js/user/routes.js
@@ -1,22 +1,22 @@
-import DashboardComponent from '../vue/Dashboard'
-import DomainInfoComponent from '../vue/Domain/Info'
-import DomainListComponent from '../vue/Domain/List'
-import LoginComponent from '../vue/Login'
-import LogoutComponent from '../vue/Logout'
-import MeetComponent from '../vue/Rooms'
-import PageComponent from '../vue/Page'
-import PasswordResetComponent from '../vue/PasswordReset'
-import SignupComponent from '../vue/Signup'
-import UserInfoComponent from '../vue/User/Info'
-import UserListComponent from '../vue/User/List'
-import UserProfileComponent from '../vue/User/Profile'
-import UserProfileDeleteComponent from '../vue/User/ProfileDelete'
-import WalletComponent from '../vue/Wallet'
+import DashboardComponent from '../../vue/Dashboard'
+import DomainInfoComponent from '../../vue/Domain/Info'
+import DomainListComponent from '../../vue/Domain/List'
+import LoginComponent from '../../vue/Login'
+import LogoutComponent from '../../vue/Logout'
+import MeetComponent from '../../vue/Rooms'
+import PageComponent from '../../vue/Page'
+import PasswordResetComponent from '../../vue/PasswordReset'
+import SignupComponent from '../../vue/Signup'
+import UserInfoComponent from '../../vue/User/Info'
+import UserListComponent from '../../vue/User/List'
+import UserProfileComponent from '../../vue/User/Profile'
+import UserProfileDeleteComponent from '../../vue/User/ProfileDelete'
+import WalletComponent from '../../vue/Wallet'
// Here's a list of lazy-loaded components
// Note: you can pack multiple components into the same chunk, webpackChunkName
// is also used to get a sensible file name instead of numbers
-const RoomComponent = () => import(/* webpackChunkName: "room" */ '../vue/Meet/Room.vue')
+const RoomComponent = () => import(/* webpackChunkName: "room" */ '../../vue/Meet/Room.vue')
const routes = [
{
@@ -76,6 +76,11 @@
component: MeetComponent,
meta: { requiresAuth: true }
},
+ {
+ path: '/signup/invite/:param',
+ name: 'signup-invite',
+ component: SignupComponent
+ },
{
path: '/signup/:param?',
alias: '/signup/voucher/:param',
diff --git a/src/resources/lang/en/app.php b/src/resources/lang/en/app.php
--- a/src/resources/lang/en/app.php
+++ b/src/resources/lang/en/app.php
@@ -45,6 +45,12 @@
'search-foundxdomains' => ':x domains have been found.',
'search-foundxusers' => ':x user accounts have been found.',
+ 'signup-invitations-created' => 'The invitation has been created.|:count invitations has been created.',
+ 'signup-invitations-csv-empty' => 'Failed to find any valid email addresses in the uploaded file.',
+ 'signup-invitations-csv-invalid-email' => 'Found an invalid email address (:email) on line :line.',
+ 'signup-invitation-delete-success' => 'Invitation deleted successfully.',
+ 'signup-invitation-resend-success' => 'Invitation added to the sending queue successfully.',
+
'support-request-success' => 'Support request submitted successfully.',
'support-request-error' => 'Failed to submit the support request.',
diff --git a/src/resources/lang/en/mail.php b/src/resources/lang/en/mail.php
--- a/src/resources/lang/en/mail.php
+++ b/src/resources/lang/en/mail.php
@@ -75,6 +75,11 @@
'signupcode-body1' => "This is your verification code for the :site registration process:",
'signupcode-body2' => "You can also click the link below to continue the registration process:",
+ 'signupinvitation-subject' => ":site Invitation",
+ 'signupinvitation-header' => "TODO",
+ 'signupinvitation-body1' => "TODO",
+ 'signupinvitation-body2' => "TODO",
+
'suspendeddebtor-subject' => ":site Account Suspended",
'suspendeddebtor-body' => "You have been behind on paying for your :site account "
."for over :days days. Your account has been suspended.",
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
@@ -115,7 +115,10 @@
}
table {
- td.buttons,
+ th {
+ white-space: nowrap;
+ }
+
td.email,
td.price,
td.datetime,
@@ -124,6 +127,7 @@
white-space: nowrap;
}
+ td.buttons,
th.price,
td.price {
width: 1%;
@@ -279,6 +283,13 @@
opacity: 0.6;
}
+ // Some icons are too big, scale them down
+ &.link-invitations {
+ svg {
+ transform: scale(0.9);
+ }
+ }
+
.badge {
position: absolute;
top: 0.5rem;
diff --git a/src/resources/themes/default/theme.json b/src/resources/themes/default/theme.json
--- a/src/resources/themes/default/theme.json
+++ b/src/resources/themes/default/theme.json
@@ -3,23 +3,27 @@
{
"title": "Explore",
"location": "https://kolabnow.com/",
- "admin": true
+ "admin": true,
+ "reseller": true
},
{
"title": "Blog",
"location": "https://blogs.kolabnow.com/",
- "admin": true
+ "admin": true,
+ "reseller": true
},
{
"title": "Support",
"location": "/support",
"page": "support",
- "admin": true
+ "admin": true,
+ "reseller": true
},
{
"title": "ToS",
"location": "https://kolabnow.com/tos",
- "footer": true
+ "footer": true,
+ "reseller": true
}
],
"faq": {
diff --git a/src/resources/views/emails/html/signup_invitation.blade.php b/src/resources/views/emails/html/signup_invitation.blade.php
new file mode 100644
--- /dev/null
+++ b/src/resources/views/emails/html/signup_invitation.blade.php
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ {{ __('mail.signupinvitation-header') }}
+
+ {{ __('mail.signupinvitation-body1', ['site' => $site]) }}
+
+ {!! $href !!}
+
+ {{ __('mail.signupinvitation-body2') }}
+
+ {{ __('mail.footer1') }}
+ {{ __('mail.footer2', ['site' => $site]) }}
+
+
diff --git a/src/resources/views/emails/plain/signup_invitation.blade.php b/src/resources/views/emails/plain/signup_invitation.blade.php
new file mode 100644
--- /dev/null
+++ b/src/resources/views/emails/plain/signup_invitation.blade.php
@@ -0,0 +1,11 @@
+{!! __('mail.signupinvitation-header') !!}
+
+{!! __('mail.signupinvitation-body1', ['site' => $site]) !!}
+
+{!! $href !!}
+
+{!! __('mail.signupinvitation-body2') !!}
+
+--
+{!! __('mail.footer1') !!}
+{!! __('mail.footer2', ['site' => $site]) !!}
diff --git a/src/resources/vue/Admin/Dashboard.vue b/src/resources/vue/Admin/Dashboard.vue
--- a/src/resources/vue/Admin/Dashboard.vue
+++ b/src/resources/vue/Admin/Dashboard.vue
@@ -1,42 +1,6 @@
-
-
-
-
-
-
- Primary Email |
- ID |
- Created |
- Deleted |
-
-
-
-
-
-
- {{ user.email }}
- {{ user.email }}
- |
-
- {{ user.id }}
- {{ user.id }}
- |
- {{ toDate(user.created_at) }} |
- {{ toDate(user.deleted_at) }} |
-
-
-
-
-
+
Stats
@@ -46,44 +10,15 @@
diff --git a/src/resources/vue/Login.vue b/src/resources/vue/Login.vue
--- a/src/resources/vue/Login.vue
+++ b/src/resources/vue/Login.vue
@@ -23,7 +23,7 @@
-
diff --git a/src/resources/vue/Reseller/Dashboard.vue b/src/resources/vue/Reseller/Dashboard.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/Reseller/Dashboard.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
+ Invitations
+
+
+
+
+
+
diff --git a/src/resources/vue/Reseller/Invitations.vue b/src/resources/vue/Reseller/Invitations.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/Reseller/Invitations.vue
@@ -0,0 +1,283 @@
+
+
+
+
+
+ Signup Invitations
+
+
+
+
+
+
+
+
+
+
+
+
+ External Email |
+ Created |
+ |
+
+
+
+
+
+
+ {{ inv.email }}
+ |
+
+ {{ inv.created }}
+ |
+
+
+
+ |
+
+
+
+
+ There are no invitations in the database. |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/resources/vue/Signup.vue b/src/resources/vue/Signup.vue
--- a/src/resources/vue/Signup.vue
+++ b/src/resources/vue/Signup.vue
@@ -1,6 +1,6 @@
-
+
-
+
Sign Up - Step 1/3
@@ -39,7 +39,7 @@
-
+
Sign Up - Step 2/3
@@ -60,20 +60,28 @@
-
Sign Up - Step 3/3
+
Sign Up - Step 3/3
Create your Kolab identity (you can choose additional addresses later).
@@ -109,8 +119,10 @@
password: '',
password_confirmation: '',
domain: '',
- plan: null,
+ domains: [],
+ invitation: null,
is_domain: false,
+ plan: null,
plan_icons: {
individual: 'user',
group: 'users'
@@ -122,7 +134,25 @@
mounted() {
let param = this.$route.params.param;
- if (param) {
+ if (this.$route.name == 'signup-invite') {
+ this.$root.startLoading()
+ axios.get('/api/auth/signup/invitations/' + param)
+ .then(response => {
+ this.invitation = response.data
+ this.login = response.data.login
+ this.voucher = response.data.voucher
+ this.first_name = response.data.first_name
+ this.last_name = response.data.last_name
+ this.plan = response.data.plan
+ this.is_domain = response.data.is_domain
+ this.setDomain(response.data)
+ this.$root.stopLoading()
+ this.displayForm(3, true)
+ })
+ .catch(error => {
+ this.$root.errorHandler(error)
+ })
+ } else if (param) {
if (this.$route.path.indexOf('/signup/voucher/') === 0) {
// Voucher (discount) code
this.voucher = param
@@ -199,13 +229,7 @@
// Fill the domain selector with available domains
if (!this.is_domain) {
- let options = []
- $('select#signup_domain').html('')
- $.each(response.data.domains, (i, v) => {
- options.push($('