Fix various bad tenant isolation

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-10-30 15:39:38 +01:00
parent a42670d5bd
commit 29917578fc
30 changed files with 1259 additions and 1369 deletions

View File

@@ -53,9 +53,6 @@ const MainLayoutQuery = graphql`
fullName
email
}
invitations(first: 1, filter: {statuses: [PENDING]}) {
totalCount
}
}
organization: node(id: $organizationId) {
... on Organization {
@@ -258,49 +255,79 @@ interface OrganizationsResponse {
organizations: Organization[];
}
interface Invitation {
id: string;
email: string;
fullName: string;
role: string;
expiresAt: string;
acceptedAt?: string | null;
createdAt: string;
organization: {
id: string;
name: string;
};
}
interface InvitationsResponse {
invitations: Invitation[];
}
function OrganizationSelectorWrapper({ organizationId }: { organizationId: string }) {
const data = useLazyLoadQuery<MainLayoutQueryType>(MainLayoutQuery, { organizationId });
return <OrganizationSelector viewer={data.viewer} currentOrganization={data.organization} />;
return <OrganizationSelector currentOrganization={data.organization} />;
}
function OrganizationSelector({
viewer,
currentOrganization
}: {
viewer: MainLayoutQueryType["response"]["viewer"];
currentOrganization: MainLayoutQueryType["response"]["organization"];
}) {
const [organizations, setOrganizations] = useState<Organization[]>([]);
const [pendingInvitationsCount, setPendingInvitationsCount] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const { __ } = useTranslate();
const pendingInvitationsCount = viewer.invitations.totalCount;
useEffect(() => {
const fetchOrganizations = async () => {
const fetchData = async () => {
try {
setIsLoading(true);
const response = await fetch('/auth/organizations', {
credentials: 'include',
});
if (!response.ok) {
// Fetch organizations and invitations in parallel
const [orgsResponse, invitationsResponse] = await Promise.all([
fetch('/auth/organizations', { credentials: 'include' }),
fetch('/auth/invitations', { credentials: 'include' })
]);
if (!orgsResponse.ok) {
throw new Error('Failed to fetch organizations');
}
const data: OrganizationsResponse = await response.json();
setOrganizations(data.organizations);
if (!invitationsResponse.ok) {
throw new Error('Failed to fetch invitations');
}
const orgsData: OrganizationsResponse = await orgsResponse.json();
const invitationsData: InvitationsResponse = await invitationsResponse.json();
// Count pending invitations (those without acceptedAt)
const pendingCount = invitationsData.invitations.filter(
inv => !inv.acceptedAt
).length;
setOrganizations(orgsData.organizations);
setPendingInvitationsCount(pendingCount);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
console.error('Failed to fetch organizations:', err);
console.error('Failed to fetch data:', err);
} finally {
setIsLoading(false);
}
};
fetchOrganizations();
fetchData();
}, []);
if (error) {

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<26b3620e6aed7f97ffb1710be1eb267a>>
* @generated SignedSource<<b7983d3d3c089aa0efebaab639de76fb>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -20,9 +20,6 @@ export type MainLayoutQuery$data = {
};
readonly viewer: {
readonly id: string;
readonly invitations: {
readonly totalCount: number;
};
readonly user: {
readonly email: string;
readonly fullName: string;
@@ -63,54 +60,21 @@ v3 = {
"name": "email",
"storageKey": null
},
v4 = {
"alias": null,
"args": [
{
"kind": "Literal",
"name": "filter",
"value": {
"statuses": [
"PENDING"
]
}
},
{
"kind": "Literal",
"name": "first",
"value": 1
}
],
"concreteType": "InvitationConnection",
"kind": "LinkedField",
"name": "invitations",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "totalCount",
"storageKey": null
}
],
"storageKey": "invitations(filter:{\"statuses\":[\"PENDING\"]},first:1)"
},
v5 = [
v4 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v6 = {
v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
v7 = {
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
@@ -145,14 +109,13 @@ return {
(v3/*: any*/)
],
"storageKey": null
},
(v4/*: any*/)
}
],
"storageKey": null
},
{
"alias": "organization",
"args": (v5/*: any*/),
"args": (v4/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
@@ -162,8 +125,8 @@ return {
"kind": "InlineFragment",
"selections": [
(v1/*: any*/),
(v6/*: any*/),
(v7/*: any*/)
(v5/*: any*/),
(v6/*: any*/)
],
"type": "Organization",
"abstractKey": null
@@ -203,14 +166,13 @@ return {
(v1/*: any*/)
],
"storageKey": null
},
(v4/*: any*/)
}
],
"storageKey": null
},
{
"alias": "organization",
"args": (v5/*: any*/),
"args": (v4/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
@@ -227,8 +189,8 @@ return {
{
"kind": "InlineFragment",
"selections": [
(v6/*: any*/),
(v7/*: any*/)
(v5/*: any*/),
(v6/*: any*/)
],
"type": "Organization",
"abstractKey": null
@@ -239,16 +201,16 @@ return {
]
},
"params": {
"cacheID": "a8f9f58d27677c55b5a217617db83e27",
"cacheID": "ee5a60e709dee856df7d2fef13974c9f",
"id": null,
"metadata": {},
"name": "MainLayoutQuery",
"operationKind": "query",
"text": "query MainLayoutQuery(\n $organizationId: ID!\n) {\n viewer {\n id\n user {\n fullName\n email\n id\n }\n invitations(first: 1, filter: {statuses: [PENDING]}) {\n totalCount\n }\n }\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n logoUrl\n }\n id\n }\n}\n"
"text": "query MainLayoutQuery(\n $organizationId: ID!\n) {\n viewer {\n id\n user {\n fullName\n email\n id\n }\n }\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n logoUrl\n }\n id\n }\n}\n"
}
};
})();
(node as any).hash = "17986fcea321c4567d86584d1a9f89c1";
(node as any).hash = "9ea3e5a91a2d2be0993e7deebafa11b0";
export default node;