mirror of
https://github.com/kunkunsh/kunkun.git
synced 2025-04-04 14:46:42 +00:00
Feature: Deep Link + Supabase OAuth + open extension in store with deep link (#16)
* feat(auth): add deep link and supabase auth * fix(deep-link): fix some routing and reactive page rendering * feat: implement supabase auth with pkce auth flow
This commit is contained in:
parent
2c99f231f7
commit
605a7844f2
@ -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",
|
||||
|
@ -36,7 +36,7 @@ interface AppConfigAPI {
|
||||
}
|
||||
|
||||
function createAppConfig(): Writable<AppConfig> & AppConfigAPI {
|
||||
const { subscribe, update, set } = writable<AppConfig>(defaultAppConfig)
|
||||
const store = writable<AppConfig>(defaultAppConfig)
|
||||
|
||||
async function init() {
|
||||
debug("Initializing app config")
|
||||
@ -46,7 +46,7 @@ function createAppConfig(): Writable<AppConfig> & 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<AppConfig> & 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<AppConfig> & 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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<AppState> & AppStateAPI {
|
||||
const store = writable<AppState>(defaultAppState)
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
update: store.update,
|
||||
set: store.set,
|
||||
...store,
|
||||
get: () => get(store),
|
||||
clearSearchTerm: () => {
|
||||
store.update((state) => ({ ...state, searchTerm: "" }))
|
||||
|
47
apps/desktop/src/lib/stores/auth.ts
Normal file
47
apps/desktop/src/lib/stores/auth.ts
Normal file
@ -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<void>
|
||||
signOut: () => Promise<{ error: AuthError | null }>
|
||||
signInExchange: (code: string) => Promise<{ error: AuthError | null }>
|
||||
}
|
||||
|
||||
function createAuth(): Writable<State> & AuthAPI {
|
||||
const store = writable<State>({ 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()
|
@ -16,11 +16,11 @@ function createExtensionsStore(): Writable<ExtPackageJsonExtra[]> & {
|
||||
uninstallStoreExtensionByIdentifier: (identifier: string) => Promise<ExtPackageJsonExtra>
|
||||
upgradeStoreExtension: (identifier: string, tarballUrl: string) => Promise<ExtPackageJsonExtra>
|
||||
} {
|
||||
const { subscribe, update, set } = writable<ExtPackageJsonExtra[]>([])
|
||||
const store = writable<ExtPackageJsonExtra[]>([])
|
||||
|
||||
function init() {
|
||||
return extAPI.loadAllExtensionsFromDb().then((exts) => {
|
||||
set(exts)
|
||||
store.set(exts)
|
||||
})
|
||||
}
|
||||
|
||||
@ -43,7 +43,7 @@ function createExtensionsStore(): Writable<ExtPackageJsonExtra[]> & {
|
||||
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<ExtPackageJsonExtra[]> & {
|
||||
|
||||
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<ExtPackageJsonExtra[]> & {
|
||||
}
|
||||
|
||||
return {
|
||||
...store,
|
||||
init,
|
||||
getExtensionsFromStore,
|
||||
findStoreExtensionByIdentifier,
|
||||
registerNewExtensionByPath,
|
||||
installFromTarballUrl,
|
||||
uninstallStoreExtensionByIdentifier,
|
||||
upgradeStoreExtension,
|
||||
subscribe,
|
||||
update,
|
||||
set
|
||||
upgradeStoreExtension
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,3 +2,4 @@ export * from "./appConfig"
|
||||
export * from "./appState"
|
||||
export * from "./winExtMap"
|
||||
export * from "./extensions"
|
||||
export * from "./auth"
|
||||
|
@ -40,6 +40,7 @@ function createWinExtMapStore(): Writable<WinExtMap> & API {
|
||||
async function init() {}
|
||||
|
||||
return {
|
||||
...store,
|
||||
init,
|
||||
registerExtensionWithWindow: async ({
|
||||
extPath,
|
||||
@ -114,10 +115,7 @@ function createWinExtMapStore(): Writable<WinExtMap> & API {
|
||||
return unregisterExtensionSpawnedProcess(windowLabel, pid).then(() => {
|
||||
ext.pids = ext.pids.filter((p) => p !== pid)
|
||||
})
|
||||
},
|
||||
subscribe: store.subscribe,
|
||||
update: store.update,
|
||||
set: store.set
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
115
apps/desktop/src/lib/utils/deeplink.ts
Normal file
115
apps/desktop/src/lib/utils/deeplink.ts
Normal file
@ -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<UnlistenFn> {
|
||||
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
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { goHome } from "@/utils/route"
|
||||
import { Error, Layouts } from "@kksh/ui"
|
||||
import { page } from "$app/stores"
|
||||
|
||||
@ -11,12 +12,12 @@
|
||||
|
||||
<svelte:window on:keydown={handleKeyDown} />
|
||||
|
||||
<Layouts.Center class="h-screen">
|
||||
<Layouts.Center class="min-h-screen py-5">
|
||||
<Error.RawErrorJSONPreset
|
||||
title="Unknown Error"
|
||||
class="w-fit max-w-screen-sm"
|
||||
title="Error"
|
||||
class="w-fit max-w-screen-sm border-2 border-red-500"
|
||||
message={$page.error?.message ?? "Unknown Error"}
|
||||
onnGoBack={() => window.history.back()}
|
||||
onGoBack={goHome}
|
||||
rawJsonError={JSON.stringify($page, null, 2)}
|
||||
/>
|
||||
</Layouts.Center>
|
||||
|
@ -2,6 +2,7 @@
|
||||
import AppContext from "@/components/context/AppContext.svelte"
|
||||
import "../app.css"
|
||||
import { appConfig, appState, extensions } from "@/stores"
|
||||
import { initDeeplink } from "@/utils/deeplink"
|
||||
import { isInMainWindow } from "@/utils/window"
|
||||
import {
|
||||
ModeWatcher,
|
||||
@ -21,6 +22,7 @@
|
||||
|
||||
onMount(async () => {
|
||||
unlisteners.push(await attachConsole())
|
||||
unlisteners.push(await initDeeplink())
|
||||
appConfig.init()
|
||||
if (isInMainWindow()) {
|
||||
extensions.init()
|
||||
|
62
apps/desktop/src/routes/auth/+page.svelte
Normal file
62
apps/desktop/src/routes/auth/+page.svelte
Normal file
@ -0,0 +1,62 @@
|
||||
<script lang="ts">
|
||||
import { auth } from "@/stores"
|
||||
import { supabase } from "@/supabase"
|
||||
import { goBackOnEscape } from "@/utils/key"
|
||||
import { goBack, goHome } from "@/utils/route"
|
||||
import Icon from "@iconify/svelte"
|
||||
import { DEEP_LINK_PATH_AUTH_CONFIRM } from "@kksh/api"
|
||||
import { Button, Card } from "@kksh/svelte5"
|
||||
import { Layouts } from "@kksh/ui"
|
||||
import { goto } from "$app/navigation"
|
||||
import { ArrowLeft } from "lucide-svelte"
|
||||
import { onMount } from "svelte"
|
||||
import { toast } from "svelte-sonner"
|
||||
import { open } from "tauri-plugin-shellx-api"
|
||||
|
||||
const redirectTo = DEEP_LINK_PATH_AUTH_CONFIRM
|
||||
|
||||
const signInWithOAuth = async (provider: "github" | "google") => {
|
||||
console.log(`Login with ${provider} redirecting to ${redirectTo}`)
|
||||
const { error, data } = await supabase.auth.signInWithOAuth({
|
||||
provider,
|
||||
options: {
|
||||
redirectTo,
|
||||
skipBrowserRedirect: true
|
||||
}
|
||||
})
|
||||
if (error) {
|
||||
toast.error("Failed to sign in with OAuth", { description: error.message })
|
||||
} else {
|
||||
data.url && open(data.url)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if ($auth.session) {
|
||||
toast.success("Already Signed In")
|
||||
goHome()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={goBackOnEscape} />
|
||||
<Button variant="outline" size="icon" onclick={goBack} class="absolute left-2 top-2 z-50">
|
||||
<ArrowLeft class="size-4" />
|
||||
</Button>
|
||||
<div class="absolute h-10 w-full" data-tauri-drag-region></div>
|
||||
<Layouts.Center class="h-screen w-screen" data-tauri-drag-region>
|
||||
<Card.Root class="w-80">
|
||||
<Card.Header class="flex flex-col items-center">
|
||||
<img src="/favicon.png" alt="Kunkun" class="h-12 w-12 invert" />
|
||||
<Card.Title class="text-xl">Sign In</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content class="flex flex-col gap-2">
|
||||
<Button variant="outline" size="lg" class="w-full" onclick={() => signInWithOAuth("github")}>
|
||||
<Icon icon="fa6-brands:github" class="h-5 w-5" />
|
||||
</Button>
|
||||
<Button variant="outline" size="lg" class="w-full" onclick={() => signInWithOAuth("google")}>
|
||||
<Icon icon="logos:google-icon" class="h-5 w-5" />
|
||||
</Button>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Layouts.Center>
|
84
apps/desktop/src/routes/auth/confirm/+page.svelte
Normal file
84
apps/desktop/src/routes/auth/confirm/+page.svelte
Normal file
@ -0,0 +1,84 @@
|
||||
<script lang="ts">
|
||||
import { auth } from "@/stores"
|
||||
import { supabase } from "@/supabase"
|
||||
import { goHomeOnEscape } from "@/utils/key"
|
||||
import { goBack, goHome } from "@/utils/route"
|
||||
import { Avatar, Button } from "@kksh/svelte5"
|
||||
import { goto } from "$app/navigation"
|
||||
import { ArrowLeft } from "lucide-svelte"
|
||||
import { onMount } from "svelte"
|
||||
import { toast } from "svelte-sonner"
|
||||
|
||||
const { data } = $props()
|
||||
|
||||
async function authExchange() {
|
||||
if (data.code) {
|
||||
auth.signInExchange(data.code).then((res) => {
|
||||
if (res.error) {
|
||||
toast.error("Failed to sign in", { description: res.error.message })
|
||||
} else {
|
||||
toast.success("Signed In")
|
||||
}
|
||||
})
|
||||
} else {
|
||||
toast.error("No code found")
|
||||
}
|
||||
}
|
||||
|
||||
const avatarFallback = $derived.by(() => {
|
||||
if (!$auth.session) return "?"
|
||||
const nameSplit = $auth.session.user.user_metadata.name.split(" ").filter(Boolean)
|
||||
if (nameSplit.length > 1) {
|
||||
return nameSplit[0][0] + nameSplit.at(-1)[0]
|
||||
} else if (nameSplit.length === 1) {
|
||||
return nameSplit[0][0]
|
||||
} else {
|
||||
return "?"
|
||||
}
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
authExchange()
|
||||
})
|
||||
|
||||
function onSignOut() {
|
||||
auth
|
||||
.signOut()
|
||||
.then(() => goto("/auth"))
|
||||
.catch((err) => toast.error("Failed to sign out", { description: err.message }))
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={goHomeOnEscape} />
|
||||
<Button
|
||||
class="absolute left-2 top-2 z-50"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onclick={() => {
|
||||
console.log("go Home")
|
||||
goto("/")
|
||||
}}
|
||||
>
|
||||
<ArrowLeft class="size-4" />
|
||||
</Button>
|
||||
<div class="h-10 w-full" data-tauri-drag-region></div>
|
||||
<main class="container pt-10">
|
||||
<div class="flex grow items-center justify-center pt-16">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
{#if $auth.session}
|
||||
<span class="font-mono text-4xl font-bold">Welcome, You are Logged In</span>
|
||||
{:else}
|
||||
<span class="font-mono text-4xl font-bold">You Are Not Logged In</span>
|
||||
{/if}
|
||||
<span class="flex flex-col items-center gap-5 text-xl">
|
||||
{#if $auth.session}
|
||||
<Avatar.Root class="h-32 w-32 border">
|
||||
<Avatar.Image src={$auth.session?.user.user_metadata.avatar_url} alt="avatar" />
|
||||
<Avatar.Fallback>{avatarFallback}</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
{/if}
|
||||
<Button variant="outline" onclick={onSignOut}>Sign Out</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
10
apps/desktop/src/routes/auth/confirm/+page.ts
Normal file
10
apps/desktop/src/routes/auth/confirm/+page.ts
Normal file
@ -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 }
|
||||
}
|
@ -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 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={goBackOnEscapeClearSearchTerm} />
|
||||
<svelte:window on:keydown={goHomeOnEscapeClearSearchTerm} />
|
||||
{#snippet leftSlot()}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onclick={goBack}
|
||||
onclick={goHome}
|
||||
class={Constants.CLASSNAMES.BACK_BUTTON}
|
||||
data-flip-id={Constants.CLASSNAMES.BACK_BUTTON}
|
||||
>
|
||||
|
@ -12,12 +12,12 @@
|
||||
|
||||
<svelte:window on:keydown={handleKeyDown} />
|
||||
|
||||
<Layouts.Center class="h-screen">
|
||||
<Layouts.Center class="min-h-screen py-5">
|
||||
<Error.RawErrorJSONPreset
|
||||
title="Fail to Load Extension"
|
||||
class="w-fit max-w-screen-sm"
|
||||
class="w-fit max-w-screen-sm border-2 border-red-500"
|
||||
message={$page.error?.message ?? "Unknown Error"}
|
||||
onnGoBack={() => goto("/")}
|
||||
onGoBack={() => goto("/")}
|
||||
rawJsonError={JSON.stringify($page, null, 2)}
|
||||
/>
|
||||
</Layouts.Center>
|
||||
|
@ -9,13 +9,16 @@
|
||||
import { StoreExtDetail } from "@kksh/ui/extension"
|
||||
import { greaterThan, parse as parseSemver } from "@std/semver"
|
||||
import { error } from "@tauri-apps/plugin-log"
|
||||
import { goto } from "$app/navigation"
|
||||
import { ArrowLeftIcon } from "lucide-svelte"
|
||||
import { onMount } from "svelte"
|
||||
import { toast } from "svelte-sonner"
|
||||
import { get, derived as storeDerived } from "svelte/store"
|
||||
|
||||
const { data } = $props()
|
||||
let { ext, manifest } = data
|
||||
// let { ext, manifest } = data
|
||||
const ext = $derived(data.ext)
|
||||
const manifest = $derived(data.manifest)
|
||||
const installedExt = storeDerived(installedStoreExts, ($e) => {
|
||||
return $e.find((e) => e.kunkun.identifier === ext.identifier)
|
||||
})
|
||||
@ -133,7 +136,7 @@
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
if (!delayedImageDialogOpen) {
|
||||
goBack()
|
||||
goto("/extension/store")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -145,7 +148,7 @@
|
||||
size="icon"
|
||||
class={cn("fixed left-3 top-3", Constants.CLASSNAMES.BACK_BUTTON)}
|
||||
data-flip-id={Constants.CLASSNAMES.BACK_BUTTON}
|
||||
onclick={goBack}
|
||||
onclick={() => goto("/extension/store")}
|
||||
>
|
||||
<ArrowLeftIcon />
|
||||
</Button>
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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<Database>(
|
||||
const supabase = createSB(
|
||||
parse(string(), process.env.SUPABASE_URL),
|
||||
parse(string(), process.env.SUPABASE_SERVICE_ROLE_KEY)
|
||||
)
|
||||
|
@ -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<Database>(supabaseUrl, supabaseAnonKey)
|
||||
return createClient<Database>(supabaseUrl, supabaseAnonKey, {
|
||||
auth: {
|
||||
flowType: "pkce"
|
||||
}
|
||||
})
|
||||
}
|
||||
export { SupabaseAPI } from "./api"
|
||||
|
||||
|
@ -1,22 +1,25 @@
|
||||
<script lang="ts">
|
||||
import autoAnimate from "@formkit/auto-animate"
|
||||
import Icon from "@iconify/svelte"
|
||||
import { Button, ScrollArea } from "@kksh/svelte5"
|
||||
import { Button, buttonVariants, Collapsible, ScrollArea } from "@kksh/svelte5"
|
||||
import { Error, Layouts, Shiki } from "@kksh/ui"
|
||||
import { ChevronsUpDown } from "lucide-svelte"
|
||||
import { type Snippet } from "svelte"
|
||||
import { fade, slide } from "svelte/transition"
|
||||
|
||||
const {
|
||||
title,
|
||||
message,
|
||||
class: className,
|
||||
rawJsonError,
|
||||
onnGoBack,
|
||||
onGoBack,
|
||||
footer
|
||||
}: {
|
||||
title: string
|
||||
message: string
|
||||
class?: string
|
||||
rawJsonError: string
|
||||
onnGoBack?: () => void
|
||||
onGoBack?: () => void
|
||||
footer?: Snippet
|
||||
} = $props()
|
||||
|
||||
@ -38,15 +41,27 @@
|
||||
<svelte:window on:keydown={handleKeyDown} on:keyup={handleKeyUp} />
|
||||
|
||||
<Error.General {title} {message} class={className}>
|
||||
<ScrollArea class="" orientation="both">
|
||||
<Shiki class="" code={rawJsonError} lang="json" />
|
||||
</ScrollArea>
|
||||
<Collapsible.Root class="w-full space-y-2">
|
||||
<div class="flex items-center justify-between space-x-4 px-4">
|
||||
<h4 class="text-sm font-semibold">Raw Error JSON</h4>
|
||||
<Collapsible.Trigger
|
||||
class={buttonVariants({ variant: "ghost", size: "sm", class: "w-9 p-0" })}
|
||||
>
|
||||
<ChevronsUpDown class="size-4" />
|
||||
</Collapsible.Trigger>
|
||||
</div>
|
||||
<Collapsible.Content class="space-y-2">
|
||||
<ScrollArea class="h-64" orientation="both">
|
||||
<Shiki class="" code={rawJsonError} lang="json" />
|
||||
</ScrollArea>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
<br />
|
||||
{#snippet footer()}
|
||||
{#if footer}
|
||||
{@render footer()}
|
||||
{:else}
|
||||
<Button variant="default" class="w-full" onclick={onnGoBack} disabled={enterDown}>
|
||||
<Button variant="default" class="w-full" onclick={onGoBack} disabled={enterDown}>
|
||||
Go Back
|
||||
<Icon icon="mi:enter" />
|
||||
</Button>
|
||||
|
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@ -415,9 +415,6 @@ importers:
|
||||
'@kksh/supabase':
|
||||
specifier: workspace:*
|
||||
version: link:../supabase
|
||||
'@supabase/supabase-js':
|
||||
specifier: ^2.43.4
|
||||
version: 2.46.1
|
||||
typescript:
|
||||
specifier: ^5.0.0
|
||||
version: 5.5.4
|
||||
@ -431,9 +428,6 @@ importers:
|
||||
'@types/bun':
|
||||
specifier: latest
|
||||
version: 1.1.13
|
||||
supabase:
|
||||
specifier: '>=1.8.1'
|
||||
version: 1.207.9
|
||||
|
||||
packages/supabase:
|
||||
dependencies:
|
||||
|
Loading…
x
Reference in New Issue
Block a user