Page MenuHomePhorge

D5664.1774828816.diff
No OneTemporary

Authored By
Unknown
Size
19 KB
Referenced Files
None
Subscribers
None

D5664.1774828816.diff

diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php
--- a/src/app/Http/Controllers/API/V4/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/UsersController.php
@@ -113,8 +113,22 @@
$result = $user->users();
+ // Search by role
+ if (str_starts_with($search, 'role:')) {
+ // Finding out account controllers is tricky. Which wallet(s)?
+ $wallets = array_filter($result->getBindings(), fn ($v) => !str_contains($v, '\\'));
+
+ $controllers = User::whereIn('id', DB::table('user_accounts')->select('user_id')->whereIn('wallet_id', $wallets));
+
+ if ($search == 'role:controller') {
+ $result = $controllers;
+ } else {
+ // role:user
+ $result = $result->whereNotIn('users.id', $controllers->pluck('id')->all());
+ }
+ }
// Search by user email, alias or name
- if ($search !== '') {
+ elseif ($search !== '') {
// thanks to cloning we skip some extra queries in $user->users()
$allUsers1 = clone $result;
$allUsers2 = clone $result;
diff --git a/src/app/Http/Controllers/API/V4/WalletsController.php b/src/app/Http/Controllers/API/V4/WalletsController.php
--- a/src/app/Http/Controllers/API/V4/WalletsController.php
+++ b/src/app/Http/Controllers/API/V4/WalletsController.php
@@ -10,6 +10,7 @@
use App\ReferralCode;
use App\ReferralProgram;
use App\Transaction;
+use App\User;
use App\Wallet;
use Dedoc\Scramble\Attributes\QueryParameter;
use Dedoc\Scramble\Attributes\Response as ResponseDefinition;
@@ -18,11 +19,82 @@
use Illuminate\Http\Response;
use Illuminate\Support\Facades\DB;
-/**
- * API\WalletsController
- */
class WalletsController extends ResourceController
{
+ /**
+ * Add controller.
+ *
+ * @param string $id Wallet identifier
+ * @param int $userid User identifier
+ */
+ public function controllerAdd($id, $userid): JsonResponse
+ {
+ $wallet = Wallet::find($id);
+
+ if (empty($wallet) || !$this->checkTenant($wallet->owner)) {
+ return $this->errorResponse(404);
+ }
+
+ $user = User::find($userid);
+
+ if (empty($user) || !$this->checkTenant($user)) {
+ return $this->errorResponse(404);
+ }
+
+ $actor = $this->guard()->user();
+
+ // Only wallet owner can add controllers
+ if ($wallet->user_id != $actor->id || $user->id == $wallet->user_id || !$actor->canRead($user)) {
+ return $this->errorResponse(403);
+ }
+
+ $wallet->addController($user);
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => self::trans('app.wallet-add-controller-success'),
+ ]);
+ }
+
+ /**
+ * Delete controller.
+ *
+ * @param string $id Wallet identifier
+ * @param int $userid User identifier
+ */
+ public function controllerDelete($id, $userid): JsonResponse
+ {
+ $wallet = Wallet::find($id);
+
+ if (empty($wallet) || !$this->checkTenant($wallet->owner)) {
+ return $this->errorResponse(404);
+ }
+
+ $user = User::find($userid);
+
+ if (empty($user) || !$this->checkTenant($user)) {
+ return $this->errorResponse(404);
+ }
+
+ $actor = $this->guard()->user();
+
+ if (!$wallet->isController($user)) {
+ return $this->errorResponse(404);
+ }
+
+ // Only wallet owner can remove controllers
+ if ($wallet->user_id != $actor->id || $user->id == $wallet->user_id) {
+ return $this->errorResponse(403);
+ }
+
+ $wallet->removeController($user);
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => self::trans('app.wallet-delete-controller-success'),
+ ]);
+ }
+
/**
* Wallet information.
*
diff --git a/src/resources/lang/en/app.php b/src/resources/lang/en/app.php
--- a/src/resources/lang/en/app.php
+++ b/src/resources/lang/en/app.php
@@ -179,6 +179,9 @@
'password-rule-special' => 'Password contains a special character',
'password-rule-last' => 'Password cannot be the same as the last :param passwords',
+ 'wallet-add-controller-success' => 'Account controller role set successfully.',
+ 'wallet-delete-controller-success' => 'Account controller role removed successfully.',
+
'wallet-notice-date' => 'With your current subscriptions your account balance will last until about :date (:days).',
'wallet-notice-nocredit' => 'You are out of credit, top up your balance now.',
'wallet-notice-today' => 'You will run out of credit today, top up your balance now.',
diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php
--- a/src/resources/lang/en/ui.php
+++ b/src/resources/lang/en/ui.php
@@ -574,6 +574,11 @@
'reset-2fa' => "Reset 2-Factor Auth",
'reset-2fa-title' => "2-Factor Authentication Reset",
'resources' => "Resources",
+ 'role' => "User role",
+ 'role-user' => "User",
+ 'role-controller' => "Controller",
+ 'roleselect' => "Select a role",
+ 'roleselectbody' => "By assigning a controller role you give permissions to manage your account by this user.",
'title' => "User account",
'search' => "User email address or name",
'search-pl' => "User ID, email or domain",
diff --git a/src/resources/vue/User/Info.vue b/src/resources/vue/User/Info.vue
--- a/src/resources/vue/User/Info.vue
+++ b/src/resources/vue/User/Info.vue
@@ -23,7 +23,13 @@
<div class="col-sm-8">
<span class="form-control-plaintext">
<span id="status" :class="$root.statusClass(user)">{{ $root.statusText(user) }}</span>
- <span id="userid" v-if="$route.name === 'settings'">&nbsp;({{ user_id }})</span>
+ <span v-if="$route.name === 'settings'" id="userid">&nbsp;({{ user_id }})</span>
+ <btn v-else-if="!$root.hasPermission('wallets')" id="user_role"
+ class="ms-2 badge rounded-pill btn-secondary" :title="$t('user.role')"
+ @click="!isSelf && $refs.roleSelectDialog.show()"
+ >
+ {{ $t(user.isController ? 'user.role-controller' : 'user.role-user') }}
+ </btn>
</span>
</div>
</div>
@@ -279,6 +285,17 @@
<p><input type="text" class="form-control" id="short_code" value=""></p>
</div>
</modal-dialog>
+ <modal-dialog id="role-select" ref="roleSelectDialog" :buttons="['save']" @click="roleSelect()" :title="$t('user.roleselect')">
+ <div>
+ <p>
+ <select class="form-select" id="user_role_select">
+ <option value="user" selected>{{ $t('user.role-user') }}</option>
+ <option value="controller">{{ $t('user.role-controller') }}</option>
+ </select>
+ </p>
+ <span class="form-text">{{ $t('user.roleselectbody') }}</span>
+ </div>
+ </modal-dialog>
</div>
</template>
@@ -423,6 +440,7 @@
this.user = { ...response.data, ...response.data.settings }
this.status = response.data.statusInfo
this.passwordLinkCode = this.user.passwordLinkCode
+ this.user.isController = this.user.statusInfo.enableUsers
})
.catch(this.$root.errorHandler)
@@ -449,6 +467,12 @@
this.delegatee = null
}
})
+
+ this.$refs.roleSelectDialog.events({
+ show: (event) => {
+ $('select', this.$refs.roleSelectDialog.$el).val(this.user.isController ? 'controller' : 'user')
+ }
+ })
},
methods: {
codeValidate() {
@@ -486,6 +510,22 @@
}
})
},
+ roleSelect() {
+ const dialog = this.$refs.roleSelectDialog
+ const role = $('select', dialog.$el).val()
+
+ if (role == (this.user.isController ? 'controller' : 'user')) {
+ dialog.hide()
+ return
+ }
+
+ axios[role == 'user' ? 'delete' : 'post']('/api/v4/wallets/' + this.user.wallet.id + '/controllers/' + this.user_id)
+ .then(response => {
+ dialog.hide()
+ this.user = { ...this.user, ...{ isController: (role == 'controller') } }
+ this.$toast.success(response.data.message)
+ })
+ },
setPasswordMode(event) {
const mode = event.target.checked ? event.target.value : ''
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -256,6 +256,8 @@
Route::get('wallets/{id}/receipts', [API\V4\WalletsController::class, 'receipts']);
Route::get('wallets/{id}/receipts/{receipt}', [API\V4\WalletsController::class, 'receiptDownload']);
Route::get('wallets/{id}/referral-programs', [API\V4\WalletsController::class, 'referralPrograms']);
+ Route::post('wallets/{id}/controllers/{userid}', [API\V4\WalletsController::class, 'controllerAdd']);
+ Route::delete('wallets/{id}/controllers/{userid}', [API\V4\WalletsController::class, 'controllerDelete']);
Route::get('policies', [API\V4\PolicyController::class, 'index']);
Route::post('password-reset/code', [API\PasswordResetController::class, 'codeCreate']);
diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php
--- a/src/tests/Browser/UsersTest.php
+++ b/src/tests/Browser/UsersTest.php
@@ -846,6 +846,58 @@
});
}
+ /**
+ * Test controller "role"
+ */
+ public function testControllerRole(): void
+ {
+ $this->browse(function (Browser $browser) {
+ $john = User::where('email', 'john@kolab.org')->first();
+ $julia = $this->getTestUser('julia.roberts@kolab.org');
+ $storage_sku = Sku::withEnvTenantContext()->where('title', 'storage')->first();
+ $wallet = $john->wallets()->first();
+ $julia->assignSku($storage_sku, 1, $wallet);
+
+ $browser->visit('/logout')
+ ->on(new Home())
+ ->submitLogon('john@kolab.org', 'simple123', true)
+ // Disabling wallets is required to access this feature
+ ->withConfig(['app.with_wallet' => false])
+ ->visit('/user/' . $julia->id)
+ ->on(new UserInfo())
+ ->assertSeeIn('#user_role', 'User')
+ ->click('#user_role')
+ ->with(new Dialog('#role-select'), static function (Browser $browser) {
+ $browser->assertSeeIn('@title', 'Select a role')
+ ->assertFocused('#user_role_select')
+ ->assertSelected('#user_role_select', 'user')
+ ->assertSelectHasOptions('#user_role_select', ['user', 'controller'])
+ ->assertVisible('span.form-text')
+ ->select('#user_role_select', 'controller')
+ ->assertSeeIn('@button-cancel', 'Cancel')
+ ->assertSeeIn('@button-action', 'Save')
+ ->click('@button-action');
+ })
+ ->waitUntilMissing('#role-select')
+ ->assertToast(Toast::TYPE_SUCCESS, 'Account controller role set successfully.')
+ ->assertSeeIn('#user_role', 'Controller');
+
+ $this->assertTrue($wallet->fresh()->isController($julia));
+
+ $browser->click('#user_role')
+ ->with(new Dialog('#role-select'), static function (Browser $browser) {
+ $browser->assertSelected('#user_role_select', 'controller')
+ ->select('#user_role_select', 'user')
+ ->click('@button-action');
+ })
+ ->waitUntilMissing('#role-select')
+ ->assertToast(Toast::TYPE_SUCCESS, 'Account controller role removed successfully.')
+ ->assertSeeIn('#user_role', 'User');
+
+ $this->assertFalse($wallet->fresh()->isController($julia));
+ });
+ }
+
/**
* Test non-default currency in the UI
*/
diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php
--- a/src/tests/Feature/Controller/UsersTest.php
+++ b/src/tests/Feature/Controller/UsersTest.php
@@ -291,6 +291,28 @@
$this->assertCount(1, $json['list']);
$this->assertSame($ned->email, $json['list'][0]['email']);
+ // Search by role:controller and role:user
+ $response = $this->actingAs($john)->get("/api/v4/users?search=role:controller");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertFalse($json['hasMore']);
+ $this->assertSame(1, $json['count']);
+ $this->assertCount(1, $json['list']);
+ $this->assertSame($ned->email, $json['list'][0]['email']);
+
+ $response = $this->actingAs($john)->get("/api/v4/users?search=role:user");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(3, $json['count']);
+ $this->assertCount(3, $json['list']);
+ $this->assertSame($jack->email, $json['list'][0]['email']);
+ $this->assertSame($joe->email, $json['list'][1]['email']);
+ $this->assertSame($john->email, $json['list'][2]['email']);
+
// TODO: Test paging
}
diff --git a/src/tests/Feature/Controller/WalletsTest.php b/src/tests/Feature/Controller/WalletsTest.php
--- a/src/tests/Feature/Controller/WalletsTest.php
+++ b/src/tests/Feature/Controller/WalletsTest.php
@@ -5,6 +5,7 @@
use App\Package;
use App\Payment;
use App\ReferralProgram;
+use App\Sku;
use App\Transaction;
use Carbon\Carbon;
use Tests\TestCase;
@@ -16,17 +17,119 @@
parent::setUp();
$this->deleteTestUser('wallets-controller@kolabnow.com');
+ $this->deleteTestUser('jane@kolabnow.com');
ReferralProgram::query()->delete();
}
protected function tearDown(): void
{
$this->deleteTestUser('wallets-controller@kolabnow.com');
+ $this->deleteTestUser('jane@kolabnow.com');
ReferralProgram::query()->delete();
parent::tearDown();
}
+ /**
+ * Test adding a wallet controller
+ */
+ public function testControllerAdd(): void
+ {
+ $user = $this->getTestUser('wallets-controller@kolabnow.com');
+ $jane = $this->getTestUser('jane@kolabnow.com');
+ $wallet = $user->wallets()->first();
+ $janes_wallet = $jane->wallets()->first();
+
+ // Unauth access not allowed
+ $response = $this->post("api/v4/wallets/{$wallet->id}/controllers/{$jane->id}");
+ $response->assertStatus(401);
+
+ // Unknown wallet or user
+ $response = $this->actingAs($user)->post("api/v4/wallets/{$wallet->id}/controllers/123");
+ $response->assertStatus(404);
+ $response = $this->actingAs($user)->post("api/v4/wallets/123/controllers/{$jane->id}");
+ $response->assertStatus(404);
+
+ // Other user's wallet
+ $response = $this->actingAs($user)->post("api/v4/wallets/{$janes_wallet->id}/controllers/{$jane->id}");
+ $response->assertStatus(403);
+
+ // Wallet owner can't make himself a controller
+ $response = $this->actingAs($user)->post("api/v4/wallets/{$wallet->id}/controllers/{$user->id}");
+ $response->assertStatus(403);
+
+ // Target user is not part of the same account
+ $response = $this->actingAs($user)->post("api/v4/wallets/{$wallet->id}/controllers/{$jane->id}");
+ $response->assertStatus(403);
+
+ // Valid user
+ $sku = Sku::withObjectTenantContext($user)->where(['title' => 'storage'])->first();
+ $jane->assignSku($sku, 1, $wallet);
+
+ $response = $this->actingAs($user)->post("api/v4/wallets/{$wallet->id}/controllers/{$jane->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+ $this->assertSame('Account controller role set successfully.', $json['message']);
+ $wallet->refresh();
+ $this->assertTrue($wallet->isController($jane));
+ $this->assertSame(1, $wallet->controllers()->count());
+
+ // Controller already assigned
+ $response = $this->actingAs($user)->post("api/v4/wallets/{$wallet->id}/controllers/{$jane->id}");
+ $response->assertStatus(200);
+
+ $wallet->refresh();
+ $this->assertTrue($wallet->isController($jane));
+ $this->assertSame(1, $wallet->controllers()->count());
+ }
+
+ /**
+ * Test deleting a wallet controller
+ */
+ public function testControllerDelete(): void
+ {
+ $user = $this->getTestUser('wallets-controller@kolabnow.com');
+ $jane = $this->getTestUser('jane@kolabnow.com');
+ $wallet = $user->wallets()->first();
+ $janes_wallet = $jane->wallets()->first();
+
+ // Unauth access not allowed
+ $response = $this->delete("api/v4/wallets/{$wallet->id}/controllers/{$jane->id}");
+ $response->assertStatus(401);
+
+ // Unknown wallet or user
+ $response = $this->actingAs($user)->delete("api/v4/wallets/{$wallet->id}/controllers/123");
+ $response->assertStatus(404);
+ $response = $this->actingAs($user)->delete("api/v4/wallets/123/controllers/{$jane->id}");
+ $response->assertStatus(404);
+
+ // Other user's wallet
+ $response = $this->actingAs($user)->delete("api/v4/wallets/{$janes_wallet->id}/controllers/{$jane->id}");
+ $response->assertStatus(403);
+
+ // Wallet owner can't remove himself
+ $response = $this->actingAs($user)->delete("api/v4/wallets/{$wallet->id}/controllers/{$user->id}");
+ $response->assertStatus(403);
+
+ // Target user is not the wallet controller
+ $response = $this->actingAs($user)->delete("api/v4/wallets/{$wallet->id}/controllers/{$jane->id}");
+ $response->assertStatus(404);
+
+ $wallet->addController($jane);
+
+ $response = $this->actingAs($user)->delete("api/v4/wallets/{$wallet->id}/controllers/{$jane->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+ $this->assertSame('Account controller role removed successfully.', $json['message']);
+ $this->assertFalse($wallet->fresh()->isController($jane));
+ }
+
/**
* Test fetching pdf receipt
*/

File Metadata

Mime Type
text/plain
Expires
Mon, Mar 30, 12:00 AM (4 d, 1 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18780045
Default Alt Text
D5664.1774828816.diff (19 KB)

Event Timeline