Skip to content

Commit

Permalink
chore: improve authorizations
Browse files Browse the repository at this point in the history
  • Loading branch information
williamluke4 committed Sep 2, 2024
1 parent f0984be commit 7b60a03
Show file tree
Hide file tree
Showing 22 changed files with 459 additions and 151 deletions.
71 changes: 71 additions & 0 deletions __tests__/utils/persmissions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, it, expect } from 'vitest';

// Import the functions and types
import { getPermissions, hasPermission } from '../../src/utils/permissions'; // Adjust the path accordingly

describe('Permission Tests', () => {
const superAdmin = { role: 'SUPER_ADMIN' } as const;
const admin = { role: 'ADMIN' } as const;
const staff = { role: 'STAFF' } as const;
const user = { role: 'USER' } as const;

it('should allow SUPER_ADMIN to perform all actions', () => {
const permissions = getPermissions(superAdmin, false);

for (const resource in permissions) {
for (const action in permissions[resource as keyof typeof permissions]) {
expect(permissions[resource as keyof typeof permissions][action as keyof typeof permissions[keyof typeof permissions]]).toBe(true);
}
}
});

it('should allow ADMIN to perform certain actions', () => {
const permissions = getPermissions(admin, false);

expect(permissions.Vouchers.UPDATE).toBe(true);
expect(permissions.Vouchers.DELETE).toBe(false); // Only OWNER or SUPER_ADMIN can delete
expect(permissions.Users.VIEW_PII).toBe(true);
expect(permissions.Gas.APPROVE).toBe(true);
});

it('should allow STAFF to approve gas', () => {
const permissions = getPermissions(staff, false);

expect(permissions.Gas.APPROVE).toBe(true);
expect(permissions.Vouchers.UPDATE).toBe(false);
});

it('should not allow USER to perform any restricted actions', () => {
const permissions = getPermissions(user, false);

for (const resource in permissions) {
for (const action in permissions[resource as keyof typeof permissions]) {
expect(permissions[resource as keyof typeof permissions][action as keyof typeof permissions[keyof typeof permissions]]).toBe(false);
}
}
});

it('should allow OWNER to delete if isOwner is true', () => {
const permissions = getPermissions(admin, true); // Here isOwner is true

expect(permissions.Vouchers.DELETE).toBe(true);
expect(permissions.Users.DELETE).toBe(true);
});

it('should not allow ADMIN to delete if isOwner is false', () => {
const permissions = getPermissions(admin, false);

expect(permissions.Vouchers.DELETE).toBe(false);
expect(permissions.Users.DELETE).toBe(false);
});

it('should return false if no user is provided', () => {
const result = hasPermission(null, false, 'Vouchers', 'DELETE');
expect(result).toBe(false);
});

it('should return false if action is not defined', () => {
const permissions = getPermissions(admin, false);
expect(permissions.Users['NON_EXISTENT_ACTION' as keyof typeof permissions.Users]).toBeUndefined();
});
});
9 changes: 5 additions & 4 deletions src/components/pools/forms/update-pool-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import { TextAreaField } from "~/components/forms/fields/textarea-field";
import { Loading } from "~/components/loading";
import { Button } from "~/components/ui/button";
import { Form } from "~/components/ui/form";
import { useAuth } from "~/hooks/useAuth";
import { Authorization } from "~/hooks/useAuth";
import { useIsOwner } from "~/hooks/useIsOwner";
import { api } from "~/utils/api";

