diff --git a/apps/desktop/src/lib/cmds/builtin.ts b/apps/desktop/src/lib/cmds/builtin.ts index 8d81220..0271c36 100644 --- a/apps/desktop/src/lib/cmds/builtin.ts +++ b/apps/desktop/src/lib/cmds/builtin.ts @@ -1,10 +1,9 @@ -import { appConfig, appState } from "@/stores" +import { appConfig, appState, auth } from "@/stores" import { checkUpdateAndInstall } from "@/utils/updater" import type { BuiltinCmd } from "@kksh/ui/types" import { getVersion } from "@tauri-apps/api/app" import { WebviewWindow } from "@tauri-apps/api/webviewWindow" import { exit } from "@tauri-apps/plugin-process" -import { dev } from "$app/environment" import { goto } from "$app/navigation" import { toast } from "svelte-sonner" import { v4 as uuidv4 } from "uuid" @@ -19,23 +18,25 @@ export const builtinCmds: BuiltinCmd[] = [ goto("/extension/store") } }, - // { - // name: "Sign In", - // iconifyIcon: "mdi:login-variant", - // description: "", - // function: async () => { - // goto("/auth") - // } - // }, - // { - // name: "Sign Out", - // iconifyIcon: "mdi:logout-variant", - // description: "", - // function: async () => { - // const supabase = useSupabaseClient() - // supabase.auth.signOut() - // } - // }, + { + name: "Sign In", + iconifyIcon: "mdi:login-variant", + description: "", + function: async () => { + goto("/auth") + } + }, + { + name: "Sign Out", + iconifyIcon: "mdi:logout-variant", + description: "", + function: async () => { + auth + .signOut() + .then(() => toast.success("Signed out")) + .catch((err) => toast.error("Failed to sign out: ", { description: err.message })) + } + }, { name: "Show Draggable Area", iconifyIcon: "mingcute:move-fill", diff --git a/apps/desktop/src/lib/stores/appConfig.ts b/apps/desktop/src/lib/stores/appConfig.ts index 5467e64..1eecad1 100644 --- a/apps/desktop/src/lib/stores/appConfig.ts +++ b/apps/desktop/src/lib/stores/appConfig.ts @@ -36,7 +36,7 @@ interface AppConfigAPI { } function createAppConfig(): Writable & AppConfigAPI { - const { subscribe, update, set } = writable(defaultAppConfig) + const store = writable(defaultAppConfig) async function init() { debug("Initializing app config") @@ -46,7 +46,7 @@ function createAppConfig(): Writable & AppConfigAPI { if (parseRes.success) { console.log("Parse Persisted App Config Success", parseRes.output) const extensionsInstallDir = await getExtensionsFolder() - update((config) => ({ + store.update((config) => ({ ...config, ...parseRes.output, isInitialized: true, @@ -60,7 +60,7 @@ function createAppConfig(): Writable & AppConfigAPI { await persistStore.set("config", v.parse(PersistedAppConfig, defaultAppConfig)) } - subscribe(async (config) => { + store.subscribe(async (config) => { console.log("Saving app config", config) await persistStore.set("config", config) updateTheme(config.theme) @@ -68,15 +68,13 @@ function createAppConfig(): Writable & AppConfigAPI { } return { - setTheme: (theme: ThemeConfig) => update((config) => ({ ...config, theme })), + ...store, + setTheme: (theme: ThemeConfig) => store.update((config) => ({ ...config, theme })), setDevExtensionPath: (devExtensionPath: string | null) => { console.log("setDevExtensionPath", devExtensionPath) - update((config) => ({ ...config, devExtensionPath })) + store.update((config) => ({ ...config, devExtensionPath })) }, - init, - subscribe, - update, - set + init } } diff --git a/apps/desktop/src/lib/stores/appState.ts b/apps/desktop/src/lib/stores/appState.ts index aaaf45d..041a40e 100644 --- a/apps/desktop/src/lib/stores/appState.ts +++ b/apps/desktop/src/lib/stores/appState.ts @@ -1,4 +1,4 @@ -import type { AppState } from "@/types" +import type { AppState } from "@kksh/types" import { get, writable, type Writable } from "svelte/store" export const defaultAppState: AppState = { @@ -15,9 +15,7 @@ function createAppState(): Writable & AppStateAPI { const store = writable(defaultAppState) return { - subscribe: store.subscribe, - update: store.update, - set: store.set, + ...store, get: () => get(store), clearSearchTerm: () => { store.update((state) => ({ ...state, searchTerm: "" })) diff --git a/apps/desktop/src/lib/stores/auth.ts b/apps/desktop/src/lib/stores/auth.ts new file mode 100644 index 0000000..c88e3bd --- /dev/null +++ b/apps/desktop/src/lib/stores/auth.ts @@ -0,0 +1,47 @@ +import { supabase } from "@/supabase" +import type { AuthError, Session, User } from "@supabase/supabase-js" +import { get, writable, type Writable } from "svelte/store" + +type State = { session: Session | null; user: User | null } + +interface AuthAPI { + get: () => State + refresh: () => Promise + signOut: () => Promise<{ error: AuthError | null }> + signInExchange: (code: string) => Promise<{ error: AuthError | null }> +} + +function createAuth(): Writable & AuthAPI { + const store = writable({ session: null, user: null }) + async function refresh() { + const { + data: { session }, + error + } = await supabase.auth.getSession() + const { + data: { user } + } = await supabase.auth.getUser() + store.update((state) => ({ ...state, session, user })) + } + async function signOut() { + return supabase.auth.signOut().then((res) => { + refresh() + return res + }) + } + async function signInExchange(code: string) { + return supabase.auth.exchangeCodeForSession(code).then((res) => { + refresh() + return res + }) + } + return { + ...store, + get: () => get(store), + refresh, + signOut, + signInExchange + } +} + +export const auth = createAuth() diff --git a/apps/desktop/src/lib/stores/extensions.ts b/apps/desktop/src/lib/stores/extensions.ts index 0769078..62d949c 100644 --- a/apps/desktop/src/lib/stores/extensions.ts +++ b/apps/desktop/src/lib/stores/extensions.ts @@ -16,11 +16,11 @@ function createExtensionsStore(): Writable & { uninstallStoreExtensionByIdentifier: (identifier: string) => Promise upgradeStoreExtension: (identifier: string, tarballUrl: string) => Promise } { - const { subscribe, update, set } = writable([]) + const store = writable([]) function init() { return extAPI.loadAllExtensionsFromDb().then((exts) => { - set(exts) + store.set(exts) }) } @@ -43,7 +43,7 @@ function createExtensionsStore(): Writable & { return extAPI .loadExtensionManifestFromDisk(await path.join(extPath, "package.json")) .then((ext) => { - update((exts) => { + store.update((exts) => { const existingExt = exts.find((e) => e.extPath === ext.extPath) if (existingExt) return exts return [...exts, ext] @@ -69,7 +69,7 @@ function createExtensionsStore(): Writable & { return extAPI .uninstallExtensionByPath(targetPath) - .then(() => update((exts) => exts.filter((ext) => ext.extPath !== targetExt.extPath))) + .then(() => store.update((exts) => exts.filter((ext) => ext.extPath !== targetExt.extPath))) .then(() => targetExt) } @@ -91,16 +91,14 @@ function createExtensionsStore(): Writable & { } return { + ...store, init, getExtensionsFromStore, findStoreExtensionByIdentifier, registerNewExtensionByPath, installFromTarballUrl, uninstallStoreExtensionByIdentifier, - upgradeStoreExtension, - subscribe, - update, - set + upgradeStoreExtension } } diff --git a/apps/desktop/src/lib/stores/index.ts b/apps/desktop/src/lib/stores/index.ts index daaa348..dec33b0 100644 --- a/apps/desktop/src/lib/stores/index.ts +++ b/apps/desktop/src/lib/stores/index.ts @@ -2,3 +2,4 @@ export * from "./appConfig" export * from "./appState" export * from "./winExtMap" export * from "./extensions" +export * from "./auth" diff --git a/apps/desktop/src/lib/stores/winExtMap.ts b/apps/desktop/src/lib/stores/winExtMap.ts index 62f67c7..2bea26a 100644 --- a/apps/desktop/src/lib/stores/winExtMap.ts +++ b/apps/desktop/src/lib/stores/winExtMap.ts @@ -40,6 +40,7 @@ function createWinExtMapStore(): Writable & API { async function init() {} return { + ...store, init, registerExtensionWithWindow: async ({ extPath, @@ -114,10 +115,7 @@ function createWinExtMapStore(): Writable & API { return unregisterExtensionSpawnedProcess(windowLabel, pid).then(() => { ext.pids = ext.pids.filter((p) => p !== pid) }) - }, - subscribe: store.subscribe, - update: store.update, - set: store.set + } } } diff --git a/apps/desktop/src/lib/utils/deeplink.ts b/apps/desktop/src/lib/utils/deeplink.ts new file mode 100644 index 0000000..11bcc7f --- /dev/null +++ b/apps/desktop/src/lib/utils/deeplink.ts @@ -0,0 +1,115 @@ +import { emitRefreshDevExt } from "@/utils/tauri-events" +import { + DEEP_LINK_PATH_AUTH_CONFIRM, + DEEP_LINK_PATH_OPEN, + DEEP_LINK_PATH_REFRESH_DEV_EXTENSION, + DEEP_LINK_PATH_STORE +} from "@kksh/api" +import type { UnlistenFn } from "@tauri-apps/api/event" +import { extname } from "@tauri-apps/api/path" +import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow" +import * as deepLink from "@tauri-apps/plugin-deep-link" +import { error } from "@tauri-apps/plugin-log" +import { goto } from "$app/navigation" +import { toast } from "svelte-sonner" +import * as v from "valibot" +import { isInMainWindow } from "./window" + +const StorePathSearchParams = v.object({ + identifier: v.optional(v.string()) +}) + +export function initDeeplink(): Promise { + console.log("init deeplink") + if (!isInMainWindow()) { + return Promise.resolve(() => {}) + } + // deepLink.getCurrent() + return deepLink.onOpenUrl((urls) => { + console.log("deep link:", urls) + urls.forEach(handleDeepLink) + }) +} + +/** + * Show and focus on the main window + */ +function openMainWindow() { + const appWindow = getCurrentWebviewWindow() + return appWindow + .show() + .then(() => appWindow.setFocus()) + .catch((err) => { + console.error(err) + error(`Failed to show window upon deep link: ${err.message}`) + toast.error("Failed to show window upon deep link", { + description: err.message + }) + }) +} + +export async function handleKunkunProtocol(parsedUrl: URL) { + const params = Object.fromEntries(parsedUrl.searchParams) + const { host, pathname, href } = parsedUrl + if (href.startsWith(DEEP_LINK_PATH_OPEN)) { + openMainWindow() + } else if (href.startsWith(DEEP_LINK_PATH_STORE)) { + const parsed = v.parse(StorePathSearchParams, params) + openMainWindow() + if (parsed.identifier) { + goto(`/extension/store/${parsed.identifier}`) + } else { + goto("/extension/store") + } + } else if (href.startsWith(DEEP_LINK_PATH_REFRESH_DEV_EXTENSION)) { + emitRefreshDevExt() + } else if (href.startsWith(DEEP_LINK_PATH_AUTH_CONFIRM)) { + openMainWindow() + goto(`/auth/confirm?${parsedUrl.searchParams.toString()}`) + } else { + console.error("Invalid path:", pathname) + toast.error("Invalid path", { + description: parsedUrl.href + }) + } +} + +export async function handleFileProtocol(parsedUrl: URL) { + console.log("File protocol:", parsedUrl) + const filePath = parsedUrl.pathname // Remove the leading '//' kunkun://open?identifier=qrcode gives "open" + console.log("File path:", filePath) + // from file absolute path, get file extension + const fileExt = await extname(filePath) + console.log("File extension:", fileExt) + switch (fileExt) { + case "kunkun": + // TODO: Handle file protocol, install extension from file (essentially a .tgz file) + break + default: + console.error("Unknown file extension:", fileExt) + toast.error("Unknown file extension", { + description: fileExt + }) + break + } +} + +/** + * + * @param url Deep Link URl, e.g. kunkun://open + */ +export async function handleDeepLink(url: string) { + const parsedUrl = new URL(url) + switch (parsedUrl.protocol) { + case "kunkun:": + return handleKunkunProtocol(parsedUrl) + case "file:": + return handleFileProtocol(parsedUrl) + default: + console.error("Invalid Protocol:", parsedUrl.protocol) + toast.error("Invalid Protocol", { + description: parsedUrl.protocol + }) + break + } +} diff --git a/apps/desktop/src/lib/utils/key.ts b/apps/desktop/src/lib/utils/key.ts index 62f16f1..61e9527 100644 --- a/apps/desktop/src/lib/utils/key.ts +++ b/apps/desktop/src/lib/utils/key.ts @@ -23,3 +23,13 @@ export function goBackOnEscapeClearSearchTerm(e: KeyboardEvent) { } } } + +export function goHomeOnEscapeClearSearchTerm(e: KeyboardEvent) { + if (e.key === "Escape") { + if (appState.get().searchTerm) { + appState.clearSearchTerm() + } else { + goHome() + } + } +} diff --git a/apps/desktop/src/routes/+error.svelte b/apps/desktop/src/routes/+error.svelte index 91a5da3..a4ae1af 100644 --- a/apps/desktop/src/routes/+error.svelte +++ b/apps/desktop/src/routes/+error.svelte @@ -1,4 +1,5 @@ + + + +
+ + + + Kunkun + Sign In + + + + + + + diff --git a/apps/desktop/src/routes/auth/confirm/+page.svelte b/apps/desktop/src/routes/auth/confirm/+page.svelte new file mode 100644 index 0000000..78e2035 --- /dev/null +++ b/apps/desktop/src/routes/auth/confirm/+page.svelte @@ -0,0 +1,84 @@ + + + + +
+
+
+
+ {#if $auth.session} + Welcome, You are Logged In + {:else} + You Are Not Logged In + {/if} + + {#if $auth.session} + + + {avatarFallback} + + {/if} + + +
+
+
diff --git a/apps/desktop/src/routes/auth/confirm/+page.ts b/apps/desktop/src/routes/auth/confirm/+page.ts new file mode 100644 index 0000000..3418ef7 --- /dev/null +++ b/apps/desktop/src/routes/auth/confirm/+page.ts @@ -0,0 +1,10 @@ +import { error } from "@sveltejs/kit" +import type { PageLoad } from "./$types" + +export const load: PageLoad = async ({ params, url }) => { + const code = url.searchParams.get("code") + if (!code) { + throw error(400, "Auth Exchange Code is Required") + } + return { params, code } +} diff --git a/apps/desktop/src/routes/extension/store/+page.svelte b/apps/desktop/src/routes/extension/store/+page.svelte index bb11470..41f9c69 100644 --- a/apps/desktop/src/routes/extension/store/+page.svelte +++ b/apps/desktop/src/routes/extension/store/+page.svelte @@ -2,8 +2,8 @@ import { getExtensionsFolder } from "@/constants" import { appState, extensions } from "@/stores" import { supabaseAPI } from "@/supabase" - import { goBackOnEscapeClearSearchTerm } from "@/utils/key" - import { goBack } from "@/utils/route" + import { goBackOnEscapeClearSearchTerm, goHomeOnEscapeClearSearchTerm } from "@/utils/key" + import { goBack, goHome } from "@/utils/route" import { SBExt } from "@kksh/api/supabase" import { isUpgradable } from "@kksh/extension" import { Button, Command } from "@kksh/svelte5" @@ -64,12 +64,12 @@ } - + {#snippet leftSlot()} diff --git a/apps/desktop/src/routes/extension/store/[identifier]/+page.ts b/apps/desktop/src/routes/extension/store/[identifier]/+page.ts index c79c946..db46357 100644 --- a/apps/desktop/src/routes/extension/store/[identifier]/+page.ts +++ b/apps/desktop/src/routes/extension/store/[identifier]/+page.ts @@ -13,7 +13,12 @@ export const load: PageLoad = async ({ }): Promise<{ ext: Tables<"ext_publish"> manifest: KunkunExtManifest + params: { + identifier: string + } }> => { + console.log("store[identifier] params", params) + const { error: dbError, data: ext } = await supabaseAPI.getLatestExtPublish(params.identifier) if (dbError) { return error(400, { @@ -30,6 +35,7 @@ export const load: PageLoad = async ({ return { ext, + params, manifest: parseManifest.output } } diff --git a/packages/schema/package.json b/packages/schema/package.json index e3070c8..0b33136 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -15,8 +15,7 @@ }, "devDependencies": { "@gcornut/valibot-json-schema": "^0.42.0", - "@types/bun": "latest", - "supabase": ">=1.8.1" + "@types/bun": "latest" }, "peerDependencies": { "@kksh/supabase": "workspace:*", @@ -25,7 +24,6 @@ "dependencies": { "@aws-sdk/client-s3": "^3.583.0", "@kksh/api": "workspace:*", - "@supabase/supabase-js": "^2.43.4", "valibot": "^0.40.0" } } diff --git a/packages/schema/scripts/upload-schema-to-supabase.ts b/packages/schema/scripts/upload-schema-to-supabase.ts index f4bf48a..eddd837 100644 --- a/packages/schema/scripts/upload-schema-to-supabase.ts +++ b/packages/schema/scripts/upload-schema-to-supabase.ts @@ -1,10 +1,9 @@ import { ExtPackageJson } from "@kksh/api/models" -import { type Database } from "@kksh/supabase" -import { createClient } from "@supabase/supabase-js" +import { createSB } from "@kksh/supabase" import { parse, string } from "valibot" import { getJsonSchema } from "../src" -const supabase = createClient( +const supabase = createSB( parse(string(), process.env.SUPABASE_URL), parse(string(), process.env.SUPABASE_SERVICE_ROLE_KEY) ) diff --git a/packages/supabase/src/index.ts b/packages/supabase/src/index.ts index 9285d65..09aae5e 100644 --- a/packages/supabase/src/index.ts +++ b/packages/supabase/src/index.ts @@ -2,7 +2,11 @@ import type { Database } from "@kksh/api/supabase/types" import { createClient } from "@supabase/supabase-js" export function createSB(supabaseUrl: string, supabaseAnonKey: string) { - return createClient(supabaseUrl, supabaseAnonKey) + return createClient(supabaseUrl, supabaseAnonKey, { + auth: { + flowType: "pkce" + } + }) } export { SupabaseAPI } from "./api" diff --git a/packages/ui/src/components/error/raw-error-json-preset.svelte b/packages/ui/src/components/error/raw-error-json-preset.svelte index 8f13239..bfea64b 100644 --- a/packages/ui/src/components/error/raw-error-json-preset.svelte +++ b/packages/ui/src/components/error/raw-error-json-preset.svelte @@ -1,22 +1,25 @@