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
30 changed files with 246 additions and 209 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

@ -3,16 +3,16 @@
"devices": { "devices": {
"capabilities": "Capacités", "capabilities": "Capacités",
"lastConnected": "Dernière Connexion", "lastConnected": "Dernière Connexion",
"noDevices": "Aucun appareil connecté à vôtre compte.", "noDevices": "Aucun appareil n'est connecté à vôtre compte.",
"platform": "Plateforme", "platform": "Plateforme",
"revoke": "Révoquer", "revoke": "Révoquer",
"subheader": "Gérer les appareils authorisés à accéder à votre compte Drop.", "subheader": "Gérer les appareils authorisés à accéder à votre compte Drop.",
"title": "Appareils" "title": "Appareils"
}, },
"notifications": { "notifications": {
"all": "Voir tout {arrow}", "all": "Tout voir {arrow}",
"desc": "Voir et gérer vos notifications.", "desc": "Voir et gérer vos notifications.",
"markAllAsRead": "Marquer tout comme lu", "markAllAsRead": "Tout marqué comme lu",
"markAsRead": "Marquer comme lu", "markAsRead": "Marquer comme lu",
"none": "Pas de notification", "none": "Pas de notification",
"notifications": "Notifications", "notifications": "Notifications",
@ -62,7 +62,6 @@
"description": "Utiliser un code pour vous connecter à votre client Drop si vous ne pouvez pas ouvrir un navigateur web sur votre appareil.", "description": "Utiliser un code pour vous connecter à votre client Drop si vous ne pouvez pas ouvrir un navigateur web sur votre appareil.",
"title": "Connecter votre client Drop" "title": "Connecter votre client Drop"
}, },
"confirmPassword": "Confirmez @:auth.password",
"displayName": "Nom d'Affichage", "displayName": "Nom d'Affichage",
"email": "Email", "email": "Email",
"password": "Mot de passe", "password": "Mot de passe",
@ -106,9 +105,7 @@
"friends": "Amis", "friends": "Amis",
"groups": "Groupes", "groups": "Groupes",
"insert": "Insérer", "insert": "Insérer",
"labelValueColon": "{label} : {value}",
"name": "Nom", "name": "Nom",
"noData": "Pas de donnée",
"noResults": "Pas de résultat", "noResults": "Pas de résultat",
"noSelected": "Pas d'élément sélectionné.", "noSelected": "Pas d'élément sélectionné.",
"remove": "Retirer", "remove": "Retirer",
@ -150,7 +147,6 @@
"auth": { "auth": {
"disabled": "Compte invalide or désactivé. Merci de contacter l'administrateur du serveur.", "disabled": "Compte invalide or désactivé. Merci de contacter l'administrateur du serveur.",
"invalidInvite": "Invitation invalide ou expirée", "invalidInvite": "Invitation invalide ou expirée",
"invalidPassState": "Le mot de passe enregistré est invalide. Merci de contacter l'administrateur du serveur.",
"invalidUserOrPass": "Nom d'utilisateur ou password invalide.", "invalidUserOrPass": "Nom d'utilisateur ou password invalide.",
"inviteIdRequired": "id est requis pour récupérer l'invitation", "inviteIdRequired": "id est requis pour récupérer l'invitation",
"method": { "method": {
@ -159,10 +155,6 @@
"usernameTaken": "Nom d'utilisateur déjà pris." "usernameTaken": "Nom d'utilisateur déjà pris."
}, },
"backHome": "{arrow} Retour a l'accueil", "backHome": "{arrow} Retour a l'accueil",
"externalUrl": {
"subtitle": "Ce message n'est visible qu'aux administrateurs.",
"title": "Accès via une EXTERNAL_URL différente. Veuillez consulter la documentation."
},
"game": { "game": {
"banner": { "banner": {
"description": "Drop a échoué a mettre à jour l'image de la bannière : {0}", "description": "Drop a échoué a mettre à jour l'image de la bannière : {0}",
@ -223,7 +215,6 @@
"revokeClient": "Échec de la révocation du client", "revokeClient": "Échec de la révocation du client",
"revokeClientFull": "Échec de la revocation du client {0}", "revokeClientFull": "Échec de la revocation du client {0}",
"signIn": "Se connecter {arrow}", "signIn": "Se connecter {arrow}",
"support": "Assistance Discord",
"unknown": "Une erreur inconnue est survenue", "unknown": "Une erreur inconnue est survenue",
"upload": { "upload": {
"description": "Drop n'a pas pu uploader le fichier : {0}", "description": "Drop n'a pas pu uploader le fichier : {0}",
@ -262,14 +253,8 @@
"header": { "header": {
"admin": { "admin": {
"admin": "Administration", "admin": "Administration",
"home": "Accueil",
"library": "Bibliothèque",
"metadata": "Méta", "metadata": "Méta",
"settings": { "settings": "Paramètres",
"store": "Store",
"title": "Paramètres",
"tokens": "API tokens"
},
"tasks": "Tâches", "tasks": "Tâches",
"users": "Utilisateurs" "users": "Utilisateurs"
}, },
@ -278,24 +263,7 @@
}, },
"helpUsTranslate": "Aidez nous à traduire Drop {arrow}", "helpUsTranslate": "Aidez nous à traduire Drop {arrow}",
"highest": "le plus haut", "highest": "le plus haut",
"home": { "home": "Accueil",
"admin": {
"activeInactiveUsers": "Utilisateurs actifs/inactifs",
"activeUsers": "Utilisateurs actifs",
"allVersionsCombined": "Toutes les versions combinées",
"biggestGamesOnServer": "Les plus gros jeux sur le serveur",
"biggestGamesToDownload": "Les plus gros jeux à télécharger",
"games": "Jeux",
"goToUsers": "Aller aux utilisateurs",
"inactiveUsers": "Utilisateurs inactifs",
"latestVersionOnly": "Dernière version seulement",
"librarySources": "Sources de bibliothèques",
"subheader": "Résumé de l'instance",
"title": "Accueil",
"users": "Utilisateurs",
"version": "Version"
}
},
"library": { "library": {
"addGames": "Tous les jeux", "addGames": "Tous les jeux",
"addToLib": "Ajouter à la bibliothèque", "addToLib": "Ajouter à la bibliothèque",
@ -311,7 +279,6 @@
"deleteImage": "Supprimer l'image", "deleteImage": "Supprimer l'image",
"editGameDescription": "Description du jeu", "editGameDescription": "Description du jeu",
"editGameName": "Nom du jeu", "editGameName": "Nom du jeu",
"editReleaseDate": "Date de sortie",
"imageCarousel": "Carrousel d'images", "imageCarousel": "Carrousel d'images",
"imageCarouselDescription": "Personnaliser quelles images et dans quel ordre elles sont affichées sur la page du Store.", "imageCarouselDescription": "Personnaliser quelles images et dans quel ordre elles sont affichées sur la page du Store.",
"imageCarouselEmpty": "Aucune image n'a encore été ajoutée au carousel.", "imageCarouselEmpty": "Aucune image n'a encore été ajoutée au carousel.",
@ -360,8 +327,6 @@
}, },
"withoutMetadata": "Importer sans les données méta" "withoutMetadata": "Importer sans les données méta"
}, },
"libraryHint": "Pas de bibliothèque configurée.",
"libraryHintDocsLink": "Qu'est-ce que cela veut dire ? {arrow}",
"metadata": { "metadata": {
"companies": { "companies": {
"action": "Gérer {arrow}", "action": "Gérer {arrow}",
@ -375,25 +340,15 @@
"description": "Les sociétés organisent les jeux par qui les a développer ou éditer.", "description": "Les sociétés organisent les jeux par qui les a développer ou éditer.",
"editor": { "editor": {
"action": "Ajouter un jeu {plus}", "action": "Ajouter un jeu {plus}",
"descriptionPlaceholder": "{'<'}description{'>'}",
"developed": "Développé", "developed": "Développé",
"libraryDescription": "Ajouter, supprimer ou personnaliser ce que cette société a développé et/ou publié.", "libraryDescription": "Ajouter, supprimer ou personnaliser ce que cette société a développé et/ou publié.",
"libraryTitle": "Bibliothèque de jeux", "libraryTitle": "Bibliothèque de jeux",
"noDescription": "(pas de description)", "noDescription": "(pas de description)",
"published": "Publié", "published": "Publié",
"uploadBanner": "Uploader bannière", "uploadBanner": "Uploader bannière",
"uploadIcon": "Uplader icône", "uploadIcon": "Uplader icône"
"websitePlaceholder": "{'<'}site web{'>'}"
}, },
"modals": { "modals": {
"createDescription": "Créez une société pour mieux organizer vos jeux.",
"createFieldDescription": "Description de la Société",
"createFieldDescriptionPlaceholder": "Un petit studio indépendant qui...",
"createFieldName": "Nom de la société",
"createFieldNamePlaceholder": "Ma nouvelle société...",
"createFieldWebsite": "Site web de la société",
"createFieldWebsitePlaceholder": "https://exemple com/",
"createTitle": "Créer une société",
"nameDescription": "Éditer le nom de la société. Ce nom est utilisé pour trouver les jeux nouvellement importés.", "nameDescription": "Éditer le nom de la société. Ce nom est utilisé pour trouver les jeux nouvellement importés.",
"nameTitle": "Éditer le nom de la société", "nameTitle": "Éditer le nom de la société",
"shortDeckDescription": "Éditer la description de la company. Cela n'affecte pas la description longue (markdown).", "shortDeckDescription": "Éditer la description de la company. Cela n'affecte pas la description longue (markdown).",
@ -429,24 +384,17 @@
"create": "Créer une source", "create": "Créer une source",
"createDesc": "Drop va utiliser cette source pour accéder à votre bibliothèque de jeux, et les rendre disponible.", "createDesc": "Drop va utiliser cette source pour accéder à votre bibliothèque de jeux, et les rendre disponible.",
"desc": "Configurer vos sources de bibliothèques où Drop va regarder pour les nouveaux jeux et versions à importer.", "desc": "Configurer vos sources de bibliothèques où Drop va regarder pour les nouveaux jeux et versions à importer.",
"documentationLink": "Documentation {arrow}",
"edit": "Éditer la source", "edit": "Éditer la source",
"freeSpace": "Espace disponible",
"fsDesc": "Importe les jeux à partir d'un chemin d'accès sur le disque. Cela requière une structure des dossiers basées sur la version, et qui supporte les jeux archivés.", "fsDesc": "Importe les jeux à partir d'un chemin d'accès sur le disque. Cela requière une structure des dossiers basées sur la version, et qui supporte les jeux archivés.",
"fsFlatDesc": "Importe les jeux à partir d'un chemin daccès sur le disque, mais sans le sous-dossier version séparé. Utile pour migrer une bibliothèque vers Drop.", "fsFlatDesc": "Importe les jeux à partir d'un chemin daccès sur le disque, mais sans le sous-dossier version séparé. Utile pour migrer une bibliothèque vers Drop.",
"fsFlatTitle": "Compatibilité",
"fsPath": "Chemin daccès", "fsPath": "Chemin daccès",
"fsPathDesc": "Un chemin daccès absolu à votre bibliothèque de jeux.", "fsPathDesc": "Un chemin daccès absolu à votre bibliothèque de jeux.",
"fsPathPlaceholder": "/mnt/jeux", "fsPathPlaceholder": "/mnt/jeux",
"fsTitle": "Drop-style",
"link": "Sources {arrow}", "link": "Sources {arrow}",
"nameDesc": "Le nom de votre source, pour référence.", "nameDesc": "Le nom de votre source, pour référence.",
"namePlaceholder": "Mes Nouvelle Source", "namePlaceholder": "Mes Nouvelle Source",
"percentage": "{number}%",
"sources": "Sources de Bibliothèques", "sources": "Sources de Bibliothèques",
"totalSpace": "Espace total",
"typeDesc": "Le type de source. Affecte les options requises.", "typeDesc": "Le type de source. Affecte les options requises.",
"utilizationPercentage": "Pourcentage d'utilisation",
"working": "Marche ?" "working": "Marche ?"
}, },
"subheader": "Lorsque que vous rajoutez des dossiers à vos sources de bibliothèques, Drop le détectera et vous demandera de les importer. Chaque jeu a besoin dêtre importé avant que vous puissiez importer une version.", "subheader": "Lorsque que vous rajoutez des dossiers à vos sources de bibliothèques, Drop le détectera et vous demandera de les importer. Chaque jeu a besoin dêtre importé avant que vous puissiez importer une version.",
@ -499,7 +447,6 @@
"checkLater": "Vérifier plus tard pour les mises à jour.", "checkLater": "Vérifier plus tard pour les mises à jour.",
"delete": "Supprimer l'Article", "delete": "Supprimer l'Article",
"filter": { "filter": {
"all": "Tous les temps",
"month": "Ce mois", "month": "Ce mois",
"week": "Cette semaine", "week": "Cette semaine",
"year": "Cette année" "year": "Cette année"
@ -562,19 +509,15 @@
"store": { "store": {
"about": "À propos", "about": "À propos",
"commingSoon": "prochainement", "commingSoon": "prochainement",
"developers": "Développeurs | Développeur | Développeurs",
"exploreMore": "Explorer plus {arrow}", "exploreMore": "Explorer plus {arrow}",
"featured": "Mis en avant", "featured": "Mis en avant",
"images": "Images de Jeux", "images": "Images de Jeux",
"lookAt": "Découvrez le maintenant",
"noDevelopers": "Pas de développeur", "noDevelopers": "Pas de développeur",
"noFeatured": "PAS DE JEU MIS EN AVANT", "noGame": "pas de jeu",
"noGame": "PAS DE JEU",
"noImages": "Pas d'image", "noImages": "Pas d'image",
"noPublishers": "Pas d'éditeur.", "noPublishers": "Pas d'éditeur.",
"noTags": "Pas de tag", "noTags": "Pas de tag",
"openAdminDashboard": "Ouvrir dans le Tableau de Bord d'Administration", "openAdminDashboard": "Ouvrir dans le Tableau de Bord d'Administration",
"openFeatured": "Mettez des étoiles aux jeux dans l'administration de la bibliothèque {arrow}",
"platform": "Plateforme | Plateforme | Plateformes", "platform": "Plateforme | Plateforme | Plateformes",
"publishers": "Éditeurs | Éditeur | Éditeurs", "publishers": "Éditeurs | Éditeur | Éditeurs",
"rating": "Note", "rating": "Note",
@ -585,7 +528,6 @@
"recentlyUpdated": "Récemment Mis à Jour", "recentlyUpdated": "Récemment Mis à Jour",
"released": "Publié", "released": "Publié",
"reviews": "({0} Avis)", "reviews": "({0} Avis)",
"size": "Taille",
"tags": "Tags", "tags": "Tags",
"title": "Store", "title": "Store",
"view": { "view": {
@ -602,9 +544,7 @@
"back": "{arrow} Retour aux Tâches", "back": "{arrow} Retour aux Tâches",
"completedTasksTitle": "Tâches complétées", "completedTasksTitle": "Tâches complétées",
"dailyScheduledTitle": "Tâches quotidiennes planifiées", "dailyScheduledTitle": "Tâches quotidiennes planifiées",
"execute": "{arrow} Exécuter",
"noTasksRunning": "Pas de tâche en cours", "noTasksRunning": "Pas de tâche en cours",
"progress": "{0]%",
"runningTasksTitle": "Tâches en cours d'exécution", "runningTasksTitle": "Tâches en cours d'exécution",
"scheduled": { "scheduled": {
"checkUpdateDescription": "Vérifier si Drop a une mise à jour.", "checkUpdateDescription": "Vérifier si Drop a une mise à jour.",
@ -648,7 +588,6 @@
"description": "Drop supporte une variété de \"mécanismes d'authentification\". Lorsque vous les activez ou les désactivez, ils sont affichés sur la page de connection pour que les utilisateurs puissent les sélectionner. Cliquer sur le menu à points pour configurer le mécanisme d'authentification.", "description": "Drop supporte une variété de \"mécanismes d'authentification\". Lorsque vous les activez ou les désactivez, ils sont affichés sur la page de connection pour que les utilisateurs puissent les sélectionner. Cliquer sur le menu à points pour configurer le mécanisme d'authentification.",
"disabled": "Désactivé", "disabled": "Désactivé",
"enabled": "Activé", "enabled": "Activé",
"enabledKey": "Activée ?",
"oidc": "OpenID Connect", "oidc": "OpenID Connect",
"simple": "Simple (nom d'utilisateur/mot de passe)", "simple": "Simple (nom d'utilisateur/mot de passe)",
"srOpenOptions": "Ouvrir les options", "srOpenOptions": "Ouvrir les options",
@ -666,7 +605,7 @@
"createInvitation": "Créer invitation", "createInvitation": "Créer invitation",
"description": "L'authentification simple utilise un système d'invitations pour créer les utilisateurs. Tu peux créer une invitation et optionnellement spécifier le nom d'utilisateur ou email de cet utilisateur, et un lien magique sera généré un lien magique qui peut être utilisé pour créer le compte.", "description": "L'authentification simple utilise un système d'invitations pour créer les utilisateurs. Tu peux créer une invitation et optionnellement spécifier le nom d'utilisateur ou email de cet utilisateur, et un lien magique sera généré un lien magique qui peut être utilisé pour créer le compte.",
"expires": "Expire : {expiry}", "expires": "Expire : {expiry}",
"invitationTitle": "Invitations", "invitationTitle": "invitations",
"invite3Days": "3 jours", "invite3Days": "3 jours",
"invite6Months": "6 mois", "invite6Months": "6 mois",
"inviteAdminSwitchDescription": "Créer cet utilisateur en tant qu'adminstrateur", "inviteAdminSwitchDescription": "Créer cet utilisateur en tant qu'adminstrateur",

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,12 +1,15 @@
<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>
<div class="inline-flex gap-x-2">
<button <button
:disabled="notifications.length === 0" :disabled="notifications.length === 0"
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" 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"
@ -15,6 +18,15 @@
<CheckIcon class="size-4" /> <CheckIcon class="size-4" />
{{ $t("account.notifications.markAllAsRead") }} {{ $t("account.notifications.markAllAsRead") }}
</button> </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",
});
notifications.value.forEach((notification) => {
notification.read = true; 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,25 +378,23 @@ 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,
}, },
},
}); });
await gameSizeManager.deleteGameVersion(gameId, version); await gameSizeManager.deleteGameVersion(gameId, version);
} }
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 {
// need to catch in case the object doesn't exist
await prisma.objectHash.delete({
where: { where: {
id, 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,9 +101,7 @@ 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({
(
await prisma.collectionEntry.deleteMany({
where: { where: {
collectionId, collectionId,
gameId, gameId,
@ -111,9 +109,8 @@ class UserLibraryManager {
userId, userId,
}, },
}, },
}) });
).count > 0 return 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
} }
} }