Skip to content

Commit

Permalink
Desktop Auth (#73)
Browse files Browse the repository at this point in the history
* feat: add supabase nuxt module, experiment with supabase oauth

* fix: integrate with windows, fix mac security crate, prevent it to compile on windows

* fix: deep link fixed for windows, js URL works differently on windows and macos, wtf?

* feat: supabase oauth, internal without deeplink

Had problem with deep link, failed to verify code.

* feat: implement supabase oauth with deep link

* update deno lock
  • Loading branch information
HuakunShen authored Oct 26, 2024
1 parent 7ce224e commit 7b9cc5c
Show file tree
Hide file tree
Showing 24 changed files with 4,521 additions and 3,654 deletions.
26 changes: 25 additions & 1 deletion apps/desktop/components/cmd-palette/Footer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Kbd from "@/components/Kbd.vue"
import { cn } from "@/lib/utils"
import { useAppUiStore } from "@/stores/ui"
import { Icon } from "@iconify/vue"
import { Avatar, AvatarFallback, AvatarImage } from "@kksh/vue/avatar"
import { Button } from "@kksh/vue/button"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@kksh/vue/tooltip"
import { platform } from "@tauri-apps/plugin-os"
Expand All @@ -14,6 +15,7 @@ import ActionPanel from "./ActionPanel.vue"
const _platform = platform()
const appUiStore = useAppUiStore()
const appConfig = useAppConfigStore()
const session = useSupabaseSession()
function onReload() {
appConfig.init()
Expand All @@ -40,10 +42,32 @@ onMounted(() => {
onUnmounted(() => {
document.removeEventListener("keydown", onKeyDown)
})
const avatarFallback = computed(() => {
if (!session.value) return "?"
const nameSplit = session.value?.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 "?"
}
})
function onAvatarClick() {
navigateTo("/auth")
}
</script>
<template>
<div data-tauri-drag-region class="z-50 flex h-12 items-center justify-between border p-2">
<img class="h-6 w-6 invert dark:invert-0" src="/img/logo-w-bg.png" alt="logo" />
<div flex gap-2 items-center>
<img class="h-6 w-6 invert dark:invert-0" src="/img/logo-w-bg.png" alt="logo" />
<Avatar v-if="session" class="h-6 w-6 cursor-pointer border" @click="onAvatarClick">
<AvatarImage :src="session?.user.user_metadata.avatar_url" alt="avatar" />
<AvatarFallback>{{ avatarFallback }}</AvatarFallback>
</Avatar>
</div>
<span class="flex gap-2">
<TooltipProvider>
<Tooltip>
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ unlistenRefreshConfig = await listenToRefreshConfig(async () => {
initStores()
// useRegisterAppShortcuts()
})
initDeeplink()
const windowExtMapStore = useWindowExtMapStore()
if (appWindow.label === "main") {
Expand All @@ -47,6 +47,7 @@ onMounted(async () => {
if (!isMainWindow) {
return
}
initDeeplink()
await appConfig.init()
appConfig.refreshWindowStyles()
useRegisterAppShortcuts()
Expand Down
43 changes: 24 additions & 19 deletions apps/desktop/lib/init/deeplink.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
DEEP_LINK_PATH_AUTH_CONFIRM,
DEEP_LINK_PATH_OPEN,
DEEP_LINK_PATH_REFRESH_DEV_EXTENSION,
DEEP_LINK_PATH_STORE
Expand All @@ -16,6 +17,8 @@ const StorePathSearchParams = v.object({
})

export async function initDeeplink() {
console.log("init deeplink")

const appWindow = getCurrentWebviewWindow()
if (appWindow.label !== "main") {
return
Expand Down Expand Up @@ -44,26 +47,28 @@ function openMainWindow() {
}

export async function handleKunkunProtocol(parsedUrl: URL) {
const path = parsedUrl.host // Remove the leading '//' kunkun://open gives "open"
const params = Object.fromEntries(parsedUrl.searchParams)
switch (path) {
case DEEP_LINK_PATH_OPEN:
openMainWindow()
break
case DEEP_LINK_PATH_STORE:
const parsed = v.parse(StorePathSearchParams, params)
openMainWindow()
if (parsed.identifier) {
navigateTo(`/store/${parsed.identifier}`)
} else {
navigateTo("/extension-store")
}
break
case DEEP_LINK_PATH_REFRESH_DEV_EXTENSION:
emitRefreshDevExt()
break
default:
console.warn("Unknown deep link path:", path)
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) {
navigateTo(`/store/${parsed.identifier}`)
} else {
navigateTo("/extension-store")
}
} else if (href.startsWith(DEEP_LINK_PATH_REFRESH_DEV_EXTENSION)) {
emitRefreshDevExt()
} else if (href.startsWith(DEEP_LINK_PATH_AUTH_CONFIRM)) {
openMainWindow()
navigateTo(`/auth/confirm?${parsedUrl.searchParams.toString()}`);
} else {
console.error("Invalid path:", pathname)
toast.error("Invalid path", {
description: parsedUrl.href
})
}
}

Expand Down
20 changes: 18 additions & 2 deletions apps/desktop/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ export default defineNuxtConfig({
compatibilityDate: "2024-04-03",
ssr: false,
css: ["@kksh/vue/css", "@kksh/vue/themes", "./assets/css/app.css"],
devtools: { enabled: true },
devtools: {
enabled: true,

timeline: {
enabled: true
}
},
modules: [
"@nuxtjs/i18n",
"@pinia/nuxt",
Expand All @@ -18,14 +24,24 @@ export default defineNuxtConfig({
"@unocss/nuxt",
"shadcn-nuxt",
"@nuxt/image",
"@nuxtjs/color-mode"
"@nuxtjs/color-mode",
"@nuxtjs/supabase"
],
mdc: {},
i18n: {
locales: ["en", "zh"],
defaultLocale: "en",
vueI18n: "./i18n/i18n.config.ts"
},
supabase: {
key: process.env.SUPABASE_ANON_KEY,
redirectOptions: {
login: "/auth",
callback: "/auth/confirm",
exclude: ["/", "/**"],
cookieRedirect: false
}
},
nitro: {
output: {
publicDir: "dist"
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@kksh/vue": "0.1.3",
"@nanostores/vue": "^0.10.0",
"@nuxt/icon": "^1.4.4",
"@nuxtjs/supabase": "1.4.0",
"@nuxt/image": "^1.7.0",
"@nuxtjs/color-mode": "^3.5.1",
"@nuxtjs/i18n": "^8.3.3",
Expand Down
68 changes: 68 additions & 0 deletions apps/desktop/pages/auth/confirm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<script setup lang="ts">
import { Avatar, AvatarFallback, AvatarImage } from "@kksh/vue/avatar"
import { Button } from "@kksh/vue/button"
import { ArrowLeftIcon } from "@radix-icons/vue"
import { onKeyStroke } from "@vueuse/core"
import { toast } from "vue-sonner"
const session = useSupabaseSession()
const supabase = useSupabaseClient()
const user = useSupabaseUser()
onKeyStroke("Escape", (e) => {
e.preventDefault()
onBack()
})
function onBack() {
navigateTo("/")
}
const route = useRoute()
onMounted(async () => {
const code = route.query.code
console.log("Exchange Code", code)
if (code) {
await supabase.auth.exchangeCodeForSession(code as string)
} else {
toast.error("No code found")
}
})
const avatarFallback = computed(() => {
if (!session.value) return "?"
const nameSplit = session.value?.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 "?"
}
})
function onSignOut() {
supabase.auth.signOut()
navigateTo("/auth")
}
</script>
<template>
<main class="container h-screen w-screen pt-10">
<Button variant="outline" size="icon" class="absolute left-2 top-2 z-50" @click="onBack">
<ArrowLeftIcon />
</Button>
<div class="flex grow items-center justify-center pt-16">
<div class="flex flex-col items-center gap-4">
<span v-if="session" class="font-mono text-4xl font-bold">Welcome, You are Logged In</span>
<span v-else class="font-mono text-4xl font-bold">You Are Not Logged In</span>
<span flex flex-col items-center gap-5 text-xl>
<Avatar v-if="session" class="h-32 w-32 border">
<AvatarImage :src="session?.user.user_metadata.avatar_url" alt="avatar" />
<AvatarFallback>{{ avatarFallback }}</AvatarFallback>
</Avatar>
<Button variant="outline" @click="onSignOut">Sign Out</Button>
</span>
</div>
</div>
</main>
</template>
88 changes: 88 additions & 0 deletions apps/desktop/pages/auth/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<script setup lang="ts">
import { DEEP_LINK_PATH_AUTH_CONFIRM } from "@kksh/api"
import { Button } from "@kksh/vue/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle
} from "@kksh/vue/card"
import { ArrowLeftIcon } from "@radix-icons/vue"
import { onKeyStroke } from "@vueuse/core"
import { open } from "tauri-plugin-shellx-api"
import { toast } from "vue-sonner"
const supabase = useSupabaseClient()
const session = useSupabaseSession()
const redirectTo = DEEP_LINK_PATH_AUTH_CONFIRM
// const redirectTo = "http://localhost:3000/auth/confirm"
onKeyStroke("Escape", (e) => {
e.preventDefault()
onBack()
})
function onBack() {
navigateTo("/")
}
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
}
})
console.log("Sign In With OAuth", data)
if (error) {
console.log(error)
toast.error("Failed to sign in with OAuth", { description: error.message })
} else {
data.url && open(data.url)
}
}
onMounted(async () => {
if (session.value) {
// navigateTo("/")
}
})
</script>

<template>
<main h-screen w-screen flex flex-col justify-center items-center>
<Button variant="outline" size="icon" class="absolute left-2 top-2 z-50" @click="onBack">
<ArrowLeftIcon />
</Button>
<!-- <Button @click="supabase.auth.signOut()">Sign Out</Button> -->
<Card class="w-80">
<CardHeader class="space-y-1">
<CardTitle class="flex flex-col items-center text-2xl">
<div class="p-5">
<nuxt-img src="/img/logo.png" alt="Kunkun" class="h-12 w-12 invert" />
</div>
<Button v-if="session" variant="outline" @click="supabase.auth.signOut()"
>Sign Out</Button
>
<span v-else>Sign In</span>
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent class="grid gap-4">
<div v-if="!session" class="grid grid-cols-2 place-items-center gap-4">
<Button variant="outline" size="lg" class="w-full" @click="signInWithOAuth('github')">
<Icon name="fa6-brands:github" class="h-5 w-5" />
</Button>
<Button variant="outline" size="lg" class="w-full" @click="signInWithOAuth('google')">
<Icon name="logos:google-icon" class="h-5 w-5" />
</Button>
</div>
</CardContent>
</Card>
</main>
</template>
20 changes: 11 additions & 9 deletions apps/desktop/pages/dev/index.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
<script setup lang="ts"></script>
<script setup lang="ts">
definePageMeta({
layout: false
})
const session = useSupabaseSession()
</script>

<template>
<MetadataLabel title="test" text="149.5%" />
<MetadataSeparator />
<MetadataLabel title="test" text="149.5%" />
<MetadataSeparator />
<MetadataLabel title="test" text="149.5%" />
<MetadataSeparator />
<MetadataLabel title="test" text="149.5%" />
<MetadataSeparator />
<div>
<h1>dev</h1>
<pre>session: {{ session }}</pre>
</div>
</template>
3 changes: 3 additions & 0 deletions apps/desktop/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { flatten, parse, safeParse } from "valibot"
import { toast } from "vue-sonner"
import { z } from "zod"
const session = useSupabaseSession()
const builtinCmdStore = useBuiltInCmdStore()
const appsStore = useAppsLoaderStore()
const sysCmdsStore = useSystemCmdsStore()
Expand Down Expand Up @@ -93,6 +94,8 @@ useListenToWindowFocus(() => {
})
onMounted(async () => {
console.log("session", session.value);
appUiStore.setDefaultAction("Open")
if (appWindow.label !== "main") {
setTimeout(() => {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/pages/store/[identifier].vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const extStore = useExtensionStore()
onKeyStroke("Escape", (e) => {
e.preventDefault()
navigateTo("/extension-store")
onBack()
})
function onBack() {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ tauri-plugin-store = "2.0.1"
tauri-plugin-deep-link = "2"
tauri-plugin-log = { version = "2.0.1", features = ["colored"] }
zip = "2.1.3"
mac-security-rs = { workspace = true }

[target."cfg(target_os = \"macos\")".dependencies]
cocoa = "0.24.1"
mac-security-rs = { workspace = true }
objc = "0.2.7"


Expand Down
Loading

0 comments on commit 7b9cc5c

Please sign in to comment.