Skip to content
Open
Changes from 1 commit
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
199 changes: 181 additions & 18 deletions packages/server/src/services/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,13 @@ export const canPerformCreationService = async (
projectId: string,
organizationId: string,
) => {
const { accessedProjects, canCreateServices } = await findMemberById(
userId,
organizationId,
);
const member = await findMemberById(userId, organizationId);

if (member.role === "owner" || member.role === "admin") {
return true;
}

const { accessedProjects, canCreateServices } = member;
const haveAccessToProject = accessedProjects.includes(projectId);

if (canCreateServices && haveAccessToProject) {
Expand All @@ -62,7 +65,13 @@ export const canPerformAccessService = async (
serviceId: string,
organizationId: string,
) => {
const { accessedServices } = await findMemberById(userId, organizationId);
const member = await findMemberById(userId, organizationId);

if (member.role === "owner" || member.role === "admin") {
return true;
}

const { accessedServices } = member;
const haveAccessToService = accessedServices.includes(serviceId);

if (haveAccessToService) {
Expand All @@ -77,10 +86,13 @@ export const canPeformDeleteService = async (
serviceId: string,
organizationId: string,
) => {
const { accessedServices, canDeleteServices } = await findMemberById(
userId,
organizationId,
);
const member = await findMemberById(userId, organizationId);

if (member.role === "owner" || member.role === "admin") {
return true;
}

const { accessedServices, canDeleteServices } = member;
const haveAccessToService = accessedServices.includes(serviceId);

if (canDeleteServices && haveAccessToService) {
Expand All @@ -94,7 +106,13 @@ export const canPerformCreationProject = async (
userId: string,
organizationId: string,
) => {
const { canCreateProjects } = await findMemberById(userId, organizationId);
const member = await findMemberById(userId, organizationId);

if (member.role === "owner" || member.role === "admin") {
return true;
}

const { canCreateProjects } = member;

if (canCreateProjects) {
return true;
Expand All @@ -107,7 +125,13 @@ export const canPerformDeleteProject = async (
userId: string,
organizationId: string,
) => {
const { canDeleteProjects } = await findMemberById(userId, organizationId);
const member = await findMemberById(userId, organizationId);

if (member.role === "owner" || member.role === "admin") {
return true;
}

const { canDeleteProjects } = member;

if (canDeleteProjects) {
return true;
Expand All @@ -121,7 +145,13 @@ export const canPerformAccessProject = async (
projectId: string,
organizationId: string,
) => {
const { accessedProjects } = await findMemberById(userId, organizationId);
const member = await findMemberById(userId, organizationId);

if (member.role === "owner" || member.role === "admin") {
return true;
}

const { accessedProjects } = member;

const haveAccessToProject = accessedProjects.includes(projectId);

Expand All @@ -135,10 +165,13 @@ export const canAccessToTraefikFiles = async (
userId: string,
organizationId: string,
) => {
const { canAccessToTraefikFiles } = await findMemberById(
userId,
organizationId,
);
const member = await findMemberById(userId, organizationId);

if (member.role === "owner" || member.role === "admin") {
return true;
}

const { canAccessToTraefikFiles } = member;
return canAccessToTraefikFiles;
};

Expand Down Expand Up @@ -182,6 +215,83 @@ export const checkServiceAccess = async (
}
};

export const checkEnvironmentAccess = async (
userId: string,
environmentId: string,
organizationId: string,
action = "access" as const,
) => {
let hasPermission = false;
switch (action) {
case "access":
hasPermission = await canPerformAccessEnvironment(
userId,
environmentId,
organizationId,
);
break;
default:
hasPermission = false;
}
if (!hasPermission) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Permission denied",
});
}
};

export const canPerformAccessEnvironment = async (
userId: string,
environmentId: string,
organizationId: string,
) => {
const { accessedEnvironments } = await findMemberById(userId, organizationId);
const haveAccessToEnvironment = accessedEnvironments.includes(environmentId);

if (haveAccessToEnvironment) {
return true;
}

return false;
};
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The canPerformAccessEnvironment function is missing the admin/owner role check that has been added to all other similar permission functions. Admins and owners should have access to all environments without needing to be in the accessedEnvironments list. This inconsistency means admins/owners may be incorrectly denied access to environments.

Copilot uses AI. Check for mistakes.

export const checkEnvironmentDeletionPermission = async (
userId: string,
projectId: string,
organizationId: string,
) => {
const member = await findMemberById(userId, organizationId);

if (!member) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "User not found in organization",
});
}

Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The null check for member is redundant because findMemberById already throws a TRPCError with code "UNAUTHORIZED" if the member is not found (line 381-386). This check will never be reached since the function would have already thrown an exception.

Suggested change
if (!member) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "User not found in organization",
});
}

Copilot uses AI. Check for mistakes.
if (member.role === "owner" || member.role === "admin") {
return true;
}

if (!member.canDeleteEnvironments) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have permission to delete environments",
});
}

