Skip to content
Merged
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
6 changes: 5 additions & 1 deletion apps/obsidian/src/components/AdminPanelSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ export const AdminPanelSettings = () => {
new Notice("Failed to connect to the database", 3000);
return;
}
if (data) window.open(`${nextRoot()}/auth/token?t=${data}&url=/`, "_blank");
if (data)
window.open(
`${nextRoot()}auth/token?t=${data}&url=/auth/group`,
"_blank",
);
};

return (
Expand Down
6 changes: 5 additions & 1 deletion apps/roam/src/components/settings/AdminPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,11 @@ const FeatureFlagsTab = (): React.ReactElement => {
});
return;
}
if (data) window.open(`${nextRoot()}/auth/token?t=${data}&url=/`, "_blank");
if (data)
window.open(
`${nextRoot()}auth/token?t=${data}&url=/auth/group`,
"_blank",
);
};

return (
Expand Down
11 changes: 11 additions & 0 deletions apps/website/app/(home)/auth/group/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ListGroups } from "~/components/auth/ListGroups";

const Page = () => (
<main>
Comment thread
maparent marked this conversation as resolved.
<div className="mx-auto max-w-6xl space-y-12 px-6 py-12">
<ListGroups />
</div>
</main>
);

export default Page;
85 changes: 85 additions & 0 deletions apps/website/app/components/auth/ListGroups.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { createClient } from "~/utils/supabase/server";
import { getSessionUserData } from "~/utils/supabase/account";
import Link from "next/link";
import { Tables } from "@repo/database/dbTypes";
import internalError from "~/utils/internalErrorSsr";

type GroupData = Tables<"my_groups">;

export const ListGroups = async () => {
let groupData: GroupData[] | null = null;
let adminData: Record<string, boolean> = {};
let userName: string | undefined;
let error: string | undefined;

try {
const client = await createClient();
const userData = await getSessionUserData(client);
if (!userData) {
throw new Error("Not logged in.\nPlease log in from application.");
}
const { name, type, id } = userData;
if (type === "anonymous") userName = "Space " + name;
else if (type === "group") userName = "group " + name;
else if (type === "person") userName = name;
const groupResponse = await client.from("my_groups").select();
if (groupResponse.error) {
internalError({
error: groupResponse.error,
});
throw new Error("Could not access Discourse Graphs");
}
groupData = groupResponse.data;
const membershipReq = await client
.from("group_membership")
.select("group_id,admin")
.eq("member_id", id);
if (membershipReq.error) {
internalError({
error: membershipReq.error,
});
throw new Error("Could not access Discourse Graphs");
}
adminData = Object.fromEntries(
// eslint-disable-next-line @typescript-eslint/naming-convention
membershipReq.data.map(({ group_id, admin }) => [
group_id,
admin || false,
]),
);
} catch (e) {
error = e instanceof Error ? e.message : "An unknown error occured";
}

return (
<>
<div className="text-right text-sm">
{userName ? <p>Logged in as {userName}</p> : ""}
</div>
<div>
{error ? (
"Error: " + error
) : groupData === null ? (
"Error" // we should have had an error in that case
) : groupData.length === 0 ? (
<p>You are not part of any group.</p>
) : (
<>
<p>Your groups:</p>
<ul className="list-inside list-disc space-y-2">
{groupData.map((d) => (
<li key={d.id}>
{adminData[d.id || ""] ? (
<Link href={"/auth/group/" + d.id!}>{d.name}</Link>
) : (
d.name
)}
</li>
))}
</ul>
</>
)}
</div>
</>
);
};
48 changes: 48 additions & 0 deletions apps/website/app/utils/supabase/account.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,53 @@
import type { Database } from "@repo/database/dbTypes";
import type { DGSupabaseClient } from "@repo/database/lib/client";

type AgentType = Database["public"]["Enums"]["AgentType"] | "group";

export const getSessionUserData = async (
client: DGSupabaseClient,
): Promise<{
id: string;
name: string;
type: AgentType;
email?: string;
} | null> => {
const { data, error } = await client.auth.getUser();
if (error || !data?.user) return null;
const userData = data.user;
if (typeof userData.id !== "string") return null;
const { id, email }: { id: string; email?: string } = userData;
if (email) {
const [name, host] = email.split("@") as [string, string];
if (host === "database.discoursegraphs.com" && name.endsWith("-anon")) {
const parts = name.split("-");
const spaceId = Number.parseInt(parts[1]!);
Comment thread
maparent marked this conversation as resolved.
if (Number.isNaN(spaceId)) return null;
const spaceReq = await client
.from("Space")
.select("name")
.eq("id", spaceId)
.maybeSingle();
if (spaceReq.error || !spaceReq.data) {
return null;
}
return { name: spaceReq.data.name, id, type: "anonymous", email };
}
if (host === "groups.discoursegraphs.com") {
return { name, id, email, type: "group" };
}
}
const accountReq = await client
.from("PlatformAccount")
.select("name")
.eq("dg_account", id)
.eq("agent_type", "person")
.maybeSingle();
if (accountReq.error || !accountReq.data) {
return null;
}
return { id, name: accountReq.data.name, type: "person", email };
};

