Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b9fb98c
Remove TODOs and implement correct types in environmentDataSources.ts
kapicic Aug 28, 2025
feeaa0d
Add delete cascade to datasource properties
kapicic Aug 28, 2025
34f4563
Add secrets management
kapicic Aug 29, 2025
add4c2d
Add all environments filter in the SecretsManagerTab.tsx
kapicic Aug 29, 2025
5d94aca
Tidy edit environment variable form
kapicic Aug 29, 2025
6b75354
Remove round dot prefix from environment variable name, style the dro…
kapicic Aug 29, 2025
6e255a1
Merge branch 'dev' into stevan/eng-842
kapicic Aug 29, 2025
0d91d83
Disable deleting data source secrets in secret manager
kapicic Sep 1, 2025
1e74225
Get all environment variables instead of just per environment
kapicic Sep 1, 2025
288425a
Add post body validator, replace console with logger
kapicic Sep 1, 2025
6b14e9d
Merge branch 'dev' into stevan/eng-842
kapicic Sep 3, 2025
f0e9bf2
Update Add secret form
skosijer Sep 3, 2025
8b344d5
Update Edit secrets form
skosijer Sep 3, 2025
8fdd0cb
Fix input for secret key
skosijer Sep 3, 2025
e32dc6c
Remove comments
skosijer Sep 4, 2025
1ea45b2
Merge branch 'dev' into stevan/eng-842
skosijer Sep 4, 2025
d2bb0c0
Show only global environment variables
skosijer Sep 4, 2025
2ebc1f9
Enable editing environment id on environment variable
skosijer Sep 4, 2025
92a6ff6
Remove console log
skosijer Sep 4, 2025
0ecee62
Refactor the SecretsManagerTab fetch environments calls
skosijer Sep 4, 2025
2a317ba
Add comment
skosijer Sep 4, 2025
3684cfb
Refactor code based on PR comments
skosijer Sep 4, 2025
068310f
Merge branch 'dev' into stevank/eng-940
skosijer Sep 8, 2025
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
124 changes: 124 additions & 0 deletions app/api/environment-variables/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { NextRequest, NextResponse } from 'next/server';
import { requireUserAbility } from '~/auth/session';
import { prisma } from '~/lib/prisma';
import { deleteEnvironmentVariable, updateEnvironmentVariable } from '~/lib/services/environmentVariablesService';
import { logger } from '~/utils/logger';
import { PermissionAction, PermissionResource } from '@prisma/client';

export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const { userAbility } = await requireUserAbility(request);

const { id } = await params;

if (!userAbility.can(PermissionAction.update, PermissionResource.EnvironmentVariable)) {
return NextResponse.json({ success: false, error: 'Forbidden' }, { status: 403 });
}

const body = (await request.json()) as {
key: string;
environmentId: string;
value: string;
type: string;
description?: string;
};
Comment thread
skosijer marked this conversation as resolved.

const { key, environmentId, value, type, description } = body;

if (!key || !value || !type) {
return NextResponse.json({ success: false, error: 'Missing required fields: key, value, type' }, { status: 400 });
}

// Check if user has access to this environment variable
const envVar = await prisma.environmentVariable.findUnique({
where: { id },
include: {
environment: true,
},
});

if (!envVar) {
return NextResponse.json({ success: false, error: 'Environment variable not found' }, { status: 404 });
}

if (!userAbility.can(PermissionAction.read, PermissionResource.Environment)) {
return NextResponse.json(
{ success: false, error: 'Access denied to this environment variable' },
{ status: 403 },
);
Comment thread
skosijer marked this conversation as resolved.
}

// Check if key already exists for this environment (excluding current env var)
const existingEnvVar = await prisma.environmentVariable.findFirst({
where: {
key,
environmentId: environmentId || envVar.environmentId,
id: { not: id },
},
});

if (existingEnvVar) {
return NextResponse.json(
{ success: false, error: 'Environment variable with this key already exists' },
{ status: 409 },
);
}

const updatedEnvironmentVariable = await updateEnvironmentVariable(
id,
key,
value,
type as any, // Type will be validated by the service
environmentId || envVar.environmentId,
description,
);

return NextResponse.json({
success: true,
environmentVariable: updatedEnvironmentVariable,
});
} catch (error) {
logger.error('Failed to update environment variable:', error);
return NextResponse.json({ success: false, error: 'Failed to update environment variable' }, { status: 500 });
}
}

