unsharing logic

This commit is contained in:
Ishaan Dey 2024-05-01 02:22:02 -04:00
parent 5ba1c03030
commit e23b38875e
4 changed files with 118 additions and 55 deletions

View File

@ -5,7 +5,7 @@ import { ZodError, z } from "zod";
import { user, sandbox, usersToSandboxes } from "./schema"; import { user, sandbox, usersToSandboxes } from "./schema";
import * as schema from "./schema"; import * as schema from "./schema";
import { eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
export interface Env { export interface Env {
DB: D1Database; DB: D1Database;
@ -91,33 +91,48 @@ export default {
console.log(method); console.log(method);
return methodNotAllowed; return methodNotAllowed;
} }
} else if (path === "/api/sandbox/share" && method === "POST") { } else if (path === "/api/sandbox/share") {
const shareSchema = z.object({ if (method === "POST") {
sandboxId: z.string(), const shareSchema = z.object({
email: z.string(), sandboxId: z.string(),
}); email: z.string(),
});
const body = await request.json(); const body = await request.json();
const { sandboxId, email } = shareSchema.parse(body); const { sandboxId, email } = shareSchema.parse(body);
const user = await db.query.user.findFirst({ const user = await db.query.user.findFirst({
where: (user, { eq }) => eq(user.email, email), where: (user, { eq }) => eq(user.email, email),
with: { with: {
usersToSandboxes: true, usersToSandboxes: true,
}, },
}); });
if (!user) { if (!user) {
return new Response("No user associated with email.", { status: 400 }); return new Response("No user associated with email.", { status: 400 });
} }
if (user.usersToSandboxes.find((uts) => uts.sandboxId === sandboxId)) { if (user.usersToSandboxes.find((uts) => uts.sandboxId === sandboxId)) {
return new Response("User already has access.", { status: 400 }); return new Response("User already has access.", { status: 400 });
} }
await db.insert(usersToSandboxes).values({ userId: user.id, sandboxId }).get(); await db.insert(usersToSandboxes).values({ userId: user.id, sandboxId }).get();
return success; return success;
} else if (method === "DELETE") {
const deleteShareSchema = z.object({
sandboxId: z.string(),
userId: z.string(),
});
const body = await request.json();
const { sandboxId, userId } = deleteShareSchema.parse(body);
console.log("DELETE", sandboxId, userId);
await db.delete(usersToSandboxes).where(and(eq(usersToSandboxes.userId, userId), eq(usersToSandboxes.sandboxId, sandboxId)));
return success;
} else return methodNotAllowed;
} else if (path === "/api/user") { } else if (path === "/api/user") {
if (method === "GET") { if (method === "GET") {
const params = url.searchParams; const params = url.searchParams;

View File

@ -3,10 +3,8 @@
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { z } from "zod" import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
@ -15,27 +13,19 @@ import { useForm } from "react-hook-form"
import { import {
Form, Form,
FormControl, FormControl,
FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form" } from "@/components/ui/form"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Loader2, UserPlus, X } from "lucide-react" import { Loader2, UserPlus, X } from "lucide-react"
import { useEffect, useState, useTransition } from "react" import { useState } from "react"
import { Sandbox, User } from "@/lib/types" import { Sandbox } from "@/lib/types"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import Avatar from "@/components/ui/avatar"
import { shareSandbox } from "@/lib/actions" import { shareSandbox } from "@/lib/actions"
import { toast } from "sonner" import { toast } from "sonner"
import SharedUser from "./sharedUser"
import { DialogDescription } from "@radix-ui/react-dialog"
const formSchema = z.object({ const formSchema = z.object({
email: z.string().email(), email: z.string().email(),
@ -79,9 +69,15 @@ export default function ShareSandboxModal({
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="p-0"> <DialogContent className="p-0">
<div className="p-6 pb-3 space-y-6"> <div className={`p-6 ${shared.length > 0 ? "pb-3" : null} space-y-6`}>
<DialogHeader> <DialogHeader>
<DialogTitle>Share Sandbox</DialogTitle> <DialogTitle>Share Sandbox</DialogTitle>
{data.visibility === "private" ? (
<DialogDescription className="text-sm text-muted-foreground">
This sandbox is private. Making it public will allow shared
users to view and collaborate.
</DialogDescription>
) : null}
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex"> <form onSubmit={form.handleSubmit(onSubmit)} className="flex">
@ -115,25 +111,21 @@ export default function ShareSandboxModal({
</form> </form>
</Form> </Form>
</div> </div>
<div className="w-full h-[1px] bg-border" /> {shared.length > 0 ? (
<div className="p-6 pt-3"> <>
<DialogHeader className="mb-6"> <div className="w-full h-[1px] mb- bg-border" />
<DialogTitle>Manage Access</DialogTitle> <div className="p-6 pt-3">
</DialogHeader> <DialogHeader className="mb-6">
<div className="space-y-2"> <DialogTitle>Manage Access</DialogTitle>
{shared.map((user) => ( </DialogHeader>
<div key={user.id} className="flex items-center justify-between"> <div className="space-y-2">
<div className="flex items-center"> {shared.map((user) => (
<Avatar name={user.name} className="mr-2" /> <SharedUser key={user.id} user={user} sandboxId={data.id} />
{user.name} ))}
</div>
<Button variant="ghost" size="smIcon">
<X className="w-4 h-4" />
</Button>
</div> </div>
))} </div>
</div> </>
</div> ) : null}
</DialogContent> </DialogContent>
</Dialog> </Dialog>
) )

View File

@ -0,0 +1,44 @@
"use client"
import Avatar from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { unshareSandbox } from "@/lib/actions"
import { Loader2, X } from "lucide-react"
import { useState } from "react"
export default function SharedUser({
user,
sandboxId,
}: {
user: { id: string; name: string }
sandboxId: string
}) {
const [loading, setLoading] = useState(false)
async function handleUnshare(id: string) {
setLoading(true)
await unshareSandbox(sandboxId, user.id)
}
return (
<div className="flex items-center justify-between">
<div className="flex items-center">
<Avatar name={user.name} className="mr-2" />
{user.name}
</div>
<Button
disabled={loading}
onClick={() => handleUnshare(user.id)}
variant="ghost"
size="smIcon"
>
{loading ? (
<Loader2 className="animate-spin w-4 h-4" />
) : (
<X className="w-4 h-4" />
)}
</Button>
</div>
)
}

View File

@ -60,3 +60,15 @@ export async function shareSandbox(sandboxId: string, email: string) {
revalidatePath(`/code/${sandboxId}`) revalidatePath(`/code/${sandboxId}`)
return { success: true, message: "Shared successfully." } return { success: true, message: "Shared successfully." }
} }
export async function unshareSandbox(sandboxId: string, userId: string) {
const res = await fetch("http://localhost:8787/api/sandbox/share", {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ sandboxId, userId }),
})
revalidatePath(`/code/${sandboxId}`)
}