diff --git a/package.json b/package.json index 2791c58..36f52ea 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@algolia/autocomplete-core": "^1.19.2", "@headlessui/react": "^2.2.6", + "@heroicons/react": "^2.2.0", "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", "@next/mdx": "^15", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eae9cef..e794640 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@headlessui/react': specifier: ^2.2.6 version: 2.2.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@heroicons/react': + specifier: ^2.2.0 + version: 2.2.0(react@19.1.1) '@mdx-js/loader': specifier: ^3.1.0 version: 3.1.0(acorn@8.15.0) @@ -284,6 +287,11 @@ packages: react: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc + '@heroicons/react@2.2.0': + resolution: {integrity: sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==} + peerDependencies: + react: '>= 16 || ^19.0.0-rc' + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -2886,6 +2894,10 @@ snapshots: react-dom: 19.1.1(react@19.1.1) use-sync-external-store: 1.5.0(react@19.1.1) + '@heroicons/react@2.2.0(react@19.1.1)': + dependencies: + react: 19.1.1 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': diff --git a/src/app/guides/web/page.mdx b/src/app/guides/web/page.mdx index f2b1f1d..a61e0f4 100644 --- a/src/app/guides/web/page.mdx +++ b/src/app/guides/web/page.mdx @@ -52,6 +52,8 @@ With the payload, you can create a URL, depending on the kind of token you want When the user visits the URL, it will automatically open the token generation modal with your values pre-filled. + + ## Authentication To authenticate requests to the Drop API, use your token in the Authorization header, like so: diff --git a/src/app/web/import/page.mdx b/src/app/web/import/page.mdx new file mode 100644 index 0000000..593c746 --- /dev/null +++ b/src/app/web/import/page.mdx @@ -0,0 +1,587 @@ +export const metadata = { + title: 'Import', + description: + "On this page, we'll dive into how to import games and versions on Drop, and the options for both.", +} + +# Import + +While games and versions should be covered in separate sections, importing is a complicated enough of a process to warrant a separate page. Importing is the process of pulling and providing metadata for various complex objects in Drop, namely games and versions. + +Both games and versions in Drop are required to imported manually, due to them having additional metadata that must be user-provided. + +## Game metadata + +Game metadata is provided by a series of backend 'metadata providers'. Drop unifies them all into a single API to import the metadata, and handle authentication seamlessly. + +--- + +## Fetch unimported games {{ tag: 'GET', label: '/api/v1/admin/import/game', apilevel: "system", acl: "import:game:read" }} + + + + + This endpoint fetches all unimported games on the instance. + + + + + + + ```bash {{ title: 'cURL' }} + curl -G http://localhost:3000/api/v1/admin/import/game \ + -H "Authorization: Bearer {token}" + ``` + + ```js + const response = await fetch("http://localhost:3000/api/v1/admin/import/game", { + headers: { + Authorization: "Bearer {token}" + }, + }); + + const results = await response.json(); + + ``` + + + + ```json {{ title: 'Response' }} + { + "unimportedGames": [ + { + "game": "Abiotic Factor", + "library": { + "id": "8dc4b769-090f-4aec-b73a-d8fafc84f418", + "name": "Example Library", + "backend": "Filesystem", + "options": { + "baseDir": "./.data/library" + }, + "working": true + } + }, + { + "game": "Balatro", + "library": { + "id": "8dc4b769-090f-4aec-b73a-d8fafc84f418", + "name": "Example Library", + "backend": "Filesystem", + "options": { + "baseDir": "./.data/library" + }, + "working": true + } + }, + { + "game": "SuperTuxKart", + "library": { + "id": "8dc4b769-090f-4aec-b73a-d8fafc84f418", + "name": "Example Library", + "backend": "Filesystem", + "options": { + "baseDir": "./.data/library" + }, + "working": true + } + } + ] + } + ``` + + + + +--- + +## Search metadata {{ tag: 'GET', label: '/api/v1/admin/import/game/search', apilevel: "system", acl: "import:game:read" }} + + + + + This endpoint searches all metadata providers for a query. + + + ### Query parameters + + + + URL-encoded query you want to search. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl -G http://localhost:3000/api/v1/admin/import/game/search?q={query} \ + -H "Authorization: Bearer {token}" + ``` + + ```js + const response = await fetch("http://localhost:3000/api/v1/admin/import/game?q={query}", { + headers: { + Authorization: "Bearer {token}" + }, + }); + + const results = await response.json(); + + ``` + + + + ```json {{ title: 'Response' }} + [ + { + "id": "182101", + "name": "Bird by Example", + "icon": "https://images.igdb.com/igdb/image/upload/t_thumb/co587b.jpg", + "description": "Deep Learning Bird RPG", + "year": 0, + "sourceId": "IGDB", + "sourceName": "IGDB", + "fuzzy": 1 + }, + { + "id": "31640", + "name": "Glory by Example", + "icon": "", + "description": "A story driven, visual novel inspired, narrative experience chronicling the mysteries of an artificial island city and the tragedy of its few remaining survivors following the downfall of mankind by a world-wide virus outbreak and subsequent global flood. *Based on the released novel!", + "year": 2016, + "sourceId": "IGDB", + "sourceName": "IGDB", + "fuzzy": 1 + }, + { + "id": "289018", + "name": "Example Block Game", + "icon": "https://images.igdb.com/igdb/image/upload/t_thumb/co7u03.jpg", + "description": "Example Block Game is an open source implementation of basic block stacking in Lua/LÖVE. Featuring its own standardized kick table, the game has over 7 modes in total to use as reference for implementing your own gamemodes in its extensible engine.\n\nIn most modes, gameplay continues until you top out. High scores are saved per mode and can be viewed at any time from the main menu.", + "year": 2022, + "sourceId": "IGDB", + "sourceName": "IGDB", + "fuzzy": 1 + } + ] + ``` + + + + +--- + +## Import game {{ tag: 'POST', label: '/api/v1/admin/import/game', apilevel: "system", acl: "import:game:new" }} + + + + + This endpoint searches all metadata providers for a query. + + + ### Request parameters + + + + The ID of the library you're importing from. Fetched from `library.id` on the GET endpoint. + + + Path of the game you're importing. Fetched from the `game` on the GET endpoint. + + + Optional, metadata to import from. It requires three fields if set: + ```json + { + "id": "game ID", + "sourceId": "source ID", + "name": "Name of game" + } + ``` + + All these properties are returned from the search endpoint. While you can guess these values, as they are generally the internal IDs of the respective platforms, they *are* internal values and are not recommended to be guessed. + + For example, if you had the game already from IGDB, you may be able to use: + ```json + { + "id": "", + "sourceId": "IGDB", + "name": "" + } + ``` + + Without searching for the game first. *This is officially not recommended, but we are unlikely to break this behaviour.* + + + + + ### Response + For the response and how to use task IDs, see [Tasks](/web/tasks). You need the `import:game:read` ACL to connect. + + + + + + + ```bash {{ title: 'cURL' }} + curl -X POST http://localhost:3000/api/v1/admin/import/game \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d "{ ... }" + + ``` + + ```js + const response = await fetch("http://localhost:3000/api/v1/admin/import", { + headers: { + Authorization: "Bearer {token}" + }, + method: "POST", + body: { + library: "8dc4b769-090f-4aec-b73a-d8fafc84f418", + path: "SuperTuxKart", + metadata: { + id: "289018", + sourceId: "IGDB", + name: "Example Block Game" + } + } + }); + + const { taskId } = await response.json(); + + ``` + + + + ```json {{ title: 'Response' }} + { + "taskId": "..." + } + ``` + + + + +## Versions + +Versions, in Drop, require a lot of metadata to be imported correctly, due to the various configurations that Drop supports. For that reason, the import endpoint is split into multiple by purpose. + +--- + +## Fetch unimported versions {{ tag: 'GET', label: '/api/v1/admin/import/version', apilevel: "system", acl: "import:version:read" }} + + + + + This endpoint fetches all unimported versions on the instance. + + ### Query parameters + + + + The game ID to fetch the unimported versions from. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl -G http://localhost:3000/api/v1/admin/import/version?id={game id} \ + -H "Authorization: Bearer {token}" + ``` + + ```js + const response = await fetch("http://localhost:3000/api/v1/admin/import/version?id={game id}", { + headers: { + Authorization: "Bearer {token}" + }, + }); + + const versions = await response.json(); + + ``` + + + + ```json {{ title: 'Response' }} + { + "versionName", + "version2Name", + "thatOtherVersion" + } + ``` + + + + +--- + +## Fetch unimported version 'preload' (metadata) {{ tag: 'GET', label: '/api/v1/admin/import/version/preload', apilevel: "system", acl: "import:version:read" }} + + + + + This endpoint fetches helpful metadata for unimported versions, and takes guesses on the type of version it is. + + You could thereotically use this endpoint to auto-import versions, but Drop opts for a manual process. + + ### Query parameters + + + + The game ID of the unimported version. + + + The version name of the unimported version. + + + + + ### Response properties + This endpoint returns a **sorted** list of launch/setup command guesses. + + + The relative filename of the executable (includes folders, like `SubDir/MyGame.exe`). + + + The platform this guess is for. Currently either "Windows", "Linux", or "macOS" + + + How closely the name of the executable matches the name of the game. It's used to make educated guesses on which executable is the right one, as games usually name their executables the same as the game name. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl -G http://localhost:3000/api/v1/admin/import/version/preload?id={game id}&version={version name} \ + -H "Authorization: Bearer {token}" + ``` + + ```js + const response = await fetch("http://localhost:3000/api/v1/admin/import/version/preload?id={game id}&version={version name}", { + headers: { + Authorization: "Bearer {token}" + }, + }); + + const versions = await response.json(); + + ``` + + + + ```json {{ title: 'Response' }} + [ + { + "filename": "run.sh", + "platform": "Linux", + "match": 0.332 + }, + { + "filename": "MyGame", + "platform": "Windows", + "match": 0.1 + } + ] + ``` + + + + +--- + +## Portable, import version {{ tag: 'POST', label: '/api/v1/admin/import/version', apilevel: "system", acl: "import:version:new" }} + + + + + This way of using this endpoint is to import the game as a **portable** executable, meaning that there are separate launch commands, and an optional setup command. + + ### Request parameters + + + + The game ID of the unimported version. + + + The version name of the unimported version. + + + The platform this version is for. Must be, currently, either "Windows", "Linux", or "macOS". You can mess up the capitalisation. + + + Whether or not this version is a 'delta' version. Read more about delta versions on the main docs: [Version deltas & ordering](https://docs.droposs.org/docs/library#version-deltas--ordering) + + + Set as `false` for this example. + + + The command to launch the game. + + + Additional arguments to pass to the game. + + + Optional, a command to set up the game. If it returns successfully, the game is considered successfully installed, and clients will allow the user to run the `launch` command. + + + Optional, additional arguments to pass to the setup executable. + + + Optional, override the UMU ID for this game. Read more on the [UMU database](https://github.com/Open-Wine-Components/umu-database). + + + + ### Response + For the response and how to use task IDs, see [Tasks](/web/tasks). You need the `import:version:read` ACL to connect. + + + + + + + ```bash {{ title: 'cURL' }} + curl -X POST http://localhost:3000/api/v1/admin/import/version \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d "{ ... }" + + ``` + + ```js + const response = await fetch("http://localhost:3000/api/v1/admin/import/version", { + headers: { + Authorization: "Bearer {token}" + }, + method: "POST", + body: { + id: "...", + version: "myVersion", + platform: "Linux", + + delta: false, + onlySetup: false, // Required for this example + launch: "run.sh", + + // ... rest of the fields are optional + } + }); + + const { taskId } = await response.json(); + + ``` + + + + ```json {{ title: 'Response' }} + { + "taskId": "..." + } + ``` + + + + +--- + +## Setup-only, import version {{ tag: 'POST', label: '/api/v1/admin/import/version', apilevel: "system", acl: "import:version:new" }} + + + + + This way of using this endpoint is to import the game as a **setup-only** executable, meaning that Drop just executes an installer **and that's it**. This is how many repacked and GOG games come, but it is recommended you install/unpack them first and then add them as portable games. + + ### Request parameters + + + + The game ID of the unimported version. + + + The version name of the unimported version. + + + The platform this version is for. Must be, currently, either "Windows", "Linux", or "macOS". You can mess up the capitalisation. + + + Whether or not this version is a 'delta' version. Read more about delta versions on the main docs: [Version deltas & ordering](https://docs.droposs.org/docs/library#version-deltas--ordering) + + + Set as `true` for this example. + + + Not required for this example. + + + Not required for this example. + + + A command to set up the game. Users will be able to run it multiple times, the installer should repair the installation. + + + Optional, additional arguments to pass to the setup executable. + + + Optional, override the UMU ID for this game. Read more on the [UMU database](https://github.com/Open-Wine-Components/umu-database). + + + + ### Response + For the response and how to use task IDs, see [Tasks](/web/tasks). You need the `import:version:read` ACL to connect. + + + + + + + ```bash {{ title: 'cURL' }} + curl -X POST http://localhost:3000/api/v1/admin/import/version \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d "{ ... }" + + ``` + + ```js + const response = await fetch("http://localhost:3000/api/v1/admin/import/version", { + headers: { + Authorization: "Bearer {token}" + }, + method: "POST", + body: { + id: "...", + version: "myVersion", + platform: "Linux", + + delta: false, + onlySetup: true, // Required for this example + setup: "setup.sh", + setupArgs: "--gui" + + // ... rest of the fields are optional + } + }); + + const { taskId } = await response.json(); + + ``` + + + + ```json {{ title: 'Response' }} + { + "taskId": "..." + } + ``` + + + diff --git a/src/app/web/users/page.mdx b/src/app/web/users/page.mdx index e67c53c..f88edc9 100644 --- a/src/app/web/users/page.mdx +++ b/src/app/web/users/page.mdx @@ -31,7 +31,7 @@ The user model contains most of the profile information about a user, including The object ID of this user's profile picture. Check out the - [Objects](../objects) API on how to fetch the image. + [Objects](/web/objects) API on how to fetch the image. Admin flag. If enabled, this user has access to system-level endpoints. @@ -514,9 +514,9 @@ Simple authentication is the default and fallback in Drop. It relies on a simple - ### Response parameters + ### Response properties - The response parameters are the same as described in [Fetch invitation](#fetch-invitations). + The response properties are the same as described in [Fetch invitation](#fetch-invitations). diff --git a/src/components/ACLSelector.tsx b/src/components/ACLSelector.tsx new file mode 100644 index 0000000..9010696 --- /dev/null +++ b/src/components/ACLSelector.tsx @@ -0,0 +1,181 @@ +'use client' + +import { + Combobox, + ComboboxButton, + ComboboxInput, + ComboboxOption, + ComboboxOptions, + Label, +} from '@headlessui/react' +import { ChevronDownIcon } from '@heroicons/react/20/solid' +import { useState } from 'react' + +/* === COPY FROM DROP SOURCE === */ +export const userACLs = [ + 'read', + + 'store:read', + + 'object:read', + 'object:update', + 'object:delete', + + 'notifications:read', + 'notifications:mark', + 'notifications:listen', + 'notifications:delete', + + 'screenshots:new', + 'screenshots:read', + 'screenshots:delete', + + 'collections:new', + 'collections:read', + 'collections:delete', + 'collections:add', + 'collections:remove', + 'library:add', + 'library:remove', + + 'clients:read', + 'clients:revoke', + + 'news:read', + + 'settings:read', +] as const +const userACLPrefix = 'user:' + +export type UserACL = Array<(typeof userACLs)[number]> + +export const systemACLs = [ + 'setup', + + 'auth:read', + 'auth:simple:invitation:read', + 'auth:simple:invitation:new', + 'auth:simple:invitation:delete', + + 'notifications:read', + 'notifications:mark', + 'notifications:listen', + 'notifications:delete', + + 'library:read', + 'library:sources:read', + 'library:sources:new', + 'library:sources:update', + 'library:sources:delete', + + 'game:read', + 'game:update', + 'game:delete', + 'game:version:update', + 'game:version:delete', + 'game:image:new', + 'game:image:delete', + + 'company:read', + 'company:update', + 'company:create', + 'company:delete', + + 'import:version:read', + 'import:version:new', + + 'import:game:read', + 'import:game:new', + + 'user:read', + 'user:delete', + + 'news:read', + 'news:create', + 'news:delete', + + 'tags:read', + 'tags:create', + 'tags:delete', + + 'task:read', + 'task:start', + + 'maintenance:read', + + 'settings:update', +] as const +const systemACLPrefix = 'system:' + +export type SystemACL = Array<(typeof systemACLs)[number]> + +export type GlobalACL = + | `${typeof systemACLPrefix}${(typeof systemACLs)[number]}` + | `${typeof userACLPrefix}${(typeof userACLs)[number]}` +/* === END COPY FROM DROP SOURCE === */ + +export default function ACLSelector({ + mode, + addACL, + selectedACLS, +}: { + mode: 'system' | 'user' + addACL: (v: string) => void + selectedACLS: string[] +}) { + const [query, setQuery] = useState('') + + const acls = (mode === 'system' ? systemACLs : userACLs).filter( + (e) => !selectedACLS.includes(e), + ) + + return ( + { + setQuery('') + addACL(acl as string) + }} + > + +
+ setQuery(event.target.value)} + onBlur={() => setQuery('')} + /> + + + + + {query.length > 0 && ( + + {query} + + )} + {acls.map((acl) => ( + + {acl} + + ))} + +
+
+ ) +} diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx index 50e462a..5aa284e 100644 --- a/src/components/Navigation.tsx +++ b/src/components/Navigation.tsx @@ -270,6 +270,7 @@ export const navigation: Array = [ { title: 'Users', href: '/web/users' }, { title: 'Objects', href: '/web/objects' }, { title: 'Tasks', href: '/web/tasks' }, + { title: 'Import', href: '/web/import' }, ], }, ] diff --git a/src/components/PayloadGenerator.tsx b/src/components/PayloadGenerator.tsx new file mode 100644 index 0000000..ba74e9d --- /dev/null +++ b/src/components/PayloadGenerator.tsx @@ -0,0 +1,139 @@ +'use client' +import { ChangeEvent, useState } from 'react' +import ACLSelector from './ACLSelector' + +export default function PayloadGenerator() { + const [appName, setAppName] = useState('') + const [acls, setAcls] = useState([]) + const [mode, setMode] = useState<'system' | 'user'>('user') + + const payload = btoa(JSON.stringify({ name: appName, acls })) + + function updateAppName(event: ChangeEvent) { + setAppName(event.target.value) + } + + function addACL(acl: string) { + if (acls.includes(acl)) return + if (!acl) return + setAcls([...acls, acl]) + } + + function removeACL(acl: string) { + const index = acls.findIndex((e) => e === acl) + if (index == -1) return + setAcls([...acls.slice(0, index), ...acls.slice(index + 1)]) + } + + function setToggleMode(systemOn: boolean) { + setMode(systemOn ? 'system' : 'user') + setAcls([]) + } + + return ( + <> +
+

Payload Generator

+

+ If you don't want to mess around with JavaScript consoles, you can use + this UI tool to generate your payload. +

+
+
+
+ +
+ +
+
+
+ + + + Use User ACLs + + +
+ + setToggleMode(v.target.checked)} + /> +
+ + + + Use System ACLs + + +
+ +
+ {acls.map((acl) => ( + <> + + {acl} + + + + ))} +
+ + +
+
{payload}
+
+
+ + ) +} diff --git a/src/components/mdx.tsx b/src/components/mdx.tsx index 4d25784..3555c15 100644 --- a/src/components/mdx.tsx +++ b/src/components/mdx.tsx @@ -4,10 +4,11 @@ import Link from 'next/link' import { Feedback } from '@/components/Feedback' import { Heading } from '@/components/Heading' import { Prose } from '@/components/Prose' +import React from 'react' export const a = Link export { Button } from '@/components/Button' -export { CodeGroup, Code as code, Pre as pre } from '@/components/Code' +export { Code as code, CodeGroup, Pre as pre } from '@/components/Code' export function wrapper({ children }: { children: React.ReactNode }) { return ( @@ -125,3 +126,6 @@ export function Property({ ) } + +import PG from './PayloadGenerator'; +export const PayloadGenerator = PG; \ No newline at end of file