Switched frontend API calls to Nuxt
This commit is contained in:
1 parent
4621a81384
commit
47db7abd6a
5 files changed
+89
-69
No files matched your search
@@ -3,11 +3,11 @@ const { data: genresData } = await useApiFetch<Genre[]>("/genres/random")
|
|||||||
|
|
||||||
const data = await Promise.allSettled(
|
const data = await Promise.allSettled(
|
||||||
(genresData.value ?? []).map(async (genre: Genre) => {
|
(genresData.value ?? []).map(async (genre: Genre) => {
|
||||||
const res = await cfetch(`/genres/albums/${genre._id}`)
|
const albums = await cfetch<Album[]>(`/genres/albums/${genre._id}`)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
genre,
|
genre,
|
||||||
albums: res.data,
|
albums,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
).then((results) =>
|
).then((results) =>
|
||||||
|
|||||||
@@ -6,14 +6,11 @@ export function useApiFetch<DataT = any, ErrorT = any>(
|
|||||||
options?: UseFetchOptions<DataT, ErrorT>,
|
options?: UseFetchOptions<DataT, ErrorT>,
|
||||||
authorize: boolean = true,
|
authorize: boolean = true,
|
||||||
): ReturnType<typeof useFetch<DataT, ErrorT>> {
|
): ReturnType<typeof useFetch<DataT, ErrorT>> {
|
||||||
const baseUrl = import.meta.env.VITE_PUBLIC_BACKEND
|
|
||||||
|
|
||||||
if (!baseUrl) throw new Error("BACKEND URL not set")
|
|
||||||
|
|
||||||
const { sessionToken, refreshSessionToken } = useAuth()
|
const { sessionToken, refreshSessionToken } = useAuth()
|
||||||
|
|
||||||
const defaults: UseFetchOptions<DataT, ErrorT> = {
|
const defaults: UseFetchOptions<DataT, ErrorT> = {
|
||||||
baseURL: baseUrl,
|
// The migrated API lives in this Nuxt app under server/api.
|
||||||
|
baseURL: "/api",
|
||||||
|
|
||||||
retry: 1,
|
retry: 1,
|
||||||
retryDelay: 0,
|
retryDelay: 0,
|
||||||
@@ -29,11 +26,9 @@ export function useApiFetch<DataT = any, ErrorT = any>(
|
|||||||
|
|
||||||
if (!newToken) return
|
if (!newToken) return
|
||||||
|
|
||||||
options.headers = {
|
const headers = new Headers(options.headers as HeadersInit)
|
||||||
...(options.headers as unknown as Record<string, string>),
|
headers.set("Authorization", `Bearer ${newToken}`)
|
||||||
// @ts-ignore
|
options.headers = headers
|
||||||
Authorization: `Bearer ${newToken}`,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { appendResponseHeader } from "h3"
|
||||||
|
|
||||||
type SessionResponse = {
|
type SessionResponse = {
|
||||||
sessionToken: string
|
sessionToken: string
|
||||||
expireAt: string
|
expireAt: string
|
||||||
@@ -50,9 +52,28 @@ export const useAuth = () => {
|
|||||||
const refreshSessionToken = async (): Promise<string | null> => {
|
const refreshSessionToken = async (): Promise<string | null> => {
|
||||||
if (refreshPromise) return refreshPromise
|
if (refreshPromise) return refreshPromise
|
||||||
|
|
||||||
refreshPromise = $fetch<SessionResponse>("/api/auth/session", {
|
refreshPromise = (async () => {
|
||||||
method: "POST",
|
const response = await $fetch.raw<SessionResponse>(
|
||||||
})
|
"/api/auth/session",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: import.meta.server
|
||||||
|
? useRequestHeaders(["cookie", "authorization"])
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (import.meta.server) {
|
||||||
|
const event = useRequestEvent()
|
||||||
|
if (event) {
|
||||||
|
for (const cookie of response.headers.getSetCookie()) {
|
||||||
|
appendResponseHeader(event, "set-cookie", cookie)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response._data!
|
||||||
|
})()
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
sessionToken.value = response.sessionToken
|
sessionToken.value = response.sessionToken
|
||||||
return response.sessionToken
|
return response.sessionToken
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ const save = async (value: any) => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const albumResponse = await cfetch("/admin/album", {
|
const albumResponse = await cfetch<{ _id: string }>("/admin/album", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
data: {
|
data: {
|
||||||
name: `${value.name} - Single`,
|
name: `${value.name} - Single`,
|
||||||
@@ -156,7 +156,7 @@ const save = async (value: any) => {
|
|||||||
file: value.cover,
|
file: value.cover,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
value.album = albumResponse.data._id
|
value.album = albumResponse._id
|
||||||
} else if (!value.album) {
|
} else if (!value.album) {
|
||||||
toast.show(
|
toast.show(
|
||||||
"Choose an album or create a single album",
|
"Choose an album or create a single album",
|
||||||
|
|||||||
+56
-52
@@ -1,78 +1,82 @@
|
|||||||
import axios, { type AxiosRequestConfig, type AxiosResponse } from "axios"
|
import type { FetchOptions } from "ofetch"
|
||||||
import defu from "defu"
|
|
||||||
|
|
||||||
const hasFile = (value: any): boolean => {
|
type RequestConfig = Omit<FetchOptions, "body" | "query"> & {
|
||||||
|
data?: unknown
|
||||||
|
params?: Record<string, unknown>
|
||||||
|
forceFormData?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFile = (value: unknown): boolean => {
|
||||||
if (!value || typeof value !== "object") return false
|
if (!value || typeof value !== "object") return false
|
||||||
|
|
||||||
|
const isFile = (item: unknown) =>
|
||||||
|
typeof File !== "undefined" && item instanceof File
|
||||||
|
const isBlob = (item: unknown) =>
|
||||||
|
typeof Blob !== "undefined" && item instanceof Blob
|
||||||
|
|
||||||
return Object.values(value).some(
|
return Object.values(value).some(
|
||||||
(v) =>
|
(item) =>
|
||||||
v instanceof File ||
|
isFile(item) ||
|
||||||
v instanceof Blob ||
|
isBlob(item) ||
|
||||||
(Array.isArray(v) && v.some((i) => i instanceof File)),
|
(Array.isArray(item) && item.some((entry) => isFile(entry) || isBlob(entry))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type RequestConfig = {
|
const toApiUrl = (url: string) => {
|
||||||
forceFormData?: boolean
|
if (url === "/api" || url.startsWith("/api/")) return url
|
||||||
} & AxiosRequestConfig
|
return `/api${url.startsWith("/") ? url : `/${url}`}`
|
||||||
|
}
|
||||||
|
|
||||||
export const cfetch = async (
|
export const cfetch = async <DataT = any>(
|
||||||
url: string,
|
url: string,
|
||||||
options: RequestConfig = {},
|
options: RequestConfig = {},
|
||||||
authorize: boolean = true,
|
authorize = true,
|
||||||
): Promise<AxiosResponse> => {
|
): Promise<DataT> => {
|
||||||
const baseUrl = import.meta.env.VITE_PUBLIC_BACKEND
|
const { data, params, forceFormData, ...fetchOptions } = options
|
||||||
|
const headers = new Headers(fetchOptions.headers as HeadersInit | undefined)
|
||||||
|
const requestOptions: FetchOptions = {
|
||||||
|
...fetchOptions,
|
||||||
|
query: params ?? (fetchOptions as FetchOptions).query,
|
||||||
|
body: data ?? (fetchOptions as FetchOptions).body,
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
|
||||||
if (!baseUrl) throw new Error("BACKEND URL not set")
|
if (
|
||||||
|
requestOptions.body &&
|
||||||
|
(typeof requestOptions.body === "object" || forceFormData) &&
|
||||||
|
(hasFile(requestOptions.body) || forceFormData)
|
||||||
|
) {
|
||||||
|
const form = new FormData()
|
||||||
|
|
||||||
const defaults: AxiosRequestConfig = {
|
for (const [key, value] of Object.entries(requestOptions.body as Record<string, unknown>)) {
|
||||||
baseURL: baseUrl,
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((entry) => form.append(key, entry as string | Blob))
|
||||||
|
} else if (value !== undefined && value !== null) {
|
||||||
|
form.append(key, value as string | Blob)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestOptions.body = form
|
||||||
|
headers.delete("Content-Type")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authorize) {
|
if (authorize) {
|
||||||
const { sessionToken, refreshSessionToken } = useAuth()
|
const { sessionToken, refreshSessionToken } = useAuth()
|
||||||
|
|
||||||
let token = sessionToken.value
|
let token = sessionToken.value
|
||||||
|
|
||||||
if (token) defaults.headers = { Authorization: `Bearer ${token}` }
|
if (!token) token = await refreshSessionToken()
|
||||||
else
|
if (token) headers.set("Authorization", `Bearer ${token}`)
|
||||||
defaults.headers = {
|
|
||||||
Authorization: `Bearer ${await refreshSessionToken()}`,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const config: RequestConfig = defu(options, defaults)
|
|
||||||
|
|
||||||
if (config.data && (hasFile(config.data) || config.forceFormData)) {
|
|
||||||
const form = new FormData()
|
|
||||||
|
|
||||||
for (const [key, val] of Object.entries(config.data)) {
|
|
||||||
if (Array.isArray(val)) {
|
|
||||||
val.forEach((v) => form.append(key, v)) // key as-is, no []
|
|
||||||
} else if (val !== undefined && val !== null) {
|
|
||||||
form.append(key, val as any)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.data = form
|
|
||||||
if (!config.headers) config.headers = {}
|
|
||||||
delete config.headers["Content-Type"]
|
|
||||||
}
|
|
||||||
|
|
||||||
let response
|
|
||||||
try {
|
try {
|
||||||
response = await axios(url, config)
|
return (await $fetch(toApiUrl(url), requestOptions as any)) as DataT
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.response?.status == 401) {
|
if (!authorize || error?.response?.status !== 401) throw error
|
||||||
const { sessionToken, refreshSessionToken } = useAuth()
|
|
||||||
|
|
||||||
if (!config.headers) config.headers = {}
|
const token = await useAuth().refreshSessionToken()
|
||||||
config.headers.Authorization = `Bearer ${await refreshSessionToken()}`
|
if (!token) throw error
|
||||||
|
|
||||||
response = await axios(url, config)
|
headers.set("Authorization", `Bearer ${token}`)
|
||||||
} else throw error
|
return (await $fetch(toApiUrl(url), requestOptions as any)) as DataT
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response) throw new Error("Something went wrong. Please try again")
|
|
||||||
return response
|
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user