export async function DELETE(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const { userAbility } = await requireUserAbility(request);

const { id } = await params;

if (!userAbility.can(PermissionAction.delete, PermissionResource.EnvironmentVariable)) {
return NextResponse.json({ success: false, error: 'Forbidden' }, { status: 403 });
}

// Check if user has access to this environment variable
const envVar = await prisma.environmentVariable.findUnique({
where: { id },
include: {
environment: true,
},
});

if (!envVar) {
return NextResponse.json({ success: false, error: 'Environment variable not found' }, { status: 404 });
}

if (!userAbility.can(PermissionAction.read, PermissionResource.Environment)) {
return NextResponse.json(
{ success: false, error: 'Access denied to this environment variable' },
{ status: 403 },
);
}

await deleteEnvironmentVariable(id);

return NextResponse.json({
success: true,
});
} catch (error) {
logger.error('Failed to delete environment variable:', error);
return NextResponse.json({ success: false, error: 'Failed to delete environment variable' }, { status: 500 });
}
}
102 changes: 102 additions & 0 deletions app/api/environment-variables/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { NextRequest, NextResponse } from 'next/server';
import { requireUserAbility } from '~/auth/session';
import {
createEnvironmentVariable,
getEnvironmentVariablesWithEnvironmentDetails,
} from '~/lib/services/environmentVariablesService';
import { logger } from '~/utils/logger';
import { type EnvironmentVariableType, PermissionAction, PermissionResource } from '@prisma/client';
import { z } from 'zod';

const postBodySchema = z.object({
key: z.string().min(1),
value: z.string().min(1),
type: z.enum(['GLOBAL', 'DATA_SOURCE']),
environmentId: z.string().min(1),
description: z.string().optional(),
});

export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const environmentId = searchParams.get('environmentId');
const type = searchParams.get('type') as EnvironmentVariableType | null;

// Check if user has permission to read environment variables
const { userAbility } = await requireUserAbility(request);

if (!userAbility.can(PermissionAction.read, PermissionResource.EnvironmentVariable)) {
return NextResponse.json(
{ success: false, error: 'Insufficient permissions to read environment variables' },
{ status: 403 },
);
}

if (!environmentId) {
return NextResponse.json({ success: false, error: 'Environment ID is required' }, { status: 400 });
}

const environmentVariables = await getEnvironmentVariablesWithEnvironmentDetails(environmentId, type);

return NextResponse.json({
success: true,
environmentVariables,
});
} catch (error) {
logger.error('Failed to fetch environment variables:', error);
return NextResponse.json({ success: false, error: 'Failed to fetch environment variables' }, { status: 500 });
}
}

export async function POST(request: NextRequest) {
try {
const { userId, userAbility } = await requireUserAbility(request);

if (!userAbility.can(PermissionAction.create, PermissionResource.EnvironmentVariable)) {
return NextResponse.json({ success: false, error: 'Forbidden' }, { status: 403 });
}

const body = postBodySchema.parse(await request.json());

const { key, value, type, environmentId, description } = body;

// Check if user has access to this environment
if (!userAbility.can(PermissionAction.read, PermissionResource.Environment)) {
return NextResponse.json({ success: false, error: 'Access denied to this environment' }, { status: 403 });
}
Comment thread
skosijer marked this conversation as resolved.

// Check if environment variable with this key already exists
const existingEnvVar = await prisma.environmentVariable.findUnique({
where: {
key_environmentId: {
key,
environmentId,
},
},
});

if (existingEnvVar) {
return NextResponse.json(
{ success: false, error: 'Environment variable with this key already exists' },
{ status: 409 },
);
}

const environmentVariable = await createEnvironmentVariable(
key,
value,
type as EnvironmentVariableType,
environmentId,
userId,
description,
);

return NextResponse.json({
success: true,
environmentVariable,
});
} catch (error) {
logger.error('Failed to create environment variable:', error);
return NextResponse.json({ success: false, error: 'Failed to create environment variable' }, { status: 500 });
}
}
3 changes: 3 additions & 0 deletions app/components/@settings/core/ControlPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { AbilityContext } from '~/components/ability/AbilityProvider';
import MembersTab from '~/components/@settings/tabs/users/UsersTab';
import RolesTab from '~/components/@settings/tabs/roles/RolesTab';
import EnvironmentsTab from '~/components/@settings/tabs/environments';
import SecretsManagerTab from '~/components/@settings/tabs/secrets-manager';

