mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-16 20:22:15 +01:00
make OrganizationPolicy use “organizations:update” to remove jetstream inconsistencies
The frontend did not show organization settings for admin users because of the team ownership check
This commit is contained in:
@@ -6,6 +6,7 @@ namespace App\Policies;
|
|||||||
|
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Service\PermissionStore;
|
||||||
use Filament\Facades\Filament;
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ class OrganizationPolicy
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $user->ownsTeam($organization);
|
return app(PermissionStore::class)->userHas($organization, $user, 'organizations:update');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -369,6 +369,40 @@ test('test that format settings persist after page reload', async ({ page }) =>
|
|||||||
await expect(page.getByLabel('Date Format')).toContainText('DD/MM/YYYY');
|
await expect(page.getByLabel('Date Format')).toContainText('DD/MM/YYYY');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// Admin Permission Tests
|
||||||
|
// =============================================
|
||||||
|
|
||||||
|
test.describe('Admin Organization Settings Access', () => {
|
||||||
|
test('admin can see and edit organization settings', async ({ ctx, admin }) => {
|
||||||
|
await admin.page.goto(PLAYWRIGHT_BASE_URL + '/teams/' + ctx.orgId);
|
||||||
|
|
||||||
|
// Organization Name section is visible
|
||||||
|
await expect(
|
||||||
|
admin.page.getByRole('heading', { name: 'Organization Name', level: 3 })
|
||||||
|
).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
|
// Editable settings sections should be visible
|
||||||
|
await expect(
|
||||||
|
admin.page.getByRole('heading', { name: 'Billable Rate', level: 3 })
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
admin.page.getByRole('heading', { name: 'Format Settings', level: 3 })
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
admin.page.getByRole('heading', { name: 'Organization Settings', level: 3 })
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// Save buttons should be visible (admin can update)
|
||||||
|
await expect(admin.page.getByRole('button', { name: 'Save' }).first()).toBeVisible();
|
||||||
|
|
||||||
|
// Delete organization should NOT be visible (owner only)
|
||||||
|
await expect(
|
||||||
|
admin.page.getByRole('heading', { name: 'Delete Organization' })
|
||||||
|
).not.toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// =============================================
|
// =============================================
|
||||||
// Employee Permission Tests
|
// Employee Permission Tests
|
||||||
// =============================================
|
// =============================================
|
||||||
|
|||||||
@@ -68,6 +68,85 @@ export async function inviteAndAcceptMember(
|
|||||||
await secondUser.close();
|
await secondUser.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up an admin member in the owner's organization.
|
||||||
|
* Returns the admin's page, their member ID, and a cleanup function.
|
||||||
|
*/
|
||||||
|
export async function setupAdminUser(
|
||||||
|
ownerPage: Page,
|
||||||
|
ownerCtx: TestContext,
|
||||||
|
browser: Browser
|
||||||
|
): Promise<{
|
||||||
|
adminPage: Page;
|
||||||
|
adminMemberId: string;
|
||||||
|
closeAdmin: () => Promise<void>;
|
||||||
|
}> {
|
||||||
|
const memberId = Math.floor(Math.random() * 100000);
|
||||||
|
const memberEmail = `admin+${memberId}@admin-perms.test`;
|
||||||
|
const memberName = 'Admin ' + memberId;
|
||||||
|
|
||||||
|
const admin = await registerUser(browser, memberName, memberEmail);
|
||||||
|
|
||||||
|
await ownerPage.goto(PLAYWRIGHT_BASE_URL + '/members');
|
||||||
|
await ownerPage.getByRole('button', { name: 'Invite Member' }).click();
|
||||||
|
await expect(ownerPage.getByPlaceholder('Member Email')).toBeVisible();
|
||||||
|
await ownerPage.getByPlaceholder('Member Email').fill(memberEmail);
|
||||||
|
await ownerPage.getByRole('button', { name: 'Administrator' }).click();
|
||||||
|
await Promise.all([
|
||||||
|
ownerPage.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes('/invitations') &&
|
||||||
|
response.request().method() === 'POST' &&
|
||||||
|
response.status() === 204
|
||||||
|
),
|
||||||
|
ownerPage.getByRole('button', { name: 'Invite Member', exact: true }).click(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const acceptUrl = await getInvitationAcceptUrl(admin.page.request, memberEmail);
|
||||||
|
await admin.page.goto(acceptUrl);
|
||||||
|
await admin.page.waitForURL(/dashboard/);
|
||||||
|
|
||||||
|
await admin.page.goto(PLAYWRIGHT_BASE_URL + '/dashboard');
|
||||||
|
await expect(admin.page.getByTestId('dashboard_view')).toBeVisible({ timeout: 15000 });
|
||||||
|
|
||||||
|
const orgSwitcherText = await admin.page
|
||||||
|
.getByTestId('organization_switcher')
|
||||||
|
.first()
|
||||||
|
.textContent();
|
||||||
|
if (!orgSwitcherText?.includes("John's Organization")) {
|
||||||
|
const cookies = await admin.page.context().cookies();
|
||||||
|
const xsrfCookie = cookies.find((c) => c.name === 'XSRF-TOKEN');
|
||||||
|
const xsrfToken = xsrfCookie ? decodeURIComponent(xsrfCookie.value) : '';
|
||||||
|
|
||||||
|
await admin.page.request.put(`${PLAYWRIGHT_BASE_URL}/current-team`, {
|
||||||
|
headers: {
|
||||||
|
'X-XSRF-TOKEN': xsrfToken,
|
||||||
|
Accept: 'text/html',
|
||||||
|
},
|
||||||
|
data: { team_id: ownerCtx.orgId },
|
||||||
|
});
|
||||||
|
|
||||||
|
await admin.page.goto(PLAYWRIGHT_BASE_URL + '/dashboard');
|
||||||
|
await expect(admin.page.getByTestId('dashboard_view')).toBeVisible({ timeout: 15000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const membersResponse = await ownerCtx.request.get(
|
||||||
|
`${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ownerCtx.orgId}/members`
|
||||||
|
);
|
||||||
|
expect(membersResponse.status()).toBe(200);
|
||||||
|
const membersBody = await membersResponse.json();
|
||||||
|
const adminMember = membersBody.data.find(
|
||||||
|
(m: { role: string; name: string }) => m.role === 'admin' && m.name === memberName
|
||||||
|
);
|
||||||
|
expect(adminMember).toBeTruthy();
|
||||||
|
|
||||||
|
return {
|
||||||
|
adminPage: admin.page,
|
||||||
|
adminMemberId: adminMember.id,
|
||||||
|
closeAdmin: admin.close,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up an employee member in the owner's organization.
|
* Set up an employee member in the owner's organization.
|
||||||
* Returns the employee's page, their member ID, and a cleanup function.
|
* Returns the employee's page, their member ID, and a cleanup function.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { test as baseTest } from '@playwright/test';
|
|||||||
import type { Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
import { PLAYWRIGHT_BASE_URL, TEST_USER_PASSWORD } from './config';
|
import { PLAYWRIGHT_BASE_URL, TEST_USER_PASSWORD } from './config';
|
||||||
import { type TestContext, setupTestContext } from '../e2e/utils/api';
|
import { type TestContext, setupTestContext } from '../e2e/utils/api';
|
||||||
import { setupEmployeeUser } from '../e2e/utils/members';
|
import { setupAdminUser, setupEmployeeUser } from '../e2e/utils/members';
|
||||||
|
|
||||||
export * from '@playwright/test';
|
export * from '@playwright/test';
|
||||||
export type { TestContext };
|
export type { TestContext };
|
||||||
@@ -12,6 +12,11 @@ export interface EmployeeFixture {
|
|||||||
memberId: string;
|
memberId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminFixture {
|
||||||
|
page: Page;
|
||||||
|
memberId: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API-based authentication fixture - creates a new user via HTTP requests instead of UI interactions.
|
* API-based authentication fixture - creates a new user via HTTP requests instead of UI interactions.
|
||||||
* This is ~10-25x faster than UI-based authentication (~100-200ms vs ~3-5s).
|
* This is ~10-25x faster than UI-based authentication (~100-200ms vs ~3-5s).
|
||||||
@@ -19,7 +24,7 @@ export interface EmployeeFixture {
|
|||||||
* Uses page.context().request() to ensure cookies are shared between the API request and page.
|
* Uses page.context().request() to ensure cookies are shared between the API request and page.
|
||||||
*/
|
*/
|
||||||
export const test = baseTest.extend<
|
export const test = baseTest.extend<
|
||||||
{ ctx: TestContext; employee: EmployeeFixture },
|
{ ctx: TestContext; employee: EmployeeFixture; admin: AdminFixture },
|
||||||
{ workerStorageState: string }
|
{ workerStorageState: string }
|
||||||
>({
|
>({
|
||||||
page: async ({ page }, use) => {
|
page: async ({ page }, use) => {
|
||||||
@@ -100,4 +105,10 @@ export const test = baseTest.extend<
|
|||||||
await use({ page: employeePage, memberId: employeeMemberId });
|
await use({ page: employeePage, memberId: employeeMemberId });
|
||||||
await closeEmployee();
|
await closeEmployee();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
admin: async ({ page, ctx, browser }, use) => {
|
||||||
|
const { adminPage, adminMemberId, closeAdmin } = await setupAdminUser(page, ctx, browser);
|
||||||
|
await use({ page: adminPage, memberId: adminMemberId });
|
||||||
|
await closeAdmin();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user