|
| 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 | +}); |
0 commit comments