nobalmako 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -0
- package/components.json +22 -0
- package/dist/nobalmako.js +272 -0
- package/drizzle/0000_pink_spiral.sql +126 -0
- package/drizzle/meta/0000_snapshot.json +1027 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +10 -0
- package/eslint.config.mjs +18 -0
- package/next.config.ts +7 -0
- package/package.json +80 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/server/index.ts +118 -0
- package/src/app/api/api-keys/[id]/route.ts +147 -0
- package/src/app/api/api-keys/route.ts +151 -0
- package/src/app/api/audit-logs/route.ts +84 -0
- package/src/app/api/auth/forgot-password/route.ts +47 -0
- package/src/app/api/auth/login/route.ts +99 -0
- package/src/app/api/auth/logout/route.ts +15 -0
- package/src/app/api/auth/me/route.ts +23 -0
- package/src/app/api/auth/mfa/setup/route.ts +33 -0
- package/src/app/api/auth/mfa/verify/route.ts +45 -0
- package/src/app/api/auth/register/route.ts +140 -0
- package/src/app/api/auth/reset-password/route.ts +52 -0
- package/src/app/api/auth/update/route.ts +71 -0
- package/src/app/api/auth/verify/route.ts +39 -0
- package/src/app/api/environments/route.ts +227 -0
- package/src/app/api/team-members/route.ts +385 -0
- package/src/app/api/teams/route.ts +217 -0
- package/src/app/api/variable-history/route.ts +218 -0
- package/src/app/api/variables/route.ts +476 -0
- package/src/app/api/webhooks/route.ts +77 -0
- package/src/app/api-keys/APIKeysClient.tsx +316 -0
- package/src/app/api-keys/page.tsx +10 -0
- package/src/app/api-reference/page.tsx +324 -0
- package/src/app/audit-log/AuditLogClient.tsx +229 -0
- package/src/app/audit-log/page.tsx +10 -0
- package/src/app/auth/forgot-password/page.tsx +121 -0
- package/src/app/auth/login/LoginForm.tsx +145 -0
- package/src/app/auth/login/page.tsx +11 -0
- package/src/app/auth/register/RegisterForm.tsx +156 -0
- package/src/app/auth/register/page.tsx +16 -0
- package/src/app/auth/reset-password/page.tsx +160 -0
- package/src/app/dashboard/DashboardClient.tsx +219 -0
- package/src/app/dashboard/page.tsx +11 -0
- package/src/app/docs/page.tsx +251 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +123 -0
- package/src/app/layout.tsx +35 -0
- package/src/app/page.tsx +231 -0
- package/src/app/profile/ProfileClient.tsx +230 -0
- package/src/app/profile/page.tsx +10 -0
- package/src/app/project/[id]/ProjectDetailsClient.tsx +512 -0
- package/src/app/project/[id]/page.tsx +17 -0
- package/src/bin/nobalmako.ts +341 -0
- package/src/components/ApiKeysManager.tsx +529 -0
- package/src/components/AppLayout.tsx +193 -0
- package/src/components/BulkActions.tsx +138 -0
- package/src/components/CreateEnvironmentDialog.tsx +207 -0
- package/src/components/CreateTeamDialog.tsx +174 -0
- package/src/components/CreateVariableDialog.tsx +311 -0
- package/src/components/DeleteEnvironmentDialog.tsx +104 -0
- package/src/components/DeleteTeamDialog.tsx +112 -0
- package/src/components/DeleteVariableDialog.tsx +103 -0
- package/src/components/EditEnvironmentDialog.tsx +202 -0
- package/src/components/EditMemberDialog.tsx +143 -0
- package/src/components/EditTeamDialog.tsx +178 -0
- package/src/components/EditVariableDialog.tsx +231 -0
- package/src/components/ImportVariablesDialog.tsx +347 -0
- package/src/components/InviteMemberDialog.tsx +191 -0
- package/src/components/LeaveProjectDialog.tsx +111 -0
- package/src/components/MFASettings.tsx +136 -0
- package/src/components/ProjectDiff.tsx +123 -0
- package/src/components/Providers.tsx +24 -0
- package/src/components/RemoveMemberDialog.tsx +112 -0
- package/src/components/SearchDialog.tsx +276 -0
- package/src/components/SecurityOverview.tsx +92 -0
- package/src/components/TeamMembersManager.tsx +103 -0
- package/src/components/VariableHistoryDialog.tsx +265 -0
- package/src/components/WebhooksManager.tsx +169 -0
- package/src/components/ui/alert-dialog.tsx +160 -0
- package/src/components/ui/alert.tsx +59 -0
- package/src/components/ui/avatar.tsx +53 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/button.tsx +62 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/checkbox.tsx +32 -0
- package/src/components/ui/dialog.tsx +143 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sonner.tsx +37 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +117 -0
- package/src/components/ui/tabs.tsx +66 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/hooks/use-api-keys.ts +95 -0
- package/src/hooks/use-audit-logs.ts +58 -0
- package/src/hooks/use-auth.tsx +121 -0
- package/src/hooks/use-environments.ts +33 -0
- package/src/hooks/use-project-permissions.ts +49 -0
- package/src/hooks/use-team-members.ts +30 -0
- package/src/hooks/use-teams.ts +33 -0
- package/src/hooks/use-variables.ts +38 -0
- package/src/lib/audit.ts +36 -0
- package/src/lib/auth.ts +108 -0
- package/src/lib/crypto.ts +39 -0
- package/src/lib/db.ts +15 -0
- package/src/lib/dynamic-providers.ts +19 -0
- package/src/lib/email.ts +110 -0
- package/src/lib/mail.ts +51 -0
- package/src/lib/permissions.ts +51 -0
- package/src/lib/schema.ts +240 -0
- package/src/lib/seed.ts +107 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/webhooks.ts +42 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
2
|
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
3
|
+
import nextTs from "eslint-config-next/typescript";
|
|
4
|
+
|
|
5
|
+
const eslintConfig = defineConfig([
|
|
6
|
+
...nextVitals,
|
|
7
|
+
...nextTs,
|
|
8
|
+
// Override default ignores of eslint-config-next.
|
|
9
|
+
globalIgnores([
|
|
10
|
+
// Default ignores of eslint-config-next:
|
|
11
|
+
".next/**",
|
|
12
|
+
"out/**",
|
|
13
|
+
"build/**",
|
|
14
|
+
"next-env.d.ts",
|
|
15
|
+
]),
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export default eslintConfig;
|
package/next.config.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nobalmako",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"bin": {
|
|
6
|
+
"nobalmako": "./dist/nobalmako.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "next dev",
|
|
10
|
+
"build": "next build",
|
|
11
|
+
"build:cli": "esbuild src/bin/nobalmako.ts --bundle --platform=node --outfile=dist/nobalmako.js --external:dotenv --external:commander --external:fs --external:path --external:os --external:enquirer",
|
|
12
|
+
"start": "next start",
|
|
13
|
+
"lint": "next lint",
|
|
14
|
+
"db:generate": "drizzle-kit generate",
|
|
15
|
+
"db:push": "drizzle-kit push",
|
|
16
|
+
"db:migrate": "drizzle-kit migrate",
|
|
17
|
+
"db:studio": "drizzle-kit studio",
|
|
18
|
+
"db:seed": "tsx src/lib/seed.ts",
|
|
19
|
+
"cli": "tsx src/bin/nobalmako.ts"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@hookform/resolvers": "^5.2.2",
|
|
23
|
+
"@neondatabase/serverless": "^1.0.2",
|
|
24
|
+
"@radix-ui/react-alert-dialog": "^1.1.15",
|
|
25
|
+
"@radix-ui/react-avatar": "^1.1.11",
|
|
26
|
+
"@radix-ui/react-checkbox": "^1.3.3",
|
|
27
|
+
"@radix-ui/react-dialog": "^1.1.15",
|
|
28
|
+
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
29
|
+
"@radix-ui/react-label": "^2.1.8",
|
|
30
|
+
"@radix-ui/react-popover": "^1.1.15",
|
|
31
|
+
"@radix-ui/react-progress": "^1.1.8",
|
|
32
|
+
"@radix-ui/react-select": "^2.2.6",
|
|
33
|
+
"@radix-ui/react-separator": "^1.1.8",
|
|
34
|
+
"@radix-ui/react-slot": "^1.2.4",
|
|
35
|
+
"@radix-ui/react-switch": "^1.2.6",
|
|
36
|
+
"@radix-ui/react-tabs": "^1.1.13",
|
|
37
|
+
"@radix-ui/react-toast": "^1.2.15",
|
|
38
|
+
"@radix-ui/react-tooltip": "^1.2.8",
|
|
39
|
+
"@tanstack/react-query": "^5.90.19",
|
|
40
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
41
|
+
"@types/qrcode": "^1.5.6",
|
|
42
|
+
"@types/speakeasy": "^2.0.10",
|
|
43
|
+
"bcryptjs": "^3.0.3",
|
|
44
|
+
"class-variance-authority": "^0.7.1",
|
|
45
|
+
"clsx": "^2.1.1",
|
|
46
|
+
"commander": "^14.0.3",
|
|
47
|
+
"date-fns": "^4.1.0",
|
|
48
|
+
"dotenv": "^17.2.3",
|
|
49
|
+
"drizzle-kit": "^0.31.8",
|
|
50
|
+
"drizzle-orm": "^0.45.1",
|
|
51
|
+
"enquirer": "^2.4.1",
|
|
52
|
+
"jsonwebtoken": "^9.0.3",
|
|
53
|
+
"lucide-react": "^0.562.0",
|
|
54
|
+
"next": "16.1.3",
|
|
55
|
+
"nodemailer": "^7.0.13",
|
|
56
|
+
"postgres": "^3.4.8",
|
|
57
|
+
"qrcode": "^1.5.4",
|
|
58
|
+
"react": "19.2.3",
|
|
59
|
+
"react-dom": "19.2.3",
|
|
60
|
+
"react-hook-form": "^7.71.1",
|
|
61
|
+
"sonner": "^2.0.7",
|
|
62
|
+
"speakeasy": "^2.0.0",
|
|
63
|
+
"tailwind-merge": "^3.4.0",
|
|
64
|
+
"zod": "^4.3.5"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@tailwindcss/postcss": "^4",
|
|
68
|
+
"@types/node": "^20",
|
|
69
|
+
"@types/nodemailer": "^7.0.9",
|
|
70
|
+
"@types/react": "^19",
|
|
71
|
+
"@types/react-dom": "^19",
|
|
72
|
+
"esbuild": "^0.27.2",
|
|
73
|
+
"eslint": "^9",
|
|
74
|
+
"eslint-config-next": "16.1.3",
|
|
75
|
+
"tailwindcss": "^4",
|
|
76
|
+
"tsx": "^4.21.0",
|
|
77
|
+
"tw-animate-css": "^1.4.0",
|
|
78
|
+
"typescript": "^5"
|
|
79
|
+
}
|
|
80
|
+
}
|
package/public/file.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
package/public/globe.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
package/public/next.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
package/server/index.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import express, { type Request, Response, NextFunction } from "express";
|
|
2
|
+
import { registerRoutes } from "./routes";
|
|
3
|
+
import { serveStatic } from "./static";
|
|
4
|
+
import { createServer } from "http";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import mime from "mime-types";
|
|
8
|
+
import { createServer as createViteServer } from "vite";
|
|
9
|
+
import { config } from "dotenv";
|
|
10
|
+
|
|
11
|
+
config(); // Load environment variables from .env file
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
|
|
15
|
+
const app = express();
|
|
16
|
+
const httpServer = createServer(app);
|
|
17
|
+
|
|
18
|
+
declare module "http" {
|
|
19
|
+
interface IncomingMessage {
|
|
20
|
+
rawBody: unknown;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
app.use(
|
|
25
|
+
express.json({
|
|
26
|
+
verify: (req, _res, buf) => {
|
|
27
|
+
req.rawBody = buf;
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
app.use(express.urlencoded({ extended: false }));
|
|
33
|
+
|
|
34
|
+
export function log(message: string, source = "express") {
|
|
35
|
+
const formattedTime = new Date().toLocaleTimeString("en-US", {
|
|
36
|
+
hour: "numeric",
|
|
37
|
+
minute: "2-digit",
|
|
38
|
+
second: "2-digit",
|
|
39
|
+
hour12: true,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
console.log(`${formattedTime} [${source}] ${message}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
app.use((req, res, next) => {
|
|
46
|
+
const start = Date.now();
|
|
47
|
+
const path = req.path;
|
|
48
|
+
let capturedJsonResponse: Record<string, any> | undefined = undefined;
|
|
49
|
+
|
|
50
|
+
const originalResJson = res.json;
|
|
51
|
+
res.json = function (bodyJson, ...args) {
|
|
52
|
+
capturedJsonResponse = bodyJson;
|
|
53
|
+
return originalResJson.apply(res, [bodyJson, ...args]);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
res.on("finish", () => {
|
|
57
|
+
const duration = Date.now() - start;
|
|
58
|
+
if (path.startsWith("/api")) {
|
|
59
|
+
let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;
|
|
60
|
+
if (capturedJsonResponse) {
|
|
61
|
+
logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
log(logLine);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
next();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
(async () => {
|
|
72
|
+
await registerRoutes(httpServer, app);
|
|
73
|
+
|
|
74
|
+
app.use((err: any, _req: Request, res: Response, next: NextFunction) => {
|
|
75
|
+
const status = err.status || err.statusCode || 500;
|
|
76
|
+
const message = err.message || "Internal Server Error";
|
|
77
|
+
|
|
78
|
+
console.error("Internal Server Error:", err);
|
|
79
|
+
|
|
80
|
+
if (res.headersSent) {
|
|
81
|
+
return next(err);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return res.status(status).json({ message });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// importantly only setup vite in development and after
|
|
88
|
+
// setting up all the other routes so the catch-all route
|
|
89
|
+
// doesn't interfere with the other routes
|
|
90
|
+
if (process.env.NODE_ENV === "production") {
|
|
91
|
+
serveStatic(app);
|
|
92
|
+
} else {
|
|
93
|
+
// Create Vite server in middleware mode for development
|
|
94
|
+
const vite = await createViteServer({
|
|
95
|
+
server: { middlewareMode: true },
|
|
96
|
+
appType: 'spa'
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Use vite's connect instance as middleware
|
|
100
|
+
app.use(vite.middlewares);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ALWAYS serve the app on the port specified in the environment variable PORT
|
|
104
|
+
// Other ports are firewalled. Default to 5000 if not specified.
|
|
105
|
+
// this serves both the API and the client.
|
|
106
|
+
// It is the only port that is not firewalled.
|
|
107
|
+
const port = parseInt(process.env.PORT || "5000", 10);
|
|
108
|
+
httpServer.listen(
|
|
109
|
+
{
|
|
110
|
+
port,
|
|
111
|
+
host: "0.0.0.0",
|
|
112
|
+
reusePort: true,
|
|
113
|
+
},
|
|
114
|
+
() => {
|
|
115
|
+
log(`serving on port ${port}`);
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
})();
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { eq, and } from 'drizzle-orm'
|
|
3
|
+
import { db } from '@/lib/db'
|
|
4
|
+
import { apiKeys, teamMembers } from '@/lib/schema'
|
|
5
|
+
import { createAuditLog } from '@/lib/audit'
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
import { getUserFromToken } from '@/lib/auth'
|
|
8
|
+
|
|
9
|
+
const updateApiKeySchema = z.object({
|
|
10
|
+
name: z.string().min(1).max(100).optional(),
|
|
11
|
+
permissions: z.array(z.string()).optional(),
|
|
12
|
+
expiresAt: z.string().optional(),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
// PUT /api/api-keys/[id] - Update an API key
|
|
16
|
+
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
17
|
+
try {
|
|
18
|
+
const user = await getUserFromToken()
|
|
19
|
+
if (!user) {
|
|
20
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const userId = user.id
|
|
24
|
+
const { id: apiKeyId } = await params
|
|
25
|
+
const body = await request.json()
|
|
26
|
+
|
|
27
|
+
const validation = updateApiKeySchema.safeParse(body)
|
|
28
|
+
if (!validation.success) {
|
|
29
|
+
return NextResponse.json({ error: 'Invalid input', details: validation.error.issues }, { status: 400 })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const updates = validation.data
|
|
33
|
+
|
|
34
|
+
// Find the API key and verify ownership
|
|
35
|
+
const apiKey = await db
|
|
36
|
+
.select()
|
|
37
|
+
.from(apiKeys)
|
|
38
|
+
.where(eq(apiKeys.id, apiKeyId))
|
|
39
|
+
.limit(1)
|
|
40
|
+
|
|
41
|
+
if (apiKey.length === 0) {
|
|
42
|
+
return NextResponse.json({ error: 'API key not found' }, { status: 404 })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const key = apiKey[0]
|
|
46
|
+
|
|
47
|
+
// Check if user owns this key or is a member of the team that owns it
|
|
48
|
+
let hasAccess = key.userId === userId
|
|
49
|
+
|
|
50
|
+
if (!hasAccess && key.teamId) {
|
|
51
|
+
const teamMember = await db
|
|
52
|
+
.select()
|
|
53
|
+
.from(teamMembers)
|
|
54
|
+
.where(and(eq(teamMembers.teamId, key.teamId), eq(teamMembers.userId, userId)))
|
|
55
|
+
.limit(1)
|
|
56
|
+
hasAccess = teamMember.length > 0
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!hasAccess) {
|
|
60
|
+
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Update the API key
|
|
64
|
+
const [updatedKey] = await db
|
|
65
|
+
.update(apiKeys)
|
|
66
|
+
.set({
|
|
67
|
+
...updates,
|
|
68
|
+
expiresAt: updates.expiresAt ? new Date(updates.expiresAt) : null,
|
|
69
|
+
})
|
|
70
|
+
.where(eq(apiKeys.id, apiKeyId))
|
|
71
|
+
.returning()
|
|
72
|
+
|
|
73
|
+
// Log audit event
|
|
74
|
+
await createAuditLog({
|
|
75
|
+
userId,
|
|
76
|
+
action: 'update',
|
|
77
|
+
resourceType: 'team', // API keys belong to teams
|
|
78
|
+
resourceId: key.teamId || userId, // Use team ID or user ID as resource ID
|
|
79
|
+
oldValue: { name: key.name, permissions: key.permissions },
|
|
80
|
+
newValue: updates,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
return NextResponse.json({ apiKey: updatedKey })
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error('Error updating API key:', error)
|
|
86
|
+
return NextResponse.json({ error: 'Failed to update API key' }, { status: 500 })
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// DELETE /api/api-keys/[id] - Delete an API key
|
|
91
|
+
export async function DELETE(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
92
|
+
try {
|
|
93
|
+
const user = await getUserFromToken()
|
|
94
|
+
if (!user) {
|
|
95
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const userId = user.id
|
|
99
|
+
const { id: apiKeyId } = await params
|
|
100
|
+
|
|
101
|
+
// Find the API key and verify ownership
|
|
102
|
+
const apiKey = await db
|
|
103
|
+
.select()
|
|
104
|
+
.from(apiKeys)
|
|
105
|
+
.where(eq(apiKeys.id, apiKeyId))
|
|
106
|
+
.limit(1)
|
|
107
|
+
|
|
108
|
+
if (apiKey.length === 0) {
|
|
109
|
+
return NextResponse.json({ error: 'API key not found' }, { status: 404 })
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const key = apiKey[0]
|
|
113
|
+
|
|
114
|
+
// Check if user owns this key or is a member of the team that owns it
|
|
115
|
+
let hasAccess = key.userId === userId
|
|
116
|
+
|
|
117
|
+
if (!hasAccess && key.teamId) {
|
|
118
|
+
const teamMember = await db
|
|
119
|
+
.select()
|
|
120
|
+
.from(teamMembers)
|
|
121
|
+
.where(and(eq(teamMembers.teamId, key.teamId), eq(teamMembers.userId, userId)))
|
|
122
|
+
.limit(1)
|
|
123
|
+
hasAccess = teamMember.length > 0
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!hasAccess) {
|
|
127
|
+
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Delete the API key
|
|
131
|
+
await db.delete(apiKeys).where(eq(apiKeys.id, apiKeyId))
|
|
132
|
+
|
|
133
|
+
// Log audit event
|
|
134
|
+
await createAuditLog({
|
|
135
|
+
userId,
|
|
136
|
+
action: 'delete',
|
|
137
|
+
resourceType: 'team', // API keys belong to teams
|
|
138
|
+
resourceId: key.teamId || userId, // Use team ID or user ID as resource ID
|
|
139
|
+
oldValue: { name: key.name, permissions: key.permissions },
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
return NextResponse.json({ success: true })
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('Error deleting API key:', error)
|
|
145
|
+
return NextResponse.json({ error: 'Failed to delete API key' }, { status: 500 })
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { eq, and, desc } from 'drizzle-orm'
|
|
3
|
+
import { db } from '@/lib/db'
|
|
4
|
+
import { apiKeys, teams, teamMembers } from '@/lib/schema'
|
|
5
|
+
import { createAuditLog } from '@/lib/audit'
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
import bcrypt from 'bcryptjs'
|
|
8
|
+
import crypto from 'crypto'
|
|
9
|
+
import { getUserFromToken } from '@/lib/auth'
|
|
10
|
+
|
|
11
|
+
// Validation schemas
|
|
12
|
+
const createApiKeySchema = z.object({
|
|
13
|
+
name: z.string().min(1).max(100),
|
|
14
|
+
teamId: z.string().optional(),
|
|
15
|
+
permissions: z.array(z.string()).default(['read']),
|
|
16
|
+
expiresAt: z.string().optional(),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
// Generate a secure API key
|
|
20
|
+
function generateApiKey(): string {
|
|
21
|
+
return 'nm_' + crypto.randomBytes(32).toString('hex')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Hash the API key for storage
|
|
25
|
+
async function hashApiKey(key: string): Promise<string> {
|
|
26
|
+
return await bcrypt.hash(key, 12)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// GET /api/api-keys - Get all API keys for the user
|
|
30
|
+
export async function GET() {
|
|
31
|
+
try {
|
|
32
|
+
const user = await getUserFromToken()
|
|
33
|
+
if (!user) {
|
|
34
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const userId = user.id
|
|
38
|
+
|
|
39
|
+
// Get user's API keys
|
|
40
|
+
const userApiKeys = await db
|
|
41
|
+
.select({
|
|
42
|
+
id: apiKeys.id,
|
|
43
|
+
name: apiKeys.name,
|
|
44
|
+
permissions: apiKeys.permissions,
|
|
45
|
+
lastUsed: apiKeys.lastUsed,
|
|
46
|
+
createdAt: apiKeys.createdAt,
|
|
47
|
+
expiresAt: apiKeys.expiresAt,
|
|
48
|
+
teamId: apiKeys.teamId,
|
|
49
|
+
teamName: teams.name,
|
|
50
|
+
})
|
|
51
|
+
.from(apiKeys)
|
|
52
|
+
.leftJoin(teams, eq(apiKeys.teamId, teams.id))
|
|
53
|
+
.where(eq(apiKeys.userId, userId))
|
|
54
|
+
.orderBy(desc(apiKeys.createdAt))
|
|
55
|
+
|
|
56
|
+
// Get API keys for teams where user is a member
|
|
57
|
+
const teamApiKeys = await db
|
|
58
|
+
.select({
|
|
59
|
+
id: apiKeys.id,
|
|
60
|
+
name: apiKeys.name,
|
|
61
|
+
permissions: apiKeys.permissions,
|
|
62
|
+
lastUsed: apiKeys.lastUsed,
|
|
63
|
+
createdAt: apiKeys.createdAt,
|
|
64
|
+
expiresAt: apiKeys.expiresAt,
|
|
65
|
+
teamId: apiKeys.teamId,
|
|
66
|
+
teamName: teams.name,
|
|
67
|
+
})
|
|
68
|
+
.from(apiKeys)
|
|
69
|
+
.innerJoin(teams, eq(apiKeys.teamId, teams.id))
|
|
70
|
+
.innerJoin(teamMembers, eq(teams.id, teamMembers.teamId))
|
|
71
|
+
.where(eq(teamMembers.userId, userId))
|
|
72
|
+
.orderBy(desc(apiKeys.createdAt))
|
|
73
|
+
|
|
74
|
+
const allApiKeys = [...userApiKeys, ...teamApiKeys]
|
|
75
|
+
|
|
76
|
+
return NextResponse.json({ apiKeys: allApiKeys })
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('Error fetching API keys:', error)
|
|
79
|
+
return NextResponse.json({ error: 'Failed to fetch API keys' }, { status: 500 })
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// POST /api/api-keys - Create a new API key
|
|
84
|
+
export async function POST(request: NextRequest) {
|
|
85
|
+
try {
|
|
86
|
+
const user = await getUserFromToken()
|
|
87
|
+
if (!user) {
|
|
88
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const userId = user.id
|
|
92
|
+
const body = await request.json()
|
|
93
|
+
|
|
94
|
+
const validation = createApiKeySchema.safeParse(body)
|
|
95
|
+
if (!validation.success) {
|
|
96
|
+
return NextResponse.json({ error: 'Invalid input', details: validation.error.issues }, { status: 400 })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const { name, teamId, permissions, expiresAt } = validation.data
|
|
100
|
+
|
|
101
|
+
// If teamId is provided, verify user is a member of the team
|
|
102
|
+
if (teamId) {
|
|
103
|
+
const teamMember = await db
|
|
104
|
+
.select()
|
|
105
|
+
.from(teamMembers)
|
|
106
|
+
.where(and(eq(teamMembers.teamId, teamId), eq(teamMembers.userId, userId)))
|
|
107
|
+
.limit(1)
|
|
108
|
+
|
|
109
|
+
if (teamMember.length === 0) {
|
|
110
|
+
return NextResponse.json({ error: 'You are not a member of this team' }, { status: 403 })
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Generate and hash the API key
|
|
115
|
+
const apiKey = generateApiKey()
|
|
116
|
+
const keyHash = await hashApiKey(apiKey)
|
|
117
|
+
|
|
118
|
+
// Create the API key record
|
|
119
|
+
const [newApiKey] = await db
|
|
120
|
+
.insert(apiKeys)
|
|
121
|
+
.values({
|
|
122
|
+
userId,
|
|
123
|
+
teamId: teamId || null,
|
|
124
|
+
name,
|
|
125
|
+
keyHash,
|
|
126
|
+
permissions,
|
|
127
|
+
expiresAt: expiresAt ? new Date(expiresAt) : null,
|
|
128
|
+
})
|
|
129
|
+
.returning()
|
|
130
|
+
|
|
131
|
+
// Log audit event
|
|
132
|
+
await createAuditLog({
|
|
133
|
+
userId,
|
|
134
|
+
action: 'create',
|
|
135
|
+
resourceType: 'team', // API keys belong to teams
|
|
136
|
+
resourceId: teamId || userId, // Use team ID or user ID as resource ID
|
|
137
|
+
newValue: { name, permissions, teamId },
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
// Return the API key (this is the only time it will be shown)
|
|
141
|
+
return NextResponse.json({
|
|
142
|
+
apiKey: {
|
|
143
|
+
...newApiKey,
|
|
144
|
+
key: apiKey, // Include the plain key for the user to copy
|
|
145
|
+
},
|
|
146
|
+
})
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error('Error creating API key:', error)
|
|
149
|
+
return NextResponse.json({ error: 'Failed to create API key' }, { status: 500 })
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { eq, and, desc, or, inArray } from 'drizzle-orm'
|
|
3
|
+
import { db } from '@/lib/db'
|
|
4
|
+
import { auditLogs, users, teams, teamMembers } from '@/lib/schema'
|
|
5
|
+
import { getUserFromToken } from '@/lib/auth'
|
|
6
|
+
|
|
7
|
+
export async function GET(request: NextRequest) {
|
|
8
|
+
try {
|
|
9
|
+
const user = await getUserFromToken()
|
|
10
|
+
if (!user) {
|
|
11
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { searchParams } = new URL(request.url)
|
|
15
|
+
const teamId = searchParams.get('teamId')
|
|
16
|
+
|
|
17
|
+
let query = db
|
|
18
|
+
.select({
|
|
19
|
+
id: auditLogs.id,
|
|
20
|
+
action: auditLogs.action,
|
|
21
|
+
resourceType: auditLogs.resourceType,
|
|
22
|
+
resourceId: auditLogs.resourceId,
|
|
23
|
+
oldValue: auditLogs.oldValue,
|
|
24
|
+
newValue: auditLogs.newValue,
|
|
25
|
+
ipAddress: auditLogs.ipAddress,
|
|
26
|
+
userAgent: auditLogs.userAgent,
|
|
27
|
+
createdAt: auditLogs.createdAt,
|
|
28
|
+
userId: auditLogs.userId,
|
|
29
|
+
userName: users.name,
|
|
30
|
+
userEmail: users.email,
|
|
31
|
+
teamId: auditLogs.teamId,
|
|
32
|
+
teamName: teams.name,
|
|
33
|
+
})
|
|
34
|
+
.from(auditLogs)
|
|
35
|
+
.leftJoin(users, eq(auditLogs.userId, users.id))
|
|
36
|
+
.leftJoin(teams, eq(auditLogs.teamId, teams.id))
|
|
37
|
+
|
|
38
|
+
if (teamId) {
|
|
39
|
+
// Check if user is member of the team
|
|
40
|
+
const membership = await db
|
|
41
|
+
.select()
|
|
42
|
+
.from(teamMembers)
|
|
43
|
+
.where(
|
|
44
|
+
and(
|
|
45
|
+
eq(teamMembers.teamId, teamId),
|
|
46
|
+
eq(teamMembers.userId, user.id)
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
.limit(1)
|
|
50
|
+
|
|
51
|
+
if (membership.length === 0) {
|
|
52
|
+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
query = query.where(eq(auditLogs.teamId, teamId)) as any
|
|
56
|
+
} else {
|
|
57
|
+
// Get teams where user is member
|
|
58
|
+
const userTeams = await db
|
|
59
|
+
.select({ id: teamMembers.teamId })
|
|
60
|
+
.from(teamMembers)
|
|
61
|
+
.where(eq(teamMembers.userId, user.id))
|
|
62
|
+
|
|
63
|
+
const teamIds = userTeams.map(t => t.id)
|
|
64
|
+
|
|
65
|
+
if (teamIds.length > 0) {
|
|
66
|
+
query = query.where(
|
|
67
|
+
or(
|
|
68
|
+
eq(auditLogs.userId, user.id),
|
|
69
|
+
inArray(auditLogs.teamId, teamIds)
|
|
70
|
+
)
|
|
71
|
+
) as any
|
|
72
|
+
} else {
|
|
73
|
+
query = query.where(eq(auditLogs.userId, user.id)) as any
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const logs = await (query as any).orderBy(desc(auditLogs.createdAt)).limit(100)
|
|
78
|
+
|
|
79
|
+
return NextResponse.json({ logs })
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('Error fetching audit logs:', error)
|
|
82
|
+
return NextResponse.json({ error: 'Failed to fetch audit logs' }, { status: 500 })
|
|
83
|
+
}
|
|
84
|
+
}
|