Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2513163
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
22 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
R2 kolab
Attached
Detach File
Event Timeline
Log In to Comment