Openstatus www.openstatus.dev

feat: remove user if owner (#515)

authored by

Maximilian Kaske and committed by
GitHub
a2bdd705 45ade72c

+53 -36
+12 -10
apps/web/src/components/data-table/user/columns.tsx
··· 44 44 return <span>{formatDate(row.getValue("createdAt"))}</span>; 45 45 }, 46 46 }, 47 - // { 48 - // id: "actions", 49 - // cell: ({ row }) => { 50 - // return ( 51 - // <div className="text-right"> 52 - // <DataTableRowActions row={row} /> 53 - // </div> 54 - // ); 55 - // }, 56 - // }, 47 + { 48 + id: "actions", 49 + cell: ({ row }) => { 50 + const role = row.original.role; 51 + if (role === "owner") return null; 52 + return ( 53 + <div className="text-right"> 54 + <DataTableRowActions row={row} /> 55 + </div> 56 + ); 57 + }, 58 + }, 57 59 ];
+9 -25
apps/web/src/components/data-table/user/data-table-row-actions.tsx
··· 1 - // TODO: once TeamM=ember is released, use that 2 - 3 1 "use client"; 4 2 5 3 import * as React from "react"; 6 - import Link from "next/link"; 7 4 import { useRouter } from "next/navigation"; 8 5 import type { Row } from "@tanstack/react-table"; 9 6 import { MoreHorizontal } from "lucide-react"; 10 7 11 - import { selectPageSchema } from "@openstatus/db/src/schema"; 8 + import { selectUserSchema } from "@openstatus/db/src/schema"; 12 9 import { 13 10 AlertDialog, 14 11 AlertDialogAction, ··· 37 34 export function DataTableRowActions<TData>({ 38 35 row, 39 36 }: DataTableRowActionsProps<TData>) { 40 - const page = selectPageSchema.parse(row.original); 37 + const user = selectUserSchema.parse(row.original); 41 38 const router = useRouter(); 42 39 const { toast } = useToastAction(); 43 40 const [alertOpen, setAlertOpen] = React.useState(false); ··· 46 43 async function onDelete() { 47 44 startTransition(async () => { 48 45 try { 49 - if (!page.id) return; 50 - await api.page.delete.mutate({ id: page.id }); 51 - toast("deleted"); 46 + if (!user.id) return; 47 + await api.workspace.removeWorkspaceUser.mutate({ id: user.id }); 48 + toast("removed"); 52 49 router.refresh(); 53 50 setAlertOpen(false); 54 51 } catch { ··· 70 67 </Button> 71 68 </DropdownMenuTrigger> 72 69 <DropdownMenuContent align="end"> 73 - <Link href={`./status-pages/edit?id=${page.id}`}> 74 - <DropdownMenuItem>Edit</DropdownMenuItem> 75 - </Link> 76 - <Link 77 - href={ 78 - process.env.NODE_ENV === "production" 79 - ? `https://${page.slug}.openstatus.dev` 80 - : `/status-page/${page.slug}` 81 - } 82 - target="_blank" 83 - > 84 - <DropdownMenuItem>Visit</DropdownMenuItem> 85 - </Link> 86 70 <AlertDialogTrigger asChild> 87 71 <DropdownMenuItem className="text-destructive focus:bg-destructive focus:text-background"> 88 - Delete 72 + Remove 89 73 </DropdownMenuItem> 90 74 </AlertDialogTrigger> 91 75 </DropdownMenuContent> ··· 94 78 <AlertDialogHeader> 95 79 <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> 96 80 <AlertDialogDescription> 97 - This action cannot be undone. This will permanently delete the 98 - monitor. 81 + This action cannot be undone. This will remove the user from your 82 + team. 99 83 </AlertDialogDescription> 100 84 </AlertDialogHeader> 101 85 <AlertDialogFooter> ··· 108 92 disabled={isPending} 109 93 className="bg-destructive text-destructive-foreground hover:bg-destructive/90" 110 94 > 111 - {!isPending ? "Delete" : <LoadingAnimation />} 95 + {!isPending ? "Remove" : <LoadingAnimation />} 112 96 </AlertDialogAction> 113 97 </AlertDialogFooter> 114 98 </AlertDialogContent>
+1
apps/web/src/hooks/use-toast-action.tsx
··· 21 21 }, 22 22 success: { title: "Success" }, 23 23 deleted: { title: "Deleted successfully" }, // TODO: we are not informing the user besides the visual changes when an entry has been deleted 24 + removed: { title: "Removed successfully" }, 24 25 saved: { title: "Saved successfully" }, 25 26 "test-error": { 26 27 title: "Connection Failed",
+31 -1
packages/api/src/router/workspace.ts
··· 1 1 import { generateSlug } from "random-word-slugs"; 2 2 import { z } from "zod"; 3 3 4 - import { eq } from "@openstatus/db"; 4 + import { and, eq } from "@openstatus/db"; 5 5 import { 6 6 selectWorkspaceSchema, 7 7 user, ··· 63 63 }); 64 64 return result; 65 65 }), 66 + 67 + removeWorkspaceUser: protectedProcedure 68 + .input(z.object({ id: z.number() })) 69 + .mutation(async (opts) => { 70 + const _userToWorkspace = 71 + await opts.ctx.db.query.usersToWorkspaces.findFirst({ 72 + where: and( 73 + eq(usersToWorkspaces.userId, opts.ctx.user.id), 74 + eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id), 75 + ), 76 + }); 77 + 78 + if (!_userToWorkspace) throw new Error("No user to workspace found"); 79 + 80 + if (!["owner"].includes(_userToWorkspace.role)) 81 + throw new Error("Not authorized to remove user from workspace"); 82 + 83 + if (opts.input.id === opts.ctx.user.id) 84 + throw new Error("Cannot remove yourself from workspace"); 85 + 86 + await opts.ctx.db 87 + .delete(usersToWorkspaces) 88 + .where( 89 + and( 90 + eq(usersToWorkspaces.userId, opts.input.id), 91 + eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id), 92 + ), 93 + ) 94 + .run(); 95 + }), 66 96 67 97 createWorkspace: protectedProcedure 68 98 .input(z.object({ name: z.string() }))