Skip to content
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
1c04dd9
implement atproto oauth
zeucapua Jan 26, 2026
560f04d
Revert "implement atproto oauth"
zeucapua Jan 27, 2026
6215eb3
working atproto auth w/o nuxt-auth-utils
zeucapua Jan 28, 2026
8955ae6
Merge branch 'main' into feat/atproto-oauth
zeucapua Jan 29, 2026
477cebb
working atproto oauth login and logout
zeucapua Jan 29, 2026
50decf2
update auth styles
zeucapua Jan 29, 2026
1a678ee
moved to server side storage for oauth sessions
fatfingers23 Jan 29, 2026
cea37b4
adds oauth to middleware
fatfingers23 Jan 29, 2026
86fb98a
moved to a defineEventHandler
fatfingers23 Jan 29, 2026
a4cecc9
wip
fatfingers23 Jan 29, 2026
aa3bf51
Merge pull request #1 from fatfingers23/feat/atproto-oauth
zeucapua Jan 29, 2026
01b1fca
Merge branch 'main' into feat/atproto-oauth
zeucapua Jan 29, 2026
ad9e665
throw early on unset session password env
zeucapua Jan 29, 2026
e990fbd
proof of concept login/create buttons
fatfingers23 Jan 29, 2026
626c25d
Merge pull request #2 from fatfingers23/feat/atproto-oauth
zeucapua Jan 30, 2026
e721cb8
update session miniDoc types
zeucapua Jan 30, 2026
0aad561
update login copy
zeucapua Jan 30, 2026
e267dff
add env step on CONTRIBUTING
zeucapua Jan 30, 2026
0ecce4e
Merge remote-tracking branch 'origin/main' into feat/atproto-oauth
danielroe Jan 30, 2026
cb66960
chore: ignore exports
danielroe Jan 30, 2026
a45e707
chore: remove missing prop
danielroe Jan 30, 2026
9102628
fix: use runtimeConfig and auto-gen dev session password
danielroe Jan 30, 2026
005f661
follow up on items
fatfingers23 Jan 30, 2026
c17612d
added schema checks
fatfingers23 Jan 30, 2026
209e8a3
changed some wording around as discussed in discord
fatfingers23 Jan 30, 2026
9dc0452
typo
fatfingers23 Jan 30, 2026
d01037a
bit more explicit
fatfingers23 Jan 31, 2026
dce80cb
Merge pull request #3 from fatfingers23/feat/atproto-oauth
zeucapua Jan 31, 2026
4eb5907
Merge remote-tracking branch 'origin/main' into feat/atproto-oauth
danielroe Jan 31, 2026
8f9ebf3
fix: sync composable usage + $fetch
danielroe Jan 31, 2026
f2ad398
fix: use http-only cookies
danielroe Jan 31, 2026
164eb35
chore: let -> const
danielroe Jan 31, 2026
d954a82
chore: don't init auth on server
danielroe Jan 31, 2026
6b3e006
fix: set up upstash for prod use
danielroe Jan 31, 2026
257effe
fix(cli): update to use 127.0.0.1
danielroe Jan 31, 2026
f9bdefb
fix: add locking implementation
danielroe Jan 31, 2026
5f61c17
feat: use dropdown for atmosphere + npm
danielroe Jan 31, 2026
5d77118
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 31, 2026
979783c
chore: knip
danielroe Jan 31, 2026
d389c0f
feat: npmx.social! 🚀
danielroe Jan 31, 2026
3b4f0ee
chore: fix correct object
danielroe Jan 31, 2026
a947448
chore: knip
danielroe Jan 31, 2026
1315097
fix: extract strings + use 'disconnect'
danielroe Jan 31, 2026
d23e02b
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#secure password, can use openssl rand --hex 32
NUXT_SESSION_PASSWORD=""
6 changes: 1 addition & 5 deletions app/components/AppHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@
withDefaults(
defineProps<{
showLogo?: boolean
showConnector?: boolean
}>(),
{
showLogo: true,
showConnector: true,
},
)

