refactor: 🛂 Refactor account switching to ask for instance
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 54s
Deploy to GitHub Pages / build (push) Failing after 1m45s
Deploy to GitHub Pages / deploy (push) Has been skipped
Docker / build (push) Failing after 17s
Mirror to Codeberg / Mirror (push) Failing after 1s

This commit is contained in:
Jesse Wierzbinski 2025-02-09 20:14:27 +01:00
parent 7e9ccbc932
commit 2e67e7858f
No known key found for this signature in database
9 changed files with 71 additions and 104 deletions

View file

@ -61,7 +61,7 @@ for (const name of [
} }
const issuerRedirectUrl = (issuerId: string) => { const issuerRedirectUrl = (issuerId: string) => {
const url = new URL("/oauth/sso", useBaseUrl().value); const url = new URL("/oauth/sso", client.value.url);
for (const name of [ for (const name of [
"redirect_uri", "redirect_uri",

View file

@ -21,7 +21,7 @@
<UserPlus /> <UserPlus />
{{ m.sunny_pink_hyena_walk() }} {{ m.sunny_pink_hyena_walk() }}
</Button> </Button>
<Button variant="secondary" size="lg" @click="signOut()" v-if="identity"> <Button variant="secondary" size="lg" @click="signOut(appData, identity)" v-if="identity">
<LogOut /> <LogOut />
{{ m.sharp_big_mallard_reap() }} {{ m.sharp_big_mallard_reap() }}
</Button> </Button>
@ -47,7 +47,9 @@
identity.account.display_name identity.account.display_name
}}</span> }}</span>
<span class="truncate text-xs">@{{ <span class="truncate text-xs">@{{
identity.account.acct identity.account.username
}}@{{
identity.instance.domain
}}</span> }}</span>
</div> </div>
</Button> </Button>
@ -64,7 +66,7 @@
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuGroup> </DropdownMenuGroup>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem @click="signOut()" v-if="identity"> <DropdownMenuItem @click="signOut(appData, identity)" v-if="identity">
<LogOut /> <LogOut />
{{ m.sharp_big_mallard_reap() }} {{ m.sharp_big_mallard_reap() }}
</DropdownMenuItem> </DropdownMenuItem>
@ -98,48 +100,7 @@ import {
const appData = useAppData(); const appData = useAppData();
const isMobile = useMediaQuery("(max-width: 768px)"); const isMobile = useMediaQuery("(max-width: 768px)");
const signInAction = () => signIn(appData, new URL(useBaseUrl().value)); const signInAction = async () => signIn(appData, await askForInstance());
const signOut = async (userId?: string) => {
const id = toast.loading("Signing out...");
if (!(appData.value && identity.value)) {
toast.dismiss(id);
toast.error("No app or identity data to sign out");
return;
}
const identityToRevoke = userId
? identities.value.find((i) => i.account.id === userId)
: identity.value;
if (!identityToRevoke) {
toast.dismiss(id);
toast.error("No identity to revoke");
return;
}
// Don't do anything on error, as Versia Server doesn't implement the revoke endpoint yet
await client.value
?.revokeToken(
appData.value.client_id,
identityToRevoke.tokens.access_token,
identityToRevoke.tokens.access_token,
)
.catch(() => {
// Do nothing
});
if (!userId) {
identity.value = null;
await navigateTo("/");
return;
}
identities.value = identities.value.filter((i) => i.id !== userId);
toast.dismiss(id);
toast.success("Signed out");
};
const switchAccount = async (userId: string) => { const switchAccount = async (userId: string) => {
if (userId === identity.value?.account.id) { if (userId === identity.value?.account.id) {

View file

@ -12,7 +12,7 @@
<div class="grid flex-1 text-left text-sm leading-tight"> <div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{{ instance?.title ?? m.short_zippy_felix_kick() <span class="truncate font-semibold">{{ instance?.title ?? m.short_zippy_felix_kick()
}}</span> }}</span>
<span class="truncate text-xs">{{ m.top_active_ocelot_cure() }}</span> <span class="truncate text-xs">{{ instance?.description ?? m.top_active_ocelot_cure() }}</span>
</div> </div>
<!-- <ChevronsUpDown class="ml-auto" /> --> <!-- <ChevronsUpDown class="ml-auto" /> -->
</SidebarMenuButton> </SidebarMenuButton>

View file

@ -1,10 +0,0 @@
export const useBaseUrl = () => {
// Check if running on Onion URL
if (useRequestURL().hostname.endsWith(".onion")) {
return ref(
useRuntimeConfig().public.onionApiHost ?? useRequestURL().origin,
);
}
return ref(useRuntimeConfig().public.apiHost ?? useRequestURL().origin);
};

View file

@ -1,7 +1,7 @@
<template> <template>
<Sidebar> <Sidebar>
<slot v-if="!route.meta.requiresAuth || identity" /> <slot v-if="!route.meta.requiresAuth || identity" />
<Card v-else class="shadow-none bg-transparent border-none p-4 max-w-md mx-auto"> <Card v-else class="shadow-none border-none p-4 max-w-md mx-auto">
<CardHeader class="text-center gap-y-4"> <CardHeader class="text-center gap-y-4">
<CardTitle>{{ m.sunny_quick_lionfish_flip() }}</CardTitle> <CardTitle>{{ m.sunny_quick_lionfish_flip() }}</CardTitle>
<CardDescription> <CardDescription>
@ -35,7 +35,7 @@ import * as m from "~/paraglide/messages.js";
import { SettingIds } from "~/settings"; import { SettingIds } from "~/settings";
const appData = useAppData(); const appData = useAppData();
const signInAction = () => signIn(appData, new URL(useBaseUrl().value)); const signInAction = async () => signIn(appData, await askForInstance());
const colorMode = useColorMode(); const colorMode = useColorMode();
const themeSetting = useSetting(SettingIds.Theme); const themeSetting = useSetting(SettingIds.Theme);
const { n, d } = useMagicKeys(); const { n, d } = useMagicKeys();

View file

@ -256,12 +256,6 @@ export default defineNuxtConfig({
}, },
], ],
}, },
runtimeConfig: {
public: {
apiHost: "https://beta.versia.social",
onionApiHost: undefined,
},
},
devtools: { devtools: {
enabled: false, enabled: false,

View file

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { Client } from "@versia/client"; import { Client } from "@versia/client";
import { AlertCircle, AppWindow, Loader } from "lucide-vue-next"; import { AlertCircle, Loader } from "lucide-vue-next";
import { confirmModalService } from "~/components/modals/composable";
import UserAuthForm from "~/components/oauth/login.vue"; import UserAuthForm from "~/components/oauth/login.vue";
import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
@ -12,41 +11,19 @@ useHead({
title: m.fuzzy_sea_moth_absorb(), title: m.fuzzy_sea_moth_absorb(),
}); });
const baseUrl = useBaseUrl(); const baseUrl = useRequestURL();
const client = computed(() => new Client(new URL(baseUrl.value))); const client = computed(() => new Client(new URL(baseUrl)));
const instance = useInstanceFromClient(client); const instance = useInstanceFromClient(client);
const { const {
error, error,
error_description, error_description,
redirect_uri, redirect_uri,
instance_switch_uri,
response_type, response_type,
client_id, client_id,
scope, scope,
} = useUrlSearchParams(); } = useUrlSearchParams();
const hasValidUrlSearchParams = const hasValidUrlSearchParams =
redirect_uri && response_type && client_id && scope; redirect_uri && response_type && client_id && scope;
const getHost = (uri: string) => new URL(uri).host;
const changeInstance = async () => {
const { confirmed, value } = await confirmModalService.confirm({
title: m.sharp_alive_anteater_fade(),
inputType: "url",
message: m.noble_misty_rook_slide(),
});
if (confirmed && value) {
// Redirect to the client's instance switch URI
const url = new URL(instance_switch_uri as string);
url.searchParams.set("origin", value);
await navigateTo(url.toString(), {
external: true,
});
}
};
</script> </script>
<template> <template>
@ -92,16 +69,12 @@ const changeInstance = async () => {
{{ m.novel_fine_stork_snap() }} {{ m.novel_fine_stork_snap() }}
</h1> </h1>
<p class="text-sm text-muted-foreground" v-html="m.smug_main_whale_snip({ <p class="text-sm text-muted-foreground" v-html="m.smug_main_whale_snip({
host: getHost(baseUrl), host: baseUrl.host,
})"> })">
</p> </p>
</div> </div>
<template v-if="instance && hasValidUrlSearchParams"> <template v-if="instance && hasValidUrlSearchParams">
<UserAuthForm :instance="instance" /> <UserAuthForm :instance="instance" />
<Button variant="ghost" @click="changeInstance" v-if="instance_switch_uri">
<AppWindow />
{{ m.muddy_topical_pelican_gasp() }}
</Button>
</template> </template>
<div v-else-if="hasValidUrlSearchParams" class="p-4 flex items-center justify-center h-48"> <div v-else-if="hasValidUrlSearchParams" class="p-4 flex items-center justify-center h-48">
<Loader class="size-8 animate-spin" /> <Loader class="size-8 animate-spin" />

View file

@ -160,7 +160,7 @@ const schema = toTypedSchema(
}), }),
); );
const instance = useInstanceFromClient(new Client(new URL(useBaseUrl().value))); const instance = useInstanceFromClient(new Client(client.value.url));
const form = useForm({ const form = useForm({
validationSchema: schema, validationSchema: schema,
}); });

