Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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>
189 changes: 189 additions & 0 deletions app/components/AuthModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<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">Logged in 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">Login 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> is built on the
<a
href="https://atproto.com"
target="_blank"
class="text-blue-400 hover:underline"
>
AT Protocol </a
>, 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 login.
</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"
>
Login
</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"
>
Sign in 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>
31 changes: 31 additions & 0 deletions app/composables/useAtproto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
type MiniDoc = {
did: string
handle: string
pds: string
}

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

return data
})

const logout = async () => {
await useRequestFetch()<MiniDoc>('/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`)
}
},
})
12 changes: 12 additions & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ export default defineNuxtConfig({

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

runtimeConfig: {
sessionPassword: '',
},

devtools: { enabled: true },

app: {
Expand Down Expand Up @@ -127,6 +131,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