From 15553eb822bf0344ae27a1d84751138a40450503 Mon Sep 17 00:00:00 2001 From: Gabriel Massadas <5445926+G4brym@users.noreply.github.com> Date: Sun, 15 Dec 2024 00:05:06 +0000 Subject: [PATCH] Bundle dashboard with worker assets (#73) * Bundle dashboard with worker assets * Show server configs in info side menu --- .github/workflows/{test.yml => build.yml} | 15 +- .github/workflows/publish.yml | 10 +- .gitignore | 3 + .husky/pre-commit | 3 - package.json | 3 +- .../src/components/main/LeftSidebar.vue | 103 ++++--- packages/dashboard/src/stores/main-store.js | 10 +- packages/worker/config/preparePublish.js | 26 -- packages/worker/dev/index.ts | 4 +- packages/worker/dev/wrangler.toml | 12 +- packages/worker/package.json | 22 +- packages/worker/src/foundation/dashbord.ts | 55 ---- packages/worker/src/foundation/dates.ts | 4 - .../foundation/middlewares/authentication.ts | 258 ------------------ packages/worker/src/foundation/settings.ts | 4 +- packages/worker/src/index.ts | 20 +- .../worker/src/modules/buckets/listBuckets.ts | 8 +- .../worker/src/modules/buckets/listObjects.ts | 2 + packages/worker/src/modules/dashboard.ts | 40 +++ packages/worker/src/modules/server/getInfo.ts | 9 +- packages/worker/src/types.d.ts | 8 +- packages/worker/tsconfig.json | 1 + pnpm-lock.yaml | 54 ++-- 23 files changed, 200 insertions(+), 474 deletions(-) rename .github/workflows/{test.yml => build.yml} (72%) delete mode 100644 packages/worker/config/preparePublish.js delete mode 100644 packages/worker/src/foundation/dashbord.ts delete mode 100644 packages/worker/src/foundation/middlewares/authentication.ts create mode 100644 packages/worker/src/modules/dashboard.ts diff --git a/.github/workflows/test.yml b/.github/workflows/build.yml similarity index 72% rename from .github/workflows/test.yml rename to .github/workflows/build.yml index 9249fed..97ee0fb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: Test Build +name: Build on: push: @@ -17,7 +17,7 @@ on: - '.editorconfig' jobs: - test: + build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -25,10 +25,6 @@ jobs: uses: actions/setup-node@v4 with: node-version: '20.x' - - name: Set RELEASE_VERSION - run: echo "RELEASE_VERSION=0.0.1" >> $GITHUB_ENV - - name: Apply new version - run: node packages/worker/config/preparePublish.js - name: Install pnpm run: npm install -g pnpm - name: Install dependencies @@ -37,3 +33,10 @@ jobs: run: pnpm lint - name: Build everything run: pnpm build + - name: Package artifact + run: pnpm package + - name: Archive package + uses: actions/upload-artifact@v4 + with: + name: r2-explorer-npm-package + path: packages/worker/r2-explorer-* diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a750f0c..f32d2f6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,17 +15,17 @@ jobs: with: node-version: '20.x' registry-url: 'https://registry.npmjs.org' - - name: Set RELEASE_VERSION - run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - - name: Apply new version - run: node packages/worker/config/preparePublish.js - name: Install pnpm run: npm install -g pnpm - name: Install dependencies run: pnpm install + - name: Check Lint + run: pnpm lint - name: Build everything run: pnpm build + - name: Package artifact + run: pnpm package - name: Publish to npm - run: pnpm publish-npm + run: pnpm publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} diff --git a/.gitignore b/.gitignore index 58a37c6..9c53cd9 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,9 @@ my-r2-explorer packages/worker/bin packages/worker/README.md packages/worker/LICENSE +packages/worker/dist +packages/worker/dashboard +packages/worker/r2-explorer-* packages/worker/dev/.wrangler diff --git a/.husky/pre-commit b/.husky/pre-commit index e9884e8..6ca46a7 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - pnpm run lint diff --git a/package.json b/package.json index 50f1c43..30cdebb 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "deploy-dashboard": "pnpm run --filter r2-explorer-dashboard deploy", "deploy-dashboard-dev": "pnpm run --filter r2-explorer-dashboard deploy-dev", "deploy-dev-worker": "pnpm run --filter r2-explorer-dev-worker deploy", - "publish-npm": "pnpm run --filter worker publish" + "package": "pnpm run --filter r2-explorer package", + "publish": "pnpm run --filter r2-explorer publish" }, "devDependencies": { "@biomejs/biome": "1.9.4", diff --git a/packages/dashboard/src/components/main/LeftSidebar.vue b/packages/dashboard/src/components/main/LeftSidebar.vue index 27c4781..6c2a60d 100644 --- a/packages/dashboard/src/components/main/LeftSidebar.vue +++ b/packages/dashboard/src/components/main/LeftSidebar.vue @@ -45,62 +45,35 @@ - - - - + -
🎉 Welcome to the new Dashboard v2! 🚀
+
🎉 Thank you for using R2-Explorer! 🚀
- We're thrilled to introduce our revamped interface, designed to enhance your experience and productivity. As you - explore the new features and improvements, feel free to provide feedback or report any issues you encounter. - Your input helps us fine-tune the dashboard to meet your needs better.
+ You are running version {{ mainStore.version }}
+
- To revisit this message in the future, simply click on the question mark icon located in the left corner. Your - feedback is invaluable to us, so don't hesitate to reach out with any thoughts or concerns.
-
- Please report issues here: https://github.com/G4brym/R2-Explorer/issues
-
- If you would like to continue using the old Dashboard, please follow this guide from the - documentation
-
- Thank you for being a part of our journey towards excellence! 🌟
-
- Best regards
-
- - - - -
-
- - - - -
Your server configurations
-
- - - - + + +

+ Server Configuration
+ {{ JSON.stringify(mainStore.config, null, 2) }}
@@ -122,8 +95,9 @@ import { defineComponent } from "vue"; export default defineComponent({ name: "LeftSidebar", data: () => ({ - upgradePopup: false, - settingsPopup: false, + infoPopup: false, + updateAvailable: false, + latestVersion: "", }), components: { CreateFolder, CreateFile }, methods: { @@ -139,6 +113,24 @@ export default defineComponent({ params: { bucket: this.selectedBucket }, }); }, + isUpdateAvailable: (currentVersion, latestVersion) => { + // Split versions into parts and convert to numbers + const current = currentVersion.split(".").map(Number); + const latest = latestVersion.split(".").map(Number); + + // Compare major version + if (latest[0] > current[0]) return true; + if (latest[0] < current[0]) return false; + + // Compare minor version + if (latest[1] > current[1]) return true; + if (latest[1] < current[1]) return false; + + // Compare patch version + if (latest[2] > current[2]) return true; + + return false; + }, }, computed: { selectedBucket: function () { @@ -148,12 +140,15 @@ export default defineComponent({ return this.$route.name.split("-")[0]; }, }, - mounted: function () { - const alertSeen = localStorage.getItem("DASH_V2_ALERT"); - - if (!alertSeen) { - this.upgradePopup = true; - localStorage.setItem("DASH_V2_ALERT", true); + async mounted() { + const resp = await fetch( + "https://api.github.com/repos/G4brym/R2-Explorer/releases/latest", + ); + const parsed = await resp.json(); + const latestVersion = parsed.tag_name.replace("v", ""); + if (this.isUpdateAvailable(this.mainStore.version, latestVersion)) { + this.latestVersion = latestVersion; + this.updateAvailable = true; } }, setup() { diff --git a/packages/dashboard/src/stores/main-store.js b/packages/dashboard/src/stores/main-store.js index 1ab89e2..d0c96f3 100644 --- a/packages/dashboard/src/stores/main-store.js +++ b/packages/dashboard/src/stores/main-store.js @@ -5,9 +5,9 @@ export const useMainStore = defineStore("main", { state: () => ({ // Config apiReadonly: true, - username: "", + auth: {}, + config: {}, version: "", - dashboardUrl: "", showHiddenFiles: false, // Frontend data @@ -37,9 +37,9 @@ export const useMainStore = defineStore("main", { }); this.apiReadonly = response.data.config.readonly; - this.username = response.data.config.user?.username; - this.version = response.data.config.version; - this.dashboardUrl = response.data.config.dashboardUrl; + this.config = response.data.config; + this.auth = response.data.auth; + this.version = response.data.version; this.showHiddenFiles = response.data.config.showHiddenFiles; } catch (error) { console.log(error); diff --git a/packages/worker/config/preparePublish.js b/packages/worker/config/preparePublish.js deleted file mode 100644 index cefe924..0000000 --- a/packages/worker/config/preparePublish.js +++ /dev/null @@ -1,26 +0,0 @@ -const fs = require("node:fs"); - -// Apply version numbers -const files = [ - "packages/worker/src/foundation/settings.ts", - "packages/worker/package.json", -]; - -for (const file of files) { - fs.readFile(file, "utf8", (err, data) => { - if (err) { - console.log(err); - process.exit(1); - } - - const version = process.env.RELEASE_VERSION.replace("v", ""); - const result = data.replace("0.0.1", version); - - fs.writeFile(file, result, "utf8", (err) => { - if (err) { - console.log(err); - process.exit(1); - } - }); - }); -} diff --git a/packages/worker/dev/index.ts b/packages/worker/dev/index.ts index eab49d9..4f29b60 100644 --- a/packages/worker/dev/index.ts +++ b/packages/worker/dev/index.ts @@ -3,9 +3,7 @@ import { R2Explorer } from "../src"; const baseConfig = { readonly: false, cors: true, - showHiddenFiles: true, - dashboardUrl: "https://dev.r2-explorer-dashboard.pages.dev/", - cacheAssets: false, + showHiddenFiles: true }; export default { diff --git a/packages/worker/dev/wrangler.toml b/packages/worker/dev/wrangler.toml index ddec8bf..93abcaf 100644 --- a/packages/worker/dev/wrangler.toml +++ b/packages/worker/dev/wrangler.toml @@ -1,19 +1,11 @@ name = "my-r2-explorer" -compatibility_date = "2023-05-12" +compatibility_date = "2024-11-06" main = "index.ts" workers_dev = true +assets = { directory = "../../dashboard/dist/spa", binding = "ASSETS", html_handling = "auto-trailing-slash", not_found_handling = "single-page-application" } [[r2_buckets]] binding = 'teste' bucket_name = 'teste' preview_bucket_name = 'teste' -[[r2_buckets]] -binding = 'storage' -bucket_name = 'storage' -preview_bucket_name = 'storage' - -[[r2_buckets]] -binding = 'drive' -bucket_name = 'drive' -preview_bucket_name = 'drive' diff --git a/packages/worker/package.json b/packages/worker/package.json index b469e72..a8e2399 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,17 +1,22 @@ { "name": "r2-explorer", - "version": "0.0.1", + "version": "1.1.0", "description": "A Google Drive Interface for your Cloudflare R2 Buckets", "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", - "files": ["dist", "LICENSE", "README.md"], + "files": [ + "dashboard", + "dist", + "LICENSE", + "README.md" + ], "scripts": { - "build": "tsup src/index.ts --format cjs,esm --dts", + "build": "tsup src/index.ts --format cjs,esm --dts && cp -R ../dashboard/dist/spa/ dashboard/ && cp ../../README.md . && cp ../../LICENSE .", "lint": "npx @biomejs/biome check src/ tests/ || (npx @biomejs/biome check --write src/ tests/; exit 1)", "test": "vitest run --root tests", - "prepare": "husky", - "package": "npm run build && npm pack" + "package": "npm run build && npm pack", + "publish": "npm publish" }, "publishConfig": { "access": "public" @@ -54,9 +59,10 @@ "wrangler": "^3.91.0" }, "dependencies": { - "chanfana": "^2.4.2", - "hono": "^4.6.12", + "@hono/cloudflare-access": "^0.1.0", + "chanfana": "^2.5.1", + "hono": "^4.6.14", "postal-mime": "^2.3.2", - "zod": "^3.23.8" + "zod": "^3.24.1" } } diff --git a/packages/worker/src/foundation/dashbord.ts b/packages/worker/src/foundation/dashbord.ts deleted file mode 100644 index fdd879e..0000000 --- a/packages/worker/src/foundation/dashbord.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { AppContext } from "../types"; - -export async function dashboardProxy(c: AppContext) { - // Initialize the default cache - //@ts-ignore - const cache = caches.default; - - let path = new URL(c.req.raw.url).pathname; - - // Support page navigation - if (!path.includes(".")) { - path = "/"; - } - - let result; - const config = c.get("config"); - - if (config.cacheAssets !== false) { - // use .match() to see if we have a cache hit, if so return the caches response early - result = await cache.match(c.req.raw); - if (result) { - return result; - } - } - - let dashboardUrl = "https://demo.r2explorer.dev"; - if (config.dashboardUrl) { - if (config.dashboardUrl.endsWith("/")) { - dashboardUrl = config.dashboardUrl.slice(0, -1); - } else { - dashboardUrl = config.dashboardUrl; - } - } - - // we'll chain our await calls to get the JSON response in one line - const response = await fetch(`${dashboardUrl}${path}`); - - result = new Response(await response.body, { - status: response.status, - headers: { - "Content-Type": response.headers.get("Content-Type"), - "Access-Control-Allow-Origin": "*", - // We set a max-age of 300 seconds which is equivalent to 5 minutes. - // If the last response is older than that the cache.match() call returns nothing and and a new response is fetched - "Cache-Control": "max-age: 300", - }, - }); - - if (response.status === 200 && config.cacheAssets !== false) { - // before returning the response we put a clone of our response object into the cache so it can be resolved later - c.executionCtx.waitUntil(cache.put(c.req.raw, result.clone())); - } - - return result; -} diff --git a/packages/worker/src/foundation/dates.ts b/packages/worker/src/foundation/dates.ts index e93b5f1f..adb158b 100644 --- a/packages/worker/src/foundation/dates.ts +++ b/packages/worker/src/foundation/dates.ts @@ -1,7 +1,3 @@ -export function getCurrentTimestampSeconds(): number { - return Math.floor(Date.now() / 1000); -} - export function getCurrentTimestampMilliseconds(): number { return Math.floor(Date.now()); } diff --git a/packages/worker/src/foundation/middlewares/authentication.ts b/packages/worker/src/foundation/middlewares/authentication.ts deleted file mode 100644 index 7cb81a8..0000000 --- a/packages/worker/src/foundation/middlewares/authentication.ts +++ /dev/null @@ -1,258 +0,0 @@ -import type { AppContext } from "../../types"; -import { getCurrentTimestampSeconds } from "../dates"; - -// This var will hold already imported jwt keys, this reduces the load of importing the key on every request -let builtJWTKeys: Record = {}; -let JWTExpiration = 0; - -function getAccessHost(teamName: string): string { - return `https://${teamName}.cloudflareaccess.com`; -} - -export async function accessMiddleware(c: AppContext, next: CallableFunction) { - const encodedToken = getJwt(c); - - if (encodedToken === null) { - return Response.json( - { - success: false, - errors: [ - { - code: 10000, - message: "Authentication error: Missing bearer token", - }, - ], - }, - { status: 401 }, - ); - } - - const cfAccessTeamName = c.get("config").cfAccessTeamName as string; - let decodedJwt: any = false; - try { - decodedJwt = await isValidJwt(encodedToken, cfAccessTeamName); - } catch (e) {} - - if (decodedJwt === false) { - return Response.json( - { - success: false, - errors: [ - { - code: 10001, - message: "Authentication error: Unable to decode Bearer token", - }, - ], - }, - { status: 401 }, - ); - } - - c.set("username", decodedJwt.payload.email); - await next(); -} - -async function getPublicKeys( - cfAccessTeamName: string, -): Promise> { - const jwtUrl = `${getAccessHost(cfAccessTeamName)}/cdn-cgi/access/certs`; - - const result = await fetch(jwtUrl, { - method: "GET", - // @ts-ignore - cf: { - // Dont cache error responses - cacheTtlByStatus: { "200-299": 30, "300-599": 0 }, - }, - headers: { - "Content-Type": "application/json; charset=UTF-8", - }, - }); - - const data: any = await result.json(); - - JWTExpiration = getCurrentTimestampSeconds() + 3600; // 1h - const importedKeys: Record = {}; - for (const key of data.keys) { - importedKeys[key.kid] = await crypto.subtle.importKey( - "jwk", - key, - { - name: "RSASSA-PKCS1-v1_5", - hash: "SHA-256", - }, - false, - ["verify"], - ); - } - - return importedKeys; -} - -export async function isValidJwt( - encodedToken: string, - cfAccessTeamName: string, -) { - // Load jwt keys if they are not in memory or already expired - if ( - Object.keys(builtJWTKeys).length === 0 || - Math.floor(Date.now() / 1000) < JWTExpiration - ) { - builtJWTKeys = await getPublicKeys(cfAccessTeamName); - } - - // Decode payload - let token; - try { - token = decodeJwt(encodedToken); - } catch (err) { - return Response.json( - { - success: false, - errors: [ - { - code: 10001, - message: "Authentication error: Unable to decode Bearer token", - }, - ], - }, - { status: 401 }, - ); - } - - // Is the token expired? - const expiryDate = new Date(token.payload.exp * 1000); - const currentDate = new Date(Date.now()); - if (expiryDate <= currentDate) { - return Response.json( - { - success: false, - errors: [ - { - code: 10002, - message: "Authentication error: Token is expired", - }, - ], - }, - { status: 401 }, - ); - } - - if (token.payload?.iss !== getAccessHost(cfAccessTeamName)) { - return Response.json( - { - success: false, - errors: [ - { - code: 10003, - message: `Authentication error: Expected team name ${cfAccessTeamName}, but received ${token.payload?.iss}`, - }, - ], - }, - { status: 401 }, - ); - } - - // Check is token is valid against all public keys - if (!(await isValidJwtSignature(token))) { - return Response.json( - { - success: false, - errors: [ - { - code: 10004, - message: "Authentication error: Invalid Token", - }, - ], - }, - { status: 401 }, - ); - } - - // All good, return payload - return token; -} - -/** - * For this example, the JWT is passed in as part of the Authorization header, - * after the Bearer scheme. - * Parse the JWT out of the header and return it. - */ -function getJwt(c: AppContext) { - const authHeader = c.req.header("cf-access-jwt-assertion"); - if (!authHeader) { - return null; - } - return authHeader.trim(); -} - -/** - * Parse and decode a JWT. - * A JWT is three, base64 encoded, strings concatenated with ‘.’: - * a header, a payload, and the signature. - * The signature is “URL safe”, in that ‘/+’ characters have been replaced by ‘_-’ - * - * Steps: - * 1. Split the token at the ‘.’ character - * 2. Base64 decode the individual parts - * 3. Retain the raw Bas64 encoded strings to verify the signature - */ -type DecodedToken = { - header: object; - payload: { iss?: string; exp: number }; - signature: string; - raw: { header?: string; payload?: string; signature?: string }; -}; - -function decodeJwt(token: string): DecodedToken { - const parts = token.split("."); - if (parts.length !== 3) { - throw new Error("Invalid token"); - } - - const header = JSON.parse(atob(parts[0] as string)); - const payload = JSON.parse(atob(parts[1] as string)); - const signature = atob( - (parts[2] as string).replace(/_/g, "/").replace(/-/g, "+"), - ); - - return { - header: header, - payload: payload, - signature: signature, - raw: { header: parts[0], payload: parts[1], signature: parts[2] }, - }; -} - -/** - * Validate the JWT. - * - * Steps: - * Reconstruct the signed message from the Base64 encoded strings. - * Load the RSA public key into the crypto library. - * Verify the signature with the message and the key. - */ -async function isValidJwtSignature(token: DecodedToken) { - const encoder = new TextEncoder(); - const data = encoder.encode([token.raw.header, token.raw.payload].join(".")); - // @ts-ignore - const signature = new Uint8Array( - Array.from(token.signature).map((c) => c.charCodeAt(0)), - ); - - for (const key of Object.values(builtJWTKeys)) { - const isValid = await validateSingleKey(key, signature, data); - - if (isValid) return true; - } - - return false; -} - -async function validateSingleKey( - key: CryptoKey, - signature: Uint8Array, - data: Uint8Array, -): Promise { - return crypto.subtle.verify("RSASSA-PKCS1-v1_5", key, signature, data); -} diff --git a/packages/worker/src/foundation/settings.ts b/packages/worker/src/foundation/settings.ts index 90267f5..fe50819 100644 --- a/packages/worker/src/foundation/settings.ts +++ b/packages/worker/src/foundation/settings.ts @@ -1,3 +1,5 @@ +import * as packageJson from "../../package.json"; + export const settings = { - version: "0.0.1", + version: packageJson.version, }; diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts index b73dac6..983d3f7 100644 --- a/packages/worker/src/index.ts +++ b/packages/worker/src/index.ts @@ -1,3 +1,4 @@ +import { cloudflareAccess } from "@hono/cloudflare-access"; import { type OpenAPIObjectConfigV31, extendZodWithOpenApi, @@ -7,8 +8,6 @@ import { type ExecutionContext, Hono } from "hono"; import { basicAuth } from "hono/basic-auth"; import { cors } from "hono/cors"; import { z } from "zod"; -import { dashboardProxy } from "./foundation/dashbord"; -import { accessMiddleware } from "./foundation/middlewares/authentication"; import { readOnlyMiddleware } from "./foundation/middlewares/readonly"; import { settings } from "./foundation/settings"; import { CreateFolder } from "./modules/buckets/createFolder"; @@ -23,6 +22,7 @@ import { CreateUpload } from "./modules/buckets/multipart/createUpload"; import { PartUpload } from "./modules/buckets/multipart/partUpload"; import { PutMetadata } from "./modules/buckets/putMetadata"; import { PutObject } from "./modules/buckets/putObject"; +import { dashboardIndex, dashboardRedirect } from "./modules/dashboard"; import { receiveEmail } from "./modules/emails/receiveEmail"; import { SendEmail } from "./modules/emails/sendEmail"; import { GetInfo } from "./modules/server/getInfo"; @@ -79,11 +79,16 @@ export function R2Explorer(config?: R2ExplorerConfig) { } if (config.readonly === true) { - app.use(readOnlyMiddleware); + app.use("*", readOnlyMiddleware); } if (config.cfAccessTeamName) { - app.use(accessMiddleware); + app.use("*", cloudflareAccess(config.cfAccessTeamName)); + app.use("*", async (c, next) => { + c.set("authentication_type", "cloudflare-access"); + c.set("authentication_username", c.get("accessPayload").email); + await next(); + }); } if (config.basicAuth) { @@ -92,6 +97,7 @@ export function R2Explorer(config?: R2ExplorerConfig) { scheme: "basic", }); app.use( + "*", basicAuth({ verifyUser: (username, password, c: AppContext) => { const users = ( @@ -102,7 +108,8 @@ export function R2Explorer(config?: R2ExplorerConfig) { for (const user of users) { if (user.username === username && user.password === password) { - c.set("username", username); + c.set("authentication_type", "basic-auth"); + c.set("authentication_username", username); return true; } } @@ -131,7 +138,8 @@ export function R2Explorer(config?: R2ExplorerConfig) { openapi.post("/api/emails/send", SendEmail); - app.get("*", dashboardProxy); + openapi.get("/", dashboardIndex); + openapi.get("*", dashboardRedirect); app.all("*", () => Response.json({ msg: "404, not found!" }, { status: 404 }), diff --git a/packages/worker/src/modules/buckets/listBuckets.ts b/packages/worker/src/modules/buckets/listBuckets.ts index 2e28950..8e77cee 100644 --- a/packages/worker/src/modules/buckets/listBuckets.ts +++ b/packages/worker/src/modules/buckets/listBuckets.ts @@ -12,8 +12,12 @@ export class ListBuckets extends OpenAPIRoute { const buckets = []; for (const [key, value] of Object.entries(c.env)) { - // @ts-ignore - check if the field in Env is actually a R2 bucket by its properties - if (value.get && value.put) { + if ( + value.get && + value.put && + value.get.toString().includes("function") && + value.put.toString().includes("function") + ) { buckets.push({ name: key }); } } diff --git a/packages/worker/src/modules/buckets/listObjects.ts b/packages/worker/src/modules/buckets/listObjects.ts index d3512f4..1a8825e 100644 --- a/packages/worker/src/modules/buckets/listObjects.ts +++ b/packages/worker/src/modules/buckets/listObjects.ts @@ -31,6 +31,8 @@ export class ListObjects extends OpenAPIRoute { const bucket = c.env[data.params.bucket]; + c.header("Access-Control-Allow-Credentials", "asads"); + return await bucket.list({ limit: data.query.limit, prefix: data.query.prefix diff --git a/packages/worker/src/modules/dashboard.ts b/packages/worker/src/modules/dashboard.ts new file mode 100644 index 0000000..fc6fad4 --- /dev/null +++ b/packages/worker/src/modules/dashboard.ts @@ -0,0 +1,40 @@ +import type { AppContext } from "../types"; + +export function dashboardIndex(c: AppContext) { + if (c.env.ASSETS === undefined) { + return c.text( + "ASSETS binding is not defined, learn more here: https://r2explorer.dev/guides/migrating-to-1.1/", + 500, + ); + } + + return c.text( + "ASSETS binding is not pointing to a valid dashboard, learn more here: https://r2explorer.dev/guides/migrating-to-1.1/", + 500, + ); +} + +export async function dashboardRedirect(c: AppContext, next) { + if (c.env.ASSETS === undefined) { + return c.text( + "ASSETS binding is not defined, learn more here: https://r2explorer.dev/guides/migrating-to-1.1/", + 500, + ); + } + + const url = new URL(c.req.url); + + if (!url.pathname.includes(".")) { + // This is required for SPA + return fetch(`${url.origin}/`, { + cf: { + // Always cache this fetch regardless of content type + // for a max of 60 seconds before revalidating the resource + cacheTtl: 60, + cacheEverything: true, + }, + }); + } + + await next(); +} diff --git a/packages/worker/src/modules/server/getInfo.ts b/packages/worker/src/modules/server/getInfo.ts index fd7860f..66da474 100644 --- a/packages/worker/src/modules/server/getInfo.ts +++ b/packages/worker/src/modules/server/getInfo.ts @@ -15,9 +15,12 @@ export class GetInfo extends OpenAPIRoute { return { version: settings.version, config: config, - user: { - username: c.get("username"), - }, + auth: c.get("authentication_type") + ? { + type: c.get("authentication_type"), + username: c.get("authentication_username"), + } + : undefined, }; } } diff --git a/packages/worker/src/types.d.ts b/packages/worker/src/types.d.ts index 28c0fa7..67b9cf3 100644 --- a/packages/worker/src/types.d.ts +++ b/packages/worker/src/types.d.ts @@ -1,3 +1,4 @@ +import type { CloudflareAccessVariables } from "@hono/cloudflare-access"; import type { Context } from "hono"; export type BasicAuth = { @@ -14,15 +15,16 @@ export type R2ExplorerConfig = { targetBucket: string; }; showHiddenFiles?: boolean; - cacheAssets?: boolean; basicAuth?: BasicAuth | BasicAuth[]; }; export type AppEnv = { + ASSETS: Fetcher; [key: string]: R2Bucket; }; export type AppVariables = { config: R2ExplorerConfig; - username?: string; -}; + authentication_type?: string; + authentication_username?: string; +} & CloudflareAccessVariables; export type AppContext = Context<{ Bindings: AppEnv; Variables: AppVariables }>; diff --git a/packages/worker/tsconfig.json b/packages/worker/tsconfig.json index 0c3ab7c..94ddcb3 100644 --- a/packages/worker/tsconfig.json +++ b/packages/worker/tsconfig.json @@ -3,6 +3,7 @@ "target": "esNext", "module": "commonjs", "esModuleInterop": true, + "resolveJsonModule": true, "forceConsistentCasingInFileNames": true, "strict": false, "skipLibCheck": true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b24b3e7..3b8a018 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,18 +72,21 @@ importers: packages/worker: dependencies: + '@hono/cloudflare-access': + specifier: ^0.1.0 + version: 0.1.0(hono@4.6.14) chanfana: - specifier: ^2.4.2 - version: 2.4.2 + specifier: ^2.5.1 + version: 2.5.1 hono: - specifier: ^4.6.12 - version: 4.6.12 + specifier: ^4.6.14 + version: 4.6.14 postal-mime: specifier: ^2.3.2 version: 2.3.2 zod: - specifier: ^3.23.8 - version: 3.23.8 + specifier: ^3.24.1 + version: 3.24.1 devDependencies: '@cloudflare/workers-types': specifier: ^4.20241127.0 @@ -519,6 +522,11 @@ packages: resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} + '@hono/cloudflare-access@0.1.0': + resolution: {integrity: sha512-W3LjtQBb2w5rIttN46iTyh7ku5kKWsh1vzVfH4SGKiafek1JmUxLscAh53x8FCijgG+RLCnezP4Igu/yh+aZ9w==} + peerDependencies: + hono: '*' + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1044,8 +1052,8 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chanfana@2.4.2: - resolution: {integrity: sha512-EM78Nj6dXKQECyRxUveEqSS+HCmVMJXp6UByn8j0kNs0g6NtByE0cbe9I419gpFiOrxnHgPpj1rL8oL9JMpx7w==} + chanfana@2.5.1: + resolution: {integrity: sha512-VpYnEY/5bHWp73emMMB4bKvjgOeeUnnaJSzvR7iDcGGMFMLOWzkO+hDBA7sUfU2xBorvmutMV7Oc2RpbwwTzzA==} chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} @@ -1603,8 +1611,8 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - hono@4.6.12: - resolution: {integrity: sha512-eHtf4kSDNw6VVrdbd5IQi16r22m3s7mWPLd7xOMhg1a/Yyb1A0qpUFq8xYMX4FMuDe1nTKeMX5rTx7Nmw+a+Ag==} + hono@4.6.14: + resolution: {integrity: sha512-j4VkyUp2xazGJ8eCCLN1Vm/bxdvm/j5ZuU9AIjLu9vapn2M44p9L3Ktr9Vnb2RN2QtcR/wVjZVMlT5k7GJQgPw==} engines: {node: '>=16.9.0'} html-minifier-terser@7.2.0: @@ -2608,15 +2616,15 @@ packages: resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} engines: {node: '>= 10'} - zod@3.23.8: - resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + zod@3.24.1: + resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} snapshots: - '@asteasolutions/zod-to-openapi@7.2.0(zod@3.23.8)': + '@asteasolutions/zod-to-openapi@7.2.0(zod@3.24.1)': dependencies: openapi3-ts: 4.4.0 - zod: 3.23.8 + zod: 3.24.1 '@babel/helper-string-parser@7.25.9': {} @@ -2688,7 +2696,7 @@ snapshots: '@cloudflare/workers-shared@0.9.0': dependencies: mime: 3.0.0 - zod: 3.23.8 + zod: 3.24.1 '@cloudflare/workers-types@4.20241202.0': {} @@ -2846,6 +2854,10 @@ snapshots: '@fastify/busboy@2.1.1': {} + '@hono/cloudflare-access@0.1.0(hono@4.6.14)': + dependencies: + hono: 4.6.14 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -3417,12 +3429,12 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - chanfana@2.4.2: + chanfana@2.5.1: dependencies: - '@asteasolutions/zod-to-openapi': 7.2.0(zod@3.23.8) + '@asteasolutions/zod-to-openapi': 7.2.0(zod@3.24.1) js-yaml: 4.1.0 openapi3-ts: 4.4.0 - zod: 3.23.8 + zod: 3.24.1 chardet@0.7.0: {} @@ -3970,7 +3982,7 @@ snapshots: dependencies: function-bind: 1.1.2 - hono@4.6.12: {} + hono@4.6.14: {} html-minifier-terser@7.2.0: dependencies: @@ -4197,7 +4209,7 @@ snapshots: workerd: 1.20241106.1 ws: 8.18.0 youch: 3.3.4 - zod: 3.23.8 + zod: 3.24.1 transitivePeerDependencies: - bufferutil - supports-color @@ -4966,4 +4978,4 @@ snapshots: compress-commons: 4.1.2 readable-stream: 3.6.2 - zod@3.23.8: {} + zod@3.24.1: {}