-
+
New token
@@ -38,7 +39,7 @@ useSeoMeta({ title: 'New token', robots: 'noindex' })
-
+
diff --git a/app/pages/me/devices.vue b/app/pages/me/devices.vue
index 418f2843..22c365b6 100644
--- a/app/pages/me/devices.vue
+++ b/app/pages/me/devices.vue
@@ -70,10 +70,11 @@ useSeoMeta({ title: 'Devices and tokens', robots: 'noindex' })
-
+
+
-
+
Devices and tokens
diff --git a/layers/identity/app/components/HeaderAccount.vue b/layers/identity/app/components/HeaderAccount.vue
index 07ec462f..3d802afd 100644
--- a/layers/identity/app/components/HeaderAccount.vue
+++ b/layers/identity/app/components/HeaderAccount.vue
@@ -15,14 +15,17 @@ const { variant = 'bar' } = defineProps<{
variant?: 'bar' | 'menu'
}>()
-const { state, logout } = useAuth()
+const { state, logout, isLoading } = useAuth()
+
+const route = useRoute()
+const signInTo = computed(() => route.path === '/login' ? route.fullPath : { path: '/login', query: { return_to: route.fullPath } })
const accountItems = computed(() => {
if (state.value._tag !== 'signed-in')
return []
return [
{ label: `@${state.value.user.login}`, icon: 'i-lucide-user', to: '/me' },
- { label: 'Sign out', icon: 'i-lucide-log-out', onSelect: () => { void logout() } },
+ { label: 'Sign out', disabled: isLoading.value, icon: 'i-lucide-log-out', onSelect: () => { void logout() } },
]
})
@@ -39,7 +42,7 @@ const accountItems = computed(() => {
/>
(() => {
class="size-11 justify-center p-0"
:aria-label="`Signed in as @${state.user.login}`"
>
-
+
+
+
+
+
+ {{ state.user.login.charAt(0) }}
+
+
+
@@ -82,7 +92,7 @@ const accountItems = computed(() => {
/>
(() => {
variant="ghost"
block
class="min-h-11 justify-start"
+ :loading="isLoading"
@click="() => { void logout() }"
/>
diff --git a/layers/identity/app/composables/useAuth.ts b/layers/identity/app/composables/useAuth.ts
index 1a34d64e..df08f73f 100644
--- a/layers/identity/app/composables/useAuth.ts
+++ b/layers/identity/app/composables/useAuth.ts
@@ -17,18 +17,31 @@ export type AuthState
// Backed by nuxt-auth-utils useUserSession().
export function useAuth() {
- const { ready, loggedIn, user, clear, fetch: fetchSession } = useUserSession()
+ const { ready, loggedIn, user, session, fetch: fetchSession } = useUserSession()
const state = computed(() => {
if (!ready.value)
return { _tag: 'pending' }
return user.value ? { _tag: 'signed-in', user: user.value } : { _tag: 'anonymous' }
})
const isAuthenticated = computed(() => loggedIn.value)
- const isLoading = ref(false)
+ const isLoading = useState('auth:logging-out', () => false)
+ const toast = useToast()
async function logout() {
- await $fetch('/api/auth/logout', { method: 'POST' })
- await clear()
- await navigateTo('/')
+ if (isLoading.value)
+ return
+ isLoading.value = true
+ const result = await $fetch('/api/auth/logout', { method: 'POST', retry: false })
+ .then(() => ({ _tag: 'ok' as const }))
+ .catch((cause: unknown) => ({ _tag: 'err' as const, cause }))
+ isLoading.value = false
+ if (result._tag === 'err') {
+ console.warn('[logout]', result.cause)
+ toast.add({ title: 'Could not sign out', description: 'Check your connection and try again.', color: 'error' })
+ return
+ }
+ session.value = null
+ removeNuxtQueries()
+ await navigateTo('/', { external: true })
}
function loginUrl(opts: { returnTo?: string, action?: string } = {}): string {
const params = new URLSearchParams()
diff --git a/layers/identity/app/pages/login.vue b/layers/identity/app/pages/login.vue
index 247230b9..87e68925 100644
--- a/layers/identity/app/pages/login.vue
+++ b/layers/identity/app/pages/login.vue
@@ -26,10 +26,9 @@ useSeoMeta({
-
-
-
-
+
+
+
Sign in to skilld
@@ -42,13 +41,15 @@ useSeoMeta({
label="Continue with GitHub"
icon="i-lucide-github"
trailing-icon="i-lucide-arrow-right"
- size="sm"
+ size="md"
+ class="min-h-11"
block
/>
Couldn't complete sign-in. Try again.
diff --git a/layers/identity/app/pages/me/index.vue b/layers/identity/app/pages/me/index.vue
index bb57dc9f..aa002510 100644
--- a/layers/identity/app/pages/me/index.vue
+++ b/layers/identity/app/pages/me/index.vue
@@ -26,8 +26,8 @@ interface LikedSkill {
registryPath: string
}
-const { data: me } = await useNuxtRpcQuery(identityAccountQueries.me(), identityAccountQueryOptions)
-const { data: subs } = await useNuxtRpcQuery(identityAccountQueries.subscriptions(), identityAccountQueryOptions)
+const { data: me, error: accountError, status: accountStatus, refresh: retryAccount } = await useNuxtRpcQuery(identityAccountQueries.me(), identityAccountQueryOptions)
+const { data: subs, error: subscriptionsError, status: subscriptionsStatus, refresh: retrySubscriptions } = await useNuxtRpcQuery(identityAccountQueries.subscriptions(), identityAccountQueryOptions)
const {
data: likes,
error: likesError,
@@ -124,12 +124,19 @@ const emailForm = reactive({
email_opt_in: !!me.value?.email_opt_in,
weekly_opt_in: me.value?.weekly_opt_in ?? true,
})
+watch(showEmail, (open) => {
+ if (open && me.value) {
+ emailForm.digest_email = me.value.digest_email ?? me.value.email ?? ''
+ emailForm.email_opt_in = me.value.email_opt_in
+ emailForm.weekly_opt_in = me.value.weekly_opt_in
+ }
+})
const emailMissingAddress = computed(() =>
(emailForm.email_opt_in || emailForm.weekly_opt_in)
&& !/^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/.test(emailForm.digest_email.trim()),
)
async function saveEmail() {
- if (emailMissingAddress.value)
+ if (emailMissingAddress.value || saveEmailMutation.pending.value)
return
const saved = await saveEmailMutation.mutateSafe({ ...emailForm })
if (saved._tag === 'ok')
@@ -236,8 +243,16 @@ async function deleteAccount() {
-