Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2528619
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
148 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Feb 1, 4:24 PM (1 d, 21 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
426744
Default Alt Text
(148 KB)
Attached To
Mode
R2 kolab
Attached
Detach File
Event Timeline
Log In to Comment