import { createQuicksliceClient } from 'quickslice-client-js'; import { router } from '../router.js'; import { grainApi } from './grain-api.js'; class AuthService { #client = null; #user = null; #listeners = new Set(); #initialized = false; async init() { if (this.#initialized) return; this.#client = await createQuicksliceClient({ server: 'https://quickslice-production-9cf4.up.railway.app', clientId: 'client_h62Ea0FUeXTJ4pWBg4ZIkQ', scope: 'atproto blob:image/* repo:social.grain.gallery repo:social.grain.gallery.item repo:social.grain.photo repo:social.grain.favorite repo:social.grain.graph.follow repo:social.grain.actor.profile repo:social.grain.comment' }); // Handle OAuth callback if present if (window.location.search.includes('code=')) { await this.#client.handleRedirectCallback(); // Check if user has a Grain profile const hasProfile = await grainApi.hasGrainProfile(this.#client); if (!hasProfile) { // First-time user - redirect to onboarding window.location.replace('/onboarding'); return; } // Existing user - redirect to their destination const returnUrl = sessionStorage.getItem('oauth_return_url') || '/'; sessionStorage.removeItem('oauth_return_url'); window.location.replace(returnUrl); return; } // Load user if authenticated if (await this.#client.isAuthenticated()) { await this.#loadUser(); } this.#initialized = true; } async #loadUser() { const clientUser = this.#client.getUser(); const did = clientUser?.did || clientUser?.sub; const result = await this.#client.query(` query { viewer { did handle socialGrainActorProfileByDid { displayName avatar { url(preset: "avatar") } } } } `); const viewer = result.viewer; const grainProfile = viewer?.socialGrainActorProfileByDid; this.#user = { did: did || viewer?.did, handle: viewer?.handle, displayName: grainProfile?.displayName || '', avatar: grainProfile?.avatar || null }; this.#notify(); } async login(handle) { sessionStorage.setItem('oauth_return_url', window.location.pathname); sessionStorage.setItem('oauth_handle', handle); window.location.href = '/oauth/callback?start=1'; } async startOAuthFromCallback() { const handle = sessionStorage.getItem('oauth_handle'); sessionStorage.removeItem('oauth_handle'); if (!handle) { router.replace('/'); return; } await this.#client.loginWithRedirect({ handle }); } logout() { this.#client.logout(); } get user() { return this.#user; } get isAuthenticated() { return !!this.#user; } subscribe(callback) { this.#listeners.add(callback); return () => this.#listeners.delete(callback); } #notify() { this.#listeners.forEach(cb => cb(this.#user)); } getClient() { return this.#client; } async refreshUser() { await this.#loadUser(); } } // Preserve auth instance across HMR export const auth = window.__auth || (window.__auth = new AuthService());