-
Notifications
You must be signed in to change notification settings - Fork 6
ENG-1703 Allow to access a Web UI with a space's credentials #1005
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f54f975
5480707
df1ed55
d18db3b
0727c7f
130bf15
a4947a7
da3aee4
dbddf83
6cc8359
32efdb6
602e098
3ca5e64
4a5300b
3236347
cbdd73f
09299c8
91948fc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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(); | ||
|
|
@@ -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", | ||
| }); | ||
| /* 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"> | ||
|
|
@@ -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> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
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> | ||
| ); | ||
| }; | ||
| 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; |
| 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; | ||
|
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> | ||
| ); | ||
| }; | ||
| 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; |
| 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, | ||
| ); | ||
| }; |
| 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"; | ||
| }; |
Uh oh!
There was an error while loading. Please reload this page.