mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 21:26:40 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d2e1a8be9 | ||
|
|
81f19eeb8d | ||
|
|
4b4282cfbf | ||
|
|
7b3471944d | ||
|
|
d2abeb434c | ||
|
|
0aecfa3140 |
@@ -38,6 +38,7 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
|
||||
showProjectPageQuickServerButton: false,
|
||||
newProjectGeneralSettings: false,
|
||||
newProjectEnvironmentSettings: true,
|
||||
archonSentryCapture: false,
|
||||
hideRussiaCensorshipBanner: false,
|
||||
disablePrettyProjectUrlRedirects: false,
|
||||
hidePreviewBanner: false,
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
} from '@modrinth/api-client'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import { useFeatureFlags } from '~/composables/featureFlags.ts'
|
||||
|
||||
async function getRateLimitKeyFromSecretsStore(): Promise<string | undefined> {
|
||||
try {
|
||||
const mod = 'cloudflare:workers'
|
||||
@@ -28,6 +30,7 @@ export function createModrinthClient(
|
||||
auth: Ref<{ token: string | undefined }>,
|
||||
config: { apiBaseUrl: string; archonBaseUrl: string; rateLimitKey?: string },
|
||||
): NuxtModrinthClient {
|
||||
const flags = useFeatureFlags()
|
||||
const optionalFeatures = [
|
||||
import.meta.dev ? (new VerboseLoggingFeature() as AbstractFeature) : undefined,
|
||||
].filter(Boolean) as AbstractFeature[]
|
||||
@@ -35,6 +38,7 @@ export function createModrinthClient(
|
||||
const clientConfig: NuxtClientConfig = {
|
||||
labrinthBaseUrl: config.apiBaseUrl,
|
||||
archonBaseUrl: config.archonBaseUrl,
|
||||
archonSentryCapture: () => flags.value.archonSentryCapture,
|
||||
rateLimitKey: config.rateLimitKey || getRateLimitKeyFromSecretsStore,
|
||||
features: [
|
||||
// for modrinth hosting
|
||||
|
||||
@@ -1313,6 +1313,9 @@
|
||||
"hosting.loader.failed-to-repair": {
|
||||
"message": "Failed to repair server"
|
||||
},
|
||||
"hosting.loader.failed-to-reset-to-onboarding": {
|
||||
"message": "Failed to reset server to onboarding"
|
||||
},
|
||||
"hosting.loader.failed-to-save-settings": {
|
||||
"message": "Failed to save installation settings"
|
||||
},
|
||||
@@ -1334,6 +1337,24 @@
|
||||
"hosting.loader.reset-server-description": {
|
||||
"message": "Removes all data on your server, including your worlds, mods, and configuration files. Backups will remain and can be restored."
|
||||
},
|
||||
"hosting.loader.reset-to-onboarding-button": {
|
||||
"message": "Reset to onboarding"
|
||||
},
|
||||
"hosting.loader.reset-to-onboarding-modal-description": {
|
||||
"message": "This will send the server back into onboarding so setup can be completed again. Are you sure you want to continue?"
|
||||
},
|
||||
"hosting.loader.reset-to-onboarding-modal-title": {
|
||||
"message": "Reset to onboarding"
|
||||
},
|
||||
"hosting.loader.reset-to-onboarding-success-description": {
|
||||
"message": "The server has been returned to the onboarding flow."
|
||||
},
|
||||
"hosting.loader.reset-to-onboarding-success-title": {
|
||||
"message": "Server reset to onboarding"
|
||||
},
|
||||
"hosting.loader.support-options-title": {
|
||||
"message": "Support options"
|
||||
},
|
||||
"hosting.plan.out-of-stock": {
|
||||
"message": "Out of stock"
|
||||
},
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
<h1>{{ formatMessage(commonMessages.errorLabel) }}</h1>
|
||||
</div>
|
||||
<p>
|
||||
<span>{{ error.data.error }}: </span>
|
||||
{{ error.data.description }}
|
||||
<span>{{ error.data?.error }}: </span>
|
||||
{{ error.data?.description }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-else class="oauth-items">
|
||||
<div v-else-if="app && createdBy && authorizationData" class="oauth-items">
|
||||
<div class="connected-items">
|
||||
<div class="profile-pics">
|
||||
<Avatar size="md" :src="app.icon_url" />
|
||||
@@ -164,24 +164,31 @@ const {
|
||||
data: authorizationData,
|
||||
isPending: pending,
|
||||
error,
|
||||
suspense: authSusp,
|
||||
} = useQuery({
|
||||
queryKey: computed(() => ['authorization', clientId, redirectUri, scope, state]),
|
||||
queryFn: getFlowIdAuthorization,
|
||||
enabled: computed(() => !!clientId && !!redirectUri && !!scope),
|
||||
})
|
||||
|
||||
const { data: app } = useQuery({
|
||||
const { data: app, suspense: appSusp } = useQuery({
|
||||
queryKey: computed(() => ['oauth/app', clientId]),
|
||||
queryFn: () => client.labrinth.oauth_internal.getApp(clientId),
|
||||
enabled: computed(() => !!clientId),
|
||||
})
|
||||
|
||||
const { data: createdBy } = useQuery({
|
||||
const { data: createdBy, suspense: userSusp } = useQuery({
|
||||
queryKey: computed(() => ['user', app.value?.created_by]),
|
||||
queryFn: () => client.labrinth.users_v2.get(app.value.created_by),
|
||||
enabled: computed(() => !!app.value?.created_by),
|
||||
})
|
||||
|
||||
onServerPrefetch(async () => {
|
||||
await authSusp()
|
||||
await appSusp()
|
||||
await userSusp()
|
||||
})
|
||||
|
||||
const scopeDefinitions = computed(() =>
|
||||
scopesToDefinitions(BigInt(authorizationData.value?.requested_scopes || 0)),
|
||||
)
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-6 rounded-2xl bg-surface-3 p-6">
|
||||
<ConfirmModal
|
||||
ref="resetToOnboardingModal"
|
||||
:title="formatMessage(messages.resetToOnboardingModalTitle)"
|
||||
:description="formatMessage(messages.resetToOnboardingModalDescription)"
|
||||
:proceed-label="formatMessage(messages.resetToOnboardingButton)"
|
||||
@proceed="confirmResetToOnboarding"
|
||||
/>
|
||||
|
||||
<InstallationSettingsLayout ref="installationSettingsLayout">
|
||||
<template #extra>
|
||||
<div class="flex flex-col gap-2.5">
|
||||
@@ -28,6 +36,24 @@
|
||||
/>
|
||||
</template>
|
||||
</InstallationSettingsLayout>
|
||||
|
||||
<div v-if="isSiteAdmin" class="flex flex-col gap-2.5">
|
||||
<span class="text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.supportOptionsTitle) }}
|
||||
</span>
|
||||
<div>
|
||||
<ButtonStyled color="red">
|
||||
<button
|
||||
class="!shadow-none"
|
||||
:disabled="!worldId || isResettingToOnboarding"
|
||||
@click="resetToOnboardingModal?.show()"
|
||||
>
|
||||
<RotateCounterClockwiseIcon class="size-5" />
|
||||
{{ formatMessage(messages.resetToOnboardingButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -37,6 +63,7 @@ import { RotateCounterClockwiseIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
ConfirmModal,
|
||||
defineMessages,
|
||||
formatLoaderLabel,
|
||||
injectModrinthClient,
|
||||
@@ -106,6 +133,35 @@ const messages = defineMessages({
|
||||
id: 'hosting.loader.failed-to-unlink',
|
||||
defaultMessage: 'Failed to unlink modpack',
|
||||
},
|
||||
supportOptionsTitle: {
|
||||
id: 'hosting.loader.support-options-title',
|
||||
defaultMessage: 'Support options',
|
||||
},
|
||||
resetToOnboardingButton: {
|
||||
id: 'hosting.loader.reset-to-onboarding-button',
|
||||
defaultMessage: 'Reset to onboarding',
|
||||
},
|
||||
resetToOnboardingModalTitle: {
|
||||
id: 'hosting.loader.reset-to-onboarding-modal-title',
|
||||
defaultMessage: 'Reset to onboarding',
|
||||
},
|
||||
resetToOnboardingModalDescription: {
|
||||
id: 'hosting.loader.reset-to-onboarding-modal-description',
|
||||
defaultMessage:
|
||||
'This will send the server back into onboarding so setup can be completed again. Are you sure you want to continue?',
|
||||
},
|
||||
resetToOnboardingSuccessTitle: {
|
||||
id: 'hosting.loader.reset-to-onboarding-success-title',
|
||||
defaultMessage: 'Server reset to onboarding',
|
||||
},
|
||||
resetToOnboardingSuccessDescription: {
|
||||
id: 'hosting.loader.reset-to-onboarding-success-description',
|
||||
defaultMessage: 'The server has been returned to the onboarding flow.',
|
||||
},
|
||||
failedToResetToOnboarding: {
|
||||
id: 'hosting.loader.failed-to-reset-to-onboarding',
|
||||
defaultMessage: 'Failed to reset server to onboarding',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -156,8 +212,13 @@ const modpackVersionsQuery = useQuery({
|
||||
enabled: computed(() => !!modpack.value?.spec.project_id),
|
||||
})
|
||||
|
||||
const auth = await useAuth()
|
||||
const isSiteAdmin = computed(() => auth.value?.user?.role === 'admin')
|
||||
|
||||
const editingPlatform = ref(server.value?.loader?.toLowerCase() ?? 'vanilla')
|
||||
const editingGameVersion = ref(server.value?.mc_version ?? '')
|
||||
const resetToOnboardingModal = ref<InstanceType<typeof ConfirmModal>>()
|
||||
const isResettingToOnboarding = ref(false)
|
||||
|
||||
const modLoaders = ['fabric', 'forge', 'quilt', 'neoforge']
|
||||
|
||||
@@ -337,11 +398,17 @@ provideInstallationSettings({
|
||||
const currentPlatform = server.value?.loader?.toLowerCase() ?? 'vanilla'
|
||||
const platformChanged = platform !== currentPlatform
|
||||
|
||||
let resolvedLoaderVersion = loaderVersionId
|
||||
if (!resolvedLoaderVersion && platform !== 'vanilla') {
|
||||
const versions = getLoaderVersionsForGameVersion(platform, gameVersion)
|
||||
resolvedLoaderVersion = versions[0]?.id ?? null
|
||||
}
|
||||
|
||||
debug('save: emitting reinstall before API call')
|
||||
emit(
|
||||
'reinstall',
|
||||
platformChanged
|
||||
? { loader: platform, lVersion: loaderVersionId, mVersion: gameVersion }
|
||||
? { loader: platform, lVersion: resolvedLoaderVersion, mVersion: gameVersion }
|
||||
: { mVersion: gameVersion },
|
||||
)
|
||||
try {
|
||||
@@ -349,7 +416,7 @@ provideInstallationSettings({
|
||||
const request: Archon.Content.v1.InstallWorldContent = {
|
||||
content_variant: 'bare',
|
||||
loader: toApiLoader(platform),
|
||||
version: loaderVersionId ?? '',
|
||||
version: resolvedLoaderVersion ?? '',
|
||||
game_version: gameVersion || undefined,
|
||||
soft_override: true,
|
||||
}
|
||||
@@ -590,4 +657,30 @@ function onBrowseModpacks() {
|
||||
query: { sid: serverId, from: 'reset-server', wid: worldId.value },
|
||||
})
|
||||
}
|
||||
|
||||
async function confirmResetToOnboarding() {
|
||||
if (!worldId.value) return
|
||||
|
||||
try {
|
||||
isResettingToOnboarding.value = true
|
||||
await client.archon.servers_v1.resetToOnboarding(serverId, worldId.value)
|
||||
server.value.flows = { intro: true }
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['servers', 'detail', serverId] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['servers', 'v1', 'detail', serverId] }),
|
||||
])
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: formatMessage(messages.resetToOnboardingSuccessTitle),
|
||||
text: formatMessage(messages.resetToOnboardingSuccessDescription),
|
||||
})
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
text: err instanceof Error ? err.message : formatMessage(messages.failedToResetToOnboarding),
|
||||
})
|
||||
} finally {
|
||||
isResettingToOnboarding.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -150,6 +150,7 @@ const JAVA_VERSIONS = [
|
||||
{ value: 11, label: 'Java 11' },
|
||||
{ value: 17, label: 'Java 17' },
|
||||
{ value: 21, label: 'Java 21' },
|
||||
{ value: 25, label: 'Java 25' },
|
||||
]
|
||||
|
||||
const JRE_VENDORS: { value: Archon.Content.v1.JreVendor; label: string }[] = [
|
||||
@@ -203,19 +204,50 @@ const hasUnsavedChanges = computed(
|
||||
// Java version filtering
|
||||
const showAllVersions = ref(false)
|
||||
|
||||
type MinecraftReleaseVersion = {
|
||||
major: number
|
||||
minor: number
|
||||
}
|
||||
|
||||
function parseMinecraftReleaseVersion(version: string): MinecraftReleaseVersion | null {
|
||||
const [majorPart, minorPart] = version.split('.')
|
||||
|
||||
if (!majorPart || !minorPart) return null
|
||||
|
||||
const major = Number(majorPart)
|
||||
const minor = Number(minorPart)
|
||||
|
||||
if (!Number.isInteger(major) || !Number.isInteger(minor)) return null
|
||||
|
||||
return { major, minor }
|
||||
}
|
||||
|
||||
function filterJavaVersions(compatibleVersions: number[]) {
|
||||
return JAVA_VERSIONS.filter((version) => compatibleVersions.includes(version.value))
|
||||
}
|
||||
|
||||
const displayedJavaVersions = computed(() => {
|
||||
if (showAllVersions.value) return JAVA_VERSIONS
|
||||
|
||||
const mcVersion = server.value?.mc_version ?? ''
|
||||
if (!mcVersion) return JAVA_VERSIONS
|
||||
|
||||
const [, minor] = mcVersion.split('.').map(Number)
|
||||
const releaseVersion = parseMinecraftReleaseVersion(mcVersion)
|
||||
if (!releaseVersion) return JAVA_VERSIONS
|
||||
|
||||
if (minor >= 20) return JAVA_VERSIONS.filter((v) => v.value === 21)
|
||||
if (minor >= 17) return JAVA_VERSIONS.filter((v) => [17, 21].includes(v.value))
|
||||
if (minor >= 12) return JAVA_VERSIONS
|
||||
if (minor >= 6) return JAVA_VERSIONS.filter((v) => [8, 11].includes(v.value))
|
||||
return JAVA_VERSIONS.filter((v) => v.value === 8)
|
||||
if (releaseVersion.major > 1) {
|
||||
if (releaseVersion.major >= 26) {
|
||||
return filterJavaVersions([25])
|
||||
}
|
||||
|
||||
return JAVA_VERSIONS
|
||||
}
|
||||
|
||||
if (releaseVersion.minor >= 20) return filterJavaVersions([21])
|
||||
if (releaseVersion.minor >= 17) return filterJavaVersions([17, 21])
|
||||
if (releaseVersion.minor >= 12) return filterJavaVersions([8, 11, 17, 21])
|
||||
if (releaseVersion.minor >= 6) return filterJavaVersions([8, 11])
|
||||
return filterJavaVersions([8])
|
||||
})
|
||||
|
||||
// Save mutation
|
||||
|
||||
@@ -126,6 +126,7 @@ export abstract class AbstractModrinthClient extends AbstractUploadClient {
|
||||
...options.headers,
|
||||
},
|
||||
}
|
||||
this.attachArchonSentryCaptureHeader(mergedOptions)
|
||||
|
||||
const headers = mergedOptions.headers
|
||||
if (headers && 'Content-Type' in headers && headers['Content-Type'] === '') {
|
||||
@@ -309,6 +310,21 @@ export abstract class AbstractModrinthClient extends AbstractUploadClient {
|
||||
return headers
|
||||
}
|
||||
|
||||
protected attachArchonSentryCaptureHeader(options: RequestOptions): void {
|
||||
if (options.api !== 'archon' || !options.headers || !this.shouldCaptureArchonRequests()) {
|
||||
return
|
||||
}
|
||||
|
||||
options.headers['modrinth-sentry-capture'] = '1'
|
||||
}
|
||||
|
||||
private shouldCaptureArchonRequests(): boolean {
|
||||
const archonSentryCapture = this.config.archonSentryCapture
|
||||
return typeof archonSentryCapture === 'function'
|
||||
? archonSentryCapture()
|
||||
: archonSentryCapture === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the actual HTTP request
|
||||
*
|
||||
|
||||
@@ -54,4 +54,16 @@ export class ArchonServersV1Module extends AbstractModule {
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset a world to onboarding
|
||||
* POST /v1/servers/:id/worlds/:wid/onboard
|
||||
*/
|
||||
public async resetToOnboarding(serverId: string, worldId: string): Promise<void> {
|
||||
await this.client.request(`/servers/${serverId}/worlds/${worldId}/onboard`, {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ export abstract class XHRUploadClient extends AbstractModrinthClient {
|
||||
...options.headers,
|
||||
},
|
||||
}
|
||||
this.attachArchonSentryCaptureHeader(mergedOptions)
|
||||
|
||||
const context = this.buildUploadContext(url, path, mergedOptions)
|
||||
|
||||
|
||||
@@ -55,6 +55,14 @@ export interface ClientConfig {
|
||||
*/
|
||||
headers?: Record<string, string>
|
||||
|
||||
/**
|
||||
* Whether to attach `modrinth-sentry-capture: 1` to Archon requests.
|
||||
* Can be a callback so apps can drive this from runtime feature flags.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
archonSentryCapture?: boolean | (() => boolean)
|
||||
|
||||
/**
|
||||
* Features to enable for this client
|
||||
* Features are applied in the order they appear in this array
|
||||
|
||||
@@ -10,6 +10,12 @@ export type VersionEntry = {
|
||||
}
|
||||
|
||||
const VERSIONS: VersionEntry[] = [
|
||||
{
|
||||
date: `2026-03-24T21:14:30-08:00`,
|
||||
product: 'hosting',
|
||||
body: `## Added
|
||||
- Added support for Java 25.`,
|
||||
},
|
||||
{
|
||||
date: `2026-03-17T19:30:00-08:00`,
|
||||
product: 'web',
|
||||
|
||||
@@ -212,8 +212,8 @@ const deleteHovered = ref(false)
|
||||
>
|
||||
<span ref="versionNumberRef" class="truncate">{{
|
||||
version.version_number.slice(0, Math.ceil(version.version_number.length / 2))
|
||||
}}</span>
|
||||
<span class="shrink-0">{{
|
||||
}}</span
|
||||
><span class="shrink-0">{{
|
||||
version.version_number.slice(Math.ceil(version.version_number.length / 2))
|
||||
}}</span>
|
||||
</AutoLink>
|
||||
@@ -223,8 +223,8 @@ const deleteHovered = ref(false)
|
||||
>
|
||||
<span ref="fileNameRef" class="truncate">{{
|
||||
version.file_name.slice(0, Math.ceil(version.file_name.length / 2))
|
||||
}}</span>
|
||||
<span class="shrink-0">{{
|
||||
}}</span
|
||||
><span class="shrink-0">{{
|
||||
version.file_name.slice(Math.ceil(version.file_name.length / 2))
|
||||
}}</span>
|
||||
</span>
|
||||
|
||||
@@ -115,6 +115,7 @@ const contentQuery = useQuery({
|
||||
queryFn: () =>
|
||||
client.archon.content_v1.getAddons(serverId, worldId.value!, { from_modpack: false }),
|
||||
enabled: computed(() => worldId.value !== null),
|
||||
staleTime: 0,
|
||||
})
|
||||
|
||||
const modpackProjectId = computed(() => contentQuery.data.value?.modpack?.spec.project_id ?? null)
|
||||
@@ -483,7 +484,7 @@ function addonToContentItem(addon: Archon.Content.v1.Addon): ContentItem {
|
||||
link: `/${addon.owner.type}/${addon.owner.id}`,
|
||||
}
|
||||
: undefined,
|
||||
id: addon.id,
|
||||
id: addon.id ?? addon.filename,
|
||||
enabled: !addon.disabled,
|
||||
file_name: addon.filename,
|
||||
project_type: addon.kind,
|
||||
|
||||
Reference in New Issue
Block a user