Skip to content

Commit cd9d14b

Browse files
authored
ENG-589 Transfer the functionality to create spaces from vercel's space route to a supabase edge function (#291)
* Transfer the functionality to create spaces from vercel's space route to a supabase function. This allows to not use the service key in vercel, improving security. * Use direct route * some repairs * remove .npmrc * some repairs * Make deno typechecking of supabase edge functions part of lint stage. Deno seems to require a very up-to-date supabase client, so update that. * cite duplicate origins * internal references * Use deno lint instead of deno check. Simpler if done locally. * re-export syntax * comment on reexport * ts-ignore * make linting work, and apply linting to scripts
1 parent c080346 commit cd9d14b

19 files changed

Lines changed: 458 additions & 216 deletions

File tree

apps/roam/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
"@octokit/auth-app": "^7.1.4",
3131
"@octokit/core": "^6.1.3",
3232
"@repo/types": "*",
33-
"@supabase/auth-js": "^2.70.0",
34-
"@supabase/supabase-js": "^2.50.0",
33+
"@supabase/auth-js": "^2.71.1",
34+
"@supabase/supabase-js": "^2.52.0",
3535
"@tldraw/tldraw": "^2.0.0-alpha.12",
3636
"@vercel/blob": "^0.27.0",
3737
"contrast-color": "^1.0.1",

