Skip to content

Commit 2ed9954

Browse files
authored
Merge pull request #72 from zecrypt-io/preview
env
2 parents 345a493 + 4683911 commit 2ed9954

12 files changed

Lines changed: 2145 additions & 14 deletions

File tree

Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
"use client";
2+
3+
import { useState, useEffect, useMemo, useRef } from "react";
4+
import { useSelector } from "react-redux";
5+
import { RootState } from "@/libs/Redux/store";
6+
import { Button } from "@/components/ui/button";
7+
import { Input } from "@/components/ui/input";
8+
import { Label } from "@/components/ui/label";
9+
import { Eye, EyeOff, X, Plus, AlertCircle, Code } from "lucide-react";
10+
import { Badge } from "@/components/ui/badge";
11+
import { Textarea } from "@/components/ui/textarea";
12+
import { toast } from "@/components/ui/use-toast";
13+
import { useTranslator } from "@/hooks/use-translations";
14+
import axiosInstance from "@/libs/Middleware/axiosInstace";
15+
import { encryptDataField } from "@/libs/encryption";
16+
import { secureGetItem, decryptFromLocalStorage } from "@/libs/local-storage-utils";
17+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
18+
import { EnvCodeEditor } from "@/components/ui/env-code-editor";
19+
20+
interface AddEnvDialogProps {
21+
open: boolean;
22+
onOpenChange: (open: boolean) => void;
23+
onEnvAdded: () => void;
24+
}
25+
26+
export function AddEnvDialog({ open, onOpenChange, onEnvAdded }: AddEnvDialogProps) {
27+
const { translate } = useTranslator();
28+
const [title, setTitle] = useState("");
29+
const [data, setData] = useState("");
30+
const [notes, setNotes] = useState("");
31+
const [tags, setTags] = useState<string[]>([]);
32+
const [newTag, setNewTag] = useState("");
33+
const [error, setError] = useState("");
34+
const [isSubmitting, setIsSubmitting] = useState(false);
35+
const [projectKey, setProjectKey] = useState<string | null>(null);
36+
const keyLoadAttemptedRef = useRef(false);
37+
const [codeEditorOpen, setCodeEditorOpen] = useState(false);
38+
39+
const selectedWorkspaceId = useSelector((state: RootState) => state.workspace.selectedWorkspaceId);
40+
const selectedProjectId = useSelector((state: RootState) => state.workspace.selectedProjectId);
41+
const workspaces = useSelector((state: RootState) => state.workspace.workspaces);
42+
const selectedProjectName = useMemo(() => {
43+
if (!workspaces || !selectedWorkspaceId || !selectedProjectId) return null;
44+
const workspace = workspaces.find(w => w.workspaceId === selectedWorkspaceId);
45+
if (!workspace) return null;
46+
const project = workspace.projects.find(p => p.project_id === selectedProjectId);
47+
return project?.name || null;
48+
}, [workspaces, selectedWorkspaceId, selectedProjectId]);
49+
50+
// Reset state when dialog opens
51+
useEffect(() => {
52+
if (open) {
53+
setTitle("");
54+
setData("");
55+
setNotes("");
56+
setTags([]);
57+
setNewTag("");
58+
setError("");
59+
keyLoadAttemptedRef.current = false;
60+
}
61+
}, [open]);
62+
63+
// Load the project key when the component opens or when project changes
64+
useEffect(() => {
65+
let isMounted = true;
66+
67+
const loadProjectKey = async () => {
68+
if (!open || !selectedProjectName || keyLoadAttemptedRef.current) {
69+
return;
70+
}
71+
72+
keyLoadAttemptedRef.current = true;
73+
74+
try {
75+
// Try session storage first (faster)
76+
const sessionKey = sessionStorage.getItem(`projectKey_${selectedProjectName}`);
77+
let key = null;
78+
79+
if (sessionKey) {
80+
key = sessionKey;
81+
} else {
82+
// If not in session storage, try secure storage
83+
key = await secureGetItem(`projectKey_${selectedProjectName}`);
84+
85+
// Cache in session storage for faster access
86+
if (key) {
87+
sessionStorage.setItem(`projectKey_${selectedProjectName}`, key);
88+
}
89+
}
90+
91+
if (isMounted) {
92+
setProjectKey(key);
93+
}
94+
} catch (error) {
95+
console.error("Error loading project key:", error);
96+
if (isMounted) {
97+
setProjectKey(null);
98+
}
99+
}
100+
};
101+
102+
loadProjectKey();
103+
104+
return () => {
105+
isMounted = false;
106+
};
107+
}, [open, selectedProjectName]);
108+
109+
// Safely get translation with fallback
110+
const safeTranslate = (key: string, namespace: string, options?: any) => {
111+
try {
112+
return translate(key, namespace, options);
113+
} catch (error) {
114+
return options?.default || key;
115+
}
116+
};
117+
118+
const addTag = (tag: string) => {
119+
const normalizedTag = tag.toLowerCase().trim();
120+
if (normalizedTag && !tags.includes(normalizedTag)) {
121+
setTags([...tags, normalizedTag]);
122+
setNewTag("");
123+
}
124+
};
125+
126+
const removeTag = (tag: string) => {
127+
setTags(tags.filter((t) => t !== tag));
128+
};
129+
130+
const handleSubmit = async () => {
131+
if (!title || !data) {
132+
setError(safeTranslate("please_fill_all_required_fields", "env", { default: "Please fill all required fields" }));
133+
return;
134+
}
135+
136+
if (!selectedWorkspaceId || !selectedProjectId) {
137+
setError(safeTranslate("no_project_selected", "env", { default: "No project selected" }));
138+
return;
139+
}
140+
141+
setIsSubmitting(true);
142+
setError("");
143+
144+
try {
145+
// Use the project key we already have in state if available
146+
let effectiveProjectKey = projectKey;
147+
148+
// If not in state, try session storage (faster)
149+
if (!effectiveProjectKey && selectedProjectName) {
150+
const sessionKey = sessionStorage.getItem(`projectKey_${selectedProjectName}`);
151+
if (sessionKey) {
152+
effectiveProjectKey = sessionKey;
153+
} else {
154+
// Last resort: try localStorage
155+
try {
156+
const rawProjectKey = localStorage.getItem(`projectKey_${selectedProjectName}`);
157+
if (rawProjectKey) {
158+
effectiveProjectKey = await decryptFromLocalStorage(rawProjectKey);
159+
// Cache for future use
160+
if (effectiveProjectKey) {
161+
sessionStorage.setItem(`projectKey_${selectedProjectName}`, effectiveProjectKey);
162+
}
163+
}
164+
} catch (error) {
165+
console.error("Failed to get project key:", error);
166+
}
167+
}
168+
}
169+
170+
let processedData = data;
171+
if (effectiveProjectKey) {
172+
try {
173+
// Encrypt the environment variables data
174+
processedData = await encryptDataField(data, effectiveProjectKey);
175+
} catch (encryptError) {
176+
console.error("Encryption failed:", encryptError);
177+
processedData = data; // Fallback to unencrypted data
178+
}
179+
}
180+
181+
const payload = {
182+
title,
183+
data: processedData,
184+
notes: notes || null,
185+
tags,
186+
};
187+
188+
const response = await axiosInstance.post(
189+
`/${selectedWorkspaceId}/${selectedProjectId}/env`,
190+
payload
191+
);
192+
193+
if (response.status === 200 || response.status === 201 || (response.data && (response.data.status_code === 200 || response.data.status_code === 201))) {
194+
onEnvAdded();
195+
onOpenChange(false);
196+
toast({
197+
title: safeTranslate("env_added_successfully", "env", { default: "Environment variables added successfully" }),
198+
description: safeTranslate("env_added_description", "env", { default: "The environment variables have been added." }),
199+
});
200+
201+
// Reset form
202+
setTitle("");
203+
setData("");
204+
setNotes("");
205+
setTags([]);
206+
setNewTag("");
207+
setError("");
208+
} else {
209+
throw new Error(response.data?.message || safeTranslate("failed_to_add_env", "env", { default: "Failed to add environment variables" }));
210+
}
211+
} catch (error: any) {
212+
console.error("Error adding environment variables:", error);
213+
if (error.response?.status === 400 && error.response.data?.message === "Environment variables already exists") {
214+
setError(safeTranslate("env_already_exists", "env", { default: "Environment variables already exists" }));
215+
} else if (error.response?.status === 422) {
216+
setError(safeTranslate("invalid_input_data", "env", { default: "Invalid input data" }));
217+
} else {
218+
setError(error.response?.data?.message || safeTranslate("failed_to_add_env", "env", { default: "Failed to add environment variables" }));
219+
}
220+
} finally {
221+
setIsSubmitting(false);
222+
}
223+
};
224+
225+
return (
226+
<>
227+
<Dialog open={open} onOpenChange={onOpenChange}>
228+
<DialogContent className="sm:max-w-md max-h-[90vh] overflow-y-auto">
229+
<DialogHeader>
230+
<DialogTitle>{safeTranslate("add_new_env", "env", { default: "Add New Environment Variables" })}</DialogTitle>
231+
<DialogDescription>
232+
{safeTranslate("add_new_env_description", "env", { default: "Enter your environment variables details below" })}
233+
</DialogDescription>
234+
</DialogHeader>
235+
<div className="space-y-4 py-4">
236+
{error && (
237+
<div className="p-2 bg-red-50 border border-red-200 rounded-md flex items-center gap-2 text-red-600">
238+
<AlertCircle className="h-4 w-4 flex-shrink-0" />
239+
<p className="text-sm">{error}</p>
240+
</div>
241+
)}
242+
<div className="space-y-2">
243+
<Label htmlFor="title">
244+
{safeTranslate("env_name", "env", { default: "Environment Name" })}
245+
<span className="text-red-500">*</span>
246+
</Label>
247+
<Input
248+
id="title"
249+
placeholder={safeTranslate("enter_env_name", "env", { default: "Enter environment name" })}
250+
value={title}
251+
onChange={(e) => setTitle(e.target.value)}
252+
className={error && !title ? "border-red-500" : ""}
253+
/>
254+
</div>
255+
256+
<div className="space-y-2">
257+
<Label htmlFor="data">
258+
{safeTranslate("env_variables", "env", { default: "Environment Variables" })}
259+
<span className="text-red-500">*</span>
260+
</Label>
261+
<div className="relative">
262+
<div
263+
className={`min-h-[150px] border rounded-md p-4 cursor-pointer flex items-center justify-center bg-muted/20 ${error && !data ? "border-red-500" : ""}`}
264+
onClick={() => setCodeEditorOpen(true)}
265+
>
266+
<div className="flex flex-col items-center gap-2 text-muted-foreground">
267+
<Code className="h-6 w-6" />
268+
<p>
269+
{data ?
270+
safeTranslate("click_to_edit_env", "env", { default: "Click to edit environment variables" }) :
271+
safeTranslate("click_to_add_env", "env", { default: "Click to add environment variables" })
272+
}
273+
</p>
274+
</div>
275+
</div>
276+
</div>
277+
<p className="text-xs text-muted-foreground">
278+
{safeTranslate("env_format_hint", "env", { default: "Enter your environment variables in KEY=value format, one per line" })}
279+
</p>
280+
</div>
281+
282+
<div className="space-y-2">
283+
<Label htmlFor="notes">{safeTranslate("notes", "env", { default: "Notes" })}</Label>
284+
<Textarea
285+
id="notes"
286+
placeholder={safeTranslate("enter_notes", "env", { default: "Enter notes" })}
287+
value={notes}
288+
onChange={(e) => setNotes(e.target.value)}
289+
className="min-h-[100px]"
290+
/>
291+
</div>
292+
293+
<div className="space-y-2">
294+
<Label htmlFor="tags">{safeTranslate("tags", "env", { default: "Tags" })}</Label>
295+
<div className="flex items-center space-x-2">
296+
<Input
297+
id="tags"
298+
placeholder={safeTranslate("add_tag", "env", { default: "Add tag" })}
299+
value={newTag}
300+
onChange={(e) => setNewTag(e.target.value)}
301+
onKeyDown={(e) => {
302+
if (e.key === "Enter") {
303+
e.preventDefault();
304+
addTag(newTag);
305+
}
306+
}}
307+
className="w-full"
308+
/>
309+
</div>
310+
<p className="text-xs text-muted-foreground">
311+
{safeTranslate("press_enter_to_add", "env", { default: "Press Enter to add a tag" })}
312+
</p>
313+
<div className="flex flex-wrap gap-2 mt-2">
314+
{tags.map((tag) => (
315+
<Badge key={tag} variant="secondary" className="flex items-center gap-1">
316+
{tag}
317+
<Button
318+
type="button"
319+
variant="ghost"
320+
size="icon"
321+
className="h-4 w-4 p-0 hover:bg-transparent"
322+
onClick={() => removeTag(tag)}
323+
>
324+
<X className="h-3 w-3" />
325+
</Button>
326+
</Badge>
327+
))}
328+
</div>
329+
</div>
330+
</div>
331+
<DialogFooter>
332+
<Button
333+
type="button"
334+
variant="outline"
335+
onClick={() => onOpenChange(false)}
336+
disabled={isSubmitting}
337+
>
338+
{safeTranslate("cancel", "actions", { default: "Cancel" })}
339+
</Button>
340+
<Button type="button" onClick={handleSubmit} disabled={isSubmitting}>
341+
{isSubmitting
342+
? safeTranslate("saving", "actions", { default: "Saving..." })
343+
: safeTranslate("save", "actions", { default: "Save" })}
344+
</Button>
345+
</DialogFooter>
346+
</DialogContent>
347+
</Dialog>
348+
349+
<EnvCodeEditor
350+
value={data}
351+
onChange={setData}
352+
open={codeEditorOpen}
353+
onOpenChange={setCodeEditorOpen}
354+
title={safeTranslate("add_new_env", "env", { default: "Add New Environment Variables" })}
355+
/>
356+
</>
357+
);
358+
}

0 commit comments

Comments
 (0)