mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-14 16:30:15 +02:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e9a040174 | |||
| a5332bb0cc | |||
| b3963cc34b | |||
| ddb132f9fa | |||
| 18d380ce30 |
@@ -1,4 +1,4 @@
|
|||||||
import { APP_PATH } from "@server/lib/consts";
|
import { APP_PATH } from "./server/lib/consts";
|
||||||
import { defineConfig } from "drizzle-kit";
|
import { defineConfig } from "drizzle-kit";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -5,7 +5,7 @@ go 1.25.0
|
|||||||
require (
|
require (
|
||||||
github.com/charmbracelet/huh v1.0.0
|
github.com/charmbracelet/huh v1.0.0
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
golang.org/x/term v0.42.0
|
golang.org/x/term v0.43.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,6 +33,6 @@ require (
|
|||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
golang.org/x/sync v0.15.0 // indirect
|
golang.org/x/sync v0.15.0 // indirect
|
||||||
golang.org/x/sys v0.43.0 // indirect
|
golang.org/x/sys v0.44.0 // indirect
|
||||||
golang.org/x/text v0.23.0 // indirect
|
golang.org/x/text v0.23.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
+4
-4
@@ -69,10 +69,10 @@ golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
|||||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
|||||||
@@ -1354,6 +1354,29 @@
|
|||||||
"otpAuthBack": "Back to Password",
|
"otpAuthBack": "Back to Password",
|
||||||
"navbar": "Navigation Menu",
|
"navbar": "Navigation Menu",
|
||||||
"navbarDescription": "Main navigation menu for the application",
|
"navbarDescription": "Main navigation menu for the application",
|
||||||
|
"commandPaletteTitle": "Command palette",
|
||||||
|
"commandPaletteDescription": "Search for pages, organizations, resources, and actions",
|
||||||
|
"commandPaletteSearchPlaceholder": "Search pages, resources, actions...",
|
||||||
|
"commandPaletteNoResults": "No results found.",
|
||||||
|
"commandPaletteSearching": "Searching...",
|
||||||
|
"commandPaletteNavigation": "Navigation",
|
||||||
|
"commandPaletteOrganizations": "Organizations",
|
||||||
|
"commandPaletteSites": "Sites",
|
||||||
|
"commandPaletteResources": "Resources",
|
||||||
|
"commandPaletteUsers": "Users",
|
||||||
|
"commandPaletteClients": "Machine clients",
|
||||||
|
"commandPaletteActions": "Actions",
|
||||||
|
"commandPaletteCreateSite": "Create site",
|
||||||
|
"commandPaletteCreateProxyResource": "Create public resource",
|
||||||
|
"commandPaletteCreateUser": "Create user",
|
||||||
|
"commandPaletteCreateApiKey": "Create API key",
|
||||||
|
"commandPaletteCreateMachineClient": "Create machine client",
|
||||||
|
"commandPaletteCreateAlertRule": "Create alert rule",
|
||||||
|
"commandPaletteCreateIdentityProvider": "Create identity provider",
|
||||||
|
"commandPaletteToggleTheme": "Toggle theme",
|
||||||
|
"commandPaletteChooseOrganization": "Choose organization",
|
||||||
|
"commandPaletteShortcutMac": "⌘K",
|
||||||
|
"commandPaletteShortcutWindows": "Ctrl K",
|
||||||
"navbarDocsLink": "Documentation",
|
"navbarDocsLink": "Documentation",
|
||||||
"otpErrorEnable": "Unable to enable 2FA",
|
"otpErrorEnable": "Unable to enable 2FA",
|
||||||
"otpErrorEnableDescription": "An error occurred while enabling 2FA",
|
"otpErrorEnableDescription": "An error occurred while enabling 2FA",
|
||||||
|
|||||||
+1
-6
@@ -5,12 +5,7 @@ const withNextIntl = createNextIntlPlugin();
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
reactStrictMode: false,
|
reactStrictMode: false,
|
||||||
eslint: {
|
reactCompiler: true,
|
||||||
ignoreDuringBuilds: true
|
|
||||||
},
|
|
||||||
experimental: {
|
|
||||||
reactCompiler: true
|
|
||||||
},
|
|
||||||
output: "standalone"
|
output: "standalone"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Generated
+2193
-3403
File diff suppressed because it is too large
Load Diff
+52
-50
@@ -32,10 +32,10 @@
|
|||||||
"format": "prettier --write ."
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@asteasolutions/zod-to-openapi": "8.4.1",
|
"@asteasolutions/zod-to-openapi": "8.5.0",
|
||||||
"@aws-sdk/client-s3": "3.1011.0",
|
"@aws-sdk/client-s3": "3.1047.0",
|
||||||
"@faker-js/faker": "10.3.0",
|
"@faker-js/faker": "10.4.0",
|
||||||
"@headlessui/react": "2.2.9",
|
"@headlessui/react": "2.2.10",
|
||||||
"@hookform/resolvers": "5.2.2",
|
"@hookform/resolvers": "5.2.2",
|
||||||
"@monaco-editor/react": "4.7.0",
|
"@monaco-editor/react": "4.7.0",
|
||||||
"@node-rs/argon2": "2.0.2",
|
"@node-rs/argon2": "2.0.2",
|
||||||
@@ -59,16 +59,17 @@
|
|||||||
"@radix-ui/react-tabs": "1.1.13",
|
"@radix-ui/react-tabs": "1.1.13",
|
||||||
"@radix-ui/react-toast": "1.2.15",
|
"@radix-ui/react-toast": "1.2.15",
|
||||||
"@radix-ui/react-tooltip": "1.2.8",
|
"@radix-ui/react-tooltip": "1.2.8",
|
||||||
"@react-email/components": "1.0.8",
|
"@react-email/body": "0.3.0",
|
||||||
"@react-email/render": "2.0.4",
|
"@react-email/components": "1.0.12",
|
||||||
"@react-email/tailwind": "2.0.5",
|
"@react-email/render": "2.0.8",
|
||||||
|
"@react-email/tailwind": "2.0.7",
|
||||||
"@simplewebauthn/browser": "13.3.0",
|
"@simplewebauthn/browser": "13.3.0",
|
||||||
"@simplewebauthn/server": "13.3.0",
|
"@simplewebauthn/server": "13.3.0",
|
||||||
"@tailwindcss/forms": "0.5.11",
|
"@tailwindcss/forms": "0.5.11",
|
||||||
"@tanstack/react-query": "5.90.21",
|
"@tanstack/react-query": "5.100.10",
|
||||||
"@tanstack/react-table": "8.21.3",
|
"@tanstack/react-table": "8.21.3",
|
||||||
"arctic": "3.7.0",
|
"arctic": "3.7.0",
|
||||||
"axios": "1.15.0",
|
"axios": "1.16.1",
|
||||||
"better-sqlite3": "11.9.1",
|
"better-sqlite3": "11.9.1",
|
||||||
"canvas-confetti": "1.9.4",
|
"canvas-confetti": "1.9.4",
|
||||||
"class-variance-authority": "0.7.1",
|
"class-variance-authority": "0.7.1",
|
||||||
@@ -80,76 +81,76 @@
|
|||||||
"d3": "7.9.0",
|
"d3": "7.9.0",
|
||||||
"drizzle-orm": "0.45.2",
|
"drizzle-orm": "0.45.2",
|
||||||
"express": "5.2.1",
|
"express": "5.2.1",
|
||||||
"express-rate-limit": "8.3.0",
|
"express-rate-limit": "8.5.2",
|
||||||
"glob": "13.0.6",
|
"glob": "13.0.6",
|
||||||
"helmet": "8.1.0",
|
"helmet": "8.1.0",
|
||||||
"http-errors": "2.0.1",
|
"http-errors": "2.0.1",
|
||||||
"input-otp": "1.4.2",
|
"input-otp": "1.4.2",
|
||||||
"ioredis": "5.10.0",
|
"ioredis": "5.10.1",
|
||||||
"jmespath": "0.16.0",
|
"jmespath": "0.16.0",
|
||||||
"js-yaml": "4.1.1",
|
"js-yaml": "4.1.1",
|
||||||
"jsonwebtoken": "9.0.3",
|
"jsonwebtoken": "9.0.3",
|
||||||
"lucide-react": "0.577.0",
|
"lucide-react": "0.577.0",
|
||||||
"maxmind": "5.0.5",
|
"maxmind": "5.0.6",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"next": "15.5.15",
|
"next": "16.2.6",
|
||||||
"next-intl": "4.8.3",
|
"next-intl": "4.12.0",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"nextjs-toploader": "3.9.17",
|
"nextjs-toploader": "3.9.17",
|
||||||
"node-cache": "5.1.2",
|
"node-cache": "5.1.2",
|
||||||
"nodemailer": "8.0.5",
|
"nodemailer": "8.0.7",
|
||||||
"oslo": "1.2.1",
|
"oslo": "1.2.1",
|
||||||
"pg": "8.20.0",
|
"pg": "8.20.0",
|
||||||
"posthog-node": "5.28.0",
|
"posthog-node": "5.34.1",
|
||||||
"qrcode.react": "4.2.0",
|
"qrcode.react": "4.2.0",
|
||||||
"react": "19.2.4",
|
"react": "19.2.6",
|
||||||
"react-day-picker": "9.14.0",
|
"react-day-picker": "9.14.0",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.6",
|
||||||
"react-easy-sort": "1.8.0",
|
"react-easy-sort": "1.8.0",
|
||||||
"react-hook-form": "7.71.2",
|
"react-hook-form": "7.75.0",
|
||||||
"react-icons": "5.6.0",
|
"react-icons": "5.6.0",
|
||||||
"recharts": "2.15.4",
|
"recharts": "3.8.1",
|
||||||
"reodotdev": "1.1.0",
|
"reodotdev": "1.1.0",
|
||||||
"resend": "6.9.2",
|
"resend": "6.12.3",
|
||||||
"semver": "7.7.4",
|
"semver": "7.8.0",
|
||||||
"sshpk": "1.18.0",
|
"sshpk": "1.18.0",
|
||||||
"stripe": "20.4.1",
|
"stripe": "20.4.1",
|
||||||
"swagger-ui-express": "5.0.1",
|
"swagger-ui-express": "5.0.1",
|
||||||
"tailwind-merge": "3.5.0",
|
"tailwind-merge": "3.6.0",
|
||||||
"topojson-client": "3.1.0",
|
"topojson-client": "3.1.0",
|
||||||
"tw-animate-css": "1.4.0",
|
"tw-animate-css": "1.4.0",
|
||||||
"use-debounce": "10.1.0",
|
"use-debounce": "10.1.1",
|
||||||
"uuid": "13.0.0",
|
"uuid": "14.0.0",
|
||||||
"vaul": "1.1.2",
|
"vaul": "1.1.2",
|
||||||
"visionscarto-world-atlas": "1.0.0",
|
"visionscarto-world-atlas": "1.0.0",
|
||||||
"winston": "3.19.0",
|
"winston": "3.19.0",
|
||||||
"winston-daily-rotate-file": "5.0.0",
|
"winston-daily-rotate-file": "5.0.0",
|
||||||
"ws": "8.19.0",
|
"ws": "8.20.1",
|
||||||
"yaml": "2.8.3",
|
"yaml": "2.9.0",
|
||||||
"yargs": "18.0.0",
|
"yargs": "18.0.0",
|
||||||
"zod": "4.3.6",
|
"zod": "4.4.3",
|
||||||
"zod-validation-error": "5.0.0"
|
"zod-validation-error": "5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dotenvx/dotenvx": "1.54.1",
|
"@dotenvx/dotenvx": "1.66.0",
|
||||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||||
"@react-email/preview-server": "5.2.10",
|
"@react-email/ui": "^6.1.4",
|
||||||
"@tailwindcss/postcss": "4.2.2",
|
"@tailwindcss/postcss": "4.3.0",
|
||||||
"@tanstack/react-query-devtools": "5.91.3",
|
"@tanstack/react-query-devtools": "5.100.10",
|
||||||
"@types/better-sqlite3": "7.6.13",
|
"@types/better-sqlite3": "7.6.13",
|
||||||
"@types/cookie-parser": "1.4.10",
|
"@types/cookie-parser": "1.4.10",
|
||||||
"@types/cors": "2.8.19",
|
"@types/cors": "2.8.19",
|
||||||
"@types/crypto-js": "4.2.2",
|
"@types/crypto-js": "4.2.2",
|
||||||
"@types/d3": "7.4.3",
|
"@types/d3": "7.4.3",
|
||||||
"@types/express": "5.0.6",
|
"@types/express": "5.0.6",
|
||||||
"@types/express-session": "1.18.2",
|
"@types/express-session": "1.19.0",
|
||||||
"@types/jmespath": "0.15.2",
|
"@types/jmespath": "0.15.2",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/jsonwebtoken": "9.0.10",
|
"@types/jsonwebtoken": "9.0.10",
|
||||||
"@types/node": "25.3.5",
|
"@types/node": "25.8.0",
|
||||||
"@types/nodemailer": "7.0.11",
|
"@types/nodemailer": "8.0.0",
|
||||||
"@types/nprogress": "0.2.3",
|
"@types/nprogress": "0.2.3",
|
||||||
"@types/pg": "8.18.0",
|
"@types/pg": "8.20.0",
|
||||||
"@types/react": "19.2.14",
|
"@types/react": "19.2.14",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
"@types/semver": "7.7.1",
|
"@types/semver": "7.7.1",
|
||||||
@@ -160,21 +161,22 @@
|
|||||||
"@types/yargs": "17.0.35",
|
"@types/yargs": "17.0.35",
|
||||||
"babel-plugin-react-compiler": "1.0.0",
|
"babel-plugin-react-compiler": "1.0.0",
|
||||||
"drizzle-kit": "0.31.10",
|
"drizzle-kit": "0.31.10",
|
||||||
"esbuild": "0.27.4",
|
"esbuild": "0.28.0",
|
||||||
"esbuild-node-externals": "1.20.1",
|
"esbuild-node-externals": "1.22.0",
|
||||||
"eslint": "10.0.3",
|
"eslint": "10.3.0",
|
||||||
"eslint-config-next": "16.1.7",
|
"eslint-config-next": "16.2.6",
|
||||||
"postcss": "8.5.8",
|
"postcss": "8.5.14",
|
||||||
"prettier": "3.8.1",
|
"prettier": "3.8.3",
|
||||||
"react-email": "5.2.10",
|
"react-email": "6.1.4",
|
||||||
"tailwindcss": "4.2.2",
|
"tailwindcss": "4.3.0",
|
||||||
"tsc-alias": "1.8.16",
|
"tsc-alias": "1.8.17",
|
||||||
"tsx": "4.21.0",
|
"tsx": "4.22.0",
|
||||||
"typescript": "5.9.3",
|
"typescript": "6.0.3",
|
||||||
"typescript-eslint": "8.56.1"
|
"typescript-eslint": "8.59.3"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"esbuild": "0.27.4",
|
"esbuild": "0.28.0",
|
||||||
"dompurify": "3.3.2"
|
"dompurify": "3.4.0",
|
||||||
|
"postcss": "8.5.14"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
#! /usr/bin/env node
|
#! /usr/bin/env node
|
||||||
import "./extendZod.ts";
|
import "./extendZod";
|
||||||
|
|
||||||
import { runSetupFunctions } from "./setup";
|
import { runSetupFunctions } from "./setup";
|
||||||
import { createApiServer } from "./apiServer";
|
import { createApiServer } from "./apiServer";
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export function getFirstString(value: unknown): string | undefined {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value) && typeof value[0] === "string") {
|
||||||
|
return value[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { resourceAccessToken, resources, apiKeyOrg } from "@server/db";
|
|||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
export async function verifyApiKeyAccessTokenAccess(
|
export async function verifyApiKeyAccessTokenAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -12,7 +13,7 @@ export async function verifyApiKeyAccessTokenAccess(
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const apiKey = req.apiKey;
|
const apiKey = req.apiKey;
|
||||||
const accessTokenId = req.params.accessTokenId;
|
const accessTokenId = getFirstString(req.params.accessTokenId);
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
return next(
|
return next(
|
||||||
@@ -20,6 +21,12 @@ export async function verifyApiKeyAccessTokenAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!accessTokenId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid access token ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const [accessToken] = await db
|
const [accessToken] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(resourceAccessToken)
|
.from(resourceAccessToken)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { apiKeys, apiKeyOrg } from "@server/db";
|
|||||||
import { and, eq, or } from "drizzle-orm";
|
import { and, eq, or } from "drizzle-orm";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
export async function verifyApiKeyApiKeyAccess(
|
export async function verifyApiKeyApiKeyAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -14,8 +15,10 @@ export async function verifyApiKeyApiKeyAccess(
|
|||||||
const { apiKey: callerApiKey } = req;
|
const { apiKey: callerApiKey } = req;
|
||||||
|
|
||||||
const apiKeyId =
|
const apiKeyId =
|
||||||
req.params.apiKeyId || req.body.apiKeyId || req.query.apiKeyId;
|
getFirstString(req.params.apiKeyId) ||
|
||||||
const orgId = req.params.orgId;
|
getFirstString(req.body.apiKeyId) ||
|
||||||
|
getFirstString(req.query.apiKeyId);
|
||||||
|
const orgId = getFirstString(req.params.orgId);
|
||||||
|
|
||||||
if (!callerApiKey) {
|
if (!callerApiKey) {
|
||||||
return next(
|
return next(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { db, domains, orgDomains, apiKeyOrg } from "@server/db";
|
|||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
export async function verifyApiKeyDomainAccess(
|
export async function verifyApiKeyDomainAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -12,8 +13,10 @@ export async function verifyApiKeyDomainAccess(
|
|||||||
try {
|
try {
|
||||||
const apiKey = req.apiKey;
|
const apiKey = req.apiKey;
|
||||||
const domainId =
|
const domainId =
|
||||||
req.params.domainId || req.body.domainId || req.query.domainId;
|
getFirstString(req.params.domainId) ||
|
||||||
const orgId = req.params.orgId;
|
getFirstString(req.body.domainId) ||
|
||||||
|
getFirstString(req.query.domainId);
|
||||||
|
const orgId = getFirstString(req.params.orgId);
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
return next(
|
return next(
|
||||||
@@ -27,6 +30,12 @@ export async function verifyApiKeyDomainAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!orgId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (apiKey.isRoot) {
|
if (apiKey.isRoot) {
|
||||||
// Root keys can access any domain in any org
|
// Root keys can access any domain in any org
|
||||||
return next();
|
return next();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { idp, idpOrg, apiKeyOrg } from "@server/db";
|
|||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
export async function verifyApiKeyIdpAccess(
|
export async function verifyApiKeyIdpAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -12,8 +13,12 @@ export async function verifyApiKeyIdpAccess(
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const apiKey = req.apiKey;
|
const apiKey = req.apiKey;
|
||||||
const idpId = req.params.idpId || req.body.idpId || req.query.idpId;
|
const idpIdRaw =
|
||||||
const orgId = req.params.orgId;
|
getFirstString(req.params.idpId) ||
|
||||||
|
getFirstString(req.body.idpId) ||
|
||||||
|
getFirstString(req.query.idpId);
|
||||||
|
const idpId = Number.parseInt(idpIdRaw ?? "", 10);
|
||||||
|
const orgId = getFirstString(req.params.orgId);
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
return next(
|
return next(
|
||||||
@@ -27,7 +32,7 @@ export async function verifyApiKeyIdpAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!idpId) {
|
if (Number.isNaN(idpId)) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid IDP ID")
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid IDP ID")
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { apiKeyOrg } from "@server/db";
|
|||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
export async function verifyApiKeyOrgAccess(
|
export async function verifyApiKeyOrgAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -12,7 +13,7 @@ export async function verifyApiKeyOrgAccess(
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const apiKeyId = req.apiKey?.apiKeyId;
|
const apiKeyId = req.apiKey?.apiKeyId;
|
||||||
const orgId = req.params.orgId;
|
const orgId = getFirstString(req.params.orgId);
|
||||||
|
|
||||||
if (!apiKeyId) {
|
if (!apiKeyId) {
|
||||||
return next(
|
return next(
|
||||||
@@ -45,7 +46,7 @@ export async function verifyApiKeyOrgAccess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!req.apiKeyOrg) {
|
if (!req.apiKeyOrg) {
|
||||||
next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.FORBIDDEN,
|
||||||
"Key does not have access to this organization"
|
"Key does not have access to this organization"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { siteResources, apiKeyOrg } from "@server/db";
|
|||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
export async function verifyApiKeySiteResourceAccess(
|
export async function verifyApiKeySiteResourceAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -12,7 +13,8 @@ export async function verifyApiKeySiteResourceAccess(
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const apiKey = req.apiKey;
|
const apiKey = req.apiKey;
|
||||||
const siteResourceId = parseInt(req.params.siteResourceId);
|
const siteResourceIdRaw = getFirstString(req.params.siteResourceId);
|
||||||
|
const siteResourceId = Number.parseInt(siteResourceIdRaw ?? "", 10);
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
return next(
|
return next(
|
||||||
@@ -20,7 +22,7 @@ export async function verifyApiKeySiteResourceAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!siteResourceId) {
|
if (Number.isNaN(siteResourceId)) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { resources, targets, apiKeyOrg } from "@server/db";
|
|||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
export async function verifyApiKeyTargetAccess(
|
export async function verifyApiKeyTargetAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -12,7 +13,8 @@ export async function verifyApiKeyTargetAccess(
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const apiKey = req.apiKey;
|
const apiKey = req.apiKey;
|
||||||
const targetId = parseInt(req.params.targetId);
|
const targetIdRaw = getFirstString(req.params.targetId);
|
||||||
|
const targetId = Number.parseInt(targetIdRaw ?? "", 10);
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
return next(
|
return next(
|
||||||
@@ -20,7 +22,7 @@ export async function verifyApiKeyTargetAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNaN(targetId)) {
|
if (Number.isNaN(targetId)) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid target ID")
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid target ID")
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import HttpCode from "@server/types/HttpCode";
|
|||||||
import { canUserAccessResource } from "@server/auth/canUserAccessResource";
|
import { canUserAccessResource } from "@server/auth/canUserAccessResource";
|
||||||
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
export async function verifyAccessTokenAccess(
|
export async function verifyAccessTokenAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -14,7 +15,7 @@ export async function verifyAccessTokenAccess(
|
|||||||
next: NextFunction
|
next: NextFunction
|
||||||
) {
|
) {
|
||||||
const userId = req.user!.userId;
|
const userId = req.user!.userId;
|
||||||
const accessTokenId = req.params.accessTokenId;
|
const accessTokenId = getFirstString(req.params.accessTokenId);
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return next(
|
return next(
|
||||||
@@ -22,6 +23,12 @@ export async function verifyAccessTokenAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!accessTokenId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid access token ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const [accessToken] = await db
|
const [accessToken] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(resourceAccessToken)
|
.from(resourceAccessToken)
|
||||||
@@ -87,7 +94,7 @@ export async function verifyAccessTokenAccess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!req.userOrg) {
|
if (!req.userOrg) {
|
||||||
next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.FORBIDDEN,
|
||||||
"User does not have access to this organization"
|
"User does not have access to this organization"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import createHttpError from "http-errors";
|
|||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
export async function verifyApiKeyAccess(
|
export async function verifyApiKeyAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -14,9 +15,24 @@ export async function verifyApiKeyAccess(
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const userId = req.user!.userId;
|
const userId = req.user!.userId;
|
||||||
const apiKeyId =
|
const apiKeyIdFromParams = getFirstString(req.params?.apiKeyId);
|
||||||
req.params.apiKeyId || req.body.apiKeyId || req.query.apiKeyId;
|
const apiKeyIdFromBody = getFirstString(req.body?.apiKeyId);
|
||||||
const orgId = req.params.orgId;
|
|
||||||
|
if (
|
||||||
|
apiKeyIdFromParams &&
|
||||||
|
apiKeyIdFromBody &&
|
||||||
|
apiKeyIdFromParams !== apiKeyIdFromBody
|
||||||
|
) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"API key ID provided in both URL and body with different values"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKeyId = apiKeyIdFromParams || apiKeyIdFromBody;
|
||||||
|
const orgId = getFirstString(req.params.orgId);
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return next(
|
return next(
|
||||||
@@ -104,10 +120,7 @@ export async function verifyApiKeyAccess(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
req.userOrgRoleIds = await getUserOrgRoleIds(
|
req.userOrgRoleIds = await getUserOrgRoleIds(req.userOrg.userId, orgId);
|
||||||
req.userOrg.userId,
|
|
||||||
orgId
|
|
||||||
);
|
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import createHttpError from "http-errors";
|
|||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
export async function verifyDomainAccess(
|
export async function verifyDomainAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -14,9 +15,8 @@ export async function verifyDomainAccess(
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const userId = req.user!.userId;
|
const userId = req.user!.userId;
|
||||||
const domainId =
|
const domainId = getFirstString(req.params.domainId);
|
||||||
req.params.domainId;
|
const orgId = getFirstString(req.params.orgId);
|
||||||
const orgId = req.params.orgId;
|
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return next(
|
return next(
|
||||||
@@ -62,10 +62,7 @@ export async function verifyDomainAccess(
|
|||||||
.select()
|
.select()
|
||||||
.from(userOrgs)
|
.from(userOrgs)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))
|
||||||
eq(userOrgs.userId, userId),
|
|
||||||
eq(userOrgs.orgId, orgId)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
req.userOrg = userOrgRole[0];
|
req.userOrg = userOrgRole[0];
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import createHttpError from "http-errors";
|
|||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { usageService } from "@server/lib/billing/usageService";
|
import { usageService } from "@server/lib/billing/usageService";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
export async function verifyLimits(
|
export async function verifyLimits(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -13,7 +14,10 @@ export async function verifyLimits(
|
|||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
const orgId = req.userOrgId || req.apiKeyOrg?.orgId || req.params.orgId;
|
const orgId =
|
||||||
|
req.userOrgId ||
|
||||||
|
req.apiKeyOrg?.orgId ||
|
||||||
|
getFirstString(req.params.orgId);
|
||||||
|
|
||||||
if (!orgId) {
|
if (!orgId) {
|
||||||
return next(); // its fine if we silently fail here because this is not critical to operation or security and its better user experience if we dont fail
|
return next(); // its fine if we silently fail here because this is not critical to operation or security and its better user experience if we dont fail
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import createHttpError from "http-errors";
|
|||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
export async function verifyOrgAccess(
|
export async function verifyOrgAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -13,7 +14,7 @@ export async function verifyOrgAccess(
|
|||||||
next: NextFunction
|
next: NextFunction
|
||||||
) {
|
) {
|
||||||
const userId = req.user!.userId;
|
const userId = req.user!.userId;
|
||||||
const orgId = req.params.orgId;
|
const orgId = getFirstString(req.params.orgId);
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return next(
|
return next(
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { db, userOrgs, siteProvisioningKeys, siteProvisioningKeyOrg } from "@server/db";
|
import {
|
||||||
|
db,
|
||||||
|
userOrgs,
|
||||||
|
siteProvisioningKeys,
|
||||||
|
siteProvisioningKeyOrg
|
||||||
|
} from "@server/db";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
export async function verifySiteProvisioningKeyAccess(
|
export async function verifySiteProvisioningKeyAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -13,8 +19,10 @@ export async function verifySiteProvisioningKeyAccess(
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const userId = req.user!.userId;
|
const userId = req.user!.userId;
|
||||||
const siteProvisioningKeyId = req.params.siteProvisioningKeyId;
|
const siteProvisioningKeyId = getFirstString(
|
||||||
const orgId = req.params.orgId;
|
req.params.siteProvisioningKeyId
|
||||||
|
);
|
||||||
|
const orgId = getFirstString(req.params.orgId);
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return next(
|
return next(
|
||||||
@@ -80,10 +88,7 @@ export async function verifySiteProvisioningKeyAccess(
|
|||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(userOrgs.userId, userId),
|
eq(userOrgs.userId, userId),
|
||||||
eq(
|
eq(userOrgs.orgId, row.siteProvisioningKeyOrg.orgId)
|
||||||
userOrgs.orgId,
|
|
||||||
row.siteProvisioningKeyOrg.orgId
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import HttpCode from "@server/types/HttpCode";
|
|||||||
import { canUserAccessResource } from "../auth/canUserAccessResource";
|
import { canUserAccessResource } from "../auth/canUserAccessResource";
|
||||||
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
export async function verifyTargetAccess(
|
export async function verifyTargetAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -14,7 +15,8 @@ export async function verifyTargetAccess(
|
|||||||
next: NextFunction
|
next: NextFunction
|
||||||
) {
|
) {
|
||||||
const userId = req.user!.userId;
|
const userId = req.user!.userId;
|
||||||
const targetId = parseInt(req.params.targetId);
|
const targetIdRaw = getFirstString(req.params.targetId);
|
||||||
|
const targetId = Number.parseInt(targetIdRaw ?? "", 10);
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return next(
|
return next(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { userOrgs } from "@server/db";
|
|||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
export async function verifyUserIsOrgOwner(
|
export async function verifyUserIsOrgOwner(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -11,7 +12,7 @@ export async function verifyUserIsOrgOwner(
|
|||||||
next: NextFunction
|
next: NextFunction
|
||||||
) {
|
) {
|
||||||
const userId = req.user!.userId;
|
const userId = req.user!.userId;
|
||||||
const orgId = req.params.orgId;
|
const orgId = getFirstString(req.params.orgId);
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return next(
|
return next(
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { eq, and } from "drizzle-orm";
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
export async function verifyCertificateAccess(
|
export async function verifyCertificateAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -27,11 +28,43 @@ export async function verifyCertificateAccess(
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
// Assume user/org access is already verified
|
// Assume user/org access is already verified
|
||||||
const orgId = req.params.orgId;
|
const orgId = getFirstString(req.params.orgId);
|
||||||
const certId =
|
|
||||||
req.params.certId || req.body?.certId || req.query?.certId;
|
const certIdFromParams = getFirstString(req.params?.certId);
|
||||||
let domainId =
|
const certIdFromBody = getFirstString(req.body?.certId);
|
||||||
req.params.domainId || req.body?.domainId || req.query?.domainId;
|
|
||||||
|
if (
|
||||||
|
certIdFromParams &&
|
||||||
|
certIdFromBody &&
|
||||||
|
certIdFromParams !== certIdFromBody
|
||||||
|
) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Certificate ID provided in both URL and body with different values"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const certId = certIdFromParams || certIdFromBody;
|
||||||
|
|
||||||
|
const domainIdFromParams = getFirstString(req.params?.domainId);
|
||||||
|
const domainIdFromBody = getFirstString(req.body?.domainId);
|
||||||
|
|
||||||
|
if (
|
||||||
|
domainIdFromParams &&
|
||||||
|
domainIdFromBody &&
|
||||||
|
domainIdFromParams !== domainIdFromBody
|
||||||
|
) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Domain ID provided in both URL and body with different values"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let domainId = domainIdFromParams || domainIdFromBody;
|
||||||
|
|
||||||
if (!orgId) {
|
if (!orgId) {
|
||||||
return next(
|
return next(
|
||||||
@@ -65,7 +98,7 @@ export async function verifyCertificateAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
domainId = cert.domainId;
|
domainId = cert.domainId ?? undefined;
|
||||||
if (!domainId) {
|
if (!domainId) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { and, eq } from "drizzle-orm";
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
export async function verifyIdpAccess(
|
export async function verifyIdpAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -25,8 +26,12 @@ export async function verifyIdpAccess(
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const userId = req.user!.userId;
|
const userId = req.user!.userId;
|
||||||
const idpId = req.params.idpId || req.body.idpId || req.query.idpId;
|
const idpIdRaw =
|
||||||
const orgId = req.params.orgId;
|
getFirstString(req.params.idpId) ||
|
||||||
|
getFirstString(req.body?.idpId) ||
|
||||||
|
getFirstString(req.query?.idpId);
|
||||||
|
const idpId = Number.parseInt(idpIdRaw ?? "", 10);
|
||||||
|
const orgId = getFirstString(req.params.orgId);
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return next(
|
return next(
|
||||||
@@ -40,7 +45,7 @@ export async function verifyIdpAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!idpId) {
|
if (Number.isNaN(idpId)) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid key ID")
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid key ID")
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { and, eq } from "drizzle-orm";
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
export async function verifyRemoteExitNodeAccess(
|
export async function verifyRemoteExitNodeAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -25,11 +26,11 @@ export async function verifyRemoteExitNodeAccess(
|
|||||||
next: NextFunction
|
next: NextFunction
|
||||||
) {
|
) {
|
||||||
const userId = req.user!.userId; // Assuming you have user information in the request
|
const userId = req.user!.userId; // Assuming you have user information in the request
|
||||||
const orgId = req.params.orgId;
|
const orgId = getFirstString(req.params.orgId);
|
||||||
const remoteExitNodeId =
|
const remoteExitNodeId =
|
||||||
req.params.remoteExitNodeId ||
|
getFirstString(req.params.remoteExitNodeId) ||
|
||||||
req.body.remoteExitNodeId ||
|
getFirstString(req.body?.remoteExitNodeId) ||
|
||||||
req.query.remoteExitNodeId;
|
getFirstString(req.query?.remoteExitNodeId);
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return next(
|
return next(
|
||||||
@@ -37,6 +38,15 @@ export async function verifyRemoteExitNodeAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!orgId || !remoteExitNodeId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Invalid organization or remote exit node ID"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [remoteExitNode] = await db
|
const [remoteExitNode] = await db
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
@@ -16,40 +16,44 @@ import HttpCode from "@server/types/HttpCode";
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { response as sendResponse } from "@server/lib/response";
|
import { response as sendResponse } from "@server/lib/response";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
import privateConfig from "#private/lib/config";
|
import privateConfig from "#private/lib/config";
|
||||||
import { GenerateNewLicenseResponse } from "@server/routers/generatedLicense/types";
|
import { GenerateNewLicenseResponse } from "@server/routers/generatedLicense/types";
|
||||||
|
|
||||||
export interface CreateNewLicenseResponse {
|
export interface CreateNewLicenseResponse {
|
||||||
data: Data
|
data: Data;
|
||||||
success: boolean
|
success: boolean;
|
||||||
error: boolean
|
error: boolean;
|
||||||
message: string
|
message: string;
|
||||||
status: number
|
status: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Data {
|
export interface Data {
|
||||||
licenseKey: LicenseKey
|
licenseKey: LicenseKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LicenseKey {
|
export interface LicenseKey {
|
||||||
id: number
|
id: number;
|
||||||
instanceName: any
|
instanceName: any;
|
||||||
instanceId: string
|
instanceId: string;
|
||||||
licenseKey: string
|
licenseKey: string;
|
||||||
tier: string
|
tier: string;
|
||||||
type: string
|
type: string;
|
||||||
quantity: number
|
quantity: number;
|
||||||
quantity_2: number
|
quantity_2: number;
|
||||||
isValid: boolean
|
isValid: boolean;
|
||||||
updatedAt: string
|
updatedAt: string;
|
||||||
createdAt: string
|
createdAt: string;
|
||||||
expiresAt: string
|
expiresAt: string;
|
||||||
paidFor: boolean
|
paidFor: boolean;
|
||||||
orgId: string
|
orgId: string;
|
||||||
metadata: string
|
metadata: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createNewLicense(orgId: string, licenseData: any): Promise<CreateNewLicenseResponse> {
|
export async function createNewLicense(
|
||||||
|
orgId: string,
|
||||||
|
licenseData: any
|
||||||
|
): Promise<CreateNewLicenseResponse> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/${orgId}/create`, // this says enterprise but it does both
|
`${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/${orgId}/create`, // this says enterprise but it does both
|
||||||
@@ -80,7 +84,7 @@ export async function generateNewLicense(
|
|||||||
next: NextFunction
|
next: NextFunction
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const { orgId } = req.params;
|
const orgId = getFirstString(req.params.orgId);
|
||||||
|
|
||||||
if (!orgId) {
|
if (!orgId) {
|
||||||
return next(
|
return next(
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import HttpCode from "@server/types/HttpCode";
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { response as sendResponse } from "@server/lib/response";
|
import { response as sendResponse } from "@server/lib/response";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
import privateConfig from "#private/lib/config";
|
import privateConfig from "#private/lib/config";
|
||||||
import {
|
import {
|
||||||
GeneratedLicenseKey,
|
GeneratedLicenseKey,
|
||||||
@@ -55,7 +56,7 @@ export async function listSaasLicenseKeys(
|
|||||||
next: NextFunction
|
next: NextFunction
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const { orgId } = req.params;
|
const orgId = getFirstString(req.params.orgId);
|
||||||
|
|
||||||
if (!orgId) {
|
if (!orgId) {
|
||||||
return next(
|
return next(
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { UserType } from "@server/types/UserTypes";
|
|||||||
import { verifyPassword } from "@server/auth/password";
|
import { verifyPassword } from "@server/auth/password";
|
||||||
import { unauthorized } from "@server/auth/unauthorizedResponse";
|
import { unauthorized } from "@server/auth/unauthorizedResponse";
|
||||||
import { verifyTotpCode } from "@server/auth/totp";
|
import { verifyTotpCode } from "@server/auth/totp";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
// The RP ID is the domain name of your application
|
// The RP ID is the domain name of your application
|
||||||
const rpID = (() => {
|
const rpID = (() => {
|
||||||
@@ -406,7 +407,12 @@ export async function deleteSecurityKey(
|
|||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const { credentialId: encodedCredentialId } = req.params;
|
const encodedCredentialId = getFirstString(req.params.credentialId);
|
||||||
|
if (!encodedCredentialId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid credential ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
const credentialId = decodeURIComponent(encodedCredentialId);
|
const credentialId = decodeURIComponent(encodedCredentialId);
|
||||||
const user = req.user as User;
|
const user = req.user as User;
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import createHttpError from "http-errors";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { domain } from "zod/v4/core/regexes";
|
|
||||||
|
|
||||||
const getDomainSchema = z.strictObject({
|
const getDomainSchema = z.strictObject({
|
||||||
domainId: z.string().optional(),
|
domainId: z.string().optional(),
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { response } from "@server/lib/response";
|
import { response } from "@server/lib/response";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
export async function getUserResources(
|
export async function getUserResources(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -26,7 +27,7 @@ export async function getUserResources(
|
|||||||
next: NextFunction
|
next: NextFunction
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const { orgId } = req.params;
|
const orgId = getFirstString(req.params.orgId);
|
||||||
const userId = req.user?.userId;
|
const userId = req.user?.userId;
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
@@ -35,6 +36,12 @@ export async function getUserResources(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!orgId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Check user is in organization and get their role IDs
|
// Check user is in organization and get their role IDs
|
||||||
const [userOrg] = await db
|
const [userOrg] = await db
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ import { build } from "@server/build";
|
|||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import { UserType } from "@server/types/UserTypes";
|
import { UserType } from "@server/types/UserTypes";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import SetResourcePasswordForm from "components/SetResourcePasswordForm";
|
import SetResourcePasswordForm from "@app/components/SetResourcePasswordForm";
|
||||||
import { Binary, Bot, InfoIcon, Key } from "lucide-react";
|
import { Binary, Bot, InfoIcon, Key } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|||||||
+3
-1
@@ -21,9 +21,11 @@ export default async function Page(props: {
|
|||||||
searchParams: Promise<{
|
searchParams: Promise<{
|
||||||
redirect: string | undefined;
|
redirect: string | undefined;
|
||||||
t: string | undefined;
|
t: string | undefined;
|
||||||
|
orgs?: string | undefined;
|
||||||
}>;
|
}>;
|
||||||
}) {
|
}) {
|
||||||
const params = await props.searchParams; // this is needed to prevent static optimization
|
const params = await props.searchParams; // this is needed to prevent static optimization
|
||||||
|
const showOrgPicker = params.orgs === "1";
|
||||||
|
|
||||||
const env = pullEnv();
|
const env = pullEnv();
|
||||||
|
|
||||||
@@ -106,7 +108,7 @@ export default async function Page(props: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetOrgId) {
|
if (targetOrgId && !showOrgPicker) {
|
||||||
return <RedirectToOrg targetOrgId={targetOrgId} />;
|
return <RedirectToOrg targetOrgId={targetOrgId} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+41
-38
@@ -5,6 +5,7 @@ import type { SidebarNavSection } from "@app/app/navigation";
|
|||||||
import { LayoutSidebar } from "@app/components/LayoutSidebar";
|
import { LayoutSidebar } from "@app/components/LayoutSidebar";
|
||||||
import { LayoutHeader } from "@app/components/LayoutHeader";
|
import { LayoutHeader } from "@app/components/LayoutHeader";
|
||||||
import { LayoutMobileMenu } from "@app/components/LayoutMobileMenu";
|
import { LayoutMobileMenu } from "@app/components/LayoutMobileMenu";
|
||||||
|
import { CommandPaletteProvider } from "@app/components/command-palette/CommandPaletteProvider";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
@@ -37,51 +38,53 @@ export async function Layout({
|
|||||||
(sidebarStateCookie !== "expanded" && defaultSidebarCollapsed);
|
(sidebarStateCookie !== "expanded" && defaultSidebarCollapsed);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen-safe overflow-hidden">
|
<CommandPaletteProvider orgId={orgId} orgs={orgs} navItems={navItems}>
|
||||||
{/* Desktop Sidebar */}
|
<div className="flex h-screen-safe overflow-hidden">
|
||||||
{showSidebar && (
|
{/* Desktop Sidebar */}
|
||||||
<LayoutSidebar
|
{showSidebar && (
|
||||||
orgId={orgId}
|
<LayoutSidebar
|
||||||
orgs={orgs}
|
|
||||||
navItems={navItems}
|
|
||||||
defaultSidebarCollapsed={initialSidebarCollapsed}
|
|
||||||
hasCookiePreference={hasCookiePreference}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Main content area */}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex-1 flex flex-col h-full min-w-0 relative",
|
|
||||||
!showSidebar && "w-full"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{/* Mobile header */}
|
|
||||||
{showHeader && (
|
|
||||||
<LayoutMobileMenu
|
|
||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
orgs={orgs}
|
orgs={orgs}
|
||||||
navItems={navItems}
|
navItems={navItems}
|
||||||
showSidebar={showSidebar}
|
defaultSidebarCollapsed={initialSidebarCollapsed}
|
||||||
showTopBar={showTopBar}
|
hasCookiePreference={hasCookiePreference}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Desktop header */}
|
{/* Main content area */}
|
||||||
{showHeader && <LayoutHeader showTopBar={showTopBar} />}
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex-1 flex flex-col h-full min-w-0 relative",
|
||||||
|
!showSidebar && "w-full"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Mobile header */}
|
||||||
|
{showHeader && (
|
||||||
|
<LayoutMobileMenu
|
||||||
|
orgId={orgId}
|
||||||
|
orgs={orgs}
|
||||||
|
navItems={navItems}
|
||||||
|
showSidebar={showSidebar}
|
||||||
|
showTopBar={showTopBar}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Main content */}
|
{/* Desktop header */}
|
||||||
<main className="flex-1 overflow-y-auto p-3 md:p-6 w-full">
|
{showHeader && <LayoutHeader showTopBar={showTopBar} />}
|
||||||
<div
|
|
||||||
className={cn(
|
{/* Main content */}
|
||||||
"container mx-auto max-w-12xl mb-12",
|
<main className="flex-1 overflow-y-auto p-3 md:p-6 w-full">
|
||||||
showHeader && "md:pt-14" // Add top padding only on desktop to account for fixed header
|
<div
|
||||||
)}
|
className={cn(
|
||||||
>
|
"container mx-auto max-w-12xl mb-12",
|
||||||
{children}
|
showHeader && "md:pt-14" // Add top padding only on desktop to account for fixed header
|
||||||
</div>
|
)}
|
||||||
</main>
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CommandPaletteProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import ProfileIcon from "@app/components/ProfileIcon";
|
|||||||
import ThemeSwitcher from "@app/components/ThemeSwitcher";
|
import ThemeSwitcher from "@app/components/ThemeSwitcher";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import BrandingLogo from "./BrandingLogo";
|
import BrandingLogo from "./BrandingLogo";
|
||||||
|
import { CommandPaletteTrigger } from "@app/components/command-palette/CommandPaletteTrigger";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||||
|
|
||||||
@@ -67,6 +68,7 @@ export function LayoutHeader({ showTopBar }: LayoutHeaderProps) {
|
|||||||
|
|
||||||
{showTopBar && (
|
{showTopBar && (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
<CommandPaletteTrigger />
|
||||||
<ThemeSwitcher />
|
<ThemeSwitcher />
|
||||||
<ProfileIcon />
|
<ProfileIcon />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { useUserContext } from "@app/hooks/useUserContext";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import ProfileIcon from "@app/components/ProfileIcon";
|
import ProfileIcon from "@app/components/ProfileIcon";
|
||||||
import ThemeSwitcher from "@app/components/ThemeSwitcher";
|
import ThemeSwitcher from "@app/components/ThemeSwitcher";
|
||||||
|
import { CommandPaletteTrigger } from "@app/components/command-palette/CommandPaletteTrigger";
|
||||||
import type { SidebarNavSection } from "@app/app/navigation";
|
import type { SidebarNavSection } from "@app/app/navigation";
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Sheet,
|
||||||
@@ -121,6 +122,7 @@ export function LayoutMobileMenu({
|
|||||||
{showTopBar && (
|
{showTopBar && (
|
||||||
<div className="ml-auto flex items-center justify-end">
|
<div className="ml-auto flex items-center justify-end">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
<CommandPaletteTrigger variant="mobile" />
|
||||||
<ThemeSwitcher />
|
<ThemeSwitcher />
|
||||||
<ProfileIcon />
|
<ProfileIcon />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,270 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CommandDialog,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
CommandSeparator
|
||||||
|
} from "@app/components/ui/command";
|
||||||
|
import type { SidebarNavSection } from "@app/app/navigation";
|
||||||
|
import { Badge } from "@app/components/ui/badge";
|
||||||
|
import { ListUserOrgsResponse } from "@server/routers/org";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useCommandPalette } from "./commandPaletteContext";
|
||||||
|
import { useCommandPaletteActions } from "./useCommandPaletteActions";
|
||||||
|
import { useCommandPaletteNavigation } from "./useCommandPaletteNavigation";
|
||||||
|
import { useCommandPaletteOrganizations } from "./useCommandPaletteOrganizations";
|
||||||
|
import { useCommandPaletteSearch } from "./useCommandPaletteSearch";
|
||||||
|
|
||||||
|
type CommandPaletteProps = {
|
||||||
|
orgId?: string;
|
||||||
|
orgs?: ListUserOrgsResponse["orgs"];
|
||||||
|
navItems: SidebarNavSection[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CommandPalette({ orgId, orgs, navItems }: CommandPaletteProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
const router = useRouter();
|
||||||
|
const { open, setOpen } = useCommandPalette();
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
|
const navigationGroups = useCommandPaletteNavigation(navItems);
|
||||||
|
const organizations = useCommandPaletteOrganizations(orgs);
|
||||||
|
const actions = useCommandPaletteActions(orgId, orgs);
|
||||||
|
const { shouldSearch, sites, resources, users, machineClients, isLoading } =
|
||||||
|
useCommandPaletteSearch({
|
||||||
|
orgId,
|
||||||
|
query: search,
|
||||||
|
enabled: open
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleOpenChange = useCallback(
|
||||||
|
(nextOpen: boolean) => {
|
||||||
|
setOpen(nextOpen);
|
||||||
|
if (!nextOpen) {
|
||||||
|
setSearch("");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setOpen]
|
||||||
|
);
|
||||||
|
|
||||||
|
const runCommand = useCallback(
|
||||||
|
(command: () => void) => {
|
||||||
|
setOpen(false);
|
||||||
|
setSearch("");
|
||||||
|
command();
|
||||||
|
},
|
||||||
|
[setOpen]
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasEntityResults =
|
||||||
|
sites.length > 0 ||
|
||||||
|
resources.length > 0 ||
|
||||||
|
users.length > 0 ||
|
||||||
|
machineClients.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandDialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
title={t("commandPaletteTitle")}
|
||||||
|
description={t("commandPaletteDescription")}
|
||||||
|
>
|
||||||
|
<CommandInput
|
||||||
|
placeholder={t("commandPaletteSearchPlaceholder")}
|
||||||
|
value={search}
|
||||||
|
onValueChange={setSearch}
|
||||||
|
/>
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>{t("commandPaletteNoResults")}</CommandEmpty>
|
||||||
|
|
||||||
|
{navigationGroups.map((group) => (
|
||||||
|
<CommandGroup key={group.heading} heading={group.heading}>
|
||||||
|
{group.items.map((item) => (
|
||||||
|
<CommandItem
|
||||||
|
key={item.id}
|
||||||
|
value={`${item.title} ${group.heading}`}
|
||||||
|
onSelect={() =>
|
||||||
|
runCommand(() => router.push(item.href))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item.icon}
|
||||||
|
<span>{item.title}</span>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{organizations.length > 1 && (
|
||||||
|
<>
|
||||||
|
<CommandSeparator />
|
||||||
|
<CommandGroup
|
||||||
|
heading={t("commandPaletteOrganizations")}
|
||||||
|
>
|
||||||
|
{organizations.map((org) => (
|
||||||
|
<CommandItem
|
||||||
|
key={org.id}
|
||||||
|
value={`${org.name} ${org.orgId}`}
|
||||||
|
onSelect={() =>
|
||||||
|
runCommand(() => router.push(org.href))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="truncate">{org.name}</span>
|
||||||
|
<span className="text-xs text-muted-foreground font-mono truncate">
|
||||||
|
{org.orgId}
|
||||||
|
</span>
|
||||||
|
{org.isPrimaryOrg && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="ml-auto shrink-0 text-[10px] px-1.5 py-0"
|
||||||
|
>
|
||||||
|
{t("primary")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{shouldSearch && orgId && (
|
||||||
|
<>
|
||||||
|
<CommandSeparator />
|
||||||
|
{isLoading && !hasEntityResults ? (
|
||||||
|
<div className="flex items-center justify-center gap-2 py-6 text-sm text-muted-foreground">
|
||||||
|
<Loader2 className="size-4 animate-spin" />
|
||||||
|
{t("commandPaletteSearching")}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{sites.length > 0 && (
|
||||||
|
<CommandGroup
|
||||||
|
heading={t("commandPaletteSites")}
|
||||||
|
>
|
||||||
|
{sites.map((site) => (
|
||||||
|
<CommandItem
|
||||||
|
key={site.id}
|
||||||
|
value={`${site.name} site`}
|
||||||
|
onSelect={() =>
|
||||||
|
runCommand(() =>
|
||||||
|
router.push(site.href)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="truncate">
|
||||||
|
{site.name}
|
||||||
|
</span>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
)}
|
||||||
|
{resources.length > 0 && (
|
||||||
|
<CommandGroup
|
||||||
|
heading={t("commandPaletteResources")}
|
||||||
|
>
|
||||||
|
{resources.map((resource) => (
|
||||||
|
<CommandItem
|
||||||
|
key={resource.id}
|
||||||
|
value={`${resource.name} resource`}
|
||||||
|
onSelect={() =>
|
||||||
|
runCommand(() =>
|
||||||
|
router.push(
|
||||||
|
resource.href
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="truncate">
|
||||||
|
{resource.name}
|
||||||
|
</span>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
)}
|
||||||
|
{users.length > 0 && (
|
||||||
|
<CommandGroup
|
||||||
|
heading={t("commandPaletteUsers")}
|
||||||
|
>
|
||||||
|
{users.map((user) => (
|
||||||
|
<CommandItem
|
||||||
|
key={user.id}
|
||||||
|
value={`${user.name} ${user.email}`}
|
||||||
|
onSelect={() =>
|
||||||
|
runCommand(() =>
|
||||||
|
router.push(user.href)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex min-w-0 flex-col">
|
||||||
|
<span className="truncate">
|
||||||
|
{user.name}
|
||||||
|
</span>
|
||||||
|
<span className="truncate text-xs text-muted-foreground">
|
||||||
|
{user.email}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
)}
|
||||||
|
{machineClients.length > 0 && (
|
||||||
|
<CommandGroup
|
||||||
|
heading={t("commandPaletteClients")}
|
||||||
|
>
|
||||||
|
{machineClients.map((client) => (
|
||||||
|
<CommandItem
|
||||||
|
key={client.id}
|
||||||
|
value={`${client.name} client`}
|
||||||
|
onSelect={() =>
|
||||||
|
runCommand(() =>
|
||||||
|
router.push(client.href)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="truncate">
|
||||||
|
{client.name}
|
||||||
|
</span>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{actions.length > 0 && (
|
||||||
|
<>
|
||||||
|
<CommandSeparator />
|
||||||
|
<CommandGroup heading={t("commandPaletteActions")}>
|
||||||
|
{actions.map((action) => (
|
||||||
|
<CommandItem
|
||||||
|
key={action.id}
|
||||||
|
value={action.label}
|
||||||
|
onSelect={() =>
|
||||||
|
runCommand(() => {
|
||||||
|
if (action.onSelect) {
|
||||||
|
action.onSelect();
|
||||||
|
} else if (action.href) {
|
||||||
|
router.push(action.href);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{action.icon}
|
||||||
|
<span>{action.label}</span>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CommandList>
|
||||||
|
</CommandDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { SidebarNavSection } from "@app/app/navigation";
|
||||||
|
import { ListUserOrgsResponse } from "@server/routers/org";
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
CommandPaletteContextProvider,
|
||||||
|
type CommandPaletteContextValue
|
||||||
|
} from "./commandPaletteContext";
|
||||||
|
import { CommandPalette } from "./CommandPalette";
|
||||||
|
|
||||||
|
type CommandPaletteProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
orgId?: string;
|
||||||
|
orgs?: ListUserOrgsResponse["orgs"];
|
||||||
|
navItems: SidebarNavSection[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function isEditableTarget(target: EventTarget | null) {
|
||||||
|
if (!(target instanceof HTMLElement)) return false;
|
||||||
|
if (target.isContentEditable) return true;
|
||||||
|
const tagName = target.tagName;
|
||||||
|
return (
|
||||||
|
tagName === "INPUT" || tagName === "TEXTAREA" || tagName === "SELECT"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CommandPaletteProvider({
|
||||||
|
children,
|
||||||
|
orgId,
|
||||||
|
orgs,
|
||||||
|
navItems
|
||||||
|
}: CommandPaletteProviderProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const toggle = useCallback(() => {
|
||||||
|
setOpen((current) => !current);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const contextValue = useMemo<CommandPaletteContextValue>(
|
||||||
|
() => ({
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
toggle
|
||||||
|
}),
|
||||||
|
[open, toggle]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function onKeyDown(event: KeyboardEvent) {
|
||||||
|
if (
|
||||||
|
event.key.toLowerCase() !== "k" ||
|
||||||
|
!(event.metaKey || event.ctrlKey)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!open && isEditableTarget(event.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
toggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("keydown", onKeyDown);
|
||||||
|
return () => document.removeEventListener("keydown", onKeyDown);
|
||||||
|
}, [open, toggle]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandPaletteContextProvider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
<CommandPalette orgId={orgId} orgs={orgs} navItems={navItems} />
|
||||||
|
</CommandPaletteContextProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@app/components/ui/button";
|
||||||
|
import { CommandShortcut } from "@app/components/ui/command";
|
||||||
|
import { cn } from "@app/lib/cn";
|
||||||
|
import { Search } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useCommandPalette } from "./commandPaletteContext";
|
||||||
|
|
||||||
|
type CommandPaletteTriggerProps = {
|
||||||
|
variant?: "header" | "mobile";
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function useIsMac() {
|
||||||
|
const [isMac, setIsMac] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsMac(/Mac|iPhone|iPod|iPad/.test(navigator.platform));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return isMac;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CommandPaletteTrigger({
|
||||||
|
variant = "header",
|
||||||
|
className
|
||||||
|
}: CommandPaletteTriggerProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
const { setOpen } = useCommandPalette();
|
||||||
|
const isMac = useIsMac();
|
||||||
|
|
||||||
|
if (variant === "mobile") {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={className}
|
||||||
|
aria-label={t("commandPaletteTitle")}
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
<Search className="size-5" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"hidden h-9 w-56 justify-start gap-2 px-3 text-muted-foreground md:flex lg:w-64",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
aria-label={t("commandPaletteTitle")}
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
<Search className="size-4 shrink-0 opacity-50" />
|
||||||
|
<span className="flex-1 truncate text-left text-sm font-normal">
|
||||||
|
{t("commandPaletteSearchPlaceholder")}
|
||||||
|
</span>
|
||||||
|
<CommandShortcut>
|
||||||
|
{isMac
|
||||||
|
? t("commandPaletteShortcutMac")
|
||||||
|
: t("commandPaletteShortcutWindows")}
|
||||||
|
</CommandShortcut>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { createContext, useContext } from "react";
|
||||||
|
|
||||||
|
export type CommandPaletteContextValue = {
|
||||||
|
open: boolean;
|
||||||
|
setOpen: (open: boolean) => void;
|
||||||
|
toggle: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CommandPaletteContext = createContext<CommandPaletteContextValue | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
export function CommandPaletteContextProvider({
|
||||||
|
value,
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
value: CommandPaletteContextValue;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<CommandPaletteContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</CommandPaletteContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCommandPalette() {
|
||||||
|
const context = useContext(CommandPaletteContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
"useCommandPalette must be used within CommandPaletteProvider"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { useUserContext } from "@app/hooks/useUserContext";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
import {
|
||||||
|
BellRing,
|
||||||
|
Building2,
|
||||||
|
Globe,
|
||||||
|
KeyRound,
|
||||||
|
MonitorUp,
|
||||||
|
Plus,
|
||||||
|
SunMoon,
|
||||||
|
UserPlus
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { ListUserOrgsResponse } from "@server/routers/org";
|
||||||
|
|
||||||
|
export type CommandPaletteAction = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
icon: ReactNode;
|
||||||
|
href?: string;
|
||||||
|
onSelect?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useCommandPaletteActions(
|
||||||
|
orgId?: string,
|
||||||
|
orgs?: ListUserOrgsResponse["orgs"]
|
||||||
|
): CommandPaletteAction[] {
|
||||||
|
const t = useTranslations();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const { env } = useEnvContext();
|
||||||
|
const { user } = useUserContext();
|
||||||
|
const { setTheme, theme } = useTheme();
|
||||||
|
const isAdminPage = pathname?.startsWith("/admin");
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const actions: CommandPaletteAction[] = [];
|
||||||
|
|
||||||
|
function cycleTheme() {
|
||||||
|
const currentTheme = theme || "system";
|
||||||
|
if (currentTheme === "light") {
|
||||||
|
setTheme("dark");
|
||||||
|
} else if (currentTheme === "dark") {
|
||||||
|
setTheme("system");
|
||||||
|
} else {
|
||||||
|
setTheme("light");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAdminPage) {
|
||||||
|
actions.push({
|
||||||
|
id: "create-admin-api-key",
|
||||||
|
label: t("commandPaletteCreateApiKey"),
|
||||||
|
icon: <KeyRound className="size-4" />,
|
||||||
|
href: "/admin/api-keys/create"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
build === "oss" ||
|
||||||
|
env?.app.identityProviderMode === "global" ||
|
||||||
|
env?.app.identityProviderMode === undefined
|
||||||
|
) {
|
||||||
|
actions.push({
|
||||||
|
id: "create-admin-idp",
|
||||||
|
label: t("commandPaletteCreateIdentityProvider"),
|
||||||
|
icon: <Plus className="size-4" />,
|
||||||
|
href: "/admin/idp/create"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (orgId) {
|
||||||
|
actions.push({
|
||||||
|
id: "create-site",
|
||||||
|
label: t("commandPaletteCreateSite"),
|
||||||
|
icon: <Plus className="size-4" />,
|
||||||
|
href: `/${orgId}/settings/sites/create`
|
||||||
|
});
|
||||||
|
actions.push({
|
||||||
|
id: "create-proxy-resource",
|
||||||
|
label: t("commandPaletteCreateProxyResource"),
|
||||||
|
icon: <Globe className="size-4" />,
|
||||||
|
href: `/${orgId}/settings/resources/proxy/create`
|
||||||
|
});
|
||||||
|
actions.push({
|
||||||
|
id: "create-user",
|
||||||
|
label: t("commandPaletteCreateUser"),
|
||||||
|
icon: <UserPlus className="size-4" />,
|
||||||
|
href: `/${orgId}/settings/access/users/create`
|
||||||
|
});
|
||||||
|
actions.push({
|
||||||
|
id: "create-api-key",
|
||||||
|
label: t("commandPaletteCreateApiKey"),
|
||||||
|
icon: <KeyRound className="size-4" />,
|
||||||
|
href: `/${orgId}/settings/api-keys/create`
|
||||||
|
});
|
||||||
|
actions.push({
|
||||||
|
id: "create-machine-client",
|
||||||
|
label: t("commandPaletteCreateMachineClient"),
|
||||||
|
icon: <MonitorUp className="size-4" />,
|
||||||
|
href: `/${orgId}/settings/clients/machine/create`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!env?.flags.disableEnterpriseFeatures) {
|
||||||
|
actions.push({
|
||||||
|
id: "create-alert-rule",
|
||||||
|
label: t("commandPaletteCreateAlertRule"),
|
||||||
|
icon: <BellRing className="size-4" />,
|
||||||
|
href: `/${orgId}/settings/alerting/create`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(build === "oss" && !env?.flags.disableEnterpriseFeatures) ||
|
||||||
|
build === "saas" ||
|
||||||
|
env?.app.identityProviderMode === "org" ||
|
||||||
|
(env?.app.identityProviderMode === undefined && build !== "oss")
|
||||||
|
) {
|
||||||
|
actions.push({
|
||||||
|
id: "create-idp",
|
||||||
|
label: t("commandPaletteCreateIdentityProvider"),
|
||||||
|
icon: <Plus className="size-4" />,
|
||||||
|
href: `/${orgId}/settings/idp/create`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const canChooseOrganization = !isAdminPage && (orgs?.length ?? 0) > 1;
|
||||||
|
|
||||||
|
if (canChooseOrganization) {
|
||||||
|
actions.push({
|
||||||
|
id: "choose-org",
|
||||||
|
label: t("commandPaletteChooseOrganization"),
|
||||||
|
icon: <Building2 className="size-4" />,
|
||||||
|
href: "/?orgs=1"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.push({
|
||||||
|
id: "toggle-theme",
|
||||||
|
label: t("commandPaletteToggleTheme"),
|
||||||
|
icon: <SunMoon className="size-4" />,
|
||||||
|
onSelect: cycleTheme
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user.serverAdmin && !isAdminPage) {
|
||||||
|
actions.push({
|
||||||
|
id: "go-admin",
|
||||||
|
label: t("serverAdmin"),
|
||||||
|
icon: <Building2 className="size-4" />,
|
||||||
|
href: "/admin/users"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}, [isAdminPage, orgId, orgs, env, user.serverAdmin, theme, setTheme, t]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { SidebarNavSection } from "@app/components/SidebarNav";
|
||||||
|
import { flattenNavSections } from "@app/lib/flattenNavItems";
|
||||||
|
import {
|
||||||
|
hydrateNavHref,
|
||||||
|
navHrefParamsFromRoute
|
||||||
|
} from "@app/lib/hydrateNavHref";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export type NavigationCommand = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
href: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
sectionHeading: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NavigationCommandGroup = {
|
||||||
|
heading: string;
|
||||||
|
items: NavigationCommand[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useCommandPaletteNavigation(
|
||||||
|
navItems: SidebarNavSection[]
|
||||||
|
): NavigationCommandGroup[] {
|
||||||
|
const params = useParams();
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const hrefParams = navHrefParamsFromRoute(params);
|
||||||
|
const flat = flattenNavSections(navItems);
|
||||||
|
const groups = new Map<string, NavigationCommand[]>();
|
||||||
|
|
||||||
|
for (const item of flat) {
|
||||||
|
const href = hydrateNavHref(item.href, hrefParams);
|
||||||
|
if (!href) continue;
|
||||||
|
|
||||||
|
const groupItems = groups.get(item.sectionHeading) ?? [];
|
||||||
|
groupItems.push({
|
||||||
|
id: `nav-${item.sectionHeading}-${item.title}-${href}`,
|
||||||
|
title: t(item.title),
|
||||||
|
href,
|
||||||
|
icon: item.icon,
|
||||||
|
sectionHeading: item.sectionHeading
|
||||||
|
});
|
||||||
|
groups.set(item.sectionHeading, groupItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(groups.entries()).map(([heading, items]) => ({
|
||||||
|
heading: t(heading),
|
||||||
|
items
|
||||||
|
}));
|
||||||
|
}, [navItems, params, t]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ListUserOrgsResponse } from "@server/routers/org";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
export type OrganizationCommand = {
|
||||||
|
id: string;
|
||||||
|
orgId: string;
|
||||||
|
name: string;
|
||||||
|
isPrimaryOrg?: boolean;
|
||||||
|
href: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useCommandPaletteOrganizations(
|
||||||
|
orgs: ListUserOrgsResponse["orgs"] | undefined
|
||||||
|
): OrganizationCommand[] {
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!orgs?.length) return [];
|
||||||
|
|
||||||
|
const sortedOrgs = [...orgs].sort((a, b) => {
|
||||||
|
const aPrimary = Boolean(a.isPrimaryOrg);
|
||||||
|
const bPrimary = Boolean(b.isPrimaryOrg);
|
||||||
|
if (aPrimary && !bPrimary) return -1;
|
||||||
|
if (!aPrimary && bPrimary) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortedOrgs.map((org) => {
|
||||||
|
const newPath = pathname.includes("/settings/")
|
||||||
|
? pathname.replace(/^\/[^/]+/, `/${org.orgId}`)
|
||||||
|
: `/${org.orgId}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `org-${org.orgId}`,
|
||||||
|
orgId: org.orgId,
|
||||||
|
name: org.name,
|
||||||
|
isPrimaryOrg: org.isPrimaryOrg,
|
||||||
|
href: newPath
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [orgs, pathname]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { orgQueries } from "@app/lib/queries";
|
||||||
|
import { useQueries } from "@tanstack/react-query";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useDebounce } from "use-debounce";
|
||||||
|
|
||||||
|
const SEARCH_PER_PAGE = 5;
|
||||||
|
const MIN_QUERY_LENGTH = 2;
|
||||||
|
|
||||||
|
export type SiteSearchResult = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
href: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ResourceSearchResult = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
href: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserSearchResult = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
href: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ClientSearchResult = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
href: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useCommandPaletteSearch({
|
||||||
|
orgId,
|
||||||
|
query,
|
||||||
|
enabled
|
||||||
|
}: {
|
||||||
|
orgId?: string;
|
||||||
|
query: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}) {
|
||||||
|
const [debouncedQuery] = useDebounce(query, 150);
|
||||||
|
const trimmedQuery = debouncedQuery.trim();
|
||||||
|
const shouldSearch =
|
||||||
|
enabled && !!orgId && trimmedQuery.length >= MIN_QUERY_LENGTH;
|
||||||
|
|
||||||
|
const [sitesQuery, resourcesQuery, usersQuery, clientsQuery] = useQueries({
|
||||||
|
queries: [
|
||||||
|
{
|
||||||
|
...orgQueries.sites({
|
||||||
|
orgId: orgId ?? "",
|
||||||
|
query: trimmedQuery,
|
||||||
|
perPage: SEARCH_PER_PAGE
|
||||||
|
}),
|
||||||
|
enabled: shouldSearch
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...orgQueries.resources({
|
||||||
|
orgId: orgId ?? "",
|
||||||
|
query: trimmedQuery,
|
||||||
|
perPage: SEARCH_PER_PAGE
|
||||||
|
}),
|
||||||
|
enabled: shouldSearch
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...orgQueries.users({
|
||||||
|
orgId: orgId ?? "",
|
||||||
|
query: trimmedQuery,
|
||||||
|
perPage: SEARCH_PER_PAGE
|
||||||
|
}),
|
||||||
|
enabled: shouldSearch
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...orgQueries.machineClients({
|
||||||
|
orgId: orgId ?? "",
|
||||||
|
query: trimmedQuery,
|
||||||
|
perPage: SEARCH_PER_PAGE
|
||||||
|
}),
|
||||||
|
enabled: shouldSearch
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const sites = useMemo((): SiteSearchResult[] => {
|
||||||
|
if (!orgId || !sitesQuery.data) return [];
|
||||||
|
return sitesQuery.data.map((site) => ({
|
||||||
|
id: `site-${site.siteId}`,
|
||||||
|
name: site.name,
|
||||||
|
href: `/${orgId}/settings/sites/${site.niceId}`
|
||||||
|
}));
|
||||||
|
}, [orgId, sitesQuery.data]);
|
||||||
|
|
||||||
|
const resources = useMemo((): ResourceSearchResult[] => {
|
||||||
|
if (!orgId || !resourcesQuery.data) return [];
|
||||||
|
return resourcesQuery.data.map((resource) => ({
|
||||||
|
id: `resource-${resource.resourceId}`,
|
||||||
|
name: resource.name,
|
||||||
|
href: `/${orgId}/settings/resources/proxy/${resource.niceId}`
|
||||||
|
}));
|
||||||
|
}, [orgId, resourcesQuery.data]);
|
||||||
|
|
||||||
|
const users = useMemo((): UserSearchResult[] => {
|
||||||
|
if (!orgId || !usersQuery.data) return [];
|
||||||
|
return usersQuery.data.map((user) => ({
|
||||||
|
id: `user-${user.id}`,
|
||||||
|
name: user.name ?? user.email ?? user.username ?? "",
|
||||||
|
email: user.email ?? user.username ?? "",
|
||||||
|
href: `/${orgId}/settings/access/users/${user.id}`
|
||||||
|
}));
|
||||||
|
}, [orgId, usersQuery.data]);
|
||||||
|
|
||||||
|
const machineClients = useMemo((): ClientSearchResult[] => {
|
||||||
|
if (!orgId || !clientsQuery.data) return [];
|
||||||
|
return clientsQuery.data
|
||||||
|
.filter((client) => !client.userId)
|
||||||
|
.map((client) => ({
|
||||||
|
id: `client-${client.clientId}`,
|
||||||
|
name: client.name,
|
||||||
|
href: `/${orgId}/settings/clients/machine/${client.niceId}`
|
||||||
|
}));
|
||||||
|
}, [orgId, clientsQuery.data]);
|
||||||
|
|
||||||
|
const isLoading =
|
||||||
|
shouldSearch &&
|
||||||
|
(sitesQuery.isFetching ||
|
||||||
|
resourcesQuery.isFetching ||
|
||||||
|
usersQuery.isFetching ||
|
||||||
|
clientsQuery.isFetching);
|
||||||
|
|
||||||
|
return {
|
||||||
|
debouncedQuery: trimmedQuery,
|
||||||
|
shouldSearch,
|
||||||
|
sites,
|
||||||
|
resources,
|
||||||
|
users,
|
||||||
|
machineClients,
|
||||||
|
isLoading
|
||||||
|
};
|
||||||
|
}
|
||||||
+55
-18
@@ -101,16 +101,41 @@ ${colorConfig
|
|||||||
|
|
||||||
const ChartTooltip = RechartsPrimitive.Tooltip;
|
const ChartTooltip = RechartsPrimitive.Tooltip;
|
||||||
|
|
||||||
|
type ChartTooltipValue = number | string | Array<number | string>;
|
||||||
|
type ChartTooltipName = number | string;
|
||||||
|
type ChartTooltipPayload = RechartsPrimitive.DefaultTooltipContentProps<
|
||||||
|
ChartTooltipValue,
|
||||||
|
ChartTooltipName
|
||||||
|
>["payload"];
|
||||||
|
type ChartTooltipPayloadItem = NonNullable<ChartTooltipPayload>[number];
|
||||||
|
|
||||||
|
type ChartTooltipContentProps = React.ComponentProps<"div"> & {
|
||||||
|
active?: boolean;
|
||||||
|
payload?: ChartTooltipPayload;
|
||||||
|
label?: RechartsPrimitive.DefaultTooltipContentProps<
|
||||||
|
ChartTooltipValue,
|
||||||
|
ChartTooltipName
|
||||||
|
>["label"];
|
||||||
|
labelFormatter?: RechartsPrimitive.DefaultTooltipContentProps<
|
||||||
|
ChartTooltipValue,
|
||||||
|
ChartTooltipName
|
||||||
|
>["labelFormatter"];
|
||||||
|
formatter?: RechartsPrimitive.DefaultTooltipContentProps<
|
||||||
|
ChartTooltipValue,
|
||||||
|
ChartTooltipName
|
||||||
|
>["formatter"];
|
||||||
|
hideLabel?: boolean;
|
||||||
|
hideIndicator?: boolean;
|
||||||
|
indicator?: "line" | "dot" | "dashed";
|
||||||
|
nameKey?: string;
|
||||||
|
labelKey?: string;
|
||||||
|
color?: string;
|
||||||
|
labelClassName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const ChartTooltipContent = React.forwardRef<
|
const ChartTooltipContent = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
ChartTooltipContentProps
|
||||||
React.ComponentProps<"div"> & {
|
|
||||||
hideLabel?: boolean;
|
|
||||||
hideIndicator?: boolean;
|
|
||||||
indicator?: "line" | "dot" | "dashed";
|
|
||||||
nameKey?: string;
|
|
||||||
labelKey?: string;
|
|
||||||
}
|
|
||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
@@ -187,8 +212,11 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
{!nestLabel ? tooltipLabel : null}
|
{!nestLabel ? tooltipLabel : null}
|
||||||
<div className="grid gap-1.5">
|
<div className="grid gap-1.5">
|
||||||
{payload
|
{payload
|
||||||
.filter((item) => item.type !== "none")
|
.filter(
|
||||||
.map((item, index) => {
|
(item: ChartTooltipPayloadItem) =>
|
||||||
|
item.type !== "none"
|
||||||
|
)
|
||||||
|
.map((item: ChartTooltipPayloadItem, index: number) => {
|
||||||
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
||||||
|
|
||||||
const itemConfig = getPayloadConfigFromPayload(
|
const itemConfig = getPayloadConfigFromPayload(
|
||||||
@@ -201,7 +229,7 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.dataKey}
|
key={String(item.dataKey ?? index)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
||||||
indicator === "dot" && "items-center"
|
indicator === "dot" && "items-center"
|
||||||
@@ -301,13 +329,20 @@ ChartTooltipContent.displayName = "ChartTooltip";
|
|||||||
|
|
||||||
const ChartLegend = RechartsPrimitive.Legend;
|
const ChartLegend = RechartsPrimitive.Legend;
|
||||||
|
|
||||||
|
type ChartLegendPayload =
|
||||||
|
RechartsPrimitive.DefaultLegendContentProps["payload"];
|
||||||
|
type ChartLegendPayloadItem = NonNullable<ChartLegendPayload>[number];
|
||||||
|
|
||||||
|
type ChartLegendContentProps = React.ComponentProps<"div"> & {
|
||||||
|
payload?: ChartLegendPayload;
|
||||||
|
verticalAlign?: RechartsPrimitive.DefaultLegendContentProps["verticalAlign"];
|
||||||
|
hideIcon?: boolean;
|
||||||
|
nameKey?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const ChartLegendContent = React.forwardRef<
|
const ChartLegendContent = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.ComponentProps<"div"> &
|
ChartLegendContentProps
|
||||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
|
||||||
hideIcon?: boolean;
|
|
||||||
nameKey?: string;
|
|
||||||
}
|
|
||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
@@ -335,8 +370,10 @@ const ChartLegendContent = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{payload
|
{payload
|
||||||
.filter((item) => item.type !== "none")
|
.filter(
|
||||||
.map((item) => {
|
(item: ChartLegendPayloadItem) => item.type !== "none"
|
||||||
|
)
|
||||||
|
.map((item: ChartLegendPayloadItem) => {
|
||||||
const key = `${nameKey || item.dataKey || "value"}`;
|
const key = `${nameKey || item.dataKey || "value"}`;
|
||||||
const itemConfig = getPayloadConfigFromPayload(
|
const itemConfig = getPayloadConfigFromPayload(
|
||||||
config,
|
config,
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import type {
|
||||||
|
SidebarNavItem,
|
||||||
|
SidebarNavSection
|
||||||
|
} from "@app/components/SidebarNav";
|
||||||
|
|
||||||
|
export type FlatNavItem = {
|
||||||
|
title: string;
|
||||||
|
href: string;
|
||||||
|
icon?: ReactNode;
|
||||||
|
sectionHeading: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function flattenItems(
|
||||||
|
items: SidebarNavItem[],
|
||||||
|
sectionHeading: string,
|
||||||
|
result: FlatNavItem[]
|
||||||
|
) {
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.href) {
|
||||||
|
result.push({
|
||||||
|
title: item.title,
|
||||||
|
href: item.href,
|
||||||
|
icon: item.icon,
|
||||||
|
sectionHeading
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (item.items?.length) {
|
||||||
|
flattenItems(item.items, sectionHeading, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flattenNavSections(
|
||||||
|
sections: SidebarNavSection[]
|
||||||
|
): FlatNavItem[] {
|
||||||
|
const result: FlatNavItem[] = [];
|
||||||
|
for (const section of sections) {
|
||||||
|
flattenItems(section.items, section.heading, result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
export type NavHrefParams = {
|
||||||
|
orgId?: string;
|
||||||
|
niceId?: string;
|
||||||
|
resourceId?: string;
|
||||||
|
userId?: string;
|
||||||
|
apiKeyId?: string;
|
||||||
|
clientId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function hydrateNavHref(
|
||||||
|
val: string | undefined,
|
||||||
|
params: NavHrefParams
|
||||||
|
): string | undefined {
|
||||||
|
if (!val) return undefined;
|
||||||
|
return val
|
||||||
|
.replace("{orgId}", params.orgId ?? "")
|
||||||
|
.replace("{niceId}", params.niceId ?? "")
|
||||||
|
.replace("{resourceId}", params.resourceId ?? "")
|
||||||
|
.replace("{userId}", params.userId ?? "")
|
||||||
|
.replace("{apiKeyId}", params.apiKeyId ?? "")
|
||||||
|
.replace("{clientId}", params.clientId ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function navHrefParamsFromRoute(
|
||||||
|
params: Record<string, string | string[] | undefined>
|
||||||
|
): NavHrefParams {
|
||||||
|
return {
|
||||||
|
orgId: params.orgId as string | undefined,
|
||||||
|
niceId: params.niceId as string | undefined,
|
||||||
|
resourceId: params.resourceId as string | undefined,
|
||||||
|
userId: params.userId as string | undefined,
|
||||||
|
apiKeyId: params.apiKeyId as string | undefined,
|
||||||
|
clientId: params.clientId as string | undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
|
||||||
export function middleware(request: NextRequest) {
|
export function proxy(request: NextRequest) {
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
declare module "*.css";
|
||||||
+45
-15
@@ -1,28 +1,49 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"baseUrl": "src",
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@server/*": ["../server/*"],
|
"@server/*": [
|
||||||
"@test/*": ["../test/*"],
|
"./server/*"
|
||||||
"@app/*": ["*"],
|
],
|
||||||
"@cli/*": ["../cli/*"],
|
"@test/*": [
|
||||||
"@/*": ["./*"],
|
"./test/*"
|
||||||
"#private/*": ["../server/private/*"],
|
],
|
||||||
"#open/*": ["../server/*"],
|
"@app/*": [
|
||||||
"#closed/*": ["../server/private/*"],
|
"./src/*"
|
||||||
"#dynamic/*": ["../server/private/*"]
|
],
|
||||||
|
"@cli/*": [
|
||||||
|
"./cli/*"
|
||||||
|
],
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
],
|
||||||
|
"#private/*": [
|
||||||
|
"./server/private/*"
|
||||||
|
],
|
||||||
|
"#open/*": [
|
||||||
|
"./server/*"
|
||||||
|
],
|
||||||
|
"#closed/*": [
|
||||||
|
"./server/private/*"
|
||||||
|
],
|
||||||
|
"#dynamic/*": [
|
||||||
|
"./server/private/*"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
@@ -31,6 +52,15 @@
|
|||||||
],
|
],
|
||||||
"target": "ES2024"
|
"target": "ES2024"
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": [
|
||||||
"exclude": ["node_modules"]
|
"next-env.d.ts",
|
||||||
}
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
"**/*.d.ts",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
+46
-16
@@ -1,28 +1,49 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "react-jsx",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"baseUrl": "src",
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@server/*": ["../server/*"],
|
"@server/*": [
|
||||||
"@test/*": ["../test/*"],
|
"./server/*"
|
||||||
"@app/*": ["*"],
|
],
|
||||||
"@cli/*": ["../cli/*"],
|
"@test/*": [
|
||||||
"@/*": ["./*"],
|
"./test/*"
|
||||||
"#private/*": ["../server/private/*"],
|
],
|
||||||
"#open/*": ["../server/*"],
|
"@app/*": [
|
||||||
"#closed/*": ["../server/private/*"],
|
"./src/*"
|
||||||
"#dynamic/*": ["../server/*"]
|
],
|
||||||
|
"@cli/*": [
|
||||||
|
"./cli/*"
|
||||||
|
],
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
],
|
||||||
|
"#private/*": [
|
||||||
|
"./server/private/*"
|
||||||
|
],
|
||||||
|
"#open/*": [
|
||||||
|
"./server/*"
|
||||||
|
],
|
||||||
|
"#closed/*": [
|
||||||
|
"./server/private/*"
|
||||||
|
],
|
||||||
|
"#dynamic/*": [
|
||||||
|
"./server/*"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
@@ -31,6 +52,15 @@
|
|||||||
],
|
],
|
||||||
"target": "ES2024"
|
"target": "ES2024"
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": [
|
||||||
"exclude": ["node_modules"]
|
"next-env.d.ts",
|
||||||
}
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
"**/*.d.ts",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
+46
-16
@@ -1,28 +1,49 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "react-jsx",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"baseUrl": "src",
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@server/*": ["../server/*"],
|
"@server/*": [
|
||||||
"@test/*": ["../test/*"],
|
"./server/*"
|
||||||
"@app/*": ["*"],
|
],
|
||||||
"@cli/*": ["../cli/*"],
|
"@test/*": [
|
||||||
"@/*": ["./*"],
|
"./test/*"
|
||||||
"#private/*": ["../server/private/*"],
|
],
|
||||||
"#open/*": ["../server/*"],
|
"@app/*": [
|
||||||
"#closed/*": ["../server/private/*"],
|
"./src/*"
|
||||||
"#dynamic/*": ["../server/private/*"]
|
],
|
||||||
|
"@cli/*": [
|
||||||
|
"./cli/*"
|
||||||
|
],
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
],
|
||||||
|
"#private/*": [
|
||||||
|
"./server/private/*"
|
||||||
|
],
|
||||||
|
"#open/*": [
|
||||||
|
"./server/*"
|
||||||
|
],
|
||||||
|
"#closed/*": [
|
||||||
|
"./server/private/*"
|
||||||
|
],
|
||||||
|
"#dynamic/*": [
|
||||||
|
"./server/private/*"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
@@ -31,6 +52,15 @@
|
|||||||
],
|
],
|
||||||
"target": "ES2024"
|
"target": "ES2024"
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": [
|
||||||
"exclude": ["node_modules"]
|
"next-env.d.ts",
|
||||||
}
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
"**/*.d.ts",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user