Important Fixes, New mechanics, and many more
Some checks failed
CI / validate (push) Failing after 9m21s

This commit is contained in:
2026-08-15 17:37:57 +05:00
parent 1774496cf9
commit 438e5af0ad
56 changed files with 5090 additions and 119 deletions

View File

@@ -0,0 +1,33 @@
interface DngApiOptions {
method?: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'
body?: Record<string, unknown>
query?: Record<string, string | number | boolean | undefined>
}
export function useDngApi() {
const auth = useDngAuth()
async function api<T>(path: string, options: DngApiOptions = {}): Promise<T> {
const token = await auth.accessToken()
try {
const response = await $fetch(path, {
...options,
headers: { Authorization: `Bearer ${token}` },
} as Parameters<typeof $fetch>[1])
return response as T
} catch (cause) {
const error = cause as { statusCode?: number; status?: number; response?: { status?: number } }
const status = error.statusCode ?? error.status ?? error.response?.status
if (status === 401) {
auth.invalidate()
if (import.meta.client) {
localStorage.setItem('dng-post-auth-redirect', window.location.pathname + window.location.search)
await navigateTo('/')
}
}
throw cause
}
}
return { api }
}

View File

@@ -0,0 +1,188 @@
interface DngAuthUser {
id: string
email?: string | null
is_anonymous?: boolean
user_metadata?: Record<string, unknown>
}
interface DngSession {
accessToken: string
refreshToken: string
expiresAt: number
user: DngAuthUser
}
interface AuthResponse {
access_token: string
refresh_token: string
expires_in: number
user: DngAuthUser
}
const STORAGE_KEY = 'dng-auth-session'
export function useDngAuth() {
const config = useRuntimeConfig()
const session = useState<DngSession | null>('dng-auth-session', () => null)
const hydrated = useState('dng-auth-hydrated', () => false)
function authHeaders(accessToken?: string) {
return {
apikey: config.public.supabaseAnonKey,
'Content-Type': 'application/json',
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
}
}
function persist(next: DngSession | null) {
session.value = next
if (!import.meta.client) return
if (next) localStorage.setItem(STORAGE_KEY, JSON.stringify(next))
else localStorage.removeItem(STORAGE_KEY)
}
function normalizeUser(user: DngAuthUser): DngAuthUser {
return {
...user,
email: user.email || undefined,
}
}
function parseStoredSession(value: string): DngSession | null {
try {
const saved = JSON.parse(value) as Partial<DngSession>
if (
typeof saved.accessToken !== 'string' || !saved.accessToken
|| typeof saved.refreshToken !== 'string' || !saved.refreshToken
|| typeof saved.expiresAt !== 'number' || !Number.isFinite(saved.expiresAt)
|| !saved.user || typeof saved.user.id !== 'string' || !saved.user.id
) return null
return { ...saved, user: normalizeUser(saved.user) } as DngSession
} catch {
return null
}
}
async function fetchUser(accessToken: string) {
const user = await $fetch<DngAuthUser>(`${config.public.supabaseUrl}/auth/v1/user`, {
headers: authHeaders(accessToken),
})
return normalizeUser(user)
}
function fromResponse(response: AuthResponse): DngSession {
return {
accessToken: response.access_token,
refreshToken: response.refresh_token,
expiresAt: Date.now() + response.expires_in * 1000,
user: normalizeUser(response.user),
}
}
async function refresh() {
if (!session.value?.refreshToken) return null
try {
const response = await $fetch<AuthResponse>(`${config.public.supabaseUrl}/auth/v1/token?grant_type=refresh_token`, {
method: 'POST',
headers: authHeaders(),
body: { refresh_token: session.value.refreshToken },
})
const next = fromResponse(response)
persist(next)
return next
} catch {
persist(null)
return null
}
}
async function restore() {
if (!import.meta.client || hydrated.value) return session.value
let restoredFromStorage = false
const hash = new URLSearchParams(window.location.hash.replace(/^#/, ''))
const hashAccessToken = hash.get('access_token')
const hashRefreshToken = hash.get('refresh_token')
if (hashAccessToken && hashRefreshToken) {
const expiresIn = Number(hash.get('expires_in') ?? 3600)
const user = await fetchUser(hashAccessToken)
persist({ accessToken: hashAccessToken, refreshToken: hashRefreshToken, expiresAt: Date.now() + expiresIn * 1000, user })
history.replaceState(null, '', `${window.location.pathname}${window.location.search}`)
} else {
const saved = localStorage.getItem(STORAGE_KEY)
if (saved) {
const parsed = parseStoredSession(saved)
if (parsed) {
session.value = parsed
restoredFromStorage = true
} else {
persist(null)
}
}
}
if (session.value && session.value.expiresAt <= Date.now() + 60_000) {
await refresh()
} else if (session.value && restoredFromStorage) {
try {
const user = await fetchUser(session.value.accessToken)
persist({ ...session.value, user })
} catch {
// A saved token may belong to an old Supabase project or have been
// revoked before its local expiry. Refresh once, then discard it.
await refresh()
}
}
hydrated.value = true
return session.value
}
async function requestMagicLink(email: string) {
const redirectTo = `${window.location.origin}/auth/callback`
await $fetch(`${config.public.supabaseUrl}/auth/v1/otp?redirect_to=${encodeURIComponent(redirectTo)}`, {
method: 'POST',
headers: authHeaders(),
body: { email: email.trim().toLowerCase(), create_user: true },
})
}
async function signInAnonymously() {
const response = await $fetch<AuthResponse>(`${config.public.supabaseUrl}/auth/v1/signup`, {
method: 'POST',
headers: authHeaders(),
body: {
data: { display_name: 'Guest Adventurer' },
gotrue_meta_security: {},
},
})
const next = fromResponse(response)
persist(next)
hydrated.value = true
return next
}
async function accessToken() {
await restore()
if (!session.value) throw new Error('Enter the alpha to continue.')
if (session.value.expiresAt <= Date.now() + 60_000) await refresh()
if (!session.value) throw new Error('Your session expired. Sign in again.')
return session.value.accessToken
}
async function signOut() {
const token = session.value?.accessToken
persist(null)
if (!token) return
await $fetch(`${config.public.supabaseUrl}/auth/v1/logout`, {
method: 'POST',
headers: authHeaders(token),
}).catch(() => undefined)
}
function invalidate() {
persist(null)
hydrated.value = true
}
return { session, hydrated, restore, refresh, requestMagicLink, signInAnonymously, accessToken, signOut, invalidate }
}