Skip to content

Commit a56966e

Browse files
authored
ENG-1238 Group creation function (#670)
* eng-1238 group creation edge function
1 parent 7579fb7 commit a56966e

File tree

3 files changed

+132
-1
lines changed

3 files changed

+132
-1
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"imports": {
3+
"@supabase/functions-js/edge-runtime": "jsr:@supabase/functions-js/edge-runtime.d.ts",
4+
"@supabase/supabase-js": "jsr:@supabase/supabase-js@2",
5+
"@supabase/functions-js": "jsr:@supabase/functions-js@2",
6+
"@repo/database/lib/client": "../../../src/lib/client.ts"
7+
}
8+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Follow this setup guide to integrate the Deno language server with your editor:
2+
// https://deno.land/manual/getting_started/setup_your_environment
3+
// This enables autocomplete, go to definition, etc.
4+
5+
import "@supabase/functions-js/edge-runtime";
6+
import { createClient, type UserResponse } from "@supabase/supabase-js";
7+
import type { DGSupabaseClient } from "@repo/database/lib/client";
8+
9+
// The following lines are duplicated from apps/website/app/utils/llm/cors.ts
10+
const allowedOrigins = ["https://roamresearch.com", "http://localhost:3000", "app://obsidian.md"];
11+
12+
const isVercelPreviewUrl = (origin: string): boolean =>
13+
/^https:\/\/.*-discourse-graph-[a-z0-9]+\.vercel\.app$/.test(origin);
14+
15+
const isAllowedOrigin = (origin: string): boolean =>
16+
allowedOrigins.some((allowed) => origin.startsWith(allowed)) ||
17+
isVercelPreviewUrl(origin);
18+
19+
// @ts-ignore Deno is not visible to the IDE
20+
Deno.serve(async (req) => {
21+
const origin = req.headers.get("origin");
22+
const originIsAllowed = origin && isAllowedOrigin(origin);
23+
if (req.method === "OPTIONS") {
24+
return new Response(null, {
25+
status: 204,
26+
headers: {
27+
...(originIsAllowed ? { "Access-Control-Allow-Origin": origin } : {}),
28+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
29+
"Access-Control-Allow-Headers":
30+
"Content-Type, Authorization, x-vercel-protection-bypass, x-client-info, apikey",
31+
"Access-Control-Max-Age": "86400",
32+
},
33+
});
34+
}
35+
if (req.method !== "POST") {
36+
return Response.json(
37+
{ msg: 'Method not allowed' },
38+
{ status: 405 }
39+
);
40+
}
41+
42+
const input: {name?: string} = await req.json();
43+
const groupName = input.name;
44+
if (groupName === undefined) {
45+
return new Response("Missing group name", {
46+
status: 400,
47+
headers: { "Content-Type": "application/json" },
48+
});
49+
}
50+
// @ts-ignore Deno is not visible to the IDE
51+
const url = Deno.env.get("SUPABASE_URL");
52+
// @ts-ignore Deno is not visible to the IDE
53+
const service_key = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
54+
// @ts-ignore Deno is not visible to the IDE
55+
const anon_key = Deno.env.get("SUPABASE_ANON_KEY");
56+
57+
if (!url || !anon_key || !service_key) {
58+
return new Response("Missing SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY or SUPABASE_ANON_KEY", {
59+
status: 500,
60+
headers: { "Content-Type": "application/json" },
61+
});
62+
}
63+
const supabase = createClient(url, anon_key)
64+
const authHeader = req.headers.get('Authorization')!
65+
const token = authHeader.replace('Bearer ', '')
66+
const { data, error } = await supabase.auth.getClaims(token)
67+
68+
const userEmail = data?.claims?.email
69+
if (!userEmail || error || data?.claims?.is_anonymous === true) {
70+
return Response.json(
71+
{ msg: 'Invalid JWT' },
72+
{
73+
status: 401,
74+
}
75+
)
76+
}
77+
// This password is discarded; nobody is expected to ever login as a group.
78+
const password = crypto.randomUUID();
79+
const email = `${groupName}@groups.discoursegraphs.com`;
80+
const supabaseAdmin: DGSupabaseClient = createClient(url, service_key);
81+
let userResponse: UserResponse | undefined;
82+
try {
83+
userResponse = await supabaseAdmin.auth.admin.createUser({
84+
email,
85+
password,
86+
role:'anon',
87+
user_metadata: {group: true},
88+
email_confirm: false, // eslint-disable-line @typescript-eslint/naming-convention
89+
});
90+
if (userResponse.error)
91+
throw userResponse.error;
92+
if (!userResponse.data.user)
93+
throw new Error("Did not create user");
94+
} catch (error) {
95+
if (error.code === 'email_exists') {
96+
return Response.json(
97+
{ msg: 'A group by this name exists' },
98+
{
99+
status: 400,
100+
});
101+
}
102+
return Response.json({ msg: 'Failed to create group user', error: error.message }, { status: 500 });
103+
}
104+
// eslint-disable-next-line @typescript-eslint/naming-convention
105+
const group_id = userResponse.data.user.id;
106+
// eslint-disable-next-line @typescript-eslint/naming-convention
107+
const membershipResponse = await supabaseAdmin.from("group_membership").insert({group_id, member_id:data.claims.sub, admin:true});
108+
if (membershipResponse.error)
109+
return Response.json({ msg: `Failed to create membership for group ${group_id}`, error: membershipResponse.error.message }, { status: 500 });
110+
111+
const res = Response.json({group_id});
112+
113+
if (originIsAllowed) {
114+
res.headers.set("Access-Control-Allow-Origin", origin as string);
115+
res.headers.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
116+
res.headers.set(
117+
"Access-Control-Allow-Headers",
118+
"Content-Type, Authorization, x-vercel-protection-bypass, x-client-info, apikey",
119+
);
120+
}
121+
122+
return res;
123+
});

packages/database/supabase/functions/create-space/deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@
66
"@repo/database/dbTypes": "../../../src/dbTypes.ts",
77
"@repo/database/lib/contextFunctions": "../../../src/lib/contextFunctions.ts",
88
"@repo/database/lib/client": "../../../src/lib/client.ts",
9-
"@repo/utils/lib/execContext": "../../../../utils/src/lib/execContext.ts"
9+
"@repo/utils/lib/execContext": "../../../../utils/src/execContext.ts"
1010
}
1111
}

0 commit comments

Comments
 (0)