Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 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
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=""
2 changes: 2 additions & 0 deletions app/components/AppHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ onKeyStroke(',', e => {
<div v-if="showConnector" class="hidden sm:block">
<ConnectorStatus />
</div>

<AuthButton />
</div>
</nav>
</header>
Expand Down
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 } = await 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 } = await 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?
</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>
25 changes: 25 additions & 0 deletions app/composables/useAtproto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { UserSession } from '#shared/schemas/userSession'

/** @public */
export async function useAtproto() {
const {
data: user,
pending,
clear,
} = await useAsyncData<UserSession | null>('user-state', async () => {
return await useRequestFetch()<UserSession>('/api/auth/session', {
headers: { accept: 'application/json' },
})
})

const logout = async () => {
await useRequestFetch()<UserSession>('/api/auth/session', {
method: 'delete',
headers: { accept: 'application/json' },
})

clear()
}

return { user, pending, logout }
}
2 changes: 1 addition & 1 deletion app/composables/useRepoMeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,7 @@ const tangledAdapter: ProviderAdapter = {
try {
//Get counts of records that reference this repo in the atmosphere using constellation
const allLinks = await cachedFetch<ConstellationAllLinksResponse>(
`https://constellation.microcosm.blue/links/all?target=${atUri}`,
`${CONSTELLATION_ENDPOINT}/links/all?target=${atUri}`,
{ headers: { 'User-Agent': 'npmx' } },
REPO_META_TTL,
)
Expand Down
28 changes: 28 additions & 0 deletions modules/dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { defineNuxtModule, useNuxt } from 'nuxt/kit'
import { join } from 'node:path'
import { appendFileSync, existsSync, readFileSync } from 'node:fs'
import { randomUUID } from 'node:crypto'

export default defineNuxtModule({
meta: {
name: 'dev',
},
setup() {
const nuxt = useNuxt()
if (nuxt.options._prepare || process.env.NUXT_SESSION_PASSWORD) {
return
}

const envPath = join(nuxt.options.rootDir, '.env')
const hasPassword =
existsSync(envPath) && /^NUXT_SESSION_PASSWORD=/m.test(readFileSync(envPath, 'utf-8'))

if (!hasPassword) {
console.info('Generating NUXT_SESSION_PASSWORD for development environment.')
const password = randomUUID().replace(/-/g, '')

nuxt.options.runtimeConfig.sessionPassword = password
appendFileSync(envPath, `# generated by dev module\nNUXT_SESSION_PASSWORD=${password}\n`)
}
},
})
18 changes: 18 additions & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,18 @@ export default defineNuxtConfig({

css: ['~/assets/main.css', 'vue-data-ui/style.css'],

runtimeConfig: {
sessionPassword: '',
},

devtools: { enabled: true },

devServer: {
// Used with atproto oauth
// https://atproto.com/specs/oauth#localhost-client-development
host: '127.0.0.1',
},

app: {
head: {
htmlAttrs: { lang: 'en-US' },
Expand Down Expand Up @@ -127,6 +137,14 @@ export default defineNuxtConfig({
driver: 'fsLite',
base: './.cache/fetch',
},
'oauth-atproto-state': {
driver: 'fsLite',
base: './.cache/atproto-oauth/state',
},
'oauth-atproto-session': {
driver: 'fsLite',
base: './.cache/atproto-oauth/session',
},
},
},

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
"test:unit": "vite test --project unit"
},
"dependencies": {
"@atproto/api": "^0.18.17",
"@atproto/oauth-client-node": "^0.3.15",
"@atproto/lex": "^0.0.13",
"@deno/doc": "jsr:^0.189.1",
"@iconify-json/simple-icons": "^1.2.67",
Expand Down
Loading
Loading