fix(organization): custom permissions access control type inference breaking on the client

This commit is contained in:
Bereket Engida
2025-03-05 13:30:32 +03:00
parent 88babb275c
commit c051c7589a

View File

@@ -13,14 +13,7 @@ import type { BetterAuthClientPlugin } from "../../client/types";
import type { organization } from "./organization";
import { useAuthQuery } from "../../client";
import { BetterAuthError } from "../../error";
import {
defaultStatements,
defaultAc,
defaultRoles,
adminAc,
memberAc,
ownerAc,
} from "./access";
import { defaultStatements, adminAc, memberAc, ownerAc } from "./access";
interface OrganizationClientOptions {
ac?: AccessControl;
@@ -32,171 +25,165 @@ interface OrganizationClientOptions {
};
}
export const organizationClient = Object.assign(
<O extends OrganizationClientOptions>(options?: O) => {
const $listOrg = atom<boolean>(false);
const $activeOrgSignal = atom<boolean>(false);
const $activeMemberSignal = atom<boolean>(false);
export const organizationClient = <O extends OrganizationClientOptions>(
options?: O,
) => {
const $listOrg = atom<boolean>(false);
const $activeOrgSignal = atom<boolean>(false);
const $activeMemberSignal = atom<boolean>(false);
type DefaultStatements = typeof defaultStatements;
type Statements = O["ac"] extends AccessControl<infer S>
type DefaultStatements = typeof defaultStatements;
type Statements = O["ac"] extends AccessControl<infer S>
? S extends Record<string, Array<any>>
? S & DefaultStatements
: DefaultStatements;
const roles = {
admin: adminAc,
member: memberAc,
owner: ownerAc,
...options?.roles,
};
: DefaultStatements
: DefaultStatements;
const roles = {
admin: adminAc,
member: memberAc,
owner: ownerAc,
...options?.roles,
};
type OrganizationReturn = O["teams"] extends { enabled: true }
? {
members: InferMember<O>[];
invitations: InferInvitation<O>[];
teams: Team[];
} & Organization
: {
members: InferMember<O>[];
invitations: InferInvitation<O>[];
} & Organization;
return {
id: "organization",
$InferServerPlugin: {} as ReturnType<
typeof organization<{
ac: O["ac"] extends AccessControl
? O["ac"]
: AccessControl<DefaultStatements>;
roles: O["roles"] extends Record<string, Role>
? O["roles"]
: {
admin: Role;
member: Role;
owner: Role;
};
teams: {
enabled: O["teams"] extends { enabled: true } ? true : false;
};
}>
>,
getActions: ($fetch) => ({
$Infer: {
ActiveOrganization: {} as OrganizationReturn,
Organization: {} as Organization,
Invitation: {} as InferInvitation<O>,
Member: {} as InferInvitation<O>,
Team: {} as Team,
},
organization: {
checkRolePermission: <
R extends O extends { roles: any }
? keyof O["roles"]
: "admin" | "member" | "owner",
>(data: {
role: R;
permission: {
//@ts-expect-error fix this later
[key in keyof Statements]?: Statements[key][number][];
type OrganizationReturn = O["teams"] extends { enabled: true }
? {
members: InferMember<O>[];
invitations: InferInvitation<O>[];
teams: Team[];
} & Organization
: {
members: InferMember<O>[];
invitations: InferInvitation<O>[];
} & Organization;
return {
id: "organization",
$InferServerPlugin: {} as ReturnType<
typeof organization<{
ac: O["ac"] extends AccessControl
? O["ac"]
: AccessControl<DefaultStatements>;
roles: O["roles"] extends Record<string, Role>
? O["roles"]
: {
admin: Role;
member: Role;
owner: Role;
};
}) => {
if (Object.keys(data.permission).length > 1) {
throw new BetterAuthError(
"you can only check one resource permission at a time.",
);
}
const role = roles[data.role as unknown as "admin"];
if (!role) {
return false;
}
const isAuthorized = role?.authorize(data.permission as any);
return isAuthorized.success;
},
},
}),
getAtoms: ($fetch) => {
const listOrganizations = useAuthQuery<Organization[]>(
$listOrg,
"/organization/list",
$fetch,
{
method: "GET",
},
);
const activeOrganization = useAuthQuery<
Prettify<
Organization & {
members: (Member & {
user: {
id: string;
name: string;
email: string;
image: string | undefined;
};
})[];
invitations: Invitation[];
}
>
>(
[$activeOrgSignal],
"/organization/get-full-organization",
$fetch,
() => ({
method: "GET",
}),
);
const activeMember = useAuthQuery<Member>(
[$activeMemberSignal],
"/organization/get-active-member",
$fetch,
{
method: "GET",
},
);
return {
$listOrg,
$activeOrgSignal,
$activeMemberSignal,
activeOrganization,
listOrganizations,
activeMember,
teams: {
enabled: O["teams"] extends { enabled: true } ? true : false;
};
}>
>,
getActions: ($fetch) => ({
$Infer: {
ActiveOrganization: {} as OrganizationReturn,
Organization: {} as Organization,
Invitation: {} as InferInvitation<O>,
Member: {} as InferInvitation<O>,
Team: {} as Team,
},
pathMethods: {
"/organization/get-full-organization": "GET",
},
atomListeners: [
{
matcher(path) {
return (
path === "/organization/create" ||
path === "/organization/delete" ||
path === "/organization/update"
organization: {
checkRolePermission: <
R extends O extends { roles: any }
? keyof O["roles"]
: "admin" | "member" | "owner",
>(data: {
role: R;
permission: {
//@ts-expect-error fix this later
[key in keyof Statements]?: Statements[key][number][];
};
}) => {
if (Object.keys(data.permission).length > 1) {
throw new BetterAuthError(
"you can only check one resource permission at a time.",
);
},
signal: "$listOrg",
}
const role = roles[data.role as unknown as "admin"];
if (!role) {
return false;
}
const isAuthorized = role?.authorize(data.permission as any);
return isAuthorized.success;
},
},
}),
getAtoms: ($fetch) => {
const listOrganizations = useAuthQuery<Organization[]>(
$listOrg,
"/organization/list",
$fetch,
{
matcher(path) {
return path.startsWith("/organization");
},
signal: "$activeOrgSignal",
method: "GET",
},
);
const activeOrganization = useAuthQuery<
Prettify<
Organization & {
members: (Member & {
user: {
id: string;
name: string;
email: string;
image: string | undefined;
};
})[];
invitations: Invitation[];
}
>
>(
[$activeOrgSignal],
"/organization/get-full-organization",
$fetch,
() => ({
method: "GET",
}),
);
const activeMember = useAuthQuery<Member>(
[$activeMemberSignal],
"/organization/get-active-member",
$fetch,
{
matcher(path) {
return path.includes("/organization/update-member-role");
},
signal: "$activeMemberSignal",
method: "GET",
},
],
} satisfies BetterAuthClientPlugin;
},
{
defaultStatements,
defaultRoles,
defaultAc,
ownerAc,
adminAc,
memberAc,
},
);
);
return {
$listOrg,
$activeOrgSignal,
$activeMemberSignal,
activeOrganization,
listOrganizations,
activeMember,
};
},
pathMethods: {
"/organization/get-full-organization": "GET",
},
atomListeners: [
{
matcher(path) {
return (
path === "/organization/create" ||
path === "/organization/delete" ||
path === "/organization/update"
);
},
signal: "$listOrg",
},
{
matcher(path) {
return path.startsWith("/organization");
},
signal: "$activeOrgSignal",
},
{
matcher(path) {
return path.includes("/organization/update-member-role");
},
signal: "$activeMemberSignal",
},
],
} satisfies BetterAuthClientPlugin;
};