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

<AuthButton />
</div>
</nav>
</header>
Expand Down
19 changes: 19 additions & 0 deletions app/components/AuthButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<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"
:aria-label="ariaLabel"

Check failure on line 11 in app/components/AuthButton.vue

View workflow job for this annotation

GitHub Actions / test

Property 'ariaLabel' does not exist on type '{ showModal: boolean; user: MiniDoc | null | undefined; $: ComponentInternalInstance; $data: {}; $props: {}; $attrs: Data; $refs: Data; ... 578 more ...; $nuxtSiteConfig: any; }'.
@click="showModal = true"
>
{{ user?.miniDoc?.handle || 'login' }}

Check failure on line 14 in app/components/AuthButton.vue

View workflow job for this annotation

GitHub Actions / test

Property 'miniDoc' does not exist on type 'MiniDoc'.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

user itself is a "minidoc", no? so this and the other usages can just be user?.handle?

</button>

<AuthModal v-model:open="showModal" />
</div>
</template>
134 changes: 134 additions & 0 deletions app/components/AuthModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<script setup lang="ts">
const open = defineModel<boolean>('open', { default: false })
const handleInput = ref('')
const { user, logout } = await useAtproto()
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 Login</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?.miniDoc?.handle" class="space-y-4">

Check failure on line 58 in app/components/AuthModal.vue

View workflow job for this annotation

GitHub Actions / test

Property 'miniDoc' does not exist on type 'MiniDoc'.
<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.miniDoc.handle }}

Check failure on line 63 in app/components/AuthModal.vue

View workflow job for this annotation

GitHub Actions / test

Property 'miniDoc' does not exist on type 'MiniDoc'.
</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"
>
Internet 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?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we need to figure out a nice way to make account creation very clear, too.

currently if someone doesn't have a bsky account, and has no idea what atproto is, this info doesn't give them much. they need to now dig through the atproto docs and do some googling to find out how to make an account

so we should probably at least mention bsky as an example of how you might have such an account

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm good if we want to go ahead use selfhosted.social (it's mine and zeu's and I'm the admin of it) PDS for account creations. You start the OAuth flow to the server with the PDS's url and a create flag. With that the user can sign up for an account on the PDS and be redirected back to npmx authenticated.
This will give users handles ending in .selfhosted.social

I am also good if y'all want me to manage a PDS server with an alternate handle ending for npmx, like npmx.town. atproto oauth doesn't like serving apps if the account logging in shares a domain with the PDS. This is also just more branding if we want them to have that ending in the handle

</summary>
<div class="mt-3">
<p>
<span class="font-bold">npmx.dev</span> is an atmosphere application, meaning
it's built on the
<a
href="https://atproto.com"
target="_blank"
class="text-blue-400 hover:underline"
>AT Protocol</a
>. This means users can own their data and use one account for all atmosphere
accounts.
</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>
</form>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
30 changes: 30 additions & 0 deletions app/composables/useAtproto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
type MiniDoc = {
did: string
handle: string
pds: string
}

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 }
}
3 changes: 3 additions & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export default defineNuxtConfig({
css: ['~/assets/main.css', 'vue-data-ui/style.css'],

devtools: { enabled: true },
devServer: {
host: '127.0.0.1',
},

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