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.
Files changed (123) hide show
  1. package/README.md +112 -0
  2. package/components.json +22 -0
  3. package/dist/nobalmako.js +272 -0
  4. package/drizzle/0000_pink_spiral.sql +126 -0
  5. package/drizzle/meta/0000_snapshot.json +1027 -0
  6. package/drizzle/meta/_journal.json +13 -0
  7. package/drizzle.config.ts +10 -0
  8. package/eslint.config.mjs +18 -0
  9. package/next.config.ts +7 -0
  10. package/package.json +80 -0
  11. package/postcss.config.mjs +7 -0
  12. package/public/file.svg +1 -0
  13. package/public/globe.svg +1 -0
  14. package/public/next.svg +1 -0
  15. package/public/vercel.svg +1 -0
  16. package/public/window.svg +1 -0
  17. package/server/index.ts +118 -0
  18. package/src/app/api/api-keys/[id]/route.ts +147 -0
  19. package/src/app/api/api-keys/route.ts +151 -0
  20. package/src/app/api/audit-logs/route.ts +84 -0
  21. package/src/app/api/auth/forgot-password/route.ts +47 -0
  22. package/src/app/api/auth/login/route.ts +99 -0
  23. package/src/app/api/auth/logout/route.ts +15 -0
  24. package/src/app/api/auth/me/route.ts +23 -0
  25. package/src/app/api/auth/mfa/setup/route.ts +33 -0
  26. package/src/app/api/auth/mfa/verify/route.ts +45 -0
  27. package/src/app/api/auth/register/route.ts +140 -0
  28. package/src/app/api/auth/reset-password/route.ts +52 -0
  29. package/src/app/api/auth/update/route.ts +71 -0
  30. package/src/app/api/auth/verify/route.ts +39 -0
  31. package/src/app/api/environments/route.ts +227 -0
  32. package/src/app/api/team-members/route.ts +385 -0
  33. package/src/app/api/teams/route.ts +217 -0
  34. package/src/app/api/variable-history/route.ts +218 -0
  35. package/src/app/api/variables/route.ts +476 -0
  36. package/src/app/api/webhooks/route.ts +77 -0
  37. package/src/app/api-keys/APIKeysClient.tsx +316 -0
  38. package/src/app/api-keys/page.tsx +10 -0
  39. package/src/app/api-reference/page.tsx +324 -0
  40. package/src/app/audit-log/AuditLogClient.tsx +229 -0
  41. package/src/app/audit-log/page.tsx +10 -0
  42. package/src/app/auth/forgot-password/page.tsx +121 -0
  43. package/src/app/auth/login/LoginForm.tsx +145 -0
  44. package/src/app/auth/login/page.tsx +11 -0
  45. package/src/app/auth/register/RegisterForm.tsx +156 -0
  46. package/src/app/auth/register/page.tsx +16 -0
  47. package/src/app/auth/reset-password/page.tsx +160 -0
  48. package/src/app/dashboard/DashboardClient.tsx +219 -0
  49. package/src/app/dashboard/page.tsx +11 -0
  50. package/src/app/docs/page.tsx +251 -0
  51. package/src/app/favicon.ico +0 -0
  52. package/src/app/globals.css +123 -0
  53. package/src/app/layout.tsx +35 -0
  54. package/src/app/page.tsx +231 -0
  55. package/src/app/profile/ProfileClient.tsx +230 -0
  56. package/src/app/profile/page.tsx +10 -0
  57. package/src/app/project/[id]/ProjectDetailsClient.tsx +512 -0
  58. package/src/app/project/[id]/page.tsx +17 -0
  59. package/src/bin/nobalmako.ts +341 -0
  60. package/src/components/ApiKeysManager.tsx +529 -0
  61. package/src/components/AppLayout.tsx +193 -0
  62. package/src/components/BulkActions.tsx +138 -0
  63. package/src/components/CreateEnvironmentDialog.tsx +207 -0
  64. package/src/components/CreateTeamDialog.tsx +174 -0
  65. package/src/components/CreateVariableDialog.tsx +311 -0
  66. package/src/components/DeleteEnvironmentDialog.tsx +104 -0
  67. package/src/components/DeleteTeamDialog.tsx +112 -0
  68. package/src/components/DeleteVariableDialog.tsx +103 -0
  69. package/src/components/EditEnvironmentDialog.tsx +202 -0
  70. package/src/components/EditMemberDialog.tsx +143 -0
  71. package/src/components/EditTeamDialog.tsx +178 -0
  72. package/src/components/EditVariableDialog.tsx +231 -0
  73. package/src/components/ImportVariablesDialog.tsx +347 -0
  74. package/src/components/InviteMemberDialog.tsx +191 -0
  75. package/src/components/LeaveProjectDialog.tsx +111 -0
  76. package/src/components/MFASettings.tsx +136 -0
  77. package/src/components/ProjectDiff.tsx +123 -0
  78. package/src/components/Providers.tsx +24 -0
  79. package/src/components/RemoveMemberDialog.tsx +112 -0
  80. package/src/components/SearchDialog.tsx +276 -0
  81. package/src/components/SecurityOverview.tsx +92 -0
  82. package/src/components/TeamMembersManager.tsx +103 -0
  83. package/src/components/VariableHistoryDialog.tsx +265 -0
  84. package/src/components/WebhooksManager.tsx +169 -0
  85. package/src/components/ui/alert-dialog.tsx +160 -0
  86. package/src/components/ui/alert.tsx +59 -0
  87. package/src/components/ui/avatar.tsx +53 -0
  88. package/src/components/ui/badge.tsx +46 -0
  89. package/src/components/ui/button.tsx +62 -0
  90. package/src/components/ui/card.tsx +92 -0
  91. package/src/components/ui/checkbox.tsx +32 -0
  92. package/src/components/ui/dialog.tsx +143 -0
  93. package/src/components/ui/dropdown-menu.tsx +257 -0
  94. package/src/components/ui/input.tsx +21 -0
  95. package/src/components/ui/label.tsx +24 -0
  96. package/src/components/ui/select.tsx +190 -0
  97. package/src/components/ui/separator.tsx +28 -0
  98. package/src/components/ui/sonner.tsx +37 -0
  99. package/src/components/ui/switch.tsx +31 -0
  100. package/src/components/ui/table.tsx +117 -0
  101. package/src/components/ui/tabs.tsx +66 -0
  102. package/src/components/ui/textarea.tsx +18 -0
  103. package/src/hooks/use-api-keys.ts +95 -0
  104. package/src/hooks/use-audit-logs.ts +58 -0
  105. package/src/hooks/use-auth.tsx +121 -0
  106. package/src/hooks/use-environments.ts +33 -0
  107. package/src/hooks/use-project-permissions.ts +49 -0
  108. package/src/hooks/use-team-members.ts +30 -0
  109. package/src/hooks/use-teams.ts +33 -0
  110. package/src/hooks/use-variables.ts +38 -0
  111. package/src/lib/audit.ts +36 -0
  112. package/src/lib/auth.ts +108 -0
  113. package/src/lib/crypto.ts +39 -0
  114. package/src/lib/db.ts +15 -0
  115. package/src/lib/dynamic-providers.ts +19 -0
  116. package/src/lib/email.ts +110 -0
  117. package/src/lib/mail.ts +51 -0
  118. package/src/lib/permissions.ts +51 -0
  119. package/src/lib/schema.ts +240 -0
  120. package/src/lib/seed.ts +107 -0
  121. package/src/lib/utils.ts +6 -0
  122. package/src/lib/webhooks.ts +42 -0
  123. package/tsconfig.json +34 -0