apps/roam/src/utils/supabaseContext.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import getBlockProps from "~/utils/getBlockProps";
99
import setBlockProps from "~/utils/setBlockProps";
1010
import { type DGSupabaseClient } from "@repo/ui/lib/supabase/client";
1111
import {
12-
fetchOrCreateSpaceId,
12+
fetchOrCreateSpaceDirect,
1313
fetchOrCreatePlatformAccount,
1414
createLoggedInClient,
1515
} from "@repo/ui/lib/supabase/contextFunctions";
@@ -57,12 +57,14 @@ export const getSupabaseContext = async (): Promise<SupabaseContext | null> => {
5757
const url = getRoamUrl();
5858
const spaceName = window.roamAlphaAPI.graph.name;
5959
const platform: Platform = "Roam";
60-
const spaceId = await fetchOrCreateSpaceId({
60+
const spaceResult = await fetchOrCreateSpaceDirect({
6161
password: spacePassword,
6262
url,
6363
name: spaceName,
6464
platform,
6565
});
66+
if (!spaceResult.data) throw new Error("Failed to create space");
67+
const spaceId = spaceResult.data.id;
6668
const userId = await fetchOrCreatePlatformAccount({
6769
platform: "Roam",
6870
accountLocalId,

apps/website/app/api/supabase/space/route.ts

Lines changed: 3 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,138 +1,15 @@
11
import { NextResponse, NextRequest } from "next/server";
2-
import {
3-
type PostgrestSingleResponse,
4-
PostgrestError,
5-
type User,
6-
} from "@supabase/supabase-js";
7-
import { createClient } from "~/utils/supabase/server";
8-
import { getOrCreateEntity, ItemValidator } from "~/utils/supabase/dbUtils";
92
import {
103
createApiResponse,
114
handleRouteError,
125
defaultOptionsHandler,
13-
asPostgrestFailure,
146
} from "~/utils/supabase/apiUtils";
15-
import { Tables, TablesInsert } from "@repo/database/types.gen.ts";
16-
import { spaceAnonUserEmail } from "@repo/ui/lib/utils";
17-
18-
type SpaceDataInput = TablesInsert<"Space">;
19-
type SpaceRecord = Tables<"Space">;
20-
21-
type SpaceCreationInput = SpaceDataInput & { password: string };
22-
23-
const spaceValidator: ItemValidator<SpaceCreationInput> = (space) => {
24-
if (!space || typeof space !== "object")
25-
return "Invalid request body: expected a JSON object.";
26-
const { name, url, platform, password } = space;
27-
28-
if (!name || typeof name !== "string" || name.trim() === "")
29-
return "Missing or invalid name.";
30-
if (!url || typeof url !== "string" || url.trim() === "")
31-
return "Missing or invalid URL.";
32-
if (platform === undefined || !["Roam", "Obsidian"].includes(platform))
33-
return "Missing or invalid platform.";
34-
if (!password || typeof password !== "string" || password.length < 8)
35-
return "password must be at least 8 characters";
36-
return null;
37-
};
38-
39-
const processAndGetOrCreateSpace = async (
40-
supabasePromise: ReturnType<typeof createClient>,
41-
data: SpaceCreationInput,
42-
): Promise<PostgrestSingleResponse<SpaceRecord>> => {
43-
const { name, url, platform, password } = data;
44-
const error = spaceValidator(data);
45-
if (error !== null) return asPostgrestFailure(error, "invalid space");
46-
47-
const supabase = await supabasePromise;
48-
49-
const result = await getOrCreateEntity<"Space">({
50-
supabase,
51-
tableName: "Space",
52-
insertData: {
53-
name: name.trim(),
54-
url: url.trim().replace(/\/$/, ""),
55-
platform,
56-
},
57-
uniqueOn: ["url"],
58-
});
59-
if (result.error) return result;
60-
const space_id = result.data.id;
61-
62-
// this is related but each step is idempotent, so con retry w/o transaction
63-
const email = spaceAnonUserEmail(platform, result.data.id);
64-
let anonymousUser: User | null = null;
65-
{
66-
const { error, data } = await supabase.auth.signInWithPassword({
67-
email,
68-
password,
69-
});
70-
if (error && error.message !== "Invalid login credentials") {
71-
// Handle unexpected errors
72-
return asPostgrestFailure(error.message, "authentication_error");
73-
}
74-
anonymousUser = data.user;
75-
}
76-
if (anonymousUser === null) {
77-
const resultCreateAnonymousUser = await supabase.auth.admin.createUser({
78-
email,
79-
password,
80-
email_confirm: true,
81-
});
82-
if (resultCreateAnonymousUser.error) {
83-
return {
84-
count: null,
85-
status: resultCreateAnonymousUser.error.status || -1,
86-
statusText: resultCreateAnonymousUser.error.message,
87-
data: null,
88-
error: new PostgrestError({
89-
message: resultCreateAnonymousUser.error.message,
90-
details:
91-
typeof resultCreateAnonymousUser.error.cause === "string"
92-
? resultCreateAnonymousUser.error.cause
93-
: "",
94-
hint: "",
95-
code: resultCreateAnonymousUser.error.code || "unknown",
96-
}),
97-
}; // space created but not its user, try again
98-
}
99-
anonymousUser = resultCreateAnonymousUser.data.user;
100-
}
101-
// NOTE: The next few steps could be done as the new user, except the SpaceAccess
102-
const anonPlatformUserResult = await getOrCreateEntity<"PlatformAccount">({
103-
supabase,
104-
tableName: "PlatformAccount",
105-
insertData: {
106-
platform,
107-
account_local_id: email,
108-
name: `Anonymous of space ${space_id}`,
109-
agent_type: "anonymous",
110-
dg_account: anonymousUser.id,
111-
},
112-
uniqueOn: ["account_local_id", "platform"],
113-
});
114-
if (anonPlatformUserResult.error) return anonPlatformUserResult;
115-
116-
const resultAnonUserSpaceAccess = await getOrCreateEntity<"SpaceAccess">({
117-
supabase,
118-
tableName: "SpaceAccess",
119-
insertData: {
120-
space_id,
121-
account_id: anonPlatformUserResult.data.id,
122-
editor: true,
123-
},
124-
uniqueOn: ["space_id", "account_id"],
125-
});
126-
if (resultAnonUserSpaceAccess.error) return resultAnonUserSpaceAccess; // space created but not connected, try again
127-
return result;
128-
};
7+
import { fetchOrCreateSpaceDirect } from "@repo/ui/lib/supabase/contextFunctions";
1298

1309
export const POST = async (request: NextRequest): Promise<NextResponse> => {
131-
const supabasePromise = createClient();
132-
13310
try {
134-
const body: SpaceCreationInput = await request.json();
135-
const result = await processAndGetOrCreateSpace(supabasePromise, body);
11+
const body = await request.json();
12+
const result = await fetchOrCreateSpaceDirect(body);
13613
return createApiResponse(request, result);
13714
} catch (e: unknown) {
13815
return handleRouteError(request, e, "/api/supabase/space");

apps/website/app/utils/supabase/apiUtils.ts

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import {
66

77
import { Database } from "@repo/database/types.gen.ts";
88
import { createClient } from "~/utils/supabase/server";
9+
import { asPostgrestFailure } from "@repo/ui/lib/supabase/contextFunctions";
10+
// Temporarily re-exporting because many imports point here. Will be moved to a future packages/utils.
11+
export { asPostgrestFailure } from "@repo/ui/lib/supabase/contextFunctions";
912
import cors from "~/utils/llm/cors";
1013

1114
/**
@@ -134,23 +137,3 @@ export const makeDefaultDeleteHandler =
134137
const response = await supabase.from(tableName).delete().eq(pk, idN);
135138
return createApiResponse(request, response);
136139
};
137-
138-
export const asPostgrestFailure = (
139-
message: string,
140-
code: string,
141-
status: number = 400,
142-
): PostgrestSingleResponse<any> => {
143-
return {
144-
data: null,
145-
error: {
146-
message,
147-
code,
148-
details: "",
149-
hint: "",
150-
name: code,
151-
},
152-
count: null,
153-
statusText: code,
154-
status,
155-
};
156-
};

apps/website/app/utils/supabase/server.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@ import { Database } from "@repo/database/types.gen.ts";
44

55
// Inspired by https://supabase.com/ui/docs/nextjs/password-based-auth
66

7-
export const createClient = async (service = true) => {
7+
export const createClient = async () => {
88
const cookieStore = await cookies();
99
const supabaseUrl = process.env.SUPABASE_URL;
10-
const supabaseKey = service
11-
? process.env.SUPABASE_SERVICE_ROLE_KEY
12-
: process.env.SUPABASE_ANON_KEY;
10+
const supabaseKey = process.env.SUPABASE_ANON_KEY;
1311

1412
if (!supabaseUrl || !supabaseKey) {
1513
throw new Error("Missing required Supabase environment variables");

package-lock.json

Lines changed: 13 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/database/eslint.config.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import base from "@repo/eslint-config/react-internal";
1+
import { config as base } from "@repo/eslint-config/react-internal";
22

33
export default [
44
...base,
55
{
66
languageOptions: {
77
parserOptions: {
88
tsconfigRootDir: ".",
9+
project: "./tsconfig.json",
910
},
1011
},
1112
}

packages/database/features/step-definitions/stepdefs.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import {
66
type Database,
77
type Enums,
88
} from "@repo/database/types.gen.ts";
9-
import { spaceAnonUserEmail } from "@repo/ui/lib/utils";
109
import {
11-
fetchOrCreateSpaceId,
10+
spaceAnonUserEmail,
11+
fetchOrCreateSpaceIndirect,
1212
fetchOrCreatePlatformAccount,
1313
} from "@repo/ui/lib/supabase/contextFunctions";
1414

@@ -117,12 +117,17 @@ When(
117117
if (PLATFORMS.indexOf(platform) < 0)
118118
throw new Error(`Platform must be one of ${PLATFORMS}`);
119119
const localRefs: Record<string, any> = world.localRefs || {};
120-
const spaceId = await fetchOrCreateSpaceId({
120+
const spaceResponse = await fetchOrCreateSpaceIndirect({
121121
password: SPACE_ANONYMOUS_PASSWORD,
122122
url: `https://roamresearch.com/#/app/${spaceName}`,
123123
name: spaceName,
124124
platform,
125125
});
126+
if (!spaceResponse.data)
127+
throw new Error(
128+
`Could not create space: ${JSON.stringify(spaceResponse.error)}`,
129+
);
130+
const spaceId = spaceResponse.data.id;
126131
localRefs[spaceName] = spaceId;
127132
const userId = await fetchOrCreatePlatformAccount({
128133
platform,

packages/database/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010
},
1111
"scripts": {
1212
"init": "supabase login",
13-
"dev": "supabase start",
13+
"dev": "supabase start && supabase functions serve",
1414
"stop": "supabase stop",
1515
"check-types": "npm run lint && supabase stop && npm run dbdiff",
16-
"lint": "tsx scripts/lint.ts",
16+
"lint": "eslint . && tsx scripts/lint.ts",
1717
"lint:fix": "tsx scripts/lint.ts -f",
1818
"build": "tsx scripts/build.ts",
1919
"test": "tsc && cucumber-js",

0 commit comments

Comments
 (0)