import { Client } from "@versia/client"; import type { Account, CredentialApplication, CustomEmoji, Instance, RolePermission, Token, } from "@versia/client/schemas"; import { defineStore } from "pinia"; import { toast } from "vue-sonner"; import type { z } from "zod"; import pkg from "~~/package.json"; /** * Represents an identity with associated tokens, account, instance, permissions, and emojis. */ export interface Identity { id: string; token: z.infer; account: z.infer; instance: z.infer; emojis: z.infer[]; } interface AuthStoreState { identities: Identity[]; activeIdentityId: string | null; applications: { [domain: string]: z.infer; }; } export const useAuthStore = defineStore("auth", { state: (): AuthStoreState => ({ identities: [], activeIdentityId: null, applications: {}, }), getters: { identity(state): Identity | null { return state.activeIdentityId ? state.identities.find( (id) => id.id === state.activeIdentityId, ) || null : null; }, emojis(): z.infer[] { return this.identity?.emojis || []; }, instance(): z.infer | null { return this.identity?.instance || null; }, account(): z.infer | null { return this.identity?.account || null; }, application(): z.infer | null { if (!this.identity) { return null; } return this.applications[this.identity.instance.domain] || null; }, token(): z.infer | null { return this.identity?.token || null; }, permissions(): RolePermission[] { const roles = this.account?.roles ?? []; return roles .flatMap((r) => r.permissions) .filter((p, i, arr) => arr.indexOf(p) === i); }, isSignedIn(state): boolean { return state.activeIdentityId !== null; }, client(): Client { const apiHost = window.location.origin; const domain = this.identity?.instance.domain; return new Client( domain ? new URL(`https://${domain}`) : new URL(apiHost), this.identity?.token.access_token ?? undefined, { globalCatch: (error) => { toast.error( error.response?.data.error ?? "No error message provided", ); }, throwOnError: false, }, ); }, }, actions: { setActiveIdentity(id: string | null) { this.activeIdentityId = id; }, updateActiveIdentity(data: Partial) { if (this.activeIdentityId) { this.$patch({ identities: this.identities.map((id) => id.id === this.activeIdentityId ? { ...id, ...data } : id, ), }); } }, async createApp( origin: URL, ): Promise> { const redirectUri = new URL( `/callback?${new URLSearchParams({ domain: origin.host }).toString()}`, useRequestURL().origin, ); const client = new Client(origin); const output = await client.createApp("Versia-FE", { scopes: ["read", "write", "follow", "push"], redirect_uris: redirectUri.href, // @ts-expect-error Package.json types are missing this field website: pkg.homepage ?? undefined, }); this.applications[origin.host] = output.data; return output.data; }, async startSignIn(origin: URL) { const client = new Client(origin); const appData = this.applications[origin.host] ?? (await this.createApp(origin)); const url = await client.generateAuthUrl( appData.client_id, appData.client_secret, { scopes: ["read", "write", "follow", "push"], redirect_uri: appData.redirect_uris[0], }, ); window.location.href = url; }, async finishSignIn(code: string, origin: URL): Promise { const appData = this.applications[origin.host]; if (!appData) { toast.error(`No application data found for ${origin.host}`); return; } const client = new Client(origin); const token = await client.fetchAccessToken( appData.client_id, appData.client_secret, code, appData.redirect_uris[0], ); const authClient = new Client(origin, token.data.access_token); const [account, instance, emojis] = await Promise.all([ authClient.verifyAccountCredentials(), authClient.getInstance(), authClient.getInstanceCustomEmojis(), ]); if ( !this.identities.find((i) => i.account.id === account.data.id) ) { const newIdentity: Identity = { id: crypto.randomUUID(), token: token.data, account: account.data, instance: instance.data, emojis: emojis.data, }; this.identities.push(newIdentity); this.activeIdentityId = newIdentity.id; } else { this.activeIdentityId = this.identities.find( (i) => i.account.id === account.data.id, )?.id as string; } }, async signOut(identityId?: string) { const id = identityId ?? this.activeIdentityId; const identity = this.identities.find((i) => i.id === id); if (!identity) { return; } const appData = this.applications[identity.instance.domain]; if (!appData) { return; } await this.client.revokeToken( appData.client_id, appData.client_secret, identity.token.access_token, ); if (this.activeIdentityId === id) { this.activeIdentityId = null; } this.identities = this.identities.filter((i) => i.id !== id); }, }, persist: { storage: localStorage, }, });