diff --git a/bin/phpstan b/bin/phpstan
--- a/bin/phpstan
+++ b/bin/phpstan
@@ -4,7 +4,7 @@
pushd ${cwd}/../src/
-php -dmemory_limit=400M \
+php -dmemory_limit=500M \
vendor/bin/phpstan \
analyse
diff --git a/docker-compose.yml b/docker-compose.yml
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -59,6 +59,7 @@
container_name: kolab-mariadb
environment:
MYSQL_ROOT_PASSWORD: Welcome2KolabSystems
+ TZ: "+02:00"
healthcheck:
interval: 10s
test: test -e /var/run/mysqld/mysqld.sock
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
APP_LOCALE=en
APP_LOCALES=en,de
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,18 +2,37 @@
namespace App\Console;
-class Command extends \Illuminate\Console\Command
+use Illuminate\Support\Facades\DB;
+
+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.
*
- * @param string $domain Domain ID or namespace
+ * @param string $domain Domain ID or namespace
+ * @param bool $withDeleted Include deleted
*
* @return \App\Domain|null
*/
- public function getDomain($domain)
+ public function getDomain($domain, $withDeleted = false)
{
- return $this->getObject(\App\Domain::class, $domain, 'namespace');
+ return $this->getObject(\App\Domain::class, $domain, 'namespace', $withDeleted);
}
/**
@@ -22,38 +41,86 @@
* @param string $objectClass The name of the class
* @param string $objectIdOrTitle The name of a database field to match.
* @param string|null $objectTitle An additional database field to match.
+ * @param bool $withDeleted Act as if --with-deleted was used
*
* @return mixed
*/
- public function getObject($objectClass, $objectIdOrTitle, $objectTitle)
+ public function getObject($objectClass, $objectIdOrTitle, $objectTitle = null, $withDeleted = false)
{
- if ($this->hasOption('with-deleted') && $this->option('with-deleted')) {
- $object = $objectClass::withTrashed()->find($objectIdOrTitle);
- } else {
- $object = $objectClass::find($objectIdOrTitle);
+ if (!$withDeleted) {
+ $withDeleted = $this->hasOption('with-deleted') && $this->option('with-deleted');
}
+ $object = $this->getObjectModel($objectClass, $withDeleted)->find($objectIdOrTitle);
+
if (!$object && !empty($objectTitle)) {
- if ($this->hasOption('with-deleted') && $this->option('with-deleted')) {
- $object = $objectClass::withTrashed()->where($objectTitle, $objectIdOrTitle)->first();
- } else {
- $object = $objectClass::where($objectTitle, $objectIdOrTitle)->first();
- }
+ $object = $this->getObjectModel($objectClass, $withDeleted)
+ ->where($objectTitle, $objectIdOrTitle)->first();
}
return $object;
}
+ /**
+ * Returns a preconfigured Model object for a specified class.
+ *
+ * @param string $objectClass The name of the class
+ * @param bool $withDeleted Include withTrashed() query
+ *
+ * @return mixed
+ */
+ protected function getObjectModel($objectClass, $withDeleted = false)
+ {
+ if ($withDeleted) {
+ $model = $objectClass::withTrashed();
+ } else {
+ $model = new $objectClass();
+ }
+
+ $modelsWithTenant = [
+ \App\Discount::class,
+ \App\Domain::class,
+ \App\Group::class,
+ \App\Package::class,
+ \App\Plan::class,
+ \App\Sku::class,
+ \App\User::class,
+ ];
+
+ $modelsWithOwner = [
+ \App\Wallet::class,
+ ];
+
+ $tenant_id = \config('app.tenant_id');
+
+ // Add tenant filter
+ if (in_array($objectClass, $modelsWithTenant)) {
+ $model = $model->withEnvTenant();
+ } elseif (in_array($objectClass, $modelsWithOwner)) {
+ $model = $model->whereExists(function ($query) use ($tenant_id) {
+ $query->select(DB::raw(1))
+ ->from('users')
+ ->whereRaw('wallets.user_id = users.id')
+ ->whereRaw('users.tenant_id ' . ($tenant_id ? "= $tenant_id" : 'is null'));
+ });
+ }
+
+ // TODO: tenant check for Entitlement, Transaction, etc.
+
+ return $model;
+ }
+
/**
* Find the user.
*
- * @param string $user User ID or email
+ * @param string $user User ID or email
+ * @param bool $withDeleted Include deleted
*
* @return \App\User|null
*/
- public function getUser($user)
+ public function getUser($user, $withDeleted = false)
{
- return $this->getObject(\App\User::class, $user, 'email');
+ return $this->getObject(\App\User::class, $user, 'email', $withDeleted);
}
/**
@@ -68,6 +135,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/DBPing.php b/src/app/Console/Commands/DB/PingCommand.php
rename from src/app/Console/Commands/DBPing.php
rename to src/app/Console/Commands/DB/PingCommand.php
--- a/src/app/Console/Commands/DBPing.php
+++ b/src/app/Console/Commands/DB/PingCommand.php
@@ -1,11 +1,11 @@
error("The application timezone is not configured to be UTC");
+ return 1;
+ }
+
+ if ($result[0]->{'Value'} != '+00:00' && $result[0]->{'Value'} != 'UTC') {
+ $this->error("The database timezone is neither configured as '+00:00' nor 'UTC'");
+ return 1;
+ }
+
+ return 0;
+ }
+}
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,85 @@
+ 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';
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $source = $this->getObject(\App\Discount::class, $this->argument('source'));
+
+ if (!$source) {
+ $this->error("No such source discount: {$source}");
+ return 1;
+ }
+
+ $target = $this->getObject(\App\Discount::class, $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/DomainDelete.php b/src/app/Console/Commands/DomainDelete.php
--- a/src/app/Console/Commands/DomainDelete.php
+++ b/src/app/Console/Commands/DomainDelete.php
@@ -2,7 +2,7 @@
namespace App\Console\Commands;
-use Illuminate\Console\Command;
+use App\Console\Command;
class DomainDelete extends Command
{
@@ -20,16 +20,6 @@
*/
protected $description = 'Delete a domain';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -37,7 +27,7 @@
*/
public function handle()
{
- $domain = \App\Domain::where('id', $this->argument('domain'))->first();
+ $domain = $this->getDomain($this->argument('domain'));
if (!$domain) {
return 1;
diff --git a/src/app/Console/Commands/DomainList.php b/src/app/Console/Commands/DomainList.php
--- a/src/app/Console/Commands/DomainList.php
+++ b/src/app/Console/Commands/DomainList.php
@@ -2,8 +2,8 @@
namespace App\Console\Commands;
+use App\Console\Command;
use App\Domain;
-use Illuminate\Console\Command;
class DomainList extends Command
{
@@ -21,16 +21,6 @@
*/
protected $description = 'List domains';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -44,7 +34,7 @@
$domains = Domain::orderBy('namespace');
}
- $domains->each(
+ $domains->withEnvTenant()->each(
function ($domain) {
$msg = $domain->namespace;
diff --git a/src/app/Console/Commands/DomainListUsers.php b/src/app/Console/Commands/DomainListUsers.php
--- a/src/app/Console/Commands/DomainListUsers.php
+++ b/src/app/Console/Commands/DomainListUsers.php
@@ -2,7 +2,7 @@
namespace App\Console\Commands;
-use Illuminate\Console\Command;
+use App\Console\Command;
class DomainListUsers extends Command
{
@@ -37,7 +37,7 @@
*/
public function handle()
{
- $domain = \App\Domain::where('namespace', $this->argument('domain'))->first();
+ $domain = $this->getDomain($this->argument('domain'));
if (!$domain) {
return 1;
diff --git a/src/app/Console/Commands/DomainRestore.php b/src/app/Console/Commands/DomainRestore.php
--- a/src/app/Console/Commands/DomainRestore.php
+++ b/src/app/Console/Commands/DomainRestore.php
@@ -2,7 +2,7 @@
namespace App\Console\Commands;
-use Illuminate\Console\Command;
+use App\Console\Command;
use Illuminate\Support\Facades\DB;
class DomainRestore extends Command
@@ -28,7 +28,7 @@
*/
public function handle()
{
- $domain = \App\Domain::withTrashed()->where('namespace', $this->argument('domain'))->first();
+ $domain = $this->getDomain($this->argument('domain'), true);
if (!$domain) {
$this->error("Domain not found.");
diff --git a/src/app/Console/Commands/DomainSetStatus.php b/src/app/Console/Commands/DomainSetStatus.php
--- a/src/app/Console/Commands/DomainSetStatus.php
+++ b/src/app/Console/Commands/DomainSetStatus.php
@@ -2,8 +2,8 @@
namespace App\Console\Commands;
+use App\Console\Command;
use App\Domain;
-use Illuminate\Console\Command;
use Illuminate\Support\Facades\Queue;
class DomainSetStatus extends Command
@@ -29,7 +29,7 @@
*/
public function handle()
{
- $domain = Domain::where('namespace', $this->argument('domain'))->first();
+ $domain = $this->getDomain($this->argument('domain'));
if (!$domain) {
return 1;
diff --git a/src/app/Console/Commands/DomainSetWallet.php b/src/app/Console/Commands/DomainSetWallet.php
--- a/src/app/Console/Commands/DomainSetWallet.php
+++ b/src/app/Console/Commands/DomainSetWallet.php
@@ -2,11 +2,11 @@
namespace App\Console\Commands;
+use App\Console\Command;
use App\Entitlement;
use App\Domain;
use App\Sku;
use App\Wallet;
-use Illuminate\Console\Command;
use Illuminate\Support\Facades\Queue;
class DomainSetWallet extends Command
@@ -32,14 +32,14 @@
*/
public function handle()
{
- $domain = Domain::where('namespace', $this->argument('domain'))->first();
+ $domain = $this->getDomain($this->argument('domain'));
if (!$domain) {
$this->error("Domain not found.");
return 1;
}
- $wallet = Wallet::find($this->argument('wallet'));
+ $wallet = $this->getWallet($this->argument('wallet'));
if (!$wallet) {
$this->error("Wallet not found.");
@@ -60,6 +60,7 @@
'wallet_id' => $wallet->id,
'sku_id' => $sku->id,
'cost' => 0,
+ 'fee' => 0,
'entitleable_id' => $domain->id,
'entitleable_type' => Domain::class,
]
diff --git a/src/app/Console/Commands/DomainStatus.php b/src/app/Console/Commands/DomainStatus.php
--- a/src/app/Console/Commands/DomainStatus.php
+++ b/src/app/Console/Commands/DomainStatus.php
@@ -2,8 +2,8 @@
namespace App\Console\Commands;
+use App\Console\Command;
use App\Domain;
-use Illuminate\Console\Command;
class DomainStatus extends Command
{
@@ -21,16 +21,6 @@
*/
protected $description = 'Display the status of a domain';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -38,7 +28,7 @@
*/
public function handle()
{
- $domain = Domain::where('namespace', $this->argument('domain'))->first();
+ $domain = $this->getDomain($this->argument('domain'));
if (!$domain) {
return 1;
diff --git a/src/app/Console/Commands/DomainSuspend.php b/src/app/Console/Commands/DomainSuspend.php
--- a/src/app/Console/Commands/DomainSuspend.php
+++ b/src/app/Console/Commands/DomainSuspend.php
@@ -2,8 +2,8 @@
namespace App\Console\Commands;
+use App\Console\Command;
use App\Domain;
-use Illuminate\Console\Command;
class DomainSuspend extends Command
{
@@ -21,16 +21,6 @@
*/
protected $description = 'Suspend a domain';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -38,7 +28,7 @@
*/
public function handle()
{
- $domain = Domain::where('namespace', $this->argument('domain'))->first();
+ $domain = $this->getDomain($this->argument('domain'));
if (!$domain) {
return 1;
diff --git a/src/app/Console/Commands/DomainUnsuspend.php b/src/app/Console/Commands/DomainUnsuspend.php
--- a/src/app/Console/Commands/DomainUnsuspend.php
+++ b/src/app/Console/Commands/DomainUnsuspend.php
@@ -2,8 +2,8 @@
namespace App\Console\Commands;
+use App\Console\Command;
use App\Domain;
-use Illuminate\Console\Command;
class DomainUnsuspend extends Command
{
@@ -21,16 +21,6 @@
*/
protected $description = 'Remove a domain suspension';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -38,7 +28,7 @@
*/
public function handle()
{
- $domain = Domain::where('namespace', $this->argument('domain'))->first();
+ $domain = $this->getDomain($this->argument('domain'));
if (!$domain) {
return 1;
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
@@ -7,6 +7,11 @@
use App\Http\Controllers\API\V4\GroupsController;
use Illuminate\Support\Facades\DB;
+/**
+ * Create a (mail-enabled) distribution group.
+ *
+ * @see \App\Console\Commands\Scalpel\Group\CreateCommand
+ */
class CreateCommand extends Command
{
/**
diff --git a/src/app/Console/Commands/Job/DomainCreate.php b/src/app/Console/Commands/Job/DomainCreate.php
--- a/src/app/Console/Commands/Job/DomainCreate.php
+++ b/src/app/Console/Commands/Job/DomainCreate.php
@@ -2,8 +2,8 @@
namespace App\Console\Commands\Job;
+use App\Console\Command;
use App\Domain;
-use Illuminate\Console\Command;
class DomainCreate extends Command
{
@@ -28,7 +28,7 @@
*/
public function handle()
{
- $domain = Domain::where('namespace', $this->argument('domain'))->first();
+ $domain = $this->getDomain($this->argument('domain'));
if (!$domain) {
return 1;
diff --git a/src/app/Console/Commands/Job/DomainUpdate.php b/src/app/Console/Commands/Job/DomainUpdate.php
--- a/src/app/Console/Commands/Job/DomainUpdate.php
+++ b/src/app/Console/Commands/Job/DomainUpdate.php
@@ -2,8 +2,8 @@
namespace App\Console\Commands\Job;
+use App\Console\Command;
use App\Domain;
-use Illuminate\Console\Command;
class DomainUpdate extends Command
{
@@ -28,7 +28,7 @@
*/
public function handle()
{
- $domain = Domain::where('namespace', $this->argument('domain'))->first();
+ $domain = $this->getDomain($this->argument('domain'));
if (!$domain) {
return 1;
diff --git a/src/app/Console/Commands/Job/UserCreate.php b/src/app/Console/Commands/Job/UserCreate.php
--- a/src/app/Console/Commands/Job/UserCreate.php
+++ b/src/app/Console/Commands/Job/UserCreate.php
@@ -2,8 +2,8 @@
namespace App\Console\Commands\Job;
+use App\Console\Command;
use App\User;
-use Illuminate\Console\Command;
class UserCreate extends Command
{
@@ -28,7 +28,7 @@
*/
public function handle()
{
- $user = User::where('email', $this->argument('user'))->first();
+ $user = $this->getUser($this->argument('user'));
if (!$user) {
return 1;
diff --git a/src/app/Console/Commands/Job/UserUpdate.php b/src/app/Console/Commands/Job/UserUpdate.php
--- a/src/app/Console/Commands/Job/UserUpdate.php
+++ b/src/app/Console/Commands/Job/UserUpdate.php
@@ -2,8 +2,8 @@
namespace App\Console\Commands\Job;
+use App\Console\Command;
use App\User;
-use Illuminate\Console\Command;
class UserUpdate extends Command
{
@@ -28,7 +28,7 @@
*/
public function handle()
{
- $user = User::where('email', $this->argument('user'))->first();
+ $user = $this->getUser($this->argument('user'));
if (!$user) {
return 1;
diff --git a/src/app/Console/Commands/Job/WalletCheck.php b/src/app/Console/Commands/Job/WalletCheck.php
--- a/src/app/Console/Commands/Job/WalletCheck.php
+++ b/src/app/Console/Commands/Job/WalletCheck.php
@@ -2,8 +2,8 @@
namespace App\Console\Commands\Job;
+use App\Console\Command;
use App\Wallet;
-use Illuminate\Console\Command;
class WalletCheck extends Command
{
@@ -28,7 +28,7 @@
*/
public function handle()
{
- $wallet = Wallet::find($this->argument('wallet'));
+ $wallet = $this->getWallet($this->argument('wallet'));
if (!$wallet) {
return 1;
diff --git a/src/app/Console/Commands/MollieInfo.php b/src/app/Console/Commands/MollieInfo.php
--- a/src/app/Console/Commands/MollieInfo.php
+++ b/src/app/Console/Commands/MollieInfo.php
@@ -2,8 +2,8 @@
namespace App\Console\Commands;
+use App\Console\Command;
use App\User;
-use Illuminate\Console\Command;
class MollieInfo extends Command
{
@@ -29,7 +29,7 @@
public function handle()
{
if ($this->argument('user')) {
- $user = User::where('email', $this->argument('user'))->first();
+ $user = $this->getUser($this->argument('user'));
if (!$user) {
return 1;
diff --git a/src/app/Console/Commands/OpenVidu/RoomCreate.php b/src/app/Console/Commands/OpenVidu/RoomCreate.php
--- a/src/app/Console/Commands/OpenVidu/RoomCreate.php
+++ b/src/app/Console/Commands/OpenVidu/RoomCreate.php
@@ -2,7 +2,7 @@
namespace App\Console\Commands\OpenVidu;
-use Illuminate\Console\Command;
+use App\Console\Command;
class RoomCreate extends Command
{
@@ -20,16 +20,6 @@
*/
protected $description = 'Create a room for a user';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -37,7 +27,7 @@
*/
public function handle()
{
- $user = \App\User::where('email', $this->argument('user'))->first();
+ $user = $this->getUser($this->argument('user'));
if (!$user) {
return 1;
diff --git a/src/app/Console/Commands/OpenVidu/Rooms.php b/src/app/Console/Commands/OpenVidu/Rooms.php
--- a/src/app/Console/Commands/OpenVidu/Rooms.php
+++ b/src/app/Console/Commands/OpenVidu/Rooms.php
@@ -20,16 +20,6 @@
*/
protected $description = 'List OpenVidu rooms';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
diff --git a/src/app/Console/Commands/OpenVidu/Sessions.php b/src/app/Console/Commands/OpenVidu/Sessions.php
--- a/src/app/Console/Commands/OpenVidu/Sessions.php
+++ b/src/app/Console/Commands/OpenVidu/Sessions.php
@@ -20,16 +20,6 @@
*/
protected $description = 'List OpenVidu sessions';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
diff --git a/src/app/Console/Commands/PackageSkus.php b/src/app/Console/Commands/PackageSkus.php
--- a/src/app/Console/Commands/PackageSkus.php
+++ b/src/app/Console/Commands/PackageSkus.php
@@ -2,8 +2,8 @@
namespace App\Console\Commands;
+use App\Console\Command;
use App\Package;
-use Illuminate\Console\Command;
class PackageSkus extends Command
{
@@ -21,16 +21,6 @@
*/
protected $description = "List SKUs for packages.";
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -38,7 +28,7 @@
*/
public function handle()
{
- $packages = Package::all();
+ $packages = Package::withEnvTenant()->get();
foreach ($packages as $package) {
$this->info(sprintf("Package: %s", $package->title));
diff --git a/src/app/Console/Commands/PlanPackages.php b/src/app/Console/Commands/PlanPackages.php
--- a/src/app/Console/Commands/PlanPackages.php
+++ b/src/app/Console/Commands/PlanPackages.php
@@ -38,7 +38,7 @@
*/
public function handle()
{
- $plans = Plan::all();
+ $plans = Plan::withEnvTenant()->get();
foreach ($plans as $plan) {
$this->info(sprintf("Plan: %s", $plan->title));
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 @@
+argument('sku'));
-
- if (!$sku) {
- $sku = \App\Sku::where('title', $this->argument('sku'))->first();
- }
+ $sku = $this->getObject(\App\Sku::class, $this->argument('sku'), 'title');
if (!$sku) {
$this->error("Unable to find the SKU.");
diff --git a/src/app/Console/Commands/StripeInfo.php b/src/app/Console/Commands/StripeInfo.php
--- a/src/app/Console/Commands/StripeInfo.php
+++ b/src/app/Console/Commands/StripeInfo.php
@@ -2,9 +2,9 @@
namespace App\Console\Commands;
+use App\Console\Command;
use App\Providers\PaymentProvider;
use App\User;
-use Illuminate\Console\Command;
use Stripe as StripeAPI;
class StripeInfo extends Command
@@ -31,7 +31,7 @@
public function handle()
{
if ($this->argument('user')) {
- $user = User::where('email', $this->argument('user'))->first();
+ $user = $this->getUser($this->argument('user'));
if (!$user) {
return 1;
diff --git a/src/app/Console/Commands/UserAddAlias.php b/src/app/Console/Commands/UserAddAlias.php
--- a/src/app/Console/Commands/UserAddAlias.php
+++ b/src/app/Console/Commands/UserAddAlias.php
@@ -2,8 +2,8 @@
namespace App\Console\Commands;
+use App\Console\Command;
use App\Http\Controllers\API\V4\UsersController;
-use Illuminate\Console\Command;
class UserAddAlias extends Command
{
@@ -21,16 +21,6 @@
*/
protected $description = 'Add an email alias to a user (forcefully)';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -38,7 +28,7 @@
*/
public function handle()
{
- $user = \App\User::where('email', $this->argument('user'))->first();
+ $user = $this->getUser($this->argument('user'));
if (!$user) {
return 1;
diff --git a/src/app/Console/Commands/UserAssignSku.php b/src/app/Console/Commands/UserAssignSku.php
--- a/src/app/Console/Commands/UserAssignSku.php
+++ b/src/app/Console/Commands/UserAssignSku.php
@@ -2,7 +2,7 @@
namespace App\Console\Commands;
-use Illuminate\Console\Command;
+use App\Console\Command;
class UserAssignSku extends Command
{
@@ -27,18 +27,14 @@
*/
public function handle()
{
- $user = \App\User::where('email', $this->argument('user'))->first();
+ $user = $this->getUser($this->argument('user'));
if (!$user) {
$this->error("Unable to find the user {$this->argument('user')}.");
return 1;
}
- $sku = \App\Sku::find($this->argument('sku'));
-
- if (!$sku) {
- $sku = \App\Sku::where('title', $this->argument('sku'))->first();
- }
+ $sku = $this->getObject(\App\Sku::class, $this->argument('sku'), 'title');
if (!$sku) {
$this->error("Unable to find the SKU {$this->argument('sku')}.");
diff --git a/src/app/Console/Commands/UserDelete.php b/src/app/Console/Commands/UserDelete.php
--- a/src/app/Console/Commands/UserDelete.php
+++ b/src/app/Console/Commands/UserDelete.php
@@ -2,7 +2,7 @@
namespace App\Console\Commands;
-use Illuminate\Console\Command;
+use App\Console\Command;
class UserDelete extends Command
{
@@ -20,16 +20,6 @@
*/
protected $description = 'Delete a user';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -37,7 +27,7 @@
*/
public function handle()
{
- $user = \App\User::where('email', $this->argument('user'))->first();
+ $user = $this->getUser($this->argument('user'));
if (!$user) {
return 1;
diff --git a/src/app/Console/Commands/UserDiscount.php b/src/app/Console/Commands/UserDiscount.php
--- a/src/app/Console/Commands/UserDiscount.php
+++ b/src/app/Console/Commands/UserDiscount.php
@@ -2,7 +2,7 @@
namespace App\Console\Commands;
-use Illuminate\Console\Command;
+use App\Console\Command;
class UserDiscount extends Command
{
@@ -20,16 +20,6 @@
*/
protected $description = "Apply a discount to all of the user's wallets";
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -37,7 +27,7 @@
*/
public function handle()
{
- $user = \App\User::where('email', $this->argument('user'))->first();
+ $user = $this->getUser($this->argument('user'));
if (!$user) {
return 1;
@@ -48,7 +38,7 @@
if ($this->argument('discount') === '0') {
$discount = null;
} else {
- $discount = \App\Discount::find($this->argument('discount'));
+ $discount = $this->getObject(\App\Discount::class, $this->argument('discount'));
if (!$discount) {
return 1;
diff --git a/src/app/Console/Commands/UserDomains.php b/src/app/Console/Commands/UserDomains.php
--- a/src/app/Console/Commands/UserDomains.php
+++ b/src/app/Console/Commands/UserDomains.php
@@ -2,10 +2,7 @@
namespace App\Console\Commands;
-use App\Domain;
-use App\User;
-use Illuminate\Console\Command;
-use Illuminate\Support\Facades\DB;
+use App\Console\Command;
class UserDomains extends Command
{
@@ -23,16 +20,6 @@
*/
protected $description = 'Command description';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -40,7 +27,7 @@
*/
public function handle()
{
- $user = User::where('email', $this->argument('userid'))->first();
+ $user = $this->getUser($this->argument('userid'));
if (!$user) {
return 1;
diff --git a/src/app/Console/Commands/UserEntitlements.php b/src/app/Console/Commands/UserEntitlements.php
--- a/src/app/Console/Commands/UserEntitlements.php
+++ b/src/app/Console/Commands/UserEntitlements.php
@@ -2,9 +2,7 @@
namespace App\Console\Commands;
-use App\Sku;
-use App\User;
-use Illuminate\Console\Command;
+use App\Console\Command;
class UserEntitlements extends Command
{
@@ -22,16 +20,6 @@
*/
protected $description = "List a user's entitlements.";
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -39,7 +27,7 @@
*/
public function handle()
{
- $user = User::where('email', $this->argument('userid'))->first();
+ $user = $this->getUser($this->argument('userid'));
if (!$user) {
return 1;
@@ -58,7 +46,7 @@
}
foreach ($skus_counted as $id => $qty) {
- $sku = Sku::find($id);
+ $sku = \App\Sku::find($id);
$this->info("SKU: {$sku->title} ({$qty})");
}
}
diff --git a/src/app/Console/Commands/UserForceDelete.php b/src/app/Console/Commands/UserForceDelete.php
--- a/src/app/Console/Commands/UserForceDelete.php
+++ b/src/app/Console/Commands/UserForceDelete.php
@@ -2,7 +2,7 @@
namespace App\Console\Commands;
-use Illuminate\Console\Command;
+use App\Console\Command;
use Illuminate\Support\Facades\DB;
class UserForceDelete extends Command
@@ -28,7 +28,7 @@
*/
public function handle()
{
- $user = \App\User::withTrashed()->where('email', $this->argument('user'))->first();
+ $user = $this->getUser($this->argument('user'), true);
if (!$user) {
return 1;
diff --git a/src/app/Console/Commands/UserRestore.php b/src/app/Console/Commands/UserRestore.php
--- a/src/app/Console/Commands/UserRestore.php
+++ b/src/app/Console/Commands/UserRestore.php
@@ -2,7 +2,7 @@
namespace App\Console\Commands;
-use Illuminate\Console\Command;
+use App\Console\Command;
use Illuminate\Support\Facades\DB;
class UserRestore extends Command
@@ -28,7 +28,7 @@
*/
public function handle()
{
- $user = \App\User::withTrashed()->where('email', $this->argument('user'))->first();
+ $user = $this->getUser($this->argument('user'), true);
if (!$user) {
$this->error('User not found.');
diff --git a/src/app/Console/Commands/UserStatus.php b/src/app/Console/Commands/UserStatus.php
--- a/src/app/Console/Commands/UserStatus.php
+++ b/src/app/Console/Commands/UserStatus.php
@@ -2,8 +2,8 @@
namespace App\Console\Commands;
+use App\Console\Command;
use App\User;
-use Illuminate\Console\Command;
class UserStatus extends Command
{
@@ -21,16 +21,6 @@
*/
protected $description = 'Display the status of a user';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -38,7 +28,7 @@
*/
public function handle()
{
- $user = User::where('email', $this->argument('user'))->first();
+ $user = $this->getUser($this->argument('user'));
if (!$user) {
return 1;
diff --git a/src/app/Console/Commands/UserSuspend.php b/src/app/Console/Commands/UserSuspend.php
--- a/src/app/Console/Commands/UserSuspend.php
+++ b/src/app/Console/Commands/UserSuspend.php
@@ -3,7 +3,7 @@
namespace App\Console\Commands;
use App\User;
-use Illuminate\Console\Command;
+use App\Console\Command;
class UserSuspend extends Command
{
@@ -21,16 +21,6 @@
*/
protected $description = 'Suspend a user';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -38,7 +28,7 @@
*/
public function handle()
{
- $user = User::where('email', $this->argument('user'))->first();
+ $user = $this->getUser($this->argument('user'));
if (!$user) {
return 1;
diff --git a/src/app/Console/Commands/UserUnsuspend.php b/src/app/Console/Commands/UserUnsuspend.php
--- a/src/app/Console/Commands/UserUnsuspend.php
+++ b/src/app/Console/Commands/UserUnsuspend.php
@@ -2,8 +2,7 @@
namespace App\Console\Commands;
-use App\User;
-use Illuminate\Console\Command;
+use App\Console\Command;
class UserUnsuspend extends Command
{
@@ -21,16 +20,6 @@
*/
protected $description = 'Remove a user suspension';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -38,7 +27,7 @@
*/
public function handle()
{
- $user = User::where('email', $this->argument('user'))->first();
+ $user = $this->getUser($this->argument('user'));
if (!$user) {
return 1;
diff --git a/src/app/Console/Commands/UserVerify.php b/src/app/Console/Commands/UserVerify.php
--- a/src/app/Console/Commands/UserVerify.php
+++ b/src/app/Console/Commands/UserVerify.php
@@ -2,7 +2,7 @@
namespace App\Console\Commands;
-use Illuminate\Console\Command;
+use App\Console\Command;
class UserVerify extends Command
{
@@ -20,16 +20,6 @@
*/
protected $description = 'Verify the state of a user account';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -37,7 +27,7 @@
*/
public function handle()
{
- $user = \App\User::where('email', $this->argument('user'))->first();
+ $user = $this->getUser($this->argument('user'));
if (!$user) {
return 1;
diff --git a/src/app/Console/Commands/UserWallets.php b/src/app/Console/Commands/UserWallets.php
--- a/src/app/Console/Commands/UserWallets.php
+++ b/src/app/Console/Commands/UserWallets.php
@@ -2,7 +2,7 @@
namespace App\Console\Commands;
-use Illuminate\Console\Command;
+use App\Console\Command;
class UserWallets extends Command
{
@@ -20,16 +20,6 @@
*/
protected $description = 'List wallets for a user';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -37,7 +27,7 @@
*/
public function handle()
{
- $user = \App\User::where('email', $this->argument('user'))->first();
+ $user = $this->getUser($this->argument('user'));
if (!$user) {
return 1;
diff --git a/src/app/Console/Commands/WalletAddTransaction.php b/src/app/Console/Commands/WalletAddTransaction.php
--- a/src/app/Console/Commands/WalletAddTransaction.php
+++ b/src/app/Console/Commands/WalletAddTransaction.php
@@ -2,7 +2,7 @@
namespace App\Console\Commands;
-use Illuminate\Console\Command;
+use App\Console\Command;
class WalletAddTransaction extends Command
{
@@ -20,16 +20,6 @@
*/
protected $description = 'Add a transaction to a wallet';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -37,7 +27,7 @@
*/
public function handle()
{
- $wallet = \App\Wallet::find($this->argument('wallet'));
+ $wallet = $this->getWallet($this->argument('wallet'));
if (!$wallet) {
return 1;
diff --git a/src/app/Console/Commands/WalletBalances.php b/src/app/Console/Commands/WalletBalances.php
--- a/src/app/Console/Commands/WalletBalances.php
+++ b/src/app/Console/Commands/WalletBalances.php
@@ -20,16 +20,6 @@
*/
protected $description = 'Show the balance on wallets';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -37,13 +27,18 @@
*/
public function handle()
{
- \App\Wallet::all()->each(
+ $wallets = \App\Wallet::select('wallets.*')
+ ->join('users', 'users.id', '=', 'wallets.user_id')
+ ->withEnvTenant('users')
+ ->all();
+
+ $wallets->each(
function ($wallet) {
if ($wallet->balance == 0) {
return;
}
- $user = \App\User::where('id', $wallet->user_id)->first();
+ $user = $wallet->owner;
if (!$user) {
return;
diff --git a/src/app/Console/Commands/WalletCharge.php b/src/app/Console/Commands/WalletCharge.php
--- a/src/app/Console/Commands/WalletCharge.php
+++ b/src/app/Console/Commands/WalletCharge.php
@@ -2,8 +2,8 @@
namespace App\Console\Commands;
+use App\Console\Command;
use App\Wallet;
-use Illuminate\Console\Command;
class WalletCharge extends Command
{
@@ -21,16 +21,6 @@
*/
protected $description = 'Charge wallets';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -40,7 +30,7 @@
{
if ($wallet = $this->argument('wallet')) {
// Find specified wallet by ID
- $wallet = Wallet::find($wallet);
+ $wallet = $this->getWallet($wallet);
if (!$wallet || !$wallet->owner) {
return 1;
@@ -51,6 +41,7 @@
// Get all wallets, excluding deleted accounts
$wallets = Wallet::select('wallets.*')
->join('users', 'users.id', '=', 'wallets.user_id')
+ ->withEnvTenant('users')
->whereNull('users.deleted_at')
->cursor();
}
diff --git a/src/app/Console/Commands/WalletDiscount.php b/src/app/Console/Commands/WalletDiscount.php
--- a/src/app/Console/Commands/WalletDiscount.php
+++ b/src/app/Console/Commands/WalletDiscount.php
@@ -2,7 +2,7 @@
namespace App\Console\Commands;
-use Illuminate\Console\Command;
+use App\Console\Command;
class WalletDiscount extends Command
{
@@ -20,16 +20,6 @@
*/
protected $description = 'Apply a discount to a wallet';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -37,7 +27,7 @@
*/
public function handle()
{
- $wallet = \App\Wallet::where('id', $this->argument('wallet'))->first();
+ $wallet = $this->getWallet($this->argument('wallet'));
if (!$wallet) {
return 1;
@@ -48,7 +38,7 @@
if ($this->argument('discount') === '0') {
$wallet->discount()->dissociate();
} else {
- $discount = \App\Discount::find($this->argument('discount'));
+ $discount = $this->getObject(\App\Discount::class, $this->argument('discount'));
if (!$discount) {
return 1;
diff --git a/src/app/Console/Commands/WalletExpected.php b/src/app/Console/Commands/WalletExpected.php
--- a/src/app/Console/Commands/WalletExpected.php
+++ b/src/app/Console/Commands/WalletExpected.php
@@ -2,7 +2,7 @@
namespace App\Console\Commands;
-use Illuminate\Console\Command;
+use App\Console\Command;
class WalletExpected extends Command
{
@@ -20,16 +20,6 @@
*/
protected $description = 'Show expected charges to wallets (for user)';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -38,8 +28,7 @@
public function handle()
{
if ($this->option('user')) {
- $user = \App\User::where('email', $this->option('user'))
- ->orWhere('id', $this->option('user'))->first();
+ $user = $this->getUser($this->option('user'));
if (!$user) {
return 1;
@@ -47,7 +36,10 @@
$wallets = $user->wallets;
} else {
- $wallets = \App\Wallet::all();
+ $wallets = \App\Wallet::select('wallets.*')
+ ->join('users', 'users.id', '=', 'wallets.user_id')
+ ->withEnvTenant('users')
+ ->all();
}
foreach ($wallets as $wallet) {
diff --git a/src/app/Console/Commands/WalletGetBalance.php b/src/app/Console/Commands/WalletGetBalance.php
--- a/src/app/Console/Commands/WalletGetBalance.php
+++ b/src/app/Console/Commands/WalletGetBalance.php
@@ -2,7 +2,7 @@
namespace App\Console\Commands;
-use Illuminate\Console\Command;
+use App\Console\Command;
class WalletGetBalance extends Command
{
@@ -20,16 +20,6 @@
*/
protected $description = 'Display the balance of a wallet';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -37,7 +27,7 @@
*/
public function handle()
{
- $wallet = \App\Wallet::find($this->argument('wallet'));
+ $wallet = $this->getWallet($this->argument('wallet'));
if (!$wallet) {
return 1;
diff --git a/src/app/Console/Commands/WalletGetDiscount.php b/src/app/Console/Commands/WalletGetDiscount.php
--- a/src/app/Console/Commands/WalletGetDiscount.php
+++ b/src/app/Console/Commands/WalletGetDiscount.php
@@ -2,7 +2,7 @@
namespace App\Console\Commands;
-use Illuminate\Console\Command;
+use App\Console\Command;
class WalletGetDiscount extends Command
{
@@ -20,16 +20,6 @@
*/
protected $description = 'Display the existing discount to a wallet, if any.';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -37,7 +27,7 @@
*/
public function handle()
{
- $wallet = \App\Wallet::find($this->argument('wallet'));
+ $wallet = $this->getWallet($this->argument('wallet'));
if (!$wallet) {
return 1;
diff --git a/src/app/Console/Commands/WalletMandate.php b/src/app/Console/Commands/WalletMandate.php
--- a/src/app/Console/Commands/WalletMandate.php
+++ b/src/app/Console/Commands/WalletMandate.php
@@ -2,8 +2,8 @@
namespace App\Console\Commands;
+use App\Console\Command;
use App\Http\Controllers\API\V4\PaymentsController;
-use Illuminate\Console\Command;
class WalletMandate extends Command
{
@@ -21,16 +21,6 @@
*/
protected $description = 'Show expected charges to wallets';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -38,7 +28,7 @@
*/
public function handle()
{
- $wallet = \App\Wallet::find($this->argument('wallet'));
+ $wallet = $this->getWallet($this->argument('wallet'));
if (!$wallet) {
return 1;
diff --git a/src/app/Console/Commands/WalletSetBalance.php b/src/app/Console/Commands/WalletSetBalance.php
--- a/src/app/Console/Commands/WalletSetBalance.php
+++ b/src/app/Console/Commands/WalletSetBalance.php
@@ -2,7 +2,7 @@
namespace App\Console\Commands;
-use Illuminate\Console\Command;
+use App\Console\Command;
class WalletSetBalance extends Command
{
@@ -20,16 +20,6 @@
*/
protected $description = 'Set the balance of a wallet';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -37,13 +27,13 @@
*/
public function handle()
{
- $wallet = \App\Wallet::find($this->argument('wallet'));
+ $wallet = $this->getWallet($this->argument('wallet'));
if (!$wallet) {
return 1;
}
- $wallet->balance = (int)($this->argument('balance'));
+ $wallet->balance = (int) $this->argument('balance');
$wallet->save();
}
}
diff --git a/src/app/Console/Commands/WalletSetDiscount.php b/src/app/Console/Commands/WalletSetDiscount.php
--- a/src/app/Console/Commands/WalletSetDiscount.php
+++ b/src/app/Console/Commands/WalletSetDiscount.php
@@ -2,7 +2,7 @@
namespace App\Console\Commands;
-use Illuminate\Console\Command;
+use App\Console\Command;
class WalletSetDiscount extends Command
{
@@ -20,16 +20,6 @@
*/
protected $description = 'Apply a discount to a wallet';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -37,7 +27,7 @@
*/
public function handle()
{
- $wallet = \App\Wallet::where('id', $this->argument('wallet'))->first();
+ $wallet = $this->getWallet($this->argument('wallet'));
if (!$wallet) {
return 1;
@@ -48,7 +38,7 @@
if ($this->argument('discount') === '0') {
$wallet->discount()->dissociate();
} else {
- $discount = \App\Discount::find($this->argument('discount'));
+ $discount = $this->getObject(\App\Discount::class, $this->argument('discount'));
if (!$discount) {
return 1;
diff --git a/src/app/Console/Commands/WalletTransactions.php b/src/app/Console/Commands/WalletTransactions.php
--- a/src/app/Console/Commands/WalletTransactions.php
+++ b/src/app/Console/Commands/WalletTransactions.php
@@ -2,7 +2,7 @@
namespace App\Console\Commands;
-use Illuminate\Console\Command;
+use App\Console\Command;
class WalletTransactions extends Command
{
@@ -20,16 +20,6 @@
*/
protected $description = 'List the transactions against a wallet.';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -37,13 +27,13 @@
*/
public function handle()
{
- $wallet = \App\Wallet::where('id', $this->argument('wallet'))->first();
+ $wallet = $this->getWallet($this->argument('wallet'));
if (!$wallet) {
return 1;
}
- foreach ($wallet->transactions()->orderBy('created_at')->get() as $transaction) {
+ $wallet->transactions()->orderBy('created_at')->each(function ($transaction) {
$this->info(
sprintf(
"%s: %s %s",
@@ -67,6 +57,6 @@
);
}
}
- }
+ });
}
}
diff --git a/src/app/Console/Commands/WalletUntil.php b/src/app/Console/Commands/WalletUntil.php
--- a/src/app/Console/Commands/WalletUntil.php
+++ b/src/app/Console/Commands/WalletUntil.php
@@ -2,7 +2,7 @@
namespace App\Console\Commands;
-use Illuminate\Console\Command;
+use App\Console\Command;
class WalletUntil extends Command
{
@@ -20,16 +20,6 @@
*/
protected $description = 'Show until when the balance on a wallet lasts.';
- /**
- * Create a new command instance.
- *
- * @return void
- */
- public function __construct()
- {
- parent::__construct();
- }
-
/**
* Execute the console command.
*
@@ -37,7 +27,7 @@
*/
public function handle()
{
- $wallet = \App\Wallet::find($this->argument('wallet'));
+ $wallet = $this->getWallet($this->argument('wallet'));
if (!$wallet) {
return 1;
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
{
@@ -53,6 +59,16 @@
$this->attributes['discount'] = $discount;
}
+ /**
+ * The tenant for this discount.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
+ public function tenant()
+ {
+ return $this->belongsTo('App\Tenant', 'tenant_id', 'id');
+ }
+
/**
* List of wallets with this discount assigned.
*
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
{
@@ -86,6 +89,7 @@
'wallet_id' => $wallet_id,
'sku_id' => $sku->id,
'cost' => $sku->pivot->cost(),
+ 'fee' => $sku->pivot->fee(),
'entitleable_id' => $this->id,
'entitleable_type' => Domain::class
]
@@ -107,13 +111,13 @@
}
/**
- * Return list of public+active domain names
+ * Return list of public+active domain names (for current tenant)
*/
public static function getPublicDomains(): array
{
- $where = sprintf('(type & %s)', Domain::TYPE_PUBLIC);
-
- return self::whereRaw($where)->get(['namespace'])->pluck('namespace')->toArray();
+ return self::withEnvTenant()
+ ->whereRaw(sprintf('(type & %s)', Domain::TYPE_PUBLIC))
+ ->get(['namespace'])->pluck('namespace')->toArray();
}
/**
@@ -376,6 +380,16 @@
$this->save();
}
+ /**
+ * The tenant for this domain.
+ *
+ * @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/Entitlement.php b/src/app/Entitlement.php
--- a/src/app/Entitlement.php
+++ b/src/app/Entitlement.php
@@ -11,10 +11,18 @@
*
* Owned by a {@link \App\User}, billed to a {@link \App\Wallet}.
*
- * @property \App\User $owner The owner of this entitlement (subject).
- * @property \App\Sku $sku The SKU to which this entitlement applies.
- * @property \App\Wallet $wallet The wallet to which this entitlement is charged.
- * @property \App\Domain|\App\User $entitleable The entitled object (receiver of the entitlement).
+ * @property int $cost
+ * @property ?string $description
+ * @property \App\Domain|\App\User $entitleable The entitled object (receiver of the entitlement).
+ * @property int $entitleable_id
+ * @property string $entitleable_type
+ * @property int $fee
+ * @property string $id
+ * @property \App\User $owner The owner of this entitlement (subject).
+ * @property \App\Sku $sku The SKU to which this entitlement applies.
+ * @property string $sku_id
+ * @property \App\Wallet $wallet The wallet to which this entitlement is charged.
+ * @property string $wallet_id
*/
class Entitlement extends Model
{
@@ -45,11 +53,13 @@
'entitleable_id',
'entitleable_type',
'cost',
- 'description'
+ 'description',
+ 'fee',
];
protected $casts = [
'cost' => 'integer',
+ 'fee' => 'integer'
];
/**
diff --git a/src/app/Group.php b/src/app/Group.php
--- a/src/app/Group.php
+++ b/src/app/Group.php
@@ -9,10 +9,11 @@
/**
* The eloquent definition of a Group.
*
- * @property int $id The group identifier
- * @property string $email An email address
- * @property string $members A comma-separated list of email addresses
- * @property int $status The group status
+ * @property int $id The group identifier
+ * @property string $email An email address
+ * @property string $members A comma-separated list of email addresses
+ * @property int $status The group status
+ * @property int $tenant_id Tenant identifier
*/
class Group extends Model
{
@@ -64,6 +65,7 @@
'wallet_id' => $wallet->id,
'sku_id' => $sku->id,
'cost' => $exists >= $sku->units_free ? $sku->cost : 0,
+ 'fee' => $exists >= $sku->units_free ? $sku->fee : 0,
'entitleable_id' => $this->id,
'entitleable_type' => Group::class
]);
@@ -250,6 +252,16 @@
$this->save();
}
+ /**
+ * The tenant for this group.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
+ public function tenant()
+ {
+ return $this->belongsTo('App\Tenant', 'tenant_id', 'id');
+ }
+
/**
* Unsuspend this group.
*
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;
@@ -111,6 +112,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.
*
@@ -188,10 +216,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
@@ -217,10 +285,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);
@@ -252,14 +316,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/Admin/DiscountsController.php b/src/app/Http/Controllers/API/V4/Admin/DiscountsController.php
--- a/src/app/Http/Controllers/API/V4/Admin/DiscountsController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/DiscountsController.php
@@ -16,7 +16,10 @@
{
$discounts = [];
- Discount::where('active', true)->orderBy('discount')->get()
+ Discount::withEnvTenant()
+ ->where('active', true)
+ ->orderBy('discount')
+ ->get()
->map(function ($discount) use (&$discounts) {
$label = $discount->discount . '% - ' . $discount->description;
diff --git a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
--- a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
@@ -20,7 +20,7 @@
$result = collect([]);
if ($owner) {
- if ($owner = User::find($owner)) {
+ if ($owner = User::withEnvTenant()->find($owner)) {
foreach ($owner->wallets as $wallet) {
$entitlements = $wallet->entitlements()->where('entitleable_type', Domain::class)->get();
@@ -33,7 +33,7 @@
$result = $result->sortBy('namespace')->values();
}
} elseif (!empty($search)) {
- if ($domain = Domain::where('namespace', $search)->first()) {
+ if ($domain = Domain::withEnvTenant()->where('namespace', $search)->first()) {
$result->push($domain);
}
}
@@ -64,7 +64,7 @@
*/
public function suspend(Request $request, $id)
{
- $domain = Domain::find($id);
+ $domain = Domain::withEnvTenant()->find($id);
if (empty($domain) || $domain->isPublic()) {
return $this->errorResponse(404);
@@ -88,7 +88,7 @@
*/
public function unsuspend(Request $request, $id)
{
- $domain = Domain::find($id);
+ $domain = Domain::withEnvTenant()->find($id);
if (empty($domain) || $domain->isPublic()) {
return $this->errorResponse(404);
diff --git a/src/app/Http/Controllers/API/V4/Admin/EntitlementsController.php b/src/app/Http/Controllers/API/V4/Admin/EntitlementsController.php
deleted file mode 100644
--- a/src/app/Http/Controllers/API/V4/Admin/EntitlementsController.php
+++ /dev/null
@@ -1,7 +0,0 @@
-find($owner)) {
foreach ($owner->wallets as $wallet) {
$wallet->entitlements()->where('entitleable_type', Group::class)->get()
->each(function ($entitlement) use ($result) {
@@ -31,7 +31,7 @@
$result = $result->sortBy('namespace')->values();
}
} elseif (!empty($search)) {
- if ($group = Group::where('email', $search)->first()) {
+ if ($group = Group::withEnvTenant()->where('email', $search)->first()) {
$result->push($group);
}
}
@@ -78,7 +78,7 @@
*/
public function suspend(Request $request, $id)
{
- $group = Group::find($id);
+ $group = Group::withEnvTenant()->find($id);
if (empty($group)) {
return $this->errorResponse(404);
@@ -102,7 +102,7 @@
*/
public function unsuspend(Request $request, $id)
{
- $group = Group::find($id);
+ $group = Group::withEnvTenant()->find($id);
if (empty($group)) {
return $this->errorResponse(404);
diff --git a/src/app/Http/Controllers/API/V4/Admin/StatsController.php b/src/app/Http/Controllers/API/V4/Admin/StatsController.php
--- a/src/app/Http/Controllers/API/V4/Admin/StatsController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/StatsController.php
@@ -6,6 +6,7 @@
use App\User;
use Carbon\Carbon;
use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class StatsController extends \App\Http\Controllers\Controller
@@ -18,6 +19,14 @@
public const COLOR_BLUE_DARK = '#0056b3';
public const COLOR_ORANGE = '#f1a539';
+ /** @var array List of enabled charts */
+ protected $charts = [
+ 'discounts',
+ 'income',
+ 'users',
+ 'users-all',
+ ];
+
/**
* Fetch chart data
*
@@ -33,7 +42,7 @@
$method = 'chart' . implode('', array_map('ucfirst', explode('-', $chart)));
- if (!method_exists($this, $method)) {
+ if (!in_array($chart, $this->charts) || !method_exists($this, $method)) {
return $this->errorResponse(404);
}
@@ -53,9 +62,14 @@
->join('users', 'users.id', '=', 'wallets.user_id')
->where('discount', '>', 0)
->whereNull('users.deleted_at')
- ->groupBy('discounts.discount')
- ->pluck('cnt', 'discount')
- ->all();
+ ->groupBy('discounts.discount');
+
+ $addTenantScope = function ($builder, $tenantId) {
+ return $builder->where('users.tenant_id', $tenantId);
+ };
+
+ $discounts = $this->applyTenantScope($discounts, $addTenantScope)
+ ->pluck('cnt', 'discount')->all();
$labels = array_keys($discounts);
$discounts = array_values($discounts);
@@ -119,7 +133,20 @@
->where('updated_at', '>=', $start->toDateString())
->where('status', PaymentProvider::STATUS_PAID)
->whereIn('type', [PaymentProvider::TYPE_ONEOFF, PaymentProvider::TYPE_RECURRING])
- ->groupByRaw('1')
+ ->groupByRaw('1');
+
+ $addTenantScope = function ($builder, $tenantId) {
+ $where = '`wallet_id` IN ('
+ . 'select `id` from `wallets` '
+ . 'join `users` on (`wallets`.`user_id` = `users`.`id`) '
+ . 'where `payments`.`wallet_id` = `wallets`.`id` '
+ . 'and `users`.`tenant_id` = ' . intval($tenantId)
+ . ')';
+
+ return $builder->whereRaw($where);
+ };
+
+ $payments = $this->applyTenantScope($payments, $addTenantScope)
->pluck('amount', 'period')
->map(function ($amount) {
return $amount / 100;
@@ -185,14 +212,15 @@
$created = DB::table('users')
->selectRaw("date_format(created_at, '%Y-%v') as period, count(*) as cnt")
->where('created_at', '>=', $start->toDateString())
- ->groupByRaw('1')
- ->get();
+ ->groupByRaw('1');
$deleted = DB::table('users')
->selectRaw("date_format(deleted_at, '%Y-%v') as period, count(*) as cnt")
->where('deleted_at', '>=', $start->toDateString())
- ->groupByRaw('1')
- ->get();
+ ->groupByRaw('1');
+
+ $created = $this->applyTenantScope($created)->get();
+ $deleted = $this->applyTenantScope($deleted)->get();
$empty = array_fill_keys($labels, 0);
$created = array_values(array_merge($empty, $created->pluck('cnt', 'period')->all()));
@@ -260,16 +288,16 @@
$created = DB::table('users')
->selectRaw("date_format(created_at, '%Y-%v') as period, count(*) as cnt")
->where('created_at', '>=', $start->toDateString())
- ->groupByRaw('1')
- ->get();
+ ->groupByRaw('1');
$deleted = DB::table('users')
->selectRaw("date_format(deleted_at, '%Y-%v') as period, count(*) as cnt")
->where('deleted_at', '>=', $start->toDateString())
- ->groupByRaw('1')
- ->get();
+ ->groupByRaw('1');
- $count = DB::table('users')->whereNull('deleted_at')->count();
+ $created = $this->applyTenantScope($created)->get();
+ $deleted = $this->applyTenantScope($deleted)->get();
+ $count = $this->applyTenantScope(DB::table('users')->whereNull('deleted_at'))->count();
$empty = array_fill_keys($labels, 0);
$created = array_merge($empty, $created->pluck('cnt', 'period')->all());
@@ -313,4 +341,29 @@
]
];
}
+
+ /**
+ * Add tenant scope to the queries when needed
+ *
+ * @param \Illuminate\Database\Query\Builder $query The query
+ * @param callable $addQuery Additional tenant-scope query-modifier
+ *
+ * @return \Illuminate\Database\Query\Builder
+ */
+ protected function applyTenantScope($query, $addQuery = null)
+ {
+ $user = Auth::guard()->user();
+
+ if ($user->role == 'reseller') {
+ if ($addQuery) {
+ $query = $addQuery($query, \config('app.tenant_id'));
+ } else {
+ $query = $query->withEnvTenant();
+ }
+ }
+
+ // TODO: Tenant selector for admins
+
+ return $query;
+ }
}
diff --git a/src/app/Http/Controllers/API/V4/Admin/UsersController.php b/src/app/Http/Controllers/API/V4/Admin/UsersController.php
--- a/src/app/Http/Controllers/API/V4/Admin/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/UsersController.php
@@ -13,6 +13,18 @@
class UsersController extends \App\Http\Controllers\API\V4\UsersController
{
+ /**
+ * Delete a user.
+ *
+ * @param int $id User identifier
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ public function destroy($id)
+ {
+ return $this->errorResponse(404);
+ }
+
/**
* Searching of user accounts.
*
@@ -25,13 +37,21 @@
$result = collect([]);
if ($owner) {
- if ($owner = User::find($owner)) {
- $result = $owner->users(false)->orderBy('email')->get();
+ $owner = User::where('id', $owner)
+ ->withEnvTenant()
+ ->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)
- ->orderBy('email')->get();
+ ->withEnvTenant()
+ ->whereNull('role')
+ ->orderBy('email')
+ ->get();
if ($result->isEmpty()) {
// Search by an alias
@@ -39,7 +59,9 @@
// Search by an external email
$ext_user_ids = UserSetting::where('key', 'external_email')
- ->where('value', $search)->get()->pluck('user_id');
+ ->where('value', $search)
+ ->get()
+ ->pluck('user_id');
$user_ids = $user_ids->merge($ext_user_ids)->unique();
@@ -50,19 +72,35 @@
if (!$user_ids->isEmpty()) {
$result = User::withTrashed()->whereIn('id', $user_ids)
- ->orderBy('email')->get();
+ ->withEnvTenant()
+ ->whereNull('role')
+ ->orderBy('email')
+ ->get();
}
}
} elseif (is_numeric($search)) {
// Search by user ID
- if ($user = User::withTrashed()->find($search)) {
+ $user = User::withTrashed()->where('id', $search)
+ ->withEnvTenant()
+ ->whereNull('role')
+ ->first();
+
+ if ($user) {
$result->push($user);
}
} elseif (!empty($search)) {
// Search by domain
- if ($domain = Domain::withTrashed()->where('namespace', $search)->first()) {
- if ($wallet = $domain->wallet()) {
- $result->push($wallet->owner()->withTrashed()->first());
+ $domain = Domain::withTrashed()->where('namespace', $search)
+ ->withEnvTenant()
+ ->first();
+
+ if ($domain) {
+ if (
+ ($wallet = $domain->wallet())
+ && ($owner = $wallet->owner()->withTrashed()->withEnvTenant()->first())
+ && empty($owner->role)
+ ) {
+ $result->push($owner);
}
}
}
@@ -93,9 +131,9 @@
*/
public function reset2FA(Request $request, $id)
{
- $user = User::find($id);
+ $user = User::withEnvTenant()->find($id);
- if (empty($user)) {
+ if (empty($user) || !$this->guard()->user()->canUpdate($user)) {
return $this->errorResponse(404);
}
@@ -113,6 +151,18 @@
]);
}
+ /**
+ * Create a new user record.
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ public function store(Request $request)
+ {
+ return $this->errorResponse(404);
+ }
+
/**
* Suspend the user
*
@@ -123,9 +173,9 @@
*/
public function suspend(Request $request, $id)
{
- $user = User::find($id);
+ $user = User::withEnvTenant()->find($id);
- if (empty($user)) {
+ if (empty($user) || !$this->guard()->user()->canUpdate($user)) {
return $this->errorResponse(404);
}
@@ -147,9 +197,9 @@
*/
public function unsuspend(Request $request, $id)
{
- $user = User::find($id);
+ $user = User::withEnvTenant()->find($id);
- if (empty($user)) {
+ if (empty($user) || !$this->guard()->user()->canUpdate($user)) {
return $this->errorResponse(404);
}
@@ -171,9 +221,9 @@
*/
public function update(Request $request, $id)
{
- $user = User::find($id);
+ $user = User::withEnvTenant()->find($id);
- if (empty($user)) {
+ if (empty($user) || !$this->guard()->user()->canUpdate($user)) {
return $this->errorResponse(404);
}
diff --git a/src/app/Http/Controllers/API/V4/Admin/WalletsController.php b/src/app/Http/Controllers/API/V4/Admin/WalletsController.php
--- a/src/app/Http/Controllers/API/V4/Admin/WalletsController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/WalletsController.php
@@ -8,6 +8,7 @@
use App\Transaction;
use App\Wallet;
use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
@@ -24,7 +25,7 @@
{
$wallet = Wallet::find($id);
- if (empty($wallet)) {
+ if (empty($wallet) || !Auth::guard()->user()->canRead($wallet)) {
return $this->errorResponse(404);
}
@@ -44,6 +45,7 @@
$result['provider'] = $provider->name();
$result['providerLink'] = $provider->customerLink($wallet);
+ $result['notice'] = $this->getWalletNotice($wallet); // for resellers
return response()->json($result);
}
@@ -59,8 +61,9 @@
public function oneOff(Request $request, $id)
{
$wallet = Wallet::find($id);
+ $user = Auth::guard()->user();
- if (empty($wallet)) {
+ if (empty($wallet) || !$user->canRead($wallet)) {
return $this->errorResponse(404);
}
@@ -96,6 +99,14 @@
]
);
+ if ($user->role == 'reseller') {
+ if ($user->tenant && ($tenant_wallet = $user->tenant->wallet())) {
+ $desc = ($amount > 0 ? 'Awarded' : 'Penalized') . " user {$wallet->owner->email}";
+ $method = $amount > 0 ? 'debit' : 'credit';
+ $tenant_wallet->{$method}(abs($amount), $desc);
+ }
+ }
+
DB::commit();
$response = [
@@ -119,7 +130,7 @@
{
$wallet = Wallet::find($id);
- if (empty($wallet)) {
+ if (empty($wallet) || !Auth::guard()->user()->canRead($wallet)) {
return $this->errorResponse(404);
}
@@ -127,7 +138,7 @@
if (empty($request->discount)) {
$wallet->discount()->dissociate();
$wallet->save();
- } elseif ($discount = Discount::find($request->discount)) {
+ } elseif ($discount = Discount::withEnvTenant()->find($request->discount)) {
$wallet->discount()->associate($discount);
$wallet->save();
}
diff --git a/src/app/Http/Controllers/API/V4/DomainsController.php b/src/app/Http/Controllers/API/V4/DomainsController.php
--- a/src/app/Http/Controllers/API/V4/DomainsController.php
+++ b/src/app/Http/Controllers/API/V4/DomainsController.php
@@ -117,7 +117,7 @@
*/
public function show($id)
{
- $domain = Domain::findOrFail($id);
+ $domain = Domain::withEnvTenant()->findOrFail($id);
// Only owner (or admin) has access to the domain
if (!Auth::guard()->user()->canRead($domain)) {
@@ -152,7 +152,7 @@
*/
public function status($id)
{
- $domain = Domain::find($id);
+ $domain = Domain::withEnvTenant()->findOrFail($id);
// Only owner (or admin) has access to the domain
if (!Auth::guard()->user()->canRead($domain)) {
diff --git a/src/app/Http/Controllers/API/V4/EntitlementsController.php b/src/app/Http/Controllers/API/V4/EntitlementsController.php
deleted file mode 100644
--- a/src/app/Http/Controllers/API/V4/EntitlementsController.php
+++ /dev/null
@@ -1,97 +0,0 @@
-errorResponse(404);
- }
-
- /**
- * Remove the specified resource from storage.
- *
- * @param int $id
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function destroy($id)
- {
- // TODO
- return $this->errorResponse(404);
- }
-
- /**
- * Show the form for editing the specified resource.
- *
- * @param int $id
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function edit($id)
- {
- // TODO
- return $this->errorResponse(404);
- }
-
- /**
- * Display a listing of the resource.
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function index()
- {
- // TODO
- return $this->errorResponse(404);
- }
-
- /**
- * Store a newly created resource in storage.
- *
- * @param \Illuminate\Http\Request $request
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function store(Request $request)
- {
- // TODO
- return $this->errorResponse(404);
- }
-
- /**
- * Display the specified resource.
- *
- * @param int $id
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function show($id)
- {
- // TODO
- 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)
- {
- // TODO
- return $this->errorResponse(404);
- }
-}
diff --git a/src/app/Http/Controllers/API/V4/GroupsController.php b/src/app/Http/Controllers/API/V4/GroupsController.php
--- a/src/app/Http/Controllers/API/V4/GroupsController.php
+++ b/src/app/Http/Controllers/API/V4/GroupsController.php
@@ -32,7 +32,7 @@
*/
public function destroy($id)
{
- $group = Group::find($id);
+ $group = Group::withEnvTenant()->find($id);
if (empty($group)) {
return $this->errorResponse(404);
@@ -96,7 +96,7 @@
*/
public function show($id)
{
- $group = Group::find($id);
+ $group = Group::withEnvTenant()->find($id);
if (empty($group)) {
return $this->errorResponse(404);
@@ -123,7 +123,7 @@
*/
public function status($id)
{
- $group = Group::find($id);
+ $group = Group::withEnvTenant()->find($id);
if (empty($group)) {
return $this->errorResponse(404);
@@ -308,7 +308,7 @@
*/
public function update(Request $request, $id)
{
- $group = Group::find($id);
+ $group = Group::withEnvTenant()->find($id);
if (empty($group)) {
return $this->errorResponse(404);
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,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++;
+
+ // @phpstan-ignore-next-line
+ 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 @@
+find($id);
if (empty($user)) {
return $this->errorResponse(404);
@@ -186,7 +186,7 @@
$data['name'] = $sku->name;
$data['description'] = $sku->description;
- unset($data['handler_class'], $data['created_at'], $data['updated_at']);
+ unset($data['handler_class'], $data['created_at'], $data['updated_at'], $data['fee'], $data['tenant_id']);
return $data;
}
diff --git a/src/app/Http/Controllers/Controller.php b/src/app/Http/Controllers/Controller.php
--- a/src/app/Http/Controllers/Controller.php
+++ b/src/app/Http/Controllers/Controller.php
@@ -24,7 +24,7 @@
*
* @return \Illuminate\Http\JsonResponse
*/
- protected function errorResponse(int $code, string $message = null, array $data = [])
+ public static function errorResponse(int $code, string $message = null, array $data = [])
{
$errors = [
400 => "Bad request",
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
@@ -64,6 +64,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,
@@ -80,11 +81,11 @@
\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/AuthenticateAdmin.php b/src/app/Http/Middleware/AuthenticateAdmin.php
--- a/src/app/Http/Middleware/AuthenticateAdmin.php
+++ b/src/app/Http/Middleware/AuthenticateAdmin.php
@@ -18,7 +18,7 @@
$user = auth()->user();
if (!$user) {
- abort(403, "Unauthorized");
+ abort(401, "Unauthorized");
}
if ($user->role !== "admin") {
diff --git a/src/app/Http/Middleware/AuthenticateAdmin.php b/src/app/Http/Middleware/AuthenticateReseller.php
copy from src/app/Http/Middleware/AuthenticateAdmin.php
copy to src/app/Http/Middleware/AuthenticateReseller.php
--- a/src/app/Http/Middleware/AuthenticateAdmin.php
+++ b/src/app/Http/Middleware/AuthenticateReseller.php
@@ -4,7 +4,7 @@
use Closure;
-class AuthenticateAdmin
+class AuthenticateReseller
{
/**
* Handle an incoming request.
@@ -18,10 +18,14 @@
$user = auth()->user();
if (!$user) {
+ abort(401, "Unauthorized");
+ }
+
+ if ($user->role !== "reseller") {
abort(403, "Unauthorized");
}
- if ($user->role !== "admin") {
+ if ($user->tenant_id != \config('app.tenant_id')) {
abort(403, "Unauthorized");
}
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/EntitlementObserver.php b/src/app/Observers/EntitlementObserver.php
--- a/src/app/Observers/EntitlementObserver.php
+++ b/src/app/Observers/EntitlementObserver.php
@@ -123,7 +123,6 @@
return;
}
- $cost = 0;
$now = Carbon::now();
// get the discount rate applied to the wallet.
@@ -131,7 +130,8 @@
// just in case this had not been billed yet, ever
$diffInMonths = $entitlement->updated_at->diffInMonths($now);
- $cost += (int) ($entitlement->cost * $discount * $diffInMonths);
+ $cost = (int) ($entitlement->cost * $discount * $diffInMonths);
+ $fee = (int) ($entitlement->fee * $diffInMonths);
// this moves the hypothetical updated at forward to however many months past the original
$updatedAt = $entitlement->updated_at->copy()->addMonthsWithoutOverflow($diffInMonths);
@@ -153,8 +153,18 @@
}
$pricePerDay = $entitlement->cost / $daysInMonth;
+ $feePerDay = $entitlement->fee / $daysInMonth;
$cost += (int) (round($pricePerDay * $discount * $diffInDays, 0));
+ $fee += (int) (round($feePerDay * $diffInDays, 0));
+
+ $profit = $cost - $fee;
+
+ if ($profit != 0 && $owner->tenant && ($wallet = $owner->tenant->wallet())) {
+ $desc = "Charged user {$owner->email}";
+ $method = $profit > 0 ? 'credit' : 'debit';
+ $wallet->{$method}(abs($profit), $desc);
+ }
if ($cost == 0) {
return;
diff --git a/src/app/Observers/GroupObserver.php b/src/app/Observers/GroupObserver.php
--- a/src/app/Observers/GroupObserver.php
+++ b/src/app/Observers/GroupObserver.php
@@ -25,6 +25,8 @@
}
$group->status |= Group::STATUS_NEW | Group::STATUS_ACTIVE;
+
+ $group->tenant_id = \config('app.tenant_id');
}
/**
diff --git a/src/app/Observers/PackageObserver.php b/src/app/Observers/PackageObserver.php
--- a/src/app/Observers/PackageObserver.php
+++ b/src/app/Observers/PackageObserver.php
@@ -27,5 +27,7 @@
break;
}
}
+
+ $package->tenant_id = \config('app.tenant_id');
}
}
diff --git a/src/app/Observers/PackageSkuObserver.php b/src/app/Observers/PackageSkuObserver.php
--- a/src/app/Observers/PackageSkuObserver.php
+++ b/src/app/Observers/PackageSkuObserver.php
@@ -6,6 +6,25 @@
class PackageSkuObserver
{
+ /**
+ * Handle the "creating" event on an PackageSku relation.
+ *
+ * Ensures that the entries belong to the same tenant.
+ *
+ * @param \App\PackageSku $packageSku The package-sku relation
+ *
+ * @return void
+ */
+ public function creating(PackageSku $packageSku)
+ {
+ $package = $packageSku->package;
+ $sku = $packageSku->sku;
+
+ if ($package->tenant_id != $sku->tenant_id) {
+ throw new \Exception("Package and SKU owned by different tenants");
+ }
+ }
+
/**
* Handle the "created" event on an PackageSku relation
*
diff --git a/src/app/Observers/PlanObserver.php b/src/app/Observers/PlanObserver.php
--- a/src/app/Observers/PlanObserver.php
+++ b/src/app/Observers/PlanObserver.php
@@ -27,5 +27,7 @@
break;
}
}
+
+ $plan->tenant_id = \config('app.tenant_id');
}
}
diff --git a/src/app/Observers/PlanPackageObserver.php b/src/app/Observers/PlanPackageObserver.php
new file mode 100644
--- /dev/null
+++ b/src/app/Observers/PlanPackageObserver.php
@@ -0,0 +1,27 @@
+package;
+ $plan = $planPackage->plan;
+
+ if ($package->tenant_id != $plan->tenant_id) {
+ throw new \Exception("Package and Plan owned by different tenants");
+ }
+ }
+}
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/SkuObserver.php b/src/app/Observers/SkuObserver.php
--- a/src/app/Observers/SkuObserver.php
+++ b/src/app/Observers/SkuObserver.php
@@ -22,5 +22,9 @@
break;
}
}
+
+ $sku->tenant_id = \config('app.tenant_id');
+
+ // TODO: We should make sure that tenant_id + title is unique
}
}
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');
}
/**
@@ -108,6 +108,18 @@
}
});
}
+
+ // Debit the reseller's wallet with the user negative balance
+ $balance = 0;
+ foreach ($user->wallets as $wallet) {
+ // Note: here we assume all user wallets are using the same currency.
+ // It might get changed in the future
+ $balance += $wallet->balance;
+ }
+
+ if ($balance < 0 && $user->tenant && ($wallet = $user->tenant->wallet())) {
+ $wallet->debit($balance * -1, "Deleted user {$user->email}");
+ }
}
/**
diff --git a/src/app/Package.php b/src/app/Package.php
--- a/src/app/Package.php
+++ b/src/app/Package.php
@@ -21,6 +21,13 @@
* * Free package: mailbox + quota.
*
* Selecting a package will therefore create a set of entitlments from SKUs.
+ *
+ * @property string $description
+ * @property int $discount_rate
+ * @property string $id
+ * @property string $name
+ * @property ?int $tenant_id
+ * @property string $title
*/
class Package extends Model
{
@@ -69,7 +76,10 @@
return $costs;
}
- public function isDomain()
+ /**
+ * Checks whether the package contains a domain SKU.
+ */
+ public function isDomain(): bool
{
foreach ($this->skus as $sku) {
if ($sku->handler_class::entitleableClass() == \App\Domain::class) {
@@ -94,4 +104,14 @@
['qty']
);
}
+
+ /**
+ * The tenant for this package.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
+ public function tenant()
+ {
+ return $this->belongsTo('App\Tenant', 'tenant_id', 'id');
+ }
}
diff --git a/src/app/PackageSku.php b/src/app/PackageSku.php
--- a/src/app/PackageSku.php
+++ b/src/app/PackageSku.php
@@ -35,30 +35,50 @@
*/
public function cost()
{
- $costs = 0;
-
$units = $this->qty - $this->sku->units_free;
if ($units < 0) {
- \Log::debug(
- "Package {$this->package_id} is misconfigured for more free units than qty."
- );
-
$units = 0;
}
+ // FIXME: Why package_skus.cost value is not used anywhere?
+
$ppu = $this->sku->cost * ((100 - $this->package->discount_rate) / 100);
- $costs += $units * $ppu;
+ return $units * $ppu;
+ }
+
+ /**
+ * Under this package, what fee this SKU has?
+ *
+ * @return int The fee for this SKU under this package in cents.
+ */
+ public function fee()
+ {
+ $units = $this->qty - $this->sku->units_free;
+
+ if ($units < 0) {
+ $units = 0;
+ }
- return $costs;
+ return $this->sku->fee * $units;
}
+ /**
+ * The package for this relation.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
public function package()
{
return $this->belongsTo('App\Package');
}
+ /**
+ * The SKU for this relation.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
public function sku()
{
return $this->belongsTo('App\Sku');
diff --git a/src/app/Plan.php b/src/app/Plan.php
--- a/src/app/Plan.php
+++ b/src/app/Plan.php
@@ -13,7 +13,16 @@
* A "Family Plan" as such may exist of "2 or more Kolab packages",
* and apply a discount for the third and further Kolab packages.
*
+ * @property string $description
+ * @property int $discount_qty
+ * @property int $discount_rate
+ * @property string $id
+ * @property string $name
* @property \App\Package[] $packages
+ * @property datetime $promo_from
+ * @property datetime $promo_to
+ * @property ?int $tenant_id
+ * @property string $title
*/
class Plan extends Model
{
@@ -105,4 +114,14 @@
return false;
}
+
+ /**
+ * The tenant for this plan.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
+ public function tenant()
+ {
+ return $this->belongsTo('App\Tenant', 'tenant_id', 'id');
+ }
}
diff --git a/src/app/PlanPackage.php b/src/app/PlanPackage.php
--- a/src/app/PlanPackage.php
+++ b/src/app/PlanPackage.php
@@ -15,6 +15,7 @@
* @property int $qty_max
* @property int $qty_min
* @property \App\Package $package
+ * @property \App\Plan $plan
*/
class PlanPackage extends Pivot
{
@@ -54,8 +55,23 @@
return $costs;
}
+ /**
+ * The package in this relation.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
public function package()
{
return $this->belongsTo('App\Package');
}
+
+ /**
+ * The plan in this relation.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
+ public function plan()
+ {
+ return $this->belongsTo('App\Plan');
+ }
}
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;
@@ -34,7 +35,9 @@
\App\Package::observe(\App\Observers\PackageObserver::class);
\App\PackageSku::observe(\App\Observers\PackageSkuObserver::class);
\App\Plan::observe(\App\Observers\PlanObserver::class);
+ \App\PlanPackage::observe(\App\Observers\PlanPackageObserver::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 +60,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/Providers/Payment/Mollie.php b/src/app/Providers/Payment/Mollie.php
--- a/src/app/Providers/Payment/Mollie.php
+++ b/src/app/Providers/Payment/Mollie.php
@@ -68,7 +68,7 @@
'sequenceType' => 'first',
'description' => $payment['description'],
'webhookUrl' => Utils::serviceUrl('/api/webhooks/payment/mollie'),
- 'redirectUrl' => Utils::serviceUrl('/wallet'),
+ 'redirectUrl' => self::redirectUrl(),
'locale' => 'en_US',
'method' => $payment['methodId']
];
@@ -198,7 +198,7 @@
'webhookUrl' => Utils::serviceUrl('/api/webhooks/payment/mollie'),
'locale' => 'en_US',
'method' => $payment['methodId'],
- 'redirectUrl' => Utils::serviceUrl('/wallet') // required for non-recurring payments
+ 'redirectUrl' => self::redirectUrl() // required for non-recurring payments
];
// TODO: Additional payment parameters for better fraud protection:
diff --git a/src/app/Providers/Payment/Stripe.php b/src/app/Providers/Payment/Stripe.php
--- a/src/app/Providers/Payment/Stripe.php
+++ b/src/app/Providers/Payment/Stripe.php
@@ -70,8 +70,8 @@
$request = [
'customer' => $customer_id,
- 'cancel_url' => Utils::serviceUrl('/wallet'), // required
- 'success_url' => Utils::serviceUrl('/wallet'), // required
+ 'cancel_url' => self::redirectUrl(), // required
+ 'success_url' => self::redirectUrl(), // required
'payment_method_types' => ['card'], // required
'locale' => 'en',
'mode' => 'setup',
@@ -188,8 +188,8 @@
$request = [
'customer' => $customer_id,
- 'cancel_url' => Utils::serviceUrl('/wallet'), // required
- 'success_url' => Utils::serviceUrl('/wallet'), // required
+ 'cancel_url' => self::redirectUrl(), // required
+ 'success_url' => self::redirectUrl(), // required
'payment_method_types' => ['card'], // required
'locale' => 'en',
'line_items' => [
diff --git a/src/app/Providers/PaymentProvider.php b/src/app/Providers/PaymentProvider.php
--- a/src/app/Providers/PaymentProvider.php
+++ b/src/app/Providers/PaymentProvider.php
@@ -261,6 +261,8 @@
$refund['status'] = self::STATUS_PAID;
$refund['amount'] = -1 * $amount;
+ // FIXME: Refunds/chargebacks are out of the reseller comissioning for now
+
$this->storePayment($refund, $wallet->id);
}
@@ -385,4 +387,22 @@
return $methods;
}
+
+ /**
+ * Returns the full URL for the wallet page, used when returning from an external payment page.
+ * Depending on the request origin it will return a URL for the User or Reseller UI.
+ *
+ * @return string The redirect URL
+ */
+ public static function redirectUrl(): string
+ {
+ $url = \App\Utils::serviceUrl('/wallet');
+ $domain = preg_replace('/:[0-9]+$/', '', request()->getHttpHost());
+
+ if (strpos($domain, 'reseller') === 0) {
+ $url = preg_replace('|^(https?://)([^/]+)|', '\\1' . $domain, $url);
+ }
+
+ return $url;
+ }
}
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/Sku.php b/src/app/Sku.php
--- a/src/app/Sku.php
+++ b/src/app/Sku.php
@@ -7,6 +7,18 @@
/**
* The eloquent definition of a Stock Keeping Unit (SKU).
+ *
+ * @property bool $active
+ * @property int $cost
+ * @property string $description
+ * @property int $fee The fee that the tenant pays to us
+ * @property string $handler_class
+ * @property string $id
+ * @property string $name
+ * @property string $period
+ * @property ?int $tenant_id
+ * @property string $title
+ * @property int $units_free
*/
class Sku extends Model
{
@@ -23,6 +35,7 @@
'active',
'cost',
'description',
+ 'fee',
'handler_class',
'name',
// persist for annual domain registration
@@ -59,4 +72,14 @@
'package_skus'
)->using('App\PackageSku')->withPivot(['cost', 'qty']);
}
+
+ /**
+ * The tenant for this SKU.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
+ public function tenant()
+ {
+ return $this->belongsTo('App\Tenant', 'tenant_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,52 @@
+hasMany('App\Discount');
+ }
+
+ /**
+ * SignupInvitations assigned to this tenant.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ */
+ public function signupInvitations()
+ {
+ return $this->hasMany('App\SignupInvitation');
+ }
+
+ /*
+ * Returns the wallet of the tanant (reseller's wallet).
+ *
+ * @return ?\App\Wallet A wallet object
+ */
+ public function wallet(): ?Wallet
+ {
+ $user = \App\User::where('role', 'reseller')->where('tenant_id', $this->id)->first();
+
+ return $user ? $user->wallets->first() : null;
+ }
+}
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -20,6 +20,7 @@
* @property int $id
* @property string $password
* @property int $status
+ * @property int $tenant_id
*/
class User extends Authenticatable implements JWTSubject
{
@@ -56,7 +57,7 @@
'email',
'password',
'password_ldap',
- 'status'
+ 'status',
];
/**
@@ -125,6 +126,7 @@
'wallet_id' => $wallet_id,
'sku_id' => $sku->id,
'cost' => $sku->pivot->cost(),
+ 'fee' => $sku->pivot->fee(),
'entitleable_id' => $user->id,
'entitleable_type' => User::class
]
@@ -178,6 +180,7 @@
'wallet_id' => $wallet->id,
'sku_id' => $sku->id,
'cost' => $exists >= $sku->units_free ? $sku->cost : 0,
+ 'fee' => $exists >= $sku->units_free ? $sku->fee : 0,
'entitleable_id' => $this->id,
'entitleable_type' => User::class
]);
@@ -192,7 +195,7 @@
/**
* Check if current user can delete another object.
*
- * @param \App\User|\App\Domain $object A user|domain object
+ * @param mixed $object A user|domain|wallet|group object
*
* @return bool True if he can, False otherwise
*/
@@ -213,13 +216,13 @@
/**
* Check if current user can read data of another object.
*
- * @param \App\User|\App\Domain|\App\Wallet $object A user|domain|wallet object
+ * @param mixed $object A user|domain|wallet|group object
*
* @return bool True if he can, False otherwise
*/
public function canRead($object): bool
{
- if ($this->role == "admin") {
+ if ($this->role == 'admin') {
return true;
}
@@ -227,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);
}
@@ -237,26 +252,38 @@
$wallet = $object->wallet();
- return $this->wallets->contains($wallet) || $this->accounts->contains($wallet);
+ return $wallet && ($this->wallets->contains($wallet) || $this->accounts->contains($wallet));
}
/**
* Check if current user can update data of another object.
*
- * @param \App\User|\App\Domain $object A user|domain object
+ * @param mixed $object A user|domain|wallet|group object
*
* @return bool True if he can, False otherwise
*/
public function canUpdate($object): bool
{
- if (!method_exists($object, 'wallet')) {
- return false;
+ if ($object instanceof User && $this->id == $object->id) {
+ return true;
}
- if ($object instanceof User && $this->id == $object->id) {
+ if ($this->role == 'admin') {
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;
+ }
+
return $this->canDelete($object);
}
@@ -276,12 +303,19 @@
/**
* List the domains to which this user is entitled.
+ * Note: Active public domains are also returned (for the user tenant).
*
- * @return Domain[]
+ * @return Domain[] List of Domain objects
*/
- public function domains()
+ public function domains(): array
{
- $domains = Domain::whereRaw(sprintf('(type & %s)', Domain::TYPE_PUBLIC))
+ if ($this->tenant_id) {
+ $domains = Domain::where('tenant_id', $this->tenant_id);
+ } else {
+ $domains = Domain::withEnvTenant();
+ }
+
+ $domains = $domains->whereRaw(sprintf('(type & %s)', Domain::TYPE_PUBLIC))
->whereRaw(sprintf('(status & %s)', Domain::STATUS_ACTIVE))
->get()
->all();
@@ -579,6 +613,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
@@ -363,7 +363,6 @@
$countries = include resource_path('countries.php');
$req_domain = preg_replace('/:[0-9]+$/', '', request()->getHttpHost());
$sys_domain = \config('app.domain');
- $path = request()->path();
$opts = [
'app.name',
'app.url',
@@ -382,6 +381,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/app/Wallet.php b/src/app/Wallet.php
--- a/src/app/Wallet.php
+++ b/src/app/Wallet.php
@@ -76,6 +76,7 @@
return 0;
}
+ $profit = 0;
$charges = 0;
$discount = $this->getDiscountRate();
@@ -101,8 +102,10 @@
$diff = $entitlement->updated_at->diffInMonths(Carbon::now());
$cost = (int) ($entitlement->cost * $discount * $diff);
+ $fee = (int) ($entitlement->fee * $diff);
$charges += $cost;
+ $profit += $cost - $fee;
// if we're in dry-run, you know...
if (!$apply) {
@@ -126,7 +129,17 @@
}
if ($apply) {
- $this->debit($charges, $entitlementTransactions);
+ $this->debit($charges, '', $entitlementTransactions);
+
+ // Credit/debit the reseller
+ if ($profit != 0 && $this->owner->tenant) {
+ // FIXME: Should we have a simpler way to skip this for non-reseller tenant(s)
+ if ($wallet = $this->owner->tenant->wallet()) {
+ $desc = "Charged user {$this->owner->email}";
+ $method = $profit > 0 ? 'credit' : 'debit';
+ $wallet->{$method}(abs($profit), $desc);
+ }
+ }
}
DB::commit();
@@ -234,12 +247,13 @@
/**
* Deduct an amount of pecunia from this wallet's balance.
*
- * @param int $amount The amount of pecunia to deduct (in cents).
- * @param array $eTIDs List of transaction IDs for the individual entitlements that make up
- * this debit record, if any.
+ * @param int $amount The amount of pecunia to deduct (in cents).
+ * @param string $description The transaction description
+ * @param array $eTIDs List of transaction IDs for the individual entitlements
+ * that make up this debit record, if any.
* @return Wallet Self
*/
- public function debit(int $amount, array $eTIDs = []): Wallet
+ public function debit(int $amount, string $description = '', array $eTIDs = []): Wallet
{
if ($amount == 0) {
return $this;
@@ -254,11 +268,14 @@
'object_id' => $this->id,
'object_type' => \App\Wallet::class,
'type' => \App\Transaction::WALLET_DEBIT,
- 'amount' => $amount * -1
+ 'amount' => $amount * -1,
+ 'description' => $description
]
);
- \App\Transaction::whereIn('id', $eTIDs)->update(['transaction_id' => $transaction->id]);
+ if (!empty($eTIDs)) {
+ \App\Transaction::whereIn('id', $eTIDs)->update(['transaction_id' => $transaction->id]);
+ }
return $this;
}
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/config/database.php b/src/config/database.php
--- a/src/config/database.php
+++ b/src/config/database.php
@@ -62,6 +62,7 @@
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
+ 'timezone' => '+00:00',
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
diff --git a/src/database/migrations/2019_12_10_100355_create_package_skus_table.php b/src/database/migrations/2019_12_10_100355_create_package_skus_table.php
--- a/src/database/migrations/2019_12_10_100355_create_package_skus_table.php
+++ b/src/database/migrations/2019_12_10_100355_create_package_skus_table.php
@@ -21,7 +21,6 @@
$table->string('package_id', 36);
$table->string('sku_id', 36);
$table->integer('qty')->default(1);
-
$table->integer('cost')->default(0)->nullable();
$table->foreign('package_id')->references('id')->on('packages')
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,84 @@
+bigIncrements('id');
+ $table->string('title', 32);
+ $table->timestamps();
+ }
+ );
+
+ \App\Tenant::create(['title' => 'Kolab Now']);
+
+ foreach (['users', 'discounts', 'domains', 'plans', 'packages', 'skus'] as $table_name) {
+ Schema::table(
+ $table_name,
+ function (Blueprint $table) {
+ $table->bigInteger('tenant_id')->unsigned()->nullable();
+ $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('set null');
+ }
+ );
+
+ if ($tenant_id = \config('app.tenant_id')) {
+ DB::statement("UPDATE `{$table_name}` SET `tenant_id` = {$tenant_id}");
+ }
+ }
+
+ // Add fee column
+ foreach (['entitlements', 'skus'] as $table) {
+ Schema::table(
+ $table,
+ function (Blueprint $table) {
+ $table->integer('fee')->nullable();
+ }
+ );
+ }
+
+ // FIXME: Should we also have package_skus.fee ?
+ // We have package_skus.cost, but I think it is not used anywhere.
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ foreach (['users', 'discounts', 'domains', 'plans', 'packages', 'skus'] as $table_name) {
+ Schema::table(
+ $table_name,
+ function (Blueprint $table) {
+ $table->dropForeign(['tenant_id']);
+ $table->dropColumn('tenant_id');
+ }
+ );
+ }
+
+ foreach (['entitlements', 'skus'] as $table) {
+ Schema::table(
+ $table,
+ function (Blueprint $table) {
+ $table->dropColumn('fee');
+ }
+ );
+ }
+
+ Schema::dropIfExists('tenants');
+ }
+}
diff --git a/src/database/migrations/2021_01_26_150000_change_sku_descriptions.php b/src/database/migrations/2021_01_26_150000_change_sku_descriptions.php
--- a/src/database/migrations/2021_01_26_150000_change_sku_descriptions.php
+++ b/src/database/migrations/2021_01_26_150000_change_sku_descriptions.php
@@ -15,14 +15,20 @@
public function up()
{
$beta_sku = \App\Sku::where('title', 'beta')->first();
- $beta_sku->name = 'Private Beta (invitation only)';
- $beta_sku->description = 'Access to the private beta program subscriptions';
- $beta_sku->save();
+
+ if ($beta_sku) {
+ $beta_sku->name = 'Private Beta (invitation only)';
+ $beta_sku->description = 'Access to the private beta program subscriptions';
+ $beta_sku->save();
+ }
$meet_sku = \App\Sku::where('title', 'meet')->first();
- $meet_sku->name = 'Voice & Video Conferencing (public beta)';
- $meet_sku->handler_class = 'App\Handlers\Meet';
- $meet_sku->save();
+
+ if ($meet_sku) {
+ $meet_sku->name = 'Voice & Video Conferencing (public beta)';
+ $meet_sku->handler_class = 'App\Handlers\Meet';
+ $meet_sku->save();
+ }
}
/**
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/migrations/2021_05_12_150000_groups_add_tenant_id.php b/src/database/migrations/2021_05_12_150000_groups_add_tenant_id.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2021_05_12_150000_groups_add_tenant_id.php
@@ -0,0 +1,46 @@
+bigInteger('tenant_id')->unsigned()->nullable();
+ $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('set null');
+ }
+ );
+
+ if ($tenant_id = \config('app.tenant_id')) {
+ DB::statement("UPDATE `groups` SET `tenant_id` = {$tenant_id}");
+ }
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table(
+ 'groups',
+ function (Blueprint $table) {
+ $table->dropForeign(['tenant_id']);
+ $table->dropColumn('tenant_id');
+ }
+ );
+ }
+}
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'
+ ]);
+ }
+
+ if (!Tenant::find(2)) {
+ 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/phpstan.neon b/src/phpstan.neon
--- a/src/phpstan.neon
+++ b/src/phpstan.neon
@@ -4,8 +4,12 @@
ignoreErrors:
- '#Access to an undefined property Illuminate\\Contracts\\Auth\\Authenticatable#'
- '#Access to an undefined property [a-zA-Z\\]+::\$pivot#'
+ - '#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,11 +1,11 @@
-import DashboardComponent from '../vue/Admin/Dashboard'
-import DistlistComponent from '../vue/Admin/Distlist'
-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 DistlistComponent from '../../vue/Admin/Distlist'
+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
@@ -79,7 +79,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']
@@ -462,7 +462,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,11 +1,13 @@
-import DashboardComponent from '../vue/Admin/Dashboard'
-import DistlistComponent from '../vue/Admin/Distlist'
-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 DistlistComponent from '../../vue/Admin/Distlist'
+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'
+import WalletComponent from '../../vue/Wallet'
const routes = [
{
@@ -40,6 +42,12 @@
name: 'logout',
component: LogoutComponent
},
+ {
+ path: '/invitations',
+ name: 'invitations',
+ component: InvitationsComponent,
+ meta: { requiresAuth: true }
+ },
{
path: '/stats',
name: 'stats',
@@ -52,6 +60,12 @@
component: UserComponent,
meta: { requiresAuth: true }
},
+ {
+ path: '/wallet',
+ name: 'wallet',
+ component: WalletComponent,
+ meta: { requiresAuth: true }
+ },
{
name: '404',
path: '*',
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,24 +1,24 @@
-import DashboardComponent from '../vue/Dashboard'
-import DistlistInfoComponent from '../vue/Distlist/Info'
-import DistlistListComponent from '../vue/Distlist/List'
-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 DistlistInfoComponent from '../../vue/Distlist/Info'
+import DistlistListComponent from '../../vue/Distlist/List'
+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 = [
{
@@ -90,6 +90,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
@@ -57,6 +57,12 @@
'search-foundxgroups' => ':x distribution lists 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' => "Hi,",
+ 'signupinvitation-body1' => "You have been invited to join :site. Click the link below to sign up.",
+ 'signupinvitation-body2' => "",
+
'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
@@ -123,7 +123,10 @@
}
table {
- td.buttons,
+ th {
+ white-space: nowrap;
+ }
+
td.email,
td.price,
td.datetime,
@@ -132,6 +135,7 @@
white-space: nowrap;
}
+ td.buttons,
th.price,
td.price {
width: 1%;
@@ -287,6 +291,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 @@
{
"label": "explore",
"location": "https://kolabnow.com/",
- "admin": true
+ "admin": true,
+ "reseller": true
},
{
"label": "blog",
"location": "https://blogs.kolabnow.com/",
- "admin": true
+ "admin": true,
+ "reseller": true
},
{
"label": "support",
"location": "/support",
"page": "support",
- "admin": true
+ "admin": true,
+ "reseller": true
},
{
"label": "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/Admin/Stats.vue b/src/resources/vue/Admin/Stats.vue
--- a/src/resources/vue/Admin/Stats.vue
+++ b/src/resources/vue/Admin/Stats.vue
@@ -9,11 +9,12 @@
export default {
data() {
return {
- charts: {}
+ charts: {},
+ chartTypes: ['users', 'users-all', 'income', 'discounts']
}
},
mounted() {
- ['users', 'users-all', 'income', 'discounts'].forEach(chart => this.loadChart(chart))
+ this.chartTypes.forEach(chart => this.loadChart(chart))
},
methods: {
drawChart(name, data) {
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,51 @@
+
+
+
+
+
+ Wallet
+ {{ $root.price(balance) }}
+
+
+ Invitations
+
+
+ Stats
+
+
+
+
+
+
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/Reseller/Stats.vue b/src/resources/vue/Reseller/Stats.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/Reseller/Stats.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+
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($('