Add per-store managers for Woo Business accounts.
Deploy Ladill Woo Manager / deploy (push) Successful in 39s

Agencies can invite store-scoped managers via Identity, with access limited to one WooCommerce store while owners retain full multi-store control.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-08 07:51:08 +00:00
co-authored by Cursor
parent 1b0831b176
commit 09359ca86a
26 changed files with 921 additions and 28 deletions
@@ -0,0 +1,64 @@
<?php
namespace App\Services\Identity;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use RuntimeException;
class IdentityTeamClient
{
/**
* @param list<string> $apps
* @param array<string, mixed> $metadata
*
* @return array<string, mixed>
*/
public function inviteAppMember(
string $ownerPublicId,
string $email,
array $apps,
array $metadata = [],
string $role = 'member',
): array {
$response = $this->request('post', '/identity/team/invite', [
'owner' => $ownerPublicId,
'email' => strtolower(trim($email)),
'apps' => $apps,
'role' => $role,
'metadata' => $metadata,
]);
return (array) $response->json('data', []);
}
/** @return list<array<string, mixed>> */
public function teamAccess(string $userPublicId): array
{
$response = $this->request('get', '/identity/team/access', [
'user' => $userPublicId,
]);
return (array) $response->json('data', []);
}
private function request(string $method, string $path, array $query = []): Response
{
$url = rtrim((string) config('identity.api_url'), '/').$path;
$key = config('identity.api_key');
if ($url === '' || ! is_string($key) || $key === '') {
throw new RuntimeException('Identity API is not configured.');
}
$response = Http::withToken($key)
->timeout(10)
->{$method}($url, $query);
if (! $response->successful()) {
throw new RuntimeException($response->json('message') ?: 'Identity API request failed.');
}
return $response;
}
}
+5
View File
@@ -123,6 +123,11 @@ class SubscriptionService
return $this->productCount($user) < $this->maxProducts($user);
}
public function canManageStoreTeam(User $user): bool
{
return $this->isEnterprise($user);
}
public function canIngestProduct(User $user, WooStore $store, int $externalId): bool
{
if ($this->isPro($user)) {
@@ -0,0 +1,56 @@
<?php
namespace App\Services\Woo;
use App\Models\User;
use App\Models\WooStoreMember;
use App\Services\Identity\IdentityTeamClient;
class TeamMemberProvisioner
{
public function __construct(
protected IdentityTeamClient $identity,
) {}
public function sync(User $user): void
{
WooStoreMember::query()
->where('user_ref', strtolower((string) $user->email))
->update(['user_ref' => $user->public_id]);
try {
$access = $this->identity->teamAccess($user->public_id);
} catch (\Throwable) {
return;
}
foreach ($access as $grant) {
if (($grant['self'] ?? false) || ($grant['full_access'] ?? false)) {
continue;
}
$apps = (array) ($grant['apps'] ?? []);
if (! in_array('woo', $apps, true)) {
continue;
}
$meta = (array) data_get($grant, 'metadata.woo', []);
$storeId = (int) ($meta['store_id'] ?? 0);
if ($storeId <= 0) {
continue;
}
WooStoreMember::updateOrCreate(
[
'owner_ref' => (string) ($grant['account'] ?? $user->public_id),
'user_ref' => $user->public_id,
'woo_store_id' => $storeId,
],
[
'role' => (string) ($meta['role'] ?? WooStoreMember::ROLE_MANAGER),
],
);
}
}
}
@@ -0,0 +1,69 @@
<?php
namespace App\Services\Woo;
use App\Models\User;
use App\Models\WooStoreMember;
use Illuminate\Support\Collection;
class WooStoreMemberResolver
{
public function isAccountOwner(User $actor, string $ownerRef): bool
{
return $actor->public_id === $ownerRef;
}
public function memberFor(User $actor, string $ownerRef): ?WooStoreMember
{
if ($this->isAccountOwner($actor, $ownerRef)) {
return null;
}
return WooStoreMember::query()
->where('owner_ref', $ownerRef)
->where('user_ref', $actor->public_id)
->first();
}
/** @return list<int> */
public function accessibleStoreIds(User $actor, string $ownerRef): ?array
{
if ($this->isAccountOwner($actor, $ownerRef)) {
return null;
}
$ids = WooStoreMember::query()
->where('owner_ref', $ownerRef)
->where('user_ref', $actor->public_id)
->pluck('woo_store_id')
->map(fn ($id) => (int) $id)
->unique()
->values()
->all();
abort_if($ids === [], 403, 'You do not have access to this Woo Manager account.');
return $ids;
}
public function canAccessStore(User $actor, string $ownerRef, int $storeId): bool
{
$scope = $this->accessibleStoreIds($actor, $ownerRef);
return $scope === null || in_array($storeId, $scope, true);
}
public function canManageStoreTeam(?WooStoreMember $member, string $ownerRef, User $actor): bool
{
return $this->isAccountOwner($actor, $ownerRef);
}
/** @return Collection<int, WooStoreMember> */
public function membershipsFor(User $actor): Collection
{
return WooStoreMember::query()
->where('user_ref', $actor->public_id)
->with('store')
->get();
}
}