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
54 changes: 53 additions & 1 deletion apps/obsidian/src/components/AdminPanelSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useState, useCallback } from "react";
import { usePlugin } from "./PluginContext";
import { Notice } from "obsidian";
import { Notice, setIcon } from "obsidian";
import { updateUsername } from "~/utils/supabaseContext";
import { initializeSupabaseSync } from "~/utils/syncDgNodesToSupabase";
import { nextRoot } from "@repo/utils/execContext";
import { getLoggedInClient } from "~/utils/supabaseContext";

export const AdminPanelSettings = () => {
const plugin = usePlugin();
Expand Down Expand Up @@ -41,6 +43,31 @@ export const AdminPanelSettings = () => {
await updateUsername(plugin, newValue);
};

const handleLoginHandoff = async () => {
const client = await getLoggedInClient(plugin);
if (!client) {
new Notice("Failed to connect to the database", 3000);
return;
}
const sessionData = await client.auth.getSession();
if (!sessionData.data.session) {
new Notice("Failed to connect to the database", 3000);
return;
}
/* eslint-disable @typescript-eslint/naming-convention */
const { access_token, refresh_token } = sessionData.data.session;
const { data, error } = await client.rpc("create_secret_token", {
v_payload: JSON.stringify({ access_token, refresh_token }),
expiry_interval: "45s",
});
Comment thread
maparent marked this conversation as resolved.
/* eslint-enable @typescript-eslint/naming-convention */
if (error || typeof data !== "string") {
new Notice("Failed to connect to the database", 3000);
return;
}
if (data) window.open(`${nextRoot()}/auth/token?t=${data}&url=/`, "_blank");
};

return (
<div className="general-settings">
<div className="setting-item">
Expand Down Expand Up @@ -80,6 +107,31 @@ export const AdminPanelSettings = () => {
/>
</div>
</div>
<div
className={
"setting-item " + (plugin.settings.syncModeEnabled ? "" : "hidden")
}
>
<div className="setting-item-info">
<div className="setting-item-name">Group management</div>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What if I'm not an admin? In the future we should name this something more generic "Sync management" or "Discourse Graph sync"

cc @jsmorabito

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Whoever you are, you can create a group, and that makes you admin of that specific group. So that's a group management operation you can always do.

<div className="setting-item-description">
This will allow you to view and manage your sharing groups
Comment thread
maparent marked this conversation as resolved.
</div>
</div>
<div className="setting-item-control">
<button
onClick={() => {
void handleLoginHandoff();
}}
>
Manage groups
<span
className="icon"
ref={(el) => (el && setIcon(el, "arrow-up-right")) || undefined}
/>
</button>
</div>
</div>
</div>
);
};
51 changes: 50 additions & 1 deletion apps/roam/src/components/settings/AdminPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Tabs,
} from "@blueprintjs/core";
import { Select } from "@blueprintjs/select";
import { render as renderToast } from "roamjs-components/components/Toast";
import {
getSupabaseContext,
getLoggedInClient,
Expand All @@ -34,6 +35,7 @@ import {
} from "~/components/settings/utils/accessors";
import { FeatureFlagPanel } from "./components/BlockPropSettingPanels";
import type { FeatureFlags } from "./utils/zodSchema";
import { nextRoot } from "@repo/utils/execContext";

const NodeRow = ({ node }: { node: PConceptFull }) => {
return (
Expand Down Expand Up @@ -266,7 +268,6 @@ const FeatureFlagsTab = (): React.ReactElement => {
const [suggestiveOverlayValue, setSuggestiveOverlayValue] = useState(
getFeatureFlag("Suggestive mode overlay enabled"),
);

const syncAlreadyEnabled = duplicateNodeAlertValue || suggestiveOverlayValue;
Comment thread
maparent marked this conversation as resolved.

const ensureSyncEnabled = (
Expand All @@ -290,6 +291,43 @@ const FeatureFlagsTab = (): React.ReactElement => {
}
};

const handleLoginHandoff = async () => {
const client = await getLoggedInClient();
if (!client) {
renderToast({
content: "Could not connect to database",
intent: Intent.DANGER,
id: "client-access",
});
return;
}
const sessionData = await client.auth.getSession();
if (!sessionData.data.session) {
internalError({
error: "Client w/o session",
type: "database-login",
userMessage: "Could not connect to database",
});
return;
}
/* eslint-disable @typescript-eslint/naming-convention */
const { access_token, refresh_token } = sessionData.data.session;
const { data, error } = await client.rpc("create_secret_token", {
v_payload: JSON.stringify({ access_token, refresh_token }),
expiry_interval: "45s",
});
/* eslint-enable @typescript-eslint/naming-convention */
if (error || typeof data !== "string") {
internalError({
error: "Call to create-secret-token",
type: "create-secret-token",
userMessage: "Could not connect to database",
});
return;
}
if (data) window.open(`${nextRoot()}/auth/token?t=${data}&url=/`, "_blank");
};

return (
<div className="flex flex-col gap-4 p-4">
<FeatureFlagPanel
Expand Down Expand Up @@ -393,6 +431,17 @@ const FeatureFlagsTab = (): React.ReactElement => {
>
Send Error Email
</Button>
{syncAlreadyEnabled && (
<Button
className="w-96"
icon="document-open"
onClick={() => {
void handleLoginHandoff();
}}
>
Manage groups
</Button>
)}
</div>
);
};
Expand Down
12 changes: 12 additions & 0 deletions apps/website/app/(home)/auth/token/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { LoginWithToken } from "~/components/auth/LoginWithToken";
import { Suspense } from "react";

const Page = () => (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<Suspense fallback={<>Logging in</>}>
<LoginWithToken />
</Suspense>
</div>
);

export default Page;
97 changes: 97 additions & 0 deletions apps/website/app/components/auth/LoginWithToken.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"use client";

import { createClient } from "~/utils/supabase/client";
import { useRouter, useSearchParams } from "next/navigation";
import { useState, useEffect, useCallback, useRef } from "react";
import useInternalError from "~/utils/internalError";

export const LoginWithToken = () => {
const loginAttempted = useRef(false);
const searchParams = useSearchParams();
const internalError = useInternalError();
const router = useRouter();
const [secretToken] = useState(() => searchParams.get("t"));
useEffect(() => {
if (secretToken && typeof window !== "undefined") {
const url = new URL(window.location.href);
url.searchParams.delete("t");
window.history.replaceState({}, "", url);
}
}, [secretToken]);
const [url] = useState(searchParams.get("url"));
const [done, setDone] = useState(false);
const [error, setError] = useState<string | null>(
secretToken === null ? "Please provide token" : null,
);

const login = useCallback(async () => {
try {
const client = createClient();
const result = await client.rpc("get_secret_token", {
token: secretToken!,
});
if (result.error) {
setError("Could not connect to DiscourseGraphs");
internalError({ error: result.error, type: "get-secret-token" });
return;
}
if (result.data == null) {
setError("Could not retrieve information, please try again.");
internalError({ error: "missing token", type: "get-secret-token" });
return;
}
if (typeof result.data !== "string") {
setError(
"DiscourseGraphs configuration error, the team has been warned",
);
internalError({
error: "payload-not-string",
type: "get-secret-token",
});
return;
Comment thread
maparent marked this conversation as resolved.
}
const data = JSON.parse(result.data) as {
/* eslint-disable @typescript-eslint/naming-convention */
access_token: string;
refresh_token: string;
/* eslint-enable @typescript-eslint/naming-convention */
};
if (
!data ||
typeof data !== "object" ||
!data.access_token ||
!data.refresh_token
) {
setError(
"DiscourseGraphs configuration error, the team has been warned",
);
internalError({ error: "misshaped-payload", type: "get-secret-token" });
return;
}
const response = await client.auth.setSession(data);
if (response.error) {
setError(response.error.message);
} else if (url) {
router.replace(url);
}
} catch (error) {
setError("Unkown error while logging you in.");
internalError({ error, type: "token-login-exception" });
} finally {
setDone(true);
}
}, [secretToken, url, router, internalError]);
useEffect(() => {
if (!error && !done && !loginAttempted.current) {
loginAttempted.current = true;
void login();
}
}, [error, login, secretToken, done]);
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm">
{error ? "Error: " + error : done ? "Logged in" : "Logging in"}
</div>
</div>
);
};
49 changes: 49 additions & 0 deletions apps/website/app/utils/internalError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"use client";

import { useCallback } from "react";
import type { Properties } from "posthog-js";
import { usePostHog } from "posthog-js/react";

const NON_WORD = /\W+/g;
export const useInternalError = () => {
const posthog = usePostHog();
return useCallback(
({
error,
type,
context,
forceSendInDev = false,
}: {
error: unknown;
type?: string;
context?: Properties;
forceSendInDev?: boolean;
}): void => {
if (process.env.NODE_ENV === "development" && forceSendInDev !== true) {
console.error(error);
} else {
type = type || "Internal Error";
const slugType = type.replaceAll(NON_WORD, "-").toLowerCase();
context = {
type,
...(context || {}),
};

if (typeof error === "string") {
error = new Error(error);
} else if (!(error instanceof Error)) {
try {
const serialized = JSON.stringify(error);
error = new Error(serialized);
} catch {
error = new Error(typeof error);
}
}
posthog.captureException(error, { ...context, type: slugType });
}
},
[posthog],
);
};

export default useInternalError;
14 changes: 14 additions & 0 deletions apps/website/app/utils/supabase/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createBrowserClient } from "@supabase/ssr";
import type { Database } from "@repo/database/dbTypes";

export const createClient = () => {
if (
!process.env.NEXT_PUBLIC_SUPABASE_URL ||
!process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY
)
throw new Error("Configuration error: supabase variables not configured.");
return createBrowserClient<Database, "public">(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY,
);
};
13 changes: 13 additions & 0 deletions apps/website/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import { DOCS_REDIRECTS } from "./docsRouteMap";

config();

// expose supabase credentials to the client
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY =
process.env.SUPABASE_PUBLISHABLE_KEY;
process.env.NEXT_PUBLIC_SUPABASE_URL = process.env.SUPABASE_URL;

const withNextra = nextra({
contentDirBasePath: "/docs",
});
Expand All @@ -22,6 +27,14 @@ const nextConfig: NextConfig = {
"next-mdx-import-source-file": "./mdx-components.tsx",
},
},
async headers() {
return [
{
source: "/auth/token",
headers: [{ key: "Referrer-Policy", value: "no-referrer" }],
},
];
},
};

export default withNextra(nextConfig);
1 change: 1 addition & 0 deletions apps/website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@repo/ui": "workspace:*",
"@supabase/ssr": "catalog:",
"@supabase/supabase-js": "catalog:",
"@supabase/auth-js": "catalog:",
"clsx": "^2.1.1",
"gray-matter": "^4.0.3",
"lucide-react": "^0.540.0",
Expand Down
13 changes: 10 additions & 3 deletions packages/utils/src/execContext.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
const IS_DEV = process.env.NODE_ENV !== "production";

export const nextRoot = (): string => {
if (
process.env.NEXT_API_ROOT !== undefined &&
process.env.NEXT_API_ROOT !== ""
)
return process.env.NEXT_API_ROOT.split("/").slice(0, 3).join("/");
return IS_DEV ? "http://localhost:3000/" : "https://discoursegraphs.com/";
};

export const nextApiRoot = (): string => {
if (
process.env.NEXT_API_ROOT !== undefined &&
process.env.NEXT_API_ROOT !== ""
)
return process.env.NEXT_API_ROOT;
return IS_DEV
? "http://localhost:3000/api"
: "https://discoursegraphs.com/api";
return nextRoot() + "api";
};
Loading
Loading