Page MenuHomePhorge

No OneTemporary

Size
25 KB
Referenced Files
None
Subscribers
None
diff --git a/src/app/Http/Controllers/API/V4/Admin/UsersController.php b/src/app/Http/Controllers/API/V4/Admin/UsersController.php
index 0caefacf..494ad9ed 100644
--- a/src/app/Http/Controllers/API/V4/Admin/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/UsersController.php
@@ -1,308 +1,333 @@
<?php
namespace App\Http\Controllers\API\V4\Admin;
use App\Domain;
use App\Group;
use App\Sku;
use App\User;
use App\UserAlias;
use App\UserSetting;
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
*
* @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 = UserAlias::where('alias', $search)->get()->pluck('user_id');
// Search by an external email
$ext_user_ids = 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 email
if ($group = Group::withTrashed()->where('email', $search)->first()) {
$user_ids = $user_ids->merge([$group->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) {
$data = $user->toArray();
$data = array_merge($data, self::userStatuses($user));
return $data;
}
);
$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' => __('app.user-reset-2fa-success'),
]);
}
/**
* 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' => __('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' => __('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' => __('app.user-update-success'),
]);
}
}
diff --git a/src/tests/Feature/Controller/Admin/UsersTest.php b/src/tests/Feature/Controller/Admin/UsersTest.php
index a5f0a74a..b5431433 100644
--- a/src/tests/Feature/Controller/Admin/UsersTest.php
+++ b/src/tests/Feature/Controller/Admin/UsersTest.php
@@ -1,385 +1,436 @@
<?php
namespace Tests\Feature\Controller\Admin;
use App\Auth\SecondFactor;
use App\Sku;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class UsersTest extends TestCase
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
self::useAdminUrl();
$this->deleteTestUser('UsersControllerTest1@userscontroller.com');
$this->deleteTestUser('test@testsearch.com');
$this->deleteTestDomain('testsearch.com');
$this->deleteTestGroup('group-test@kolab.org');
$jack = $this->getTestUser('jack@kolab.org');
$jack->setSetting('external_email', null);
}
/**
* {@inheritDoc}
*/
public function tearDown(): void
{
$this->deleteTestUser('UsersControllerTest1@userscontroller.com');
$this->deleteTestUser('test@testsearch.com');
$this->deleteTestDomain('testsearch.com');
$jack = $this->getTestUser('jack@kolab.org');
$jack->setSetting('external_email', null);
parent::tearDown();
}
/**
* Test user deleting (DELETE /api/v4/users/<id>)
*/
public function testDestroy(): void
{
$john = $this->getTestUser('john@kolab.org');
$user = $this->getTestUser('UsersControllerTest1@userscontroller.com');
$admin = $this->getTestUser('jeroen@jeroen.jeroen');
// Test unauth access
$response = $this->delete("api/v4/users/{$user->id}");
$response->assertStatus(401);
// The end-point does not exist
$response = $this->actingAs($admin)->delete("api/v4/users/{$user->id}");
$response->assertStatus(404);
}
/**
* Test users searching (/api/v4/users)
*/
public function testIndex(): void
{
$user = $this->getTestUser('john@kolab.org');
$admin = $this->getTestUser('jeroen@jeroen.jeroen');
$group = $this->getTestGroup('group-test@kolab.org');
$group->assignToWallet($user->wallets->first());
// Non-admin user
$response = $this->actingAs($user)->get("api/v4/users");
$response->assertStatus(403);
// Search with no search criteria
$response = $this->actingAs($admin)->get("api/v4/users");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame(0, $json['count']);
$this->assertSame([], $json['list']);
// Search with no matches expected
$response = $this->actingAs($admin)->get("api/v4/users?search=abcd1234efgh5678");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame(0, $json['count']);
$this->assertSame([], $json['list']);
// Search by domain
$response = $this->actingAs($admin)->get("api/v4/users?search=kolab.org");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame(1, $json['count']);
$this->assertCount(1, $json['list']);
$this->assertSame($user->id, $json['list'][0]['id']);
$this->assertSame($user->email, $json['list'][0]['email']);
// Search by user ID
$response = $this->actingAs($admin)->get("api/v4/users?search={$user->id}");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame(1, $json['count']);
$this->assertCount(1, $json['list']);
$this->assertSame($user->id, $json['list'][0]['id']);
$this->assertSame($user->email, $json['list'][0]['email']);
// Search by email (primary)
$response = $this->actingAs($admin)->get("api/v4/users?search=john@kolab.org");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame(1, $json['count']);
$this->assertCount(1, $json['list']);
$this->assertSame($user->id, $json['list'][0]['id']);
$this->assertSame($user->email, $json['list'][0]['email']);
// Search by email (alias)
$response = $this->actingAs($admin)->get("api/v4/users?search=john.doe@kolab.org");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame(1, $json['count']);
$this->assertCount(1, $json['list']);
$this->assertSame($user->id, $json['list'][0]['id']);
$this->assertSame($user->email, $json['list'][0]['email']);
// Search by email (external), expect two users in a result
$jack = $this->getTestUser('jack@kolab.org');
$jack->setSetting('external_email', 'john.doe.external@gmail.com');
$response = $this->actingAs($admin)->get("api/v4/users?search=john.doe.external@gmail.com");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame(2, $json['count']);
$this->assertCount(2, $json['list']);
$emails = array_column($json['list'], 'email');
$this->assertContains($user->email, $emails);
$this->assertContains($jack->email, $emails);
// Search by owner
$response = $this->actingAs($admin)->get("api/v4/users?owner={$user->id}");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame(4, $json['count']);
$this->assertCount(4, $json['list']);
// Search by owner (Ned is a controller on John's wallets,
// here we expect only users assigned to Ned's wallet(s))
$ned = $this->getTestUser('ned@kolab.org');
$response = $this->actingAs($admin)->get("api/v4/users?owner={$ned->id}");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame(0, $json['count']);
$this->assertCount(0, $json['list']);
// Search by distribution list email
$response = $this->actingAs($admin)->get("api/v4/users?search=group-test@kolab.org");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame(1, $json['count']);
$this->assertCount(1, $json['list']);
$this->assertSame($user->id, $json['list'][0]['id']);
$this->assertSame($user->email, $json['list'][0]['email']);
// Deleted users/domains
$domain = $this->getTestDomain('testsearch.com', ['type' => \App\Domain::TYPE_EXTERNAL]);
$user = $this->getTestUser('test@testsearch.com');
$plan = \App\Plan::where('title', 'group')->first();
$user->assignPlan($plan, $domain);
$user->setAliases(['alias@testsearch.com']);
+
+ $wallet = $user->wallets()->first();
+ $wallet->setSetting('mollie_id', 'cst_nonsense');
+
+ \App\Payment::create(
+ [
+ 'id' => 'tr_nonsense',
+ 'wallet_id' => $wallet->id,
+ 'status' => 'paid',
+ 'amount' => 1337,
+ 'description' => 'nonsense transaction for testing',
+ 'provider' => 'self',
+ 'type' => 'oneoff',
+ 'currency' => 'CHF',
+ 'currency_amount' => 1337
+ ]
+ );
+
Queue::fake();
$user->delete();
$response = $this->actingAs($admin)->get("api/v4/users?search=test@testsearch.com");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame(1, $json['count']);
$this->assertCount(1, $json['list']);
$this->assertSame($user->id, $json['list'][0]['id']);
$this->assertSame($user->email, $json['list'][0]['email']);
$this->assertTrue($json['list'][0]['isDeleted']);
$response = $this->actingAs($admin)->get("api/v4/users?search=alias@testsearch.com");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame(1, $json['count']);
$this->assertCount(1, $json['list']);
$this->assertSame($user->id, $json['list'][0]['id']);
$this->assertSame($user->email, $json['list'][0]['email']);
$this->assertTrue($json['list'][0]['isDeleted']);
$response = $this->actingAs($admin)->get("api/v4/users?search=testsearch.com");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame(1, $json['count']);
$this->assertCount(1, $json['list']);
$this->assertSame($user->id, $json['list'][0]['id']);
$this->assertSame($user->email, $json['list'][0]['email']);
$this->assertTrue($json['list'][0]['isDeleted']);
+
+ $response = $this->actingAs($admin)->get("api/v4/users?search={$wallet->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(1, $json['count']);
+ $this->assertCount(1, $json['list']);
+ $this->assertSame($user->id, $json['list'][0]['id']);
+ $this->assertSame($user->email, $json['list'][0]['email']);
+ $this->assertTrue($json['list'][0]['isDeleted']);
+
+ $response = $this->actingAs($admin)->get("api/v4/users?search=tr_nonsense");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(1, $json['count']);
+ $this->assertCount(1, $json['list']);
+ $this->assertSame($user->id, $json['list'][0]['id']);
+ $this->assertSame($user->email, $json['list'][0]['email']);
+ $this->assertTrue($json['list'][0]['isDeleted']);
+
+ $response = $this->actingAs($admin)->get("api/v4/users?search=cst_nonsense");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(1, $json['count']);
+ $this->assertCount(1, $json['list']);
+ $this->assertSame($user->id, $json['list'][0]['id']);
+ $this->assertSame($user->email, $json['list'][0]['email']);
+ $this->assertTrue($json['list'][0]['isDeleted']);
}
/**
* Test reseting 2FA (POST /api/v4/users/<user-id>/reset2FA)
*/
public function testReset2FA(): void
{
$user = $this->getTestUser('UsersControllerTest1@userscontroller.com');
$admin = $this->getTestUser('jeroen@jeroen.jeroen');
$sku2fa = Sku::withEnvTenantContext()->where(['title' => '2fa'])->first();
$user->assignSku($sku2fa);
SecondFactor::seed('userscontrollertest1@userscontroller.com');
// Test unauthorized access to admin API
$response = $this->actingAs($user)->post("/api/v4/users/{$user->id}/reset2FA", []);
$response->assertStatus(403);
$entitlements = $user->fresh()->entitlements()->where('sku_id', $sku2fa->id)->get();
$this->assertCount(1, $entitlements);
$sf = new SecondFactor($user);
$this->assertCount(1, $sf->factors());
// Test reseting 2FA
$response = $this->actingAs($admin)->post("/api/v4/users/{$user->id}/reset2FA", []);
$response->assertStatus(200);
$json = $response->json();
$this->assertSame('success', $json['status']);
$this->assertSame("2-Factor authentication reset successfully.", $json['message']);
$this->assertCount(2, $json);
$entitlements = $user->fresh()->entitlements()->where('sku_id', $sku2fa->id)->get();
$this->assertCount(0, $entitlements);
$sf = new SecondFactor($user);
$this->assertCount(0, $sf->factors());
}
/**
* Test user creation (POST /api/v4/users)
*/
public function testStore(): void
{
$admin = $this->getTestUser('jeroen@jeroen.jeroen');
// The end-point does not exist
$response = $this->actingAs($admin)->post("/api/v4/users", []);
$response->assertStatus(404);
}
/**
* Test user suspending (POST /api/v4/users/<user-id>/suspend)
*/
public function testSuspend(): void
{
Queue::fake(); // disable jobs
$user = $this->getTestUser('UsersControllerTest1@userscontroller.com');
$admin = $this->getTestUser('jeroen@jeroen.jeroen');
// Test unauthorized access to admin API
$response = $this->actingAs($user)->post("/api/v4/users/{$user->id}/suspend", []);
$response->assertStatus(403);
$this->assertFalse($user->isSuspended());
// Test suspending the user
$response = $this->actingAs($admin)->post("/api/v4/users/{$user->id}/suspend", []);
$response->assertStatus(200);
$json = $response->json();
$this->assertSame('success', $json['status']);
$this->assertSame("User suspended successfully.", $json['message']);
$this->assertCount(2, $json);
$this->assertTrue($user->fresh()->isSuspended());
}
/**
* Test user un-suspending (POST /api/v4/users/<user-id>/unsuspend)
*/
public function testUnsuspend(): void
{
Queue::fake(); // disable jobs
$user = $this->getTestUser('UsersControllerTest1@userscontroller.com');
$admin = $this->getTestUser('jeroen@jeroen.jeroen');
// Test unauthorized access to admin API
$response = $this->actingAs($user)->post("/api/v4/users/{$user->id}/unsuspend", []);
$response->assertStatus(403);
$this->assertFalse($user->isSuspended());
$user->suspend();
$this->assertTrue($user->isSuspended());
// Test suspending the user
$response = $this->actingAs($admin)->post("/api/v4/users/{$user->id}/unsuspend", []);
$response->assertStatus(200);
$json = $response->json();
$this->assertSame('success', $json['status']);
$this->assertSame("User unsuspended successfully.", $json['message']);
$this->assertCount(2, $json);
$this->assertFalse($user->fresh()->isSuspended());
}
/**
* Test user update (PUT /api/v4/users/<user-id>)
*/
public function testUpdate(): void
{
$user = $this->getTestUser('UsersControllerTest1@userscontroller.com');
$admin = $this->getTestUser('jeroen@jeroen.jeroen');
// Test unauthorized access to admin API
$response = $this->actingAs($user)->put("/api/v4/users/{$user->id}", []);
$response->assertStatus(403);
// Test updatig the user data (empty data)
$response = $this->actingAs($admin)->put("/api/v4/users/{$user->id}", []);
$response->assertStatus(200);
$json = $response->json();
$this->assertSame('success', $json['status']);
$this->assertSame("User data updated successfully.", $json['message']);
$this->assertCount(2, $json);
// Test error handling
$post = ['external_email' => 'aaa'];
$response = $this->actingAs($admin)->put("/api/v4/users/{$user->id}", $post);
$response->assertStatus(422);
$json = $response->json();
$this->assertSame('error', $json['status']);
$this->assertSame("The external email must be a valid email address.", $json['errors']['external_email'][0]);
$this->assertCount(2, $json);
// Test real update
$post = ['external_email' => 'modified@test.com'];
$response = $this->actingAs($admin)->put("/api/v4/users/{$user->id}", $post);
$response->assertStatus(200);
$json = $response->json();
$this->assertSame('success', $json['status']);
$this->assertSame("User data updated successfully.", $json['message']);
$this->assertCount(2, $json);
$this->assertSame('modified@test.com', $user->getSetting('external_email'));
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Apr 18, 10:16 AM (12 h, 13 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
435704
Default Alt Text
(25 KB)

Event Timeline