Page MenuHomePhorge

No OneTemporary

Size
148 KB
Referenced Files
None
Subscribers
None
diff --git a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
index e1b4b78d..06b7962a 100644
--- a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
@@ -1,128 +1,128 @@
<?php
namespace App\Http\Controllers\API\V4\Admin;
use App\Domain;
use App\User;
use Illuminate\Http\Request;
class DomainsController extends \App\Http\Controllers\API\V4\DomainsController
{
/**
* Remove the specified domain.
*
- * @param int $id Domain identifier
+ * @param string $id Domain identifier
*
* @return \Illuminate\Http\JsonResponse
*/
public function destroy($id)
{
return $this->errorResponse(404);
}
/**
* Search for domains
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
$search = trim(request()->input('search'));
$owner = trim(request()->input('owner'));
$result = collect([]);
if ($owner) {
if ($owner = User::find($owner)) {
foreach ($owner->wallets as $wallet) {
$entitlements = $wallet->entitlements()->where('entitleable_type', Domain::class)->get();
foreach ($entitlements as $entitlement) {
$domain = $entitlement->entitleable;
$result->push($domain);
}
}
$result = $result->sortBy('namespace')->values();
}
} elseif (!empty($search)) {
if ($domain = Domain::where('namespace', $search)->first()) {
$result->push($domain);
}
}
// Process the result
$result = $result->map(
function ($domain) {
return $this->objectToClient($domain);
}
);
$result = [
'list' => $result,
'count' => count($result),
'message' => \trans('app.search-foundxdomains', ['x' => count($result)]),
];
return response()->json($result);
}
/**
* Create a domain.
*
* @param \Illuminate\Http\Request $request
*
* @return \Illuminate\Http\JsonResponse
*/
public function store(Request $request)
{
return $this->errorResponse(404);
}
/**
* Suspend the domain
*
* @param \Illuminate\Http\Request $request The API request.
* @param string $id Domain identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function suspend(Request $request, $id)
{
$domain = Domain::find($id);
if (!$this->checkTenant($domain) || $domain->isPublic()) {
return $this->errorResponse(404);
}
$domain->suspend();
return response()->json([
'status' => 'success',
'message' => \trans('app.domain-suspend-success'),
]);
}
/**
* Un-Suspend the domain
*
* @param \Illuminate\Http\Request $request The API request.
* @param string $id Domain identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function unsuspend(Request $request, $id)
{
$domain = Domain::find($id);
if (!$this->checkTenant($domain) || $domain->isPublic()) {
return $this->errorResponse(404);
}
$domain->unsuspend();
return response()->json([
'status' => 'success',
'message' => \trans('app.domain-unsuspend-success'),
]);
}
}
diff --git a/src/app/Http/Controllers/API/V4/Admin/UsersController.php b/src/app/Http/Controllers/API/V4/Admin/UsersController.php
index d6665bbb..57286355 100644
--- a/src/app/Http/Controllers/API/V4/Admin/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/UsersController.php
@@ -1,382 +1,344 @@
<?php
namespace App\Http\Controllers\API\V4\Admin;
use App\Domain;
use App\Sku;
use App\User;
use App\Wallet;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class UsersController extends \App\Http\Controllers\API\V4\UsersController
{
/**
* Delete a user.
*
- * @param int $id User identifier
+ * @param string $id User identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function destroy($id)
{
return $this->errorResponse(404);
}
/**
* Searching of user accounts.
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
$search = trim(request()->input('search'));
$owner = trim(request()->input('owner'));
$result = collect([]);
if ($owner) {
$owner = User::find($owner);
if ($owner) {
$result = $owner->users(false)->orderBy('email')->get();
}
} elseif (strpos($search, '@')) {
// Search by email
$result = User::withTrashed()->where('email', $search)
->orderBy('email')
->get();
if ($result->isEmpty()) {
// Search by an alias
$user_ids = \App\UserAlias::where('alias', $search)->get()->pluck('user_id');
// Search by an external email
$ext_user_ids = \App\UserSetting::where('key', 'external_email')
->where('value', $search)
->get()
->pluck('user_id');
$user_ids = $user_ids->merge($ext_user_ids)->unique();
// Search by a distribution list or resource email
if ($group = \App\Group::withTrashed()->where('email', $search)->first()) {
$user_ids = $user_ids->merge([$group->wallet()->user_id])->unique();
} elseif ($resource = \App\Resource::withTrashed()->where('email', $search)->first()) {
$user_ids = $user_ids->merge([$resource->wallet()->user_id])->unique();
} elseif ($folder = \App\SharedFolder::withTrashed()->where('email', $search)->first()) {
$user_ids = $user_ids->merge([$folder->wallet()->user_id])->unique();
}
if (!$user_ids->isEmpty()) {
$result = User::withTrashed()->whereIn('id', $user_ids)
->orderBy('email')
->get();
}
}
} elseif (is_numeric($search)) {
// Search by user ID
$user = User::withTrashed()->where('id', $search)
->first();
if ($user) {
$result->push($user);
}
} elseif (strpos($search, '.') !== false) {
// Search by domain
$domain = Domain::withTrashed()->where('namespace', $search)
->first();
if ($domain) {
if (($wallet = $domain->wallet()) && ($owner = $wallet->owner()->withTrashed()->first())) {
$result->push($owner);
}
}
// A mollie customer ID
} elseif (substr($search, 0, 4) == 'cst_') {
$setting = \App\WalletSetting::where(
[
'key' => 'mollie_id',
'value' => $search
]
)->first();
if ($setting) {
if ($wallet = $setting->wallet) {
if ($owner = $wallet->owner()->withTrashed()->first()) {
$result->push($owner);
}
}
}
// A mollie transaction ID
} elseif (substr($search, 0, 3) == 'tr_') {
$payment = \App\Payment::find($search);
if ($payment) {
if ($owner = $payment->wallet->owner()->withTrashed()->first()) {
$result->push($owner);
}
}
} elseif (!empty($search)) {
$wallet = Wallet::find($search);
if ($wallet) {
if ($owner = $wallet->owner()->withTrashed()->first()) {
$result->push($owner);
}
}
}
// Process the result
$result = $result->map(
function ($user) {
return $this->objectToClient($user, true);
}
);
$result = [
'list' => $result,
'count' => count($result),
'message' => \trans('app.search-foundxusers', ['x' => count($result)]),
];
return response()->json($result);
}
/**
* Reset 2-Factor Authentication for the user
*
* @param \Illuminate\Http\Request $request The API request.
* @param string $id User identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function reset2FA(Request $request, $id)
{
$user = User::find($id);
if (!$this->checkTenant($user)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canUpdate($user)) {
return $this->errorResponse(403);
}
$sku = Sku::withObjectTenantContext($user)->where('title', '2fa')->first();
// Note: we do select first, so the observer can delete
// 2FA preferences from Roundcube database, so don't
// be tempted to replace first() with delete() below
$entitlement = $user->entitlements()->where('sku_id', $sku->id)->first();
$entitlement->delete();
return response()->json([
'status' => 'success',
'message' => \trans('app.user-reset-2fa-success'),
]);
}
/**
* Set/Add a SKU for the user
*
* @param \Illuminate\Http\Request $request The API request.
* @param string $id User identifier
* @param string $sku SKU title
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function setSku(Request $request, $id, $sku)
{
// For now we allow adding the 'beta' SKU only
if ($sku != 'beta') {
return $this->errorResponse(404);
}
$user = User::find($id);
if (!$this->checkTenant($user)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canUpdate($user)) {
return $this->errorResponse(403);
}
$sku = Sku::withObjectTenantContext($user)->where('title', $sku)->first();
if (!$sku) {
return $this->errorResponse(404);
}
if ($user->entitlements()->where('sku_id', $sku->id)->first()) {
return $this->errorResponse(422, \trans('app.user-set-sku-already-exists'));
}
$user->assignSku($sku);
$entitlement = $user->entitlements()->where('sku_id', $sku->id)->first();
return response()->json([
'status' => 'success',
'message' => \trans('app.user-set-sku-success'),
'sku' => [
'cost' => $entitlement->cost,
'name' => $sku->name,
'id' => $sku->id,
]
]);
}
- /**
- * Display information on the user account specified by $id.
- *
- * @param int $id The account to show information for.
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function show($id)
- {
- $user = User::find($id);
-
- if (!$this->checkTenant($user)) {
- return $this->errorResponse(404);
- }
-
- if (!$this->guard()->user()->canRead($user)) {
- return $this->errorResponse(403);
- }
-
- $response = $this->userResponse($user);
-
- // Simplified Entitlement/SKU information,
- // TODO: I agree this format may need to be extended in future
- $response['skus'] = [];
- foreach ($user->entitlements as $ent) {
- $sku = $ent->sku;
- if (!isset($response['skus'][$sku->id])) {
- $response['skus'][$sku->id] = ['costs' => [], 'count' => 0];
- }
- $response['skus'][$sku->id]['count']++;
- $response['skus'][$sku->id]['costs'][] = $ent->cost;
- }
-
- $response['config'] = $user->getConfig();
-
- return response()->json($response);
- }
-
/**
* 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
*
* @param \Illuminate\Http\Request $request The API request.
* @param string $id User identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function suspend(Request $request, $id)
{
$user = User::find($id);
if (!$this->checkTenant($user)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canUpdate($user)) {
return $this->errorResponse(403);
}
$user->suspend();
return response()->json([
'status' => 'success',
'message' => \trans('app.user-suspend-success'),
]);
}
/**
* Un-Suspend the user
*
* @param \Illuminate\Http\Request $request The API request.
* @param string $id User identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function unsuspend(Request $request, $id)
{
$user = User::find($id);
if (!$this->checkTenant($user)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canUpdate($user)) {
return $this->errorResponse(403);
}
$user->unsuspend();
return response()->json([
'status' => 'success',
'message' => \trans('app.user-unsuspend-success'),
]);
}
/**
* Update user data.
*
* @param \Illuminate\Http\Request $request The API request.
* @param string $id User identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function update(Request $request, $id)
{
$user = User::find($id);
if (!$this->checkTenant($user)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canUpdate($user)) {
return $this->errorResponse(403);
}
// For now admins can change only user external email address
$rules = [];
if (array_key_exists('external_email', $request->input())) {
$rules['external_email'] = 'email';
}
// Validate input
$v = Validator::make($request->all(), $rules);
if ($v->fails()) {
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
}
// Update user settings
$settings = $request->only(array_keys($rules));
if (!empty($settings)) {
$user->setSettings($settings);
}
return response()->json([
'status' => 'success',
'message' => \trans('app.user-update-success'),
]);
}
}
diff --git a/src/app/Http/Controllers/API/V4/DomainsController.php b/src/app/Http/Controllers/API/V4/DomainsController.php
index 0ba683c5..ce67f32d 100644
--- a/src/app/Http/Controllers/API/V4/DomainsController.php
+++ b/src/app/Http/Controllers/API/V4/DomainsController.php
@@ -1,460 +1,360 @@
<?php
namespace App\Http\Controllers\API\V4;
use App\Domain;
-use App\Http\Controllers\Controller;
+use App\Http\Controllers\RelationController;
use App\Backends\LDAP;
use App\Rules\UserEmailDomain;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
-class DomainsController extends Controller
+class DomainsController extends RelationController
{
- /** @var array Common object properties in the API response */
- protected static $objectProps = ['namespace', 'type'];
+ /** @var string Resource localization label */
+ protected $label = 'domain';
+ /** @var string Resource model name */
+ protected $model = Domain::class;
- /**
- * Return a list of domains owned by the current user
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function index()
- {
- $user = $this->guard()->user();
+ /** @var array Common object properties in the API response */
+ protected $objectProps = ['namespace', 'type'];
- $list = $user->domains(true, false)
- ->orderBy('namespace')
- ->get()
- ->map(function ($domain) {
- return $this->objectToClient($domain);
- })
- ->all();
+ /** @var array Resource listing order (column names) */
+ protected $order = ['namespace'];
- return response()->json($list);
- }
+ /** @var array Resource relation method arguments */
+ protected $relationArgs = [true, false];
- /**
- * Show the form for creating a new domain.
- *
- * @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 domain.
*
- * @param int $id Domain identifier
+ * @param string $id Domain identifier
*
* @return \Illuminate\Http\JsonResponse
*/
public function destroy($id)
{
$domain = Domain::withEnvTenantContext()->find($id);
if (empty($domain)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canDelete($domain)) {
return $this->errorResponse(403);
}
// It is possible to delete domain only if there are no users/aliases/groups using it.
if (!$domain->isEmpty()) {
$response = ['status' => 'error', 'message' => \trans('app.domain-notempty-error')];
return response()->json($response, 422);
}
$domain->delete();
return response()->json([
'status' => 'success',
'message' => \trans('app.domain-delete-success'),
]);
}
- /**
- * Show the form for editing the specified domain.
- *
- * @param int $id Domain identifier
- *
- * @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()->canUpdate($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'),
- ]);
- }
-
/**
* Create a domain.
*
* @param \Illuminate\Http\Request $request
*
* @return \Illuminate\Http\JsonResponse
*/
public function store(Request $request)
{
$current_user = $this->guard()->user();
$owner = $current_user->wallet()->owner;
if ($owner->id != $current_user->id) {
return $this->errorResponse(403);
}
// Validate the input
$v = Validator::make(
$request->all(),
[
'namespace' => ['required', 'string', new UserEmailDomain()]
]
);
if ($v->fails()) {
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
}
$namespace = \strtolower(request()->input('namespace'));
// Domain already exists
if ($domain = Domain::withTrashed()->where('namespace', $namespace)->first()) {
// Check if the domain is soft-deleted and belongs to the same user
$deleteBeforeCreate = $domain->trashed() && ($wallet = $domain->wallet())
&& $wallet->owner && $wallet->owner->id == $owner->id;
if (!$deleteBeforeCreate) {
$errors = ['namespace' => \trans('validation.domainnotavailable')];
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
}
if (empty($request->package) || !($package = \App\Package::withEnvTenantContext()->find($request->package))) {
$errors = ['package' => \trans('validation.packagerequired')];
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
if (!$package->isDomain()) {
$errors = ['package' => \trans('validation.packageinvalid')];
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
DB::beginTransaction();
// Force-delete the existing domain if it is soft-deleted and belongs to the same user
if (!empty($deleteBeforeCreate)) {
$domain->forceDelete();
}
// Create the domain
$domain = Domain::create([
'namespace' => $namespace,
'type' => \App\Domain::TYPE_EXTERNAL,
]);
$domain->assignPackage($package, $owner);
DB::commit();
return response()->json([
'status' => 'success',
'message' => __('app.domain-create-success'),
]);
}
/**
* Get the information about the specified domain.
*
- * @param int $id Domain identifier
+ * @param string $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 = $this->objectToClient($domain, true);
// 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);
// Entitlements info
$response['skus'] = \App\Entitlement::objectEntitlementsSummary($domain);
// Some basic information about the domain wallet
$wallet = $domain->wallet();
$response['wallet'] = $wallet->toArray();
if ($wallet->discount) {
$response['wallet']['discount'] = $wallet->discount->discount;
$response['wallet']['discount_description'] = $wallet->discount->description;
}
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::find($id);
-
- if (!$this->checkTenant($domain)) {
- return $this->errorResponse(404);
- }
-
- if (!$this->guard()->user()->canRead($domain)) {
- return $this->errorResponse(403);
- }
-
- $response = $this->processStateUpdate($domain);
- $response = array_merge($response, self::objectState($domain));
-
- return response()->json($response);
- }
-
- /**
- * Update the specified domain.
- *
- * @param \Illuminate\Http\Request $request
- * @param int $id Domain identifier
- *
- * @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 objectState(Domain $domain): array
+ protected static function objectState($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
+ public static function statusInfo($domain): array
{
// If that is not a public domain, add domain specific steps
return self::processStateInfo(
$domain,
[
'domain-new' => true,
'domain-ldap-ready' => $domain->isLdapReady(),
'domain-verified' => $domain->isVerified(),
'domain-confirmed' => [$domain->isConfirmed(), "/domain/{$domain->id}"],
]
);
}
/**
* 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/app/Http/Controllers/API/V4/GroupsController.php b/src/app/Http/Controllers/API/V4/GroupsController.php
index e63d46d3..55d8baf2 100644
--- a/src/app/Http/Controllers/API/V4/GroupsController.php
+++ b/src/app/Http/Controllers/API/V4/GroupsController.php
@@ -1,488 +1,344 @@
<?php
namespace App\Http\Controllers\API\V4;
-use App\Http\Controllers\Controller;
+use App\Http\Controllers\RelationController;
use App\Domain;
use App\Group;
use App\Rules\GroupName;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
-class GroupsController extends Controller
+class GroupsController extends RelationController
{
- /** @var array Common object properties in the API response */
- protected static $objectProps = ['email', 'name'];
-
-
- /**
- * Show the form for creating a new group.
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function create()
- {
- return $this->errorResponse(404);
- }
-
- /**
- * Delete a group.
- *
- * @param int $id Group identifier
- *
- * @return \Illuminate\Http\JsonResponse The response
- */
- public function destroy($id)
- {
- $group = Group::find($id);
-
- if (!$this->checkTenant($group)) {
- return $this->errorResponse(404);
- }
-
- if (!$this->guard()->user()->canDelete($group)) {
- return $this->errorResponse(403);
- }
-
- $group->delete();
-
- return response()->json([
- 'status' => 'success',
- 'message' => \trans('app.distlist-delete-success'),
- ]);
- }
-
- /**
- * Show the form for editing the specified group.
- *
- * @param int $id Group identifier
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function edit($id)
- {
- return $this->errorResponse(404);
- }
-
- /**
- * Listing of groups belonging to the authenticated user.
- *
- * The group-entitlements billed to the current user wallet(s)
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function index()
- {
- $user = $this->guard()->user();
-
- $result = $user->groups()->orderBy('name')->orderBy('email')->get()
- ->map(function ($group) {
- return $this->objectToClient($group);
- });
-
- return response()->json($result);
- }
-
- /**
- * Set the group configuration.
- *
- * @param int $id Group identifier
- *
- * @return \Illuminate\Http\JsonResponse|void
- */
- public function setConfig($id)
- {
- $group = Group::find($id);
-
- if (!$this->checkTenant($group)) {
- return $this->errorResponse(404);
- }
-
- if (!$this->guard()->user()->canUpdate($group)) {
- return $this->errorResponse(403);
- }
-
- $errors = $group->setConfig(request()->input());
-
- if (!empty($errors)) {
- return response()->json(['status' => 'error', 'errors' => $errors], 422);
- }
+ /** @var string Resource localization label */
+ protected $label = 'distlist';
- return response()->json([
- 'status' => 'success',
- 'message' => \trans('app.distlist-setconfig-success'),
- ]);
- }
+ /** @var string Resource model name */
+ protected $model = Group::class;
- /**
- * Display information of a group specified by $id.
- *
- * @param int $id The group to show information for.
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function show($id)
- {
- $group = Group::find($id);
+ /** @var array Resource listing order (column names) */
+ protected $order = ['name', 'email'];
- if (!$this->checkTenant($group)) {
- return $this->errorResponse(404);
- }
-
- if (!$this->guard()->user()->canRead($group)) {
- return $this->errorResponse(403);
- }
-
- $response = $this->objectToClient($group, true);
-
- $response['statusInfo'] = self::statusInfo($group);
-
- // Group configuration, e.g. sender_policy
- $response['config'] = $group->getConfig();
-
- return response()->json($response);
- }
-
- /**
- * Fetch group status (and reload setup process)
- *
- * @param int $id Group identifier
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function status($id)
- {
- $group = Group::find($id);
-
- if (!$this->checkTenant($group)) {
- return $this->errorResponse(404);
- }
-
- if (!$this->guard()->user()->canRead($group)) {
- return $this->errorResponse(403);
- }
-
- $response = $this->processStateUpdate($group);
- $response = array_merge($response, self::objectState($group));
+ /** @var array Common object properties in the API response */
+ protected $objectProps = ['email', 'name'];
- return response()->json($response);
- }
/**
* Group status (extended) information
*
* @param \App\Group $group Group object
*
* @return array Status information
*/
- public static function statusInfo(Group $group): array
+ public static function statusInfo($group): array
{
return self::processStateInfo(
$group,
[
'distlist-new' => true,
'distlist-ldap-ready' => $group->isLdapReady(),
]
);
}
/**
* Create a new group record.
*
* @param \Illuminate\Http\Request $request The API request.
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function store(Request $request)
{
$current_user = $this->guard()->user();
$owner = $current_user->wallet()->owner;
if ($owner->id != $current_user->id) {
return $this->errorResponse(403);
}
$email = $request->input('email');
$members = $request->input('members');
$errors = [];
$rules = [
'name' => 'required|string|max:191',
];
// Validate group address
if ($error = GroupsController::validateGroupEmail($email, $owner)) {
$errors['email'] = $error;
} else {
list(, $domainName) = explode('@', $email);
$rules['name'] = ['required', 'string', new GroupName($owner, $domainName)];
}
// Validate the group name
$v = Validator::make($request->all(), $rules);
if ($v->fails()) {
$errors = array_merge($errors, $v->errors()->toArray());
}
// Validate members' email addresses
if (empty($members) || !is_array($members)) {
$errors['members'] = \trans('validation.listmembersrequired');
} else {
foreach ($members as $i => $member) {
if (is_string($member) && !empty($member)) {
if ($error = GroupsController::validateMemberEmail($member, $owner)) {
$errors['members'][$i] = $error;
} elseif (\strtolower($member) === \strtolower($email)) {
$errors['members'][$i] = \trans('validation.memberislist');
}
} else {
unset($members[$i]);
}
}
}
if (!empty($errors)) {
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
DB::beginTransaction();
// Create the group
$group = new Group();
$group->name = $request->input('name');
$group->email = $email;
$group->members = $members;
$group->save();
$group->assignToWallet($owner->wallets->first());
DB::commit();
return response()->json([
'status' => 'success',
'message' => \trans('app.distlist-create-success'),
]);
}
/**
* Update a group.
*
* @param \Illuminate\Http\Request $request The API request.
* @param string $id Group identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function update(Request $request, $id)
{
$group = Group::find($id);
if (!$this->checkTenant($group)) {
return $this->errorResponse(404);
}
$current_user = $this->guard()->user();
if (!$current_user->canUpdate($group)) {
return $this->errorResponse(403);
}
$owner = $group->wallet()->owner;
$name = $request->input('name');
$members = $request->input('members');
$errors = [];
// Validate the group name
if ($name !== null && $name != $group->name) {
list(, $domainName) = explode('@', $group->email);
$rules = ['name' => ['required', 'string', new GroupName($owner, $domainName)]];
$v = Validator::make($request->all(), $rules);
if ($v->fails()) {
$errors = array_merge($errors, $v->errors()->toArray());
} else {
$group->name = $name;
}
}
// Validate members' email addresses
if (empty($members) || !is_array($members)) {
$errors['members'] = \trans('validation.listmembersrequired');
} else {
foreach ((array) $members as $i => $member) {
if (is_string($member) && !empty($member)) {
if ($error = GroupsController::validateMemberEmail($member, $owner)) {
$errors['members'][$i] = $error;
} elseif (\strtolower($member) === $group->email) {
$errors['members'][$i] = \trans('validation.memberislist');
}
} else {
unset($members[$i]);
}
}
}
if (!empty($errors)) {
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
$group->members = $members;
$group->save();
return response()->json([
'status' => 'success',
'message' => \trans('app.distlist-update-success'),
]);
}
/**
* Execute (synchronously) specified step in a group setup process.
*
* @param \App\Group $group Group object
* @param string $step Step identifier (as in self::statusInfo())
*
* @return bool|null True if the execution succeeded, False if not, Null when
* the job has been sent to the worker (result unknown)
*/
public static function execProcessStep(Group $group, string $step): ?bool
{
try {
if (strpos($step, 'domain-') === 0) {
return DomainsController::execProcessStep($group->domain(), $step);
}
switch ($step) {
case 'distlist-ldap-ready':
// Group not in LDAP, create it
$job = new \App\Jobs\Group\CreateJob($group->id);
$job->handle();
$group->refresh();
return $group->isLdapReady();
}
} catch (\Exception $e) {
\Log::error($e);
}
return false;
}
/**
* Prepare group statuses for the UI
*
* @param \App\Group $group Group object
*
* @return array Statuses array
*/
- protected static function objectState(Group $group): array
+ protected static function objectState($group): array
{
return [
'isLdapReady' => $group->isLdapReady(),
'isSuspended' => $group->isSuspended(),
'isActive' => $group->isActive(),
'isDeleted' => $group->isDeleted() || $group->trashed(),
];
}
/**
* Validate an email address for use as a group email
*
* @param string $email Email address
* @param \App\User $user The group owner
*
* @return ?string Error message on validation error
*/
public static function validateGroupEmail($email, \App\User $user): ?string
{
if (empty($email)) {
return \trans('validation.required', ['attribute' => 'email']);
}
if (strpos($email, '@') === false) {
return \trans('validation.entryinvalid', ['attribute' => 'email']);
}
list($login, $domain) = explode('@', \strtolower($email));
if (strlen($login) === 0 || strlen($domain) === 0) {
return \trans('validation.entryinvalid', ['attribute' => 'email']);
}
// Check if domain exists
$domain = Domain::where('namespace', $domain)->first();
if (empty($domain)) {
return \trans('validation.domaininvalid');
}
$wallet = $domain->wallet();
// The domain must be owned by the user
if (!$wallet || !$user->wallets()->find($wallet->id)) {
return \trans('validation.domainnotavailable');
}
// Validate login part alone
$v = Validator::make(
['email' => $login],
['email' => [new \App\Rules\UserEmailLocal(true)]]
);
if ($v->fails()) {
return $v->errors()->toArray()['email'][0];
}
// Check if a user with specified address already exists
if (User::emailExists($email)) {
return \trans('validation.entryexists', ['attribute' => 'email']);
}
// Check if an alias with specified address already exists.
if (User::aliasExists($email)) {
return \trans('validation.entryexists', ['attribute' => 'email']);
}
if (Group::emailExists($email)) {
return \trans('validation.entryexists', ['attribute' => 'email']);
}
return null;
}
/**
* Validate an email address for use as a group member
*
* @param string $email Email address
* @param \App\User $user The group owner
*
* @return ?string Error message on validation error
*/
public static function validateMemberEmail($email, \App\User $user): ?string
{
$v = Validator::make(
['email' => $email],
['email' => [new \App\Rules\ExternalEmail()]]
);
if ($v->fails()) {
return $v->errors()->toArray()['email'][0];
}
// A local domain user must exist
if (!User::where('email', \strtolower($email))->first()) {
list($login, $domain) = explode('@', \strtolower($email));
$domain = Domain::where('namespace', $domain)->first();
// We return an error only if the domain belongs to the group owner
if ($domain && ($wallet = $domain->wallet()) && $user->wallets()->find($wallet->id)) {
return \trans('validation.notalocaluser');
}
}
return null;
}
}
diff --git a/src/app/Http/Controllers/API/V4/PackagesController.php b/src/app/Http/Controllers/API/V4/PackagesController.php
index 1e592bc8..35d02e02 100644
--- a/src/app/Http/Controllers/API/V4/PackagesController.php
+++ b/src/app/Http/Controllers/API/V4/PackagesController.php
@@ -1,112 +1,35 @@
<?php
namespace App\Http\Controllers\API\V4;
use App\Package;
-use App\Http\Controllers\Controller;
+use App\Http\Controllers\ResourceController;
use Illuminate\Http\Request;
-class PackagesController extends Controller
+class PackagesController extends ResourceController
{
- /**
- * Show the form for creating a new package.
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function create()
- {
- // TODO
- return $this->errorResponse(404);
- }
-
- /**
- * Remove the specified package from storage.
- *
- * @param int $id Package identifier
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function destroy($id)
- {
- // TODO
- return $this->errorResponse(404);
- }
-
- /**
- * Show the form for editing the specified package.
- *
- * @param int $id Package identifier
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function edit($id)
- {
- // TODO
- return $this->errorResponse(404);
- }
-
/**
* Display a listing of packages.
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
// TODO: Packages should have an 'active' flag too, I guess
$response = [];
$packages = Package::withSubjectTenantContext()->select()->orderBy('title')->get();
foreach ($packages as $package) {
$response[] = [
'id' => $package->id,
'title' => $package->title,
'name' => $package->name,
'description' => $package->description,
'cost' => $package->cost(),
'isDomain' => $package->isDomain(),
];
}
return response()->json($response);
}
-
- /**
- * Store a newly created package in storage.
- *
- * @param \Illuminate\Http\Request $request
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function store(Request $request)
- {
- // TODO
- return $this->errorResponse(404);
- }
-
- /**
- * Display the specified package.
- *
- * @param int $id Package identifier
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function show($id)
- {
- // TODO
- return $this->errorResponse(404);
- }
-
- /**
- * Update the specified package in storage.
- *
- * @param \Illuminate\Http\Request $request Request object
- * @param int $id Package identifier
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function update(Request $request, $id)
- {
- // TODO
- return $this->errorResponse(404);
- }
}
diff --git a/src/app/Http/Controllers/API/V4/ResourcesController.php b/src/app/Http/Controllers/API/V4/ResourcesController.php
index d912df83..0e09118a 100644
--- a/src/app/Http/Controllers/API/V4/ResourcesController.php
+++ b/src/app/Http/Controllers/API/V4/ResourcesController.php
@@ -1,352 +1,209 @@
<?php
namespace App\Http\Controllers\API\V4;
-use App\Http\Controllers\Controller;
+use App\Http\Controllers\RelationController;
use App\Resource;
use App\Rules\ResourceName;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
-class ResourcesController extends Controller
+class ResourcesController extends RelationController
{
- /** @var array Common object properties in the API response */
- protected static $objectProps = ['email', 'name'];
-
- /**
- * Show the form for creating a new resource.
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function create()
- {
- return $this->errorResponse(404);
- }
-
- /**
- * Delete a resource.
- *
- * @param int $id Resource identifier
- *
- * @return \Illuminate\Http\JsonResponse The response
- */
- public function destroy($id)
- {
- $resource = Resource::find($id);
-
- if (!$this->checkTenant($resource)) {
- return $this->errorResponse(404);
- }
-
- if (!$this->guard()->user()->canDelete($resource)) {
- return $this->errorResponse(403);
- }
-
- $resource->delete();
-
- return response()->json([
- 'status' => 'success',
- 'message' => \trans('app.resource-delete-success'),
- ]);
- }
-
- /**
- * Show the form for editing the specified resource.
- *
- * @param int $id Resource identifier
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function edit($id)
- {
- return $this->errorResponse(404);
- }
-
- /**
- * Listing of resources belonging to the authenticated user.
- *
- * The resource-entitlements billed to the current user wallet(s)
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function index()
- {
- $user = $this->guard()->user();
-
- $result = $user->resources()->orderBy('name')->get()
- ->map(function (Resource $resource) {
- return $this->objectToClient($resource);
- });
-
- return response()->json($result);
- }
-
- /**
- * Set the resource configuration.
- *
- * @param int $id Resource identifier
- *
- * @return \Illuminate\Http\JsonResponse|void
- */
- public function setConfig($id)
- {
- $resource = Resource::find($id);
+ /** @var string Resource localization label */
+ protected $label = 'resource';
- if (!$this->checkTenant($resource)) {
- return $this->errorResponse(404);
- }
-
- if (!$this->guard()->user()->canUpdate($resource)) {
- return $this->errorResponse(403);
- }
+ /** @var string Resource model name */
+ protected $model = Resource::class;
- $errors = $resource->setConfig(request()->input());
+ /** @var array Resource listing order (column names) */
+ protected $order = ['name'];
- if (!empty($errors)) {
- return response()->json(['status' => 'error', 'errors' => $errors], 422);
- }
-
- return response()->json([
- 'status' => 'success',
- 'message' => \trans('app.resource-setconfig-success'),
- ]);
- }
-
- /**
- * Display information of a resource specified by $id.
- *
- * @param int $id Resource identifier
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function show($id)
- {
- $resource = Resource::find($id);
-
- if (!$this->checkTenant($resource)) {
- return $this->errorResponse(404);
- }
-
- if (!$this->guard()->user()->canRead($resource)) {
- return $this->errorResponse(403);
- }
-
- $response = $this->objectToClient($resource, true);
-
- $response['statusInfo'] = self::statusInfo($resource);
+ /** @var array Common object properties in the API response */
+ protected $objectProps = ['email', 'name'];
- // Resource configuration, e.g. invitation_policy
- $response['config'] = $resource->getConfig();
-
- return response()->json($response);
- }
/**
- * Fetch resource status (and reload setup process)
+ * Prepare resource statuses for the UI
*
- * @param int $id Resource identifier
+ * @param \App\Resource $resource Resource object
*
- * @return \Illuminate\Http\JsonResponse
+ * @return array Statuses array
*/
- public function status($id)
+ protected static function objectState($resource): array
{
- $resource = Resource::find($id);
-
- if (!$this->checkTenant($resource)) {
- return $this->errorResponse(404);
- }
-
- if (!$this->guard()->user()->canRead($resource)) {
- return $this->errorResponse(403);
- }
-
- $response = $this->processStateUpdate($resource);
- $response = array_merge($response, self::objectState($resource));
-
- return response()->json($response);
+ return [
+ 'isLdapReady' => $resource->isLdapReady(),
+ 'isImapReady' => $resource->isImapReady(),
+ 'isActive' => $resource->isActive(),
+ 'isDeleted' => $resource->isDeleted() || $resource->trashed(),
+ ];
}
/**
* Resource status (extended) information
*
* @param \App\Resource $resource Resource object
*
* @return array Status information
*/
- public static function statusInfo(Resource $resource): array
+ public static function statusInfo($resource): array
{
return self::processStateInfo(
$resource,
[
'resource-new' => true,
'resource-ldap-ready' => $resource->isLdapReady(),
'resource-imap-ready' => $resource->isImapReady(),
]
);
}
/**
* Create a new resource record.
*
* @param \Illuminate\Http\Request $request The API request.
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function store(Request $request)
{
$current_user = $this->guard()->user();
$owner = $current_user->wallet()->owner;
if ($owner->id != $current_user->id) {
return $this->errorResponse(403);
}
$domain = request()->input('domain');
$rules = ['name' => ['required', 'string', new ResourceName($owner, $domain)]];
$v = Validator::make($request->all(), $rules);
if ($v->fails()) {
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
}
DB::beginTransaction();
// Create the resource
$resource = new Resource();
$resource->name = request()->input('name');
$resource->domain = $domain;
$resource->save();
$resource->assignToWallet($owner->wallets->first());
DB::commit();
return response()->json([
'status' => 'success',
'message' => \trans('app.resource-create-success'),
]);
}
/**
* Update a resource.
*
* @param \Illuminate\Http\Request $request The API request.
* @param string $id Resource identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function update(Request $request, $id)
{
$resource = Resource::find($id);
if (!$this->checkTenant($resource)) {
return $this->errorResponse(404);
}
$current_user = $this->guard()->user();
if (!$current_user->canUpdate($resource)) {
return $this->errorResponse(403);
}
$owner = $resource->wallet()->owner;
$name = $request->input('name');
$errors = [];
// Validate the resource name
if ($name !== null && $name != $resource->name) {
$domainName = explode('@', $resource->email, 2)[1];
$rules = ['name' => ['required', 'string', new ResourceName($owner, $domainName)]];
$v = Validator::make($request->all(), $rules);
if ($v->fails()) {
$errors = $v->errors()->toArray();
} else {
$resource->name = $name;
}
}
if (!empty($errors)) {
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
$resource->save();
return response()->json([
'status' => 'success',
'message' => \trans('app.resource-update-success'),
]);
}
/**
* Execute (synchronously) specified step in a resource setup process.
*
* @param \App\Resource $resource Resource object
* @param string $step Step identifier (as in self::statusInfo())
*
* @return bool|null True if the execution succeeded, False if not, Null when
* the job has been sent to the worker (result unknown)
*/
public static function execProcessStep(Resource $resource, string $step): ?bool
{
try {
if (strpos($step, 'domain-') === 0) {
return DomainsController::execProcessStep($resource->domain(), $step);
}
switch ($step) {
case 'resource-ldap-ready':
// Resource not in LDAP, create it
$job = new \App\Jobs\Resource\CreateJob($resource->id);
$job->handle();
$resource->refresh();
return $resource->isLdapReady();
case 'resource-imap-ready':
// Resource not in IMAP? Verify again
// Do it synchronously if the imap admin credentials are available
// otherwise let the worker do the job
if (!\config('imap.admin_password')) {
\App\Jobs\Resource\VerifyJob::dispatch($resource->id);
return null;
}
$job = new \App\Jobs\Resource\VerifyJob($resource->id);
$job->handle();
$resource->refresh();
return $resource->isImapReady();
}
} catch (\Exception $e) {
\Log::error($e);
}
return false;
}
-
- /**
- * Prepare resource statuses for the UI
- *
- * @param \App\Resource $resource Resource object
- *
- * @return array Statuses array
- */
- protected static function objectState(Resource $resource): array
- {
- return [
- 'isLdapReady' => $resource->isLdapReady(),
- 'isImapReady' => $resource->isImapReady(),
- 'isActive' => $resource->isActive(),
- 'isDeleted' => $resource->isDeleted() || $resource->trashed(),
- ];
- }
}
diff --git a/src/app/Http/Controllers/API/V4/SharedFoldersController.php b/src/app/Http/Controllers/API/V4/SharedFoldersController.php
index 0bee2b5a..146a365e 100644
--- a/src/app/Http/Controllers/API/V4/SharedFoldersController.php
+++ b/src/app/Http/Controllers/API/V4/SharedFoldersController.php
@@ -1,357 +1,214 @@
<?php
namespace App\Http\Controllers\API\V4;
-use App\Http\Controllers\Controller;
+use App\Http\Controllers\RelationController;
use App\SharedFolder;
use App\Rules\SharedFolderName;
use App\Rules\SharedFolderType;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
-class SharedFoldersController extends Controller
+class SharedFoldersController extends RelationController
{
- /** @var array Common object properties in the API response */
- protected static $objectProps = ['email', 'name', 'type'];
-
- /**
- * Show the form for creating a new shared folder.
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function create()
- {
- return $this->errorResponse(404);
- }
-
- /**
- * Delete a shared folder.
- *
- * @param int $id Shared folder identifier
- *
- * @return \Illuminate\Http\JsonResponse The response
- */
- public function destroy($id)
- {
- $folder = SharedFolder::find($id);
-
- if (!$this->checkTenant($folder)) {
- return $this->errorResponse(404);
- }
-
- if (!$this->guard()->user()->canDelete($folder)) {
- return $this->errorResponse(403);
- }
-
- $folder->delete();
-
- return response()->json([
- 'status' => 'success',
- 'message' => \trans('app.shared-folder-delete-success'),
- ]);
- }
-
- /**
- * Show the form for editing the specified shared folder.
- *
- * @param int $id Shared folder identifier
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function edit($id)
- {
- return $this->errorResponse(404);
- }
-
- /**
- * Listing of a shared folders belonging to the authenticated user.
- *
- * The shared-folder entitlements billed to the current user wallet(s)
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function index()
- {
- $user = $this->guard()->user();
-
- $result = $user->sharedFolders()->orderBy('name')->get()
- ->map(function (SharedFolder $folder) {
- return $this->objectToClient($folder);
- });
-
- return response()->json($result);
- }
-
- /**
- * Set the shared folder configuration.
- *
- * @param int $id Shared folder identifier
- *
- * @return \Illuminate\Http\JsonResponse|void
- */
- public function setConfig($id)
- {
- $folder = SharedFolder::find($id);
+ /** @var string Resource localization label */
+ protected $label = 'shared-folder';
- if (!$this->checkTenant($folder)) {
- return $this->errorResponse(404);
- }
-
- if (!$this->guard()->user()->canUpdate($folder)) {
- return $this->errorResponse(403);
- }
+ /** @var string Resource model name */
+ protected $model = SharedFolder::class;
- $errors = $folder->setConfig(request()->input());
+ /** @var array Resource listing order (column names) */
+ protected $order = ['name'];
- if (!empty($errors)) {
- return response()->json(['status' => 'error', 'errors' => $errors], 422);
- }
-
- return response()->json([
- 'status' => 'success',
- 'message' => \trans('app.shared-folder-setconfig-success'),
- ]);
- }
-
- /**
- * Display information of a shared folder specified by $id.
- *
- * @param int $id Shared folder identifier
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function show($id)
- {
- $folder = SharedFolder::find($id);
-
- if (!$this->checkTenant($folder)) {
- return $this->errorResponse(404);
- }
-
- if (!$this->guard()->user()->canRead($folder)) {
- return $this->errorResponse(403);
- }
-
- $response = $this->objectToClient($folder, true);
-
- $response['statusInfo'] = self::statusInfo($folder);
+ /** @var array Common object properties in the API response */
+ protected $objectProps = ['email', 'name', 'type'];
- // Shared folder configuration, e.g. acl
- $response['config'] = $folder->getConfig();
-
- return response()->json($response);
- }
/**
- * Fetch a shared folder status (and reload setup process)
+ * Prepare shared folder statuses for the UI
*
- * @param int $id Shared folder identifier
+ * @param \App\SharedFolder $folder Shared folder object
*
- * @return \Illuminate\Http\JsonResponse
+ * @return array Statuses array
*/
- public function status($id)
+ protected static function objectState($folder): array
{
- $folder = SharedFolder::find($id);
-
- if (!$this->checkTenant($folder)) {
- return $this->errorResponse(404);
- }
-
- if (!$this->guard()->user()->canRead($folder)) {
- return $this->errorResponse(403);
- }
-
- $response = $this->processStateUpdate($folder);
- $response = array_merge($response, self::objectState($folder));
-
- return response()->json($response);
+ return [
+ 'isLdapReady' => $folder->isLdapReady(),
+ 'isImapReady' => $folder->isImapReady(),
+ 'isActive' => $folder->isActive(),
+ 'isDeleted' => $folder->isDeleted() || $folder->trashed(),
+ ];
}
/**
* SharedFolder status (extended) information
*
* @param \App\SharedFolder $folder SharedFolder object
*
* @return array Status information
*/
- public static function statusInfo(SharedFolder $folder): array
+ public static function statusInfo($folder): array
{
return self::processStateInfo(
$folder,
[
'shared-folder-new' => true,
'shared-folder-ldap-ready' => $folder->isLdapReady(),
'shared-folder-imap-ready' => $folder->isImapReady(),
]
);
}
/**
* Create a new shared folder record.
*
* @param \Illuminate\Http\Request $request The API request.
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function store(Request $request)
{
$current_user = $this->guard()->user();
$owner = $current_user->wallet()->owner;
if ($owner->id != $current_user->id) {
return $this->errorResponse(403);
}
$domain = request()->input('domain');
$rules = [
'name' => ['required', 'string', new SharedFolderName($owner, $domain)],
'type' => ['required', 'string', new SharedFolderType()]
];
$v = Validator::make($request->all(), $rules);
if ($v->fails()) {
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
}
DB::beginTransaction();
// Create the shared folder
$folder = new SharedFolder();
$folder->name = request()->input('name');
$folder->type = request()->input('type');
$folder->domain = $domain;
$folder->save();
$folder->assignToWallet($owner->wallets->first());
DB::commit();
return response()->json([
'status' => 'success',
'message' => \trans('app.shared-folder-create-success'),
]);
}
/**
* Update a shared folder.
*
* @param \Illuminate\Http\Request $request The API request.
* @param string $id Shared folder identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function update(Request $request, $id)
{
$folder = SharedFolder::find($id);
if (!$this->checkTenant($folder)) {
return $this->errorResponse(404);
}
$current_user = $this->guard()->user();
if (!$current_user->canUpdate($folder)) {
return $this->errorResponse(403);
}
$owner = $folder->wallet()->owner;
$name = $request->input('name');
$errors = [];
// Validate the folder name
if ($name !== null && $name != $folder->name) {
$domainName = explode('@', $folder->email, 2)[1];
$rules = ['name' => ['required', 'string', new SharedFolderName($owner, $domainName)]];
$v = Validator::make($request->all(), $rules);
if ($v->fails()) {
$errors = $v->errors()->toArray();
} else {
$folder->name = $name;
}
}
if (!empty($errors)) {
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
$folder->save();
return response()->json([
'status' => 'success',
'message' => \trans('app.shared-folder-update-success'),
]);
}
/**
* Execute (synchronously) specified step in a shared folder setup process.
*
* @param \App\SharedFolder $folder Shared folder object
* @param string $step Step identifier (as in self::statusInfo())
*
* @return bool|null True if the execution succeeded, False if not, Null when
* the job has been sent to the worker (result unknown)
*/
public static function execProcessStep(SharedFolder $folder, string $step): ?bool
{
try {
if (strpos($step, 'domain-') === 0) {
return DomainsController::execProcessStep($folder->domain(), $step);
}
switch ($step) {
case 'shared-folder-ldap-ready':
// Shared folder not in LDAP, create it
$job = new \App\Jobs\SharedFolder\CreateJob($folder->id);
$job->handle();
$folder->refresh();
return $folder->isLdapReady();
case 'shared-folder-imap-ready':
// Shared folder not in IMAP? Verify again
// Do it synchronously if the imap admin credentials are available
// otherwise let the worker do the job
if (!\config('imap.admin_password')) {
\App\Jobs\SharedFolder\VerifyJob::dispatch($folder->id);
return null;
}
$job = new \App\Jobs\SharedFolder\VerifyJob($folder->id);
$job->handle();
$folder->refresh();
return $folder->isImapReady();
}
} catch (\Exception $e) {
\Log::error($e);
}
return false;
}
-
- /**
- * Prepare shared folder statuses for the UI
- *
- * @param \App\SharedFolder $folder Shared folder object
- *
- * @return array Statuses array
- */
- protected static function objectState(SharedFolder $folder): array
- {
- return [
- 'isLdapReady' => $folder->isLdapReady(),
- 'isImapReady' => $folder->isImapReady(),
- 'isActive' => $folder->isActive(),
- 'isDeleted' => $folder->isDeleted() || $folder->trashed(),
- ];
- }
}
diff --git a/src/app/Http/Controllers/API/V4/SkusController.php b/src/app/Http/Controllers/API/V4/SkusController.php
index 3ea2360d..11590bfb 100644
--- a/src/app/Http/Controllers/API/V4/SkusController.php
+++ b/src/app/Http/Controllers/API/V4/SkusController.php
@@ -1,226 +1,149 @@
<?php
namespace App\Http\Controllers\API\V4;
-use App\Http\Controllers\Controller;
+use App\Http\Controllers\ResourceController;
use App\Sku;
use Illuminate\Http\Request;
-class SkusController extends Controller
+class SkusController extends ResourceController
{
- /**
- * Show the form for creating a new sku.
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function create()
- {
- // TODO
- return $this->errorResponse(404);
- }
-
- /**
- * Remove the specified sku from storage.
- *
- * @param int $id SKU identifier
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function destroy($id)
- {
- // TODO
- return $this->errorResponse(404);
- }
-
/**
* Get a list of SKUs available to the domain.
*
* @param int $id Domain identifier
*
* @return \Illuminate\Http\JsonResponse
*/
public function domainSkus($id)
{
$domain = \App\Domain::find($id);
if (!$this->checkTenant($domain)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canRead($domain)) {
return $this->errorResponse(403);
}
return $this->objectSkus($domain);
}
- /**
- * Show the form for editing the specified sku.
- *
- * @param int $id SKU identifier
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function edit($id)
- {
- // TODO
- return $this->errorResponse(404);
- }
-
/**
* Get a list of active SKUs.
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
// Note: Order by title for consistent ordering in tests
$skus = Sku::withSubjectTenantContext()->where('active', true)->orderBy('title')->get();
$response = [];
foreach ($skus as $sku) {
if ($data = $this->skuElement($sku)) {
$response[] = $data;
}
}
usort($response, function ($a, $b) {
return ($b['prio'] <=> $a['prio']);
});
return response()->json($response);
}
- /**
- * Store a newly created sku in storage.
- *
- * @param \Illuminate\Http\Request $request
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function store(Request $request)
- {
- // TODO
- return $this->errorResponse(404);
- }
-
- /**
- * Display the specified sku.
- *
- * @param int $id SKU identifier
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function show($id)
- {
- // TODO
- return $this->errorResponse(404);
- }
-
- /**
- * Update the specified sku in storage.
- *
- * @param \Illuminate\Http\Request $request Request object
- * @param int $id SKU identifier
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function update(Request $request, $id)
- {
- // TODO
- return $this->errorResponse(404);
- }
-
/**
* Get a list of SKUs available to the user.
*
* @param int $id User identifier
*
* @return \Illuminate\Http\JsonResponse
*/
public function userSkus($id)
{
$user = \App\User::find($id);
if (!$this->checkTenant($user)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canRead($user)) {
return $this->errorResponse(403);
}
return $this->objectSkus($user);
}
/**
* Return SKUs available to the specified user/domain.
*
* @param object $object User or Domain object
*
* @return \Illuminate\Http\JsonResponse
*/
protected static function objectSkus($object)
{
$type = $object instanceof \App\Domain ? 'domain' : 'user';
$response = [];
// Note: Order by title for consistent ordering in tests
$skus = Sku::withObjectTenantContext($object)->orderBy('title')->get();
foreach ($skus as $sku) {
if (!class_exists($sku->handler_class)) {
continue;
}
if (!$sku->handler_class::isAvailable($sku, $object)) {
continue;
}
if ($data = self::skuElement($sku)) {
if ($type != $data['type']) {
continue;
}
$response[] = $data;
}
}
usort($response, function ($a, $b) {
return ($b['prio'] <=> $a['prio']);
});
return response()->json($response);
}
/**
* Convert SKU information to metadata used by UI to
* display the form control
*
* @param \App\Sku $sku SKU object
*
* @return array|null Metadata
*/
protected static function skuElement($sku): ?array
{
if (!class_exists($sku->handler_class)) {
return null;
}
$data = array_merge($sku->toArray(), $sku->handler_class::metadata($sku));
// ignore incomplete handlers
if (empty($data['type'])) {
return null;
}
// Use localized value, toArray() does not get them right
$data['name'] = $sku->name;
$data['description'] = $sku->description;
unset($data['handler_class'], $data['created_at'], $data['updated_at'], $data['fee'], $data['tenant_id']);
return $data;
}
}
diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php
index 597db507..6fb39771 100644
--- a/src/app/Http/Controllers/API/V4/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/UsersController.php
@@ -1,806 +1,728 @@
<?php
namespace App\Http\Controllers\API\V4;
-use App\Http\Controllers\Controller;
+use App\Http\Controllers\RelationController;
use App\Domain;
use App\Group;
use App\Rules\UserEmailDomain;
use App\Rules\UserEmailLocal;
use App\Sku;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
-class UsersController extends Controller
+class UsersController extends RelationController
{
/** @const array List of user setting keys available for modification in UI */
public const USER_SETTINGS = [
'billing_address',
'country',
'currency',
'external_email',
'first_name',
'last_name',
'organization',
'phone',
];
/**
* On user create it is filled with a user or group object to force-delete
* before the creation of a new user record is possible.
*
* @var \App\User|\App\Group|null
*/
protected $deleteBeforeCreate;
- /** @var array Common object properties in the API response */
- protected static $objectProps = ['email'];
-
-
- /**
- * Delete a user.
- *
- * @param int $id User identifier
- *
- * @return \Illuminate\Http\JsonResponse The response
- */
- public function destroy($id)
- {
- $user = User::withEnvTenantContext()->find($id);
+ /** @var string Resource localization label */
+ protected $label = 'user';
- if (empty($user)) {
- return $this->errorResponse(404);
- }
+ /** @var string Resource model name */
+ protected $model = User::class;
- // User can't remove himself until he's the controller
- if (!$this->guard()->user()->canDelete($user)) {
- return $this->errorResponse(403);
- }
+ /** @var array Common object properties in the API response */
+ protected $objectProps = ['email'];
- $user->delete();
-
- return response()->json([
- 'status' => 'success',
- 'message' => \trans('app.user-delete-success'),
- ]);
- }
/**
* Listing of users.
*
* The user-entitlements billed to the current user wallet(s)
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
$user = $this->guard()->user();
$search = trim(request()->input('search'));
$page = intval(request()->input('page')) ?: 1;
$pageSize = 20;
$hasMore = false;
$result = $user->users();
// Search by user email, alias or name
if (strlen($search) > 0) {
// thanks to cloning we skip some extra queries in $user->users()
$allUsers1 = clone $result;
$allUsers2 = clone $result;
$result->whereLike('email', $search)
->union(
$allUsers1->join('user_aliases', 'users.id', '=', 'user_aliases.user_id')
->whereLike('alias', $search)
)
->union(
$allUsers2->join('user_settings', 'users.id', '=', 'user_settings.user_id')
->whereLike('value', $search)
->whereIn('key', ['first_name', 'last_name'])
);
}
$result = $result->orderBy('email')
->limit($pageSize + 1)
->offset($pageSize * ($page - 1))
->get();
if (count($result) > $pageSize) {
$result->pop();
$hasMore = true;
}
// Process the result
$result = $result->map(
function ($user) {
return $this->objectToClient($user);
}
);
$result = [
'list' => $result,
'count' => count($result),
'hasMore' => $hasMore,
];
return response()->json($result);
}
- /**
- * Set user config.
- *
- * @param int $id The user
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function setConfig($id)
- {
- $user = User::find($id);
-
- if (empty($user)) {
- return $this->errorResponse(404);
- }
-
- if (!$this->guard()->user()->canUpdate($user)) {
- return $this->errorResponse(403);
- }
-
- $errors = $user->setConfig(request()->input());
-
- if (!empty($errors)) {
- return response()->json(['status' => 'error', 'errors' => $errors], 422);
- }
-
- return response()->json([
- 'status' => 'success',
- 'message' => \trans('app.user-setconfig-success'),
- ]);
- }
-
/**
* Display information on the user account specified by $id.
*
- * @param int $id The account to show information for.
+ * @param string $id The account to show information for.
*
* @return \Illuminate\Http\JsonResponse
*/
public function show($id)
{
- $user = User::withEnvTenantContext()->find($id);
+ $user = User::find($id);
- if (empty($user)) {
+ if (!$this->checkTenant($user)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canRead($user)) {
return $this->errorResponse(403);
}
$response = $this->userResponse($user);
$response['skus'] = \App\Entitlement::objectEntitlementsSummary($user);
$response['config'] = $user->getConfig();
return response()->json($response);
}
- /**
- * Fetch user status (and reload setup process)
- *
- * @param int $id User identifier
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function status($id)
- {
- $user = User::withEnvTenantContext()->find($id);
-
- if (empty($user)) {
- return $this->errorResponse(404);
- }
-
- if (!$this->guard()->user()->canRead($user)) {
- return $this->errorResponse(403);
- }
-
- $response = $this->processStateUpdate($user);
- $response = array_merge($response, self::objectState($user));
-
- return response()->json($response);
- }
-
/**
* User status (extended) information
*
* @param \App\User $user User object
*
* @return array Status information
*/
- public static function statusInfo(User $user): array
+ public static function statusInfo($user): array
{
$process = self::processStateInfo(
$user,
[
'user-new' => true,
'user-ldap-ready' => $user->isLdapReady(),
'user-imap-ready' => $user->isImapReady(),
]
);
// Check if the user is a controller of his wallet
$isController = $user->canDelete($user);
$hasCustomDomain = $user->wallet()->entitlements()
->where('entitleable_type', Domain::class)
->count() > 0;
// Get user's entitlements titles
$skus = $user->entitlements()->select('skus.title')
->join('skus', 'skus.id', '=', 'entitlements.sku_id')
->get()
->pluck('title')
->sort()
->unique()
->values()
->all();
$result = [
'skus' => $skus,
// TODO: This will change when we enable all users to create domains
'enableDomains' => $isController && $hasCustomDomain,
// TODO: Make 'enableDistlists' working for wallet controllers that aren't account owners
'enableDistlists' => $isController && $hasCustomDomain && in_array('distlist', $skus),
// TODO: Make 'enableFolders' working for wallet controllers that aren't account owners
'enableFolders' => $isController && $hasCustomDomain && in_array('beta-shared-folders', $skus),
// TODO: Make 'enableResources' working for wallet controllers that aren't account owners
'enableResources' => $isController && $hasCustomDomain && in_array('beta-resources', $skus),
'enableUsers' => $isController,
'enableWallets' => $isController,
];
return array_merge($process, $result);
}
/**
* Create a new user record.
*
* @param \Illuminate\Http\Request $request The API request.
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function store(Request $request)
{
$current_user = $this->guard()->user();
$owner = $current_user->wallet()->owner;
if ($owner->id != $current_user->id) {
return $this->errorResponse(403);
}
$this->deleteBeforeCreate = null;
if ($error_response = $this->validateUserRequest($request, null, $settings)) {
return $error_response;
}
if (empty($request->package) || !($package = \App\Package::withEnvTenantContext()->find($request->package))) {
$errors = ['package' => \trans('validation.packagerequired')];
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
if ($package->isDomain()) {
$errors = ['package' => \trans('validation.packageinvalid')];
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
DB::beginTransaction();
// @phpstan-ignore-next-line
if ($this->deleteBeforeCreate) {
$this->deleteBeforeCreate->forceDelete();
}
// Create user record
$user = User::create([
'email' => $request->email,
'password' => $request->password,
]);
$owner->assignPackage($package, $user);
if (!empty($settings)) {
$user->setSettings($settings);
}
if (!empty($request->aliases)) {
$user->setAliases($request->aliases);
}
DB::commit();
return response()->json([
'status' => 'success',
'message' => \trans('app.user-create-success'),
]);
}
/**
* Update user data.
*
* @param \Illuminate\Http\Request $request The API request.
* @param string $id User identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function update(Request $request, $id)
{
$user = User::withEnvTenantContext()->find($id);
if (empty($user)) {
return $this->errorResponse(404);
}
$current_user = $this->guard()->user();
// TODO: Decide what attributes a user can change on his own profile
if (!$current_user->canUpdate($user)) {
return $this->errorResponse(403);
}
if ($error_response = $this->validateUserRequest($request, $user, $settings)) {
return $error_response;
}
// Entitlements, only controller can do that
if ($request->skus !== null && !$current_user->canDelete($user)) {
return $this->errorResponse(422, "You have no permission to change entitlements");
}
DB::beginTransaction();
$this->updateEntitlements($user, $request->skus);
if (!empty($settings)) {
$user->setSettings($settings);
}
if (!empty($request->password)) {
$user->password = $request->password;
$user->save();
}
if (isset($request->aliases)) {
$user->setAliases($request->aliases);
}
// TODO: Make sure that UserUpdate job is created in case of entitlements update
// and no password change. So, for example quota change is applied to LDAP
// TODO: Review use of $user->save() in the above context
DB::commit();
$response = [
'status' => 'success',
'message' => \trans('app.user-update-success'),
];
// For self-update refresh the statusInfo in the UI
if ($user->id == $current_user->id) {
$response['statusInfo'] = self::statusInfo($user);
}
return response()->json($response);
}
/**
* Update user entitlements.
*
* @param \App\User $user The user
* @param array $rSkus List of SKU IDs requested for the user in the form [id=>qty]
*/
protected function updateEntitlements(User $user, $rSkus)
{
if (!is_array($rSkus)) {
return;
}
// list of skus, [id=>obj]
$skus = Sku::withEnvTenantContext()->get()->mapWithKeys(
function ($sku) {
return [$sku->id => $sku];
}
);
// existing entitlement's SKUs
$eSkus = [];
$user->entitlements()->groupBy('sku_id')
->selectRaw('count(*) as total, sku_id')->each(
function ($e) use (&$eSkus) {
$eSkus[$e->sku_id] = $e->total;
}
);
foreach ($skus as $skuID => $sku) {
$e = array_key_exists($skuID, $eSkus) ? $eSkus[$skuID] : 0;
$r = array_key_exists($skuID, $rSkus) ? $rSkus[$skuID] : 0;
if ($sku->handler_class == \App\Handlers\Mailbox::class) {
if ($r != 1) {
throw new \Exception("Invalid quantity of mailboxes");
}
}
if ($e > $r) {
// remove those entitled more than existing
$user->removeSku($sku, ($e - $r));
} elseif ($e < $r) {
// add those requested more than entitled
$user->assignSku($sku, ($r - $e));
}
}
}
/**
* Create a response data array for specified user.
*
* @param \App\User $user User object
*
* @return array Response data
*/
public static function userResponse(User $user): array
{
- $response = self::objectToClient($user, true);
+ $response = array_merge($user->toArray(), self::objectState($user));
// Settings
$response['settings'] = [];
foreach ($user->settings()->whereIn('key', self::USER_SETTINGS)->get() as $item) {
$response['settings'][$item->key] = $item->value;
}
// Aliases
$response['aliases'] = [];
foreach ($user->aliases as $item) {
$response['aliases'][] = $item->alias;
}
// Status info
$response['statusInfo'] = self::statusInfo($user);
// Add more info to the wallet object output
$map_func = function ($wallet) use ($user) {
$result = $wallet->toArray();
if ($wallet->discount) {
$result['discount'] = $wallet->discount->discount;
$result['discount_description'] = $wallet->discount->description;
}
if ($wallet->user_id != $user->id) {
$result['user_email'] = $wallet->owner->email;
}
$provider = \App\Providers\PaymentProvider::factory($wallet);
$result['provider'] = $provider->name();
return $result;
};
// Information about wallets and accounts for access checks
$response['wallets'] = $user->wallets->map($map_func)->toArray();
$response['accounts'] = $user->accounts->map($map_func)->toArray();
$response['wallet'] = $map_func($user->wallet());
return $response;
}
/**
* Prepare user statuses for the UI
*
* @param \App\User $user User object
*
* @return array Statuses array
*/
- protected static function objectState(User $user): array
+ protected static function objectState($user): array
{
return [
'isImapReady' => $user->isImapReady(),
'isLdapReady' => $user->isLdapReady(),
'isSuspended' => $user->isSuspended(),
'isActive' => $user->isActive(),
'isDeleted' => $user->isDeleted() || $user->trashed(),
];
}
/**
* Validate user input
*
* @param \Illuminate\Http\Request $request The API request.
* @param \App\User|null $user User identifier
* @param array $settings User settings (from the request)
*
* @return \Illuminate\Http\JsonResponse|null The error response on error
*/
protected function validateUserRequest(Request $request, $user, &$settings = [])
{
$rules = [
'external_email' => 'nullable|email',
'phone' => 'string|nullable|max:64|regex:/^[0-9+() -]+$/',
'first_name' => 'string|nullable|max:128',
'last_name' => 'string|nullable|max:128',
'organization' => 'string|nullable|max:512',
'billing_address' => 'string|nullable|max:1024',
'country' => 'string|nullable|alpha|size:2',
'currency' => 'string|nullable|alpha|size:3',
'aliases' => 'array|nullable',
];
if (empty($user) || !empty($request->password) || !empty($request->password_confirmation)) {
$rules['password'] = 'required|min:4|max:2048|confirmed';
}
$errors = [];
// Validate input
$v = Validator::make($request->all(), $rules);
if ($v->fails()) {
$errors = $v->errors()->toArray();
}
$controller = $user ? $user->wallet()->owner : $this->guard()->user();
// For new user validate email address
if (empty($user)) {
$email = $request->email;
if (empty($email)) {
$errors['email'] = \trans('validation.required', ['attribute' => 'email']);
} elseif ($error = self::validateEmail($email, $controller, $this->deleteBeforeCreate)) {
$errors['email'] = $error;
}
}
// Validate aliases input
if (isset($request->aliases)) {
$aliases = [];
$existing_aliases = $user ? $user->aliases()->get()->pluck('alias')->toArray() : [];
foreach ($request->aliases as $idx => $alias) {
if (is_string($alias) && !empty($alias)) {
// Alias cannot be the same as the email address (new user)
if (!empty($email) && Str::lower($alias) == Str::lower($email)) {
continue;
}
// validate new aliases
if (
!in_array($alias, $existing_aliases)
&& ($error = self::validateAlias($alias, $controller))
) {
if (!isset($errors['aliases'])) {
$errors['aliases'] = [];
}
$errors['aliases'][$idx] = $error;
continue;
}
$aliases[] = $alias;
}
}
$request->aliases = $aliases;
}
if (!empty($errors)) {
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
// Update user settings
$settings = $request->only(array_keys($rules));
unset($settings['password'], $settings['aliases'], $settings['email']);
return null;
}
/**
* Execute (synchronously) specified step in a user setup process.
*
* @param \App\User $user User object
* @param string $step Step identifier (as in self::statusInfo())
*
* @return bool|null True if the execution succeeded, False if not, Null when
* the job has been sent to the worker (result unknown)
*/
public static function execProcessStep(User $user, string $step): ?bool
{
try {
if (strpos($step, 'domain-') === 0) {
list ($local, $domain) = explode('@', $user->email);
$domain = Domain::where('namespace', $domain)->first();
return DomainsController::execProcessStep($domain, $step);
}
switch ($step) {
case 'user-ldap-ready':
// User not in LDAP, create it
$job = new \App\Jobs\User\CreateJob($user->id);
$job->handle();
$user->refresh();
return $user->isLdapReady();
case 'user-imap-ready':
// User not in IMAP? Verify again
// Do it synchronously if the imap admin credentials are available
// otherwise let the worker do the job
if (!\config('imap.admin_password')) {
\App\Jobs\User\VerifyJob::dispatch($user->id);
return null;
}
$job = new \App\Jobs\User\VerifyJob($user->id);
$job->handle();
$user->refresh();
return $user->isImapReady();
}
} catch (\Exception $e) {
\Log::error($e);
}
return false;
}
/**
* Email address validation for use as a user mailbox (login).
*
* @param string $email Email address
* @param \App\User $user The account owner
* @param null|\App\User|\App\Group $deleted Filled with an instance of a deleted user or group
* with the specified email address, if exists
*
* @return ?string Error message on validation error
*/
public static function validateEmail(string $email, \App\User $user, &$deleted = null): ?string
{
$deleted = null;
if (strpos($email, '@') === false) {
return \trans('validation.entryinvalid', ['attribute' => 'email']);
}
list($login, $domain) = explode('@', Str::lower($email));
if (strlen($login) === 0 || strlen($domain) === 0) {
return \trans('validation.entryinvalid', ['attribute' => 'email']);
}
// Check if domain exists
$domain = Domain::withObjectTenantContext($user)->where('namespace', $domain)->first();
if (empty($domain)) {
return \trans('validation.domaininvalid');
}
// Validate login part alone
$v = Validator::make(
['email' => $login],
['email' => ['required', new UserEmailLocal(!$domain->isPublic())]]
);
if ($v->fails()) {
return $v->errors()->toArray()['email'][0];
}
// Check if it is one of domains available to the user
if (!$user->domains()->where('namespace', $domain->namespace)->exists()) {
return \trans('validation.entryexists', ['attribute' => 'domain']);
}
// Check if a user with specified address already exists
if ($existing_user = User::emailExists($email, true)) {
// If this is a deleted user in the same custom domain
// we'll force delete him before
if (!$domain->isPublic() && $existing_user->trashed()) {
$deleted = $existing_user;
} else {
return \trans('validation.entryexists', ['attribute' => 'email']);
}
}
// Check if an alias with specified address already exists.
if (User::aliasExists($email)) {
return \trans('validation.entryexists', ['attribute' => 'email']);
}
// Check if a group or resource with specified address already exists
if (
($existing = Group::emailExists($email, true))
|| ($existing = \App\Resource::emailExists($email, true))
) {
// If this is a deleted group/resource in the same custom domain
// we'll force delete it before
if (!$domain->isPublic() && $existing->trashed()) {
$deleted = $existing;
} else {
return \trans('validation.entryexists', ['attribute' => 'email']);
}
}
return null;
}
/**
* Email address validation for use as an alias.
*
* @param string $email Email address
* @param \App\User $user The account owner
*
* @return ?string Error message on validation error
*/
public static function validateAlias(string $email, \App\User $user): ?string
{
if (strpos($email, '@') === false) {
return \trans('validation.entryinvalid', ['attribute' => 'alias']);
}
list($login, $domain) = explode('@', Str::lower($email));
if (strlen($login) === 0 || strlen($domain) === 0) {
return \trans('validation.entryinvalid', ['attribute' => 'alias']);
}
// Check if domain exists
$domain = Domain::withObjectTenantContext($user)->where('namespace', $domain)->first();
if (empty($domain)) {
return \trans('validation.domaininvalid');
}
// Validate login part alone
$v = Validator::make(
['alias' => $login],
['alias' => ['required', new UserEmailLocal(!$domain->isPublic())]]
);
if ($v->fails()) {
return $v->errors()->toArray()['alias'][0];
}
// Check if it is one of domains available to the user
if (!$user->domains()->where('namespace', $domain->namespace)->exists()) {
return \trans('validation.entryexists', ['attribute' => 'domain']);
}
// Check if a user with specified address already exists
if ($existing_user = User::emailExists($email, true)) {
// Allow an alias in a custom domain to an address that was a user before
if ($domain->isPublic() || !$existing_user->trashed()) {
return \trans('validation.entryexists', ['attribute' => 'alias']);
}
}
// Check if an alias with specified address already exists
if (User::aliasExists($email)) {
// Allow assigning the same alias to a user in the same group account,
// but only for non-public domains
if ($domain->isPublic()) {
return \trans('validation.entryexists', ['attribute' => 'alias']);
}
}
// Check if a group with specified address already exists
if (Group::emailExists($email)) {
return \trans('validation.entryexists', ['attribute' => 'alias']);
}
return null;
}
}
diff --git a/src/app/Http/Controllers/API/V4/WalletsController.php b/src/app/Http/Controllers/API/V4/WalletsController.php
index 388bf4e7..0c99857a 100644
--- a/src/app/Http/Controllers/API/V4/WalletsController.php
+++ b/src/app/Http/Controllers/API/V4/WalletsController.php
@@ -1,342 +1,273 @@
<?php
namespace App\Http\Controllers\API\V4;
use App\Transaction;
use App\Wallet;
-use App\Http\Controllers\Controller;
+use App\Http\Controllers\ResourceController;
use App\Providers\PaymentProvider;
use Carbon\Carbon;
use Illuminate\Http\Request;
/**
* API\WalletsController
*/
-class WalletsController extends Controller
+class WalletsController extends ResourceController
{
- /**
- * Display a listing of the resource.
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function index()
- {
- return $this->errorResponse(404);
- }
-
- /**
- * Show the form for creating a new resource.
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function create()
- {
- 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)
- {
- return $this->errorResponse(404);
- }
-
/**
* Return data of the specified wallet.
*
* @param string $id A wallet identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function show($id)
{
$wallet = Wallet::find($id);
if (empty($wallet) || !$this->checkTenant($wallet->owner)) {
return $this->errorResponse(404);
}
// Only owner (or admin) has access to the wallet
if (!$this->guard()->user()->canRead($wallet)) {
return $this->errorResponse(403);
}
$result = $wallet->toArray();
$provider = \App\Providers\PaymentProvider::factory($wallet);
$result['provider'] = $provider->name();
$result['notice'] = $this->getWalletNotice($wallet);
return response()->json($result);
}
- /**
- * Show the form for editing the specified resource.
- *
- * @param int $id
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function edit($id)
- {
- return $this->errorResponse(404);
- }
-
- /**
- * Update the specified resource in storage.
- *
- * @param \Illuminate\Http\Request $request
- * @param string $id
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function update(Request $request, $id)
- {
- return $this->errorResponse(404);
- }
-
- /**
- * Remove the specified resource from storage.
- *
- * @param int $id
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function destroy($id)
- {
- return $this->errorResponse(404);
- }
-
/**
* Download a receipt in pdf format.
*
* @param string $id Wallet identifier
* @param string $receipt Receipt identifier (YYYY-MM)
*
* @return \Illuminate\Http\Response
*/
public function receiptDownload($id, $receipt)
{
$wallet = Wallet::find($id);
if (empty($wallet) || !$this->checkTenant($wallet->owner)) {
abort(404);
}
// Only owner (or admin) has access to the wallet
if (!$this->guard()->user()->canRead($wallet)) {
abort(403);
}
list ($year, $month) = explode('-', $receipt);
if (empty($year) || empty($month) || $year < 2000 || $month < 1 || $month > 12) {
abort(404);
}
if ($receipt >= date('Y-m')) {
abort(404);
}
$params = [
'id' => sprintf('%04d-%02d', $year, $month),
'site' => \config('app.name')
];
$filename = \trans('documents.receipt-filename', $params);
$receipt = new \App\Documents\Receipt($wallet, (int) $year, (int) $month);
$content = $receipt->pdfOutput();
return response($content)
->withHeaders([
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
'Content-Length' => strlen($content),
]);
}
/**
* Fetch wallet receipts list.
*
* @param string $id Wallet identifier
*
* @return \Illuminate\Http\JsonResponse
*/
public function receipts($id)
{
$wallet = Wallet::find($id);
if (empty($wallet) || !$this->checkTenant($wallet->owner)) {
return $this->errorResponse(404);
}
// Only owner (or admin) has access to the wallet
if (!$this->guard()->user()->canRead($wallet)) {
return $this->errorResponse(403);
}
$result = $wallet->payments()
->selectRaw('distinct date_format(updated_at, "%Y-%m") as ident')
->where('status', PaymentProvider::STATUS_PAID)
->where('amount', '<>', 0)
->orderBy('ident', 'desc')
->get()
->whereNotIn('ident', [date('Y-m')]) // exclude current month
->pluck('ident');
return response()->json([
'status' => 'success',
'list' => $result,
'count' => count($result),
'hasMore' => false,
'page' => 1,
]);
}
/**
* Fetch wallet transactions.
*
* @param string $id Wallet identifier
*
* @return \Illuminate\Http\JsonResponse
*/
public function transactions($id)
{
$wallet = Wallet::find($id);
if (empty($wallet) || !$this->checkTenant($wallet->owner)) {
return $this->errorResponse(404);
}
// Only owner (or admin) has access to the wallet
if (!$this->guard()->user()->canRead($wallet)) {
return $this->errorResponse(403);
}
$pageSize = 10;
$page = intval(request()->input('page')) ?: 1;
$hasMore = false;
$isAdmin = $this instanceof Admin\WalletsController;
if ($transaction = request()->input('transaction')) {
// Get sub-transactions for the specified transaction ID, first
// check access rights to the transaction's wallet
$transaction = $wallet->transactions()->where('id', $transaction)->first();
if (!$transaction) {
return $this->errorResponse(404);
}
$result = Transaction::where('transaction_id', $transaction->id)->get();
} else {
// Get main transactions (paged)
$result = $wallet->transactions()
// FIXME: Do we know which (type of) transaction has sub-transactions
// without the sub-query?
->selectRaw("*, (SELECT count(*) FROM transactions sub "
. "WHERE sub.transaction_id = transactions.id) AS cnt")
->whereNull('transaction_id')
->latest()
->limit($pageSize + 1)
->offset($pageSize * ($page - 1))
->get();
if (count($result) > $pageSize) {
$result->pop();
$hasMore = true;
}
}
$result = $result->map(function ($item) use ($isAdmin, $wallet) {
$entry = [
'id' => $item->id,
'createdAt' => $item->created_at->format('Y-m-d H:i'),
'type' => $item->type,
'description' => $item->shortDescription(),
'amount' => $item->amount,
'currency' => $wallet->currency,
'hasDetails' => !empty($item->cnt),
];
if ($isAdmin && $item->user_email) {
$entry['user'] = $item->user_email;
}
return $entry;
});
return response()->json([
'status' => 'success',
'list' => $result,
'count' => count($result),
'hasMore' => $hasMore,
'page' => $page,
]);
}
/**
* Returns human readable notice about the wallet state.
*
* @param \App\Wallet $wallet The wallet
*/
protected function getWalletNotice(Wallet $wallet): ?string
{
// there is no credit
if ($wallet->balance < 0) {
return \trans('app.wallet-notice-nocredit');
}
// the discount is 100%, no credit is needed
if ($wallet->discount && $wallet->discount->discount == 100) {
return null;
}
// the owner was created less than a month ago
if ($wallet->owner->created_at > Carbon::now()->subMonthsWithoutOverflow(1)) {
// but more than two weeks ago, notice of trial ending
if ($wallet->owner->created_at <= Carbon::now()->subWeeks(2)) {
return \trans('app.wallet-notice-trial-end');
}
return \trans('app.wallet-notice-trial');
}
if ($until = $wallet->balanceLastsUntil()) {
if ($until->isToday()) {
return \trans('app.wallet-notice-today');
}
// Once in a while we got e.g. "3 weeks" instead of expected "4 weeks".
// It's because $until uses full seconds, but $now is more precise.
// We make sure both have the same time set.
$now = Carbon::now()->setTimeFrom($until);
$diffOptions = [
'syntax' => Carbon::DIFF_ABSOLUTE,
'parts' => 1,
];
if ($now->diff($until)->days > 31) {
$diffOptions['parts'] = 2;
}
$params = [
'date' => $until->toDateString(),
'days' => $now->diffForHumans($until, $diffOptions),
];
return \trans('app.wallet-notice-date', $params);
}
return null;
}
}
diff --git a/src/app/Http/Controllers/Controller.php b/src/app/Http/Controllers/Controller.php
index 6bbe4b58..73e7c7d5 100644
--- a/src/app/Http/Controllers/Controller.php
+++ b/src/app/Http/Controllers/Controller.php
@@ -1,226 +1,84 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
class Controller extends BaseController
{
use AuthorizesRequests;
use DispatchesJobs;
use ValidatesRequests;
- /** @var array Common object properties in the API response */
- protected static $objectProps = [];
-
/**
* Common error response builder for API (JSON) responses
*
* @param int $code Error code
* @param string $message Error message
* @param array $data Additional response data
*
* @return \Illuminate\Http\JsonResponse
*/
public static function errorResponse(int $code, string $message = null, array $data = [])
{
$errors = [
400 => "Bad request",
401 => "Unauthorized",
403 => "Access denied",
404 => "Not found",
405 => "Method not allowed",
422 => "Input validation error",
429 => "Too many requests",
500 => "Internal server error",
];
$response = [
'status' => 'error',
'message' => $message ?: (isset($errors[$code]) ? $errors[$code] : "Server error"),
];
if (!empty($data)) {
$response = $response + $data;
}
return response()->json($response, $code);
}
/**
* Check if current user has access to the specified object
* by being an admin or existing in the same tenant context.
*
* @param ?object $object Model object
*
* @return bool
*/
protected function checkTenant(object $object = null): bool
{
if (empty($object)) {
return false;
}
$user = $this->guard()->user();
if ($user->role == 'admin') {
return true;
}
return $object->tenant_id == $user->tenant_id;
}
/**
* Get the guard to be used during authentication.
*
* @return \Illuminate\Contracts\Auth\Guard
*/
protected function guard()
{
return Auth::guard();
}
-
- /**
- * Object status' process information.
- *
- * @param object $object The object to process
- * @param array $steps The steps definition
- *
- * @return array Process state information
- */
- protected static function processStateInfo($object, array $steps): array
- {
- $process = [];
-
- // Create a process check list
- foreach ($steps as $step_name => $state) {
- $step = [
- 'label' => $step_name,
- 'title' => \trans("app.process-{$step_name}"),
- ];
-
- if (is_array($state)) {
- $step['link'] = $state[1];
- $state = $state[0];
- }
-
- $step['state'] = $state;
-
- $process[] = $step;
- }
-
- // Add domain specific steps
- if (method_exists($object, 'domain')) {
- $domain = $object->domain();
-
- // If that is not a public domain
- if ($domain && !$domain->isPublic()) {
- $domain_status = API\V4\DomainsController::statusInfo($domain);
- $process = array_merge($process, $domain_status['process']);
- }
- }
-
- $all = count($process);
- $checked = count(array_filter($process, function ($v) {
- return $v['state'];
- }));
-
- $state = $all === $checked ? 'done' : 'running';
-
- // After 180 seconds assume the process is in failed state,
- // this should unlock the Refresh button in the UI
- if ($all !== $checked && $object->created_at->diffInSeconds(\Carbon\Carbon::now()) > 180) {
- $state = 'failed';
- }
-
- return [
- 'process' => $process,
- 'processState' => $state,
- 'isReady' => $all === $checked,
- ];
- }
-
- /**
- * Object status' process information update.
- *
- * @param object $object The object to process
- *
- * @return array Process state information
- */
- protected function processStateUpdate($object): array
- {
- $response = $this->statusInfo($object); // @phpstan-ignore-line
-
- if (!empty(request()->input('refresh'))) {
- $updated = false;
- $async = false;
- $last_step = 'none';
-
- foreach ($response['process'] as $idx => $step) {
- $last_step = $step['label'];
-
- if (!$step['state']) {
- $exec = $this->execProcessStep($object, $step['label']); // @phpstan-ignore-line
-
- if (!$exec) {
- if ($exec === null) {
- $async = true;
- }
-
- break;
- }
-
- $updated = true;
- }
- }
-
- if ($updated) {
- $response = $this->statusInfo($object); // @phpstan-ignore-line
- }
-
- $success = $response['isReady'];
- $suffix = $success ? 'success' : 'error-' . $last_step;
-
- $response['status'] = $success ? 'success' : 'error';
- $response['message'] = \trans('app.process-' . $suffix);
-
- if ($async && !$success) {
- $response['processState'] = 'waiting';
- $response['status'] = 'success';
- $response['message'] = \trans('app.process-async');
- }
- }
-
- return $response;
- }
-
- /**
- * Prepare an object for the UI.
- *
- * @param object $object An object
- * @param bool $full Include all object properties
- *
- * @return array Object information
- */
- protected static function objectToClient($object, bool $full = false): array
- {
- if ($full) {
- $result = $object->toArray();
- } else {
- $result = ['id' => $object->id];
-
- foreach (static::$objectProps as $prop) {
- $result[$prop] = $object->{$prop};
- }
- }
-
- $result = array_merge($result, static::objectState($object)); // @phpstan-ignore-line
-
- return $result;
- }
}
diff --git a/src/app/Http/Controllers/RelationController.php b/src/app/Http/Controllers/RelationController.php
new file mode 100644
index 00000000..e8451fcc
--- /dev/null
+++ b/src/app/Http/Controllers/RelationController.php
@@ -0,0 +1,335 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Support\Str;
+
+class RelationController extends ResourceController
+{
+ /** @var array Common object properties in the API response */
+ protected $objectProps = [];
+
+ /** @var string Resource localization label */
+ protected $label = '';
+
+ /** @var string Resource model name */
+ protected $model = '';
+
+ /** @var array Resource listing order (column names) */
+ protected $order = [];
+
+ /** @var array Resource relation method arguments */
+ protected $relationArgs = [];
+
+ /**
+ * Delete a resource.
+ *
+ * @param string $id Resource identifier
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ public function destroy($id)
+ {
+ $resource = $this->model::find($id);
+
+ if (!$this->checkTenant($resource)) {
+ return $this->errorResponse(404);
+ }
+
+ if (!$this->guard()->user()->canDelete($resource)) {
+ return $this->errorResponse(403);
+ }
+
+ $resource->delete();
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => \trans("app.{$this->label}-delete-success"),
+ ]);
+ }
+
+ /**
+ * Listing of resources belonging to the authenticated user.
+ *
+ * The resource entitlements billed to the current user wallet(s)
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function index()
+ {
+ $user = $this->guard()->user();
+
+ $method = Str::plural(\lcfirst(\class_basename($this->model)));
+
+ $query = call_user_func_array([$user, $method], $this->relationArgs);
+
+ if (!empty($this->order)) {
+ foreach ($this->order as $col) {
+ $query->orderBy($col);
+ }
+ }
+
+ $result = $query->get()
+ ->map(function ($resource) {
+ return $this->objectToClient($resource);
+ });
+
+ return response()->json($result);
+ }
+
+ /**
+ * Prepare resource statuses for the UI
+ *
+ * @param object $resource Resource object
+ *
+ * @return array Statuses array
+ */
+ protected static function objectState($resource): array
+ {
+ return [];
+ }
+
+ /**
+ * Prepare a resource object for the UI.
+ *
+ * @param object $object An object
+ * @param bool $full Include all object properties
+ *
+ * @return array Object information
+ */
+ protected function objectToClient($object, bool $full = false): array
+ {
+ if ($full) {
+ $result = $object->toArray();
+ } else {
+ $result = ['id' => $object->id];
+
+ foreach ($this->objectProps as $prop) {
+ $result[$prop] = $object->{$prop};
+ }
+ }
+
+ $result = array_merge($result, $this->objectState($object));
+
+ return $result;
+ }
+
+ /**
+ * Object status' process information.
+ *
+ * @param object $object The object to process
+ * @param array $steps The steps definition
+ *
+ * @return array Process state information
+ */
+ protected static function processStateInfo($object, array $steps): array
+ {
+ $process = [];
+
+ // Create a process check list
+ foreach ($steps as $step_name => $state) {
+ $step = [
+ 'label' => $step_name,
+ 'title' => \trans("app.process-{$step_name}"),
+ ];
+
+ if (is_array($state)) {
+ $step['link'] = $state[1];
+ $state = $state[0];
+ }
+
+ $step['state'] = $state;
+
+ $process[] = $step;
+ }
+
+ // Add domain specific steps
+ if (method_exists($object, 'domain')) {
+ $domain = $object->domain();
+
+ // If that is not a public domain
+ if ($domain && !$domain->isPublic()) {
+ $domain_status = API\V4\DomainsController::statusInfo($domain);
+ $process = array_merge($process, $domain_status['process']);
+ }
+ }
+
+ $all = count($process);
+ $checked = count(array_filter($process, function ($v) {
+ return $v['state'];
+ }));
+
+ $state = $all === $checked ? 'done' : 'running';
+
+ // After 180 seconds assume the process is in failed state,
+ // this should unlock the Refresh button in the UI
+ if ($all !== $checked && $object->created_at->diffInSeconds(\Carbon\Carbon::now()) > 180) {
+ $state = 'failed';
+ }
+
+ return [
+ 'process' => $process,
+ 'processState' => $state,
+ 'isReady' => $all === $checked,
+ ];
+ }
+
+ /**
+ * Object status' process information update.
+ *
+ * @param object $object The object to process
+ *
+ * @return array Process state information
+ */
+ protected function processStateUpdate($object): array
+ {
+ $response = $this->statusInfo($object);
+
+ if (!empty(request()->input('refresh'))) {
+ $updated = false;
+ $async = false;
+ $last_step = 'none';
+
+ foreach ($response['process'] as $idx => $step) {
+ $last_step = $step['label'];
+
+ if (!$step['state']) {
+ $exec = $this->execProcessStep($object, $step['label']); // @phpstan-ignore-line
+
+ if (!$exec) {
+ if ($exec === null) {
+ $async = true;
+ }
+
+ break;
+ }
+
+ $updated = true;
+ }
+ }
+
+ if ($updated) {
+ $response = $this->statusInfo($object);
+ }
+
+ $success = $response['isReady'];
+ $suffix = $success ? 'success' : 'error-' . $last_step;
+
+ $response['status'] = $success ? 'success' : 'error';
+ $response['message'] = \trans('app.process-' . $suffix);
+
+ if ($async && !$success) {
+ $response['processState'] = 'waiting';
+ $response['status'] = 'success';
+ $response['message'] = \trans('app.process-async');
+ }
+ }
+
+ return $response;
+ }
+
+ /**
+ * Set the resource configuration.
+ *
+ * @param int $id Resource identifier
+ *
+ * @return \Illuminate\Http\JsonResponse|void
+ */
+ public function setConfig($id)
+ {
+ $resource = $this->model::find($id);
+
+ if (!method_exists($this->model, 'setConfig')) {
+ return $this->errorResponse(404);
+ }
+
+ if (!$this->checkTenant($resource)) {
+ return $this->errorResponse(404);
+ }
+
+ if (!$this->guard()->user()->canUpdate($resource)) {
+ return $this->errorResponse(403);
+ }
+
+ $errors = $resource->setConfig(request()->input());
+
+ if (!empty($errors)) {
+ return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ }
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => \trans("app.{$this->label}-setconfig-success"),
+ ]);
+ }
+
+ /**
+ * Display information of a resource specified by $id.
+ *
+ * @param string $id The resource to show information for.
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function show($id)
+ {
+ $resource = $this->model::find($id);
+
+ if (!$this->checkTenant($resource)) {
+ return $this->errorResponse(404);
+ }
+
+ if (!$this->guard()->user()->canRead($resource)) {
+ return $this->errorResponse(403);
+ }
+
+ $response = $this->objectToClient($resource, true);
+
+ if (!empty($statusInfo = $this->statusInfo($resource))) {
+ $response['statusInfo'] = $statusInfo;
+ }
+
+ // Resource configuration, e.g. sender_policy, invitation_policy, acl
+ if (method_exists($resource, 'getConfig')) {
+ $response['config'] = $resource->getConfig();
+ }
+
+ return response()->json($response);
+ }
+
+ /**
+ * Fetch resource status (and reload setup process)
+ *
+ * @param int $id Resource identifier
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function status($id)
+ {
+ $resource = $this->model::find($id);
+
+ if (!$this->checkTenant($resource)) {
+ return $this->errorResponse(404);
+ }
+
+ if (!$this->guard()->user()->canRead($resource)) {
+ return $this->errorResponse(403);
+ }
+
+ $response = $this->processStateUpdate($resource);
+ $response = array_merge($response, $this->objectState($resource));
+
+ return response()->json($response);
+ }
+
+ /**
+ * Resource status (extended) information
+ *
+ * @param object $resource Resource object
+ *
+ * @return array Status information
+ */
+ public static function statusInfo($resource): array
+ {
+ return [];
+ }
+}
diff --git a/src/app/Http/Controllers/ResourceController.php b/src/app/Http/Controllers/ResourceController.php
new file mode 100644
index 00000000..7b298531
--- /dev/null
+++ b/src/app/Http/Controllers/ResourceController.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+
+class ResourceController extends Controller
+{
+ /**
+ * Show the form for creating a new resource.
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function create()
+ {
+ return $this->errorResponse(404);
+ }
+
+ /**
+ * Delete a resource.
+ *
+ * @param string $id Resource identifier
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ public function destroy($id)
+ {
+ return $this->errorResponse(404);
+ }
+
+ /**
+ * Show the form for editing the specified resource.
+ *
+ * @param string $id Resource identifier
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function edit($id)
+ {
+ return $this->errorResponse(404);
+ }
+
+ /**
+ * Listing of resources belonging to the authenticated user.
+ *
+ * The resource entitlements billed to the current user wallet(s)
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function index()
+ {
+ return $this->errorResponse(404);
+ }
+
+ /**
+ * Display information of a resource specified by $id.
+ *
+ * @param string $id The resource to show information for.
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function show($id)
+ {
+ return $this->errorResponse(404);
+ }
+
+ /**
+ * Create a new resource.
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ public function store(Request $request)
+ {
+ return $this->errorResponse(404);
+ }
+
+ /**
+ * Update a resource.
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ * @param string $id Resource identifier
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ public function update(Request $request, $id)
+ {
+ return $this->errorResponse(404);
+ }
+}
diff --git a/src/routes/api.php b/src/routes/api.php
index 5459a435..036745d2 100644
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -1,263 +1,263 @@
<?php
use Illuminate\Http\Request;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
$prefix = \trim(\parse_url(\config('app.url'), PHP_URL_PATH), '/') . '/';
Route::group(
[
'middleware' => 'api',
'prefix' => $prefix . 'api/auth'
],
function ($router) {
Route::post('login', 'API\AuthController@login');
Route::group(
['middleware' => 'auth:api'],
function ($router) {
Route::get('info', 'API\AuthController@info');
Route::post('info', 'API\AuthController@info');
Route::post('logout', 'API\AuthController@logout');
Route::post('refresh', 'API\AuthController@refresh');
}
);
}
);
Route::group(
[
'domain' => \config('app.website_domain'),
'middleware' => 'api',
'prefix' => $prefix . 'api/auth'
],
function ($router) {
Route::post('password-reset/init', 'API\PasswordResetController@init');
Route::post('password-reset/verify', 'API\PasswordResetController@verify');
Route::post('password-reset', 'API\PasswordResetController@reset');
Route::post('signup/init', 'API\SignupController@init');
Route::get('signup/invitations/{id}', 'API\SignupController@invitation');
Route::get('signup/plans', 'API\SignupController@plans');
Route::post('signup/verify', 'API\SignupController@verify');
Route::post('signup', 'API\SignupController@signup');
}
);
Route::group(
[
'domain' => \config('app.website_domain'),
'middleware' => 'auth:api',
'prefix' => $prefix . 'api/v4'
],
function () {
Route::post('companion/register', 'API\V4\CompanionAppsController@register');
Route::post('auth-attempts/{id}/confirm', 'API\V4\AuthAttemptsController@confirm');
Route::post('auth-attempts/{id}/deny', 'API\V4\AuthAttemptsController@deny');
Route::get('auth-attempts/{id}/details', 'API\V4\AuthAttemptsController@details');
Route::get('auth-attempts', 'API\V4\AuthAttemptsController@index');
- Route::apiResource('domains', API\V4\DomainsController::class);
+ Route::apiResource('domains', 'API\V4\DomainsController');
Route::get('domains/{id}/confirm', 'API\V4\DomainsController@confirm');
Route::get('domains/{id}/skus', 'API\V4\SkusController@domainSkus');
Route::get('domains/{id}/status', 'API\V4\DomainsController@status');
Route::post('domains/{id}/config', 'API\V4\DomainsController@setConfig');
- Route::apiResource('groups', API\V4\GroupsController::class);
+ Route::apiResource('groups', 'API\V4\GroupsController');
Route::get('groups/{id}/status', 'API\V4\GroupsController@status');
Route::post('groups/{id}/config', 'API\V4\GroupsController@setConfig');
- Route::apiResource('packages', API\V4\PackagesController::class);
+ Route::apiResource('packages', 'API\V4\PackagesController');
- Route::apiResource('resources', API\V4\ResourcesController::class);
+ Route::apiResource('resources', 'API\V4\ResourcesController');
Route::get('resources/{id}/status', 'API\V4\ResourcesController@status');
Route::post('resources/{id}/config', 'API\V4\ResourcesController@setConfig');
- Route::apiResource('shared-folders', API\V4\SharedFoldersController::class);
+ Route::apiResource('shared-folders', 'API\V4\SharedFoldersController');
Route::get('shared-folders/{id}/status', 'API\V4\SharedFoldersController@status');
Route::post('shared-folders/{id}/config', 'API\V4\SharedFoldersController@setConfig');
- Route::apiResource('skus', API\V4\SkusController::class);
+ Route::apiResource('skus', 'API\V4\SkusController');
- Route::apiResource('users', API\V4\UsersController::class);
+ Route::apiResource('users', 'API\V4\UsersController');
Route::post('users/{id}/config', 'API\V4\UsersController@setConfig');
Route::get('users/{id}/skus', 'API\V4\SkusController@userSkus');
Route::get('users/{id}/status', 'API\V4\UsersController@status');
- Route::apiResource('wallets', API\V4\WalletsController::class);
+ Route::apiResource('wallets', 'API\V4\WalletsController');
Route::get('wallets/{id}/transactions', 'API\V4\WalletsController@transactions');
Route::get('wallets/{id}/receipts', 'API\V4\WalletsController@receipts');
Route::get('wallets/{id}/receipts/{receipt}', 'API\V4\WalletsController@receiptDownload');
Route::post('payments', 'API\V4\PaymentsController@store');
//Route::delete('payments', 'API\V4\PaymentsController@cancel');
Route::get('payments/mandate', 'API\V4\PaymentsController@mandate');
Route::post('payments/mandate', 'API\V4\PaymentsController@mandateCreate');
Route::put('payments/mandate', 'API\V4\PaymentsController@mandateUpdate');
Route::delete('payments/mandate', 'API\V4\PaymentsController@mandateDelete');
Route::get('payments/methods', 'API\V4\PaymentsController@paymentMethods');
Route::get('payments/pending', 'API\V4\PaymentsController@payments');
Route::get('payments/has-pending', 'API\V4\PaymentsController@hasPayments');
Route::get('openvidu/rooms', 'API\V4\OpenViduController@index');
Route::post('openvidu/rooms/{id}/close', 'API\V4\OpenViduController@closeRoom');
Route::post('openvidu/rooms/{id}/config', 'API\V4\OpenViduController@setRoomConfig');
// FIXME: I'm not sure about this one, should we use DELETE request maybe?
Route::post('openvidu/rooms/{id}/connections/{conn}/dismiss', 'API\V4\OpenViduController@dismissConnection');
Route::put('openvidu/rooms/{id}/connections/{conn}', 'API\V4\OpenViduController@updateConnection');
Route::post('openvidu/rooms/{id}/request/{reqid}/accept', 'API\V4\OpenViduController@acceptJoinRequest');
Route::post('openvidu/rooms/{id}/request/{reqid}/deny', 'API\V4\OpenViduController@denyJoinRequest');
}
);
// Note: In Laravel 7.x we could just use withoutMiddleware() instead of a separate group
Route::group(
[
'domain' => \config('app.website_domain'),
'prefix' => $prefix . 'api/v4'
],
function () {
Route::post('openvidu/rooms/{id}', 'API\V4\OpenViduController@joinRoom');
Route::post('openvidu/rooms/{id}/connections', 'API\V4\OpenViduController@createConnection');
// FIXME: I'm not sure about this one, should we use DELETE request maybe?
Route::post('openvidu/rooms/{id}/connections/{conn}/dismiss', 'API\V4\OpenViduController@dismissConnection');
Route::put('openvidu/rooms/{id}/connections/{conn}', 'API\V4\OpenViduController@updateConnection');
Route::post('openvidu/rooms/{id}/request/{reqid}/accept', 'API\V4\OpenViduController@acceptJoinRequest');
Route::post('openvidu/rooms/{id}/request/{reqid}/deny', 'API\V4\OpenViduController@denyJoinRequest');
}
);
Route::group(
[
'domain' => \config('app.website_domain'),
'middleware' => 'api',
'prefix' => $prefix . 'api/v4'
],
function ($router) {
Route::post('support/request', 'API\V4\SupportController@request');
}
);
Route::group(
[
'domain' => \config('app.website_domain'),
'prefix' => $prefix . 'api/webhooks'
],
function () {
Route::post('payment/{provider}', 'API\V4\PaymentsController@webhook');
Route::post('meet/openvidu', 'API\V4\OpenViduController@webhook');
}
);
if (\config('app.with_services')) {
Route::group(
[
'domain' => 'services.' . \config('app.website_domain'),
'prefix' => $prefix . 'api/webhooks'
],
function () {
Route::get('nginx', 'API\V4\NGINXController@authenticate');
Route::post('policy/greylist', 'API\V4\PolicyController@greylist');
Route::post('policy/ratelimit', 'API\V4\PolicyController@ratelimit');
Route::post('policy/spf', 'API\V4\PolicyController@senderPolicyFramework');
}
);
}
if (\config('app.with_admin')) {
Route::group(
[
'domain' => 'admin.' . \config('app.website_domain'),
'middleware' => ['auth:api', 'admin'],
'prefix' => $prefix . 'api/v4',
],
function () {
- Route::apiResource('domains', API\V4\Admin\DomainsController::class);
+ Route::apiResource('domains', 'API\V4\Admin\DomainsController');
Route::get('domains/{id}/skus', 'API\V4\Admin\SkusController@domainSkus');
Route::post('domains/{id}/suspend', 'API\V4\Admin\DomainsController@suspend');
Route::post('domains/{id}/unsuspend', 'API\V4\Admin\DomainsController@unsuspend');
- Route::apiResource('groups', API\V4\Admin\GroupsController::class);
+ Route::apiResource('groups', 'API\V4\Admin\GroupsController');
Route::post('groups/{id}/suspend', 'API\V4\Admin\GroupsController@suspend');
Route::post('groups/{id}/unsuspend', 'API\V4\Admin\GroupsController@unsuspend');
- Route::apiResource('resources', API\V4\Admin\ResourcesController::class);
- Route::apiResource('shared-folders', API\V4\Admin\SharedFoldersController::class);
- Route::apiResource('skus', API\V4\Admin\SkusController::class);
- Route::apiResource('users', API\V4\Admin\UsersController::class);
+ Route::apiResource('resources', 'API\V4\Admin\ResourcesController');
+ Route::apiResource('shared-folders', 'API\V4\Admin\SharedFoldersController');
+ Route::apiResource('skus', 'API\V4\Admin\SkusController');
+ Route::apiResource('users', 'API\V4\Admin\UsersController');
Route::get('users/{id}/discounts', 'API\V4\Reseller\DiscountsController@userDiscounts');
Route::post('users/{id}/reset2FA', 'API\V4\Admin\UsersController@reset2FA');
Route::get('users/{id}/skus', 'API\V4\Admin\SkusController@userSkus');
Route::post('users/{id}/skus/{sku}', 'API\V4\Admin\UsersController@setSku');
Route::post('users/{id}/suspend', 'API\V4\Admin\UsersController@suspend');
Route::post('users/{id}/unsuspend', 'API\V4\Admin\UsersController@unsuspend');
- Route::apiResource('wallets', API\V4\Admin\WalletsController::class);
+ Route::apiResource('wallets', 'API\V4\Admin\WalletsController');
Route::post('wallets/{id}/one-off', 'API\V4\Admin\WalletsController@oneOff');
Route::get('wallets/{id}/transactions', 'API\V4\Admin\WalletsController@transactions');
Route::get('stats/chart/{chart}', 'API\V4\Admin\StatsController@chart');
}
);
}
if (\config('app.with_reseller')) {
Route::group(
[
'domain' => 'reseller.' . \config('app.website_domain'),
'middleware' => ['auth:api', 'reseller'],
'prefix' => $prefix . 'api/v4',
],
function () {
- Route::apiResource('domains', API\V4\Reseller\DomainsController::class);
+ Route::apiResource('domains', 'API\V4\Reseller\DomainsController');
Route::get('domains/{id}/skus', 'API\V4\Reseller\SkusController@domainSkus');
Route::post('domains/{id}/suspend', 'API\V4\Reseller\DomainsController@suspend');
Route::post('domains/{id}/unsuspend', 'API\V4\Reseller\DomainsController@unsuspend');
- Route::apiResource('groups', API\V4\Reseller\GroupsController::class);
+ Route::apiResource('groups', 'API\V4\Reseller\GroupsController');
Route::post('groups/{id}/suspend', 'API\V4\Reseller\GroupsController@suspend');
Route::post('groups/{id}/unsuspend', 'API\V4\Reseller\GroupsController@unsuspend');
- Route::apiResource('invitations', API\V4\Reseller\InvitationsController::class);
+ Route::apiResource('invitations', 'API\V4\Reseller\InvitationsController');
Route::post('invitations/{id}/resend', 'API\V4\Reseller\InvitationsController@resend');
Route::post('payments', 'API\V4\Reseller\PaymentsController@store');
Route::get('payments/mandate', 'API\V4\Reseller\PaymentsController@mandate');
Route::post('payments/mandate', 'API\V4\Reseller\PaymentsController@mandateCreate');
Route::put('payments/mandate', 'API\V4\Reseller\PaymentsController@mandateUpdate');
Route::delete('payments/mandate', 'API\V4\Reseller\PaymentsController@mandateDelete');
Route::get('payments/methods', 'API\V4\Reseller\PaymentsController@paymentMethods');
Route::get('payments/pending', 'API\V4\Reseller\PaymentsController@payments');
Route::get('payments/has-pending', 'API\V4\Reseller\PaymentsController@hasPayments');
- Route::apiResource('resources', API\V4\Reseller\ResourcesController::class);
- Route::apiResource('shared-folders', API\V4\Reseller\SharedFoldersController::class);
- Route::apiResource('skus', API\V4\Reseller\SkusController::class);
- Route::apiResource('users', API\V4\Reseller\UsersController::class);
+ Route::apiResource('resources', 'API\V4\Reseller\ResourcesController');
+ Route::apiResource('shared-folders', 'API\V4\Reseller\SharedFoldersController');
+ Route::apiResource('skus', 'API\V4\Reseller\SkusController');
+ Route::apiResource('users', 'API\V4\Reseller\UsersController');
Route::get('users/{id}/discounts', 'API\V4\Reseller\DiscountsController@userDiscounts');
Route::post('users/{id}/reset2FA', 'API\V4\Reseller\UsersController@reset2FA');
Route::get('users/{id}/skus', 'API\V4\Reseller\SkusController@userSkus');
Route::post('users/{id}/skus/{sku}', 'API\V4\Admin\UsersController@setSku');
Route::post('users/{id}/suspend', 'API\V4\Reseller\UsersController@suspend');
Route::post('users/{id}/unsuspend', 'API\V4\Reseller\UsersController@unsuspend');
- Route::apiResource('wallets', API\V4\Reseller\WalletsController::class);
+ Route::apiResource('wallets', 'API\V4\Reseller\WalletsController');
Route::post('wallets/{id}/one-off', 'API\V4\Reseller\WalletsController@oneOff');
Route::get('wallets/{id}/receipts', 'API\V4\Reseller\WalletsController@receipts');
Route::get('wallets/{id}/receipts/{receipt}', 'API\V4\Reseller\WalletsController@receiptDownload');
Route::get('wallets/{id}/transactions', 'API\V4\Reseller\WalletsController@transactions');
Route::get('stats/chart/{chart}', 'API\V4\Reseller\StatsController@chart');
}
);
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Feb 1, 4:24 PM (1 d, 19 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
426744
Default Alt Text
(148 KB)

Event Timeline