feat: user page & $dropFetch util

This commit is contained in:
DecDuck
2025-03-14 12:22:08 +11:00
parent 3225f536ce
commit bd1cb67cd0
39 changed files with 416 additions and 166 deletions

View File

@ -7,15 +7,24 @@
<div
class="relative flex h-full flex-col overflow-hidden rounded-[calc(var(--radius-lg)+1px)] lg:rounded-l-[calc(2rem+1px)]"
>
<div class="px-8 pt-8 pb-3 sm:px-10 sm:pt-10 sm:pb-0">
<div class="px-8 pt-8 pb-3 sm:px-10 sm:py-10">
<p
class="mt-2 text-lg font-medium tracking-tight text-zinc-100 max-lg:text-center"
>
Library
</p>
<p class="mt-2 max-w-lg text-sm/6 text-zinc-400 max-lg:text-center">
Anim aute id magna aliqua ad ad non deserunt sunt. Qui irure qui
lorem cupidatat commodo.
Manage your Drop library, and import new games. Your library is the
list of all games currently configured on this instance.
</p>
<p class="mt-3 text-sm">
<NuxtLink
href="/admin/library"
class="whitespace-nowrap font-medium text-blue-400 hover:text-blue-500"
>
Check it out
<span aria-hidden="true"> &rarr;</span>
</NuxtLink>
</p>
<div
@ -45,31 +54,6 @@
</div>
</div>
</div>
<dl
class="mt-4 grid max-w-xl grid-cols-1 gap-8 sm:grid-cols-2 xl:mt-8"
>
<div class="flex flex-col gap-y-3 border-l border-zinc-100/10 pl-6">
<dt class="text-sm/6 text-zinc-400">Games</dt>
<dd
class="order-first text-3xl font-semibold tracking-tight text-zinc-100"
>
{{ libraryState.games.length }}
</dd>
</div>
<div class="flex flex-col gap-y-3 border-l border-zinc-100/10 pl-6">
<dt class="text-sm/6 text-zinc-400">Versions</dt>
<dd
class="order-first text-3xl font-semibold tracking-tight text-zinc-100"
>
{{
libraryState.games
.map((e) => e.game.versions.length)
.reduce((a, b) => a + b, 0)
}}
</dd>
</div>
</dl>
</div>
</div>
<div
@ -78,30 +62,30 @@
</div>
<div class="relative max-lg:row-start-1">
<div
class="absolute inset-px rounded-lg bg-white max-lg:rounded-t-[2rem]"
class="absolute inset-px rounded-lg bg-zinc-950 max-lg:rounded-t-[2rem]"
/>
<div
class="relative flex h-full flex-col overflow-hidden rounded-[calc(var(--radius-lg)+1px)] max-lg:rounded-t-[calc(2rem+1px)]"
>
<div class="px-8 pt-8 sm:px-10 sm:pt-10">
<div class="px-8 py-8 sm:px-10 sm:py-10">
<p
class="mt-2 text-lg font-medium tracking-tight text-gray-950 max-lg:text-center"
class="mt-2 text-lg font-medium tracking-tight text-zinc-100 max-lg:text-center"
>
Performance
Users
</p>
<p class="mt-2 max-w-lg text-sm/6 text-gray-600 max-lg:text-center">
Lorem ipsum, dolor sit amet consectetur adipisicing elit maiores
impedit.
<p class="mt-2 max-w-lg text-sm/6 text-zinc-400 max-lg:text-center">
Your users are people who can access your Drop instance, download
games from it, and configure API keys for it.
</p>
<p class="mt-3 text-sm">
<NuxtLink
href="/admin/users"
class="whitespace-nowrap font-medium text-blue-400 hover:text-blue-500"
>
Check it out
<span aria-hidden="true"> &rarr;</span>
</NuxtLink>
</p>
</div>
<div
class="flex flex-1 items-center justify-center px-8 max-lg:pt-10 max-lg:pb-12 sm:px-10 lg:pb-2"
>
<img
class="w-full max-lg:max-w-xs"
src="https://tailwindcss.com/plus-assets/img/component-images/bento-03-performance.png"
alt=""
/>
</div>
</div>
<div
@ -193,5 +177,5 @@ useHead({
});
const headers = useRequestHeaders(["cookie"]);
const libraryState = await $fetch("/api/v1/admin/library", { headers });
const libraryState = await $dropFetch("/api/v1/admin/library", { headers });
</script>

View File

