Page MenuHomePhorge

No OneTemporary

Size
22 KB
Referenced Files
None
Subscribers
None
diff --git a/src/app/Console/Commands/Tenant/CreateCommand.php b/src/app/Console/Commands/Tenant/CreateCommand.php
index 73023c06..859d7f27 100644
--- a/src/app/Console/Commands/Tenant/CreateCommand.php
+++ b/src/app/Console/Commands/Tenant/CreateCommand.php
@@ -1,166 +1,166 @@
<?php
namespace App\Console\Commands\Tenant;
use App\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
class CreateCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'tenant:create {user} {--title=}';
/**
* The console command description.
*
* @var string
*/
protected $description = "Create a tenant (with a set of SKUs/plans/packages) and make the user a reseller.";
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$user = $this->getUser($this->argument('user'));
if (!$user) {
$this->error('User not found.');
return 1;
}
DB::beginTransaction();
// Create a tenant
$tenant = \App\Tenant::create(['title' => $this->option('title') ?: $user->name()]);
// Clone plans, packages, skus for the tenant
$sku_map = [];
- \App\Sku::withEnvTenant()->where('active', true)->get()
+ \App\Sku::withEnvTenantContext()->where('active', true)->get()
->each(function ($sku) use ($sku_map, $tenant) {
$sku_new = \App\Sku::create([
'title' => $sku->title,
'name' => $sku->getTranslations('name'),
'description' => $sku->getTranslations('description'),
'cost' => $sku->cost,
'units_free' => $sku->units_free,
'period' => $sku->period,
'handler_class' => $sku->handler_class,
'active' => true,
'fee' => $sku->fee,
]);
$sku_new->tenant_id = $tenant->id;
$sku_new->save();
$sku_map[$sku->id] = $sku_new->id;
});
$plan_map = [];
- \App\Plan::withEnvTenant()->get()
+ \App\Plan::withEnvTenantContext()->get()
->each(function ($plan) use ($plan_map, $tenant) {
$plan_new = \App\Plan::create([
'title' => $plan->title,
'name' => $plan->getTranslations('name'),
'description' => $plan->getTranslations('description'),
'promo_from' => $plan->promo_from,
'promo_to' => $plan->promo_to,
'qty_min' => $plan->qty_min,
'qty_max' => $plan->qty_max,
'discount_qty' => $plan->discount_qty,
'discount_rate' => $plan->discount_rate,
]);
$plan_new->tenant_id = $tenant->id;
$plan_new->save();
$plan_map[$plan->id] = $plan_new->id;
});
$package_map = [];
- \App\Package::withEnvTenant()->get()
+ \App\Package::withEnvTenantContext()->get()
->each(function ($package) use ($package_map, $tenant) {
$package_new = \App\Package::create([
'title' => $package->title,
'name' => $package->getTranslations('name'),
'description' => $package->getTranslations('description'),
'discount_rate' => $package->discount_rate,
]);
$package_new->tenant_id = $tenant->id;
$package_new->save();
$package_map[$package->id] = $package_new->id;
});
DB::table('package_skus')->whereIn('package_id', array_keys($package_map))->get()
->each(function ($item) use ($package_map, $sku_map) {
if (isset($sku_map[$item->sku_id])) {
DB::table('package_skus')->insert([
'qty' => $item->qty,
'cost' => $item->cost,
'sku_id' => $sku_map[$item->sku_id],
'package_id' => $package_map[$item->package_id],
]);
}
});
DB::table('plan_packages')->whereIn('plan_id', array_keys($plan_map))->get()
->each(function ($item) use ($package_map, $plan_map) {
if (isset($package_map[$item->package_id])) {
DB::table('plan_packages')->insert([
'qty' => $item->qty,
'qty_min' => $item->qty_min,
'qty_max' => $item->qty_max,
'discount_qty' => $item->discount_qty,
'discount_rate' => $item->discount_rate,
'plan_id' => $plan_map[$item->plan_id],
'package_id' => $package_map[$item->package_id],
]);
}
});
// Disable jobs, they would fail anyway as the TENANT_ID is different
// TODO: We could probably do config(['app.tenant' => $tenant->id]) here
Queue::fake();
// Assign 'reseller' role to the user
$user->role = 'reseller';
$user->tenant_id = $tenant->id;
$user->save();
// Switch tenant_id for all of the user belongings
$user->wallets->each(function ($wallet) use ($tenant) {
$wallet->entitlements->each(function ($entitlement) use ($tenant) {
$entitlement->entitleable->tenant_id = $tenant->id;
$entitlement->entitleable->save();
// TODO: If user already has any entitlements, they will have to be
// removed/replaced by SKUs in the newly created tenant
// I think we don't really support this yet anyway.
});
// TODO: If the wallet has a discount we should remove/replace it too
// I think we don't really support this yet anyway.
});
DB::commit();
// Make sure the transaction wasn't aborted
$tenant = \App\Tenant::find($tenant->id);
if (!$tenant) {
$this->error("Failed to create a tenant.");
return 1;
}
$this->info("Created tenant {$tenant->id}.");
}
}
diff --git a/src/app/Http/Controllers/API/V4/DomainsController.php b/src/app/Http/Controllers/API/V4/DomainsController.php
index 790e8033..a288ffe0 100644
--- a/src/app/Http/Controllers/API/V4/DomainsController.php
+++ b/src/app/Http/Controllers/API/V4/DomainsController.php
@@ -1,425 +1,425 @@
<?php
namespace App\Http\Controllers\API\V4;
use App\Domain;
use App\Http\Controllers\Controller;
use App\Backends\LDAP;
use Carbon\Carbon;
use Illuminate\Http\Request;
class DomainsController extends Controller
{
/**
* Return a list of domains owned by the current user
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
$user = $this->guard()->user();
$list = [];
foreach ($user->domains() as $domain) {
if (!$domain->isPublic()) {
$data = $domain->toArray();
$data = array_merge($data, self::domainStatuses($domain));
$list[] = $data;
}
}
return response()->json($list);
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\JsonResponse
*/
public function create()
{
return $this->errorResponse(404);
}
/**
* Confirm ownership of the specified domain (via DNS check).
*
* @param int $id Domain identifier
*
* @return \Illuminate\Http\JsonResponse|void
*/
public function confirm($id)
{
$domain = Domain::find($id);
if (!$this->checkTenant($domain)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canRead($domain)) {
return $this->errorResponse(403);
}
if (!$domain->confirm()) {
return response()->json([
'status' => 'error',
'message' => \trans('app.domain-verify-error'),
]);
}
return response()->json([
'status' => 'success',
'statusInfo' => self::statusInfo($domain),
'message' => \trans('app.domain-verify-success'),
]);
}
/**
* Remove the specified resource from storage.
*
* @param int $id
*
* @return \Illuminate\Http\JsonResponse
*/
public function destroy($id)
{
return $this->errorResponse(404);
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
*
* @return \Illuminate\Http\JsonResponse
*/
public function edit($id)
{
return $this->errorResponse(404);
}
/**
* Set the domain configuration.
*
* @param int $id Domain identifier
*
* @return \Illuminate\Http\JsonResponse|void
*/
public function setConfig($id)
{
$domain = Domain::find($id);
if (empty($domain)) {
return $this->errorResponse(404);
}
// Only owner (or admin) has access to the domain
if (!$this->guard()->user()->canRead($domain)) {
return $this->errorResponse(403);
}
$errors = $domain->setConfig(request()->input());
if (!empty($errors)) {
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
return response()->json([
'status' => 'success',
'message' => \trans('app.domain-setconfig-success'),
]);
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
*
* @return \Illuminate\Http\JsonResponse
*/
public function store(Request $request)
{
return $this->errorResponse(404);
}
/**
* Get the information about the specified domain.
*
* @param int $id Domain identifier
*
* @return \Illuminate\Http\JsonResponse|void
*/
public function show($id)
{
$domain = Domain::find($id);
if (!$this->checkTenant($domain)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canRead($domain)) {
return $this->errorResponse(403);
}
$response = $domain->toArray();
// Add hash information to the response
$response['hash_text'] = $domain->hash(Domain::HASH_TEXT);
$response['hash_cname'] = $domain->hash(Domain::HASH_CNAME);
$response['hash_code'] = $domain->hash(Domain::HASH_CODE);
// Add DNS/MX configuration for the domain
$response['dns'] = self::getDNSConfig($domain);
$response['mx'] = self::getMXConfig($domain->namespace);
// Domain configuration, e.g. spf whitelist
$response['config'] = $domain->getConfig();
// Status info
$response['statusInfo'] = self::statusInfo($domain);
$response = array_merge($response, self::domainStatuses($domain));
return response()->json($response);
}
/**
* Fetch domain status (and reload setup process)
*
* @param int $id Domain identifier
*
* @return \Illuminate\Http\JsonResponse
*/
public function status($id)
{
- $domain = Domain::withEnvTenant()->findOrFail($id);
+ $domain = Domain::find($id);
if (!$this->checkTenant($domain)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canRead($domain)) {
return $this->errorResponse(403);
}
$response = self::statusInfo($domain);
if (!empty(request()->input('refresh'))) {
$updated = false;
$last_step = 'none';
foreach ($response['process'] as $idx => $step) {
$last_step = $step['label'];
if (!$step['state']) {
if (!$this->execProcessStep($domain, $step['label'])) {
break;
}
$updated = true;
}
}
if ($updated) {
$response = self::statusInfo($domain);
}
$success = $response['isReady'];
$suffix = $success ? 'success' : 'error-' . $last_step;
$response['status'] = $success ? 'success' : 'error';
$response['message'] = \trans('app.process-' . $suffix);
}
$response = array_merge($response, self::domainStatuses($domain));
return response()->json($response);
}
/**
* 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);
}
/**
* Provide DNS MX information to configure specified domain for
*/
protected static function getMXConfig(string $namespace): array
{
$entries = [];
// copy MX entries from an existing domain
if ($master = \config('dns.copyfrom')) {
// TODO: cache this lookup
foreach ((array) dns_get_record($master, DNS_MX) as $entry) {
$entries[] = sprintf(
"@\t%s\t%s\tMX\t%d %s.",
\config('dns.ttl', $entry['ttl']),
$entry['class'],
$entry['pri'],
$entry['target']
);
}
} elseif ($static = \config('dns.static')) {
$entries[] = strtr($static, array('\n' => "\n", '%s' => $namespace));
}
// display SPF settings
if ($spf = \config('dns.spf')) {
$entries[] = ';';
foreach (['TXT', 'SPF'] as $type) {
$entries[] = sprintf(
"@\t%s\tIN\t%s\t\"%s\"",
\config('dns.ttl'),
$type,
$spf
);
}
}
return $entries;
}
/**
* Provide sample DNS config for domain confirmation
*/
protected static function getDNSConfig(Domain $domain): array
{
$serial = date('Ymd01');
$hash_txt = $domain->hash(Domain::HASH_TEXT);
$hash_cname = $domain->hash(Domain::HASH_CNAME);
$hash = $domain->hash(Domain::HASH_CODE);
return [
"@ IN SOA ns1.dnsservice.com. hostmaster.{$domain->namespace}. (",
" {$serial} 10800 3600 604800 86400 )",
";",
"@ IN A <some-ip>",
"www IN A <some-ip>",
";",
"{$hash_cname}.{$domain->namespace}. IN CNAME {$hash}.{$domain->namespace}.",
"@ 3600 TXT \"{$hash_txt}\"",
];
}
/**
* Prepare domain statuses for the UI
*
* @param \App\Domain $domain Domain object
*
* @return array Statuses array
*/
protected static function domainStatuses(Domain $domain): array
{
return [
'isLdapReady' => $domain->isLdapReady(),
'isConfirmed' => $domain->isConfirmed(),
'isVerified' => $domain->isVerified(),
'isSuspended' => $domain->isSuspended(),
'isActive' => $domain->isActive(),
'isDeleted' => $domain->isDeleted() || $domain->trashed(),
];
}
/**
* Domain status (extended) information.
*
* @param \App\Domain $domain Domain object
*
* @return array Status information
*/
public static function statusInfo(Domain $domain): array
{
$process = [];
// If that is not a public domain, add domain specific steps
$steps = [
'domain-new' => true,
'domain-ldap-ready' => $domain->isLdapReady(),
'domain-verified' => $domain->isVerified(),
'domain-confirmed' => $domain->isConfirmed(),
];
$count = count($steps);
// Create a process check list
foreach ($steps as $step_name => $state) {
$step = [
'label' => $step_name,
'title' => \trans("app.process-{$step_name}"),
'state' => $state,
];
if ($step_name == 'domain-confirmed' && !$state) {
$step['link'] = "/domain/{$domain->id}";
}
$process[] = $step;
if ($state) {
$count--;
}
}
$state = $count === 0 ? 'done' : 'running';
// After 180 seconds assume the process is in failed state,
// this should unlock the Refresh button in the UI
if ($count > 0 && $domain->created_at->diffInSeconds(Carbon::now()) > 180) {
$state = 'failed';
}
return [
'process' => $process,
'processState' => $state,
'isReady' => $count === 0,
];
}
/**
* Execute (synchronously) specified step in a domain setup process.
*
* @param \App\Domain $domain Domain object
* @param string $step Step identifier (as in self::statusInfo())
*
* @return bool True if the execution succeeded, False otherwise
*/
public static function execProcessStep(Domain $domain, string $step): bool
{
try {
switch ($step) {
case 'domain-ldap-ready':
// Domain not in LDAP, create it
if (!$domain->isLdapReady()) {
LDAP::createDomain($domain);
$domain->status |= Domain::STATUS_LDAP_READY;
$domain->save();
}
return $domain->isLdapReady();
case 'domain-verified':
// Domain existence not verified
$domain->verify();
return $domain->isVerified();
case 'domain-confirmed':
// Domain ownership confirmation
$domain->confirm();
return $domain->isConfirmed();
}
} catch (\Exception $e) {
\Log::error($e);
}
return false;
}
}
diff --git a/src/tests/Feature/Console/Tenant/CreateTest.php b/src/tests/Feature/Console/Tenant/CreateTest.php
index cd6d8017..628b3779 100644
--- a/src/tests/Feature/Console/Tenant/CreateTest.php
+++ b/src/tests/Feature/Console/Tenant/CreateTest.php
@@ -1,98 +1,98 @@
<?php
namespace Tests\Feature\Console\Tenant;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class CreateTest extends TestCase
{
private $tenantId;
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$this->deleteTestUser('test-tenant@kolabnow.com');
}
/**
* {@inheritDoc}
*/
public function tearDown(): void
{
if ($this->tenantId) {
Queue::fake();
\App\User::where('tenant_id', $this->tenantId)->forceDelete();
\App\Plan::where('tenant_id', $this->tenantId)->delete();
\App\Package::where('tenant_id', $this->tenantId)->delete();
\App\Sku::where('tenant_id', $this->tenantId)->delete();
\App\Tenant::find($this->tenantId)->delete();
}
parent::tearDown();
}
/**
* Test command runs
*/
public function testHandle(): void
{
Queue::fake();
// Warning: We're not using artisan() here, as this will not
// allow us to test "empty output" cases
// User not existing
$code = \Artisan::call("tenant:create unknown@user.com");
$output = trim(\Artisan::output());
$this->assertSame(1, $code);
$this->assertSame("User not found.", $output);
$user = $this->getTestUser('test-tenant@kolabnow.com');
$this->assertEmpty($user->role);
$this->assertEquals($user->tenant_id, \config('app.tenant_id'));
// User not existing
$code = \Artisan::call("tenant:create {$user->email} --title=\"Test Tenant\"");
$output = trim(\Artisan::output());
$this->assertSame(0, $code);
- $this->assertRegExp("/^Created tenant [0-9]+./", $output);
+ $this->assertMatchesRegularExpression("/^Created tenant [0-9]+./", $output);
preg_match("/^Created tenant ([0-9]+)./", $output, $matches);
$this->tenantId = $matches[1];
$tenant = \App\Tenant::find($this->tenantId);
$user->refresh();
$this->assertNotEmpty($tenant);
$this->assertSame('Test Tenant', $tenant->title);
$this->assertSame('reseller', $user->role);
$this->assertSame($tenant->id, $user->tenant_id);
// Assert cloned SKUs
$skus = \App\Sku::where('tenant_id', \config('app.tenant_id'))->where('active', true);
$skus->each(function ($sku) use ($tenant) {
$sku_new = \App\Sku::where('tenant_id', $tenant->id)
->where('title', $sku->title)->get();
$this->assertSame(1, $sku_new->count());
$sku_new = $sku_new->first();
$this->assertSame($sku->name, $sku_new->name);
$this->assertSame($sku->description, $sku_new->description);
$this->assertSame($sku->cost, $sku_new->cost);
$this->assertSame($sku->units_free, $sku_new->units_free);
$this->assertSame($sku->period, $sku_new->period);
$this->assertSame($sku->handler_class, $sku_new->handler_class);
$this->assertNotEmpty($sku_new->active);
});
// TODO: Plans, packages
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Apr 18, 10:07 AM (6 h, 13 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
436163
Default Alt Text
(22 KB)

Event Timeline