feat(frontend): lookup server by subdomain (#7210)

feat(frontend): lookup server by subdomain admin
This commit is contained in:
François-Xavier Talbot
2026-08-19 17:49:27 -04:00
committed by GitHub
parent 10b1d7002a
commit 79fcf41316
6 changed files with 154 additions and 0 deletions
+9
View File
@@ -427,6 +427,14 @@
tone: 'brand',
shown: isAdmin(auth.user),
},
{
id: 'servers-lookup',
label: 'Server lookup',
type: 'link',
to: '/admin/servers/lookup',
tone: 'brand',
shown: isAdmin(auth.user),
},
{
id: 'servers-notices',
label: formatMessage(messages.manageServerNotices),
@@ -480,6 +488,7 @@
<template #file-lookup>
<FileIcon aria-hidden="true" /> {{ formatMessage(messages.fileLookup) }}
</template>
<template #servers-lookup> <ServerIcon aria-hidden="true" /> Server lookup </template>
<template #servers-notices>
<IssuesIcon aria-hidden="true" /> {{ formatMessage(messages.manageServerNotices) }}
</template>
@@ -0,0 +1,113 @@
<template>
<div class="normal-page no-sidebar">
<h1>Server lookup</h1>
<div class="normal-page__content">
<form class="card flex flex-col gap-3" @submit.prevent="lookupServer">
<div class="flex flex-col gap-2">
<label for="server-subdomain">
<span class="text-lg font-semibold text-contrast">
Server subdomain
<span class="text-brand-red">*</span>
</span>
</label>
<StyledInput
id="server-subdomain"
v-model="subdomainInput"
:icon="ServerIcon"
:error="submitted && !!validationError"
placeholder="example or example.modrinth.gg"
autocomplete="off"
autocapitalize="none"
:spellcheck="false"
/>
<span v-if="submitted && validationError" class="text-sm font-medium text-red">
{{ validationError }}
</span>
</div>
<div>
<Button type="colored" color="brand" native-type="submit" :loading="isLoading">
<SearchIcon aria-hidden="true" />
Look up server
</Button>
</div>
<Admonition v-if="lookupError" type="critical" header="Lookup failed">
{{ lookupError }}
</Admonition>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import { ModrinthApiError } from '@modrinth/api-client'
import { SearchIcon, ServerIcon } from '@modrinth/assets'
import { Admonition, Button, injectModrinthClient, StyledInput } from '@modrinth/ui'
const client = injectModrinthClient()
const subdomainInput = ref('')
const submitted = ref(false)
const isLoading = ref(false)
const lookupError = ref('')
const normalizedSubdomain = computed(() =>
subdomainInput.value
.trim()
.replace(/\.modrinth\.gg$/i, '')
.toLowerCase(),
)
const validationError = computed(() => {
if (!normalizedSubdomain.value) {
return 'Enter a server subdomain.'
}
if (normalizedSubdomain.value.length > 63) {
return 'Subdomain must be 63 characters or fewer.'
}
if (!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(normalizedSubdomain.value)) {
return 'Use only letters, numbers, and hyphens, without a leading or trailing hyphen.'
}
return undefined
})
watch(subdomainInput, (value) => {
const withoutDomain = value.replace(/\.modrinth\.gg$/i, '')
if (withoutDomain !== value) {
subdomainInput.value = withoutDomain
}
lookupError.value = ''
})
async function lookupServer() {
if (isLoading.value) {
return
}
submitted.value = true
lookupError.value = ''
if (validationError.value) {
return
}
subdomainInput.value = normalizedSubdomain.value
isLoading.value = true
try {
const server = await client.archon.servers_internal.getBySubdomain(normalizedSubdomain.value)
await navigateTo(`/hosting/manage/${server.id}`)
} catch (error) {
if (error instanceof ModrinthApiError && error.statusCode === 404) {
lookupError.value = `No server was found for ${normalizedSubdomain.value}.modrinth.gg.`
} else {
lookupError.value = 'The server could not be looked up. Try again.'
}
} finally {
isLoading.value = false
}
}
</script>
@@ -3,6 +3,7 @@ export * from './backups/v1'
export * from './backups-queue/v1'
export * from './content/v1'
export * from './properties/v1'
export * from './servers/internal'
export * from './servers/v0'
export * from './servers/v1'
export * from './types'
@@ -0,0 +1,23 @@
import { AbstractModule } from '../../../core/abstract-module'
import type { Archon } from '../types'
export class ArchonServersInternalModule extends AbstractModule {
public getModuleID(): string {
return 'archon_servers_internal'
}
/**
* Get the server assigned to a subdomain.
* GET /_internal/servers/by-subdomain/:subdomain
*/
public async getBySubdomain(subdomain: string): Promise<Archon.Servers.Internal.Lookup> {
return this.client.request<Archon.Servers.Internal.Lookup>(
`/servers/by-subdomain/${encodeURIComponent(subdomain)}`,
{
api: 'archon',
version: 'internal',
method: 'GET',
},
)
}
}
@@ -573,6 +573,12 @@ export namespace Archon {
}
export namespace Servers {
export namespace Internal {
export type Lookup = {
id: string
}
}
export namespace v0 {
export type ServerGetResponse = {
servers: Server[]
+2
View File
@@ -9,6 +9,7 @@ import { ArchonNoticesV0Module } from './archon/notices/v0'
import { ArchonOptionsV1Module } from './archon/options/v1'
import { ArchonPropertiesV1Module } from './archon/properties/v1'
import { ArchonServerUsersV1Module } from './archon/server-users/v1'
import { ArchonServersInternalModule } from './archon/servers/internal'
import { ArchonServersV0Module } from './archon/servers/v0'
import { ArchonServersV1Module } from './archon/servers/v1'
import { ArchonTransfersInternalModule } from './archon/transfers/internal'
@@ -87,6 +88,7 @@ export const MODULE_REGISTRY = {
archon_options_v1: ArchonOptionsV1Module,
archon_properties_v1: ArchonPropertiesV1Module,
archon_server_users_v1: ArchonServerUsersV1Module,
archon_servers_internal: ArchonServersInternalModule,
archon_servers_v0: ArchonServersV0Module,
archon_servers_v1: ArchonServersV1Module,
archon_transfers_internal: ArchonTransfersInternalModule,