View file

@ -2,24 +2,43 @@ import type { Client } from "@versia/client";
import type { ApplicationData } from "@versia/client/types"; import type { ApplicationData } from "@versia/client/types";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { toast } from "vue-sonner"; import { toast } from "vue-sonner";
import { confirmModalService } from "~/components/modals/composable";
import pkg from "~/package.json";
import * as m from "~/paraglide/messages.js"; import * as m from "~/paraglide/messages.js";
const getRedirectUri = () => new URL("/", useRequestURL().origin);
export const askForInstance = async (): Promise<URL> => {
const { confirmed, value } = await confirmModalService.confirm({
title: m.sharp_alive_anteater_fade(),
inputType: "url",
message: m.noble_misty_rook_slide(),
});
if (confirmed && value) {
return new URL(URL.canParse(value) ? value : `https://${value}`);
}
throw new Error("No instance provided");
};
export const signIn = async ( export const signIn = async (
appData: Ref<ApplicationData | null>, appData: Ref<ApplicationData | null>,
origin: URL, origin: URL,
) => { ) => {
const id = toast.loading(m.level_due_ox_greet()); const id = toast.loading(m.level_due_ox_greet());
const redirectUri = new URL("/", useRequestURL().origin);
const client = useClient(origin); const client = useClient(origin);
redirectUri.searchParams.append("origin", client.value.url.origin); const redirectUri = getRedirectUri();
const output = await client.value.createApp("Versia", { redirectUri.searchParams.append("origin", origin.toString());
const output = await client.value.createApp("Versia-FE", {
scopes: ["read", "write", "follow", "push"], scopes: ["read", "write", "follow", "push"],
redirect_uris: redirectUri.toString(), redirect_uris: redirectUri.toString(),
website: useBaseUrl().value, // @ts-expect-error Package.json types are missing this field
website: pkg.homepage ?? undefined,
}); });
if (!output?.data) { if (!output?.data) {
@ -59,9 +78,9 @@ export const signInWithCode = (
origin: URL, origin: URL,
) => { ) => {
const client = useClient(origin); const client = useClient(origin);
const redirectUri = new URL("/", useRequestURL().origin); const redirectUri = getRedirectUri();
redirectUri.searchParams.append("origin", client.value.url.origin); redirectUri.searchParams.append("origin", origin.toString());
client.value client.value
?.fetchAccessToken( ?.fetchAccessToken(
@ -105,3 +124,33 @@ export const signInWithCode = (
window.location.pathname = "/"; window.location.pathname = "/";
}); });
}; };
export const signOut = async (
appData: ApplicationData | null,
identityToRevoke: Identity,
) => {
const id = toast.loading("Signing out...");
if (!appData) {
toast.dismiss(id);
toast.error("No app or identity data to sign out");
return;
}
// Don't do anything on error, as Versia Server doesn't implement the revoke endpoint yet
await client.value
?.revokeToken(
appData.client_id,
identityToRevoke.tokens.access_token,
identityToRevoke.tokens.access_token,
)
.catch(() => {
// Do nothing
});
identities.value = identities.value.filter(
(i) => i.id !== identityToRevoke.id,
);
toast.dismiss(id);
toast.success("Signed out");
};