diff --git a/electron/ipc-handlers.js b/electron/ipc-handlers.js index 9b92c1f..a3b4555 100644 --- a/electron/ipc-handlers.js +++ b/electron/ipc-handlers.js @@ -301,6 +301,149 @@ export function setupIpcHandlers() { } }); + ipcMain.handle('envvars:import', async (event, { projectId, content, format, overwrite = false }) => { + try { + if (!masterKey) { + return { success: false, error: 'Not authenticated' }; + } + + let parsedVars = []; + + if (format === 'env') { + // Parse .env format + const lines = content.split('\n'); + let currentComment = ''; + + for (const line of lines) { + const trimmed = line.trim(); + + // Skip empty lines + if (!trimmed) { + currentComment = ''; + continue; + } + + // Capture comments + if (trimmed.startsWith('#')) { + currentComment = trimmed.substring(1).trim(); + continue; + } + + // Parse key=value + const equalIndex = trimmed.indexOf('='); + if (equalIndex > 0) { + const key = trimmed.substring(0, equalIndex).trim(); + let value = trimmed.substring(equalIndex + 1).trim(); + + // Remove quotes if present + if ((value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'"))) { + value = value.substring(1, value.length - 1); + } + + parsedVars.push({ + key, + value, + description: currentComment || null, + }); + currentComment = ''; + } + } + } else if (format === 'json') { + // Parse JSON format + const obj = JSON.parse(content); + parsedVars = Object.entries(obj).map(([key, value]) => ({ + key, + value: String(value), + description: null, + })); + } + + if (parsedVars.length === 0) { + return { success: false, error: 'No valid environment variables found' }; + } + + // Get existing keys + const existingVars = await prisma.envVar.findMany({ + where: { projectId }, + select: { key: true, id: true }, + }); + + const existingKeys = new Map(existingVars.map(v => [v.key, v.id])); + + let imported = 0; + let updated = 0; + let skipped = 0; + const errors = []; + + for (const { key, value, description } of parsedVars) { + try { + const encryptedValue = encrypt(value, masterKey); + + if (existingKeys.has(key)) { + if (overwrite) { + await prisma.envVar.update({ + where: { id: existingKeys.get(key) }, + data: { + encryptedValue, + description: description || undefined, + }, + }); + + await prisma.auditLog.create({ + data: { + action: 'UPDATE', + entityType: 'ENVVAR', + entityId: existingKeys.get(key), + details: `Updated env var during import: ${key}`, + }, + }); + + updated++; + } else { + skipped++; + } + } else { + const envVar = await prisma.envVar.create({ + data: { + projectId, + key, + encryptedValue, + description, + }, + }); + + await prisma.auditLog.create({ + data: { + action: 'CREATE', + entityType: 'ENVVAR', + entityId: envVar.id, + details: `Imported env var: ${key}`, + }, + }); + + imported++; + } + } catch (error) { + errors.push(`Failed to import ${key}: ${error.message}`); + } + } + + return { + success: true, + data: { + imported, + updated, + skipped, + total: parsedVars.length, + errors: errors.length > 0 ? errors : undefined, + } + }; + } catch (error) { + return { success: false, error: error.message }; + } + }); + // Audit log handlers ipcMain.handle('audit:list', async (event, { limit = 50 }) => { try { diff --git a/electron/preload.js b/electron/preload.js index d477891..cf16795 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -27,6 +27,7 @@ contextBridge.exposeInMainWorld('electronAPI', { update: (data) => ipcRenderer.invoke('envvars:update', data), delete: (id) => ipcRenderer.invoke('envvars:delete', id), export: (data) => ipcRenderer.invoke('envvars:export', data), + import: (data) => ipcRenderer.invoke('envvars:import', data), }, // Audit Logs diff --git a/src/components/ImportEnvModal.jsx b/src/components/ImportEnvModal.jsx new file mode 100644 index 0000000..6370e4a --- /dev/null +++ b/src/components/ImportEnvModal.jsx @@ -0,0 +1,173 @@ +import { useState } from 'react'; +import { Modal, Input, Select, Switch, message, Alert } from 'antd'; +import { Upload } from 'lucide-react'; + +const { TextArea } = Input; +const { Option } = Select; + +export default function ImportEnvModal({ open, onClose, projectId, onSuccess }) { + const [content, setContent] = useState(''); + const [format, setFormat] = useState('env'); + const [overwrite, setOverwrite] = useState(false); + const [loading, setLoading] = useState(false); + + const handleFileUpload = (e) => { + const file = e.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (event) => { + setContent(event.target.result); + + // Auto-detect format + if (file.name.endsWith('.json')) { + setFormat('json'); + } else { + setFormat('env'); + } + }; + reader.readAsText(file); + }; + + const handleImport = async () => { + if (!content.trim()) { + message.error('Please provide content to import'); + return; + } + + setLoading(true); + try { + const result = await window.electronAPI.envVars.import({ + projectId, + content, + format, + overwrite, + }); + + if (result.success) { + const { imported, updated, skipped, total, errors } = result.data; + + let messageText = `Import complete: ${imported} imported`; + if (updated > 0) messageText += `, ${updated} updated`; + if (skipped > 0) messageText += `, ${skipped} skipped`; + + message.success(messageText); + + if (errors && errors.length > 0) { + console.error('Import errors:', errors); + message.warning(`${errors.length} variables had errors`); + } + + onSuccess(); + handleClose(); + } else { + message.error(result.error || 'Failed to import'); + } + } catch (error) { + message.error('Failed to import: ' + error.message); + } finally { + setLoading(false); + } + }; + + const handleClose = () => { + setContent(''); + setFormat('env'); + setOverwrite(false); + onClose(); + }; + + return ( + + + Import Environment Variables + + } + open={open} + onCancel={handleClose} + onOk={handleImport} + okText="Import" + confirmLoading={loading} + width={600} + destroyOnClose + > +
+ + +
+ + +
+ +
+ + +
+ +
+ +