mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-22 12:41:10 +10:00
Compare commits
16 Commits
0ce1f3124f
...
small-fixe
| Author | SHA1 | Date | |
|---|---|---|---|
| c0b69048cf | |||
| 1e7ed34a60 | |||
| e230f79b54 | |||
| 973c3efa18 | |||
| bcb88f8069 | |||
| b842d78b94 | |||
| b0bf1a2795 | |||
| 2d165bf997 | |||
| 650a3ca98d | |||
| 246c97ccc9 | |||
| f1fccd9bff | |||
| 2ae7f41be0 | |||
| beb824c8d9 | |||
| 8f41024be2 | |||
| 2420814862 | |||
| 41855bccd2 |
@ -1,54 +0,0 @@
|
|||||||
variables:
|
|
||||||
GIT_SUBMODULE_STRATEGY: recursive
|
|
||||||
|
|
||||||
stages:
|
|
||||||
- build
|
|
||||||
|
|
||||||
services:
|
|
||||||
- docker:24.0.5-dind
|
|
||||||
|
|
||||||
before_script:
|
|
||||||
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
|
|
||||||
|
|
||||||
build:
|
|
||||||
stage: build
|
|
||||||
image: docker:latest
|
|
||||||
variables:
|
|
||||||
IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_NAME:$CI_COMMIT_SHORT_SHA
|
|
||||||
LATEST_IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_NAME:latest
|
|
||||||
PUBLISH_IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
|
|
||||||
PUBLISH_LATEST_IMAGE_NAME: $CI_REGISTRY_IMAGE:latest
|
|
||||||
script:
|
|
||||||
- docker build -t $IMAGE_NAME .
|
|
||||||
- docker image tag $IMAGE_NAME $LATEST_IMAGE_NAME
|
|
||||||
- docker push $IMAGE_NAME
|
|
||||||
- docker push $LATEST_IMAGE_NAME
|
|
||||||
- |
|
|
||||||
if [ $CI_COMMIT_TAG ]; then
|
|
||||||
docker image tag $IMAGE_NAME $PUBLISH_IMAGE_NAME
|
|
||||||
docker image tag $IMAGE_NAME $PUBLISH_LATEST_IMAGE_NAME
|
|
||||||
docker push $PUBLISH_IMAGE_NAME $PUBLISH_LATEST_IMAGE_NAME
|
|
||||||
fi
|
|
||||||
|
|
||||||
build-arm64:
|
|
||||||
stage: build
|
|
||||||
image: arm64v8/docker:latest
|
|
||||||
tags:
|
|
||||||
- aarch64
|
|
||||||
variables:
|
|
||||||
IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_NAME:$CI_COMMIT_SHORT_SHA-arm64
|
|
||||||
LATEST_IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_NAME:latest-arm64
|
|
||||||
PUBLISH_IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG-arm64
|
|
||||||
PUBLISH_LATEST_IMAGE_NAME: $CI_REGISTRY_IMAGE:latest-arm64
|
|
||||||
script:
|
|
||||||
- docker build -t $IMAGE_NAME . --platform=linux/arm64
|
|
||||||
- docker image tag $IMAGE_NAME $LATEST_IMAGE_NAME
|
|
||||||
- docker push $IMAGE_NAME
|
|
||||||
- docker push $LATEST_IMAGE_NAME
|
|
||||||
- |
|
|
||||||
if [ $CI_COMMIT_TAG ]; then
|
|
||||||
docker image tag $IMAGE_NAME $PUBLISH_IMAGE_NAME
|
|
||||||
docker image tag $IMAGE_NAME $PUBLISH_LATEST_IMAGE_NAME
|
|
||||||
docker push $PUBLISH_IMAGE_NAME
|
|
||||||
docker push $PUBLISH_LATEST_IMAGE_NAME
|
|
||||||
fi
|
|
||||||
@ -45,12 +45,12 @@ ENV NODE_ENV=production
|
|||||||
ENV NUXT_TELEMETRY_DISABLED=1
|
ENV NUXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
# RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn add --network-timeout 1000000 --no-lockfile --ignore-scripts prisma@6.11.1
|
# RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn add --network-timeout 1000000 --no-lockfile --ignore-scripts prisma@6.11.1
|
||||||
RUN apk add --no-cache pnpm
|
RUN apk add --no-cache pnpm 7zip
|
||||||
RUN pnpm install prisma@6.11.1
|
RUN pnpm install prisma@6.11.1
|
||||||
# init prisma to download all required files
|
# init prisma to download all required files
|
||||||
RUN pnpm prisma init
|
RUN pnpm prisma init
|
||||||
|
|
||||||
COPY --from=build-system /app/package.json ./
|
COPY --from=build-system /app/prisma.config.ts ./
|
||||||
COPY --from=build-system /app/.output ./app
|
COPY --from=build-system /app/.output ./app
|
||||||
COPY --from=build-system /app/prisma ./prisma
|
COPY --from=build-system /app/prisma ./prisma
|
||||||
COPY --from=build-system /app/build ./startup
|
COPY --from=build-system /app/build ./startup
|
||||||
|
|||||||
@ -44,9 +44,12 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { XMarkIcon } from "@heroicons/vue/24/solid";
|
import { XMarkIcon } from "@heroicons/vue/24/solid";
|
||||||
|
import type { SerializeObject } from "nitropack";
|
||||||
import type { NotificationModel } from "~/prisma/client/models";
|
import type { NotificationModel } from "~/prisma/client/models";
|
||||||
|
|
||||||
const props = defineProps<{ notification: NotificationModel }>();
|
const props = defineProps<{
|
||||||
|
notification: SerializeObject<NotificationModel>;
|
||||||
|
}>();
|
||||||
|
|
||||||
async function deleteMe() {
|
async function deleteMe() {
|
||||||
await $dropFetch(`/api/v1/notifications/:id`, {
|
await $dropFetch(`/api/v1/notifications/:id`, {
|
||||||
|
|||||||
@ -46,7 +46,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { SerializeObject } from "nitropack";
|
||||||
import type { NotificationModel } from "~/prisma/client/models";
|
import type { NotificationModel } from "~/prisma/client/models";
|
||||||
|
|
||||||
const props = defineProps<{ notifications: Array<NotificationModel> }>();
|
const props = defineProps<{
|
||||||
|
notifications: Array<SerializeObject<NotificationModel>>;
|
||||||
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import type { RouteLocationNormalized } from "vue-router";
|
|
||||||
import type { NavigationItem } from "./types";
|
import type { NavigationItem } from "./types";
|
||||||
|
|
||||||
export const useCurrentNavigationIndex = (
|
export const useCurrentNavigationIndex = (
|
||||||
@ -9,7 +8,7 @@ export const useCurrentNavigationIndex = (
|
|||||||
|
|
||||||
const currentNavigation = ref(-1);
|
const currentNavigation = ref(-1);
|
||||||
|
|
||||||
function calculateCurrentNavIndex(to: RouteLocationNormalized) {
|
function calculateCurrentNavIndex(to: typeof route) {
|
||||||
const validOptions = navigation
|
const validOptions = navigation
|
||||||
.map((e, i) => ({ ...e, index: i }))
|
.map((e, i) => ({ ...e, index: i }))
|
||||||
.filter((e) => to.fullPath.startsWith(e.prefix));
|
.filter((e) => to.fullPath.startsWith(e.prefix));
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
|
import type { SerializeObject } from "nitropack";
|
||||||
import type { NotificationModel } from "~/prisma/client/models";
|
import type { NotificationModel } from "~/prisma/client/models";
|
||||||
|
|
||||||
const ws = new WebSocketHandler("/api/v1/notifications/ws");
|
const ws = new WebSocketHandler("/api/v1/notifications/ws");
|
||||||
|
|
||||||
export const useNotifications = () =>
|
export const useNotifications = () =>
|
||||||
useState<Array<NotificationModel>>("notifications", () => []);
|
useState<Array<SerializeObject<NotificationModel>>>(
|
||||||
|
"notifications",
|
||||||
|
() => [],
|
||||||
|
);
|
||||||
|
|
||||||
ws.listen((e) => {
|
ws.listen((e) => {
|
||||||
const notification = JSON.parse(e) as NotificationModel;
|
const notification = JSON.parse(e) as SerializeObject<NotificationModel>;
|
||||||
const notifications = useNotifications();
|
const notifications = useNotifications();
|
||||||
notifications.value.push(notification);
|
notifications.value.push(notification);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import withNuxt from "./.nuxt/eslint.config.mjs";
|
import withNuxt from "./.nuxt/eslint.config.mjs";
|
||||||
import eslintConfigPrettier from "eslint-config-prettier/flat";
|
import eslintConfigPrettier from "eslint-config-prettier/flat";
|
||||||
import vueI18n from "@intlify/eslint-plugin-vue-i18n";
|
import vueI18n from "@intlify/eslint-plugin-vue-i18n";
|
||||||
|
import noPrismaDelete from "./rules/no-prisma-delete.mts";
|
||||||
|
|
||||||
export default withNuxt([
|
export default withNuxt([
|
||||||
eslintConfigPrettier,
|
eslintConfigPrettier,
|
||||||
@ -19,6 +20,7 @@ export default withNuxt([
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"@intlify/vue-i18n/no-missing-keys": "error",
|
"@intlify/vue-i18n/no-missing-keys": "error",
|
||||||
|
"drop/no-prisma-delete": "error",
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
"vue-i18n": {
|
"vue-i18n": {
|
||||||
@ -29,5 +31,8 @@ export default withNuxt([
|
|||||||
messageSyntaxVersion: "^11.0.0",
|
messageSyntaxVersion: "^11.0.0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
plugins: {
|
||||||
|
drop: { rules: { "no-prisma-delete": noPrismaDelete } },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
"all": "View all {arrow}",
|
"all": "View all {arrow}",
|
||||||
"desc": "View and manage your notifications.",
|
"desc": "View and manage your notifications.",
|
||||||
"markAllAsRead": "Mark all as read",
|
"markAllAsRead": "Mark all as read",
|
||||||
|
"clear": "Clear notifications",
|
||||||
"markAsRead": "Mark as read",
|
"markAsRead": "Mark as read",
|
||||||
"none": "No notifications",
|
"none": "No notifications",
|
||||||
"notifications": "Notifications",
|
"notifications": "Notifications",
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="!noWrapper" class="flex flex-col w-full min-h-screen bg-zinc-900">
|
<div v-if="!noWrapper" class="flex flex-col w-full min-h-screen bg-zinc-900">
|
||||||
<UserHeader class="z-50" hydrate-on-idle />
|
<LazyUserHeader class="z-50" hydrate-on-idle />
|
||||||
<div class="grow flex">
|
<div class="grow flex">
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</div>
|
</div>
|
||||||
<UserFooter class="z-50" hydrate-on-interaction />
|
<LazyUserFooter class="z-50" hydrate-on-interaction />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex w-full min-h-screen bg-zinc-900">
|
<div v-else class="flex w-full min-h-screen bg-zinc-900">
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
|
|||||||
@ -175,6 +175,9 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
|
|
||||||
i18n: {
|
i18n: {
|
||||||
|
bundle: {
|
||||||
|
optimizeTranslationDirective: false,
|
||||||
|
},
|
||||||
defaultLocale: "en-us",
|
defaultLocale: "en-us",
|
||||||
strategy: "no_prefix",
|
strategy: "no_prefix",
|
||||||
experimental: {
|
experimental: {
|
||||||
|
|||||||
11
package.json
11
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "drop",
|
"name": "drop",
|
||||||
"version": "0.3.3",
|
"version": "0.3.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
@ -21,7 +21,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordapp/twemoji": "^16.0.1",
|
"@discordapp/twemoji": "^16.0.1",
|
||||||
"@drop-oss/droplet": "3.2.0",
|
"@drop-oss/droplet": "3.5.0",
|
||||||
"@headlessui/vue": "^1.7.23",
|
"@headlessui/vue": "^1.7.23",
|
||||||
"@heroicons/vue": "^2.1.5",
|
"@heroicons/vue": "^2.1.5",
|
||||||
"@lobomfz/prismark": "0.0.3",
|
"@lobomfz/prismark": "0.0.3",
|
||||||
@ -37,6 +37,7 @@
|
|||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"cheerio": "^1.0.0",
|
"cheerio": "^1.0.0",
|
||||||
"cookie-es": "^2.0.0",
|
"cookie-es": "^2.0.0",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
"fast-fuzzy": "^1.12.0",
|
"fast-fuzzy": "^1.12.0",
|
||||||
"file-type-mime": "^0.4.3",
|
"file-type-mime": "^0.4.3",
|
||||||
"jdenticon": "^3.3.0",
|
"jdenticon": "^3.3.0",
|
||||||
@ -47,7 +48,7 @@
|
|||||||
"nuxt-security": "2.2.0",
|
"nuxt-security": "2.2.0",
|
||||||
"pino": "^9.7.0",
|
"pino": "^9.7.0",
|
||||||
"pino-pretty": "^13.0.0",
|
"pino-pretty": "^13.0.0",
|
||||||
"prisma": "^6.11.1",
|
"prisma": "6.11.1",
|
||||||
"sanitize-filename": "^1.6.3",
|
"sanitize-filename": "^1.6.3",
|
||||||
"semver": "^7.7.1",
|
"semver": "^7.7.1",
|
||||||
"stream-mime-type": "^2.0.0",
|
"stream-mime-type": "^2.0.0",
|
||||||
@ -65,7 +66,6 @@
|
|||||||
"@nuxt/eslint": "^1.3.0",
|
"@nuxt/eslint": "^1.3.0",
|
||||||
"@tailwindcss/forms": "^0.5.9",
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@types/bcryptjs": "^3.0.0",
|
|
||||||
"@types/luxon": "^3.6.2",
|
"@types/luxon": "^3.6.2",
|
||||||
"@types/node": "^22.13.16",
|
"@types/node": "^22.13.16",
|
||||||
"@types/semver": "^7.7.0",
|
"@types/semver": "^7.7.0",
|
||||||
@ -87,8 +87,5 @@
|
|||||||
"vue3-carousel": "^0.16.0"
|
"vue3-carousel": "^0.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"prisma": {
|
|
||||||
"schema": "./prisma"
|
|
||||||
},
|
|
||||||
"packageManager": "pnpm@10.15.0+sha512.486ebc259d3e999a4e8691ce03b5cac4a71cbeca39372a9b762cb500cfdf0873e2cb16abe3d951b1ee2cf012503f027b98b6584e4df22524e0c7450d9ec7aa7b"
|
"packageManager": "pnpm@10.15.0+sha512.486ebc259d3e999a4e8691ce03b5cac4a71cbeca39372a9b762cb500cfdf0873e2cb16abe3d951b1ee2cf012503f027b98b6584e4df22524e0c7450d9ec7aa7b"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,20 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="border-b border-zinc-800 pb-4 w-full">
|
<div class="border-b border-zinc-800 pb-4 w-full">
|
||||||
<div class="flex items-center justify-between w-full">
|
<div
|
||||||
|
class="gap-2 flex flex-col lg:flex-row lg:items-center justify-between w-full"
|
||||||
|
>
|
||||||
<h2
|
<h2
|
||||||
class="text-xl font-semibold tracking-tight text-zinc-100 sm:text-3xl"
|
class="text-xl font-semibold tracking-tight text-zinc-100 sm:text-3xl"
|
||||||
>
|
>
|
||||||
{{ $t("account.notifications.notifications") }}
|
{{ $t("account.notifications.notifications") }}
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<div class="inline-flex gap-x-2">
|
||||||
:disabled="notifications.length === 0"
|
<button
|
||||||
class="inline-flex items-center justify-center gap-x-2 rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm transition-all duration-200 hover:bg-zinc-700 hover:scale-[1.02] hover:shadow-lg active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-600 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-zinc-800 disabled:hover:scale-100 disabled:hover:shadow-none"
|
:disabled="notifications.length === 0"
|
||||||
@click="markAllAsRead"
|
class="inline-flex items-center justify-center gap-x-2 rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm transition-all duration-200 hover:bg-zinc-700 hover:scale-[1.02] hover:shadow-lg active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-600 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-zinc-800 disabled:hover:scale-100 disabled:hover:shadow-none"
|
||||||
>
|
@click="markAllAsRead"
|
||||||
<CheckIcon class="size-4" />
|
>
|
||||||
{{ $t("account.notifications.markAllAsRead") }}
|
<CheckIcon class="size-4" />
|
||||||
</button>
|
{{ $t("account.notifications.markAllAsRead") }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:disabled="notifications.length === 0"
|
||||||
|
class="inline-flex items-center justify-center gap-x-2 rounded-md bg-red-800 px-3 py-2 text-sm font-semibold text-red-100 shadow-sm transition-all duration-200 hover:bg-red-700 hover:scale-[1.02] hover:shadow-lg active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-red-800 disabled:hover:scale-100 disabled:hover:shadow-none"
|
||||||
|
@click="clearNotifications"
|
||||||
|
>
|
||||||
|
<TrashIcon class="size-4" />
|
||||||
|
{{ $t("account.notifications.clear") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
class="mt-2 text-pretty text-sm font-medium text-zinc-400 sm:text-md/8"
|
class="mt-2 text-pretty text-sm font-medium text-zinc-400 sm:text-md/8"
|
||||||
@ -31,7 +43,7 @@
|
|||||||
:class="{ 'opacity-75': notification.read }"
|
:class="{ 'opacity-75': notification.read }"
|
||||||
>
|
>
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex flex-col lg:flex-row items-start justify-between">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h3 class="text-base font-semibold text-zinc-100">
|
<h3 class="text-base font-semibold text-zinc-100">
|
||||||
{{ notification.title }}
|
{{ notification.title }}
|
||||||
@ -52,7 +64,9 @@
|
|||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-4 flex flex-shrink-0 items-center gap-x-2">
|
<div
|
||||||
|
class="mt-4 lg:mt-0 lg:ml-4 flex flex-shrink-0 items-center gap-x-2"
|
||||||
|
>
|
||||||
<span class="text-xs text-zinc-500">
|
<span class="text-xs text-zinc-500">
|
||||||
<RelativeTime :date="notification.created" />
|
<RelativeTime :date="notification.created" />
|
||||||
</span>
|
</span>
|
||||||
@ -106,22 +120,12 @@ useHead({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Fetch notifications
|
// Fetch notifications
|
||||||
const notifications = ref<SerializeObject<NotificationModel>[]>([]);
|
const notifications = useNotifications();
|
||||||
|
|
||||||
async function fetchNotifications() {
|
|
||||||
const { data } = await useFetch("/api/v1/notifications");
|
|
||||||
notifications.value = data.value || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial fetch
|
|
||||||
await fetchNotifications();
|
|
||||||
|
|
||||||
// Mark a notification as read
|
// Mark a notification as read
|
||||||
async function markAsRead(id: string) {
|
async function markAsRead(id: string) {
|
||||||
await $dropFetch(`/api/v1/notifications/${id}/read`, { method: "POST" });
|
await $dropFetch(`/api/v1/notifications/${id}/read`, { method: "POST" });
|
||||||
const notification = notifications.value.find(
|
const notification = notifications.value.find((n) => n.id === id);
|
||||||
(n: SerializeObject<NotificationModel>) => n.id === id,
|
|
||||||
);
|
|
||||||
if (notification) {
|
if (notification) {
|
||||||
notification.read = true;
|
notification.read = true;
|
||||||
}
|
}
|
||||||
@ -129,12 +133,21 @@ async function markAsRead(id: string) {
|
|||||||
|
|
||||||
// Mark all notifications as read
|
// Mark all notifications as read
|
||||||
async function markAllAsRead() {
|
async function markAllAsRead() {
|
||||||
await $dropFetch("/api/v1/notifications/readall", { method: "POST" });
|
await $dropFetch("/api/v1/notifications/readall", {
|
||||||
notifications.value.forEach(
|
method: "POST",
|
||||||
(notification: SerializeObject<NotificationModel>) => {
|
failTitle: "Failed to read all notifications",
|
||||||
notification.read = true;
|
});
|
||||||
},
|
notifications.value.forEach((notification) => {
|
||||||
);
|
notification.read = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearNotifications() {
|
||||||
|
await $dropFetch("/api/v1/notifications/clear", {
|
||||||
|
method: "POST",
|
||||||
|
failTitle: "Failed to clear notifications",
|
||||||
|
});
|
||||||
|
notifications.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete a notification
|
// Delete a notification
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<!-- tabs-->
|
<!-- tabs-->
|
||||||
<div>
|
<div>
|
||||||
<div class="border-b border-gray-200 dark:border-white/10">
|
<div class="border-b border-white/10">
|
||||||
<nav class="-mb-px flex gap-x-2" aria-label="Tabs">
|
<nav class="-mb-px flex gap-x-2" aria-label="Tabs">
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-for="(tab, tabIdx) in navigation"
|
v-for="(tab, tabIdx) in navigation"
|
||||||
@ -10,8 +10,8 @@
|
|||||||
:href="tab.route"
|
:href="tab.route"
|
||||||
:class="[
|
:class="[
|
||||||
currentNavigationIndex == tabIdx
|
currentNavigationIndex == tabIdx
|
||||||
? 'border-blue-500 text-blue-600 dark:border-blue-400 dark:text-blue-400'
|
? 'border-blue-400 text-blue-400'
|
||||||
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:border-white/20 dark:hover:text-gray-300',
|
: 'border-transparent text-gray-400 hover:border-white/20 hover:text-gray-300',
|
||||||
'group inline-flex items-center border-b-2 px-1 py-4 text-sm font-medium',
|
'group inline-flex items-center border-b-2 px-1 py-4 text-sm font-medium',
|
||||||
]"
|
]"
|
||||||
:aria-current="tab ? 'page' : undefined"
|
:aria-current="tab ? 'page' : undefined"
|
||||||
@ -20,8 +20,8 @@
|
|||||||
:is="tab.icon"
|
:is="tab.icon"
|
||||||
:class="[
|
:class="[
|
||||||
currentNavigationIndex == tabIdx
|
currentNavigationIndex == tabIdx
|
||||||
? 'text-blue-500 dark:text-blue-400'
|
? 'text-blue-400'
|
||||||
: 'text-gray-400 group-hover:text-gray-500 dark:text-gray-500 dark:group-hover:text-gray-400',
|
: 'text-gray-500 group-hover:text-gray-400',
|
||||||
'mr-2 -ml-0.5 size-5',
|
'mr-2 -ml-0.5 size-5',
|
||||||
]"
|
]"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
|||||||
3691
pnpm-lock.yaml
generated
3691
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,10 @@
|
|||||||
|
onlyBuiltDependencies:
|
||||||
|
- "@prisma/client"
|
||||||
|
- "@prisma/engines"
|
||||||
|
- "@tailwindcss/oxide"
|
||||||
|
- esbuild
|
||||||
|
- prisma
|
||||||
|
|
||||||
overrides:
|
overrides:
|
||||||
droplet: link:../../.local/share/pnpm/global/5/node_modules/@drop-oss/droplet
|
droplet: link:../../.local/share/pnpm/global/5/node_modules/@drop-oss/droplet
|
||||||
|
|
||||||
|
|||||||
10
prisma.config.ts
Normal file
10
prisma.config.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { config } from "dotenv";
|
||||||
|
import type { PrismaConfig } from "prisma";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
config();
|
||||||
|
|
||||||
|
export default {
|
||||||
|
schema: path.join("prisma"),
|
||||||
|
earlyAccess: true,
|
||||||
|
} satisfies PrismaConfig;
|
||||||
34
rules/no-prisma-delete.mts
Normal file
34
rules/no-prisma-delete.mts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import type { TSESLint } from "@typescript-eslint/utils";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
meta: {
|
||||||
|
type: "problem",
|
||||||
|
docs: {
|
||||||
|
description: "Don't use Prisma error-prone .delete function",
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
noPrismaDelete:
|
||||||
|
"Prisma .delete(...) function is used. Use .deleteMany(..) and check count instead.",
|
||||||
|
},
|
||||||
|
schema: [],
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
return {
|
||||||
|
CallExpression: function (node) {
|
||||||
|
// @ts-expect-error It ain't typing properly
|
||||||
|
const funcId = node.callee.property;
|
||||||
|
if (!funcId || funcId.name !== "delete") return;
|
||||||
|
// @ts-expect-error It ain't typing properly
|
||||||
|
const tableExpr = node.callee.object;
|
||||||
|
if (!tableExpr) return;
|
||||||
|
const prismaExpr = tableExpr.object;
|
||||||
|
if (!prismaExpr || prismaExpr.name !== "prisma") return;
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: "noPrismaDelete",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
defaultOptions: [],
|
||||||
|
} satisfies TSESLint.RuleModule<"noPrismaDelete">;
|
||||||
@ -17,6 +17,10 @@ export default defineEventHandler<{
|
|||||||
|
|
||||||
const body = await readDropValidatedBody(h3, DeleteInvite);
|
const body = await readDropValidatedBody(h3, DeleteInvite);
|
||||||
|
|
||||||
await prisma.invitation.delete({ where: { id: body.id } });
|
const { count } = await prisma.invitation.deleteMany({
|
||||||
|
where: { id: body.id },
|
||||||
|
});
|
||||||
|
if (count == 0)
|
||||||
|
throw createError({ statusCode: 404, message: "Invitation not found." });
|
||||||
return {};
|
return {};
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export default defineEventHandler(async (h3) => {
|
|||||||
|
|
||||||
const gameId = getRouterParam(h3, "id")!;
|
const gameId = getRouterParam(h3, "id")!;
|
||||||
|
|
||||||
libraryManager.deleteGame(gameId);
|
await libraryManager.deleteGame(gameId);
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
});
|
});
|
||||||
|
|||||||
@ -18,11 +18,13 @@ export default defineEventHandler<{ body: typeof DeleteLibrarySource.infer }>(
|
|||||||
|
|
||||||
const body = await readDropValidatedBody(h3, DeleteLibrarySource);
|
const body = await readDropValidatedBody(h3, DeleteLibrarySource);
|
||||||
|
|
||||||
await prisma.library.delete({
|
const { count } = await prisma.library.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
id: body.id,
|
id: body.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (count == 0)
|
||||||
|
throw createError({ statusCode: 404, message: "Library not found." });
|
||||||
|
|
||||||
libraryManager.removeLibrary(body.id);
|
libraryManager.removeLibrary(body.id);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -13,10 +13,10 @@ export default defineEventHandler(async (h3) => {
|
|||||||
statusMessage: "No id in router params",
|
statusMessage: "No id in router params",
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleted = await prisma.aPIToken.delete({
|
const { count } = await prisma.aPIToken.deleteMany({
|
||||||
where: { id: id, mode: APITokenMode.System },
|
where: { id: id, mode: APITokenMode.System },
|
||||||
})!;
|
})!;
|
||||||
if (!deleted)
|
if (count == 0)
|
||||||
throw createError({ statusCode: 404, statusMessage: "Token not found" });
|
throw createError({ statusCode: 404, statusMessage: "Token not found" });
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -27,6 +27,7 @@ export default defineEventHandler(async (h3) => {
|
|||||||
if (!user)
|
if (!user)
|
||||||
throw createError({ statusCode: 404, statusMessage: "User not found." });
|
throw createError({ statusCode: 404, statusMessage: "User not found." });
|
||||||
|
|
||||||
|
// eslint-disable-next-line drop/no-prisma-delete
|
||||||
await prisma.user.delete({ where: { id: userId } });
|
await prisma.user.delete({ where: { id: userId } });
|
||||||
await userStatsManager.deleteUser();
|
await userStatsManager.deleteUser();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
|
|||||||
@ -84,7 +84,7 @@ export default defineEventHandler<{
|
|||||||
user: true,
|
user: true,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.invitation.delete({ where: { id: user.invitation } }),
|
prisma.invitation.deleteMany({ where: { id: user.invitation } }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await userStatsManager.addUser();
|
await userStatsManager.addUser();
|
||||||
|
|||||||
@ -42,7 +42,7 @@ export default defineEventHandler(async (h3) => {
|
|||||||
)
|
)
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
statusMessage: "Invalid capabilities.",
|
message: "Invalid capabilities.",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@ -17,21 +17,10 @@ export default defineClientEventHandler(async (h3) => {
|
|||||||
orderBy: {
|
orderBy: {
|
||||||
versionIndex: "desc", // Latest one first
|
versionIndex: "desc", // Latest one first
|
||||||
},
|
},
|
||||||
|
omit: {
|
||||||
|
dropletManifest: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const mappedVersions = versions
|
return versions;
|
||||||
.map((version) => {
|
|
||||||
if (!version.dropletManifest) return undefined;
|
|
||||||
|
|
||||||
const newVersion = { ...version, dropletManifest: undefined };
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore idk why we delete an undefined object
|
|
||||||
delete newVersion.dropletManifest;
|
|
||||||
return {
|
|
||||||
...newVersion,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter((e) => e);
|
|
||||||
|
|
||||||
return mappedVersions;
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -38,16 +38,14 @@ export default defineClientEventHandler(
|
|||||||
if (!game)
|
if (!game)
|
||||||
throw createError({ statusCode: 400, statusMessage: "Invalid game ID" });
|
throw createError({ statusCode: 400, statusMessage: "Invalid game ID" });
|
||||||
|
|
||||||
const save = await prisma.saveSlot.delete({
|
const { count } = await prisma.saveSlot.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
id: {
|
userId: user.id,
|
||||||
userId: user.id,
|
gameId: gameId,
|
||||||
gameId: gameId,
|
index: slotIndex,
|
||||||
index: slotIndex,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!save)
|
if (count == 0)
|
||||||
throw createError({ statusCode: 404, statusMessage: "Save not found" });
|
throw createError({ statusCode: 404, statusMessage: "Save not found" });
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -20,14 +20,14 @@ export default defineEventHandler(async (h3) => {
|
|||||||
userIds.push("system");
|
userIds.push("system");
|
||||||
}
|
}
|
||||||
|
|
||||||
const notification = await prisma.notification.delete({
|
const { count } = await prisma.notification.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
id: notificationId,
|
id: notificationId,
|
||||||
userId: { in: userIds },
|
userId: { in: userIds },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!notification)
|
if (count == 0)
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
statusMessage: "Invalid notification ID",
|
statusMessage: "Invalid notification ID",
|
||||||
|
|||||||
25
server/api/v1/notifications/clear.post.ts
Normal file
25
server/api/v1/notifications/clear.post.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import aclManager from "~/server/internal/acls";
|
||||||
|
import prisma from "~/server/internal/db/database";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (h3) => {
|
||||||
|
const userId = await aclManager.getUserIdACL(h3, ["notifications:mark"]);
|
||||||
|
if (!userId) throw createError({ statusCode: 403 });
|
||||||
|
|
||||||
|
const acls = await aclManager.fetchAllACLs(h3);
|
||||||
|
if (!acls)
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: "Got userId but no ACLs - what?",
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.notification.deleteMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
acls: {
|
||||||
|
hasSome: acls,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
});
|
||||||
@ -13,10 +13,10 @@ export default defineEventHandler(async (h3) => {
|
|||||||
statusMessage: "No id in router params",
|
statusMessage: "No id in router params",
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleted = await prisma.aPIToken.delete({
|
const { count } = await prisma.aPIToken.deleteMany({
|
||||||
where: { id: id, userId: userId, mode: APITokenMode.User },
|
where: { id: id, userId: userId, mode: APITokenMode.User },
|
||||||
})!;
|
})!;
|
||||||
if (!deleted)
|
if (count == 0)
|
||||||
throw createError({ statusCode: 404, statusMessage: "Token not found" });
|
throw createError({ statusCode: 404, statusMessage: "Token not found" });
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -66,6 +66,7 @@ export class OIDCManager {
|
|||||||
|
|
||||||
async create() {
|
async create() {
|
||||||
const wellKnownUrl = process.env.OIDC_WELLKNOWN as string | undefined;
|
const wellKnownUrl = process.env.OIDC_WELLKNOWN as string | undefined;
|
||||||
|
const scopes = process.env.OIDC_SCOPES as string | undefined;
|
||||||
let configuration: OIDCWellKnown;
|
let configuration: OIDCWellKnown;
|
||||||
if (wellKnownUrl) {
|
if (wellKnownUrl) {
|
||||||
const response: OIDCWellKnown = await $fetch<OIDCWellKnown>(wellKnownUrl);
|
const response: OIDCWellKnown = await $fetch<OIDCWellKnown>(wellKnownUrl);
|
||||||
@ -77,6 +78,9 @@ export class OIDCManager {
|
|||||||
) {
|
) {
|
||||||
throw new Error("Well known response was invalid");
|
throw new Error("Well known response was invalid");
|
||||||
}
|
}
|
||||||
|
if (scopes) {
|
||||||
|
response.scopes_supported = scopes.split(",");
|
||||||
|
}
|
||||||
|
|
||||||
configuration = response;
|
configuration = response;
|
||||||
} else {
|
} else {
|
||||||
@ -85,7 +89,6 @@ export class OIDCManager {
|
|||||||
| undefined;
|
| undefined;
|
||||||
const tokenEndpoint = process.env.OIDC_TOKEN as string | undefined;
|
const tokenEndpoint = process.env.OIDC_TOKEN as string | undefined;
|
||||||
const userinfoEndpoint = process.env.OIDC_USERINFO as string | undefined;
|
const userinfoEndpoint = process.env.OIDC_USERINFO as string | undefined;
|
||||||
const scopes = process.env.OIDC_SCOPES as string | undefined;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!authorizationEndpoint ||
|
!authorizationEndpoint ||
|
||||||
|
|||||||
@ -185,15 +185,19 @@ export class ClientHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async removeClient(id: string) {
|
async removeClient(id: string) {
|
||||||
|
const client = await prisma.client.findUnique({ where: { id } });
|
||||||
|
if (!client) return false;
|
||||||
const ca = useCertificateAuthority();
|
const ca = useCertificateAuthority();
|
||||||
await ca.blacklistClient(id);
|
await ca.blacklistClient(id);
|
||||||
|
|
||||||
|
// eslint-disable-next-line drop/no-prisma-delete
|
||||||
await prisma.client.delete({
|
await prisma.client.delete({
|
||||||
where: {
|
where: {
|
||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await userStatsManager.cacheUserStats();
|
await userStatsManager.cacheUserStats();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,8 @@ class SystemConfig {
|
|||||||
private libraryFolder = process.env.LIBRARY ?? "./.data/library";
|
private libraryFolder = process.env.LIBRARY ?? "./.data/library";
|
||||||
private dataFolder = process.env.DATA ?? "./.data/data";
|
private dataFolder = process.env.DATA ?? "./.data/data";
|
||||||
|
|
||||||
|
private metadataTimeout = parseInt(process.env.METADATA_TIMEOUT ?? "5000");
|
||||||
|
|
||||||
private externalUrl = normalizeUrl(
|
private externalUrl = normalizeUrl(
|
||||||
process.env.EXTERNAL_URL ?? "http://localhost:3000",
|
process.env.EXTERNAL_URL ?? "http://localhost:3000",
|
||||||
{ stripWWW: false },
|
{ stripWWW: false },
|
||||||
@ -28,6 +30,10 @@ class SystemConfig {
|
|||||||
return this.dataFolder;
|
return this.dataFolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMetadataTimeout() {
|
||||||
|
return this.metadataTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
getDropVersion() {
|
getDropVersion() {
|
||||||
return this.dropVersion;
|
return this.dropVersion;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -105,7 +105,10 @@ class LibraryManager {
|
|||||||
if (!game) return undefined;
|
if (!game) return undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const versions = await provider.listVersions(libraryPath);
|
const versions = await provider.listVersions(
|
||||||
|
libraryPath,
|
||||||
|
game.versions.map((v) => v.versionName),
|
||||||
|
);
|
||||||
const unimportedVersions = versions.filter(
|
const unimportedVersions = versions.filter(
|
||||||
(e) =>
|
(e) =>
|
||||||
game.versions.findIndex((v) => v.versionName == e) == -1 &&
|
game.versions.findIndex((v) => v.versionName == e) == -1 &&
|
||||||
@ -375,12 +378,10 @@ class LibraryManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteGameVersion(gameId: string, version: string) {
|
async deleteGameVersion(gameId: string, version: string) {
|
||||||
await prisma.gameVersion.delete({
|
await prisma.gameVersion.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
gameId_versionName: {
|
gameId: gameId,
|
||||||
gameId: gameId,
|
versionName: version,
|
||||||
versionName: version,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -388,12 +389,12 @@ class LibraryManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteGame(gameId: string) {
|
async deleteGame(gameId: string) {
|
||||||
await prisma.game.delete({
|
await prisma.game.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
id: gameId,
|
id: gameId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
gameSizeManager.deleteGame(gameId);
|
await gameSizeManager.deleteGame(gameId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getGameVersionSize(
|
async getGameVersionSize(
|
||||||
|
|||||||
@ -24,7 +24,10 @@ export abstract class LibraryProvider<CFG> {
|
|||||||
* @param game folder name of the game to list versions for
|
* @param game folder name of the game to list versions for
|
||||||
* @returns list of version folder names
|
* @returns list of version folder names
|
||||||
*/
|
*/
|
||||||
abstract listVersions(game: string): Promise<string[]>;
|
abstract listVersions(
|
||||||
|
game: string,
|
||||||
|
existingPaths?: string[],
|
||||||
|
): Promise<string[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param game folder name of the game
|
* @param game folder name of the game
|
||||||
|
|||||||
@ -54,11 +54,15 @@ export class FilesystemProvider
|
|||||||
return folderDirs;
|
return folderDirs;
|
||||||
}
|
}
|
||||||
|
|
||||||
async listVersions(game: string): Promise<string[]> {
|
async listVersions(
|
||||||
|
game: string,
|
||||||
|
ignoredVersions?: string[],
|
||||||
|
): Promise<string[]> {
|
||||||
const gameDir = path.join(this.config.baseDir, game);
|
const gameDir = path.join(this.config.baseDir, game);
|
||||||
if (!fs.existsSync(gameDir)) throw new GameNotFoundError();
|
if (!fs.existsSync(gameDir)) throw new GameNotFoundError();
|
||||||
const versionDirs = fs.readdirSync(gameDir);
|
const versionDirs = fs.readdirSync(gameDir);
|
||||||
const validVersionDirs = versionDirs.filter((e) => {
|
const validVersionDirs = versionDirs.filter((e) => {
|
||||||
|
if (ignoredVersions && ignoredVersions.includes(e)) return false;
|
||||||
const fullDir = path.join(this.config.baseDir, game, e);
|
const fullDir = path.join(this.config.baseDir, game, e);
|
||||||
return DROPLET_HANDLER.hasBackendForPath(fullDir);
|
return DROPLET_HANDLER.hasBackendForPath(fullDir);
|
||||||
});
|
});
|
||||||
@ -109,17 +113,12 @@ export class FilesystemProvider
|
|||||||
) {
|
) {
|
||||||
const filepath = path.join(this.config.baseDir, game, version);
|
const filepath = path.join(this.config.baseDir, game, version);
|
||||||
if (!fs.existsSync(filepath)) return undefined;
|
if (!fs.existsSync(filepath)) return undefined;
|
||||||
let stream;
|
const stream = DROPLET_HANDLER.readFile(
|
||||||
while (!(stream instanceof ReadableStream)) {
|
filepath,
|
||||||
const v = DROPLET_HANDLER.readFile(
|
filename,
|
||||||
filepath,
|
options?.start ? BigInt(options.start) : undefined,
|
||||||
filename,
|
options?.end ? BigInt(options.end) : undefined,
|
||||||
options?.start ? BigInt(options.start) : undefined,
|
);
|
||||||
options?.end ? BigInt(options.end) : undefined,
|
|
||||||
);
|
|
||||||
if (!v) return undefined;
|
|
||||||
stream = v.getStream() as ReadableStream<unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return stream;
|
return stream;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -112,7 +112,7 @@ export class FlatFilesystemProvider
|
|||||||
);
|
);
|
||||||
if (!stream) return undefined;
|
if (!stream) return undefined;
|
||||||
|
|
||||||
return stream.getStream();
|
return stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
fsStats() {
|
fsStats() {
|
||||||
|
|||||||
@ -82,6 +82,10 @@ export class MetadataHandler {
|
|||||||
// TODO: fix eslint error
|
// TODO: fix eslint error
|
||||||
// eslint-disable-next-line no-async-promise-executor
|
// eslint-disable-next-line no-async-promise-executor
|
||||||
>(async (resolve, reject) => {
|
>(async (resolve, reject) => {
|
||||||
|
setTimeout(
|
||||||
|
() => reject(new Error("Timeout while fetching results")),
|
||||||
|
systemConfig.getMetadataTimeout(),
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
const results = await provider.search(query);
|
const results = await provider.search(query);
|
||||||
const mappedResults: InternalGameMetadataResult[] = results.map(
|
const mappedResults: InternalGameMetadataResult[] = results.map(
|
||||||
|
|||||||
@ -117,6 +117,10 @@ interface SteamAppDetailsLarge extends SteamAppDetailsSmall {
|
|||||||
filename: string;
|
filename: string;
|
||||||
ordinal: number;
|
ordinal: number;
|
||||||
}[];
|
}[];
|
||||||
|
mature_content_screenshots: {
|
||||||
|
filename: string;
|
||||||
|
ordinal: number;
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
full_description: string;
|
full_description: string;
|
||||||
}
|
}
|
||||||
@ -689,16 +693,20 @@ export class SteamProvider implements MetadataProvider {
|
|||||||
context?.progress(40);
|
context?.progress(40);
|
||||||
|
|
||||||
const images = [cover, banner];
|
const images = [cover, banner];
|
||||||
const screenshotCount = game.screenshots?.all_ages_screenshots?.length || 0;
|
|
||||||
context?.logger.info(`Processing ${screenshotCount} screenshots...`);
|
|
||||||
|
|
||||||
for (const image of game.screenshots?.all_ages_screenshots || []) {
|
const screenshots = game.screenshots?.all_ages_screenshots || [];
|
||||||
|
screenshots.push(...(game.screenshots?.mature_content_screenshots || []));
|
||||||
|
screenshots.sort((a, b) => a.ordinal - b.ordinal);
|
||||||
|
|
||||||
|
context?.logger.info(`Processing ${screenshots.length} screenshots...`);
|
||||||
|
|
||||||
|
for (const image of screenshots) {
|
||||||
const imageUrl = this._getImageUrl(image.filename);
|
const imageUrl = this._getImageUrl(image.filename);
|
||||||
images.push(createObject(imageUrl));
|
images.push(createObject(imageUrl));
|
||||||
}
|
}
|
||||||
|
|
||||||
context?.logger.info(
|
context?.logger.info(
|
||||||
`Image processing complete: icon, cover, banner and ${screenshotCount} screenshots`,
|
`Image processing complete: icon, cover, banner and ${screenshots.length} screenshots`,
|
||||||
);
|
);
|
||||||
context?.progress(50);
|
context?.progress(50);
|
||||||
|
|
||||||
|
|||||||
@ -124,7 +124,10 @@ class NewsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: string) {
|
async delete(id: string) {
|
||||||
const article = await prisma.article.delete({
|
const article = await prisma.article.findUnique({ where: { id } });
|
||||||
|
if (!article) return false;
|
||||||
|
// eslint-disable-next-line drop/no-prisma-delete
|
||||||
|
await prisma.article.delete({
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
if (article.imageObjectId) {
|
if (article.imageObjectId) {
|
||||||
|
|||||||
@ -259,16 +259,10 @@ class FsHashStore {
|
|||||||
*/
|
*/
|
||||||
async delete(id: ObjectReference) {
|
async delete(id: ObjectReference) {
|
||||||
await this.cache.remove(id);
|
await this.cache.remove(id);
|
||||||
|
await prisma.objectHash.deleteMany({
|
||||||
try {
|
where: {
|
||||||
// need to catch in case the object doesn't exist
|
id,
|
||||||
await prisma.objectHash.delete({
|
},
|
||||||
where: {
|
});
|
||||||
id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,12 +53,16 @@ class ScreenshotManager {
|
|||||||
* @param id
|
* @param id
|
||||||
*/
|
*/
|
||||||
async delete(id: string) {
|
async delete(id: string) {
|
||||||
const deletedScreenshot = await prisma.screenshot.delete({
|
const screenshot = await prisma.screenshot.findUnique({ where: { id } });
|
||||||
|
if (!screenshot) return false;
|
||||||
|
// eslint-disable-next-line drop/no-prisma-delete
|
||||||
|
await prisma.screenshot.delete({
|
||||||
where: {
|
where: {
|
||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await objectHandler.deleteAsSystem(deletedScreenshot.objectId);
|
await objectHandler.deleteAsSystem(screenshot.objectId);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -43,12 +43,12 @@ export default function createDBSessionHandler(): SessionProvider {
|
|||||||
},
|
},
|
||||||
async removeSession(token) {
|
async removeSession(token) {
|
||||||
await cache.remove(token);
|
await cache.remove(token);
|
||||||
await prisma.session.delete({
|
const { count } = await prisma.session.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
token,
|
token,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return true;
|
return count > 0;
|
||||||
},
|
},
|
||||||
async cleanupSessions() {
|
async cleanupSessions() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|||||||
@ -101,19 +101,16 @@ class UserLibraryManager {
|
|||||||
|
|
||||||
async collectionRemove(gameId: string, collectionId: string, userId: string) {
|
async collectionRemove(gameId: string, collectionId: string, userId: string) {
|
||||||
// Delete if exists
|
// Delete if exists
|
||||||
return (
|
const { count } = await prisma.collectionEntry.deleteMany({
|
||||||
(
|
where: {
|
||||||
await prisma.collectionEntry.deleteMany({
|
collectionId,
|
||||||
where: {
|
gameId,
|
||||||
collectionId,
|
collection: {
|
||||||
gameId,
|
userId,
|
||||||
collection: {
|
},
|
||||||
userId,
|
},
|
||||||
},
|
});
|
||||||
},
|
return count > 0;
|
||||||
})
|
|
||||||
).count > 0
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async collectionCreate(name: string, userId: string) {
|
async collectionCreate(name: string, userId: string) {
|
||||||
@ -133,12 +130,13 @@ class UserLibraryManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteCollection(collectionId: string) {
|
async deleteCollection(collectionId: string) {
|
||||||
await prisma.collection.delete({
|
const { count } = await prisma.collection.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
id: collectionId,
|
id: collectionId,
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
return count > 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import fs from "fs";
|
import fs from "node:fs";
|
||||||
import nodePath from "path";
|
import nodePath from "node:path";
|
||||||
|
|
||||||
export function fsStats(folderPath: string) {
|
export function fsStats(folderPath: string) {
|
||||||
const stats = fs.statfsSync(folderPath);
|
const stats = fs.statfsSync(folderPath);
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
// https://nuxt.com/docs/guide/concepts/typescript
|
// https://nuxt.com/docs/guide/concepts/typescript
|
||||||
"extends": "./.nuxt/tsconfig.json",
|
"extends": "./.nuxt/tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"exactOptionalPropertyTypes": false
|
"exactOptionalPropertyTypes": false,
|
||||||
|
"allowImportingTsExtensions": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user