|
| 1 | +import type { Enums } from "@repo/database/dbTypes"; |
| 2 | +import type { DGSupabaseClient } from "@repo/database/lib/client"; |
| 3 | +import { |
| 4 | + fetchOrCreateSpaceDirect, |
| 5 | + fetchOrCreatePlatformAccount, |
| 6 | + createLoggedInClient, |
| 7 | +} from "@repo/database/lib/contextFunctions"; |
| 8 | +import type DiscourseGraphPlugin from "~/index"; |
| 9 | + |
| 10 | +type Platform = Enums<"Platform">; |
| 11 | + |
| 12 | +export type SupabaseContext = { |
| 13 | + platform: Platform; |
| 14 | + spaceId: number; |
| 15 | + userId: number; |
| 16 | + spacePassword: string; |
| 17 | +}; |
| 18 | + |
| 19 | +let contextCache: SupabaseContext | null = null; |
| 20 | + |
| 21 | +const generateAccountLocalId = (vaultName: string): string => { |
| 22 | + const randomSuffix = Math.random().toString(36).substring(2, 8).toUpperCase(); |
| 23 | + const sanitizedVaultName = vaultName |
| 24 | + .replace(/\s+/g, "") |
| 25 | + .replace(/[^a-zA-Z0-9]/g, "") |
| 26 | + .replace(/-+/g, "-"); |
| 27 | + return `${sanitizedVaultName}${randomSuffix}`; |
| 28 | +}; |
| 29 | + |
| 30 | +const getOrCreateSpacePassword = async ( |
| 31 | + plugin: DiscourseGraphPlugin, |
| 32 | +): Promise<string> => { |
| 33 | + if (plugin.settings.spacePassword) { |
| 34 | + return plugin.settings.spacePassword; |
| 35 | + } |
| 36 | + const password = crypto.randomUUID(); |
| 37 | + plugin.settings.spacePassword = password; |
| 38 | + await plugin.saveSettings(); |
| 39 | + return password; |
| 40 | +}; |
| 41 | + |
| 42 | +const getOrCreateAccountLocalId = async ( |
| 43 | + plugin: DiscourseGraphPlugin, |
| 44 | + vaultName: string, |
| 45 | +): Promise<string> => { |
| 46 | + if (plugin.settings.accountLocalId) { |
| 47 | + return plugin.settings.accountLocalId; |
| 48 | + } |
| 49 | + const accountLocalId = generateAccountLocalId(vaultName); |
| 50 | + plugin.settings.accountLocalId = accountLocalId; |
| 51 | + await plugin.saveSettings(); |
| 52 | + return accountLocalId; |
| 53 | +}; |
| 54 | + |
| 55 | +/** |
| 56 | + * Gets the unique vault ID from Obsidian's internal API. |
| 57 | + * @see https://help.obsidian.md/Extending+Obsidian/Obsidian+URI |
| 58 | + */ |
| 59 | +const getVaultId = (app: DiscourseGraphPlugin["app"]): string => { |
| 60 | + return (app as unknown as { appId: string }).appId; |
| 61 | +}; |
| 62 | + |
| 63 | +const canonicalObsidianUrl = (vaultId: string): string => { |
| 64 | + return `obsidian:${vaultId}`; |
| 65 | +}; |
| 66 | + |
| 67 | +export const getSupabaseContext = async ( |
| 68 | + plugin: DiscourseGraphPlugin, |
| 69 | +): Promise<SupabaseContext | null> => { |
| 70 | + if (contextCache === null) { |
| 71 | + try { |
| 72 | + const vaultName = plugin.app.vault.getName() || "obsidian-vault"; |
| 73 | + const vaultId = getVaultId(plugin.app); |
| 74 | + |
| 75 | + const spacePassword = await getOrCreateSpacePassword(plugin); |
| 76 | + const accountLocalId = await getOrCreateAccountLocalId(plugin, vaultName); |
| 77 | + |
| 78 | + const url = canonicalObsidianUrl(vaultId); |
| 79 | + const platform: Platform = "Obsidian"; |
| 80 | + |
| 81 | + const spaceResult = await fetchOrCreateSpaceDirect({ |
| 82 | + password: spacePassword, |
| 83 | + url, |
| 84 | + name: vaultName, |
| 85 | + platform, |
| 86 | + }); |
| 87 | + |
| 88 | + if (!spaceResult.data) { |
| 89 | + console.error("Failed to create space"); |
| 90 | + return null; |
| 91 | + } |
| 92 | + |
| 93 | + const spaceId = spaceResult.data.id; |
| 94 | + const userId = await fetchOrCreatePlatformAccount({ |
| 95 | + platform: "Obsidian", |
| 96 | + accountLocalId, |
| 97 | + name: vaultName, |
| 98 | + email: accountLocalId, |
| 99 | + spaceId, |
| 100 | + password: spacePassword, |
| 101 | + }); |
| 102 | + |
| 103 | + contextCache = { |
| 104 | + platform: "Obsidian", |
| 105 | + spaceId, |
| 106 | + userId, |
| 107 | + spacePassword, |
| 108 | + }; |
| 109 | + } catch (error) { |
| 110 | + console.error(error); |
| 111 | + return null; |
| 112 | + } |
| 113 | + } |
| 114 | + return contextCache; |
| 115 | +}; |
| 116 | + |
| 117 | +let loggedInClient: DGSupabaseClient | null = null; |
| 118 | + |
| 119 | +export const getLoggedInClient = async ( |
| 120 | + plugin: DiscourseGraphPlugin, |
| 121 | +): Promise<DGSupabaseClient | null> => { |
| 122 | + if (loggedInClient === null) { |
| 123 | + const context = await getSupabaseContext(plugin); |
| 124 | + if (context === null) { |
| 125 | + throw new Error("Could not create Supabase context"); |
| 126 | + } |
| 127 | + try { |
| 128 | + loggedInClient = await createLoggedInClient({ |
| 129 | + platform: context.platform, |
| 130 | + spaceId: context.spaceId, |
| 131 | + password: context.spacePassword, |
| 132 | + }); |
| 133 | + if (!loggedInClient) { |
| 134 | + throw new Error( |
| 135 | + "Failed to create Supabase client - check environment variables", |
| 136 | + ); |
| 137 | + } |
| 138 | + } catch (error) { |
| 139 | + const errorMessage = |
| 140 | + error instanceof Error ? error.message : String(error); |
| 141 | + console.error("Failed to create logged-in client:", errorMessage); |
| 142 | + throw new Error(`Supabase authentication failed: ${errorMessage}`); |
| 143 | + } |
| 144 | + } else { |
| 145 | + // renew session |
| 146 | + const { error } = await loggedInClient.auth.getSession(); |
| 147 | + if (error) { |
| 148 | + console.warn("Session renewal failed, re-authenticating:", error); |
| 149 | + loggedInClient = null; |
| 150 | + const context = await getSupabaseContext(plugin); |
| 151 | + if (context === null) { |
| 152 | + throw new Error( |
| 153 | + "Could not create Supabase context for re-authentication", |
| 154 | + ); |
| 155 | + } |
| 156 | + |
| 157 | + loggedInClient = await createLoggedInClient({ |
| 158 | + platform: context.platform, |
| 159 | + spaceId: context.spaceId, |
| 160 | + password: context.spacePassword, |
| 161 | + }); |
| 162 | + if (!loggedInClient) { |
| 163 | + throw new Error("Failed to re-authenticate Supabase client"); |
| 164 | + } |
| 165 | + } |
| 166 | + } |
| 167 | + return loggedInClient; |
| 168 | +}; |
0 commit comments