mirror of
https://github.com/modrinth/code.git
synced 2026-08-27 01:54:47 +00:00
feat: hosting access tab (#5995)
* feat: implement access tab with dummy data * fix: spacing * feat: qa * feat: implement backend * qa: qa pass * feat: fix user "search" * fix: lint * feat: change to bitfield * feat: fix fields * fix: lint * fix: lint * feat: hook up api * feat: fix permissions * feat: audit log table event start * feat: better mobile mode for audit log table * feat: i18n * feat: qa * feat: enforce permissions * feat: email template start * feat: qa * fix: tooltip bug * feat: qa * impl: sse support in api-client * feat: sse impl * fix: desync path * feat: time frame picker from analytics * feat: QA * fix: spacing * fix: permisison audit log entries * fix: hosting manage page shared server detection * fix: lint * feat: qa + lint * feat: audit log table sort by time * feat: finish frontend panel stuff * fix: lint * fix: backend alignment * fix: lint * fix: supress friend errors * feat: qa * fix: qa * fix: lint * fix: utils barrel * fix: safari cookies in dev * fix: pin nuxt * feat: fixes + notif fix * fix: notifications * feat: qa * fix: notification sync not happening immediately * fix: qa * fix: qa * feat: qa * blog + prepr * feat: toast shit * blog images * thumbnail update one last time * prepr * feat: use reinvite route * update images * fix: reinvite stuff * fix: lint * fix: alignment of save bar * fix: notif sizing * fix: split up access * fix: lint * fix: lint * fix: link --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
@@ -3,9 +3,9 @@ import { LRUCache } from 'lru-cache'
|
||||
import { injectI18n } from '../providers/i18n'
|
||||
import { LOCALES } from './i18n.ts'
|
||||
|
||||
const formatterCache = new LRUCache<string, Intl.RelativeTimeFormat>({ max: 5 })
|
||||
const formatterCache = new LRUCache<string, Intl.RelativeTimeFormat>({ max: 15 })
|
||||
|
||||
export function useRelativeTime() {
|
||||
export function useRelativeTime(options?: Intl.RelativeTimeFormatOptions) {
|
||||
const { locale } = injectI18n()
|
||||
|
||||
return (value: Date | number | string | null | undefined) => {
|
||||
@@ -29,7 +29,7 @@ export function useRelativeTime() {
|
||||
const months = Math.round(diff / 2629746000)
|
||||
const years = Math.round(diff / 31556952000)
|
||||
|
||||
const rtf = getFormatter(locale.value)
|
||||
const rtf = getFormatter(locale.value, options)
|
||||
|
||||
if (Math.abs(seconds) < 60) {
|
||||
return rtf.format(seconds, 'second')
|
||||
@@ -49,15 +49,22 @@ export function useRelativeTime() {
|
||||
}
|
||||
}
|
||||
|
||||
function getFormatter(locale: string): Intl.RelativeTimeFormat {
|
||||
let formatter = formatterCache.get(locale)
|
||||
function getFormatter(
|
||||
locale: string,
|
||||
options?: Intl.RelativeTimeFormatOptions,
|
||||
): Intl.RelativeTimeFormat {
|
||||
const localeDefinition = LOCALES.find((loc) => loc.code === locale)
|
||||
const numeric = options?.numeric ?? localeDefinition?.numeric ?? 'auto'
|
||||
const style = options?.style ?? 'long'
|
||||
const cacheKey = `${locale}:${numeric}:${style}`
|
||||
let formatter = formatterCache.get(cacheKey)
|
||||
if (!formatter) {
|
||||
const localeDefinition = LOCALES.find((loc) => loc.code === locale)
|
||||
formatter = new Intl.RelativeTimeFormat(locale, {
|
||||
numeric: localeDefinition?.numeric || 'auto',
|
||||
style: 'long',
|
||||
...options,
|
||||
numeric,
|
||||
style,
|
||||
})
|
||||
formatterCache.set(locale, formatter)
|
||||
formatterCache.set(cacheKey, formatter)
|
||||
}
|
||||
return formatter
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ export * from './server-backup'
|
||||
export * from './server-backups-queue'
|
||||
export * from './server-console'
|
||||
export * from './server-manage-core-runtime'
|
||||
export * from './server-permissions'
|
||||
export * from './sticky-observer'
|
||||
export * from './terminal'
|
||||
export * from './use-loading-bar-token'
|
||||
|
||||
@@ -23,7 +23,7 @@ export function useServerBackupsQueue(serverId: Ref<string>, worldId: Ref<string
|
||||
enabled: computed(() => !!worldId.value),
|
||||
refetchInterval: (q) => {
|
||||
const data = q.state.data as Archon.BackupsQueue.v1.BackupsQueueResponse | undefined
|
||||
return data?.active_operations?.length ? 3000 : false
|
||||
return data?.active_operations?.length ? 30_000 : false
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -4,13 +4,12 @@ import {
|
||||
setNodeAuthState,
|
||||
type UploadState,
|
||||
} from '@modrinth/api-client'
|
||||
import type { Stats } from '@modrinth/utils'
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import type { FileOperation } from '../layouts/shared/files-tab/types'
|
||||
import { injectModrinthClient, provideModrinthServerContext } from '../providers'
|
||||
import type { BusyReason, CancelUploadHandler } from '../providers/server-context'
|
||||
import type { BusyReason, CancelUploadHandler, ServerStats } from '../providers/server-context'
|
||||
import { defineMessage } from './i18n'
|
||||
import { useModrinthServersConsole } from './server-console'
|
||||
|
||||
@@ -26,6 +25,7 @@ type UseServerManageCoreRuntimeOptions = {
|
||||
serverId: ReadableRef<string>
|
||||
worldId: ReadableRef<string | null>
|
||||
server: ReadableRef<Archon.Servers.v0.Server | null | undefined>
|
||||
serverFull?: ReadableRef<Archon.Servers.v1.ServerFull | null | undefined>
|
||||
isSyncingContent: ReadableRef<boolean>
|
||||
extraBusyReasons?: ComputedRef<BusyReason[]>
|
||||
setDisconnectedOnAuthIncorrect?: boolean
|
||||
@@ -35,7 +35,7 @@ type UseServerManageCoreRuntimeOptions = {
|
||||
onStateEvent?: (data: Archon.Websocket.v0.WSStateEvent) => void
|
||||
}
|
||||
|
||||
const createInitialStats = (): Stats => ({
|
||||
const createInitialStats = (): ServerStats => ({
|
||||
current: {
|
||||
cpu_percent: 0,
|
||||
ram_usage_bytes: 0,
|
||||
@@ -91,7 +91,7 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
|
||||
const serverPowerState = ref<Archon.Websocket.v0.PowerState>('stopped')
|
||||
const powerStateDetails = ref<{ oom_killed?: boolean; exit_code?: number }>()
|
||||
const isServerRunning = computed(() => serverPowerState.value === 'running')
|
||||
const stats = ref<Stats>(createInitialStats())
|
||||
const stats = ref<ServerStats>(createInitialStats())
|
||||
const uptimeSeconds = ref(0)
|
||||
const fsAuth = ref<{ url: string; token: string } | null>(null)
|
||||
const fsOps = ref<Archon.Websocket.v0.FilesystemOperation[]>([])
|
||||
@@ -141,7 +141,7 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const updateStats = (currentStats: Stats['current']) => {
|
||||
const updateStats = (currentStats: ServerStats['current']) => {
|
||||
if (!shouldProcessEvent()) return
|
||||
if (!isConnected.value) isConnected.value = true
|
||||
cpuData.value = appendGraphData(cpuData.value, currentStats.cpu_percent)
|
||||
@@ -384,6 +384,8 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
|
||||
}
|
||||
fsAuth.value = await client.archon.servers_v0.getFilesystemAuth(options.serverId.value)
|
||||
}
|
||||
const currentUserPermissions = computed(() => options.server.value?.current_user_permissions ?? 0)
|
||||
const serverFull = computed(() => options.serverFull?.value ?? null)
|
||||
|
||||
provideModrinthServerContext({
|
||||
get serverId() {
|
||||
@@ -391,6 +393,8 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
|
||||
},
|
||||
worldId: options.worldId as Ref<string | null>,
|
||||
server: options.server as Ref<Archon.Servers.v0.Server>,
|
||||
serverFull,
|
||||
currentUserPermissions,
|
||||
isConnected,
|
||||
isWsAuthIncorrect,
|
||||
powerState: serverPowerState,
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
import type { Archon } from '@modrinth/api-client'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
import { onMounted, onUnmounted, watch } from 'vue'
|
||||
|
||||
import { injectModrinthClient } from '#ui/providers'
|
||||
|
||||
type ReadableRef<T> = Ref<T> | ComputedRef<T>
|
||||
type SyncUnsubscriber = () => void
|
||||
|
||||
type UseServerPanelSyncOptions = {
|
||||
serverId: ReadableRef<string>
|
||||
worldId: ReadableRef<string | null>
|
||||
}
|
||||
|
||||
const ACTION_LOG_INVALIDATE_DELAY_MS = 500
|
||||
|
||||
export function useServerPanelSync(options: UseServerPanelSyncOptions) {
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
let activeServerId: string | null = null
|
||||
let unsubscribers: SyncUnsubscriber[] = []
|
||||
let mounted = false
|
||||
let actionLogInvalidateTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const legacyServerDetailKey = (serverId: string) => ['servers', 'detail', serverId] as const
|
||||
const serverV1DetailKey = (serverId: string) => ['servers', 'v1', 'detail', serverId] as const
|
||||
const contentListKey = (serverId: string) => ['content', 'list', 'v1', serverId] as const
|
||||
const actionLogBaseKey = (serverId: string) =>
|
||||
['servers', 'action-log', 'v1', 'infinite', serverId] as const
|
||||
|
||||
function connect(targetServerId: string) {
|
||||
if (!targetServerId || activeServerId === targetServerId) return
|
||||
|
||||
disconnect()
|
||||
activeServerId = targetServerId
|
||||
|
||||
if (!client.archon.sync.getStatus(targetServerId)?.lastEventId) {
|
||||
void invalidateCorePanelQueries(targetServerId)
|
||||
}
|
||||
|
||||
unsubscribers = [
|
||||
client.archon.sync.onAny(targetServerId, (event) => handleSyncEvent(targetServerId, event)),
|
||||
]
|
||||
|
||||
void client.archon.sync.safeConnectServer(targetServerId, { intent: 'all' }).catch((error) => {
|
||||
console.warn(
|
||||
`[server-panel-sync] Failed to connect sync stream for ${targetServerId}:`,
|
||||
error,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
if (actionLogInvalidateTimer) {
|
||||
clearTimeout(actionLogInvalidateTimer)
|
||||
actionLogInvalidateTimer = null
|
||||
}
|
||||
|
||||
for (const unsubscribe of unsubscribers) unsubscribe()
|
||||
unsubscribers = []
|
||||
|
||||
if (activeServerId) {
|
||||
client.archon.sync.disconnect(activeServerId)
|
||||
activeServerId = null
|
||||
}
|
||||
}
|
||||
|
||||
function handleSyncEvent(serverId: string, event: Archon.Sync.v1.SyncEvent) {
|
||||
if (event.type === 'protocol.reset' || event.type === 'protocol.invalid') {
|
||||
void invalidateCorePanelQueries(serverId)
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === 'protocol.error') {
|
||||
console.warn(`[server-panel-sync] Protocol error for ${serverId}: ${event.error}`)
|
||||
return
|
||||
}
|
||||
|
||||
scheduleActionLogInvalidation(serverId)
|
||||
|
||||
if (event.type.startsWith('backup.')) {
|
||||
handleBackupEvent(serverId)
|
||||
return
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case 'server.patch':
|
||||
handleServerPatch(serverId, event)
|
||||
break
|
||||
case 'server.network.patch':
|
||||
handleServerNetworkPatch(serverId, event)
|
||||
break
|
||||
case 'server.transfer.start':
|
||||
case 'server.transfer.done':
|
||||
void invalidateServerDetails(serverId)
|
||||
break
|
||||
case 'users.patch':
|
||||
handleUsersPatch(serverId)
|
||||
break
|
||||
case 'world.patch':
|
||||
handleWorldPatch(serverId, event)
|
||||
break
|
||||
case 'world.startup.patch':
|
||||
handleWorldStartupPatch(serverId, event)
|
||||
break
|
||||
case 'world.content.addon.patch':
|
||||
handleWorldContentAddonPatch(serverId, event)
|
||||
break
|
||||
case 'world.content.base.update':
|
||||
handleWorldContentBaseUpdate(serverId, event)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function handleServerPatch(serverId: string, event: Archon.Sync.v1.ServerPatchEvent) {
|
||||
queryClient.setQueryData<Archon.Servers.v0.Server>(
|
||||
legacyServerDetailKey(serverId),
|
||||
(current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
name: event.name,
|
||||
net: {
|
||||
...current.net,
|
||||
domain: event.subdomain,
|
||||
},
|
||||
}
|
||||
: current,
|
||||
)
|
||||
queryClient.setQueryData<Archon.Servers.v1.ServerFull>(
|
||||
serverV1DetailKey(serverId),
|
||||
(current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
name: event.name,
|
||||
subdomain: event.subdomain,
|
||||
}
|
||||
: current,
|
||||
)
|
||||
}
|
||||
|
||||
function handleServerNetworkPatch(
|
||||
serverId: string,
|
||||
event: Archon.Sync.v1.ServerNetworkPatchEvent,
|
||||
) {
|
||||
queryClient.setQueryData<Archon.Servers.v0.Allocation[]>(
|
||||
['servers', 'allocations', serverId],
|
||||
event.ports,
|
||||
)
|
||||
void invalidateServerDetails(serverId)
|
||||
}
|
||||
|
||||
function handleUsersPatch(serverId: string) {
|
||||
void queryClient.invalidateQueries({ queryKey: ['servers', 'users', 'v1', serverId] })
|
||||
void invalidateServerDetails(serverId)
|
||||
}
|
||||
|
||||
function handleWorldPatch(serverId: string, event: Archon.Sync.v1.WorldPatchEvent) {
|
||||
patchServerFullWorld(serverId, event.world_id, (world) => ({
|
||||
...world,
|
||||
name: event.name,
|
||||
}))
|
||||
}
|
||||
|
||||
function handleWorldStartupPatch(serverId: string, event: Archon.Sync.v1.WorldStartupPatchEvent) {
|
||||
patchServerFullWorld(serverId, event.world_id, (world) =>
|
||||
world.content
|
||||
? {
|
||||
...world,
|
||||
content: {
|
||||
...world.content,
|
||||
java_version: event.java_version,
|
||||
invocation: event.invocation,
|
||||
original_invocation: event.original_invocation,
|
||||
},
|
||||
}
|
||||
: world,
|
||||
)
|
||||
void queryClient.invalidateQueries({ queryKey: ['servers', 'startup', 'v1', serverId] })
|
||||
}
|
||||
|
||||
function handleWorldContentAddonPatch(
|
||||
serverId: string,
|
||||
event: Archon.Sync.v1.WorldContentAddonPatchEvent,
|
||||
) {
|
||||
if (event.world_id !== options.worldId.value) {
|
||||
void invalidateContentAndServerDetails(serverId)
|
||||
return
|
||||
}
|
||||
|
||||
queryClient.setQueryData<Archon.Content.v1.Addons>(contentListKey(serverId), (current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
addons: mergeAddonSpecs(current.addons ?? [], event.specs),
|
||||
}
|
||||
: current,
|
||||
)
|
||||
void queryClient.invalidateQueries({ queryKey: contentListKey(serverId) })
|
||||
}
|
||||
|
||||
function handleWorldContentBaseUpdate(
|
||||
serverId: string,
|
||||
event: Archon.Sync.v1.WorldContentBaseUpdateEvent,
|
||||
) {
|
||||
if (event.world_id === options.worldId.value) {
|
||||
queryClient.setQueryData<Archon.Content.v1.Addons>(contentListKey(serverId), (current) =>
|
||||
current ? { ...current, ...event.spec } : event.spec,
|
||||
)
|
||||
} else {
|
||||
void queryClient.invalidateQueries({ queryKey: contentListKey(serverId) })
|
||||
}
|
||||
|
||||
void queryClient.invalidateQueries({ queryKey: serverV1DetailKey(serverId) })
|
||||
}
|
||||
|
||||
function handleBackupEvent(serverId: string) {
|
||||
void queryClient.invalidateQueries({ queryKey: ['backups', 'queue', serverId] })
|
||||
void invalidateServerDetails(serverId)
|
||||
}
|
||||
|
||||
function patchServerFullWorld(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
patch: (world: Archon.Servers.v1.WorldFull) => Archon.Servers.v1.WorldFull,
|
||||
) {
|
||||
queryClient.setQueryData<Archon.Servers.v1.ServerFull>(
|
||||
serverV1DetailKey(serverId),
|
||||
(current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
worlds: current.worlds.map((world) => (world.id === worldId ? patch(world) : world)),
|
||||
}
|
||||
: current,
|
||||
)
|
||||
}
|
||||
|
||||
function scheduleActionLogInvalidation(serverId: string) {
|
||||
if (actionLogInvalidateTimer) clearTimeout(actionLogInvalidateTimer)
|
||||
|
||||
actionLogInvalidateTimer = setTimeout(() => {
|
||||
actionLogInvalidateTimer = null
|
||||
void queryClient.invalidateQueries({ queryKey: actionLogBaseKey(serverId) })
|
||||
}, ACTION_LOG_INVALIDATE_DELAY_MS)
|
||||
}
|
||||
|
||||
async function invalidateServerDetails(serverId: string) {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: legacyServerDetailKey(serverId) }),
|
||||
queryClient.invalidateQueries({ queryKey: serverV1DetailKey(serverId) }),
|
||||
])
|
||||
}
|
||||
|
||||
async function invalidateContentAndServerDetails(serverId: string) {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: contentListKey(serverId) }),
|
||||
invalidateServerDetails(serverId),
|
||||
])
|
||||
}
|
||||
|
||||
async function invalidateCorePanelQueries(serverId: string) {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: legacyServerDetailKey(serverId) }),
|
||||
queryClient.invalidateQueries({ queryKey: serverV1DetailKey(serverId) }),
|
||||
queryClient.invalidateQueries({ queryKey: contentListKey(serverId) }),
|
||||
queryClient.invalidateQueries({ queryKey: ['backups', 'queue', serverId] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['servers', 'users', 'v1', serverId] }),
|
||||
queryClient.invalidateQueries({ queryKey: actionLogBaseKey(serverId) }),
|
||||
queryClient.invalidateQueries({ queryKey: ['servers', 'startup', 'v1', serverId] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['servers', 'allocations', serverId] }),
|
||||
])
|
||||
}
|
||||
|
||||
function mergeAddonSpecs(
|
||||
currentAddons: Archon.Content.v1.Addon[],
|
||||
incomingAddons: Archon.Content.v1.Addon[],
|
||||
): Archon.Content.v1.Addon[] {
|
||||
const currentByFilename = new Map(
|
||||
currentAddons.map((addon) => [normalizeAddonFilename(addon.filename), addon] as const),
|
||||
)
|
||||
|
||||
return incomingAddons.map((incoming) =>
|
||||
mergeAddonSpec(currentByFilename.get(normalizeAddonFilename(incoming.filename)), incoming),
|
||||
)
|
||||
}
|
||||
|
||||
function mergeAddonSpec(
|
||||
current: Archon.Content.v1.Addon | undefined,
|
||||
incoming: Archon.Content.v1.Addon,
|
||||
): Archon.Content.v1.Addon {
|
||||
if (!current) return incoming
|
||||
|
||||
return {
|
||||
...current,
|
||||
...incoming,
|
||||
filesize: incoming.filesize || current.filesize,
|
||||
name: incoming.name ?? current.name,
|
||||
owner: incoming.owner ?? current.owner,
|
||||
icon_url: incoming.icon_url ?? current.icon_url,
|
||||
has_update: incoming.has_update ?? current.has_update,
|
||||
project_id: incoming.project_id ?? current.project_id,
|
||||
version: incoming.version
|
||||
? {
|
||||
...incoming.version,
|
||||
name: incoming.version.name ?? current.version?.name ?? null,
|
||||
environment: incoming.version.environment ?? current.version?.environment ?? null,
|
||||
}
|
||||
: current.version,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeAddonFilename(filename: string): string {
|
||||
return filename.endsWith('.disabled') ? filename.slice(0, -'.disabled'.length) : filename
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
mounted = true
|
||||
connect(options.serverId.value)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => options.serverId.value,
|
||||
(serverId) => {
|
||||
if (!mounted) return
|
||||
if (serverId) {
|
||||
connect(serverId)
|
||||
} else {
|
||||
disconnect()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
mounted = false
|
||||
disconnect()
|
||||
})
|
||||
|
||||
return {
|
||||
disconnect,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import type { Archon } from '@modrinth/api-client'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useVIntl } from '#ui/composables/i18n'
|
||||
import { injectModrinthServerContext } from '#ui/providers'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
export type ServerPermissionName = keyof typeof Archon.ServerUsers.v1.UserScope
|
||||
|
||||
type ServerPermissionValue = Archon.Servers.v0.UserScope | Archon.ServerUsers.v1.UserScope
|
||||
|
||||
const U64_SIZE = 64n
|
||||
const U64_MODULUS = 1n << U64_SIZE
|
||||
|
||||
export const serverPermissionBits = {
|
||||
NONE: 0n,
|
||||
BASE_READ: 1n << 63n,
|
||||
POWER_ACTIONS: 1n << 62n,
|
||||
EXEC_COMMANDS: 1n << 61n,
|
||||
FILES_WRITE: 1n << 60n,
|
||||
SETUP: 1n << 59n,
|
||||
BACKUPS: 1n << 58n,
|
||||
ADVANCED: 1n << 57n,
|
||||
RESET_SERVER: 1n << 56n,
|
||||
MANAGE_USERS: 1n << 55n,
|
||||
SUPPORT_AGENT: 1n,
|
||||
INFRA_MANAGER: 1n << 1n,
|
||||
INFRA_MANAGER_READ: 1n << 2n,
|
||||
INFRA_SERVERS_XFER: 1n << 3n,
|
||||
INFRA_USERS: 1n << 4n,
|
||||
SERVER_ADMIN: ((1n << 64n) - 1n) ^ ((1n << 15n) - 1n),
|
||||
} as const satisfies Record<ServerPermissionName, bigint>
|
||||
|
||||
function parsePermissionNumber(value: number) {
|
||||
const bigintValue = BigInt(value)
|
||||
return bigintValue < 0n ? bigintValue + U64_MODULUS : bigintValue
|
||||
}
|
||||
|
||||
function parsePermissionString(value: string) {
|
||||
const numericValue = Number(value)
|
||||
if (value.trim() !== '' && Number.isFinite(numericValue)) {
|
||||
return parsePermissionNumber(numericValue)
|
||||
}
|
||||
|
||||
const permissions = value
|
||||
.split('|')
|
||||
.map((permission) => permission.trim())
|
||||
.filter((permission): permission is ServerPermissionName => permission in serverPermissionBits)
|
||||
|
||||
if (permissions.length === 0) return 0n
|
||||
|
||||
return permissions.reduce((mask, permission) => mask | serverPermissionBits[permission], 0n)
|
||||
}
|
||||
|
||||
function parsePermissions(permissions: ServerPermissionValue) {
|
||||
return typeof permissions === 'number'
|
||||
? parsePermissionNumber(permissions)
|
||||
: parsePermissionString(permissions)
|
||||
}
|
||||
|
||||
function hasPermissionBit(permissions: ServerPermissionValue, scope: ServerPermissionName) {
|
||||
const permission = serverPermissionBits[scope]
|
||||
if (permission === 0n) return true
|
||||
|
||||
const permissionsMask = parsePermissions(permissions)
|
||||
return (permissionsMask & permission) === permission
|
||||
}
|
||||
|
||||
export function hasServerPermission(
|
||||
permissions: ServerPermissionValue,
|
||||
scope: ServerPermissionName,
|
||||
) {
|
||||
if (
|
||||
scope !== 'NONE' &&
|
||||
scope !== 'SERVER_ADMIN' &&
|
||||
hasPermissionBit(permissions, 'SERVER_ADMIN')
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return hasPermissionBit(permissions, scope)
|
||||
}
|
||||
|
||||
export function useServerPermissions() {
|
||||
const { formatMessage } = useVIntl()
|
||||
const { currentUserPermissions } = injectModrinthServerContext()
|
||||
|
||||
const hasCurrentUserPermission = (scope: ServerPermissionName) =>
|
||||
hasServerPermission(currentUserPermissions.value, scope)
|
||||
|
||||
const permissionDeniedMessage = computed(() => formatMessage(commonMessages.noPermissionAction))
|
||||
|
||||
const canUsePowerActions = computed(() => hasCurrentUserPermission('POWER_ACTIONS'))
|
||||
const canExecuteCommands = computed(() => hasCurrentUserPermission('EXEC_COMMANDS'))
|
||||
const canWriteFiles = computed(() => hasCurrentUserPermission('FILES_WRITE'))
|
||||
const canSetup = computed(() => hasCurrentUserPermission('SETUP'))
|
||||
const canManageBackups = computed(() => hasCurrentUserPermission('BACKUPS'))
|
||||
const canUseAdvancedSettings = computed(() => hasCurrentUserPermission('ADVANCED'))
|
||||
const canResetServer = computed(() => hasCurrentUserPermission('RESET_SERVER'))
|
||||
const canManageUsers = computed(() => hasCurrentUserPermission('MANAGE_USERS'))
|
||||
|
||||
const permissionTooltip = (allowed: boolean) =>
|
||||
allowed ? undefined : permissionDeniedMessage.value
|
||||
|
||||
return {
|
||||
currentUserPermissions,
|
||||
permissionDeniedMessage,
|
||||
hasCurrentUserPermission,
|
||||
canUsePowerActions,
|
||||
canExecuteCommands,
|
||||
canWriteFiles,
|
||||
canSetup,
|
||||
canManageBackups,
|
||||
canUseAdvancedSettings,
|
||||
canResetServer,
|
||||
canManageUsers,
|
||||
permissionTooltip,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user