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,117 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
const Table = React.forwardRef<
|
|
6
|
+
HTMLTableElement,
|
|
7
|
+
React.HTMLAttributes<HTMLTableElement>
|
|
8
|
+
>(({ className, ...props }, ref) => (
|
|
9
|
+
<div className="relative w-full overflow-auto">
|
|
10
|
+
<table
|
|
11
|
+
ref={ref}
|
|
12
|
+
className={cn("w-full caption-bottom text-sm", className)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
</div>
|
|
16
|
+
))
|
|
17
|
+
Table.displayName = "Table"
|
|
18
|
+
|
|
19
|
+
const TableHeader = React.forwardRef<
|
|
20
|
+
HTMLTableSectionElement,
|
|
21
|
+
React.HTMLAttributes<HTMLTableSectionElement>
|
|
22
|
+
>(({ className, ...props }, ref) => (
|
|
23
|
+
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
|
24
|
+
))
|
|
25
|
+
TableHeader.displayName = "TableHeader"
|
|
26
|
+
|
|
27
|
+
const TableBody = React.forwardRef<
|
|
28
|
+
HTMLTableSectionElement,
|
|
29
|
+
React.HTMLAttributes<HTMLTableSectionElement>
|
|
30
|
+
>(({ className, ...props }, ref) => (
|
|
31
|
+
<tbody
|
|
32
|
+
ref={ref}
|
|
33
|
+
className={cn("[&_tr:last-child]:border-0", className)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
))
|
|
37
|
+
TableBody.displayName = "TableBody"
|
|
38
|
+
|
|
39
|
+
const TableFooter = React.forwardRef<
|
|
40
|
+
HTMLTableSectionElement,
|
|
41
|
+
React.HTMLAttributes<HTMLTableSectionElement>
|
|
42
|
+
>(({ className, ...props }, ref) => (
|
|
43
|
+
<tfoot
|
|
44
|
+
ref={ref}
|
|
45
|
+
className={cn(
|
|
46
|
+
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
|
47
|
+
className
|
|
48
|
+
)}
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
))
|
|
52
|
+
TableFooter.displayName = "TableFooter"
|
|
53
|
+
|
|
54
|
+
const TableRow = React.forwardRef<
|
|
55
|
+
HTMLTableRowElement,
|
|
56
|
+
React.HTMLAttributes<HTMLTableRowElement>
|
|
57
|
+
>(({ className, ...props }, ref) => (
|
|
58
|
+
<tr
|
|
59
|
+
ref={ref}
|
|
60
|
+
className={cn(
|
|
61
|
+
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
|
62
|
+
className
|
|
63
|
+
)}
|
|
64
|
+
{...props}
|
|
65
|
+
/>
|
|
66
|
+
))
|
|
67
|
+
TableRow.displayName = "TableRow"
|
|
68
|
+
|
|
69
|
+
const TableHead = React.forwardRef<
|
|
70
|
+
HTMLTableCellElement,
|
|
71
|
+
React.ThHTMLAttributes<HTMLTableCellElement>
|
|
72
|
+
>(({ className, ...props }, ref) => (
|
|
73
|
+
<th
|
|
74
|
+
ref={ref}
|
|
75
|
+
className={cn(
|
|
76
|
+
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
|
77
|
+
className
|
|
78
|
+
)}
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
))
|
|
82
|
+
TableHead.displayName = "TableHead"
|
|
83
|
+
|
|
84
|
+
const TableCell = React.forwardRef<
|
|
85
|
+
HTMLTableCellElement,
|
|
86
|
+
React.TdHTMLAttributes<HTMLTableCellElement>
|
|
87
|
+
>(({ className, ...props }, ref) => (
|
|
88
|
+
<td
|
|
89
|
+
ref={ref}
|
|
90
|
+
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
|
91
|
+
{...props}
|
|
92
|
+
/>
|
|
93
|
+
))
|
|
94
|
+
TableCell.displayName = "TableCell"
|
|
95
|
+
|
|
96
|
+
const TableCaption = React.forwardRef<
|
|
97
|
+
HTMLTableCaptionElement,
|
|
98
|
+
React.HTMLAttributes<HTMLTableCaptionElement>
|
|
99
|
+
>(({ className, ...props }, ref) => (
|
|
100
|
+
<caption
|
|
101
|
+
ref={ref}
|
|
102
|
+
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
|
103
|
+
{...props}
|
|
104
|
+
/>
|
|
105
|
+
))
|
|
106
|
+
TableCaption.displayName = "TableCaption"
|
|
107
|
+
|
|
108
|
+
export {
|
|
109
|
+
Table,
|
|
110
|
+
TableHeader,
|
|
111
|
+
TableBody,
|
|
112
|
+
TableFooter,
|
|
113
|
+
TableHead,
|
|
114
|
+
TableRow,
|
|
115
|
+
TableCell,
|
|
116
|
+
TableCaption,
|
|
117
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
function Tabs({
|
|
9
|
+
className,
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
|
12
|
+
return (
|
|
13
|
+
<TabsPrimitive.Root
|
|
14
|
+
data-slot="tabs"
|
|
15
|
+
className={cn("flex flex-col gap-2", className)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function TabsList({
|
|
22
|
+
className,
|
|
23
|
+
...props
|
|
24
|
+
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
|
25
|
+
return (
|
|
26
|
+
<TabsPrimitive.List
|
|
27
|
+
data-slot="tabs-list"
|
|
28
|
+
className={cn(
|
|
29
|
+
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
|
30
|
+
className
|
|
31
|
+
)}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function TabsTrigger({
|
|
38
|
+
className,
|
|
39
|
+
...props
|
|
40
|
+
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
|
41
|
+
return (
|
|
42
|
+
<TabsPrimitive.Trigger
|
|
43
|
+
data-slot="tabs-trigger"
|
|
44
|
+
className={cn(
|
|
45
|
+
"data-[state=active]:bg-background focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function TabsContent({
|
|
54
|
+
className,
|
|
55
|
+
...props
|
|
56
|
+
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
|
57
|
+
return (
|
|
58
|
+
<TabsPrimitive.Content
|
|
59
|
+
data-slot="tabs-content"
|
|
60
|
+
className={cn("flex-1 outline-none", className)}
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|
6
|
+
return (
|
|
7
|
+
<textarea
|
|
8
|
+
data-slot="textarea"
|
|
9
|
+
className={cn(
|
|
10
|
+
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 aria-invalid:border-destructive flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
11
|
+
className
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { Textarea }
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { useAuth } from "./use-auth";
|
|
5
|
+
|
|
6
|
+
export interface ApiKey {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
permissions: string[];
|
|
10
|
+
lastUsed: string | null;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
expiresAt: string | null;
|
|
13
|
+
teamId: string | null;
|
|
14
|
+
teamName: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useApiKeys() {
|
|
18
|
+
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
|
|
19
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
20
|
+
const [error, setError] = useState<string | null>(null);
|
|
21
|
+
const { user } = useAuth();
|
|
22
|
+
|
|
23
|
+
const fetchApiKeys = async () => {
|
|
24
|
+
if (!user) return;
|
|
25
|
+
|
|
26
|
+
setIsLoading(true);
|
|
27
|
+
try {
|
|
28
|
+
const response = await fetch("/api/api-keys");
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw new Error("Failed to fetch API keys");
|
|
31
|
+
}
|
|
32
|
+
const data = await response.json();
|
|
33
|
+
setApiKeys(data.apiKeys || []);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
setError(err instanceof Error ? err.message : "An error occurred");
|
|
36
|
+
} finally {
|
|
37
|
+
setIsLoading(false);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
fetchApiKeys();
|
|
43
|
+
}, [user]);
|
|
44
|
+
|
|
45
|
+
const createApiKey = async (data: {
|
|
46
|
+
name: string;
|
|
47
|
+
teamId?: string;
|
|
48
|
+
permissions?: string[];
|
|
49
|
+
expiresAt?: string;
|
|
50
|
+
}) => {
|
|
51
|
+
try {
|
|
52
|
+
const response = await fetch("/api/api-keys", {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: { "Content-Type": "application/json" },
|
|
55
|
+
body: JSON.stringify(data),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
const errorData = await response.json();
|
|
60
|
+
throw new Error(errorData.error || "Failed to create API key");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const result = await response.json();
|
|
64
|
+
await fetchApiKeys();
|
|
65
|
+
return result;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const deleteApiKey = async (id: string) => {
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch(`/api/api-keys/${id}`, {
|
|
74
|
+
method: "DELETE",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
throw new Error("Failed to delete API key");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await fetchApiKeys();
|
|
82
|
+
} catch (err) {
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
apiKeys,
|
|
89
|
+
isLoading,
|
|
90
|
+
error,
|
|
91
|
+
createApiKey,
|
|
92
|
+
deleteApiKey,
|
|
93
|
+
refetch: fetchApiKeys,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { useAuth } from "./use-auth";
|
|
5
|
+
|
|
6
|
+
export interface AuditLog {
|
|
7
|
+
id: string;
|
|
8
|
+
userId: string | null;
|
|
9
|
+
userName: string | null;
|
|
10
|
+
userEmail: string | null;
|
|
11
|
+
teamId: string | null;
|
|
12
|
+
teamName: string | null;
|
|
13
|
+
action: string;
|
|
14
|
+
resourceType: string;
|
|
15
|
+
resourceId: string;
|
|
16
|
+
oldValue: any;
|
|
17
|
+
newValue: any;
|
|
18
|
+
ipAddress: string | null;
|
|
19
|
+
userAgent: string | null;
|
|
20
|
+
createdAt: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useAuditLogs(teamId?: string) {
|
|
24
|
+
const [logs, setLogs] = useState<AuditLog[]>([]);
|
|
25
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
26
|
+
const [error, setError] = useState<string | null>(null);
|
|
27
|
+
const { user } = useAuth();
|
|
28
|
+
|
|
29
|
+
const fetchLogs = async () => {
|
|
30
|
+
if (!user) return;
|
|
31
|
+
|
|
32
|
+
setIsLoading(true);
|
|
33
|
+
try {
|
|
34
|
+
const url = teamId ? `/api/audit-logs?teamId=${teamId}` : "/api/audit-logs";
|
|
35
|
+
const response = await fetch(url);
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
throw new Error("Failed to fetch audit logs");
|
|
38
|
+
}
|
|
39
|
+
const data = await response.json();
|
|
40
|
+
setLogs(data.logs || []);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
setError(err instanceof Error ? err.message : "An error occurred");
|
|
43
|
+
} finally {
|
|
44
|
+
setIsLoading(false);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
fetchLogs();
|
|
50
|
+
}, [user, teamId]);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
logs,
|
|
54
|
+
isLoading,
|
|
55
|
+
error,
|
|
56
|
+
refetch: fetchLogs,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
interface User {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
email: string;
|
|
9
|
+
avatar?: string | null;
|
|
10
|
+
mfaEnabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface AuthContextType {
|
|
14
|
+
user: User | null;
|
|
15
|
+
login: (email: string, password: string, mfaToken?: string) => Promise<{ success: boolean; error?: string; mfaRequired?: boolean }>;
|
|
16
|
+
register: (name: string, email: string, password: string, confirmPassword: string, inviteToken?: string) => Promise<{ success: boolean; error?: string }>;
|
|
17
|
+
logout: () => Promise<void>;
|
|
18
|
+
isLoading: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
22
|
+
|
|
23
|
+
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
24
|
+
const [user, setUser] = useState<User | null>(null);
|
|
25
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
26
|
+
|
|
27
|
+
// Check if user is logged in on mount
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
checkAuthStatus();
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
const checkAuthStatus = async () => {
|
|
33
|
+
try {
|
|
34
|
+
const response = await fetch('/api/auth/me');
|
|
35
|
+
if (response.ok) {
|
|
36
|
+
const data = await response.json();
|
|
37
|
+
setUser(data.user);
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('Auth check failed:', error);
|
|
41
|
+
} finally {
|
|
42
|
+
setIsLoading(false);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const login = async (email: string, password: string, mfaToken?: string) => {
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetch('/api/auth/login', {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: {
|
|
51
|
+
'Content-Type': 'application/json',
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify({ email, password, mfaToken }),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const data = await response.json();
|
|
57
|
+
|
|
58
|
+
if (response.ok) {
|
|
59
|
+
if (data.mfaRequired) {
|
|
60
|
+
return { success: false, mfaRequired: true };
|
|
61
|
+
}
|
|
62
|
+
setUser(data.user);
|
|
63
|
+
return { success: true };
|
|
64
|
+
} else {
|
|
65
|
+
return { success: false, error: data.error };
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error('Login error:', error);
|
|
69
|
+
return { success: false, error: 'Network error' };
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const register = async (name: string, email: string, password: string, confirmPassword: string, inviteToken?: string) => {
|
|
74
|
+
try {
|
|
75
|
+
const response = await fetch('/api/auth/register', {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: {
|
|
78
|
+
'Content-Type': 'application/json',
|
|
79
|
+
},
|
|
80
|
+
body: JSON.stringify({ name, email, password, confirmPassword, inviteToken }),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const data = await response.json();
|
|
84
|
+
|
|
85
|
+
if (response.ok) {
|
|
86
|
+
// After successful registration, automatically log them in
|
|
87
|
+
return await login(email, password);
|
|
88
|
+
} else {
|
|
89
|
+
return { success: false, error: data.error };
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error('Registration error:', error);
|
|
93
|
+
return { success: false, error: 'Network error' };
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const logout = async () => {
|
|
98
|
+
try {
|
|
99
|
+
await fetch('/api/auth/logout', {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
});
|
|
102
|
+
setUser(null);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error('Logout error:', error);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<AuthContext.Provider value={{ user, login, register, logout, isLoading }}>
|
|
110
|
+
{children}
|
|
111
|
+
</AuthContext.Provider>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function useAuth() {
|
|
116
|
+
const context = useContext(AuthContext);
|
|
117
|
+
if (context === undefined) {
|
|
118
|
+
throw new Error('useAuth must be used within an AuthProvider');
|
|
119
|
+
}
|
|
120
|
+
return context;
|
|
121
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
|
+
|
|
3
|
+
export interface Environment {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
description: string | null;
|
|
7
|
+
teamId: string;
|
|
8
|
+
color: string | null;
|
|
9
|
+
isDefault: boolean;
|
|
10
|
+
createdAt: Date;
|
|
11
|
+
teamName: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useEnvironments() {
|
|
15
|
+
const { data, isLoading, error, refetch } = useQuery({
|
|
16
|
+
queryKey: ['environments'],
|
|
17
|
+
queryFn: async () => {
|
|
18
|
+
const response = await fetch('/api/environments');
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
throw new Error('Failed to fetch environments');
|
|
21
|
+
}
|
|
22
|
+
const data = await response.json();
|
|
23
|
+
return data.environments as Environment[];
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
environments: data || [],
|
|
29
|
+
isLoading,
|
|
30
|
+
error: error instanceof Error ? error.message : null,
|
|
31
|
+
refetch
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useTeams } from "./use-teams";
|
|
4
|
+
import { useTeamMembers } from "./use-team-members";
|
|
5
|
+
import { useAuth } from "./use-auth";
|
|
6
|
+
|
|
7
|
+
export type Role = 'owner' | 'admin' | 'developer' | 'viewer';
|
|
8
|
+
|
|
9
|
+
export const RoleHierarchy: Record<Role, number> = {
|
|
10
|
+
owner: 4,
|
|
11
|
+
admin: 3,
|
|
12
|
+
developer: 2,
|
|
13
|
+
viewer: 1,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function useProjectPermissions(teamId: string) {
|
|
17
|
+
const { user } = useAuth();
|
|
18
|
+
const { teams } = useTeams();
|
|
19
|
+
const { data: members } = useTeamMembers(teamId);
|
|
20
|
+
|
|
21
|
+
const team = teams?.find(t => t.id === teamId);
|
|
22
|
+
const member = members?.find(m => m.userId === user?.id);
|
|
23
|
+
|
|
24
|
+
let userRole: Role | null = null;
|
|
25
|
+
|
|
26
|
+
if (team && user && team.ownerId === user.id) {
|
|
27
|
+
userRole = 'owner';
|
|
28
|
+
} else if (member) {
|
|
29
|
+
userRole = member.role as Role;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const hasRole = (requiredRole: Role) => {
|
|
33
|
+
if (!userRole) return false;
|
|
34
|
+
return RoleHierarchy[userRole] >= RoleHierarchy[requiredRole];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
role: userRole,
|
|
39
|
+
members: members || [],
|
|
40
|
+
isLoading: !members,
|
|
41
|
+
canManageProject: hasRole('admin'),
|
|
42
|
+
canManageSecrets: hasRole('developer'),
|
|
43
|
+
canViewSecrets: hasRole('viewer'),
|
|
44
|
+
isOwner: userRole === 'owner',
|
|
45
|
+
isAdmin: userRole === 'admin',
|
|
46
|
+
isDeveloper: userRole === 'developer',
|
|
47
|
+
isViewer: userRole === 'viewer',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
|
+
|
|
3
|
+
export interface TeamMember {
|
|
4
|
+
id: string;
|
|
5
|
+
teamId: string;
|
|
6
|
+
userId: string;
|
|
7
|
+
role: 'owner' | 'admin' | 'member';
|
|
8
|
+
joinedAt: string;
|
|
9
|
+
user: {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
email: string;
|
|
13
|
+
avatar?: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useTeamMembers(teamId: string) {
|
|
18
|
+
return useQuery({
|
|
19
|
+
queryKey: ['team-members', teamId],
|
|
20
|
+
queryFn: async (): Promise<TeamMember[]> => {
|
|
21
|
+
const response = await fetch(`/api/team-members?teamId=${teamId}`);
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
throw new Error('Failed to fetch team members');
|
|
24
|
+
}
|
|
25
|
+
const data = await response.json();
|
|
26
|
+
return data.members || [];
|
|
27
|
+
},
|
|
28
|
+
enabled: !!teamId,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
|
+
|
|
3
|
+
export interface Team {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
description: string | null;
|
|
7
|
+
ownerId: string;
|
|
8
|
+
color: string | null;
|
|
9
|
+
isDefault: boolean;
|
|
10
|
+
createdAt: Date;
|
|
11
|
+
updatedAt: Date;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useTeams() {
|
|
15
|
+
const { data, isLoading, error, refetch } = useQuery({
|
|
16
|
+
queryKey: ['teams'],
|
|
17
|
+
queryFn: async () => {
|
|
18
|
+
const response = await fetch('/api/teams');
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
throw new Error('Failed to fetch teams');
|
|
21
|
+
}
|
|
22
|
+
const data = await response.json();
|
|
23
|
+
return data.teams as Team[];
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
teams: data || [],
|
|
29
|
+
isLoading,
|
|
30
|
+
error: error instanceof Error ? error.message : null,
|
|
31
|
+
refetch
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
|
+
|
|
3
|
+
export interface EnvironmentVariable {
|
|
4
|
+
id: string;
|
|
5
|
+
key: string;
|
|
6
|
+
value: string;
|
|
7
|
+
description: string | null;
|
|
8
|
+
isSecret: boolean;
|
|
9
|
+
isDynamic?: boolean;
|
|
10
|
+
provider?: string | null;
|
|
11
|
+
expiresAt?: Date | string | null;
|
|
12
|
+
teamId: string;
|
|
13
|
+
environmentId: string;
|
|
14
|
+
createdAt: Date;
|
|
15
|
+
teamName: string;
|
|
16
|
+
environmentName: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useVariables() {
|
|
20
|
+
const { data, isLoading, error, refetch } = useQuery({
|
|
21
|
+
queryKey: ['variables'],
|
|
22
|
+
queryFn: async () => {
|
|
23
|
+
const response = await fetch('/api/variables');
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
throw new Error('Failed to fetch variables');
|
|
26
|
+
}
|
|
27
|
+
const data = await response.json();
|
|
28
|
+
return data.variables as EnvironmentVariable[];
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
variables: data || [],
|
|
34
|
+
isLoading,
|
|
35
|
+
error: error instanceof Error ? error.message : null,
|
|
36
|
+
refetch
|
|
37
|
+
};
|
|
38
|
+
}
|
package/src/lib/audit.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { db } from '@/lib/db';
|
|
2
|
+
import { auditLogs } from '@/lib/schema';
|
|
3
|
+
|
|
4
|
+
export interface AuditLogData {
|
|
5
|
+
userId?: string;
|
|
6
|
+
teamId?: string;
|
|
7
|
+
action: 'create' | 'update' | 'delete' | 'view';
|
|
8
|
+
resourceType: 'variable' | 'environment' | 'team' | 'member';
|
|
9
|
+
resourceId: string;
|
|
10
|
+
oldValue?: Record<string, unknown>;
|
|
11
|
+
newValue?: Record<string, unknown>;
|
|
12
|
+
ipAddress?: string;
|
|
13
|
+
userAgent?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function createAuditLog(data: AuditLogData) {
|
|
17
|
+
try {
|
|
18
|
+
// Get IP and user agent from headers if available
|
|
19
|
+
// This would typically come from the request object in API routes
|
|
20
|
+
|
|
21
|
+
await db.insert(auditLogs).values({
|
|
22
|
+
userId: data.userId,
|
|
23
|
+
teamId: data.teamId,
|
|
24
|
+
action: data.action,
|
|
25
|
+
resourceType: data.resourceType,
|
|
26
|
+
resourceId: data.resourceId,
|
|
27
|
+
oldValue: data.oldValue ? JSON.stringify(data.oldValue) : null,
|
|
28
|
+
newValue: data.newValue ? JSON.stringify(data.newValue) : null,
|
|
29
|
+
ipAddress: data.ipAddress,
|
|
30
|
+
userAgent: data.userAgent,
|
|
31
|
+
});
|
|
32
|
+
} catch (error) {
|
|
33
|
+
// Log audit errors but don't fail the main operation
|
|
34
|
+
console.error('Failed to create audit log:', error);
|
|
35
|
+
}
|
|
36
|
+
}
|