-
Notifications
You must be signed in to change notification settings - Fork 88
feat: atproto oauth #273
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: atproto oauth #273
Changes from all commits
1c04dd9
560f04d
6215eb3
8955ae6
477cebb
50decf2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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
|
||
| @click="showModal = true" | ||
| > | ||
| {{ user?.miniDoc?.handle || 'login' }} | ||
| </button> | ||
|
|
||
| <AuthModal v-model:open="showModal" /> | ||
| </div> | ||
| </template> | ||
| 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"> | ||
| <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 }} | ||
| </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? | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. 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> | ||
| 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 } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useritself is a "minidoc", no? so this and the other usages can just beuser?.handle?