From 9cd1f5f655ea793bd1395c1805380da3204229d3 Mon Sep 17 00:00:00 2001 From: Mikkel RINGAUD Date: Mon, 14 Aug 2023 02:58:29 +0200 Subject: [PATCH] feat: upload files in shared workspaces and create new workspaces in those --- src/routes/api/workspace/create.ts | 21 ++++++++++---------- src/routes/api/workspace/index.ts | 13 +++---------- src/supabase/server.ts | 20 ------------------- src/supabase/server/index.ts | 14 +++++++++++++ src/supabase/server/utils.ts | 20 +++++++++++++++++++ supabase/functions/_shared/supabase.ts | 20 +++++++------------ supabase/functions/import_map.json | 5 +++++ supabase/functions/upload-file/index.ts | 26 +++++++++++++++++++------ 8 files changed, 80 insertions(+), 59 deletions(-) delete mode 100644 src/supabase/server.ts create mode 100644 src/supabase/server/index.ts create mode 100644 src/supabase/server/utils.ts create mode 100644 supabase/functions/import_map.json diff --git a/src/routes/api/workspace/create.ts b/src/routes/api/workspace/create.ts index ba910dd..417ceeb 100644 --- a/src/routes/api/workspace/create.ts +++ b/src/routes/api/workspace/create.ts @@ -1,6 +1,6 @@ import { type APIEvent, json } from "solid-start"; -import { supabase, getUserProfile } from "@/supabase/server"; +import { supabase, getUserProfile, getPermissionForWorkspace } from "@/supabase/server"; import type { WorkspaceMeta } from "@/types/api"; /** @@ -34,27 +34,28 @@ export const POST = async ({ request }: APIEvent) => { .select() .eq("id", parent_workspace_id) .limit(1) - .single(); + .single(); - const isCreatorOfWorkspace = workspace_data.creator === user_profile.user_id; - - // Check if we're the owner of the file. - if (!isCreatorOfWorkspace) return json({ + if (!getPermissionForWorkspace(workspace_data, user_profile)) return json({ success: false, message: "You're not allowed to access this file." }, { status: 403 }); + const isCreatorOfCurrentWorkspace = workspace_data?.creator === user_profile.user_id; + const { data: created_workspace } = await supabase .from("workspaces") .insert([{ name: workspace_name, parent_workspace_id, - creator: user_profile.user_id - }]) - .select(); + creator: user_profile.user_id, + shared_with: !isCreatorOfCurrentWorkspace ? [workspace_data?.creator] : undefined + } as WorkspaceMeta]) + .select() + .single(); return json({ success: true, - data: created_workspace![0] as WorkspaceMeta + data: created_workspace }); }; diff --git a/src/routes/api/workspace/index.ts b/src/routes/api/workspace/index.ts index 8be4321..4e0f558 100644 --- a/src/routes/api/workspace/index.ts +++ b/src/routes/api/workspace/index.ts @@ -1,6 +1,7 @@ import { type APIEvent, json } from "solid-start"; import { supabase, getUserProfile } from "@/supabase/server"; import type { UploadedFile, UserProfile, WorkspaceMeta, WorkspaceContent } from "@/types/api"; +import { getPermissionForWorkspace } from "@/supabase/server/utils"; export const GET = async ({ request }: APIEvent): Promise => { const api_token = request.headers.get("authorization"); @@ -20,16 +21,8 @@ export const GET = async ({ request }: APIEvent): Promise => { .eq("id", workspace_id) .limit(1) .single(); - - const getPermissionForWorkspace = (data: WorkspaceMeta | undefined) => { - if (!data || !user_profile) return false; - const isCreator = data.creator === user_profile.user_id; - const isSharedWith = data.shared_with.includes(user_profile.user_id); - if (!isCreator && !isSharedWith) return false; - return true; - }; - if (!getPermissionForWorkspace(workspace_data!)) return json({ + if (!getPermissionForWorkspace(workspace_data, user_profile)) return json({ success: false, message: "Not allowed to get that workspace." }, { status: 403 }); @@ -69,7 +62,7 @@ export const GET = async ({ request }: APIEvent): Promise => { .single(); // If it's not the root folder, then show a back workspace. - if (parent_workspace_data && getPermissionForWorkspace(parent_workspace_data)) { + if (parent_workspace_data && getPermissionForWorkspace(parent_workspace_data, user_profile)) { content.push({ type: "workspace", data: { diff --git a/src/supabase/server.ts b/src/supabase/server.ts deleted file mode 100644 index 4eafdad..0000000 --- a/src/supabase/server.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { type UserProfile } from "@/types/api"; -import { createClient } from "@supabase/supabase-js"; - -export const supabase = createClient( - import.meta.env.VITE_SUPABASE_PROJECT_URL as string, - process.env.SUPABASE_SERVICE_ROLE_KEY as string, - { - auth: { persistSession: false }, - } -); - -export const getUserProfile = async (token: string) => { - const { data: user_profile } = await supabase.from("profiles") - .select() - .eq("api_token", token) - .limit(1) - .single(); - - return user_profile as UserProfile; -}; diff --git a/src/supabase/server/index.ts b/src/supabase/server/index.ts new file mode 100644 index 0000000..2221fec --- /dev/null +++ b/src/supabase/server/index.ts @@ -0,0 +1,14 @@ +import { createClient } from "@supabase/supabase-js"; + +export const supabase = createClient( + import.meta.env.VITE_SUPABASE_PROJECT_URL as string, + process.env.SUPABASE_SERVICE_ROLE_KEY as string, + { + auth: { persistSession: false }, + } +); + +import { getUserProfile as _getUserProfile } from "./utils"; +export const getUserProfile = (token: string) => _getUserProfile(supabase, token); + +export { getPermissionForWorkspace } from "./utils"; diff --git a/src/supabase/server/utils.ts b/src/supabase/server/utils.ts new file mode 100644 index 0000000..cb34e95 --- /dev/null +++ b/src/supabase/server/utils.ts @@ -0,0 +1,20 @@ +import type { WorkspaceMeta, UserProfile } from "@/types/api"; +import type { SupabaseClient } from "@supabase/supabase-js"; + +export const getUserProfile = async (supabase: SupabaseClient, token: string) => { + const { data: user_profile } = await supabase.from("profiles") + .select() + .eq("api_token", token) + .limit(1) + .single(); + + return user_profile as UserProfile; +}; + +export const getPermissionForWorkspace = (workspace_data: WorkspaceMeta | undefined | null, user_profile: UserProfile | undefined) => { + if (!workspace_data || !user_profile) return false; + const isCreator = workspace_data.creator === user_profile.user_id; + const isSharedWith = workspace_data.shared_with.includes(user_profile.user_id); + if (!isCreator && !isSharedWith) return false; + return true; +}; diff --git a/supabase/functions/_shared/supabase.ts b/supabase/functions/_shared/supabase.ts index 9d774a6..6f47405 100644 --- a/supabase/functions/_shared/supabase.ts +++ b/supabase/functions/_shared/supabase.ts @@ -1,20 +1,14 @@ -import { type UserProfile } from "../../../src/types/api.ts"; -import { createClient } from "https://esm.sh/@supabase/supabase-js@2.32.0"; +import { createClient } from "@supabase/supabase-js"; export const supabase = createClient( - Deno.env.get('SUPABASE_URL') as string, - Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') as string, + Deno.env.get("SUPABASE_URL") as string, + Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") as string, { - auth: { persistSession: false }, + auth: { persistSession: false } } ); -export const getUserProfile = async (token: string) => { - const { data: user_profile } = await supabase.from("profiles") - .select() - .eq("api_token", token) - .limit(1) - .single(); +import { getUserProfile as _getUserProfile } from "../../../src/supabase/server/utils.ts"; +export const getUserProfile = (token: string) => _getUserProfile(supabase, token); - return user_profile as UserProfile; -}; +export { getPermissionForWorkspace } from "../../../src/supabase/server/utils.ts"; \ No newline at end of file diff --git a/supabase/functions/import_map.json b/supabase/functions/import_map.json new file mode 100644 index 0000000..bed1842 --- /dev/null +++ b/supabase/functions/import_map.json @@ -0,0 +1,5 @@ +{ + "imports": { + "@supabase/supabase-js": "https://esm.sh/@supabase/supabase-js@2.32.0" + } +} \ No newline at end of file diff --git a/supabase/functions/upload-file/index.ts b/supabase/functions/upload-file/index.ts index b5998d0..d41928c 100644 --- a/supabase/functions/upload-file/index.ts +++ b/supabase/functions/upload-file/index.ts @@ -1,7 +1,7 @@ import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; -import type { UploadedFile, UserProfile } from "../../../src/types/api.ts"; -import { supabase, getUserProfile } from "../_shared/supabase.ts"; +import type { UploadedFile, UserProfile, WorkspaceMeta } from "../../../src/types/api.ts"; +import { supabase, getUserProfile, getPermissionForWorkspace } from "../_shared/supabase.ts"; const corsHeaders = { "Access-Control-Allow-Origin": "*", @@ -17,6 +17,7 @@ const json = (data: T, options?: { status: number }) => new Response( /** * PUT / - Body should be FormData. * + * `api_token?: string` to authenticate the user. * `files: File[]` contains all the files to upload. * `workspace_id: string` is the workspace where we should upload the files. * `private?: "0" | "1"` is the accessibility of the file, where `0` means `public` and `1` means private. @@ -30,8 +31,21 @@ serve(async (req: Request) => { let user_profile: UserProfile | undefined; if (formDataApiToken) (user_profile = await getUserProfile(formDataApiToken)); - const workspaceId = (formData.get("workspace_id") as string | undefined) ?? user_profile?.root_workspace_id ?? undefined; - + const workspace_id = (formData.get("workspace_id") as string | undefined) ?? user_profile?.root_workspace_id ?? undefined; + if (workspace_id) { + const { data: workspace_data } = await supabase + .from("workspaces") + .select() + .eq("id", workspace_id) + .limit(1) + .single(); + + if (!getPermissionForWorkspace(workspace_data, user_profile)) return json({ + success: false, + message: "Not allowed to upload in this directory." + }, { status: 403 }); + } + const isPrivate = parseInt((formData.get("private") as string | null) ?? "1"); const newUploadsInDatabase: UploadedFile[] = []; @@ -45,7 +59,7 @@ serve(async (req: Request) => { // Always public for anonymous uploads. private: user_profile ? Boolean(isPrivate) : false, shared_with: [], - workspace_id: user_profile ? workspaceId : null, + workspace_id: user_profile ? workspace_id : null, name: file.name }) .select(); @@ -70,5 +84,5 @@ serve(async (req: Request) => { return json({ success: false, message: "Unknown method." - }); + }, { status: 404 }); });