Skip to content

Commit 1c04dd9

Browse files
committed
implement atproto oauth
1 parent ad493c0 commit 1c04dd9

7 files changed

Lines changed: 606 additions & 0 deletions

File tree

app/components/AppHeader.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ withDefaults(
5454
<span class="hidden sm:inline">github</span>
5555
</a>
5656
</li>
57+
<li class="flex">
58+
<AuthButton />
59+
</li>
5760
</ul>
5861
</nav>
5962
</header>

app/components/AuthButton.vue

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script setup lang="ts">
2+
const { loggedIn, user, session, fetch, clear, openInPopup } = useUserSession()
3+
4+
const showModal = ref(false)
5+
</script>
6+
7+
<template>
8+
<div class="relative">
9+
<button
10+
type="button"
11+
class="relative flex items-center justify-center w-8 h-8 rounded-md transition-colors duration-200 hover:bg-bg-subtle focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
12+
@click="showModal = true"
13+
>
14+
{{ user?.bluesky ?? 'Login' }}
15+
</button>
16+
17+
<AuthModal v-model:open="showModal" />
18+
</div>
19+
</template>

app/components/AuthModal.vue

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<script setup lang="ts">
2+
const open = defineModel<boolean>('open', { default: false })
3+
const { loggedIn, user, session, fetch, clear, openInPopup } = useUserSession()
4+
5+
const handleInput = ref('')
6+
7+
async function handleLogin() {
8+
navigateTo(
9+
{
10+
path: '/api/auth/atproto',
11+
query: { handle: handleInput.value },
12+
},
13+
{ external: true },
14+
)
15+
}
16+
</script>
17+
18+
<template>
19+
<Teleport to="body">
20+
<Transition
21+
enter-active-class="transition-opacity duration-200"
22+
leave-active-class="transition-opacity duration-200"
23+
enter-from-class="opacity-0"
24+
leave-to-class="opacity-0"
25+
>
26+
<div v-if="open" class="fixed inset-0 z-50 flex items-center justify-center p-4">
27+
<!-- Backdrop -->
28+
<button
29+
type="button"
30+
class="absolute inset-0 bg-black/60 cursor-default"
31+
aria-label="Close modal"
32+
@click="open = false"
33+
/>
34+
35+
<!-- Modal -->
36+
<div
37+
class="relative w-full bg-bg border border-border rounded-lg shadow-xl max-h-[90vh] overflow-y-auto overscroll-contain"
38+
role="dialog"
39+
aria-modal="true"
40+
aria-labelledby="connector-modal-title"
41+
>
42+
<div class="p-6">
43+
<div class="flex items-center justify-between mb-6">
44+
<h2 id="connector-modal-title" class="font-mono text-lg font-medium">
45+
Account Settings
46+
</h2>
47+
<button
48+
type="button"
49+
class="text-fg-subtle hover:text-fg transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 rounded"
50+
aria-label="Close"
51+
@click="open = false"
52+
>
53+
<span class="i-carbon-close block w-5 h-5" aria-hidden="true" />
54+
</button>
55+
</div>
56+
57+
<!-- Connected state -->
58+
<div v-if="loggedIn" class="space-y-4">
59+
<div class="flex items-center gap-3 p-4 bg-bg-subtle border border-border rounded-lg">
60+
<span class="w-3 h-3 rounded-full bg-green-500" aria-hidden="true" />
61+
<div>
62+
<p v-if="user" class="font-mono text-xs text-fg-muted">
63+
Logged in as @{{ user.bluesky }}
64+
</p>
65+
</div>
66+
</div>
67+
68+
<button
69+
type="button"
70+
class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
71+
@click="clear"
72+
>
73+
Logout
74+
</button>
75+
</div>
76+
77+
<!-- Disconnected state -->
78+
<form v-else class="space-y-4" @submit.prevent="handleLogin">
79+
<p class="text-sm text-fg-muted">Login with your Atmosphere handle:</p>
80+
81+
<div class="space-y-3">
82+
<div>
83+
<label
84+
for="atproto-handle"
85+
class="block text-xs text-fg-subtle uppercase tracking-wider mb-1.5"
86+
>
87+
Handle
88+
</label>
89+
<input
90+
id="atproto-handle"
91+
v-model="handleInput"
92+
type="text"
93+
name="atproto-handle"
94+
placeholder="alice.bsky.social"
95+
autocomplete="off"
96+
spellcheck="false"
97+
class="w-full px-3 py-2 font-mono text-sm bg-bg-subtle border border-border rounded-md text-fg placeholder:text-fg-subtle transition-colors duration-200 focus:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
98+
/>
99+
</div>
100+
101+
<details class="text-sm">
102+
<summary
103+
class="text-fg-subtle cursor-pointer hover:text-fg-muted transition-colors duration-200"
104+
>
105+
What is an Atmosphere handle?
106+
</summary>
107+
<div class="mt-3">TODO</div>
108+
</details>
109+
</div>
110+
111+
<button
112+
type="submit"
113+
:disabled="!handleInput.trim()"
114+
class="w-full px-4 py-2 font-mono text-sm text-bg bg-fg rounded-md transition-all duration-200 hover:bg-fg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 focus-visible:ring-offset-2 focus-visible:ring-offset-bg"
115+
>
116+
Login
117+
</button>
118+
</form>
119+
</div>
120+
</div>
121+
</div>
122+
</Transition>
123+
</Teleport>
124+
</template>

nuxt.config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,13 @@ export default defineNuxtConfig({
1515
'nuxt-og-image',
1616
'@nuxt/test-utils',
1717
'@vite-pwa/nuxt',
18+
'nuxt-auth-utils',
1819
],
1920

2021
devtools: { enabled: true },
22+
devServer: {
23+
host: '127.0.0.1',
24+
},
2125

2226
app: {
2327
head: {
@@ -129,4 +133,8 @@ export default defineNuxtConfig({
129133
],
130134
},
131135
},
136+
137+
auth: {
138+
atproto: true,
139+
},
132140
})

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
"test:unit": "vitest --project unit"
2727
},
2828
"dependencies": {
29+
"@atproto/api": "^0.18.17",
30+
"@atproto/oauth-client-node": "^0.3.15",
2931
"@iconify-json/simple-icons": "^1.2.67",
3032
"@iconify-json/vscode-icons": "^1.2.40",
3133
"@nuxt/a11y": "1.0.0-alpha.1",
@@ -37,6 +39,7 @@
3739
"@vueuse/core": "^14.1.0",
3840
"@vueuse/nuxt": "14.1.0",
3941
"nuxt": "^4.3.0",
42+
"nuxt-auth-utils": "0.5.27",
4043
"nuxt-og-image": "^5.1.13",
4144
"perfect-debounce": "^2.1.0",
4245
"sanitize-html": "^2.17.0",

0 commit comments

Comments
 (0)