Skip to content
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

feat: use oauth for login #29

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
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: 1 addition & 1 deletion app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ if (import.meta.server && !route.path.startsWith('/settings')) {
}

// We want to trigger rerendering the page when account changes
const key = computed(() => `${currentUser.value?.server ?? currentServer.value}:${currentUser.value?.account.id || ''}`)
const key = computed(() => currentUserDid.value)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Review note: did is the globally unique id of a bsyk account.

</script>

<template>
Expand Down
4 changes: 2 additions & 2 deletions components/nav/NavUser.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
const { busy, oauth, singleInstanceServer } = useSignIn()
const { busy, signIn, singleInstanceServer } = useSignIn()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Review note: renamed oauth to signIn to not get confused with oauth-callback

</script>

<template>
Expand All @@ -24,7 +24,7 @@ const { busy, oauth, singleInstanceServer } = useSignIn()
flex="~ row"
gap-x-1 items-center justify-center btn-solid text-sm px-2 py-1 xl:hidden
:disabled="busy"
@click="oauth()"
@click="signIn()"
>
<span v-if="busy" aria-hidden="true" block animate animate-spin preserve-3d class="rtl-flip">
<span block i-ri:loader-2-fill aria-hidden="true" />
Expand Down
4 changes: 2 additions & 2 deletions components/user/UserPicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const all = useUsers()
const router = useRouter()

