mirror of
https://github.com/Drop-OSS/drop.git
synced 2026-07-20 23:12:51 +10:00
feat: user page & $dropFetch util
This commit is contained in:
@@ -115,7 +115,7 @@ const inCollections = computed(() =>
|
|||||||
async function toggleLibrary() {
|
async function toggleLibrary() {
|
||||||
isLibraryLoading.value = true;
|
isLibraryLoading.value = true;
|
||||||
try {
|
try {
|
||||||
await $fetch("/api/v1/collection/default/entry", {
|
await $dropFetch("/api/v1/collection/default/entry", {
|
||||||
method: inLibrary.value ? "DELETE" : "POST",
|
method: inLibrary.value ? "DELETE" : "POST",
|
||||||
body: {
|
body: {
|
||||||
id: props.gameId,
|
id: props.gameId,
|
||||||
@@ -142,7 +142,7 @@ async function toggleCollection(id: string) {
|
|||||||
if (!collection) return;
|
if (!collection) return;
|
||||||
const index = collection.entries.findIndex((e) => e.gameId == props.gameId);
|
const index = collection.entries.findIndex((e) => e.gameId == props.gameId);
|
||||||
|
|
||||||
await $fetch(`/api/v1/collection/${id}/entry`, {
|
await $dropFetch(`/api/v1/collection/${id}/entry`, {
|
||||||
method: index == -1 ? "POST" : "DELETE",
|
method: index == -1 ? "POST" : "DELETE",
|
||||||
body: {
|
body: {
|
||||||
id: props.gameId,
|
id: props.gameId,
|
||||||
|
|||||||
@@ -72,14 +72,14 @@ async function createCollection() {
|
|||||||
createCollectionLoading.value = true;
|
createCollectionLoading.value = true;
|
||||||
|
|
||||||
// Create the collection
|
// Create the collection
|
||||||
const response = await $fetch("/api/v1/collection", {
|
const response = await $dropFetch("/api/v1/collection", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: { name: collectionName.value },
|
body: { name: collectionName.value },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add the game if provided
|
// Add the game if provided
|
||||||
if (props.gameId) {
|
if (props.gameId) {
|
||||||
const entry = await $fetch<
|
const entry = await $dropFetch<
|
||||||
CollectionEntry & { game: SerializeObject<Game> }
|
CollectionEntry & { game: SerializeObject<Game> }
|
||||||
>(`/api/v1/collection/${response.id}/entry`, {
|
>(`/api/v1/collection/${response.id}/entry`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ async function deleteCollection() {
|
|||||||
if (!collection.value) return;
|
if (!collection.value) return;
|
||||||
|
|
||||||
deleteLoading.value = true;
|
deleteLoading.value = true;
|
||||||
await $fetch(`/api/v1/collection/${collection.value.id}`, {
|
await $dropFetch(`/api/v1/collection/${collection.value.id}`, {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -348,7 +348,7 @@ async function createArticle() {
|
|||||||
formData.append("content", newArticle.value.content);
|
formData.append("content", newArticle.value.content);
|
||||||
formData.append("tags", JSON.stringify(newArticle.value.tags));
|
formData.append("tags", JSON.stringify(newArticle.value.tags));
|
||||||
|
|
||||||
await $fetch("/api/v1/admin/news", {
|
await $dropFetch("/api/v1/admin/news", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ import type { Notification } from "@prisma/client";
|
|||||||
const props = defineProps<{ notification: Notification }>();
|
const props = defineProps<{ notification: Notification }>();
|
||||||
|
|
||||||
async function deleteMe() {
|
async function deleteMe() {
|
||||||
await $fetch(`/api/v1/notifications/${props.notification.id}`, {
|
await $dropFetch(`/api/v1/notifications/${props.notification.id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
const notifications = useNotifications();
|
const notifications = useNotifications();
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ async function uploadFile() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await $fetch(props.endpoint, { method: "POST", body: form });
|
const result = await $dropFetch(props.endpoint, { method: "POST", body: form });
|
||||||
open.value = false;
|
open.value = false;
|
||||||
file.value = undefined;
|
file.value = undefined;
|
||||||
emit("upload", result);
|
emit("upload", result);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const useCollections = async () => {
|
|||||||
const state = useState<FullCollection[]>("collections", () => undefined);
|
const state = useState<FullCollection[]>("collections", () => undefined);
|
||||||
if (state.value === undefined) {
|
if (state.value === undefined) {
|
||||||
const headers = useRequestHeaders(["cookie"]);
|
const headers = useRequestHeaders(["cookie"]);
|
||||||
state.value = await $fetch<FullCollection[]>("/api/v1/collection", {
|
state.value = await $dropFetch<FullCollection[]>("/api/v1/collection", {
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@ export const useCollections = async () => {
|
|||||||
|
|
||||||
export async function refreshCollection(id: string) {
|
export async function refreshCollection(id: string) {
|
||||||
const state = useState<FullCollection[]>("collections");
|
const state = useState<FullCollection[]>("collections");
|
||||||
const collection = await $fetch<FullCollection>(`/api/v1/collection/${id}`);
|
const collection = await $dropFetch<FullCollection>(`/api/v1/collection/${id}`);
|
||||||
const index = state.value.findIndex((e) => e.id == id);
|
const index = state.value.findIndex((e) => e.id == id);
|
||||||
if (index == -1) {
|
if (index == -1) {
|
||||||
state.value.push(collection);
|
state.value.push(collection);
|
||||||
@@ -42,7 +42,7 @@ export const useLibrary = async () => {
|
|||||||
export async function refreshLibrary() {
|
export async function refreshLibrary() {
|
||||||
const state = useState<FullCollection>("library");
|
const state = useState<FullCollection>("library");
|
||||||
const headers = useRequestHeaders(["cookie"]);
|
const headers = useRequestHeaders(["cookie"]);
|
||||||
state.value = await $fetch<FullCollection>("/api/v1/collection/default", {
|
state.value = await $dropFetch<FullCollection>("/api/v1/collection/default", {
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ export const useNews = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const remove = async (id: string) => {
|
const remove = async (id: string) => {
|
||||||
return await $fetch(`/api/v1/admin/news/${id}`, {
|
return await $dropFetch(`/api/v1/admin/news/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import type {
|
||||||
|
$Fetch,
|
||||||
|
ExtractedRouteMethod,
|
||||||
|
NitroFetchOptions,
|
||||||
|
NitroFetchRequest,
|
||||||
|
TypedInternalResponse,
|
||||||
|
} from "nitropack/types";
|
||||||
|
|
||||||
|
interface DropFetch<
|
||||||
|
DefaultT = unknown,
|
||||||
|
DefaultR extends NitroFetchRequest = NitroFetchRequest
|
||||||
|
> {
|
||||||
|
<
|
||||||
|
T = DefaultT,
|
||||||
|
R extends NitroFetchRequest = DefaultR,
|
||||||
|
O extends NitroFetchOptions<R> = NitroFetchOptions<R>
|
||||||
|
>(
|
||||||
|
request: R,
|
||||||
|
opts?: O
|
||||||
|
): Promise<
|
||||||
|
// @ts-ignore
|
||||||
|
TypedInternalResponse<
|
||||||
|
R,
|
||||||
|
T,
|
||||||
|
NitroFetchOptions<R> extends O ? "get" : ExtractedRouteMethod<R, O>
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const $dropFetch: DropFetch = async (request, opts) => {
|
||||||
|
if (!getCurrentInstance()?.proxy) {
|
||||||
|
return (await $fetch(request, opts)) as any;
|
||||||
|
}
|
||||||
|
const { data, error } = await useFetch(request, opts as any);
|
||||||
|
if (error.value) throw error.value;
|
||||||
|
return data.value as any;
|
||||||
|
};
|
||||||
+1
-1
@@ -12,5 +12,5 @@ export const updateUser = async () => {
|
|||||||
if (user.value === null) return;
|
if (user.value === null) return;
|
||||||
|
|
||||||
// SSR calls have to be after uses
|
// SSR calls have to be after uses
|
||||||
user.value = await $fetch<User | null>("/api/v1/user", { headers });
|
user.value = await $dropFetch<User | null>("/api/v1/user", { headers });
|
||||||
};
|
};
|
||||||
|
|||||||
+12
-4
@@ -167,6 +167,8 @@ import {
|
|||||||
Cog6ToothIcon,
|
Cog6ToothIcon,
|
||||||
FlagIcon,
|
FlagIcon,
|
||||||
BellIcon,
|
BellIcon,
|
||||||
|
DocumentIcon,
|
||||||
|
UserGroupIcon,
|
||||||
} from "@heroicons/vue/24/outline";
|
} from "@heroicons/vue/24/outline";
|
||||||
import type { NavigationItem } from "~/composables/types";
|
import type { NavigationItem } from "~/composables/types";
|
||||||
import { useCurrentNavigationIndex } from "~/composables/current-page-engine";
|
import { useCurrentNavigationIndex } from "~/composables/current-page-engine";
|
||||||
@@ -182,10 +184,16 @@ const navigation: Array<NavigationItem & { icon: Component }> = [
|
|||||||
icon: ServerStackIcon,
|
icon: ServerStackIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Auth",
|
label: "Meta",
|
||||||
route: "/admin/auth",
|
route: "/admin/metadata",
|
||||||
prefix: "/admin/auth",
|
prefix: "/admin/metadata",
|
||||||
icon: LockClosedIcon,
|
icon: DocumentIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Users",
|
||||||
|
route: "/admin/users",
|
||||||
|
prefix: "/admin/users",
|
||||||
|
icon: UserGroupIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Settings",
|
label: "Settings",
|
||||||
|
|||||||
+29
-45
@@ -7,15 +7,24 @@
|
|||||||
<div
|
<div
|
||||||
class="relative flex h-full flex-col overflow-hidden rounded-[calc(var(--radius-lg)+1px)] lg:rounded-l-[calc(2rem+1px)]"
|
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
|
<p
|
||||||
class="mt-2 text-lg font-medium tracking-tight text-zinc-100 max-lg:text-center"
|
class="mt-2 text-lg font-medium tracking-tight text-zinc-100 max-lg:text-center"
|
||||||
>
|
>
|
||||||
Library
|
Library
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2 max-w-lg text-sm/6 text-zinc-400 max-lg:text-center">
|
<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
|
Manage your Drop library, and import new games. Your library is the
|
||||||
lorem cupidatat commodo.
|
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"> →</span>
|
||||||
|
</NuxtLink>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -45,31 +54,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -78,30 +62,30 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="relative max-lg:row-start-1">
|
<div class="relative max-lg:row-start-1">
|
||||||
<div
|
<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
|
<div
|
||||||
class="relative flex h-full flex-col overflow-hidden rounded-[calc(var(--radius-lg)+1px)] max-lg:rounded-t-[calc(2rem+1px)]"
|
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
|
<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>
|
||||||
<p class="mt-2 max-w-lg text-sm/6 text-gray-600 max-lg:text-center">
|
<p class="mt-2 max-w-lg text-sm/6 text-zinc-400 max-lg:text-center">
|
||||||
Lorem ipsum, dolor sit amet consectetur adipisicing elit maiores
|
Your users are people who can access your Drop instance, download
|
||||||
impedit.
|
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"> →</span>
|
||||||
|
</NuxtLink>
|
||||||
</p>
|
</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>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -193,5 +177,5 @@ useHead({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const headers = useRequestHeaders(["cookie"]);
|
const headers = useRequestHeaders(["cookie"]);
|
||||||
const libraryState = await $fetch("/api/v1/admin/library", { headers });
|
const libraryState = await $dropFetch("/api/v1/admin/library", { headers });
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -165,7 +165,7 @@
|
|||||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||||
active
|
active
|
||||||
? 'bg-blue-600 text-white outline-none'
|
? 'bg-blue-600 text-white outline-none'
|
||||||
: 'text-gray-900',
|
: 'text-zinc-100',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -321,7 +321,7 @@
|
|||||||
'relative cursor-default select-none py-2 pl-3 pr-9',
|
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||||
active
|
active
|
||||||
? 'bg-blue-600 text-white outline-none'
|
? 'bg-blue-600 text-white outline-none'
|
||||||
: 'text-gray-900',
|
: 'text-zinc-100',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -553,7 +553,7 @@ const router = useRouter();
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const headers = useRequestHeaders(["cookie"]);
|
const headers = useRequestHeaders(["cookie"]);
|
||||||
const gameId = route.params.id.toString();
|
const gameId = route.params.id.toString();
|
||||||
const versions = await $fetch(
|
const versions = await $dropFetch(
|
||||||
`/api/v1/admin/import/version?id=${encodeURIComponent(gameId)}`,
|
`/api/v1/admin/import/version?id=${encodeURIComponent(gameId)}`,
|
||||||
{
|
{
|
||||||
headers,
|
headers,
|
||||||
@@ -561,7 +561,7 @@ const versions = await $fetch(
|
|||||||
);
|
);
|
||||||
const currentlySelectedVersion = ref(-1);
|
const currentlySelectedVersion = ref(-1);
|
||||||
const versionSettings = ref<{
|
const versionSettings = ref<{
|
||||||
platform: string;
|
platform: PlatformClient | undefined;
|
||||||
|
|
||||||
onlySetup: boolean;
|
onlySetup: boolean;
|
||||||
launch: string;
|
launch: string;
|
||||||
@@ -572,7 +572,7 @@ const versionSettings = ref<{
|
|||||||
delta: boolean;
|
delta: boolean;
|
||||||
umuId: string;
|
umuId: string;
|
||||||
}>({
|
}>({
|
||||||
platform: "",
|
platform: undefined,
|
||||||
launch: "",
|
launch: "",
|
||||||
launchArgs: "",
|
launchArgs: "",
|
||||||
setup: "",
|
setup: "",
|
||||||
@@ -582,7 +582,8 @@ const versionSettings = ref<{
|
|||||||
umuId: "",
|
umuId: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const versionGuesses = ref<Array<{ platform: string; filename: string }>>();
|
const versionGuesses =
|
||||||
|
ref<Array<{ platform: PlatformClient; filename: string }>>();
|
||||||
const launchProcessQuery = ref("");
|
const launchProcessQuery = ref("");
|
||||||
const setupProcessQuery = ref("");
|
const setupProcessQuery = ref("");
|
||||||
|
|
||||||
@@ -637,17 +638,20 @@ async function updateCurrentlySelectedVersion(value: number) {
|
|||||||
if (currentlySelectedVersion.value == value) return;
|
if (currentlySelectedVersion.value == value) return;
|
||||||
currentlySelectedVersion.value = value;
|
currentlySelectedVersion.value = value;
|
||||||
const version = versions[currentlySelectedVersion.value];
|
const version = versions[currentlySelectedVersion.value];
|
||||||
const results = await $fetch(
|
const results = await $dropFetch(
|
||||||
`/api/v1/admin/import/version/preload?id=${encodeURIComponent(
|
`/api/v1/admin/import/version/preload?id=${encodeURIComponent(
|
||||||
gameId
|
gameId
|
||||||
)}&version=${encodeURIComponent(version)}`
|
)}&version=${encodeURIComponent(version)}`
|
||||||
);
|
);
|
||||||
versionGuesses.value = results;
|
versionGuesses.value = results.map((e) => ({
|
||||||
|
...e,
|
||||||
|
platform: e.platform as PlatformClient,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startImport() {
|
async function startImport() {
|
||||||
if (!versionSettings.value) return;
|
if (!versionSettings.value) return;
|
||||||
const taskId = await $fetch("/api/v1/admin/import/version", {
|
const taskId = await $dropFetch("/api/v1/admin/import/version", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
id: gameId,
|
id: gameId,
|
||||||
|
|||||||
@@ -530,7 +530,7 @@ const mobileShowFinalDescription = ref(true);
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const gameId = route.params.id.toString();
|
const gameId = route.params.id.toString();
|
||||||
const headers = useRequestHeaders(["cookie"]);
|
const headers = useRequestHeaders(["cookie"]);
|
||||||
const { game: rawGame, unimportedVersions } = await $fetch(
|
const { game: rawGame, unimportedVersions } = await $dropFetch(
|
||||||
`/api/v1/admin/game?id=${encodeURIComponent(gameId)}`,
|
`/api/v1/admin/game?id=${encodeURIComponent(gameId)}`,
|
||||||
{
|
{
|
||||||
headers,
|
headers,
|
||||||
@@ -579,7 +579,7 @@ async function coreMetadataUpdate() {
|
|||||||
formData.append("name", coreMetadataName.value);
|
formData.append("name", coreMetadataName.value);
|
||||||
formData.append("description", coreMetadataDescription.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",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
@@ -630,7 +630,7 @@ watch(descriptionHTML, (v) => {
|
|||||||
savingTimeout = setTimeout(async () => {
|
savingTimeout = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
descriptionSaving.value = 2;
|
descriptionSaving.value = 2;
|
||||||
await $fetch("/api/v1/admin/game", {
|
await $dropFetch("/api/v1/admin/game", {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: {
|
body: {
|
||||||
id: gameId,
|
id: gameId,
|
||||||
@@ -672,7 +672,7 @@ function insertImageAtCursor(id: string) {
|
|||||||
async function updateBannerImage(id: string) {
|
async function updateBannerImage(id: string) {
|
||||||
try {
|
try {
|
||||||
if (game.value.mBannerId == id) return;
|
if (game.value.mBannerId == id) return;
|
||||||
const { mBannerId } = await $fetch("/api/v1/admin/game", {
|
const { mBannerId } = await $dropFetch("/api/v1/admin/game", {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: {
|
body: {
|
||||||
id: gameId,
|
id: gameId,
|
||||||
@@ -698,7 +698,7 @@ async function updateBannerImage(id: string) {
|
|||||||
async function updateCoverImage(id: string) {
|
async function updateCoverImage(id: string) {
|
||||||
try {
|
try {
|
||||||
if (game.value.mCoverId == id) return;
|
if (game.value.mCoverId == id) return;
|
||||||
const { mCoverId } = await $fetch("/api/v1/admin/game", {
|
const { mCoverId } = await $dropFetch("/api/v1/admin/game", {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: {
|
body: {
|
||||||
id: gameId,
|
id: gameId,
|
||||||
@@ -723,7 +723,7 @@ async function updateCoverImage(id: string) {
|
|||||||
|
|
||||||
async function deleteImage(id: string) {
|
async function deleteImage(id: string) {
|
||||||
try {
|
try {
|
||||||
const { mBannerId, mImageLibrary } = await $fetch(
|
const { mBannerId, mImageLibrary } = await $dropFetch(
|
||||||
"/api/v1/admin/game/image",
|
"/api/v1/admin/game/image",
|
||||||
{
|
{
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
@@ -757,7 +757,7 @@ async function uploadAfterImageUpload(result: Game) {
|
|||||||
|
|
||||||
async function deleteVersion(versionName: string) {
|
async function deleteVersion(versionName: string) {
|
||||||
try {
|
try {
|
||||||
await $fetch("/api/v1/admin/game/version", {
|
await $dropFetch("/api/v1/admin/game/version", {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
body: {
|
body: {
|
||||||
id: gameId,
|
id: gameId,
|
||||||
@@ -785,7 +785,7 @@ async function deleteVersion(versionName: string) {
|
|||||||
|
|
||||||
async function updateVersionOrder() {
|
async function updateVersionOrder() {
|
||||||
try {
|
try {
|
||||||
const newVersions = await $fetch("/api/v1/admin/game/version", {
|
const newVersions = await $dropFetch("/api/v1/admin/game/version", {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: {
|
body: {
|
||||||
id: gameId,
|
id: gameId,
|
||||||
@@ -822,7 +822,7 @@ function removeImageFromCarousel(id: string) {
|
|||||||
|
|
||||||
async function updateImageCarousel() {
|
async function updateImageCarousel() {
|
||||||
try {
|
try {
|
||||||
await $fetch("/api/v1/admin/game", {
|
await $dropFetch("/api/v1/admin/game", {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: {
|
body: {
|
||||||
id: gameId,
|
id: gameId,
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ definePageMeta({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const headers = useRequestHeaders(["cookie"]);
|
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 currentlySelectedGame = ref(-1);
|
||||||
const gameSearchResultsLoading = ref(false);
|
const gameSearchResultsLoading = ref(false);
|
||||||
@@ -174,7 +174,7 @@ async function updateSelectedGame(value: number) {
|
|||||||
metadataResults.value = undefined;
|
metadataResults.value = undefined;
|
||||||
currentlySelectedMetadata.value = -1;
|
currentlySelectedMetadata.value = -1;
|
||||||
|
|
||||||
const results = await $fetch(
|
const results = await $dropFetch(
|
||||||
`/api/v1/admin/import/game/search?q=${encodeURIComponent(game)}`
|
`/api/v1/admin/import/game/search?q=${encodeURIComponent(game)}`
|
||||||
);
|
);
|
||||||
metadataResults.value = results;
|
metadataResults.value = results;
|
||||||
@@ -199,7 +199,7 @@ const importError = ref<string | undefined>();
|
|||||||
async function importGame(metadata: boolean) {
|
async function importGame(metadata: boolean) {
|
||||||
if (!metadataResults.value && metadata) return;
|
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",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
path: games.unimportedGames[currentlySelectedGame.value],
|
path: games.unimportedGames[currentlySelectedGame.value],
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ useHead({
|
|||||||
const searchQuery = ref("");
|
const searchQuery = ref("");
|
||||||
|
|
||||||
const headers = useRequestHeaders(["cookie"]);
|
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(
|
const libraryGames = ref(
|
||||||
libraryState.games.map((e) => {
|
libraryState.games.map((e) => {
|
||||||
const noVersions = e.status.noVersions;
|
const noVersions = e.status.noVersions;
|
||||||
@@ -210,7 +210,7 @@ const filteredLibraryGames = computed(() =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
async function deleteGame(id: string) {
|
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);
|
const index = libraryGames.value.findIndex((e) => e.id === id);
|
||||||
libraryGames.value.splice(index, 1);
|
libraryGames.value.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<template></template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
layout: "admin",
|
||||||
|
});
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: "Metadata",
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -23,9 +23,7 @@
|
|||||||
:key="authMech.name"
|
:key="authMech.name"
|
||||||
class="overflow-hidden rounded-xl border border-zinc-800 bg-zinc-900"
|
class="overflow-hidden rounded-xl border border-zinc-800 bg-zinc-900"
|
||||||
>
|
>
|
||||||
<div
|
<div class="flex items-center gap-x-4 border-b border-zinc-800 p-6">
|
||||||
class="flex items-center gap-x-4 border-b border-zinc-800 p-6"
|
|
||||||
>
|
|
||||||
<component
|
<component
|
||||||
:is="authMech.icon"
|
:is="authMech.icon"
|
||||||
:alt="`${authMech.name} icon`"
|
:alt="`${authMech.name} icon`"
|
||||||
@@ -101,23 +99,9 @@ import { IconsSimpleAuthenticationLogo } from "#components";
|
|||||||
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue";
|
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue";
|
||||||
import { EllipsisHorizontalIcon } from "@heroicons/vue/20/solid";
|
import { EllipsisHorizontalIcon } from "@heroicons/vue/20/solid";
|
||||||
import { CheckIcon, XMarkIcon } from "@heroicons/vue/24/solid";
|
import { CheckIcon, XMarkIcon } from "@heroicons/vue/24/solid";
|
||||||
|
import { AuthMec } from "@prisma/client";
|
||||||
import type { Component } from "vue";
|
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({
|
useHead({
|
||||||
title: "Authentication",
|
title: "Authentication",
|
||||||
});
|
});
|
||||||
@@ -125,4 +109,25 @@ useHead({
|
|||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: "admin",
|
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>
|
</script>
|
||||||
@@ -459,7 +459,7 @@ async function invite() {
|
|||||||
.add(...expiry[expiryKey.value])
|
.add(...expiry[expiryKey.value])
|
||||||
.toJSON();
|
.toJSON();
|
||||||
|
|
||||||
const newInvitation = await $fetch("/api/v1/admin/auth/invitation", {
|
const newInvitation = await $dropFetch("/api/v1/admin/auth/invitation", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
username: username.value,
|
username: username.value,
|
||||||
@@ -495,7 +495,7 @@ function invite_wrapper() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteInvitation(id: string) {
|
async function deleteInvitation(id: string) {
|
||||||
await $fetch("/api/v1/admin/auth/invitation", {
|
await $dropFetch("/api/v1/admin/auth/invitation", {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
body: {
|
body: {
|
||||||
id: id,
|
id: id,
|
||||||
@@ -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 →
|
||||||
|
</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>
|
||||||
@@ -175,7 +175,7 @@ const error = ref();
|
|||||||
const authToken = ref<string | undefined>();
|
const authToken = ref<string | undefined>();
|
||||||
|
|
||||||
async function authorize() {
|
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",
|
method: "POST",
|
||||||
body: { id: clientId },
|
body: { id: clientId },
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -224,7 +224,7 @@ const loading = ref(false);
|
|||||||
const error = ref<string | undefined>(undefined);
|
const error = ref<string | undefined>(undefined);
|
||||||
|
|
||||||
async function register() {
|
async function register() {
|
||||||
await $fetch("/api/v1/auth/signup/simple", {
|
await $dropFetch("/api/v1/auth/signup/simple", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
invitation: invitationId,
|
invitation: invitationId,
|
||||||
|
|||||||
+2
-2
@@ -149,7 +149,7 @@ function signin_wrapper() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function signin() {
|
async function signin() {
|
||||||
await $fetch("/api/v1/auth/signin/simple", {
|
await $dropFetch("/api/v1/auth/signin/simple", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
username: username.value,
|
username: username.value,
|
||||||
@@ -158,7 +158,7 @@ async function signin() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const user = useUser();
|
const user = useUser();
|
||||||
user.value = await $fetch<User | null>("/api/v1/user");
|
user.value = await $dropFetch<User | null>("/api/v1/user");
|
||||||
}
|
}
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
|
|||||||
+1
-1
@@ -36,6 +36,6 @@ const user = useUser();
|
|||||||
user.value = null;
|
user.value = null;
|
||||||
|
|
||||||
// Redirect to signin page after signout
|
// Redirect to signin page after signout
|
||||||
await $fetch("/signout");
|
await $dropFetch("/signout");
|
||||||
router.push("/signin");
|
router.push("/signin");
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ const gameId = route.params.id.toString();
|
|||||||
const user = useUser();
|
const user = useUser();
|
||||||
|
|
||||||
const headers = useRequestHeaders(["cookie"]);
|
const headers = useRequestHeaders(["cookie"]);
|
||||||
const game = await $fetch<Game & { versions: GameVersion[] }>(
|
const game = await $dropFetch<Game & { versions: GameVersion[] }>(
|
||||||
`/api/v1/games/${gameId}`,
|
`/api/v1/games/${gameId}`,
|
||||||
{ headers }
|
{ headers }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -96,13 +96,13 @@
|
|||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
|
|
||||||
const headers = useRequestHeaders(["cookie"]);
|
const headers = useRequestHeaders(["cookie"]);
|
||||||
const recent = await $fetch("/api/v1/store/recent", { headers });
|
const recent = await $dropFetch("/api/v1/store/recent", { headers });
|
||||||
const updated = await $fetch("/api/v1/store/updated", { headers });
|
const updated = await $dropFetch("/api/v1/store/updated", { headers });
|
||||||
const released = await $fetch("/api/v1/store/released", {
|
const released = await $dropFetch("/api/v1/store/released", {
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
const developers = await $fetch("/api/v1/store/developers", { headers });
|
const developers = await $dropFetch("/api/v1/store/developers", { headers });
|
||||||
const publishers = await $fetch("/api/v1/store/publishers", { headers });
|
const publishers = await $dropFetch("/api/v1/store/publishers", { headers });
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: "Store",
|
title: "Store",
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "LinkedAuthMec" ADD COLUMN "enabled" BOOLEAN NOT NULL DEFAULT true;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "enabled" BOOLEAN NOT NULL DEFAULT true;
|
||||||
@@ -3,8 +3,9 @@ enum AuthMec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model LinkedAuthMec {
|
model LinkedAuthMec {
|
||||||
userId String
|
userId String
|
||||||
mec AuthMec
|
mec AuthMec
|
||||||
|
enabled Boolean @default(true)
|
||||||
|
|
||||||
credentials Json
|
credentials Json
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ enum APITokenMode {
|
|||||||
|
|
||||||
model APIToken {
|
model APIToken {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
token String @default(uuid()) @unique
|
token String @unique @default(uuid())
|
||||||
mode APITokenMode
|
mode APITokenMode
|
||||||
name String
|
name String
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ model User {
|
|||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
username String @unique
|
username String @unique
|
||||||
admin Boolean @default(false)
|
admin Boolean @default(false)
|
||||||
|
enabled Boolean @default(true)
|
||||||
|
|
||||||
email String
|
email String
|
||||||
displayName String
|
displayName String
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { AuthMec } from "@prisma/client";
|
||||||
|
import aclManager from "~/server/internal/acls";
|
||||||
|
import { applicationSettings } from "~/server/internal/config/application-configuration";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (h3) => {
|
||||||
|
const allowed = await aclManager.allowSystemACL(h3, ["auth:read"]);
|
||||||
|
if (!allowed) throw createError({ statusCode: 403 });
|
||||||
|
|
||||||
|
const enabledMechanisms: AuthMec[] = await applicationSettings.get(
|
||||||
|
"enabledAuthencationMechanisms"
|
||||||
|
);
|
||||||
|
|
||||||
|
return enabledMechanisms;
|
||||||
|
});
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import aclManager from "~/server/internal/acls";
|
||||||
|
import prisma from "~/server/internal/db/database";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (h3) => {
|
||||||
|
const allowed = await aclManager.allowSystemACL(h3, ["user:read"]);
|
||||||
|
if (!allowed) throw createError({ statusCode: 403 });
|
||||||
|
|
||||||
|
const userId = getRouterParam(h3, "id");
|
||||||
|
if (!userId)
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: "No userId in route.",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userId == "system")
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: "Cannot delete system user.",
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({ where: { id: userId } });
|
||||||
|
if (!user)
|
||||||
|
throw createError({ statusCode: 404, statusMessage: "User not found." });
|
||||||
|
|
||||||
|
return user;
|
||||||
|
});
|
||||||
@@ -5,7 +5,18 @@ export default defineEventHandler(async (h3) => {
|
|||||||
const allowed = await aclManager.allowSystemACL(h3, ["user:read"]);
|
const allowed = await aclManager.allowSystemACL(h3, ["user:read"]);
|
||||||
if (!allowed) throw createError({ statusCode: 403 });
|
if (!allowed) throw createError({ statusCode: 403 });
|
||||||
|
|
||||||
const users = await prisma.user.findMany({});
|
const users = await prisma.user.findMany({
|
||||||
|
where: {
|
||||||
|
id: { not: "system" },
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
authMecs: {
|
||||||
|
select: {
|
||||||
|
mec: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return users;
|
return users;
|
||||||
});
|
});
|
||||||
@@ -5,34 +5,57 @@ import { checkHash } from "~/server/internal/security/simple";
|
|||||||
import sessionHandler from "~/server/internal/session";
|
import sessionHandler from "~/server/internal/session";
|
||||||
|
|
||||||
export default defineEventHandler(async (h3) => {
|
export default defineEventHandler(async (h3) => {
|
||||||
const body = await readBody(h3);
|
const body = await readBody(h3);
|
||||||
|
|
||||||
const username = body.username;
|
const username = body.username;
|
||||||
const password = body.password;
|
const password = body.password;
|
||||||
const rememberMe = body.rememberMe ?? false;
|
const rememberMe = body.rememberMe ?? false;
|
||||||
if (username === undefined || password === undefined)
|
if (username === undefined || password === undefined)
|
||||||
throw createError({ statusCode: 403, statusMessage: "Username or password missing from request." });
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
const authMek = await prisma.linkedAuthMec.findFirst({
|
statusMessage: "Username or password missing from request.",
|
||||||
where: {
|
|
||||||
mec: AuthMec.Simple,
|
|
||||||
credentials: {
|
|
||||||
array_starts_with: username
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!authMek) throw createError({ statusCode: 401, statusMessage: "Invalid username or password." });
|
const authMek = await prisma.linkedAuthMec.findFirst({
|
||||||
|
where: {
|
||||||
|
mec: AuthMec.Simple,
|
||||||
|
credentials: {
|
||||||
|
array_starts_with: username,
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const credentials = authMek.credentials as JsonArray;
|
if (!authMek)
|
||||||
const hash = credentials.at(1);
|
throw createError({
|
||||||
|
statusCode: 401,
|
||||||
|
statusMessage: "Invalid username or password.",
|
||||||
|
});
|
||||||
|
|
||||||
if (!hash) throw createError({ statusCode: 403, statusMessage: "Invalid or disabled account. Please contact the server administrator." });
|
const credentials = authMek.credentials as JsonArray;
|
||||||
|
const hash = credentials.at(1);
|
||||||
|
|
||||||
if (!await checkHash(password, hash.toString()))
|
if (!hash || !authMek.user.enabled)
|
||||||
throw createError({ statusCode: 401, statusMessage: "Invalid username or password." });
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
statusMessage:
|
||||||
|
"Invalid or disabled account. Please contact the server administrator.",
|
||||||
|
});
|
||||||
|
|
||||||
await sessionHandler.setUserId(h3, authMek.userId, rememberMe);
|
if (!(await checkHash(password, hash.toString())))
|
||||||
|
throw createError({
|
||||||
|
statusCode: 401,
|
||||||
|
statusMessage: "Invalid username or password.",
|
||||||
|
});
|
||||||
|
|
||||||
return { result: true, userId: authMek.userId }
|
await sessionHandler.setUserId(h3, authMek.userId, rememberMe);
|
||||||
});
|
|
||||||
|
return { result: true, userId: authMek.userId };
|
||||||
|
});
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export const userACLDescriptions: ObjectFromList<typeof userACLs> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const systemACLDescriptions: ObjectFromList<typeof systemACLs> = {
|
export const systemACLDescriptions: ObjectFromList<typeof systemACLs> = {
|
||||||
|
"auth:read": "Fetch the list of enabled authentication mechanisms configured.",
|
||||||
"auth:simple:invitation:read": "Fetch simple auth invitations.",
|
"auth:simple:invitation:read": "Fetch simple auth invitations.",
|
||||||
"auth:simple:invitation:new": "Create new simple auth invitations.",
|
"auth:simple:invitation:new": "Create new simple auth invitations.",
|
||||||
"auth:simple:invitation:delete": "Delete a simple auth invitation.",
|
"auth:simple:invitation:delete": "Delete a simple auth invitation.",
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const userACLPrefix = "user:";
|
|||||||
type UserACL = Array<(typeof userACLs)[number]>;
|
type UserACL = Array<(typeof userACLs)[number]>;
|
||||||
|
|
||||||
export const systemACLs = [
|
export const systemACLs = [
|
||||||
|
"auth:read",
|
||||||
"auth:simple:invitation:read",
|
"auth:simple:invitation:read",
|
||||||
"auth:simple:invitation:new",
|
"auth:simple:invitation:new",
|
||||||
"auth:simple:invitation:delete",
|
"auth:simple:invitation:delete",
|
||||||
|
|||||||
@@ -3,20 +3,12 @@ import prisma from "../db/database";
|
|||||||
|
|
||||||
class ApplicationConfiguration {
|
class ApplicationConfiguration {
|
||||||
// Reference to the currently selected application configuration
|
// Reference to the currently selected application configuration
|
||||||
private currentApplicationSettings: ApplicationSettings = {
|
private currentApplicationSettings: ApplicationSettings | undefined =
|
||||||
timestamp: new Date(),
|
undefined;
|
||||||
enabledAuthencationMechanisms: [],
|
|
||||||
metadataProviders: [],
|
|
||||||
};
|
|
||||||
private applicationStateProxy: object;
|
|
||||||
private dirty: boolean = false;
|
|
||||||
private dirtyPromise: Promise<any> | undefined = undefined;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.applicationStateProxy = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async save() {
|
private async save() {
|
||||||
|
await this.init();
|
||||||
|
|
||||||
const deepAppConfigCopy: Omit<ApplicationSettings, "timestamp"> & {
|
const deepAppConfigCopy: Omit<ApplicationSettings, "timestamp"> & {
|
||||||
timestamp?: Date;
|
timestamp?: Date;
|
||||||
} = JSON.parse(JSON.stringify(this.currentApplicationSettings));
|
} = JSON.parse(JSON.stringify(this.currentApplicationSettings));
|
||||||
@@ -28,6 +20,19 @@ class ApplicationConfiguration {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async init() {
|
||||||
|
if (this.currentApplicationSettings === undefined) {
|
||||||
|
const applicationSettingsCount = await prisma.applicationSettings.count(
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
if (applicationSettingsCount > 0) {
|
||||||
|
await applicationSettings.pullConfiguration();
|
||||||
|
} else {
|
||||||
|
await applicationSettings.initialiseConfiguration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Default application configuration
|
// Default application configuration
|
||||||
async initialiseConfiguration() {
|
async initialiseConfiguration() {
|
||||||
const initialState = await prisma.applicationSettings.create({
|
const initialState = await prisma.applicationSettings.create({
|
||||||
@@ -56,6 +61,10 @@ class ApplicationConfiguration {
|
|||||||
key: T,
|
key: T,
|
||||||
value: ApplicationSettings[T]
|
value: ApplicationSettings[T]
|
||||||
) {
|
) {
|
||||||
|
await this.init();
|
||||||
|
if (!this.currentApplicationSettings)
|
||||||
|
throw new Error("Somehow, failed to initialise application settings");
|
||||||
|
|
||||||
if (this.currentApplicationSettings[key] !== value) {
|
if (this.currentApplicationSettings[key] !== value) {
|
||||||
this.currentApplicationSettings[key] = value;
|
this.currentApplicationSettings[key] = value;
|
||||||
|
|
||||||
@@ -63,7 +72,11 @@ class ApplicationConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get<T extends keyof ApplicationSettings>(key: T): ApplicationSettings[T] {
|
async get<T extends keyof ApplicationSettings>(key: T): Promise<ApplicationSettings[T]> {
|
||||||
|
await this.init();
|
||||||
|
if (!this.currentApplicationSettings)
|
||||||
|
throw new Error("Somehow, failed to initialise application settings");
|
||||||
|
|
||||||
return this.currentApplicationSettings[key];
|
return this.currentApplicationSettings[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export abstract class ObjectBackend {
|
|||||||
// If we need to fetch a remote resource, it doesn't make sense
|
// If we need to fetch a remote resource, it doesn't make sense
|
||||||
// to immediately fetch the object, *then* check permissions.
|
// to immediately fetch the object, *then* check permissions.
|
||||||
// Instead the caller can pass a simple anonymous funciton, like
|
// Instead the caller can pass a simple anonymous funciton, like
|
||||||
// () => $fetch('/my-image');
|
// () => $dropFetch('/my-image');
|
||||||
// And if we actually have permission to write, it fetches it then.
|
// And if we actually have permission to write, it fetches it then.
|
||||||
async writeWithPermissions(
|
async writeWithPermissions(
|
||||||
id: ObjectReference,
|
id: ObjectReference,
|
||||||
|
|||||||
@@ -2,14 +2,6 @@ import { applicationSettings } from "../internal/config/application-configuratio
|
|||||||
import prisma from "../internal/db/database";
|
import prisma from "../internal/db/database";
|
||||||
|
|
||||||
export default defineNitroPlugin(async (nitro) => {
|
export default defineNitroPlugin(async (nitro) => {
|
||||||
const applicationSettingsCount = await prisma.applicationSettings.count({});
|
|
||||||
if (applicationSettingsCount > 0) {
|
|
||||||
await applicationSettings.pullConfiguration();
|
|
||||||
} else {
|
|
||||||
await applicationSettings.initialiseConfiguration();
|
|
||||||
}
|
|
||||||
console.log("initalised application config");
|
|
||||||
|
|
||||||
// Ensure system user exists
|
// Ensure system user exists
|
||||||
// The system user owns any user-based code
|
// The system user owns any user-based code
|
||||||
// that we want to re-use for the app
|
// that we want to re-use for the app
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ export default defineNitroPlugin(async (nitro) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add providers based on their position in the application settings
|
// Add providers based on their position in the application settings
|
||||||
const configuredProviderList = applicationSettings.get("metadataProviders");
|
const configuredProviderList = await applicationSettings.get(
|
||||||
|
"metadataProviders"
|
||||||
|
);
|
||||||
const max = configuredProviderList.length;
|
const max = configuredProviderList.length;
|
||||||
for (const [index, providerId] of configuredProviderList.entries()) {
|
for (const [index, providerId] of configuredProviderList.entries()) {
|
||||||
const priority = max * 2 - index; // Offset by the length --- (max - index) + max
|
const priority = max * 2 - index; // Offset by the length --- (max - index) + max
|
||||||
|
|||||||
Reference in New Issue
Block a user