const hasProjectAccess = member.accessedProjects.includes(projectId);
if (!hasProjectAccess) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have access to this project",
});
}

return true;
};

export const checkProjectAccess = async (
authId: string,
action: "create" | "delete" | "access",
Expand Down Expand Up @@ -214,6 +324,46 @@ export const checkProjectAccess = async (
}
};

export const checkEnvironmentCreationPermission = async (
userId: string,
projectId: string,
organizationId: string,
) => {
// Get user's member record
const member = await findMemberById(userId, organizationId);

if (!member) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "User not found in organization",
});
}

Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The null check for member is redundant because findMemberById already throws a TRPCError with code "UNAUTHORIZED" if the member is not found (line 381-386). This check will never be reached since the function would have already thrown an exception.

Suggested change
// Get user's member record
const member = await findMemberById(userId, organizationId);
if (!member) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "User not found in organization",
});
}
// Get user's member record (throws if not found)
const member = (await findMemberById(userId, organizationId))!;

Copilot uses AI. Check for mistakes.
// Owners and admins can always create environments
if (member.role === "owner" || member.role === "admin") {
return true;
}

// Check if user has canCreateEnvironments permission
if (!member.canCreateEnvironments) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have permission to create environments",
});
}

// Check if user has access to the project
const hasProjectAccess = member.accessedProjects.includes(projectId);
if (!hasProjectAccess) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have access to this project",
});
}

return true;
};

export const findMemberById = async (
userId: string,
organizationId: string,
Expand All @@ -238,7 +388,20 @@ export const findMemberById = async (
};

export const updateUser = async (userId: string, userData: Partial<User>) => {
const user = await db
// Validate email if it's being updated
if (userData.email !== undefined) {
if (!userData.email || userData.email.trim() === "") {
throw new Error("Email is required and cannot be empty");
}

// Basic email format validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(userData.email)) {
throw new Error("Please enter a valid email address");
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The error handling in this function uses throw new Error instead of TRPCError which is the pattern used throughout the rest of this file for validation and permission errors. This inconsistency could lead to errors being handled differently by the TRPC framework and may not provide proper error codes to API consumers.

Suggested change
throw new Error("Email is required and cannot be empty");
}
// Basic email format validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(userData.email)) {
throw new Error("Please enter a valid email address");
throw new TRPCError({
code: "BAD_REQUEST",
message: "Email is required and cannot be empty",
});
}
// Basic email format validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(userData.email)) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Please enter a valid email address",
});

Copilot uses AI. Check for mistakes.
}
}

const userResult = await db
.update(users_temp)
.set({
...userData,
Expand All @@ -247,7 +410,7 @@ export const updateUser = async (userId: string, userData: Partial<User>) => {
.returning()
.then((res) => res[0]);

return user;
return userResult;
};

export const createApiKey = async (
Expand Down