Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions electron/ipc-handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,117 @@ export function setupIpcHandlers() {
}
});

ipcMain.handle('envvars:copy-to-project', async (event, { sourceProjectId, targetProjectId, envVarIds, overwrite = false }) => {
try {
if (!masterKey) {
return { success: false, error: 'Not authenticated' };
}

// Validate projects exist
const [sourceProject, targetProject] = await Promise.all([
prisma.project.findUnique({ where: { id: sourceProjectId } }),
prisma.project.findUnique({ where: { id: targetProjectId } }),
]);

if (!sourceProject || !targetProject) {
return { success: false, error: 'Source or target project not found' };
}

// Get variables to copy
const varsToCopy = await prisma.envVar.findMany({
where: {
id: { in: envVarIds },
projectId: sourceProjectId,
},
});

if (varsToCopy.length === 0) {
return { success: false, error: 'No variables found to copy' };
}

// Get existing keys in target project
const existingVars = await prisma.envVar.findMany({
where: { projectId: targetProjectId },
select: { key: true, id: true },
});

const existingKeys = new Map(existingVars.map(v => [v.key, v.id]));

let copied = 0;
let updated = 0;
let skipped = 0;
const errors = [];

for (const envVar of varsToCopy) {
try {
// Decrypt and re-encrypt (in case keys are different, though they're not in this app)
const value = decrypt(envVar.encryptedValue, masterKey);
const encryptedValue = encrypt(value, masterKey);
Comment on lines +347 to +349
Copy link

Copilot AI Oct 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary decrypt/encrypt cycle when using the same master key. Since the comment acknowledges keys are the same, you can directly copy the encryptedValue without decryption/re-encryption to improve performance.

Suggested change
// Decrypt and re-encrypt (in case keys are different, though they're not in this app)
const value = decrypt(envVar.encryptedValue, masterKey);
const encryptedValue = encrypt(value, masterKey);
// Keys are the same, so we can directly copy the encryptedValue for better performance
const encryptedValue = envVar.encryptedValue;

Copilot uses AI. Check for mistakes.

if (existingKeys.has(envVar.key)) {
if (overwrite) {
await prisma.envVar.update({
where: { id: existingKeys.get(envVar.key) },
data: {
encryptedValue,
description: envVar.description,
},
});

await prisma.auditLog.create({
data: {
action: 'UPDATE',
entityType: 'ENVVAR',
entityId: existingKeys.get(envVar.key),
details: `Updated env var during copy from ${sourceProject.name}: ${envVar.key}`,
},
});

updated++;
} else {
skipped++;
}
} else {
const newVar = await prisma.envVar.create({
data: {
projectId: targetProjectId,
key: envVar.key,
encryptedValue,
description: envVar.description,
},
});

await prisma.auditLog.create({
data: {
action: 'CREATE',
entityType: 'ENVVAR',
entityId: newVar.id,
details: `Copied env var from ${sourceProject.name}: ${envVar.key}`,
},
});

copied++;
}
} catch (error) {
errors.push(`Failed to copy ${envVar.key}: ${error.message}`);
}
}

return {
success: true,
data: {
copied,
updated,
skipped,
total: varsToCopy.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 {
Expand Down
1 change: 1 addition & 0 deletions electron/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
copyToProject: (data) => ipcRenderer.invoke('envvars:copy-to-project', data),
},

// Audit Logs
Expand Down
190 changes: 190 additions & 0 deletions src/components/CopyToProjectModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { useState, useEffect } from 'react';
import { Modal, Select, Switch, message, Alert, Tag, Space } from 'antd';
import { Copy } from 'lucide-react';
Copy link

Copilot AI Oct 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mixed icon libraries usage. The component uses Ant Design icons elsewhere in the codebase (like SwapOutlined in ProjectView), but imports Copy from lucide-react. Consider using CopyOutlined from @ant-design/icons for consistency.

Copilot uses AI. Check for mistakes.

const { Option } = Select;

export default function CopyToProjectModal({
open,
onClose,
sourceProject,
selectedVarIds,
allProjects,
onSuccess
}) {
const [targetProjectId, setTargetProjectId] = useState(null);
const [overwrite, setOverwrite] = useState(false);
const [loading, setLoading] = useState(false);

useEffect(() => {
if (open) {
setTargetProjectId(null);
setOverwrite(false);
}
}, [open]);

const handleCopy = async () => {
if (!targetProjectId) {
message.error('Please select a target project');
return;
}

if (selectedVarIds.length === 0) {
message.error('No variables selected to copy');
return;
}

setLoading(true);
try {
const result = await window.electronAPI.envVars.copyToProject({
sourceProjectId: sourceProject.id,
targetProjectId,
envVarIds: selectedVarIds,
overwrite,
});

if (result.success) {
const { copied, updated, skipped, errors } = result.data;

let messageText = `Copy complete: ${copied} copied`;
if (updated > 0) messageText += `, ${updated} updated`;
if (skipped > 0) messageText += `, ${skipped} skipped`;

message.success(messageText);

if (errors && errors.length > 0) {
console.error('Copy errors:', errors);
message.warning(`${errors.length} variables had errors`);
}

onSuccess();
onClose();
} else {
message.error(result.error || 'Failed to copy variables');
}
} catch (error) {
message.error('Failed to copy: ' + error.message);
} finally {
setLoading(false);
}
};

// Filter out the source project from target options
const availableProjects = allProjects.filter(p => p.id !== sourceProject.id);
const targetProject = availableProjects.find(p => p.id === targetProjectId);

return (
<Modal
title={
<div className="flex items-center gap-2">
<Copy className="w-5 h-5" />
<span>Copy Variables to Another Project</span>
</div>
Comment on lines +79 to +82
Copy link

Copilot AI Oct 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Tailwind CSS classes alongside Ant Design components. This project appears to use Ant Design's styling system, so consider using Ant Design's Space component and theme tokens instead of Tailwind classes for consistency.

Suggested change
<div className="flex items-center gap-2">
<Copy className="w-5 h-5" />
<span>Copy Variables to Another Project</span>
</div>
<Space align="center">
<Copy style={{ width: 20, height: 20 }} />
<span>Copy Variables to Another Project</span>
</Space>

Copilot uses AI. Check for mistakes.
}
open={open}
onCancel={onClose}
onOk={handleCopy}
okText="Copy Variables"
confirmLoading={loading}
width={550}
destroyOnClose
>
<div className="space-y-4">
Copy link

Copilot AI Oct 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Tailwind CSS classes alongside Ant Design components. This project appears to use Ant Design's styling system, so consider using Ant Design's Space component and theme tokens instead of Tailwind classes for consistency.

Copilot uses AI. Check for mistakes.
<Alert
message={`Copying ${selectedVarIds.length} variable${selectedVarIds.length !== 1 ? 's' : ''} from ${sourceProject.name}`}
description="Select the target project where you want to copy these variables."
type="info"
showIcon
/>

<div>
<label className="block text-sm font-medium mb-2">
Source Project
</label>
<div className="p-3 bg-gray-800 rounded-lg">
Copy link

Copilot AI Oct 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Tailwind CSS classes alongside Ant Design components. This project appears to use Ant Design's styling system, so consider using Ant Design's Space component and theme tokens instead of Tailwind classes for consistency.

Copilot uses AI. Check for mistakes.
<div className="font-medium">{sourceProject.name}</div>
{sourceProject.description && (
<div className="text-sm text-gray-400 mt-1">
{sourceProject.description}
</div>
)}
<Tag color="blue" className="mt-2">
{selectedVarIds.length} variable{selectedVarIds.length !== 1 ? 's' : ''} selected
</Tag>
</div>
</div>

<div>
<label className="block text-sm font-medium mb-2">
Target Project <span className="text-red-500">*</span>
</label>
<Select
value={targetProjectId}
onChange={setTargetProjectId}
placeholder="Select target project"
className="w-full"
showSearch
optionFilterProp="children"
>
{availableProjects.map(project => (
<Option key={project.id} value={project.id}>
<div>
<div className="font-medium">{project.name}</div>
{project.description && (
<div className="text-xs text-gray-400">
{project.description}
</div>
)}
</div>
</Option>
))}
</Select>
{availableProjects.length === 0 && (
<div className="text-sm text-gray-400 mt-2">
No other projects available. Create a new project first.
</div>
)}
</div>

{targetProject && (
<div className="p-3 bg-gray-800 rounded-lg">
<div className="text-sm text-gray-400 mb-1">Target Project</div>
<div className="font-medium">{targetProject.name}</div>
{targetProject._count && (
<Tag color="green" className="mt-2">
{targetProject._count.envVars} existing variable{targetProject._count.envVars !== 1 ? 's' : ''}
</Tag>
)}
</div>
)}

<div className="flex items-center justify-between p-3 bg-gray-800 rounded-lg">
<div>
<div className="font-medium">Overwrite existing variables</div>
<div className="text-sm text-gray-400">
Update values if keys already exist in target
</div>
</div>
<Switch
checked={overwrite}
onChange={setOverwrite}
/>
</div>

<Alert
message="Note"
description={
<Space direction="vertical" size="small">
<div>• Variables will be encrypted with the same master key</div>
<div>• Descriptions will be copied along with values</div>
<div>• All operations will be logged in audit history</div>
{!overwrite && <div>• Existing keys in target will be skipped</div>}
</Space>
}
type="warning"
showIcon
/>
</div>
</Modal>
);
}
1 change: 1 addition & 0 deletions src/components/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export default function Dashboard({ onLogout }) {
<ProjectView
project={selectedProject}
onProjectUpdate={loadProjects}
allProjects={projects}
/>
) : (
<div style={{
Expand Down
Loading