mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-02-17 15:01:42 +08:00
fix(frontend): improve error handling
This commit is contained in:
parent
8f690a66d1
commit
332b21e8e2
@ -5,7 +5,10 @@ import { computed } from "vue";
|
||||
import { useSettingsStore } from "~/store/useSettingsStore";
|
||||
import { settingsLog } from "~/lib/composables/useLog";
|
||||
import { useAuthStore } from "~/store/auth";
|
||||
import "regenerator-runtime/runtime"; // popper needs this?
|
||||
import { onErrorCaptured, transformAxiosError } from "#imports";
|
||||
|
||||
// popper needs this?
|
||||
import "regenerator-runtime/runtime";
|
||||
|
||||
// keep in sync with error.vue, cause reasons
|
||||
const authStore = useAuthStore();
|
||||
@ -22,6 +25,10 @@ useHead({
|
||||
class: "background-body text-[#262626] dark:text-[#E0E6f0]",
|
||||
},
|
||||
});
|
||||
|
||||
onErrorCaptured((err) => {
|
||||
console.log("captured", transformAxiosError(err)); // TODO error handling
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -15,160 +15,162 @@ import {
|
||||
ReviewQueueEntry,
|
||||
RoleTable,
|
||||
} from "hangar-internal";
|
||||
import { Ref } from "vue";
|
||||
import { AsyncData } from "nuxt/app";
|
||||
import { useApi, useInternalApi } from "~/composables/useApi";
|
||||
import { useAsyncData } from "#imports";
|
||||
|
||||
export async function useProjects(params: Record<string, any> = { limit: 25, offset: 0 }) {
|
||||
return (await useAsyncData("useProjects", () => useApi<PaginatedResult<Project>>("projects", "get", params))).data;
|
||||
return extract(await useAsyncData("useProjects", () => useApi<PaginatedResult<Project>>("projects", "get", params)));
|
||||
}
|
||||
|
||||
export async function useUser(user: string) {
|
||||
return (await useAsyncData("useUser:" + user, () => useApi<User>("users/" + user))).data;
|
||||
return extract(await useAsyncData("useUser:" + user, () => useApi<User>("users/" + user)));
|
||||
}
|
||||
|
||||
export async function useOrganization(user: string) {
|
||||
return (await useAsyncData("useOrganization:" + user, () => useInternalApi<Organization>(`organizations/org/${user}`))).data;
|
||||
return extract(await useAsyncData("useOrganization:" + user, () => useInternalApi<Organization>(`organizations/org/${user}`)));
|
||||
}
|
||||
|
||||
export async function useProject(user: string, project: string) {
|
||||
return (await useAsyncData("useProject:" + user + ":" + project, () => useInternalApi<HangarProject>("projects/project/" + user + "/" + project))).data;
|
||||
return extract(await useAsyncData("useProject:" + user + ":" + project, () => useInternalApi<HangarProject>("projects/project/" + user + "/" + project)));
|
||||
}
|
||||
|
||||
export async function useStargazers(user: string, project: string) {
|
||||
return (await useAsyncData("useStargazers:" + user + ":" + project, () => useApi<PaginatedResult<User>>(`projects/${user}/${project}/stargazers`))).data;
|
||||
return extract(await useAsyncData("useStargazers:" + user + ":" + project, () => useApi<PaginatedResult<User>>(`projects/${user}/${project}/stargazers`)));
|
||||
}
|
||||
|
||||
export async function useWatchers(user: string, project: string) {
|
||||
return (await useAsyncData("useWatchers:" + user + ":" + project, () => useApi<PaginatedResult<User>>(`projects/${user}/${project}/watchers`))).data;
|
||||
return extract(await useAsyncData("useWatchers:" + user + ":" + project, () => useApi<PaginatedResult<User>>(`projects/${user}/${project}/watchers`)));
|
||||
}
|
||||
|
||||
export async function useStaff(params?: { offset?: number; limit?: number; sort?: string[] }) {
|
||||
return (await useAsyncData("useStaff", () => useApi<PaginatedResult<User>>("staff", "GET", params))).data;
|
||||
return extract(await useAsyncData("useStaff", () => useApi<PaginatedResult<User>>("staff", "GET", params)));
|
||||
}
|
||||
|
||||
export async function useAuthors(params?: { offset?: number; limit?: number; sort?: string[] }) {
|
||||
return (await useAsyncData("useAuthors", () => useApi<PaginatedResult<User>>("authors", "GET", params))).data;
|
||||
return extract(await useAsyncData("useAuthors", () => useApi<PaginatedResult<User>>("authors", "GET", params)));
|
||||
}
|
||||
|
||||
export async function useUsers() {
|
||||
return (await useAsyncData("useUsers", () => useApi<PaginatedResult<User>>("users"))).data;
|
||||
return extract(await useAsyncData("useUsers", () => useApi<PaginatedResult<User>>("users")));
|
||||
}
|
||||
|
||||
export async function useInvites() {
|
||||
return (await useAsyncData("useInvites", () => useInternalApi<Invites>("invites"))).data;
|
||||
return extract(await useAsyncData("useInvites", () => useInternalApi<Invites>("invites")));
|
||||
}
|
||||
|
||||
export async function useNotifications() {
|
||||
return (await useAsyncData("useNotifications", () => useInternalApi<PaginatedResult<HangarNotification>>("notifications"))).data;
|
||||
return extract(await useAsyncData("useNotifications", () => useInternalApi<PaginatedResult<HangarNotification>>("notifications")));
|
||||
}
|
||||
|
||||
export async function useUnreadNotifications() {
|
||||
return (await useAsyncData("useUnreadNotifications", () => useInternalApi<PaginatedResult<HangarNotification>>("unreadnotifications"))).data;
|
||||
return extract(await useAsyncData("useUnreadNotifications", () => useInternalApi<PaginatedResult<HangarNotification>>("unreadnotifications")));
|
||||
}
|
||||
|
||||
export async function useReadNotifications() {
|
||||
return (await useAsyncData("useReadNotifications", () => useInternalApi<PaginatedResult<HangarNotification>>("readnotifications"))).data;
|
||||
return extract(await useAsyncData("useReadNotifications", () => useInternalApi<PaginatedResult<HangarNotification>>("readnotifications")));
|
||||
}
|
||||
|
||||
export async function useRecentNotifications(amount: number) {
|
||||
return (await useAsyncData("useRecentNotifications:" + amount, () => useInternalApi<HangarNotification[]>("recentnotifications?amount=" + amount))).data;
|
||||
return extract(await useAsyncData("useRecentNotifications:" + amount, () => useInternalApi<HangarNotification[]>("recentnotifications?amount=" + amount)));
|
||||
}
|
||||
|
||||
export async function useUnreadNotificationsCount() {
|
||||
return (await useAsyncData("useUnreadNotificationsCount", () => useInternalApi<number>("unreadcount"))).data;
|
||||
return extract(await useAsyncData("useUnreadNotificationsCount", () => useInternalApi<number>("unreadcount")));
|
||||
}
|
||||
|
||||
export async function useResolvedFlags() {
|
||||
return (await useAsyncData("useResolvedFlags", () => useInternalApi<PaginatedResult<Flag>>("flags/resolved"))).data;
|
||||
return extract(await useAsyncData("useResolvedFlags", () => useInternalApi<PaginatedResult<Flag>>("flags/resolved")));
|
||||
}
|
||||
|
||||
export async function useUnresolvedFlags() {
|
||||
return (await useAsyncData("useUnresolvedFlags", () => useInternalApi<PaginatedResult<Flag>>("flags/unresolved"))).data;
|
||||
return extract(await useAsyncData("useUnresolvedFlags", () => useInternalApi<PaginatedResult<Flag>>("flags/unresolved")));
|
||||
}
|
||||
|
||||
export async function useProjectFlags(projectId: number) {
|
||||
return (await useAsyncData("useProjectFlags:" + projectId, () => useInternalApi<Flag[]>("flags/" + projectId))).data;
|
||||
return extract(await useAsyncData("useProjectFlags:" + projectId, () => useInternalApi<Flag[]>("flags/" + projectId)));
|
||||
}
|
||||
|
||||
export async function useProjectNotes(projectId: number) {
|
||||
return (await useAsyncData("useProjectNotes:" + projectId, () => useInternalApi<Note[]>("projects/notes/" + projectId))).data;
|
||||
return extract(await useAsyncData("useProjectNotes:" + projectId, () => useInternalApi<Note[]>("projects/notes/" + projectId)));
|
||||
}
|
||||
|
||||
export async function useProjectChannels(user: string, project: string) {
|
||||
return (await useAsyncData("useProjectChannels:" + user + ":" + project, () => useInternalApi<ProjectChannel[]>(`channels/${user}/${project}`))).data;
|
||||
return extract(await useAsyncData("useProjectChannels:" + user + ":" + project, () => useInternalApi<ProjectChannel[]>(`channels/${user}/${project}`)));
|
||||
}
|
||||
|
||||
export async function useProjectVersions(user: string, project: string) {
|
||||
return (await useAsyncData("useProjectVersions:" + user + ":" + project, () => useApi<PaginatedResult<Version>>(`projects/${user}/${project}/versions`)))
|
||||
.data;
|
||||
return extract(
|
||||
await useAsyncData("useProjectVersions:" + user + ":" + project, () => useApi<PaginatedResult<Version>>(`projects/${user}/${project}/versions`))
|
||||
);
|
||||
}
|
||||
|
||||
export async function useProjectVersionsInternal(user: string, project: string, version: string) {
|
||||
return (
|
||||
await useAsyncData("useProjectVersionsInternal:" + user + ":" + project + ":" + version, () =>
|
||||
useInternalApi<HangarVersion>(`versions/version/${user}/${project}/versions/${version}`)
|
||||
)
|
||||
).data;
|
||||
return await useAsyncData("useProjectVersionsInternal:" + user + ":" + project + ":" + version, () =>
|
||||
useInternalApi<HangarVersion>(`versions/version/${user}/${project}/versions/${version}`)
|
||||
);
|
||||
}
|
||||
|
||||
export async function usePage(user: string, project: string, path?: string) {
|
||||
return (
|
||||
await useAsyncData("usePage:" + user + ":" + project + ":" + path, () =>
|
||||
useInternalApi<ProjectPage>(`pages/page/${user}/${project}` + (path ? "/" + path : ""))
|
||||
)
|
||||
).data;
|
||||
return await useAsyncData("usePage:" + user + ":" + project + ":" + path, () =>
|
||||
useInternalApi<ProjectPage>(`pages/page/${user}/${project}` + (path ? "/" + path : ""))
|
||||
);
|
||||
}
|
||||
|
||||
export async function useHealthReport() {
|
||||
return (await useAsyncData("useHealthReport", () => useInternalApi<HealthReport>("admin/health"))).data;
|
||||
return extract(await useAsyncData("useHealthReport", () => useInternalApi<HealthReport>("admin/health")));
|
||||
}
|
||||
|
||||
export async function useActionLogs() {
|
||||
return (await useAsyncData("useActionLogs", () => useInternalApi<PaginatedResult<LoggedAction>>("admin/log/"))).data;
|
||||
return extract(await useAsyncData("useActionLogs", () => useInternalApi<PaginatedResult<LoggedAction>>("admin/log/")));
|
||||
}
|
||||
|
||||
export async function useVersionApprovals() {
|
||||
return (
|
||||
await useAsyncData("useVersionApprovals", () =>
|
||||
useInternalApi<{ underReview: ReviewQueueEntry[]; notStarted: ReviewQueueEntry[] }>("admin/approval/versions")
|
||||
)
|
||||
).data;
|
||||
return await useAsyncData("useVersionApprovals", () =>
|
||||
useInternalApi<{ underReview: ReviewQueueEntry[]; notStarted: ReviewQueueEntry[] }>("admin/approval/versions")
|
||||
);
|
||||
}
|
||||
|
||||
export async function usePossibleOwners() {
|
||||
return (await useAsyncData("usePossibleOwners", () => useInternalApi<ProjectOwner[]>("projects/possibleOwners"))).data;
|
||||
return extract(await useAsyncData("usePossibleOwners", () => useInternalApi<ProjectOwner[]>("projects/possibleOwners")));
|
||||
}
|
||||
|
||||
export async function useOrgVisibility(user: string) {
|
||||
return (await useAsyncData("useOrgVisibility:" + user, () => useInternalApi<{ [key: string]: boolean }>(`organizations/${user}/userOrganizationsVisibility`)))
|
||||
.data;
|
||||
return extract(
|
||||
await useAsyncData("useOrgVisibility:" + user, () => useInternalApi<{ [key: string]: boolean }>(`organizations/${user}/userOrganizationsVisibility`))
|
||||
);
|
||||
}
|
||||
|
||||
export async function useVersionInfo(): Promise<Ref<VersionInfo | undefined>> {
|
||||
return (await useAsyncData("useVersionInfo", () => useInternalApi<VersionInfo>(`data/version-info`))).data;
|
||||
export async function useVersionInfo() {
|
||||
return extract(await useAsyncData("useVersionInfo", () => useInternalApi<VersionInfo>(`data/version-info`)));
|
||||
}
|
||||
|
||||
export async function useUserData(user: string) {
|
||||
return (
|
||||
await useAsyncData("useUserData:" + user, async () => {
|
||||
// noinspection ES6MissingAwait
|
||||
const data = await Promise.all([
|
||||
useApi<PaginatedResult<ProjectCompact>>(`users/${user}/starred`),
|
||||
useApi<PaginatedResult<ProjectCompact>>(`users/${user}/watching`),
|
||||
useApi<PaginatedResult<Project>>(`projects`, "get", {
|
||||
owner: user,
|
||||
}),
|
||||
useInternalApi<{ [key: string]: RoleTable }>(`organizations/${user}/userOrganizations`),
|
||||
useApi<ProjectCompact[]>(`users/${user}/pinned`),
|
||||
]);
|
||||
return {
|
||||
starred: data[0] as PaginatedResult<ProjectCompact>,
|
||||
watching: data[1] as PaginatedResult<ProjectCompact>,
|
||||
projects: data[2] as PaginatedResult<Project>,
|
||||
organizations: data[3] as { [key: string]: RoleTable },
|
||||
pinned: data[4] as ProjectCompact[],
|
||||
};
|
||||
})
|
||||
).data;
|
||||
return await useAsyncData("useUserData:" + user, async () => {
|
||||
// noinspection ES6MissingAwait
|
||||
const data = await Promise.all([
|
||||
useApi<PaginatedResult<ProjectCompact>>(`users/${user}/starred`),
|
||||
useApi<PaginatedResult<ProjectCompact>>(`users/${user}/watching`),
|
||||
useApi<PaginatedResult<Project>>(`projects`, "get", {
|
||||
owner: user,
|
||||
}),
|
||||
useInternalApi<{ [key: string]: RoleTable }>(`organizations/${user}/userOrganizations`),
|
||||
useApi<ProjectCompact[]>(`users/${user}/pinned`),
|
||||
]);
|
||||
return {
|
||||
starred: data[0] as PaginatedResult<ProjectCompact>,
|
||||
watching: data[1] as PaginatedResult<ProjectCompact>,
|
||||
projects: data[2] as PaginatedResult<Project>,
|
||||
organizations: data[3] as { [key: string]: RoleTable },
|
||||
pinned: data[4] as ProjectCompact[],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function extract<T, E>(asyncData: AsyncData<T, E>) {
|
||||
if (asyncData.error?.value) {
|
||||
throw asyncData.error.value;
|
||||
} else {
|
||||
return asyncData.data;
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import { useConfig } from "~/lib/composables/useConfig";
|
||||
import { handleRequestError, useRequestEvent } from "#imports";
|
||||
import { useAxios } from "~/composables/useAxios";
|
||||
import { useNotificationStore } from "~/lib/store/notification";
|
||||
import { transformAxiosError } from "~/composables/useErrorHandling";
|
||||
|
||||
class Auth {
|
||||
loginUrl(redirectUrl: string): string {
|
||||
@ -25,7 +26,7 @@ class Auth {
|
||||
if ("status" in result && result?.status === 200 && result?.data) {
|
||||
location.replace(result?.data);
|
||||
} else {
|
||||
useNotificationStore().error("Error while logging out?!");
|
||||
await useNotificationStore().error("Error while logging out?!");
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,8 +93,7 @@ class Auth {
|
||||
} catch (e) {
|
||||
this.refreshPromise = null;
|
||||
if ((e as AxiosError).response?.data) {
|
||||
const { trace, ...err } = (e as AxiosError).response?.data as { trace: any };
|
||||
authLog("Refresh failed", err);
|
||||
authLog("Refresh failed", transformAxiosError(e));
|
||||
} else {
|
||||
authLog("Refresh failed");
|
||||
}
|
||||
@ -128,7 +128,7 @@ class Auth {
|
||||
return;
|
||||
}
|
||||
const user = await useInternalApi<HangarUser>("users/@me").catch((err) => {
|
||||
authLog("no user, with err", Object.assign({}, err));
|
||||
authLog("no user, with err", transformAxiosError(err));
|
||||
return this.invalidate(axios);
|
||||
});
|
||||
if (user) {
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { AxiosError } from "axios";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { HangarApiException, HangarValidationException, MultiHangarApiException } from "hangar-api";
|
||||
import { Composer } from "vue-i18n";
|
||||
import { ref } from "vue";
|
||||
import { useNotificationStore } from "~/lib/store/notification";
|
||||
import { I18n } from "~/lib/i18n";
|
||||
import { createError } from "#imports";
|
||||
|
||||
export function handleRequestError(err: AxiosError, i18n: Composer = I18n.value, msg: string | undefined = undefined) {
|
||||
if (import.meta.env.SSR) {
|
||||
@ -11,9 +12,11 @@ export function handleRequestError(err: AxiosError, i18n: Composer = I18n.value,
|
||||
return ref();
|
||||
}
|
||||
const notfication = useNotificationStore();
|
||||
const transformed = transformAxiosError(err);
|
||||
if (!err.isAxiosError) {
|
||||
// everything should be an AxiosError
|
||||
console.log(err);
|
||||
console.log("no axios request error", transformed);
|
||||
notfication.error(transformed.message?.toString() || "Unknown error");
|
||||
} else if (err.response && typeof err.response.data === "object" && err.response.data) {
|
||||
if ("isHangarApiException" in err.response.data) {
|
||||
for (const errorMsg of collectErrors(err.response.data as HangarApiException, i18n)) {
|
||||
@ -30,9 +33,10 @@ export function handleRequestError(err: AxiosError, i18n: Composer = I18n.value,
|
||||
} else {
|
||||
notfication.error(msg ? `${i18n.t(msg)}: ${err.response.statusText}` : err.response.statusText);
|
||||
}
|
||||
console.log("request error", err.response);
|
||||
console.log("request error", transformed);
|
||||
} else {
|
||||
console.log(err);
|
||||
console.log("unknown error", transformed);
|
||||
notfication.error(transformed.message?.toString() || "Unknown error");
|
||||
}
|
||||
return ref();
|
||||
}
|
||||
@ -41,13 +45,16 @@ function _handleRequestError(err: AxiosError, i18n: Composer) {
|
||||
function writeResponse(object: unknown) {
|
||||
console.log("writeResponse", object);
|
||||
// throw new Error("TODO: Implement me"); // TODO
|
||||
createError({ statusCode: object.status, statusMessage: object.statusText });
|
||||
}
|
||||
|
||||
const transformed = transformAxiosError(err);
|
||||
if (!err.isAxiosError) {
|
||||
// everything should be an AxiosError
|
||||
writeResponse({
|
||||
status: 500,
|
||||
});
|
||||
console.log(err);
|
||||
console.log("handle not axios error", transformed);
|
||||
} else if (err.response && typeof err.response.data === "object" && err.response.data) {
|
||||
if ("isHangarApiException" in err.response.data) {
|
||||
const data =
|
||||
@ -70,9 +77,9 @@ function _handleRequestError(err: AxiosError, i18n: Composer) {
|
||||
}
|
||||
} else {
|
||||
writeResponse({
|
||||
statusText: "This shouldn't happen...",
|
||||
status: 500,
|
||||
statusText: "Internal Error: " + transformed.code,
|
||||
});
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,3 +94,15 @@ function collectErrors(exception: HangarApiException | MultiHangarApiException,
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export function transformAxiosError(err: AxiosError | unknown): Record<string, unknown> {
|
||||
return axios.isAxiosError(err)
|
||||
? {
|
||||
code: err?.code,
|
||||
requestUrl: err?.request?.path || err?.config?.url,
|
||||
status: err?.response?.status,
|
||||
data: err?.response?.data,
|
||||
message: err?.message,
|
||||
}
|
||||
: (err as Record<string, unknown>);
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import { defineNuxtPlugin, useAuth, useRequestEvent } from "#imports";
|
||||
import { useAuthStore } from "~/store/auth";
|
||||
import { authLog, axiosLog } from "~/lib/composables/useLog";
|
||||
import { useConfig } from "~/lib/composables/useConfig";
|
||||
import { transformAxiosError } from "~/composables/useErrorHandling";
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp: NuxtApp) => {
|
||||
const config = useConfig();
|
||||
@ -58,13 +59,7 @@ export default defineNuxtPlugin((nuxtApp: NuxtApp) => {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const transformedError = {
|
||||
code: err?.code,
|
||||
requestUrl: err?.request?.path,
|
||||
status: err?.response?.status,
|
||||
data: err?.response?.data,
|
||||
};
|
||||
axiosLog("got error", transformedError);
|
||||
axiosLog("got error", transformAxiosError(err));
|
||||
}
|
||||
|
||||
// Progress bar
|
||||
@ -81,7 +76,7 @@ export default defineNuxtPlugin((nuxtApp: NuxtApp) => {
|
||||
};
|
||||
});
|
||||
|
||||
function addAuthHeader(config: AxiosRequestConfig, token: string | undefined) {
|
||||
function addAuthHeader(config: AxiosRequestConfig, token: string | undefined | null) {
|
||||
if (!config.headers) {
|
||||
config.headers = {};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user