Expand Down Expand Up @@ -98,9 +96,7 @@ onKeyStroke(',', e => {
</kbd>
</NuxtLink>

<div v-if="showConnector" class="hidden sm:block">
<ConnectorStatus />
</div>
<HeaderAccountMenu />
</div>
</nav>
</header>
Expand Down
18 changes: 18 additions & 0 deletions app/components/AuthButton.client.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script setup lang="ts">
const showModal = ref(false)
const { user } = useAtproto()
</script>

<template>
<div class="relative">
<button
type="button"
class="relative font-mono text-sm flex items-center justify-center w-fit rounded-md transition-colors duration-200 hover:bg-bg-subtle focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
@click="showModal = true"
>
{{ user?.handle || 'login' }}
</button>

<AuthModal v-model:open="showModal" />
</div>
</template>
10 changes: 10 additions & 0 deletions app/components/AuthButton.server.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<template>
<div class="relative">
<button
type="button"
class="relative font-mono text-sm flex items-center justify-center w-fit rounded-md transition-colors duration-200 hover:bg-bg-subtle focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
>
login
</button>
</div>
</template>
18 changes: 18 additions & 0 deletions app/components/AuthButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script setup lang="ts">
const showModal = ref(false)
const { user } = useAtproto()
</script>

<template>
<div class="relative">
<button
type="button"
class="relative font-mono text-sm flex items-center justify-center w-fit rounded-md transition-colors duration-200 hover:bg-bg-subtle focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
@click="showModal = true"
>
{{ user?.handle || 'login' }}
</button>

<AuthModal v-model:open="showModal" />
</div>
</template>
191 changes: 191 additions & 0 deletions app/components/AuthModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<script setup lang="ts">
const open = defineModel<boolean>('open', { default: false })

const handleInput = ref('')

const { user, logout } = useAtproto()

async function handleBlueskySignIn() {
await navigateTo(
{
path: '/api/auth/atproto',
query: { handle: 'https://bsky.social' },
},
{ external: true },
)
}

async function handleCreateAccount() {
await navigateTo(
{
path: '/api/auth/atproto',
query: { handle: 'https://selfhosted.social', create: 'true' },
},
{ external: true },
)
}

async function handleLogin() {
if (handleInput.value) {
await navigateTo(
{
path: '/api/auth/atproto',
query: { handle: handleInput.value },
},
{ external: true },
)
}
}
</script>

<template>
<Teleport to="body">
<Transition
enter-active-class="transition-opacity duration-200"
leave-active-class="transition-opacity duration-200"
enter-from-class="opacity-0"
leave-to-class="opacity-0"
>
<div v-if="open" class="fixed inset-0 z-50 flex items-center justify-center p-4">
<!-- Backdrop -->
<button
type="button"
class="absolute inset-0 bg-black/60 cursor-default"
aria-label="Close modal"
@click="open = false"
/>

<!-- Modal -->
<div
class="relative w-full max-w-lg bg-bg border border-border rounded-lg shadow-xl max-h-[90vh] overflow-y-auto overscroll-contain"
role="dialog"
aria-modal="true"
aria-labelledby="auth-modal-title"
>
<div class="p-6">
<div class="flex items-center justify-between mb-6">
<h2 id="auth-modal-title" class="font-mono text-lg font-medium">Account</h2>
<button
type="button"
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"
aria-label="Close"
@click="open = false"
>
<span class="i-carbon-close block w-5 h-5" aria-hidden="true" />
</button>
</div>

<div v-if="user?.handle" class="space-y-4">
<div class="flex items-center gap-3 p-4 bg-bg-subtle border border-border rounded-lg">
<span class="w-3 h-3 rounded-full bg-green-500" aria-hidden="true" />
<div>
<p class="font-mono text-xs text-fg-muted">Connected as @{{ user.handle }}</p>
</div>
</div>
<button
@click="logout"
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"
>
Logout
</button>
</div>

<!-- Disconnected state -->
<form v-else class="space-y-4" @submit.prevent="handleLogin">
<p class="text-sm text-fg-muted">Connect with your Atmosphere account</p>

<div class="space-y-3">
<div>
<label
for="handle-input"
class="block text-xs text-fg-subtle uppercase tracking-wider mb-1.5"
>
Handle
</label>
<input
id="handle-input"
v-model="handleInput"
type="text"
name="handle"
placeholder="alice.bsky.social"
autocomplete="off"
spellcheck="false"
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"
/>
</div>

<details class="text-sm">
<summary
class="text-fg-subtle cursor-pointer hover:text-fg-muted transition-colors duration-200"
>
What is an Atmosphere account?
Comment thread
zeucapua marked this conversation as resolved.
Outdated
</summary>
<div class="mt-3">
<p>
<span class="font-bold">npmx.dev</span> uses the
<a
href="https://atproto.com"
target="_blank"
class="text-blue-400 hover:underline"
>
AT Protocol
</a>
to power many of its social features, allowing users to own their data and use
one account for all compatible applications. Once you create an account, you
can use other apps like
<a
href="https://bsky.app"
target="_blank"
class="text-blue-400 hover:underline"
>
Bluesky
</a>
and
<a
href="https://tangled.org"
target="_blank"
class="text-blue-400 hover:underline"
>
Tangled
</a>
with the same account.
</p>
</div>
</details>
</div>

<button
type="submit"
:disabled="!handleInput.trim()"
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"
>
Connect
</button>
<button
type="button"
@click="handleCreateAccount"
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"
>
Create a new account
</button>
<hr />
<button
type="button"
@click="handleBlueskySignIn"
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 flex items-center justify-center gap-2"
>
Connect with Bluesky
<svg fill="none" viewBox="0 0 64 57" width="20" style="width: 20px">
<path
fill="#0F73FF"
d="M13.873 3.805C21.21 9.332 29.103 20.537 32 26.55v15.882c0-.338-.13.044-.41.867-1.512 4.456-7.418 21.847-20.923 7.944-7.111-7.32-3.819-14.64 9.125-16.85-7.405 1.264-15.73-.825-18.014-9.015C1.12 23.022 0 8.51 0 6.55 0-3.268 8.579-.182 13.873 3.805ZM50.127 3.805C42.79 9.332 34.897 20.537 32 26.55v15.882c0-.338.13.044.41.867 1.512 4.456 7.418 21.847 20.923 7.944 7.111-7.32 3.819-14.64-9.125-16.85 7.405 1.264 15.73-.825 18.014-9.015C62.88 23.022 64 8.51 64 6.55c0-9.818-8.578-6.732-13.873-2.745Z"
></path>
</svg>
</button>
</form>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
Loading
Loading