diff --git a/changelog.md b/changelog.md index 18266e7..ac147b2 100644 --- a/changelog.md +++ b/changelog.md @@ -193,6 +193,7 @@ _changelog generated by_ [go-conventional-commits](https://github.com/joselitofi ## Release 0.2.0-beta ### Fixes + - fix recursive dirs util #02d6346 - Fix username length requirement #0a5a649 - remove dynamic imports #0f10626 @@ -223,8 +224,8 @@ _changelog generated by_ [go-conventional-commits](https://github.com/joselitofi - fix FATAL: "root"... message #dbb315a - only show versions that are directories #ef8f3ae - ### Features + - update prisma & delete games #089c3e0 - manual handshake #12e3125 - fetch game endpoint #1f4d075 @@ -271,9 +272,9 @@ _changelog generated by_ [go-conventional-commits](https://github.com/joselitofi - add support for overriding UMU id #fd4a7d1 - add .sh for linux #fe9373a - ### Other Changes -- quexeky + +- quexeky - fixed manifest generation #03a37f7 - manual ci/cd #03b0b0c - ability to fetch client certs for p2p #0a715fe @@ -379,7 +380,6 @@ _changelog generated by_ [go-conventional-commits](https://github.com/joselitofi - better server side signin redirects #ef13b68 - patch signin #f3672f8 - _changelog generated by_ [go-conventional-commits](https://github.com/joselitofilho/go-conventional-commits) ## Release 0.1.0-beta diff --git a/components/LanguageSelector.vue b/components/LanguageSelector.vue index 496f8e1..9d873c1 100644 --- a/components/LanguageSelector.vue +++ b/components/LanguageSelector.vue @@ -1,80 +1,6 @@ - - diff --git a/components/LanguageSelectorListbox.vue b/components/LanguageSelectorListbox.vue new file mode 100644 index 0000000..b86bf82 --- /dev/null +++ b/components/LanguageSelectorListbox.vue @@ -0,0 +1,155 @@ + + + diff --git a/components/Setup/Account.vue b/components/Setup/Account.vue new file mode 100644 index 0000000..017125b --- /dev/null +++ b/components/Setup/Account.vue @@ -0,0 +1,159 @@ + + + diff --git a/components/Setup/Library.vue b/components/Setup/Library.vue new file mode 100644 index 0000000..3d4b3f5 --- /dev/null +++ b/components/Setup/Library.vue @@ -0,0 +1,15 @@ + + + diff --git a/composables/request.ts b/composables/request.ts index 963de6d..8396b8d 100644 --- a/composables/request.ts +++ b/composables/request.ts @@ -67,7 +67,7 @@ export const $dropFetch: DropFetch = async (rawRequest, opts) => { try { const data = await $fetch(request, { ...opts, - headers: { ...opts?.headers, ...headers }, + headers: { ...headers, ...opts?.headers }, }); if (import.meta.server) state.value = data; return data; diff --git a/i18n/locales/en_au.json b/i18n/locales/en_au.json index 0967ef4..27d8b00 100644 --- a/i18n/locales/en_au.json +++ b/i18n/locales/en_au.json @@ -1 +1,5 @@ -{} +{ + "setup": { + "welcome": "G'day." + } +} diff --git a/i18n/locales/en_us.json b/i18n/locales/en_us.json index 67408f9..58e16c9 100644 --- a/i18n/locales/en_us.json +++ b/i18n/locales/en_us.json @@ -63,6 +63,38 @@ "signout": "Signout", "username": "Username" }, + "setup": { + "welcome": "Hey there.", + "welcomeDescription": "Welcome to Drop setup wizard. It will walk you through configuring Drop for the first time, and how it works.", + "finish": "Let's go {arrow}", + "noPage": "no page", + "auth": { + "title": "Authentication", + "description": "Authentication in Drop happens through multiple configured 'providers'. Each one can allow users to sign-in through their method. To get started, have at least one authentication provider enabled, and create an account through it.", + "docs": "Documentation {arrow}", + "enabled": "Enabled?", + "simple": { + "title": "Simple authentication", + "description": "Simple authentication uses username/password to authentication users. It is enabled by default if no other authentication provider is enabled.", + "register": "Register as admin {arrow}" + }, + "openid": { + "title": "OpenID Connect", + "description": "OpenID Connect (OIDC) is an OAuth2 extension commonly supported. Drop requires OIDC configuration to be done via environment variables.", + "skip": "I have a user with OIDC" + } + }, + "stages": { + "account": { + "name": "Setup your admin account.", + "description": "You need at least one account to start using Drop." + }, + "library": { + "name": "Create a library.", + "description": "Add at least one library source to use Drop." + } + } + }, "cancel": "Cancel", "chars": { "arrow": "→", diff --git a/package.json b/package.json index 84a1836..77fb6c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "drop", - "version": "0.3.0-alpha.0", + "version": "0.3.0", "private": true, "type": "module", "license": "AGPL-3.0-or-later", diff --git a/pages/admin/library/sources/index.vue b/pages/admin/library/sources/index.vue index 251da85..d7b6c14 100644 --- a/pages/admin/library/sources/index.vue +++ b/pages/admin/library/sources/index.vue @@ -296,8 +296,15 @@ useHead({ const { t } = useI18n(); +// Optional token for setup wizard +const { token = undefined } = defineProps<{ token?: string }>(); + +const headers = token ? { Authorization: token } : undefined; + const sources = ref( - await $dropFetch("/api/v1/admin/library/sources"), + await $dropFetch("/api/v1/admin/library/sources", { + headers, + }), ); const editIndex = ref(undefined); @@ -345,6 +352,7 @@ async function performActionSource() { options: sourceConfig.value, }, method: createMode ? "POST" : "PATCH", + headers, }, ); if (createMode) { @@ -394,6 +402,7 @@ async function deleteSource(index: number) { await $dropFetch("/api/v1/admin/library/sources", { method: "DELETE", body: { id: source.id }, + headers, }); } catch (e) { createModal( diff --git a/pages/auth/register.vue b/pages/auth/register.vue index d2820ae..0784eb1 100644 --- a/pages/auth/register.vue +++ b/pages/auth/register.vue @@ -264,6 +264,10 @@ function register_wrapper() { loading.value = true; register() .then(() => { + if (route.query.after == "close") { + window.close(); + return; + } router.push("/auth/signin"); }) .catch((response) => { diff --git a/pages/news.vue b/pages/news.vue index a984018..ee9fac4 100644 --- a/pages/news.vue +++ b/pages/news.vue @@ -105,7 +105,6 @@ const news = useNews(); if (!news.value) { await fetchNews(); - console.log("fetched news"); } const { t } = useI18n(); diff --git a/pages/setup.vue b/pages/setup.vue new file mode 100644 index 0000000..2bf06f8 --- /dev/null +++ b/pages/setup.vue @@ -0,0 +1,227 @@ + + + + + diff --git a/prisma/schema/content.prisma b/prisma/schema/content.prisma deleted file mode 100644 index f64fdac..0000000 --- a/prisma/schema/content.prisma +++ /dev/null @@ -1,101 +0,0 @@ -enum MetadataSource { - Manual - GiantBomb -} - -model Game { - id String @id @default(uuid()) - - metadataSource MetadataSource - metadataId String - created DateTime @default(now()) - - // Any field prefixed with m is filled in from metadata - // Acts as a cache so we can search and filter it - mName String // Name of game - mShortDescription String // Short description - mDescription String // Supports markdown - mDevelopers Developer[] - mPublishers Publisher[] - mReleased DateTime // When the game was released - - mReviewCount Int - mReviewRating Float // 0 to 1 - - mIconId String // linked to objects in s3 - mBannerId String // linked to objects in s3 - mCoverId String - mImageCarousel String[] // linked to below array - mImageLibrary String[] // linked to objects in s3 - - versions GameVersion[] - libraryBasePath String @unique // Base dir for all the game versions - - collections CollectionEntry[] - - @@unique([metadataSource, metadataId], name: "metadataKey") -} - -// A particular set of files that relate to the version -model GameVersion { - gameId String - game Game @relation(fields: [gameId], references: [id], onDelete: Cascade) - versionName String // Sub directory for the game files - - created DateTime @default(now()) - - platform Platform - - launchCommand String @default("") // Command to run to start. Platform-specific. Windows games on Linux will wrap this command in Proton/Wine - launchArgs String[] - setupCommand String @default("") // Command to setup game (dependencies and such) - setupArgs String[] - onlySetup Boolean @default(false) - - umuIdOverride String? - - dropletManifest Json // Results from droplet - - versionIndex Int - delta Boolean @default(false) - - @@id([gameId, versionName]) -} - -model Developer { - id String @id @default(uuid()) - - metadataSource MetadataSource - metadataId String - metadataOriginalQuery String - - mName String - mShortDescription String - mDescription String - mLogo String - mBanner String - mWebsite String - - games Game[] - - @@unique([metadataSource, metadataId, metadataOriginalQuery], name: "metadataKey") -} - -model Publisher { - id String @id @default(uuid()) - - metadataSource MetadataSource - metadataId String - metadataOriginalQuery String - - mName String - mShortDescription String - mDescription String - mLogo String - mBanner String - mWebsite String - - games Game[] - - @@unique([metadataSource, metadataId, metadataOriginalQuery], name: "metadataKey") -} diff --git a/server/api/v1/admin/auth/index.get.ts b/server/api/v1/admin/auth/index.get.ts index 6d975e9..4ea1523 100644 --- a/server/api/v1/admin/auth/index.get.ts +++ b/server/api/v1/admin/auth/index.get.ts @@ -3,7 +3,7 @@ import aclManager from "~/server/internal/acls"; import authManager from "~/server/internal/auth"; export default defineEventHandler(async (h3) => { - const allowed = await aclManager.allowSystemACL(h3, ["auth:read"]); + const allowed = await aclManager.allowSystemACL(h3, ["auth:read", "setup"]); if (!allowed) throw createError({ statusCode: 403 }); const enabledAuthManagers = authManager.getAuthProviders(); diff --git a/server/api/v1/admin/auth/invitation/index.post.ts b/server/api/v1/admin/auth/invitation/index.post.ts index 50696c2..69d3649 100644 --- a/server/api/v1/admin/auth/invitation/index.post.ts +++ b/server/api/v1/admin/auth/invitation/index.post.ts @@ -14,6 +14,7 @@ const CreateInvite = SharedRegisterValidator.partial() export default defineEventHandler(async (h3) => { const allowed = await aclManager.allowSystemACL(h3, [ "auth:simple:invitation:new", + "setup", ]); if (!allowed) throw createError({ statusCode: 403 }); diff --git a/server/api/v1/admin/index.get.ts b/server/api/v1/admin/index.get.ts new file mode 100644 index 0000000..9d52d0f --- /dev/null +++ b/server/api/v1/admin/index.get.ts @@ -0,0 +1,7 @@ +import aclManager from "~/server/internal/acls"; + +export default defineEventHandler(async (h3) => { + const allowed = await aclManager.allowSystemACL(h3, []); + if (!allowed) return false; + return true; +}); diff --git a/server/api/v1/admin/library/sources/index.delete.ts b/server/api/v1/admin/library/sources/index.delete.ts index 8067f64..165a9b2 100644 --- a/server/api/v1/admin/library/sources/index.delete.ts +++ b/server/api/v1/admin/library/sources/index.delete.ts @@ -12,6 +12,7 @@ export default defineEventHandler<{ body: typeof DeleteLibrarySource.infer }>( async (h3) => { const allowed = await aclManager.allowSystemACL(h3, [ "library:sources:delete", + "setup", ]); if (!allowed) throw createError({ statusCode: 403 }); diff --git a/server/api/v1/admin/library/sources/index.get.ts b/server/api/v1/admin/library/sources/index.get.ts index 573be59..bf56134 100644 --- a/server/api/v1/admin/library/sources/index.get.ts +++ b/server/api/v1/admin/library/sources/index.get.ts @@ -5,7 +5,10 @@ import libraryManager from "~/server/internal/library"; export type WorkingLibrarySource = LibraryModel & { working: boolean }; export default defineEventHandler(async (h3) => { - const allowed = await aclManager.allowSystemACL(h3, ["library:sources:read"]); + const allowed = await aclManager.allowSystemACL(h3, [ + "library:sources:read", + "setup", + ]); if (!allowed) throw createError({ statusCode: 403 }); const sources = await libraryManager.fetchLibraries(); diff --git a/server/api/v1/admin/library/sources/index.patch.ts b/server/api/v1/admin/library/sources/index.patch.ts index be0ccc7..f098b7a 100644 --- a/server/api/v1/admin/library/sources/index.patch.ts +++ b/server/api/v1/admin/library/sources/index.patch.ts @@ -16,6 +16,7 @@ export default defineEventHandler<{ body: typeof UpdateLibrarySource.infer }>( async (h3) => { const allowed = await aclManager.allowSystemACL(h3, [ "library:sources:update", + "setup", ]); if (!allowed) throw createError({ statusCode: 403 }); diff --git a/server/api/v1/admin/library/sources/index.post.ts b/server/api/v1/admin/library/sources/index.post.ts index 31a703f..ae32b75 100644 --- a/server/api/v1/admin/library/sources/index.post.ts +++ b/server/api/v1/admin/library/sources/index.post.ts @@ -18,6 +18,7 @@ export default defineEventHandler<{ body: typeof CreateLibrarySource.infer }>( async (h3) => { const allowed = await aclManager.allowSystemACL(h3, [ "library:sources:new", + "setup", ]); if (!allowed) throw createError({ statusCode: 403 }); diff --git a/server/api/v1/setup.post.ts b/server/api/v1/setup.post.ts new file mode 100644 index 0000000..acd83f6 --- /dev/null +++ b/server/api/v1/setup.post.ts @@ -0,0 +1,20 @@ +import { APITokenMode } from "~/prisma/client/enums"; +import aclManager from "~/server/internal/acls"; +import prisma from "~/server/internal/db/database"; + +export default defineEventHandler(async (h3) => { + const allowed = await aclManager.allowSystemACL(h3, ["setup"]); + if (!allowed) + throw createError({ + statusCode: 403, + statusMessage: "Must use a setup token.", + }); + await prisma.aPIToken.deleteMany({ + where: { + mode: APITokenMode.System, + acls: { + hasSome: ["setup"], + }, + }, + }); +}); diff --git a/server/internal/acls/descriptions.ts b/server/internal/acls/descriptions.ts index 98b919d..ebb8d50 100644 --- a/server/internal/acls/descriptions.ts +++ b/server/internal/acls/descriptions.ts @@ -44,6 +44,9 @@ export const userACLDescriptions: ObjectFromList = { }; export const systemACLDescriptions: ObjectFromList = { + setup: + "All permissions required to setup a new Drop instance (setup wizard).", + "auth:read": "Fetch the list of enabled authentication mechanisms configured.", "auth:simple:invitation:read": "Fetch simple auth invitations.", diff --git a/server/internal/acls/index.ts b/server/internal/acls/index.ts index 2c2f1c6..d6997c5 100644 --- a/server/internal/acls/index.ts +++ b/server/internal/acls/index.ts @@ -41,6 +41,8 @@ const userACLPrefix = "user:"; export type UserACL = Array<(typeof userACLs)[number]>; export const systemACLs = [ + "setup", + "auth:read", "auth:simple:invitation:read", "auth:simple:invitation:new", @@ -167,9 +169,11 @@ class ACLManager { const user = await prisma.user.findUnique({ where: { id: userSession.userId }, }); - if (!user) return false; - if (user.admin) return true; - return false; + if (user) { + if (!user) return false; + if (user.admin) return true; + return false; + } } const authorizationToken = this.getAuthorizationToken(request); @@ -179,6 +183,10 @@ class ACLManager { }); if (!token) return false; if (token.mode != APITokenMode.System) return false; + + // If empty, we just want to check we are an admin *at all*, not specific ACLs + if (acls.length == 0) return true; + for (const acl of acls) { const tokenACLIndex = token.acls.findIndex((e) => e == acl); if (tokenACLIndex != -1) return true; diff --git a/server/plugins/02.setup-admin.ts b/server/plugins/02.setup-admin.ts index 1d4a128..25a8704 100644 --- a/server/plugins/02.setup-admin.ts +++ b/server/plugins/02.setup-admin.ts @@ -1,6 +1,18 @@ +import { APITokenMode } from "~/prisma/client/enums"; import prisma from "~/server/internal/db/database"; +import { systemConfig } from "../internal/config/sys-conf"; +import { logger } from "../internal/logging"; export default defineNitroPlugin(async (_nitro) => { + await prisma.aPIToken.deleteMany({ + where: { + acls: { + hasSome: ["setup"], + }, + mode: APITokenMode.System, + }, + }); + const userCount = await prisma.user.count({ where: { id: { not: "system" } }, }); @@ -10,18 +22,14 @@ export default defineNitroPlugin(async (_nitro) => { // but has not been configured // so it should be in-place - // Create admin invitation - await prisma.invitation.upsert({ - where: { - id: "admin", - }, - create: { - id: "admin", - isAdmin: true, - expires: new Date("4096-01-01"), - }, - update: { - isAdmin: true, + const token = await prisma.aPIToken.create({ + data: { + name: "Setup Wizard", + mode: APITokenMode.System, + acls: ["setup"], }, }); + + const setupUrl = `${systemConfig.getExternalUrl()}/setup?token=${token.token}`; + logger.info(`Open ${setupUrl} in a browser to get started with Drop.`); });