Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117409185
D5664.1774828816.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
19 KB
Referenced Files
None
Subscribers
None
D5664.1774828816.diff
View Options
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'"> ({{ user_id }})</span>
+ <span v-if="$route.name === 'settings'" id="userid"> ({{ 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
Details
Attached
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)
Attached To
Mode
D5664: Controller role management
Attached
Detach File
Event Timeline