export const createGroup = async (
client: DGSupabaseClient,
name: string,
Expand Down
126 changes: 126 additions & 0 deletions apps/website/test/integration/listMyGroups.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import assert from "assert";
Comment thread
maparent marked this conversation as resolved.
import { describe, it, beforeAll, afterAll } from "vitest";
import { createClient } from "@supabase/supabase-js";
import type { Database } from "@repo/database/dbTypes";
import type { DGSupabaseClient } from "@repo/database/lib/client";
import {
fetchOrCreateSpaceDirect,
spaceAnonUserEmail,
} from "@repo/database/lib/contextFunctions";
import { createGroup } from "../../app/utils/supabase/account";

const SUPABASE_URL = process.env.SUPABASE_URL!;
const ANON_KEY = process.env.SUPABASE_PUBLISHABLE_KEY!;
const SERVICE_KEY = process.env.SUPABASE_SECRET_KEY!;
const PASSWORD = "abcdefgh";

const freshClient = (): DGSupabaseClient =>
createClient<Database, "public">(SUPABASE_URL, ANON_KEY);

const serviceClient = () =>
createClient<Database, "public">(SUPABASE_URL, SERVICE_KEY);

const signedInClient = async (spaceId: number): Promise<DGSupabaseClient> => {
const client = freshClient();
const { error } = await client.auth.signInWithPassword({
email: spaceAnonUserEmail("Roam", spaceId),
password: PASSWORD,
});
if (error) throw new Error(`Sign-in failed: ${error.message}`);
return client;
};

describe("list group members flow", { tags: ["database"] }, () => {
let spaceId1: number;
let spaceId2: number;
let spaceAccountUuid1: string;
let spaceAccountUuid2: string;
let client1: DGSupabaseClient;
let client2: DGSupabaseClient;
let createdGroupId: string | null = null;

beforeAll(async () => {
const s1 = await fetchOrCreateSpaceDirect({
name: "vitest-s1",
url: "https://roamresearch.com/#/app/vitest-s1",
platform: "Roam",
password: PASSWORD,
});
if (!s1.data)
throw new Error(`Failed to create space 1: ${s1.error?.message}`);
spaceId1 = s1.data.id;
client1 = await signedInClient(spaceId1);
assert(client1);
const accountReq1 = await client1
.from("PlatformAccount")
.select("id,dg_account")
.eq(
"account_local_id",
`roam-${spaceId1}-anon@database.discoursegraphs.com`,
)
.maybeSingle();
assert(!accountReq1.error);
assert(accountReq1.data);
assert(accountReq1.data.dg_account);
spaceAccountUuid1 = accountReq1.data.dg_account;
const s2 = await fetchOrCreateSpaceDirect({
name: "vitest-s2",
url: "https://roamresearch.com/#/app/vitest-s2",
platform: "Roam",
password: PASSWORD,
});
if (!s2.data)
throw new Error(`Failed to create space 2: ${s2.error?.message}`);
spaceId2 = s2.data.id;
client2 = await signedInClient(spaceId2);
assert(client2);
const accountReq2 = await client2
.from("PlatformAccount")
.select("id,dg_account")
.eq(
"account_local_id",
`roam-${spaceId2}-anon@database.discoursegraphs.com`,
)
.maybeSingle();
assert(!accountReq2.error);
assert(accountReq2.data);
assert(accountReq2.data.dg_account);
spaceAccountUuid2 = accountReq2.data.dg_account;
});

afterAll(async () => {
if (createdGroupId)
await serviceClient().auth.admin.deleteUser(createdGroupId);
if (spaceAccountUuid1)
await serviceClient().auth.admin.deleteUser(spaceAccountUuid1);
if (spaceAccountUuid2)
await serviceClient().auth.admin.deleteUser(spaceAccountUuid2);
if (spaceId1)
await serviceClient().from("Space").delete().eq("id", spaceId1);
if (spaceId2)
await serviceClient().from("Space").delete().eq("id", spaceId2);
});

it("lists group members", async () => {
// Step 1: user1 creates a group
const groupId = await createGroup(client1, "vitest-invite-group");
assert(groupId !== null, "createGroup should return a group ID");
createdGroupId = groupId;

// Step 2: Add another member
const { error: errorAddMember } = await client1
.from("group_membership")
.insert({
member_id: spaceAccountUuid2, // eslint-disable-line @typescript-eslint/naming-convention
group_id: groupId, // eslint-disable-line @typescript-eslint/naming-convention
admin: false,
});
assert(!errorAddMember);

const groupResponse = await client2.from("my_groups").select();
assert(!groupResponse.error);
assert(groupResponse.data !== null);
assert(groupResponse.data.length === 1);
assert(groupResponse.data[0]!.id === groupId);
});
});
2 changes: 1 addition & 1 deletion packages/utils/src/execContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const nextRoot = (): string => {
process.env.NEXT_API_ROOT !== undefined &&
process.env.NEXT_API_ROOT !== ""
)
return process.env.NEXT_API_ROOT.split("/").slice(0, 3).join("/");
return process.env.NEXT_API_ROOT.split("/").slice(0, 3).join("/") + "/";
return IS_DEV ? "http://localhost:3000/" : "https://discoursegraphs.com/";
};

Expand Down
Loading