9 Commits

Author SHA1 Message Date
c0b69048cf fix: light mode style fixes 2025-11-22 11:44:32 +11:00
1e7ed34a60 fix: lint 2025-11-22 10:46:03 +11:00
e230f79b54 feat: delete all notifications 2025-11-22 10:28:21 +11:00
973c3efa18 Merge branch 'develop' into small-fixes 2025-11-21 23:21:11 +11:00
bcb88f8069 fix: type errors 2025-11-21 23:20:53 +11:00
b842d78b94 fix: oidc scopes override 2025-11-21 23:18:24 +11:00
b0bf1a2795 fix: bump droplet 2025-11-21 23:08:49 +11:00
2d165bf997 fix: typescript for lint 2025-11-21 23:07:50 +11:00
650a3ca98d fix: add no-prisma-delete lint 2025-11-21 23:04:00 +11:00
29 changed files with 238 additions and 140 deletions

View File

@ -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`, {

View File

@ -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>

View File

@ -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);
}); });

View File

@ -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 } },
},
}, },
]); ]);

View File

@ -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",

View File

@ -21,7 +21,7 @@
}, },
"dependencies": { "dependencies": {
"@discordapp/twemoji": "^16.0.1", "@discordapp/twemoji": "^16.0.1",
"@drop-oss/droplet": "3.4.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",

View File

@ -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

View File

@ -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"

90
pnpm-lock.yaml generated
View File

@ -15,8 +15,8 @@ importers:
specifier: ^16.0.1 specifier: ^16.0.1
version: 16.0.1 version: 16.0.1
'@drop-oss/droplet': '@drop-oss/droplet':
specifier: 3.4.0 specifier: 3.5.0
version: 3.4.0 version: 3.5.0
'@headlessui/vue': '@headlessui/vue':
specifier: ^1.7.23 specifier: ^1.7.23
version: 1.7.23(vue@3.5.22(typescript@5.8.3)) version: 1.7.23(vue@3.5.22(typescript@5.8.3))
@ -408,67 +408,67 @@ packages:
'@discordapp/twemoji@16.0.1': '@discordapp/twemoji@16.0.1':
resolution: {integrity: sha512-figLiBWzjS5cyrAjLaGjM8AAaowO3qvK8rg5bA2dElB4qsaPMvBVlFDMO2d3x+nC1igt7kgWH4dvNmvvUHUF8w==} resolution: {integrity: sha512-figLiBWzjS5cyrAjLaGjM8AAaowO3qvK8rg5bA2dElB4qsaPMvBVlFDMO2d3x+nC1igt7kgWH4dvNmvvUHUF8w==}
'@drop-oss/droplet-darwin-arm64@3.4.0': '@drop-oss/droplet-darwin-arm64@3.5.0':
resolution: {integrity: sha512-pwyiCSq0lMpr55J5xUys87c1Ih87WjCrxK1MbOUG2EjtwwdOx/KcB2AcmT4FWi7QaKOf+1QdNoBkOWow15uRTQ==} resolution: {integrity: sha512-tEznf8ZftvIpgpBpWom43leUBLlvGzZE3pGt1cZcUZ8KPQySD/n5qqhPbP9qTdYgbobHjF/0VLFKlSKI90iMJA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@drop-oss/droplet-darwin-universal@3.4.0': '@drop-oss/droplet-darwin-universal@3.5.0':
resolution: {integrity: sha512-OCSdsX1gV0108IoGWxQ0GmkhOHkPsdteHb/QGTYVQlu0niJTGy6BHYCR6RPyq0T+xgY10zTeZDis3UFC/aslhQ==} resolution: {integrity: sha512-FSjTKKUL0+eM1DWxFW969n3kbV6yNFjPU/F1NLwXL9xNoEKyN/A2tJOdSYvlHZNR6IaGL2O1QfBB4L6raADV+A==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
os: [darwin] os: [darwin]
'@drop-oss/droplet-darwin-x64@3.4.0': '@drop-oss/droplet-darwin-x64@3.5.0':
resolution: {integrity: sha512-AtlKKYtq3iikVi8cqywnrW5FZN90rlt4SzU9Eq/zhPCD+4qIlOOyfA8hX5Wj+qBoCexqWJfnwrkq3Ne+8t+f7A==} resolution: {integrity: sha512-Sgy/UyM7NRWdJY2lpNo1sD0iYx1fPaEQZTgGREXZPNPUkG2uVSlqcl8rsdopFI9ZFc7GD/aSGgXNy+jWhOi0DQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@drop-oss/droplet-linux-arm64-gnu@3.4.0': '@drop-oss/droplet-linux-arm64-gnu@3.5.0':
resolution: {integrity: sha512-oQMRTlzFW3TE/V7jWHFPYSA9UDSyqBJ1o9xWUg6ScJwkDy4vaGD9gdlndyuoQeD+ninM4AHYSrE94t8V7ptKFw==} resolution: {integrity: sha512-+eP8W9Hea6koV3XyotBN/iUrmRu9zb9QIHujdDpxmmkp511sKoWEIjKqV+/sC9cR3J3OGILureaXjb39k35nfg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@drop-oss/droplet-linux-arm64-musl@3.4.0': '@drop-oss/droplet-linux-arm64-musl@3.5.0':
resolution: {integrity: sha512-DmhTqoxvzggB451wdqGMROvvTGMcyPz3xWc9XT5O/ibJ2EYRYH6D15A0CBGxWjr1TmxMrXrRuU4K+q8lJfq62w==} resolution: {integrity: sha512-zEHEm9PdXncxlAJkafLn8yykpGOr+AfDsjhzTH7yVxBVGl0U8L31nS2BuxKposLD6gKIuzRpFj4mQ+AtOIn+XA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@drop-oss/droplet-linux-riscv64-gnu@3.4.0': '@drop-oss/droplet-linux-riscv64-gnu@3.5.0':
resolution: {integrity: sha512-LbzT3z//Q03KJfLeRprjb7ICiAzBv+mNlrKKlZ1Zisjzgnqk7kx32sz5dStGDL+P+26yUt6inLZBhS56cWIyvA==} resolution: {integrity: sha512-SBo5A02oQ/qBWgVrSoE82Lbi5neS6CwlNKEKahG1dmkId/ZPQ9vMxwo5Cdgq3Oa4Lyo9l3RtRQWYFnw6HdG/rw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
'@drop-oss/droplet-linux-x64-gnu@3.4.0': '@drop-oss/droplet-linux-x64-gnu@3.5.0':
resolution: {integrity: sha512-U9BIyHxmtDIobyReuaXanYQ/TJOsRGQJ4WlXk+amZbMeAp8dyVawP0pmXbQO/hny/+VlxtAlObeCZ5T3nK+etQ==} resolution: {integrity: sha512-ddJv4UqzVr3GS7W6T9pcKjsY3qv+B+ahdPKP6cIwfL5EMLrKKfFBE7+pbXWEbE0t4q7Qhmj8GxSliCHA5TgCOg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@drop-oss/droplet-linux-x64-musl@3.4.0': '@drop-oss/droplet-linux-x64-musl@3.5.0':
resolution: {integrity: sha512-TQtLGSk6M2fu9K85rS02Yv3Etb5mk77tGIDLY8KAl/fLJOWRxuRfwtTnlIA5IuNnsO/3rOmwCOvpF6tVI010mA==} resolution: {integrity: sha512-pDc85qzA4UHvQopnA8nRFg20spDi4gL4yCwlYllJfoDUmXThPIHSQnQ/DurLPwqvJTURwkrfJboBQ93Z+Hnr9A==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@drop-oss/droplet-win32-arm64-msvc@3.4.0': '@drop-oss/droplet-win32-arm64-msvc@3.5.0':
resolution: {integrity: sha512-z0NTFVefyXgFeeH8NHdM54PFxPctcLDWiQ/UZzsTMhahXj+yQwsAiM2q41GSeBIEBkR9r1IEuqMRhgXeCfyBLg==} resolution: {integrity: sha512-ZEYCBhRD5VMrbG6p0TFj/TUUskZxQOf0plFFkSqYxeK2BZkwh2cxEw2y977BPo0pfzV5QxbASzXo6I17cJIztg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@drop-oss/droplet-win32-x64-msvc@3.4.0': '@drop-oss/droplet-win32-x64-msvc@3.5.0':
resolution: {integrity: sha512-NNbIchHnnwcAPtmwBAY3Y2glhFjndGg+FO0MjvajQIf5ts+b+ss5SSEjDDRw/KkPjlQAxFymRAhBbP7ObwdWQg==} resolution: {integrity: sha512-WloxGl6hJb2mn1N2TFQrrDEmjppDGHLUwegP/6M4FKT63i/SxhIoenCsL2e7qWdhEC7XZZYtl18e6iLt80cl3g==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@drop-oss/droplet@3.4.0': '@drop-oss/droplet@3.5.0':
resolution: {integrity: sha512-hGNoSBUg/tWxYESqHvV5AXBIcra9LXvkFg7WyCdRK9o1xhwTjAOmCaoivG1eSpjXEbWsFB69aI0sdvH/4nHzKg==} resolution: {integrity: sha512-FaTwGRl9uWdA1aw/WnbZfyZ7W/b2nEPdBLkdauonQ8OPKqK0k8KgolgyZ87yPFWw0BPyzUyCz4imrmdIIKhFYw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
'@dxup/nuxt@0.2.2': '@dxup/nuxt@0.2.2':
@ -7110,48 +7110,48 @@ snapshots:
jsonfile: 5.0.0 jsonfile: 5.0.0
universalify: 0.1.2 universalify: 0.1.2
'@drop-oss/droplet-darwin-arm64@3.4.0': '@drop-oss/droplet-darwin-arm64@3.5.0':
optional: true optional: true
'@drop-oss/droplet-darwin-universal@3.4.0': '@drop-oss/droplet-darwin-universal@3.5.0':
optional: true optional: true
'@drop-oss/droplet-darwin-x64@3.4.0': '@drop-oss/droplet-darwin-x64@3.5.0':
optional: true optional: true
'@drop-oss/droplet-linux-arm64-gnu@3.4.0': '@drop-oss/droplet-linux-arm64-gnu@3.5.0':
optional: true optional: true
'@drop-oss/droplet-linux-arm64-musl@3.4.0': '@drop-oss/droplet-linux-arm64-musl@3.5.0':
optional: true optional: true
'@drop-oss/droplet-linux-riscv64-gnu@3.4.0': '@drop-oss/droplet-linux-riscv64-gnu@3.5.0':
optional: true optional: true
'@drop-oss/droplet-linux-x64-gnu@3.4.0': '@drop-oss/droplet-linux-x64-gnu@3.5.0':
optional: true optional: true
'@drop-oss/droplet-linux-x64-musl@3.4.0': '@drop-oss/droplet-linux-x64-musl@3.5.0':
optional: true optional: true
'@drop-oss/droplet-win32-arm64-msvc@3.4.0': '@drop-oss/droplet-win32-arm64-msvc@3.5.0':
optional: true optional: true
'@drop-oss/droplet-win32-x64-msvc@3.4.0': '@drop-oss/droplet-win32-x64-msvc@3.5.0':
optional: true optional: true
'@drop-oss/droplet@3.4.0': '@drop-oss/droplet@3.5.0':
optionalDependencies: optionalDependencies:
'@drop-oss/droplet-darwin-arm64': 3.4.0 '@drop-oss/droplet-darwin-arm64': 3.5.0
'@drop-oss/droplet-darwin-universal': 3.4.0 '@drop-oss/droplet-darwin-universal': 3.5.0
'@drop-oss/droplet-darwin-x64': 3.4.0 '@drop-oss/droplet-darwin-x64': 3.5.0
'@drop-oss/droplet-linux-arm64-gnu': 3.4.0 '@drop-oss/droplet-linux-arm64-gnu': 3.5.0
'@drop-oss/droplet-linux-arm64-musl': 3.4.0 '@drop-oss/droplet-linux-arm64-musl': 3.5.0
'@drop-oss/droplet-linux-riscv64-gnu': 3.4.0 '@drop-oss/droplet-linux-riscv64-gnu': 3.5.0
'@drop-oss/droplet-linux-x64-gnu': 3.4.0 '@drop-oss/droplet-linux-x64-gnu': 3.5.0
'@drop-oss/droplet-linux-x64-musl': 3.4.0 '@drop-oss/droplet-linux-x64-musl': 3.5.0
'@drop-oss/droplet-win32-arm64-msvc': 3.4.0 '@drop-oss/droplet-win32-arm64-msvc': 3.5.0
'@drop-oss/droplet-win32-x64-msvc': 3.4.0 '@drop-oss/droplet-win32-x64-msvc': 3.5.0
'@dxup/nuxt@0.2.2(magicast@0.5.1)': '@dxup/nuxt@0.2.2(magicast@0.5.1)':
dependencies: dependencies:

View 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">;

View File

@ -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 {};
}); });

View File

@ -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 {};
}); });

View File

@ -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);
}, },

View File

@ -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;

View File

@ -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 };

View File

@ -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();

View File

@ -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" });
}, },
); );

View File

@ -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",

View 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;
});

View File

@ -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;

View File

@ -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 ||

View File

@ -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;
} }
} }

View File

@ -378,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,
},
}, },
}); });
@ -391,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(

View File

@ -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) {

View File

@ -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 */
}
} }
} }

View File

@ -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;
} }
/** /**

View File

@ -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();

View File

@ -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;
} }
} }

View File

@ -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
} }
} }