function clickUser(user: UserLogin) {
if (user.account.acct === currentUser.value?.account.acct)
if (user.did === currentUser.value?.did)
router.push(getAccountRoute(user.account))
else
switchUser(user)
Expand All @@ -21,7 +21,7 @@ function clickUser(user: UserLogin) {
flex rounded
cursor-pointer
:aria-label="$t('action.switch_account')"
:class="user.account.acct === currentUser?.account.acct ? '' : 'op25 grayscale'"
:class="user.did === currentUser?.did ? '' : 'op25 grayscale'"
hover="filter-none op100"
@click="clickUser(user)"
>
Expand Down
128 changes: 6 additions & 122 deletions components/user/UserSignIn.vue
Original file line number Diff line number Diff line change
@@ -1,111 +1,30 @@
<script setup lang="ts">
import Fuse from 'fuse.js'

const input = ref<HTMLInputElement | undefined>()
const knownServers = ref<string[]>([])
const autocompleteIndex = ref(0)
const autocompleteShow = ref(false)

const { busy, error, displayError, server, oauth } = useSignIn(input)

const fuse = shallowRef(new Fuse([] as string[]))

const filteredServers = computed(() => {
if (!server.value)
return []

const results = fuse.value.search(server.value, { limit: 6 }).map(result => result.item)
if (results[0] === server.value)
return []

return results
})

function isValidUrl(str: string) {
try {
// eslint-disable-next-line no-new
new URL(str)
return true
}
catch {
return false
}
}
const { busy, error, displayError, handle, signIn } = useSignIn(input)

async function handleInput() {
const input = server.value.trim()
if (input.startsWith('https://'))
server.value = input.replace('https://', '')
const input = handle.value.trim()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Review note: removed quite some code here as you don't need to select a server with bsky as there is an integrated discovery system for a handle / did.


if (input.length)
displayError.value = false

if (
isValidUrl(`https://${input}`)
&& input.match(/^[a-z0-9-]+(\.[a-z0-9-]+)+(:\d+)?$/i)
// Do not hide the autocomplete if a result has an exact substring match on the input
&& !filteredServers.value.some(s => s.includes(input))
) {
autocompleteShow.value = false
}

else {
autocompleteShow.value = true
}
}

function toSelector(server: string) {
return server.replace(/[^\w-]/g, '-')
}
function move(delta: number) {
if (filteredServers.value.length === 0) {
autocompleteIndex.value = 0
return
}
autocompleteIndex.value = ((autocompleteIndex.value + delta) + filteredServers.value.length) % filteredServers.value.length
document.querySelector(`#${toSelector(filteredServers.value[autocompleteIndex.value])}`)?.scrollIntoView(false)
}

function onEnter(e: KeyboardEvent) {
if (autocompleteShow.value === true && filteredServers.value[autocompleteIndex.value]) {
server.value = filteredServers.value[autocompleteIndex.value]
e.preventDefault()
autocompleteShow.value = false
}
}

function escapeAutocomplete(evt: KeyboardEvent) {
if (!autocompleteShow.value)
return
autocompleteShow.value = false
evt.stopPropagation()
}

function select(index: number) {
server.value = filteredServers.value[index]
}

onMounted(async () => {
input?.value?.focus()
knownServers.value = await (globalThis.$fetch as any)('/api/list-servers')
fuse.value = new Fuse(knownServers.value, { shouldSort: true })
})

onClickOutside(input, () => {
autocompleteShow.value = false
})
</script>

<template>
<form text-center justify-center items-center max-w-150 py6 flex="~ col gap-3" @submit.prevent="oauth">
<form text-center justify-center items-center max-w-150 py6 flex="~ col gap-3" @submit.prevent="signIn">
<div flex="~ center" items-end mb2 gap-x-2>
<img :src="`/${''}logo.svg`" w-12 h-12 mxa height="48" width="48" :alt="$t('app_logo')" class="rtl-flip">
<div text-3xl>
{{ $t('action.sign_in') }}
</div>
</div>
<div>
{{ $t('user.server_address_label') }}
{{ $t('user.handle_label') }}
</div>
<div :class="error ? 'animate animate-shake-x animate-delay-100' : null">
<div
Expand All @@ -116,44 +35,17 @@ onClickOutside(input, () => {
relative
:class="displayError ? 'border-red-600 dark:border-red-400' : null"
>
<span text-secondary-light me1>https://</span>

<input
ref="input"
v-model="server"
v-model="handle"
autocapitalize="off"
inputmode="url"
outline-none bg-transparent w-full max-w-50
spellcheck="false"
autocorrect="off"
autocomplete="off"
@input="handleInput"
@keydown.down="move(1)"
@keydown.up="move(-1)"
@keydown.enter="onEnter"
@keydown.esc.prevent="escapeAutocomplete"
@focus="autocompleteShow = true"
>
<div
v-if="autocompleteShow && filteredServers.length"
absolute left-6em right-0 top="100%"
bg-base rounded border="~ base"
z-10 shadow of-auto
overflow-y-auto
class="max-h-[8rem]"
>
<button
v-for="(name, idx) in filteredServers"
:id="toSelector(name)"
:key="name"
:value="name"
px-2 py1 font-mono w-full text-left
:class="autocompleteIndex === idx ? 'text-primary font-bold' : null"
@click="select(idx)"
>
{{ name }}
</button>
</div>
</div>
<div min-h-4>
<Transition css enter-active-class="animate animate-fade-in">
Expand All @@ -163,15 +55,7 @@ onClickOutside(input, () => {
</Transition>
</div>
</div>
<div text-secondary text-sm flex>
<div i-ri:lightbulb-line me-1 />
<span>
<i18n-t keypath="user.tip_no_account">
<NuxtLink href="https://joinmastodon.org/servers" target="_blank" external class="text-primary" hover="underline">{{ $t('user.tip_register_account') }}</NuxtLink>
</i18n-t>
</span>
</div>
<button flex="~ row" gap-x-2 items-center btn-solid mt2 :disabled="!server || busy">
<button flex="~ row" gap-x-2 items-center btn-solid mt2 :disabled="!handle || busy">
<span v-if="busy" aria-hidden="true" block animate animate-spin preserve-3d class="rtl-flip">
<span block i-ri:loader-2-fill aria-hidden="true" />
</span>
Expand Down
4 changes: 2 additions & 2 deletions components/user/UserSignInEntry.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
const { busy, oauth, singleInstanceServer } = useSignIn()
const { busy, signIn, singleInstanceServer } = useSignIn()
</script>

<template>
Expand All @@ -16,7 +16,7 @@ const { busy, oauth, singleInstanceServer } = useSignIn()
v-if="singleInstanceServer"
flex="~ row" gap-x-2 items-center justify-center btn-solid text-center rounded-3
:disabled="busy"
@click="oauth()"
@click="signIn()"
>
<span v-if="busy" aria-hidden="true" block animate animate-spin preserve-3d class="rtl-flip">
<span block i-ri:loader-2-fill aria-hidden="true" />
Expand Down
10 changes: 5 additions & 5 deletions components/user/UserSwitcher.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,25 @@ const emit = defineEmits<{
}>()

const all = useUsers()
const { singleInstanceServer, oauth } = useSignIn()
const { singleInstanceServer, signIn } = useSignIn()

const sorted = computed(() => {
return [
currentUser.value!,
...all.value.filter(account => account.token !== currentUser.value?.token),
...all.value.filter(account => account.did !== currentUser.value?.did),
].filter(Boolean)
})

const router = useRouter()
function clickUser(user: UserLogin) {
if (user.account.id === currentUser.value?.account.id)
if (user.did === currentUser.value?.did)
router.push(getAccountRoute(user.account))
else
switchUser(user)
}
function processSignIn() {
if (singleInstanceServer)
oauth()
signIn()
else
openSigninDialog()
}
Expand All @@ -41,7 +41,7 @@ function processSignIn() {
>
<AccountInfo :account="user.account" :hover-card="false" square />
<div flex-auto />
<div v-if="user.token === currentUser?.token" i-ri:check-line text-primary mya text-2xl />
<div v-if="user.did === currentUser?.did" i-ri:check-line text-primary mya text-2xl />
</button>
</template>
<div border="t base" pt2>
Expand Down
8 changes: 4 additions & 4 deletions composables/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ export function provideGlobalCommands() {
const masto = useMasto()
const colorMode = useColorMode()
const userSettings = useUserSettings()
const { singleInstanceServer, oauth } = useSignIn()
const { singleInstanceServer, signIn } = useSignIn()

useCommand({
scope: 'Preferences',
Expand Down Expand Up @@ -305,7 +305,7 @@ export function provideGlobalCommands() {

onActivate() {
if (singleInstanceServer)
oauth()
signIn()
else
openSigninDialog()
},
Expand All @@ -328,13 +328,13 @@ export function provideGlobalCommands() {
parent: 'account-switch',
scope: 'Switch account',

visible: () => user.account.id !== currentUser.value?.account.id,
visible: () => user.did !== currentUser.value?.did,

name: () => t('command.switch_account', [getFullHandle(user.account)]),
icon: 'i-ri:user-shared-line',

onActivate() {
loginTo(masto, user)
loginTo(masto, user.did)
},
})))
useCommand({
Expand Down
37 changes: 30 additions & 7 deletions composables/masto/account.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import type { ProfileViewDetailed } from '@atproto/api/dist/client/types/app/bsky/actor/defs'
import type { mastodon } from 'masto'

export function getDisplayName(account: mastodon.v1.Account, options?: { rich?: boolean }) {
// TODO: remove once mastondon support was replaced
export type Account = mastodon.v1.Account | ProfileViewDetailed
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Review note: to avoid too many changes I changed most of these functions to accept bsky and mastodon accounts for now.


export function isBsykAccount(account: Account): account is ProfileViewDetailed {
return !!(account as ProfileViewDetailed).did
}

export function getDisplayName(account: Account, options?: { rich?: boolean }) {
if (isBsykAccount(account))
return account.displayName

const displayName = account.displayName || account.username || account.acct || ''
if (options?.rich)
return displayName
Expand All @@ -11,20 +22,29 @@ export function accountToShortHandle(acct: string) {
return `@${acct.includes('@') ? acct.split('@')[0] : acct}`
}

export function getShortHandle({ acct }: mastodon.v1.Account) {
if (!acct)
export function getShortHandle(account: Account) {
if (isBsykAccount(account))
return account.handle

if (!account.acct)
return ''
return accountToShortHandle(acct)
return accountToShortHandle(account.acct)
}

export function getServerName(account: mastodon.v1.Account) {
export function getServerName(account: Account) {
if (isBsykAccount(account))
return ''

if (account.acct?.includes('@'))
return account.acct.split('@')[1]
// We should only lack the server name if we're on the same server as the account
return currentInstance.value ? getInstanceDomain(currentInstance.value) : ''
}

export function getFullHandle(account: mastodon.v1.Account) {
export function getFullHandle(account: Account) {
if (isBsykAccount(account))
return account.handle

const handle = `@${account.acct}`
if (!currentUser.value || account.acct.includes('@'))
return handle
Expand All @@ -40,7 +60,10 @@ export function toShortHandle(fullHandle: string) {
return fullHandle
}

export function extractAccountHandle(account: mastodon.v1.Account) {
export function extractAccountHandle(account: Account) {
if (isBsykAccount(account))
return account.handle

let handle = getFullHandle(account).slice(1)
const uri = currentInstance.value ? getInstanceDomain(currentInstance.value) : currentServer.value
if (currentInstance.value && handle.endsWith(`@${uri}`))
Expand Down
4 changes: 1 addition & 3 deletions composables/masto/masto.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import type { Pausable } from '@vueuse/core'
import type { mastodon } from 'masto'
import type { Ref } from 'vue'
import type { ElkInstance } from '../users'
import { createRestAPIClient, createStreamingAPIClient } from 'masto'
import { createRestAPIClient, createStreamingAPIClient, type mastodon } from 'masto'
import type { UserLogin } from '~/types'

export function createMasto() {
Expand Down
2 changes: 1 addition & 1 deletion composables/masto/publish.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { DraftItem } from '~~/types'
import type { mastodon } from 'masto'
import type { Ref } from 'vue'
import { fileOpen } from 'browser-fs-access'
import type { DraftItem } from '~/types'

export function usePublish(options: {
draftItem: Ref<DraftItem>
Expand Down
Loading
Loading