diff --git a/apps/frontend/src/pages/admin.vue b/apps/frontend/src/pages/admin.vue index 1244396e9bb..76ecb41b588 100644 --- a/apps/frontend/src/pages/admin.vue +++ b/apps/frontend/src/pages/admin.vue @@ -43,6 +43,12 @@ icon: TransferIcon, shown: admin, }, + { + link: '/admin/servers/locks', + label: 'Server locks', + icon: LockIcon, + shown: admin, + }, { type: 'heading', label: 'Management', @@ -85,6 +91,7 @@ import { ChartIcon, FileSearchCornerIcon, IssuesIcon, + LockIcon, MailIcon, ServerSearchIcon, TransferIcon, diff --git a/apps/frontend/src/pages/admin/servers/locks.vue b/apps/frontend/src/pages/admin/servers/locks.vue new file mode 100644 index 00000000000..9c4e923fb63 --- /dev/null +++ b/apps/frontend/src/pages/admin/servers/locks.vue @@ -0,0 +1,155 @@ + + + + Server locks + Loading locks... + + {{ error.message }} + + + No servers are locked. + + + + + + + + + + Open panel + + + + + + Unlock + + + + + Locked {{ formatRelativeTime(lock.created) }} + + + by + + + {{ userMap.get(lock.locked_by)?.username ?? lock.locked_by }} + + + + + Reason: + {{ lock.reason }} + + + + + + + + diff --git a/packages/api-client/src/modules/archon/servers/internal.ts b/packages/api-client/src/modules/archon/servers/internal.ts index 2be3033882b..44911c265a4 100644 --- a/packages/api-client/src/modules/archon/servers/internal.ts +++ b/packages/api-client/src/modules/archon/servers/internal.ts @@ -20,4 +20,59 @@ export class ArchonServersInternalModule extends AbstractModule { }, ) } + + /** + * List all locked servers. + * GET /_internal/admin/server-locks + */ + public async getLocks(): Promise { + return this.client.request('/admin/server-locks', { + api: 'archon', + version: 'internal', + method: 'GET', + }) + } + + /** + * Get the lock on a server. + * GET /_internal/admin/server/:server_id/lock + */ + public async getLock(serverId: string): Promise { + return this.client.request( + `/admin/server/${serverId}/lock`, + { + api: 'archon', + version: 'internal', + method: 'GET', + }, + ) + } + + /** + * Lock a server. + * PUT /_internal/admin/server/:server_id/lock + */ + public async lock( + serverId: string, + request: Archon.Servers.Internal.LockServerRequest, + ): Promise { + return this.client.request(`/admin/server/${serverId}/lock`, { + api: 'archon', + version: 'internal', + method: 'PUT', + body: request, + }) + } + + /** + * Unlock a server. + * DELETE /_internal/admin/server/:server_id/lock + */ + public async unlock(serverId: string): Promise { + return this.client.request(`/admin/server/${serverId}/lock`, { + api: 'archon', + version: 'internal', + method: 'DELETE', + }) + } } diff --git a/packages/api-client/src/modules/archon/types.ts b/packages/api-client/src/modules/archon/types.ts index e0a95cc5836..3270ceb8da1 100644 --- a/packages/api-client/src/modules/archon/types.ts +++ b/packages/api-client/src/modules/archon/types.ts @@ -577,6 +577,17 @@ export namespace Archon { export type Lookup = { id: string } + + export type ServerLock = { + server_id: string + reason: string + locked_by?: string + created: string + } + + export type LockServerRequest = { + reason: string + } } export namespace v0 { @@ -645,6 +656,7 @@ export namespace Archon { flows: Flows is_medal: boolean current_user_permissions: UserScope + locked_since: string | null medal_expires?: string } diff --git a/packages/ui/src/components/servers/LockServerModal.vue b/packages/ui/src/components/servers/LockServerModal.vue new file mode 100644 index 00000000000..ac5fe0703d5 --- /dev/null +++ b/packages/ui/src/components/servers/LockServerModal.vue @@ -0,0 +1,160 @@ + + + + + {{ formatMessage(messages.warningBody, { name: serverName }) }} + + + + + {{ formatMessage(messages.reasonLabel) }} + + + + + + + + + + {{ formatMessage(commonMessages.cancelButton) }} + + + + + {{ formatMessage(messages.lockButton) }} + + + + + + + diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/files.vue b/packages/ui/src/layouts/wrapped/hosting/manage/files.vue index 8bc1608f79f..0fc14cfbf1c 100644 --- a/packages/ui/src/layouts/wrapped/hosting/manage/files.vue +++ b/packages/ui/src/layouts/wrapped/hosting/manage/files.vue @@ -10,6 +10,7 @@ import { useUploadSessionUpload } from '#ui/composables/hosting/kyros-session-up import { defineMessages, useVIntl } from '#ui/composables/i18n' import { useServerPermissions } from '#ui/composables/server-permissions' import { + injectAuth, injectModrinthClient, injectModrinthServerContext, injectNotificationManager, @@ -26,6 +27,7 @@ const props = defineProps<{ }>() const client = injectModrinthClient() +const auth = injectAuth() const serverContext = injectModrinthServerContext() const { serverId, @@ -59,20 +61,29 @@ const messages = defineMessages({ id: 'servers.files.zip-created-description', defaultMessage: 'Created {destination}', }, + serverLocked: { + id: 'servers.files.server-locked', + defaultMessage: 'This server is locked and its files are read-only.', + }, }) const zippingFolder = ref(false) +const serverLockedForViewer = computed( + () => !!serverContext.server.value?.locked_since && auth.user.value?.role !== 'admin', +) const serverBusy = computed(() => busyReasons.value.length > 0) const busyTooltip = computed(() => busyReasons.value.length > 0 ? formatMessage(busyReasons.value[0].reason) : undefined, ) const fileWriteDisabled = computed( - () => !canWriteFiles.value || serverBusy.value || zippingFolder.value, -) -const fileWriteDisabledTooltip = computed(() => - canWriteFiles.value ? busyTooltip.value : permissionDeniedMessage.value, + () => + serverLockedForViewer.value || !canWriteFiles.value || serverBusy.value || zippingFolder.value, ) +const fileWriteDisabledTooltip = computed(() => { + if (serverLockedForViewer.value) return formatMessage(messages.serverLocked) + return canWriteFiles.value ? busyTooltip.value : permissionDeniedMessage.value +}) const nonBackupBusyReasons = computed(() => busyReasons.value.filter( (r) => diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/root.vue b/packages/ui/src/layouts/wrapped/hosting/manage/root.vue index 9ccd656e70b..b21e8baca71 100644 --- a/packages/ui/src/layouts/wrapped/hosting/manage/root.vue +++ b/packages/ui/src/layouts/wrapped/hosting/manage/root.vue @@ -129,6 +129,20 @@ /> + + + {{ formatMessage(lockMessages.lockedBadge) }} + + + + + + + + + + + + + +