@@ -0,0 +1,178 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { useQueryClient } from "@tanstack/react-query";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Input } from "@/components/ui/input";
7
+ import { Label } from "@/components/ui/label";
8
+ import { Textarea } from "@/components/ui/textarea";
9
+ import {
10
+ Dialog,
11
+ DialogContent,
12
+ DialogDescription,
13
+ DialogFooter,
14
+ DialogHeader,
15
+ DialogTitle,
16
+ DialogTrigger,
17
+ } from "@/components/ui/dialog";
18
+ import { Edit, Shield, Palette, Type, AlignLeft } from "lucide-react";
19
+ import { useTeams } from "@/hooks/use-teams";
20
+
21
+ interface EditTeamDialogProps {
22
+ teamId: string;
23
+ children?: React.ReactNode;
24
+ }
25
+
26
+ export default function EditTeamDialog({ teamId, children }: EditTeamDialogProps) {
27
+ const [open, setOpen] = useState(false);
28
+ const [name, setName] = useState("");
29
+ const [description, setDescription] = useState("");
30
+ const [color, setColor] = useState("#3b82f6");
31
+ const [isLoading, setIsLoading] = useState(false);
32
+ const [error, setError] = useState("");
33
+ const { teams } = useTeams();
34
+ const queryClient = useQueryClient();
35
+
36
+ const team = teams?.find(t => t.id === teamId);
37
+
38
+ useEffect(() => {
39
+ if (team && open) {
40
+ setName(team.name);
41
+ setDescription(team.description || "");
42
+ setColor(team.color || "#3b82f6");
43
+ }
44
+ }, [team, open]);
45
+
46
+ const handleSubmit = async (e: React.FormEvent) => {
47
+ e.preventDefault();
48
+ setError("");
49
+ setIsLoading(true);
50
+
51
+ try {
52
+ const response = await fetch('/api/teams', {
53
+ method: 'PUT',
54
+ headers: {
55
+ 'Content-Type': 'application/json',
56
+ },
57
+ body: JSON.stringify({ id: teamId, name, description, color }),
58
+ });
59
+
60
+ const data = await response.json();
61
+
62
+ if (response.ok) {
63
+ setOpen(false);
64
+ queryClient.invalidateQueries({ queryKey: ['teams'] });
65
+ } else {
66
+ setError(data.error || "Failed to update team");
67
+ }
68
+ } catch (err) {
69
+ setError("An unexpected error occurred");
70
+ } finally {
71
+ setIsLoading(false);
72
+ }
73
+ };
74
+
75
+ return (
76
+ <Dialog open={open} onOpenChange={setOpen}>
77
+ <DialogTrigger asChild>
78
+ {children || (
79
+ <Button variant="ghost" size="sm">
80
+ <Edit className="h-4 w-4" />
81
+ </Button>
82
+ )}
83
+ </DialogTrigger>
84
+ <DialogContent className="sm:max-w-[425px]">
85
+ <DialogHeader>
86
+ <div className="flex items-center gap-2 mb-1">
87
+ <div className="p-1.5 bg-primary/10 rounded-md">
88
+ <Shield className="h-5 w-5 text-primary" />
89
+ </div>
90
+ <DialogTitle className="text-xl">Edit Project</DialogTitle>
91
+ </div>
92
+ <DialogDescription>
93
+ Update project metadata and branding for {team?.name}.
94
+ </DialogDescription>
95
+ </DialogHeader>
96
+ <form onSubmit={handleSubmit} className="space-y-6 pt-4">
97
+ {error && (
98
+ <div className="text-sm font-medium text-destructive bg-destructive/10 p-3 rounded-md border border-destructive/20">
99
+ {error}
100
+ </div>
101
+ )}
102
+
103
+ <div className="space-y-4">
104
+ <div className="grid gap-2">
105
+ <Label htmlFor="name" className="flex items-center gap-2">
106
+ <Type className="h-3.5 w-3.5 text-muted-foreground" />
107
+ Project Name
108
+ </Label>
109
+ <Input
110
+ id="name"
111
+ value={name}
112
+ onChange={(e) => setName(e.target.value)}
113
+ placeholder="e.g. Acme API"
114
+ required
115
+ disabled={isLoading}
116
+ className="bg-muted/30 border-muted"
117
+ />
118
+ </div>
119
+
120
+ <div className="grid gap-2">
121
+ <Label htmlFor="description" className="flex items-center gap-2">
122
+ <AlignLeft className="h-3.5 w-3.5 text-muted-foreground" />
123
+ Description <span className="text-xs text-muted-foreground font-normal">(Optional)</span>
124
+ </Label>
125
+ <Textarea
126
+ id="description"
127
+ value={description}
128
+ onChange={(e) => setDescription(e.target.value)}
129
+ className="resize-none min-h-[100px] bg-muted/30 border-muted"
130
+ disabled={isLoading}
131
+ />
132
+ </div>
133
+
134
+ <div className="grid gap-2">
135
+ <Label htmlFor="color" className="flex items-center gap-2">
136
+ <Palette className="h-3.5 w-3.5 text-muted-foreground" />
137
+ Identity Color
138
+ </Label>
139
+ <div className="flex items-center gap-4">
140
+ <Input
141
+ id="color"
142
+ type="color"
143
+ value={color}
144
+ onChange={(e) => setColor(e.target.value)}
145
+ className="w-14 h-11 p-1 rounded-md border-muted cursor-pointer"
146
+ disabled={isLoading}
147
+ />
148
+ <div className="flex-1 px-3 py-2 bg-muted/30 border border-muted rounded-md flex items-center justify-between">
149
+ <span className="text-sm font-mono uppercase font-semibold text-muted-foreground">
150
+ {color}
151
+ </span>
152
+ <div
153
+ className="w-4 h-4 rounded-full shadow-sm"
154
+ style={{ backgroundColor: color }}
155
+ />
156
+ </div>
157
+ </div>
158
+ </div>
159
+ </div>
160
+
161
+ <DialogFooter className="pt-2">
162
+ <Button
163
+ type="button"
164
+ variant="ghost"
165
+ onClick={() => setOpen(false)}
166
+ disabled={isLoading}
167
+ >
168
+ Cancel
169
+ </Button>
170
+ <Button type="submit" disabled={isLoading} className="px-8">
171
+ {isLoading ? "Updating..." : "Update Project"}
172
+ </Button>
173
+ </DialogFooter>
174
+ </form>
175
+ </DialogContent>
176
+ </Dialog>
177
+ );
178
+ }
@@ -0,0 +1,231 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { useQueryClient } from "@tanstack/react-query";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Input } from "@/components/ui/input";
7
+ import { Label } from "@/components/ui/label";
8
+ import { Textarea } from "@/components/ui/textarea";
9
+ import {
10
+ Dialog,
11
+ DialogContent,
12
+ DialogDescription,
13
+ DialogFooter,
14
+ DialogHeader,
15
+ DialogTitle,
16
+ DialogTrigger,
17
+ } from "@/components/ui/dialog";
18
+ import { Edit, Eye, EyeOff, Database, Key, Type, AlignLeft, ShieldCheck, Calendar } from "lucide-react";
19
+ import { useVariables } from "@/hooks/use-variables";
20
+ import { Checkbox } from "@/components/ui/checkbox";
21
+
22
+ interface EditVariableDialogProps {
23
+ variableId: string;
24
+ children?: React.ReactNode;
25
+ }
26
+
27
+ export default function EditVariableDialog({ variableId, children }: EditVariableDialogProps) {
28
+ const [open, setOpen] = useState(false);
29
+ const [key, setKey] = useState("");
30
+ const [value, setValue] = useState("");
31
+ const [description, setDescription] = useState("");
32
+ const [isSecret, setIsSecret] = useState(true);
33
+ const [expiresAt, setExpiresAt] = useState("");
34
+ const [showValue, setShowValue] = useState(false);
35
+ const [isLoading, setIsLoading] = useState(false);
36
+ const [error, setError] = useState("");
37
+ const { variables } = useVariables();
38
+ const queryClient = useQueryClient();
39
+
40
+ const variable = variables?.find(v => v.id === variableId);
41
+
42
+ useEffect(() => {
43
+ if (variable && open) {
44
+ setKey(variable.key);
45
+ setValue(variable.value);
46
+ setDescription(variable.description || "");
47
+ setIsSecret(variable.isSecret);
48
+ setExpiresAt(variable.expiresAt ? new Date(variable.expiresAt).toISOString().split('T')[0] : "");
49
+ setShowValue(false);
50
+ }
51
+ }, [variable, open]);
52
+
53
+ const handleSubmit = async (e: React.FormEvent) => {
54
+ e.preventDefault();
55
+ setError("");
56
+ setIsLoading(true);
57
+
58
+ try {
59
+ const response = await fetch('/api/variables', {
60
+ method: 'PUT',
61
+ headers: {
62
+ 'Content-Type': 'application/json',
63
+ },
64
+ body: JSON.stringify({ id: variableId, key, value, description, isSecret, expiresAt: expiresAt ? new Date(expiresAt).toISOString() : null }),
65
+ });
66
+
67
+ const data = await response.json();
68
+
69
+ if (response.ok) {
70
+ setOpen(false);
71
+ queryClient.invalidateQueries({ queryKey: ['variables'] });
72
+ } else {
73
+ setError(data.error || "Failed to update variable");
74
+ }
75
+ } catch (err) {
76
+ setError("An unexpected error occurred");
77
+ } finally {
78
+ setIsLoading(false);
79
+ }
80
+ };
81
+
82
+ return (
83
+ <Dialog open={open} onOpenChange={setOpen}>
84
+ <DialogTrigger asChild>
85
+ {children || (
86
+ <Button variant="ghost" size="sm" className="h-8 w-8 p-0">
87
+ <Edit className="h-4 w-4" />
88
+ </Button>
89
+ )}
90
+ </DialogTrigger>
91
+ <DialogContent className="sm:max-w-[500px]">
92
+ <DialogHeader>
93
+ <div className="flex items-center gap-2 mb-1">
94
+ <div className="p-1.5 bg-primary/10 rounded-md">
95
+ <Database className="h-5 w-5 text-primary" />
96
+ </div>
97
+ <DialogTitle className="text-xl">Edit Variable</DialogTitle>
98
+ </div>
99
+ <DialogDescription>
100
+ Update the key, value, or security settings for this variable.
101
+ </DialogDescription>
102
+ </DialogHeader>
103
+ <form onSubmit={handleSubmit} className="space-y-6 pt-4">
104
+ {error && (
105
+ <div className="text-sm font-medium text-destructive bg-destructive/10 p-3 rounded-md border border-destructive/20">
106
+ {error}
107
+ </div>
108
+ )}
109
+
110
+ <div className="space-y-4">
111
+ <div className="grid gap-2">
112
+ <Label htmlFor="key" className="flex items-center gap-2">
113
+ <Type className="h-3.5 w-3.5 text-muted-foreground" />
114
+ Variable Key
115
+ </Label>
116
+ <Input
117
+ id="key"
118
+ value={key}
119
+ onChange={(e) => setKey(e.target.value)}
120
+ placeholder="e.g. DATABASE_URL"
121
+ required
122
+ disabled={isLoading}
123
+ className="bg-muted/30 border-muted font-mono text-sm"
124
+ />
125
+ </div>
126
+
127
+ <div className="grid gap-2">
128
+ <div className="flex items-center justify-between">
129
+ <Label htmlFor="value" className="flex items-center gap-2">
130
+ <Key className="h-3.5 w-3.5 text-muted-foreground" />
131
+ Value
132
+ </Label>
133
+ {isSecret && (
134
+ <Button
135
+ type="button"
136
+ variant="ghost"
137
+ size="sm"
138
+ className="h-7 px-2 text-xs text-muted-foreground"
139
+ onClick={() => setShowValue(!showValue)}
140
+ disabled={isLoading}
141
+ >
142
+ {showValue ? <EyeOff className="h-3.5 w-3.5 mr-1.5" /> : <Eye className="h-3.5 w-3.5 mr-1.5" />}
143
+ {showValue ? "Hide" : "Show"}
144
+ </Button>
145
+ )}
146
+ </div>
147
+ <div className="relative">
148
+ <Input
149
+ id="value"
150
+ type={isSecret && !showValue ? "password" : "text"}
151
+ value={value}
152
+ onChange={(e) => setValue(e.target.value)}
153
+ placeholder="Enter variable value"
154
+ required
155
+ disabled={isLoading}
156
+ className="bg-muted/30 border-muted font-mono text-sm"
157
+ />
158
+ </div>
159
+ </div>
160
+
161
+ <div className="grid gap-2">
162
+ <Label htmlFor="description" className="flex items-center gap-2">
163
+ <AlignLeft className="h-3.5 w-3.5 text-muted-foreground" />
164
+ Description <span className="text-xs text-muted-foreground font-normal">(Optional)</span>
165
+ </Label>
166
+ <Input
167
+ id="description"
168
+ value={description}
169
+ onChange={(e) => setDescription(e.target.value)}
170
+ placeholder="What is this variable for?"
171
+ disabled={isLoading}
172
+ className="bg-muted/30 border-muted"
173
+ />
174
+ </div>
175
+
176
+ <div className="grid gap-2">
177
+ <Label htmlFor="expiresAt" className="flex items-center gap-2">
178
+ <Calendar className="h-3.5 w-3.5 text-muted-foreground" />
179
+ Expiration Date <span className="text-xs text-muted-foreground font-normal">(Optional)</span>
180
+ </Label>
181
+ <Input
182
+ id="expiresAt"
183
+ type="date"
184
+ value={expiresAt}
185
+ onChange={(e) => setExpiresAt(e.target.value)}
186
+ disabled={isLoading}
187
+ className="bg-muted/30 border-muted"
188
+ />
189
+ </div>
190
+
191
+ <div className="flex items-center space-x-3 p-3 bg-primary/5 rounded-lg border border-primary/10">
192
+ <Checkbox
193
+ id="isSecret"
194
+ checked={isSecret}
195
+ onCheckedChange={(checked) => setIsSecret(!!checked)}
196
+ disabled={isLoading}
197
+ className="data-[state=checked]:bg-primary"
198
+ />
199
+ <div className="grid gap-1.5 leading-none">
200
+ <Label
201
+ htmlFor="isSecret"
202
+ className="text-sm font-medium leading-none cursor-pointer flex items-center gap-2"
203
+ >
204
+ <ShieldCheck className="h-3.5 w-3.5 text-primary" />
205
+ Encrypt as Secret
206
+ </Label>
207
+ <p className="text-xs text-muted-foreground">
208
+ Secrets are encrypted at rest and masked in the UI by default.
209
+ </p>
210
+ </div>
211
+ </div>
212
+ </div>
213
+
214
+ <DialogFooter className="pt-2">
215
+ <Button
216
+ type="button"
217
+ variant="ghost"
218
+ onClick={() => setOpen(false)}
219
+ disabled={isLoading}
220
+ >
221
+ Cancel
222
+ </Button>
223
+ <Button type="submit" disabled={isLoading} className="px-8 shadow-sm">
224
+ {isLoading ? "Updating..." : "Update Variable"}
225
+ </Button>
226
+ </DialogFooter>
227
+ </form>
228
+ </DialogContent>
229
+ </Dialog>
230
+ );
231
+ }