const LAST_ACCESSED_TAB_KEY = 'control-panel-last-tab';

Expand Down Expand Up @@ -167,6 +168,8 @@ export const ControlPanel = () => {
return <DataTab />;
case 'environments':
return <EnvironmentsTab />;
case 'secrets-manager':
return <SecretsManagerTab />;
case 'deployed-apps':
return <DeployedAppsTab />;
case 'github':
Expand Down
10 changes: 7 additions & 3 deletions app/components/@settings/core/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { TabType, TabVisibilityConfig } from './types';
import { Database, GitBranch, type LucideIcon, Rocket, Users, Server, ShieldUser } from 'lucide-react';
import { Database, GitBranch, type LucideIcon, Rocket, Users, Server, ShieldUser, Lock } from 'lucide-react';

export const TAB_ICONS: Record<TabType, string | LucideIcon> = {
data: Database,
Expand All @@ -8,6 +8,7 @@ export const TAB_ICONS: Record<TabType, string | LucideIcon> = {
members: Users,
roles: ShieldUser,
environments: Server,
'secrets-manager': Lock,
};

export const TAB_LABELS: Record<TabType, string> = {
Expand All @@ -17,6 +18,7 @@ export const TAB_LABELS: Record<TabType, string> = {
members: 'Members',
roles: 'Roles',
environments: 'Environments',
'secrets-manager': 'Secrets Manager',
};

export const TAB_DESCRIPTIONS: Record<TabType, string> = {
Expand All @@ -26,13 +28,15 @@ export const TAB_DESCRIPTIONS: Record<TabType, string> = {
members: 'Manage your members',
roles: 'Manage roles and permissions for users',
environments: 'Manage your environments',
'secrets-manager': 'Manage environment variables and secrets',
};

export const DEFAULT_TAB_CONFIG: TabVisibilityConfig[] = [
{ id: 'data', visible: true, window: 'user', order: 0 },
{ id: 'environments', visible: true, window: 'user', order: 1 },
{ id: 'github', visible: true, window: 'user', order: 2 },
{ id: 'deployed-apps', visible: true, window: 'user', order: 3 },
{ id: 'secrets-manager', visible: true, window: 'user', order: 2 },
{ id: 'github', visible: true, window: 'user', order: 3 },
{ id: 'deployed-apps', visible: true, window: 'user', order: 4 },

{ id: 'members', visible: true, window: 'admin', order: 1 },
{ id: 'roles', visible: true, window: 'admin', order: 2 },
Expand Down
3 changes: 2 additions & 1 deletion app/components/@settings/core/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type TabType = 'data' | 'github' | 'deployed-apps' | 'members' | 'roles' | 'environments';
export type TabType = 'data' | 'github' | 'deployed-apps' | 'members' | 'roles' | 'environments' | 'secrets-manager';

export type WindowType = 'user' | 'admin';

Expand Down Expand Up @@ -31,4 +31,5 @@ export const TAB_LABELS: Record<TabType, string> = {
members: 'Members',
roles: 'Roles',
environments: 'Environments',
'secrets-manager': 'Secrets Manager',
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { classNames } from '~/utils/classNames';
import { useEffect, useState } from 'react';
import { AlertTriangle, CheckCircle, Info, Loader2, Plug, Save, Trash2, XCircle } from 'lucide-react';
import { AlertTriangle, CheckCircle, Eye, EyeOff, Info, Loader2, Plug, Save, Trash2, XCircle } from 'lucide-react';
import type { TestConnectionResponse } from '~/components/@settings/tabs/data/DataTab';
import { toast } from 'sonner';
import { BaseSelect } from '~/components/ui/Select';
import { SelectDatabaseTypeOptions, SingleValueWithTooltip } from '~/components/database/SelectDatabaseTypeOptions';
import { Eye, EyeSlash } from 'iconsax-reactjs';
import {
type DataSourceOption,
DEFAULT_DATA_SOURCES,
Expand Down Expand Up @@ -364,7 +363,11 @@ export default function EditDataSourceForm({
tabIndex={-1}
>
<span className="text-gray-400 group-hover:text-white transition-colors">
{showConnStr ? <EyeSlash variant="Bold" size={20} /> : <Eye variant="Bold" size={20} />}
{showConnStr ? (
<EyeOff className="w-4 h-4 text-gray-400" />
) : (
<Eye className="w-4 h-4 text-gray-400" />
)}
</span>
</button>
</div>
Expand Down
Loading