From 945206153d607f75e60ada111bc2477b7eecda1a Mon Sep 17 00:00:00 2001 From: Jai A Date: Tue, 22 Oct 2024 12:10:54 -0700 Subject: [PATCH 01/58] ads.txt update --- apps/frontend/src/public/ads.txt | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/apps/frontend/src/public/ads.txt b/apps/frontend/src/public/ads.txt index 907f79e9..b8722dd3 100644 --- a/apps/frontend/src/public/ads.txt +++ b/apps/frontend/src/public/ads.txt @@ -152,27 +152,26 @@ appnexus.com, 9624, RESELLER, f5ab79cb980f11d1 openx.com, 541079309, RESELLER, 6a698e2ec38604c6 pubmatic.com, 160251, RESELLER, 5d62403b186f2ace sharethrough.com, AgltJqgT, RESELLER, d53b998a7bd4ecd2 -rubiconproject.com, 23644, RESELLER, 0bfd66d529a55807 -contextweb.com, 562263, DIRECT, 89ff185a4c4e857c +contextweb.com, 562263, RESELLER, 89ff185a4c4e857c sonobi.com, da6df59178, RESELLER, d1a215d9eb5aee9e amxrtb.com, 105199420, DIRECT improvedigital.com, 2434, RESELLER -rhythmone.com, 1838093862, DIRECT, a670c89d4a324e47 -video.unrulymedia.com, 1838093862, DIRECT +admanmedia.com, 2199, RESELLER +rhythmone.com, 1838093862, RESELLER, a670c89d4a324e47 +video.unrulymedia.com, 1838093862, RESELLER 33across.com, 0010b00002QImX1AAL, DIRECT, bbea06d9c4d2853c -rubiconproject.com, 23330, DIRECT, 0bfd66d529a55807 +adyoulike.com, 1f301d3bcd723f5c372070bdfd142940, RESELLER +rubiconproject.com, 23330, RESLLER, 0bfd66d529a55807 onetag.com, 753930a353d6990, DIRECT nobid.io, 22451303355, DIRECT zetaglobal.net, 693, DIRECT -conversantmedia.com, 41848, DIRECT, 03113cd04947736d gumgum.com, 15729, RESELLER, ffdef49475d318a9 insticator.com, a4be8543-c8b1-4f3a-a4c0-73cfbdc3b652, DIRECT, b3511ffcafb23a32 #Insticator_Version_748 richaudience.com, NtMZGaQQTT, DIRECT -e-planning.net, e91ae9f917dd018e, DIRECT, c1ba615865ed87b2 -triplelift.com, 9207, RESELLER, 6c33edb13117fd86 -triplelift.com, 9207-EB, RESELLER, 6c33edb13117fd86 +xapads.com, 209164, DIRECT +google.com, pub-3990748024667386, DIRECT, f08c47fec0942fa0 smartadserver.com, 4872, RESELLER, 060d053dcf45cbf3 -smilewanted.com, 4880, RESELLER +conversantmedia.com, 100745, RESELLER, 03113cd04947736d #GumGum gumgum.com, 15913, DIRECT, ffdef49475d318a9 @@ -236,6 +235,11 @@ Media.net,8CU4JTRF9, RESELLER onetag.com,6e053d779444c00, RESELLER Contextweb.com,562952,RESELLER,89ff185a4c4e857c 33across.com, 0010b00002ODU4HAAX, RESELLER, bbea06d9c4d2853c +adform.com, 2926, RESELLER +improvedigital.com, 2106, RESELLER +smartadserver.com, 4288, RESELLER +video.unrulymedia.com, 3486482593, RESELLER +yieldmo.com, 3133660606033240149, RESELLER #Medianet Media.net, 8CUENMD10, RESELLER From ce4b4ba41dd1d0b73e2a18c230573a769ca09de7 Mon Sep 17 00:00:00 2001 From: Jai A Date: Wed, 23 Oct 2024 13:41:22 -0700 Subject: [PATCH 02/58] Fix daedalus run --- .github/workflows/daedalus-run.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/daedalus-run.yml b/.github/workflows/daedalus-run.yml index dcdbd049..5468af14 100644 --- a/.github/workflows/daedalus-run.yml +++ b/.github/workflows/daedalus-run.yml @@ -49,4 +49,4 @@ jobs: -e CLOUDFLARE_INTEGRATION=$CLOUDFLARE_INTEGRATION \ -e CLOUDFLARE_TOKEN=$CLOUDFLARE_TOKEN \ -e CLOUDFLARE_ZONE_ID=$CLOUDFLARE_ZONE_ID \ - ghcr.io/modrinth/daedalus:master + ghcr.io/modrinth/daedalus:main From 72dab1203353b8b4a52eba5f47763e4a3c7c794e Mon Sep 17 00:00:00 2001 From: Erb3 <49862976+Erb3@users.noreply.github.com> Date: Thu, 24 Oct 2024 22:24:13 +0200 Subject: [PATCH 03/58] fix(docs): correct favicon url (#2843) Starlight defaults to favicon.svg, however a favicon.ico was added to the repo. Changes: - Format astro config according to Prettier - Properly set favicon --- apps/docs/astro.config.mjs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/apps/docs/astro.config.mjs b/apps/docs/astro.config.mjs index a311a155..4fcf82fa 100644 --- a/apps/docs/astro.config.mjs +++ b/apps/docs/astro.config.mjs @@ -1,5 +1,5 @@ -import { defineConfig } from 'astro/config'; -import starlight from '@astrojs/starlight'; +import starlight from '@astrojs/starlight' +import { defineConfig } from 'astro/config' import starlightOpenAPI, { openAPISidebarGroups } from 'starlight-openapi' // https://astro.build/config @@ -8,15 +8,16 @@ export default defineConfig({ integrations: [ starlight({ title: 'Modrinth Documentation', + favicon: '/favicon.ico', editLink: { baseUrl: 'https://github.com/modrinth/code/edit/main/apps/docs/', }, social: { - github: 'https://github.com/modrinth/code', - discord: 'https://discord.modrinth.com', - 'x.com': 'https://x.com/modrinth', - mastodon: 'https://floss.social/@modrinth', - threads: 'https://threads.net/@modrinth', + github: 'https://github.com/modrinth/code', + discord: 'https://discord.modrinth.com', + 'x.com': 'https://x.com/modrinth', + mastodon: 'https://floss.social/@modrinth', + threads: 'https://threads.net/@modrinth', }, logo: { light: './src/assets/light-logo.svg', @@ -36,16 +37,16 @@ export default defineConfig({ label: 'Modrinth API', schema: './public/openapi.yaml', }, - ]) + ]), ], sidebar: [ { - label: 'Contributing to Modrinth', - autogenerate: { directory: 'contributing' }, + label: 'Contributing to Modrinth', + autogenerate: { directory: 'contributing' }, }, // Add the generated sidebar group to the sidebar. ...openAPISidebarGroups, ], }), ], -}); +}) From f5a201dd948f55e7823a2e03d7d3cbabdbbb6d63 Mon Sep 17 00:00:00 2001 From: June Date: Sun, 27 Oct 2024 18:27:06 +0000 Subject: [PATCH 04/58] add top margin to author actions section (#2861) --- apps/frontend/src/components/ui/ProjectMemberHeader.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/frontend/src/components/ui/ProjectMemberHeader.vue b/apps/frontend/src/components/ui/ProjectMemberHeader.vue index 087ddb73..63f75715 100644 --- a/apps/frontend/src/components/ui/ProjectMemberHeader.vue +++ b/apps/frontend/src/components/ui/ProjectMemberHeader.vue @@ -384,6 +384,8 @@ const submitForReview = async () => { } .author-actions { + margin-top: var(--spacing-card-md); + &:empty { display: none; } From b5aeef7ebfa160a301ffa08ab134691e19cab7ea Mon Sep 17 00:00:00 2001 From: Skye Date: Mon, 28 Oct 2024 04:04:17 +0900 Subject: [PATCH 05/58] Allow Bearer prefix on authorization tokens (#2854) Signed-off-by: Skye Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com> --- apps/labrinth/src/auth/validate.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/labrinth/src/auth/validate.rs b/apps/labrinth/src/auth/validate.rs index e69d1943..8b8769c9 100644 --- a/apps/labrinth/src/auth/validate.rs +++ b/apps/labrinth/src/auth/validate.rs @@ -154,10 +154,15 @@ pub fn extract_authorization_header( ) -> Result<&str, AuthenticationError> { let headers = req.headers(); let token_val: Option<&HeaderValue> = headers.get(AUTHORIZATION); - token_val + let token_val = token_val .ok_or_else(|| AuthenticationError::InvalidAuthMethod)? .to_str() - .map_err(|_| AuthenticationError::InvalidCredentials) + .map_err(|_| AuthenticationError::InvalidCredentials)?; + Ok(if let Some(token) = token_val.strip_prefix("Bearer ") { + token + } else { + token_val + }) } pub async fn check_is_moderator_from_headers<'a, 'b, E>( From f6af62064395d3cfec1b420edf69d8804ccdd44c Mon Sep 17 00:00:00 2001 From: Jai A Date: Wed, 30 Oct 2024 15:30:24 -0700 Subject: [PATCH 06/58] Add inmobi to app iframe --- apps/frontend/src/public/promo-frame.html | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/frontend/src/public/promo-frame.html b/apps/frontend/src/public/promo-frame.html index c0fcf84d..5bac9e3c 100644 --- a/apps/frontend/src/public/promo-frame.html +++ b/apps/frontend/src/public/promo-frame.html @@ -9,6 +9,7 @@ src="https://dn0qt3r0xannq.cloudfront.net/modrinth-7JfmkEIXEp/modrinth-longform/prebid-load.js" async > + diff --git a/apps/frontend/src/components/ui/servers/BackupCreateModal.vue b/apps/frontend/src/components/ui/servers/BackupCreateModal.vue new file mode 100644 index 00000000..e0b67486 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/BackupCreateModal.vue @@ -0,0 +1,95 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/BackupDeleteModal.vue b/apps/frontend/src/components/ui/servers/BackupDeleteModal.vue new file mode 100644 index 00000000..44d0ac11 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/BackupDeleteModal.vue @@ -0,0 +1,86 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/BackupRenameModal.vue b/apps/frontend/src/components/ui/servers/BackupRenameModal.vue new file mode 100644 index 00000000..f1ab41e2 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/BackupRenameModal.vue @@ -0,0 +1,86 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/BackupRestoreModal.vue b/apps/frontend/src/components/ui/servers/BackupRestoreModal.vue new file mode 100644 index 00000000..9439ecb2 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/BackupRestoreModal.vue @@ -0,0 +1,82 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/BackupSettingsModal.vue b/apps/frontend/src/components/ui/servers/BackupSettingsModal.vue new file mode 100644 index 00000000..e8564630 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/BackupSettingsModal.vue @@ -0,0 +1,201 @@ + + + + + diff --git a/apps/frontend/src/components/ui/servers/FileItem.vue b/apps/frontend/src/components/ui/servers/FileItem.vue new file mode 100644 index 00000000..78ca6100 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/FileItem.vue @@ -0,0 +1,229 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/FileManagerError.vue b/apps/frontend/src/components/ui/servers/FileManagerError.vue new file mode 100644 index 00000000..a15b5f82 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/FileManagerError.vue @@ -0,0 +1,40 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/FileVirtualList.vue b/apps/frontend/src/components/ui/servers/FileVirtualList.vue new file mode 100644 index 00000000..fe470cbe --- /dev/null +++ b/apps/frontend/src/components/ui/servers/FileVirtualList.vue @@ -0,0 +1,120 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/FilesBrowseNavbar.vue b/apps/frontend/src/components/ui/servers/FilesBrowseNavbar.vue new file mode 100644 index 00000000..426007c5 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/FilesBrowseNavbar.vue @@ -0,0 +1,231 @@ + + + + + diff --git a/apps/frontend/src/components/ui/servers/FilesContextMenu.vue b/apps/frontend/src/components/ui/servers/FilesContextMenu.vue new file mode 100644 index 00000000..19acda99 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/FilesContextMenu.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/apps/frontend/src/components/ui/servers/FilesCreateItemModal.vue b/apps/frontend/src/components/ui/servers/FilesCreateItemModal.vue new file mode 100644 index 00000000..0b29db26 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/FilesCreateItemModal.vue @@ -0,0 +1,95 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/FilesDeleteItemModal.vue b/apps/frontend/src/components/ui/servers/FilesDeleteItemModal.vue new file mode 100644 index 00000000..f3879a58 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/FilesDeleteItemModal.vue @@ -0,0 +1,77 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/FilesEditingNavbar.vue b/apps/frontend/src/components/ui/servers/FilesEditingNavbar.vue new file mode 100644 index 00000000..d359a992 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/FilesEditingNavbar.vue @@ -0,0 +1,140 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/FilesImageViewer.vue b/apps/frontend/src/components/ui/servers/FilesImageViewer.vue new file mode 100644 index 00000000..3792217d --- /dev/null +++ b/apps/frontend/src/components/ui/servers/FilesImageViewer.vue @@ -0,0 +1,159 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/FilesMoveItemModal.vue b/apps/frontend/src/components/ui/servers/FilesMoveItemModal.vue new file mode 100644 index 00000000..3da9fbd6 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/FilesMoveItemModal.vue @@ -0,0 +1,81 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/FilesRenameItemModal.vue b/apps/frontend/src/components/ui/servers/FilesRenameItemModal.vue new file mode 100644 index 00000000..de6657dd --- /dev/null +++ b/apps/frontend/src/components/ui/servers/FilesRenameItemModal.vue @@ -0,0 +1,94 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/LoaderSelector.vue b/apps/frontend/src/components/ui/servers/LoaderSelector.vue new file mode 100644 index 00000000..ceb3e314 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/LoaderSelector.vue @@ -0,0 +1,76 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/LogParser.vue b/apps/frontend/src/components/ui/servers/LogParser.vue new file mode 100644 index 00000000..efea3171 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/LogParser.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/apps/frontend/src/components/ui/servers/MOTDEditor.vue b/apps/frontend/src/components/ui/servers/MOTDEditor.vue new file mode 100644 index 00000000..aa4c5d0e --- /dev/null +++ b/apps/frontend/src/components/ui/servers/MOTDEditor.vue @@ -0,0 +1,660 @@ + + + + + + + diff --git a/apps/frontend/src/components/ui/servers/ModrinthServersIcon.vue b/apps/frontend/src/components/ui/servers/ModrinthServersIcon.vue new file mode 100644 index 00000000..ea883278 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/ModrinthServersIcon.vue @@ -0,0 +1,53 @@ + diff --git a/apps/frontend/src/components/ui/servers/PanelCopyIP.vue b/apps/frontend/src/components/ui/servers/PanelCopyIP.vue new file mode 100644 index 00000000..6bc45920 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/PanelCopyIP.vue @@ -0,0 +1,31 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/PanelOverviewLoading.vue b/apps/frontend/src/components/ui/servers/PanelOverviewLoading.vue new file mode 100644 index 00000000..7fbe45a3 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/PanelOverviewLoading.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/apps/frontend/src/components/ui/servers/PanelServerActionButton.vue b/apps/frontend/src/components/ui/servers/PanelServerActionButton.vue new file mode 100644 index 00000000..ebb5cf0b --- /dev/null +++ b/apps/frontend/src/components/ui/servers/PanelServerActionButton.vue @@ -0,0 +1,312 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/PanelServerStatus.vue b/apps/frontend/src/components/ui/servers/PanelServerStatus.vue new file mode 100644 index 00000000..81e6ebc6 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/PanelServerStatus.vue @@ -0,0 +1,66 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/PanelSpinner.vue b/apps/frontend/src/components/ui/servers/PanelSpinner.vue new file mode 100644 index 00000000..d9b5721d --- /dev/null +++ b/apps/frontend/src/components/ui/servers/PanelSpinner.vue @@ -0,0 +1,22 @@ + diff --git a/apps/frontend/src/components/ui/servers/PanelTerminal.vue b/apps/frontend/src/components/ui/servers/PanelTerminal.vue new file mode 100644 index 00000000..d5c43c72 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/PanelTerminal.vue @@ -0,0 +1,625 @@ + + + + + diff --git a/apps/frontend/src/components/ui/servers/PanelTerminalFullscreen.vue b/apps/frontend/src/components/ui/servers/PanelTerminalFullscreen.vue new file mode 100644 index 00000000..c908aeab --- /dev/null +++ b/apps/frontend/src/components/ui/servers/PanelTerminalFullscreen.vue @@ -0,0 +1,19 @@ + diff --git a/apps/frontend/src/components/ui/servers/PanelTerminalMinimize.vue b/apps/frontend/src/components/ui/servers/PanelTerminalMinimize.vue new file mode 100644 index 00000000..109f532f --- /dev/null +++ b/apps/frontend/src/components/ui/servers/PanelTerminalMinimize.vue @@ -0,0 +1,19 @@ + diff --git a/apps/frontend/src/components/ui/servers/PoweredByPyro.vue b/apps/frontend/src/components/ui/servers/PoweredByPyro.vue new file mode 100644 index 00000000..f6c54fa3 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/PoweredByPyro.vue @@ -0,0 +1,14 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/ProjectSelect.vue b/apps/frontend/src/components/ui/servers/ProjectSelect.vue new file mode 100644 index 00000000..bdc2e2eb --- /dev/null +++ b/apps/frontend/src/components/ui/servers/ProjectSelect.vue @@ -0,0 +1,167 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/PyroLoading.vue b/apps/frontend/src/components/ui/servers/PyroLoading.vue new file mode 100644 index 00000000..3c72d239 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/PyroLoading.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/apps/frontend/src/components/ui/servers/PyroModal.vue b/apps/frontend/src/components/ui/servers/PyroModal.vue new file mode 100644 index 00000000..d2d9648c --- /dev/null +++ b/apps/frontend/src/components/ui/servers/PyroModal.vue @@ -0,0 +1,60 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/SaveBanner.vue b/apps/frontend/src/components/ui/servers/SaveBanner.vue new file mode 100644 index 00000000..be754f73 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/SaveBanner.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/apps/frontend/src/components/ui/servers/ServerGameLabel.vue b/apps/frontend/src/components/ui/servers/ServerGameLabel.vue new file mode 100644 index 00000000..bef03781 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/ServerGameLabel.vue @@ -0,0 +1,33 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/ServerIcon.vue b/apps/frontend/src/components/ui/servers/ServerIcon.vue new file mode 100644 index 00000000..9e8d00b2 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/ServerIcon.vue @@ -0,0 +1,26 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/ServerInfoLabels.vue b/apps/frontend/src/components/ui/servers/ServerInfoLabels.vue new file mode 100644 index 00000000..a23430e7 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/ServerInfoLabels.vue @@ -0,0 +1,41 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/ServerListing.vue b/apps/frontend/src/components/ui/servers/ServerListing.vue new file mode 100644 index 00000000..18be551f --- /dev/null +++ b/apps/frontend/src/components/ui/servers/ServerListing.vue @@ -0,0 +1,101 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/ServerListingSkeleton.vue b/apps/frontend/src/components/ui/servers/ServerListingSkeleton.vue new file mode 100644 index 00000000..8ebe506e --- /dev/null +++ b/apps/frontend/src/components/ui/servers/ServerListingSkeleton.vue @@ -0,0 +1,20 @@ + diff --git a/apps/frontend/src/components/ui/servers/ServerLoaderLabel.vue b/apps/frontend/src/components/ui/servers/ServerLoaderLabel.vue new file mode 100644 index 00000000..13e8694f --- /dev/null +++ b/apps/frontend/src/components/ui/servers/ServerLoaderLabel.vue @@ -0,0 +1,35 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/ServerManageEmptyState.vue b/apps/frontend/src/components/ui/servers/ServerManageEmptyState.vue new file mode 100644 index 00000000..dd7cde95 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/ServerManageEmptyState.vue @@ -0,0 +1,19 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/ServerSidebar.vue b/apps/frontend/src/components/ui/servers/ServerSidebar.vue new file mode 100644 index 00000000..0c44c976 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/ServerSidebar.vue @@ -0,0 +1,45 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/ServerSkeleton.vue b/apps/frontend/src/components/ui/servers/ServerSkeleton.vue new file mode 100644 index 00000000..7429de93 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/ServerSkeleton.vue @@ -0,0 +1,18 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/ServerStats.vue b/apps/frontend/src/components/ui/servers/ServerStats.vue new file mode 100644 index 00000000..85547442 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/ServerStats.vue @@ -0,0 +1,330 @@ + + + + + diff --git a/apps/frontend/src/components/ui/servers/ServerSubdomainLabel.vue b/apps/frontend/src/components/ui/servers/ServerSubdomainLabel.vue new file mode 100644 index 00000000..ed7d1796 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/ServerSubdomainLabel.vue @@ -0,0 +1,40 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/ServerUptimeLabel.vue b/apps/frontend/src/components/ui/servers/ServerUptimeLabel.vue new file mode 100644 index 00000000..e5707531 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/ServerUptimeLabel.vue @@ -0,0 +1,65 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/TeleportDropdownMenu.vue b/apps/frontend/src/components/ui/servers/TeleportDropdownMenu.vue new file mode 100644 index 00000000..2038179b --- /dev/null +++ b/apps/frontend/src/components/ui/servers/TeleportDropdownMenu.vue @@ -0,0 +1,444 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/TeleportOverflowMenu.vue b/apps/frontend/src/components/ui/servers/TeleportOverflowMenu.vue new file mode 100644 index 00000000..8b808ec3 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/TeleportOverflowMenu.vue @@ -0,0 +1,433 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/Timer.vue b/apps/frontend/src/components/ui/servers/Timer.vue new file mode 100644 index 00000000..0de5f7a9 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/Timer.vue @@ -0,0 +1,17 @@ + diff --git a/apps/frontend/src/components/ui/servers/icons/CodeFileIcon.vue b/apps/frontend/src/components/ui/servers/icons/CodeFileIcon.vue new file mode 100644 index 00000000..6f7f6000 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/icons/CodeFileIcon.vue @@ -0,0 +1,18 @@ + diff --git a/apps/frontend/src/components/ui/servers/icons/CogFolderIcon.vue b/apps/frontend/src/components/ui/servers/icons/CogFolderIcon.vue new file mode 100644 index 00000000..f3e65a3f --- /dev/null +++ b/apps/frontend/src/components/ui/servers/icons/CogFolderIcon.vue @@ -0,0 +1,26 @@ + diff --git a/apps/frontend/src/components/ui/servers/icons/EarthIcon.vue b/apps/frontend/src/components/ui/servers/icons/EarthIcon.vue new file mode 100644 index 00000000..3e617ec8 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/icons/EarthIcon.vue @@ -0,0 +1,20 @@ + diff --git a/apps/frontend/src/components/ui/servers/icons/ImageFileIcon.vue b/apps/frontend/src/components/ui/servers/icons/ImageFileIcon.vue new file mode 100644 index 00000000..e7287ebf --- /dev/null +++ b/apps/frontend/src/components/ui/servers/icons/ImageFileIcon.vue @@ -0,0 +1,18 @@ + diff --git a/apps/frontend/src/components/ui/servers/icons/LoaderIcon.vue b/apps/frontend/src/components/ui/servers/icons/LoaderIcon.vue new file mode 100644 index 00000000..8647d9cd --- /dev/null +++ b/apps/frontend/src/components/ui/servers/icons/LoaderIcon.vue @@ -0,0 +1,172 @@ + + + diff --git a/apps/frontend/src/components/ui/servers/icons/LoadingIcon.vue b/apps/frontend/src/components/ui/servers/icons/LoadingIcon.vue new file mode 100644 index 00000000..c2277f28 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/icons/LoadingIcon.vue @@ -0,0 +1,9 @@ + diff --git a/apps/frontend/src/components/ui/servers/icons/PanelErrorIcon.vue b/apps/frontend/src/components/ui/servers/icons/PanelErrorIcon.vue new file mode 100644 index 00000000..0904de9d --- /dev/null +++ b/apps/frontend/src/components/ui/servers/icons/PanelErrorIcon.vue @@ -0,0 +1,16 @@ + diff --git a/apps/frontend/src/components/ui/servers/icons/SlashIcon.vue b/apps/frontend/src/components/ui/servers/icons/SlashIcon.vue new file mode 100644 index 00000000..613ec271 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/icons/SlashIcon.vue @@ -0,0 +1,10 @@ + diff --git a/apps/frontend/src/components/ui/servers/icons/TextFileIcon.vue b/apps/frontend/src/components/ui/servers/icons/TextFileIcon.vue new file mode 100644 index 00000000..fb118911 --- /dev/null +++ b/apps/frontend/src/components/ui/servers/icons/TextFileIcon.vue @@ -0,0 +1,20 @@ + diff --git a/apps/frontend/src/composables/featureFlags.ts b/apps/frontend/src/composables/featureFlags.ts index d52368f5..179f3e6e 100644 --- a/apps/frontend/src/composables/featureFlags.ts +++ b/apps/frontend/src/composables/featureFlags.ts @@ -23,6 +23,8 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({ showAdsWithPlus: false, // Feature toggles + projectTypesPrimaryNav: false, + hidePlusPromoInUserMenu: false, // advancedRendering: true, // externalLinksNewTab: true, // notUsingBlockers: false, diff --git a/apps/frontend/src/composables/pyroFetch.ts b/apps/frontend/src/composables/pyroFetch.ts new file mode 100644 index 00000000..0d0da31b --- /dev/null +++ b/apps/frontend/src/composables/pyroFetch.ts @@ -0,0 +1,103 @@ +import { $fetch, FetchError } from "ofetch"; + +interface PyroFetchOptions { + method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; + contentType?: string; + body?: Record; + version?: number; + override?: { + url?: string; + token?: string; + }; + retry?: boolean; +} + +export class PyroFetchError extends Error { + constructor( + message: string, + public statusCode?: number, + public originalError?: Error, + ) { + super(message); + this.name = "PyroFetchError"; + } +} + +export async function usePyroFetch(path: string, options: PyroFetchOptions = {}): Promise { + const config = useRuntimeConfig(); + const auth = await useAuth(); + const authToken = auth.value?.token; + + if (!authToken) { + throw new PyroFetchError("Cannot pyrofetch without auth", 10000); + } + + const { method = "GET", contentType = "application/json", body, version = 0, override } = options; + + const base = (import.meta.server ? config.pyroBaseUrl : config.public.pyroBaseUrl)?.replace( + /\/$/, + "", + ); + + if (!base) { + throw new PyroFetchError( + "Cannot pyrofetch without base url. Make sure to set a PYRO_BASE_URL in environment variables", + 10001, + ); + } + + const fullUrl = override?.url + ? `https://${override.url}/${path.replace(/^\//, "")}` + : `${base}/modrinth/v${version}/${path.replace(/^\//, "")}`; + + type HeadersRecord = Record; + + const headers: HeadersRecord = { + Authorization: `Bearer ${override?.token ?? authToken}`, + "Access-Control-Allow-Headers": "Authorization", + "User-Agent": "Pyro/1.0 (https://pyro.host)", + Vary: "Accept, Origin", + "Content-Type": contentType, + }; + + if (import.meta.client && typeof window !== "undefined") { + headers.Origin = window.location.origin; + } + + try { + const response = await $fetch(fullUrl, { + method, + headers, + body: body && contentType === "application/json" ? JSON.stringify(body) : body ?? undefined, + timeout: 10000, + retry: options.retry !== false ? (method === "GET" ? 3 : 0) : 0, + }); + return response; + } catch (error) { + console.error("Fetch error:", error); + if (error instanceof FetchError) { + const statusCode = error.response?.status; + const statusText = error.response?.statusText || "Unknown error"; + const errorMessages: { [key: number]: string } = { + 400: "Bad Request", + 401: "Unauthorized", + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 429: "Too Many Requests", + 500: "Internal Server Error", + 502: "Bad Gateway", + }; + const message = + statusCode && statusCode in errorMessages + ? errorMessages[statusCode] + : `HTTP Error: ${statusCode || "unknown"} ${statusText}`; + throw new PyroFetchError(`[PYROFETCH][PYRO] ${message}`, statusCode, error); + } + throw new PyroFetchError( + "[PYROFETCH][PYRO] An unexpected error occurred during the fetch operation.", + undefined, + error as Error, + ); + } +} diff --git a/apps/frontend/src/composables/pyroServers.ts b/apps/frontend/src/composables/pyroServers.ts new file mode 100644 index 00000000..7cbd66dd --- /dev/null +++ b/apps/frontend/src/composables/pyroServers.ts @@ -0,0 +1,1293 @@ +// usePyroServer is a composable that interfaces with the REDACTED API to get data and control the users server +import { $fetch, FetchError } from "ofetch"; + +interface PyroFetchOptions { + method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; + contentType?: string; + body?: Record; + version?: number; + override?: { + url?: string; + token?: string; + }; + retry?: boolean; +} + +async function PyroFetch(path: string, options: PyroFetchOptions = {}): Promise { + const config = useRuntimeConfig(); + const auth = await useAuth(); + const authToken = auth.value?.token; + + if (!authToken) { + throw new PyroFetchError("Cannot pyrofetch without auth", 10000); + } + + const { method = "GET", contentType = "application/json", body, version = 0, override } = options; + + const base = (import.meta.server ? config.pyroBaseUrl : config.public.pyroBaseUrl)?.replace( + /\/$/, + "", + ); + + if (!base) { + throw new PyroFetchError( + "Cannot pyrofetch without base url. Make sure to set a PYRO_BASE_URL in environment variables", + 10001, + ); + } + + const fullUrl = override?.url + ? `https://${override.url}/${path.replace(/^\//, "")}` + : `${base}/modrinth/v${version}/${path.replace(/^\//, "")}`; + + type HeadersRecord = Record; + + const headers: HeadersRecord = { + Authorization: `Bearer ${override?.token ?? authToken}`, + "Access-Control-Allow-Headers": "Authorization", + "User-Agent": "Pyro/1.0 (https://pyro.host)", + Vary: "Accept, Origin", + "Content-Type": contentType, + }; + + if (import.meta.client && typeof window !== "undefined") { + headers.Origin = window.location.origin; + } + + try { + const response = await $fetch(fullUrl, { + method, + headers, + body: body && contentType === "application/json" ? JSON.stringify(body) : body ?? undefined, + timeout: 10000, + retry: options.retry !== false ? (method === "GET" ? 3 : 0) : 0, + }); + return response; + } catch (error) { + console.error("[PYROSERVERS]:", error); + if (error instanceof FetchError) { + const statusCode = error.response?.status; + const statusText = error.response?.statusText || "Unknown error"; + const errorMessages: { [key: number]: string } = { + 400: "Bad Request", + 401: "Unauthorized", + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 429: "Too Many Requests", + 500: "Internal Server Error", + 502: "Bad Gateway", + }; + const message = + statusCode && statusCode in errorMessages + ? errorMessages[statusCode] + : `HTTP Error: ${statusCode || "unknown"} ${statusText}`; + throw new PyroFetchError(`[PYROSERVERS][PYRO] ${message}`, statusCode, error); + } + throw new PyroFetchError( + "[PYROSERVERS][PYRO] An unexpected error occurred during the fetch operation.", + undefined, + error as Error, + ); + } +} + +const internalServerRefrence = ref(null); + +interface License { + id: string; + name: string; + url: string; +} + +interface DonationUrl { + id: string; + platform: string; + url: string; +} + +interface GalleryItem { + url: string; + featured: boolean; + title: string; + description: string; + created: string; + ordering: number; +} + +export interface Project { + slug: string; + title: string; + description: string; + categories: string[]; + client_side: "required" | "optional"; + server_side: "required" | "optional"; + body: string; + status: "approved" | "pending" | "rejected"; + requested_status: "approved" | "pending" | "rejected"; + additional_categories: string[]; + issues_url: string; + source_url: string; + wiki_url: string; + discord_url: string; + donation_urls: DonationUrl[]; + project_type: "mod" | "resourcepack" | "map" | "plugin"; + downloads: number; + icon_url: string; + color: number; + thread_id: string; + monetization_status: "monetized" | "non-monetized"; + id: string; + team: string; + body_url: string | null; + moderator_message: string | null; + published: string; + updated: string; + approved: string; + queued: string; + followers: number; + license: License; + versions: string[]; + game_versions: string[]; + loaders: string[]; + gallery: GalleryItem[]; +} + +interface General { + server_id: string; + name: 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; +} + +interface Allocation { + port: number; + name: string; +} + +interface Startup { + invocation: string; + original_invocation: string; + jdk_version: "lts8" | "lts11" | "lts17" | "lts21"; + jdk_build: "corretto" | "temurin" | "graal"; +} + +interface Mod { + filename: string; + project_id: string; + version_id: string; + name: string; + version_number: string; + icon_url: string; + disabled: boolean; +} + +interface Backup { + id: string; + name: string; + created_at: string; + ongoing: boolean; +} + +interface AutoBackupSettings { + enabled: boolean; + interval: number; +} + +interface JWTAuth { + url: string; + token: string; +} + +const 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`; + } + } + + return fileContent; +}; + +const processImage = async (iconUrl: string | undefined) => { + const image = ref(null); + const auth = await PyroFetch(`servers/${internalServerRefrence.value.serverId}/fs`); + try { + const fileData = await PyroFetch(`/download?path=/server-icon-original.png`, { + override: auth, + retry: false, + }); + + if (fileData instanceof Blob) { + if (import.meta.client) { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + const img = new Image(); + img.src = URL.createObjectURL(fileData); + await new Promise((resolve) => { + img.onload = () => { + canvas.width = 512; + canvas.height = 512; + ctx?.drawImage(img, 0, 0, 512, 512); + const dataURL = canvas.toDataURL("image/png"); + internalServerRefrence.value.general.image = dataURL; + image.value = dataURL; + resolve(); + }; + }); + } + } + } catch (error) { + if (error instanceof PyroFetchError && error.statusCode === 404) { + console.log("[PYROSERVERS] No server icon found"); + } else { + console.error(error); + } + } + + if (image.value === null && iconUrl) { + console.log("iconUrl", iconUrl); + try { + const response = await fetch(iconUrl); + const file = await response.blob(); + const originalfile = new File([file], "server-icon-original.png", { + type: "image/png", + }); + if (import.meta.client) { + const scaledFile = await new Promise((resolve, reject) => { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + const img = new Image(); + img.src = URL.createObjectURL(file); + img.onload = () => { + canvas.width = 64; + canvas.height = 64; + ctx?.drawImage(img, 0, 0, 64, 64); + canvas.toBlob((blob) => { + if (blob) { + const data = new File([blob], "server-icon.png", { type: "image/png" }); + resolve(data); + } else { + reject(new Error("Canvas toBlob failed")); + } + }, "image/png"); + }; + img.onerror = reject; + }); + if (scaledFile) { + await PyroFetch(`/create?path=/server-icon.png&type=file`, { + method: "POST", + contentType: "application/octet-stream", + body: scaledFile, + override: auth, + }); + + await PyroFetch(`/create?path=/server-icon-original.png&type=file`, { + method: "POST", + contentType: "application/octet-stream", + body: originalfile, + override: auth, + }); + } + } + } catch (error) { + if (error instanceof PyroFetchError && error.statusCode === 404) { + console.log("[PYROSERVERS] No server icon found"); + } else { + console.error(error); + } + } + } + return image.value; +}; + +// ------------------ GENERAL ------------------ // + +const sendPowerAction = async (action: string) => { + try { + await PyroFetch(`servers/${internalServerRefrence.value.serverId}/power`, { + method: "POST", + body: { action }, + }); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + await internalServerRefrence.value.refresh(); + } catch (error) { + console.error("Error changing power state:", error); + throw error; + } +}; + +const updateName = async (newName: string) => { + try { + await PyroFetch(`servers/${internalServerRefrence.value.serverId}/name`, { + method: "POST", + body: { name: newName }, + }); + } catch (error) { + console.error("Error updating server name:", error); + throw error; + } +}; + +const reinstallServer = async ( + serverId: string, + loader: boolean, + projectId: string, + versionId?: string, + loaderVersionId?: string, + hardReset: boolean = false, +) => { + try { + const hardResetParam = hardReset ? "true" : "false"; + if (loader) { + if (projectId.toLowerCase() === "neoforge") { + projectId = "NeoForge"; + } + await PyroFetch(`servers/${serverId}/reinstall?hard=${hardResetParam}`, { + method: "POST", + body: { loader: projectId, loader_version: loaderVersionId, game_version: versionId }, + }); + } else { + await PyroFetch(`servers/${serverId}/reinstall?hard=${hardResetParam}`, { + method: "POST", + body: { project_id: projectId, version_id: versionId }, + }); + } + } catch (error) { + console.error("Error reinstalling server:", error); + throw error; + } +}; + +const reinstallFromMrpack = async (mrpack: File, hardReset: boolean = false) => { + const hardResetParam = hardReset ? "true" : "false"; + try { + const auth = await PyroFetch( + `servers/${internalServerRefrence.value.serverId}/reinstallFromMrpack`, + ); + + return await PyroFetch(`/reinstallMrpack?hard=${hardResetParam}`, { + method: "POST", + contentType: "application/octet-stream", + body: mrpack, + override: auth, + }); + } catch (error) { + console.error("Error reinstalling from mrpack:", error); + throw error; + } +}; + +const suspendServer = async (status: boolean) => { + try { + await PyroFetch(`servers/${internalServerRefrence.value.serverId}/suspend`, { + method: "POST", + body: { suspended: status }, + }); + } catch (error) { + console.error("Error suspending server:", error); + throw error; + } +}; + +const fetchConfigFile = async (fileName: string) => { + try { + return await PyroFetch(`servers/${internalServerRefrence.value.serverId}/config/${fileName}`); + } catch (error) { + console.error("Error fetching config file:", error); + throw error; + } +}; + +const getMotd = async () => { + try { + const props = await downloadFile("/server.properties"); + if (props) { + const lines = props.split("\n"); + for (const line of lines) { + if (line.startsWith("motd=")) { + return line.slice(5); + } + } + } + } catch { + return null; + } +}; + +const setMotd = async (motd: string) => { + try { + const props = (await fetchConfigFile("ServerProperties")) as any; + if (props) { + props.motd = motd; + const newProps = constructServerProperties(props); + const octetStream = new Blob([newProps], { type: "application/octet-stream" }); + const auth = await await PyroFetch( + `servers/${internalServerRefrence.value.serverId}/fs`, + ); + + return await PyroFetch(`/update?path=/server.properties`, { + method: "PUT", + contentType: "application/octet-stream", + body: octetStream, + override: auth, + }); + } + } catch (error) { + console.error("Error setting motd:", error); + } +}; + +// ------------------ MODS ------------------ // + +const installMod = async (projectId: string, versionId: string) => { + try { + await PyroFetch(`servers/${internalServerRefrence.value.serverId}/mods`, { + method: "POST", + body: { rinth_ids: { project_id: projectId, version_id: versionId } }, + }); + } catch (error) { + console.error("Error installing mod:", error); + throw error; + } +}; + +const removeMod = async (modId: string) => { + try { + await PyroFetch(`servers/${internalServerRefrence.value.serverId}/deleteMod`, { + method: "POST", + body: { + path: modId, + }, + }); + } catch (error) { + console.error("Error removing mod:", error); + throw error; + } +}; + +const reinstallMod = async (modId: string, versionId: string) => { + try { + await PyroFetch(`servers/${internalServerRefrence.value.serverId}/mods/${modId}`, { + method: "PUT", + body: { version_id: versionId }, + }); + } catch (error) { + console.error("Error reinstalling mod:", error); + throw error; + } +}; + +// ------------------ BACKUPS ------------------ // + +const createBackup = async (backupName: string) => { + try { + await PyroFetch(`servers/${internalServerRefrence.value.serverId}/backups`, { + method: "POST", + body: { name: backupName }, + }); + } catch (error) { + console.error("Error creating backup:", error); + throw error; + } +}; + +const renameBackup = async (backupId: string, newName: string) => { + try { + await PyroFetch(`servers/${internalServerRefrence.value.serverId}/backups/${backupId}/rename`, { + method: "POST", + body: { name: newName }, + }); + } catch (error) { + console.error("Error renaming backup:", error); + throw error; + } +}; + +const deleteBackup = async (backupId: string) => { + try { + await PyroFetch(`servers/${internalServerRefrence.value.serverId}/backups/${backupId}`, { + method: "DELETE", + }); + } catch (error) { + console.error("Error deleting backup:", error); + throw error; + } +}; + +const restoreBackup = async (backupId: string) => { + try { + await PyroFetch( + `servers/${internalServerRefrence.value.serverId}/backups/${backupId}/restore`, + { + method: "POST", + }, + ); + } catch (error) { + console.error("Error restoring backup:", error); + throw error; + } +}; + +const downloadBackup = async (backupId: string) => { + try { + return await PyroFetch(`servers/${internalServerRefrence.value.serverId}/backups/${backupId}`); + } catch (error) { + console.error("Error downloading backup:", error); + throw error; + } +}; + +const updateAutoBackup = async (autoBackup: "enable" | "disable", interval: number) => { + try { + return await PyroFetch(`servers/${internalServerRefrence.value.serverId}/autobackup`, { + method: "POST", + body: { set: autoBackup, interval }, + }); + } catch (error) { + console.error("Error updating auto backup:", error); + throw error; + } +}; + +const getAutoBackup = async () => { + try { + return await PyroFetch(`servers/${internalServerRefrence.value.serverId}/autobackup`); + } catch (error) { + console.error("Error getting auto backup settings:", error); + throw error; + } +}; + +// ------------------ NETWORK ------------------ // + +const reserveAllocation = async (name: string): Promise => { + try { + return await PyroFetch( + `servers/${internalServerRefrence.value.serverId}/allocations?name=${name}`, + { + method: "POST", + }, + ); + } catch (error) { + console.error("Error reserving new allocation:", error); + throw error; + } +}; + +const updateAllocation = async (port: number, name: string) => { + try { + await PyroFetch( + `servers/${internalServerRefrence.value.serverId}/allocations/${port}?name=${name}`, + { + method: "PUT", + }, + ); + } catch (error) { + console.error("Error updating allocations:", error); + throw error; + } +}; + +const deleteAllocation = async (port: number) => { + try { + await PyroFetch(`servers/${internalServerRefrence.value.serverId}/allocations/${port}`, { + method: "DELETE", + }); + } catch (error) { + console.error("Error deleting allocation:", error); + throw error; + } +}; + +const checkSubdomainAvailability = async (subdomain: string): Promise<{ available: boolean }> => { + try { + return (await PyroFetch(`subdomains/${subdomain}/isavailable`)) as { available: boolean }; + } catch (error) { + console.error("Error checking subdomain availability:", error); + throw error; + } +}; + +const changeSubdomain = async (subdomain: string) => { + try { + await PyroFetch(`servers/${internalServerRefrence.value.serverId}/subdomain`, { + method: "POST", + body: { subdomain }, + }); + } catch (error) { + console.error("Error changing subdomain:", error); + throw error; + } +}; + +// ------------------ STARTUP ------------------ // + +const updateStartupSettings = async ( + invocation: string, + jdkVersion: "lts8" | "lts11" | "lts17" | "lts21", + jdkBuild: "corretto" | "temurin" | "graal", +) => { + try { + await PyroFetch(`servers/${internalServerRefrence.value.serverId}/startup`, { + method: "POST", + body: { + invocation: invocation || null, + jdk_version: jdkVersion || null, + jdk_build: jdkBuild || null, + }, + }); + } catch (error) { + console.error("Error updating startup settings:", error); + throw error; + } +}; + +// ------------------ FS ------------------ // + +const retryWithAuth = async (requestFn: () => Promise) => { + try { + return await requestFn(); + } catch (error) { + if (error instanceof PyroFetchError && error.statusCode === 401) { + await internalServerRefrence.value.refresh(["fs"]); + return await requestFn(); + } + } +}; + +const listDirContents = (path: string, page: number, pageSize: number) => { + return retryWithAuth(async () => { + return await PyroFetch(`/list?path=${path}&page=${page}&page_size=${pageSize}`, { + override: internalServerRefrence.value.fs.auth, + retry: false, + }); + }); +}; + +const createFileOrFolder = (path: string, type: "file" | "directory") => { + return retryWithAuth(async () => { + return await PyroFetch(`/create?path=${path}&type=${type}`, { + method: "POST", + contentType: "application/octet-stream", + override: internalServerRefrence.value.fs.auth, + }); + }); +}; + +const uploadFile = (path: string, file: File) => { + return retryWithAuth(async () => { + return await PyroFetch(`/create?path=${path}&type=file`, { + method: "POST", + contentType: "application/octet-stream", + body: file, + override: internalServerRefrence.value.fs.auth, + }); + }); +}; + +const renameFileOrFolder = (path: string, name: string) => { + const pathName = path.split("/").slice(0, -1).join("/") + "/" + name; + return retryWithAuth(async () => { + return await PyroFetch(`/move`, { + method: "POST", + override: internalServerRefrence.value.fs.auth, + body: { + source: path, + destination: pathName, + }, + }); + }); +}; + +const updateFile = (path: string, content: string) => { + const octetStream = new Blob([content], { type: "application/octet-stream" }); + return retryWithAuth(async () => { + return await PyroFetch(`/update?path=${path}`, { + method: "PUT", + contentType: "application/octet-stream", + body: octetStream, + override: internalServerRefrence.value.fs.auth, + }); + }); +}; + +const createMissingFolders = async (path: string) => { + if (path.startsWith("/")) { + path = path.substring(1); + } + const folders = path.split("/"); + console.log(folders); + let currentPath = ""; + + for (const folder of folders) { + currentPath += "/" + folder; + try { + await createFileOrFolder(currentPath, "directory"); + } catch {} + } +}; + +const moveFileOrFolder = (path: string, newPath: string) => { + return retryWithAuth(async () => { + console.log(path); + console.log(newPath); + console.log(newPath.substring(0, newPath.lastIndexOf("/"))); + await createMissingFolders(newPath.substring(0, newPath.lastIndexOf("/"))); + + return await PyroFetch(`/move`, { + method: "POST", + override: internalServerRefrence.value.fs.auth, + body: { + source: path, + destination: newPath, + }, + }); + }); +}; + +const deleteFileOrFolder = (path: string, recursive: boolean) => { + const encodedPath = encodeURIComponent(path); + return retryWithAuth(async () => { + return await PyroFetch(`/delete?path=${encodedPath}&recursive=${recursive}`, { + method: "DELETE", + override: internalServerRefrence.value.fs.auth, + }); + }); +}; + +const downloadFile = (path: string, raw?: boolean) => { + return retryWithAuth(async () => { + const fileData = await PyroFetch(`/download?path=${path}`, { + override: internalServerRefrence.value.fs.auth, + }); + + if (fileData instanceof Blob) { + if (raw) { + return fileData; + } else { + return await fileData.text(); + } + } + }); +}; + +const modules: any = { + general: { + get: async (serverId: string) => { + try { + const data = await PyroFetch(`servers/${serverId}`); + // TODO: temp hack to fix hydration error + if (data.upstream?.project_id) { + const res = await $fetch( + `https://api.modrinth.com/v2/project/${data.upstream.project_id}`, + ); + data.project = res as Project; + } + if (import.meta.client) { + data.image = (await processImage(data.project?.icon_url)) ?? undefined; + } + const motd = await getMotd(); + if (motd === "A Minecraft Server") { + await setMotd( + `§b${data.project?.title || data.loader + " " + data.mc_version} §f♦ §aModrinth Servers`, + ); + } + data.motd = motd; + return data; + } catch (error) { + internalServerRefrence.value.setError(error); + return undefined; + } + }, + updateName, + power: sendPowerAction, + reinstall: reinstallServer, + reinstallFromMrpack, + suspend: suspendServer, + getMotd, + setMotd, + fetchConfigFile, + }, + mods: { + get: async (serverId: string) => { + try { + const mods = await PyroFetch(`servers/${serverId}/mods`); + return { + data: + internalServerRefrence.value.error === undefined + ? mods.sort((a, b) => (a?.name ?? "").localeCompare(b?.name ?? "")) + : [], + }; + } catch (error) { + internalServerRefrence.value.setError(error); + return undefined; + } + }, + install: installMod, + remove: removeMod, + reinstall: reinstallMod, + }, + backups: { + get: async (serverId: string) => { + try { + return { data: await PyroFetch(`servers/${serverId}/backups`) }; + } catch (error) { + internalServerRefrence.value.setError(error); + return undefined; + } + }, + create: createBackup, + rename: renameBackup, + delete: deleteBackup, + restore: restoreBackup, + download: downloadBackup, + updateAutoBackup, + getAutoBackup, + }, + network: { + get: async (serverId: string) => { + try { + return { allocations: await PyroFetch(`servers/${serverId}/allocations`) }; + } catch (error) { + internalServerRefrence.value.setError(error); + return undefined; + } + }, + reserveAllocation, + updateAllocation, + deleteAllocation, + checkSubdomainAvailability, + changeSubdomain, + }, + startup: { + get: async (serverId: string) => { + try { + return await PyroFetch(`servers/${serverId}/startup`); + } catch (error) { + internalServerRefrence.value.setError(error); + return undefined; + } + }, + update: updateStartupSettings, + }, + ws: { + get: async (serverId: string) => { + try { + return await PyroFetch(`servers/${serverId}/ws`); + } catch (error) { + internalServerRefrence.value.setError(error); + return undefined; + } + }, + }, + fs: { + get: async (serverId: string) => { + try { + return { auth: await PyroFetch(`servers/${serverId}/fs`) }; + } catch (error) { + internalServerRefrence.value.setError(error); + return undefined; + } + }, + listDirContents, + createFileOrFolder, + uploadFile, + renameFileOrFolder, + updateFile, + moveFileOrFolder, + deleteFileOrFolder, + downloadFile, + }, +}; + +type GeneralFunctions = { + /** + * INTERNAL: Gets the general settings of a server. + * @param serverId - The ID of the server. + */ + get: (serverId: string) => Promise; + + /** + * Updates the name of the server. + * @param newName - The new name for the server. + */ + updateName: (newName: string) => Promise; + + /** + * Sends a power action to the server. + + * @param action - The power action to send (e.g., "start", "stop", "restart"). + */ + power: (action: string) => Promise; + + /** + * Reinstalls the server with the specified project and version. + * @param loader - Whether to use a loader. + * @param projectId - The ID of the project. + * @param versionId - Optional version ID. + * @param loaderVersionId - Optional loader version ID. + * @param hardReset - Whether to perform a hard reset. + */ + reinstall: ( + serverId: string, + loader: boolean, + projectId: string, + versionId?: string, + loaderVersionId?: string, + hardReset?: boolean, + ) => Promise; + + /** + * Reinstalls the server from a mrpack. + * @param mrpack - The mrpack file. + * @param hardReset - Whether to perform a hard reset. + */ + reinstallFromMrpack: (mrpack: File, hardReset?: boolean) => Promise; + + /** + * Suspends or resumes the server. + * @param status - True to suspend the server, false to resume. + */ + suspend: (status: boolean) => Promise; + + /** + * INTERNAL: Gets the general settings of a server. + */ + getMotd: () => Promise; + + /** + * INTERNAL: Updates the general settings of a server. + * @param motd - The new motd. + */ + setMotd: (motd: string) => Promise; + + /** + * INTERNAL: Gets the config file of a server. + * @param fileName - The name of the file. + */ + fetchConfigFile: (fileName: string) => Promise; +}; + +type ModFunctions = { + /** + * INTERNAL: Gets the mods of a server. + * @param serverId - The ID of the server. + * @returns + */ + get: (serverId: string) => Promise; + + /** + * Installs a mod to a server. + * @param projectId - The ID of the project. + * @param versionId - The ID of the version. + */ + install: (projectId: string, versionId: string) => Promise; + + /** + * Removes a mod from a server. + * @param modId - The ID of the mod. + */ + remove: (modId: string) => Promise; + + /** + * Reinstalls a mod to a server. + * @param modId - The ID of the mod. + * @param versionId - The ID of the version. + */ + reinstall: (modId: string, versionId: string) => Promise; +}; + +type BackupFunctions = { + /** + * INTERNAL: Gets the backups of a server. + * @param serverId - The ID of the server. + * @returns + */ + get: (serverId: string) => Promise; + + /** + * Creates a new backup for the server. + * @param backupName - The name of the backup. + */ + create: (backupName: string) => Promise; + + /** + * Renames a backup for the server. + * @param backupId - The ID of the backup. + * @param newName - The new name for the backup. + */ + rename: (backupId: string, newName: string) => Promise; + + /** + * Deletes a backup for the server. + * @param backupId - The ID of the backup. + */ + delete: (backupId: string) => Promise; + + /** + * Restores a backup for the server. + * @param serverId - The ID of the server. + * @param backupId - The ID of the backup. + */ + restore: (backupId: string) => Promise; + + /** + * Downloads a backup for the server. + * @param backupId - The ID of the backup. + */ + download: (backupId: string) => Promise; + + /** + * Updates the auto backup settings of the server. + * @param autoBackup - Whether to enable auto backup. + * @param interval - The interval to backup at (in Hours). + */ + updateAutoBackup: (autoBackup: "enable" | "disable", interval: number) => Promise; + + /** + * Gets the auto backup settings of the server. + */ + getAutoBackup: () => Promise; +}; + +type NetworkFunctions = { + /** + * INTERNAL: Gets the network settings of a server. + * @param serverId - The ID of the server. + * @returns + */ + get: (serverId: string) => Promise; + + /** + * Reserves a new allocation for the server. + * @param name - The name of the allocation. + * @returns The allocated network port details. + */ + reserveAllocation: (name: string) => Promise; + + /** + * Updates the allocation for the server. + * @param port - The port to update. + * @param name - The new name for the allocation. + */ + updateAllocation: (port: number, name: string) => Promise; + + /** + * Deletes an allocation for the server. + * @param port - The port to delete. + */ + deleteAllocation: (port: number) => Promise; + + /** + * Checks if a subdomain is available. + * @param subdomain - The subdomain to check. + * @returns True if the subdomain is available, otherwise false. + */ + checkSubdomainAvailability: (subdomain: string) => Promise; + + /** + * Changes the subdomain of the server. + * @param subdomain - The new subdomain. + */ + changeSubdomain: (subdomain: string) => Promise; +}; + +type StartupFunctions = { + /** + * INTERNAL: Gets the startup settings of a server. + * @param serverId - The ID of the server. + * @returns + */ + get: (serverId: string) => Promise; + + /** + * Updates the startup settings of a server. + * @param invocation - The invocation of the server. + * @param jdkVersion - The version of the JDK. + * @param jdkBuild - The build of the JDK. + */ + update: ( + invocation: string, + jdkVersion: "lts8" | "lts11" | "lts17" | "lts21", + jdkBuild: "corretto" | "temurin" | "graal", + ) => Promise; +}; + +type FSFunctions = { + /** + * INTERNAL: Gets the file system settings of a server. + * @param serverId + * @returns + */ + get: (serverId: string) => Promise; + + /** + * @param path - The path to list the contents of. + * @param page - The page to list. + * @param pageSize - The page size to list. + * @returns + */ + listDirContents: (path: string, page: number, pageSize: number) => Promise; + + /** + * @param path - The path to create the file or folder at. + * @param type - The type of file or folder to create. + * @returns + */ + createFileOrFolder: (path: string, type: "file" | "directory") => Promise; + + /** + * @param path - The path to upload the file to. + * @param file - The file to upload. + * @returns + */ + uploadFile: (path: string, file: File) => Promise; + + /** + * @param path - The path to rename the file or folder at. + * @param name - The new name for the file or folder. + * @returns + */ + renameFileOrFolder: (path: string, name: string) => Promise; + + /** + * @param path - The path to update the file at. + * @param content - The new content for the file. + * @returns + */ + updateFile: (path: string, content: string) => Promise; + + /** + * @param path - The path to move the file or folder at. + * @param newPath - The new path for the file or folder. + * @returns + */ + moveFileOrFolder: (path: string, newPath: string) => Promise; + + /** + * @param path - The path to delete the file or folder at. + * @param recursive - Whether to delete the file or folder recursively. + * @returns + */ + deleteFileOrFolder: (path: string, recursive: boolean) => Promise; + + /** + * @param serverId - The ID of the server. + * @param path - The path to download the file from. + * @param raw - Whether to return the raw blob. + * @returns + */ + downloadFile: (path: string, raw?: boolean) => Promise; +}; + +type GeneralModule = General & GeneralFunctions; +type ModsModule = { data: Mod[] } & ModFunctions; +type BackupsModule = { data: Backup[] } & BackupFunctions; +type NetworkModule = { allocations: Allocation[] } & NetworkFunctions; +type StartupModule = Startup & StartupFunctions; +type FSModule = { auth: JWTAuth } & FSFunctions; + +type ModulesMap = { + general: GeneralModule; + mods: ModsModule; + backups: BackupsModule; + network: NetworkModule; + startup: StartupModule; + ws: JWTAuth; + fs: FSModule; +}; + +type avaliableModules = ("general" | "mods" | "backups" | "network" | "startup" | "ws" | "fs")[]; + +export type Server = { + [K in T[number]]?: ModulesMap[K]; +} & { + /** + * Refreshes the included modules of the server + * @param refreshModules - The modules to refresh. + */ + refresh: (refreshModules?: avaliableModules) => Promise; + setError: (error: Error) => void; + error?: Error; + serverId: string; +}; + +export const usePyroServer = async (serverId: string, includedModules: avaliableModules) => { + const server: Server = reactive({ + refresh: async (refreshModules?: avaliableModules) => { + const promises: Promise[] = []; + if (refreshModules) { + for (const module of refreshModules) { + promises.push( + (async () => { + const mods = modules[module]; + if (mods.get) { + const data = await mods.get(serverId); + server[module] = { ...server[module], ...data }; + } + })(), + ); + } + } else { + for (const module of includedModules) { + promises.push( + (async () => { + const mods = modules[module]; + if (mods.get) { + const data = await mods.get(serverId); + server[module] = { ...server[module], ...data }; + } + })(), + ); + } + } + await Promise.all(promises); + }, + setError: (error: Error) => { + server.error = error; + }, + serverId, + }); + + for (const module of includedModules) { + const mods = modules[module]; + server[module] = mods; + } + + internalServerRefrence.value = server; + + await server.refresh(); + + return server as Server; +}; diff --git a/apps/frontend/src/layouts/default.vue b/apps/frontend/src/layouts/default.vue index 8a452dac..94e681b0 100644 --- a/apps/frontend/src/layouts/default.vue +++ b/apps/frontend/src/layouts/default.vue @@ -38,7 +38,7 @@ config.public.apiBaseUrl.startsWith('https://staging-api.modrinth.com') && !cosmetics.hideStagingBanner " - class="site-banner site-banner--warning" + class="site-banner site-banner--warning [&>*]:z-[6]" >
@@ -62,121 +62,165 @@
-