Compare commits
1 Commits
self-hoste
...
ntex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
072fa47129 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +0,0 @@
|
||||
* text=auto eol=lf
|
||||
13
.github/workflows/theseus-release.yml
vendored
13
.github/workflows/theseus-release.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [macos-latest, self-hosted, ubuntu-22.04]
|
||||
platform: [macos-latest, windows-latest, ubuntu-22.04]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
@@ -53,11 +53,11 @@ jobs:
|
||||
!target/release/bundle/*/*.app.tar.gz
|
||||
!target/release/bundle/*/*.app.tar.gz.sig
|
||||
|
||||
!target/release/bundle/appimage/*.AppImage
|
||||
!target/release/bundle/appimage/*.AppImage.tar.gz
|
||||
!target/release/bundle/appimage/*.AppImage.tar.gz.sig
|
||||
!target/release/bundle/deb/*.deb
|
||||
!target/release/bundle/rpm/*.rpm
|
||||
!target/release/bundle/*/*.AppImage
|
||||
!target/release/bundle/*/*.AppImage.tar.gz
|
||||
!target/release/bundle/*/*.AppImage.tar.gz.sig
|
||||
!target/release/bundle/*/*.deb
|
||||
!target/release/bundle/*/*.rpm
|
||||
|
||||
!target/release/bundle/msi/*.msi
|
||||
!target/release/bundle/msi/*.msi.zip
|
||||
@@ -127,7 +127,6 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
DIGICERT_PASSWORD: ${{ secrets.DIGICERT_PASSWORD }}
|
||||
|
||||
- name: upload ${{ matrix.platform }}
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
1062
Cargo.lock
generated
1062
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -21,4 +21,4 @@ strip = true # Remove debug symbols
|
||||
opt-level = 3
|
||||
|
||||
[patch.crates-io]
|
||||
wry = { git = "https://github.com/modrinth/wry", rev ="cdbf938" }
|
||||
wry = { git = "https://github.com/modrinth/wry", rev ="e88d4a1" }
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@modrinth/app-frontend",
|
||||
"private": true,
|
||||
"version": "0.9.2",
|
||||
"version": "0.9.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { RouterView, useRoute, useRouter } from 'vue-router'
|
||||
import { computed, ref, onMounted, watch, onUnmounted } from 'vue'
|
||||
import { RouterView, useRouter, useRoute } from 'vue-router'
|
||||
import {
|
||||
ArrowBigUpDashIcon,
|
||||
CompassIcon,
|
||||
DownloadIcon,
|
||||
HomeIcon,
|
||||
LeftArrowIcon,
|
||||
LibraryIcon,
|
||||
LogInIcon,
|
||||
LogOutIcon,
|
||||
MaximizeIcon,
|
||||
MinimizeIcon,
|
||||
HomeIcon,
|
||||
LibraryIcon,
|
||||
PlusIcon,
|
||||
RestoreIcon,
|
||||
RightArrowIcon,
|
||||
SettingsIcon,
|
||||
XIcon,
|
||||
DownloadIcon,
|
||||
CompassIcon,
|
||||
MinimizeIcon,
|
||||
MaximizeIcon,
|
||||
RestoreIcon,
|
||||
LogOutIcon,
|
||||
RightArrowIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Avatar, Button, ButtonStyled, Notifications, OverflowMenu } from '@modrinth/ui'
|
||||
import { useLoading, useTheming } from '@/store/state'
|
||||
@@ -32,12 +31,12 @@ import ModrinthLoadingIndicator from '@/components/LoadingIndicatorBar.vue'
|
||||
import { handleError, useNotifications } from '@/store/notifications.js'
|
||||
import { command_listener, warning_listener } from '@/helpers/events.js'
|
||||
import { type } from '@tauri-apps/plugin-os'
|
||||
import { getOS, isDev, restartApp } from '@/helpers/utils.js'
|
||||
import { debugAnalytics, initAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
|
||||
import { isDev, getOS, restartApp } from '@/helpers/utils.js'
|
||||
import { initAnalytics, debugAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import { getVersion } from '@tauri-apps/api/app'
|
||||
import URLConfirmModal from '@/components/ui/URLConfirmModal.vue'
|
||||
import { create_profile_and_install_from_file } from './helpers/pack'
|
||||
import { install_from_file } from './helpers/pack'
|
||||
import { useError } from '@/store/error.js'
|
||||
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
|
||||
import ModInstallModal from '@/components/ui/install_flow/ModInstallModal.vue'
|
||||
@@ -51,7 +50,7 @@ import { renderString } from '@modrinth/utils'
|
||||
import { useFetch } from '@/helpers/fetch.js'
|
||||
import { check } from '@tauri-apps/plugin-updater'
|
||||
import NavButton from '@/components/ui/NavButton.vue'
|
||||
import { get as getCreds, login, logout } from '@/helpers/mr_auth.js'
|
||||
import { get as getCreds, logout, login } from '@/helpers/mr_auth.js'
|
||||
import { get_user } from '@/helpers/cache.js'
|
||||
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
|
||||
import dayjs from 'dayjs'
|
||||
@@ -257,19 +256,25 @@ themeStore.$subscribe(() => {
|
||||
sidebarToggled.value = !themeStore.toggleSidebar
|
||||
})
|
||||
|
||||
const forceSidebar = computed(
|
||||
() => route.path.startsWith('/browse') || route.path.startsWith('/project'),
|
||||
)
|
||||
const forceSidebar = ref(false)
|
||||
const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value)
|
||||
const showAd = computed(() => !(!sidebarVisible.value || hasPlus.value))
|
||||
|
||||
router.afterEach((to) => {
|
||||
forceSidebar.value = to.path.startsWith('/browse') || to.path.startsWith('/project')
|
||||
})
|
||||
|
||||
const currentTimeout = ref(null)
|
||||
watch(
|
||||
showAd,
|
||||
() => {
|
||||
if (!showAd.value) {
|
||||
if (currentTimeout.value) clearTimeout(currentTimeout.value)
|
||||
hide_ads_window(true)
|
||||
} else {
|
||||
init_ads_window(true)
|
||||
currentTimeout.value = setTimeout(() => {
|
||||
init_ads_window(true)
|
||||
}, 400)
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
@@ -296,7 +301,7 @@ async function handleCommand(e) {
|
||||
if (e.event === 'RunMRPack') {
|
||||
// RunMRPack should directly install a local mrpack given a path
|
||||
if (e.path.endsWith('.mrpack')) {
|
||||
await create_profile_and_install_from_file(e.path).catch(handleError)
|
||||
await install_from_file(e.path).catch(handleError)
|
||||
trackEvent('InstanceCreate', {
|
||||
source: 'CreationModalFileDrop',
|
||||
})
|
||||
@@ -438,20 +443,6 @@ function handleAuxClick(e) {
|
||||
<div data-tauri-drag-region class="app-grid-statusbar bg-bg-raised h-[--top-bar-height] flex">
|
||||
<div data-tauri-drag-region class="flex p-3">
|
||||
<ModrinthAppLogo class="h-full w-auto text-contrast pointer-events-none" />
|
||||
<div class="flex items-center gap-1 ml-3">
|
||||
<button
|
||||
class="cursor-pointer p-0 m-0 border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
|
||||
@click="router.back()"
|
||||
>
|
||||
<LeftArrowIcon />
|
||||
</button>
|
||||
<button
|
||||
class="cursor-pointer p-0 m-0 border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
|
||||
@click="router.forward()"
|
||||
>
|
||||
<RightArrowIcon />
|
||||
</button>
|
||||
</div>
|
||||
<Breadcrumbs class="pt-[2px]" />
|
||||
</div>
|
||||
<section class="flex ml-auto items-center">
|
||||
@@ -713,7 +704,7 @@ function handleAuxClick(e) {
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 0px;
|
||||
// transition: grid-template-columns 0.4s ease-in-out;
|
||||
transition: grid-template-columns 0.4s ease-in-out;
|
||||
|
||||
&.sidebar-enabled {
|
||||
grid-template-columns: 1fr 300px;
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { onUnmounted, ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
DownloadIcon,
|
||||
GameIcon,
|
||||
PlayIcon,
|
||||
SpinnerIcon,
|
||||
StopCircleIcon,
|
||||
TimerIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled } from '@modrinth/ui'
|
||||
import { SpinnerIcon, GameIcon, TimerIcon, StopCircleIcon, PlayIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, Avatar } from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { finish_install, kill, run } from '@/helpers/profile'
|
||||
import { kill, run } from '@/helpers/profile'
|
||||
import { get_by_profile_path } from '@/helpers/process'
|
||||
import { process_listener } from '@/helpers/events'
|
||||
import { handleError } from '@/store/state.js'
|
||||
@@ -42,15 +35,12 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const playing = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
const modLoading = computed(
|
||||
() =>
|
||||
loading.value ||
|
||||
currentEvent.value === 'installing' ||
|
||||
(currentEvent.value === 'launched' && !playing.value),
|
||||
currentEvent.value === 'installing' || (currentEvent.value === 'launched' && !playing.value),
|
||||
)
|
||||
const installing = computed(() => props.instance.install_stage.includes('installing'))
|
||||
const installed = computed(() => props.instance.install_stage === 'installed')
|
||||
const installing = computed(() => props.instance.install_stage !== 'installed')
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -66,7 +56,6 @@ const checkProcess = async () => {
|
||||
|
||||
const play = async (e, context) => {
|
||||
e?.stopPropagation()
|
||||
loading.value = true
|
||||
await run(props.instance.path)
|
||||
.catch((err) => handleSevereError(err, { profilePath: props.instance.path }))
|
||||
.finally(() => {
|
||||
@@ -76,7 +65,6 @@ const play = async (e, context) => {
|
||||
source: context,
|
||||
})
|
||||
})
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const stop = async (e, context) => {
|
||||
@@ -92,12 +80,6 @@ const stop = async (e, context) => {
|
||||
})
|
||||
}
|
||||
|
||||
const repair = async (e) => {
|
||||
e?.stopPropagation()
|
||||
|
||||
await finish_install(props.instance)
|
||||
}
|
||||
|
||||
const openFolder = async () => {
|
||||
await showProfileInFolder(props.instance.path)
|
||||
}
|
||||
@@ -136,7 +118,7 @@ onUnmounted(() => unlisten())
|
||||
<template>
|
||||
<template v-if="compact">
|
||||
<div
|
||||
class="card-shadow grid grid-cols-[auto_1fr_auto] bg-bg-raised rounded-xl p-3 pl-4 gap-2 cursor-pointer hover:brightness-90 transition-all"
|
||||
class="button-base card-shadow grid grid-cols-[auto_1fr_auto] bg-bg-raised rounded-xl p-3 pl-4 gap-2 cursor-pointer active:scale-[0.98] transition-transform"
|
||||
@click="seeInstance"
|
||||
@mouseenter="checkProcess"
|
||||
>
|
||||
@@ -209,15 +191,6 @@ onUnmounted(() => unlisten())
|
||||
class="animate-spin w-8 h-8"
|
||||
tabindex="-1"
|
||||
/>
|
||||
<ButtonStyled v-else-if="!installed" size="large" color="brand" circular>
|
||||
<button
|
||||
v-tooltip="'Repair'"
|
||||
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
|
||||
@click="(e) => repair(e)"
|
||||
>
|
||||
<DownloadIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else size="large" color="brand" circular>
|
||||
<button
|
||||
v-tooltip="'Play'"
|
||||
|
||||
@@ -199,16 +199,16 @@
|
||||
<script setup>
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import {
|
||||
CodeIcon,
|
||||
FolderOpenIcon,
|
||||
FolderSearchIcon,
|
||||
InfoIcon,
|
||||
PlusIcon,
|
||||
UpdatedIcon,
|
||||
UploadIcon,
|
||||
XIcon,
|
||||
CodeIcon,
|
||||
FolderOpenIcon,
|
||||
InfoIcon,
|
||||
FolderSearchIcon,
|
||||
UpdatedIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Avatar, Button, Checkbox, Chips } from '@modrinth/ui'
|
||||
import { Avatar, Button, Chips, Checkbox } from '@modrinth/ui'
|
||||
import { computed, onUnmounted, ref, shallowRef } from 'vue'
|
||||
import { get_loaders } from '@/helpers/tags'
|
||||
import { create } from '@/helpers/profile'
|
||||
@@ -218,7 +218,7 @@ import { get_game_versions, get_loader_versions } from '@/helpers/metadata'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import Multiselect from 'vue-multiselect'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { create_profile_and_install_from_file } from '@/helpers/pack.js'
|
||||
import { install_from_file } from '@/helpers/pack.js'
|
||||
import {
|
||||
get_default_launcher_path,
|
||||
get_importable_instances,
|
||||
@@ -263,7 +263,7 @@ defineExpose({
|
||||
hide()
|
||||
const { paths } = event.payload
|
||||
if (paths && paths.length > 0 && paths[0].endsWith('.mrpack')) {
|
||||
await create_profile_and_install_from_file(paths[0]).catch(handleError)
|
||||
await install_from_file(paths[0]).catch(handleError)
|
||||
trackEvent('InstanceCreate', {
|
||||
source: 'CreationModalFileDrop',
|
||||
})
|
||||
@@ -419,7 +419,7 @@ const openFile = async () => {
|
||||
const newProject = await open({ multiple: false })
|
||||
if (!newProject) return
|
||||
hide()
|
||||
await create_profile_and_install_from_file(newProject.path ?? newProject).catch(handleError)
|
||||
await install_from_file(newProject.path ?? newProject).catch(handleError)
|
||||
|
||||
trackEvent('InstanceCreate', {
|
||||
source: 'CreationModalFileOpen',
|
||||
|
||||
@@ -60,7 +60,7 @@ const toTransparent = computed(() => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="card-shadow bg-bg-raised rounded-xl overflow-clip cursor-pointer hover:brightness-90 transition-all"
|
||||
class="card-shadow button-base bg-bg-raised rounded-xl overflow-clip cursor-pointer active:scale-[0.98] transition-transform"
|
||||
@click="router.push(`/project/${project.slug}`)"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { init_ads_window } from '@/helpers/ads.js'
|
||||
import { ChevronRightIcon } from '@modrinth/assets'
|
||||
import { init_ads_window, open_ads_link, record_ads_click } from '@/helpers/ads.js'
|
||||
|
||||
const adsWrapper = ref(null)
|
||||
|
||||
@@ -28,12 +29,27 @@ function updateAdPosition() {
|
||||
initDevicePixelRatioWatcher()
|
||||
}
|
||||
}
|
||||
|
||||
async function openPlusLink() {
|
||||
await record_ads_click()
|
||||
await open_ads_link('https://modrinth.com/plus', 'https://modrinth.com')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="adsWrapper" class="ad-parent relative flex w-full justify-center cursor-pointer bg-bg">
|
||||
<div class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 p-6">
|
||||
<p class="m-0 text-2xl font-bold text-contrast">75% of ad revenue goes to creators</p>
|
||||
<button
|
||||
class="mt-auto items-center gap-1 text-purple hover:underline bg-transparent border-none text-left cursor-pointer outline-none"
|
||||
@click="openPlusLink"
|
||||
>
|
||||
<span>
|
||||
Support creators and Modrinth ad-free with
|
||||
<span class="font-bold">Modrinth+</span>
|
||||
</span>
|
||||
<ChevronRightIcon class="relative top-[3px] h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="card-shadow p-4 bg-bg-raised rounded-xl flex gap-3 group cursor-pointer hover:brightness-90 transition-all"
|
||||
class="card-shadow button-base p-4 bg-bg-raised rounded-xl flex gap-3 group"
|
||||
@click="
|
||||
() => {
|
||||
emit('open')
|
||||
@@ -12,7 +12,21 @@
|
||||
"
|
||||
>
|
||||
<div class="icon w-[96px] h-[96px] relative">
|
||||
<Avatar :src="project.icon_url" size="96px" class="search-icon origin-top transition-all" />
|
||||
<Avatar
|
||||
:src="project.icon_url"
|
||||
size="96px"
|
||||
class="search-icon origin-top transition-all"
|
||||
:class="{ 'scale-[0.85]': installed, 'brightness-50': installing }"
|
||||
/>
|
||||
<div v-if="installing" class="rounded-2xl absolute inset-0 flex items-center justify-center">
|
||||
<SpinnerIcon class="h-8 w-8 animate-spin" />
|
||||
</div>
|
||||
<div
|
||||
v-if="installed"
|
||||
class="absolute shadow-sm font-semibold bottom-0 w-full p-1 bg-button-bg rounded-full text-xs justify-center items-center flex gap-1 text-brand border-[1px] border-solid border-[--color-button-border]"
|
||||
>
|
||||
<CheckIcon class="shrink-0 stroke-[3px]" /> Installed
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 overflow-hidden">
|
||||
<div class="gap-2 overflow-hidden no-wrap text-ellipsis">
|
||||
@@ -26,42 +40,6 @@
|
||||
</div>
|
||||
<div v-if="categories.length > 0" class="mt-auto flex items-center gap-1 no-wrap">
|
||||
<TagsIcon class="h-4 w-4 shrink-0" />
|
||||
<div
|
||||
v-if="project.project_type === 'mod' || project.project_type === 'modpack'"
|
||||
class="text-sm font-semibold text-secondary flex gap-1 px-[0.375rem] py-0.5 bg-button-bg rounded-full"
|
||||
>
|
||||
<template v-if="project.client_side === 'optional' && project.server_side === 'optional'">
|
||||
Client or server
|
||||
</template>
|
||||
<template
|
||||
v-else-if="
|
||||
(project.client_side === 'optional' || project.client_side === 'required') &&
|
||||
(project.server_side === 'optional' || project.server_side === 'unsupported')
|
||||
"
|
||||
>
|
||||
Client
|
||||
</template>
|
||||
<template
|
||||
v-else-if="
|
||||
(project.server_side === 'optional' || project.server_side === 'required') &&
|
||||
(project.client_side === 'optional' || project.client_side === 'unsupported')
|
||||
"
|
||||
>
|
||||
Server
|
||||
</template>
|
||||
<template
|
||||
v-else-if="
|
||||
project.client_side === 'unsupported' && project.server_side === 'unsupported'
|
||||
"
|
||||
>
|
||||
Unsupported
|
||||
</template>
|
||||
<template
|
||||
v-else-if="project.client_side === 'required' && project.server_side === 'required'"
|
||||
>
|
||||
Client and server
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-for="tag in categories"
|
||||
:key="tag"
|
||||
@@ -87,8 +65,19 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-auto relative">
|
||||
<div class="absolute bottom-0 right-0 w-fit">
|
||||
<ButtonStyled color="brand" type="outlined">
|
||||
<div
|
||||
class="flex items-center gap-2 group-hover:-translate-y-3 group-hover:opacity-0 group-focus-within:opacity-0 group-hover:scale-95 group-focus-within:scale-95 transition-all"
|
||||
>
|
||||
<HistoryIcon class="shrink-0" />
|
||||
<span>
|
||||
<span class="text-secondary">Updated</span>
|
||||
{{ dayjs(project.date_modified ?? project.updated).fromNow() }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="opacity-0 scale-95 translate-y-3 group-hover:translate-y-0 group-hover:scale-100 group-hover:opacity-100 group-focus-within:opacity-100 group-focus-within:scale-100 absolute bottom-0 right-0 transition-all w-fit"
|
||||
>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
:disabled="installed || installing"
|
||||
class="shrink-0 no-wrap"
|
||||
@@ -117,7 +106,15 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { TagsIcon, DownloadIcon, HeartIcon, PlusIcon, CheckIcon } from '@modrinth/assets'
|
||||
import {
|
||||
SpinnerIcon,
|
||||
TagsIcon,
|
||||
DownloadIcon,
|
||||
HeartIcon,
|
||||
PlusIcon,
|
||||
CheckIcon,
|
||||
HistoryIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { ButtonStyled, Avatar } from '@modrinth/ui'
|
||||
import { formatNumber, formatCategory } from '@modrinth/utils'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
@@ -20,7 +20,7 @@ import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
credentials: unknown | null
|
||||
signIn: () => void
|
||||
signIn: () => void2
|
||||
}>()
|
||||
|
||||
const userCredentials = computed(() => props.credentials)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { DownloadIcon, XIcon } from '@modrinth/assets'
|
||||
import { XIcon, DownloadIcon } from '@modrinth/assets'
|
||||
import { Button } from '@modrinth/ui'
|
||||
import { create_profile_and_install as pack_install } from '@/helpers/pack'
|
||||
import { install as pack_install } from '@/helpers/pack'
|
||||
import { ref } from 'vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { handleError } from '@/store/state.js'
|
||||
|
||||
@@ -7,7 +7,7 @@ import { invoke } from '@tauri-apps/api/core'
|
||||
import { create } from './profile'
|
||||
|
||||
// Installs pack from a version ID
|
||||
export async function create_profile_and_install(projectId, versionId, packTitle, iconUrl) {
|
||||
export async function install(projectId, versionId, packTitle, iconUrl) {
|
||||
const location = {
|
||||
type: 'fromVersionId',
|
||||
project_id: projectId,
|
||||
@@ -28,18 +28,8 @@ export async function create_profile_and_install(projectId, versionId, packTitle
|
||||
return await invoke('plugin:pack|pack_install', { location, profile })
|
||||
}
|
||||
|
||||
export async function install_to_existing_profile(projectId, versionId, title, profilePath) {
|
||||
const location = {
|
||||
type: 'fromVersionId',
|
||||
project_id: projectId,
|
||||
version_id: versionId,
|
||||
title,
|
||||
}
|
||||
return await invoke('plugin:pack|pack_install', { location, profile: profilePath })
|
||||
}
|
||||
|
||||
// Installs pack from a path
|
||||
export async function create_profile_and_install_from_file(path) {
|
||||
export async function install_from_file(path) {
|
||||
const location = {
|
||||
type: 'fromFile',
|
||||
path: path,
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
* and deserialized into a usable JS object.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { install_to_existing_profile } from '@/helpers/pack.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
|
||||
/// Add instance
|
||||
/*
|
||||
@@ -188,17 +186,3 @@ export async function edit(path, editProfile) {
|
||||
export async function edit_icon(path, iconPath) {
|
||||
return await invoke('plugin:profile|profile_edit_icon', { path, iconPath })
|
||||
}
|
||||
|
||||
export async function finish_install(instance) {
|
||||
if (instance.install_stage !== 'pack_installed') {
|
||||
let linkedData = instance.linked_data
|
||||
await install_to_existing_profile(
|
||||
linkedData.project_id,
|
||||
linkedData.version_id,
|
||||
instance.name,
|
||||
instance.path,
|
||||
).catch(handleError)
|
||||
} else {
|
||||
await install(instance.path, false).catch(handleError)
|
||||
}
|
||||
}
|
||||
|
||||
7
apps/app-frontend/src/helpers/types.d.ts
vendored
7
apps/app-frontend/src/helpers/types.d.ts
vendored
@@ -32,12 +32,7 @@ type GameInstance = {
|
||||
hooks: Hooks
|
||||
}
|
||||
|
||||
type InstallStage =
|
||||
| 'installed'
|
||||
| 'minecraft_installing'
|
||||
| 'pack_installed'
|
||||
| 'pack_installing'
|
||||
| 'not_installed'
|
||||
type InstallStage = 'installed' | 'installing' | 'pack_installing' | 'not_installed'
|
||||
|
||||
type LinkedData = {
|
||||
project_id: ModrinthId
|
||||
|
||||
@@ -356,6 +356,12 @@ const messages = defineMessages({
|
||||
const options = ref(null)
|
||||
const handleRightClick = (event, result) => {
|
||||
options.value.showMenu(event, result, [
|
||||
{
|
||||
name: 'install',
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
name: 'open_link',
|
||||
},
|
||||
|
||||
@@ -30,23 +30,9 @@
|
||||
</template>
|
||||
<template #actions>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled
|
||||
v-if="instance.install_stage.includes('installing')"
|
||||
color="brand"
|
||||
size="large"
|
||||
>
|
||||
<ButtonStyled v-if="instance.install_stage !== 'installed'" color="brand" size="large">
|
||||
<button disabled>Installing...</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-else-if="instance.install_stage !== 'installed'"
|
||||
color="brand"
|
||||
size="large"
|
||||
>
|
||||
<button @click="repairInstance()">
|
||||
<DownloadIcon />
|
||||
Repair
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else-if="playing === true" color="red" size="large">
|
||||
<button @click="stopInstance('InstancePage')">
|
||||
<StopCircleIcon />
|
||||
@@ -151,39 +137,38 @@
|
||||
<script setup>
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
ContentPageHeader,
|
||||
LoadingIndicator,
|
||||
ButtonStyled,
|
||||
OverflowMenu,
|
||||
LoadingIndicator,
|
||||
} from '@modrinth/ui'
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ClipboardCopyIcon,
|
||||
DownloadIcon,
|
||||
EditIcon,
|
||||
ExternalIcon,
|
||||
EyeIcon,
|
||||
FolderOpenIcon,
|
||||
GameIcon,
|
||||
GlobeIcon,
|
||||
HashIcon,
|
||||
MoreVerticalIcon,
|
||||
PackageIcon,
|
||||
PlayIcon,
|
||||
PlusIcon,
|
||||
ServerIcon,
|
||||
SettingsIcon,
|
||||
StopCircleIcon,
|
||||
TimerIcon,
|
||||
UpdatedIcon,
|
||||
UserPlusIcon,
|
||||
ServerIcon,
|
||||
PackageIcon,
|
||||
SettingsIcon,
|
||||
PlayIcon,
|
||||
StopCircleIcon,
|
||||
EditIcon,
|
||||
FolderOpenIcon,
|
||||
ClipboardCopyIcon,
|
||||
PlusIcon,
|
||||
ExternalIcon,
|
||||
HashIcon,
|
||||
GlobeIcon,
|
||||
EyeIcon,
|
||||
XIcon,
|
||||
CheckCircleIcon,
|
||||
UpdatedIcon,
|
||||
MoreVerticalIcon,
|
||||
GameIcon,
|
||||
TimerIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { finish_install, get, get_full_path, kill, run } from '@/helpers/profile'
|
||||
import { get, get_full_path, kill, run } from '@/helpers/profile'
|
||||
import { get_by_profile_path } from '@/helpers/process'
|
||||
import { process_listener, profile_listener } from '@/helpers/events'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { ref, onUnmounted, computed, watch } from 'vue'
|
||||
import { handleError, useBreadcrumbs, useLoading } from '@/store/state'
|
||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
@@ -309,10 +294,6 @@ const stopInstance = async (context) => {
|
||||
})
|
||||
}
|
||||
|
||||
const repairInstance = async () => {
|
||||
await finish_install(instance.value)
|
||||
}
|
||||
|
||||
const handleRightClick = (event) => {
|
||||
const baseOptions = [
|
||||
{ name: 'add_content' },
|
||||
|
||||
@@ -176,18 +176,15 @@
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<div v-else class="w-[36px]"></div>
|
||||
<Toggle
|
||||
class="!mx-2"
|
||||
:model-value="!item.data.disabled"
|
||||
:checked="!item.data.disabled"
|
||||
@update:model-value="toggleDisableMod(item.data)"
|
||||
/>
|
||||
<ButtonStyled type="transparent" circular>
|
||||
<button v-tooltip="'Remove'" @click="removeMod(item)">
|
||||
<TrashIcon />
|
||||
<button
|
||||
v-tooltip="item.disabled ? `Enable` : `Disable`"
|
||||
@click="toggleDisableMod(item.data)"
|
||||
>
|
||||
<CheckCircleIcon v-if="item.disabled" />
|
||||
<SlashIcon v-else />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<ButtonStyled type="transparent" circular>
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
@@ -200,12 +197,23 @@
|
||||
shown: item.data !== undefined && item.data.slug !== undefined,
|
||||
action: () => copyModLink(item),
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
id: 'remove',
|
||||
color: 'red',
|
||||
action: () => removeMod(item),
|
||||
},
|
||||
]"
|
||||
direction="left"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
<template #show-file> <ExternalIcon /> Show file </template>
|
||||
<template #copy-link> <ClipboardCopyIcon /> Copy link </template>
|
||||
<template v-if="item.disabled" #toggle> <CheckCircleIcon /> Enable </template>
|
||||
<template v-else #toggle> <SlashIcon /> Disable </template>
|
||||
<template #remove> <TrashIcon /> Remove </template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
@@ -267,14 +275,7 @@ import {
|
||||
UpdatedIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Button,
|
||||
ButtonStyled,
|
||||
ContentListPanel,
|
||||
OverflowMenu,
|
||||
Pagination,
|
||||
Toggle,
|
||||
} from '@modrinth/ui'
|
||||
import { Button, ButtonStyled, ContentListPanel, OverflowMenu, Pagination } from '@modrinth/ui'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
import type { ComputedRef } from 'vue'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
@@ -461,10 +462,6 @@ const messages = defineMessages({
|
||||
id: 'instance.filter.updates-available',
|
||||
defaultMessage: 'Updates available',
|
||||
},
|
||||
disabledFilter: {
|
||||
id: 'instance.filter.disabled',
|
||||
defaultMessage: 'Disabled projects',
|
||||
},
|
||||
})
|
||||
|
||||
const filterOptions: ComputedRef<FilterOption[]> = computed(() => {
|
||||
@@ -491,30 +488,19 @@ const filterOptions: ComputedRef<FilterOption[]> = computed(() => {
|
||||
})
|
||||
}
|
||||
|
||||
if (projects.value.some((m) => m.disabled)) {
|
||||
options.push({
|
||||
id: 'disabled',
|
||||
formattedName: formatMessage(messages.disabledFilter),
|
||||
})
|
||||
}
|
||||
|
||||
return options
|
||||
})
|
||||
|
||||
const selectedFilters = ref([])
|
||||
const filteredProjects = computed(() => {
|
||||
const updatesFilter = selectedFilters.value.includes('updates')
|
||||
const disabledFilter = selectedFilters.value.includes('disabled')
|
||||
|
||||
const typeFilters = selectedFilters.value.filter(
|
||||
(filter) => filter !== 'updates' && filter !== 'disabled',
|
||||
)
|
||||
const typeFilters = selectedFilters.value.filter((filter) => filter !== 'updates')
|
||||
|
||||
return projects.value.filter((project) => {
|
||||
return (
|
||||
(typeFilters.length === 0 || typeFilters.includes(project.project_type)) &&
|
||||
(!updatesFilter || project.outdated) &&
|
||||
(!disabledFilter || project.disabled)
|
||||
(!updatesFilter || project.outdated)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,14 +2,14 @@ import { defineStore } from 'pinia'
|
||||
import {
|
||||
add_project_from_version,
|
||||
check_installed,
|
||||
list,
|
||||
get,
|
||||
get_projects,
|
||||
list,
|
||||
remove_project,
|
||||
} from '@/helpers/profile.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import { get_project, get_version_many } from '@/helpers/cache.js'
|
||||
import { create_profile_and_install as packInstall } from '@/helpers/pack.js'
|
||||
import { install as packInstall } from '@/helpers/pack.js'
|
||||
import { trackEvent } from '@/helpers/analytics.js'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "theseus_gui"
|
||||
version = "0.9.2"
|
||||
version = "0.9.0"
|
||||
description = "The Modrinth App is a desktop application for managing your Minecraft mods"
|
||||
license = "GPL-3.0-only"
|
||||
repository = "https://github.com/modrinth/code/apps/app/"
|
||||
|
||||
@@ -21,86 +21,3 @@ document.addEventListener(
|
||||
window.open = (url, target, features) => {
|
||||
window.top.postMessage({ modrinthOpenUrl: url }, 'https://modrinth.com')
|
||||
}
|
||||
|
||||
function muteAudioContext() {
|
||||
if (window.AudioContext || window.webkitAudioContext) {
|
||||
const AudioContext = window.AudioContext || window.webkitAudioContext
|
||||
const originalCreateMediaElementSource = AudioContext.prototype.createMediaElementSource
|
||||
const originalCreateMediaStreamSource = AudioContext.prototype.createMediaStreamSource
|
||||
const originalCreateMediaStreamTrackSource = AudioContext.prototype.createMediaStreamTrackSource
|
||||
const originalCreateBufferSource = AudioContext.prototype.createBufferSource
|
||||
const originalCreateOscillator = AudioContext.prototype.createOscillator
|
||||
|
||||
AudioContext.prototype.createGain = function () {
|
||||
const gain = originalCreateGain.call(this)
|
||||
gain.gain.value = 0
|
||||
return gain
|
||||
}
|
||||
|
||||
AudioContext.prototype.createMediaElementSource = function (mediaElement) {
|
||||
const source = originalCreateMediaElementSource.call(this, mediaElement)
|
||||
source.connect(this.createGain())
|
||||
return source
|
||||
}
|
||||
|
||||
AudioContext.prototype.createMediaStreamSource = function (mediaStream) {
|
||||
const source = originalCreateMediaStreamSource.call(this, mediaStream)
|
||||
source.connect(this.createGain())
|
||||
return source
|
||||
}
|
||||
|
||||
AudioContext.prototype.createMediaStreamTrackSource = function (mediaStreamTrack) {
|
||||
const source = originalCreateMediaStreamTrackSource.call(this, mediaStreamTrack)
|
||||
source.connect(this.createGain())
|
||||
return source
|
||||
}
|
||||
|
||||
AudioContext.prototype.createBufferSource = function () {
|
||||
const source = originalCreateBufferSource.call(this)
|
||||
source.connect(this.createGain())
|
||||
return source
|
||||
}
|
||||
|
||||
AudioContext.prototype.createOscillator = function () {
|
||||
const oscillator = originalCreateOscillator.call(this)
|
||||
oscillator.connect(this.createGain())
|
||||
return oscillator
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function muteVideo(mediaElement) {
|
||||
let count = Number(mediaElement.getAttribute('data-modrinth-muted-count') ?? 0)
|
||||
|
||||
if (!mediaElement.muted || mediaElement.volume !== 0) {
|
||||
mediaElement.muted = true
|
||||
mediaElement.volume = 0
|
||||
|
||||
mediaElement.setAttribute('data-modrinth-muted-count', count + 1)
|
||||
}
|
||||
|
||||
if (count > 5) {
|
||||
// Video is detected as malicious, so it is removed from the page
|
||||
mediaElement.remove()
|
||||
}
|
||||
}
|
||||
|
||||
function muteVideos() {
|
||||
document.querySelectorAll('video, audio').forEach(function (mediaElement) {
|
||||
muteVideo(mediaElement)
|
||||
|
||||
if (!mediaElement.hasAttribute('data-modrinth-muted')) {
|
||||
mediaElement.addEventListener('volumechange', () => muteVideo(mediaElement))
|
||||
|
||||
mediaElement.setAttribute('data-modrinth-muted', 'true')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
muteVideos()
|
||||
muteAudioContext()
|
||||
|
||||
const observer = new MutationObserver(muteVideos)
|
||||
observer.observe(document.body, { childList: true, subtree: true })
|
||||
})
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
)]
|
||||
|
||||
use native_dialog::{MessageDialog, MessageType};
|
||||
use std::env;
|
||||
use tauri::{Listener, Manager};
|
||||
use theseus::prelude::*;
|
||||
|
||||
@@ -30,12 +29,7 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
|
||||
theseus::EventState::init(app.clone()).await?;
|
||||
|
||||
#[cfg(feature = "updater")]
|
||||
'updater: {
|
||||
if env::var("MODRINTH_EXTERNAL_UPDATE_PROVIDER").is_ok() {
|
||||
State::init().await?;
|
||||
break 'updater;
|
||||
}
|
||||
|
||||
{
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
|
||||
let updater = app.updater_builder().build()?;
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
{
|
||||
"bundle": {
|
||||
"createUpdaterArtifacts": "v1Compatible",
|
||||
"windows": {
|
||||
"certificateThumbprint": "6007d6ed9b1ba057aa29144d58ca3ed89ac4ef94",
|
||||
"signCommand": "bash -c \"jsign.bat --storetype ETOKEN --alg SHA-256 --storepass $DIGICERT_PASSWORD --tsurl https://timestamp.digicert.com %1\""
|
||||
}
|
||||
"createUpdaterArtifacts": "v1Compatible"
|
||||
},
|
||||
"build": {
|
||||
"features": ["updater"]
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
]
|
||||
},
|
||||
"productName": "Modrinth App",
|
||||
"version": "0.9.2",
|
||||
"version": "0.9.0",
|
||||
"mainBinaryName": "Modrinth App",
|
||||
"identifier": "ModrinthApp",
|
||||
"plugins": {
|
||||
|
||||
@@ -22,10 +22,10 @@ import { ChevronRightIcon } from "@modrinth/assets";
|
||||
|
||||
useHead({
|
||||
script: [
|
||||
// {
|
||||
// // Clean.io
|
||||
// src: "https://cadmus.script.ac/d14pdm1b7fi5kh/script.js",
|
||||
// },
|
||||
{
|
||||
// Clean.io
|
||||
src: "https://cadmus.script.ac/d14pdm1b7fi5kh/script.js",
|
||||
},
|
||||
{
|
||||
// Aditude
|
||||
src: "https://dn0qt3r0xannq.cloudfront.net/modrinth-7JfmkEIXEp/modrinth-longform/prebid-load.js",
|
||||
|
||||
@@ -411,21 +411,18 @@ Per section 5.2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#m
|
||||
name: "Insufficient",
|
||||
resultingMessage: `## Insufficient Summary
|
||||
Per section 5.3 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) Your project summary should provide a brief overview of your project that informs and entices users.
|
||||
|
||||
This is the first thing most people will see about your mod other than the Logo, so it's important it be accurate, reasonably detailed, and exciting.`,
|
||||
},
|
||||
{
|
||||
name: "Repeat of title",
|
||||
resultingMessage: `## Insufficient Summary
|
||||
Per section 5.3 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) your Summary can not be the same as your project's Title. Your project summary should provide a brief overview of your project that informs and entices users.
|
||||
|
||||
This is the first thing most people will see about your mod other than the Logo, so it's important it be accurate, reasonably detailed, and exciting.`,
|
||||
},
|
||||
{
|
||||
name: "Formatting",
|
||||
resultingMessage: `## Insufficient Summary
|
||||
Per section 5.3 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) your Summary can not include any extra formatting such as lists, or links. Your project summary should provide a brief overview of your project that informs and entices users.
|
||||
|
||||
This is the first thing most people will see about your mod other than the Logo, so it's important it be accurate, reasonably detailed, and exciting.`,
|
||||
},
|
||||
],
|
||||
@@ -562,9 +559,7 @@ Per section 5.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#m
|
||||
name: "Inaccurate (modpack)",
|
||||
resultingMessage: `## Incorrect Environment Information
|
||||
Per section 5.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous), it is important that the metadata of your projects is accurate, including whether the project runs on the client or server side.
|
||||
|
||||
For a brief rundown of how this works:
|
||||
|
||||
Some modpacks can be client-side, usually aimed at providing utility and optimization while allowing the player to join an unmodded server, for instance, [Fabulously Optimized](https://modrinth.com/modpack/fabulously-optimized).
|
||||
Most other modpacks that change how the game is played are going to be required on both the client and server, like the modpack [Dying Light](https://modrinth.com/modpack/dying-light).
|
||||
When in doubt, test for yourself or check the requirements of the mods in your pack.`,
|
||||
@@ -573,11 +568,10 @@ When in doubt, test for yourself or check the requirements of the mods in your p
|
||||
name: "Inaccurate (mod)",
|
||||
resultingMessage: `## Environment Information
|
||||
Per section 5.1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous), it is important that the metadata of your projects is accurate, including whether the project runs on the client or server side.
|
||||
|
||||
For a brief rundown of how this works:
|
||||
- **Client side** refers to a mod that is only required by the client, like [Sodium](https://modrinth.com/mod/sodium).
|
||||
- **Server side** mods change the behavior of the server without the client needing the mod, like Datapacks, recipes, or server-side behaviors, like [Falling Tree](https://modrinth.com/mod/fallingtree).
|
||||
- A mod that adds features, entities, or new blocks and items, generally will be required on **both** the server and the client, for example [Cobblemon](https://modrinth.com/mod/cobblemon).`,
|
||||
**Client side** refers to a mod that is only required by the client, like [Sodium](https://modrinth.com/mod/sodium).
|
||||
**Server side** mods change the behavior of the server without the client needing the mod, like Datapacks, recipes, or server-side behaviors, like [Falling Tree](https://modrinth.com/mod/fallingtree).
|
||||
A mod that adds features, entities, or new blocks and items, generally will be required on **both** the server and the client, for example [Cobblemon](https://modrinth.com/mod/cobblemon).`,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -608,7 +602,6 @@ Per section 5.5 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#m
|
||||
name: "Incorrect additional files",
|
||||
resultingMessage: `## Incorrect Use of Additional Files
|
||||
It looks like you've uploaded multiple \`mod.jar\` files to one Version as Additional Files. Per section 5.7 of [Modrinth's Content Rules](https://modrinth.com/legal/rules#miscellaneous) each Version of your project must include only one \`mod.jar\` that corresponds to its respective Minecraft and loader versions. This allows users to easily find and download the file they need for the version they're on with ease. The Additional Files feature can be used for things like a \`Sources.jar\`.
|
||||
|
||||
Please upload each version of your mod separately, thank you.`,
|
||||
},
|
||||
{
|
||||
@@ -636,9 +629,7 @@ It looks like you've selected loaders for your Resource Pack that are causing it
|
||||
name: "Re-upload",
|
||||
resultingMessage: `## Reuploads are forbidden
|
||||
This project appears to contain content from %ORIGINAL_PROJECT% by %ORIGINAL_AUTHOR%.
|
||||
|
||||
Per section 4 of [Modrinth's Content Rules](https://modrinth.com/legal/rules) this is strictly forbidden.
|
||||
|
||||
If you believe this is an error, or you can verify you are the creator and rightful owner of this content please let us know. Otherwise, we ask that you **do not resubmit this project**.`,
|
||||
fillers: [
|
||||
{
|
||||
@@ -856,7 +847,6 @@ async function generateMessage() {
|
||||
for (const mod of mods) {
|
||||
message.value += `- ${mod}\n`;
|
||||
}
|
||||
message.value += "\n";
|
||||
}
|
||||
|
||||
if (modPackData.value && modPackData.value.length > 0) {
|
||||
@@ -923,7 +913,7 @@ async function generateMessage() {
|
||||
permanentNoMods.length > 0 ||
|
||||
unidentifiedMods.length > 0
|
||||
) {
|
||||
message.value += "## Copyrighted content \n";
|
||||
message.value += "## Copyrighted Content \n";
|
||||
|
||||
printMods(
|
||||
attributeMods,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<CompactChart
|
||||
v-if="analytics.formattedData.value.downloads"
|
||||
ref="tinyDownloadChart"
|
||||
:title="`Downloads`"
|
||||
:title="`Downloads since ${dayjs(startDate).format('MMM D, YYYY')}`"
|
||||
color="var(--color-brand)"
|
||||
:value="formatNumber(analytics.formattedData.value.downloads.sum, false)"
|
||||
:data="analytics.formattedData.value.downloads.chart.sumData"
|
||||
@@ -33,7 +33,7 @@
|
||||
<CompactChart
|
||||
v-if="analytics.formattedData.value.views"
|
||||
ref="tinyViewChart"
|
||||
:title="`Views`"
|
||||
:title="`Page views since ${dayjs(startDate).format('MMM D, YYYY')}`"
|
||||
color="var(--color-blue)"
|
||||
:value="formatNumber(analytics.formattedData.value.views.sum, false)"
|
||||
:data="analytics.formattedData.value.views.chart.sumData"
|
||||
@@ -50,7 +50,7 @@
|
||||
<CompactChart
|
||||
v-if="analytics.formattedData.value.revenue"
|
||||
ref="tinyRevenueChart"
|
||||
:title="`Revenue`"
|
||||
:title="`Revenue since ${dayjs(startDate).format('MMM D, YYYY')}`"
|
||||
color="var(--color-purple)"
|
||||
:value="formatMoney(analytics.formattedData.value.revenue.sum, false)"
|
||||
:data="analytics.formattedData.value.revenue.chart.sumData"
|
||||
@@ -71,9 +71,6 @@
|
||||
<span class="label__title">
|
||||
{{ formatCategoryHeader(selectedChart) }}
|
||||
</span>
|
||||
<span class="label__subtitle">
|
||||
{{ formattedCategorySubtitle }}
|
||||
</span>
|
||||
</h2>
|
||||
<div class="chart-controls__buttons">
|
||||
<Button v-tooltip="'Toggle project colors'" icon-only @click="onToggleColors">
|
||||
@@ -86,12 +83,11 @@
|
||||
<UpdatedIcon />
|
||||
</Button>
|
||||
<DropdownSelect
|
||||
class="range-dropdown"
|
||||
v-model="selectedRange"
|
||||
:options="ranges"
|
||||
:options="selectableRanges"
|
||||
name="Time range"
|
||||
:display-name="
|
||||
(o: RangeObject) => o?.getLabel([startDate, endDate]) ?? 'Loading...'
|
||||
(o: (typeof selectableRanges)[number] | undefined) => o?.label || 'Custom'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
@@ -326,7 +322,7 @@ const props = withDefaults(
|
||||
* @deprecated Use `ranges` instead
|
||||
*/
|
||||
resoloutions?: Record<string, number>;
|
||||
ranges?: RangeObject[];
|
||||
ranges?: Record<number, [string, number] | string>;
|
||||
personal?: boolean;
|
||||
}>(),
|
||||
{
|
||||
@@ -339,6 +335,12 @@ const props = withDefaults(
|
||||
|
||||
const projects = ref(props.projects || []);
|
||||
|
||||
const selectableRanges = Object.entries(props.ranges).map(([duration, extra]) => ({
|
||||
label: typeof extra === "string" ? extra : extra[0],
|
||||
value: Number(duration),
|
||||
res: typeof extra === "string" ? Number(duration) : extra[1],
|
||||
}));
|
||||
|
||||
// const selectedChart = ref('downloads')
|
||||
const selectedChart = computed({
|
||||
get: () => {
|
||||
@@ -411,78 +413,33 @@ const isUsingProjectColors = computed({
|
||||
},
|
||||
});
|
||||
|
||||
const startDate = ref(dayjs().startOf("day"));
|
||||
const endDate = ref(dayjs().endOf("day"));
|
||||
const timeResolution = ref(30);
|
||||
|
||||
onBeforeMount(() => {
|
||||
// Load cached data and range from localStorage - cache.
|
||||
if (import.meta.client) {
|
||||
const rangeLabel = localStorage.getItem("analyticsSelectedRange");
|
||||
if (rangeLabel) {
|
||||
const range = props.ranges.find((r) => r.getLabel([dayjs(), dayjs()]) === rangeLabel)!;
|
||||
|
||||
if (range !== undefined) {
|
||||
internalRange.value = range;
|
||||
const ranges = range.getDates(dayjs());
|
||||
timeResolution.value = range.timeResolution;
|
||||
startDate.value = ranges.startDate;
|
||||
endDate.value = ranges.endDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (internalRange.value === null) {
|
||||
internalRange.value = props.ranges.find(
|
||||
(r) => r.getLabel([dayjs(), dayjs()]) === "Previous 30 days",
|
||||
)!;
|
||||
}
|
||||
|
||||
const ranges = selectedRange.value.getDates(dayjs());
|
||||
startDate.value = ranges.startDate;
|
||||
endDate.value = ranges.endDate;
|
||||
timeResolution.value = selectedRange.value.timeResolution;
|
||||
});
|
||||
|
||||
const internalRange: Ref<RangeObject> = ref(null as unknown as RangeObject);
|
||||
|
||||
const selectedRange = computed({
|
||||
get: () => {
|
||||
return internalRange.value;
|
||||
},
|
||||
set: (newRange) => {
|
||||
const ranges = newRange.getDates(dayjs());
|
||||
startDate.value = ranges.startDate;
|
||||
endDate.value = ranges.endDate;
|
||||
timeResolution.value = newRange.timeResolution;
|
||||
|
||||
internalRange.value = newRange;
|
||||
|
||||
if (import.meta.client) {
|
||||
localStorage.setItem(
|
||||
"analyticsSelectedRange",
|
||||
internalRange.value?.getLabel([dayjs(), dayjs()]) ?? "Previous 30 days",
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const analytics = useFetchAllAnalytics(
|
||||
resetCharts,
|
||||
projects,
|
||||
selectedDisplayProjects,
|
||||
props.personal,
|
||||
startDate,
|
||||
endDate,
|
||||
timeResolution,
|
||||
);
|
||||
|
||||
const formattedCategorySubtitle = computed(() => {
|
||||
return (
|
||||
selectedRange.value?.getLabel([dayjs(startDate.value), dayjs(endDate.value)]) ?? "Loading..."
|
||||
);
|
||||
const { startDate, endDate, timeRange, timeResolution } = analytics;
|
||||
|
||||
const selectedRange = computed({
|
||||
get: () => {
|
||||
return (
|
||||
selectableRanges.find((option) => option.value === timeRange.value) || {
|
||||
label: "Custom",
|
||||
value: timeRange.value,
|
||||
}
|
||||
);
|
||||
},
|
||||
set: (newRange: { label: string; value: number; res?: number }) => {
|
||||
timeRange.value = newRange.value;
|
||||
startDate.value = Date.now() - timeRange.value * 60 * 1000;
|
||||
endDate.value = Date.now();
|
||||
|
||||
if (newRange?.res) {
|
||||
timeResolution.value = newRange.res;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const selectedDataSet = computed(() => {
|
||||
@@ -527,9 +484,6 @@ const onToggleColors = () => {
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
/**
|
||||
* @deprecated Use `ranges` instead
|
||||
*/
|
||||
const defaultResoloutions: Record<string, number> = {
|
||||
"5 minutes": 5,
|
||||
"30 minutes": 30,
|
||||
@@ -539,169 +493,17 @@ const defaultResoloutions: Record<string, number> = {
|
||||
"A week": 10080,
|
||||
};
|
||||
|
||||
type DateRange = { startDate: dayjs.Dayjs; endDate: dayjs.Dayjs };
|
||||
|
||||
type RangeObject = {
|
||||
getLabel: (dateRange: [dayjs.Dayjs, dayjs.Dayjs]) => string;
|
||||
getDates: (currentDate: dayjs.Dayjs) => DateRange;
|
||||
// A time resolution in minutes.
|
||||
timeResolution: number;
|
||||
const defaultRanges: Record<number, [string, number] | string> = {
|
||||
30: ["Last 30 minutes", 1],
|
||||
60: ["Last hour", 5],
|
||||
720: ["Last 12 hours", 15],
|
||||
1440: ["Last day", 60],
|
||||
10080: ["Last week", 720],
|
||||
43200: ["Last month", 1440],
|
||||
129600: ["Last quarter", 10080],
|
||||
525600: ["Last year", 20160],
|
||||
1051200: ["Last two years", 40320],
|
||||
};
|
||||
|
||||
const defaultRanges: RangeObject[] = [
|
||||
{
|
||||
getLabel: () => "Previous 30 minutes",
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).subtract(30, "minute"),
|
||||
endDate: currentDate,
|
||||
}),
|
||||
timeResolution: 1,
|
||||
},
|
||||
{
|
||||
getLabel: () => "Previous hour",
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).subtract(1, "hour"),
|
||||
endDate: currentDate,
|
||||
}),
|
||||
timeResolution: 5,
|
||||
},
|
||||
{
|
||||
getLabel: () => "Previous 12 hours",
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).subtract(12, "hour"),
|
||||
endDate: currentDate,
|
||||
}),
|
||||
timeResolution: 12,
|
||||
},
|
||||
{
|
||||
getLabel: () => "Previous 24 hours",
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).subtract(1, "day"),
|
||||
endDate: currentDate,
|
||||
}),
|
||||
timeResolution: 30,
|
||||
},
|
||||
{
|
||||
getLabel: () => "Today",
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).startOf("day"),
|
||||
endDate: currentDate,
|
||||
}),
|
||||
timeResolution: 30,
|
||||
},
|
||||
{
|
||||
getLabel: () => "Yesterday",
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).subtract(1, "day").startOf("day"),
|
||||
endDate: dayjs(currentDate).startOf("day").subtract(1, "second"),
|
||||
}),
|
||||
timeResolution: 30,
|
||||
},
|
||||
{
|
||||
getLabel: () => "This week",
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).startOf("week").add(1, "hour"),
|
||||
endDate: currentDate,
|
||||
}),
|
||||
timeResolution: 360,
|
||||
},
|
||||
{
|
||||
getLabel: () => "Last week",
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).subtract(1, "week").startOf("week").add(1, "hour"),
|
||||
endDate: dayjs(currentDate).startOf("week").subtract(1, "second"),
|
||||
}),
|
||||
timeResolution: 1440,
|
||||
},
|
||||
{
|
||||
getLabel: () => "Previous 7 days",
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).startOf("day").subtract(7, "day").add(1, "hour"),
|
||||
endDate: currentDate.startOf("day"),
|
||||
}),
|
||||
timeResolution: 720,
|
||||
},
|
||||
{
|
||||
getLabel: () => "This month",
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).startOf("month").add(1, "hour"),
|
||||
endDate: currentDate,
|
||||
}),
|
||||
timeResolution: 1440,
|
||||
},
|
||||
{
|
||||
getLabel: () => "Last month",
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).subtract(1, "month").startOf("month").add(1, "hour"),
|
||||
endDate: dayjs(currentDate).startOf("month").subtract(1, "second"),
|
||||
}),
|
||||
timeResolution: 1440,
|
||||
},
|
||||
{
|
||||
getLabel: () => "Previous 30 days",
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).startOf("day").subtract(30, "day").add(1, "hour"),
|
||||
endDate: currentDate.startOf("day"),
|
||||
}),
|
||||
timeResolution: 1440,
|
||||
},
|
||||
{
|
||||
getLabel: () => "This quarter",
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).startOf("quarter").add(1, "hour"),
|
||||
endDate: currentDate,
|
||||
}),
|
||||
timeResolution: 1440,
|
||||
},
|
||||
{
|
||||
getLabel: () => "Last quarter",
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).subtract(1, "quarter").startOf("quarter").add(1, "hour"),
|
||||
endDate: dayjs(currentDate).startOf("quarter").subtract(1, "second"),
|
||||
}),
|
||||
timeResolution: 1440,
|
||||
},
|
||||
{
|
||||
getLabel: () => "This year",
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).startOf("year"),
|
||||
endDate: currentDate,
|
||||
}),
|
||||
timeResolution: 20160,
|
||||
},
|
||||
{
|
||||
getLabel: () => "Last year",
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).subtract(1, "year").startOf("year"),
|
||||
endDate: dayjs(currentDate).startOf("year").subtract(1, "second"),
|
||||
}),
|
||||
timeResolution: 20160,
|
||||
},
|
||||
{
|
||||
getLabel: () => "Previous year",
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).subtract(1, "year"),
|
||||
endDate: dayjs(currentDate),
|
||||
}),
|
||||
timeResolution: 40320,
|
||||
},
|
||||
{
|
||||
getLabel: () => "Previous two years",
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(currentDate).subtract(2, "year"),
|
||||
endDate: currentDate,
|
||||
}),
|
||||
timeResolution: 40320,
|
||||
},
|
||||
{
|
||||
getLabel: () => "All Time",
|
||||
getDates: (currentDate: dayjs.Dayjs) => ({
|
||||
startDate: dayjs(0),
|
||||
endDate: currentDate,
|
||||
}),
|
||||
timeResolution: 40320,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -722,20 +524,6 @@ const defaultRanges: RangeObject[] = [
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.label__subtitle {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.range-dropdown {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.chart-area {
|
||||
@@ -900,7 +688,6 @@ const defaultRanges: RangeObject[] = [
|
||||
flex-direction: column;
|
||||
gap: var(--gap-xs);
|
||||
}
|
||||
|
||||
.percentage-bar {
|
||||
grid-area: bar;
|
||||
width: 100%;
|
||||
@@ -909,7 +696,6 @@ const defaultRanges: RangeObject[] = [
|
||||
border: 1px solid var(--color-button-bg);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
|
||||
@@ -35,7 +35,7 @@ defineProps({
|
||||
const viewMode = ref("open");
|
||||
const reports = ref([]);
|
||||
|
||||
let { data: rawReports } = await useAsyncData("report", () => useBaseFetch("report?count=1000"));
|
||||
let { data: rawReports } = await useAsyncData("report", () => useBaseFetch("report"));
|
||||
|
||||
rawReports = rawReports.value.map((report) => {
|
||||
report.item_id = report.item_id.replace(/"/g, "");
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="font-semibold text-contrast">Auto backup</div>
|
||||
<p class="m-0">
|
||||
Automatically create a backup of your server
|
||||
<strong>{{ backupIntervalsLabel.toLowerCase() }}</strong>
|
||||
Automatically create a backup of your server every
|
||||
<strong>{{ autoBackupInterval == 1 ? "hour" : `${autoBackupInterval} hours` }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -22,19 +22,54 @@
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="font-semibold text-contrast">Interval</div>
|
||||
<p class="m-0">
|
||||
The amount of time between each backup. This will only backup your server if it has been
|
||||
modified since the last backup.
|
||||
The amount of hours between each backup. This will only backup your server if it has
|
||||
been modified since the last backup.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<UiServersTeleportDropdownMenu
|
||||
:id="'interval-field'"
|
||||
v-model="backupIntervalsLabel"
|
||||
:disabled="!autoBackupEnabled || isSaving"
|
||||
name="interval"
|
||||
:options="Object.keys(backupIntervals)"
|
||||
placeholder="Backup interval"
|
||||
/>
|
||||
<div class="flex items-center gap-2 text-contrast">
|
||||
<div
|
||||
class="flex w-fit items-center rounded-xl border border-solid border-button-border bg-table-alternateRow"
|
||||
>
|
||||
<button
|
||||
class="rounded-l-xl p-3 text-secondary enabled:hover:text-contrast [&&]:bg-transparent enabled:[&&]:hover:bg-button-bg"
|
||||
:disabled="!autoBackupEnabled || isSaving"
|
||||
@click="autoBackupInterval = Math.max(autoBackupInterval - 1, 1)"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="2" viewBox="-2 0 18 2">
|
||||
<path
|
||||
d="M18,12H6"
|
||||
transform="translate(-5 -11)"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<input
|
||||
id="auto-backup-interval"
|
||||
v-model="autoBackupInterval"
|
||||
class="w-16 !appearance-none text-center [&&]:bg-transparent [&&]:focus:shadow-none"
|
||||
type="number"
|
||||
style="-moz-appearance: textfield; appearance: none"
|
||||
min="1"
|
||||
max="24"
|
||||
step="1"
|
||||
:disabled="!autoBackupEnabled || isSaving"
|
||||
/>
|
||||
|
||||
<button
|
||||
class="rounded-r-xl p-3 text-secondary enabled:hover:text-contrast [&&]:bg-transparent enabled:[&&]:hover:bg-button-bg"
|
||||
:disabled="!autoBackupEnabled || isSaving"
|
||||
@click="autoBackupInterval = Math.min(autoBackupInterval + 1, 24)"
|
||||
>
|
||||
<PlusIcon />
|
||||
</button>
|
||||
</div>
|
||||
{{ autoBackupInterval == 1 ? "hour" : "hours" }}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-start gap-4">
|
||||
<ButtonStyled color="brand">
|
||||
@@ -57,7 +92,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { XIcon, SaveIcon } from "@modrinth/assets";
|
||||
import { PlusIcon, XIcon, SaveIcon } from "@modrinth/assets";
|
||||
import { ref, computed } from "vue";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
|
||||
@@ -69,25 +104,19 @@ const modal = ref<InstanceType<typeof NewModal>>();
|
||||
|
||||
const initialSettings = ref<{ interval: number; enabled: boolean } | null>(null);
|
||||
const autoBackupEnabled = ref(false);
|
||||
const autoBackupInterval = ref(6);
|
||||
const isLoadingSettings = ref(true);
|
||||
const isSaving = ref(false);
|
||||
|
||||
const backupIntervals = {
|
||||
"Every 3 hours": 3,
|
||||
"Every 6 hours": 6,
|
||||
"Every 12 hours": 12,
|
||||
Daily: 24,
|
||||
};
|
||||
const validatedBackupInterval = computed(() => {
|
||||
const roundedValue = Math.round(autoBackupInterval.value);
|
||||
|
||||
const backupIntervalsLabel = ref<keyof typeof backupIntervals>("Every 6 hours");
|
||||
|
||||
const autoBackupInterval = computed({
|
||||
get: () => backupIntervals[backupIntervalsLabel.value],
|
||||
set: (value) => {
|
||||
const [label] =
|
||||
Object.entries(backupIntervals).find(([_, interval]) => interval === value) || [];
|
||||
if (label) backupIntervalsLabel.value = label as keyof typeof backupIntervals;
|
||||
},
|
||||
if (roundedValue < 1) {
|
||||
return 1;
|
||||
} else if (roundedValue > 24) {
|
||||
return 24;
|
||||
}
|
||||
return roundedValue;
|
||||
});
|
||||
|
||||
const hasChanges = computed(() => {
|
||||
@@ -95,7 +124,7 @@ const hasChanges = computed(() => {
|
||||
|
||||
return (
|
||||
autoBackupEnabled.value !== initialSettings.value.enabled ||
|
||||
(initialSettings.value.enabled && autoBackupInterval.value !== initialSettings.value.interval)
|
||||
autoBackupInterval.value !== initialSettings.value.interval
|
||||
);
|
||||
});
|
||||
|
||||
@@ -153,6 +182,10 @@ const saveSettings = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
watch(autoBackupInterval, () => {
|
||||
autoBackupInterval.value = validatedBackupInterval.value;
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
show: async () => {
|
||||
await fetchSettings();
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
@dragenter.prevent="handleDragEnter"
|
||||
@dragover.prevent="handleDragOver"
|
||||
@dragleave.prevent="handleDragLeave"
|
||||
@drop.prevent="handleDrop"
|
||||
>
|
||||
<slot />
|
||||
<div
|
||||
v-if="isDragging"
|
||||
:class="[
|
||||
'absolute inset-0 flex items-center justify-center rounded-2xl bg-black bg-opacity-50 text-white',
|
||||
overlayClass,
|
||||
]"
|
||||
>
|
||||
<div class="text-center">
|
||||
<UploadIcon class="mx-auto h-16 w-16" />
|
||||
<p class="mt-2 text-xl">
|
||||
Drop {{ type ? type.toLocaleLowerCase() : "file" }}s here to upload
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { UploadIcon } from "@modrinth/assets";
|
||||
import { ref } from "vue";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "filesDropped", files: File[]): void;
|
||||
}>();
|
||||
|
||||
defineProps<{
|
||||
overlayClass?: string;
|
||||
type?: string;
|
||||
}>();
|
||||
|
||||
const isDragging = ref(false);
|
||||
const dragCounter = ref(0);
|
||||
|
||||
const handleDragEnter = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
if (!event.dataTransfer?.types.includes("application/pyro-file-move")) {
|
||||
dragCounter.value++;
|
||||
isDragging.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const handleDragLeave = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
dragCounter.value--;
|
||||
if (dragCounter.value === 0) {
|
||||
isDragging.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
isDragging.value = false;
|
||||
dragCounter.value = 0;
|
||||
|
||||
const isInternalMove = event.dataTransfer?.types.includes("application/pyro-file-move");
|
||||
if (isInternalMove) return;
|
||||
|
||||
const files = event.dataTransfer?.files;
|
||||
if (files) {
|
||||
emit("filesDropped", Array.from(files));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,306 +0,0 @@
|
||||
<template>
|
||||
<Transition name="upload-status" @enter="onUploadStatusEnter" @leave="onUploadStatusLeave">
|
||||
<div v-show="isUploading" ref="uploadStatusRef" class="upload-status">
|
||||
<div
|
||||
ref="statusContentRef"
|
||||
:class="['flex flex-col p-4 text-sm text-contrast', $attrs.class]"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2 font-bold">
|
||||
<FolderOpenIcon class="size-4" />
|
||||
<span>
|
||||
<span class="capitalize">
|
||||
{{ props.fileType ? props.fileType : "File" }} Uploads
|
||||
</span>
|
||||
<span>{{ activeUploads.length > 0 ? ` - ${activeUploads.length} left` : "" }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 space-y-2">
|
||||
<div
|
||||
v-for="item in uploadQueue"
|
||||
:key="item.file.name"
|
||||
class="flex h-6 items-center justify-between gap-2 text-xs"
|
||||
>
|
||||
<div class="flex flex-1 items-center gap-2 truncate">
|
||||
<transition-group name="status-icon" mode="out-in">
|
||||
<UiServersPanelSpinner
|
||||
v-show="item.status === 'uploading'"
|
||||
key="spinner"
|
||||
class="absolute !size-4"
|
||||
/>
|
||||
<CheckCircleIcon
|
||||
v-show="item.status === 'completed'"
|
||||
key="check"
|
||||
class="absolute size-4 text-green"
|
||||
/>
|
||||
<XCircleIcon
|
||||
v-show="
|
||||
item.status === 'error' ||
|
||||
item.status === 'cancelled' ||
|
||||
item.status === 'incorrect-type'
|
||||
"
|
||||
key="error"
|
||||
class="absolute size-4 text-red"
|
||||
/>
|
||||
</transition-group>
|
||||
<span class="ml-6 truncate">{{ item.file.name }}</span>
|
||||
<span class="text-secondary">{{ item.size }}</span>
|
||||
</div>
|
||||
<div class="flex min-w-[80px] items-center justify-end gap-2">
|
||||
<template v-if="item.status === 'completed'">
|
||||
<span>Done</span>
|
||||
</template>
|
||||
<template v-else-if="item.status === 'error'">
|
||||
<span class="text-red">Failed - File already exists</span>
|
||||
</template>
|
||||
<template v-else-if="item.status === 'incorrect-type'">
|
||||
<span class="text-red">Failed - Incorrect file type</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="item.status === 'uploading'">
|
||||
<span>{{ item.progress }}%</span>
|
||||
<div class="h-1 w-20 overflow-hidden rounded-full bg-bg">
|
||||
<div
|
||||
class="h-full bg-contrast transition-all duration-200"
|
||||
:style="{ width: item.progress + '%' }"
|
||||
/>
|
||||
</div>
|
||||
<ButtonStyled color="red" type="transparent" @click="cancelUpload(item)">
|
||||
<button>Cancel</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<template v-else-if="item.status === 'cancelled'">
|
||||
<span class="text-red">Cancelled</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>{{ item.progress }}%</span>
|
||||
<div class="h-1 w-20 overflow-hidden rounded-full bg-bg">
|
||||
<div
|
||||
class="h-full bg-contrast transition-all duration-200"
|
||||
:style="{ width: item.progress + '%' }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FolderOpenIcon, CheckCircleIcon, XCircleIcon } from "@modrinth/assets";
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import { ref, computed, watch, nextTick } from "vue";
|
||||
|
||||
interface UploadItem {
|
||||
file: File;
|
||||
progress: number;
|
||||
status: "pending" | "uploading" | "completed" | "error" | "cancelled" | "incorrect-type";
|
||||
size: string;
|
||||
uploader?: any;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
currentPath: string;
|
||||
fileType?: string;
|
||||
marginBottom?: number;
|
||||
acceptedTypes?: Array<string>;
|
||||
fs: FSModule;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "uploadComplete"): void;
|
||||
}>();
|
||||
|
||||
const uploadStatusRef = ref<HTMLElement | null>(null);
|
||||
const statusContentRef = ref<HTMLElement | null>(null);
|
||||
const uploadQueue = ref<UploadItem[]>([]);
|
||||
|
||||
const isUploading = computed(() => uploadQueue.value.length > 0);
|
||||
const activeUploads = computed(() =>
|
||||
uploadQueue.value.filter((item) => item.status === "pending" || item.status === "uploading"),
|
||||
);
|
||||
|
||||
const onUploadStatusEnter = (el: Element) => {
|
||||
const height = (el as HTMLElement).scrollHeight + (props.marginBottom || 0);
|
||||
(el as HTMLElement).style.height = "0";
|
||||
// eslint-disable-next-line no-void
|
||||
void (el as HTMLElement).offsetHeight;
|
||||
(el as HTMLElement).style.height = `${height}px`;
|
||||
};
|
||||
|
||||
const onUploadStatusLeave = (el: Element) => {
|
||||
const height = (el as HTMLElement).scrollHeight + (props.marginBottom || 0);
|
||||
(el as HTMLElement).style.height = `${height}px`;
|
||||
// eslint-disable-next-line no-void
|
||||
void (el as HTMLElement).offsetHeight;
|
||||
(el as HTMLElement).style.height = "0";
|
||||
};
|
||||
|
||||
watch(
|
||||
uploadQueue,
|
||||
() => {
|
||||
if (!uploadStatusRef.value) return;
|
||||
const el = uploadStatusRef.value;
|
||||
const itemsHeight = uploadQueue.value.length * 32;
|
||||
const headerHeight = 12;
|
||||
const gap = 8;
|
||||
const padding = 32;
|
||||
const totalHeight = padding + headerHeight + gap + itemsHeight + (props.marginBottom || 0);
|
||||
el.style.height = `${totalHeight}px`;
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes < 1024) return bytes + " B";
|
||||
if (bytes < 1024 ** 2) return (bytes / 1024).toFixed(1) + " KB";
|
||||
if (bytes < 1024 ** 3) return (bytes / 1024 ** 2).toFixed(1) + " MB";
|
||||
return (bytes / 1024 ** 3).toFixed(1) + " GB";
|
||||
};
|
||||
|
||||
const cancelUpload = (item: UploadItem) => {
|
||||
if (item.uploader && item.status === "uploading") {
|
||||
item.uploader.cancel();
|
||||
item.status = "cancelled";
|
||||
|
||||
setTimeout(async () => {
|
||||
const index = uploadQueue.value.findIndex((qItem) => qItem.file.name === item.file.name);
|
||||
if (index !== -1) {
|
||||
uploadQueue.value.splice(index, 1);
|
||||
await nextTick();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
const badFileTypeMsg = "Upload had incorrect file type";
|
||||
const uploadFile = async (file: File) => {
|
||||
const uploadItem: UploadItem = {
|
||||
file,
|
||||
progress: 0,
|
||||
status: "pending",
|
||||
size: formatFileSize(file.size),
|
||||
};
|
||||
|
||||
uploadQueue.value.push(uploadItem);
|
||||
|
||||
try {
|
||||
if (
|
||||
props.acceptedTypes &&
|
||||
!props.acceptedTypes.includes(file.type) &&
|
||||
!props.acceptedTypes.some((type) => file.name.endsWith(type))
|
||||
) {
|
||||
throw new Error(badFileTypeMsg);
|
||||
}
|
||||
|
||||
uploadItem.status = "uploading";
|
||||
const filePath = `${props.currentPath}/${file.name}`.replace("//", "/");
|
||||
const uploader = await props.fs.uploadFile(filePath, file);
|
||||
uploadItem.uploader = uploader;
|
||||
|
||||
if (uploader?.onProgress) {
|
||||
uploader.onProgress(({ progress }: { progress: number }) => {
|
||||
const index = uploadQueue.value.findIndex((item) => item.file.name === file.name);
|
||||
if (index !== -1) {
|
||||
uploadQueue.value[index].progress = Math.round(progress);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await uploader?.promise;
|
||||
const index = uploadQueue.value.findIndex((item) => item.file.name === file.name);
|
||||
if (index !== -1 && uploadQueue.value[index].status !== "cancelled") {
|
||||
uploadQueue.value[index].status = "completed";
|
||||
uploadQueue.value[index].progress = 100;
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
|
||||
setTimeout(async () => {
|
||||
const removeIndex = uploadQueue.value.findIndex((item) => item.file.name === file.name);
|
||||
if (removeIndex !== -1) {
|
||||
uploadQueue.value.splice(removeIndex, 1);
|
||||
await nextTick();
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
emit("uploadComplete");
|
||||
} catch (error) {
|
||||
console.error("Error uploading file:", error);
|
||||
const index = uploadQueue.value.findIndex((item) => item.file.name === file.name);
|
||||
if (index !== -1 && uploadQueue.value[index].status !== "cancelled") {
|
||||
uploadQueue.value[index].status =
|
||||
error instanceof Error && error.message === badFileTypeMsg ? "incorrect-type" : "error";
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
const removeIndex = uploadQueue.value.findIndex((item) => item.file.name === file.name);
|
||||
if (removeIndex !== -1) {
|
||||
uploadQueue.value.splice(removeIndex, 1);
|
||||
await nextTick();
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
if (error instanceof Error && error.message !== "Upload cancelled") {
|
||||
addNotification({
|
||||
group: "files",
|
||||
title: "Upload failed",
|
||||
text: `Failed to upload ${file.name}`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
uploadFile,
|
||||
cancelUpload,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.upload-status {
|
||||
overflow: hidden;
|
||||
transition: height 0.2s ease;
|
||||
}
|
||||
|
||||
.upload-status-enter-active,
|
||||
.upload-status-leave-active {
|
||||
transition: height 0.2s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.upload-status-enter-from,
|
||||
.upload-status-leave-to {
|
||||
height: 0 !important;
|
||||
}
|
||||
|
||||
.status-icon-enter-active,
|
||||
.status-icon-leave-active {
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.status-icon-enter-from,
|
||||
.status-icon-leave-to {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.status-icon-enter-to,
|
||||
.status-icon-leave-from {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -61,15 +61,7 @@
|
||||
Your server's hardware is currently being upgraded and will be back online shortly.
|
||||
</div>
|
||||
<div
|
||||
v-if="status === 'suspended' && suspension_reason === 'support'"
|
||||
class="relative -mt-4 flex w-full flex-row items-center gap-2 rounded-b-3xl bg-bg-blue p-4 text-sm font-bold text-contrast"
|
||||
>
|
||||
<HammerIcon />
|
||||
You recently requested support for your server and we are actively working on it. It will be
|
||||
back online shortly.
|
||||
</div>
|
||||
<div
|
||||
v-else-if="status === 'suspended' && suspension_reason !== 'upgrading'"
|
||||
v-else-if="status === 'suspended'"
|
||||
class="relative -mt-4 flex w-full flex-row items-center gap-2 rounded-b-3xl bg-bg-red p-4 text-sm font-bold text-contrast"
|
||||
>
|
||||
<UiServersIconsPanelErrorIcon class="!size-5" />
|
||||
@@ -80,7 +72,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChevronRightIcon, HammerIcon, LockIcon } from "@modrinth/assets";
|
||||
import { ChevronRightIcon, LockIcon } from "@modrinth/assets";
|
||||
import type { Project, Server } from "~/types/servers";
|
||||
|
||||
const props = defineProps<Partial<Server>>();
|
||||
|
||||
@@ -72,8 +72,6 @@
|
||||
:class="{
|
||||
'bg-brand font-bold text-brand-inverted': selectedValue === item.option,
|
||||
'bg-bg-raised': focusedOptionIndex === item.index,
|
||||
'rounded-b-xl': item.index === props.options.length - 1 && !isRenderingUp,
|
||||
'rounded-t-xl': item.index === 0 && isRenderingUp,
|
||||
}"
|
||||
:aria-selected="selectedValue === item.option"
|
||||
@click="selectOption(item.option, item.index)"
|
||||
|
||||
@@ -104,15 +104,22 @@ export const initAuth = async (oldToken = null) => {
|
||||
return auth;
|
||||
};
|
||||
|
||||
export const getAuthUrl = (provider, redirect = "/dashboard") => {
|
||||
export const getAuthUrl = (provider, redirect = "") => {
|
||||
const config = useRuntimeConfig();
|
||||
const route = useNativeRoute();
|
||||
|
||||
const fullURL = route.query.launcher
|
||||
? "https://launcher-files.modrinth.com"
|
||||
: `${config.public.siteUrl}/auth/sign-in?redirect=${redirect}`;
|
||||
if (redirect === "") {
|
||||
redirect = route.path;
|
||||
}
|
||||
|
||||
return `${config.public.apiBaseUrl}auth/init?provider=${provider}&url=${encodeURIComponent(fullURL)}`;
|
||||
let fullURL;
|
||||
if (route.query.launcher) {
|
||||
fullURL = `https://launcher-files.modrinth.com`;
|
||||
} else {
|
||||
fullURL = `${config.public.siteUrl}${redirect}`;
|
||||
}
|
||||
|
||||
return `${config.public.apiBaseUrl}auth/init?provider=${provider}&url=${fullURL}`;
|
||||
};
|
||||
|
||||
export const removeAuthProvider = async (provider) => {
|
||||
|
||||
@@ -67,10 +67,10 @@ async function PyroFetch<T>(path: string, options: PyroFetchOptions = {}): Promi
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("[PyroServers/PyroFetch]:", error);
|
||||
console.error("[PYROSERVERS]:", error);
|
||||
if (error instanceof FetchError) {
|
||||
const statusCode = error.response?.status;
|
||||
const statusText = error.response?.statusText || "[no status text available]";
|
||||
const statusText = error.response?.statusText || "Unknown error";
|
||||
const errorMessages: { [key: number]: string } = {
|
||||
400: "Bad Request",
|
||||
401: "Unauthorized",
|
||||
@@ -80,16 +80,15 @@ async function PyroFetch<T>(path: string, options: PyroFetchOptions = {}): Promi
|
||||
429: "Too Many Requests",
|
||||
500: "Internal Server Error",
|
||||
502: "Bad Gateway",
|
||||
503: "Service Unavailable",
|
||||
};
|
||||
const message =
|
||||
statusCode && statusCode in errorMessages
|
||||
? errorMessages[statusCode]
|
||||
: `HTTP Error: ${statusCode || "[unhandled status code]"} ${statusText}`;
|
||||
throw new PyroFetchError(`[PyroServers/PyroFetch] ${message}`, statusCode, error);
|
||||
: `HTTP Error: ${statusCode || "unknown"} ${statusText}`;
|
||||
throw new PyroFetchError(`[PYROSERVERS][PYRO] ${message}`, statusCode, error);
|
||||
}
|
||||
throw new PyroFetchError(
|
||||
"[PyroServers/PyroFetch] An unexpected error occurred during the fetch operation.",
|
||||
"[PYROSERVERS][PYRO] An unexpected error occurred during the fetch operation.",
|
||||
undefined,
|
||||
error as Error,
|
||||
);
|
||||
@@ -169,15 +168,7 @@ interface General {
|
||||
backup_quota: number;
|
||||
used_backup_quota: number;
|
||||
status: string;
|
||||
suspension_reason:
|
||||
| "moderated"
|
||||
| "paymentfailed"
|
||||
| "cancelled"
|
||||
| "other"
|
||||
| "transferring"
|
||||
| "upgrading"
|
||||
| "support"
|
||||
| (string & {});
|
||||
suspension_reason: string;
|
||||
loader: string;
|
||||
loader_version: string;
|
||||
mc_version: string;
|
||||
@@ -207,16 +198,14 @@ interface Startup {
|
||||
jdk_build: "corretto" | "temurin" | "graal";
|
||||
}
|
||||
|
||||
export interface Mod {
|
||||
interface Mod {
|
||||
filename: string;
|
||||
project_id: string | undefined;
|
||||
version_id: string | undefined;
|
||||
name: string | undefined;
|
||||
version_number: string | undefined;
|
||||
icon_url: string | undefined;
|
||||
owner: string | undefined;
|
||||
project_id: string;
|
||||
version_id: string;
|
||||
name: string;
|
||||
version_number: string;
|
||||
icon_url: string;
|
||||
disabled: boolean;
|
||||
installing: boolean;
|
||||
}
|
||||
|
||||
interface Backup {
|
||||
@@ -1375,7 +1364,7 @@ type ContentModule = { data: Mod[] } & ContentFunctions;
|
||||
type BackupsModule = { data: Backup[] } & BackupFunctions;
|
||||
type NetworkModule = { allocations: Allocation[] } & NetworkFunctions;
|
||||
type StartupModule = Startup & StartupFunctions;
|
||||
export type FSModule = { auth: JWTAuth } & FSFunctions;
|
||||
type FSModule = { auth: JWTAuth } & FSFunctions;
|
||||
|
||||
type ModulesMap = {
|
||||
general: GeneralModule;
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
{
|
||||
"admin.billing.error.not-found": {
|
||||
"message": "User not found"
|
||||
},
|
||||
"auth.authorize.action.authorize": {
|
||||
"message": "Authorize"
|
||||
},
|
||||
@@ -341,12 +338,6 @@
|
||||
"layout.nav.search": {
|
||||
"message": "Search"
|
||||
},
|
||||
"profile.button.billing": {
|
||||
"message": "Manage user billing"
|
||||
},
|
||||
"profile.button.info": {
|
||||
"message": "View user details"
|
||||
},
|
||||
"profile.button.manage-projects": {
|
||||
"message": "Manage projects"
|
||||
},
|
||||
|
||||
@@ -184,19 +184,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<NewModal
|
||||
ref="downloadModal"
|
||||
:on-show="
|
||||
() => {
|
||||
navigateTo({ query: route.query, hash: '#download' });
|
||||
}
|
||||
"
|
||||
:on-hide="
|
||||
() => {
|
||||
navigateTo({ query: route.query, hash: '' });
|
||||
}
|
||||
"
|
||||
>
|
||||
<NewModal ref="downloadModal">
|
||||
<template #title>
|
||||
<Avatar :src="project.icon_url" :alt="project.title" class="icon" size="32px" />
|
||||
<div class="truncate text-lg font-extrabold text-contrast">
|
||||
@@ -287,7 +275,7 @@
|
||||
</div>
|
||||
<ScrollablePanel :class="project.game_versions.length > 4 ? 'h-[15rem]' : ''">
|
||||
<ButtonStyled
|
||||
v-for="gameVersion in project.game_versions
|
||||
v-for="version in project.game_versions
|
||||
.filter(
|
||||
(x) =>
|
||||
(versionFilter && x.includes(versionFilter)) ||
|
||||
@@ -296,39 +284,30 @@
|
||||
)
|
||||
.slice()
|
||||
.reverse()"
|
||||
:key="gameVersion"
|
||||
:color="currentGameVersion === gameVersion ? 'brand' : 'standard'"
|
||||
:key="version"
|
||||
:color="currentGameVersion === version ? 'brand' : 'standard'"
|
||||
>
|
||||
<button
|
||||
v-tooltip="
|
||||
!possibleGameVersions.includes(gameVersion)
|
||||
? `${project.title} does not support ${gameVersion} for ${formatCategory(currentPlatform)}`
|
||||
!possibleGameVersions.includes(version)
|
||||
? `${project.title} does not support ${version} for ${formatCategory(currentPlatform)}`
|
||||
: null
|
||||
"
|
||||
:class="{
|
||||
'looks-disabled !text-brand-red': !possibleGameVersions.includes(gameVersion),
|
||||
'looks-disabled !text-brand-red': !possibleGameVersions.includes(version),
|
||||
}"
|
||||
@click="
|
||||
() => {
|
||||
userSelectedGameVersion = gameVersion;
|
||||
userSelectedGameVersion = version;
|
||||
gameVersionAccordion.close();
|
||||
if (!currentPlatform && platformAccordion) {
|
||||
platformAccordion.open();
|
||||
}
|
||||
|
||||
navigateTo({
|
||||
query: {
|
||||
...route.query,
|
||||
...(userSelectedGameVersion && { version: userSelectedGameVersion }),
|
||||
...(userSelectedPlatform && { loader: userSelectedPlatform }),
|
||||
},
|
||||
hash: route.hash,
|
||||
});
|
||||
}
|
||||
"
|
||||
>
|
||||
{{ gameVersion }}
|
||||
<CheckIcon v-if="userSelectedGameVersion === gameVersion" />
|
||||
{{ version }}
|
||||
<CheckIcon v-if="userSelectedGameVersion === version" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</ScrollablePanel>
|
||||
@@ -400,15 +379,6 @@
|
||||
if (!currentGameVersion && gameVersionAccordion) {
|
||||
gameVersionAccordion.open();
|
||||
}
|
||||
|
||||
navigateTo({
|
||||
query: {
|
||||
...route.query,
|
||||
...(userSelectedGameVersion && { version: userSelectedGameVersion }),
|
||||
...(userSelectedPlatform && { loader: userSelectedPlatform }),
|
||||
},
|
||||
hash: route.hash,
|
||||
});
|
||||
}
|
||||
"
|
||||
>
|
||||
@@ -536,7 +506,7 @@
|
||||
placeholder="Search collections..."
|
||||
class="search-input menu-search"
|
||||
/>
|
||||
<div v-if="collections.length > 0" class="collections-list text-primary">
|
||||
<div v-if="collections.length > 0" class="collections-list">
|
||||
<Checkbox
|
||||
v-for="option in collections
|
||||
.slice()
|
||||
@@ -802,7 +772,6 @@
|
||||
:reset-members="resetMembers"
|
||||
:route="route"
|
||||
@on-download="triggerDownloadAnimation"
|
||||
@delete-version="deleteVersion"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -816,31 +785,31 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
ScaleIcon,
|
||||
AlignLeftIcon as DescriptionIcon,
|
||||
BookmarkIcon,
|
||||
BookTextIcon,
|
||||
CalendarIcon,
|
||||
ChartIcon,
|
||||
CheckIcon,
|
||||
ClipboardCopyIcon,
|
||||
CopyrightIcon,
|
||||
AlignLeftIcon as DescriptionIcon,
|
||||
DownloadIcon,
|
||||
ExternalIcon,
|
||||
ImageIcon as GalleryIcon,
|
||||
GameIcon,
|
||||
HeartIcon,
|
||||
ImageIcon as GalleryIcon,
|
||||
InfoIcon,
|
||||
LinkIcon as LinksIcon,
|
||||
MoreVerticalIcon,
|
||||
PlusIcon,
|
||||
ReportIcon,
|
||||
ScaleIcon,
|
||||
SearchIcon,
|
||||
SettingsIcon,
|
||||
TagsIcon,
|
||||
UsersIcon,
|
||||
VersionIcon,
|
||||
WrenchIcon,
|
||||
BookTextIcon,
|
||||
CalendarIcon,
|
||||
} from "@modrinth/assets";
|
||||
import {
|
||||
Avatar,
|
||||
@@ -849,33 +818,32 @@ import {
|
||||
NewModal,
|
||||
OverflowMenu,
|
||||
PopoutMenu,
|
||||
ProjectBackgroundGradient,
|
||||
ScrollablePanel,
|
||||
ProjectHeader,
|
||||
ProjectSidebarCompatibility,
|
||||
ProjectSidebarCreators,
|
||||
ProjectSidebarDetails,
|
||||
ProjectSidebarLinks,
|
||||
ScrollablePanel,
|
||||
ProjectSidebarDetails,
|
||||
ProjectBackgroundGradient,
|
||||
} from "@modrinth/ui";
|
||||
import VersionSummary from "@modrinth/ui/src/components/version/VersionSummary.vue";
|
||||
import { formatCategory, isRejected, isStaff, isUnderReview, renderString } from "@modrinth/utils";
|
||||
import { navigateTo } from "#app";
|
||||
import dayjs from "dayjs";
|
||||
import ModrinthIcon from "~/assets/images/utils/modrinth.svg?component";
|
||||
import Accordion from "~/components/ui/Accordion.vue";
|
||||
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
||||
import AutomaticAccordion from "~/components/ui/AutomaticAccordion.vue";
|
||||
import VersionSummary from "@modrinth/ui/src/components/version/VersionSummary.vue";
|
||||
import Badge from "~/components/ui/Badge.vue";
|
||||
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
|
||||
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
|
||||
import MessageBanner from "~/components/ui/MessageBanner.vue";
|
||||
import ModerationChecklist from "~/components/ui/ModerationChecklist.vue";
|
||||
import NavTabs from "~/components/ui/NavTabs.vue";
|
||||
import NavStack from "~/components/ui/NavStack.vue";
|
||||
import NavStackItem from "~/components/ui/NavStackItem.vue";
|
||||
import NavTabs from "~/components/ui/NavTabs.vue";
|
||||
import ProjectMemberHeader from "~/components/ui/ProjectMemberHeader.vue";
|
||||
import { userCollectProject } from "~/composables/user.js";
|
||||
import MessageBanner from "~/components/ui/MessageBanner.vue";
|
||||
import { reportProject } from "~/utils/report-helpers.ts";
|
||||
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
|
||||
import { userCollectProject } from "~/composables/user.js";
|
||||
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
|
||||
import ModerationChecklist from "~/components/ui/ModerationChecklist.vue";
|
||||
import Accordion from "~/components/ui/Accordion.vue";
|
||||
import ModrinthIcon from "~/assets/images/utils/modrinth.svg?component";
|
||||
import AutomaticAccordion from "~/components/ui/AutomaticAccordion.vue";
|
||||
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
||||
|
||||
const data = useNuxtApp();
|
||||
const route = useNativeRoute();
|
||||
@@ -1279,23 +1247,6 @@ if (!route.name.startsWith("type-id-settings")) {
|
||||
|
||||
const onUserCollectProject = useClientTry(userCollectProject);
|
||||
|
||||
const { version, loader } = route.query;
|
||||
if (version !== undefined && project.value.game_versions.includes(version)) {
|
||||
userSelectedGameVersion.value = version;
|
||||
}
|
||||
if (loader !== undefined && project.value.loaders.includes(loader)) {
|
||||
userSelectedPlatform.value = loader;
|
||||
}
|
||||
|
||||
watch(downloadModal, (modal) => {
|
||||
if (!modal) return;
|
||||
|
||||
// route.hash returns everything in the hash string, including the # itself
|
||||
if (route.hash === "#download") {
|
||||
modal.show();
|
||||
}
|
||||
});
|
||||
|
||||
async function setProcessing() {
|
||||
startLoading();
|
||||
|
||||
@@ -1452,20 +1403,6 @@ function onDownload(event) {
|
||||
}, 400);
|
||||
}
|
||||
|
||||
async function deleteVersion(id) {
|
||||
if (!id) return;
|
||||
|
||||
startLoading();
|
||||
|
||||
await useBaseFetch(`version/${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
versions.value = versions.value.filter((x) => x.id !== id);
|
||||
|
||||
stopLoading();
|
||||
}
|
||||
|
||||
const navLinks = computed(() => {
|
||||
const projectUrl = `/${project.value.project_type}/${project.value.slug ? project.value.slug : project.value.id}`;
|
||||
|
||||
|
||||
@@ -381,7 +381,6 @@
|
||||
/>
|
||||
<ButtonStyled v-if="isEditing">
|
||||
<button
|
||||
class="raised-button"
|
||||
:disabled="primaryFile.hashes.sha1 === file.hashes.sha1"
|
||||
@click="
|
||||
() => {
|
||||
@@ -822,13 +821,6 @@ export default defineNuxtComponent({
|
||||
if (route.query.version) {
|
||||
versionList = versionList.filter((x) => x.game_versions.includes(route.query.version));
|
||||
}
|
||||
if (versionList.length === 0) {
|
||||
throw createError({
|
||||
fatal: true,
|
||||
statusCode: 404,
|
||||
message: "No version matches the filters",
|
||||
});
|
||||
}
|
||||
version = versionList.reduce((a, b) => (a.date_published > b.date_published ? a : b));
|
||||
} else {
|
||||
version = props.versions.find((x) => x.id === route.params.version);
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
<template>
|
||||
<ConfirmModal
|
||||
v-if="currentMember"
|
||||
ref="deleteVersionModal"
|
||||
title="Are you sure you want to delete this version?"
|
||||
description="This will remove this version forever (like really forever)."
|
||||
:has-to-type="false"
|
||||
proceed-label="Delete"
|
||||
@proceed="deleteVersion()"
|
||||
/>
|
||||
<section class="experimental-styles-within overflow-visible">
|
||||
<div
|
||||
v-if="currentMember && isPermission(currentMember?.permissions, 1 << 0)"
|
||||
@@ -50,7 +41,7 @@
|
||||
:href="getPrimaryFile(version).url"
|
||||
class="group-hover:!bg-brand group-hover:[&>svg]:!text-brand-inverted"
|
||||
aria-label="Download"
|
||||
@click="emit('onDownload')"
|
||||
@click="emits('onDownload')"
|
||||
>
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
</a>
|
||||
@@ -66,7 +57,7 @@
|
||||
hoverFilled: true,
|
||||
link: getPrimaryFile(version).url,
|
||||
action: () => {
|
||||
emit('onDownload');
|
||||
emits('onDownload');
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -110,11 +101,8 @@
|
||||
id: 'delete',
|
||||
color: 'red',
|
||||
hoverFilled: true,
|
||||
action: () => {
|
||||
selectedVersion = version.id;
|
||||
deleteVersionModal.show();
|
||||
},
|
||||
shown: currentMember,
|
||||
action: () => {},
|
||||
shown: currentMember && false,
|
||||
},
|
||||
]"
|
||||
aria-label="More options"
|
||||
@@ -156,13 +144,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ButtonStyled,
|
||||
OverflowMenu,
|
||||
FileInput,
|
||||
ProjectPageVersions,
|
||||
ConfirmModal,
|
||||
} from "@modrinth/ui";
|
||||
import { ButtonStyled, OverflowMenu, FileInput, ProjectPageVersions } from "@modrinth/ui";
|
||||
import {
|
||||
DownloadIcon,
|
||||
MoreVerticalIcon,
|
||||
@@ -203,10 +185,7 @@ const tags = useTags();
|
||||
const flags = useFeatureFlags();
|
||||
const auth = await useAuth();
|
||||
|
||||
const deleteVersionModal = ref();
|
||||
const selectedVersion = ref(null);
|
||||
|
||||
const emit = defineEmits(["onDownload", "deleteVersion"]);
|
||||
const emits = defineEmits(["onDownload"]);
|
||||
|
||||
const router = useNativeRouter();
|
||||
|
||||
@@ -233,9 +212,4 @@ async function handleFiles(files) {
|
||||
async function copyToClipboard(text) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
}
|
||||
|
||||
function deleteVersion() {
|
||||
emit("deleteVersion", selectedVersion.value);
|
||||
selectedVersion.value = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
<template>
|
||||
<NewModal ref="refundModal">
|
||||
<template #title>
|
||||
<span class="text-lg font-extrabold text-contrast">Refund charge</span>
|
||||
</template>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="visibility" class="flex flex-col gap-1">
|
||||
<span class="text-lg font-semibold text-contrast">
|
||||
Refund type
|
||||
<span class="text-brand-red">*</span>
|
||||
</span>
|
||||
<span> The type of refund to issue. </span>
|
||||
</label>
|
||||
<DropdownSelect
|
||||
id="refund-type"
|
||||
v-model="refundType"
|
||||
:options="refundTypes"
|
||||
name="Refund type"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="refundType === 'partial'" class="flex flex-col gap-2">
|
||||
<label for="amount" class="flex flex-col gap-1">
|
||||
<span class="text-lg font-semibold text-contrast">
|
||||
Amount
|
||||
<span class="text-brand-red">*</span>
|
||||
</span>
|
||||
<span>
|
||||
Enter the amount in cents of USD. For example for $2, enter 200. (net
|
||||
{{ selectedCharge.net }})
|
||||
</span>
|
||||
</label>
|
||||
<input id="amount" v-model="refundAmount" type="number" autocomplete="off" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="unprovision" class="flex flex-col gap-1">
|
||||
<span class="text-lg font-semibold text-contrast">
|
||||
Unprovision
|
||||
<span class="text-brand-red">*</span>
|
||||
</span>
|
||||
<span> Whether or not the subscription should be unprovisioned on refund. </span>
|
||||
</label>
|
||||
<Toggle
|
||||
id="unprovision"
|
||||
:model-value="unprovision"
|
||||
:checked="unprovision"
|
||||
@update:model-value="() => (unprovision = !unprovision)"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="refunding" @click="refundCharge">
|
||||
<CheckIcon aria-hidden="true" />
|
||||
Refund charge
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="refundModal.hide()">
|
||||
<XIcon aria-hidden="true" />
|
||||
Cancel
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
<div class="normal-page no-sidebar">
|
||||
<h1>{{ user.username }}'s subscriptions</h1>
|
||||
<div class="normal-page__content">
|
||||
<div v-for="subscription in subscriptionCharges" :key="subscription.id" class="card">
|
||||
<span class="font-extrabold text-contrast">
|
||||
<template v-if="subscription.product.metadata.type === 'midas'"> Modrinth Plus </template>
|
||||
<template v-else-if="subscription.product.metadata.type === 'pyro'">
|
||||
Modrinth Servers
|
||||
</template>
|
||||
<template v-else> Unknown product </template>
|
||||
<template v-if="subscription.interval">
|
||||
{{ subscription.interval }}
|
||||
</template>
|
||||
</span>
|
||||
<div class="mb-4 mt-2 flex items-center gap-1">
|
||||
{{ subscription.status }} ⋅ {{ $dayjs(subscription.created).format("YYYY-MM-DD") }}
|
||||
<template v-if="subscription.metadata?.id"> ⋅ {{ subscription.metadata.id }}</template>
|
||||
</div>
|
||||
<div
|
||||
v-for="charge in subscription.charges"
|
||||
:key="charge.id"
|
||||
class="universal-card recessed flex items-center justify-between gap-4"
|
||||
>
|
||||
<div class="flex w-full items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-1">
|
||||
<Badge
|
||||
:color="charge.status === 'succeeded' ? 'green' : 'red'"
|
||||
:type="charge.status"
|
||||
/>
|
||||
⋅
|
||||
{{ charge.type }}
|
||||
⋅
|
||||
{{ $dayjs(charge.due).format("YYYY-MM-DD") }}
|
||||
⋅
|
||||
<span>{{ formatPrice(vintl.locale, charge.amount, charge.currency_code) }}</span>
|
||||
<template v-if="subscription.interval"> ⋅ {{ subscription.interval }} </template>
|
||||
</div>
|
||||
<button
|
||||
v-if="charge.status === 'succeeded' && charge.type !== 'refund'"
|
||||
class="btn"
|
||||
@click="showRefundModal(charge)"
|
||||
>
|
||||
Refund charge
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Badge, NewModal, ButtonStyled, DropdownSelect, Toggle } from "@modrinth/ui";
|
||||
import { formatPrice } from "@modrinth/utils";
|
||||
import { CheckIcon, XIcon } from "@modrinth/assets";
|
||||
import { products } from "~/generated/state.json";
|
||||
|
||||
const route = useRoute();
|
||||
const data = useNuxtApp();
|
||||
const vintl = useVIntl();
|
||||
const { formatMessage } = vintl;
|
||||
|
||||
const messages = defineMessages({
|
||||
userNotFoundError: {
|
||||
id: "admin.billing.error.not-found",
|
||||
defaultMessage: "User not found",
|
||||
},
|
||||
});
|
||||
|
||||
const { data: user } = await useAsyncData(`user/${route.params.id}`, () =>
|
||||
useBaseFetch(`user/${route.params.id}`),
|
||||
);
|
||||
|
||||
if (!user.value) {
|
||||
throw createError({
|
||||
fatal: true,
|
||||
statusCode: 404,
|
||||
message: formatMessage(messages.userNotFoundError),
|
||||
});
|
||||
}
|
||||
|
||||
let subscriptions, charges, refreshCharges;
|
||||
try {
|
||||
[{ data: subscriptions }, { data: charges, refresh: refreshCharges }] = await Promise.all([
|
||||
useAsyncData(`billing/subscriptions?user_id=${route.params.id}`, () =>
|
||||
useBaseFetch(`billing/subscriptions?user_id=${user.value.id}`, {
|
||||
internal: true,
|
||||
}),
|
||||
),
|
||||
useAsyncData(`billing/payments?user_id=${route.params.id}`, () =>
|
||||
useBaseFetch(`billing/payments?user_id=${user.value.id}`, {
|
||||
internal: true,
|
||||
}),
|
||||
),
|
||||
]);
|
||||
} catch {
|
||||
throw createError({
|
||||
fatal: true,
|
||||
statusCode: 404,
|
||||
message: formatMessage(messages.userNotFoundError),
|
||||
});
|
||||
}
|
||||
|
||||
const subscriptionCharges = computed(() => {
|
||||
return subscriptions.value.map((subscription) => {
|
||||
return {
|
||||
...subscription,
|
||||
charges: charges.value.filter((charge) => charge.subscription_id === subscription.id),
|
||||
product: products.find((product) =>
|
||||
product.prices.some((price) => price.id === subscription.price_id),
|
||||
),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const refunding = ref(false);
|
||||
const refundModal = ref();
|
||||
const selectedCharge = ref(null);
|
||||
const refundType = ref("full");
|
||||
const refundTypes = ref(["full", "partial"]);
|
||||
const refundAmount = ref(0);
|
||||
const unprovision = ref(false);
|
||||
|
||||
function showRefundModal(charge) {
|
||||
selectedCharge.value = charge;
|
||||
refundType.value = "full";
|
||||
refundAmount.value = 0;
|
||||
unprovision.value = false;
|
||||
refundModal.value.show();
|
||||
}
|
||||
|
||||
async function refundCharge() {
|
||||
refunding.value = true;
|
||||
try {
|
||||
await useBaseFetch(`billing/charge/${selectedCharge.value.id}/refund`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
type: refundType.value,
|
||||
amount: refundAmount.value,
|
||||
unprovision: unprovision.value,
|
||||
}),
|
||||
internal: true,
|
||||
});
|
||||
await refreshCharges();
|
||||
refundModal.value.hide();
|
||||
} catch (err) {
|
||||
data.$notify({
|
||||
group: "main",
|
||||
title: "Error refunding",
|
||||
text: err.data?.description ?? err,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
refunding.value = false;
|
||||
}
|
||||
</script>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<ConfirmModal
|
||||
v-if="canEdit"
|
||||
<ModalConfirm
|
||||
v-if="auth.user && auth.user.id === creator.id"
|
||||
ref="deleteModal"
|
||||
:title="formatMessage(messages.deleteModalTitle)"
|
||||
:description="formatMessage(messages.deleteModalDescription)"
|
||||
@@ -387,13 +387,12 @@ import {
|
||||
Avatar,
|
||||
Button,
|
||||
commonMessages,
|
||||
ConfirmModal,
|
||||
} from "@modrinth/ui";
|
||||
|
||||
import { isAdmin } from "@modrinth/utils";
|
||||
import WorldIcon from "assets/images/utils/world.svg";
|
||||
import UpToDate from "assets/images/illustrations/up_to_date.svg";
|
||||
import { addNotification } from "~/composables/notifs.js";
|
||||
import ModalConfirm from "~/components/ui/ModalConfirm.vue";
|
||||
import NavRow from "~/components/ui/NavRow.vue";
|
||||
import ProjectCard from "~/components/ui/ProjectCard.vue";
|
||||
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
||||
@@ -597,7 +596,7 @@ useSeoMeta({
|
||||
const canEdit = computed(
|
||||
() =>
|
||||
auth.value.user &&
|
||||
(auth.value.user.id === collection.value.user || isAdmin(auth.value.user)) &&
|
||||
auth.value.user.id === collection.value.user &&
|
||||
collection.value.id !== "following",
|
||||
);
|
||||
|
||||
@@ -686,11 +685,7 @@ async function deleteCollection() {
|
||||
method: "DELETE",
|
||||
apiVersion: 3,
|
||||
});
|
||||
if (auth.value.user.id === collection.value.user) {
|
||||
await navigateTo("/dashboard/collections");
|
||||
} else {
|
||||
await navigateTo(`/user/${collection.value.user}/collections`);
|
||||
}
|
||||
await navigateTo("/dashboard/collections");
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
group: "main",
|
||||
|
||||
@@ -38,13 +38,9 @@
|
||||
<div class="withdraw-options-scroll">
|
||||
<div class="withdraw-options">
|
||||
<button
|
||||
v-for="method in payoutMethods
|
||||
.filter((x) => x.name.toLowerCase().includes(search.toLowerCase()))
|
||||
.sort((a, b) =>
|
||||
a.type !== 'tremendous'
|
||||
? -1
|
||||
: a.name.toLowerCase().localeCompare(b.name.toLowerCase()),
|
||||
)"
|
||||
v-for="method in payoutMethods.filter((x) =>
|
||||
x.name.toLowerCase().includes(search.toLowerCase()),
|
||||
)"
|
||||
:key="method.id"
|
||||
class="withdraw-option button-base"
|
||||
:class="{ selected: selectedMethodId === method.id }"
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
<div class="users-section">
|
||||
<div class="section-header">
|
||||
<div class="section-label green">For Players</div>
|
||||
<h2 class="section-tagline">Discover over 50,000 creations</h2>
|
||||
<h2 class="section-tagline">Discover over 10,000 creations</h2>
|
||||
<p class="section-description">
|
||||
From magical biomes to cursed dungeons, you can be sure to find content to bring your
|
||||
gameplay to the next level.
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
We aim to be as transparent as possible with creator revenue. All of our code is open source,
|
||||
including our
|
||||
<a href="https://github.com/modrinth/code/blob/main/apps/labrinth/src/queue/payouts.rs#L598">
|
||||
revenue distribution system</a
|
||||
revenue distribution system </a
|
||||
>. We also have an
|
||||
<a href="https://api.modrinth.com/v3/payout/platform_revenue">API route</a> that allows users
|
||||
to query exact daily revenue for the site.
|
||||
|
||||
@@ -42,11 +42,7 @@
|
||||
Install content to server
|
||||
</h1>
|
||||
</template>
|
||||
<NavTabs
|
||||
v-if="!server && !flags.projectTypesPrimaryNav"
|
||||
:links="selectableProjectTypes"
|
||||
class="hidden md:flex"
|
||||
/>
|
||||
<NavTabs v-if="!server" :links="selectableProjectTypes" class="hidden md:flex" />
|
||||
</section>
|
||||
<aside
|
||||
:class="{
|
||||
@@ -168,7 +164,7 @@
|
||||
name="Sort by"
|
||||
:options="sortTypes"
|
||||
:display-name="(option) => option?.display"
|
||||
@change="updateSearchResults()"
|
||||
@change="updateSearchResults(1)"
|
||||
>
|
||||
<span class="font-semibold text-primary">Sort by: </span>
|
||||
<span class="font-semibold text-secondary">{{ selected }}</span>
|
||||
@@ -181,7 +177,7 @@
|
||||
:default-value="maxResults"
|
||||
:model-value="maxResults"
|
||||
class="!w-auto flex-grow md:flex-grow-0"
|
||||
@change="updateSearchResults()"
|
||||
@change="updateSearchResults(1)"
|
||||
>
|
||||
<span class="font-semibold text-primary">View: </span>
|
||||
<span class="font-semibold text-secondary">{{ selected }}</span>
|
||||
@@ -206,7 +202,7 @@
|
||||
:page="currentPage"
|
||||
:count="pageCount"
|
||||
class="mx-auto sm:ml-auto sm:mr-0"
|
||||
@switch-page="updateSearchResults"
|
||||
@switch-page="setPage"
|
||||
/>
|
||||
</div>
|
||||
<SearchFilterControl
|
||||
@@ -296,7 +292,7 @@
|
||||
:page="currentPage"
|
||||
:count="pageCount"
|
||||
class="justify-end"
|
||||
@switch-page="updateSearchResults"
|
||||
@switch-page="setPage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -342,21 +338,11 @@ const tags = useTags();
|
||||
const flags = useFeatureFlags();
|
||||
const auth = await useAuth();
|
||||
|
||||
const projectType = ref();
|
||||
function setProjectType() {
|
||||
const projType = tags.value.projectTypes.find(
|
||||
const projectType = computed(() =>
|
||||
tags.value.projectTypes.find(
|
||||
(x) => x.id === route.path.replaceAll(/^\/|s\/?$/g, ""), // Removes prefix `/` and suffixes `s` and `s/`
|
||||
);
|
||||
|
||||
if (projType) {
|
||||
projectType.value = projType;
|
||||
}
|
||||
}
|
||||
setProjectType();
|
||||
router.afterEach(() => {
|
||||
setProjectType();
|
||||
});
|
||||
|
||||
),
|
||||
);
|
||||
const projectTypes = computed(() => [projectType.value.id]);
|
||||
|
||||
const server = ref();
|
||||
@@ -530,7 +516,7 @@ const {
|
||||
const config = useRuntimeConfig();
|
||||
const base = import.meta.server ? config.apiBaseUrl : config.public.apiBaseUrl;
|
||||
|
||||
return `${base}search${requestParams.value}`;
|
||||
return `${base}/search${requestParams.value}`;
|
||||
},
|
||||
{
|
||||
transform: (hits) => {
|
||||
@@ -545,13 +531,19 @@ const pageCount = computed(() =>
|
||||
results.value ? Math.ceil(results.value.total_hits / results.value.limit) : 1,
|
||||
);
|
||||
|
||||
function setPage(newPageNumber) {
|
||||
currentPage.value = newPageNumber;
|
||||
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
|
||||
updateSearchResults();
|
||||
}
|
||||
|
||||
function scrollToTop(behavior = "smooth") {
|
||||
window.scrollTo({ top: 0, behavior });
|
||||
}
|
||||
|
||||
function updateSearchResults(pageNumber) {
|
||||
currentPage.value = pageNumber || 1;
|
||||
scrollToTop();
|
||||
function updateSearchResults() {
|
||||
noLoad.value = true;
|
||||
|
||||
if (query.value === null) {
|
||||
@@ -584,8 +576,8 @@ function updateSearchResults(pageNumber) {
|
||||
}
|
||||
}
|
||||
|
||||
watch([currentFilters], () => {
|
||||
updateSearchResults(1);
|
||||
watch([currentFilters, requestParams], () => {
|
||||
updateSearchResults();
|
||||
});
|
||||
|
||||
function cycleSearchDisplayMode() {
|
||||
|
||||
@@ -456,9 +456,9 @@
|
||||
Where are Modrinth Servers located? Can I choose a region?
|
||||
</summary>
|
||||
<p class="m-0 !leading-[190%]">
|
||||
Currently, Modrinth Servers are located in New York, Los Angeles, Seattle, and
|
||||
Miami. More regions are coming soon! Your server's location is currently chosen
|
||||
algorithmically, but you will be able to choose a region in the future.
|
||||
Currently, Modrinth Servers are located in New York, Los Angeles, and Miami. More
|
||||
regions are coming soon! Your server's location is currently chosen algorithmically,
|
||||
but you will be able to choose a region in the future.
|
||||
</p>
|
||||
</details>
|
||||
|
||||
@@ -512,9 +512,9 @@
|
||||
: "There's a plan for everyone! Choose the one that fits your needs."
|
||||
}}
|
||||
<span class="font-bold">
|
||||
Servers are currently US only, in New York, Los Angeles, Seattle, and Miami. More
|
||||
regions coming soon!
|
||||
</span>
|
||||
Servers are currently US only, in New York, Los Angeles, and Miami. More regions coming
|
||||
soon!</span
|
||||
>
|
||||
</h2>
|
||||
|
||||
<ul class="m-0 flex w-full flex-col gap-8 p-0 lg:flex-row">
|
||||
@@ -533,9 +533,9 @@
|
||||
<div class="flex flex-row flex-wrap items-center gap-3 text-nowrap">
|
||||
<p class="m-0">4 GB RAM</p>
|
||||
<div class="size-1.5 rounded-full bg-secondary opacity-25"></div>
|
||||
<p class="m-0">4 vCPUs</p>
|
||||
<div class="size-1.5 rounded-full bg-secondary opacity-25"></div>
|
||||
<p class="m-0">32 GB Storage</p>
|
||||
<div class="size-1.5 rounded-full bg-secondary opacity-25"></div>
|
||||
<p class="m-0">2 vCPUs</p>
|
||||
</div>
|
||||
<h2 class="m-0 text-3xl text-contrast">
|
||||
$12<span class="text-sm font-normal text-secondary">/month</span>
|
||||
@@ -585,9 +585,9 @@
|
||||
<div class="flex flex-row flex-wrap items-center gap-3 text-nowrap">
|
||||
<p class="m-0">6 GB RAM</p>
|
||||
<div class="size-1.5 rounded-full bg-secondary opacity-25"></div>
|
||||
<p class="m-0">6 vCPUs</p>
|
||||
<div class="size-1.5 rounded-full bg-secondary opacity-25"></div>
|
||||
<p class="m-0">48 GB Storage</p>
|
||||
<div class="size-1.5 rounded-full bg-secondary opacity-25"></div>
|
||||
<p class="m-0">3 vCPUs</p>
|
||||
</div>
|
||||
<h2 class="m-0 text-3xl text-contrast">
|
||||
$18<span class="text-sm font-normal text-secondary">/month</span>
|
||||
@@ -626,9 +626,9 @@
|
||||
<div class="flex flex-row flex-wrap items-center gap-3 text-nowrap">
|
||||
<p class="m-0">8 GB RAM</p>
|
||||
<div class="size-1.5 rounded-full bg-secondary opacity-25"></div>
|
||||
<p class="m-0">8 vCPUs</p>
|
||||
<div class="size-1.5 rounded-full bg-secondary opacity-25"></div>
|
||||
<p class="m-0">64 GB Storage</p>
|
||||
<div class="size-1.5 rounded-full bg-secondary opacity-25"></div>
|
||||
<p class="m-0">4 vCPUs</p>
|
||||
</div>
|
||||
<h2 class="m-0 text-3xl text-contrast">
|
||||
$24<span class="text-sm font-normal text-secondary">/month</span>
|
||||
@@ -656,11 +656,11 @@
|
||||
</ul>
|
||||
|
||||
<div
|
||||
class="flex w-full flex-col items-start justify-between gap-4 rounded-2xl bg-bg p-8 text-left lg:flex-row lg:gap-0"
|
||||
class="flex w-full flex-col items-start justify-between gap-4 rounded-2xl bg-bg p-8 text-left md:flex-row md:gap-0"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<h1 class="m-0">Build your own</h1>
|
||||
<h2 class="m-0 text-base font-normal text-primary">
|
||||
<h2 class="m-0 text-base font-normal">
|
||||
If you're a more technical server administrator, you can pick your own RAM and storage
|
||||
options.
|
||||
</h2>
|
||||
|
||||
@@ -19,26 +19,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="serverData?.status === 'suspended' && serverData.suspension_reason === 'support'"
|
||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="grid place-content-center rounded-full bg-bg-blue p-4">
|
||||
<TransferIcon class="size-12 text-blue" />
|
||||
</div>
|
||||
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">We're working on your server</h1>
|
||||
</div>
|
||||
<p class="text-lg text-secondary">
|
||||
You recently contacted Modrinth Support, and we're actively working on your server. It
|
||||
will be back online shortly.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="serverData?.status === 'suspended' && serverData.suspension_reason !== 'upgrading'"
|
||||
v-else-if="serverData?.status === 'suspended'"
|
||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
@@ -88,58 +69,6 @@
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="server.error && server.error.message.includes('Service Unavailable')"
|
||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="grid place-content-center rounded-full bg-bg-red p-4">
|
||||
<PanelErrorIcon class="size-12 text-red" />
|
||||
</div>
|
||||
<h1 class="m-0 mb-4 w-fit text-4xl font-bold">Server Node Unavailable</h1>
|
||||
</div>
|
||||
<p class="m-0 mb-4 leading-[170%] text-secondary">
|
||||
Your server's node, where your Modrinth Server is physically hosted, is experiencing
|
||||
issues. We are working with our datacenter to resolve the issue as quickly as possible.
|
||||
</p>
|
||||
<p class="m-0 mb-4 leading-[170%] text-secondary">
|
||||
Your data is safe and will not be lost, and your server will be back online as soon as
|
||||
the issue is resolved.
|
||||
</p>
|
||||
<p class="m-0 mb-4 leading-[170%] text-secondary">
|
||||
For updates, please join the Modrinth Discord or contact Modrinth Support via the chat
|
||||
bubble in the bottom right corner and we'll be happy to help.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<UiCopyCode :text="'Server ID: ' + server.serverId" />
|
||||
<UiCopyCode :text="'Node: ' + server.general?.datacenter" />
|
||||
</div>
|
||||
</div>
|
||||
<ButtonStyled
|
||||
size="large"
|
||||
color="standard"
|
||||
@click="
|
||||
() =>
|
||||
navigateTo('https://discord.modrinth.com', {
|
||||
external: true,
|
||||
})
|
||||
"
|
||||
>
|
||||
<button class="mt-6 !w-full">Join Modrinth Discord</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
:disabled="formattedTime !== '00'"
|
||||
size="large"
|
||||
color="standard"
|
||||
@click="() => reloadNuxtApp()"
|
||||
>
|
||||
<button class="mt-3 !w-full">Reload</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="server.error"
|
||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||
@@ -395,7 +324,6 @@ import { Intercom, shutdown } from "@intercom/messenger-js-sdk";
|
||||
import { reloadNuxtApp } from "#app";
|
||||
import type { ServerState, Stats, WSEvent, WSInstallationResultEvent } from "~/types/servers";
|
||||
import { usePyroConsole } from "~/store/console.ts";
|
||||
import PanelErrorIcon from "~/components/ui/servers/icons/PanelErrorIcon.vue";
|
||||
|
||||
const socket = ref<WebSocket | null>(null);
|
||||
const isReconnecting = ref(false);
|
||||
|
||||
@@ -15,11 +15,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="props.server.general?.upstream" class="flex gap-2">
|
||||
<div v-if="props.server.general?.upstream" class="flex items-center gap-2">
|
||||
<InfoIcon class="hidden sm:block" />
|
||||
<span class="text-sm text-secondary">
|
||||
Changing the mod version may cause unexpected issues. Because your server was created
|
||||
from a modpack, it is recommended to use the modpack's version of the mod.
|
||||
Your server was created from a modpack. Changing the mod version may cause unexpected
|
||||
issues. You can update the modpack version in your server's Options > Platform
|
||||
settings.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -56,9 +57,9 @@
|
||||
<div v-if="server.general && localMods" class="relative isolate flex h-full w-full flex-col">
|
||||
<div ref="pyroContentSentinel" class="sentinel" data-pyro-content-sentinel />
|
||||
<div class="relative flex h-full w-full flex-col">
|
||||
<div class="sticky top-0 z-20 -mt-3 flex items-center justify-between bg-bg py-3">
|
||||
<div class="flex w-full flex-col-reverse items-center gap-2 sm:flex-row">
|
||||
<div class="flex w-full items-center gap-2">
|
||||
<div class="sticky top-0 z-20 -mt-4 flex items-center justify-between bg-bg py-4">
|
||||
<div class="flex w-full flex-col items-center gap-2 sm:flex-row sm:gap-4">
|
||||
<div class="flex w-full items-center gap-2 sm:gap-4">
|
||||
<div class="relative flex-1 text-sm">
|
||||
<label class="sr-only" for="search">Search</label>
|
||||
<SearchIcon
|
||||
@@ -72,7 +73,7 @@
|
||||
type="search"
|
||||
name="search"
|
||||
autocomplete="off"
|
||||
:placeholder="`Search ${localMods.length} ${type.toLocaleLowerCase()}s...`"
|
||||
:placeholder="`Search ${type.toLocaleLowerCase()}s...`"
|
||||
@input="debouncedSearch"
|
||||
/>
|
||||
</div>
|
||||
@@ -87,7 +88,7 @@
|
||||
{ id: 'disabled', action: () => (filterMethod = 'disabled') },
|
||||
]"
|
||||
>
|
||||
<span class="hidden whitespace-pre sm:block">
|
||||
<span class="whitespace-pre text-sm font-medium">
|
||||
{{ filterMethodLabel }}
|
||||
</span>
|
||||
<FilterIcon aria-hidden="true" />
|
||||
@@ -98,71 +99,45 @@
|
||||
</UiServersTeleportOverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div v-if="hasMods" class="flex w-full items-center gap-2 sm:w-fit">
|
||||
<ButtonStyled>
|
||||
<button class="w-full text-nowrap sm:w-fit" @click="initiateFileUpload">
|
||||
<FileIcon />
|
||||
Add file
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<nuxt-link
|
||||
class="w-full text-nowrap sm:w-fit"
|
||||
:to="`/${type.toLocaleLowerCase()}s?sid=${props.server.serverId}`"
|
||||
>
|
||||
<PlusIcon />
|
||||
Add {{ type.toLocaleLowerCase() }}
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<ButtonStyled v-if="hasMods" color="brand" type="outlined">
|
||||
<nuxt-link
|
||||
class="w-full text-nowrap sm:w-fit"
|
||||
:to="`/${type.toLocaleLowerCase()}s?sid=${props.server.serverId}`"
|
||||
>
|
||||
<PlusIcon />
|
||||
Add {{ type.toLocaleLowerCase() }}
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<FilesUploadDropdown
|
||||
v-if="props.server.fs"
|
||||
ref="uploadDropdownRef"
|
||||
class="rounded-xl bg-bg-raised"
|
||||
:margin-bottom="16"
|
||||
:file-type="type"
|
||||
:current-path="'/mods'"
|
||||
:fs="props.server.fs"
|
||||
:accepted-types="acceptFileFromProjectType(type.toLocaleLowerCase()).split(',')"
|
||||
@upload-complete="() => props.server.refresh(['content'])"
|
||||
/>
|
||||
<FilesUploadDragAndDrop
|
||||
v-if="server.general && localMods"
|
||||
class="relative min-h-[50vh]"
|
||||
overlay-class="rounded-xl border-2 border-dashed border-secondary"
|
||||
:type="type"
|
||||
@files-dropped="handleDroppedFiles"
|
||||
>
|
||||
<div v-if="hasFilteredMods" class="flex flex-col gap-2 transition-all">
|
||||
<div ref="listContainer" class="relative w-full">
|
||||
<div :style="{ position: 'relative', height: `${totalHeight}px` }">
|
||||
<div :style="{ position: 'absolute', top: `${visibleTop}px`, width: '100%' }">
|
||||
<template v-for="mod in visibleItems.items" :key="mod.filename">
|
||||
<div
|
||||
class="relative mb-2 flex w-full items-center justify-between rounded-xl bg-bg-raised"
|
||||
:class="mod.disabled ? 'bg-table-alternateRow text-secondary' : ''"
|
||||
style="height: 64px"
|
||||
<div v-if="hasMods" class="flex flex-col gap-2 transition-all">
|
||||
<div ref="listContainer" class="relative w-full">
|
||||
<div :style="{ position: 'relative', height: `${totalHeight}px` }">
|
||||
<div :style="{ position: 'absolute', top: `${visibleTop}px`, width: '100%' }">
|
||||
<template v-for="mod in visibleItems.items" :key="mod.filename">
|
||||
<div
|
||||
class="relative mb-2 flex w-full items-center justify-between rounded-xl bg-bg-raised"
|
||||
:class="mod.disabled ? 'bg-table-alternateRow text-secondary' : ''"
|
||||
style="height: 64px"
|
||||
>
|
||||
<NuxtLink
|
||||
:to="
|
||||
mod.project_id
|
||||
? `/project/${mod.project_id}/version/${mod.version_id}`
|
||||
: `files?path=mods`
|
||||
"
|
||||
class="group flex min-w-0 items-center rounded-xl p-2"
|
||||
>
|
||||
<NuxtLink
|
||||
:to="
|
||||
mod.project_id
|
||||
? `/project/${mod.project_id}/version/${mod.version_id}`
|
||||
: `files?path=mods`
|
||||
"
|
||||
class="flex min-w-0 flex-1 items-center gap-2 rounded-xl p-2"
|
||||
draggable="false"
|
||||
>
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<UiAvatar
|
||||
:src="mod.icon_url"
|
||||
size="sm"
|
||||
alt="Server Icon"
|
||||
:class="mod.disabled ? 'opacity-75 grayscale' : ''"
|
||||
:class="mod.disabled ? 'grayscale' : ''"
|
||||
/>
|
||||
<div class="flex min-w-0 flex-col gap-1">
|
||||
<span class="text-md flex min-w-0 items-center gap-2 font-bold">
|
||||
<span class="truncate text-contrast">{{
|
||||
<div class="flex min-w-0 flex-col">
|
||||
<span class="flex min-w-0 items-center gap-2 text-lg font-bold">
|
||||
<span class="truncate">{{
|
||||
mod.name || mod.filename.replace(".disabled", "")
|
||||
}}</span>
|
||||
<span
|
||||
@@ -171,180 +146,132 @@
|
||||
>Disabled</span
|
||||
>
|
||||
</span>
|
||||
<div class="min-w-0 text-xs text-secondary">
|
||||
<span v-if="mod.owner" class="hidden sm:block"> by {{ mod.owner }} </span>
|
||||
<span class="block font-semibold sm:hidden">
|
||||
{{ mod.version_number || "External mod" }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
<div class="ml-2 hidden min-w-0 flex-1 flex-col text-sm sm:flex">
|
||||
<div class="truncate font-semibold text-contrast">
|
||||
<span v-tooltip="'Mod version'">{{
|
||||
<span class="min-w-0 text-xs text-secondary">{{
|
||||
mod.version_number || "External mod"
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="truncate">
|
||||
<span v-tooltip="'Mod file name'">{{ mod.filename }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center justify-end gap-2 pr-4 font-semibold text-contrast sm:min-w-44"
|
||||
>
|
||||
<ButtonStyled color="red" type="transparent">
|
||||
<button
|
||||
v-tooltip="'Delete mod'"
|
||||
:disabled="mod.changing"
|
||||
class="!hidden sm:!block"
|
||||
@click="removeMod(mod)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="transparent">
|
||||
<button
|
||||
v-tooltip="
|
||||
mod.project_id ? 'Edit mod version' : 'External mods cannot be edited'
|
||||
"
|
||||
:disabled="mod.changing || !mod.project_id"
|
||||
class="!hidden sm:!block"
|
||||
@click="beginChangeModVersion(mod)"
|
||||
>
|
||||
<template v-if="mod.changing">
|
||||
<UiServersIconsLoadingIcon />
|
||||
</template>
|
||||
<template v-else>
|
||||
<EditIcon />
|
||||
</template>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<!-- Dropdown for mobile -->
|
||||
<div class="mr-2 flex items-center sm:hidden">
|
||||
<UiServersIconsLoadingIcon
|
||||
v-if="mod.changing"
|
||||
class="mr-2 h-5 w-5 animate-spin"
|
||||
style="color: var(--color-base)"
|
||||
/>
|
||||
<ButtonStyled v-else circular type="transparent">
|
||||
<UiServersTeleportOverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'edit',
|
||||
action: () => beginChangeModVersion(mod),
|
||||
shown: !!(mod.project_id && !mod.changing),
|
||||
},
|
||||
{
|
||||
id: 'delete',
|
||||
action: () => removeMod(mod),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #edit>
|
||||
<EditIcon class="h-5 w-5" />
|
||||
<span>Edit</span>
|
||||
</template>
|
||||
<template #delete>
|
||||
<TrashIcon class="h-5 w-5" />
|
||||
<span>Delete</span>
|
||||
</template>
|
||||
</UiServersTeleportOverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<input
|
||||
:id="`toggle-${mod.filename}`"
|
||||
:checked="!mod.disabled"
|
||||
</NuxtLink>
|
||||
<div class="flex items-center gap-2 pr-4 font-semibold text-contrast">
|
||||
<ButtonStyled v-if="mod.project_id" type="transparent">
|
||||
<button
|
||||
v-tooltip="'Edit mod version'"
|
||||
:disabled="mod.changing"
|
||||
class="switch stylized-toggle"
|
||||
type="checkbox"
|
||||
@change="toggleMod(mod)"
|
||||
class="!hidden sm:!block"
|
||||
@click="beginChangeModVersion(mod)"
|
||||
>
|
||||
<template v-if="mod.changing">
|
||||
<UiServersIconsLoadingIcon />
|
||||
</template>
|
||||
<template v-else>
|
||||
<EditIcon />
|
||||
</template>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="transparent">
|
||||
<button
|
||||
v-tooltip="'Delete mod'"
|
||||
:disabled="mod.changing"
|
||||
class="!hidden sm:!block"
|
||||
@click="removeMod(mod)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<!-- Dropdown for mobile -->
|
||||
<div class="mr-2 flex items-center sm:hidden">
|
||||
<UiServersIconsLoadingIcon
|
||||
v-if="mod.changing"
|
||||
class="mr-2 h-5 w-5 animate-spin"
|
||||
style="color: var(--color-base)"
|
||||
/>
|
||||
<ButtonStyled v-else circular type="transparent">
|
||||
<UiServersTeleportOverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'edit',
|
||||
action: () => beginChangeModVersion(mod),
|
||||
shown: !!(mod.project_id && !mod.changing),
|
||||
},
|
||||
{
|
||||
id: 'delete',
|
||||
action: () => removeMod(mod),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #edit>
|
||||
<EditIcon class="h-5 w-5" />
|
||||
<span>Edit</span>
|
||||
</template>
|
||||
<template #delete>
|
||||
<TrashIcon class="h-5 w-5" />
|
||||
<span>Delete</span>
|
||||
</template>
|
||||
</UiServersTeleportOverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<input
|
||||
:id="`toggle-${mod.filename}`"
|
||||
:checked="!mod.disabled"
|
||||
:disabled="mod.changing"
|
||||
class="switch stylized-toggle"
|
||||
type="checkbox"
|
||||
@change="toggleMod(mod)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- no mods has platform -->
|
||||
<div
|
||||
v-else-if="
|
||||
props.server.general?.loader &&
|
||||
props.server.general?.loader.toLocaleLowerCase() !== 'vanilla'
|
||||
"
|
||||
class="mt-4 flex h-full flex-col items-center justify-center gap-4 text-center"
|
||||
>
|
||||
<div
|
||||
v-if="!hasFilteredMods && hasMods"
|
||||
class="mt-4 flex h-full flex-col items-center justify-center gap-4 text-center"
|
||||
>
|
||||
<SearchIcon class="size-24" />
|
||||
<p class="m-0 font-bold text-contrast">
|
||||
No {{ type.toLocaleLowerCase() }}s found for your query!
|
||||
</p>
|
||||
<p class="m-0">Try another query, or show everything.</p>
|
||||
<ButtonStyled>
|
||||
<button @click="showAll">
|
||||
<ListIcon />
|
||||
Show everything
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="mt-4 flex h-full flex-col items-center justify-center gap-4 text-center"
|
||||
>
|
||||
<PackageClosedIcon class="size-24" />
|
||||
<p class="m-0 font-bold text-contrast">No {{ type.toLocaleLowerCase() }}s found!</p>
|
||||
<p class="m-0">
|
||||
Add some {{ type.toLocaleLowerCase() }}s to your server to manage them here.
|
||||
</p>
|
||||
<div class="flex flex-row items-center gap-4">
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="w-full text-nowrap sm:w-fit" @click="initiateFileUpload">
|
||||
<FileIcon />
|
||||
Add file
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<nuxt-link
|
||||
class="w-full text-nowrap sm:w-fit"
|
||||
:to="`/${type.toLocaleLowerCase()}s?sid=${props.server.serverId}`"
|
||||
>
|
||||
<PlusIcon />
|
||||
Add {{ type.toLocaleLowerCase() }}
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- no mods has platform -->
|
||||
<div
|
||||
v-else-if="
|
||||
!hasMods &&
|
||||
props.server.general?.loader &&
|
||||
props.server.general?.loader.toLocaleLowerCase() !== 'vanilla'
|
||||
"
|
||||
class="mt-4 flex h-full flex-col items-center justify-center gap-4 text-center"
|
||||
>
|
||||
<PackageClosedIcon class="size-24" />
|
||||
<p class="m-0 font-bold text-contrast">No {{ type.toLocaleLowerCase() }}s found!</p>
|
||||
<p class="m-0">
|
||||
Add some {{ type.toLocaleLowerCase() }}s to your server to manage them here.
|
||||
</p>
|
||||
<ButtonStyled color="brand">
|
||||
<NuxtLink :to="`/${type.toLocaleLowerCase()}s?sid=${props.server.serverId}`">
|
||||
<PlusIcon />
|
||||
Add {{ type.toLocaleLowerCase() }}
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div v-else class="mt-4 flex h-full flex-col items-center justify-center gap-4 text-center">
|
||||
<UiServersIconsLoaderIcon loader="Vanilla" class="size-24" />
|
||||
<p class="m-0 pt-3 font-bold text-contrast">Your server is running Vanilla Minecraft</p>
|
||||
<p class="m-0">
|
||||
Add content to your server by installing a modpack or choosing a different platform that
|
||||
supports {{ type }}s.
|
||||
</p>
|
||||
<div class="flex flex-row items-center gap-4">
|
||||
<ButtonStyled class="mt-8">
|
||||
<NuxtLink :to="`/modpacks?sid=${props.server.serverId}`">
|
||||
<CompassIcon />
|
||||
Find a modpack
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
<div>or</div>
|
||||
<ButtonStyled class="mt-8">
|
||||
<NuxtLink :to="`/servers/manage/${props.server.serverId}/options/loader`">
|
||||
<WrenchIcon />
|
||||
Change platform
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div v-else class="mt-4 flex h-full flex-col items-center justify-center gap-4 text-center">
|
||||
<UiServersIconsLoaderIcon loader="Vanilla" class="size-24" />
|
||||
<p class="m-0 pt-3 font-bold text-contrast">Your server is running Vanilla Minecraft</p>
|
||||
<p class="m-0">
|
||||
Add content to your server by installing a modpack or choosing a different platform that
|
||||
supports {{ type }}s.
|
||||
</p>
|
||||
<div class="flex flex-row items-center gap-4">
|
||||
<ButtonStyled class="mt-8">
|
||||
<NuxtLink :to="`/modpacks?sid=${props.server.serverId}`">
|
||||
<CompassIcon />
|
||||
Find a modpack
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
<div>or</div>
|
||||
<ButtonStyled class="mt-8">
|
||||
<NuxtLink :to="`/servers/manage/${props.server.serverId}/options/loader`">
|
||||
<WrenchIcon />
|
||||
Change platform
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</FilesUploadDragAndDrop>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -363,15 +290,10 @@ import {
|
||||
MoreVerticalIcon,
|
||||
CompassIcon,
|
||||
WrenchIcon,
|
||||
ListIcon,
|
||||
FileIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
|
||||
import FilesUploadDragAndDrop from "~/components/ui/servers/FilesUploadDragAndDrop.vue";
|
||||
import FilesUploadDropdown from "~/components/ui/servers/FilesUploadDropdown.vue";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
import { acceptFileFromProjectType } from "~/helpers/fileUtils.js";
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
@@ -382,7 +304,14 @@ const type = computed(() => {
|
||||
return loader === "paper" || loader === "purpur" ? "Plugin" : "Mod";
|
||||
});
|
||||
|
||||
interface ContentItem extends Mod {
|
||||
interface Mod {
|
||||
name?: string;
|
||||
filename: string;
|
||||
project_id?: string;
|
||||
version_id?: string;
|
||||
version_number?: string;
|
||||
icon_url?: string;
|
||||
disabled: boolean;
|
||||
changing?: boolean;
|
||||
}
|
||||
|
||||
@@ -393,41 +322,12 @@ const listContainer = ref<HTMLElement | null>(null);
|
||||
const windowScrollY = ref(0);
|
||||
const windowHeight = ref(0);
|
||||
|
||||
const localMods = ref<ContentItem[]>([]);
|
||||
const localMods = ref<Mod[]>([]);
|
||||
|
||||
const searchInput = ref("");
|
||||
const modSearchInput = ref("");
|
||||
const filterMethod = ref("all");
|
||||
|
||||
const uploadDropdownRef = ref();
|
||||
|
||||
const handleDroppedFiles = (files: File[]) => {
|
||||
files.forEach((file) => {
|
||||
uploadDropdownRef.value?.uploadFile(file);
|
||||
});
|
||||
};
|
||||
|
||||
const initiateFileUpload = () => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = acceptFileFromProjectType(type.value.toLowerCase());
|
||||
input.multiple = true;
|
||||
input.onchange = () => {
|
||||
if (input.files) {
|
||||
Array.from(input.files).forEach((file) => {
|
||||
uploadDropdownRef.value?.uploadFile(file);
|
||||
});
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
};
|
||||
|
||||
const showAll = () => {
|
||||
searchInput.value = "";
|
||||
modSearchInput.value = "";
|
||||
filterMethod.value = "all";
|
||||
};
|
||||
|
||||
const filterMethodLabel = computed(() => {
|
||||
switch (filterMethod.value) {
|
||||
case "disabled":
|
||||
@@ -519,17 +419,14 @@ const debouncedSearch = debounce(() => {
|
||||
modSearchInput.value = searchInput.value;
|
||||
|
||||
if (pyroContentSentinel.value) {
|
||||
const sentinelRect = pyroContentSentinel.value.getBoundingClientRect();
|
||||
if (sentinelRect.top < 0 || sentinelRect.bottom > window.innerHeight) {
|
||||
pyroContentSentinel.value.scrollIntoView({
|
||||
// behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
}
|
||||
pyroContentSentinel.value.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
}
|
||||
}, 300);
|
||||
|
||||
async function toggleMod(mod: ContentItem) {
|
||||
async function toggleMod(mod: Mod) {
|
||||
mod.changing = true;
|
||||
|
||||
const originalFilename = mod.filename;
|
||||
@@ -561,7 +458,7 @@ async function toggleMod(mod: ContentItem) {
|
||||
mod.changing = false;
|
||||
}
|
||||
|
||||
async function removeMod(mod: ContentItem) {
|
||||
async function removeMod(mod: Mod) {
|
||||
mod.changing = true;
|
||||
|
||||
try {
|
||||
@@ -618,10 +515,6 @@ async function changeModVersion() {
|
||||
}
|
||||
|
||||
const hasMods = computed(() => {
|
||||
return localMods.value?.length > 0;
|
||||
});
|
||||
|
||||
const hasFilteredMods = computed(() => {
|
||||
return filteredMods.value?.length > 0;
|
||||
});
|
||||
|
||||
|
||||
@@ -25,9 +25,12 @@
|
||||
@delete="handleDeleteItem"
|
||||
/>
|
||||
|
||||
<FilesUploadDragAndDrop
|
||||
<div
|
||||
class="relative flex w-full flex-col rounded-2xl border border-solid border-bg-raised"
|
||||
@files-dropped="handleDroppedFiles"
|
||||
@dragenter.prevent="handleDragEnter"
|
||||
@dragover.prevent="handleDragOver"
|
||||
@dragleave.prevent="handleDragLeave"
|
||||
@drop.prevent="handleDrop"
|
||||
>
|
||||
<div ref="mainContent" class="relative isolate flex w-full flex-col">
|
||||
<div v-if="!isEditing" class="contents">
|
||||
@@ -41,14 +44,94 @@
|
||||
@upload="initiateFileUpload"
|
||||
@update:search-query="searchQuery = $event"
|
||||
/>
|
||||
<FilesUploadDropdown
|
||||
v-if="props.server.fs"
|
||||
ref="uploadDropdownRef"
|
||||
class="rounded-b-xl border-0 border-t border-solid border-bg bg-table-alternateRow"
|
||||
:current-path="currentPath"
|
||||
:fs="props.server.fs"
|
||||
@upload-complete="refreshList()"
|
||||
/>
|
||||
<Transition
|
||||
name="upload-status"
|
||||
@enter="onUploadStatusEnter"
|
||||
@leave="onUploadStatusLeave"
|
||||
>
|
||||
<div
|
||||
v-if="isUploading"
|
||||
ref="uploadStatusRef"
|
||||
class="upload-status rounded-b-xl border-0 border-t border-solid border-bg bg-table-alternateRow text-contrast"
|
||||
>
|
||||
<div class="flex flex-col p-4 text-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2 font-bold">
|
||||
<FolderOpenIcon class="size-4" />
|
||||
<span>
|
||||
File Uploads{{
|
||||
activeUploads.length > 0 ? ` - ${activeUploads.length} left` : ""
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 space-y-2">
|
||||
<div
|
||||
v-for="item in uploadQueue"
|
||||
:key="item.file.name"
|
||||
class="flex h-6 items-center justify-between gap-2 text-xs"
|
||||
>
|
||||
<div class="flex flex-1 items-center gap-2 truncate">
|
||||
<transition-group name="status-icon" mode="out-in">
|
||||
<UiServersPanelSpinner
|
||||
v-show="item.status === 'uploading'"
|
||||
key="spinner"
|
||||
class="absolute !size-4"
|
||||
/>
|
||||
<CheckCircleIcon
|
||||
v-show="item.status === 'completed'"
|
||||
key="check"
|
||||
class="absolute size-4 text-green"
|
||||
/>
|
||||
<XCircleIcon
|
||||
v-show="item.status === 'error' || item.status === 'cancelled'"
|
||||
key="error"
|
||||
class="absolute size-4 text-red"
|
||||
/>
|
||||
</transition-group>
|
||||
<span class="ml-6 truncate">{{ item.file.name }}</span>
|
||||
<span class="text-secondary">{{ item.size }}</span>
|
||||
</div>
|
||||
<div class="flex min-w-[80px] items-center justify-end gap-2">
|
||||
<template v-if="item.status === 'completed'">
|
||||
<span>Done</span>
|
||||
</template>
|
||||
<template v-else-if="item.status === 'error'">
|
||||
<span class="text-red">Failed - File already exists</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="item.status === 'uploading'">
|
||||
<span>{{ item.progress }}%</span>
|
||||
<div class="h-1 w-20 overflow-hidden rounded-full bg-bg">
|
||||
<div
|
||||
class="h-full bg-contrast transition-all duration-200"
|
||||
:style="{ width: item.progress + '%' }"
|
||||
/>
|
||||
</div>
|
||||
<ButtonStyled color="red" type="transparent" @click="cancelUpload(item)">
|
||||
<button>Cancel</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<template v-else-if="item.status === 'cancelled'">
|
||||
<span class="text-red">Cancelled</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>{{ item.progress }}%</span>
|
||||
<div class="h-1 w-20 overflow-hidden rounded-full bg-bg">
|
||||
<div
|
||||
class="h-full bg-contrast transition-all duration-200"
|
||||
:style="{ width: item.progress + '%' }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<UiServersFilesEditingNavbar
|
||||
@@ -137,7 +220,7 @@
|
||||
<p class="mt-2 text-xl">Drop files here to upload</p>
|
||||
</div>
|
||||
</div>
|
||||
</FilesUploadDragAndDrop>
|
||||
</div>
|
||||
|
||||
<UiServersFilesContextMenu
|
||||
ref="contextMenu"
|
||||
@@ -155,10 +238,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useInfiniteScroll } from "@vueuse/core";
|
||||
import { UploadIcon, FolderOpenIcon } from "@modrinth/assets";
|
||||
import { UploadIcon, FolderOpenIcon, CheckCircleIcon, XCircleIcon } from "@modrinth/assets";
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import type { DirectoryResponse, DirectoryItem, Server } from "~/composables/pyroServers";
|
||||
import FilesUploadDragAndDrop from "~/components/ui/servers/FilesUploadDragAndDrop.vue";
|
||||
import FilesUploadDropdown from "~/components/ui/servers/FilesUploadDropdown.vue";
|
||||
|
||||
interface BaseOperation {
|
||||
type: "move" | "rename";
|
||||
@@ -181,6 +263,14 @@ interface RenameOperation extends BaseOperation {
|
||||
|
||||
type Operation = MoveOperation | RenameOperation;
|
||||
|
||||
interface UploadItem {
|
||||
file: File;
|
||||
progress: number;
|
||||
status: "pending" | "uploading" | "completed" | "error" | "cancelled";
|
||||
size: string;
|
||||
uploader?: any;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
}>();
|
||||
@@ -222,8 +312,46 @@ const isEditingImage = ref(false);
|
||||
const imagePreview = ref();
|
||||
|
||||
const isDragging = ref(false);
|
||||
const dragCounter = ref(0);
|
||||
|
||||
const uploadDropdownRef = ref();
|
||||
const uploadStatusRef = ref<HTMLElement | null>(null);
|
||||
const isUploading = computed(() => uploadQueue.value.length > 0);
|
||||
const uploadQueue = ref<UploadItem[]>([]);
|
||||
|
||||
const activeUploads = computed(() =>
|
||||
uploadQueue.value.filter((item) => item.status === "pending" || item.status === "uploading"),
|
||||
);
|
||||
|
||||
const onUploadStatusEnter = (el: Element) => {
|
||||
const height = (el as HTMLElement).scrollHeight;
|
||||
(el as HTMLElement).style.height = "0";
|
||||
// eslint-disable-next-line no-void
|
||||
void (el as HTMLElement).offsetHeight;
|
||||
(el as HTMLElement).style.height = `${height}px`;
|
||||
};
|
||||
|
||||
const onUploadStatusLeave = (el: Element) => {
|
||||
const height = (el as HTMLElement).scrollHeight;
|
||||
(el as HTMLElement).style.height = `${height}px`;
|
||||
// eslint-disable-next-line no-void
|
||||
void (el as HTMLElement).offsetHeight;
|
||||
(el as HTMLElement).style.height = "0";
|
||||
};
|
||||
|
||||
watch(
|
||||
uploadQueue,
|
||||
() => {
|
||||
if (!uploadStatusRef.value) return;
|
||||
const el = uploadStatusRef.value;
|
||||
const itemsHeight = uploadQueue.value.length * 32;
|
||||
const headerHeight = 12;
|
||||
const gap = 8;
|
||||
const padding = 32;
|
||||
const totalHeight = padding + headerHeight + gap + itemsHeight;
|
||||
el.style.height = `${totalHeight}px`;
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const data = computed(() => props.server.general);
|
||||
|
||||
@@ -789,12 +917,135 @@ const requestShareLink = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDroppedFiles = (files: File[]) => {
|
||||
const handleDragEnter = (event: DragEvent) => {
|
||||
if (isEditing.value) return;
|
||||
event.preventDefault();
|
||||
if (!event.dataTransfer?.types.includes("application/pyro-file-move")) {
|
||||
dragCounter.value++;
|
||||
isDragging.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
files.forEach((file) => {
|
||||
uploadDropdownRef.value?.uploadFile(file);
|
||||
});
|
||||
const handleDragOver = (event: DragEvent) => {
|
||||
if (isEditing.value) return;
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const handleDragLeave = (event: DragEvent) => {
|
||||
if (isEditing.value) return;
|
||||
event.preventDefault();
|
||||
dragCounter.value--;
|
||||
if (dragCounter.value === 0) {
|
||||
isDragging.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line require-await
|
||||
const handleDrop = async (event: DragEvent) => {
|
||||
if (isEditing.value) return;
|
||||
event.preventDefault();
|
||||
isDragging.value = false;
|
||||
dragCounter.value = 0;
|
||||
|
||||
const isInternalMove = event.dataTransfer?.types.includes("application/pyro-file-move");
|
||||
if (isInternalMove) return;
|
||||
|
||||
const files = event.dataTransfer?.files;
|
||||
if (files) {
|
||||
Array.from(files).forEach((file) => {
|
||||
uploadFile(file);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes < 1024) return bytes + " B";
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
|
||||
};
|
||||
|
||||
const cancelUpload = (item: UploadItem) => {
|
||||
if (item.uploader && item.status === "uploading") {
|
||||
item.uploader.cancel();
|
||||
item.status = "cancelled";
|
||||
|
||||
setTimeout(async () => {
|
||||
const index = uploadQueue.value.findIndex((qItem) => qItem.file.name === item.file.name);
|
||||
if (index !== -1) {
|
||||
uploadQueue.value.splice(index, 1);
|
||||
await nextTick();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
const uploadFile = async (file: File) => {
|
||||
const uploadItem: UploadItem = {
|
||||
file,
|
||||
progress: 0,
|
||||
status: "pending",
|
||||
size: formatFileSize(file.size),
|
||||
};
|
||||
|
||||
uploadQueue.value.push(uploadItem);
|
||||
|
||||
try {
|
||||
uploadItem.status = "uploading";
|
||||
const filePath = `${currentPath.value}/${file.name}`.replace("//", "/");
|
||||
const uploader = await props.server.fs?.uploadFile(filePath, file);
|
||||
uploadItem.uploader = uploader;
|
||||
|
||||
if (uploader?.onProgress) {
|
||||
uploader.onProgress(({ progress }: { progress: number }) => {
|
||||
const index = uploadQueue.value.findIndex((item) => item.file.name === file.name);
|
||||
if (index !== -1) {
|
||||
uploadQueue.value[index].progress = Math.round(progress);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await uploader?.promise;
|
||||
const index = uploadQueue.value.findIndex((item) => item.file.name === file.name);
|
||||
if (index !== -1 && uploadQueue.value[index].status !== "cancelled") {
|
||||
uploadQueue.value[index].status = "completed";
|
||||
uploadQueue.value[index].progress = 100;
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
|
||||
setTimeout(async () => {
|
||||
const removeIndex = uploadQueue.value.findIndex((item) => item.file.name === file.name);
|
||||
if (removeIndex !== -1) {
|
||||
uploadQueue.value.splice(removeIndex, 1);
|
||||
await nextTick();
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
await refreshList();
|
||||
} catch (error) {
|
||||
console.error("Error uploading file:", error);
|
||||
const index = uploadQueue.value.findIndex((item) => item.file.name === file.name);
|
||||
if (index !== -1 && uploadQueue.value[index].status !== "cancelled") {
|
||||
uploadQueue.value[index].status = "error";
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
const removeIndex = uploadQueue.value.findIndex((item) => item.file.name === file.name);
|
||||
if (removeIndex !== -1) {
|
||||
uploadQueue.value.splice(removeIndex, 1);
|
||||
await nextTick();
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
if (error instanceof Error && error.message !== "Upload cancelled") {
|
||||
addNotification({
|
||||
group: "files",
|
||||
title: "Upload failed",
|
||||
text: `Failed to upload ${file.name}`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const initiateFileUpload = () => {
|
||||
@@ -804,7 +1055,7 @@ const initiateFileUpload = () => {
|
||||
input.onchange = () => {
|
||||
if (input.files) {
|
||||
Array.from(input.files).forEach((file) => {
|
||||
uploadDropdownRef.value?.uploadFile(file);
|
||||
uploadFile(file);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -237,11 +237,24 @@ interface ErrorData {
|
||||
}
|
||||
|
||||
const inspectingError = ref<ErrorData | null>(null);
|
||||
const mcError = ref<any>(null);
|
||||
|
||||
const inspectError = async () => {
|
||||
const log = await props.server.fs?.downloadFile("logs/latest.log");
|
||||
const response = (await $fetch("https://api.mclo.gs/1/log", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
content: log,
|
||||
}),
|
||||
})) as any;
|
||||
|
||||
mcError.value = response;
|
||||
|
||||
// @ts-ignore
|
||||
const analysis = (await $fetch(`https://api.mclo.gs/1/analyse`, {
|
||||
const analysis = (await $fetch(`https://api.mclo.gs/1/insights/${response.id}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
@@ -256,6 +269,7 @@ const inspectError = async () => {
|
||||
|
||||
const clearError = () => {
|
||||
inspectingError.value = null;
|
||||
mcError.value = null;
|
||||
};
|
||||
|
||||
watch(
|
||||
|
||||
@@ -330,11 +330,11 @@
|
||||
<UploadIcon class="size-4" /> Upload .mrpack file
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="hasNewerVersion" color="brand">
|
||||
<DownloadIcon v-if="hasNewerVersion" color="brand">
|
||||
<button class="!w-full sm:!w-auto" @click="handleUpdateToLatest">
|
||||
<UploadIcon class="size-4" /> Update modpack
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</DownloadIcon>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="data.upstream" class="contents">
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</template>
|
||||
</span>
|
||||
⋅
|
||||
<span>{{ formatPrice(vintl.locale, charge.amount, charge.currency_code) }}</span>
|
||||
<span>{{ formatPrice(charge.amount, charge.currency_code) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Badge :color="charge.status === 'succeeded' ? 'green' : 'red'" :type="charge.status" />
|
||||
@@ -39,7 +39,6 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import { Breadcrumbs, Badge } from "@modrinth/ui";
|
||||
import { formatPrice } from "@modrinth/utils";
|
||||
import { products } from "~/generated/state.json";
|
||||
|
||||
definePageMeta({
|
||||
@@ -67,4 +66,19 @@ const { data: charges } = await useAsyncData(
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// TODO move to omorphia utils , duplicated from index
|
||||
function formatPrice(price, currency) {
|
||||
const formatter = new Intl.NumberFormat(vintl.locale, {
|
||||
style: "currency",
|
||||
currency,
|
||||
});
|
||||
|
||||
const maxDigits = formatter.resolvedOptions().maximumFractionDigits;
|
||||
|
||||
const convertedPrice = price / Math.pow(10, maxDigits);
|
||||
|
||||
return formatter.format(convertedPrice);
|
||||
}
|
||||
console.log(charges);
|
||||
</script>
|
||||
|
||||
@@ -257,7 +257,7 @@
|
||||
v-else-if="getPyroCharge(subscription).status === 'processing'"
|
||||
class="text-sm text-orange"
|
||||
>
|
||||
Your payment is being processed. Your server will activate once payment is
|
||||
Your payment is being processed. Perks will activate once payment is
|
||||
complete.
|
||||
</span>
|
||||
<span
|
||||
@@ -270,8 +270,7 @@
|
||||
v-else-if="getPyroCharge(subscription).status === 'failed'"
|
||||
class="text-sm text-red"
|
||||
>
|
||||
Your subscription payment failed. Please update your payment method, then
|
||||
resubscribe.
|
||||
Your subscription payment failed. Please update your payment method.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -279,8 +278,7 @@
|
||||
<ButtonStyled
|
||||
v-if="
|
||||
getPyroCharge(subscription) &&
|
||||
getPyroCharge(subscription).status !== 'cancelled' &&
|
||||
getPyroCharge(subscription).status !== 'failed'
|
||||
getPyroCharge(subscription).status !== 'cancelled'
|
||||
"
|
||||
type="standard"
|
||||
@click="showPyroCancelModal(subscription.id)"
|
||||
@@ -293,8 +291,7 @@
|
||||
<ButtonStyled
|
||||
v-else-if="
|
||||
getPyroCharge(subscription) &&
|
||||
(getPyroCharge(subscription).status === 'cancelled' ||
|
||||
getPyroCharge(subscription).status === 'failed')
|
||||
getPyroCharge(subscription).status === 'cancelled'
|
||||
"
|
||||
type="standard"
|
||||
color="green"
|
||||
|
||||
@@ -2,57 +2,6 @@
|
||||
<div v-if="user" class="experimental-styles-within">
|
||||
<ModalCreation ref="modal_creation" />
|
||||
<CollectionCreateModal ref="modal_collection_creation" />
|
||||
<NewModal v-if="auth.user && isStaff(auth.user)" ref="userDetailsModal" header="User details">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-lg font-bold text-primary">Email</span>
|
||||
<div>
|
||||
<span
|
||||
v-tooltip="user.email_verified ? 'Email verified' : 'Email not verified'"
|
||||
class="flex w-fit items-center gap-1"
|
||||
>
|
||||
<span>{{ user.email }}</span>
|
||||
<CheckIcon v-if="user.email_verified" class="h-4 w-4 text-brand" />
|
||||
<XIcon v-else class="h-4 w-4 text-red" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-lg font-bold text-primary"> Auth providers </span>
|
||||
<span>{{ user.auth_providers.join(", ") }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-lg font-bold text-primary"> Payment methods</span>
|
||||
<span>
|
||||
<template v-if="user.payout_data?.paypal_address">
|
||||
Paypal ({{ user.payout_data.paypal_address }} - {{ user.payout_data.paypal_country }})
|
||||
</template>
|
||||
<template v-if="user.payout_data?.paypal_address && user.payout_data?.venmo_address">
|
||||
,
|
||||
</template>
|
||||
<template v-if="user.payout_data?.venmo_address">
|
||||
Venmo ({{ user.payout_data.venmo_address }})
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-lg font-bold text-primary"> Has password </span>
|
||||
<span>
|
||||
{{ user.has_password ? "Yes" : "No" }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-lg font-bold text-primary"> Has TOTP </span>
|
||||
<span>
|
||||
{{ user.has_totp ? "Yes" : "No" }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
<div class="new-page sidebar" :class="{ 'alt-layout': cosmetics.leftContentLayout }">
|
||||
<div class="normal-page__header py-4">
|
||||
<ContentPageHeader>
|
||||
@@ -125,16 +74,6 @@
|
||||
shown: auth.user?.id !== user.id,
|
||||
},
|
||||
{ id: 'copy-id', action: () => copyId() },
|
||||
{
|
||||
id: 'open-billing',
|
||||
action: () => navigateTo(`/admin/billing/${user.id}`),
|
||||
shown: auth.user && isStaff(auth.user),
|
||||
},
|
||||
{
|
||||
id: 'open-info',
|
||||
action: () => $refs.userDetailsModal.show(),
|
||||
shown: auth.user && isStaff(auth.user),
|
||||
},
|
||||
]"
|
||||
aria-label="More options"
|
||||
>
|
||||
@@ -151,14 +90,6 @@
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.copyIdButton) }}
|
||||
</template>
|
||||
<template #open-billing>
|
||||
<CurrencyIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.billingButton) }}
|
||||
</template>
|
||||
<template #open-info>
|
||||
<InfoIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.infoButton) }}
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
@@ -333,18 +264,8 @@ import {
|
||||
DownloadIcon,
|
||||
ClipboardCopyIcon,
|
||||
MoreVerticalIcon,
|
||||
CurrencyIcon,
|
||||
InfoIcon,
|
||||
CheckIcon,
|
||||
} from "@modrinth/assets";
|
||||
import {
|
||||
OverflowMenu,
|
||||
ButtonStyled,
|
||||
ContentPageHeader,
|
||||
commonMessages,
|
||||
NewModal,
|
||||
} from "@modrinth/ui";
|
||||
import { isStaff } from "~/helpers/users.js";
|
||||
import { OverflowMenu, ButtonStyled, ContentPageHeader, commonMessages } from "@modrinth/ui";
|
||||
import NavTabs from "~/components/ui/NavTabs.vue";
|
||||
import ProjectCard from "~/components/ui/ProjectCard.vue";
|
||||
import { reportUser } from "~/utils/report-helpers.ts";
|
||||
@@ -446,14 +367,6 @@ const messages = defineMessages({
|
||||
defaultMessage:
|
||||
"You don't have any collections.\nWould you like to <create-link>create one</create-link>?",
|
||||
},
|
||||
billingButton: {
|
||||
id: "profile.button.billing",
|
||||
defaultMessage: "Manage user billing",
|
||||
},
|
||||
infoButton: {
|
||||
id: "profile.button.info",
|
||||
defaultMessage: "View user details",
|
||||
},
|
||||
userNotFoundError: {
|
||||
id: "profile.error.not-found",
|
||||
defaultMessage: "User not found",
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import dayjs from "dayjs";
|
||||
import quarterOfYear from "dayjs/plugin/quarterOfYear";
|
||||
|
||||
dayjs.extend(quarterOfYear);
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
return {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// export interface Mod {
|
||||
// id: string;
|
||||
// filename: string;
|
||||
// modrinth_ids: {
|
||||
// project_id: string;
|
||||
// version_id: string;
|
||||
// };
|
||||
// }
|
||||
export interface Mod {
|
||||
id: string;
|
||||
filename: string;
|
||||
modrinth_ids: {
|
||||
project_id: string;
|
||||
version_id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface License {
|
||||
id: string;
|
||||
|
||||
@@ -304,10 +304,13 @@ export const useFetchAllAnalytics = (
|
||||
projects,
|
||||
selectedProjects,
|
||||
personalRevenue = false,
|
||||
startDate = ref(dayjs().subtract(30, "days")),
|
||||
endDate = ref(dayjs()),
|
||||
timeResolution = ref(1440),
|
||||
) => {
|
||||
const timeResolution = ref(1440); // 1 day
|
||||
const timeRange = ref(43200); // 30 days
|
||||
|
||||
const startDate = ref(Date.now() - timeRange.value * 60 * 1000);
|
||||
const endDate = ref(Date.now());
|
||||
|
||||
const downloadData = ref(null);
|
||||
const viewData = ref(null);
|
||||
const revenueData = ref(null);
|
||||
@@ -391,8 +394,8 @@ export const useFetchAllAnalytics = (
|
||||
[() => startDate.value, () => endDate.value, () => timeResolution.value, () => projects.value],
|
||||
async () => {
|
||||
const q = {
|
||||
start_date: startDate.value.toISOString(),
|
||||
end_date: endDate.value.toISOString(),
|
||||
start_date: dayjs(startDate.value).toISOString(),
|
||||
end_date: dayjs(endDate.value).toISOString(),
|
||||
resolution_minutes: timeResolution.value,
|
||||
};
|
||||
|
||||
@@ -439,6 +442,7 @@ export const useFetchAllAnalytics = (
|
||||
return {
|
||||
// Configuration
|
||||
timeResolution,
|
||||
timeRange,
|
||||
|
||||
startDate,
|
||||
endDate,
|
||||
|
||||
@@ -68,9 +68,6 @@ PAYPAL_API_URL=https://api-m.sandbox.paypal.com/v1/
|
||||
PAYPAL_WEBHOOK_ID=none
|
||||
PAYPAL_CLIENT_ID=none
|
||||
PAYPAL_CLIENT_SECRET=none
|
||||
PAYPAL_NVP_USERNAME=none
|
||||
PAYPAL_NVP_PASSWORD=none
|
||||
PAYPAL_NVP_SIGNATURE=none
|
||||
|
||||
STEAM_API_KEY=none
|
||||
|
||||
@@ -109,10 +106,4 @@ STRIPE_WEBHOOK_SECRET=none
|
||||
|
||||
ADITUDE_API_KEY=none
|
||||
|
||||
PYRO_API_KEY=none
|
||||
|
||||
BREX_API_URL=https://platform.brexapis.com/v2/
|
||||
BREX_API_KEY=none
|
||||
|
||||
DELPHI_URL=none
|
||||
DELPHI_SLACK_WEBHOOK=none
|
||||
PYRO_API_KEY=none
|
||||
@@ -99,7 +99,7 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "109b6307ff4b06297ddd45ac2385e6a445ee4a4d4f447816dfcd059892c8b68b"
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6bcbb0c584804c492ccee49ba0449a8a1cd88fa5d85d4cd6533f65d4c8021634"
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "7fb6f7e0b2b993d4b89146fdd916dbd7efe31a42127f15c9617caa00d60b7bcc"
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "8f51f552d1c63fa818cbdba581691e97f693a187ed05224a44adeee68c6809d1"
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "99cca53fd3f35325e2da3b671532bf98b8c7ad8e7cb9158e4eb9c5bac66d20b2"
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "bfcbcadda1e323d56b6a21fc060c56bff2f38a54cf65dd1cc21f209240c7091b"
|
||||
|
||||
@@ -11,14 +11,14 @@ name = "labrinth"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4.4.1"
|
||||
actix-rt = "2.9.0"
|
||||
actix-multipart = "0.6.1"
|
||||
actix-cors = "0.7.0"
|
||||
actix-ws = "0.3.0"
|
||||
actix-files = "0.6.5"
|
||||
actix-web-prom = { version = "0.8.0", features = ["process"] }
|
||||
governor = "0.6.3"
|
||||
ntex = { version = "2.0", features = ["tokio", "compress"] }
|
||||
# actix-rt = "2.9.0"
|
||||
ntex-multipart = "2.0.0"
|
||||
ntex-cors = "2.0.0"
|
||||
# actix-ws = "0.3.0"
|
||||
ntex-files = "2.0.0"
|
||||
# actix-web-prom = { version = "0.8.0", features = ["process"] }
|
||||
governor = "0.8.0"
|
||||
|
||||
tokio = { version = "1.35.1", features = ["sync"] }
|
||||
tokio-stream = "0.1.14"
|
||||
@@ -30,7 +30,7 @@ async-trait = "0.1.70"
|
||||
dashmap = "5.4.0"
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
meilisearch-sdk = "0.27.1"
|
||||
meilisearch-sdk = "0.24.3"
|
||||
rust-s3 = "0.33.0"
|
||||
reqwest = { version = "0.11.18", features = ["json", "multipart"] }
|
||||
hyper = { version = "0.14", features = ["full"] }
|
||||
@@ -97,15 +97,15 @@ maxminddb = "0.24.0"
|
||||
flate2 = "1.0.25"
|
||||
tar = "0.4.38"
|
||||
|
||||
sentry = { version = "0.34.0", default-features = false, features = [
|
||||
"backtrace",
|
||||
"contexts",
|
||||
"debug-images",
|
||||
"panic",
|
||||
"rustls",
|
||||
"reqwest",
|
||||
] }
|
||||
sentry-actix = "0.34.0"
|
||||
#sentry = { version = "0.34.0", default-features = false, features = [
|
||||
# "backtrace",
|
||||
# "contexts",
|
||||
# "debug-images",
|
||||
# "panic",
|
||||
# "rustls",
|
||||
# "reqwest",
|
||||
#] }
|
||||
#sentry-actix = "0.34.0"
|
||||
|
||||
image = "0.24.6"
|
||||
color-thief = "0.2.2"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM rust:1.84.0 as build
|
||||
FROM rust:1.81.0 as build
|
||||
ENV PKG_CONFIG_ALLOW_CROSS=1
|
||||
|
||||
WORKDIR /usr/src/labrinth
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
ALTER TABLE version_fields
|
||||
DROP CONSTRAINT version_fields_enum_value_fkey;
|
||||
|
||||
ALTER TABLE version_fields
|
||||
ALTER COLUMN enum_value SET DEFAULT -1;
|
||||
|
||||
UPDATE version_fields SET enum_value = -1 WHERE enum_value IS NULL;
|
||||
|
||||
ALTER TABLE version_fields
|
||||
ALTER COLUMN enum_value SET NOT NULL;
|
||||
|
||||
WITH CTE AS (
|
||||
SELECT ctid,
|
||||
ROW_NUMBER() OVER (PARTITION BY version_id, field_id, enum_value ORDER BY ctid) AS row_num
|
||||
FROM version_fields
|
||||
)
|
||||
DELETE FROM version_fields
|
||||
WHERE ctid IN (
|
||||
SELECT ctid
|
||||
FROM CTE
|
||||
WHERE row_num > 1
|
||||
);
|
||||
|
||||
ALTER TABLE version_fields
|
||||
ADD PRIMARY KEY (version_id, field_id, enum_value);
|
||||
|
||||
ALTER TABLE loader_fields_loaders
|
||||
ADD PRIMARY KEY (loader_id, loader_field_id);
|
||||
@@ -15,8 +15,8 @@ pub use validate::{check_is_moderator_from_headers, get_user_from_headers};
|
||||
|
||||
use crate::file_hosting::FileHostingError;
|
||||
use crate::models::error::ApiError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::HttpResponse;
|
||||
use ntex::http::StatusCode;
|
||||
use ntex::web::{HttpRequest, HttpResponse};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
@@ -51,7 +51,7 @@ pub enum AuthenticationError {
|
||||
Url,
|
||||
}
|
||||
|
||||
impl actix_web::ResponseError for AuthenticationError {
|
||||
impl ntex::web::WebResponseError for AuthenticationError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
AuthenticationError::Env(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
@@ -77,8 +77,8 @@ impl actix_web::ResponseError for AuthenticationError {
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::build(self.status_code()).json(ApiError {
|
||||
fn error_response(&self, _req: &HttpRequest) -> HttpResponse {
|
||||
HttpResponse::build(self.status_code()).json(&ApiError {
|
||||
error: self.error_name(),
|
||||
description: self.to_string(),
|
||||
})
|
||||
|
||||
@@ -2,8 +2,8 @@ use super::ValidatedRedirectUri;
|
||||
use crate::auth::AuthenticationError;
|
||||
use crate::models::error::ApiError;
|
||||
use crate::models::ids::DecodingError;
|
||||
use actix_web::http::{header::LOCATION, StatusCode};
|
||||
use actix_web::HttpResponse;
|
||||
use ntex::http::{header::LOCATION, StatusCode};
|
||||
use ntex::web::{HttpRequest, HttpResponse};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error("{}", .error_type)]
|
||||
@@ -55,7 +55,7 @@ impl OAuthError {
|
||||
}
|
||||
}
|
||||
|
||||
impl actix_web::ResponseError for OAuthError {
|
||||
impl ntex::web::WebResponseError for OAuthError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self.error_type {
|
||||
OAuthErrorType::AuthenticationError(_)
|
||||
@@ -83,7 +83,7 @@ impl actix_web::ResponseError for OAuthError {
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
fn error_response(&self, _req: &HttpRequest) -> HttpResponse {
|
||||
if let Some(ValidatedRedirectUri(mut redirect_uri)) =
|
||||
self.valid_redirect_uri.clone()
|
||||
{
|
||||
@@ -99,10 +99,10 @@ impl actix_web::ResponseError for OAuthError {
|
||||
}
|
||||
|
||||
HttpResponse::Ok()
|
||||
.append_header((LOCATION, redirect_uri.clone()))
|
||||
.header(LOCATION, redirect_uri.clone())
|
||||
.body(redirect_uri)
|
||||
} else {
|
||||
HttpResponse::build(self.status_code()).json(ApiError {
|
||||
HttpResponse::build(self.status_code()).json(&ApiError {
|
||||
error: &self.error_type.error_name(),
|
||||
description: self.error_type.to_string(),
|
||||
})
|
||||
|
||||
@@ -14,14 +14,14 @@ use crate::models;
|
||||
use crate::models::ids::OAuthClientId;
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use actix_web::http::header::LOCATION;
|
||||
use actix_web::web::{Data, Query, ServiceConfig};
|
||||
use actix_web::{get, post, web, HttpRequest, HttpResponse};
|
||||
use chrono::Duration;
|
||||
use ntex::http::header::LOCATION;
|
||||
use ntex::http::header::{CACHE_CONTROL, PRAGMA};
|
||||
use ntex::web::ServiceConfig;
|
||||
use ntex::web::{self, get, post, HttpRequest, HttpResponse};
|
||||
use rand::distributions::Alphanumeric;
|
||||
use rand::{Rng, SeedableRng};
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use reqwest::header::{CACHE_CONTROL, PRAGMA};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::postgres::PgPool;
|
||||
|
||||
@@ -59,14 +59,14 @@ pub struct OAuthClientAccessRequest {
|
||||
#[get("authorize")]
|
||||
pub async fn init_oauth(
|
||||
req: HttpRequest,
|
||||
Query(oauth_info): Query<OAuthInit>,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
web::types::Query(oauth_info): web::types::Query<OAuthInit>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
session_queue: web::types::State<AuthQueue>,
|
||||
) -> Result<HttpResponse, OAuthError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&*pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_AUTH_WRITE]),
|
||||
@@ -75,7 +75,7 @@ pub async fn init_oauth(
|
||||
.1;
|
||||
|
||||
let client_id = oauth_info.client_id.into();
|
||||
let client = DBOAuthClient::get(client_id, &**pool).await?;
|
||||
let client = DBOAuthClient::get(client_id, &*pool).await?;
|
||||
|
||||
if let Some(client) = client {
|
||||
let redirect_uri = ValidatedRedirectUri::validate(
|
||||
@@ -107,7 +107,7 @@ pub async fn init_oauth(
|
||||
}
|
||||
|
||||
let existing_authorization =
|
||||
OAuthClientAuthorization::get(client.id, user.id.into(), &**pool)
|
||||
OAuthClientAuthorization::get(client.id, user.id.into(), &*pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
OAuthError::redirect(e, &oauth_info.state, &redirect_uri)
|
||||
@@ -154,7 +154,7 @@ pub async fn init_oauth(
|
||||
flow_id,
|
||||
requested_scopes,
|
||||
};
|
||||
Ok(HttpResponse::Ok().json(access_request))
|
||||
Ok(HttpResponse::Ok().json(&access_request))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -172,10 +172,10 @@ pub struct RespondToOAuthClientScopes {
|
||||
#[post("accept")]
|
||||
pub async fn accept_client_scopes(
|
||||
req: HttpRequest,
|
||||
accept_body: web::Json<RespondToOAuthClientScopes>,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
accept_body: web::types::Json<RespondToOAuthClientScopes>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
session_queue: web::types::State<AuthQueue>,
|
||||
) -> Result<HttpResponse, OAuthError> {
|
||||
accept_or_reject_client_scopes(
|
||||
true,
|
||||
@@ -191,10 +191,10 @@ pub async fn accept_client_scopes(
|
||||
#[post("reject")]
|
||||
pub async fn reject_client_scopes(
|
||||
req: HttpRequest,
|
||||
body: web::Json<RespondToOAuthClientScopes>,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
body: web::types::Json<RespondToOAuthClientScopes>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
session_queue: web::types::State<AuthQueue>,
|
||||
) -> Result<HttpResponse, OAuthError> {
|
||||
accept_or_reject_client_scopes(false, req, body, pool, redis, session_queue)
|
||||
.await
|
||||
@@ -221,12 +221,12 @@ pub struct TokenResponse {
|
||||
/// Per IETF RFC6749 Section 4.1.3 (https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3)
|
||||
pub async fn request_token(
|
||||
req: HttpRequest,
|
||||
req_params: web::Form<TokenRequest>,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
req_params: web::types::Form<TokenRequest>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
) -> Result<HttpResponse, OAuthError> {
|
||||
let req_client_id = req_params.client_id;
|
||||
let client = DBOAuthClient::get(req_client_id.into(), &**pool).await?;
|
||||
let client = DBOAuthClient::get(req_client_id.into(), &*pool).await?;
|
||||
if let Some(client) = client {
|
||||
authenticate_client_token_request(&req, &client)?;
|
||||
|
||||
@@ -294,9 +294,9 @@ pub async fn request_token(
|
||||
|
||||
// IETF RFC6749 Section 5.1 (https://datatracker.ietf.org/doc/html/rfc6749#section-5.1)
|
||||
Ok(HttpResponse::Ok()
|
||||
.append_header((CACHE_CONTROL, "no-store"))
|
||||
.append_header((PRAGMA, "no-cache"))
|
||||
.json(TokenResponse {
|
||||
.header(CACHE_CONTROL, "no-store")
|
||||
.header(PRAGMA, "no-cache")
|
||||
.json(&TokenResponse {
|
||||
access_token: token,
|
||||
token_type: "Bearer".to_string(),
|
||||
expires_in: time_until_expiration.num_seconds(),
|
||||
@@ -314,14 +314,14 @@ pub async fn request_token(
|
||||
pub async fn accept_or_reject_client_scopes(
|
||||
accept: bool,
|
||||
req: HttpRequest,
|
||||
body: web::Json<RespondToOAuthClientScopes>,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
body: web::types::Json<RespondToOAuthClientScopes>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
session_queue: web::types::State<AuthQueue>,
|
||||
) -> Result<HttpResponse, OAuthError> {
|
||||
let current_user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&*pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
@@ -449,7 +449,7 @@ async fn init_oauth_code_flow(
|
||||
|
||||
// IETF RFC 6749 Section 4.1.2 (https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2)
|
||||
Ok(HttpResponse::Ok()
|
||||
.append_header((LOCATION, redirect_uri.clone()))
|
||||
.header(LOCATION, redirect_uri.clone())
|
||||
.body(redirect_uri))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::auth::AuthenticationError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{HttpResponse, ResponseError};
|
||||
use ntex::http::StatusCode;
|
||||
use ntex::web::{HttpRequest, HttpResponse, WebResponseError};
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
|
||||
pub struct Success<'a> {
|
||||
@@ -13,7 +13,7 @@ impl Success<'_> {
|
||||
let html = include_str!("success.html");
|
||||
|
||||
HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "text/html; charset=utf-8"))
|
||||
.header("Content-Type", "text/html; charset=utf-8")
|
||||
.body(
|
||||
html.replace("{{ icon }}", self.icon)
|
||||
.replace("{{ name }}", self.name),
|
||||
@@ -41,17 +41,17 @@ impl Display for ErrorPage {
|
||||
impl ErrorPage {
|
||||
pub fn render(&self) -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "text/html; charset=utf-8"))
|
||||
.header("Content-Type", "text/html; charset=utf-8")
|
||||
.body(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl actix_web::ResponseError for ErrorPage {
|
||||
impl WebResponseError for ErrorPage {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
self.code
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
fn error_response(&self, _req: &HttpRequest) -> HttpResponse {
|
||||
self.render()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ use crate::models::pats::Scopes;
|
||||
use crate::models::users::User;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::internal::session::get_session_metadata;
|
||||
use actix_web::http::header::{HeaderValue, AUTHORIZATION};
|
||||
use actix_web::HttpRequest;
|
||||
use chrono::Utc;
|
||||
use ntex::http::header::{HeaderValue, AUTHORIZATION};
|
||||
use ntex::web::HttpRequest;
|
||||
|
||||
pub async fn get_user_from_headers<'a, E>(
|
||||
req: &HttpRequest,
|
||||
@@ -18,7 +18,7 @@ pub async fn get_user_from_headers<'a, E>(
|
||||
required_scopes: Option<&[Scopes]>,
|
||||
) -> Result<(Scopes, User), AuthenticationError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy + Send + Sync,
|
||||
{
|
||||
// Fetch DB user record and minos user from headers
|
||||
let (scopes, db_user) = get_user_record_from_bearer_token(
|
||||
@@ -52,7 +52,7 @@ pub async fn get_user_record_from_bearer_token<'a, 'b, E>(
|
||||
session_queue: &AuthQueue,
|
||||
) -> Result<Option<(Scopes, user_item::User)>, AuthenticationError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy + Send + Sync,
|
||||
{
|
||||
let token = if let Some(token) = token {
|
||||
token
|
||||
@@ -174,7 +174,7 @@ pub async fn check_is_moderator_from_headers<'a, 'b, E>(
|
||||
required_scopes: Option<&[Scopes]>,
|
||||
) -> Result<User, AuthenticationError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy + Send + Sync,
|
||||
{
|
||||
let user = get_user_from_headers(
|
||||
req,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{models::ids::ProjectId, routes::ApiError};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -25,7 +23,7 @@ pub async fn fetch_playtimes(
|
||||
start_date: DateTime<Utc>,
|
||||
end_date: DateTime<Utc>,
|
||||
resolution_minute: u32,
|
||||
client: Arc<clickhouse::Client>,
|
||||
client: &clickhouse::Client,
|
||||
) -> Result<Vec<ReturnIntervals>, ApiError> {
|
||||
let query = client
|
||||
.query(
|
||||
@@ -56,7 +54,7 @@ pub async fn fetch_views(
|
||||
start_date: DateTime<Utc>,
|
||||
end_date: DateTime<Utc>,
|
||||
resolution_minutes: u32,
|
||||
client: Arc<clickhouse::Client>,
|
||||
client: &clickhouse::Client,
|
||||
) -> Result<Vec<ReturnIntervals>, ApiError> {
|
||||
let query = client
|
||||
.query(
|
||||
@@ -86,7 +84,7 @@ pub async fn fetch_downloads(
|
||||
start_date: DateTime<Utc>,
|
||||
end_date: DateTime<Utc>,
|
||||
resolution_minutes: u32,
|
||||
client: Arc<clickhouse::Client>,
|
||||
client: &clickhouse::Client,
|
||||
) -> Result<Vec<ReturnIntervals>, ApiError> {
|
||||
let query = client
|
||||
.query(
|
||||
@@ -113,7 +111,7 @@ pub async fn fetch_countries_downloads(
|
||||
projects: Vec<ProjectId>,
|
||||
start_date: DateTime<Utc>,
|
||||
end_date: DateTime<Utc>,
|
||||
client: Arc<clickhouse::Client>,
|
||||
client: &clickhouse::Client,
|
||||
) -> Result<Vec<ReturnCountry>, ApiError> {
|
||||
let query = client
|
||||
.query(
|
||||
@@ -140,7 +138,7 @@ pub async fn fetch_countries_views(
|
||||
projects: Vec<ProjectId>,
|
||||
start_date: DateTime<Utc>,
|
||||
end_date: DateTime<Utc>,
|
||||
client: Arc<clickhouse::Client>,
|
||||
client: &clickhouse::Client,
|
||||
) -> Result<Vec<ReturnCountry>, ApiError> {
|
||||
let query = client
|
||||
.query(
|
||||
|
||||
@@ -49,7 +49,7 @@ macro_rules! generate_bulk_ids {
|
||||
count: usize,
|
||||
con: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<Vec<$return_type>, DatabaseError> {
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut rng = ChaCha20Rng::from_entropy();
|
||||
let mut retry_count = 0;
|
||||
|
||||
// Check if ID is unique
|
||||
|
||||
@@ -757,7 +757,7 @@ impl VersionField {
|
||||
l.field_id.0,
|
||||
l.version_id.0,
|
||||
l.int_value,
|
||||
l.enum_value.as_ref().map(|e| e.0).unwrap_or(-1),
|
||||
l.enum_value.as_ref().map(|e| e.0),
|
||||
l.string_value.clone(),
|
||||
)
|
||||
})
|
||||
@@ -772,7 +772,7 @@ impl VersionField {
|
||||
&version_ids[..],
|
||||
&int_values[..] as &[Option<i32>],
|
||||
&string_values[..] as &[Option<String>],
|
||||
&enum_values[..] as &[i32]
|
||||
&enum_values[..] as &[Option<i32>]
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.await?;
|
||||
|
||||
@@ -67,7 +67,7 @@ impl Organization {
|
||||
redis: &RedisPool,
|
||||
) -> Result<Option<Self>, super::DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Send + Sync,
|
||||
{
|
||||
Self::get_many(&[string], exec, redis)
|
||||
.await
|
||||
@@ -80,7 +80,7 @@ impl Organization {
|
||||
redis: &RedisPool,
|
||||
) -> Result<Option<Self>, super::DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Send + Sync,
|
||||
{
|
||||
Self::get_many_ids(&[id], exec, redis)
|
||||
.await
|
||||
@@ -93,7 +93,7 @@ impl Organization {
|
||||
redis: &RedisPool,
|
||||
) -> Result<Vec<Self>, super::DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Send + Sync,
|
||||
{
|
||||
let ids = organization_ids
|
||||
.iter()
|
||||
@@ -105,14 +105,14 @@ impl Organization {
|
||||
pub async fn get_many<
|
||||
'a,
|
||||
E,
|
||||
T: Display + Hash + Eq + PartialEq + Clone + Debug,
|
||||
T: Display + Hash + Eq + PartialEq + Clone + Debug + Send,
|
||||
>(
|
||||
organization_strings: &[T],
|
||||
exec: E,
|
||||
redis: &RedisPool,
|
||||
) -> Result<Vec<Self>, super::DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Send + Sync,
|
||||
{
|
||||
let val = redis
|
||||
.get_cached_keys_with_slug(
|
||||
|
||||
@@ -58,14 +58,14 @@ impl PersonalAccessToken {
|
||||
pub async fn get<
|
||||
'a,
|
||||
E,
|
||||
T: Display + Hash + Eq + PartialEq + Clone + Debug,
|
||||
T: Display + Hash + Eq + PartialEq + Clone + Debug + Send,
|
||||
>(
|
||||
id: T,
|
||||
exec: E,
|
||||
redis: &RedisPool,
|
||||
) -> Result<Option<PersonalAccessToken>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Send + Sync,
|
||||
{
|
||||
Self::get_many(&[id], exec, redis)
|
||||
.await
|
||||
@@ -78,7 +78,7 @@ impl PersonalAccessToken {
|
||||
redis: &RedisPool,
|
||||
) -> Result<Vec<PersonalAccessToken>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Send + Sync,
|
||||
{
|
||||
let ids = pat_ids
|
||||
.iter()
|
||||
@@ -90,14 +90,14 @@ impl PersonalAccessToken {
|
||||
pub async fn get_many<
|
||||
'a,
|
||||
E,
|
||||
T: Display + Hash + Eq + PartialEq + Clone + Debug,
|
||||
T: Display + Hash + Eq + PartialEq + Clone + Debug + Send,
|
||||
>(
|
||||
pat_strings: &[T],
|
||||
exec: E,
|
||||
redis: &RedisPool,
|
||||
) -> Result<Vec<PersonalAccessToken>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Send + Sync,
|
||||
{
|
||||
let val = redis
|
||||
.get_cached_keys_with_slug(
|
||||
|
||||
@@ -14,6 +14,7 @@ use futures::TryStreamExt;
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::future::Future;
|
||||
use std::hash::Hash;
|
||||
|
||||
pub const PROJECTS_NAMESPACE: &str = "projects";
|
||||
@@ -473,82 +474,102 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get<'a, 'b, E>(
|
||||
string: &str,
|
||||
/// What's going on here? See: https://github.com/launchbadge/sqlx/issues/1015#issuecomment-767787777
|
||||
#[allow(clippy::manual_async_fn)]
|
||||
pub fn get<'a, 'c, E>(
|
||||
string: &'a str,
|
||||
executor: E,
|
||||
redis: &RedisPool,
|
||||
) -> Result<Option<QueryProject>, DatabaseError>
|
||||
redis: &'a RedisPool,
|
||||
) -> impl Future<Output = Result<Option<QueryProject>, DatabaseError>> + Send + 'a
|
||||
where
|
||||
E: sqlx::Acquire<'a, Database = sqlx::Postgres>,
|
||||
E: sqlx::Acquire<'c, Database = sqlx::Postgres> + Send + 'a,
|
||||
{
|
||||
Project::get_many(&[string], executor, redis)
|
||||
.await
|
||||
.map(|x| x.into_iter().next())
|
||||
async move {
|
||||
Project::get_many(&[string], executor, redis)
|
||||
.await
|
||||
.map(|x| x.into_iter().next())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_id<'a, 'b, E>(
|
||||
#[allow(clippy::manual_async_fn)]
|
||||
pub fn get_id<'a, 'c, E>(
|
||||
id: ProjectId,
|
||||
executor: E,
|
||||
redis: &RedisPool,
|
||||
) -> Result<Option<QueryProject>, DatabaseError>
|
||||
redis: &'a RedisPool,
|
||||
) -> impl Future<Output = Result<Option<QueryProject>, DatabaseError>> + Send + 'a
|
||||
where
|
||||
E: sqlx::Acquire<'a, Database = sqlx::Postgres>,
|
||||
E: sqlx::Acquire<'c, Database = sqlx::Postgres> + Send + 'a,
|
||||
{
|
||||
Project::get_many(
|
||||
&[crate::models::ids::ProjectId::from(id)],
|
||||
executor,
|
||||
redis,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.into_iter().next())
|
||||
async move {
|
||||
Project::get_many(
|
||||
&[crate::models::ids::ProjectId::from(id)],
|
||||
executor,
|
||||
redis,
|
||||
)
|
||||
.await
|
||||
.map(|x| x.into_iter().next())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_many_ids<'a, E>(
|
||||
project_ids: &[ProjectId],
|
||||
#[allow(clippy::manual_async_fn)]
|
||||
pub fn get_many_ids<'a, 'c, E>(
|
||||
project_ids: &'a [ProjectId],
|
||||
exec: E,
|
||||
redis: &RedisPool,
|
||||
) -> Result<Vec<QueryProject>, DatabaseError>
|
||||
redis: &'a RedisPool,
|
||||
) -> impl Future<Output = Result<Vec<QueryProject>, DatabaseError>> + Send + 'a
|
||||
where
|
||||
E: sqlx::Acquire<'a, Database = sqlx::Postgres>,
|
||||
E: sqlx::Acquire<'c, Database = sqlx::Postgres> + Send + 'a,
|
||||
{
|
||||
let ids = project_ids
|
||||
.iter()
|
||||
.map(|x| crate::models::ids::ProjectId::from(*x))
|
||||
.collect::<Vec<_>>();
|
||||
Project::get_many(&ids, exec, redis).await
|
||||
async move {
|
||||
let ids = project_ids
|
||||
.iter()
|
||||
.map(|x| crate::models::ids::ProjectId::from(*x))
|
||||
.collect::<Vec<_>>();
|
||||
Project::get_many(&ids, exec, redis).await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_many<
|
||||
#[allow(clippy::manual_async_fn)]
|
||||
pub fn get_many<
|
||||
'a,
|
||||
'c,
|
||||
E,
|
||||
T: Display + Hash + Eq + PartialEq + Clone + Debug,
|
||||
T: Display
|
||||
+ Hash
|
||||
+ Eq
|
||||
+ PartialEq
|
||||
+ Clone
|
||||
+ Debug
|
||||
+ std::marker::Sync
|
||||
+ std::marker::Send,
|
||||
>(
|
||||
project_strings: &[T],
|
||||
project_strings: &'a [T],
|
||||
exec: E,
|
||||
redis: &RedisPool,
|
||||
) -> Result<Vec<QueryProject>, DatabaseError>
|
||||
redis: &'a RedisPool,
|
||||
) -> impl Future<Output = Result<Vec<QueryProject>, DatabaseError>> + Send + 'a
|
||||
where
|
||||
E: sqlx::Acquire<'a, Database = sqlx::Postgres>,
|
||||
E: sqlx::Acquire<'c, Database = sqlx::Postgres> + Send + 'a,
|
||||
{
|
||||
let val = redis.get_cached_keys_with_slug(
|
||||
PROJECTS_NAMESPACE,
|
||||
PROJECTS_SLUGS_NAMESPACE,
|
||||
false,
|
||||
project_strings,
|
||||
|ids| async move {
|
||||
let mut exec = exec.acquire().await?;
|
||||
let project_ids_parsed: Vec<i64> = ids
|
||||
.iter()
|
||||
.flat_map(|x| parse_base62(&x.to_string()).ok())
|
||||
.map(|x| x as i64)
|
||||
.collect();
|
||||
let slugs = ids
|
||||
.into_iter()
|
||||
.map(|x| x.to_string().to_lowercase())
|
||||
.collect::<Vec<_>>();
|
||||
async move {
|
||||
let val = redis.get_cached_keys_with_slug(
|
||||
PROJECTS_NAMESPACE,
|
||||
PROJECTS_SLUGS_NAMESPACE,
|
||||
false,
|
||||
project_strings,
|
||||
|ids| async move {
|
||||
let mut exec = exec.acquire().await?;
|
||||
let project_ids_parsed: Vec<i64> = ids
|
||||
.iter()
|
||||
.flat_map(|x| parse_base62(&x.to_string()).ok())
|
||||
.map(|x| x as i64)
|
||||
.collect();
|
||||
let slugs = ids
|
||||
.into_iter()
|
||||
.map(|x| x.to_string().to_lowercase())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let all_version_ids = DashSet::new();
|
||||
let versions: DashMap<ProjectId, Vec<(VersionId, DateTime<Utc>)>> = sqlx::query!(
|
||||
let all_version_ids = DashSet::new();
|
||||
let versions: DashMap<ProjectId, Vec<(VersionId, DateTime<Utc>)>> = sqlx::query!(
|
||||
"
|
||||
SELECT DISTINCT mod_id, v.id as id, date_published
|
||||
FROM mods m
|
||||
@@ -562,23 +583,23 @@ impl Project {
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
)
|
||||
.fetch(&mut *exec)
|
||||
.try_fold(
|
||||
DashMap::new(),
|
||||
|acc: DashMap<ProjectId, Vec<(VersionId, DateTime<Utc>)>>, m| {
|
||||
let version_id = VersionId(m.id);
|
||||
let date_published = m.date_published;
|
||||
all_version_ids.insert(version_id);
|
||||
acc.entry(ProjectId(m.mod_id))
|
||||
.or_default()
|
||||
.push((version_id, date_published));
|
||||
async move { Ok(acc) }
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
.fetch(&mut *exec)
|
||||
.try_fold(
|
||||
DashMap::new(),
|
||||
|acc: DashMap<ProjectId, Vec<(VersionId, DateTime<Utc>)>>, m| {
|
||||
let version_id = VersionId(m.id);
|
||||
let date_published = m.date_published;
|
||||
all_version_ids.insert(version_id);
|
||||
acc.entry(ProjectId(m.mod_id))
|
||||
.or_default()
|
||||
.push((version_id, date_published));
|
||||
async move { Ok(acc) }
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let loader_field_enum_value_ids = DashSet::new();
|
||||
let version_fields: DashMap<ProjectId, Vec<QueryVersionField>> = sqlx::query!(
|
||||
let loader_field_enum_value_ids = DashSet::new();
|
||||
let version_fields: DashMap<ProjectId, Vec<QueryVersionField>> = sqlx::query!(
|
||||
"
|
||||
SELECT DISTINCT mod_id, version_id, field_id, int_value, enum_value, string_value
|
||||
FROM versions v
|
||||
@@ -587,29 +608,29 @@ impl Project {
|
||||
",
|
||||
&all_version_ids.iter().map(|x| x.0).collect::<Vec<_>>()
|
||||
)
|
||||
.fetch(&mut *exec)
|
||||
.try_fold(
|
||||
DashMap::new(),
|
||||
|acc: DashMap<ProjectId, Vec<QueryVersionField>>, m| {
|
||||
let qvf = QueryVersionField {
|
||||
version_id: VersionId(m.version_id),
|
||||
field_id: LoaderFieldId(m.field_id),
|
||||
int_value: m.int_value,
|
||||
enum_value: if m.enum_value == -1 { None } else { Some(LoaderFieldEnumValueId(m.enum_value)) },
|
||||
string_value: m.string_value,
|
||||
};
|
||||
.fetch(&mut *exec)
|
||||
.try_fold(
|
||||
DashMap::new(),
|
||||
|acc: DashMap<ProjectId, Vec<QueryVersionField>>, m| {
|
||||
let qvf = QueryVersionField {
|
||||
version_id: VersionId(m.version_id),
|
||||
field_id: LoaderFieldId(m.field_id),
|
||||
int_value: m.int_value,
|
||||
enum_value: m.enum_value.map(LoaderFieldEnumValueId),
|
||||
string_value: m.string_value,
|
||||
};
|
||||
|
||||
if m.enum_value != -1 {
|
||||
loader_field_enum_value_ids.insert(LoaderFieldEnumValueId(m.enum_value));
|
||||
}
|
||||
if let Some(enum_value) = m.enum_value {
|
||||
loader_field_enum_value_ids.insert(LoaderFieldEnumValueId(enum_value));
|
||||
}
|
||||
|
||||
acc.entry(ProjectId(m.mod_id)).or_default().push(qvf);
|
||||
async move { Ok(acc) }
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
acc.entry(ProjectId(m.mod_id)).or_default().push(qvf);
|
||||
async move { Ok(acc) }
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let loader_field_enum_values: Vec<QueryLoaderFieldEnumValue> = sqlx::query!(
|
||||
let loader_field_enum_values: Vec<QueryLoaderFieldEnumValue> = sqlx::query!(
|
||||
"
|
||||
SELECT DISTINCT id, enum_id, value, ordering, created, metadata
|
||||
FROM loader_field_enum_values lfev
|
||||
@@ -621,19 +642,19 @@ impl Project {
|
||||
.map(|x| x.0)
|
||||
.collect::<Vec<_>>()
|
||||
)
|
||||
.fetch(&mut *exec)
|
||||
.map_ok(|m| QueryLoaderFieldEnumValue {
|
||||
id: LoaderFieldEnumValueId(m.id),
|
||||
enum_id: LoaderFieldEnumId(m.enum_id),
|
||||
value: m.value,
|
||||
ordering: m.ordering,
|
||||
created: m.created,
|
||||
metadata: m.metadata,
|
||||
})
|
||||
.try_collect()
|
||||
.await?;
|
||||
.fetch(&mut *exec)
|
||||
.map_ok(|m| QueryLoaderFieldEnumValue {
|
||||
id: LoaderFieldEnumValueId(m.id),
|
||||
enum_id: LoaderFieldEnumId(m.enum_id),
|
||||
value: m.value,
|
||||
ordering: m.ordering,
|
||||
created: m.created,
|
||||
metadata: m.metadata,
|
||||
})
|
||||
.try_collect()
|
||||
.await?;
|
||||
|
||||
let mods_gallery: DashMap<ProjectId, Vec<GalleryItem>> = sqlx::query!(
|
||||
let mods_gallery: DashMap<ProjectId, Vec<GalleryItem>> = sqlx::query!(
|
||||
"
|
||||
SELECT DISTINCT mod_id, mg.image_url, mg.raw_image_url, mg.featured, mg.name, mg.description, mg.created, mg.ordering
|
||||
FROM mods_gallery mg
|
||||
@@ -643,23 +664,23 @@ impl Project {
|
||||
&project_ids_parsed,
|
||||
&slugs
|
||||
).fetch(&mut *exec)
|
||||
.try_fold(DashMap::new(), |acc : DashMap<ProjectId, Vec<GalleryItem>>, m| {
|
||||
acc.entry(ProjectId(m.mod_id))
|
||||
.or_default()
|
||||
.push(GalleryItem {
|
||||
image_url: m.image_url,
|
||||
raw_image_url: m.raw_image_url,
|
||||
featured: m.featured.unwrap_or(false),
|
||||
name: m.name,
|
||||
description: m.description,
|
||||
created: m.created,
|
||||
ordering: m.ordering,
|
||||
});
|
||||
async move { Ok(acc) }
|
||||
}
|
||||
).await?;
|
||||
.try_fold(DashMap::new(), |acc : DashMap<ProjectId, Vec<GalleryItem>>, m| {
|
||||
acc.entry(ProjectId(m.mod_id))
|
||||
.or_default()
|
||||
.push(GalleryItem {
|
||||
image_url: m.image_url,
|
||||
raw_image_url: m.raw_image_url,
|
||||
featured: m.featured.unwrap_or(false),
|
||||
name: m.name,
|
||||
description: m.description,
|
||||
created: m.created,
|
||||
ordering: m.ordering,
|
||||
});
|
||||
async move { Ok(acc) }
|
||||
}
|
||||
).await?;
|
||||
|
||||
let links: DashMap<ProjectId, Vec<LinkUrl>> = sqlx::query!(
|
||||
let links: DashMap<ProjectId, Vec<LinkUrl>> = sqlx::query!(
|
||||
"
|
||||
SELECT DISTINCT joining_mod_id as mod_id, joining_platform_id as platform_id, lp.name as platform_name, url, lp.donation as donation
|
||||
FROM mods_links ml
|
||||
@@ -670,29 +691,29 @@ impl Project {
|
||||
&project_ids_parsed,
|
||||
&slugs
|
||||
).fetch(&mut *exec)
|
||||
.try_fold(DashMap::new(), |acc : DashMap<ProjectId, Vec<LinkUrl>>, m| {
|
||||
acc.entry(ProjectId(m.mod_id))
|
||||
.or_default()
|
||||
.push(LinkUrl {
|
||||
platform_id: LinkPlatformId(m.platform_id),
|
||||
platform_name: m.platform_name,
|
||||
url: m.url,
|
||||
donation: m.donation,
|
||||
});
|
||||
async move { Ok(acc) }
|
||||
.try_fold(DashMap::new(), |acc : DashMap<ProjectId, Vec<LinkUrl>>, m| {
|
||||
acc.entry(ProjectId(m.mod_id))
|
||||
.or_default()
|
||||
.push(LinkUrl {
|
||||
platform_id: LinkPlatformId(m.platform_id),
|
||||
platform_name: m.platform_name,
|
||||
url: m.url,
|
||||
donation: m.donation,
|
||||
});
|
||||
async move { Ok(acc) }
|
||||
}
|
||||
).await?;
|
||||
|
||||
#[derive(Default)]
|
||||
struct VersionLoaderData {
|
||||
loaders: Vec<String>,
|
||||
project_types: Vec<String>,
|
||||
games: Vec<String>,
|
||||
loader_loader_field_ids: Vec<LoaderFieldId>,
|
||||
}
|
||||
).await?;
|
||||
|
||||
#[derive(Default)]
|
||||
struct VersionLoaderData {
|
||||
loaders: Vec<String>,
|
||||
project_types: Vec<String>,
|
||||
games: Vec<String>,
|
||||
loader_loader_field_ids: Vec<LoaderFieldId>,
|
||||
}
|
||||
|
||||
let loader_field_ids = DashSet::new();
|
||||
let loaders_ptypes_games: DashMap<ProjectId, VersionLoaderData> = sqlx::query!(
|
||||
let loader_field_ids = DashSet::new();
|
||||
let loaders_ptypes_games: DashMap<ProjectId, VersionLoaderData> = sqlx::query!(
|
||||
"
|
||||
SELECT DISTINCT mod_id,
|
||||
ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null) loaders,
|
||||
@@ -712,29 +733,29 @@ impl Project {
|
||||
",
|
||||
&all_version_ids.iter().map(|x| x.0).collect::<Vec<_>>()
|
||||
).fetch(&mut *exec)
|
||||
.map_ok(|m| {
|
||||
let project_id = ProjectId(m.mod_id);
|
||||
.map_ok(|m| {
|
||||
let project_id = ProjectId(m.mod_id);
|
||||
|
||||
// Add loader fields to the set we need to fetch
|
||||
let loader_loader_field_ids = m.loader_fields.unwrap_or_default().into_iter().map(LoaderFieldId).collect::<Vec<_>>();
|
||||
for loader_field_id in loader_loader_field_ids.iter() {
|
||||
loader_field_ids.insert(*loader_field_id);
|
||||
}
|
||||
|
||||
// Add loader + loader associated data to the map
|
||||
let version_loader_data = VersionLoaderData {
|
||||
loaders: m.loaders.unwrap_or_default(),
|
||||
project_types: m.project_types.unwrap_or_default(),
|
||||
games: m.games.unwrap_or_default(),
|
||||
loader_loader_field_ids,
|
||||
};
|
||||
|
||||
(project_id, version_loader_data)
|
||||
|
||||
// Add loader fields to the set we need to fetch
|
||||
let loader_loader_field_ids = m.loader_fields.unwrap_or_default().into_iter().map(LoaderFieldId).collect::<Vec<_>>();
|
||||
for loader_field_id in loader_loader_field_ids.iter() {
|
||||
loader_field_ids.insert(*loader_field_id);
|
||||
}
|
||||
).try_collect().await?;
|
||||
|
||||
// Add loader + loader associated data to the map
|
||||
let version_loader_data = VersionLoaderData {
|
||||
loaders: m.loaders.unwrap_or_default(),
|
||||
project_types: m.project_types.unwrap_or_default(),
|
||||
games: m.games.unwrap_or_default(),
|
||||
loader_loader_field_ids,
|
||||
};
|
||||
|
||||
(project_id, version_loader_data)
|
||||
|
||||
}
|
||||
).try_collect().await?;
|
||||
|
||||
let loader_fields: Vec<QueryLoaderField> = sqlx::query!(
|
||||
let loader_fields: Vec<QueryLoaderField> = sqlx::query!(
|
||||
"
|
||||
SELECT DISTINCT id, field, field_type, enum_type, min_val, max_val, optional
|
||||
FROM loader_fields lf
|
||||
@@ -742,20 +763,20 @@ impl Project {
|
||||
",
|
||||
&loader_field_ids.iter().map(|x| x.0).collect::<Vec<_>>()
|
||||
)
|
||||
.fetch(&mut *exec)
|
||||
.map_ok(|m| QueryLoaderField {
|
||||
id: LoaderFieldId(m.id),
|
||||
field: m.field,
|
||||
field_type: m.field_type,
|
||||
enum_type: m.enum_type.map(LoaderFieldEnumId),
|
||||
min_val: m.min_val,
|
||||
max_val: m.max_val,
|
||||
optional: m.optional,
|
||||
})
|
||||
.try_collect()
|
||||
.await?;
|
||||
.fetch(&mut *exec)
|
||||
.map_ok(|m| QueryLoaderField {
|
||||
id: LoaderFieldId(m.id),
|
||||
field: m.field,
|
||||
field_type: m.field_type,
|
||||
enum_type: m.enum_type.map(LoaderFieldEnumId),
|
||||
min_val: m.min_val,
|
||||
max_val: m.max_val,
|
||||
optional: m.optional,
|
||||
})
|
||||
.try_collect()
|
||||
.await?;
|
||||
|
||||
let projects = sqlx::query!(
|
||||
let projects = sqlx::query!(
|
||||
"
|
||||
SELECT m.id id, m.name name, m.summary summary, m.downloads downloads, m.follows follows,
|
||||
m.icon_url icon_url, m.raw_icon_url raw_icon_url, m.description description, m.published published,
|
||||
@@ -776,87 +797,88 @@ impl Project {
|
||||
&project_ids_parsed,
|
||||
&slugs,
|
||||
)
|
||||
.fetch(&mut *exec)
|
||||
.try_fold(DashMap::new(), |acc, m| {
|
||||
let id = m.id;
|
||||
let project_id = ProjectId(id);
|
||||
let VersionLoaderData {
|
||||
loaders,
|
||||
project_types,
|
||||
games,
|
||||
loader_loader_field_ids,
|
||||
} = loaders_ptypes_games.remove(&project_id).map(|x|x.1).unwrap_or_default();
|
||||
let mut versions = versions.remove(&project_id).map(|x| x.1).unwrap_or_default();
|
||||
let mut gallery = mods_gallery.remove(&project_id).map(|x| x.1).unwrap_or_default();
|
||||
let urls = links.remove(&project_id).map(|x| x.1).unwrap_or_default();
|
||||
let version_fields = version_fields.remove(&project_id).map(|x| x.1).unwrap_or_default();
|
||||
|
||||
let loader_fields = loader_fields.iter()
|
||||
.filter(|x| loader_loader_field_ids.contains(&x.id))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let project = QueryProject {
|
||||
inner: Project {
|
||||
id: ProjectId(id),
|
||||
team_id: TeamId(m.team_id),
|
||||
organization_id: m.organization_id.map(OrganizationId),
|
||||
name: m.name.clone(),
|
||||
summary: m.summary.clone(),
|
||||
downloads: m.downloads,
|
||||
icon_url: m.icon_url.clone(),
|
||||
raw_icon_url: m.raw_icon_url.clone(),
|
||||
published: m.published,
|
||||
updated: m.updated,
|
||||
license_url: m.license_url.clone(),
|
||||
status: ProjectStatus::from_string(
|
||||
&m.status,
|
||||
),
|
||||
requested_status: m.requested_status.map(|x| ProjectStatus::from_string(
|
||||
&x,
|
||||
)),
|
||||
license: m.license.clone(),
|
||||
slug: m.slug.clone(),
|
||||
description: m.description.clone(),
|
||||
follows: m.follows,
|
||||
moderation_message: m.moderation_message,
|
||||
moderation_message_body: m.moderation_message_body,
|
||||
approved: m.approved,
|
||||
webhook_sent: m.webhook_sent,
|
||||
color: m.color.map(|x| x as u32),
|
||||
queued: m.queued,
|
||||
monetization_status: MonetizationStatus::from_string(
|
||||
&m.monetization_status,
|
||||
),
|
||||
.fetch(&mut *exec)
|
||||
.try_fold(DashMap::new(), |acc, m| {
|
||||
let id = m.id;
|
||||
let project_id = ProjectId(id);
|
||||
let VersionLoaderData {
|
||||
loaders,
|
||||
},
|
||||
categories: m.categories.unwrap_or_default(),
|
||||
additional_categories: m.additional_categories.unwrap_or_default(),
|
||||
project_types,
|
||||
games,
|
||||
versions: {
|
||||
// Each version is a tuple of (VersionId, DateTime<Utc>)
|
||||
versions.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
versions.into_iter().map(|x| x.0).collect()
|
||||
},
|
||||
gallery_items: {
|
||||
gallery.sort_by(|a, b| a.ordering.cmp(&b.ordering));
|
||||
gallery
|
||||
},
|
||||
urls,
|
||||
aggregate_version_fields: VersionField::from_query_json(version_fields, &loader_fields, &loader_field_enum_values, true),
|
||||
thread_id: ThreadId(m.thread_id),
|
||||
};
|
||||
project_types,
|
||||
games,
|
||||
loader_loader_field_ids,
|
||||
} = loaders_ptypes_games.remove(&project_id).map(|x|x.1).unwrap_or_default();
|
||||
let mut versions = versions.remove(&project_id).map(|x| x.1).unwrap_or_default();
|
||||
let mut gallery = mods_gallery.remove(&project_id).map(|x| x.1).unwrap_or_default();
|
||||
let urls = links.remove(&project_id).map(|x| x.1).unwrap_or_default();
|
||||
let version_fields = version_fields.remove(&project_id).map(|x| x.1).unwrap_or_default();
|
||||
|
||||
acc.insert(m.id, (m.slug, project));
|
||||
async move { Ok(acc) }
|
||||
})
|
||||
.await?;
|
||||
let loader_fields = loader_fields.iter()
|
||||
.filter(|x| loader_loader_field_ids.contains(&x.id))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(projects)
|
||||
},
|
||||
).await?;
|
||||
let project = QueryProject {
|
||||
inner: Project {
|
||||
id: ProjectId(id),
|
||||
team_id: TeamId(m.team_id),
|
||||
organization_id: m.organization_id.map(OrganizationId),
|
||||
name: m.name.clone(),
|
||||
summary: m.summary.clone(),
|
||||
downloads: m.downloads,
|
||||
icon_url: m.icon_url.clone(),
|
||||
raw_icon_url: m.raw_icon_url.clone(),
|
||||
published: m.published,
|
||||
updated: m.updated,
|
||||
license_url: m.license_url.clone(),
|
||||
status: ProjectStatus::from_string(
|
||||
&m.status,
|
||||
),
|
||||
requested_status: m.requested_status.map(|x| ProjectStatus::from_string(
|
||||
&x,
|
||||
)),
|
||||
license: m.license.clone(),
|
||||
slug: m.slug.clone(),
|
||||
description: m.description.clone(),
|
||||
follows: m.follows,
|
||||
moderation_message: m.moderation_message,
|
||||
moderation_message_body: m.moderation_message_body,
|
||||
approved: m.approved,
|
||||
webhook_sent: m.webhook_sent,
|
||||
color: m.color.map(|x| x as u32),
|
||||
queued: m.queued,
|
||||
monetization_status: MonetizationStatus::from_string(
|
||||
&m.monetization_status,
|
||||
),
|
||||
loaders,
|
||||
},
|
||||
categories: m.categories.unwrap_or_default(),
|
||||
additional_categories: m.additional_categories.unwrap_or_default(),
|
||||
project_types,
|
||||
games,
|
||||
versions: {
|
||||
// Each version is a tuple of (VersionId, DateTime<Utc>)
|
||||
versions.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
versions.into_iter().map(|x| x.0).collect()
|
||||
},
|
||||
gallery_items: {
|
||||
gallery.sort_by(|a, b| a.ordering.cmp(&b.ordering));
|
||||
gallery
|
||||
},
|
||||
urls,
|
||||
aggregate_version_fields: VersionField::from_query_json(version_fields, &loader_fields, &loader_field_enum_values, true),
|
||||
thread_id: ThreadId(m.thread_id),
|
||||
};
|
||||
|
||||
Ok(val)
|
||||
acc.insert(m.id, (m.slug, project));
|
||||
async move { Ok(acc) }
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(projects)
|
||||
},
|
||||
).await?;
|
||||
|
||||
Ok(val)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_dependencies<'a, E>(
|
||||
|
||||
@@ -85,14 +85,14 @@ impl Session {
|
||||
pub async fn get<
|
||||
'a,
|
||||
E,
|
||||
T: Display + Hash + Eq + PartialEq + Clone + Debug,
|
||||
T: Display + Hash + Eq + PartialEq + Clone + Debug + Send,
|
||||
>(
|
||||
id: T,
|
||||
exec: E,
|
||||
redis: &RedisPool,
|
||||
) -> Result<Option<Session>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Send + Sync,
|
||||
{
|
||||
Self::get_many(&[id], exec, redis)
|
||||
.await
|
||||
@@ -105,7 +105,7 @@ impl Session {
|
||||
redis: &RedisPool,
|
||||
) -> Result<Option<Session>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Send + Sync,
|
||||
{
|
||||
Session::get_many(
|
||||
&[crate::models::ids::SessionId::from(id)],
|
||||
@@ -122,7 +122,7 @@ impl Session {
|
||||
redis: &RedisPool,
|
||||
) -> Result<Vec<Session>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Send + Sync,
|
||||
{
|
||||
let ids = session_ids
|
||||
.iter()
|
||||
@@ -134,14 +134,14 @@ impl Session {
|
||||
pub async fn get_many<
|
||||
'a,
|
||||
E,
|
||||
T: Display + Hash + Eq + PartialEq + Clone + Debug,
|
||||
T: Display + Hash + Eq + PartialEq + Clone + Debug + Send,
|
||||
>(
|
||||
session_strings: &[T],
|
||||
exec: E,
|
||||
redis: &RedisPool,
|
||||
) -> Result<Vec<Session>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Send + Sync,
|
||||
{
|
||||
use futures::TryStreamExt;
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ impl TeamMember {
|
||||
redis: &RedisPool,
|
||||
) -> Result<Vec<TeamMember>, super::DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy + Send + Sync,
|
||||
{
|
||||
Self::get_from_team_full_many(&[id], executor, redis).await
|
||||
}
|
||||
@@ -205,7 +205,7 @@ impl TeamMember {
|
||||
redis: &RedisPool,
|
||||
) -> Result<Vec<TeamMember>, super::DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy + Send + Sync,
|
||||
{
|
||||
if team_ids.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
@@ -405,7 +405,7 @@ impl TeamMember {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete(
|
||||
pub async fn delete<'a, 'b>(
|
||||
id: TeamId,
|
||||
user_id: UserId,
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
|
||||
@@ -103,7 +103,7 @@ impl User {
|
||||
redis: &RedisPool,
|
||||
) -> Result<Option<User>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Send + Sync,
|
||||
{
|
||||
User::get_many(&[string], executor, redis)
|
||||
.await
|
||||
@@ -116,7 +116,7 @@ impl User {
|
||||
redis: &RedisPool,
|
||||
) -> Result<Option<User>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Send + Sync,
|
||||
{
|
||||
User::get_many(&[crate::models::ids::UserId::from(id)], executor, redis)
|
||||
.await
|
||||
@@ -129,7 +129,7 @@ impl User {
|
||||
redis: &RedisPool,
|
||||
) -> Result<Vec<User>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Send + Sync,
|
||||
{
|
||||
let ids = user_ids
|
||||
.iter()
|
||||
@@ -141,14 +141,14 @@ impl User {
|
||||
pub async fn get_many<
|
||||
'a,
|
||||
E,
|
||||
T: Display + Hash + Eq + PartialEq + Clone + Debug,
|
||||
T: Display + Hash + Eq + PartialEq + Clone + Debug + Send,
|
||||
>(
|
||||
users_strings: &[T],
|
||||
exec: E,
|
||||
redis: &RedisPool,
|
||||
) -> Result<Vec<User>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Send + Sync,
|
||||
{
|
||||
use futures::TryStreamExt;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::iter;
|
||||
|
||||
pub const VERSIONS_NAMESPACE: &str = "versions";
|
||||
@@ -455,35 +456,40 @@ impl Version {
|
||||
Ok(Some(()))
|
||||
}
|
||||
|
||||
pub async fn get<'a, 'b, E>(
|
||||
#[allow(clippy::manual_async_fn)]
|
||||
pub fn get<'a, 'c, E>(
|
||||
id: VersionId,
|
||||
executor: E,
|
||||
redis: &RedisPool,
|
||||
) -> Result<Option<QueryVersion>, DatabaseError>
|
||||
redis: &'a RedisPool,
|
||||
) -> impl Future<Output = Result<Option<QueryVersion>, DatabaseError>> + Send + 'a
|
||||
where
|
||||
E: sqlx::Acquire<'a, Database = sqlx::Postgres>,
|
||||
E: sqlx::Acquire<'c, Database = sqlx::Postgres> + Send + 'a,
|
||||
{
|
||||
Self::get_many(&[id], executor, redis)
|
||||
.await
|
||||
.map(|x| x.into_iter().next())
|
||||
async move {
|
||||
Self::get_many(&[id], executor, redis)
|
||||
.await
|
||||
.map(|x| x.into_iter().next())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_many<'a, E>(
|
||||
version_ids: &[VersionId],
|
||||
#[allow(clippy::manual_async_fn)]
|
||||
pub fn get_many<'a, 'c, E>(
|
||||
version_ids: &'a [VersionId],
|
||||
exec: E,
|
||||
redis: &RedisPool,
|
||||
) -> Result<Vec<QueryVersion>, DatabaseError>
|
||||
redis: &'a RedisPool,
|
||||
) -> impl Future<Output = Result<Vec<QueryVersion>, DatabaseError>> + Send + 'a
|
||||
where
|
||||
E: sqlx::Acquire<'a, Database = sqlx::Postgres>,
|
||||
E: sqlx::Acquire<'c, Database = sqlx::Postgres> + Send + 'a,
|
||||
{
|
||||
let mut val = redis.get_cached_keys(
|
||||
VERSIONS_NAMESPACE,
|
||||
&version_ids.iter().map(|x| x.0).collect::<Vec<_>>(),
|
||||
|version_ids| async move {
|
||||
let mut exec = exec.acquire().await?;
|
||||
async move {
|
||||
let mut val = redis.get_cached_keys(
|
||||
VERSIONS_NAMESPACE,
|
||||
&version_ids.iter().map(|x| x.0).collect::<Vec<_>>(),
|
||||
|version_ids| async move {
|
||||
let mut exec = exec.acquire().await?;
|
||||
|
||||
let loader_field_enum_value_ids = DashSet::new();
|
||||
let version_fields: DashMap<VersionId, Vec<QueryVersionField>> = sqlx::query!(
|
||||
let loader_field_enum_value_ids = DashSet::new();
|
||||
let version_fields: DashMap<VersionId, Vec<QueryVersionField>> = sqlx::query!(
|
||||
"
|
||||
SELECT version_id, field_id, int_value, enum_value, string_value
|
||||
FROM version_fields
|
||||
@@ -491,38 +497,38 @@ impl Version {
|
||||
",
|
||||
&version_ids
|
||||
)
|
||||
.fetch(&mut *exec)
|
||||
.try_fold(
|
||||
DashMap::new(),
|
||||
|acc: DashMap<VersionId, Vec<QueryVersionField>>, m| {
|
||||
let qvf = QueryVersionField {
|
||||
version_id: VersionId(m.version_id),
|
||||
field_id: LoaderFieldId(m.field_id),
|
||||
int_value: m.int_value,
|
||||
enum_value: if m.enum_value == -1 { None } else { Some(LoaderFieldEnumValueId(m.enum_value)) },
|
||||
string_value: m.string_value,
|
||||
};
|
||||
.fetch(&mut *exec)
|
||||
.try_fold(
|
||||
DashMap::new(),
|
||||
|acc: DashMap<VersionId, Vec<QueryVersionField>>, m| {
|
||||
let qvf = QueryVersionField {
|
||||
version_id: VersionId(m.version_id),
|
||||
field_id: LoaderFieldId(m.field_id),
|
||||
int_value: m.int_value,
|
||||
enum_value: m.enum_value.map(LoaderFieldEnumValueId),
|
||||
string_value: m.string_value,
|
||||
};
|
||||
|
||||
if m.enum_value != -1 {
|
||||
loader_field_enum_value_ids.insert(LoaderFieldEnumValueId(m.enum_value));
|
||||
}
|
||||
if let Some(enum_value) = m.enum_value {
|
||||
loader_field_enum_value_ids.insert(LoaderFieldEnumValueId(enum_value));
|
||||
}
|
||||
|
||||
acc.entry(VersionId(m.version_id)).or_default().push(qvf);
|
||||
async move { Ok(acc) }
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
acc.entry(VersionId(m.version_id)).or_default().push(qvf);
|
||||
async move { Ok(acc) }
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
#[derive(Default)]
|
||||
struct VersionLoaderData {
|
||||
loaders: Vec<String>,
|
||||
project_types: Vec<String>,
|
||||
games: Vec<String>,
|
||||
loader_loader_field_ids: Vec<LoaderFieldId>,
|
||||
}
|
||||
#[derive(Default)]
|
||||
struct VersionLoaderData {
|
||||
loaders: Vec<String>,
|
||||
project_types: Vec<String>,
|
||||
games: Vec<String>,
|
||||
loader_loader_field_ids: Vec<LoaderFieldId>,
|
||||
}
|
||||
|
||||
let loader_field_ids = DashSet::new();
|
||||
let loaders_ptypes_games: DashMap<VersionId, VersionLoaderData> = sqlx::query!(
|
||||
let loader_field_ids = DashSet::new();
|
||||
let loaders_ptypes_games: DashMap<VersionId, VersionLoaderData> = sqlx::query!(
|
||||
"
|
||||
SELECT DISTINCT version_id,
|
||||
ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null) loaders,
|
||||
@@ -542,29 +548,29 @@ impl Version {
|
||||
",
|
||||
&version_ids
|
||||
).fetch(&mut *exec)
|
||||
.map_ok(|m| {
|
||||
let version_id = VersionId(m.version_id);
|
||||
.map_ok(|m| {
|
||||
let version_id = VersionId(m.version_id);
|
||||
|
||||
// Add loader fields to the set we need to fetch
|
||||
let loader_loader_field_ids = m.loader_fields.unwrap_or_default().into_iter().map(LoaderFieldId).collect::<Vec<_>>();
|
||||
for loader_field_id in loader_loader_field_ids.iter() {
|
||||
loader_field_ids.insert(*loader_field_id);
|
||||
}
|
||||
|
||||
// Add loader + loader associated data to the map
|
||||
let version_loader_data = VersionLoaderData {
|
||||
loaders: m.loaders.unwrap_or_default(),
|
||||
project_types: m.project_types.unwrap_or_default(),
|
||||
games: m.games.unwrap_or_default(),
|
||||
loader_loader_field_ids,
|
||||
};
|
||||
(version_id,version_loader_data)
|
||||
|
||||
// Add loader fields to the set we need to fetch
|
||||
let loader_loader_field_ids = m.loader_fields.unwrap_or_default().into_iter().map(LoaderFieldId).collect::<Vec<_>>();
|
||||
for loader_field_id in loader_loader_field_ids.iter() {
|
||||
loader_field_ids.insert(*loader_field_id);
|
||||
}
|
||||
).try_collect().await?;
|
||||
|
||||
// Add loader + loader associated data to the map
|
||||
let version_loader_data = VersionLoaderData {
|
||||
loaders: m.loaders.unwrap_or_default(),
|
||||
project_types: m.project_types.unwrap_or_default(),
|
||||
games: m.games.unwrap_or_default(),
|
||||
loader_loader_field_ids,
|
||||
};
|
||||
(version_id,version_loader_data)
|
||||
|
||||
}
|
||||
).try_collect().await?;
|
||||
|
||||
// Fetch all loader fields from any version
|
||||
let loader_fields: Vec<QueryLoaderField> = sqlx::query!(
|
||||
// Fetch all loader fields from any version
|
||||
let loader_fields: Vec<QueryLoaderField> = sqlx::query!(
|
||||
"
|
||||
SELECT DISTINCT id, field, field_type, enum_type, min_val, max_val, optional
|
||||
FROM loader_fields lf
|
||||
@@ -572,20 +578,20 @@ impl Version {
|
||||
",
|
||||
&loader_field_ids.iter().map(|x| x.0).collect::<Vec<_>>()
|
||||
)
|
||||
.fetch(&mut *exec)
|
||||
.map_ok(|m| QueryLoaderField {
|
||||
id: LoaderFieldId(m.id),
|
||||
field: m.field,
|
||||
field_type: m.field_type,
|
||||
enum_type: m.enum_type.map(LoaderFieldEnumId),
|
||||
min_val: m.min_val,
|
||||
max_val: m.max_val,
|
||||
optional: m.optional,
|
||||
})
|
||||
.try_collect()
|
||||
.await?;
|
||||
.fetch(&mut *exec)
|
||||
.map_ok(|m| QueryLoaderField {
|
||||
id: LoaderFieldId(m.id),
|
||||
field: m.field,
|
||||
field_type: m.field_type,
|
||||
enum_type: m.enum_type.map(LoaderFieldEnumId),
|
||||
min_val: m.min_val,
|
||||
max_val: m.max_val,
|
||||
optional: m.optional,
|
||||
})
|
||||
.try_collect()
|
||||
.await?;
|
||||
|
||||
let loader_field_enum_values: Vec<QueryLoaderFieldEnumValue> = sqlx::query!(
|
||||
let loader_field_enum_values: Vec<QueryLoaderFieldEnumValue> = sqlx::query!(
|
||||
"
|
||||
SELECT DISTINCT id, enum_id, value, ordering, created, metadata
|
||||
FROM loader_field_enum_values lfev
|
||||
@@ -597,38 +603,38 @@ impl Version {
|
||||
.map(|x| x.0)
|
||||
.collect::<Vec<_>>()
|
||||
)
|
||||
.fetch(&mut *exec)
|
||||
.map_ok(|m| QueryLoaderFieldEnumValue {
|
||||
id: LoaderFieldEnumValueId(m.id),
|
||||
enum_id: LoaderFieldEnumId(m.enum_id),
|
||||
value: m.value,
|
||||
ordering: m.ordering,
|
||||
created: m.created,
|
||||
metadata: m.metadata,
|
||||
})
|
||||
.try_collect()
|
||||
.await?;
|
||||
.fetch(&mut *exec)
|
||||
.map_ok(|m| QueryLoaderFieldEnumValue {
|
||||
id: LoaderFieldEnumValueId(m.id),
|
||||
enum_id: LoaderFieldEnumId(m.enum_id),
|
||||
value: m.value,
|
||||
ordering: m.ordering,
|
||||
created: m.created,
|
||||
metadata: m.metadata,
|
||||
})
|
||||
.try_collect()
|
||||
.await?;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Hash {
|
||||
pub file_id: FileId,
|
||||
pub algorithm: String,
|
||||
pub hash: String,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct Hash {
|
||||
pub file_id: FileId,
|
||||
pub algorithm: String,
|
||||
pub hash: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct File {
|
||||
pub id: FileId,
|
||||
pub url: String,
|
||||
pub filename: String,
|
||||
pub primary: bool,
|
||||
pub size: u32,
|
||||
pub file_type: Option<FileType>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct File {
|
||||
pub id: FileId,
|
||||
pub url: String,
|
||||
pub filename: String,
|
||||
pub primary: bool,
|
||||
pub size: u32,
|
||||
pub file_type: Option<FileType>,
|
||||
}
|
||||
|
||||
let file_ids = DashSet::new();
|
||||
let reverse_file_map = DashMap::new();
|
||||
let files : DashMap<VersionId, Vec<File>> = sqlx::query!(
|
||||
let file_ids = DashSet::new();
|
||||
let reverse_file_map = DashMap::new();
|
||||
let files : DashMap<VersionId, Vec<File>> = sqlx::query!(
|
||||
"
|
||||
SELECT DISTINCT version_id, f.id, f.url, f.filename, f.is_primary, f.size, f.file_type
|
||||
FROM files f
|
||||
@@ -636,27 +642,27 @@ impl Version {
|
||||
",
|
||||
&version_ids
|
||||
).fetch(&mut *exec)
|
||||
.try_fold(DashMap::new(), |acc : DashMap<VersionId, Vec<File>>, m| {
|
||||
let file = File {
|
||||
id: FileId(m.id),
|
||||
url: m.url,
|
||||
filename: m.filename,
|
||||
primary: m.is_primary,
|
||||
size: m.size as u32,
|
||||
file_type: m.file_type.map(|x| FileType::from_string(&x)),
|
||||
};
|
||||
.try_fold(DashMap::new(), |acc : DashMap<VersionId, Vec<File>>, m| {
|
||||
let file = File {
|
||||
id: FileId(m.id),
|
||||
url: m.url,
|
||||
filename: m.filename,
|
||||
primary: m.is_primary,
|
||||
size: m.size as u32,
|
||||
file_type: m.file_type.map(|x| FileType::from_string(&x)),
|
||||
};
|
||||
|
||||
file_ids.insert(FileId(m.id));
|
||||
reverse_file_map.insert(FileId(m.id), VersionId(m.version_id));
|
||||
file_ids.insert(FileId(m.id));
|
||||
reverse_file_map.insert(FileId(m.id), VersionId(m.version_id));
|
||||
|
||||
acc.entry(VersionId(m.version_id))
|
||||
.or_default()
|
||||
.push(file);
|
||||
async move { Ok(acc) }
|
||||
}
|
||||
).await?;
|
||||
acc.entry(VersionId(m.version_id))
|
||||
.or_default()
|
||||
.push(file);
|
||||
async move { Ok(acc) }
|
||||
}
|
||||
).await?;
|
||||
|
||||
let hashes: DashMap<VersionId, Vec<Hash>> = sqlx::query!(
|
||||
let hashes: DashMap<VersionId, Vec<Hash>> = sqlx::query!(
|
||||
"
|
||||
SELECT DISTINCT file_id, algorithm, encode(hash, 'escape') hash
|
||||
FROM hashes
|
||||
@@ -664,24 +670,24 @@ impl Version {
|
||||
",
|
||||
&file_ids.iter().map(|x| x.0).collect::<Vec<_>>()
|
||||
)
|
||||
.fetch(&mut *exec)
|
||||
.try_fold(DashMap::new(), |acc: DashMap<VersionId, Vec<Hash>>, m| {
|
||||
if let Some(found_hash) = m.hash {
|
||||
let hash = Hash {
|
||||
file_id: FileId(m.file_id),
|
||||
algorithm: m.algorithm,
|
||||
hash: found_hash,
|
||||
};
|
||||
.fetch(&mut *exec)
|
||||
.try_fold(DashMap::new(), |acc: DashMap<VersionId, Vec<Hash>>, m| {
|
||||
if let Some(found_hash) = m.hash {
|
||||
let hash = Hash {
|
||||
file_id: FileId(m.file_id),
|
||||
algorithm: m.algorithm,
|
||||
hash: found_hash,
|
||||
};
|
||||
|
||||
if let Some(version_id) = reverse_file_map.get(&FileId(m.file_id)) {
|
||||
acc.entry(*version_id).or_default().push(hash);
|
||||
if let Some(version_id) = reverse_file_map.get(&FileId(m.file_id)) {
|
||||
acc.entry(*version_id).or_default().push(hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
async move { Ok(acc) }
|
||||
})
|
||||
.await?;
|
||||
async move { Ok(acc) }
|
||||
})
|
||||
.await?;
|
||||
|
||||
let dependencies : DashMap<VersionId, Vec<QueryDependency>> = sqlx::query!(
|
||||
let dependencies : DashMap<VersionId, Vec<QueryDependency>> = sqlx::query!(
|
||||
"
|
||||
SELECT DISTINCT dependent_id as version_id, d.mod_dependency_id as dependency_project_id, d.dependency_id as dependency_version_id, d.dependency_file_name as file_name, d.dependency_type as dependency_type
|
||||
FROM dependencies d
|
||||
@@ -689,22 +695,22 @@ impl Version {
|
||||
",
|
||||
&version_ids
|
||||
).fetch(&mut *exec)
|
||||
.try_fold(DashMap::new(), |acc : DashMap<_,Vec<QueryDependency>>, m| {
|
||||
let dependency = QueryDependency {
|
||||
project_id: m.dependency_project_id.map(ProjectId),
|
||||
version_id: m.dependency_version_id.map(VersionId),
|
||||
file_name: m.file_name,
|
||||
dependency_type: m.dependency_type,
|
||||
};
|
||||
.try_fold(DashMap::new(), |acc : DashMap<_,Vec<QueryDependency>>, m| {
|
||||
let dependency = QueryDependency {
|
||||
project_id: m.dependency_project_id.map(ProjectId),
|
||||
version_id: m.dependency_version_id.map(VersionId),
|
||||
file_name: m.file_name,
|
||||
dependency_type: m.dependency_type,
|
||||
};
|
||||
|
||||
acc.entry(VersionId(m.version_id))
|
||||
.or_default()
|
||||
.push(dependency);
|
||||
async move { Ok(acc) }
|
||||
}
|
||||
).await?;
|
||||
acc.entry(VersionId(m.version_id))
|
||||
.or_default()
|
||||
.push(dependency);
|
||||
async move { Ok(acc) }
|
||||
}
|
||||
).await?;
|
||||
|
||||
let res = sqlx::query!(
|
||||
let res = sqlx::query!(
|
||||
"
|
||||
SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number,
|
||||
v.changelog changelog, v.date_published date_published, v.downloads downloads,
|
||||
@@ -714,96 +720,97 @@ impl Version {
|
||||
",
|
||||
&version_ids
|
||||
)
|
||||
.fetch(&mut *exec)
|
||||
.try_fold(DashMap::new(), |acc, v| {
|
||||
let version_id = VersionId(v.id);
|
||||
let VersionLoaderData {
|
||||
loaders,
|
||||
project_types,
|
||||
games,
|
||||
loader_loader_field_ids,
|
||||
} = loaders_ptypes_games.remove(&version_id).map(|x|x.1).unwrap_or_default();
|
||||
let files = files.remove(&version_id).map(|x|x.1).unwrap_or_default();
|
||||
let hashes = hashes.remove(&version_id).map(|x|x.1).unwrap_or_default();
|
||||
let version_fields = version_fields.remove(&version_id).map(|x|x.1).unwrap_or_default();
|
||||
let dependencies = dependencies.remove(&version_id).map(|x|x.1).unwrap_or_default();
|
||||
.fetch(&mut *exec)
|
||||
.try_fold(DashMap::new(), |acc, v| {
|
||||
let version_id = VersionId(v.id);
|
||||
let VersionLoaderData {
|
||||
loaders,
|
||||
project_types,
|
||||
games,
|
||||
loader_loader_field_ids,
|
||||
} = loaders_ptypes_games.remove(&version_id).map(|x|x.1).unwrap_or_default();
|
||||
let files = files.remove(&version_id).map(|x|x.1).unwrap_or_default();
|
||||
let hashes = hashes.remove(&version_id).map(|x|x.1).unwrap_or_default();
|
||||
let version_fields = version_fields.remove(&version_id).map(|x|x.1).unwrap_or_default();
|
||||
let dependencies = dependencies.remove(&version_id).map(|x|x.1).unwrap_or_default();
|
||||
|
||||
let loader_fields = loader_fields.iter()
|
||||
.filter(|x| loader_loader_field_ids.contains(&x.id))
|
||||
.collect::<Vec<_>>();
|
||||
let loader_fields = loader_fields.iter()
|
||||
.filter(|x| loader_loader_field_ids.contains(&x.id))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let query_version = QueryVersion {
|
||||
inner: Version {
|
||||
id: VersionId(v.id),
|
||||
project_id: ProjectId(v.mod_id),
|
||||
author_id: UserId(v.author_id),
|
||||
name: v.version_name,
|
||||
version_number: v.version_number,
|
||||
changelog: v.changelog,
|
||||
date_published: v.date_published,
|
||||
downloads: v.downloads,
|
||||
version_type: v.version_type,
|
||||
featured: v.featured,
|
||||
status: VersionStatus::from_string(&v.status),
|
||||
requested_status: v.requested_status
|
||||
.map(|x| VersionStatus::from_string(&x)),
|
||||
ordering: v.ordering,
|
||||
},
|
||||
files: {
|
||||
let mut files = files.into_iter().map(|x| {
|
||||
let mut file_hashes = HashMap::new();
|
||||
let query_version = QueryVersion {
|
||||
inner: Version {
|
||||
id: VersionId(v.id),
|
||||
project_id: ProjectId(v.mod_id),
|
||||
author_id: UserId(v.author_id),
|
||||
name: v.version_name,
|
||||
version_number: v.version_number,
|
||||
changelog: v.changelog,
|
||||
date_published: v.date_published,
|
||||
downloads: v.downloads,
|
||||
version_type: v.version_type,
|
||||
featured: v.featured,
|
||||
status: VersionStatus::from_string(&v.status),
|
||||
requested_status: v.requested_status
|
||||
.map(|x| VersionStatus::from_string(&x)),
|
||||
ordering: v.ordering,
|
||||
},
|
||||
files: {
|
||||
let mut files = files.into_iter().map(|x| {
|
||||
let mut file_hashes = HashMap::new();
|
||||
|
||||
for hash in hashes.iter() {
|
||||
if hash.file_id == x.id {
|
||||
file_hashes.insert(
|
||||
hash.algorithm.clone(),
|
||||
hash.hash.clone(),
|
||||
);
|
||||
for hash in hashes.iter() {
|
||||
if hash.file_id == x.id {
|
||||
file_hashes.insert(
|
||||
hash.algorithm.clone(),
|
||||
hash.hash.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QueryFile {
|
||||
id: x.id,
|
||||
url: x.url.clone(),
|
||||
filename: x.filename.clone(),
|
||||
hashes: file_hashes,
|
||||
primary: x.primary,
|
||||
size: x.size,
|
||||
file_type: x.file_type,
|
||||
}
|
||||
}).collect::<Vec<_>>();
|
||||
QueryFile {
|
||||
id: x.id,
|
||||
url: x.url.clone(),
|
||||
filename: x.filename.clone(),
|
||||
hashes: file_hashes,
|
||||
primary: x.primary,
|
||||
size: x.size,
|
||||
file_type: x.file_type,
|
||||
}
|
||||
}).collect::<Vec<_>>();
|
||||
|
||||
files.sort_by(|a, b| {
|
||||
if a.primary {
|
||||
Ordering::Less
|
||||
} else if b.primary {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
a.filename.cmp(&b.filename)
|
||||
}
|
||||
});
|
||||
files.sort_by(|a, b| {
|
||||
if a.primary {
|
||||
Ordering::Less
|
||||
} else if b.primary {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
a.filename.cmp(&b.filename)
|
||||
}
|
||||
});
|
||||
|
||||
files
|
||||
},
|
||||
version_fields: VersionField::from_query_json(version_fields, &loader_fields, &loader_field_enum_values, false),
|
||||
loaders,
|
||||
project_types,
|
||||
games,
|
||||
dependencies,
|
||||
};
|
||||
files
|
||||
},
|
||||
version_fields: VersionField::from_query_json(version_fields, &loader_fields, &loader_field_enum_values, false),
|
||||
loaders,
|
||||
project_types,
|
||||
games,
|
||||
dependencies,
|
||||
};
|
||||
|
||||
acc.insert(v.id, query_version);
|
||||
async move { Ok(acc) }
|
||||
})
|
||||
.await?;
|
||||
acc.insert(v.id, query_version);
|
||||
async move { Ok(acc) }
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(res)
|
||||
},
|
||||
).await?;
|
||||
Ok(res)
|
||||
},
|
||||
).await?;
|
||||
|
||||
val.sort();
|
||||
val.sort();
|
||||
|
||||
Ok(val)
|
||||
Ok(val)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_file_from_hash<'a, 'b, E>(
|
||||
@@ -814,7 +821,7 @@ impl Version {
|
||||
redis: &RedisPool,
|
||||
) -> Result<Option<SingleFile>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy + Send + Sync,
|
||||
{
|
||||
Self::get_files_from_hash(algo, &[hash], executor, redis)
|
||||
.await
|
||||
@@ -831,7 +838,7 @@ impl Version {
|
||||
redis: &RedisPool,
|
||||
) -> Result<Vec<SingleFile>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy + Send + Sync,
|
||||
{
|
||||
let val = redis.get_cached_keys(
|
||||
VERSION_FILES_NAMESPACE,
|
||||
|
||||
@@ -67,9 +67,9 @@ impl RedisPool {
|
||||
closure: F,
|
||||
) -> Result<Vec<T>, DatabaseError>
|
||||
where
|
||||
F: FnOnce(Vec<K>) -> Fut,
|
||||
Fut: Future<Output = Result<DashMap<K, T>, DatabaseError>>,
|
||||
T: Serialize + DeserializeOwned,
|
||||
F: FnOnce(Vec<K>) -> Fut + Send,
|
||||
Fut: Future<Output = Result<DashMap<K, T>, DatabaseError>> + Send,
|
||||
T: Serialize + DeserializeOwned + Send,
|
||||
K: Display
|
||||
+ Hash
|
||||
+ Eq
|
||||
@@ -77,7 +77,8 @@ impl RedisPool {
|
||||
+ Clone
|
||||
+ DeserializeOwned
|
||||
+ Serialize
|
||||
+ Debug,
|
||||
+ Debug
|
||||
+ Send,
|
||||
{
|
||||
Ok(self
|
||||
.get_cached_keys_raw(namespace, keys, closure)
|
||||
@@ -94,9 +95,9 @@ impl RedisPool {
|
||||
closure: F,
|
||||
) -> Result<HashMap<K, T>, DatabaseError>
|
||||
where
|
||||
F: FnOnce(Vec<K>) -> Fut,
|
||||
Fut: Future<Output = Result<DashMap<K, T>, DatabaseError>>,
|
||||
T: Serialize + DeserializeOwned,
|
||||
F: FnOnce(Vec<K>) -> Fut + Send,
|
||||
Fut: Future<Output = Result<DashMap<K, T>, DatabaseError>> + Send,
|
||||
T: Serialize + DeserializeOwned + Send,
|
||||
K: Display
|
||||
+ Hash
|
||||
+ Eq
|
||||
@@ -104,7 +105,8 @@ impl RedisPool {
|
||||
+ Clone
|
||||
+ DeserializeOwned
|
||||
+ Serialize
|
||||
+ Debug,
|
||||
+ Debug
|
||||
+ Send,
|
||||
{
|
||||
self.get_cached_keys_raw_with_slug(
|
||||
namespace,
|
||||
@@ -131,18 +133,20 @@ impl RedisPool {
|
||||
closure: F,
|
||||
) -> Result<Vec<T>, DatabaseError>
|
||||
where
|
||||
F: FnOnce(Vec<I>) -> Fut,
|
||||
Fut: Future<Output = Result<DashMap<K, (Option<S>, T)>, DatabaseError>>,
|
||||
T: Serialize + DeserializeOwned,
|
||||
I: Display + Hash + Eq + PartialEq + Clone + Debug,
|
||||
F: FnOnce(Vec<I>) -> Fut + Send,
|
||||
Fut: Future<Output = Result<DashMap<K, (Option<S>, T)>, DatabaseError>>
|
||||
+ Send,
|
||||
T: Serialize + DeserializeOwned + Send,
|
||||
I: Display + Hash + Eq + PartialEq + Clone + Debug + Send,
|
||||
K: Display
|
||||
+ Hash
|
||||
+ Eq
|
||||
+ PartialEq
|
||||
+ Clone
|
||||
+ DeserializeOwned
|
||||
+ Serialize,
|
||||
S: Display + Clone + DeserializeOwned + Serialize + Debug,
|
||||
+ Serialize
|
||||
+ Send,
|
||||
S: Display + Clone + DeserializeOwned + Serialize + Debug + Send,
|
||||
{
|
||||
Ok(self
|
||||
.get_cached_keys_raw_with_slug(
|
||||
@@ -167,18 +171,20 @@ impl RedisPool {
|
||||
closure: F,
|
||||
) -> Result<HashMap<K, T>, DatabaseError>
|
||||
where
|
||||
F: FnOnce(Vec<I>) -> Fut,
|
||||
Fut: Future<Output = Result<DashMap<K, (Option<S>, T)>, DatabaseError>>,
|
||||
T: Serialize + DeserializeOwned,
|
||||
I: Display + Hash + Eq + PartialEq + Clone + Debug,
|
||||
F: FnOnce(Vec<I>) -> Fut + Send,
|
||||
Fut: Future<Output = Result<DashMap<K, (Option<S>, T)>, DatabaseError>>
|
||||
+ Send,
|
||||
T: Serialize + DeserializeOwned + Send,
|
||||
I: Display + Hash + Eq + PartialEq + Clone + Debug + Send,
|
||||
K: Display
|
||||
+ Hash
|
||||
+ Eq
|
||||
+ PartialEq
|
||||
+ Clone
|
||||
+ DeserializeOwned
|
||||
+ Serialize,
|
||||
S: Display + Clone + DeserializeOwned + Serialize + Debug,
|
||||
+ Serialize
|
||||
+ Send,
|
||||
S: Display + Clone + DeserializeOwned + Serialize + Debug + Send,
|
||||
{
|
||||
let connection = self.connect().await?.connection;
|
||||
|
||||
@@ -295,12 +301,7 @@ impl RedisPool {
|
||||
|
||||
fetch_ids.iter().for_each(|key| {
|
||||
pipe.atomic().set_options(
|
||||
// We store locks in lowercase because they are case insensitive
|
||||
format!(
|
||||
"{}_{namespace}:{}/lock",
|
||||
self.meta_namespace,
|
||||
key.to_lowercase()
|
||||
),
|
||||
format!("{}_{namespace}:{}/lock", self.meta_namespace, key),
|
||||
100,
|
||||
SetOptions::default()
|
||||
.get(true)
|
||||
@@ -343,11 +344,11 @@ impl RedisPool {
|
||||
Pin<
|
||||
Box<
|
||||
dyn Future<
|
||||
Output = Result<
|
||||
HashMap<K, RedisValue<T, K, S>>,
|
||||
DatabaseError,
|
||||
>,
|
||||
>,
|
||||
Output = Result<
|
||||
HashMap<K, RedisValue<T, K, S>>,
|
||||
DatabaseError,
|
||||
>,
|
||||
> + Send,
|
||||
>,
|
||||
>,
|
||||
> = Vec::new();
|
||||
@@ -400,9 +401,7 @@ impl RedisPool {
|
||||
|
||||
pipe.atomic().del(format!(
|
||||
"{}_{namespace}:{}/lock",
|
||||
// Locks are stored in lowercase
|
||||
self.meta_namespace,
|
||||
actual_slug.to_lowercase()
|
||||
self.meta_namespace, actual_slug
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -415,10 +414,8 @@ impl RedisPool {
|
||||
ids.remove(&base62);
|
||||
|
||||
pipe.atomic().del(format!(
|
||||
"{}_{namespace}:{}/lock",
|
||||
self.meta_namespace,
|
||||
// Locks are stored in lowercase
|
||||
base62.to_lowercase()
|
||||
"{}_{namespace}:{base62}/lock",
|
||||
self.meta_namespace
|
||||
));
|
||||
}
|
||||
|
||||
@@ -432,11 +429,6 @@ impl RedisPool {
|
||||
}
|
||||
|
||||
for (key, _) in ids {
|
||||
pipe.atomic().del(format!(
|
||||
"{}_{namespace}:{}/lock",
|
||||
self.meta_namespace,
|
||||
key.to_lowercase()
|
||||
));
|
||||
pipe.atomic().del(format!(
|
||||
"{}_{namespace}:{key}/lock",
|
||||
self.meta_namespace
|
||||
@@ -465,8 +457,7 @@ impl RedisPool {
|
||||
format!(
|
||||
"{}_{namespace}:{}/lock",
|
||||
self.meta_namespace,
|
||||
// We lowercase key because locks are stored in lowercase
|
||||
x.key().to_lowercase()
|
||||
x.key()
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
@@ -562,23 +553,6 @@ impl RedisConnection {
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn get_many(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
ids: &[String],
|
||||
) -> Result<Vec<Option<String>>, DatabaseError> {
|
||||
let mut cmd = cmd("MGET");
|
||||
redis_args(
|
||||
&mut cmd,
|
||||
ids.iter()
|
||||
.map(|x| format!("{}_{}:{}", self.meta_namespace, namespace, x))
|
||||
.collect::<Vec<_>>()
|
||||
.as_slice(),
|
||||
);
|
||||
let res = redis_execute(&mut cmd, &mut self.connection).await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn get_deserialized_from_json<R>(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
@@ -593,22 +567,6 @@ impl RedisConnection {
|
||||
.and_then(|x| serde_json::from_str(&x).ok()))
|
||||
}
|
||||
|
||||
pub async fn get_many_deserialized_from_json<R>(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
ids: &[String],
|
||||
) -> Result<Vec<Option<R>>, DatabaseError>
|
||||
where
|
||||
R: for<'a> serde::Deserialize<'a>,
|
||||
{
|
||||
Ok(self
|
||||
.get_many(namespace, ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| x.and_then(|val| serde_json::from_str::<R>(&val).ok()))
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
pub async fn delete<T1>(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
|
||||
@@ -74,10 +74,10 @@ impl FileHost for S3Host {
|
||||
content_type,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
FileHostingError::S3Error(format!(
|
||||
"Error while uploading file {file_name} to S3: {err}"
|
||||
))
|
||||
.map_err(|_| {
|
||||
FileHostingError::S3Error(
|
||||
"Error while uploading file to S3".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(UploadFileData {
|
||||
@@ -100,10 +100,10 @@ impl FileHost for S3Host {
|
||||
self.bucket
|
||||
.delete_object(format!("/{file_name}"))
|
||||
.await
|
||||
.map_err(|err| {
|
||||
FileHostingError::S3Error(format!(
|
||||
"Error while deleting file {file_name} to S3: {err}"
|
||||
))
|
||||
.map_err(|_| {
|
||||
FileHostingError::S3Error(
|
||||
"Error while deleting file from S3".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(DeleteFileData {
|
||||
|
||||
@@ -2,9 +2,9 @@ use std::num::NonZeroU32;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use actix_web::web;
|
||||
use database::redis::RedisPool;
|
||||
use log::{info, warn};
|
||||
use ntex::{web, ServiceFactory};
|
||||
use queue::{
|
||||
analytics::AnalyticsQueue, payouts::PayoutsQueue, session::AuthQueue,
|
||||
socket::ActiveSockets,
|
||||
@@ -15,9 +15,11 @@ extern crate clickhouse as clickhouse_crate;
|
||||
use clickhouse_crate::Client;
|
||||
use governor::middleware::StateInformationMiddleware;
|
||||
use governor::{Quota, RateLimiter};
|
||||
use ntex::web::{App, DefaultError, ErrorRenderer, WebRequest};
|
||||
use util::cors::default_cors;
|
||||
|
||||
use crate::queue::moderation::AutomatedModerationQueue;
|
||||
use crate::scheduler::schedule;
|
||||
use crate::util::ratelimit::KeyedRateLimiter;
|
||||
use crate::{
|
||||
queue::payouts::process_payout,
|
||||
@@ -49,14 +51,13 @@ pub struct LabrinthConfig {
|
||||
pub clickhouse: Client,
|
||||
pub file_host: Arc<dyn file_hosting::FileHost + Send + Sync>,
|
||||
pub maxmind: Arc<queue::maxmind::MaxMindIndexer>,
|
||||
pub scheduler: Arc<scheduler::Scheduler>,
|
||||
pub ip_salt: Pepper,
|
||||
pub search_config: search::SearchConfig,
|
||||
pub session_queue: web::Data<AuthQueue>,
|
||||
pub payouts_queue: web::Data<PayoutsQueue>,
|
||||
pub session_queue: Arc<AuthQueue>,
|
||||
pub payouts_queue: Arc<PayoutsQueue>,
|
||||
pub analytics_queue: Arc<AnalyticsQueue>,
|
||||
pub active_sockets: web::Data<ActiveSockets>,
|
||||
pub automated_moderation_queue: web::Data<AutomatedModerationQueue>,
|
||||
pub active_sockets: Arc<ActiveSockets>,
|
||||
pub automated_moderation_queue: Arc<AutomatedModerationQueue>,
|
||||
pub rate_limiter: KeyedRateLimiter,
|
||||
pub stripe_client: stripe::Client,
|
||||
}
|
||||
@@ -75,27 +76,25 @@ pub fn app_setup(
|
||||
);
|
||||
|
||||
let automated_moderation_queue =
|
||||
web::Data::new(AutomatedModerationQueue::default());
|
||||
Arc::new(AutomatedModerationQueue::default());
|
||||
|
||||
{
|
||||
let automated_moderation_queue_ref = automated_moderation_queue.clone();
|
||||
let pool_ref = pool.clone();
|
||||
let redis_pool_ref = redis_pool.clone();
|
||||
actix_rt::spawn(async move {
|
||||
tokio::task::spawn(async move {
|
||||
automated_moderation_queue_ref
|
||||
.task(pool_ref, redis_pool_ref)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
let mut scheduler = scheduler::Scheduler::new();
|
||||
|
||||
let limiter: KeyedRateLimiter = Arc::new(
|
||||
RateLimiter::keyed(Quota::per_minute(NonZeroU32::new(300).unwrap()))
|
||||
.with_middleware::<StateInformationMiddleware>(),
|
||||
);
|
||||
let limiter_clone = Arc::clone(&limiter);
|
||||
scheduler.run(Duration::from_secs(60), move || {
|
||||
schedule(Duration::from_secs(60), move || {
|
||||
info!(
|
||||
"Clearing ratelimiter, storage size: {}",
|
||||
limiter_clone.len()
|
||||
@@ -118,18 +117,15 @@ pub fn app_setup(
|
||||
let pool_ref = pool.clone();
|
||||
let search_config_ref = search_config.clone();
|
||||
let redis_pool_ref = redis_pool.clone();
|
||||
scheduler.run(local_index_interval, move || {
|
||||
schedule(local_index_interval, move || {
|
||||
let pool_ref = pool_ref.clone();
|
||||
let redis_pool_ref = redis_pool_ref.clone();
|
||||
let search_config_ref = search_config_ref.clone();
|
||||
async move {
|
||||
info!("Indexing local database");
|
||||
let result = index_projects(
|
||||
pool_ref,
|
||||
redis_pool_ref.clone(),
|
||||
&search_config_ref,
|
||||
)
|
||||
.await;
|
||||
let result =
|
||||
index_projects(&pool_ref, &redis_pool_ref, &search_config_ref)
|
||||
.await;
|
||||
if let Err(e) = result {
|
||||
warn!("Local project indexing failed: {:?}", e);
|
||||
}
|
||||
@@ -140,7 +136,7 @@ pub fn app_setup(
|
||||
// Changes statuses of scheduled projects/versions
|
||||
let pool_ref = pool.clone();
|
||||
// TODO: Clear cache when these are run
|
||||
scheduler.run(std::time::Duration::from_secs(60 * 5), move || {
|
||||
schedule(std::time::Duration::from_secs(60 * 5), move || {
|
||||
let pool_ref = pool_ref.clone();
|
||||
info!("Releasing scheduled versions/projects!");
|
||||
|
||||
@@ -157,7 +153,10 @@ pub fn app_setup(
|
||||
.await;
|
||||
|
||||
if let Err(e) = projects_results {
|
||||
warn!("Syncing scheduled releases for projects failed: {:?}", e);
|
||||
warn!(
|
||||
"Syncing scheduled releases for projects failed: {:?}",
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
let versions_results = sqlx::query!(
|
||||
@@ -172,25 +171,24 @@ pub fn app_setup(
|
||||
.await;
|
||||
|
||||
if let Err(e) = versions_results {
|
||||
warn!("Syncing scheduled releases for versions failed: {:?}", e);
|
||||
warn!(
|
||||
"Syncing scheduled releases for versions failed: {:?}",
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
info!("Finished releasing scheduled versions/projects");
|
||||
}
|
||||
});
|
||||
|
||||
scheduler::schedule_versions(
|
||||
&mut scheduler,
|
||||
pool.clone(),
|
||||
redis_pool.clone(),
|
||||
);
|
||||
scheduler::schedule_versions(pool.clone(), redis_pool.clone());
|
||||
|
||||
let session_queue = web::Data::new(AuthQueue::new());
|
||||
let session_queue = Arc::new(AuthQueue::new());
|
||||
|
||||
let pool_ref = pool.clone();
|
||||
let redis_ref = redis_pool.clone();
|
||||
let session_queue_ref = session_queue.clone();
|
||||
scheduler.run(std::time::Duration::from_secs(60 * 30), move || {
|
||||
schedule(std::time::Duration::from_secs(60 * 30), move || {
|
||||
let pool_ref = pool_ref.clone();
|
||||
let redis_ref = redis_ref.clone();
|
||||
let session_queue_ref = session_queue_ref.clone();
|
||||
@@ -208,7 +206,7 @@ pub fn app_setup(
|
||||
let reader = maxmind.clone();
|
||||
{
|
||||
let reader_ref = reader;
|
||||
scheduler.run(std::time::Duration::from_secs(60 * 60 * 24), move || {
|
||||
schedule(std::time::Duration::from_secs(60 * 60 * 24), move || {
|
||||
let reader_ref = reader_ref.clone();
|
||||
|
||||
async move {
|
||||
@@ -232,7 +230,7 @@ pub fn app_setup(
|
||||
let analytics_queue_ref = analytics_queue.clone();
|
||||
let pool_ref = pool.clone();
|
||||
let redis_ref = redis_pool.clone();
|
||||
scheduler.run(std::time::Duration::from_secs(15), move || {
|
||||
schedule(std::time::Duration::from_secs(15), move || {
|
||||
let client_ref = client_ref.clone();
|
||||
let analytics_queue_ref = analytics_queue_ref.clone();
|
||||
let pool_ref = pool_ref.clone();
|
||||
@@ -254,7 +252,7 @@ pub fn app_setup(
|
||||
{
|
||||
let pool_ref = pool.clone();
|
||||
let client_ref = clickhouse.clone();
|
||||
scheduler.run(std::time::Duration::from_secs(60 * 60 * 6), move || {
|
||||
schedule(std::time::Duration::from_secs(60 * 60 * 6), move || {
|
||||
let pool_ref = pool_ref.clone();
|
||||
let client_ref = client_ref.clone();
|
||||
|
||||
@@ -276,7 +274,7 @@ pub fn app_setup(
|
||||
let redis_ref = redis_pool.clone();
|
||||
let stripe_client_ref = stripe_client.clone();
|
||||
|
||||
actix_rt::spawn(async move {
|
||||
tokio::task::spawn(async move {
|
||||
routes::internal::billing::task(
|
||||
stripe_client_ref,
|
||||
pool_ref,
|
||||
@@ -290,7 +288,7 @@ pub fn app_setup(
|
||||
let pool_ref = pool.clone();
|
||||
let redis_ref = redis_pool.clone();
|
||||
|
||||
actix_rt::spawn(async move {
|
||||
tokio::task::spawn(async move {
|
||||
routes::internal::billing::subscription_task(pool_ref, redis_ref)
|
||||
.await;
|
||||
});
|
||||
@@ -301,8 +299,8 @@ pub fn app_setup(
|
||||
.to_string(),
|
||||
};
|
||||
|
||||
let payouts_queue = web::Data::new(PayoutsQueue::new());
|
||||
let active_sockets = web::Data::new(ActiveSockets::default());
|
||||
let payouts_queue = Arc::new(PayoutsQueue::new());
|
||||
let active_sockets = Arc::new(ActiveSockets::default());
|
||||
|
||||
LabrinthConfig {
|
||||
pool,
|
||||
@@ -310,7 +308,6 @@ pub fn app_setup(
|
||||
clickhouse: clickhouse.clone(),
|
||||
file_host,
|
||||
maxmind,
|
||||
scheduler: Arc::new(scheduler),
|
||||
ip_salt,
|
||||
search_config,
|
||||
session_queue,
|
||||
@@ -323,41 +320,25 @@ pub fn app_setup(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn app_config(
|
||||
cfg: &mut web::ServiceConfig,
|
||||
labrinth_config: LabrinthConfig,
|
||||
) {
|
||||
cfg.app_data(web::FormConfig::default().error_handler(|err, _req| {
|
||||
routes::ApiError::Validation(err.to_string()).into()
|
||||
}))
|
||||
.app_data(web::PathConfig::default().error_handler(|err, _req| {
|
||||
routes::ApiError::Validation(err.to_string()).into()
|
||||
}))
|
||||
.app_data(web::QueryConfig::default().error_handler(|err, _req| {
|
||||
routes::ApiError::Validation(err.to_string()).into()
|
||||
}))
|
||||
.app_data(web::JsonConfig::default().error_handler(|err, _req| {
|
||||
routes::ApiError::Validation(err.to_string()).into()
|
||||
}))
|
||||
.app_data(web::Data::new(labrinth_config.redis_pool.clone()))
|
||||
.app_data(web::Data::new(labrinth_config.pool.clone()))
|
||||
.app_data(web::Data::new(labrinth_config.file_host.clone()))
|
||||
.app_data(web::Data::new(labrinth_config.search_config.clone()))
|
||||
.app_data(labrinth_config.session_queue.clone())
|
||||
.app_data(labrinth_config.payouts_queue.clone())
|
||||
.app_data(web::Data::new(labrinth_config.ip_salt.clone()))
|
||||
.app_data(web::Data::new(labrinth_config.analytics_queue.clone()))
|
||||
.app_data(web::Data::new(labrinth_config.clickhouse.clone()))
|
||||
.app_data(web::Data::new(labrinth_config.maxmind.clone()))
|
||||
.app_data(labrinth_config.active_sockets.clone())
|
||||
.app_data(labrinth_config.automated_moderation_queue.clone())
|
||||
.app_data(web::Data::new(labrinth_config.stripe_client.clone()))
|
||||
.configure(routes::v2::config)
|
||||
.configure(routes::v3::config)
|
||||
.configure(routes::internal::config)
|
||||
.configure(routes::root_config)
|
||||
.default_service(web::get().wrap(default_cors()).to(routes::not_found));
|
||||
}
|
||||
// TODO: fix me
|
||||
// pub fn app_config<M, F, Err: ErrorRenderer>(
|
||||
// mut app: App<M, F, Err>,
|
||||
// labrinth_config: LabrinthConfig,
|
||||
// ) -> App<M, F, Err> where F: ServiceFactory<WebRequest<Err>> {
|
||||
// app /*.app_data(web::FormConfig::default().error_handler(|err, _req| {
|
||||
// routes::ApiError::Validation(err.to_string()).into()
|
||||
// }))
|
||||
// .app_data(web::PathConfig::default().error_handler(|err, _req| {
|
||||
// routes::ApiError::Validation(err.to_string()).into()
|
||||
// }))
|
||||
// .app_data(web::QueryConfig::default().error_handler(|err, _req| {
|
||||
// routes::ApiError::Validation(err.to_string()).into()
|
||||
// }))
|
||||
// .app_data(web::JsonConfig::default().error_handler(|err, _req| {
|
||||
// routes::ApiError::Validation(err.to_string()).into()
|
||||
// }))*/
|
||||
//
|
||||
// }
|
||||
|
||||
// This is so that env vars not used immediately don't panic at runtime
|
||||
pub fn check_env_vars() -> bool {
|
||||
@@ -448,9 +429,6 @@ pub fn check_env_vars() -> bool {
|
||||
failed |= check_var::<String>("PAYPAL_WEBHOOK_ID");
|
||||
failed |= check_var::<String>("PAYPAL_CLIENT_ID");
|
||||
failed |= check_var::<String>("PAYPAL_CLIENT_SECRET");
|
||||
failed |= check_var::<String>("PAYPAL_NVP_USERNAME");
|
||||
failed |= check_var::<String>("PAYPAL_NVP_PASSWORD");
|
||||
failed |= check_var::<String>("PAYPAL_NVP_SIGNATURE");
|
||||
|
||||
failed |= check_var::<String>("HCAPTCHA_SECRET");
|
||||
|
||||
@@ -485,14 +463,9 @@ pub fn check_env_vars() -> bool {
|
||||
failed |= check_var::<String>("STRIPE_API_KEY");
|
||||
failed |= check_var::<String>("STRIPE_WEBHOOK_SECRET");
|
||||
|
||||
failed |= check_var::<String>("ADITUDE_API_KEY");
|
||||
failed |= check_var::<u64>("ADITUDE_API_KEY");
|
||||
|
||||
failed |= check_var::<String>("PYRO_API_KEY");
|
||||
|
||||
failed |= check_var::<String>("BREX_API_URL");
|
||||
failed |= check_var::<String>("BREX_API_KEY");
|
||||
|
||||
failed |= check_var::<String>("DELPHI_URL");
|
||||
|
||||
failed
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use actix_web::{App, HttpServer};
|
||||
use actix_web_prom::PrometheusMetricsBuilder;
|
||||
use ntex::web::{App, HttpServer};
|
||||
// use actix_web_prom::PrometheusMetricsBuilder;
|
||||
use env_logger::Env;
|
||||
use labrinth::database::redis::RedisPool;
|
||||
use labrinth::file_hosting::S3Host;
|
||||
@@ -18,7 +18,7 @@ pub struct Pepper {
|
||||
pub pepper: String,
|
||||
}
|
||||
|
||||
#[actix_rt::main]
|
||||
#[ntex::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
dotenvy::dotenv().ok();
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or("info"))
|
||||
@@ -90,11 +90,10 @@ async fn main() -> std::io::Result<()> {
|
||||
let maxmind_reader =
|
||||
Arc::new(queue::maxmind::MaxMindIndexer::new().await.unwrap());
|
||||
|
||||
let prometheus = PrometheusMetricsBuilder::new("labrinth")
|
||||
.endpoint("/metrics")
|
||||
.exclude("/_internal/launcher_socket")
|
||||
.build()
|
||||
.expect("Failed to create prometheus metrics middleware");
|
||||
// let prometheus = PrometheusMetricsBuilder::new("labrinth")
|
||||
// .endpoint("/metrics")
|
||||
// .build()
|
||||
// .expect("Failed to create prometheus metrics middleware");
|
||||
|
||||
let search_config = search::SearchConfig::new(None);
|
||||
|
||||
@@ -112,11 +111,29 @@ async fn main() -> std::io::Result<()> {
|
||||
// Init App
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.wrap(prometheus.clone())
|
||||
// .wrap(prometheus.clone())
|
||||
.wrap(RateLimit(Arc::clone(&labrinth_config.rate_limiter)))
|
||||
.wrap(actix_web::middleware::Compress::default())
|
||||
.wrap(sentry_actix::Sentry::new())
|
||||
.configure(|cfg| labrinth::app_config(cfg, labrinth_config.clone()))
|
||||
.wrap(ntex::web::middleware::Compress::default())
|
||||
// .wrap(sentry_actix::Sentry::new())
|
||||
.state(labrinth_config.redis_pool.clone())
|
||||
.state(labrinth_config.pool.clone())
|
||||
.state(labrinth_config.file_host.clone())
|
||||
.state(labrinth_config.search_config.clone())
|
||||
.state(labrinth_config.session_queue.clone())
|
||||
.state(labrinth_config.payouts_queue.clone())
|
||||
.state(labrinth_config.ip_salt.clone())
|
||||
.state(labrinth_config.analytics_queue.clone())
|
||||
.state(labrinth_config.clickhouse.clone())
|
||||
.state(labrinth_config.maxmind.clone())
|
||||
.state(labrinth_config.active_sockets.clone())
|
||||
.state(labrinth_config.automated_moderation_queue.clone())
|
||||
.state(labrinth_config.stripe_client.clone())
|
||||
.configure(labrinth::routes::v2::config)
|
||||
.configure(labrinth::routes::v3::config)
|
||||
.configure(labrinth::routes::internal::config)
|
||||
.configure(labrinth::routes::root_config)
|
||||
// // TODO: fix me
|
||||
.default_service(ntex::web::get()/*.wrap(default_cors())*/.to(labrinth::routes::not_found))
|
||||
})
|
||||
.bind(dotenvy::var("BIND_ADDR").unwrap())?
|
||||
.run()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use std::future::Future;
|
||||
use super::super::ids::OrganizationId;
|
||||
use super::super::teams::TeamId;
|
||||
use super::super::users::UserId;
|
||||
@@ -226,30 +226,33 @@ impl LegacyProject {
|
||||
}
|
||||
|
||||
// Because from needs a version_item, this is a helper function to get many from one db query.
|
||||
pub async fn from_many<'a, E>(
|
||||
#[allow(clippy::manual_async_fn)]
|
||||
pub fn from_many<'a, 'c, E>(
|
||||
data: Vec<Project>,
|
||||
exec: E,
|
||||
redis: &RedisPool,
|
||||
) -> Result<Vec<Self>, DatabaseError>
|
||||
redis: &'a RedisPool,
|
||||
) -> impl Future<Output = Result<Vec<Self>, DatabaseError>> + Send + 'a
|
||||
where
|
||||
E: sqlx::Acquire<'a, Database = sqlx::Postgres>,
|
||||
E: sqlx::Acquire<'c, Database = sqlx::Postgres> + Send + 'a,
|
||||
{
|
||||
let version_ids: Vec<_> = data
|
||||
.iter()
|
||||
.filter_map(|p| p.versions.first().map(|i| (*i).into()))
|
||||
.collect();
|
||||
let example_versions =
|
||||
version_item::Version::get_many(&version_ids, exec, redis).await?;
|
||||
let mut legacy_projects = Vec::new();
|
||||
for project in data {
|
||||
let version_item = example_versions
|
||||
async move {
|
||||
let version_ids: Vec<_> = data
|
||||
.iter()
|
||||
.find(|v| v.inner.project_id == project.id.into())
|
||||
.cloned();
|
||||
let project = LegacyProject::from(project, version_item);
|
||||
legacy_projects.push(project);
|
||||
.filter_map(|p| p.versions.first().map(|i| (*i).into()))
|
||||
.collect();
|
||||
let example_versions =
|
||||
version_item::Version::get_many(&version_ids, exec, redis).await?;
|
||||
let mut legacy_projects = Vec::new();
|
||||
for project in data {
|
||||
let version_item = example_versions
|
||||
.iter()
|
||||
.find(|v| v.inner.project_id == project.id.into())
|
||||
.cloned();
|
||||
let project = LegacyProject::from(project, version_item);
|
||||
legacy_projects.push(project);
|
||||
}
|
||||
Ok(legacy_projects)
|
||||
}
|
||||
Ok(legacy_projects)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ pub struct Charge {
|
||||
pub id: ChargeId,
|
||||
pub user_id: UserId,
|
||||
pub price_id: ProductPriceId,
|
||||
pub amount: i64,
|
||||
pub amount: u64,
|
||||
pub currency_code: String,
|
||||
pub status: ChargeStatus,
|
||||
pub due: DateTime<Utc>,
|
||||
@@ -171,9 +171,6 @@ pub struct Charge {
|
||||
pub subscription_id: Option<UserSubscriptionId>,
|
||||
pub subscription_interval: Option<PriceDuration>,
|
||||
pub platform: PaymentPlatform,
|
||||
|
||||
pub parent_charge_id: Option<ChargeId>,
|
||||
pub net: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
|
||||
@@ -18,7 +18,7 @@ use std::io::{Cursor, Read};
|
||||
use std::time::Duration;
|
||||
use zip::ZipArchive;
|
||||
|
||||
pub const AUTOMOD_ID: i64 = 0;
|
||||
const AUTOMOD_ID: i64 = 0;
|
||||
|
||||
pub struct ModerationMessages {
|
||||
pub messages: Vec<ModerationMessage>,
|
||||
|
||||
@@ -23,7 +23,7 @@ pub struct PayoutsQueue {
|
||||
payout_options: RwLock<Option<PayoutMethods>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone)]
|
||||
struct PayPalCredentials {
|
||||
access_token: String,
|
||||
token_type: String,
|
||||
@@ -36,12 +36,6 @@ struct PayoutMethods {
|
||||
expires: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct AccountBalance {
|
||||
pub available: Decimal,
|
||||
pub pending: Decimal,
|
||||
}
|
||||
|
||||
impl Default for PayoutsQueue {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
@@ -551,136 +545,6 @@ impl PayoutsQueue {
|
||||
|
||||
Ok(options.options)
|
||||
}
|
||||
|
||||
pub async fn get_brex_balance() -> Result<Option<AccountBalance>, ApiError>
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct BrexBalance {
|
||||
pub amount: i64,
|
||||
// pub currency: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct BrexAccount {
|
||||
pub current_balance: BrexBalance,
|
||||
pub available_balance: BrexBalance,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct BrexResponse {
|
||||
pub items: Vec<BrexAccount>,
|
||||
}
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let res = client
|
||||
.get(format!("{}accounts/cash", dotenvy::var("BREX_API_URL")?))
|
||||
.bearer_auth(&dotenvy::var("BREX_API_KEY")?)
|
||||
.send()
|
||||
.await?
|
||||
.json::<BrexResponse>()
|
||||
.await?;
|
||||
|
||||
Ok(Some(AccountBalance {
|
||||
available: Decimal::from(
|
||||
res.items
|
||||
.iter()
|
||||
.map(|x| x.available_balance.amount)
|
||||
.sum::<i64>(),
|
||||
) / Decimal::from(100),
|
||||
pending: Decimal::from(
|
||||
res.items
|
||||
.iter()
|
||||
.map(|x| {
|
||||
x.current_balance.amount - x.available_balance.amount
|
||||
})
|
||||
.sum::<i64>(),
|
||||
) / Decimal::from(100),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn get_paypal_balance() -> Result<Option<AccountBalance>, ApiError>
|
||||
{
|
||||
let api_username = dotenvy::var("PAYPAL_NVP_USERNAME")?;
|
||||
let api_password = dotenvy::var("PAYPAL_NVP_PASSWORD")?;
|
||||
let api_signature = dotenvy::var("PAYPAL_NVP_SIGNATURE")?;
|
||||
|
||||
let mut params = HashMap::new();
|
||||
params.insert("METHOD", "GetBalance");
|
||||
params.insert("VERSION", "204");
|
||||
params.insert("USER", &api_username);
|
||||
params.insert("PWD", &api_password);
|
||||
params.insert("SIGNATURE", &api_signature);
|
||||
params.insert("RETURNALLCURRENCIES", "1");
|
||||
|
||||
let endpoint = "https://api-3t.paypal.com/nvp";
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client.post(endpoint).form(¶ms).send().await?;
|
||||
|
||||
let text = response.text().await?;
|
||||
let body = urlencoding::decode(&text).unwrap_or_default();
|
||||
|
||||
let mut key_value_map = HashMap::new();
|
||||
|
||||
for pair in body.split('&') {
|
||||
let mut iter = pair.splitn(2, '=');
|
||||
if let (Some(key), Some(value)) = (iter.next(), iter.next()) {
|
||||
key_value_map.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(amount) = key_value_map
|
||||
.get("L_AMT0")
|
||||
.and_then(|x| Decimal::from_str_exact(x).ok())
|
||||
{
|
||||
Ok(Some(AccountBalance {
|
||||
available: amount,
|
||||
pending: Decimal::ZERO,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_tremendous_balance(
|
||||
&self,
|
||||
) -> Result<Option<AccountBalance>, ApiError> {
|
||||
#[derive(Deserialize)]
|
||||
struct FundingSourceMeta {
|
||||
available_cents: u64,
|
||||
pending_cents: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct FundingSource {
|
||||
method: String,
|
||||
meta: FundingSourceMeta,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct FundingSourceRequest {
|
||||
pub funding_sources: Vec<FundingSource>,
|
||||
}
|
||||
|
||||
let val = self
|
||||
.make_tremendous_request::<(), FundingSourceRequest>(
|
||||
Method::GET,
|
||||
"funding_sources",
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(val
|
||||
.funding_sources
|
||||
.into_iter()
|
||||
.find(|x| x.method == "balance")
|
||||
.map(|x| AccountBalance {
|
||||
available: Decimal::from(x.meta.available_cents)
|
||||
/ Decimal::from(100),
|
||||
pending: Decimal::from(x.meta.pending_cents)
|
||||
/ Decimal::from(100),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
//! "Database" for Hydra
|
||||
use crate::models::users::{UserId, UserStatus};
|
||||
use actix_ws::Session;
|
||||
use dashmap::DashMap;
|
||||
|
||||
pub struct ActiveSockets {
|
||||
pub auth_sockets: DashMap<UserId, (UserStatus, Session)>,
|
||||
pub auth_sockets: DashMap<UserId, (UserStatus)>,
|
||||
}
|
||||
|
||||
impl Default for ActiveSockets {
|
||||
|
||||
@@ -8,8 +8,8 @@ use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::date::get_current_tenths_of_ms;
|
||||
use crate::util::env::parse_strings_from_var;
|
||||
use actix_web::{post, web};
|
||||
use actix_web::{HttpRequest, HttpResponse};
|
||||
use ntex::web;
|
||||
use ntex::web::{post, HttpRequest, HttpResponse};
|
||||
use serde::Deserialize;
|
||||
use sqlx::PgPool;
|
||||
use std::collections::HashMap;
|
||||
@@ -48,18 +48,18 @@ pub struct UrlInput {
|
||||
#[post("view")]
|
||||
pub async fn page_view_ingest(
|
||||
req: HttpRequest,
|
||||
maxmind: web::Data<Arc<MaxMindIndexer>>,
|
||||
analytics_queue: web::Data<Arc<AnalyticsQueue>>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
url_input: web::Json<UrlInput>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
maxmind: web::types::State<Arc<MaxMindIndexer>>,
|
||||
analytics_queue: web::types::State<Arc<AnalyticsQueue>>,
|
||||
session_queue: web::types::State<AuthQueue>,
|
||||
url_input: web::types::Json<UrlInput>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user =
|
||||
get_user_from_headers(&req, &**pool, &redis, &session_queue, None)
|
||||
get_user_from_headers(&req, &*pool, &redis, &session_queue, None)
|
||||
.await
|
||||
.ok();
|
||||
let conn_info = req.connection_info().peer_addr().map(|x| x.to_string());
|
||||
let conn_info = req.peer_addr().map(|x| x.to_string());
|
||||
|
||||
let url = Url::parse(&url_input.url).map_err(|_| {
|
||||
ApiError::InvalidInput("invalid page view URL specified!".to_string())
|
||||
@@ -132,7 +132,7 @@ pub async fn page_view_ingest(
|
||||
if PROJECT_TYPES.contains(&segments_vec[0]) {
|
||||
let project = crate::database::models::Project::get(
|
||||
segments_vec[1],
|
||||
&**pool,
|
||||
&*pool,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
@@ -164,17 +164,17 @@ pub struct PlaytimeInput {
|
||||
#[post("playtime")]
|
||||
pub async fn playtime_ingest(
|
||||
req: HttpRequest,
|
||||
analytics_queue: web::Data<Arc<AnalyticsQueue>>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
playtime_input: web::Json<
|
||||
analytics_queue: web::types::State<Arc<AnalyticsQueue>>,
|
||||
session_queue: web::types::State<AuthQueue>,
|
||||
playtime_input: web::types::Json<
|
||||
HashMap<crate::models::ids::VersionId, PlaytimeInput>,
|
||||
>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let (_, user) = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&*pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PERFORM_ANALYTICS]),
|
||||
@@ -191,7 +191,7 @@ pub async fn playtime_ingest(
|
||||
|
||||
let versions = crate::database::models::Version::get_many(
|
||||
&playtimes.iter().map(|x| (*x.0).into()).collect::<Vec<_>>(),
|
||||
&**pool,
|
||||
&*pool,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use actix_web::{get, HttpResponse};
|
||||
use ntex::web::{get, HttpResponse};
|
||||
use serde_json::json;
|
||||
|
||||
#[get("/")]
|
||||
@@ -10,5 +10,5 @@ pub async fn index_get() -> HttpResponse {
|
||||
"about": "Welcome traveler!"
|
||||
});
|
||||
|
||||
HttpResponse::Ok().json(data)
|
||||
HttpResponse::Ok().json(&data)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
use crate::auth::validate::get_user_record_from_bearer_token;
|
||||
use crate::database::models::thread_item::ThreadMessageBuilder;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::analytics::Download;
|
||||
use crate::models::ids::ProjectId;
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::models::threads::MessageBody;
|
||||
use crate::queue::analytics::AnalyticsQueue;
|
||||
use crate::queue::maxmind::MaxMindIndexer;
|
||||
use crate::queue::moderation::AUTOMOD_ID;
|
||||
use crate::queue::payouts::PayoutsQueue;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::search::SearchConfig;
|
||||
use crate::util::date::get_current_tenths_of_ms;
|
||||
use crate::util::guards::admin_key_guard;
|
||||
use actix_web::{get, patch, post, web, HttpRequest, HttpResponse};
|
||||
use log::info;
|
||||
// use crate::util::guards::admin_key_guard;
|
||||
use ntex::web::{self, patch, post, HttpRequest, HttpResponse};
|
||||
use serde::Deserialize;
|
||||
use sqlx::PgPool;
|
||||
use std::collections::HashMap;
|
||||
@@ -26,9 +21,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::scope("admin")
|
||||
.service(count_download)
|
||||
.service(force_reindex)
|
||||
.service(get_balances)
|
||||
.service(delphi_result_ingest),
|
||||
.service(force_reindex),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,16 +36,18 @@ pub struct DownloadBody {
|
||||
}
|
||||
|
||||
// This is an internal route, cannot be used without key
|
||||
#[patch("/_count-download", guard = "admin_key_guard")]
|
||||
#[patch("/_count-download")]
|
||||
// TODO: fix me
|
||||
// #[patch("/_count-download", guard = "admin_key_guard")]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn count_download(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
maxmind: web::Data<Arc<MaxMindIndexer>>,
|
||||
analytics_queue: web::Data<Arc<AnalyticsQueue>>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
download_body: web::Json<DownloadBody>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
maxmind: web::types::State<Arc<MaxMindIndexer>>,
|
||||
analytics_queue: web::types::State<Arc<AnalyticsQueue>>,
|
||||
session_queue: web::types::State<AuthQueue>,
|
||||
download_body: web::types::Json<DownloadBody>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let token = download_body
|
||||
.headers
|
||||
@@ -63,7 +58,7 @@ pub async fn count_download(
|
||||
let user = get_user_record_from_bearer_token(
|
||||
&req,
|
||||
token,
|
||||
&**pool,
|
||||
&*pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
)
|
||||
@@ -88,7 +83,7 @@ pub async fn count_download(
|
||||
",
|
||||
download_body.url,
|
||||
)
|
||||
.fetch_optional(pool.as_ref())
|
||||
.fetch_optional(&*pool)
|
||||
.await?
|
||||
{
|
||||
(version.id, version.mod_id)
|
||||
@@ -101,7 +96,7 @@ pub async fn count_download(
|
||||
project_id as crate::database::models::ids::ProjectId,
|
||||
id_option
|
||||
)
|
||||
.fetch_optional(pool.as_ref())
|
||||
.fetch_optional(&*pool)
|
||||
.await?
|
||||
{
|
||||
(version.id, version.mod_id)
|
||||
@@ -154,116 +149,16 @@ pub async fn count_download(
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
#[post("/_force_reindex", guard = "admin_key_guard")]
|
||||
#[post("/_force_reindex")]
|
||||
// TODO: fix me
|
||||
// #[post("/_force_reindex", guard = "admin_key_guard")]
|
||||
pub async fn force_reindex(
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
config: web::Data<SearchConfig>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
config: web::types::State<SearchConfig>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
use crate::search::indexing::index_projects;
|
||||
let redis = redis.get_ref();
|
||||
index_projects(pool.as_ref().clone(), redis.clone(), &config).await?;
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
}
|
||||
|
||||
#[get("/_balances", guard = "admin_key_guard")]
|
||||
pub async fn get_balances(
|
||||
payouts: web::Data<PayoutsQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let (paypal, brex, tremendous) = futures::future::try_join3(
|
||||
PayoutsQueue::get_paypal_balance(),
|
||||
PayoutsQueue::get_brex_balance(),
|
||||
payouts.get_tremendous_balance(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||
"paypal": paypal,
|
||||
"brex": brex,
|
||||
"tremendous": tremendous,
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct DelphiIngest {
|
||||
pub url: String,
|
||||
pub project_id: crate::models::ids::ProjectId,
|
||||
pub version_id: crate::models::ids::VersionId,
|
||||
pub issues: HashMap<String, HashMap<String, String>>,
|
||||
}
|
||||
|
||||
#[post("/_delphi", guard = "admin_key_guard")]
|
||||
pub async fn delphi_result_ingest(
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
body: web::Json<DelphiIngest>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
if body.issues.is_empty() {
|
||||
info!("No issues found for file {}", body.url);
|
||||
return Ok(HttpResponse::NoContent().finish());
|
||||
}
|
||||
|
||||
let webhook_url = dotenvy::var("DELPHI_SLACK_WEBHOOK")?;
|
||||
|
||||
let project = crate::database::models::Project::get_id(
|
||||
body.project_id.into(),
|
||||
&**pool,
|
||||
&redis,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(format!(
|
||||
"Project {} does not exist",
|
||||
body.project_id
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut header = format!("Suspicious traces found at {}", body.url);
|
||||
|
||||
for (issue, trace) in &body.issues {
|
||||
for (path, code) in trace {
|
||||
header.push_str(&format!(
|
||||
"\n issue {issue} found at file {}: \n ```\n{}\n```",
|
||||
path, code
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
crate::util::webhook::send_slack_webhook(
|
||||
body.project_id,
|
||||
&pool,
|
||||
&redis,
|
||||
webhook_url,
|
||||
Some(header),
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let mut thread_header = format!("Suspicious traces found at [version {}](https://modrinth.com/project/{}/version/{})", body.version_id, body.project_id, body.version_id);
|
||||
|
||||
for (issue, trace) in &body.issues {
|
||||
for path in trace.keys() {
|
||||
thread_header
|
||||
.push_str(&format!("\n issue {issue} found at file {}", path));
|
||||
}
|
||||
}
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
ThreadMessageBuilder {
|
||||
author_id: Some(crate::database::models::UserId(AUTOMOD_ID)),
|
||||
body: MessageBody::Text {
|
||||
body: thread_header,
|
||||
private: true,
|
||||
replying_to: None,
|
||||
associated_images: vec![],
|
||||
},
|
||||
thread_id: project.thread_id,
|
||||
hide_identity: false,
|
||||
}
|
||||
.insert(&mut transaction)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
index_projects(&*pool, &redis, &config).await?;
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@ use crate::models::pats::Scopes;
|
||||
use crate::models::users::Badges;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
|
||||
use chrono::Utc;
|
||||
use log::{info, warn};
|
||||
use ntex::web::{self, delete, get, patch, post, HttpRequest, HttpResponse};
|
||||
use rust_decimal::prelude::ToPrimitive;
|
||||
use rust_decimal::Decimal;
|
||||
use serde::Serialize;
|
||||
@@ -56,10 +56,10 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
|
||||
#[get("products")]
|
||||
pub async fn products(
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let products = product_item::QueryProduct::list(&**pool, &redis).await?;
|
||||
let products = product_item::QueryProduct::list(&*pool, &redis).await?;
|
||||
|
||||
let products = products
|
||||
.into_iter()
|
||||
@@ -80,25 +80,19 @@ pub async fn products(
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(HttpResponse::Ok().json(products))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SubscriptionsQuery {
|
||||
pub user_id: Option<crate::models::ids::UserId>,
|
||||
Ok(HttpResponse::Ok().json(&products))
|
||||
}
|
||||
|
||||
#[get("subscriptions")]
|
||||
pub async fn subscriptions(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
query: web::Query<SubscriptionsQuery>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
session_queue: web::types::State<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&*pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
@@ -108,26 +102,15 @@ pub async fn subscriptions(
|
||||
|
||||
let subscriptions =
|
||||
user_subscription_item::UserSubscriptionItem::get_all_user(
|
||||
if let Some(user_id) = query.user_id {
|
||||
if user.role.is_admin() {
|
||||
user_id.into()
|
||||
} else {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"You cannot see the subscriptions of other users!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
user.id.into()
|
||||
},
|
||||
&**pool,
|
||||
user.id.into(),
|
||||
&*pool,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(UserSubscription::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(HttpResponse::Ok().json(subscriptions))
|
||||
Ok(HttpResponse::Ok().json(&subscriptions))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -147,16 +130,16 @@ pub struct ChargeRefund {
|
||||
#[post("charge/{id}/refund")]
|
||||
pub async fn refund_charge(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
info: web::Path<(crate::models::ids::ChargeId,)>,
|
||||
body: web::Json<ChargeRefund>,
|
||||
stripe_client: web::Data<stripe::Client>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
session_queue: web::types::State<AuthQueue>,
|
||||
info: web::types::Path<(crate::models::ids::ChargeId,)>,
|
||||
body: web::types::Json<ChargeRefund>,
|
||||
stripe_client: web::types::State<stripe::Client>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&*pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
@@ -172,8 +155,8 @@ pub async fn refund_charge(
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(charge) = ChargeItem::get(id.into(), &**pool).await? {
|
||||
let refunds = ChargeItem::get_children(id.into(), &**pool).await?;
|
||||
if let Some(charge) = ChargeItem::get(id.into(), &*pool).await? {
|
||||
let refunds = ChargeItem::get_children(id.into(), &*pool).await?;
|
||||
let refunds = -refunds
|
||||
.into_iter()
|
||||
.filter_map(|x| match x.status {
|
||||
@@ -276,7 +259,7 @@ pub async fn refund_charge(
|
||||
if body.0.unprovision.unwrap_or(false) {
|
||||
if let Some(subscription_id) = charge.subscription_id {
|
||||
let open_charge =
|
||||
ChargeItem::get_open_subscription(subscription_id, &**pool)
|
||||
ChargeItem::get_open_subscription(subscription_id, &*pool)
|
||||
.await?;
|
||||
if let Some(mut open_charge) = open_charge {
|
||||
open_charge.status = ChargeStatus::Cancelled;
|
||||
@@ -304,16 +287,16 @@ pub struct SubscriptionEdit {
|
||||
#[patch("subscription/{id}")]
|
||||
pub async fn edit_subscription(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(crate::models::ids::UserSubscriptionId,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
edit_subscription: web::Json<SubscriptionEdit>,
|
||||
stripe_client: web::Data<stripe::Client>,
|
||||
info: web::types::Path<(crate::models::ids::UserSubscriptionId,)>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
session_queue: web::types::State<AuthQueue>,
|
||||
edit_subscription: web::types::Json<SubscriptionEdit>,
|
||||
stripe_client: web::types::State<stripe::Client>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&*pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
@@ -324,7 +307,7 @@ pub async fn edit_subscription(
|
||||
let (id,) = info.into_inner();
|
||||
|
||||
if let Some(subscription) =
|
||||
user_subscription_item::UserSubscriptionItem::get(id.into(), &**pool)
|
||||
user_subscription_item::UserSubscriptionItem::get(id.into(), &*pool)
|
||||
.await?
|
||||
{
|
||||
if subscription.user_id != user.id.into() && !user.role.is_admin() {
|
||||
@@ -543,7 +526,7 @@ pub async fn edit_subscription(
|
||||
transaction.commit().await?;
|
||||
|
||||
if let Some((amount, tax, payment_intent)) = intent {
|
||||
Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||
Ok(HttpResponse::Ok().json(&serde_json::json!({
|
||||
"payment_intent_id": payment_intent.id,
|
||||
"client_secret": payment_intent.client_secret,
|
||||
"tax": tax,
|
||||
@@ -560,14 +543,14 @@ pub async fn edit_subscription(
|
||||
#[get("customer")]
|
||||
pub async fn user_customer(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
stripe_client: web::Data<stripe::Client>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
session_queue: web::types::State<AuthQueue>,
|
||||
stripe_client: web::types::State<stripe::Client>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&*pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
@@ -587,25 +570,19 @@ pub async fn user_customer(
|
||||
let customer =
|
||||
stripe::Customer::retrieve(&stripe_client, &customer_id, &[]).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(customer))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ChargesQuery {
|
||||
pub user_id: Option<crate::models::ids::UserId>,
|
||||
Ok(HttpResponse::Ok().json(&customer))
|
||||
}
|
||||
|
||||
#[get("payments")]
|
||||
pub async fn charges(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
query: web::Query<ChargesQuery>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
session_queue: web::types::State<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&*pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
@@ -615,30 +592,19 @@ pub async fn charges(
|
||||
|
||||
let charges =
|
||||
crate::database::models::charge_item::ChargeItem::get_from_user(
|
||||
if let Some(user_id) = query.user_id {
|
||||
if user.role.is_admin() {
|
||||
user_id.into()
|
||||
} else {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"You cannot see the subscriptions of other users!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
user.id.into()
|
||||
},
|
||||
&**pool,
|
||||
user.id.into(),
|
||||
&*pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(
|
||||
charges
|
||||
&charges
|
||||
.into_iter()
|
||||
.map(|x| Charge {
|
||||
id: x.id.into(),
|
||||
user_id: x.user_id.into(),
|
||||
price_id: x.price_id.into(),
|
||||
amount: x.amount,
|
||||
amount: x.amount as u64,
|
||||
currency_code: x.currency_code,
|
||||
status: x.status,
|
||||
due: x.due,
|
||||
@@ -647,8 +613,6 @@ pub async fn charges(
|
||||
subscription_id: x.subscription_id.map(|x| x.into()),
|
||||
subscription_interval: x.subscription_interval,
|
||||
platform: x.payment_platform,
|
||||
parent_charge_id: x.parent_charge_id.map(|x| x.into()),
|
||||
net: if user.role.is_admin() { x.net } else { None },
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
@@ -657,14 +621,14 @@ pub async fn charges(
|
||||
#[post("payment_method")]
|
||||
pub async fn add_payment_method_flow(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
stripe_client: web::Data<stripe::Client>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
session_queue: web::types::State<AuthQueue>,
|
||||
stripe_client: web::types::State<stripe::Client>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&*pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
@@ -697,7 +661,7 @@ pub async fn add_payment_method_flow(
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||
Ok(HttpResponse::Ok().json(&serde_json::json!({
|
||||
"client_secret": intent.client_secret
|
||||
})))
|
||||
}
|
||||
@@ -710,15 +674,15 @@ pub struct EditPaymentMethod {
|
||||
#[patch("payment_method/{id}")]
|
||||
pub async fn edit_payment_method(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
stripe_client: web::Data<stripe::Client>,
|
||||
info: web::types::Path<(String,)>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
session_queue: web::types::State<AuthQueue>,
|
||||
stripe_client: web::types::State<stripe::Client>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&*pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
@@ -779,15 +743,15 @@ pub async fn edit_payment_method(
|
||||
#[delete("payment_method/{id}")]
|
||||
pub async fn remove_payment_method(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
stripe_client: web::Data<stripe::Client>,
|
||||
info: web::types::Path<(String,)>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
session_queue: web::types::State<AuthQueue>,
|
||||
stripe_client: web::types::State<stripe::Client>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&*pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
@@ -823,7 +787,7 @@ pub async fn remove_payment_method(
|
||||
let user_subscriptions =
|
||||
user_subscription_item::UserSubscriptionItem::get_all_user(
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
&*pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -867,14 +831,14 @@ pub async fn remove_payment_method(
|
||||
#[get("payment_methods")]
|
||||
pub async fn payment_methods(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
stripe_client: web::Data<stripe::Client>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
session_queue: web::types::State<AuthQueue>,
|
||||
stripe_client: web::types::State<stripe::Client>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&*pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
@@ -897,7 +861,7 @@ pub async fn payment_methods(
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(methods.data))
|
||||
Ok(HttpResponse::Ok().json(&methods.data))
|
||||
} else {
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
}
|
||||
@@ -911,16 +875,16 @@ pub struct ActiveServersQuery {
|
||||
#[get("active_servers")]
|
||||
pub async fn active_servers(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
query: web::Query<ActiveServersQuery>,
|
||||
pool: web::types::State<PgPool>,
|
||||
query: web::types::Query<ActiveServersQuery>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let master_key = dotenvy::var("PYRO_API_KEY")?;
|
||||
|
||||
if req
|
||||
if !req
|
||||
.head()
|
||||
.headers()
|
||||
.get("X-Master-Key")
|
||||
.is_none_or(|it| it.as_bytes() != master_key.as_bytes())
|
||||
.map_or(false, |it| it.as_bytes() == master_key.as_bytes())
|
||||
{
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"Invalid master key".to_string(),
|
||||
@@ -930,7 +894,7 @@ pub async fn active_servers(
|
||||
let servers =
|
||||
user_subscription_item::UserSubscriptionItem::get_all_servers(
|
||||
query.subscription_status,
|
||||
&**pool,
|
||||
&*pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -954,7 +918,7 @@ pub async fn active_servers(
|
||||
})
|
||||
.collect::<Vec<ActiveServer>>();
|
||||
|
||||
Ok(HttpResponse::Ok().json(server_ids))
|
||||
Ok(HttpResponse::Ok().json(&server_ids))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -1057,15 +1021,15 @@ fn infer_currency_code(country: &str) -> String {
|
||||
#[post("payment")]
|
||||
pub async fn initiate_payment(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
stripe_client: web::Data<stripe::Client>,
|
||||
payment_request: web::Json<PaymentRequest>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
session_queue: web::types::State<AuthQueue>,
|
||||
stripe_client: web::types::State<stripe::Client>,
|
||||
payment_request: web::types::Json<PaymentRequest>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&*pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
@@ -1145,7 +1109,7 @@ pub async fn initiate_payment(
|
||||
let charge =
|
||||
crate::database::models::charge_item::ChargeItem::get(
|
||||
id.into(),
|
||||
&**pool,
|
||||
&*pool,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
@@ -1167,7 +1131,7 @@ pub async fn initiate_payment(
|
||||
interval,
|
||||
} => {
|
||||
let product =
|
||||
product_item::ProductItem::get(product_id.into(), &**pool)
|
||||
product_item::ProductItem::get(product_id.into(), &*pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
@@ -1178,7 +1142,7 @@ pub async fn initiate_payment(
|
||||
|
||||
let mut product_prices =
|
||||
product_item::ProductPriceItem::get_all_product_prices(
|
||||
product.id, &**pool,
|
||||
product.id, &*pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1220,7 +1184,7 @@ pub async fn initiate_payment(
|
||||
let user_subscriptions =
|
||||
user_subscription_item::UserSubscriptionItem::get_all_user(
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
&*pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1234,7 +1198,7 @@ pub async fn initiate_payment(
|
||||
})
|
||||
.map(|x| x.price_id)
|
||||
.collect::<Vec<_>>(),
|
||||
&**pool,
|
||||
&*pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1295,7 +1259,7 @@ pub async fn initiate_payment(
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||
Ok(HttpResponse::Ok().json(&serde_json::json!({
|
||||
"price_id": to_base62(price_id.0 as u64),
|
||||
"tax": 0,
|
||||
"total": price,
|
||||
@@ -1361,7 +1325,7 @@ pub async fn initiate_payment(
|
||||
let payment_intent =
|
||||
stripe::PaymentIntent::create(&stripe_client, intent).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||
Ok(HttpResponse::Ok().json(&serde_json::json!({
|
||||
"payment_intent_id": payment_intent.id,
|
||||
"client_secret": payment_intent.client_secret,
|
||||
"price_id": to_base62(price_id.0 as u64),
|
||||
@@ -1376,9 +1340,9 @@ pub async fn initiate_payment(
|
||||
pub async fn stripe_webhook(
|
||||
req: HttpRequest,
|
||||
payload: String,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
stripe_client: web::Data<stripe::Client>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
stripe_client: web::types::State<stripe::Client>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let stripe_signature = req
|
||||
.headers()
|
||||
@@ -1752,7 +1716,7 @@ pub async fn stripe_webhook(
|
||||
let minecraft_versions = crate::database::models::legacy_loader_fields::MinecraftGameVersion::list(
|
||||
Some("release"),
|
||||
None,
|
||||
&**pool,
|
||||
&*pool,
|
||||
&redis,
|
||||
).await?;
|
||||
|
||||
|
||||
@@ -16,12 +16,12 @@ use crate::util::env::parse_strings_from_var;
|
||||
use crate::util::ext::get_image_ext;
|
||||
use crate::util::img::upload_image_optimized;
|
||||
use crate::util::validate::{validation_errors_to_string, RE_URL_SAFE};
|
||||
use actix_web::web::{scope, Data, Query, ServiceConfig};
|
||||
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
|
||||
use argon2::password_hash::SaltString;
|
||||
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||
use base64::Engine;
|
||||
use chrono::{Duration, Utc};
|
||||
use ntex::web::{self, delete, get, patch, post, HttpRequest, HttpResponse};
|
||||
use ntex::web::{scope, ServiceConfig};
|
||||
use rand_chacha::rand_core::SeedableRng;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use reqwest::header::AUTHORIZATION;
|
||||
@@ -1052,10 +1052,10 @@ pub struct Authorization {
|
||||
#[get("init")]
|
||||
pub async fn init(
|
||||
req: HttpRequest,
|
||||
Query(info): Query<AuthorizationInit>, // callback url
|
||||
client: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
web::types::Query(info): web::types::Query<AuthorizationInit>, // callback url
|
||||
client: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
session_queue: web::types::State<AuthQueue>,
|
||||
) -> Result<HttpResponse, AuthenticationError> {
|
||||
let url =
|
||||
url::Url::parse(&info.url).map_err(|_| AuthenticationError::Url)?;
|
||||
@@ -1073,7 +1073,7 @@ pub async fn init(
|
||||
let (_, user) = get_user_record_from_bearer_token(
|
||||
&req,
|
||||
Some(&token),
|
||||
&**client,
|
||||
&*client,
|
||||
&redis,
|
||||
&session_queue,
|
||||
)
|
||||
@@ -1095,17 +1095,17 @@ pub async fn init(
|
||||
|
||||
let url = info.provider.get_redirect_url(state)?;
|
||||
Ok(HttpResponse::TemporaryRedirect()
|
||||
.append_header(("Location", &*url))
|
||||
.json(serde_json::json!({ "url": url })))
|
||||
.header("Location", &*url)
|
||||
.json(&serde_json::json!({ "url": url })))
|
||||
}
|
||||
|
||||
#[get("callback")]
|
||||
pub async fn auth_callback(
|
||||
req: HttpRequest,
|
||||
Query(query): Query<HashMap<String, String>>,
|
||||
client: Data<PgPool>,
|
||||
file_host: Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
redis: Data<RedisPool>,
|
||||
web::types::Query(query): web::types::Query<HashMap<String, String>>,
|
||||
client: web::types::State<PgPool>,
|
||||
file_host: web::types::State<Arc<dyn FileHost + Send + Sync>>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
) -> Result<HttpResponse, crate::auth::templates::ErrorPage> {
|
||||
let state_string = query
|
||||
.get("state")
|
||||
@@ -1128,7 +1128,7 @@ pub async fn auth_callback(
|
||||
let token = provider.get_token(query).await?;
|
||||
let oauth_user = provider.get_user(&token).await?;
|
||||
|
||||
let user_id_opt = provider.get_user_id(&oauth_user.id, &**client).await?;
|
||||
let user_id_opt = provider.get_user_id(&oauth_user.id, &*client).await?;
|
||||
|
||||
let mut transaction = client.begin().await?;
|
||||
if let Some(id) = user_id {
|
||||
@@ -1140,7 +1140,7 @@ pub async fn auth_callback(
|
||||
.update_user_id(id, Some(&oauth_user.id), &mut transaction)
|
||||
.await?;
|
||||
|
||||
let user = crate::database::models::User::get_id(id, &**client, &redis).await?;
|
||||
let user = crate::database::models::User::get_id(id, &*client, &redis).await?;
|
||||
|
||||
if provider == AuthProvider::PayPal {
|
||||
sqlx::query!(
|
||||
@@ -1170,11 +1170,11 @@ pub async fn auth_callback(
|
||||
crate::database::models::User::clear_caches(&[(id, None)], &redis).await?;
|
||||
|
||||
Ok(HttpResponse::TemporaryRedirect()
|
||||
.append_header(("Location", &*url))
|
||||
.json(serde_json::json!({ "url": url })))
|
||||
.header("Location", &*url)
|
||||
.json(&serde_json::json!({ "url": url })))
|
||||
} else {
|
||||
let user_id = if let Some(user_id) = user_id_opt {
|
||||
let user = crate::database::models::User::get_id(user_id, &**client, &redis)
|
||||
let user = crate::database::models::User::get_id(user_id, &*client, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
|
||||
@@ -1191,8 +1191,8 @@ pub async fn auth_callback(
|
||||
);
|
||||
|
||||
return Ok(HttpResponse::TemporaryRedirect()
|
||||
.append_header(("Location", &*redirect_url))
|
||||
.json(serde_json::json!({ "url": redirect_url })));
|
||||
.header("Location", &*redirect_url)
|
||||
.json(&serde_json::json!({ "url": redirect_url })));
|
||||
}
|
||||
|
||||
user_id
|
||||
@@ -1216,8 +1216,8 @@ pub async fn auth_callback(
|
||||
);
|
||||
|
||||
Ok(HttpResponse::TemporaryRedirect()
|
||||
.append_header(("Location", &*redirect_url))
|
||||
.json(serde_json::json!({ "url": redirect_url })))
|
||||
.header("Location", &*redirect_url)
|
||||
.json(&serde_json::json!({ "url": redirect_url })))
|
||||
}
|
||||
} else {
|
||||
Err::<HttpResponse, AuthenticationError>(AuthenticationError::InvalidCredentials)
|
||||
@@ -1235,14 +1235,14 @@ pub struct DeleteAuthProvider {
|
||||
#[delete("provider")]
|
||||
pub async fn delete_auth_provider(
|
||||
req: HttpRequest,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
delete_provider: web::Json<DeleteAuthProvider>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
delete_provider: web::types::Json<DeleteAuthProvider>,
|
||||
session_queue: web::types::State<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&*pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_AUTH_WRITE]),
|
||||
@@ -1328,9 +1328,9 @@ pub struct NewAccount {
|
||||
#[post("create")]
|
||||
pub async fn create_account_with_password(
|
||||
req: HttpRequest,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
new_account: web::Json<NewAccount>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
new_account: web::types::Json<NewAccount>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
new_account.0.validate().map_err(|err| {
|
||||
ApiError::InvalidInput(validation_errors_to_string(err, None))
|
||||
@@ -1340,13 +1340,9 @@ pub async fn create_account_with_password(
|
||||
return Err(ApiError::Turnstile);
|
||||
}
|
||||
|
||||
if crate::database::models::User::get(
|
||||
&new_account.username,
|
||||
&**pool,
|
||||
&redis,
|
||||
)
|
||||
.await?
|
||||
.is_some()
|
||||
if crate::database::models::User::get(&new_account.username, &*pool, &redis)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
return Err(ApiError::InvalidInput("Username is taken!".to_string()));
|
||||
}
|
||||
@@ -1381,7 +1377,7 @@ pub async fn create_account_with_password(
|
||||
.hash_password(new_account.password.as_bytes(), &salt)?
|
||||
.to_string();
|
||||
|
||||
if crate::database::models::User::get_email(&new_account.email, &**pool)
|
||||
if crate::database::models::User::get_email(&new_account.email, &*pool)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
@@ -1441,7 +1437,7 @@ pub async fn create_account_with_password(
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(res))
|
||||
Ok(HttpResponse::Ok().json(&res))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Validate)]
|
||||
@@ -1454,26 +1450,26 @@ pub struct Login {
|
||||
#[post("login")]
|
||||
pub async fn login_password(
|
||||
req: HttpRequest,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
login: web::Json<Login>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
login: web::types::Json<Login>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
if !check_hcaptcha(&req, &login.challenge).await? {
|
||||
return Err(ApiError::Turnstile);
|
||||
}
|
||||
|
||||
let user = if let Some(user) =
|
||||
crate::database::models::User::get(&login.username, &**pool, &redis)
|
||||
crate::database::models::User::get(&login.username, &*pool, &redis)
|
||||
.await?
|
||||
{
|
||||
user
|
||||
} else {
|
||||
let user =
|
||||
crate::database::models::User::get_email(&login.username, &**pool)
|
||||
crate::database::models::User::get_email(&login.username, &*pool)
|
||||
.await?
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
|
||||
crate::database::models::User::get_id(user, &**pool, &redis)
|
||||
crate::database::models::User::get_id(user, &*pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?
|
||||
};
|
||||
@@ -1495,7 +1491,7 @@ pub async fn login_password(
|
||||
.insert(Duration::minutes(30), &redis)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||
Ok(HttpResponse::Ok().json(&serde_json::json!({
|
||||
"error": "2fa_required",
|
||||
"description": "2FA is required to complete this operation.",
|
||||
"flow": flow,
|
||||
@@ -1507,7 +1503,7 @@ pub async fn login_password(
|
||||
let res = crate::models::sessions::Session::from(session, true, None);
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(res))
|
||||
Ok(HttpResponse::Ok().json(&res))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1599,9 +1595,9 @@ async fn validate_2fa_code(
|
||||
#[post("login/2fa")]
|
||||
pub async fn login_2fa(
|
||||
req: HttpRequest,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
login: web::Json<Login2FA>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
login: web::types::Json<Login2FA>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let flow = Flow::get(&login.flow, &redis)
|
||||
.await?
|
||||
@@ -1609,7 +1605,7 @@ pub async fn login_2fa(
|
||||
|
||||
if let Flow::Login2FA { user_id } = flow {
|
||||
let user =
|
||||
crate::database::models::User::get_id(user_id, &**pool, &redis)
|
||||
crate::database::models::User::get_id(user_id, &*pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
|
||||
@@ -1637,7 +1633,7 @@ pub async fn login_2fa(
|
||||
let res = crate::models::sessions::Session::from(session, true, None);
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(res))
|
||||
Ok(HttpResponse::Ok().json(&res))
|
||||
} else {
|
||||
Err(ApiError::Authentication(
|
||||
AuthenticationError::InvalidCredentials,
|
||||
@@ -1648,13 +1644,13 @@ pub async fn login_2fa(
|
||||
#[post("2fa/get_secret")]
|
||||
pub async fn begin_2fa_flow(
|
||||
req: HttpRequest,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
session_queue: web::types::State<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&*pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_AUTH_WRITE]),
|
||||
@@ -1673,7 +1669,7 @@ pub async fn begin_2fa_flow(
|
||||
.insert(Duration::minutes(30), &redis)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||
Ok(HttpResponse::Ok().json(&serde_json::json!({
|
||||
"secret": encoded.to_string(),
|
||||
"flow": flow,
|
||||
})))
|
||||
@@ -1687,10 +1683,10 @@ pub async fn begin_2fa_flow(
|
||||
#[post("2fa")]
|
||||
pub async fn finish_2fa_flow(
|
||||
req: HttpRequest,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
login: web::Json<Login2FA>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
login: web::types::Json<Login2FA>,
|
||||
session_queue: web::types::State<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let flow = Flow::get(&login.flow, &redis)
|
||||
.await?
|
||||
@@ -1699,7 +1695,7 @@ pub async fn finish_2fa_flow(
|
||||
if let Flow::Initialize2FA { user_id, secret } = flow {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&*pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_AUTH_WRITE]),
|
||||
@@ -1796,7 +1792,7 @@ pub async fn finish_2fa_flow(
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||
Ok(HttpResponse::Ok().json(&serde_json::json!({
|
||||
"backup_codes": codes,
|
||||
})))
|
||||
} else {
|
||||
@@ -1814,15 +1810,15 @@ pub struct Remove2FA {
|
||||
#[delete("2fa")]
|
||||
pub async fn remove_2fa(
|
||||
req: HttpRequest,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
login: web::Json<Remove2FA>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
login: web::types::Json<Remove2FA>,
|
||||
session_queue: web::types::State<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let (scopes, user) = get_user_record_from_bearer_token(
|
||||
&req,
|
||||
None,
|
||||
&**pool,
|
||||
&*pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
)
|
||||
@@ -1904,9 +1900,9 @@ pub struct ResetPassword {
|
||||
#[post("password/reset")]
|
||||
pub async fn reset_password_begin(
|
||||
req: HttpRequest,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
reset_password: web::Json<ResetPassword>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
reset_password: web::types::Json<ResetPassword>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
if !check_hcaptcha(&req, &reset_password.challenge).await? {
|
||||
return Err(ApiError::Turnstile);
|
||||
@@ -1914,15 +1910,15 @@ pub async fn reset_password_begin(
|
||||
|
||||
let user = if let Some(user_id) = crate::database::models::User::get_email(
|
||||
&reset_password.username,
|
||||
&**pool,
|
||||
&*pool,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
crate::database::models::User::get_id(user_id, &**pool, &redis).await?
|
||||
crate::database::models::User::get_id(user_id, &*pool, &redis).await?
|
||||
} else {
|
||||
crate::database::models::User::get(
|
||||
&reset_password.username,
|
||||
&**pool,
|
||||
&*pool,
|
||||
&redis,
|
||||
)
|
||||
.await?
|
||||
@@ -1957,17 +1953,17 @@ pub struct ChangePassword {
|
||||
#[patch("password")]
|
||||
pub async fn change_password(
|
||||
req: HttpRequest,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
change_password: web::Json<ChangePassword>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
change_password: web::types::Json<ChangePassword>,
|
||||
session_queue: web::types::State<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = if let Some(flow) = &change_password.flow {
|
||||
let flow = Flow::get(flow, &redis).await?;
|
||||
|
||||
if let Some(Flow::ForgotPassword { user_id }) = flow {
|
||||
let user =
|
||||
crate::database::models::User::get_id(user_id, &**pool, &redis)
|
||||
crate::database::models::User::get_id(user_id, &*pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
|
||||
@@ -1985,7 +1981,7 @@ pub async fn change_password(
|
||||
let (scopes, user) = get_user_record_from_bearer_token(
|
||||
&req,
|
||||
None,
|
||||
&**pool,
|
||||
&*pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
)
|
||||
@@ -2108,11 +2104,11 @@ pub struct SetEmail {
|
||||
#[patch("email")]
|
||||
pub async fn set_email(
|
||||
req: HttpRequest,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
email: web::Json<SetEmail>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
stripe_client: Data<stripe::Client>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
email: web::types::Json<SetEmail>,
|
||||
session_queue: web::types::State<AuthQueue>,
|
||||
stripe_client: web::types::State<stripe::Client>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
email.0.validate().map_err(|err| {
|
||||
ApiError::InvalidInput(validation_errors_to_string(err, None))
|
||||
@@ -2120,7 +2116,7 @@ pub async fn set_email(
|
||||
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&*pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_AUTH_WRITE]),
|
||||
@@ -2194,13 +2190,13 @@ pub async fn set_email(
|
||||
#[post("email/resend_verify")]
|
||||
pub async fn resend_verify_email(
|
||||
req: HttpRequest,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
session_queue: web::types::State<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&*pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_AUTH_WRITE]),
|
||||
@@ -2243,9 +2239,9 @@ pub struct VerifyEmail {
|
||||
|
||||
#[post("email/verify")]
|
||||
pub async fn verify_email(
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
email: web::Json<VerifyEmail>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
email: web::types::Json<VerifyEmail>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let flow = Flow::get(&email.flow, &redis).await?;
|
||||
|
||||
@@ -2255,7 +2251,7 @@ pub async fn verify_email(
|
||||
}) = flow
|
||||
{
|
||||
let user =
|
||||
crate::database::models::User::get_id(user_id, &**pool, &redis)
|
||||
crate::database::models::User::get_id(user_id, &*pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
||||
|
||||
@@ -2296,13 +2292,13 @@ pub async fn verify_email(
|
||||
#[post("email/subscribe")]
|
||||
pub async fn subscribe_newsletter(
|
||||
req: HttpRequest,
|
||||
pool: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
pool: web::types::State<PgPool>,
|
||||
redis: web::types::State<RedisPool>,
|
||||
session_queue: web::types::State<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&*pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_AUTH_WRITE]),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user