@ -165,7 +165,7 @@
'relative cursor-default select-none py-2 pl-3 pr-9',
active
? 'bg-blue-600 text-white outline-none'
: 'text-gray-900',
: 'text-zinc-100',
]"
>
<span
@ -321,7 +321,7 @@
'relative cursor-default select-none py-2 pl-3 pr-9',
active
? 'bg-blue-600 text-white outline-none'
: 'text-gray-900',
: 'text-zinc-100',
]"
>
<span
@ -553,7 +553,7 @@ const router = useRouter();
const route = useRoute();
const headers = useRequestHeaders(["cookie"]);
const gameId = route.params.id.toString();
const versions = await $fetch(
const versions = await $dropFetch(
`/api/v1/admin/import/version?id=${encodeURIComponent(gameId)}`,
{
headers,
@ -561,7 +561,7 @@ const versions = await $fetch(
);
const currentlySelectedVersion = ref(-1);
const versionSettings = ref<{
platform: string;
platform: PlatformClient | undefined;
onlySetup: boolean;
launch: string;
@ -572,7 +572,7 @@ const versionSettings = ref<{
delta: boolean;
umuId: string;
}>({
platform: "",
platform: undefined,
launch: "",
launchArgs: "",
setup: "",
@ -582,7 +582,8 @@ const versionSettings = ref<{
umuId: "",
});
const versionGuesses = ref<Array<{ platform: string; filename: string }>>();
const versionGuesses =
ref<Array<{ platform: PlatformClient; filename: string }>>();
const launchProcessQuery = ref("");
const setupProcessQuery = ref("");
@ -637,17 +638,20 @@ async function updateCurrentlySelectedVersion(value: number) {
if (currentlySelectedVersion.value == value) return;
currentlySelectedVersion.value = value;
const version = versions[currentlySelectedVersion.value];
const results = await $fetch(
const results = await $dropFetch(
`/api/v1/admin/import/version/preload?id=${encodeURIComponent(
gameId
)}&version=${encodeURIComponent(version)}`
);
versionGuesses.value = results;
versionGuesses.value = results.map((e) => ({
...e,
platform: e.platform as PlatformClient,
}));
}
async function startImport() {
if (!versionSettings.value) return;
const taskId = await $fetch("/api/v1/admin/import/version", {
const taskId = await $dropFetch("/api/v1/admin/import/version", {
method: "POST",
body: {
id: gameId,

View File

@ -530,7 +530,7 @@ const mobileShowFinalDescription = ref(true);
const route = useRoute();
const gameId = route.params.id.toString();
const headers = useRequestHeaders(["cookie"]);
const { game: rawGame, unimportedVersions } = await $fetch(
const { game: rawGame, unimportedVersions } = await $dropFetch(
`/api/v1/admin/game?id=${encodeURIComponent(gameId)}`,
{
headers,
@ -579,7 +579,7 @@ async function coreMetadataUpdate() {
formData.append("name", coreMetadataName.value);
formData.append("description", coreMetadataDescription.value);
const result = await $fetch(`/api/v1/admin/game/metadata`, {
const result = await $dropFetch(`/api/v1/admin/game/metadata`, {
method: "POST",
body: formData,
});
@ -630,7 +630,7 @@ watch(descriptionHTML, (v) => {
savingTimeout = setTimeout(async () => {
try {
descriptionSaving.value = 2;
await $fetch("/api/v1/admin/game", {
await $dropFetch("/api/v1/admin/game", {
method: "PATCH",
body: {
id: gameId,
@ -672,7 +672,7 @@ function insertImageAtCursor(id: string) {
async function updateBannerImage(id: string) {
try {
if (game.value.mBannerId == id) return;
const { mBannerId } = await $fetch("/api/v1/admin/game", {
const { mBannerId } = await $dropFetch("/api/v1/admin/game", {
method: "PATCH",
body: {
id: gameId,
@ -698,7 +698,7 @@ async function updateBannerImage(id: string) {
async function updateCoverImage(id: string) {
try {
if (game.value.mCoverId == id) return;
const { mCoverId } = await $fetch("/api/v1/admin/game", {
const { mCoverId } = await $dropFetch("/api/v1/admin/game", {
method: "PATCH",
body: {
id: gameId,
@ -723,7 +723,7 @@ async function updateCoverImage(id: string) {
async function deleteImage(id: string) {
try {
const { mBannerId, mImageLibrary } = await $fetch(
const { mBannerId, mImageLibrary } = await $dropFetch(
"/api/v1/admin/game/image",
{
method: "DELETE",
@ -757,7 +757,7 @@ async function uploadAfterImageUpload(result: Game) {
async function deleteVersion(versionName: string) {
try {
await $fetch("/api/v1/admin/game/version", {
await $dropFetch("/api/v1/admin/game/version", {
method: "DELETE",
body: {
id: gameId,
@ -785,7 +785,7 @@ async function deleteVersion(versionName: string) {
async function updateVersionOrder() {
try {
const newVersions = await $fetch("/api/v1/admin/game/version", {
const newVersions = await $dropFetch("/api/v1/admin/game/version", {
method: "PATCH",
body: {
id: gameId,
@ -822,7 +822,7 @@ function removeImageFromCarousel(id: string) {
async function updateImageCarousel() {
try {
await $fetch("/api/v1/admin/game", {
await $dropFetch("/api/v1/admin/game", {
method: "PATCH",
body: {
id: gameId,

View File

@ -158,7 +158,7 @@ definePageMeta({
});
const headers = useRequestHeaders(["cookie"]);
const games = await $fetch("/api/v1/admin/import/game", { headers });
const games = await $dropFetch("/api/v1/admin/import/game", { headers });
const currentlySelectedGame = ref(-1);
const gameSearchResultsLoading = ref(false);
@ -174,7 +174,7 @@ async function updateSelectedGame(value: number) {
metadataResults.value = undefined;
currentlySelectedMetadata.value = -1;
const results = await $fetch(
const results = await $dropFetch(
`/api/v1/admin/import/game/search?q=${encodeURIComponent(game)}`
);
metadataResults.value = results;
@ -199,7 +199,7 @@ const importError = ref<string | undefined>();
async function importGame(metadata: boolean) {
if (!metadataResults.value && metadata) return;
const game = await $fetch("/api/v1/admin/import/game", {
const game = await $dropFetch("/api/v1/admin/import/game", {
method: "POST",
body: {
path: games.unimportedGames[currentlySelectedGame.value],

View File

@ -180,7 +180,7 @@ useHead({
const searchQuery = ref("");
const headers = useRequestHeaders(["cookie"]);
const libraryState = await $fetch("/api/v1/admin/library", { headers });
const libraryState = await $dropFetch("/api/v1/admin/library", { headers });
const libraryGames = ref(
libraryState.games.map((e) => {
const noVersions = e.status.noVersions;
@ -210,7 +210,7 @@ const filteredLibraryGames = computed(() =>
);
async function deleteGame(id: string) {
await $fetch(`/api/v1/admin/game?id=${id}`, { method: "DELETE" });
await $dropFetch(`/api/v1/admin/game?id=${id}`, { method: "DELETE" });
const index = libraryGames.value.findIndex((e) => e.id === id);
libraryGames.value.splice(index, 1);
}

View File

@ -0,0 +1,11 @@
<template></template>
<script setup lang="ts">
definePageMeta({
layout: "admin",
});
useHead({
title: "Metadata",
});
</script>

View File

@ -23,9 +23,7 @@
:key="authMech.name"
class="overflow-hidden rounded-xl border border-zinc-800 bg-zinc-900"
>
<div
class="flex items-center gap-x-4 border-b border-zinc-800 p-6"
>
<div class="flex items-center gap-x-4 border-b border-zinc-800 p-6">
<component
:is="authMech.icon"
:alt="`${authMech.name} icon`"
@ -101,23 +99,9 @@ import { IconsSimpleAuthenticationLogo } from "#components";
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue";
import { EllipsisHorizontalIcon } from "@heroicons/vue/20/solid";
import { CheckIcon, XMarkIcon } from "@heroicons/vue/24/solid";
import { AuthMec } from "@prisma/client";
import type { Component } from "vue";
const authenticationMechanisms: Array<{
name: string;
enabled: boolean;
icon: Component;
route: string;
settings?: { [key: string]: string };
}> = [
{
name: "Simple (username/password)",
enabled: true,
icon: IconsSimpleAuthenticationLogo,
route: "/admin/auth/simple",
},
];
useHead({
title: "Authentication",
});
@ -125,4 +109,25 @@ useHead({
definePageMeta({
layout: "admin",
});
const headers = useRequestHeaders(["cookie"]);
const enabledMechanisms = await $dropFetch("/api/v1/admin/auth", {
headers,
});
const authenticationMechanisms: Array<{
name: string;
mec: AuthMec;
icon: Component;
route: string;
enabled: boolean;
settings?: { [key: string]: string };
}> = [
{
name: "Simple (username/password)",
mec: AuthMec.Simple,
icon: IconsSimpleAuthenticationLogo,
route: "/admin/users/auth/simple",
},
].map((e) => ({ ...e, enabled: enabledMechanisms.includes(e.mec) }));
</script>

View File

@ -459,7 +459,7 @@ async function invite() {
.add(...expiry[expiryKey.value])
.toJSON();
const newInvitation = await $fetch("/api/v1/admin/auth/invitation", {
const newInvitation = await $dropFetch("/api/v1/admin/auth/invitation", {
method: "POST",
body: {
username: username.value,
@ -495,7 +495,7 @@ function invite_wrapper() {
}
async function deleteInvitation(id: string) {
await $fetch("/api/v1/admin/auth/invitation", {
await $dropFetch("/api/v1/admin/auth/invitation", {
method: "DELETE",
body: {
id: id,

111
pages/admin/users/index.vue Normal file
View File

@ -0,0 +1,111 @@
<template>
<div>
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-base font-semibold text-zinc-100">Users</h1>
<p class="mt-2 text-sm text-zinc-400">
Manage the users on your Drop instance, and configure your
authentication methods.
</p>
</div>
<div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
<NuxtLink
to="/admin/users/auth"
class="block rounded-md bg-blue-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
>
Authentication &rarr;
</NuxtLink>
</div>
</div>
<div class="mt-8 flow-root">
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<table class="min-w-full divide-y divide-zinc-700">
<thead>
<tr>
<th
scope="col"
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-zinc-100 sm:pl-3"
>
Display Name
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
>
Username
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
>
Email
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
>
Admin?
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
>
Auth Options
</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-3">
<span class="sr-only">Edit</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id" class="even:bg-zinc-800">
<td
class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-zinc-100 sm:pl-3"
>
{{ user.displayName }}
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400">
{{ user.username }}
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400">
{{ user.email }}
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400">
{{ user.admin ? "Admin User" : "Normal user" }}
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400">
{{ user.authMecs.map((e) => e.mec).join(", ") }}
</td>
<td
class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-3"
>
<!--
<a href="#" class="text-blue-600 hover:text-blue-500"
>Edit<span class="sr-only"
>, {{ user.displayName }}</span
></a
>
-->
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
useHead({
title: "Users",
});
definePageMeta({
layout: "admin",
});
const headers = useRequestHeaders(["cookie"]);
const { data: users } = await useFetch("/api/v1/admin/users", { headers });
</script>

View File

@ -175,7 +175,7 @@ const error = ref();
const authToken = ref<string | undefined>();
async function authorize() {
const { redirect, token } = await $fetch("/api/v1/client/auth/callback", {
const { redirect, token } = await $dropFetch("/api/v1/client/auth/callback", {
method: "POST",
body: { id: clientId },
});

View File

@ -224,7 +224,7 @@ const loading = ref(false);
const error = ref<string | undefined>(undefined);
async function register() {
await $fetch("/api/v1/auth/signup/simple", {
await $dropFetch("/api/v1/auth/signup/simple", {
method: "POST",
body: {
invitation: invitationId,

View File

@ -149,7 +149,7 @@ function signin_wrapper() {
}
async function signin() {
await $fetch("/api/v1/auth/signin/simple", {
await $dropFetch("/api/v1/auth/signin/simple", {
method: "POST",
body: {
username: username.value,
@ -158,7 +158,7 @@ async function signin() {
},
});
const user = useUser();
user.value = await $fetch<User | null>("/api/v1/user");
user.value = await $dropFetch<User | null>("/api/v1/user");
}
definePageMeta({

View File

@ -36,6 +36,6 @@ const user = useUser();
user.value = null;
// Redirect to signin page after signout
await $fetch("/signout");
await $dropFetch("/signout");
router.push("/signin");
</script>

View File

@ -177,7 +177,7 @@ const gameId = route.params.id.toString();
const user = useUser();
const headers = useRequestHeaders(["cookie"]);
const game = await $fetch<Game & { versions: GameVersion[] }>(
const game = await $dropFetch<Game & { versions: GameVersion[] }>(
`/api/v1/games/${gameId}`,
{ headers }
);

View File

@ -96,13 +96,13 @@
import { ref, onMounted } from "vue";
const headers = useRequestHeaders(["cookie"]);
const recent = await $fetch("/api/v1/store/recent", { headers });
const updated = await $fetch("/api/v1/store/updated", { headers });
const released = await $fetch("/api/v1/store/released", {
const recent = await $dropFetch("/api/v1/store/recent", { headers });
const updated = await $dropFetch("/api/v1/store/updated", { headers });
const released = await $dropFetch("/api/v1/store/released", {
headers,
});
const developers = await $fetch("/api/v1/store/developers", { headers });
const publishers = await $fetch("/api/v1/store/publishers", { headers });
const developers = await $dropFetch("/api/v1/store/developers", { headers });
const publishers = await $dropFetch("/api/v1/store/publishers", { headers });
useHead({
title: "Store",