Skip to content

Commit cdcc0bf

Browse files
Merge pull request #5 from GreenHacker420/feature/import-env-files
feat: Add Import Functionality for Environment Variables
2 parents 5e6e6e4 + e036a0e commit cdcc0bf

File tree

4 files changed

+337
-1
lines changed

4 files changed

+337
-1
lines changed

electron/ipc-handlers.js

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,149 @@ export function setupIpcHandlers() {
301301
}
302302
});
303303

304+
ipcMain.handle('envvars:import', async (event, { projectId, content, format, overwrite = false }) => {
305+
try {
306+
if (!masterKey) {
307+
return { success: false, error: 'Not authenticated' };
308+
}
309+
310+
let parsedVars = [];
311+
312+
if (format === 'env') {
313+
// Parse .env format
314+
const lines = content.split('\n');
315+
let currentComment = '';
316+
317+
for (const line of lines) {
318+
const trimmed = line.trim();
319+
320+
// Skip empty lines
321+
if (!trimmed) {
322+
currentComment = '';
323+
continue;
324+
}
325+
326+
// Capture comments
327+
if (trimmed.startsWith('#')) {
328+
currentComment = trimmed.substring(1).trim();
329+
continue;
330+
}
331+
332+
// Parse key=value
333+
const equalIndex = trimmed.indexOf('=');
334+
if (equalIndex > 0) {
335+
const key = trimmed.substring(0, equalIndex).trim();
336+
let value = trimmed.substring(equalIndex + 1).trim();
337+
338+
// Remove quotes if present
339+
if ((value.startsWith('"') && value.endsWith('"')) ||
340+
(value.startsWith("'") && value.endsWith("'"))) {
341+
value = value.substring(1, value.length - 1);
342+
}
343+
344+
parsedVars.push({
345+
key,
346+
value,
347+
description: currentComment || null,
348+
});
349+
currentComment = '';
350+
}
351+
}
352+
} else if (format === 'json') {
353+
// Parse JSON format
354+
const obj = JSON.parse(content);
355+
parsedVars = Object.entries(obj).map(([key, value]) => ({
356+
key,
357+
value: String(value),
358+
description: null,
359+
}));
360+
}
361+
362+
if (parsedVars.length === 0) {
363+
return { success: false, error: 'No valid environment variables found' };
364+
}
365+
366+
// Get existing keys
367+
const existingVars = await prisma.envVar.findMany({
368+
where: { projectId },
369+
select: { key: true, id: true },
370+
});
371+
372+
const existingKeys = new Map(existingVars.map(v => [v.key, v.id]));
373+
374+
let imported = 0;
375+
let updated = 0;
376+
let skipped = 0;
377+
const errors = [];
378+
379+
for (const { key, value, description } of parsedVars) {
380+
try {
381+
const encryptedValue = encrypt(value, masterKey);
382+
383+
if (existingKeys.has(key)) {
384+
if (overwrite) {
385+
await prisma.envVar.update({
386+
where: { id: existingKeys.get(key) },
387+
data: {
388+
encryptedValue,
389+
description: description || undefined,
390+
},
391+
});
392+
393+
await prisma.auditLog.create({
394+
data: {
395+
action: 'UPDATE',
396+
entityType: 'ENVVAR',
397+
entityId: existingKeys.get(key),
398+
details: `Updated env var during import: ${key}`,
399+
},
400+
});
401+
402+
updated++;
403+
} else {
404+
skipped++;
405+
}
406+
} else {
407+
const envVar = await prisma.envVar.create({
408+
data: {
409+
projectId,
410+
key,
411+
encryptedValue,
412+
description,
413+
},
414+
});
415+
416+
await prisma.auditLog.create({
417+
data: {
418+
action: 'CREATE',
419+
entityType: 'ENVVAR',
420+
entityId: envVar.id,
421+
details: `Imported env var: ${key}`,
422+
},
423+
});
424+
425+
imported++;
426+
}
427+
} catch (error) {
428+
errors.push(`Failed to import ${key}: ${error.message}`);
429+
}
430+
}
431+
432+
return {
433+
success: true,
434+
data: {
435+
imported,
436+
updated,
437+
skipped,
438+
total: parsedVars.length,
439+
errors: errors.length > 0 ? errors : undefined,
440+
}
441+
};
442+
} catch (error) {
443+
return { success: false, error: error.message };
444+
}
445+
});
446+
304447
// Audit log handlers
305448
ipcMain.handle('audit:list', async (event, { limit = 50 }) => {
306449
try {

electron/preload.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
2727
update: (data) => ipcRenderer.invoke('envvars:update', data),
2828
delete: (id) => ipcRenderer.invoke('envvars:delete', id),
2929
export: (data) => ipcRenderer.invoke('envvars:export', data),
30+
import: (data) => ipcRenderer.invoke('envvars:import', data),
3031
},
3132

3233
// Audit Logs

src/components/ImportEnvModal.jsx

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { useState } from 'react';
2+
import { Modal, Input, Select, Switch, message, Alert } from 'antd';
3+
import { Upload } from 'lucide-react';
4+
5+
const { TextArea } = Input;
6+
const { Option } = Select;
7+
8+
export default function ImportEnvModal({ open, onClose, projectId, onSuccess }) {
9+
const [content, setContent] = useState('');
10+
const [format, setFormat] = useState('env');
11+
const [overwrite, setOverwrite] = useState(false);
12+
const [loading, setLoading] = useState(false);
13+
14+
const handleFileUpload = (e) => {
15+
const file = e.target.files[0];
16+
if (!file) return;
17+
18+
const reader = new FileReader();
19+
reader.onload = (event) => {
20+
setContent(event.target.result);
21+
22+
// Auto-detect format
23+
if (file.name.endsWith('.json')) {
24+
setFormat('json');
25+
} else {
26+
setFormat('env');
27+
}
28+
};
29+
reader.readAsText(file);
30+
};
31+
32+
const handleImport = async () => {
33+
if (!content.trim()) {
34+
message.error('Please provide content to import');
35+
return;
36+
}
37+
38+
setLoading(true);
39+
try {
40+
const result = await window.electronAPI.envVars.import({
41+
projectId,
42+
content,
43+
format,
44+
overwrite,
45+
});
46+
47+
if (result.success) {
48+
const { imported, updated, skipped, total, errors } = result.data;
49+
50+
let messageText = `Import complete: ${imported} imported`;
51+
if (updated > 0) messageText += `, ${updated} updated`;
52+
if (skipped > 0) messageText += `, ${skipped} skipped`;
53+
54+
message.success(messageText);
55+
56+
if (errors && errors.length > 0) {
57+
console.error('Import errors:', errors);
58+
message.warning(`${errors.length} variables had errors`);
59+
}
60+
61+
onSuccess();
62+
handleClose();
63+
} else {
64+
message.error(result.error || 'Failed to import');
65+
}
66+
} catch (error) {
67+
message.error('Failed to import: ' + error.message);
68+
} finally {
69+
setLoading(false);
70+
}
71+
};
72+
73+
const handleClose = () => {
74+
setContent('');
75+
setFormat('env');
76+
setOverwrite(false);
77+
onClose();
78+
};
79+
80+
return (
81+
<Modal
82+
title={
83+
<div className="flex items-center gap-2">
84+
<Upload className="w-5 h-5" />
85+
<span>Import Environment Variables</span>
86+
</div>
87+
}
88+
open={open}
89+
onCancel={handleClose}
90+
onOk={handleImport}
91+
okText="Import"
92+
confirmLoading={loading}
93+
width={600}
94+
destroyOnClose
95+
>
96+
<div className="space-y-4">
97+
<Alert
98+
message="Import your existing .env or JSON files"
99+
description="Paste the content below or upload a file. Comments in .env files will be preserved as descriptions."
100+
type="info"
101+
showIcon
102+
/>
103+
104+
<div>
105+
<label className="block text-sm font-medium mb-2">
106+
Upload File (Optional)
107+
</label>
108+
<input
109+
type="file"
110+
accept=".env,.json,.txt"
111+
onChange={handleFileUpload}
112+
className="block w-full text-sm text-gray-400
113+
file:mr-4 file:py-2 file:px-4
114+
file:rounded-md file:border-0
115+
file:text-sm file:font-semibold
116+
file:bg-blue-600 file:text-white
117+
hover:file:bg-blue-700
118+
file:cursor-pointer cursor-pointer"
119+
/>
120+
</div>
121+
122+
<div>
123+
<label className="block text-sm font-medium mb-2">Format</label>
124+
<Select
125+
value={format}
126+
onChange={setFormat}
127+
className="w-full"
128+
>
129+
<Option value="env">.env format (KEY=value)</Option>
130+
<Option value="json">JSON format</Option>
131+
</Select>
132+
</div>
133+
134+
<div>
135+
<label className="block text-sm font-medium mb-2">Content</label>
136+
<TextArea
137+
value={content}
138+
onChange={(e) => setContent(e.target.value)}
139+
placeholder={
140+
format === 'env'
141+
? '# Database configuration\nDB_HOST=localhost\nDB_PORT=5432\nDB_NAME=myapp'
142+
: '{\n "DB_HOST": "localhost",\n "DB_PORT": "5432",\n "DB_NAME": "myapp"\n}'
143+
}
144+
rows={10}
145+
className="font-mono text-sm"
146+
/>
147+
</div>
148+
149+
<div className="flex items-center justify-between p-3 bg-gray-800 rounded-lg">
150+
<div>
151+
<div className="font-medium">Overwrite existing variables</div>
152+
<div className="text-sm text-gray-400">
153+
Update values if keys already exist
154+
</div>
155+
</div>
156+
<Switch
157+
checked={overwrite}
158+
onChange={setOverwrite}
159+
/>
160+
</div>
161+
162+
{format === 'env' && (
163+
<Alert
164+
message="Tip"
165+
description="Comments above variables (lines starting with #) will be imported as descriptions."
166+
type="success"
167+
showIcon
168+
/>
169+
)}
170+
</div>
171+
</Modal>
172+
);
173+
}

src/components/ProjectView.jsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,19 @@ import {
88
EyeOutlined,
99
EyeInvisibleOutlined,
1010
CopyOutlined,
11-
CheckOutlined
11+
CheckOutlined,
12+
UploadOutlined
1213
} from '@ant-design/icons';
1314
import EnvVarModal from './EnvVarModal';
15+
import ImportEnvModal from './ImportEnvModal';
1416

1517
const { Title, Text } = Typography;
1618

1719
export default function ProjectView({ project, onProjectUpdate }) {
1820
const [envVars, setEnvVars] = useState([]);
1921
const [loading, setLoading] = useState(true);
2022
const [showModal, setShowModal] = useState(false);
23+
const [showImportModal, setShowImportModal] = useState(false);
2124
const [editingVar, setEditingVar] = useState(null);
2225
const [visibleValues, setVisibleValues] = useState({});
2326
const [copiedId, setCopiedId] = useState(null);
@@ -221,6 +224,12 @@ export default function ProjectView({ project, onProjectUpdate }) {
221224
)}
222225
</div>
223226
<Space>
227+
<Button
228+
icon={<UploadOutlined />}
229+
onClick={() => setShowImportModal(true)}
230+
>
231+
Import
232+
</Button>
224233
<Button
225234
icon={<DownloadOutlined />}
226235
onClick={() => handleExport('env')}
@@ -289,6 +298,16 @@ export default function ProjectView({ project, onProjectUpdate }) {
289298
onUpdate={handleUpdate}
290299
editingVar={editingVar}
291300
/>
301+
302+
<ImportEnvModal
303+
open={showImportModal}
304+
onClose={() => setShowImportModal(false)}
305+
projectId={project.id}
306+
onSuccess={async () => {
307+
await loadEnvVars();
308+
await onProjectUpdate();
309+
}}
310+
/>
292311
</div>
293312
);
294313
}

0 commit comments

Comments
 (0)