mirror of
https://github.com/kunkunsh/kunkun.git
synced 2025-04-11 17:29:44 +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 { checkUpdateAndInstall } from "@/utils/updater"
|
||||||
import type { BuiltinCmd } from "@kksh/ui/types"
|
import type { BuiltinCmd } from "@kksh/ui/types"
|
||||||
import { getVersion } from "@tauri-apps/api/app"
|
import { getVersion } from "@tauri-apps/api/app"
|
||||||
import { WebviewWindow } from "@tauri-apps/api/webviewWindow"
|
import { WebviewWindow } from "@tauri-apps/api/webviewWindow"
|
||||||
import { exit } from "@tauri-apps/plugin-process"
|
import { exit } from "@tauri-apps/plugin-process"
|
||||||
import { dev } from "$app/environment"
|
|
||||||
import { goto } from "$app/navigation"
|
import { goto } from "$app/navigation"
|
||||||
import { toast } from "svelte-sonner"
|
import { toast } from "svelte-sonner"
|
||||||
import { v4 as uuidv4 } from "uuid"
|
import { v4 as uuidv4 } from "uuid"
|
||||||
@ -19,23 +18,25 @@ export const builtinCmds: BuiltinCmd[] = [
|
|||||||
goto("/extension/store")
|
goto("/extension/store")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// name: "Sign In",
|
name: "Sign In",
|
||||||
// iconifyIcon: "mdi:login-variant",
|
iconifyIcon: "mdi:login-variant",
|
||||||
// description: "",
|
description: "",
|
||||||
// function: async () => {
|
function: async () => {
|
||||||
// goto("/auth")
|
goto("/auth")
|
||||||
// }
|
}
|
||||||
// },
|
},
|
||||||
// {
|
{
|
||||||
// name: "Sign Out",
|
name: "Sign Out",
|
||||||
// iconifyIcon: "mdi:logout-variant",
|
iconifyIcon: "mdi:logout-variant",
|
||||||
// description: "",
|
description: "",
|
||||||
// function: async () => {
|
function: async () => {
|
||||||
// const supabase = useSupabaseClient()
|
auth
|
||||||
// supabase.auth.signOut()
|
.signOut()
|
||||||
// }
|
.then(() => toast.success("Signed out"))
|
||||||
// },
|
.catch((err) => toast.error("Failed to sign out: ", { description: err.message }))
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Show Draggable Area",
|
name: "Show Draggable Area",
|
||||||
iconifyIcon: "mingcute:move-fill",
|
iconifyIcon: "mingcute:move-fill",
|
||||||
|
@ -36,7 +36,7 @@ interface AppConfigAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createAppConfig(): Writable<AppConfig> & AppConfigAPI {
|
function createAppConfig(): Writable<AppConfig> & AppConfigAPI {
|
||||||
const { subscribe, update, set } = writable<AppConfig>(defaultAppConfig)
|
const store = writable<AppConfig>(defaultAppConfig)
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
debug("Initializing app config")
|
debug("Initializing app config")
|
||||||
@ -46,7 +46,7 @@ function createAppConfig(): Writable<AppConfig> & AppConfigAPI {
|
|||||||
if (parseRes.success) {
|
if (parseRes.success) {
|
||||||
console.log("Parse Persisted App Config Success", parseRes.output)
|
console.log("Parse Persisted App Config Success", parseRes.output)
|
||||||
const extensionsInstallDir = await getExtensionsFolder()
|
const extensionsInstallDir = await getExtensionsFolder()
|
||||||
update((config) => ({
|
store.update((config) => ({
|
||||||
...config,
|
...config,
|
||||||
...parseRes.output,
|
...parseRes.output,
|
||||||
isInitialized: true,
|
isInitialized: true,
|
||||||
@ -60,7 +60,7 @@ function createAppConfig(): Writable<AppConfig> & AppConfigAPI {
|
|||||||
await persistStore.set("config", v.parse(PersistedAppConfig, defaultAppConfig))
|
await persistStore.set("config", v.parse(PersistedAppConfig, defaultAppConfig))
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(async (config) => {
|
store.subscribe(async (config) => {
|
||||||
console.log("Saving app config", config)
|
console.log("Saving app config", config)
|
||||||
await persistStore.set("config", config)
|
await persistStore.set("config", config)
|
||||||
updateTheme(config.theme)
|
updateTheme(config.theme)
|
||||||
@ -68,15 +68,13 @@ function createAppConfig(): Writable<AppConfig> & AppConfigAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
setTheme: (theme: ThemeConfig) => update((config) => ({ ...config, theme })),
|
...store,
|
||||||
|
setTheme: (theme: ThemeConfig) => store.update((config) => ({ ...config, theme })),
|
||||||
setDevExtensionPath: (devExtensionPath: string | null) => {
|
setDevExtensionPath: (devExtensionPath: string | null) => {
|
||||||
console.log("setDevExtensionPath", devExtensionPath)
|
console.log("setDevExtensionPath", devExtensionPath)
|
||||||
update((config) => ({ ...config, devExtensionPath }))
|
store.update((config) => ({ ...config, devExtensionPath }))
|
||||||
},
|
},
|
||||||
init,
|
init
|
||||||
subscribe,
|
|
||||||
update,
|
|
||||||
set
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { AppState } from "@/types"
|
import type { AppState } from "@kksh/types"
|
||||||
import { get, writable, type Writable } from "svelte/store"
|
import { get, writable, type Writable } from "svelte/store"
|
||||||
|
|
||||||
export const defaultAppState: AppState = {
|
export const defaultAppState: AppState = {
|
||||||
@ -15,9 +15,7 @@ function createAppState(): Writable<AppState> & AppStateAPI {
|
|||||||
const store = writable<AppState>(defaultAppState)
|
const store = writable<AppState>(defaultAppState)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe: store.subscribe,
|
...store,
|
||||||
update: store.update,
|
|
||||||
set: store.set,
|
|
||||||
get: () => get(store),
|
get: () => get(store),
|
||||||
clearSearchTerm: () => {
|
clearSearchTerm: () => {
|
||||||
store.update((state) => ({ ...state, searchTerm: "" }))
|
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>
|
uninstallStoreExtensionByIdentifier: (identifier: string) => Promise<ExtPackageJsonExtra>
|
||||||
upgradeStoreExtension: (identifier: string, tarballUrl: string) => Promise<ExtPackageJsonExtra>
|
upgradeStoreExtension: (identifier: string, tarballUrl: string) => Promise<ExtPackageJsonExtra>
|
||||||
} {
|
} {
|
||||||
const { subscribe, update, set } = writable<ExtPackageJsonExtra[]>([])
|
const store = writable<ExtPackageJsonExtra[]>([])
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
return extAPI.loadAllExtensionsFromDb().then((exts) => {
|
return extAPI.loadAllExtensionsFromDb().then((exts) => {
|
||||||
set(exts)
|
store.set(exts)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ function createExtensionsStore(): Writable<ExtPackageJsonExtra[]> & {
|
|||||||
return extAPI
|
return extAPI
|
||||||
.loadExtensionManifestFromDisk(await path.join(extPath, "package.json"))
|
.loadExtensionManifestFromDisk(await path.join(extPath, "package.json"))
|
||||||
.then((ext) => {
|
.then((ext) => {
|
||||||
update((exts) => {
|
store.update((exts) => {
|
||||||
const existingExt = exts.find((e) => e.extPath === ext.extPath)
|
const existingExt = exts.find((e) => e.extPath === ext.extPath)
|
||||||
if (existingExt) return exts
|
if (existingExt) return exts
|
||||||
return [...exts, ext]
|
return [...exts, ext]
|
||||||
@ -69,7 +69,7 @@ function createExtensionsStore(): Writable<ExtPackageJsonExtra[]> & {
|
|||||||
|
|
||||||
return extAPI
|
return extAPI
|
||||||
.uninstallExtensionByPath(targetPath)
|
.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)
|
.then(() => targetExt)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,16 +91,14 @@ function createExtensionsStore(): Writable<ExtPackageJsonExtra[]> & {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
...store,
|
||||||
init,
|
init,
|
||||||
getExtensionsFromStore,
|
getExtensionsFromStore,
|
||||||
findStoreExtensionByIdentifier,
|
findStoreExtensionByIdentifier,
|
||||||
registerNewExtensionByPath,
|
registerNewExtensionByPath,
|
||||||
installFromTarballUrl,
|
installFromTarballUrl,
|
||||||
uninstallStoreExtensionByIdentifier,
|
uninstallStoreExtensionByIdentifier,
|
||||||
upgradeStoreExtension,
|
upgradeStoreExtension
|
||||||
subscribe,
|
|
||||||
update,
|
|
||||||
set
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,3 +2,4 @@ export * from "./appConfig"
|
|||||||
export * from "./appState"
|
export * from "./appState"
|
||||||
export * from "./winExtMap"
|
export * from "./winExtMap"
|
||||||
export * from "./extensions"
|
export * from "./extensions"
|
||||||
|
export * from "./auth"
|
||||||
|
@ -40,6 +40,7 @@ function createWinExtMapStore(): Writable<WinExtMap> & API {
|
|||||||
async function init() {}
|
async function init() {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
...store,
|
||||||
init,
|
init,
|
||||||
registerExtensionWithWindow: async ({
|
registerExtensionWithWindow: async ({
|
||||||
extPath,
|
extPath,
|
||||||
@ -114,10 +115,7 @@ function createWinExtMapStore(): Writable<WinExtMap> & API {
|
|||||||
return unregisterExtensionSpawnedProcess(windowLabel, pid).then(() => {
|
return unregisterExtensionSpawnedProcess(windowLabel, pid).then(() => {
|
||||||
ext.pids = ext.pids.filter((p) => p !== pid)
|
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">
|
<script lang="ts">
|
||||||
|
import { goHome } from "@/utils/route"
|
||||||
import { Error, Layouts } from "@kksh/ui"
|
import { Error, Layouts } from "@kksh/ui"
|
||||||
import { page } from "$app/stores"
|
import { page } from "$app/stores"
|
||||||
|
|
||||||
@ -11,12 +12,12 @@
|
|||||||
|
|
||||||
<svelte:window on:keydown={handleKeyDown} />
|
<svelte:window on:keydown={handleKeyDown} />
|
||||||
|
|
||||||
<Layouts.Center class="h-screen">
|
<Layouts.Center class="min-h-screen py-5">
|
||||||
<Error.RawErrorJSONPreset
|
<Error.RawErrorJSONPreset
|
||||||
title="Unknown Error"
|
title="Error"
|
||||||
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"}
|
message={$page.error?.message ?? "Unknown Error"}
|
||||||
onnGoBack={() => window.history.back()}
|
onGoBack={goHome}
|
||||||
rawJsonError={JSON.stringify($page, null, 2)}
|
rawJsonError={JSON.stringify($page, null, 2)}
|
||||||
/>
|
/>
|
||||||
</Layouts.Center>
|
</Layouts.Center>
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import AppContext from "@/components/context/AppContext.svelte"
|
import AppContext from "@/components/context/AppContext.svelte"
|
||||||
import "../app.css"
|
import "../app.css"
|
||||||
import { appConfig, appState, extensions } from "@/stores"
|
import { appConfig, appState, extensions } from "@/stores"
|
||||||
|
import { initDeeplink } from "@/utils/deeplink"
|
||||||
import { isInMainWindow } from "@/utils/window"
|
import { isInMainWindow } from "@/utils/window"
|
||||||
import {
|
import {
|
||||||
ModeWatcher,
|
ModeWatcher,
|
||||||
@ -21,6 +22,7 @@
|
|||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
unlisteners.push(await attachConsole())
|
unlisteners.push(await attachConsole())
|
||||||
|
unlisteners.push(await initDeeplink())
|
||||||
appConfig.init()
|
appConfig.init()
|
||||||
if (isInMainWindow()) {
|
if (isInMainWindow()) {
|
||||||
extensions.init()
|
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 { getExtensionsFolder } from "@/constants"
|
||||||
import { appState, extensions } from "@/stores"
|
import { appState, extensions } from "@/stores"
|
||||||
import { supabaseAPI } from "@/supabase"
|
import { supabaseAPI } from "@/supabase"
|
||||||
import { goBackOnEscapeClearSearchTerm } from "@/utils/key"
|
import { goBackOnEscapeClearSearchTerm, goHomeOnEscapeClearSearchTerm } from "@/utils/key"
|
||||||
import { goBack } from "@/utils/route"
|
import { goBack, goHome } from "@/utils/route"
|
||||||
import { SBExt } from "@kksh/api/supabase"
|
import { SBExt } from "@kksh/api/supabase"
|
||||||
import { isUpgradable } from "@kksh/extension"
|
import { isUpgradable } from "@kksh/extension"
|
||||||
import { Button, Command } from "@kksh/svelte5"
|
import { Button, Command } from "@kksh/svelte5"
|
||||||
@ -64,12 +64,12 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={goBackOnEscapeClearSearchTerm} />
|
<svelte:window on:keydown={goHomeOnEscapeClearSearchTerm} />
|
||||||
{#snippet leftSlot()}
|
{#snippet leftSlot()}
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onclick={goBack}
|
onclick={goHome}
|
||||||
class={Constants.CLASSNAMES.BACK_BUTTON}
|
class={Constants.CLASSNAMES.BACK_BUTTON}
|
||||||
data-flip-id={Constants.CLASSNAMES.BACK_BUTTON}
|
data-flip-id={Constants.CLASSNAMES.BACK_BUTTON}
|
||||||
>
|
>
|
||||||
|
@ -12,12 +12,12 @@
|
|||||||
|
|
||||||
<svelte:window on:keydown={handleKeyDown} />
|
<svelte:window on:keydown={handleKeyDown} />
|
||||||
|
|
||||||
<Layouts.Center class="h-screen">
|
<Layouts.Center class="min-h-screen py-5">
|
||||||
<Error.RawErrorJSONPreset
|
<Error.RawErrorJSONPreset
|
||||||
title="Fail to Load Extension"
|
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"}
|
message={$page.error?.message ?? "Unknown Error"}
|
||||||
onnGoBack={() => goto("/")}
|
onGoBack={() => goto("/")}
|
||||||
rawJsonError={JSON.stringify($page, null, 2)}
|
rawJsonError={JSON.stringify($page, null, 2)}
|
||||||
/>
|
/>
|
||||||
</Layouts.Center>
|
</Layouts.Center>
|
||||||
|
@ -9,13 +9,16 @@
|
|||||||
import { StoreExtDetail } from "@kksh/ui/extension"
|
import { StoreExtDetail } from "@kksh/ui/extension"
|
||||||
import { greaterThan, parse as parseSemver } from "@std/semver"
|
import { greaterThan, parse as parseSemver } from "@std/semver"
|
||||||
import { error } from "@tauri-apps/plugin-log"
|
import { error } from "@tauri-apps/plugin-log"
|
||||||
|
import { goto } from "$app/navigation"
|
||||||
import { ArrowLeftIcon } from "lucide-svelte"
|
import { ArrowLeftIcon } from "lucide-svelte"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { toast } from "svelte-sonner"
|
import { toast } from "svelte-sonner"
|
||||||
import { get, derived as storeDerived } from "svelte/store"
|
import { get, derived as storeDerived } from "svelte/store"
|
||||||
|
|
||||||
const { data } = $props()
|
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) => {
|
const installedExt = storeDerived(installedStoreExts, ($e) => {
|
||||||
return $e.find((e) => e.kunkun.identifier === ext.identifier)
|
return $e.find((e) => e.kunkun.identifier === ext.identifier)
|
||||||
})
|
})
|
||||||
@ -133,7 +136,7 @@
|
|||||||
function handleKeydown(e: KeyboardEvent) {
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
if (!delayedImageDialogOpen) {
|
if (!delayedImageDialogOpen) {
|
||||||
goBack()
|
goto("/extension/store")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -145,7 +148,7 @@
|
|||||||
size="icon"
|
size="icon"
|
||||||
class={cn("fixed left-3 top-3", Constants.CLASSNAMES.BACK_BUTTON)}
|
class={cn("fixed left-3 top-3", Constants.CLASSNAMES.BACK_BUTTON)}
|
||||||
data-flip-id={Constants.CLASSNAMES.BACK_BUTTON}
|
data-flip-id={Constants.CLASSNAMES.BACK_BUTTON}
|
||||||
onclick={goBack}
|
onclick={() => goto("/extension/store")}
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon />
|
<ArrowLeftIcon />
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -13,7 +13,12 @@ export const load: PageLoad = async ({
|
|||||||
}): Promise<{
|
}): Promise<{
|
||||||
ext: Tables<"ext_publish">
|
ext: Tables<"ext_publish">
|
||||||
manifest: KunkunExtManifest
|
manifest: KunkunExtManifest
|
||||||
|
params: {
|
||||||
|
identifier: string
|
||||||
|
}
|
||||||
}> => {
|
}> => {
|
||||||
|
console.log("store[identifier] params", params)
|
||||||
|
|
||||||
const { error: dbError, data: ext } = await supabaseAPI.getLatestExtPublish(params.identifier)
|
const { error: dbError, data: ext } = await supabaseAPI.getLatestExtPublish(params.identifier)
|
||||||
if (dbError) {
|
if (dbError) {
|
||||||
return error(400, {
|
return error(400, {
|
||||||
@ -30,6 +35,7 @@ export const load: PageLoad = async ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
ext,
|
ext,
|
||||||
|
params,
|
||||||
manifest: parseManifest.output
|
manifest: parseManifest.output
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,8 +15,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@gcornut/valibot-json-schema": "^0.42.0",
|
"@gcornut/valibot-json-schema": "^0.42.0",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest"
|
||||||
"supabase": ">=1.8.1"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@kksh/supabase": "workspace:*",
|
"@kksh/supabase": "workspace:*",
|
||||||
@ -25,7 +24,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.583.0",
|
"@aws-sdk/client-s3": "^3.583.0",
|
||||||
"@kksh/api": "workspace:*",
|
"@kksh/api": "workspace:*",
|
||||||
"@supabase/supabase-js": "^2.43.4",
|
|
||||||
"valibot": "^0.40.0"
|
"valibot": "^0.40.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { ExtPackageJson } from "@kksh/api/models"
|
import { ExtPackageJson } from "@kksh/api/models"
|
||||||
import { type Database } from "@kksh/supabase"
|
import { createSB } from "@kksh/supabase"
|
||||||
import { createClient } from "@supabase/supabase-js"
|
|
||||||
import { parse, string } from "valibot"
|
import { parse, string } from "valibot"
|
||||||
import { getJsonSchema } from "../src"
|
import { getJsonSchema } from "../src"
|
||||||
|
|
||||||
const supabase = createClient<Database>(
|
const supabase = createSB(
|
||||||
parse(string(), process.env.SUPABASE_URL),
|
parse(string(), process.env.SUPABASE_URL),
|
||||||
parse(string(), process.env.SUPABASE_SERVICE_ROLE_KEY)
|
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"
|
import { createClient } from "@supabase/supabase-js"
|
||||||
|
|
||||||
export function createSB(supabaseUrl: string, supabaseAnonKey: string) {
|
export function createSB(supabaseUrl: string, supabaseAnonKey: string) {
|
||||||
return createClient<Database>(supabaseUrl, supabaseAnonKey)
|
return createClient<Database>(supabaseUrl, supabaseAnonKey, {
|
||||||
|
auth: {
|
||||||
|
flowType: "pkce"
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
export { SupabaseAPI } from "./api"
|
export { SupabaseAPI } from "./api"
|
||||||
|
|
||||||
|
@ -1,22 +1,25 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import autoAnimate from "@formkit/auto-animate"
|
||||||
import Icon from "@iconify/svelte"
|
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 { Error, Layouts, Shiki } from "@kksh/ui"
|
||||||
|
import { ChevronsUpDown } from "lucide-svelte"
|
||||||
import { type Snippet } from "svelte"
|
import { type Snippet } from "svelte"
|
||||||
|
import { fade, slide } from "svelte/transition"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
class: className,
|
class: className,
|
||||||
rawJsonError,
|
rawJsonError,
|
||||||
onnGoBack,
|
onGoBack,
|
||||||
footer
|
footer
|
||||||
}: {
|
}: {
|
||||||
title: string
|
title: string
|
||||||
message: string
|
message: string
|
||||||
class?: string
|
class?: string
|
||||||
rawJsonError: string
|
rawJsonError: string
|
||||||
onnGoBack?: () => void
|
onGoBack?: () => void
|
||||||
footer?: Snippet
|
footer?: Snippet
|
||||||
} = $props()
|
} = $props()
|
||||||
|
|
||||||
@ -38,15 +41,27 @@
|
|||||||
<svelte:window on:keydown={handleKeyDown} on:keyup={handleKeyUp} />
|
<svelte:window on:keydown={handleKeyDown} on:keyup={handleKeyUp} />
|
||||||
|
|
||||||
<Error.General {title} {message} class={className}>
|
<Error.General {title} {message} class={className}>
|
||||||
<ScrollArea class="" orientation="both">
|
<Collapsible.Root class="w-full space-y-2">
|
||||||
<Shiki class="" code={rawJsonError} lang="json" />
|
<div class="flex items-center justify-between space-x-4 px-4">
|
||||||
</ScrollArea>
|
<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 />
|
<br />
|
||||||
{#snippet footer()}
|
{#snippet footer()}
|
||||||
{#if footer}
|
{#if footer}
|
||||||
{@render footer()}
|
{@render footer()}
|
||||||
{:else}
|
{:else}
|
||||||
<Button variant="default" class="w-full" onclick={onnGoBack} disabled={enterDown}>
|
<Button variant="default" class="w-full" onclick={onGoBack} disabled={enterDown}>
|
||||||
Go Back
|
Go Back
|
||||||
<Icon icon="mi:enter" />
|
<Icon icon="mi:enter" />
|
||||||
</Button>
|
</Button>
|
||||||
|
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@ -415,9 +415,6 @@ importers:
|
|||||||
'@kksh/supabase':
|
'@kksh/supabase':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../supabase
|
version: link:../supabase
|
||||||
'@supabase/supabase-js':
|
|
||||||
specifier: ^2.43.4
|
|
||||||
version: 2.46.1
|
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.5.4
|
version: 5.5.4
|
||||||
@ -431,9 +428,6 @@ importers:
|
|||||||
'@types/bun':
|
'@types/bun':
|
||||||
specifier: latest
|
specifier: latest
|
||||||
version: 1.1.13
|
version: 1.1.13
|
||||||
supabase:
|
|
||||||
specifier: '>=1.8.1'
|
|
||||||
version: 1.207.9
|
|
||||||
|
|
||||||
packages/supabase:
|
packages/supabase:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user