You've already forked AstralRinth
forked from didirus/AstralRinth
refactor: migrate to common eslint+prettier configs (#4168)
* refactor: migrate to common eslint+prettier configs * fix: prettier frontend * feat: config changes * fix: lint issues * fix: lint * fix: type imports * fix: cyclical import issue * fix: lockfile * fix: missing dep * fix: switch to tabs * fix: continue switch to tabs * fix: rustfmt parity * fix: moderation lint issue * fix: lint issues * fix: ui intl * fix: lint issues * Revert "fix: rustfmt parity" This reverts commit cb99d2376c321d813d4b7fc7e2a213bb30a54711. * feat: revert last rs
This commit is contained in:
@@ -1,141 +1,141 @@
|
||||
import { injectNotificationManager } from "@modrinth/ui";
|
||||
import { injectNotificationManager } from '@modrinth/ui'
|
||||
|
||||
export const useAuth = async (oldToken = null) => {
|
||||
const auth = useState("auth", () => ({
|
||||
user: null,
|
||||
token: "",
|
||||
headers: {},
|
||||
}));
|
||||
const auth = useState('auth', () => ({
|
||||
user: null,
|
||||
token: '',
|
||||
headers: {},
|
||||
}))
|
||||
|
||||
if (!auth.value.user || oldToken) {
|
||||
auth.value = await initAuth(oldToken);
|
||||
}
|
||||
if (!auth.value.user || oldToken) {
|
||||
auth.value = await initAuth(oldToken)
|
||||
}
|
||||
|
||||
return auth;
|
||||
};
|
||||
return auth
|
||||
}
|
||||
|
||||
export const initAuth = async (oldToken = null) => {
|
||||
const auth = {
|
||||
user: null,
|
||||
token: "",
|
||||
};
|
||||
const auth = {
|
||||
user: null,
|
||||
token: '',
|
||||
}
|
||||
|
||||
if (oldToken === "none") {
|
||||
return auth;
|
||||
}
|
||||
if (oldToken === 'none') {
|
||||
return auth
|
||||
}
|
||||
|
||||
const route = useRoute();
|
||||
const authCookie = useCookie("auth-token", {
|
||||
maxAge: 60 * 60 * 24 * 365 * 10,
|
||||
sameSite: "lax",
|
||||
secure: true,
|
||||
httpOnly: false,
|
||||
path: "/",
|
||||
});
|
||||
const route = useRoute()
|
||||
const authCookie = useCookie('auth-token', {
|
||||
maxAge: 60 * 60 * 24 * 365 * 10,
|
||||
sameSite: 'lax',
|
||||
secure: true,
|
||||
httpOnly: false,
|
||||
path: '/',
|
||||
})
|
||||
|
||||
if (oldToken) {
|
||||
authCookie.value = oldToken;
|
||||
}
|
||||
if (oldToken) {
|
||||
authCookie.value = oldToken
|
||||
}
|
||||
|
||||
if (route.query.code && !route.fullPath.includes("new_account=true")) {
|
||||
authCookie.value = route.query.code;
|
||||
}
|
||||
if (route.query.code && !route.fullPath.includes('new_account=true')) {
|
||||
authCookie.value = route.query.code
|
||||
}
|
||||
|
||||
if (route.fullPath.includes("new_account=true") && route.path !== "/auth/welcome") {
|
||||
const redirect = route.path.startsWith("/auth/") ? null : route.fullPath;
|
||||
if (route.fullPath.includes('new_account=true') && route.path !== '/auth/welcome') {
|
||||
const redirect = route.path.startsWith('/auth/') ? null : route.fullPath
|
||||
|
||||
await navigateTo(
|
||||
`/auth/welcome?authToken=${route.query.code}${
|
||||
redirect ? `&redirect=${encodeURIComponent(redirect)}` : ""
|
||||
}`,
|
||||
);
|
||||
}
|
||||
await navigateTo(
|
||||
`/auth/welcome?authToken=${route.query.code}${
|
||||
redirect ? `&redirect=${encodeURIComponent(redirect)}` : ''
|
||||
}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (authCookie.value) {
|
||||
auth.token = authCookie.value;
|
||||
if (authCookie.value) {
|
||||
auth.token = authCookie.value
|
||||
|
||||
if (!auth.token || !auth.token.startsWith("mra_")) {
|
||||
return auth;
|
||||
}
|
||||
if (!auth.token || !auth.token.startsWith('mra_')) {
|
||||
return auth
|
||||
}
|
||||
|
||||
try {
|
||||
auth.user = await useBaseFetch(
|
||||
"user",
|
||||
{
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
try {
|
||||
auth.user = await useBaseFetch(
|
||||
'user',
|
||||
{
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
)
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
if (!auth.user && auth.token) {
|
||||
try {
|
||||
const session = await useBaseFetch(
|
||||
"session/refresh",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
);
|
||||
if (!auth.user && auth.token) {
|
||||
try {
|
||||
const session = await useBaseFetch(
|
||||
'session/refresh',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
auth.token = session.session;
|
||||
authCookie.value = auth.token;
|
||||
auth.token = session.session
|
||||
authCookie.value = auth.token
|
||||
|
||||
auth.user = await useBaseFetch(
|
||||
"user",
|
||||
{
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
);
|
||||
} catch {
|
||||
authCookie.value = null;
|
||||
}
|
||||
}
|
||||
auth.user = await useBaseFetch(
|
||||
'user',
|
||||
{
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
},
|
||||
true,
|
||||
)
|
||||
} catch {
|
||||
authCookie.value = null
|
||||
}
|
||||
}
|
||||
|
||||
return auth;
|
||||
};
|
||||
return auth
|
||||
}
|
||||
|
||||
export const getAuthUrl = (provider, redirect = "/dashboard") => {
|
||||
const config = useRuntimeConfig();
|
||||
const route = useNativeRoute();
|
||||
export const getAuthUrl = (provider, redirect = '/dashboard') => {
|
||||
const config = useRuntimeConfig()
|
||||
const route = useNativeRoute()
|
||||
|
||||
const fullURL = route.query.launcher
|
||||
? "https://launcher-files.modrinth.com"
|
||||
: `${config.public.siteUrl}/auth/sign-in?redirect=${redirect}`;
|
||||
const fullURL = route.query.launcher
|
||||
? 'https://launcher-files.modrinth.com'
|
||||
: `${config.public.siteUrl}/auth/sign-in?redirect=${redirect}`
|
||||
|
||||
return `${config.public.apiBaseUrl}auth/init?provider=${provider}&url=${encodeURIComponent(fullURL)}`;
|
||||
};
|
||||
return `${config.public.apiBaseUrl}auth/init?provider=${provider}&url=${encodeURIComponent(fullURL)}`
|
||||
}
|
||||
|
||||
export const removeAuthProvider = async (provider) => {
|
||||
startLoading();
|
||||
try {
|
||||
const auth = await useAuth();
|
||||
startLoading()
|
||||
try {
|
||||
const auth = await useAuth()
|
||||
|
||||
await useBaseFetch("auth/provider", {
|
||||
method: "DELETE",
|
||||
body: {
|
||||
provider,
|
||||
},
|
||||
});
|
||||
await useAuth(auth.value.token);
|
||||
} catch (err) {
|
||||
const { addNotification } = injectNotificationManager();
|
||||
addNotification({
|
||||
title: "An error occurred",
|
||||
text: err.data.description,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
stopLoading();
|
||||
};
|
||||
await useBaseFetch('auth/provider', {
|
||||
method: 'DELETE',
|
||||
body: {
|
||||
provider,
|
||||
},
|
||||
})
|
||||
await useAuth(auth.value.token)
|
||||
} catch (err) {
|
||||
const { addNotification } = injectNotificationManager()
|
||||
addNotification({
|
||||
title: 'An error occurred',
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,26 +1,26 @@
|
||||
const formatters = new WeakMap<object, Intl.NumberFormat>();
|
||||
const formatters = new WeakMap<object, Intl.NumberFormat>()
|
||||
|
||||
export function useCompactNumber(truncate = false, fractionDigits = 2, locale?: string) {
|
||||
const context = {};
|
||||
const context = {}
|
||||
|
||||
let formatter = formatters.get(context);
|
||||
let formatter = formatters.get(context)
|
||||
|
||||
if (!formatter) {
|
||||
formatter = new Intl.NumberFormat(locale, {
|
||||
notation: "compact",
|
||||
maximumFractionDigits: fractionDigits,
|
||||
});
|
||||
formatters.set(context, formatter);
|
||||
}
|
||||
if (!formatter) {
|
||||
formatter = new Intl.NumberFormat(locale, {
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: fractionDigits,
|
||||
})
|
||||
formatters.set(context, formatter)
|
||||
}
|
||||
|
||||
function format(value: number): string {
|
||||
let formattedValue = value;
|
||||
if (truncate) {
|
||||
const scale = Math.pow(10, fractionDigits);
|
||||
formattedValue = Math.floor(value * scale) / scale;
|
||||
}
|
||||
return formatter!.format(formattedValue);
|
||||
}
|
||||
function format(value: number): string {
|
||||
let formattedValue = value
|
||||
if (truncate) {
|
||||
const scale = Math.pow(10, fractionDigits)
|
||||
formattedValue = Math.floor(value * scale) / scale
|
||||
}
|
||||
return formatter!.format(formattedValue)
|
||||
}
|
||||
|
||||
return format;
|
||||
return format
|
||||
}
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
import { useState, useRequestHeaders } from "#imports";
|
||||
import { useRequestHeaders, useState } from '#imports'
|
||||
|
||||
export const useUserCountry = () => {
|
||||
const country = useState<string>("userCountry", () => "US");
|
||||
const fromServer = useState<boolean>("userCountryFromServer", () => false);
|
||||
const country = useState<string>('userCountry', () => 'US')
|
||||
const fromServer = useState<boolean>('userCountryFromServer', () => false)
|
||||
|
||||
if (import.meta.server) {
|
||||
const headers = useRequestHeaders(["cf-ipcountry", "accept-language"]);
|
||||
const cf = headers["cf-ipcountry"];
|
||||
if (cf) {
|
||||
country.value = cf.toUpperCase();
|
||||
fromServer.value = true;
|
||||
} else {
|
||||
const al = headers["accept-language"] || "";
|
||||
const tag = al.split(",")[0];
|
||||
const val = tag.split("-")[1]?.toLowerCase();
|
||||
if (val) {
|
||||
country.value = val;
|
||||
fromServer.value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (import.meta.server) {
|
||||
const headers = useRequestHeaders(['cf-ipcountry', 'accept-language'])
|
||||
const cf = headers['cf-ipcountry']
|
||||
if (cf) {
|
||||
country.value = cf.toUpperCase()
|
||||
fromServer.value = true
|
||||
} else {
|
||||
const al = headers['accept-language'] || ''
|
||||
const tag = al.split(',')[0]
|
||||
const val = tag.split('-')[1]?.toLowerCase()
|
||||
if (val) {
|
||||
country.value = val
|
||||
fromServer.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.client) {
|
||||
onMounted(() => {
|
||||
if (fromServer.value) return;
|
||||
// @ts-expect-error - ignore TS not knowing about navigator.userLanguage
|
||||
const lang = navigator.language || navigator.userLanguage || "";
|
||||
const region = lang.split("-")[1];
|
||||
if (region) {
|
||||
country.value = region.toUpperCase();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (import.meta.client) {
|
||||
onMounted(() => {
|
||||
if (fromServer.value) return
|
||||
// @ts-expect-error - ignore TS not knowing about navigator.userLanguage
|
||||
const lang = navigator.language || navigator.userLanguage || ''
|
||||
const region = lang.split('-')[1]
|
||||
if (region) {
|
||||
country.value = region.toUpperCase()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return country;
|
||||
};
|
||||
return country
|
||||
}
|
||||
|
||||
@@ -1,92 +1,90 @@
|
||||
const safeTags = new Map<string, string>();
|
||||
const safeTags = new Map<string, string>()
|
||||
|
||||
function safeTagFor(locale: string) {
|
||||
let safeTag = safeTags.get(locale);
|
||||
if (safeTag == null) {
|
||||
safeTag = new Intl.Locale(locale).baseName;
|
||||
safeTags.set(locale, safeTag);
|
||||
}
|
||||
return safeTag;
|
||||
let safeTag = safeTags.get(locale)
|
||||
if (safeTag == null) {
|
||||
safeTag = new Intl.Locale(locale).baseName
|
||||
safeTags.set(locale, safeTag)
|
||||
}
|
||||
return safeTag
|
||||
}
|
||||
|
||||
type DisplayNamesWrapper = Intl.DisplayNames & {
|
||||
of(tag: string): string | undefined;
|
||||
};
|
||||
of(tag: string): string | undefined
|
||||
}
|
||||
|
||||
const displayNamesDicts = new Map<string, DisplayNamesWrapper>();
|
||||
const displayNamesDicts = new Map<string, DisplayNamesWrapper>()
|
||||
|
||||
function getWrapperKey(locale: string, options: Intl.DisplayNamesOptions) {
|
||||
return JSON.stringify({ ...options, locale });
|
||||
return JSON.stringify({ ...options, locale })
|
||||
}
|
||||
|
||||
export function createDisplayNames(
|
||||
locale: string,
|
||||
options: Intl.DisplayNamesOptions = { type: "language" },
|
||||
locale: string,
|
||||
options: Intl.DisplayNamesOptions = { type: 'language' },
|
||||
) {
|
||||
const wrapperKey = getWrapperKey(locale, options);
|
||||
let wrapper = displayNamesDicts.get(wrapperKey);
|
||||
const wrapperKey = getWrapperKey(locale, options)
|
||||
let wrapper = displayNamesDicts.get(wrapperKey)
|
||||
|
||||
if (wrapper == null) {
|
||||
const dict = new Intl.DisplayNames(locale, options);
|
||||
if (wrapper == null) {
|
||||
const dict = new Intl.DisplayNames(locale, options)
|
||||
|
||||
const badTags: string[] = [];
|
||||
const badTags: string[] = []
|
||||
|
||||
wrapper = {
|
||||
resolvedOptions() {
|
||||
return dict.resolvedOptions();
|
||||
},
|
||||
of(tag: string) {
|
||||
let attempt = 0;
|
||||
wrapper = {
|
||||
resolvedOptions() {
|
||||
return dict.resolvedOptions()
|
||||
},
|
||||
of(tag: string) {
|
||||
let attempt = 0
|
||||
|
||||
// eslint-disable-next-line no-labels
|
||||
lookupLoop: do {
|
||||
let lookup: string;
|
||||
switch (attempt) {
|
||||
case 0:
|
||||
lookup = tag;
|
||||
break;
|
||||
case 1:
|
||||
lookup = safeTagFor(tag);
|
||||
break;
|
||||
default:
|
||||
// eslint-disable-next-line no-labels
|
||||
break lookupLoop;
|
||||
}
|
||||
lookupLoop: do {
|
||||
let lookup: string
|
||||
switch (attempt) {
|
||||
case 0:
|
||||
lookup = tag
|
||||
break
|
||||
case 1:
|
||||
lookup = safeTagFor(tag)
|
||||
break
|
||||
default:
|
||||
break lookupLoop
|
||||
}
|
||||
|
||||
if (badTags.includes(lookup)) continue;
|
||||
if (badTags.includes(lookup)) continue
|
||||
|
||||
try {
|
||||
return dict.of(lookup);
|
||||
} catch {
|
||||
console.warn(
|
||||
`Failed to get display name for ${lookup} using dictionary for ${
|
||||
this.resolvedOptions().locale
|
||||
}`,
|
||||
);
|
||||
badTags.push(lookup);
|
||||
continue;
|
||||
}
|
||||
} while (++attempt < 5);
|
||||
try {
|
||||
return dict.of(lookup)
|
||||
} catch {
|
||||
console.warn(
|
||||
`Failed to get display name for ${lookup} using dictionary for ${
|
||||
this.resolvedOptions().locale
|
||||
}`,
|
||||
)
|
||||
badTags.push(lookup)
|
||||
continue
|
||||
}
|
||||
} while (++attempt < 5)
|
||||
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
return undefined
|
||||
},
|
||||
}
|
||||
|
||||
displayNamesDicts.set(wrapperKey, wrapper);
|
||||
}
|
||||
displayNamesDicts.set(wrapperKey, wrapper)
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
return wrapper
|
||||
}
|
||||
|
||||
export function useDisplayNames(
|
||||
locale: string | (() => string) | Ref<string>,
|
||||
options?:
|
||||
| (Intl.DisplayNamesOptions | undefined)
|
||||
| (() => Intl.DisplayNamesOptions | undefined)
|
||||
| Ref<Intl.DisplayNamesOptions | undefined>,
|
||||
locale: string | (() => string) | Ref<string>,
|
||||
options?:
|
||||
| (Intl.DisplayNamesOptions | undefined)
|
||||
| (() => Intl.DisplayNamesOptions | undefined)
|
||||
| Ref<Intl.DisplayNamesOptions | undefined>,
|
||||
) {
|
||||
const $locale = toRef(locale);
|
||||
const $options = toRef(options);
|
||||
const $locale = toRef(locale)
|
||||
const $options = toRef(options)
|
||||
|
||||
return computed(() => createDisplayNames($locale.value, $options.value));
|
||||
return computed(() => createDisplayNames($locale.value, $options.value))
|
||||
}
|
||||
|
||||
@@ -1,107 +1,107 @@
|
||||
import type { CookieOptions } from "#app";
|
||||
import type { CookieOptions } from '#app'
|
||||
|
||||
export type ProjectDisplayMode = "list" | "grid" | "gallery";
|
||||
export type DarkColorTheme = "dark" | "oled" | "retro";
|
||||
export type ProjectDisplayMode = 'list' | 'grid' | 'gallery'
|
||||
export type DarkColorTheme = 'dark' | 'oled' | 'retro'
|
||||
|
||||
export interface NumberFlag {
|
||||
min: number;
|
||||
max: number;
|
||||
min: number
|
||||
max: number
|
||||
}
|
||||
|
||||
export type BooleanFlag = boolean;
|
||||
export type BooleanFlag = boolean
|
||||
|
||||
export type RadioFlag = ProjectDisplayMode | DarkColorTheme;
|
||||
export type RadioFlag = ProjectDisplayMode | DarkColorTheme
|
||||
|
||||
export type FlagValue = BooleanFlag; /* | NumberFlag | RadioFlag */
|
||||
export type FlagValue = BooleanFlag /* | NumberFlag | RadioFlag */
|
||||
|
||||
const validateValues = <K extends PropertyKey>(flags: Record<K, FlagValue>) => flags;
|
||||
const validateValues = <K extends PropertyKey>(flags: Record<K, FlagValue>) => flags
|
||||
|
||||
export const DEFAULT_FEATURE_FLAGS = validateValues({
|
||||
// Developer flags
|
||||
developerMode: false,
|
||||
showVersionFilesInTable: false,
|
||||
showAdsWithPlus: false,
|
||||
alwaysShowChecklistAsPopup: true,
|
||||
// Developer flags
|
||||
developerMode: false,
|
||||
showVersionFilesInTable: false,
|
||||
showAdsWithPlus: false,
|
||||
alwaysShowChecklistAsPopup: true,
|
||||
|
||||
// Feature toggles
|
||||
projectTypesPrimaryNav: false,
|
||||
hidePlusPromoInUserMenu: false,
|
||||
oldProjectCards: true,
|
||||
newProjectCards: false,
|
||||
projectBackground: false,
|
||||
searchBackground: false,
|
||||
advancedDebugInfo: false,
|
||||
showProjectPageDownloadModalServersPromo: false,
|
||||
showProjectPageCreateServersTooltip: true,
|
||||
showProjectPageQuickServerButton: false,
|
||||
// advancedRendering: true,
|
||||
// externalLinksNewTab: true,
|
||||
// notUsingBlockers: false,
|
||||
// hideModrinthAppPromos: false,
|
||||
// preferredDarkTheme: 'dark',
|
||||
// hideStagingBanner: false,
|
||||
// Feature toggles
|
||||
projectTypesPrimaryNav: false,
|
||||
hidePlusPromoInUserMenu: false,
|
||||
oldProjectCards: true,
|
||||
newProjectCards: false,
|
||||
projectBackground: false,
|
||||
searchBackground: false,
|
||||
advancedDebugInfo: false,
|
||||
showProjectPageDownloadModalServersPromo: false,
|
||||
showProjectPageCreateServersTooltip: true,
|
||||
showProjectPageQuickServerButton: false,
|
||||
// advancedRendering: true,
|
||||
// externalLinksNewTab: true,
|
||||
// notUsingBlockers: false,
|
||||
// hideModrinthAppPromos: false,
|
||||
// preferredDarkTheme: 'dark',
|
||||
// hideStagingBanner: false,
|
||||
|
||||
// Project display modes
|
||||
// modSearchDisplayMode: 'list',
|
||||
// pluginSearchDisplayMode: 'list',
|
||||
// resourcePackSearchDisplayMode: 'gallery',
|
||||
// modpackSearchDisplayMode: 'list',
|
||||
// shaderSearchDisplayMode: 'gallery',
|
||||
// dataPackSearchDisplayMode: 'list',
|
||||
// userProjectDisplayMode: 'list',
|
||||
// collectionProjectDisplayMode: 'list',
|
||||
} as const);
|
||||
// Project display modes
|
||||
// modSearchDisplayMode: 'list',
|
||||
// pluginSearchDisplayMode: 'list',
|
||||
// resourcePackSearchDisplayMode: 'gallery',
|
||||
// modpackSearchDisplayMode: 'list',
|
||||
// shaderSearchDisplayMode: 'gallery',
|
||||
// dataPackSearchDisplayMode: 'list',
|
||||
// userProjectDisplayMode: 'list',
|
||||
// collectionProjectDisplayMode: 'list',
|
||||
} as const)
|
||||
|
||||
export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS;
|
||||
export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS
|
||||
|
||||
export type AllFeatureFlags = {
|
||||
[key in FeatureFlag]: (typeof DEFAULT_FEATURE_FLAGS)[key];
|
||||
};
|
||||
[key in FeatureFlag]: (typeof DEFAULT_FEATURE_FLAGS)[key]
|
||||
}
|
||||
|
||||
export type PartialFeatureFlags = Partial<AllFeatureFlags>;
|
||||
export type PartialFeatureFlags = Partial<AllFeatureFlags>
|
||||
|
||||
const COOKIE_OPTIONS = {
|
||||
maxAge: 60 * 60 * 24 * 365 * 10,
|
||||
sameSite: "lax",
|
||||
secure: true,
|
||||
httpOnly: false,
|
||||
path: "/",
|
||||
} satisfies CookieOptions<PartialFeatureFlags>;
|
||||
maxAge: 60 * 60 * 24 * 365 * 10,
|
||||
sameSite: 'lax',
|
||||
secure: true,
|
||||
httpOnly: false,
|
||||
path: '/',
|
||||
} satisfies CookieOptions<PartialFeatureFlags>
|
||||
|
||||
export const useFeatureFlags = () =>
|
||||
useState<AllFeatureFlags>("featureFlags", () => {
|
||||
const config = useRuntimeConfig();
|
||||
useState<AllFeatureFlags>('featureFlags', () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const savedFlags = useCookie<PartialFeatureFlags>("featureFlags", COOKIE_OPTIONS);
|
||||
const savedFlags = useCookie<PartialFeatureFlags>('featureFlags', COOKIE_OPTIONS)
|
||||
|
||||
if (!savedFlags.value) {
|
||||
savedFlags.value = {};
|
||||
}
|
||||
if (!savedFlags.value) {
|
||||
savedFlags.value = {}
|
||||
}
|
||||
|
||||
const flags: AllFeatureFlags = JSON.parse(JSON.stringify(DEFAULT_FEATURE_FLAGS));
|
||||
const flags: AllFeatureFlags = JSON.parse(JSON.stringify(DEFAULT_FEATURE_FLAGS))
|
||||
|
||||
const overrides = config.public.featureFlagOverrides as PartialFeatureFlags;
|
||||
for (const key in overrides) {
|
||||
if (key in flags) {
|
||||
const flag = key as FeatureFlag;
|
||||
const value = overrides[flag] as (typeof flags)[FeatureFlag];
|
||||
flags[flag] = value;
|
||||
}
|
||||
}
|
||||
const overrides = config.public.featureFlagOverrides as PartialFeatureFlags
|
||||
for (const key in overrides) {
|
||||
if (key in flags) {
|
||||
const flag = key as FeatureFlag
|
||||
const value = overrides[flag] as (typeof flags)[FeatureFlag]
|
||||
flags[flag] = value
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in savedFlags.value) {
|
||||
if (key in flags) {
|
||||
const flag = key as FeatureFlag;
|
||||
const value = savedFlags.value[flag] as (typeof flags)[FeatureFlag];
|
||||
flags[flag] = value;
|
||||
}
|
||||
}
|
||||
for (const key in savedFlags.value) {
|
||||
if (key in flags) {
|
||||
const flag = key as FeatureFlag
|
||||
const value = savedFlags.value[flag] as (typeof flags)[FeatureFlag]
|
||||
flags[flag] = value
|
||||
}
|
||||
}
|
||||
|
||||
return flags;
|
||||
});
|
||||
return flags
|
||||
})
|
||||
|
||||
export const saveFeatureFlags = () => {
|
||||
const flags = useFeatureFlags();
|
||||
const cookie = useCookie<PartialFeatureFlags>("featureFlags", COOKIE_OPTIONS);
|
||||
cookie.value = flags.value;
|
||||
};
|
||||
const flags = useFeatureFlags()
|
||||
const cookie = useCookie<PartialFeatureFlags>('featureFlags', COOKIE_OPTIONS)
|
||||
cookie.value = flags.value
|
||||
}
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
export const useBaseFetch = async (url, options = {}, skipAuth = false) => {
|
||||
const config = useRuntimeConfig();
|
||||
let base = import.meta.server ? config.apiBaseUrl : config.public.apiBaseUrl;
|
||||
const config = useRuntimeConfig()
|
||||
let base = import.meta.server ? config.apiBaseUrl : config.public.apiBaseUrl
|
||||
|
||||
if (!options.headers) {
|
||||
options.headers = {};
|
||||
}
|
||||
if (!options.headers) {
|
||||
options.headers = {}
|
||||
}
|
||||
|
||||
if (import.meta.server) {
|
||||
options.headers["x-ratelimit-key"] = config.rateLimitKey;
|
||||
}
|
||||
if (import.meta.server) {
|
||||
options.headers['x-ratelimit-key'] = config.rateLimitKey
|
||||
}
|
||||
|
||||
if (!skipAuth) {
|
||||
const auth = await useAuth();
|
||||
if (!skipAuth) {
|
||||
const auth = await useAuth()
|
||||
|
||||
options.headers.Authorization = auth.value.token;
|
||||
}
|
||||
options.headers.Authorization = auth.value.token
|
||||
}
|
||||
|
||||
if (options.apiVersion || options.internal) {
|
||||
// Base may end in /vD/ or /vD. We would need to replace the digit with the new version number
|
||||
// and keep the trailing slash if it exists
|
||||
const baseVersion = base.match(/\/v\d\//);
|
||||
if (options.apiVersion || options.internal) {
|
||||
// Base may end in /vD/ or /vD. We would need to replace the digit with the new version number
|
||||
// and keep the trailing slash if it exists
|
||||
const baseVersion = base.match(/\/v\d\//)
|
||||
|
||||
const replaceStr = options.internal ? `/_internal/` : `/v${options.apiVersion}/`;
|
||||
const replaceStr = options.internal ? `/_internal/` : `/v${options.apiVersion}/`
|
||||
|
||||
if (baseVersion) {
|
||||
base = base.replace(baseVersion[0], replaceStr);
|
||||
} else {
|
||||
base = base.replace(/\/v\d$/, replaceStr);
|
||||
}
|
||||
if (baseVersion) {
|
||||
base = base.replace(baseVersion[0], replaceStr)
|
||||
} else {
|
||||
base = base.replace(/\/v\d$/, replaceStr)
|
||||
}
|
||||
|
||||
delete options.apiVersion;
|
||||
}
|
||||
delete options.apiVersion
|
||||
}
|
||||
|
||||
return await $fetch(`${base}${url}`, options);
|
||||
};
|
||||
return await $fetch(`${base}${url}`, options)
|
||||
}
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
type ImageUploadContext = {
|
||||
projectID?: string;
|
||||
context: "project" | "version" | "thread_message" | "report";
|
||||
};
|
||||
projectID?: string
|
||||
context: 'project' | 'version' | 'thread_message' | 'report'
|
||||
}
|
||||
|
||||
interface ImageUploadResponse {
|
||||
id: string;
|
||||
url: string;
|
||||
id: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export const useImageUpload = async (file: File, ctx: ImageUploadContext) => {
|
||||
// Make sure file is of type image/png, image/jpeg, image/gif, or image/webp
|
||||
if (
|
||||
!file.type.startsWith("image/") ||
|
||||
!["png", "jpeg", "gif", "webp"].includes(file.type.split("/")[1])
|
||||
) {
|
||||
throw new Error("File is not an accepted image type");
|
||||
}
|
||||
// Make sure file is of type image/png, image/jpeg, image/gif, or image/webp
|
||||
if (
|
||||
!file.type.startsWith('image/') ||
|
||||
!['png', 'jpeg', 'gif', 'webp'].includes(file.type.split('/')[1])
|
||||
) {
|
||||
throw new Error('File is not an accepted image type')
|
||||
}
|
||||
|
||||
// Make sure file is less than 1MB
|
||||
if (file.size > 1024 * 1024) {
|
||||
throw new Error("File exceeds the 1MiB size limit");
|
||||
}
|
||||
// Make sure file is less than 1MB
|
||||
if (file.size > 1024 * 1024) {
|
||||
throw new Error('File exceeds the 1MiB size limit')
|
||||
}
|
||||
|
||||
const qs = new URLSearchParams();
|
||||
if (ctx.projectID) qs.set("project_id", ctx.projectID);
|
||||
qs.set("context", ctx.context);
|
||||
qs.set("ext", file.type.split("/")[1]);
|
||||
const url = `image?${qs.toString()}`;
|
||||
const qs = new URLSearchParams()
|
||||
if (ctx.projectID) qs.set('project_id', ctx.projectID)
|
||||
qs.set('context', ctx.context)
|
||||
qs.set('ext', file.type.split('/')[1])
|
||||
const url = `image?${qs.toString()}`
|
||||
|
||||
const response = (await useBaseFetch(url, {
|
||||
method: "POST",
|
||||
body: file,
|
||||
apiVersion: 3,
|
||||
})) as ImageUploadResponse;
|
||||
const response = (await useBaseFetch(url, {
|
||||
method: 'POST',
|
||||
body: file,
|
||||
apiVersion: 3,
|
||||
})) as ImageUploadResponse
|
||||
|
||||
// Type check to see if response has a url property and an id property
|
||||
if (!response?.id || typeof response.id !== "string") {
|
||||
throw new Error("Unexpected response from server");
|
||||
}
|
||||
if (!response?.url || typeof response.url !== "string") {
|
||||
throw new Error("Unexpected response from server");
|
||||
}
|
||||
// Type check to see if response has a url property and an id property
|
||||
if (!response?.id || typeof response.id !== 'string') {
|
||||
throw new Error('Unexpected response from server')
|
||||
}
|
||||
if (!response?.url || typeof response.url !== 'string') {
|
||||
throw new Error('Unexpected response from server')
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
return response
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
export const useLoading = () => useState("loading", () => false);
|
||||
export const useLoading = () => useState('loading', () => false)
|
||||
|
||||
export const startLoading = () => {
|
||||
const loading = useLoading();
|
||||
const loading = useLoading()
|
||||
|
||||
loading.value = true;
|
||||
};
|
||||
loading.value = true
|
||||
}
|
||||
|
||||
export const stopLoading = () => {
|
||||
const loading = useLoading();
|
||||
const loading = useLoading()
|
||||
|
||||
loading.value = false;
|
||||
};
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export function useTheme() {
|
||||
return useNuxtApp().$theme;
|
||||
return useNuxtApp().$theme
|
||||
}
|
||||
|
||||
export function useCosmetics() {
|
||||
return useNuxtApp().$cosmetics;
|
||||
return useNuxtApp().$cosmetics
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { useRoute as useNativeRoute, useRouter as useNativeRouter } from "vue-router";
|
||||
export { useRoute as useNativeRoute, useRouter as useNativeRouter } from 'vue-router'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export const getArrayOrString = (x) => {
|
||||
if (typeof x === "string" || x instanceof String) {
|
||||
return [x];
|
||||
} else {
|
||||
return x;
|
||||
}
|
||||
};
|
||||
if (typeof x === 'string' || x instanceof String) {
|
||||
return [x]
|
||||
} else {
|
||||
return x
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* @param {string?} key The key of the route param to extract.
|
||||
* @returns {import('vue').Ref<string | string[] | undefined>}
|
||||
*/
|
||||
export const useRouteId = (key = "id") => {
|
||||
const route = useNativeRoute();
|
||||
return route.params?.[key] || undefined;
|
||||
};
|
||||
export const useRouteId = (key = 'id') => {
|
||||
const route = useNativeRoute()
|
||||
return route.params?.[key] || undefined
|
||||
}
|
||||
|
||||
@@ -1,330 +1,330 @@
|
||||
import type { JWTAuth, ModuleError, ModuleName } from "@modrinth/utils";
|
||||
import { ModrinthServerError } from "@modrinth/utils";
|
||||
import { injectNotificationManager } from "@modrinth/ui";
|
||||
import { useServersFetch } from "./servers-fetch.ts";
|
||||
import { injectNotificationManager } from '@modrinth/ui'
|
||||
import type { JWTAuth, ModuleError, ModuleName } from '@modrinth/utils'
|
||||
import { ModrinthServerError } from '@modrinth/utils'
|
||||
|
||||
import {
|
||||
BackupsModule,
|
||||
ContentModule,
|
||||
FSModule,
|
||||
GeneralModule,
|
||||
NetworkModule,
|
||||
StartupModule,
|
||||
WSModule,
|
||||
} from "./modules/index.ts";
|
||||
BackupsModule,
|
||||
ContentModule,
|
||||
FSModule,
|
||||
GeneralModule,
|
||||
NetworkModule,
|
||||
StartupModule,
|
||||
WSModule,
|
||||
} from './modules/index.ts'
|
||||
import { useServersFetch } from './servers-fetch.ts'
|
||||
|
||||
export function handleError(err: any) {
|
||||
const { addNotification } = injectNotificationManager();
|
||||
if (err instanceof ModrinthServerError && err.v1Error) {
|
||||
addNotification({
|
||||
title: err.v1Error?.context ?? `An error occurred`,
|
||||
type: "error",
|
||||
text: err.v1Error.description,
|
||||
errorCode: err.v1Error.error,
|
||||
});
|
||||
} else {
|
||||
addNotification({
|
||||
title: "An error occurred",
|
||||
type: "error",
|
||||
text: err.message ?? (err.data ? err.data.description : err),
|
||||
});
|
||||
}
|
||||
const { addNotification } = injectNotificationManager()
|
||||
if (err instanceof ModrinthServerError && err.v1Error) {
|
||||
addNotification({
|
||||
title: err.v1Error?.context ?? `An error occurred`,
|
||||
type: 'error',
|
||||
text: err.v1Error.description,
|
||||
errorCode: err.v1Error.error,
|
||||
})
|
||||
} else {
|
||||
addNotification({
|
||||
title: 'An error occurred',
|
||||
type: 'error',
|
||||
text: err.message ?? (err.data ? err.data.description : err),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class ModrinthServer {
|
||||
readonly serverId: string;
|
||||
private errors: Partial<Record<ModuleName, ModuleError>> = {};
|
||||
readonly serverId: string
|
||||
private errors: Partial<Record<ModuleName, ModuleError>> = {}
|
||||
|
||||
readonly general: GeneralModule;
|
||||
readonly content: ContentModule;
|
||||
readonly backups: BackupsModule;
|
||||
readonly network: NetworkModule;
|
||||
readonly startup: StartupModule;
|
||||
readonly ws: WSModule;
|
||||
readonly fs: FSModule;
|
||||
readonly general: GeneralModule
|
||||
readonly content: ContentModule
|
||||
readonly backups: BackupsModule
|
||||
readonly network: NetworkModule
|
||||
readonly startup: StartupModule
|
||||
readonly ws: WSModule
|
||||
readonly fs: FSModule
|
||||
|
||||
constructor(serverId: string) {
|
||||
this.serverId = serverId;
|
||||
constructor(serverId: string) {
|
||||
this.serverId = serverId
|
||||
|
||||
this.general = new GeneralModule(this);
|
||||
this.content = new ContentModule(this);
|
||||
this.backups = new BackupsModule(this);
|
||||
this.network = new NetworkModule(this);
|
||||
this.startup = new StartupModule(this);
|
||||
this.ws = new WSModule(this);
|
||||
this.fs = new FSModule(this);
|
||||
}
|
||||
this.general = new GeneralModule(this)
|
||||
this.content = new ContentModule(this)
|
||||
this.backups = new BackupsModule(this)
|
||||
this.network = new NetworkModule(this)
|
||||
this.startup = new StartupModule(this)
|
||||
this.ws = new WSModule(this)
|
||||
this.fs = new FSModule(this)
|
||||
}
|
||||
|
||||
async createMissingFolders(path: string): Promise<void> {
|
||||
if (path.startsWith("/")) {
|
||||
path = path.substring(1);
|
||||
}
|
||||
const folders = path.split("/");
|
||||
let currentPath = "";
|
||||
async createMissingFolders(path: string): Promise<void> {
|
||||
if (path.startsWith('/')) {
|
||||
path = path.substring(1)
|
||||
}
|
||||
const folders = path.split('/')
|
||||
let currentPath = ''
|
||||
|
||||
for (const folder of folders) {
|
||||
currentPath += "/" + folder;
|
||||
try {
|
||||
await this.fs.createFileOrFolder(currentPath, "directory");
|
||||
} catch {
|
||||
// Folder might already exist, ignore error
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const folder of folders) {
|
||||
currentPath += '/' + folder
|
||||
try {
|
||||
await this.fs.createFileOrFolder(currentPath, 'directory')
|
||||
} catch {
|
||||
// Folder might already exist, ignore error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fetchConfigFile(fileName: string): Promise<any> {
|
||||
return await useServersFetch(`servers/${this.serverId}/config/${fileName}`);
|
||||
}
|
||||
async fetchConfigFile(fileName: string): Promise<any> {
|
||||
return await useServersFetch(`servers/${this.serverId}/config/${fileName}`)
|
||||
}
|
||||
|
||||
constructServerProperties(properties: any): string {
|
||||
let fileContent = `#Minecraft server properties\n#${new Date().toUTCString()}\n`;
|
||||
constructServerProperties(properties: any): string {
|
||||
let fileContent = `#Minecraft server properties\n#${new Date().toUTCString()}\n`
|
||||
|
||||
for (const [key, value] of Object.entries(properties)) {
|
||||
if (typeof value === "object") {
|
||||
fileContent += `${key}=${JSON.stringify(value)}\n`;
|
||||
} else if (typeof value === "boolean") {
|
||||
fileContent += `${key}=${value ? "true" : "false"}\n`;
|
||||
} else {
|
||||
fileContent += `${key}=${value}\n`;
|
||||
}
|
||||
}
|
||||
for (const [key, value] of Object.entries(properties)) {
|
||||
if (typeof value === 'object') {
|
||||
fileContent += `${key}=${JSON.stringify(value)}\n`
|
||||
} else if (typeof value === 'boolean') {
|
||||
fileContent += `${key}=${value ? 'true' : 'false'}\n`
|
||||
} else {
|
||||
fileContent += `${key}=${value}\n`
|
||||
}
|
||||
}
|
||||
|
||||
return fileContent;
|
||||
}
|
||||
return fileContent
|
||||
}
|
||||
|
||||
async processImage(iconUrl: string | undefined): Promise<string | undefined> {
|
||||
const sharedImage = useState<string | undefined>(`server-icon-${this.serverId}`);
|
||||
async processImage(iconUrl: string | undefined): Promise<string | undefined> {
|
||||
const sharedImage = useState<string | undefined>(`server-icon-${this.serverId}`)
|
||||
|
||||
if (sharedImage.value) {
|
||||
return sharedImage.value;
|
||||
}
|
||||
if (sharedImage.value) {
|
||||
return sharedImage.value
|
||||
}
|
||||
|
||||
try {
|
||||
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`);
|
||||
try {
|
||||
const fileData = await useServersFetch(`/download?path=/server-icon-original.png`, {
|
||||
override: auth,
|
||||
retry: 1, // Reduce retries for optional resources
|
||||
});
|
||||
try {
|
||||
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`)
|
||||
try {
|
||||
const fileData = await useServersFetch(`/download?path=/server-icon-original.png`, {
|
||||
override: auth,
|
||||
retry: 1, // Reduce retries for optional resources
|
||||
})
|
||||
|
||||
if (fileData instanceof Blob && import.meta.client) {
|
||||
const dataURL = await new Promise<string>((resolve) => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
canvas.width = 512;
|
||||
canvas.height = 512;
|
||||
ctx?.drawImage(img, 0, 0, 512, 512);
|
||||
const dataURL = canvas.toDataURL("image/png");
|
||||
sharedImage.value = dataURL;
|
||||
resolve(dataURL);
|
||||
URL.revokeObjectURL(img.src);
|
||||
};
|
||||
img.src = URL.createObjectURL(fileData);
|
||||
});
|
||||
return dataURL;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ModrinthServerError) {
|
||||
if (error.statusCode && error.statusCode >= 500) {
|
||||
console.debug("Service unavailable, skipping icon processing");
|
||||
sharedImage.value = undefined;
|
||||
return undefined;
|
||||
}
|
||||
if (fileData instanceof Blob && import.meta.client) {
|
||||
const dataURL = await new Promise<string>((resolve) => {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
canvas.width = 512
|
||||
canvas.height = 512
|
||||
ctx?.drawImage(img, 0, 0, 512, 512)
|
||||
const dataURL = canvas.toDataURL('image/png')
|
||||
sharedImage.value = dataURL
|
||||
resolve(dataURL)
|
||||
URL.revokeObjectURL(img.src)
|
||||
}
|
||||
img.src = URL.createObjectURL(fileData)
|
||||
})
|
||||
return dataURL
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ModrinthServerError) {
|
||||
if (error.statusCode && error.statusCode >= 500) {
|
||||
console.debug('Service unavailable, skipping icon processing')
|
||||
sharedImage.value = undefined
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (error.statusCode === 404 && iconUrl) {
|
||||
try {
|
||||
const response = await fetch(iconUrl);
|
||||
if (!response.ok) throw new Error("Failed to fetch icon");
|
||||
const file = await response.blob();
|
||||
const originalFile = new File([file], "server-icon-original.png", {
|
||||
type: "image/png",
|
||||
});
|
||||
if (error.statusCode === 404 && iconUrl) {
|
||||
try {
|
||||
const response = await fetch(iconUrl)
|
||||
if (!response.ok) throw new Error('Failed to fetch icon')
|
||||
const file = await response.blob()
|
||||
const originalFile = new File([file], 'server-icon-original.png', {
|
||||
type: 'image/png',
|
||||
})
|
||||
|
||||
if (import.meta.client) {
|
||||
const dataURL = await new Promise<string>((resolve) => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
ctx?.drawImage(img, 0, 0, 64, 64);
|
||||
canvas.toBlob(async (blob) => {
|
||||
if (blob) {
|
||||
const scaledFile = new File([blob], "server-icon.png", {
|
||||
type: "image/png",
|
||||
});
|
||||
await useServersFetch(`/create?path=/server-icon.png&type=file`, {
|
||||
method: "POST",
|
||||
contentType: "application/octet-stream",
|
||||
body: scaledFile,
|
||||
override: auth,
|
||||
});
|
||||
await useServersFetch(`/create?path=/server-icon-original.png&type=file`, {
|
||||
method: "POST",
|
||||
contentType: "application/octet-stream",
|
||||
body: originalFile,
|
||||
override: auth,
|
||||
});
|
||||
}
|
||||
}, "image/png");
|
||||
const dataURL = canvas.toDataURL("image/png");
|
||||
sharedImage.value = dataURL;
|
||||
resolve(dataURL);
|
||||
URL.revokeObjectURL(img.src);
|
||||
};
|
||||
img.src = URL.createObjectURL(file);
|
||||
});
|
||||
return dataURL;
|
||||
}
|
||||
} catch (externalError: any) {
|
||||
console.debug("Could not process external icon:", externalError.message);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.debug("Icon processing failed:", error.message);
|
||||
}
|
||||
if (import.meta.client) {
|
||||
const dataURL = await new Promise<string>((resolve) => {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
canvas.width = 64
|
||||
canvas.height = 64
|
||||
ctx?.drawImage(img, 0, 0, 64, 64)
|
||||
canvas.toBlob(async (blob) => {
|
||||
if (blob) {
|
||||
const scaledFile = new File([blob], 'server-icon.png', {
|
||||
type: 'image/png',
|
||||
})
|
||||
await useServersFetch(`/create?path=/server-icon.png&type=file`, {
|
||||
method: 'POST',
|
||||
contentType: 'application/octet-stream',
|
||||
body: scaledFile,
|
||||
override: auth,
|
||||
})
|
||||
await useServersFetch(`/create?path=/server-icon-original.png&type=file`, {
|
||||
method: 'POST',
|
||||
contentType: 'application/octet-stream',
|
||||
body: originalFile,
|
||||
override: auth,
|
||||
})
|
||||
}
|
||||
}, 'image/png')
|
||||
const dataURL = canvas.toDataURL('image/png')
|
||||
sharedImage.value = dataURL
|
||||
resolve(dataURL)
|
||||
URL.revokeObjectURL(img.src)
|
||||
}
|
||||
img.src = URL.createObjectURL(file)
|
||||
})
|
||||
return dataURL
|
||||
}
|
||||
} catch (externalError: any) {
|
||||
console.debug('Could not process external icon:', externalError.message)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.debug('Icon processing failed:', error.message)
|
||||
}
|
||||
|
||||
sharedImage.value = undefined;
|
||||
return undefined;
|
||||
}
|
||||
sharedImage.value = undefined
|
||||
return undefined
|
||||
}
|
||||
|
||||
async testNodeReachability(): Promise<boolean> {
|
||||
if (!this.general?.node?.instance) {
|
||||
console.warn("No node instance available for ping test");
|
||||
return false;
|
||||
}
|
||||
async testNodeReachability(): Promise<boolean> {
|
||||
if (!this.general?.node?.instance) {
|
||||
console.warn('No node instance available for ping test')
|
||||
return false
|
||||
}
|
||||
|
||||
const wsUrl = `wss://${this.general.node.instance}/pingtest`;
|
||||
const wsUrl = `wss://${this.general.node.instance}/pingtest`
|
||||
|
||||
try {
|
||||
return await new Promise((resolve) => {
|
||||
const socket = new WebSocket(wsUrl);
|
||||
const timeout = setTimeout(() => {
|
||||
socket.close();
|
||||
resolve(false);
|
||||
}, 5000);
|
||||
try {
|
||||
return await new Promise((resolve) => {
|
||||
const socket = new WebSocket(wsUrl)
|
||||
const timeout = setTimeout(() => {
|
||||
socket.close()
|
||||
resolve(false)
|
||||
}, 5000)
|
||||
|
||||
socket.onopen = () => {
|
||||
clearTimeout(timeout);
|
||||
socket.send(performance.now().toString());
|
||||
};
|
||||
socket.onopen = () => {
|
||||
clearTimeout(timeout)
|
||||
socket.send(performance.now().toString())
|
||||
}
|
||||
|
||||
socket.onmessage = () => {
|
||||
clearTimeout(timeout);
|
||||
socket.close();
|
||||
resolve(true);
|
||||
};
|
||||
socket.onmessage = () => {
|
||||
clearTimeout(timeout)
|
||||
socket.close()
|
||||
resolve(true)
|
||||
}
|
||||
|
||||
socket.onerror = () => {
|
||||
clearTimeout(timeout);
|
||||
resolve(false);
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Failed to ping node ${wsUrl}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
socket.onerror = () => {
|
||||
clearTimeout(timeout)
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`Failed to ping node ${wsUrl}:`, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async refresh(
|
||||
modules: ModuleName[] = [],
|
||||
options?: {
|
||||
preserveConnection?: boolean;
|
||||
preserveInstallState?: boolean;
|
||||
},
|
||||
): Promise<void> {
|
||||
const modulesToRefresh =
|
||||
modules.length > 0
|
||||
? modules
|
||||
: (["general", "content", "backups", "network", "startup", "ws", "fs"] as ModuleName[]);
|
||||
async refresh(
|
||||
modules: ModuleName[] = [],
|
||||
options?: {
|
||||
preserveConnection?: boolean
|
||||
preserveInstallState?: boolean
|
||||
},
|
||||
): Promise<void> {
|
||||
const modulesToRefresh =
|
||||
modules.length > 0
|
||||
? modules
|
||||
: (['general', 'content', 'backups', 'network', 'startup', 'ws', 'fs'] as ModuleName[])
|
||||
|
||||
for (const module of modulesToRefresh) {
|
||||
this.errors[module] = undefined;
|
||||
for (const module of modulesToRefresh) {
|
||||
this.errors[module] = undefined
|
||||
|
||||
try {
|
||||
switch (module) {
|
||||
case "general": {
|
||||
if (options?.preserveConnection) {
|
||||
const currentImage = this.general.image;
|
||||
const currentMotd = this.general.motd;
|
||||
const currentStatus = this.general.status;
|
||||
try {
|
||||
switch (module) {
|
||||
case 'general': {
|
||||
if (options?.preserveConnection) {
|
||||
const currentImage = this.general.image
|
||||
const currentMotd = this.general.motd
|
||||
const currentStatus = this.general.status
|
||||
|
||||
await this.general.fetch();
|
||||
await this.general.fetch()
|
||||
|
||||
if (currentImage) {
|
||||
this.general.image = currentImage;
|
||||
}
|
||||
if (currentMotd) {
|
||||
this.general.motd = currentMotd;
|
||||
}
|
||||
if (options.preserveInstallState && currentStatus === "installing") {
|
||||
this.general.status = "installing";
|
||||
}
|
||||
} else {
|
||||
await this.general.fetch();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "content":
|
||||
await this.content.fetch();
|
||||
break;
|
||||
case "backups":
|
||||
await this.backups.fetch();
|
||||
break;
|
||||
case "network":
|
||||
await this.network.fetch();
|
||||
break;
|
||||
case "startup":
|
||||
await this.startup.fetch();
|
||||
break;
|
||||
case "ws":
|
||||
await this.ws.fetch();
|
||||
break;
|
||||
case "fs":
|
||||
await this.fs.fetch();
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ModrinthServerError) {
|
||||
if (error.statusCode === 404 && ["fs", "content"].includes(module)) {
|
||||
console.debug(`Optional ${module} resource not found:`, error.message);
|
||||
continue;
|
||||
}
|
||||
if (currentImage) {
|
||||
this.general.image = currentImage
|
||||
}
|
||||
if (currentMotd) {
|
||||
this.general.motd = currentMotd
|
||||
}
|
||||
if (options.preserveInstallState && currentStatus === 'installing') {
|
||||
this.general.status = 'installing'
|
||||
}
|
||||
} else {
|
||||
await this.general.fetch()
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'content':
|
||||
await this.content.fetch()
|
||||
break
|
||||
case 'backups':
|
||||
await this.backups.fetch()
|
||||
break
|
||||
case 'network':
|
||||
await this.network.fetch()
|
||||
break
|
||||
case 'startup':
|
||||
await this.startup.fetch()
|
||||
break
|
||||
case 'ws':
|
||||
await this.ws.fetch()
|
||||
break
|
||||
case 'fs':
|
||||
await this.fs.fetch()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ModrinthServerError) {
|
||||
if (error.statusCode === 404 && ['fs', 'content'].includes(module)) {
|
||||
console.debug(`Optional ${module} resource not found:`, error.message)
|
||||
continue
|
||||
}
|
||||
|
||||
if (error.statusCode && error.statusCode >= 500) {
|
||||
console.debug(`Temporary ${module} unavailable:`, error.message);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (error.statusCode && error.statusCode >= 500) {
|
||||
console.debug(`Temporary ${module} unavailable:`, error.message)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
this.errors[module] = {
|
||||
error:
|
||||
error instanceof ModrinthServerError
|
||||
? error
|
||||
: new ModrinthServerError("Unknown error", undefined, error as Error),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
this.errors[module] = {
|
||||
error:
|
||||
error instanceof ModrinthServerError
|
||||
? error
|
||||
: new ModrinthServerError('Unknown error', undefined, error as Error),
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get moduleErrors() {
|
||||
return this.errors;
|
||||
}
|
||||
get moduleErrors() {
|
||||
return this.errors
|
||||
}
|
||||
}
|
||||
|
||||
export const useModrinthServers = async (
|
||||
serverId: string,
|
||||
includedModules: ModuleName[] = ["general"],
|
||||
serverId: string,
|
||||
includedModules: ModuleName[] = ['general'],
|
||||
) => {
|
||||
const server = new ModrinthServer(serverId);
|
||||
await server.refresh(includedModules);
|
||||
return reactive(server);
|
||||
};
|
||||
const server = new ModrinthServer(serverId)
|
||||
await server.refresh(includedModules)
|
||||
return reactive(server)
|
||||
}
|
||||
|
||||
@@ -1,79 +1,80 @@
|
||||
import type { Backup, AutoBackupSettings } from "@modrinth/utils";
|
||||
import { useServersFetch } from "../servers-fetch.ts";
|
||||
import { ServerModule } from "./base.ts";
|
||||
import type { AutoBackupSettings, Backup } from '@modrinth/utils'
|
||||
|
||||
import { useServersFetch } from '../servers-fetch.ts'
|
||||
import { ServerModule } from './base.ts'
|
||||
|
||||
export class BackupsModule extends ServerModule {
|
||||
data: Backup[] = [];
|
||||
data: Backup[] = []
|
||||
|
||||
async fetch(): Promise<void> {
|
||||
this.data = await useServersFetch<Backup[]>(`servers/${this.serverId}/backups`, {}, "backups");
|
||||
}
|
||||
async fetch(): Promise<void> {
|
||||
this.data = await useServersFetch<Backup[]>(`servers/${this.serverId}/backups`, {}, 'backups')
|
||||
}
|
||||
|
||||
async create(backupName: string): Promise<string> {
|
||||
const response = await useServersFetch<{ id: string }>(`servers/${this.serverId}/backups`, {
|
||||
method: "POST",
|
||||
body: { name: backupName },
|
||||
});
|
||||
await this.fetch(); // Refresh this module
|
||||
return response.id;
|
||||
}
|
||||
async create(backupName: string): Promise<string> {
|
||||
const response = await useServersFetch<{ id: string }>(`servers/${this.serverId}/backups`, {
|
||||
method: 'POST',
|
||||
body: { name: backupName },
|
||||
})
|
||||
await this.fetch() // Refresh this module
|
||||
return response.id
|
||||
}
|
||||
|
||||
async rename(backupId: string, newName: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/rename`, {
|
||||
method: "POST",
|
||||
body: { name: newName },
|
||||
});
|
||||
await this.fetch(); // Refresh this module
|
||||
}
|
||||
async rename(backupId: string, newName: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/rename`, {
|
||||
method: 'POST',
|
||||
body: { name: newName },
|
||||
})
|
||||
await this.fetch() // Refresh this module
|
||||
}
|
||||
|
||||
async delete(backupId: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
await this.fetch(); // Refresh this module
|
||||
}
|
||||
async delete(backupId: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
await this.fetch() // Refresh this module
|
||||
}
|
||||
|
||||
async restore(backupId: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/restore`, {
|
||||
method: "POST",
|
||||
});
|
||||
await this.fetch(); // Refresh this module
|
||||
}
|
||||
async restore(backupId: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/restore`, {
|
||||
method: 'POST',
|
||||
})
|
||||
await this.fetch() // Refresh this module
|
||||
}
|
||||
|
||||
async prepare(backupId: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/prepare-download`, {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
async prepare(backupId: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/prepare-download`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
async lock(backupId: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/lock`, {
|
||||
method: "POST",
|
||||
});
|
||||
await this.fetch(); // Refresh this module
|
||||
}
|
||||
async lock(backupId: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/lock`, {
|
||||
method: 'POST',
|
||||
})
|
||||
await this.fetch() // Refresh this module
|
||||
}
|
||||
|
||||
async unlock(backupId: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/unlock`, {
|
||||
method: "POST",
|
||||
});
|
||||
await this.fetch(); // Refresh this module
|
||||
}
|
||||
async unlock(backupId: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/unlock`, {
|
||||
method: 'POST',
|
||||
})
|
||||
await this.fetch() // Refresh this module
|
||||
}
|
||||
|
||||
async retry(backupId: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/retry`, {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
async retry(backupId: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/retry`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
async updateAutoBackup(autoBackup: "enable" | "disable", interval: number): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/autobackup`, {
|
||||
method: "POST",
|
||||
body: { set: autoBackup, interval },
|
||||
});
|
||||
}
|
||||
async updateAutoBackup(autoBackup: 'enable' | 'disable', interval: number): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/autobackup`, {
|
||||
method: 'POST',
|
||||
body: { set: autoBackup, interval },
|
||||
})
|
||||
}
|
||||
|
||||
async getAutoBackup(): Promise<AutoBackupSettings> {
|
||||
return await useServersFetch(`servers/${this.serverId}/autobackup`);
|
||||
}
|
||||
async getAutoBackup(): Promise<AutoBackupSettings> {
|
||||
return await useServersFetch(`servers/${this.serverId}/autobackup`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { ModrinthServer } from "../modrinth-servers.ts";
|
||||
import type { ModrinthServer } from '../modrinth-servers.ts'
|
||||
|
||||
export abstract class ServerModule {
|
||||
protected server: ModrinthServer;
|
||||
protected server: ModrinthServer
|
||||
|
||||
constructor(server: ModrinthServer) {
|
||||
this.server = server;
|
||||
}
|
||||
constructor(server: ModrinthServer) {
|
||||
this.server = server
|
||||
}
|
||||
|
||||
protected get serverId(): string {
|
||||
return this.server.serverId;
|
||||
}
|
||||
protected get serverId(): string {
|
||||
return this.server.serverId
|
||||
}
|
||||
|
||||
abstract fetch(): Promise<void>;
|
||||
abstract fetch(): Promise<void>
|
||||
}
|
||||
|
||||
@@ -1,36 +1,37 @@
|
||||
import type { Mod, ContentType } from "@modrinth/utils";
|
||||
import { useServersFetch } from "../servers-fetch.ts";
|
||||
import { ServerModule } from "./base.ts";
|
||||
import type { ContentType, Mod } from '@modrinth/utils'
|
||||
|
||||
import { useServersFetch } from '../servers-fetch.ts'
|
||||
import { ServerModule } from './base.ts'
|
||||
|
||||
export class ContentModule extends ServerModule {
|
||||
data: Mod[] = [];
|
||||
data: Mod[] = []
|
||||
|
||||
async fetch(): Promise<void> {
|
||||
const mods = await useServersFetch<Mod[]>(`servers/${this.serverId}/mods`, {}, "content");
|
||||
this.data = mods.sort((a, b) => (a?.name ?? "").localeCompare(b?.name ?? ""));
|
||||
}
|
||||
async fetch(): Promise<void> {
|
||||
const mods = await useServersFetch<Mod[]>(`servers/${this.serverId}/mods`, {}, 'content')
|
||||
this.data = mods.sort((a, b) => (a?.name ?? '').localeCompare(b?.name ?? ''))
|
||||
}
|
||||
|
||||
async install(contentType: ContentType, projectId: string, versionId: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/mods`, {
|
||||
method: "POST",
|
||||
body: {
|
||||
rinth_ids: { project_id: projectId, version_id: versionId },
|
||||
install_as: contentType,
|
||||
},
|
||||
});
|
||||
}
|
||||
async install(contentType: ContentType, projectId: string, versionId: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/mods`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
rinth_ids: { project_id: projectId, version_id: versionId },
|
||||
install_as: contentType,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async remove(path: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/deleteMod`, {
|
||||
method: "POST",
|
||||
body: { path },
|
||||
});
|
||||
}
|
||||
async remove(path: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/deleteMod`, {
|
||||
method: 'POST',
|
||||
body: { path },
|
||||
})
|
||||
}
|
||||
|
||||
async reinstall(replace: string, projectId: string, versionId: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/mods/update`, {
|
||||
method: "POST",
|
||||
body: { replace, project_id: projectId, version_id: versionId },
|
||||
});
|
||||
}
|
||||
async reinstall(replace: string, projectId: string, versionId: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/mods/update`, {
|
||||
method: 'POST',
|
||||
body: { replace, project_id: projectId, version_id: versionId },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,247 +1,248 @@
|
||||
import type {
|
||||
FileUploadQuery,
|
||||
JWTAuth,
|
||||
DirectoryResponse,
|
||||
FilesystemOp,
|
||||
FSQueuedOp,
|
||||
} from "@modrinth/utils";
|
||||
import { ModrinthServerError } from "@modrinth/utils";
|
||||
import { useServersFetch } from "../servers-fetch.ts";
|
||||
import { ServerModule } from "./base.ts";
|
||||
DirectoryResponse,
|
||||
FilesystemOp,
|
||||
FileUploadQuery,
|
||||
FSQueuedOp,
|
||||
JWTAuth,
|
||||
} from '@modrinth/utils'
|
||||
import { ModrinthServerError } from '@modrinth/utils'
|
||||
|
||||
import { useServersFetch } from '../servers-fetch.ts'
|
||||
import { ServerModule } from './base.ts'
|
||||
|
||||
export class FSModule extends ServerModule {
|
||||
auth!: JWTAuth;
|
||||
ops: FilesystemOp[] = [];
|
||||
queuedOps: FSQueuedOp[] = [];
|
||||
opsQueuedForModification: string[] = [];
|
||||
auth!: JWTAuth
|
||||
ops: FilesystemOp[] = []
|
||||
queuedOps: FSQueuedOp[] = []
|
||||
opsQueuedForModification: string[] = []
|
||||
|
||||
async fetch(): Promise<void> {
|
||||
this.auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`, {}, "fs");
|
||||
this.ops = [];
|
||||
this.queuedOps = [];
|
||||
this.opsQueuedForModification = [];
|
||||
}
|
||||
async fetch(): Promise<void> {
|
||||
this.auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`, {}, 'fs')
|
||||
this.ops = []
|
||||
this.queuedOps = []
|
||||
this.opsQueuedForModification = []
|
||||
}
|
||||
|
||||
private async retryWithAuth<T>(
|
||||
requestFn: () => Promise<T>,
|
||||
ignoreFailure: boolean = false,
|
||||
): Promise<T> {
|
||||
try {
|
||||
return await requestFn();
|
||||
} catch (error) {
|
||||
if (error instanceof ModrinthServerError && error.statusCode === 401) {
|
||||
console.debug("Auth failed, refreshing JWT and retrying");
|
||||
await this.fetch(); // Refresh auth
|
||||
return await requestFn();
|
||||
}
|
||||
private async retryWithAuth<T>(
|
||||
requestFn: () => Promise<T>,
|
||||
ignoreFailure: boolean = false,
|
||||
): Promise<T> {
|
||||
try {
|
||||
return await requestFn()
|
||||
} catch (error) {
|
||||
if (error instanceof ModrinthServerError && error.statusCode === 401) {
|
||||
console.debug('Auth failed, refreshing JWT and retrying')
|
||||
await this.fetch() // Refresh auth
|
||||
return await requestFn()
|
||||
}
|
||||
|
||||
const available = await this.server.testNodeReachability();
|
||||
if (!available && !ignoreFailure) {
|
||||
this.server.moduleErrors.general = {
|
||||
error: new ModrinthServerError(
|
||||
"Unable to reach node. FS operation failed and subsequent ping test failed.",
|
||||
500,
|
||||
error as Error,
|
||||
"fs",
|
||||
),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
const available = await this.server.testNodeReachability()
|
||||
if (!available && !ignoreFailure) {
|
||||
this.server.moduleErrors.general = {
|
||||
error: new ModrinthServerError(
|
||||
'Unable to reach node. FS operation failed and subsequent ping test failed.',
|
||||
500,
|
||||
error as Error,
|
||||
'fs',
|
||||
),
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
listDirContents(
|
||||
path: string,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
ignoreFailure: boolean = false,
|
||||
): Promise<DirectoryResponse> {
|
||||
return this.retryWithAuth(async () => {
|
||||
const encodedPath = encodeURIComponent(path);
|
||||
return await useServersFetch(`/list?path=${encodedPath}&page=${page}&page_size=${pageSize}`, {
|
||||
override: this.auth,
|
||||
retry: false,
|
||||
});
|
||||
}, ignoreFailure);
|
||||
}
|
||||
listDirContents(
|
||||
path: string,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
ignoreFailure: boolean = false,
|
||||
): Promise<DirectoryResponse> {
|
||||
return this.retryWithAuth(async () => {
|
||||
const encodedPath = encodeURIComponent(path)
|
||||
return await useServersFetch(`/list?path=${encodedPath}&page=${page}&page_size=${pageSize}`, {
|
||||
override: this.auth,
|
||||
retry: false,
|
||||
})
|
||||
}, ignoreFailure)
|
||||
}
|
||||
|
||||
createFileOrFolder(path: string, type: "file" | "directory"): Promise<void> {
|
||||
return this.retryWithAuth(async () => {
|
||||
const encodedPath = encodeURIComponent(path);
|
||||
await useServersFetch(`/create?path=${encodedPath}&type=${type}`, {
|
||||
method: "POST",
|
||||
contentType: "application/octet-stream",
|
||||
override: this.auth,
|
||||
});
|
||||
});
|
||||
}
|
||||
createFileOrFolder(path: string, type: 'file' | 'directory'): Promise<void> {
|
||||
return this.retryWithAuth(async () => {
|
||||
const encodedPath = encodeURIComponent(path)
|
||||
await useServersFetch(`/create?path=${encodedPath}&type=${type}`, {
|
||||
method: 'POST',
|
||||
contentType: 'application/octet-stream',
|
||||
override: this.auth,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
uploadFile(path: string, file: File): FileUploadQuery {
|
||||
const encodedPath = encodeURIComponent(path);
|
||||
const progressSubject = new EventTarget();
|
||||
const abortController = new AbortController();
|
||||
uploadFile(path: string, file: File): FileUploadQuery {
|
||||
const encodedPath = encodeURIComponent(path)
|
||||
const progressSubject = new EventTarget()
|
||||
const abortController = new AbortController()
|
||||
|
||||
const uploadPromise = new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
const uploadPromise = new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
|
||||
xhr.upload.addEventListener("progress", (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const progress = (e.loaded / e.total) * 100;
|
||||
progressSubject.dispatchEvent(
|
||||
new CustomEvent("progress", {
|
||||
detail: { loaded: e.loaded, total: e.total, progress },
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const progress = (e.loaded / e.total) * 100
|
||||
progressSubject.dispatchEvent(
|
||||
new CustomEvent('progress', {
|
||||
detail: { loaded: e.loaded, total: e.total, progress },
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
resolve(xhr.response);
|
||||
} else {
|
||||
reject(new Error(`Upload failed with status ${xhr.status}`));
|
||||
}
|
||||
};
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
resolve(xhr.response)
|
||||
} else {
|
||||
reject(new Error(`Upload failed with status ${xhr.status}`))
|
||||
}
|
||||
}
|
||||
|
||||
xhr.onerror = () => reject(new Error("Upload failed"));
|
||||
xhr.onabort = () => reject(new Error("Upload cancelled"));
|
||||
xhr.onerror = () => reject(new Error('Upload failed'))
|
||||
xhr.onabort = () => reject(new Error('Upload cancelled'))
|
||||
|
||||
xhr.open("POST", `https://${this.auth.url}/create?path=${encodedPath}&type=file`);
|
||||
xhr.setRequestHeader("Authorization", `Bearer ${this.auth.token}`);
|
||||
xhr.setRequestHeader("Content-Type", "application/octet-stream");
|
||||
xhr.send(file);
|
||||
xhr.open('POST', `https://${this.auth.url}/create?path=${encodedPath}&type=file`)
|
||||
xhr.setRequestHeader('Authorization', `Bearer ${this.auth.token}`)
|
||||
xhr.setRequestHeader('Content-Type', 'application/octet-stream')
|
||||
xhr.send(file)
|
||||
|
||||
abortController.signal.addEventListener("abort", () => xhr.abort());
|
||||
});
|
||||
abortController.signal.addEventListener('abort', () => xhr.abort())
|
||||
})
|
||||
|
||||
return {
|
||||
promise: uploadPromise,
|
||||
onProgress: (
|
||||
callback: (progress: { loaded: number; total: number; progress: number }) => void,
|
||||
) => {
|
||||
progressSubject.addEventListener("progress", ((e: CustomEvent) => {
|
||||
callback(e.detail);
|
||||
}) as EventListener);
|
||||
},
|
||||
cancel: () => abortController.abort(),
|
||||
} as FileUploadQuery;
|
||||
}
|
||||
return {
|
||||
promise: uploadPromise,
|
||||
onProgress: (
|
||||
callback: (progress: { loaded: number; total: number; progress: number }) => void,
|
||||
) => {
|
||||
progressSubject.addEventListener('progress', ((e: CustomEvent) => {
|
||||
callback(e.detail)
|
||||
}) as EventListener)
|
||||
},
|
||||
cancel: () => abortController.abort(),
|
||||
} as FileUploadQuery
|
||||
}
|
||||
|
||||
renameFileOrFolder(path: string, name: string): Promise<void> {
|
||||
const pathName = path.split("/").slice(0, -1).join("/") + "/" + name;
|
||||
return this.retryWithAuth(async () => {
|
||||
await useServersFetch(`/move`, {
|
||||
method: "POST",
|
||||
override: this.auth,
|
||||
body: { source: path, destination: pathName },
|
||||
});
|
||||
});
|
||||
}
|
||||
renameFileOrFolder(path: string, name: string): Promise<void> {
|
||||
const pathName = path.split('/').slice(0, -1).join('/') + '/' + name
|
||||
return this.retryWithAuth(async () => {
|
||||
await useServersFetch(`/move`, {
|
||||
method: 'POST',
|
||||
override: this.auth,
|
||||
body: { source: path, destination: pathName },
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
updateFile(path: string, content: string): Promise<void> {
|
||||
const octetStream = new Blob([content], { type: "application/octet-stream" });
|
||||
return this.retryWithAuth(async () => {
|
||||
await useServersFetch(`/update?path=${path}`, {
|
||||
method: "PUT",
|
||||
contentType: "application/octet-stream",
|
||||
body: octetStream,
|
||||
override: this.auth,
|
||||
});
|
||||
});
|
||||
}
|
||||
updateFile(path: string, content: string): Promise<void> {
|
||||
const octetStream = new Blob([content], { type: 'application/octet-stream' })
|
||||
return this.retryWithAuth(async () => {
|
||||
await useServersFetch(`/update?path=${path}`, {
|
||||
method: 'PUT',
|
||||
contentType: 'application/octet-stream',
|
||||
body: octetStream,
|
||||
override: this.auth,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
moveFileOrFolder(path: string, newPath: string): Promise<void> {
|
||||
return this.retryWithAuth(async () => {
|
||||
await this.server.createMissingFolders(newPath.substring(0, newPath.lastIndexOf("/")));
|
||||
await useServersFetch(`/move`, {
|
||||
method: "POST",
|
||||
override: this.auth,
|
||||
body: { source: path, destination: newPath },
|
||||
});
|
||||
});
|
||||
}
|
||||
moveFileOrFolder(path: string, newPath: string): Promise<void> {
|
||||
return this.retryWithAuth(async () => {
|
||||
await this.server.createMissingFolders(newPath.substring(0, newPath.lastIndexOf('/')))
|
||||
await useServersFetch(`/move`, {
|
||||
method: 'POST',
|
||||
override: this.auth,
|
||||
body: { source: path, destination: newPath },
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
deleteFileOrFolder(path: string, recursive: boolean): Promise<void> {
|
||||
const encodedPath = encodeURIComponent(path);
|
||||
return this.retryWithAuth(async () => {
|
||||
await useServersFetch(`/delete?path=${encodedPath}&recursive=${recursive}`, {
|
||||
method: "DELETE",
|
||||
override: this.auth,
|
||||
});
|
||||
});
|
||||
}
|
||||
deleteFileOrFolder(path: string, recursive: boolean): Promise<void> {
|
||||
const encodedPath = encodeURIComponent(path)
|
||||
return this.retryWithAuth(async () => {
|
||||
await useServersFetch(`/delete?path=${encodedPath}&recursive=${recursive}`, {
|
||||
method: 'DELETE',
|
||||
override: this.auth,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
downloadFile(path: string, raw: boolean = false, ignoreFailure: boolean = false): Promise<any> {
|
||||
return this.retryWithAuth(async () => {
|
||||
const encodedPath = encodeURIComponent(path);
|
||||
const fileData = await useServersFetch(`/download?path=${encodedPath}`, {
|
||||
override: this.auth,
|
||||
});
|
||||
downloadFile(path: string, raw: boolean = false, ignoreFailure: boolean = false): Promise<any> {
|
||||
return this.retryWithAuth(async () => {
|
||||
const encodedPath = encodeURIComponent(path)
|
||||
const fileData = await useServersFetch(`/download?path=${encodedPath}`, {
|
||||
override: this.auth,
|
||||
})
|
||||
|
||||
if (fileData instanceof Blob) {
|
||||
return raw ? fileData : await fileData.text();
|
||||
}
|
||||
return fileData;
|
||||
}, ignoreFailure);
|
||||
}
|
||||
if (fileData instanceof Blob) {
|
||||
return raw ? fileData : await fileData.text()
|
||||
}
|
||||
return fileData
|
||||
}, ignoreFailure)
|
||||
}
|
||||
|
||||
extractFile(
|
||||
path: string,
|
||||
override = true,
|
||||
dry = false,
|
||||
silentQueue = false,
|
||||
): Promise<{ modpack_name: string | null; conflicting_files: string[] }> {
|
||||
return this.retryWithAuth(async () => {
|
||||
const encodedPath = encodeURIComponent(path);
|
||||
extractFile(
|
||||
path: string,
|
||||
override = true,
|
||||
dry = false,
|
||||
silentQueue = false,
|
||||
): Promise<{ modpack_name: string | null; conflicting_files: string[] }> {
|
||||
return this.retryWithAuth(async () => {
|
||||
const encodedPath = encodeURIComponent(path)
|
||||
|
||||
if (!silentQueue) {
|
||||
this.queuedOps.push({ op: "unarchive", src: path });
|
||||
setTimeout(() => this.removeQueuedOp("unarchive", path), 4000);
|
||||
}
|
||||
if (!silentQueue) {
|
||||
this.queuedOps.push({ op: 'unarchive', src: path })
|
||||
setTimeout(() => this.removeQueuedOp('unarchive', path), 4000)
|
||||
}
|
||||
|
||||
try {
|
||||
return await useServersFetch(
|
||||
`/unarchive?src=${encodedPath}&trg=/&override=${override}&dry=${dry}`,
|
||||
{
|
||||
method: "POST",
|
||||
override: this.auth,
|
||||
version: 1,
|
||||
},
|
||||
undefined,
|
||||
"Error extracting file",
|
||||
);
|
||||
} catch (err) {
|
||||
this.removeQueuedOp("unarchive", path);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
try {
|
||||
return await useServersFetch(
|
||||
`/unarchive?src=${encodedPath}&trg=/&override=${override}&dry=${dry}`,
|
||||
{
|
||||
method: 'POST',
|
||||
override: this.auth,
|
||||
version: 1,
|
||||
},
|
||||
undefined,
|
||||
'Error extracting file',
|
||||
)
|
||||
} catch (err) {
|
||||
this.removeQueuedOp('unarchive', path)
|
||||
throw err
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
modifyOp(id: string, action: "dismiss" | "cancel"): Promise<void> {
|
||||
return this.retryWithAuth(async () => {
|
||||
await useServersFetch(
|
||||
`/ops/${action}?id=${id}`,
|
||||
{
|
||||
method: "POST",
|
||||
override: this.auth,
|
||||
version: 1,
|
||||
},
|
||||
undefined,
|
||||
`Error ${action === "dismiss" ? "dismissing" : "cancelling"} filesystem operation`,
|
||||
);
|
||||
modifyOp(id: string, action: 'dismiss' | 'cancel'): Promise<void> {
|
||||
return this.retryWithAuth(async () => {
|
||||
await useServersFetch(
|
||||
`/ops/${action}?id=${id}`,
|
||||
{
|
||||
method: 'POST',
|
||||
override: this.auth,
|
||||
version: 1,
|
||||
},
|
||||
undefined,
|
||||
`Error ${action === 'dismiss' ? 'dismissing' : 'cancelling'} filesystem operation`,
|
||||
)
|
||||
|
||||
this.opsQueuedForModification = this.opsQueuedForModification.filter((x: string) => x !== id);
|
||||
this.ops = this.ops.filter((x: FilesystemOp) => x.id !== id);
|
||||
});
|
||||
}
|
||||
this.opsQueuedForModification = this.opsQueuedForModification.filter((x: string) => x !== id)
|
||||
this.ops = this.ops.filter((x: FilesystemOp) => x.id !== id)
|
||||
})
|
||||
}
|
||||
|
||||
removeQueuedOp(op: FSQueuedOp["op"], src: string): void {
|
||||
this.queuedOps = this.queuedOps.filter((x: FSQueuedOp) => x.op !== op || x.src !== src);
|
||||
}
|
||||
removeQueuedOp(op: FSQueuedOp['op'], src: string): void {
|
||||
this.queuedOps = this.queuedOps.filter((x: FSQueuedOp) => x.op !== op || x.src !== src)
|
||||
}
|
||||
|
||||
clearQueuedOps(): void {
|
||||
this.queuedOps = [];
|
||||
}
|
||||
clearQueuedOps(): void {
|
||||
this.queuedOps = []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,224 +1,229 @@
|
||||
import { $fetch } from "ofetch";
|
||||
import type { ServerGeneral, Project, PowerAction, JWTAuth } from "@modrinth/utils";
|
||||
import { useServersFetch } from "../servers-fetch.ts";
|
||||
import { ServerModule } from "./base.ts";
|
||||
import type { JWTAuth, PowerAction, Project, ServerGeneral } from '@modrinth/utils'
|
||||
import { $fetch } from 'ofetch'
|
||||
|
||||
import { useServersFetch } from '../servers-fetch.ts'
|
||||
import { ServerModule } from './base.ts'
|
||||
|
||||
export class GeneralModule extends ServerModule implements ServerGeneral {
|
||||
server_id!: string;
|
||||
name!: string;
|
||||
owner_id!: string;
|
||||
net!: { ip: string; port: number; domain: string };
|
||||
game!: string;
|
||||
backup_quota!: number;
|
||||
used_backup_quota!: number;
|
||||
status!: string;
|
||||
suspension_reason!: string;
|
||||
loader!: string;
|
||||
loader_version!: string;
|
||||
mc_version!: string;
|
||||
upstream!: {
|
||||
kind: "modpack" | "mod" | "resourcepack";
|
||||
version_id: string;
|
||||
project_id: string;
|
||||
} | null;
|
||||
server_id!: string
|
||||
name!: string
|
||||
owner_id!: string
|
||||
net!: { ip: string; port: number; domain: string }
|
||||
game!: string
|
||||
backup_quota!: number
|
||||
used_backup_quota!: number
|
||||
status!: string
|
||||
suspension_reason!: string
|
||||
loader!: string
|
||||
loader_version!: string
|
||||
mc_version!: string
|
||||
upstream!: {
|
||||
kind: 'modpack' | 'mod' | 'resourcepack'
|
||||
version_id: string
|
||||
project_id: string
|
||||
} | null
|
||||
|
||||
motd?: string;
|
||||
image?: string;
|
||||
project?: Project;
|
||||
sftp_username!: string;
|
||||
sftp_password!: string;
|
||||
sftp_host!: string;
|
||||
datacenter?: string;
|
||||
notices?: any[];
|
||||
node!: { token: string; instance: string };
|
||||
flows?: { intro?: boolean };
|
||||
motd?: string
|
||||
image?: string
|
||||
project?: Project
|
||||
sftp_username!: string
|
||||
sftp_password!: string
|
||||
sftp_host!: string
|
||||
datacenter?: string
|
||||
notices?: any[]
|
||||
node!: { token: string; instance: string }
|
||||
flows?: { intro?: boolean }
|
||||
|
||||
async fetch(): Promise<void> {
|
||||
const data = await useServersFetch<ServerGeneral>(`servers/${this.serverId}`, {}, "general");
|
||||
async fetch(): Promise<void> {
|
||||
const data = await useServersFetch<ServerGeneral>(`servers/${this.serverId}`, {}, 'general')
|
||||
|
||||
if (data.upstream?.project_id) {
|
||||
const project = await $fetch(
|
||||
`https://api.modrinth.com/v2/project/${data.upstream.project_id}`,
|
||||
);
|
||||
data.project = project as Project;
|
||||
}
|
||||
if (data.upstream?.project_id) {
|
||||
const project = await $fetch(
|
||||
`https://api.modrinth.com/v2/project/${data.upstream.project_id}`,
|
||||
)
|
||||
data.project = project as Project
|
||||
}
|
||||
|
||||
if (import.meta.client) {
|
||||
data.image = (await this.server.processImage(data.project?.icon_url)) ?? undefined;
|
||||
}
|
||||
if (import.meta.client) {
|
||||
data.image = (await this.server.processImage(data.project?.icon_url)) ?? undefined
|
||||
}
|
||||
|
||||
try {
|
||||
const motd = await this.getMotd();
|
||||
if (motd === "A Minecraft Server") {
|
||||
await this.setMotd(
|
||||
`§b${data.project?.title || data.loader + " " + data.mc_version} §f♦ §aModrinth Servers`,
|
||||
);
|
||||
}
|
||||
data.motd = motd;
|
||||
} catch {
|
||||
console.error("[Modrinth Servers] [General] Failed to fetch MOTD.");
|
||||
data.motd = undefined;
|
||||
}
|
||||
try {
|
||||
const motd = await this.getMotd()
|
||||
if (motd === 'A Minecraft Server') {
|
||||
await this.setMotd(
|
||||
`§b${data.project?.title || data.loader + ' ' + data.mc_version} §f♦ §aModrinth Servers`,
|
||||
)
|
||||
}
|
||||
data.motd = motd
|
||||
} catch {
|
||||
console.error('[Modrinth Servers] [General] Failed to fetch MOTD.')
|
||||
data.motd = undefined
|
||||
}
|
||||
|
||||
// Copy data to this module
|
||||
Object.assign(this, data);
|
||||
}
|
||||
// Copy data to this module
|
||||
Object.assign(this, data)
|
||||
}
|
||||
|
||||
async updateName(newName: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/name`, {
|
||||
method: "POST",
|
||||
body: { name: newName },
|
||||
});
|
||||
}
|
||||
async updateName(newName: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/name`, {
|
||||
method: 'POST',
|
||||
body: { name: newName },
|
||||
})
|
||||
}
|
||||
|
||||
async power(action: PowerAction): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/power`, {
|
||||
method: "POST",
|
||||
body: { action },
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
await this.fetch(); // Refresh this module
|
||||
}
|
||||
async power(action: PowerAction): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/power`, {
|
||||
method: 'POST',
|
||||
body: { action },
|
||||
})
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
await this.fetch() // Refresh this module
|
||||
}
|
||||
|
||||
async reinstall(
|
||||
loader: boolean,
|
||||
projectId: string,
|
||||
versionId?: string,
|
||||
loaderVersionId?: string,
|
||||
hardReset: boolean = false,
|
||||
): Promise<void> {
|
||||
const hardResetParam = hardReset ? "true" : "false";
|
||||
if (loader) {
|
||||
if (projectId.toLowerCase() === "neoforge") {
|
||||
projectId = "NeoForge";
|
||||
}
|
||||
await useServersFetch(`servers/${this.serverId}/reinstall?hard=${hardResetParam}`, {
|
||||
method: "POST",
|
||||
body: { loader: projectId, loader_version: loaderVersionId, game_version: versionId },
|
||||
});
|
||||
} else {
|
||||
await useServersFetch(`servers/${this.serverId}/reinstall?hard=${hardResetParam}`, {
|
||||
method: "POST",
|
||||
body: { project_id: projectId, version_id: versionId },
|
||||
});
|
||||
}
|
||||
}
|
||||
async reinstall(
|
||||
loader: boolean,
|
||||
projectId: string,
|
||||
versionId?: string,
|
||||
loaderVersionId?: string,
|
||||
hardReset: boolean = false,
|
||||
): Promise<void> {
|
||||
const hardResetParam = hardReset ? 'true' : 'false'
|
||||
if (loader) {
|
||||
if (projectId.toLowerCase() === 'neoforge') {
|
||||
projectId = 'NeoForge'
|
||||
}
|
||||
await useServersFetch(`servers/${this.serverId}/reinstall?hard=${hardResetParam}`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
loader: projectId,
|
||||
loader_version: loaderVersionId,
|
||||
game_version: versionId,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
await useServersFetch(`servers/${this.serverId}/reinstall?hard=${hardResetParam}`, {
|
||||
method: 'POST',
|
||||
body: { project_id: projectId, version_id: versionId },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
reinstallFromMrpack(
|
||||
mrpack: File,
|
||||
hardReset: boolean = false,
|
||||
): {
|
||||
promise: Promise<void>;
|
||||
onProgress: (cb: (p: { loaded: number; total: number; progress: number }) => void) => void;
|
||||
} {
|
||||
const hardResetParam = hardReset ? "true" : "false";
|
||||
reinstallFromMrpack(
|
||||
mrpack: File,
|
||||
hardReset: boolean = false,
|
||||
): {
|
||||
promise: Promise<void>
|
||||
onProgress: (cb: (p: { loaded: number; total: number; progress: number }) => void) => void
|
||||
} {
|
||||
const hardResetParam = hardReset ? 'true' : 'false'
|
||||
|
||||
const progressSubject = new EventTarget();
|
||||
const progressSubject = new EventTarget()
|
||||
|
||||
const uploadPromise = (async () => {
|
||||
try {
|
||||
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/reinstallFromMrpack`);
|
||||
const uploadPromise = (async () => {
|
||||
try {
|
||||
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/reinstallFromMrpack`)
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
|
||||
xhr.upload.addEventListener("progress", (e) => {
|
||||
if (e.lengthComputable) {
|
||||
progressSubject.dispatchEvent(
|
||||
new CustomEvent("progress", {
|
||||
detail: {
|
||||
loaded: e.loaded,
|
||||
total: e.total,
|
||||
progress: (e.loaded / e.total) * 100,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
progressSubject.dispatchEvent(
|
||||
new CustomEvent('progress', {
|
||||
detail: {
|
||||
loaded: e.loaded,
|
||||
total: e.total,
|
||||
progress: (e.loaded / e.total) * 100,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
xhr.onload = () =>
|
||||
xhr.status >= 200 && xhr.status < 300
|
||||
? resolve()
|
||||
: reject(new Error(`[pyroservers] XHR error status: ${xhr.status}`));
|
||||
xhr.onload = () =>
|
||||
xhr.status >= 200 && xhr.status < 300
|
||||
? resolve()
|
||||
: reject(new Error(`[pyroservers] XHR error status: ${xhr.status}`))
|
||||
|
||||
xhr.onerror = () => reject(new Error("[pyroservers] .mrpack upload failed"));
|
||||
xhr.onabort = () => reject(new Error("[pyroservers] .mrpack upload cancelled"));
|
||||
xhr.ontimeout = () => reject(new Error("[pyroservers] .mrpack upload timed out"));
|
||||
xhr.timeout = 30 * 60 * 1000;
|
||||
xhr.onerror = () => reject(new Error('[pyroservers] .mrpack upload failed'))
|
||||
xhr.onabort = () => reject(new Error('[pyroservers] .mrpack upload cancelled'))
|
||||
xhr.ontimeout = () => reject(new Error('[pyroservers] .mrpack upload timed out'))
|
||||
xhr.timeout = 30 * 60 * 1000
|
||||
|
||||
xhr.open("POST", `https://${auth.url}/reinstallMrpackMultiparted?hard=${hardResetParam}`);
|
||||
xhr.setRequestHeader("Authorization", `Bearer ${auth.token}`);
|
||||
xhr.open('POST', `https://${auth.url}/reinstallMrpackMultiparted?hard=${hardResetParam}`)
|
||||
xhr.setRequestHeader('Authorization', `Bearer ${auth.token}`)
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", mrpack);
|
||||
xhr.send(formData);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error reinstalling from mrpack:", err);
|
||||
throw err;
|
||||
}
|
||||
})();
|
||||
const formData = new FormData()
|
||||
formData.append('file', mrpack)
|
||||
xhr.send(formData)
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Error reinstalling from mrpack:', err)
|
||||
throw err
|
||||
}
|
||||
})()
|
||||
|
||||
return {
|
||||
promise: uploadPromise,
|
||||
onProgress: (cb: (p: { loaded: number; total: number; progress: number }) => void) =>
|
||||
progressSubject.addEventListener("progress", ((e: CustomEvent) =>
|
||||
cb(e.detail)) as EventListener),
|
||||
};
|
||||
}
|
||||
return {
|
||||
promise: uploadPromise,
|
||||
onProgress: (cb: (p: { loaded: number; total: number; progress: number }) => void) =>
|
||||
progressSubject.addEventListener('progress', ((e: CustomEvent) =>
|
||||
cb(e.detail)) as EventListener),
|
||||
}
|
||||
}
|
||||
|
||||
async suspend(status: boolean): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/suspend`, {
|
||||
method: "POST",
|
||||
body: { suspended: status },
|
||||
});
|
||||
}
|
||||
async suspend(status: boolean): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/suspend`, {
|
||||
method: 'POST',
|
||||
body: { suspended: status },
|
||||
})
|
||||
}
|
||||
|
||||
async endIntro(): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/flows/intro`, {
|
||||
method: "DELETE",
|
||||
version: 1,
|
||||
});
|
||||
await this.fetch(); // Refresh this module
|
||||
}
|
||||
async endIntro(): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/flows/intro`, {
|
||||
method: 'DELETE',
|
||||
version: 1,
|
||||
})
|
||||
await this.fetch() // Refresh this module
|
||||
}
|
||||
|
||||
async getMotd(): Promise<string | undefined> {
|
||||
try {
|
||||
const props = await this.server.fs.downloadFile("/server.properties", false, true);
|
||||
if (props) {
|
||||
const lines = props.split("\n");
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("motd=")) {
|
||||
return line.slice(5);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
async getMotd(): Promise<string | undefined> {
|
||||
try {
|
||||
const props = await this.server.fs.downloadFile('/server.properties', false, true)
|
||||
if (props) {
|
||||
const lines = props.split('\n')
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('motd=')) {
|
||||
return line.slice(5)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
async setMotd(motd: string): Promise<void> {
|
||||
try {
|
||||
const props = (await this.server.fetchConfigFile("ServerProperties")) as any;
|
||||
if (props) {
|
||||
props.motd = motd;
|
||||
const newProps = this.server.constructServerProperties(props);
|
||||
const octetStream = new Blob([newProps], { type: "application/octet-stream" });
|
||||
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`);
|
||||
async setMotd(motd: string): Promise<void> {
|
||||
try {
|
||||
const props = (await this.server.fetchConfigFile('ServerProperties')) as any
|
||||
if (props) {
|
||||
props.motd = motd
|
||||
const newProps = this.server.constructServerProperties(props)
|
||||
const octetStream = new Blob([newProps], { type: 'application/octet-stream' })
|
||||
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`)
|
||||
|
||||
await useServersFetch(`/update?path=/server.properties`, {
|
||||
method: "PUT",
|
||||
contentType: "application/octet-stream",
|
||||
body: octetStream,
|
||||
override: auth,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
console.error(
|
||||
"[Modrinth Servers] [General] Failed to set MOTD due to lack of server properties file.",
|
||||
);
|
||||
}
|
||||
}
|
||||
await useServersFetch(`/update?path=/server.properties`, {
|
||||
method: 'PUT',
|
||||
contentType: 'application/octet-stream',
|
||||
body: octetStream,
|
||||
override: auth,
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
console.error(
|
||||
'[Modrinth Servers] [General] Failed to set MOTD due to lack of server properties file.',
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export * from "./base.ts";
|
||||
export * from "./backups.ts";
|
||||
export * from "./content.ts";
|
||||
export * from "./fs.ts";
|
||||
export * from "./general.ts";
|
||||
export * from "./network.ts";
|
||||
export * from "./startup.ts";
|
||||
export * from "./ws.ts";
|
||||
export * from './backups.ts'
|
||||
export * from './base.ts'
|
||||
export * from './content.ts'
|
||||
export * from './fs.ts'
|
||||
export * from './general.ts'
|
||||
export * from './network.ts'
|
||||
export * from './startup.ts'
|
||||
export * from './ws.ts'
|
||||
|
||||
@@ -1,47 +1,48 @@
|
||||
import type { Allocation } from "@modrinth/utils";
|
||||
import { useServersFetch } from "../servers-fetch.ts";
|
||||
import { ServerModule } from "./base.ts";
|
||||
import type { Allocation } from '@modrinth/utils'
|
||||
|
||||
import { useServersFetch } from '../servers-fetch.ts'
|
||||
import { ServerModule } from './base.ts'
|
||||
|
||||
export class NetworkModule extends ServerModule {
|
||||
allocations: Allocation[] = [];
|
||||
allocations: Allocation[] = []
|
||||
|
||||
async fetch(): Promise<void> {
|
||||
this.allocations = await useServersFetch<Allocation[]>(
|
||||
`servers/${this.serverId}/allocations`,
|
||||
{},
|
||||
"network",
|
||||
);
|
||||
}
|
||||
async fetch(): Promise<void> {
|
||||
this.allocations = await useServersFetch<Allocation[]>(
|
||||
`servers/${this.serverId}/allocations`,
|
||||
{},
|
||||
'network',
|
||||
)
|
||||
}
|
||||
|
||||
async reserveAllocation(name: string): Promise<Allocation> {
|
||||
return await useServersFetch<Allocation>(`servers/${this.serverId}/allocations?name=${name}`, {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
async reserveAllocation(name: string): Promise<Allocation> {
|
||||
return await useServersFetch<Allocation>(`servers/${this.serverId}/allocations?name=${name}`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
async updateAllocation(port: number, name: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/allocations/${port}?name=${name}`, {
|
||||
method: "PUT",
|
||||
});
|
||||
}
|
||||
async updateAllocation(port: number, name: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/allocations/${port}?name=${name}`, {
|
||||
method: 'PUT',
|
||||
})
|
||||
}
|
||||
|
||||
async deleteAllocation(port: number): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/allocations/${port}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
async deleteAllocation(port: number): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/allocations/${port}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
async checkSubdomainAvailability(subdomain: string): Promise<boolean> {
|
||||
const result = (await useServersFetch(`subdomains/${subdomain}/isavailable`)) as {
|
||||
available: boolean;
|
||||
};
|
||||
return result.available;
|
||||
}
|
||||
async checkSubdomainAvailability(subdomain: string): Promise<boolean> {
|
||||
const result = (await useServersFetch(`subdomains/${subdomain}/isavailable`)) as {
|
||||
available: boolean
|
||||
}
|
||||
return result.available
|
||||
}
|
||||
|
||||
async changeSubdomain(subdomain: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/subdomain`, {
|
||||
method: "POST",
|
||||
body: { subdomain },
|
||||
});
|
||||
}
|
||||
async changeSubdomain(subdomain: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/subdomain`, {
|
||||
method: 'POST',
|
||||
body: { subdomain },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
import type { Startup, JDKVersion, JDKBuild } from "@modrinth/utils";
|
||||
import { useServersFetch } from "../servers-fetch.ts";
|
||||
import { ServerModule } from "./base.ts";
|
||||
import type { JDKBuild, JDKVersion, Startup } from '@modrinth/utils'
|
||||
|
||||
import { useServersFetch } from '../servers-fetch.ts'
|
||||
import { ServerModule } from './base.ts'
|
||||
|
||||
export class StartupModule extends ServerModule implements Startup {
|
||||
invocation!: string;
|
||||
original_invocation!: string;
|
||||
jdk_version!: JDKVersion;
|
||||
jdk_build!: JDKBuild;
|
||||
invocation!: string
|
||||
original_invocation!: string
|
||||
jdk_version!: JDKVersion
|
||||
jdk_build!: JDKBuild
|
||||
|
||||
async fetch(): Promise<void> {
|
||||
const data = await useServersFetch<Startup>(`servers/${this.serverId}/startup`, {}, "startup");
|
||||
Object.assign(this, data);
|
||||
}
|
||||
async fetch(): Promise<void> {
|
||||
const data = await useServersFetch<Startup>(`servers/${this.serverId}/startup`, {}, 'startup')
|
||||
Object.assign(this, data)
|
||||
}
|
||||
|
||||
async update(invocation: string, jdkVersion: JDKVersion, jdkBuild: JDKBuild): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/startup`, {
|
||||
method: "POST",
|
||||
body: {
|
||||
invocation: invocation || null,
|
||||
jdk_version: jdkVersion || null,
|
||||
jdk_build: jdkBuild || null,
|
||||
},
|
||||
});
|
||||
}
|
||||
async update(invocation: string, jdkVersion: JDKVersion, jdkBuild: JDKBuild): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/startup`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
invocation: invocation || null,
|
||||
jdk_version: jdkVersion || null,
|
||||
jdk_build: jdkBuild || null,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import type { JWTAuth } from "@modrinth/utils";
|
||||
import { useServersFetch } from "../servers-fetch.ts";
|
||||
import { ServerModule } from "./base.ts";
|
||||
import type { JWTAuth } from '@modrinth/utils'
|
||||
|
||||
import { useServersFetch } from '../servers-fetch.ts'
|
||||
import { ServerModule } from './base.ts'
|
||||
|
||||
export class WSModule extends ServerModule implements JWTAuth {
|
||||
url!: string;
|
||||
token!: string;
|
||||
url!: string
|
||||
token!: string
|
||||
|
||||
async fetch(): Promise<void> {
|
||||
const data = await useServersFetch<JWTAuth>(`servers/${this.serverId}/ws`, {}, "ws");
|
||||
Object.assign(this, data);
|
||||
}
|
||||
async fetch(): Promise<void> {
|
||||
const data = await useServersFetch<JWTAuth>(`servers/${this.serverId}/ws`, {}, 'ws')
|
||||
Object.assign(this, data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,222 +1,222 @@
|
||||
import { $fetch, FetchError } from "ofetch";
|
||||
import { ModrinthServerError, ModrinthServersFetchError } from "@modrinth/utils";
|
||||
import type { V1ErrorInfo } from "@modrinth/utils";
|
||||
import type { V1ErrorInfo } from '@modrinth/utils'
|
||||
import { ModrinthServerError, ModrinthServersFetchError } from '@modrinth/utils'
|
||||
import { $fetch, FetchError } from 'ofetch'
|
||||
|
||||
export interface ServersFetchOptions {
|
||||
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||||
contentType?: string;
|
||||
body?: Record<string, any>;
|
||||
version?: number;
|
||||
override?: {
|
||||
url?: string;
|
||||
token?: string;
|
||||
};
|
||||
retry?: number | boolean;
|
||||
bypassAuth?: boolean;
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
||||
contentType?: string
|
||||
body?: Record<string, any>
|
||||
version?: number
|
||||
override?: {
|
||||
url?: string
|
||||
token?: string
|
||||
}
|
||||
retry?: number | boolean
|
||||
bypassAuth?: boolean
|
||||
}
|
||||
|
||||
export async function useServersFetch<T>(
|
||||
path: string,
|
||||
options: ServersFetchOptions = {},
|
||||
module?: string,
|
||||
errorContext?: string,
|
||||
path: string,
|
||||
options: ServersFetchOptions = {},
|
||||
module?: string,
|
||||
errorContext?: string,
|
||||
): Promise<T> {
|
||||
const config = useRuntimeConfig();
|
||||
const auth = await useAuth();
|
||||
const authToken = auth.value?.token;
|
||||
const config = useRuntimeConfig()
|
||||
const auth = await useAuth()
|
||||
const authToken = auth.value?.token
|
||||
|
||||
if (!authToken && !options.bypassAuth) {
|
||||
const error = new ModrinthServersFetchError(
|
||||
"[Modrinth Servers] Cannot fetch without auth",
|
||||
10000,
|
||||
);
|
||||
throw new ModrinthServerError("Missing auth token", 401, error, module);
|
||||
}
|
||||
if (!authToken && !options.bypassAuth) {
|
||||
const error = new ModrinthServersFetchError(
|
||||
'[Modrinth Servers] Cannot fetch without auth',
|
||||
10000,
|
||||
)
|
||||
throw new ModrinthServerError('Missing auth token', 401, error, module)
|
||||
}
|
||||
|
||||
const {
|
||||
method = "GET",
|
||||
contentType = "application/json",
|
||||
body,
|
||||
version = 0,
|
||||
override,
|
||||
retry = method === "GET" ? 3 : 0,
|
||||
} = options;
|
||||
const {
|
||||
method = 'GET',
|
||||
contentType = 'application/json',
|
||||
body,
|
||||
version = 0,
|
||||
override,
|
||||
retry = method === 'GET' ? 3 : 0,
|
||||
} = options
|
||||
|
||||
const circuitBreakerKey = `${module || "default"}_${path}`;
|
||||
const failureCount = useState<number>(`fetch_failures_${circuitBreakerKey}`, () => 0);
|
||||
const lastFailureTime = useState<number>(`last_failure_${circuitBreakerKey}`, () => 0);
|
||||
const circuitBreakerKey = `${module || 'default'}_${path}`
|
||||
const failureCount = useState<number>(`fetch_failures_${circuitBreakerKey}`, () => 0)
|
||||
const lastFailureTime = useState<number>(`last_failure_${circuitBreakerKey}`, () => 0)
|
||||
|
||||
const now = Date.now();
|
||||
if (failureCount.value >= 3 && now - lastFailureTime.value < 30000) {
|
||||
const error = new ModrinthServersFetchError(
|
||||
"[Modrinth Servers] Circuit breaker open - too many recent failures",
|
||||
503,
|
||||
);
|
||||
throw new ModrinthServerError("Service temporarily unavailable", 503, error, module);
|
||||
}
|
||||
const now = Date.now()
|
||||
if (failureCount.value >= 3 && now - lastFailureTime.value < 30000) {
|
||||
const error = new ModrinthServersFetchError(
|
||||
'[Modrinth Servers] Circuit breaker open - too many recent failures',
|
||||
503,
|
||||
)
|
||||
throw new ModrinthServerError('Service temporarily unavailable', 503, error, module)
|
||||
}
|
||||
|
||||
if (now - lastFailureTime.value > 30000) {
|
||||
failureCount.value = 0;
|
||||
}
|
||||
if (now - lastFailureTime.value > 30000) {
|
||||
failureCount.value = 0
|
||||
}
|
||||
|
||||
const base = (import.meta.server ? config.pyroBaseUrl : config.public.pyroBaseUrl)?.replace(
|
||||
/\/$/,
|
||||
"",
|
||||
);
|
||||
const base = (import.meta.server ? config.pyroBaseUrl : config.public.pyroBaseUrl)?.replace(
|
||||
/\/$/,
|
||||
'',
|
||||
)
|
||||
|
||||
if (!base) {
|
||||
const error = new ModrinthServersFetchError(
|
||||
"[Modrinth Servers] Cannot fetch without base url. Make sure to set a PYRO_BASE_URL in environment variables",
|
||||
10001,
|
||||
);
|
||||
throw new ModrinthServerError("Configuration error: Missing PYRO_BASE_URL", 500, error, module);
|
||||
}
|
||||
if (!base) {
|
||||
const error = new ModrinthServersFetchError(
|
||||
'[Modrinth Servers] Cannot fetch without base url. Make sure to set a PYRO_BASE_URL in environment variables',
|
||||
10001,
|
||||
)
|
||||
throw new ModrinthServerError('Configuration error: Missing PYRO_BASE_URL', 500, error, module)
|
||||
}
|
||||
|
||||
const versionString = `v${version}`;
|
||||
let newOverrideUrl = override?.url;
|
||||
if (newOverrideUrl && newOverrideUrl.includes("v0") && version !== 0) {
|
||||
newOverrideUrl = newOverrideUrl.replace("v0", versionString);
|
||||
}
|
||||
const versionString = `v${version}`
|
||||
let newOverrideUrl = override?.url
|
||||
if (newOverrideUrl && newOverrideUrl.includes('v0') && version !== 0) {
|
||||
newOverrideUrl = newOverrideUrl.replace('v0', versionString)
|
||||
}
|
||||
|
||||
const fullUrl = newOverrideUrl
|
||||
? `https://${newOverrideUrl}/${path.replace(/^\//, "")}`
|
||||
: version === 0
|
||||
? `${base}/modrinth/v${version}/${path.replace(/^\//, "")}`
|
||||
: `${base}/v${version}/${path.replace(/^\//, "")}`;
|
||||
const fullUrl = newOverrideUrl
|
||||
? `https://${newOverrideUrl}/${path.replace(/^\//, '')}`
|
||||
: version === 0
|
||||
? `${base}/modrinth/v${version}/${path.replace(/^\//, '')}`
|
||||
: `${base}/v${version}/${path.replace(/^\//, '')}`
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
"User-Agent": "Modrinth/1.0 (https://modrinth.com)",
|
||||
"X-Archon-Request": "true",
|
||||
Vary: "Accept, Origin",
|
||||
};
|
||||
const headers: Record<string, string> = {
|
||||
'User-Agent': 'Modrinth/1.0 (https://modrinth.com)',
|
||||
'X-Archon-Request': 'true',
|
||||
Vary: 'Accept, Origin',
|
||||
}
|
||||
|
||||
if (!options.bypassAuth) {
|
||||
headers.Authorization = `Bearer ${override?.token ?? authToken}`;
|
||||
headers["Access-Control-Allow-Headers"] = "Authorization";
|
||||
}
|
||||
if (!options.bypassAuth) {
|
||||
headers.Authorization = `Bearer ${override?.token ?? authToken}`
|
||||
headers['Access-Control-Allow-Headers'] = 'Authorization'
|
||||
}
|
||||
|
||||
if (contentType !== "none") {
|
||||
headers["Content-Type"] = contentType;
|
||||
}
|
||||
if (contentType !== 'none') {
|
||||
headers['Content-Type'] = contentType
|
||||
}
|
||||
|
||||
if (import.meta.client && typeof window !== "undefined") {
|
||||
headers.Origin = window.location.origin;
|
||||
}
|
||||
if (import.meta.client && typeof window !== 'undefined') {
|
||||
headers.Origin = window.location.origin
|
||||
}
|
||||
|
||||
let attempts = 0;
|
||||
const maxAttempts = (typeof retry === "boolean" ? (retry ? 3 : 1) : retry) + 1;
|
||||
let lastError: Error | null = null;
|
||||
let attempts = 0
|
||||
const maxAttempts = (typeof retry === 'boolean' ? (retry ? 3 : 1) : retry) + 1
|
||||
let lastError: Error | null = null
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
const response = await $fetch<T>(fullUrl, {
|
||||
method,
|
||||
headers,
|
||||
body:
|
||||
body && contentType === "application/json" ? JSON.stringify(body) : (body ?? undefined),
|
||||
timeout: 10000,
|
||||
});
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
const response = await $fetch<T>(fullUrl, {
|
||||
method,
|
||||
headers,
|
||||
body:
|
||||
body && contentType === 'application/json' ? JSON.stringify(body) : (body ?? undefined),
|
||||
timeout: 10000,
|
||||
})
|
||||
|
||||
failureCount.value = 0;
|
||||
return response;
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
attempts++;
|
||||
failureCount.value = 0
|
||||
return response
|
||||
} catch (error) {
|
||||
lastError = error as Error
|
||||
attempts++
|
||||
|
||||
if (error instanceof FetchError) {
|
||||
const statusCode = error.response?.status;
|
||||
const statusText = error.response?.statusText || "Unknown error";
|
||||
if (error instanceof FetchError) {
|
||||
const statusCode = error.response?.status
|
||||
const statusText = error.response?.statusText || 'Unknown error'
|
||||
|
||||
if (statusCode && statusCode >= 500) {
|
||||
failureCount.value++;
|
||||
lastFailureTime.value = now;
|
||||
}
|
||||
if (statusCode && statusCode >= 500) {
|
||||
failureCount.value++
|
||||
lastFailureTime.value = now
|
||||
}
|
||||
|
||||
let v1Error: V1ErrorInfo | undefined;
|
||||
if (error.data?.error && error.data?.description) {
|
||||
v1Error = {
|
||||
context: errorContext,
|
||||
...error.data,
|
||||
};
|
||||
}
|
||||
let v1Error: V1ErrorInfo | undefined
|
||||
if (error.data?.error && error.data?.description) {
|
||||
v1Error = {
|
||||
context: errorContext,
|
||||
...error.data,
|
||||
}
|
||||
}
|
||||
|
||||
const errorMessages: { [key: number]: string } = {
|
||||
400: "Bad Request",
|
||||
401: "Unauthorized",
|
||||
403: "Forbidden",
|
||||
404: "Not Found",
|
||||
405: "Method Not Allowed",
|
||||
408: "Request Timeout",
|
||||
429: "You're making requests too quickly. Please wait a moment and try again.",
|
||||
500: "Internal Server Error",
|
||||
502: "Bad Gateway",
|
||||
503: "Service Unavailable",
|
||||
504: "Gateway Timeout",
|
||||
};
|
||||
const errorMessages: { [key: number]: string } = {
|
||||
400: 'Bad Request',
|
||||
401: 'Unauthorized',
|
||||
403: 'Forbidden',
|
||||
404: 'Not Found',
|
||||
405: 'Method Not Allowed',
|
||||
408: 'Request Timeout',
|
||||
429: "You're making requests too quickly. Please wait a moment and try again.",
|
||||
500: 'Internal Server Error',
|
||||
502: 'Bad Gateway',
|
||||
503: 'Service Unavailable',
|
||||
504: 'Gateway Timeout',
|
||||
}
|
||||
|
||||
const message =
|
||||
statusCode && statusCode in errorMessages
|
||||
? errorMessages[statusCode]
|
||||
: `HTTP Error: ${statusCode || "unknown"} ${statusText}`;
|
||||
const message =
|
||||
statusCode && statusCode in errorMessages
|
||||
? errorMessages[statusCode]
|
||||
: `HTTP Error: ${statusCode || 'unknown'} ${statusText}`
|
||||
|
||||
const isRetryable = statusCode ? [408, 429].includes(statusCode) : false;
|
||||
const is5xxRetryable =
|
||||
statusCode && statusCode >= 500 && statusCode < 600 && method === "GET" && attempts === 1;
|
||||
const isRetryable = statusCode ? [408, 429].includes(statusCode) : false
|
||||
const is5xxRetryable =
|
||||
statusCode && statusCode >= 500 && statusCode < 600 && method === 'GET' && attempts === 1
|
||||
|
||||
if (!(isRetryable || is5xxRetryable) || attempts >= maxAttempts) {
|
||||
console.error("Fetch error:", error);
|
||||
if (!(isRetryable || is5xxRetryable) || attempts >= maxAttempts) {
|
||||
console.error('Fetch error:', error)
|
||||
|
||||
const fetchError = new ModrinthServersFetchError(
|
||||
`[Modrinth Servers] ${error.message}`,
|
||||
statusCode,
|
||||
error,
|
||||
);
|
||||
throw new ModrinthServerError(
|
||||
`[Modrinth Servers] ${message}`,
|
||||
statusCode,
|
||||
fetchError,
|
||||
module,
|
||||
v1Error,
|
||||
);
|
||||
}
|
||||
const fetchError = new ModrinthServersFetchError(
|
||||
`[Modrinth Servers] ${error.message}`,
|
||||
statusCode,
|
||||
error,
|
||||
)
|
||||
throw new ModrinthServerError(
|
||||
`[Modrinth Servers] ${message}`,
|
||||
statusCode,
|
||||
fetchError,
|
||||
module,
|
||||
v1Error,
|
||||
)
|
||||
}
|
||||
|
||||
const baseDelay = statusCode && statusCode >= 500 ? 5000 : 1000;
|
||||
const delay = Math.min(baseDelay * Math.pow(2, attempts - 1) + Math.random() * 1000, 15000);
|
||||
console.warn(`Retrying request in ${delay}ms (attempt ${attempts}/${maxAttempts - 1})`);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
continue;
|
||||
}
|
||||
const baseDelay = statusCode && statusCode >= 500 ? 5000 : 1000
|
||||
const delay = Math.min(baseDelay * Math.pow(2, attempts - 1) + Math.random() * 1000, 15000)
|
||||
console.warn(`Retrying request in ${delay}ms (attempt ${attempts}/${maxAttempts - 1})`)
|
||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||
continue
|
||||
}
|
||||
|
||||
console.error("Unexpected fetch error:", error);
|
||||
const fetchError = new ModrinthServersFetchError(
|
||||
"[Modrinth Servers] An unexpected error occurred during the fetch operation.",
|
||||
undefined,
|
||||
error as Error,
|
||||
);
|
||||
throw new ModrinthServerError(
|
||||
"Unexpected error during fetch operation",
|
||||
undefined,
|
||||
fetchError,
|
||||
module,
|
||||
);
|
||||
}
|
||||
}
|
||||
console.error('Unexpected fetch error:', error)
|
||||
const fetchError = new ModrinthServersFetchError(
|
||||
'[Modrinth Servers] An unexpected error occurred during the fetch operation.',
|
||||
undefined,
|
||||
error as Error,
|
||||
)
|
||||
throw new ModrinthServerError(
|
||||
'Unexpected error during fetch operation',
|
||||
undefined,
|
||||
fetchError,
|
||||
module,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
console.error("All retry attempts failed:", lastError);
|
||||
if (lastError instanceof FetchError) {
|
||||
const statusCode = lastError.response?.status;
|
||||
const pyroError = new ModrinthServersFetchError(
|
||||
"Maximum retry attempts reached",
|
||||
statusCode,
|
||||
lastError,
|
||||
);
|
||||
throw new ModrinthServerError("Maximum retry attempts reached", statusCode, pyroError, module);
|
||||
}
|
||||
console.error('All retry attempts failed:', lastError)
|
||||
if (lastError instanceof FetchError) {
|
||||
const statusCode = lastError.response?.status
|
||||
const pyroError = new ModrinthServersFetchError(
|
||||
'Maximum retry attempts reached',
|
||||
statusCode,
|
||||
lastError,
|
||||
)
|
||||
throw new ModrinthServerError('Maximum retry attempts reached', statusCode, pyroError, module)
|
||||
}
|
||||
|
||||
const fetchError = new ModrinthServersFetchError(
|
||||
"Maximum retry attempts reached",
|
||||
undefined,
|
||||
lastError || undefined,
|
||||
);
|
||||
throw new ModrinthServerError("Maximum retry attempts reached", undefined, fetchError, module);
|
||||
const fetchError = new ModrinthServersFetchError(
|
||||
'Maximum retry attempts reached',
|
||||
undefined,
|
||||
lastError || undefined,
|
||||
)
|
||||
throw new ModrinthServerError('Maximum retry attempts reached', undefined, fetchError, module)
|
||||
}
|
||||
|
||||
@@ -1,64 +1,64 @@
|
||||
import tags from "~/generated/state.json";
|
||||
import tags from '~/generated/state.json'
|
||||
|
||||
export const useTags = () =>
|
||||
useState("tags", () => ({
|
||||
categories: tags.categories,
|
||||
loaders: tags.loaders,
|
||||
gameVersions: tags.gameVersions,
|
||||
donationPlatforms: tags.donationPlatforms,
|
||||
reportTypes: tags.reportTypes,
|
||||
projectTypes: [
|
||||
{
|
||||
actual: "mod",
|
||||
id: "mod",
|
||||
display: "mod",
|
||||
},
|
||||
{
|
||||
actual: "mod",
|
||||
id: "plugin",
|
||||
display: "plugin",
|
||||
},
|
||||
{
|
||||
actual: "mod",
|
||||
id: "datapack",
|
||||
display: "data pack",
|
||||
},
|
||||
{
|
||||
actual: "shader",
|
||||
id: "shader",
|
||||
display: "shader",
|
||||
},
|
||||
{
|
||||
actual: "resourcepack",
|
||||
id: "resourcepack",
|
||||
display: "resource pack",
|
||||
},
|
||||
{
|
||||
actual: "modpack",
|
||||
id: "modpack",
|
||||
display: "modpack",
|
||||
},
|
||||
],
|
||||
loaderData: {
|
||||
pluginLoaders: ["bukkit", "spigot", "paper", "purpur", "sponge", "folia"],
|
||||
pluginPlatformLoaders: ["bungeecord", "waterfall", "velocity"],
|
||||
allPluginLoaders: [
|
||||
"bukkit",
|
||||
"spigot",
|
||||
"paper",
|
||||
"purpur",
|
||||
"sponge",
|
||||
"bungeecord",
|
||||
"waterfall",
|
||||
"velocity",
|
||||
"folia",
|
||||
],
|
||||
dataPackLoaders: ["datapack"],
|
||||
modLoaders: ["forge", "fabric", "quilt", "liteloader", "modloader", "rift", "neoforge"],
|
||||
hiddenModLoaders: ["liteloader", "modloader", "rift"],
|
||||
},
|
||||
projectViewModes: ["list", "grid", "gallery"],
|
||||
approvedStatuses: ["approved", "archived", "unlisted", "private"],
|
||||
rejectedStatuses: ["rejected", "withheld"],
|
||||
staffRoles: ["moderator", "admin"],
|
||||
}));
|
||||
useState('tags', () => ({
|
||||
categories: tags.categories,
|
||||
loaders: tags.loaders,
|
||||
gameVersions: tags.gameVersions,
|
||||
donationPlatforms: tags.donationPlatforms,
|
||||
reportTypes: tags.reportTypes,
|
||||
projectTypes: [
|
||||
{
|
||||
actual: 'mod',
|
||||
id: 'mod',
|
||||
display: 'mod',
|
||||
},
|
||||
{
|
||||
actual: 'mod',
|
||||
id: 'plugin',
|
||||
display: 'plugin',
|
||||
},
|
||||
{
|
||||
actual: 'mod',
|
||||
id: 'datapack',
|
||||
display: 'data pack',
|
||||
},
|
||||
{
|
||||
actual: 'shader',
|
||||
id: 'shader',
|
||||
display: 'shader',
|
||||
},
|
||||
{
|
||||
actual: 'resourcepack',
|
||||
id: 'resourcepack',
|
||||
display: 'resource pack',
|
||||
},
|
||||
{
|
||||
actual: 'modpack',
|
||||
id: 'modpack',
|
||||
display: 'modpack',
|
||||
},
|
||||
],
|
||||
loaderData: {
|
||||
pluginLoaders: ['bukkit', 'spigot', 'paper', 'purpur', 'sponge', 'folia'],
|
||||
pluginPlatformLoaders: ['bungeecord', 'waterfall', 'velocity'],
|
||||
allPluginLoaders: [
|
||||
'bukkit',
|
||||
'spigot',
|
||||
'paper',
|
||||
'purpur',
|
||||
'sponge',
|
||||
'bungeecord',
|
||||
'waterfall',
|
||||
'velocity',
|
||||
'folia',
|
||||
],
|
||||
dataPackLoaders: ['datapack'],
|
||||
modLoaders: ['forge', 'fabric', 'quilt', 'liteloader', 'modloader', 'rift', 'neoforge'],
|
||||
hiddenModLoaders: ['liteloader', 'modloader', 'rift'],
|
||||
},
|
||||
projectViewModes: ['list', 'grid', 'gallery'],
|
||||
approvedStatuses: ['approved', 'archived', 'unlisted', 'private'],
|
||||
rejectedStatuses: ['rejected', 'withheld'],
|
||||
staffRoles: ['moderator', 'admin'],
|
||||
}))
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
import { injectNotificationManager } from "@modrinth/ui";
|
||||
import { injectNotificationManager } from '@modrinth/ui'
|
||||
|
||||
type AsyncFunction<TArgs extends any[], TResult> = (...args: TArgs) => Promise<TResult>;
|
||||
type ErrorFunction = (err: any) => void | Promise<void>;
|
||||
type VoidFunction = () => void | Promise<void>;
|
||||
type AsyncFunction<TArgs extends any[], TResult> = (...args: TArgs) => Promise<TResult>
|
||||
type ErrorFunction = (err: any) => void | Promise<void>
|
||||
type VoidFunction = () => void | Promise<void>
|
||||
|
||||
type useClientTry = <TArgs extends any[], TResult>(
|
||||
fn: AsyncFunction<TArgs, TResult>,
|
||||
onFail?: ErrorFunction,
|
||||
onFinish?: VoidFunction,
|
||||
) => (...args: TArgs) => Promise<TResult | undefined>;
|
||||
fn: AsyncFunction<TArgs, TResult>,
|
||||
onFail?: ErrorFunction,
|
||||
onFinish?: VoidFunction,
|
||||
) => (...args: TArgs) => Promise<TResult | undefined>
|
||||
|
||||
const defaultOnError: ErrorFunction = (error) => {
|
||||
const { addNotification } = injectNotificationManager();
|
||||
addNotification({
|
||||
title: "An error occurred",
|
||||
text: error?.data?.description || error.message || error || "Unknown error",
|
||||
type: "error",
|
||||
});
|
||||
};
|
||||
const { addNotification } = injectNotificationManager()
|
||||
addNotification({
|
||||
title: 'An error occurred',
|
||||
text: error?.data?.description || error.message || error || 'Unknown error',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
export const useClientTry: useClientTry =
|
||||
(fn, onFail = defaultOnError, onFinish) =>
|
||||
async (...args) => {
|
||||
startLoading();
|
||||
try {
|
||||
return await fn(...args);
|
||||
} catch (err) {
|
||||
if (onFail) {
|
||||
await onFail(err);
|
||||
} else {
|
||||
console.error("[CLIENT TRY ERROR]", err);
|
||||
}
|
||||
} finally {
|
||||
if (onFinish) await onFinish();
|
||||
stopLoading();
|
||||
}
|
||||
};
|
||||
(fn, onFail = defaultOnError, onFinish) =>
|
||||
async (...args) => {
|
||||
startLoading()
|
||||
try {
|
||||
return await fn(...args)
|
||||
} catch (err) {
|
||||
if (onFail) {
|
||||
await onFail(err)
|
||||
} else {
|
||||
console.error('[CLIENT TRY ERROR]', err)
|
||||
}
|
||||
} finally {
|
||||
if (onFinish) await onFinish()
|
||||
stopLoading()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,177 +1,179 @@
|
||||
export const useUser = async (force = false) => {
|
||||
const user = useState("user", () => {});
|
||||
const user = useState('user', () => {})
|
||||
|
||||
if (!user.value || force || (user.value && Date.now() - user.value.lastUpdated > 300000)) {
|
||||
user.value = await initUser();
|
||||
}
|
||||
if (!user.value || force || (user.value && Date.now() - user.value.lastUpdated > 300000)) {
|
||||
user.value = await initUser()
|
||||
}
|
||||
|
||||
return user;
|
||||
};
|
||||
return user
|
||||
}
|
||||
|
||||
export const initUser = async () => {
|
||||
const auth = (await useAuth()).value;
|
||||
const auth = (await useAuth()).value
|
||||
|
||||
const user = {
|
||||
collections: [],
|
||||
follows: [],
|
||||
subscriptions: [],
|
||||
lastUpdated: 0,
|
||||
};
|
||||
const user = {
|
||||
collections: [],
|
||||
follows: [],
|
||||
subscriptions: [],
|
||||
lastUpdated: 0,
|
||||
}
|
||||
|
||||
if (auth.user && auth.user.id) {
|
||||
try {
|
||||
const headers = {
|
||||
Authorization: auth.token,
|
||||
};
|
||||
if (auth.user && auth.user.id) {
|
||||
try {
|
||||
const headers = {
|
||||
Authorization: auth.token,
|
||||
}
|
||||
|
||||
const [follows, collections, subscriptions] = await Promise.all([
|
||||
useBaseFetch(`user/${auth.user.id}/follows`, { headers }, true),
|
||||
useBaseFetch(`user/${auth.user.id}/collections`, { apiVersion: 3, headers }, true),
|
||||
useBaseFetch(`billing/subscriptions`, { internal: true, headers }, true),
|
||||
]);
|
||||
const [follows, collections, subscriptions] = await Promise.all([
|
||||
useBaseFetch(`user/${auth.user.id}/follows`, { headers }, true),
|
||||
useBaseFetch(`user/${auth.user.id}/collections`, { apiVersion: 3, headers }, true),
|
||||
useBaseFetch(`billing/subscriptions`, { internal: true, headers }, true),
|
||||
])
|
||||
|
||||
user.collections = collections;
|
||||
user.follows = follows;
|
||||
user.subscriptions = subscriptions;
|
||||
user.lastUpdated = Date.now();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
user.collections = collections
|
||||
user.follows = follows
|
||||
user.subscriptions = subscriptions
|
||||
user.lastUpdated = Date.now()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
return user;
|
||||
};
|
||||
return user
|
||||
}
|
||||
|
||||
export const initUserCollections = async () => {
|
||||
const auth = (await useAuth()).value;
|
||||
const user = (await useUser()).value;
|
||||
const auth = (await useAuth()).value
|
||||
const user = (await useUser()).value
|
||||
|
||||
if (auth.user && auth.user.id) {
|
||||
try {
|
||||
user.collections = await useBaseFetch(`user/${auth.user.id}/collections`, { apiVersion: 3 });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (auth.user && auth.user.id) {
|
||||
try {
|
||||
user.collections = await useBaseFetch(`user/${auth.user.id}/collections`, {
|
||||
apiVersion: 3,
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const initUserFollows = async () => {
|
||||
const auth = (await useAuth()).value;
|
||||
const user = (await useUser()).value;
|
||||
const auth = (await useAuth()).value
|
||||
const user = (await useUser()).value
|
||||
|
||||
if (auth.user && auth.user.id) {
|
||||
try {
|
||||
user.follows = await useBaseFetch(`user/${auth.user.id}/follows`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (auth.user && auth.user.id) {
|
||||
try {
|
||||
user.follows = await useBaseFetch(`user/${auth.user.id}/follows`)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const initUserProjects = async () => {
|
||||
const auth = (await useAuth()).value;
|
||||
const user = (await useUser()).value;
|
||||
const auth = (await useAuth()).value
|
||||
const user = (await useUser()).value
|
||||
|
||||
if (auth.user && auth.user.id) {
|
||||
try {
|
||||
user.projects = await useBaseFetch(`user/${auth.user.id}/projects`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (auth.user && auth.user.id) {
|
||||
try {
|
||||
user.projects = await useBaseFetch(`user/${auth.user.id}/projects`)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const userCollectProject = async (collection, projectId) => {
|
||||
const user = (await useUser()).value;
|
||||
await initUserCollections();
|
||||
const user = (await useUser()).value
|
||||
await initUserCollections()
|
||||
|
||||
const collectionId = collection.id;
|
||||
const collectionId = collection.id
|
||||
|
||||
const latestCollection = user.collections.find((x) => x.id === collectionId);
|
||||
if (!latestCollection) {
|
||||
throw new Error("This collection was not found. Has it been deleted?");
|
||||
}
|
||||
const latestCollection = user.collections.find((x) => x.id === collectionId)
|
||||
if (!latestCollection) {
|
||||
throw new Error('This collection was not found. Has it been deleted?')
|
||||
}
|
||||
|
||||
const add = !latestCollection.projects.includes(projectId);
|
||||
const projects = add
|
||||
? [...latestCollection.projects, projectId]
|
||||
: [...latestCollection.projects].filter((x) => x !== projectId);
|
||||
const add = !latestCollection.projects.includes(projectId)
|
||||
const projects = add
|
||||
? [...latestCollection.projects, projectId]
|
||||
: [...latestCollection.projects].filter((x) => x !== projectId)
|
||||
|
||||
const idx = user.collections.findIndex((x) => x.id === latestCollection.id);
|
||||
if (idx >= 0) {
|
||||
user.collections[idx].projects = projects;
|
||||
}
|
||||
const idx = user.collections.findIndex((x) => x.id === latestCollection.id)
|
||||
if (idx >= 0) {
|
||||
user.collections[idx].projects = projects
|
||||
}
|
||||
|
||||
await useBaseFetch(`collection/${collection.id}`, {
|
||||
method: "PATCH",
|
||||
body: {
|
||||
new_projects: projects,
|
||||
},
|
||||
apiVersion: 3,
|
||||
});
|
||||
};
|
||||
await useBaseFetch(`collection/${collection.id}`, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
new_projects: projects,
|
||||
},
|
||||
apiVersion: 3,
|
||||
})
|
||||
}
|
||||
|
||||
export const userFollowProject = async (project) => {
|
||||
const user = (await useUser()).value;
|
||||
const user = (await useUser()).value
|
||||
|
||||
if (user.follows.find((x) => x.id === project.id)) {
|
||||
user.follows = user.follows.filter((x) => x.id !== project.id);
|
||||
project.followers--;
|
||||
if (user.follows.find((x) => x.id === project.id)) {
|
||||
user.follows = user.follows.filter((x) => x.id !== project.id)
|
||||
project.followers--
|
||||
|
||||
setTimeout(() => {
|
||||
useBaseFetch(`project/${project.id}/follow`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
});
|
||||
} else {
|
||||
user.follows = user.follows.concat(project);
|
||||
project.followers++;
|
||||
setTimeout(() => {
|
||||
useBaseFetch(`project/${project.id}/follow`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
})
|
||||
} else {
|
||||
user.follows = user.follows.concat(project)
|
||||
project.followers++
|
||||
|
||||
setTimeout(() => {
|
||||
useBaseFetch(`project/${project.id}/follow`, {
|
||||
method: "POST",
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
setTimeout(() => {
|
||||
useBaseFetch(`project/${project.id}/follow`, {
|
||||
method: 'POST',
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
export const resendVerifyEmail = async () => {
|
||||
// const { injectNotificationManager } = await import("@modrinth/ui");
|
||||
// const { addNotification } = injectNotificationManager();
|
||||
// const { injectNotificationManager } = await import("@modrinth/ui");
|
||||
// const { addNotification } = injectNotificationManager();
|
||||
|
||||
startLoading();
|
||||
try {
|
||||
await useBaseFetch("auth/email/resend_verify", {
|
||||
method: "POST",
|
||||
});
|
||||
startLoading()
|
||||
try {
|
||||
await useBaseFetch('auth/email/resend_verify', {
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
const auth = await useAuth();
|
||||
addNotification({
|
||||
title: "Email sent",
|
||||
text: `An email with a link to verify your account has been sent to ${auth.value.user.email}.`,
|
||||
type: "success",
|
||||
});
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: "An error occurred",
|
||||
text: err.data.description,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
stopLoading();
|
||||
};
|
||||
const auth = await useAuth()
|
||||
addNotification({
|
||||
title: 'Email sent',
|
||||
text: `An email with a link to verify your account has been sent to ${auth.value.user.email}.`,
|
||||
type: 'success',
|
||||
})
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: 'An error occurred',
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
export const logout = async () => {
|
||||
startLoading();
|
||||
const auth = await useAuth();
|
||||
try {
|
||||
await useBaseFetch(`session/${auth.value.token}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
startLoading()
|
||||
const auth = await useAuth()
|
||||
try {
|
||||
await useBaseFetch(`session/${auth.value.token}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
await useAuth("none");
|
||||
useCookie("auth-token").value = null;
|
||||
stopLoading();
|
||||
};
|
||||
await useAuth('none')
|
||||
useCookie('auth-token').value = null
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
export const useNotificationRightwards = () => {
|
||||
const isVisible = useState("moderation-checklist-notifications", () => false);
|
||||
const isVisible = useState('moderation-checklist-notifications', () => false)
|
||||
|
||||
const setVisible = (visible: boolean) => {
|
||||
isVisible.value = visible;
|
||||
};
|
||||
const setVisible = (visible: boolean) => {
|
||||
isVisible.value = visible
|
||||
}
|
||||
|
||||
return {
|
||||
isVisible: readonly(isVisible),
|
||||
setVisible,
|
||||
};
|
||||
};
|
||||
return {
|
||||
isVisible: readonly(isVisible),
|
||||
setVisible,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
* @returns A computed reference that changes when component becomes mounted or unmounted.
|
||||
*/
|
||||
export function useMountedValue<T>(getter: (isMounted: boolean) => T) {
|
||||
const mounted = ref(getCurrentInstance()?.isMounted ?? false);
|
||||
const mounted = ref(getCurrentInstance()?.isMounted ?? false)
|
||||
|
||||
onMounted(() => (mounted.value = true));
|
||||
onMounted(() => (mounted.value = true))
|
||||
|
||||
onUnmounted(() => (mounted.value = false));
|
||||
onUnmounted(() => (mounted.value = false))
|
||||
|
||||
return computed(() => getter(mounted.value));
|
||||
return computed(() => getter(mounted.value))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user