From a479ec803d16b43b0bd39a21768c9b69a6b49e27 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Tue, 9 Apr 2024 09:48:58 +0200 Subject: [PATCH] feat!: module option to enable features (#42) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(module): module options to toggle NuxtHub features * feat: write `dist/hub.config.json` * chore: update module options type * fix: set features default value to false * docs: add module options * fix: respect cache feature * fix(dev-tools): register panel if feature is enabled * fix: send features on `build:before` hook * chore: remove `c12` workaround, issue fixed in current version * chore: simplify module setup function * fix: wranger template * Update docs/content/docs/1.getting-started/2.installation.md * Apply suggestions from code review * docs: up --------- Co-authored-by: Sébastien Chopin --- .../docs/1.getting-started/2.installation.md | 20 +++ docs/content/docs/2.storage/1.database.md | 12 ++ docs/content/docs/2.storage/2.kv.md | 12 ++ docs/content/docs/2.storage/3.blob.md | 12 ++ package.json | 1 - pnpm-lock.yaml | 3 - src/module.ts | 142 +++++++++--------- .../server/api/_hub/analytics/index.put.ts | 5 + .../api/_hub/blob/[...pathname].delete.ts | 5 + .../server/api/_hub/blob/[...pathname].get.ts | 5 + .../server/api/_hub/blob/[...pathname].put.ts | 5 + .../server/api/_hub/blob/delete.post.ts | 5 + src/runtime/server/api/_hub/blob/index.get.ts | 7 +- .../server/api/_hub/blob/index.post.ts | 5 + .../api/_hub/database/[command].post.ts | 5 + .../server/api/_hub/database/query.post.ts | 5 + src/runtime/server/api/_hub/index.head.ts | 7 +- src/runtime/server/api/_hub/kv/[...path].ts | 5 + src/runtime/server/api/_hub/manifest.get.ts | 4 +- src/runtime/server/api/_hub/openapi.get.ts | 2 + src/runtime/server/utils/analytics.ts | 5 + .../1.hub-auth.ts => utils/auth.ts} | 18 +-- src/runtime/server/utils/blob.ts | 7 + src/runtime/server/utils/database.ts | 5 + src/runtime/server/utils/features.ts | 38 +++++ src/runtime/server/utils/kv.ts | 5 + src/utils.ts | 80 ++++++++-- 27 files changed, 323 insertions(+), 102 deletions(-) rename src/runtime/server/{middleware/1.hub-auth.ts => utils/auth.ts} (87%) create mode 100644 src/runtime/server/utils/features.ts diff --git a/docs/content/docs/1.getting-started/2.installation.md b/docs/content/docs/1.getting-started/2.installation.md index c208ef92..4b82f531 100644 --- a/docs/content/docs/1.getting-started/2.installation.md +++ b/docs/content/docs/1.getting-started/2.installation.md @@ -93,6 +93,26 @@ export default defineNuxtConfig({ Default to `false` - Allows working with remote storage (database, kv, blob) from your deployed project. :br [Read more about remote storage for usage](/docs/getting-started/remote-storage). :: + + ::field{name="analytics" type="boolean"} + Default to `false` - Enables analytics for your project (coming soon). + :: + + ::field{name="blob" type="boolean"} + Default to `false` - Enables blob storage to store static assets, such as images, videos and more. + :: + + ::field{name="cache" type="boolean"} + Default to `false` - Enables cache storage to cache your server route responses or functions using Nitro's `cachedEventHandler` and `cachedFunction` + :: + + ::field{name="database" type="boolean"} + Default to `false` - Enables SQL database to store your application's data. + :: + + ::field{name="kv" type="boolean"} + Default to `false` - Enables Key-Value to store JSON data accessible globally. + :: :: ::tip{icon="i-ph-rocket-launch-duotone"} diff --git a/docs/content/docs/2.storage/1.database.md b/docs/content/docs/2.storage/1.database.md index 4cee0cd8..c371f64c 100644 --- a/docs/content/docs/2.storage/1.database.md +++ b/docs/content/docs/2.storage/1.database.md @@ -5,6 +5,18 @@ description: How to create a database and store entries with NuxtHub. NuxtHub Database is a layer on top of [Cloudflare D1](https://developers.cloudflare.com/d1), a serverless SQLite databases. +## Getting Started + +Enable the database in your NuxtHub project by adding the `database` property to the `hub` object in your `nuxt.config.ts` file. + +```ts [nuxt.config.ts] +defineNuxtConfig({ + hub: { + database: true + } +}) +``` + ## `hubDatabase()` Server composable that returns a [D1 database client](https://developers.cloudflare.com/d1/build-databases/query-databases/). diff --git a/docs/content/docs/2.storage/2.kv.md b/docs/content/docs/2.storage/2.kv.md index b8706036..ee1e365a 100644 --- a/docs/content/docs/2.storage/2.kv.md +++ b/docs/content/docs/2.storage/2.kv.md @@ -5,6 +5,18 @@ description: How to use key-value data storage with NuxtHub. NuxtHub KV is a layer on top of [Cloudflare Workers KV](https://developers.cloudflare.com/kv), a global, low-latency, key-value data storage. +## Getting Started + +Enable the key-value storage in your NuxtHub project by adding the `kv` property to the `hub` object in your `nuxt.config.ts` file. + +```ts [nuxt.config.ts] +defineNuxtConfig({ + hub: { + kv: true + } +}) +``` + ## `hubKV()` Server method that returns an [unstorage instance](https://unstorage.unjs.io/guide#interface) with `keys()`, `get()`, `set()` and `del()` aliases. diff --git a/docs/content/docs/2.storage/3.blob.md b/docs/content/docs/2.storage/3.blob.md index 202fbc3d..12261069 100644 --- a/docs/content/docs/2.storage/3.blob.md +++ b/docs/content/docs/2.storage/3.blob.md @@ -5,6 +5,18 @@ description: How to store objects with NuxtHub. NuxtHub Blob is a layer on top of [Cloudflare R2](https://developers.cloudflare.com/r2), allowing to store large amounts of unstructured data (images, videos, etc.). +## Getting Started + +Enable the blob storage in your NuxtHub project by adding the `blob` property to the `hub` object in your `nuxt.config.ts` file. + +```ts [nuxt.config.ts] +defineNuxtConfig({ + hub: { + blob: true + } +}) +``` + ## `hubBlob()` Server composable that returns a set of methods to manipulate the blob storage. diff --git a/package.json b/package.json index 910a7783..07dbf039 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,6 @@ "ofetch": "^1.3.4", "pathe": "^1.1.2", "pkg-types": "^1.0.3", - "rc9": "^2.1.1", "ufo": "^1.5.3", "uncrypto": "^0.1.3", "unstorage": "^1.10.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d58797f..289f0237 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,9 +56,6 @@ importers: pkg-types: specifier: ^1.0.3 version: 1.0.3 - rc9: - specifier: ^2.1.1 - version: 2.1.1 ufo: specifier: ^1.5.3 version: 1.5.3 diff --git a/src/module.ts b/src/module.ts index 742e4ee0..867be649 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,14 +1,12 @@ import { defineNuxtModule, createResolver, logger, addServerScanDir, installModule, addServerImportsDir } from '@nuxt/kit' -import { addCustomTab } from '@nuxt/devtools-kit' import { join } from 'pathe' import { defu } from 'defu' import { mkdir, writeFile, readFile } from 'node:fs/promises' import { findWorkspaceDir } from 'pkg-types' -import { readUser } from 'rc9' import { $fetch } from 'ofetch' import { joinURL } from 'ufo' import { parseArgs } from 'citty' -import { generateWrangler } from './utils' +import { addDevtoolsCustomTabs, generateWrangler } from './utils' import { version } from '../package.json' import { execSync } from 'node:child_process' import { argv } from 'node:process' @@ -16,6 +14,39 @@ import { argv } from 'node:process' const log = logger.withTag('nuxt:hub') export interface ModuleOptions { + /** + * Set `true` to enable the analytics for the project. + * + * @default false + */ + analytics?: boolean + /** + * Set `true` to enable the Blob storage for the project. + * + * @default false + */ + blob?: boolean + /** + * Set `true` to enable caching for the project. + * + * @default false + * @see https://hub.nuxt.com/docs/storage/blob + */ + cache?: boolean + /** + * Set `true` to enable the database for the project. + * + * @default false + * @see https://hub.nuxt.com/docs/storage/database + */ + database?: boolean + /** + * Set `true` to enable the Key-Value storage for the project. + * + * @default false + * @see https://hub.nuxt.com/docs/storage/kv + */ + kv?: boolean /** * Set to `true`, 'preview' or 'production' to use the remote storage. * Only set the value on a project you are deploying outside of NuxtHub or Cloudflare. @@ -61,13 +92,6 @@ export default defineNuxtModule({ async setup (options, nuxt) { const rootDir = nuxt.options.rootDir const { resolve } = createResolver(import.meta.url) - const resolveRuntimeModule = (path: string) => resolve('./runtime', path) - - // Waiting for https://github.com/unjs/c12/pull/139 - // Then adding the c12 dependency to the project to 1.8.1 - options = defu(options, { - ...readUser('.nuxtrc').hub, - }) let remoteArg = parseArgs(argv, { remote: { type: 'string' } }).remote as string remoteArg = (remoteArg === '' ? 'true' : remoteArg) @@ -82,6 +106,12 @@ export default defineNuxtModule({ userToken: process.env.NUXT_HUB_USER_TOKEN || '', // Remote storage remote: remoteArg || process.env.NUXT_HUB_REMOTE, + // NuxtHub features + analytics: false, + blob: false, + cache: false, + database: false, + kv: false, // Other options version, env: process.env.NUXT_HUB_ENV || 'production', @@ -97,22 +127,24 @@ export default defineNuxtModule({ log.info(`Using \`${hub.url}\` as NuxtHub Admin URL`) } - // Add Server caching (Nitro) - nuxt.options.nitro = defu(nuxt.options.nitro, { - storage: { - cache: { - driver: 'cloudflare-kv-binding', - binding: 'CACHE', - base: 'cache' - } - }, - devStorage: { - cache: { - driver: 'fs', - base: join(rootDir, '.data/cache') + if (hub.cache) { + // Add Server caching (Nitro) + nuxt.options.nitro = defu(nuxt.options.nitro, { + storage: { + cache: { + driver: 'cloudflare-kv-binding', + binding: 'CACHE', + base: 'cache' + } + }, + devStorage: { + cache: { + driver: 'fs', + base: join(rootDir, '.data/cache') + } } - } - }) + }) + } // nuxt prepare, stop here if (nuxt.options._prepare) { @@ -120,7 +152,7 @@ export default defineNuxtModule({ } // Register composables - addServerImportsDir(resolveRuntimeModule('./server/utils')) + addServerImportsDir(resolve('./runtime/server/utils')) // Within CF Pages CI/CD to notice NuxtHub about the build and hub config if (!nuxt.options.dev && process.env.CF_PAGES && process.env.NUXT_HUB_PROJECT_DEPLOY_TOKEN && process.env.NUXT_HUB_PROJECT_KEY && process.env.NUXT_HUB_ENV) { @@ -132,11 +164,11 @@ export default defineNuxtModule({ authorization: `Bearer ${process.env.NUXT_HUB_PROJECT_DEPLOY_TOKEN}` }, body: { - analytics: true, - blob: true, - cache: true, - database: true, - kv: true + analytics: hub.analytics, + blob: hub.blob, + cache: hub.cache, + database: hub.database, + kv: hub.kv }, }).catch(() => {}) }) @@ -155,11 +187,11 @@ export default defineNuxtModule({ // Write `dist/hub.config.json` after public assets are built nuxt.hook('nitro:build:public-assets', async (nitro) => { const hubConfig = { - analytics: true, - blob: true, - cache: true, - database: true, - kv: true + analytics: hub.analytics, + blob: hub.blob, + cache: hub.cache, + database: hub.database, + kv: hub.kv } await writeFile(join(nitro.options.output.publicDir, 'hub.config.json'), JSON.stringify(hubConfig, null, 2), 'utf-8') }) @@ -248,44 +280,14 @@ export default defineNuxtModule({ logger.info(`Remote storage available: ${Object.keys(manifest.storage).filter(k => manifest.storage[k]).map(k => `\`${k}\``).join(', ')} `) } + // Add Proxy routes only if not remote or in development (used for devtools) if (nuxt.options.dev || !hub.remote) { - // Add Proxy routes only if not remote or in development (used for devtools) addServerScanDir(resolve('./runtime/server')) } + // Add custom tabs to Nuxt Devtools if (nuxt.options.dev) { - nuxt.hook('listen', (_, { url }) => { - addCustomTab({ - category: 'server', - name: 'hub-database', - title: 'Hub Database', - icon: 'i-ph-database', - view: { - type: 'iframe', - src: `https://admin.hub.nuxt.com/embed/database?url=${url}`, - }, - }) - addCustomTab({ - category: 'server', - name: 'hub-kv', - title: 'Hub KV', - icon: 'i-ph-coin', - view: { - type: 'iframe', - src: `https://admin.hub.nuxt.com/embed/kv?url=${url}`, - }, - }) - addCustomTab({ - category: 'server', - name: 'hub-blob', - title: 'Hub Blob', - icon: 'i-ph-shapes', - view: { - type: 'iframe', - src: `https://admin.hub.nuxt.com/embed/blob?url=${url}`, - }, - }) - }) + addDevtoolsCustomTabs(nuxt, hub) } // Local development without remote connection @@ -313,7 +315,7 @@ export default defineNuxtModule({ // Generate the wrangler.toml file const wranglerPath = join(hubDir, './wrangler.toml') - await writeFile(wranglerPath, generateWrangler(), 'utf-8') + await writeFile(wranglerPath, generateWrangler(hub), 'utf-8') // @ts-ignore nuxt.options.nitro.cloudflareDev = { persistDir: hubDir, diff --git a/src/runtime/server/api/_hub/analytics/index.put.ts b/src/runtime/server/api/_hub/analytics/index.put.ts index c3689296..14b9be37 100644 --- a/src/runtime/server/api/_hub/analytics/index.put.ts +++ b/src/runtime/server/api/_hub/analytics/index.put.ts @@ -2,8 +2,13 @@ import type { AnalyticsEngineDataPoint } from '@cloudflare/workers-types/experim import { eventHandler, readValidatedBody } from 'h3' import { z } from 'zod' import { hubAnalytics } from '../../../utils/analytics' +import { requireNuxtHubAuthorization } from '../../../utils/auth' +import { requireNuxtHubFeature } from '../../../utils/features' export default eventHandler(async (event) => { + await requireNuxtHubAuthorization(event) + requireNuxtHubFeature('analytics') + const { data } = await readValidatedBody(event, z.object({ data: z.custom() }).parse) diff --git a/src/runtime/server/api/_hub/blob/[...pathname].delete.ts b/src/runtime/server/api/_hub/blob/[...pathname].delete.ts index 23878832..6440ee56 100644 --- a/src/runtime/server/api/_hub/blob/[...pathname].delete.ts +++ b/src/runtime/server/api/_hub/blob/[...pathname].delete.ts @@ -1,8 +1,13 @@ import { eventHandler, getValidatedRouterParams, sendNoContent } from 'h3' import { z } from 'zod' import { hubBlob } from '../../../utils/blob' +import { requireNuxtHubAuthorization } from '../../../utils/auth' +import { requireNuxtHubFeature } from '../../../utils/features' export default eventHandler(async (event) => { + await requireNuxtHubAuthorization(event) + requireNuxtHubFeature('blob') + const { pathname } = await getValidatedRouterParams(event, z.object({ pathname: z.string().min(1) }).parse) diff --git a/src/runtime/server/api/_hub/blob/[...pathname].get.ts b/src/runtime/server/api/_hub/blob/[...pathname].get.ts index e0ea68f4..7239807a 100644 --- a/src/runtime/server/api/_hub/blob/[...pathname].get.ts +++ b/src/runtime/server/api/_hub/blob/[...pathname].get.ts @@ -1,8 +1,13 @@ import { eventHandler, getValidatedRouterParams } from 'h3' import { z } from 'zod' import { hubBlob } from '../../../utils/blob' +import { requireNuxtHubAuthorization } from '../../../utils/auth' +import { requireNuxtHubFeature } from '../../../utils/features' export default eventHandler(async (event) => { + await requireNuxtHubAuthorization(event) + requireNuxtHubFeature('blob') + // TODO: handle caching in production const { pathname } = await getValidatedRouterParams(event, z.object({ pathname: z.string().min(1) diff --git a/src/runtime/server/api/_hub/blob/[...pathname].put.ts b/src/runtime/server/api/_hub/blob/[...pathname].put.ts index ef4aad1d..4afd677a 100644 --- a/src/runtime/server/api/_hub/blob/[...pathname].put.ts +++ b/src/runtime/server/api/_hub/blob/[...pathname].put.ts @@ -1,6 +1,8 @@ import { eventHandler, getValidatedRouterParams, getHeader, getRequestWebStream, getQuery } from 'h3' import { z } from 'zod' import { hubBlob } from '../../../utils/blob' +import { requireNuxtHubAuthorization } from '../../../utils/auth' +import { requireNuxtHubFeature } from '../../../utils/features' async function streamToArrayBuffer(stream: ReadableStream, streamSize: number) { const result = new Uint8Array(streamSize) @@ -19,6 +21,9 @@ async function streamToArrayBuffer(stream: ReadableStream, streamSize: number) { } export default eventHandler(async (event) => { + await requireNuxtHubAuthorization(event) + requireNuxtHubFeature('blob') + const { pathname } = await getValidatedRouterParams(event, z.object({ pathname: z.string().min(1) }).parse) diff --git a/src/runtime/server/api/_hub/blob/delete.post.ts b/src/runtime/server/api/_hub/blob/delete.post.ts index ce4f0430..de8604d6 100644 --- a/src/runtime/server/api/_hub/blob/delete.post.ts +++ b/src/runtime/server/api/_hub/blob/delete.post.ts @@ -1,8 +1,13 @@ import { eventHandler, readValidatedBody, sendNoContent } from 'h3' import { z } from 'zod' import { hubBlob } from '../../../utils/blob' +import { requireNuxtHubAuthorization } from '../../../utils/auth' +import { requireNuxtHubFeature } from '../../../utils/features' export default eventHandler(async (event) => { + await requireNuxtHubAuthorization(event) + requireNuxtHubFeature('blob') + const { pathnames } = await readValidatedBody(event, z.object({ pathnames: z.array(z.string().min(1)).min(1) }).parse) diff --git a/src/runtime/server/api/_hub/blob/index.get.ts b/src/runtime/server/api/_hub/blob/index.get.ts index 774dfc0a..d5d11b09 100644 --- a/src/runtime/server/api/_hub/blob/index.get.ts +++ b/src/runtime/server/api/_hub/blob/index.get.ts @@ -1,6 +1,11 @@ import { eventHandler } from 'h3' import { hubBlob } from '../../../utils/blob' +import { requireNuxtHubAuthorization } from '../../../utils/auth' +import { requireNuxtHubFeature } from '../../../utils/features' -export default eventHandler(async () => { +export default eventHandler(async (event) => { + await requireNuxtHubAuthorization(event) + requireNuxtHubFeature('blob') + return hubBlob().list() }) diff --git a/src/runtime/server/api/_hub/blob/index.post.ts b/src/runtime/server/api/_hub/blob/index.post.ts index 1289024e..74dc341b 100644 --- a/src/runtime/server/api/_hub/blob/index.post.ts +++ b/src/runtime/server/api/_hub/blob/index.post.ts @@ -1,7 +1,12 @@ import { createError, eventHandler, readFormData } from 'h3' import { hubBlob, type BlobObject } from '../../../utils/blob' +import { requireNuxtHubAuthorization } from '../../../utils/auth' +import { requireNuxtHubFeature } from '../../../utils/features' export default eventHandler(async (event) => { + await requireNuxtHubAuthorization(event) + requireNuxtHubFeature('blob') + const form = await readFormData(event) const files = form.getAll('files') as File[] if (!files) { diff --git a/src/runtime/server/api/_hub/database/[command].post.ts b/src/runtime/server/api/_hub/database/[command].post.ts index c7c2b71b..90d9b460 100644 --- a/src/runtime/server/api/_hub/database/[command].post.ts +++ b/src/runtime/server/api/_hub/database/[command].post.ts @@ -1,6 +1,8 @@ import { eventHandler, getValidatedRouterParams, readValidatedBody } from 'h3' import { z } from 'zod' import { hubDatabase } from '../../../utils/database' +import { requireNuxtHubAuthorization } from '../../../utils/auth' +import { requireNuxtHubFeature } from '../../../utils/features' const statementValidation = z.object({ query: z.string().min(1).max(1e6).trim(), @@ -8,6 +10,9 @@ const statementValidation = z.object({ }) export default eventHandler(async (event) => { + await requireNuxtHubAuthorization(event) + requireNuxtHubFeature('database') + // https://developers.cloudflare.com/d1/build-databases/query-databases/ const { command } = await getValidatedRouterParams(event, z.object({ command: z.enum(['first', 'all', 'raw', 'run', 'dump', 'exec', 'batch']) diff --git a/src/runtime/server/api/_hub/database/query.post.ts b/src/runtime/server/api/_hub/database/query.post.ts index 4dc09d75..20e1cab2 100644 --- a/src/runtime/server/api/_hub/database/query.post.ts +++ b/src/runtime/server/api/_hub/database/query.post.ts @@ -1,6 +1,8 @@ import { eventHandler, readValidatedBody } from 'h3' import { z } from 'zod' import { hubDatabase } from '../../../utils/database' +import { requireNuxtHubAuthorization } from '../../../utils/auth' +import { requireNuxtHubFeature } from '../../../utils/features' const statementValidation = z.object({ query: z.string().min(1).max(1e6).trim(), @@ -9,6 +11,9 @@ const statementValidation = z.object({ }) export default eventHandler(async (event) => { + await requireNuxtHubAuthorization(event) + requireNuxtHubFeature('database') + const { query, params, mode } = await readValidatedBody(event, statementValidation.parse) return hubDatabase().prepare(query).bind(...params)[mode === 'raw' ? 'raw' : 'all']({ columnNames: true }) diff --git a/src/runtime/server/api/_hub/index.head.ts b/src/runtime/server/api/_hub/index.head.ts index a63aba47..269d939f 100644 --- a/src/runtime/server/api/_hub/index.head.ts +++ b/src/runtime/server/api/_hub/index.head.ts @@ -1,3 +1,8 @@ import { eventHandler, sendNoContent } from 'h3' +import { requireNuxtHubAuthorization } from '../../utils/auth' -export default eventHandler((event) => sendNoContent(event)) +export default eventHandler(async (event) => { + await requireNuxtHubAuthorization(event) + + return sendNoContent(event) +}) diff --git a/src/runtime/server/api/_hub/kv/[...path].ts b/src/runtime/server/api/_hub/kv/[...path].ts index c90ec8e8..acec0c18 100644 --- a/src/runtime/server/api/_hub/kv/[...path].ts +++ b/src/runtime/server/api/_hub/kv/[...path].ts @@ -1,8 +1,13 @@ import { eventHandler } from 'h3' import { createH3StorageHandler } from 'unstorage/server' import { hubKV } from '../../../utils/kv' +import { requireNuxtHubAuthorization } from '../../../utils/auth' +import { requireNuxtHubFeature } from '../../../utils/features' export default eventHandler(async (event) => { + await requireNuxtHubAuthorization(event) + requireNuxtHubFeature('kv') + const storage = hubKV() return createH3StorageHandler(storage, { resolvePath(event) { diff --git a/src/runtime/server/api/_hub/manifest.get.ts b/src/runtime/server/api/_hub/manifest.get.ts index 782555d4..0b067930 100644 --- a/src/runtime/server/api/_hub/manifest.get.ts +++ b/src/runtime/server/api/_hub/manifest.get.ts @@ -3,8 +3,10 @@ import { useRuntimeConfig } from '#imports' import { hubDatabase } from '../../utils/database' import { hubKV } from '../../utils/kv' import { hubBlob } from '../../utils/blob' +import { requireNuxtHubAuthorization } from '../../utils/auth' -export default eventHandler(async () => { +export default eventHandler(async (event) => { + await requireNuxtHubAuthorization(event) const { version } = useRuntimeConfig().hub const [ dbCheck, kvCheck, blobCheck ] = await Promise.all([ falseIfFail(() => hubDatabase().exec('PRAGMA table_list')), diff --git a/src/runtime/server/api/_hub/openapi.get.ts b/src/runtime/server/api/_hub/openapi.get.ts index c8fa837d..45570835 100644 --- a/src/runtime/server/api/_hub/openapi.get.ts +++ b/src/runtime/server/api/_hub/openapi.get.ts @@ -1,7 +1,9 @@ import { eventHandler, createError } from 'h3' import { useRuntimeConfig } from '#imports' +import { requireNuxtHubAuthorization } from '../../utils/auth' export default eventHandler(async (event) => { + await requireNuxtHubAuthorization(event) const hub = useRuntimeConfig().hub if (!hub.openapi) { diff --git a/src/runtime/server/utils/analytics.ts b/src/runtime/server/utils/analytics.ts index 63be7161..5497db55 100644 --- a/src/runtime/server/utils/analytics.ts +++ b/src/runtime/server/utils/analytics.ts @@ -3,6 +3,7 @@ import { ofetch } from 'ofetch' import { joinURL } from 'ufo' import { createError } from 'h3' import { useRuntimeConfig } from '#imports' +import { requireNuxtHubFeature } from './features' const _datasets: Record = {} @@ -26,6 +27,8 @@ function _useDataset(name: string = 'ANALYTICS') { } export function hubAnalytics() { + requireNuxtHubFeature('analytics') + const hub = useRuntimeConfig().hub const binding = getAnalyticsBinding() if (hub.remote && hub.projectUrl && !binding) { @@ -42,6 +45,8 @@ export function hubAnalytics() { } export function proxyHubAnalytics(projectUrl: string, secretKey?: string) { + requireNuxtHubFeature('analytics') + const analyticsAPI = ofetch.create({ baseURL: joinURL(projectUrl, '/api/_hub/analytics'), headers: { diff --git a/src/runtime/server/middleware/1.hub-auth.ts b/src/runtime/server/utils/auth.ts similarity index 87% rename from src/runtime/server/middleware/1.hub-auth.ts rename to src/runtime/server/utils/auth.ts index af003415..8af72de3 100644 --- a/src/runtime/server/middleware/1.hub-auth.ts +++ b/src/runtime/server/utils/auth.ts @@ -1,12 +1,9 @@ +import type { H3Event } from 'h3' import { handleCors } from 'h3' -import { eventHandler, getHeader, createError } from 'h3' +import { getHeader, createError } from 'h3' import { $fetch } from 'ofetch' -export default eventHandler(async (event) => { - // Skip if not a hub request - if (/^\/api\/_hub\//.test(event.path) === false) { - return - } +export async function requireNuxtHubAuthorization(event: H3Event) { // Skip if in development if (import.meta.dev) { // add cors for devtools embed @@ -16,6 +13,7 @@ export default eventHandler(async (event) => { }) return } + const secretKeyOrUserToken = (getHeader(event, 'authorization') || '').split(' ')[1] if (!secretKeyOrUserToken) { throw createError({ @@ -24,7 +22,7 @@ export default eventHandler(async (event) => { }) } const projectKey = process.env.NUXT_HUB_PROJECT_KEY - + // Self-hosted NuxtHub project, user has to set a secret key to access the proxy const projectSecretKey = process.env.NUXT_HUB_PROJECT_SECRET_KEY if (projectSecretKey && secretKeyOrUserToken === projectSecretKey) { @@ -35,7 +33,7 @@ export default eventHandler(async (event) => { message: 'Invalid secret key' }) } - + // Hosted on NuxtHub if (projectKey) { // Here the secretKey is a user token @@ -48,9 +46,9 @@ export default eventHandler(async (event) => { }) return } - + throw createError({ statusCode: 401, message: 'Missing NUXT_HUB_PROJECT_SECRET_KEY envrionment variable or NUXT_HUB_PROJECT_KEY envrionment variable' }) -}) +} \ No newline at end of file diff --git a/src/runtime/server/utils/blob.ts b/src/runtime/server/utils/blob.ts index 932d5b27..46f0e3d4 100644 --- a/src/runtime/server/utils/blob.ts +++ b/src/runtime/server/utils/blob.ts @@ -10,6 +10,7 @@ import { randomUUID } from 'uncrypto' import { parse } from 'pathe' import { joinURL } from 'ufo' import { useRuntimeConfig } from '#imports' +import { requireNuxtHubFeature } from './features' export interface BlobObject { /** @@ -135,6 +136,8 @@ interface HubBlob { * @see https://hub.nuxt.com/docs/storage/blob */ export function hubBlob(): HubBlob { + requireNuxtHubFeature('blob') + const hub = useRuntimeConfig().hub const binding = getBlobBinding() if (hub.remote && hub.projectUrl && !binding) { @@ -237,6 +240,8 @@ export function hubBlob(): HubBlob { * @see https://hub.nuxt.com/docs/storage/blob */ export function proxyHubBlob(projectUrl: string, secretKey?: string) { + requireNuxtHubFeature('blob') + const blobAPI = ofetch.create({ baseURL: joinURL(projectUrl, '/api/_hub/blob'), headers: { @@ -349,6 +354,8 @@ function fileSizeToBytes(input: string) { * @throws If the blob does not meet the requirements */ export function ensureBlob(blob: Blob, options: { maxSize?: BlobSize, types?: BlobType[] }) { + requireNuxtHubFeature('blob') + if (!options.maxSize && !options.types?.length) { throw createError({ statusCode: 400, diff --git a/src/runtime/server/utils/database.ts b/src/runtime/server/utils/database.ts index 8e13ef57..a97dfd89 100644 --- a/src/runtime/server/utils/database.ts +++ b/src/runtime/server/utils/database.ts @@ -4,6 +4,7 @@ import { joinURL } from 'ufo' import { createError } from 'h3' import type { H3Error } from 'h3' import { useRuntimeConfig } from '#imports' +import { requireNuxtHubFeature } from './features' let _db: D1Database @@ -18,6 +19,8 @@ let _db: D1Database * @see https://hub.nuxt.com/docs/storage/database */ export function hubDatabase(): D1Database { + requireNuxtHubFeature('database') + if (_db) { return _db } @@ -49,6 +52,8 @@ export function hubDatabase(): D1Database { * @see https://hub.nuxt.com/docs/storage/database */ export function proxyHubDatabase(projectUrl: string, secretKey?: string): D1Database { + requireNuxtHubFeature('database') + const d1API = ofetch.create({ baseURL: joinURL(projectUrl, '/api/_hub/database'), method: 'POST', diff --git a/src/runtime/server/utils/features.ts b/src/runtime/server/utils/features.ts new file mode 100644 index 00000000..8754f05d --- /dev/null +++ b/src/runtime/server/utils/features.ts @@ -0,0 +1,38 @@ +import { useRuntimeConfig } from '#imports' +import { createError } from 'h3' + +const featureMessages = { + analytics: [ + 'NuxtHub Analytics is not enabled, set `hub.analytics = true` in your `nuxt.config.ts`' + ].join('\n'), + blob: [ + 'NuxtHub Blob is not enabled, set `hub.blob = true` in your `nuxt.config.ts`', + 'Read more at https://hub.nuxt.com/docs/storage/blob' + ].join('\n'), + cache: [ + 'NuxtHub Cache is not enabled, set `hub.cache = true` in your `nuxt.config.ts`' + ].join('\n'), + database: [ + 'NuxtHub Database is not enabled, set `hub.database = true` in your `nuxt.config.ts`', + 'Read more at https://hub.nuxt.com/docs/storage/database' + ].join('\n'), + kv: [ + 'NuxtHub KV is not enabled, set `hub.kv = true` in your `nuxt.config.ts`', + 'Read more at https://hub.nuxt.com/docs/storage/kv' + ].join('\n'), +} + +export function requireNuxtHubFeature(feature: keyof typeof featureMessages) { + const hub = useRuntimeConfig().hub + + if (!hub[feature]) { + throw createError({ + statusCode: 422, + statusMessage: 'Unprocessable Entity', + message: `"${feature}" not enabled`, + data: { + reason: featureMessages[feature] + } + }) + } +} \ No newline at end of file diff --git a/src/runtime/server/utils/kv.ts b/src/runtime/server/utils/kv.ts index 48af5df4..10e32b5d 100644 --- a/src/runtime/server/utils/kv.ts +++ b/src/runtime/server/utils/kv.ts @@ -5,6 +5,7 @@ import cloudflareKVBindingDriver from 'unstorage/drivers/cloudflare-kv-binding' import { joinURL } from 'ufo' import { createError } from 'h3' import { useRuntimeConfig } from '#imports' +import { requireNuxtHubFeature } from './features' export interface HubKV extends Storage { /** @@ -61,6 +62,8 @@ let _kv: HubKV * @see https://hub.nuxt.com/docs/storage/kv */ export function hubKV(): HubKV { + requireNuxtHubFeature('kv') + if (_kv) { return _kv } @@ -103,6 +106,8 @@ export function hubKV(): HubKV { * @see https://hub.nuxt.com/docs/storage/kv */ export function proxyHubKV(projectUrl: string, secretKey?: string): HubKV { + requireNuxtHubFeature('kv') + const storage = createStorage({ driver: httpDriver({ base: joinURL(projectUrl, '/api/_hub/kv/'), diff --git a/src/utils.ts b/src/utils.ts index 3e53ddb1..f962dce5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,16 +1,66 @@ -export function generateWrangler() { - return `d1_databases = [ - { binding = "DB", database_name = "default", database_id = "default" }, -] -kv_namespaces = [ - { binding = "KV", id = "kv_default" }, - { binding = "CACHE", id = "cache_default" }, -] -r2_buckets = [ - { binding = "BLOB", bucket_name = "default" }, -] -analytics_engine_datasets = [ - { binding = "ANALYTICS", dataset = "default" } -] -` +import { addCustomTab } from '@nuxt/devtools-kit' +import type { Nuxt } from 'nuxt/schema' + +export function generateWrangler(hub: { kv: boolean, database: boolean, blob: boolean, cache: boolean, analytics: boolean }) { + return [ + hub.analytics ? [ + 'analytics_engine_datasets = [', + ' { binding = "ANALYTICS", dataset = "default" }', + ']', + ] : [], + hub.blob ? [ + 'r2_buckets = [', + ' { binding = "BLOB", bucket_name = "default" }', + ']' + ] : [], + hub.cache || hub.kv ? [ + 'kv_namespaces = [', + hub.kv ? ' { binding = "KV", id = "kv_default" },' : '', + hub.cache ? ' { binding = "CACHE", id = "cache_default" },' : '', + ']', + ] : [], + hub.database ? [ + 'd1_databases = [', + ' { binding = "DB", database_name = "default", database_id = "default" }', + ']' + ] : [], + ].flat().join('\n') } + + +export function addDevtoolsCustomTabs(nuxt: Nuxt, hub: { kv: boolean, database: boolean, blob: boolean }) { + nuxt.hook('listen', (_, { url }) => { + hub.database && addCustomTab({ + category: 'server', + name: 'hub-database', + title: 'Hub Database', + icon: 'i-ph-database', + view: { + type: 'iframe', + src: `https://admin.hub.nuxt.com/embed/database?url=${url}`, + }, + }) + + hub.kv && addCustomTab({ + category: 'server', + name: 'hub-kv', + title: 'Hub KV', + icon: 'i-ph-coin', + view: { + type: 'iframe', + src: `https://admin.hub.nuxt.com/embed/kv?url=${url}`, + }, + }) + + hub.blob && addCustomTab({ + category: 'server', + name: 'hub-blob', + title: 'Hub Blob', + icon: 'i-ph-shapes', + view: { + type: 'iframe', + src: `https://admin.hub.nuxt.com/embed/blob?url=${url}`, + }, + }) + }) +} \ No newline at end of file