const updatePoolSchema = z.object({
Expand Down Expand Up @@ -41,6 +42,7 @@ export function UpdatePoolForm({
poolTags: poolTags ?? [],
},
});
const isOwner = useIsOwner(address);
const utils = api.useUtils();
const router = useRouter();
const update = api.pool.update.useMutation({
Expand All @@ -59,7 +61,6 @@ export function UpdatePoolForm({
router.push("/pools").catch(console.error);
},
});
const auth = useAuth();
const { data: tags } = api.tags.list.useQuery();
const createTag = api.tags.create.useMutation();
const onSubmit = async (data: z.infer<typeof updatePoolSchema>) => {
Expand Down Expand Up @@ -115,14 +116,14 @@ export function UpdatePoolForm({
>
{update.isPending || remove.isPending ? <Loading /> : "Update"}
</Button>
{auth?.isAdmin && address && (
<Authorization resource={"Pools"} action="DELETE" isOwner={isOwner}>
<AreYouSureDialog
disabled={update.isPending || remove.isPending}
title="Are you sure?"
description="This will remove the Pool from the index"
onYes={() => remove.mutate(address)}
/>
)}
</Authorization>
</div>
</form>
</Form>
Expand Down
22 changes: 13 additions & 9 deletions src/components/products/product-form.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { FormProvider, useForm } from "react-hook-form";
import { Authorization } from "~/hooks/useAuth";
import { type Override } from "~/utils/type-helpers";
import AreYouSureDialog from "../dialogs/are-you-sure";
import { InputField } from "../forms/fields/input-field";
Expand All @@ -13,6 +14,7 @@ import {
} from "./schema";

interface ProductFormProps {
isOwner: boolean;
loading: boolean;
onCreate: (data: Omit<UpdateProductListingInput, "id">) => Promise<void>;
onUpdate: (data: UpdateProductListingInput) => Promise<void>;
Expand All @@ -21,6 +23,7 @@ interface ProductFormProps {
}

export const ProductForm = ({
isOwner,
onCreate,
onUpdate,
onDelete,
Expand All @@ -38,7 +41,6 @@ export const ProductForm = ({
const { handleSubmit } = form;

const onSubmit = async (data: UpdateProductListingInput) => {
console.log("first");
if (product?.id) {
await onUpdate({ ...data, id: product.id });
} else {
Expand Down Expand Up @@ -100,14 +102,16 @@ export const ProductForm = ({
>
{loading ? <Loading /> : product?.id ? "Update" : "Create"}
</Button>
{product && (
<AreYouSureDialog
disabled={loading}
title="Are you sure?"
description="This will remove the Pool from the index"
onYes={() => onDelete(product.id)}
/>
)}
<Authorization resource="Products" action="DELETE" isOwner={isOwner}>
{product && (
<AreYouSureDialog
disabled={loading}
title="Are you sure?"
description="This will remove the Pool from the index"
onYes={() => onDelete(product.id)}
/>
)}
</Authorization>
</div>
</form>
</FormProvider>
Expand Down
11 changes: 7 additions & 4 deletions src/components/products/product-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@ import {
type InsertProductListingInput,
type UpdateProductListingInput,
} from "./schema";
import { Authorization } from "~/hooks/useAuth";

export const ProductList = ({
voucher_id,
className,
canEdit,
isOwner,
}: {
voucher_id: number;
className?: string;
canEdit?: boolean;
isOwner: boolean;
}) => {
const [selectedProduct, setSelectedProduct] = useState<
RouterOutput["voucher"]["commodities"][0] | null
Expand All @@ -39,6 +40,7 @@ export const ProductList = ({
const deleteMutation = api.products.remove.useMutation();
const utils = api.useUtils();


const handleDelete = async (id: number) => {
try {
await deleteMutation.mutateAsync({ id });
Expand Down Expand Up @@ -77,7 +79,7 @@ export const ProductList = ({
<h2 className="text-primary-foreground bg-primary rounded-full p-1 px-6 text-base w-fit font-light text-center">
Products
</h2>
{Boolean(canEdit) && (
<Authorization resource="Products" action="UPDATE" isOwner={isOwner}>
<ResponsiveModal
button={
<Button variant="ghost" size="xs">
Expand All @@ -97,14 +99,15 @@ export const ProductList = ({
onCreate={handleCreate}
onUpdate={handleUpdate}
onDelete={handleDelete}
isOwner={isOwner}
loading={
insertMutation.isPending ||
updateMutation.isPending ||
deleteMutation.isPending
}
/>
</ResponsiveModal>
)}
</Authorization>
</div>

{products && products.length === 0 ? (
Expand Down
15 changes: 15 additions & 0 deletions src/components/users/forms/profile-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import { MapField } from "../../forms/fields/map-field";
import { Loading } from "../../loading";
import { Button } from "../../ui/button";
import { Form } from "../../ui/form";
import { Authorization } from "~/hooks/useAuth";
import { SelectField } from "~/components/forms/fields/select-field";
import { AccountRoleType } from "~/server/enums";

const VPA_PATTERN = /^[a-zA-Z0-9]+@[a-zA-Z]+$/;

Expand All @@ -28,6 +31,7 @@ export const UserProfileFormSchema = z.object({
given_names: z.string().trim().nullable(),
location_name: z.string().trim().max(64).nullable(),
default_voucher: z.string().nullable(),
account_role: z.nativeEnum(AccountRoleType).nullable(),
geo: z
.object({
x: z.number(),
Expand Down Expand Up @@ -138,6 +142,17 @@ export const ProfileForm = (props: ProfileFormProps) => {
label="Year of Birth"
disabled={props.viewOnly}
/>
<Authorization resource="Users" action={'UPDATE_ROLE'}>
<SelectField
form={form}
name="account_role"
label="Role"
items={Object.keys(AccountRoleType).map((value) => ({
value: value,
label: value,
}))}
/>
</Authorization>
</div>
<div>
<MapField
Expand Down
14 changes: 13 additions & 1 deletion src/components/users/forms/users-filter-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ import { Loading } from "~/components/loading";
import { Button } from "~/components/ui/button";
import { Form } from "~/components/ui/form";
import { cn } from "~/lib/utils";
import { GasGiftStatus, InterfaceType } from "~/server/enums";
import { AccountRoleType, GasGiftStatus, InterfaceType } from "~/server/enums";

// Schema for user profile form
export const UsersFilterSchema = z.object({
search: z.string().trim().nullish(),
interfaceType: z.array(z.nativeEnum(InterfaceType)).nullish(),
gasGiftStatus: z.array(z.nativeEnum(GasGiftStatus)).nullish(),
accountRole: z.array(z.nativeEnum(AccountRoleType)).nullish(),
limit: z.number().min(1).nullish(),
});

Expand All @@ -37,6 +38,7 @@ export const UserFilterForm = (props: UsersFilterFormProps) => {
search: "",
interfaceType: [],
gasGiftStatus: [],
accountRole: [],
},
});
const onValid = (data: UsersFilterFormData) => {
Expand Down Expand Up @@ -71,6 +73,16 @@ export const UserFilterForm = (props: UsersFilterFormProps) => {
}))}
className="flex-1 min-w-[200px]"
/>
<MultiSelectField
form={form}
name="accountRole"
label="Role"
items={Object.keys(AccountRoleType).map((value) => ({
value: value,
label: value,
}))}
className="flex-1 min-w-[200px]"
/>
<MultiSelectField
form={form}
name="gasGiftStatus"
Expand Down
44 changes: 25 additions & 19 deletions src/components/users/staff-gas-status.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Authorization } from "~/hooks/useAuth";
import { GasGiftStatus } from "~/server/enums";
import { api } from "~/utils/api";
import { Loading } from "../loading";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";

const StaffGasApproval = ({ address }: { address: `0x${string}` }) => {
const { data: status, isLoading } = api.gas.get.useQuery({
address,
Expand Down Expand Up @@ -35,25 +37,29 @@ const StaffGasApproval = ({ address }: { address: `0x${string}` }) => {
</Badge>
)}
</div>
<div className="flex space-x-2 h-7 align-middle items-center">
<Button
onClick={() => approve.mutate({ address })}
disabled={
isLoading || approve.isPending || status === GasGiftStatus.APPROVED
}
>
{approve.isPending ? <Loading /> : "Approve"}
</Button>
<Button
onClick={() => reject.mutate({ address })}
disabled={
isLoading || reject.isPending || status === GasGiftStatus.REJECTED
}
variant={"destructive"}
>
{reject.isPending ? <Loading /> : "Reject"}
</Button>
</div>
<Authorization resource={"Gas"} action="APPROVE">
<div className="flex space-x-2 h-7 align-middle items-center">
<Button
onClick={() => approve.mutate({ address })}
disabled={
isLoading ||
approve.isPending ||
status === GasGiftStatus.APPROVED
}
>
{approve.isPending ? <Loading /> : "Approve"}
</Button>
<Button
onClick={() => reject.mutate({ address })}
disabled={
isLoading || reject.isPending || status === GasGiftStatus.REJECTED
}
variant={"destructive"}
>
{reject.isPending ? <Loading /> : "Reject"}
</Button>
</div>
</Authorization>
</div>
);
};
Expand Down
4 changes: 4 additions & 0 deletions src/components/users/tables/staff-users-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ export function StaffUsersTable() {
info.getValue<string>()
),
},
{
header: "Role",
accessorKey: "account_role",
},
{
header: "Given Names",
accessorKey: "given_names",
Expand Down
5 changes: 3 additions & 2 deletions src/components/voucher/forms/update-voucher-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { useIsOwner } from "~/hooks/useIsOwner";
import { type RouterOutput } from "~/server/api/root";
import { type UpdateVoucherInput } from "~/server/api/routers/voucher";
import { api } from "~/utils/api";
import { hasPermission } from "~/utils/permissions";

// Form validation schema
const formSchema = z.object({
Expand Down Expand Up @@ -50,8 +51,8 @@ const UpdateVoucherForm = ({ onSuccess, voucher }: UpdateFormProps) => {
const isPending = update.isPending || remove.isPending;

const isOwner = useIsOwner(voucher?.voucher_address as string);
const canUpdate = isOwner || auth?.isStaff;
const canDelete = isOwner || auth?.isAdmin;
const canUpdate = hasPermission(auth?.user, isOwner, "Vouchers", "UPDATE");
const canDelete = hasPermission(auth?.user, isOwner, "Vouchers", "DELETE");

const form = useForm<Omit<UpdateVoucherInput, "voucherAddress">>({
resolver: zodResolver(formSchema),
Expand Down
2 changes: 1 addition & 1 deletion src/contracts/helpers.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { publicClient } from "~/lib/web3";
import { abi } from "./erc20-demurrage-token/contract";

export const isOwner = async (
export const getIsOwner = async (
address: `0x${string}`,
voucherAddress: `0x${string}`
) => {
Expand Down
Loading

0 comments on commit 7b60a03

Please sign in to comment.