Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/database/supabase/functions/create-group/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"imports": {
"@supabase/functions-js/edge-runtime": "jsr:@supabase/functions-js/edge-runtime.d.ts",
"@supabase/supabase-js": "jsr:@supabase/supabase-js@2",
"@supabase/functions-js": "jsr:@supabase/functions-js@2",
"@repo/database/lib/client": "../../../src/lib/client.ts"
}
}
123 changes: 123 additions & 0 deletions packages/database/supabase/functions/create-group/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Follow this setup guide to integrate the Deno language server with your editor:
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.

import "@supabase/functions-js/edge-runtime";
import { createClient, type UserResponse } from "@supabase/supabase-js";
import type { DGSupabaseClient } from "@repo/database/lib/client";

// The following lines are duplicated from apps/website/app/utils/llm/cors.ts
const allowedOrigins = ["https://roamresearch.com", "http://localhost:3000"];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for the Obsidian Publish project, correct? Should that be reflected here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, @trangdoan982 I know I was asking you about whether the Obsidian origin you had was Mac-only?


const isVercelPreviewUrl = (origin: string): boolean =>
/^https:\/\/.*-discourse-graph-[a-z0-9]+\.vercel\.app$/.test(origin);

const isAllowedOrigin = (origin: string): boolean =>
allowedOrigins.some((allowed) => origin.startsWith(allowed)) ||
isVercelPreviewUrl(origin);

// @ts-ignore Deno is not visible to the IDE
Deno.serve(async (req) => {
const origin = req.headers.get("origin");
const originIsAllowed = origin && isAllowedOrigin(origin);
if (req.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: {
...(originIsAllowed ? { "Access-Control-Allow-Origin": origin } : {}),
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers":
"Content-Type, Authorization, x-vercel-protection-bypass, x-client-info, apikey",
"Access-Control-Max-Age": "86400",
},
});
}
if (req.method !== "POST") {
return Response.json(
{ msg: 'Method not allowed' },
{ status: 405 }
);
}

const input: {name?: string} = await req.json();
const groupName = input.name;
if (groupName === undefined) {
return new Response("Missing group name", {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
// @ts-ignore Deno is not visible to the IDE
const url = Deno.env.get("SUPABASE_URL");
// @ts-ignore Deno is not visible to the IDE
const service_key = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
// @ts-ignore Deno is not visible to the IDE
const anon_key = Deno.env.get("SUPABASE_ANON_KEY");

if (!url || !anon_key || !service_key) {
return new Response("Missing SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY or SUPABASE_ANON_KEY", {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
const supabase = createClient(url, anon_key)
const authHeader = req.headers.get('Authorization')!
const token = authHeader.replace('Bearer ', '')
const { data, error } = await supabase.auth.getClaims(token)

const userEmail = data?.claims?.email
if (!userEmail || error || data?.claims?.is_anonymous === true) {
return Response.json(
{ msg: 'Invalid JWT' },
{
status: 401,
}
)
}
// This password is discarded; nobody is expected to ever login as a group.
const password = crypto.randomUUID();
const email = `${groupName}@groups.discoursegraphs.com`;
const supabaseAdmin: DGSupabaseClient = createClient(url, service_key);
let userResponse: UserResponse | undefined;
try {
userResponse = await supabaseAdmin.auth.admin.createUser({
email,
password,
role:'anon',
user_metadata: {group: true},
email_confirm: false, // eslint-disable-line @typescript-eslint/naming-convention
});
if (userResponse.error)
throw userResponse.error;
if (!userResponse.data.user)
throw new Error("Did not create user");
} catch (error) {
if (error.code === 'email_exists') {
return Response.json(
{ msg: 'A group by this name exists' },
{
status: 400,
});
}
return Response.json({ msg: 'Failed to create group user', error: error.message }, { status: 500 });
}
// eslint-disable-next-line @typescript-eslint/naming-convention
const group_id = userResponse.data.user.id;
// eslint-disable-next-line @typescript-eslint/naming-convention
const membershipResponse = await supabaseAdmin.from("group_membership").insert({group_id, member_id:data.claims.sub, admin:true});
if (membershipResponse.error)
return Response.json({ msg: `Failed to create membership for group ${group_id}`, error: membershipResponse.error.message }, { status: 500 });

const res = Response.json({group_id});

if (originIsAllowed) {
res.headers.set("Access-Control-Allow-Origin", origin as string);
res.headers.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.headers.set(
"Access-Control-Allow-Headers",
"Content-Type, Authorization, x-vercel-protection-bypass, x-client-info, apikey",
);
}

return res;
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
"@repo/database/dbTypes": "../../../src/dbTypes.ts",
"@repo/database/lib/contextFunctions": "../../../src/lib/contextFunctions.ts",
"@repo/database/lib/client": "../../../src/lib/client.ts",
"@repo/utils/lib/execContext": "../../../../utils/src/lib/execContext.ts"
"@repo/utils/lib/execContext": "../../../../utils/src/execContext.ts"
}
}