diff --git a/components/AddLibraryButton.vue b/components/AddLibraryButton.vue index 629483b..6e29887 100644 --- a/components/AddLibraryButton.vue +++ b/components/AddLibraryButton.vue @@ -115,7 +115,7 @@ const inCollections = computed(() => async function toggleLibrary() { isLibraryLoading.value = true; try { - await $fetch("/api/v1/collection/default/entry", { + await $dropFetch("/api/v1/collection/default/entry", { method: inLibrary.value ? "DELETE" : "POST", body: { id: props.gameId, @@ -142,7 +142,7 @@ async function toggleCollection(id: string) { if (!collection) return; 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", body: { id: props.gameId, diff --git a/components/CreateCollectionModal.vue b/components/CreateCollectionModal.vue index 73e6569..b84e90b 100644 --- a/components/CreateCollectionModal.vue +++ b/components/CreateCollectionModal.vue @@ -72,14 +72,14 @@ async function createCollection() { createCollectionLoading.value = true; // Create the collection - const response = await $fetch("/api/v1/collection", { + const response = await $dropFetch("/api/v1/collection", { method: "POST", body: { name: collectionName.value }, }); // Add the game if provided if (props.gameId) { - const entry = await $fetch< + const entry = await $dropFetch< CollectionEntry & { game: SerializeObject } >(`/api/v1/collection/${response.id}/entry`, { method: "POST", diff --git a/components/DeleteCollectionModal.vue b/components/DeleteCollectionModal.vue index 49ac94d..8be2971 100644 --- a/components/DeleteCollectionModal.vue +++ b/components/DeleteCollectionModal.vue @@ -48,7 +48,7 @@ async function deleteCollection() { if (!collection.value) return; deleteLoading.value = true; - await $fetch(`/api/v1/collection/${collection.value.id}`, { + await $dropFetch(`/api/v1/collection/${collection.value.id}`, { // @ts-ignore method: "DELETE", }); diff --git a/components/NewsArticleCreate.vue b/components/NewsArticleCreate.vue index 058c82d..da29f58 100644 --- a/components/NewsArticleCreate.vue +++ b/components/NewsArticleCreate.vue @@ -348,7 +348,7 @@ async function createArticle() { formData.append("content", newArticle.value.content); formData.append("tags", JSON.stringify(newArticle.value.tags)); - await $fetch("/api/v1/admin/news", { + await $dropFetch("/api/v1/admin/news", { method: "POST", body: formData, }); diff --git a/components/Notification.vue b/components/Notification.vue index aa40240..a1c92c3 100644 --- a/components/Notification.vue +++ b/components/Notification.vue @@ -48,7 +48,7 @@ import type { Notification } from "@prisma/client"; const props = defineProps<{ notification: Notification }>(); async function deleteMe() { - await $fetch(`/api/v1/notifications/${props.notification.id}`, { + await $dropFetch(`/api/v1/notifications/${props.notification.id}`, { method: "DELETE", }); const notifications = useNotifications(); diff --git a/components/UploadFileDialog.vue b/components/UploadFileDialog.vue index fd1dd77..e74cf6e 100644 --- a/components/UploadFileDialog.vue +++ b/components/UploadFileDialog.vue @@ -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; file.value = undefined; emit("upload", result); diff --git a/composables/collection.ts b/composables/collection.ts index f831d14..320d00c 100644 --- a/composables/collection.ts +++ b/composables/collection.ts @@ -10,7 +10,7 @@ export const useCollections = async () => { const state = useState("collections", () => undefined); if (state.value === undefined) { const headers = useRequestHeaders(["cookie"]); - state.value = await $fetch("/api/v1/collection", { + state.value = await $dropFetch("/api/v1/collection", { headers, }); } @@ -20,7 +20,7 @@ export const useCollections = async () => { export async function refreshCollection(id: string) { const state = useState("collections"); - const collection = await $fetch(`/api/v1/collection/${id}`); + const collection = await $dropFetch(`/api/v1/collection/${id}`); const index = state.value.findIndex((e) => e.id == id); if (index == -1) { state.value.push(collection); @@ -42,7 +42,7 @@ export const useLibrary = async () => { export async function refreshLibrary() { const state = useState("library"); const headers = useRequestHeaders(["cookie"]); - state.value = await $fetch("/api/v1/collection/default", { + state.value = await $dropFetch("/api/v1/collection/default", { headers, }); } diff --git a/composables/news.ts b/composables/news.ts index 6c6ac22..cda4c77 100644 --- a/composables/news.ts +++ b/composables/news.ts @@ -22,7 +22,7 @@ export const useNews = () => { }; const remove = async (id: string) => { - return await $fetch(`/api/v1/admin/news/${id}`, { + return await $dropFetch(`/api/v1/admin/news/${id}`, { method: "DELETE", }); }; diff --git a/composables/request.ts b/composables/request.ts new file mode 100644 index 0000000..bfc2b54 --- /dev/null +++ b/composables/request.ts @@ -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 = NitroFetchOptions + >( + request: R, + opts?: O + ): Promise< + // @ts-ignore + TypedInternalResponse< + R, + T, + NitroFetchOptions extends O ? "get" : ExtractedRouteMethod + > + >; +} + +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; +}; diff --git a/composables/user.ts b/composables/user.ts index a86cb67..6fe8d85 100644 --- a/composables/user.ts +++ b/composables/user.ts @@ -12,5 +12,5 @@ export const updateUser = async () => { if (user.value === null) return; // SSR calls have to be after uses - user.value = await $fetch("/api/v1/user", { headers }); + user.value = await $dropFetch("/api/v1/user", { headers }); }; diff --git a/layouts/admin.vue b/layouts/admin.vue index 86a6183..b7d0013 100644 --- a/layouts/admin.vue +++ b/layouts/admin.vue @@ -167,6 +167,8 @@ import { Cog6ToothIcon, FlagIcon, BellIcon, + DocumentIcon, + UserGroupIcon, } from "@heroicons/vue/24/outline"; import type { NavigationItem } from "~/composables/types"; import { useCurrentNavigationIndex } from "~/composables/current-page-engine"; @@ -182,10 +184,16 @@ const navigation: Array = [ icon: ServerStackIcon, }, { - label: "Auth", - route: "/admin/auth", - prefix: "/admin/auth", - icon: LockClosedIcon, + label: "Meta", + route: "/admin/metadata", + prefix: "/admin/metadata", + icon: DocumentIcon, + }, + { + label: "Users", + route: "/admin/users", + prefix: "/admin/users", + icon: UserGroupIcon, }, { label: "Settings", diff --git a/pages/admin/index.vue b/pages/admin/index.vue index b8321b5..6068318 100644 --- a/pages/admin/index.vue +++ b/pages/admin/index.vue @@ -7,15 +7,24 @@
-
+

Library

- 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. +

+

+ + Check it out + +

- -
-
-
Games
-
- {{ libraryState.games.length }} -
-
-
-
Versions
-
- {{ - libraryState.games - .map((e) => e.game.versions.length) - .reduce((a, b) => a + b, 0) - }} -
-
-
-
+

- Performance + Users

-

- Lorem ipsum, dolor sit amet consectetur adipisicing elit maiores - impedit. +

+ Your users are people who can access your Drop instance, download + games from it, and configure API keys for it. +

+

+ + Check it out + +

-
-
-
diff --git a/pages/admin/library/[id]/import.vue b/pages/admin/library/[id]/import.vue index 3e688b1..47c1219 100644 --- a/pages/admin/library/[id]/import.vue +++ b/pages/admin/library/[id]/import.vue @@ -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', ]" > ({ - platform: "", + platform: undefined, launch: "", launchArgs: "", setup: "", @@ -582,7 +582,8 @@ const versionSettings = ref<{ umuId: "", }); -const versionGuesses = ref>(); +const versionGuesses = + ref>(); 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, diff --git a/pages/admin/library/[id]/index.vue b/pages/admin/library/[id]/index.vue index ca5f0f6..d8beecb 100644 --- a/pages/admin/library/[id]/index.vue +++ b/pages/admin/library/[id]/index.vue @@ -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, diff --git a/pages/admin/library/import.vue b/pages/admin/library/import.vue index 18ddeba..9baee28 100644 --- a/pages/admin/library/import.vue +++ b/pages/admin/library/import.vue @@ -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(); 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], diff --git a/pages/admin/library/index.vue b/pages/admin/library/index.vue index e50b9eb..fcdb834 100644 --- a/pages/admin/library/index.vue +++ b/pages/admin/library/index.vue @@ -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); } diff --git a/pages/admin/metadata/index.vue b/pages/admin/metadata/index.vue new file mode 100644 index 0000000..c7080d1 --- /dev/null +++ b/pages/admin/metadata/index.vue @@ -0,0 +1,11 @@ + + + diff --git a/pages/admin/auth/index.vue b/pages/admin/users/auth/index.vue similarity index 91% rename from pages/admin/auth/index.vue rename to pages/admin/users/auth/index.vue index 5f72b3f..7ac31f0 100644 --- a/pages/admin/auth/index.vue +++ b/pages/admin/users/auth/index.vue @@ -23,9 +23,7 @@ :key="authMech.name" class="overflow-hidden rounded-xl border border-zinc-800 bg-zinc-900" > -
+
= [ - { - 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) })); diff --git a/pages/admin/auth/simple/index.vue b/pages/admin/users/auth/simple/index.vue similarity index 99% rename from pages/admin/auth/simple/index.vue rename to pages/admin/users/auth/simple/index.vue index e30a4f0..e854dd4 100644 --- a/pages/admin/auth/simple/index.vue +++ b/pages/admin/users/auth/simple/index.vue @@ -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, diff --git a/pages/admin/users/index.vue b/pages/admin/users/index.vue new file mode 100644 index 0000000..4a4c0d4 --- /dev/null +++ b/pages/admin/users/index.vue @@ -0,0 +1,111 @@ + + + diff --git a/pages/client/[id]/callback.vue b/pages/client/[id]/callback.vue index 6e6e1ad..9d99224 100644 --- a/pages/client/[id]/callback.vue +++ b/pages/client/[id]/callback.vue @@ -175,7 +175,7 @@ const error = ref(); const authToken = ref(); 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 }, }); diff --git a/pages/register.vue b/pages/register.vue index eb77710..c16390f 100644 --- a/pages/register.vue +++ b/pages/register.vue @@ -224,7 +224,7 @@ const loading = ref(false); const error = ref(undefined); async function register() { - await $fetch("/api/v1/auth/signup/simple", { + await $dropFetch("/api/v1/auth/signup/simple", { method: "POST", body: { invitation: invitationId, diff --git a/pages/signin.vue b/pages/signin.vue index f7bdd56..58e42df 100644 --- a/pages/signin.vue +++ b/pages/signin.vue @@ -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("/api/v1/user"); + user.value = await $dropFetch("/api/v1/user"); } definePageMeta({ diff --git a/pages/signout.vue b/pages/signout.vue index 200e8f1..00cf4f7 100644 --- a/pages/signout.vue +++ b/pages/signout.vue @@ -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"); diff --git a/pages/store/[id]/index.vue b/pages/store/[id]/index.vue index fd3b184..f995f68 100644 --- a/pages/store/[id]/index.vue +++ b/pages/store/[id]/index.vue @@ -177,7 +177,7 @@ const gameId = route.params.id.toString(); const user = useUser(); const headers = useRequestHeaders(["cookie"]); -const game = await $fetch( +const game = await $dropFetch( `/api/v1/games/${gameId}`, { headers } ); diff --git a/pages/store/index.vue b/pages/store/index.vue index ac68f4e..60d547f 100644 --- a/pages/store/index.vue +++ b/pages/store/index.vue @@ -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", diff --git a/prisma/migrations/20250313053250_add_enable_fields_to_auth_and_users/migration.sql b/prisma/migrations/20250313053250_add_enable_fields_to_auth_and_users/migration.sql new file mode 100644 index 0000000..de27367 --- /dev/null +++ b/prisma/migrations/20250313053250_add_enable_fields_to_auth_and_users/migration.sql @@ -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; diff --git a/prisma/schema/auth.prisma b/prisma/schema/auth.prisma index ac3b5d0..bf862dd 100644 --- a/prisma/schema/auth.prisma +++ b/prisma/schema/auth.prisma @@ -3,8 +3,9 @@ enum AuthMec { } model LinkedAuthMec { - userId String - mec AuthMec + userId String + mec AuthMec + enabled Boolean @default(true) credentials Json @@ -29,7 +30,7 @@ enum APITokenMode { model APIToken { id String @id @default(uuid()) - token String @default(uuid()) @unique + token String @unique @default(uuid()) mode APITokenMode name String diff --git a/prisma/schema/user.prisma b/prisma/schema/user.prisma index a664f50..b450163 100644 --- a/prisma/schema/user.prisma +++ b/prisma/schema/user.prisma @@ -2,6 +2,7 @@ model User { id String @id @default(uuid()) username String @unique admin Boolean @default(false) + enabled Boolean @default(true) email String displayName String diff --git a/server/api/v1/admin/auth/index.get.ts b/server/api/v1/admin/auth/index.get.ts new file mode 100644 index 0000000..3a35b51 --- /dev/null +++ b/server/api/v1/admin/auth/index.get.ts @@ -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; +}); diff --git a/server/api/v1/admin/users/[id]/index.get.ts b/server/api/v1/admin/users/[id]/index.get.ts new file mode 100644 index 0000000..f90db23 --- /dev/null +++ b/server/api/v1/admin/users/[id]/index.get.ts @@ -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; +}); diff --git a/server/api/v1/admin/user/index.get.ts b/server/api/v1/admin/users/index.get.ts similarity index 60% rename from server/api/v1/admin/user/index.get.ts rename to server/api/v1/admin/users/index.get.ts index 8b1df15..6efc549 100644 --- a/server/api/v1/admin/user/index.get.ts +++ b/server/api/v1/admin/users/index.get.ts @@ -5,7 +5,18 @@ export default defineEventHandler(async (h3) => { const allowed = await aclManager.allowSystemACL(h3, ["user:read"]); 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; }); diff --git a/server/api/v1/auth/signin/simple.post.ts b/server/api/v1/auth/signin/simple.post.ts index 996bf2b..db0ad35 100644 --- a/server/api/v1/auth/signin/simple.post.ts +++ b/server/api/v1/auth/signin/simple.post.ts @@ -5,34 +5,57 @@ import { checkHash } from "~/server/internal/security/simple"; import sessionHandler from "~/server/internal/session"; export default defineEventHandler(async (h3) => { - const body = await readBody(h3); + const body = await readBody(h3); - const username = body.username; - const password = body.password; - const rememberMe = body.rememberMe ?? false; - if (username === undefined || password === undefined) - throw createError({ statusCode: 403, statusMessage: "Username or password missing from request." }); - - const authMek = await prisma.linkedAuthMec.findFirst({ - where: { - mec: AuthMec.Simple, - credentials: { - array_starts_with: username - } - } + const username = body.username; + const password = body.password; + const rememberMe = body.rememberMe ?? false; + if (username === undefined || password === undefined) + throw createError({ + statusCode: 403, + statusMessage: "Username or password missing from request.", }); - 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; - const hash = credentials.at(1); + if (!authMek) + 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())) - throw createError({ statusCode: 401, statusMessage: "Invalid username or password." }); + if (!hash || !authMek.user.enabled) + 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 } -}); \ No newline at end of file + await sessionHandler.setUserId(h3, authMek.userId, rememberMe); + + return { result: true, userId: authMek.userId }; +}); diff --git a/server/internal/acls/descriptions.ts b/server/internal/acls/descriptions.ts index de88a2e..2d3f560 100644 --- a/server/internal/acls/descriptions.ts +++ b/server/internal/acls/descriptions.ts @@ -35,6 +35,7 @@ export const userACLDescriptions: ObjectFromList = { }; export const systemACLDescriptions: ObjectFromList = { + "auth:read": "Fetch the list of enabled authentication mechanisms configured.", "auth:simple:invitation:read": "Fetch simple auth invitations.", "auth:simple:invitation:new": "Create new simple auth invitations.", "auth:simple:invitation:delete": "Delete a simple auth invitation.", diff --git a/server/internal/acls/index.ts b/server/internal/acls/index.ts index 7e27876..089bb8f 100644 --- a/server/internal/acls/index.ts +++ b/server/internal/acls/index.ts @@ -33,6 +33,7 @@ const userACLPrefix = "user:"; type UserACL = Array<(typeof userACLs)[number]>; export const systemACLs = [ + "auth:read", "auth:simple:invitation:read", "auth:simple:invitation:new", "auth:simple:invitation:delete", diff --git a/server/internal/config/application-configuration.ts b/server/internal/config/application-configuration.ts index ac4f7c4..25f3c0f 100644 --- a/server/internal/config/application-configuration.ts +++ b/server/internal/config/application-configuration.ts @@ -3,20 +3,12 @@ import prisma from "../db/database"; class ApplicationConfiguration { // Reference to the currently selected application configuration - private currentApplicationSettings: ApplicationSettings = { - timestamp: new Date(), - enabledAuthencationMechanisms: [], - metadataProviders: [], - }; - private applicationStateProxy: object; - private dirty: boolean = false; - private dirtyPromise: Promise | undefined = undefined; - - constructor() { - this.applicationStateProxy = {}; - } + private currentApplicationSettings: ApplicationSettings | undefined = + undefined; private async save() { + await this.init(); + const deepAppConfigCopy: Omit & { timestamp?: Date; } = 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 async initialiseConfiguration() { const initialState = await prisma.applicationSettings.create({ @@ -56,6 +61,10 @@ class ApplicationConfiguration { key: T, value: ApplicationSettings[T] ) { + await this.init(); + if (!this.currentApplicationSettings) + throw new Error("Somehow, failed to initialise application settings"); + if (this.currentApplicationSettings[key] !== value) { this.currentApplicationSettings[key] = value; @@ -63,7 +72,11 @@ class ApplicationConfiguration { } } - get(key: T): ApplicationSettings[T] { + async get(key: T): Promise { + await this.init(); + if (!this.currentApplicationSettings) + throw new Error("Somehow, failed to initialise application settings"); + return this.currentApplicationSettings[key]; } } diff --git a/server/internal/objects/objectHandler.ts b/server/internal/objects/objectHandler.ts index 8ea4803..9d67185 100644 --- a/server/internal/objects/objectHandler.ts +++ b/server/internal/objects/objectHandler.ts @@ -125,7 +125,7 @@ export abstract class ObjectBackend { // If we need to fetch a remote resource, it doesn't make sense // to immediately fetch the object, *then* check permissions. // 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. async writeWithPermissions( id: ObjectReference, diff --git a/server/plugins/01.system-init.ts b/server/plugins/01.system-init.ts index aebc802..eb10ab4 100644 --- a/server/plugins/01.system-init.ts +++ b/server/plugins/01.system-init.ts @@ -2,14 +2,6 @@ import { applicationSettings } from "../internal/config/application-configuratio import prisma from "../internal/db/database"; 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 // The system user owns any user-based code // that we want to re-use for the app diff --git a/server/plugins/03.metadata-init.ts b/server/plugins/03.metadata-init.ts index a4fd6de..20c7adc 100644 --- a/server/plugins/03.metadata-init.ts +++ b/server/plugins/03.metadata-init.ts @@ -27,7 +27,9 @@ export default defineNitroPlugin(async (nitro) => { } // 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; for (const [index, providerId] of configuredProviderList.entries()) { const priority = max * 2 - index; // Offset by the length --- (max - index) + max