dokploy/apps/dokploy/server/api/routers/user.ts
Mauricio Siu 314438b84c Enhance impersonation functionality and user management
- Updated the ImpersonationBar component to fetch users dynamically and handle impersonation actions more efficiently.
- Refactored the ProfileForm to set the allowImpersonation value directly, improving form handling.
- Modified the DashboardLayout to conditionally render the ImpersonationBar based on user permissions and cloud settings.
- Added a new role column to the user_temp table to support user role management.
- Updated API routes to include checks for root access and improved user listing functionality.
2025-05-06 02:32:08 -06:00

366 lines
8.4 KiB
TypeScript

import {
IS_CLOUD,
createApiKey,
findOrganizationById,
findUserById,
getUserByToken,
removeUserById,
updateUser,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import {
account,
apiAssignPermissions,
apiFindOneToken,
apiUpdateUser,
apikey,
invitation,
member,
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import * as bcrypt from "bcrypt";
import { and, asc, eq, gt } from "drizzle-orm";
import { z } from "zod";
import {
adminProcedure,
createTRPCRouter,
protectedProcedure,
publicProcedure,
} from "../trpc";
const apiCreateApiKey = z.object({
name: z.string().min(1),
prefix: z.string().optional(),
expiresIn: z.number().optional(),
metadata: z.object({
organizationId: z.string(),
}),
// Rate limiting
rateLimitEnabled: z.boolean().optional(),
rateLimitTimeWindow: z.number().optional(),
rateLimitMax: z.number().optional(),
// Request limiting
remaining: z.number().optional(),
refillAmount: z.number().optional(),
refillInterval: z.number().optional(),
});
export const userRouter = createTRPCRouter({
all: adminProcedure.query(async ({ ctx }) => {
return await db.query.member.findMany({
where: eq(member.organizationId, ctx.session.activeOrganizationId),
with: {
user: true,
},
orderBy: [asc(member.createdAt)],
});
}),
one: protectedProcedure
.input(
z.object({
userId: z.string(),
}),
)
.query(async ({ input, ctx }) => {
const memberResult = await db.query.member.findFirst({
where: and(
eq(member.userId, input.userId),
eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
),
with: {
user: true,
},
});
return memberResult;
}),
get: protectedProcedure.query(async ({ ctx }) => {
const memberResult = await db.query.member.findFirst({
where: and(
eq(member.userId, ctx.user.id),
eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
),
with: {
user: {
with: {
apiKeys: true,
},
},
},
});
return memberResult;
}),
haveRootAccess: protectedProcedure.query(async ({ ctx }) => {
if (!IS_CLOUD) {
return false;
}
if (
process.env.USER_ADMIN_ID === ctx.user.id ||
ctx.session?.impersonatedBy === process.env.USER_ADMIN_ID
) {
return true;
}
return false;
}),
getBackups: adminProcedure.query(async ({ ctx }) => {
const memberResult = await db.query.member.findFirst({
where: and(
eq(member.userId, ctx.user.id),
eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
),
with: {
user: {
with: {
backups: {
with: {
destination: true,
deployments: true,
},
},
apiKeys: true,
},
},
},
});
return memberResult?.user;
}),
getServerMetrics: protectedProcedure.query(async ({ ctx }) => {
const memberResult = await db.query.member.findFirst({
where: and(
eq(member.userId, ctx.user.id),
eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
),
with: {
user: true,
},
});
return memberResult?.user;
}),
update: protectedProcedure
.input(apiUpdateUser)
.mutation(async ({ input, ctx }) => {
if (input.password || input.currentPassword) {
const currentAuth = await db.query.account.findFirst({
where: eq(account.userId, ctx.user.id),
});
const correctPassword = bcrypt.compareSync(
input.currentPassword || "",
currentAuth?.password || "",
);
if (!correctPassword) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Current password is incorrect",
});
}
if (!input.password) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "New password is required",
});
}
await db
.update(account)
.set({
password: bcrypt.hashSync(input.password, 10),
})
.where(eq(account.userId, ctx.user.id));
}
return await updateUser(ctx.user.id, input);
}),
getUserByToken: publicProcedure
.input(apiFindOneToken)
.query(async ({ input }) => {
return await getUserByToken(input.token);
}),
getMetricsToken: protectedProcedure.query(async ({ ctx }) => {
const user = await findUserById(ctx.user.ownerId);
return {
serverIp: user.serverIp,
enabledFeatures: user.enablePaidFeatures,
metricsConfig: user?.metricsConfig,
};
}),
remove: protectedProcedure
.input(
z.object({
userId: z.string(),
}),
)
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return true;
}
return await removeUserById(input.userId);
}),
assignPermissions: adminProcedure
.input(apiAssignPermissions)
.mutation(async ({ input, ctx }) => {
try {
const organization = await findOrganizationById(
ctx.session?.activeOrganizationId || "",
);
if (organization?.ownerId !== ctx.user.ownerId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to assign permissions",
});
}
const { id, ...rest } = input;
await db
.update(member)
.set({
...rest,
})
.where(
and(
eq(member.userId, input.id),
eq(
member.organizationId,
ctx.session?.activeOrganizationId || "",
),
),
);
} catch (error) {
throw error;
}
}),
getInvitations: protectedProcedure.query(async ({ ctx }) => {
return await db.query.invitation.findMany({
where: and(
eq(invitation.email, ctx.user.email),
gt(invitation.expiresAt, new Date()),
eq(invitation.status, "pending"),
),
with: {
organization: true,
},
});
}),
getContainerMetrics: protectedProcedure
.input(
z.object({
url: z.string(),
token: z.string(),
appName: z.string(),
dataPoints: z.string(),
}),
)
.query(async ({ input }) => {
try {
if (!input.appName) {
throw new Error(
[
"No Application Selected:",
"",
"Make Sure to select an application to monitor.",
].join("\n"),
);
}
const url = new URL(`${input.url}/metrics/containers`);
url.searchParams.append("limit", input.dataPoints);
url.searchParams.append("appName", input.appName);
const response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${input.token}`,
},
});
if (!response.ok) {
throw new Error(
`Error ${response.status}: ${response.statusText}. Please verify that the application "${input.appName}" is running and this service is included in the monitoring configuration.`,
);
}
const data = await response.json();
if (!Array.isArray(data) || data.length === 0) {
throw new Error(
[
`No monitoring data available for "${input.appName}". This could be because:`,
"",
"1. The container was recently started - wait a few minutes for data to be collected",
"2. The container is not running - verify its status",
"3. The service is not included in your monitoring configuration",
].join("\n"),
);
}
return data as {
containerId: string;
containerName: string;
containerImage: string;
containerLabels: string;
containerCommand: string;
containerCreated: string;
}[];
} catch (error) {
throw error;
}
}),
generateToken: protectedProcedure.mutation(async () => {
return "token";
}),
deleteApiKey: protectedProcedure
.input(
z.object({
apiKeyId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
try {
const apiKeyToDelete = await db.query.apikey.findFirst({
where: eq(apikey.id, input.apiKeyId),
});
if (!apiKeyToDelete) {
throw new TRPCError({
code: "NOT_FOUND",
message: "API key not found",
});
}
if (apiKeyToDelete.userId !== ctx.user.id) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this API key",
});
}
await db.delete(apikey).where(eq(apikey.id, input.apiKeyId));
return true;
} catch (error) {
throw error;
}
}),
createApiKey: protectedProcedure
.input(apiCreateApiKey)
.mutation(async ({ input, ctx }) => {
const apiKey = await createApiKey(ctx.user.id, input);
return apiKey;
}),
checkUserOrganizations: protectedProcedure
.input(
z.object({
userId: z.string(),
}),
)
.query(async ({ input }) => {
const organizations = await db.query.member.findMany({
where: eq(member.userId, input.userId),
});
return organizations.length;
}),
});